#[cfg(target_arch = "wasm32")]
use crate::telemetry::RecordedSpan;
#[cfg(target_arch = "wasm32")]
use std::collections::HashMap;
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Clone, Default)]
pub struct ExportStats {
pub total_exports: u64,
pub successful_exports: u64,
pub failed_exports: u64,
}
#[cfg(target_arch = "wasm32")]
pub struct WasmSpanExporter {
endpoint: String,
buffer: Vec<RecordedSpan>,
batch_size: usize,
headers: HashMap<String, String>,
auto_export_enabled: bool,
stats: ExportStats,
devtools_enabled: bool,
}
#[cfg(target_arch = "wasm32")]
impl WasmSpanExporter {
pub fn new(endpoint: String) -> Self {
Self {
endpoint,
buffer: Vec::new(),
batch_size: 100, headers: HashMap::new(),
auto_export_enabled: false, stats: ExportStats::default(),
devtools_enabled: false, }
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size;
self
}
pub fn with_auto_export(mut self, enabled: bool) -> Self {
self.auto_export_enabled = enabled;
self
}
pub fn is_auto_export_enabled(&self) -> bool {
self.auto_export_enabled
}
pub fn with_devtools(mut self, enabled: bool) -> Self {
self.devtools_enabled = enabled;
self
}
pub fn is_devtools_enabled(&self) -> bool {
self.devtools_enabled
}
#[cfg(target_arch = "wasm32")]
fn post_to_devtools(&self, event: &str, data: &serde_json::Value) {
if !self.devtools_enabled {
return;
}
use wasm_bindgen::prelude::*;
let message = js_sys::Object::new();
js_sys::Reflect::set(
&message,
&JsValue::from_str("type"),
&JsValue::from_str(event),
)
.unwrap_or_default();
js_sys::Reflect::set(
&message,
&JsValue::from_str("data"),
&JsValue::from_str(&data.to_string()),
)
.unwrap_or_default();
if let Ok(chrome) = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("chrome")) {
if let Ok(runtime) = js_sys::Reflect::get(&chrome, &JsValue::from_str("runtime")) {
if let Ok(send_msg) =
js_sys::Reflect::get(&runtime, &JsValue::from_str("sendMessage"))
{
if let Ok(func) = send_msg.dyn_into::<js_sys::Function>() {
let _ = func.call1(&runtime, &message);
}
}
}
}
}
pub fn endpoint(&self) -> &str {
&self.endpoint
}
pub fn batch_size(&self) -> usize {
self.batch_size
}
pub fn buffered_count(&self) -> usize {
self.buffer.len()
}
pub fn add_header(&mut self, key: &str, value: &str) {
self.headers.insert(key.to_string(), value.to_string());
}
pub fn header_count(&self) -> usize {
self.headers.len()
}
pub fn buffer_span(&mut self, span: RecordedSpan) {
#[cfg(target_arch = "wasm32")]
{
if self.devtools_enabled {
if let Ok(json_span) = serde_json::to_value(&span) {
self.post_to_devtools("span_recorded", &json_span);
}
}
}
self.buffer.push(span);
#[cfg(target_arch = "wasm32")]
{
if self.devtools_enabled {
let buffer_data = serde_json::json!({
"count": self.buffer.len(),
"threshold": self.batch_size,
"size": self.buffer.len() * std::mem::size_of::<RecordedSpan>()
});
self.post_to_devtools("buffer_update", &buffer_data);
}
}
}
pub fn get_buffered_spans(&self) -> &[RecordedSpan] {
&self.buffer
}
pub fn clear_buffer(&mut self) {
self.buffer.clear();
}
pub fn serialize_buffer(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self.buffer)
}
#[cfg(target_arch = "wasm32")]
pub async fn flush(&mut self) -> Result<(), String> {
if self.buffer.is_empty() {
return Ok(());
}
self.stats.total_exports += 1;
let json_body = match self.serialize_buffer() {
Ok(json) => json,
Err(e) => {
self.stats.failed_exports += 1;
if self.devtools_enabled {
let error_data = serde_json::json!({
"message": "Failed to serialize spans",
"details": format!("{}", e)
});
self.post_to_devtools("export_error", &error_data);
if let Ok(stats_json) = serde_json::to_value(&self.stats) {
self.post_to_devtools("export_stats", &stats_json);
}
}
return Err(format!("Failed to serialize spans: {}", e));
}
};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
let opts = RequestInit::new();
opts.set_method("POST");
opts.set_mode(RequestMode::Cors);
opts.set_body(&JsValue::from_str(&json_body));
let request = match Request::new_with_str_and_init(&self.endpoint, &opts) {
Ok(req) => req,
Err(e) => {
self.stats.failed_exports += 1;
return Err(format!("Failed to create request: {:?}", e));
}
};
let headers = request.headers();
if let Err(e) = headers.set("Content-Type", "application/json") {
self.stats.failed_exports += 1;
return Err(format!("Failed to set Content-Type header: {:?}", e));
}
for (key, value) in &self.headers {
if let Err(e) = headers.set(key, value) {
self.stats.failed_exports += 1;
return Err(format!("Failed to set header {}: {:?}", key, e));
}
}
let window = match web_sys::window() {
Some(w) => w,
None => {
self.stats.failed_exports += 1;
return Err("No window object available".to_string());
}
};
let resp_value = match JsFuture::from(window.fetch_with_request(&request)).await {
Ok(val) => val,
Err(e) => {
self.stats.failed_exports += 1;
return Err(format!("Fetch failed: {:?}", e));
}
};
let resp: Response = match resp_value.dyn_into() {
Ok(r) => r,
Err(e) => {
self.stats.failed_exports += 1;
return Err(format!("Response cast failed: {:?}", e));
}
};
if !resp.ok() {
self.stats.failed_exports += 1;
if self.devtools_enabled {
let error_data = serde_json::json!({
"message": format!("HTTP error: {}", resp.status()),
"details": format!("Export failed with status {}", resp.status())
});
self.post_to_devtools("export_error", &error_data);
}
if self.devtools_enabled {
if let Ok(stats_json) = serde_json::to_value(&self.stats) {
self.post_to_devtools("export_stats", &stats_json);
}
}
return Err(format!("HTTP error: {}", resp.status()));
}
self.clear_buffer();
self.stats.successful_exports += 1;
if self.devtools_enabled {
if let Ok(stats_json) = serde_json::to_value(&self.stats) {
self.post_to_devtools("export_stats", &stats_json);
}
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn buffer_span_async(&mut self, span: RecordedSpan) -> Result<(), String> {
self.buffer.push(span);
if self.auto_export_enabled && self.buffer.len() >= self.batch_size && self.batch_size > 0 {
return self.flush().await;
}
Ok(())
}
pub fn get_export_stats(&self) -> ExportStats {
self.stats.clone()
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct WasmSpanExporter;
#[cfg(not(target_arch = "wasm32"))]
impl WasmSpanExporter {
pub fn new(_endpoint: String) -> Self {
Self
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod tests {
use super::*;
#[test]
fn test_exporter_creation() {
let exporter = WasmSpanExporter::new("http://localhost:4318/v1/traces".to_string());
assert_eq!(exporter.endpoint(), "http://localhost:4318/v1/traces");
assert_eq!(exporter.buffered_count(), 0);
assert_eq!(exporter.batch_size(), 100);
}
#[test]
fn test_batch_size_configuration() {
let exporter = WasmSpanExporter::new("http://localhost:4318/v1/traces".to_string())
.with_batch_size(50);
assert_eq!(exporter.batch_size(), 50);
}
}