From bf4a4e0bb6d2d3964858cea494a91777e099f64e Mon Sep 17 00:00:00 2001 From: ajianaz Date: Wed, 7 Oct 2026 14:57:40 +0700 Subject: [PATCH 1/2] fix(proto): accept numeric epoch timestamps on Event and Breadcrumb Real Sentry JS/Python SDKs send epoch int/float timestamps; the Option field made envelope parsing fail and drop the event. Add tolerant deserialize_timestamp (string, i64, f64) normalizing to RFC3339 so storage is unchanged. Fixes #309 Co-Authored-By: Claude Sonnet 5.5 --- crates/trapfall-ingest/src/envelope.rs | 14 ++++++ crates/trapfall-proto/src/lib.rs | 68 +++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/crates/trapfall-ingest/src/envelope.rs b/crates/trapfall-ingest/src/envelope.rs index 780c172..85bc62c 100644 --- a/crates/trapfall-ingest/src/envelope.rs +++ b/crates/trapfall-ingest/src/envelope.rs @@ -313,6 +313,20 @@ mod tests { assert_eq!(result.events[0].message.as_deref(), Some("Null check operator used on a null value")); } + #[test] + fn parse_envelope_with_numeric_timestamps_ingests() { + // Real JS/Python SDKs send epoch numbers for event and breadcrumb timestamps. + let envelope = r#"{"event_id":"abc123","sent_at":"2026-08-27T00:00:00Z"} +{"type":"event"} +{"event_id":"abc123","timestamp":1756278600.5,"message":"boom","level":"error","breadcrumbs":{"values":[{"timestamp":1756278599,"category":"console","message":"hi"}]}}"#; + + let result = parse_envelope_text(envelope).unwrap(); + assert_eq!(result.events.len(), 1, "numeric timestamps must not drop the event"); + let ev = &result.events[0]; + assert_eq!(ev.timestamp.as_deref(), Some("2025-08-27T07:10:00.500Z")); + assert_eq!(ev.breadcrumbs.values[0].timestamp.as_deref(), Some("2025-08-27T07:09:59Z")); + } + #[test] fn parse_event_message_object_falls_back_to_message_key() { // When `formatted` is absent, fall back to the `message` template. diff --git a/crates/trapfall-proto/src/lib.rs b/crates/trapfall-proto/src/lib.rs index 9a88de8..32f148a 100644 --- a/crates/trapfall-proto/src/lib.rs +++ b/crates/trapfall-proto/src/lib.rs @@ -97,7 +97,7 @@ pub struct Stacktrace { /// A breadcrumb event leading up to an error. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Breadcrumb { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, deserialize_with = "deserialize_timestamp", skip_serializing_if = "Option::is_none")] pub timestamp: Option, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub bc_type: Option, @@ -133,9 +133,37 @@ pub struct Event { pub extra: serde_json::Value, #[serde(rename = "contexts", default)] pub contexts: serde_json::Value, + #[serde(default, deserialize_with = "deserialize_timestamp")] pub timestamp: Option, } +/// Deserialize a Sentry `timestamp` that may arrive as an RFC3339 string +/// (Rust SDK) or as epoch seconds, integer or float (JavaScript/Python SDKs). +/// +/// Numeric values are normalized to an RFC3339 string (UTC) so storage is +/// unchanged. Null yields `None`; unparseable or out-of-range numbers also +/// yield `None` rather than dropping the whole event. +pub fn deserialize_timestamp<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + Ok(value.and_then(|v| match v { + serde_json::Value::String(s) => Some(s), + serde_json::Value::Number(n) => if let Some(i) = n.as_i64() { + chrono::DateTime::from_timestamp(i, 0) + } else { + n.as_f64().filter(|f| f.is_finite()).and_then(|f| { + let secs = f.floor(); + let nanos = ((f - secs) * 1e9).round().clamp(0.0, 999_999_999.0) as u32; + chrono::DateTime::from_timestamp(secs as i64, nanos) + }) + } + .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)), + _ => None, + })) +} + /// Deserialize a Sentry event `message` field that may arrive either as a /// plain string (minimal SDKs) or as the `Message` interface object /// `{ "formatted": "...", "message": "..." }` (modern SDKs incl. Dart/Flutter). @@ -479,6 +507,44 @@ pub struct ParsedEnvelope { mod tests { use super::*; + fn event_ts(ts: &str) -> Event { + serde_json::from_str(&format!(r#"{{"event_id":"e1"{ts}}}"#)).unwrap() + } + + #[test] + fn timestamp_accepts_float_epoch() { + let e = event_ts(r#","timestamp":1756278600.5"#); + assert_eq!(e.timestamp.as_deref(), Some("2025-08-27T07:10:00.500Z")); + } + + #[test] + fn timestamp_accepts_int_epoch() { + let e = event_ts(r#","timestamp":1756278600"#); + assert_eq!(e.timestamp.as_deref(), Some("2025-08-27T07:10:00Z")); + } + + #[test] + fn timestamp_accepts_rfc3339_string() { + let e = event_ts(r#","timestamp":"2026-08-27T07:52:30Z""#); + assert_eq!(e.timestamp.as_deref(), Some("2026-08-27T07:52:30Z")); + } + + #[test] + fn timestamp_null_or_missing_is_none() { + assert!(event_ts(r#","timestamp":null"#).timestamp.is_none()); + assert!(event_ts("").timestamp.is_none()); + } + + #[test] + fn breadcrumb_timestamp_accepts_numeric_and_missing() { + let b: Breadcrumb = serde_json::from_str(r#"{"timestamp":1756278600.25,"message":"x"}"#).unwrap(); + assert_eq!(b.timestamp.as_deref(), Some("2025-08-27T07:10:00.250Z")); + let b: Breadcrumb = serde_json::from_str(r#"{"timestamp":1756278600,"message":"x"}"#).unwrap(); + assert_eq!(b.timestamp.as_deref(), Some("2025-08-27T07:10:00Z")); + let b: Breadcrumb = serde_json::from_str(r#"{"message":"x"}"#).unwrap(); + assert!(b.timestamp.is_none()); + } + #[test] fn issue_status_default_is_unresolved() { assert_eq!(IssueStatus::default(), IssueStatus::Unresolved); From 2192253f90ef9c73fbf214ff447984d4ff63e1cd Mon Sep 17 00:00:00 2001 From: ajianaz Date: Wed, 7 Oct 2026 15:52:10 +0700 Subject: [PATCH 2/2] chore(deps): bump rustls to 0.23.45 and fix npm audit advisories Resolves RUSTSEC-2026-0285 (rustls TLS 1.3 handshake) and the devalue, source-map-js and undici advisories that fail the Security workflow. Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 8 ++++---- web/package-lock.json | 22 ++++++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab88afb..29ca5ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2044,9 +2044,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -2089,9 +2089,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", diff --git a/web/package-lock.json b/web/package-lock.json index b17ad41..90ebfd1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1556,10 +1556,11 @@ } }, "node_modules/devalue": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", - "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", - "dev": true + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" }, "node_modules/dom-accessibility-api": { "version": "0.6.3", @@ -2481,10 +2482,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -2800,9 +2802,9 @@ } }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", "dev": true, "license": "MIT", "engines": {