From a9f41fa8eeb5e93f57773b1599a7eef06281a8d4 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Mon, 27 Apr 2026 11:00:02 -0400 Subject: [PATCH] add WASM support --- Cargo.lock | 4 ++++ sentry-contexts/Cargo.toml | 4 +++- sentry-contexts/src/utils.rs | 20 ++++++++++++++++++- sentry-core/Cargo.toml | 1 + sentry-core/src/batcher.rs | 17 +++++++++------- sentry-core/src/lib.rs | 3 +++ sentry-core/src/logger.rs | 8 ++++---- sentry-core/src/performance.rs | 4 ++-- sentry-core/src/session.rs | 29 +++++++++++++++++----------- sentry-core/src/utils.rs | 12 ++++++++++++ sentry-log/src/converters.rs | 9 +++++---- sentry-types/Cargo.toml | 3 ++- sentry-types/src/auth.rs | 4 ++-- sentry-types/src/protocol/session.rs | 2 +- sentry-types/src/protocol/v7.rs | 22 ++++++++++----------- sentry-types/src/utils.rs | 10 ++++++++++ 16 files changed, 107 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7babb356..0a504a578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,7 +658,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-link", ] @@ -3605,6 +3607,7 @@ name = "sentry-core" version = "0.47.0" dependencies = [ "anyhow", + "chrono", "criterion", "futures", "log", @@ -3710,6 +3713,7 @@ dependencies = [ name = "sentry-types" version = "0.47.0" dependencies = [ + "chrono", "debugid", "hex", "rand 0.9.3", diff --git a/sentry-contexts/Cargo.toml b/sentry-contexts/Cargo.toml index 40f0f7de6..a417d1284 100644 --- a/sentry-contexts/Cargo.toml +++ b/sentry-contexts/Cargo.toml @@ -16,9 +16,11 @@ rust-version = { workspace = true } [dependencies] sentry-core = { version = "0.47.0", path = "../sentry-core" } libc = "0.2.66" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] hostname = "0.4" -[target."cfg(not(windows))".dependencies] +[target.'cfg(all(not(windows), not(target_arch = "wasm32")))'.dependencies] uname = "0.1.1" [target."cfg(windows)".dependencies] diff --git a/sentry-contexts/src/utils.rs b/sentry-contexts/src/utils.rs index 8acd9c83b..3b45ba6a1 100644 --- a/sentry-contexts/src/utils.rs +++ b/sentry-contexts/src/utils.rs @@ -110,12 +110,19 @@ mod model_support { } /// Returns the server name (hostname) if available. +#[cfg(not(target_arch = "wasm32"))] pub fn server_name() -> Option { hostname::get().ok().and_then(|s| s.into_string().ok()) } +#[cfg(target_arch = "wasm32")] +pub fn server_name() -> Option { + // TODO: What other options are available? + None +} + /// Returns the OS context -#[cfg(not(windows))] +#[cfg(all(not(windows), not(target_arch = "wasm32")))] pub fn os_context() -> Option { use uname::uname; if let Ok(info) = uname() { @@ -168,6 +175,17 @@ pub fn os_context() -> Option { ) } +#[cfg(target_arch = "wasm32")] +pub fn os_context() -> Option { + Some( + OsContext { + name: Some("WASM".into()), + ..Default::default() + } + .into(), + ) +} + /// Returns the rust info. pub fn rust_context() -> Context { RuntimeContext { diff --git a/sentry-core/Cargo.toml b/sentry-core/Cargo.toml index c37d9d949..db0082ddd 100644 --- a/sentry-core/Cargo.toml +++ b/sentry-core/Cargo.toml @@ -34,6 +34,7 @@ serde = { version = "1.0.104", features = ["derive"] } serde_json = { version = "1.0.46" } url = { version = "2.1.1" } uuid = { version = "1.0.0", features = ["v4", "serde"], optional = true } +chrono = { version = "0.4.44" } [dev-dependencies] # Because we re-export all the public API in `sentry`, we actually run all the diff --git a/sentry-core/src/batcher.rs b/sentry-core/src/batcher.rs index dfc28bf5e..bca9cc541 100644 --- a/sentry-core/src/batcher.rs +++ b/sentry-core/src/batcher.rs @@ -2,17 +2,17 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard}; use std::thread::JoinHandle; -use std::time::{Duration, Instant}; use crate::client::TransportArc; use crate::protocol::EnvelopeItem; use crate::Envelope; +use chrono::{Duration, Utc}; use sentry_types::protocol::v7::Log; // Flush when there's 100 items in the buffer const MAX_ITEMS: usize = 100; // Or when 5 seconds have passed from the last flush -const FLUSH_INTERVAL: Duration = Duration::from_secs(5); +const FLUSH_INTERVAL: Duration = Duration::seconds(5); #[derive(Debug)] struct BatchQueue { @@ -71,21 +71,24 @@ where if *shutdown { return; } - let mut last_flush = Instant::now(); + let mut last_flush = Utc::now(); loop { + let elapsed = Utc::now() - last_flush; let timeout = FLUSH_INTERVAL - .checked_sub(last_flush.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); + .checked_sub(&elapsed) + .unwrap_or_else(|| Duration::seconds(0)) + .to_std() + .unwrap_or_else(|_| std::time::Duration::from_secs(0)); shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; if *shutdown { return; } - if last_flush.elapsed() >= FLUSH_INTERVAL { + if Utc::now() - last_flush >= FLUSH_INTERVAL { Batcher::flush_queue_internal( worker_queue.lock().unwrap(), &worker_transport, ); - last_flush = Instant::now(); + last_flush = Utc::now(); } } }) diff --git a/sentry-core/src/lib.rs b/sentry-core/src/lib.rs index 6a2a1b5dd..8c41200d2 100644 --- a/sentry-core/src/lib.rs +++ b/sentry-core/src/lib.rs @@ -163,3 +163,6 @@ pub use sentry_types::protocol::v7::{Breadcrumb, Envelope, Level, User}; // utilities reused across integrations pub mod utils; + +#[doc(hidden)] +pub use crate::utils::now_system_time; diff --git a/sentry-core/src/logger.rs b/sentry-core/src/logger.rs index 293bf3740..398778051 100644 --- a/sentry-core/src/logger.rs +++ b/sentry-core/src/logger.rs @@ -10,7 +10,7 @@ macro_rules! logger_log { level: $level, body: $msg.to_owned(), trace_id: None, - timestamp: ::std::time::SystemTime::now(), + timestamp: $crate::now_system_time(), severity_number: None, attributes: $crate::protocol::Map::new(), }; @@ -39,7 +39,7 @@ macro_rules! logger_log { level: $level, body: format!($fmt, $($arg),*), trace_id: None, - timestamp: ::std::time::SystemTime::now(), + timestamp: $crate::now_system_time(), severity_number: None, attributes, }; @@ -58,7 +58,7 @@ macro_rules! logger_log { level: $level, body: $msg.to_owned(), trace_id: None, - timestamp: ::std::time::SystemTime::now(), + timestamp: $crate::now_system_time(), severity_number: None, #[allow(clippy::redundant_field_names)] attributes: $attrs, @@ -86,7 +86,7 @@ macro_rules! logger_log { level: $level, body: format!($fmt, $($arg),*), trace_id: None, - timestamp: ::std::time::SystemTime::now(), + timestamp: $crate::now_system_time(), severity_number: None, #[allow(clippy::redundant_field_names)] attributes: $attrs, diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 5416746ea..9c08d02bb 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -898,7 +898,7 @@ impl Transaction { /// This records the current timestamp as the end timestamp and sends the transaction together with /// all finished child spans to Sentry. pub fn finish(self) { - self.finish_with_timestamp(SystemTime::now()); + self.finish_with_timestamp(crate::utils::now_system_time()); } /// Starts a new child Span with the given `op` and `description`. @@ -1149,7 +1149,7 @@ impl Span { /// This will record the current timestamp as the end timestamp and add the span to the /// transaction in which it was started. pub fn finish(self) { - self.finish_with_timestamp(SystemTime::now()); + self.finish_with_timestamp(crate::utils::now_system_time()); } /// Starts a new child Span with the given `op` and `description`. diff --git a/sentry-core/src/session.rs b/sentry-core/src/session.rs index e9fd2f298..afd41cb74 100644 --- a/sentry-core/src/session.rs +++ b/sentry-core/src/session.rs @@ -11,7 +11,9 @@ mod session_impl { use std::collections::HashMap; use std::sync::{Arc, Condvar, Mutex, MutexGuard}; use std::thread::JoinHandle; - use std::time::{Duration, Instant, SystemTime}; + use std::time::{Duration, SystemTime}; + + use chrono::{DateTime, Utc}; use crate::client::TransportArc; use crate::clientoptions::SessionMode; @@ -29,7 +31,7 @@ mod session_impl { pub struct Session { client: Arc, session_update: SessionUpdate<'static>, - started: Instant, + started: DateTime, dirty: bool, } @@ -62,7 +64,7 @@ mod session_impl { distinct_id, sequence: None, timestamp: None, - started: SystemTime::now(), + started: crate::utils::now_system_time(), init: true, duration: None, status: SessionStatus::Ok, @@ -74,7 +76,7 @@ mod session_impl { user_agent: None, }, }, - started: Instant::now(), + started: Utc::now(), dirty: true, }) } @@ -112,7 +114,8 @@ mod session_impl { SessionStatus::Ok => SessionStatus::Exited, s => s, }; - self.session_update.duration = Some(self.started.elapsed().as_secs_f64()); + let elapsed = Utc::now() - self.started; + self.session_update.duration = Some(elapsed.as_seconds_f64()); self.session_update.status = status; self.dirty = true; } @@ -214,23 +217,27 @@ mod session_impl { if *shutdown { return; } - let mut last_flush = Instant::now(); + let mut last_flush = Utc::now(); + let flush_interval = chrono::Duration::from_std(FLUSH_INTERVAL).unwrap(); loop { - let timeout = FLUSH_INTERVAL - .checked_sub(last_flush.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); + let elapsed = Utc::now() - last_flush; + let timeout = flush_interval + .checked_sub(&elapsed) + .unwrap_or_else(|| chrono::Duration::seconds(0)) + .to_std() + .unwrap_or_else(|_| Duration::from_secs(0)); shutdown = cvar.wait_timeout(shutdown, timeout).unwrap().0; if *shutdown { return; } - if last_flush.elapsed() < FLUSH_INTERVAL { + if Utc::now() - last_flush < flush_interval { continue; } SessionFlusher::flush_queue_internal( worker_queue.lock().unwrap(), &worker_transport, ); - last_flush = Instant::now(); + last_flush = Utc::now(); } }) .unwrap(); diff --git a/sentry-core/src/utils.rs b/sentry-core/src/utils.rs index 8f1333fcd..c41c4ea2e 100644 --- a/sentry-core/src/utils.rs +++ b/sentry-core/src/utils.rs @@ -1,5 +1,17 @@ //! Utilities reused across dependant crates and integrations. +use std::time::{Duration, SystemTime}; + +/// Returns the current wall-clock time as a [`std::time::SystemTime`], sourced from +/// [`chrono::Utc::now`] so it works on `wasm32-unknown-unknown` (where +/// [`std::time::SystemTime::now`] panics). +pub fn now_system_time() -> std::time::SystemTime { + let now = chrono::Utc::now(); + let secs = now.timestamp() as u64; + let nanos = now.timestamp_subsec_nanos(); + SystemTime::UNIX_EPOCH + Duration::new(secs, nanos) +} + const SENSITIVE_HEADERS_UPPERCASE: &[&str] = &[ "AUTHORIZATION", "PROXY_AUTHORIZATION", diff --git a/sentry-log/src/converters.rs b/sentry-log/src/converters.rs index 1a0b12533..27b38e3a4 100644 --- a/sentry-log/src/converters.rs +++ b/sentry-log/src/converters.rs @@ -1,10 +1,11 @@ use sentry_core::protocol::{Event, Value}; #[cfg(feature = "logs")] -use sentry_core::protocol::{Log, LogAttribute, LogLevel}; +use sentry_core::{ + protocol::{Log, LogAttribute, LogLevel}, + utils::now_system_time, +}; use sentry_core::{Breadcrumb, Level}; use std::collections::BTreeMap; -#[cfg(feature = "logs")] -use std::time::SystemTime; /// Converts a [`log::Level`] to a Sentry [`Level`], used for [`Event`] and [`Breadcrumb`]. pub fn convert_log_level(level: log::Level) -> Level { @@ -160,7 +161,7 @@ pub fn log_from_record(record: &log::Record<'_>) -> Log { level: convert_log_level_to_sentry_log_level(record.level()), body: format!("{}", record.args()), trace_id: None, - timestamp: SystemTime::now(), + timestamp: now_system_time(), severity_number: None, attributes, } diff --git a/sentry-types/Cargo.toml b/sentry-types/Cargo.toml index 274737f08..de8abe384 100644 --- a/sentry-types/Cargo.toml +++ b/sentry-types/Cargo.toml @@ -27,9 +27,10 @@ rand = "0.9.3" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.46" thiserror = "2.0.12" -time = { version = "0.3.47", features = ["formatting", "parsing"] } +time = { version = "0.3.47", default-features = false, features = ["formatting", "parsing"] } url = { version = "2.1.1", features = ["serde"] } uuid = { version = "1.0.0", features = ["serde"] } +chrono = { version = "0.4.44" } [dev-dependencies] rstest = "0.25.0" diff --git a/sentry-types/src/auth.rs b/sentry-types/src/auth.rs index 5edb715a8..cbe5948d2 100644 --- a/sentry-types/src/auth.rs +++ b/sentry-types/src/auth.rs @@ -9,7 +9,7 @@ use url::form_urlencoded; use crate::dsn::Dsn; use crate::protocol; -use crate::utils::{datetime_to_timestamp, timestamp_to_datetime}; +use crate::utils::{datetime_to_timestamp, now_system_time, timestamp_to_datetime}; /// Represents an auth header parsing error. #[derive(Debug, Error, Copy, Clone, Eq, PartialEq)] @@ -175,7 +175,7 @@ impl FromStr for Auth { pub(crate) fn auth_from_dsn_and_client(dsn: &Dsn, client: Option<&str>) -> Auth { Auth { - timestamp: Some(SystemTime::now()), + timestamp: Some(now_system_time()), client: client.map(|x| x.to_string()), version: protocol::LATEST, key: dsn.public_key().to_string(), diff --git a/sentry-types/src/protocol/session.rs b/sentry-types/src/protocol/session.rs index 948efd301..025d59951 100644 --- a/sentry-types/src/protocol/session.rs +++ b/sentry-types/src/protocol/session.rs @@ -108,7 +108,7 @@ pub struct SessionUpdate<'a> { pub timestamp: Option, /// The timestamp of when the session itself started. - #[serde(default = "SystemTime::now", with = "ts_rfc3339")] + #[serde(default = "crate::utils::now_system_time", with = "ts_rfc3339")] pub started: SystemTime, /// A flag that indicates that this is the initial transmission of the session. diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index b2f8ee99a..749977049 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -22,7 +22,7 @@ use thiserror::Error; pub use url::Url; pub use uuid::Uuid; -use crate::utils::{display_from_str_opt, ts_rfc3339_opt, ts_seconds_float}; +use crate::utils::{display_from_str_opt, now_system_time, ts_rfc3339_opt, ts_seconds_float}; pub use super::attachment::*; pub use super::envelope::*; @@ -739,7 +739,7 @@ mod breadcrumb { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Breadcrumb { /// The timestamp of the breadcrumb. This is required. - #[serde(default = "SystemTime::now", with = "ts_seconds_float")] + #[serde(default = "crate::utils::now_system_time", with = "ts_seconds_float")] pub timestamp: SystemTime, /// The type of the breadcrumb. #[serde( @@ -769,7 +769,7 @@ pub struct Breadcrumb { impl Default for Breadcrumb { fn default() -> Breadcrumb { Breadcrumb { - timestamp: SystemTime::now(), + timestamp: now_system_time(), ty: breadcrumb::default_type(), category: Default::default(), level: breadcrumb::default_level(), @@ -1667,7 +1667,7 @@ pub struct Event<'a> { /// The timestamp of when the event was created. /// /// This can be set to `None` in which case the server will set a timestamp. - #[serde(default = "SystemTime::now", with = "ts_seconds_float")] + #[serde(default = "crate::utils::now_system_time", with = "ts_seconds_float")] pub timestamp: SystemTime, /// Optionally the server (or device) name of this event. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1736,7 +1736,7 @@ impl Default for Event<'_> { logger: Default::default(), modules: Default::default(), platform: event::default_platform(), - timestamp: SystemTime::now(), + timestamp: now_system_time(), server_name: Default::default(), release: Default::default(), dist: Default::default(), @@ -1843,7 +1843,7 @@ pub struct Span { )] pub timestamp: Option, /// The timestamp at the measuring of the span started. - #[serde(default = "SystemTime::now", with = "ts_seconds_float")] + #[serde(default = "crate::utils::now_system_time", with = "ts_seconds_float")] pub start_timestamp: SystemTime, /// Describes the status of the span (e.g. `ok`, `cancelled`, etc.) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1863,7 +1863,7 @@ impl Default for Span { trace_id: Default::default(), timestamp: Default::default(), tags: Default::default(), - start_timestamp: SystemTime::now(), + start_timestamp: now_system_time(), description: Default::default(), status: Default::default(), parent_span_id: Default::default(), @@ -1887,7 +1887,7 @@ impl Span { /// Finalizes the span. pub fn finish(&mut self) { - self.timestamp = Some(SystemTime::now()); + self.timestamp = Some(now_system_time()); } } @@ -2060,7 +2060,7 @@ pub struct Transaction<'a> { )] pub timestamp: Option, /// The start time of the transaction. - #[serde(default = "SystemTime::now", with = "ts_seconds_float")] + #[serde(default = "crate::utils::now_system_time", with = "ts_seconds_float")] pub start_timestamp: SystemTime, /// The collection of finished spans part of this transaction. pub spans: Vec, @@ -2092,7 +2092,7 @@ impl Default for Transaction<'_> { sdk: Default::default(), platform: event::default_platform(), timestamp: Default::default(), - start_timestamp: SystemTime::now(), + start_timestamp: now_system_time(), spans: Default::default(), contexts: Default::default(), request: Default::default(), @@ -2130,7 +2130,7 @@ impl<'a> Transaction<'a> { /// Finalizes the transaction to be dispatched. pub fn finish(&mut self) { - self.timestamp = Some(SystemTime::now()); + self.timestamp = Some(now_system_time()); } /// Finalizes the transaction to be dispatched with the given end timestamp. diff --git a/sentry-types/src/utils.rs b/sentry-types/src/utils.rs index 83d7a6581..382433db4 100644 --- a/sentry-types/src/utils.rs +++ b/sentry-types/src/utils.rs @@ -248,6 +248,16 @@ pub(crate) mod display_from_str_opt { } } +/// Returns the current wall-clock time as a [`std::time::SystemTime`], sourced from +/// [`chrono::Utc::now`] so it works on `wasm32-unknown-unknown` (where +/// [`std::time::SystemTime::now`] panics). +pub fn now_system_time() -> std::time::SystemTime { + let now = chrono::Utc::now(); + let secs = now.timestamp() as u64; + let nanos = now.timestamp_subsec_nanos(); + SystemTime::UNIX_EPOCH + Duration::new(secs, nanos) +} + #[cfg(test)] mod tests { use super::timestamp_to_datetime;