@barrysolomon/mobile-react-native 0.1.1-alpha → 0.2.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/Dash0Mobile.podspec +4 -0
  2. package/README.md +113 -28
  3. package/android/build.gradle +73 -6
  4. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  5. package/android/gradle/wrapper/gradle-wrapper.properties +8 -0
  6. package/android/gradle.properties +27 -0
  7. package/android/gradlew +251 -0
  8. package/android/gradlew.bat +94 -0
  9. package/android/settings.gradle +62 -0
  10. package/android/src/main/java/com/dash0/mobile/reactnative/BridgeCallSink.kt +42 -0
  11. package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobileModule.kt +35 -7
  12. package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobilePackage.kt +71 -2
  13. package/android/src/main/java/com/dash0/mobile/reactnative/NetworkInstrumentation.kt +28 -0
  14. package/android/src/main/java/com/dash0/mobile/reactnative/OTelMobileCallSink.kt +61 -1
  15. package/android/src/main/java/com/dash0/mobile/reactnative/OTelNetworkInterceptor.kt +206 -0
  16. package/android/src/test/java/com/dash0/mobile/reactnative/Dash0MobileModuleTest.kt +81 -6
  17. package/android/src/test/java/com/dash0/mobile/reactnative/OTelNetworkInterceptorTest.kt +381 -0
  18. package/ios/BoundedLiveSpanStore.swift +138 -0
  19. package/ios/BridgeCallSink.swift +49 -1
  20. package/ios/Dash0MobileBridgeDispatcher.swift +23 -1
  21. package/ios/OTelMobileCallSink.swift +205 -34
  22. package/ios/RCTDash0MobileModule.swift +10 -2
  23. package/ios/Tests/BoundedLiveSpanStoreTests.swift +143 -0
  24. package/ios/Tests/Dash0MobileBridgeDispatcherTests.swift +63 -0
  25. package/ios/Tests/OTelMobileCallSinkTests.swift +243 -0
  26. package/lib/bridge/types.d.ts +35 -0
  27. package/lib/bridge/types.d.ts.map +1 -1
  28. package/lib/index.d.ts +1 -1
  29. package/lib/index.d.ts.map +1 -1
  30. package/lib/index.js +42 -2
  31. package/lib/instrumentation/errors.d.ts.map +1 -1
  32. package/lib/instrumentation/errors.js +13 -2
  33. package/lib/instrumentation/fetch.d.ts.map +1 -1
  34. package/lib/instrumentation/fetch.js +58 -22
  35. package/lib/instrumentation/navigation.d.ts.map +1 -1
  36. package/lib/instrumentation/navigation.js +32 -8
  37. package/lib/instrumentation/unhandledRejection.d.ts.map +1 -1
  38. package/lib/instrumentation/unhandledRejection.js +13 -3
  39. package/lib/instrumentation/xhr.d.ts.map +1 -1
  40. package/lib/instrumentation/xhr.js +63 -36
  41. package/lib/redact.d.ts +30 -0
  42. package/lib/redact.d.ts.map +1 -0
  43. package/lib/redact.js +67 -0
  44. package/package.json +1 -1
  45. package/src/bridge/types.ts +36 -0
  46. package/src/index.ts +44 -3
  47. package/src/instrumentation/errors.ts +12 -2
  48. package/src/instrumentation/fetch.ts +60 -27
  49. package/src/instrumentation/navigation.ts +40 -8
  50. package/src/instrumentation/unhandledRejection.ts +12 -3
  51. package/src/instrumentation/xhr.ts +65 -40
  52. package/src/redact.ts +71 -0
@@ -0,0 +1,243 @@
1
+ // Direct unit tests for the PRODUCTION RN-iOS sink, `OTelMobileCallSink`.
2
+ //
3
+ // Historically this file was excluded from every build/test target (it needs
4
+ // `OTelMobileSDK`), so the code that turns RN bridge calls into real OTel
5
+ // spans/logs on iOS had ZERO direct coverage — the #1 RN-iOS risk flagged in
6
+ // the production-readiness review. The package now path-depends on the sibling
7
+ // iOS SDK so the sink compiles, and the sink exposes an `init(telemetry:)`
8
+ // seam. These tests drive the real sink against in-memory exporters and assert
9
+ // the resulting spans/logs — exercising parent linkage, LRU eviction,
10
+ // span-kind / severity mapping, attribute coercion, and the no-crash guards.
11
+ //
12
+ // Runs under `xcodebuild test` / `swift test` where swift-testing resolves.
13
+
14
+ #if canImport(OTelMobileSDK)
15
+ import Foundation
16
+ import Testing
17
+ import OpenTelemetryApi
18
+ import OpenTelemetrySdk
19
+ @testable import Dash0MobileReactNative
20
+
21
+ /// Minimal in-memory `SpanExporter`. Paired with `ImmediateSpanProcessor`
22
+ /// (below) so finished spans are readable synchronously, the instant an
23
+ /// `endSpan` / eviction returns.
24
+ private final class CapturingSpanExporter: SpanExporter, @unchecked Sendable {
25
+ private let lock = NSLock()
26
+ private var _spans: [SpanData] = []
27
+ var spans: [SpanData] { lock.lock(); defer { lock.unlock() }; return _spans }
28
+
29
+ func export(spans: [SpanData], explicitTimeout: TimeInterval?) -> SpanExporterResultCode {
30
+ lock.lock(); _spans.append(contentsOf: spans); lock.unlock()
31
+ return .success
32
+ }
33
+ func flush(explicitTimeout: TimeInterval?) -> SpanExporterResultCode { .success }
34
+ func shutdown(explicitTimeout: TimeInterval?) {}
35
+ }
36
+
37
+ /// Minimal in-memory `LogRecordExporter` (the SDK's own `InMemoryLogRecordExporter`
38
+ /// has an internal initializer, so we capture here). `SimpleLogRecordProcessor`
39
+ /// calls `export` synchronously on every `emit`.
40
+ private final class CapturingLogRecordExporter: LogRecordExporter, @unchecked Sendable {
41
+ private let lock = NSLock()
42
+ private var _records: [ReadableLogRecord] = []
43
+ var records: [ReadableLogRecord] { lock.lock(); defer { lock.unlock() }; return _records }
44
+
45
+ func export(logRecords: [ReadableLogRecord], explicitTimeout: TimeInterval?) -> ExportResult {
46
+ lock.lock(); _records.append(contentsOf: logRecords); lock.unlock()
47
+ return .success
48
+ }
49
+ func forceFlush(explicitTimeout: TimeInterval?) -> ExportResult { .success }
50
+ func shutdown(explicitTimeout: TimeInterval?) {}
51
+ }
52
+
53
+ /// Synchronous span processor for tests. The SDK's `SimpleSpanProcessor`
54
+ /// exports on a background `DispatchQueue`, which races test assertions; this
55
+ /// one exports inline on `onEnd` so a finished span is visible to the exporter
56
+ /// the instant `endSpan` (or an eviction) returns — fully deterministic.
57
+ private final class ImmediateSpanProcessor: SpanProcessor {
58
+ private let exporter: SpanExporter
59
+ init(_ exporter: SpanExporter) { self.exporter = exporter }
60
+ var isStartRequired: Bool { false }
61
+ var isEndRequired: Bool { true }
62
+ func onStart(parentContext: SpanContext?, span: ReadableSpan) {}
63
+ func onEnd(span: ReadableSpan) { _ = exporter.export(spans: [span.toSpanData()], explicitTimeout: nil) }
64
+ func shutdown(explicitTimeout: TimeInterval?) {}
65
+ func forceFlush(timeout: TimeInterval?) {}
66
+ }
67
+
68
+ /// Builds a sink wired to capturing exporters via the test-injection seam, plus
69
+ /// the exporters so tests can assert on what was emitted. `capacity` controls
70
+ /// the live-span LRU cap so eviction is testable without 2048 spans.
71
+ private func makeSink(capacity: Int = OTelMobileCallSink.maxLiveSpans)
72
+ -> (sink: OTelMobileCallSink, spans: CapturingSpanExporter, logs: CapturingLogRecordExporter) {
73
+ let spanExporter = CapturingSpanExporter()
74
+ let tracer = TracerProviderBuilder()
75
+ .add(spanProcessor: ImmediateSpanProcessor(spanExporter))
76
+ .build()
77
+ .get(instrumentationName: "test")
78
+
79
+ let logExporter = CapturingLogRecordExporter()
80
+ let logger = LoggerProviderBuilder()
81
+ .with(processors: [SimpleLogRecordProcessor(logRecordExporter: logExporter)])
82
+ .build()
83
+ .get(instrumentationScopeName: "test")
84
+
85
+ let telemetry = SinkTelemetry(
86
+ tracer: tracer, logger: logger, meter: nil,
87
+ forceFlush: {}, flushWindow: { _ in }, shutdown: {}
88
+ )
89
+ let sink = OTelMobileCallSink(telemetry: telemetry, capacity: capacity)
90
+ sink.start(BridgeStartConfig(
91
+ serviceName: "test", serviceVersion: "1.0",
92
+ endpoint: "https://example.test", authToken: nil, dataset: nil
93
+ ))
94
+ return (sink, spanExporter, logExporter)
95
+ }
96
+
97
+ @Suite("OTelMobileCallSink (RN-iOS production sink)")
98
+ struct OTelMobileCallSinkTests {
99
+ private let t0: UInt64 = 1_700_000_000_000_000_000
100
+ private func t(_ offsetNanos: UInt64) -> UInt64 { t0 + offsetNanos }
101
+
102
+ /// Spin-wait for a condition. With `ImmediateSpanProcessor` exports are
103
+ /// synchronous so this returns on the first check; it is retained as a
104
+ /// small safety margin and to drain (timeout form) when asserting that a
105
+ /// no-op produced nothing. Returns the final value of `condition`.
106
+ private func waitFor(_ condition: @autoclosure () -> Bool, timeout: TimeInterval = 3.0) -> Bool {
107
+ let deadline = Date().addingTimeInterval(timeout)
108
+ while !condition() && Date() < deadline {
109
+ RunLoop.current.run(until: Date().addingTimeInterval(0.01))
110
+ }
111
+ return condition()
112
+ }
113
+
114
+ @Test("emitLog produces a record with mapped severity, body, and coerced attributes")
115
+ func emitLogProducesRecord() {
116
+ let (sink, _, logs) = makeSink()
117
+ sink.emitLog(name: "app.error", severity: 17,
118
+ attributes: ["http.status": 500, "ok": false, "where": "x"],
119
+ timeUnixNano: t(0))
120
+ let records = logs.records
121
+ #expect(records.count == 1)
122
+ #expect(records.first?.severity == .error) // 17 -> ERROR
123
+ #expect(records.first?.body == .string("app.error"))
124
+ // RN bridges 500 as an integer-valued NSNumber -> must land as .int.
125
+ #expect(records.first?.attributes["http.status"] == .int(500))
126
+ #expect(records.first?.attributes["ok"] == .bool(false))
127
+ }
128
+
129
+ @Test("startSpan + endSpan exports one span with the right name, kind, and status")
130
+ func spanRoundTrip() {
131
+ let (sink, spans, _) = makeSink()
132
+ sink.startSpan(spanId: "a", parentSpanId: nil, name: "GET /x",
133
+ spanKind: "CLIENT", attributes: [:], startTimeUnixNano: t(0))
134
+ #expect(spans.spans.isEmpty) // not exported until ended
135
+ sink.endSpan(spanId: "a", status: "OK", statusMessage: nil,
136
+ attributes: ["http.method": "GET"], endTimeUnixNano: t(1_000))
137
+ #expect(waitFor(spans.spans.count == 1))
138
+ let s = spans.spans[0]
139
+ #expect(s.name == "GET /x")
140
+ #expect(s.kind == .client)
141
+ #expect(s.status == .ok)
142
+ #expect(s.attributes["http.method"] == .string("GET"))
143
+ }
144
+
145
+ @Test("parent linkage stitches child into the parent's trace")
146
+ func parentLinkage() {
147
+ let (sink, spans, _) = makeSink()
148
+ sink.startSpan(spanId: "p", parentSpanId: nil, name: "screen",
149
+ spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(0))
150
+ sink.startSpan(spanId: "c", parentSpanId: "p", name: "fetch",
151
+ spanKind: "CLIENT", attributes: [:], startTimeUnixNano: t(10))
152
+ sink.endSpan(spanId: "c", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(20))
153
+ sink.endSpan(spanId: "p", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(30))
154
+
155
+ #expect(waitFor(spans.spans.count == 2))
156
+ let parent = spans.spans.first { $0.name == "screen" }
157
+ let child = spans.spans.first { $0.name == "fetch" }
158
+ #expect(parent != nil && child != nil)
159
+ // Real OTel parent-child linkage: shared trace id + child points at parent.
160
+ #expect(child?.traceId == parent?.traceId)
161
+ #expect(child?.parentSpanId == parent?.spanId)
162
+ // Bridge-supplied JS id mirrored as an attribute for cross-referencing.
163
+ #expect(child?.attributes["parent.span.id"] == .string("p"))
164
+ }
165
+
166
+ @Test("liveSpans LRU evicts the oldest orphaned span and ends it as ERROR")
167
+ func evictionEndsOldestAsError() {
168
+ let (sink, spans, _) = makeSink(capacity: 2)
169
+ // Three starts, no ends. At capacity 2, starting s3 must evict s1.
170
+ sink.startSpan(spanId: "s1", parentSpanId: nil, name: "s1", spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(0))
171
+ sink.startSpan(spanId: "s2", parentSpanId: nil, name: "s2", spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(10))
172
+ sink.startSpan(spanId: "s3", parentSpanId: nil, name: "s3", spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(20))
173
+
174
+ // Only the evicted s1 has been ended (by eviction) and thus exported
175
+ // (async via SimpleSpanProcessor — wait for it).
176
+ #expect(waitFor(spans.spans.contains { $0.name == "s1" }))
177
+ let evicted = spans.spans.first { $0.name == "s1" }
178
+ #expect(evicted != nil)
179
+ #expect(evicted?.status != .ok) // ended as ERROR, not OK
180
+ if case .error = evicted?.status {} else { Issue.record("evicted span should be ERROR") }
181
+ #expect(spans.spans.contains { $0.name == "s2" } == false) // still live, not ended
182
+
183
+ // Ending the still-live ones works and does not double-export s1.
184
+ sink.endSpan(spanId: "s2", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(30))
185
+ sink.endSpan(spanId: "s3", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(40))
186
+ #expect(waitFor(spans.spans.count == 3))
187
+ #expect(spans.spans.filter { $0.name == "s1" }.count == 1)
188
+ }
189
+
190
+ @Test("ending an unknown span id is a safe no-op")
191
+ func endUnknownSpanNoop() {
192
+ let (sink, spans, _) = makeSink()
193
+ sink.endSpan(spanId: "nope", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(0))
194
+ #expect(spans.spans.isEmpty)
195
+ }
196
+
197
+ @Test("recordMetric with a nil meter is a safe no-op (no crash on non-finite values)")
198
+ func metricNoMeterNoCrash() {
199
+ // Sink built WITHOUT a meter (meter: nil) — recordMetric must early-return.
200
+ // Passing NaN/Inf also exercises that the counter path's value guard would
201
+ // never trap even if a meter were present.
202
+ let (sink, _, _) = makeSink()
203
+ sink.recordMetric(name: "c", instrumentType: "counter", value: .nan, attributes: [:], timeUnixNano: t(0))
204
+ sink.recordMetric(name: "c", instrumentType: "counter", value: .infinity, attributes: [:], timeUnixNano: t(0))
205
+ sink.recordMetric(name: "g", instrumentType: "gauge", value: 1.5, attributes: [:], timeUnixNano: t(0))
206
+ // No assertion needed beyond "did not crash"; reaching here is the pass.
207
+ #expect(Bool(true))
208
+ }
209
+
210
+ @Test("emit before start / after shutdown is a safe no-op")
211
+ func lifecycleGuards() {
212
+ // A sink with injected telemetry only adopts it on start(); before start,
213
+ // telemetry is nil so emits are no-ops. After shutdown, telemetry is
214
+ // cleared and live spans drained.
215
+ let spanExporter = CapturingSpanExporter()
216
+ let tracer = TracerProviderBuilder()
217
+ .add(spanProcessor: ImmediateSpanProcessor(spanExporter))
218
+ .build().get(instrumentationName: "test")
219
+ let telemetry = SinkTelemetry(tracer: tracer, logger: nil, meter: nil,
220
+ forceFlush: {}, flushWindow: { _ in }, shutdown: {})
221
+ let sink = OTelMobileCallSink(telemetry: telemetry, capacity: 8)
222
+
223
+ // Before start: no-op (telemetry not yet adopted).
224
+ sink.startSpan(spanId: "x", parentSpanId: nil, name: "x", spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(0))
225
+ sink.endSpan(spanId: "x", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(1))
226
+
227
+ // After start: works.
228
+ sink.start(BridgeStartConfig(serviceName: "s", serviceVersion: nil, endpoint: "https://e.test", authToken: nil, dataset: nil))
229
+ sink.startSpan(spanId: "y", parentSpanId: nil, name: "y", spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(0))
230
+ sink.endSpan(spanId: "y", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(1))
231
+ #expect(waitFor(spanExporter.spans.count == 1))
232
+ #expect(spanExporter.spans.allSatisfy { $0.name == "y" }) // x (pre-start) never exported
233
+
234
+ // After shutdown: emits are no-ops again.
235
+ sink.shutdown()
236
+ sink.startSpan(spanId: "z", parentSpanId: nil, name: "z", spanKind: "INTERNAL", attributes: [:], startTimeUnixNano: t(0))
237
+ sink.endSpan(spanId: "z", status: "OK", statusMessage: nil, attributes: [:], endTimeUnixNano: t(1))
238
+ // Drain the processor queue, then confirm z produced nothing new.
239
+ _ = waitFor(false, timeout: 0.3)
240
+ #expect(spanExporter.spans.count == 1) // unchanged
241
+ }
242
+ }
243
+ #endif
@@ -48,6 +48,35 @@ export interface WireframeAutoCapture {
48
48
  /** Emit `ui.wireframe.ref` with `mobile.wireframe.id` instead of the full JSON when content matches the last capture. Default `true`. */
49
49
  dedupeByContentHash?: boolean;
50
50
  }
51
+ /**
52
+ * Trace sampling strategy passed through the RN bridge into the native SDK's
53
+ * `SamplingConfig`. Field names and semantics mirror Android's
54
+ * `io.opentelemetry.android.mobile.sampling.SamplingConfig` and iOS's
55
+ * `SamplingConfig` factory constructors one-to-one.
56
+ *
57
+ * - `always_on` → `SamplingConfig.alwaysOn()` — sample 100% of spans.
58
+ * - `always_off` → `SamplingConfig.alwaysOff()` — drop all spans.
59
+ * - `dynamic` → `SamplingConfig.dynamic(normalRate, highPriorityRate)`
60
+ * — `normalRate` baseline with `highPriorityRate` for high-priority
61
+ * spans (page.* / app.startup).
62
+ *
63
+ * RN DEFAULT: when `sampling` is omitted the bridge defaults to `always_on`
64
+ * (NOT the native SDK's `dynamic(0.1)` default). RN manual spans
65
+ * (`Dash0Mobile.startSpan()`) are ROOT spans with arbitrary names; a 10%
66
+ * baseline silently drops ~90% of a user's very first span, which is a
67
+ * terrible first-run experience and historically read as "spans vanish."
68
+ * Sampling/rate-limiting for RN architectures belongs in the collector, not
69
+ * the on-device SDK. Pass an explicit `sampling` to opt back into on-device
70
+ * sampling. The native-only Android/iOS SDKs keep their `dynamic(0.1)`
71
+ * default — only the RN-bridged default changes.
72
+ */
73
+ export interface SamplingConfig {
74
+ strategy: 'always_on' | 'always_off' | 'dynamic';
75
+ /** Baseline rate for `dynamic` (0.0–1.0). Ignored for always_on/always_off. */
76
+ normalRate?: number;
77
+ /** Rate for high-priority spans under `dynamic` (0.0–1.0). Defaults to 1.0 natively. */
78
+ highPriorityRate?: number;
79
+ }
51
80
  export interface StartConfig {
52
81
  serviceName: string;
53
82
  serviceVersion?: string;
@@ -59,6 +88,12 @@ export interface StartConfig {
59
88
  diskBytes?: number;
60
89
  };
61
90
  enablePolicyPolling?: boolean;
91
+ /**
92
+ * Trace sampling strategy. Defaults to `always_on` on RN (see
93
+ * {@link SamplingConfig}) — unlike the native SDKs' `dynamic(0.1)` default.
94
+ * Omit to keep all spans on-device and rate-limit in the collector.
95
+ */
96
+ sampling?: SamplingConfig;
62
97
  /**
63
98
  * Toggles for JS-side auto-instrumentation (fetch/XHR spans, JS error +
64
99
  * unhandled rejection logs). Defaults to all-on. RN bridge uses the same
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bridge/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bridge/types.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * See docs/epics/REACT_NATIVE_EPIC.md.
8
8
  */
9
9
  import type { Attributes, NativeDash0MobileModule, SeverityNumber, SpanKind, StartConfig } from './bridge/types';
10
- export type { Attributes, SeverityNumber, StartConfig } from './bridge/types';
10
+ export type { Attributes, SamplingConfig, SeverityNumber, StartConfig } from './bridge/types';
11
11
  export { installReactNavigationInstrumentation } from './instrumentation/navigation';
12
12
  export { withTapTelemetry } from './instrumentation/touch';
13
13
  export { otel } from './otel-compat';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,UAAU,EAEV,uBAAuB,EACvB,cAAc,EACd,QAAQ,EAER,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,qCAAqC,EAAE,MAAM,8BAA8B,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAClE,SAAS,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1D,GAAG,IAAI,IAAI,CAAC;CACb;AA8ED,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;AAyCD,eAAO,MAAM,WAAW;kBACF,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;cA6DrC,MAAM,eAAc,UAAU,aAAiB,cAAc,GAAO,IAAI;oBAwBlE,MAAM,eAAc,UAAU,aAAiB,QAAQ,GAAgB,UAAU;SAsDtF,CAAC,QACJ,MAAM,MACR,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,eAC7B,UAAU,GACtB,OAAO,CAAC,CAAC,CAAC;uBAeL,MAAM,SACL,MAAM,mBACG,SAAS,GAAG,WAAW,GAAG,OAAO,eACrC,UAAU,GACrB,IAAI;yBAYoB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;uBAYxB,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;0BAO5B,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gCAOjB,MAAM,GAAc,OAAO,CAAC,IAAI,CAAC;+BAOlC,MAAM,GAAc,OAAO,CAAC,IAAI,CAAC;gBAO/C,OAAO,CAAC,IAAI,CAAC;CAgBhC,CAAC;AA4DF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI,CAElF;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAKxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,UAAU,EAEV,uBAAuB,EAEvB,cAAc,EACd,QAAQ,EAER,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC9F,OAAO,EAAE,qCAAqC,EAAE,MAAM,8BAA8B,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAClE,SAAS,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1D,GAAG,IAAI,IAAI,CAAC;CACb;AA8ED,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;AAyCD,eAAO,MAAM,WAAW;kBACF,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;cAoFrC,MAAM,eAAc,UAAU,aAAiB,cAAc,GAAO,IAAI;oBAwBlE,MAAM,eAAc,UAAU,aAAiB,QAAQ,GAAgB,UAAU;SAsDtF,CAAC,QACJ,MAAM,MACR,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,eAC7B,UAAU,GACtB,OAAO,CAAC,CAAC,CAAC;uBAeL,MAAM,SACL,MAAM,mBACG,SAAS,GAAG,WAAW,GAAG,OAAO,eACrC,UAAU,GACrB,IAAI;yBAYoB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;uBAYxB,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;0BAO5B,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gCAOjB,MAAM,GAAc,OAAO,CAAC,IAAI,CAAC;+BAOlC,MAAM,GAAc,OAAO,CAAC,IAAI,CAAC;gBAO/C,OAAO,CAAC,IAAI,CAAC;CAgBhC,CAAC;AA6EF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI,CAElF;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAKxC"}
package/lib/index.js CHANGED
@@ -105,7 +105,7 @@ function resolveNative() {
105
105
  // callers and correlate issues to a specific bridge release. Keep this in
106
106
  // sync with package.json on each release.
107
107
  const DISTRO_NAME = 'dash0-react-native';
108
- const DISTRO_VERSION = '0.1.0-alpha';
108
+ const DISTRO_VERSION = '0.2.1-alpha';
109
109
  function resolveReactNativeVersion() {
110
110
  try {
111
111
  // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -136,8 +136,16 @@ export const Dash0Mobile = {
136
136
  // iOS URLProtocol / NSException / signal-handler swizzles by default,
137
137
  // which collide with RN's new-arch JS event loop.
138
138
  const nativeAutoCapture = buildNativeAutoCaptureTokens(config.autoCapture);
139
+ // RN sampling default: always_on. RN manual spans are root spans with
140
+ // arbitrary names, so the native SDKs' dynamic(0.1) default would
141
+ // silently drop ~90% of a user's first span (Loper finding #4). Sampling
142
+ // for RN architectures belongs in the collector — callers can still opt
143
+ // into on-device sampling by passing an explicit `sampling`. See the
144
+ // SamplingConfig doc comment in bridge/types.ts.
145
+ const sampling = config.sampling ?? { strategy: 'always_on' };
139
146
  const mergedConfig = {
140
147
  ...config,
148
+ sampling,
141
149
  extraResourceAttributes: {
142
150
  'telemetry.distro.name': DISTRO_NAME,
143
151
  'telemetry.distro.version': DISTRO_VERSION,
@@ -162,7 +170,22 @@ export const Dash0Mobile = {
162
170
  if (!isReactNative()) {
163
171
  autoInstrUninstallers.push(installFetchInstrumentation({ ignoredHosts }));
164
172
  }
165
- autoInstrUninstallers.push(installXhrInstrumentation({ ignoredHosts }));
173
+ // Android network capture is owned by the native OkHttp interceptor
174
+ // (OTelNetworkInterceptor), installed pre-JS in Dash0MobilePackage. The
175
+ // native layer is the only one that sees traffic under Expo SDK 52+ —
176
+ // expo/fetch routes through OkHttp directly and never touches the JS
177
+ // `fetch`/`XMLHttpRequest` globals these shims wrap, so the JS XHR shim
178
+ // sees zero traffic there. The native interceptor also injects a
179
+ // W3C `traceparent` built from the real native span context, which the
180
+ // JS shim cannot do. Installing the JS XHR shim on Android on top of the
181
+ // native interceptor would (for non-Expo apps) double-count every
182
+ // request. So: gate the JS XHR shim OFF on Android. iOS keeps the JS XHR
183
+ // shim — its native URLProtocol is opt-in (off by default for RN), and
184
+ // RN iOS fetch is XHR-backed, so XHR remains the authoritative JS layer
185
+ // there.
186
+ if (!isAndroid()) {
187
+ autoInstrUninstallers.push(installXhrInstrumentation({ ignoredHosts }));
188
+ }
166
189
  }
167
190
  if (auto.errors !== false) {
168
191
  autoInstrUninstallers.push(installErrorInstrumentation());
@@ -358,6 +381,23 @@ function isReactNative() {
358
381
  const nav = globalThis.navigator;
359
382
  return nav?.product === 'ReactNative';
360
383
  }
384
+ // True only on React Native running on Android. Reads `Platform.OS` from the
385
+ // react-native module rather than a global so it's accurate on both old and
386
+ // new arch. Used to gate OFF the JS `XMLHttpRequest` shim on Android, where
387
+ // the native OkHttp interceptor (OTelNetworkInterceptor) owns network capture
388
+ // and traceparent injection. Any failure to resolve `Platform` (Jest, SSR,
389
+ // non-RN) returns false so those environments keep the JS shim — exactly the
390
+ // behavior the dedup test pins.
391
+ function isAndroid() {
392
+ try {
393
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
394
+ const rn = require('react-native');
395
+ return rn?.Platform?.OS === 'android';
396
+ }
397
+ catch {
398
+ return false;
399
+ }
400
+ }
361
401
  const NATIVE_AUTO_CAPTURE_FLAGS = [
362
402
  ['network', 'network'],
363
403
  ['errors', 'errors'],
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/instrumentation/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAsB9C,wBAAgB,2BAA2B,IAAI,MAAM,IAAI,CA+CxD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/instrumentation/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAwB9C,wBAAgB,2BAA2B,IAAI,MAAM,IAAI,CAsDxD"}
@@ -10,7 +10,10 @@
10
10
  * policy (loops that throw on every frame shouldn't DoS the collector).
11
11
  */
12
12
  import { Dash0Mobile } from '../index';
13
+ import { sanitizeMessage, sanitizeStacktrace } from '../redact';
13
14
  export const DEDUPE_WINDOW_MS = 5 * 60 * 1000;
15
+ /** Cap on the dedupe map so a high-cardinality error storm can't leak memory. */
16
+ const DEDUPE_MAX_ENTRIES = 256;
14
17
  const SEVERITY_ERROR = 17;
15
18
  const SEVERITY_FATAL = 21;
16
19
  function resolveErrorUtils() {
@@ -36,11 +39,19 @@ export function installErrorInstrumentation() {
36
39
  const now = Date.now();
37
40
  const lastAt = recentlySeen.get(key);
38
41
  if (lastAt === undefined || now - lastAt >= DEDUPE_WINDOW_MS) {
42
+ // LRU-style insert: re-key so the freshest entries stay, then evict the
43
+ // oldest if we've blown the cap. Bounds memory under an error storm.
44
+ recentlySeen.delete(key);
39
45
  recentlySeen.set(key, now);
46
+ if (recentlySeen.size > DEDUPE_MAX_ENTRIES) {
47
+ const oldest = recentlySeen.keys().next().value;
48
+ if (oldest !== undefined)
49
+ recentlySeen.delete(oldest);
50
+ }
40
51
  Dash0Mobile.log('app.error', {
41
52
  'exception.type': error?.name ?? 'Error',
42
- 'exception.message': error?.message ?? String(error),
43
- 'exception.stacktrace': error?.stack ?? '',
53
+ 'exception.message': sanitizeMessage(error?.message ?? String(error)),
54
+ 'exception.stacktrace': sanitizeStacktrace(error?.stack ?? ''),
44
55
  }, isFatal ? SEVERITY_FATAL : SEVERITY_ERROR);
45
56
  }
46
57
  // Always chain — our capture must never swallow crashes from other
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/instrumentation/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAoBD,wBAAgB,2BAA2B,CACzC,MAAM,GAAE,0BAA+B,GACtC,MAAM,IAAI,CAsDZ"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/instrumentation/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAoBD,wBAAgB,2BAA2B,CACzC,MAAM,GAAE,0BAA+B,GACtC,MAAM,IAAI,CAsFZ"}
@@ -10,6 +10,7 @@
10
10
  * OTLP export requests don't generate spans that then need to be exported.
11
11
  */
12
12
  import { Dash0Mobile } from '../index';
13
+ import { sanitizeUrl } from '../redact';
13
14
  function hostFromUrl(url) {
14
15
  const match = /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i.exec(url);
15
16
  return match ? match[1].toLowerCase() : null;
@@ -29,47 +30,82 @@ export function installFetchInstrumentation(config = {}) {
29
30
  const original = globalThis.fetch;
30
31
  const ignored = new Set((config.ignoredHosts ?? []).map(h => h.toLowerCase()));
31
32
  const wrapped = async (input, init) => {
32
- const url = resolveUrl(input);
33
- const host = hostFromUrl(url);
34
- if (host && ignored.has(host)) {
33
+ // Telemetry setup must never break the host's request. Build the span
34
+ // defensively; on ANY failure fall through to the raw call.
35
+ let handle;
36
+ try {
37
+ const url = resolveUrl(input);
38
+ const host = hostFromUrl(url);
39
+ if (host && ignored.has(host)) {
40
+ return original(input, init);
41
+ }
42
+ const method = resolveMethod(input, init);
43
+ handle = Dash0Mobile.startSpan(method, {
44
+ 'http.request.method': method,
45
+ 'url.full': sanitizeUrl(url),
46
+ ...(host ? { 'server.address': host } : {}),
47
+ }, 'CLIENT');
48
+ }
49
+ catch (telemetryErr) {
50
+ // Span setup failed — host request must still proceed unobserved.
51
+ // eslint-disable-next-line no-console
52
+ console.warn?.('[@dash0/mobile] fetch instrumentation setup failed', telemetryErr);
35
53
  return original(input, init);
36
54
  }
37
- const method = resolveMethod(input, init);
38
- const handle = Dash0Mobile.startSpan(method, {
39
- 'http.request.method': method,
40
- 'url.full': url,
41
- ...(host ? { 'server.address': host } : {}),
42
- }, 'CLIENT');
43
55
  try {
44
56
  const response = await original(input, init);
45
- const status = response?.status;
46
- if (typeof status === 'number') {
47
- handle.setAttribute('http.response.status_code', status);
48
- if (status >= 400) {
49
- handle.setStatus('ERROR', `HTTP ${status}`);
57
+ try {
58
+ const status = response?.status;
59
+ if (typeof status === 'number') {
60
+ handle.setAttribute('http.response.status_code', status);
61
+ if (status >= 400) {
62
+ handle.setStatus('ERROR', `HTTP ${status}`);
63
+ }
64
+ else {
65
+ handle.setStatus('OK');
66
+ }
50
67
  }
51
68
  else {
52
69
  handle.setStatus('OK');
53
70
  }
54
71
  }
55
- else {
56
- handle.setStatus('OK');
72
+ catch {
73
+ // Telemetry bookkeeping failure must not mask a successful response.
57
74
  }
58
75
  return response;
59
76
  }
60
77
  catch (err) {
61
- const message = err instanceof Error ? err.message : String(err);
62
- handle.setStatus('ERROR', message);
78
+ try {
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ handle.setStatus('ERROR', message);
81
+ }
82
+ catch {
83
+ // ignore telemetry failure on the error path
84
+ }
63
85
  throw err;
64
86
  }
65
87
  finally {
66
- handle.end();
88
+ try {
89
+ handle.end();
90
+ }
91
+ catch {
92
+ // ignore
93
+ }
67
94
  }
68
95
  };
69
- globalThis.fetch = wrapped;
96
+ const g = globalThis;
97
+ // Double-install guard: Fast Refresh / repeated start() would otherwise
98
+ // stack wrappers and leak the captured `original`.
99
+ if (g.fetch && g.fetch.__dash0_installed) {
100
+ return () => { };
101
+ }
102
+ wrapped.__dash0_installed = true;
103
+ g.fetch = wrapped;
70
104
  return function uninstall() {
71
- if (globalThis.fetch === wrapped) {
72
- globalThis.fetch = original;
105
+ if (g.fetch === wrapped) {
106
+ g.fetch = original;
73
107
  }
108
+ // Clear the guard so a later install() can re-instrument.
109
+ delete wrapped.__dash0_installed;
74
110
  };
75
111
  }
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/instrumentation/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,UAAU,UAAU;IAClB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5D,eAAe,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACjD;AAED,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,IAAI,CA8BZ"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/instrumentation/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,UAAU,UAAU;IAClB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5D,eAAe,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACjD;AAED,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,IAAI,CA8DZ"}
@@ -14,26 +14,50 @@ export function installReactNavigationInstrumentation(navRef) {
14
14
  return () => { };
15
15
  let currentName = null;
16
16
  let currentSpan = null;
17
+ const endCurrentSpan = () => {
18
+ if (currentSpan) {
19
+ currentSpan.end();
20
+ currentSpan = null;
21
+ }
22
+ };
17
23
  const onState = () => {
18
24
  const route = navRef.getCurrentRoute();
19
25
  if (!route)
20
26
  return;
21
27
  if (route.name === currentName)
22
28
  return;
23
- if (currentSpan) {
24
- currentSpan.end();
25
- currentSpan = null;
26
- }
29
+ endCurrentSpan();
27
30
  currentName = route.name;
28
31
  Dash0Mobile.log('ui.screen_view', { 'screen.name': route.name }, 9);
29
32
  currentSpan = Dash0Mobile.startSpan(`page.${route.name}`);
30
33
  };
31
34
  const unsub = navRef.addListener('state', onState);
35
+ // End the active route span when the app leaves the foreground. Without
36
+ // this, a span started on the visible screen stays open until the next
37
+ // route change or uninstall — which on background may be minutes/never,
38
+ // producing absurd durations. Re-entering 'active' lets the next route
39
+ // change open a fresh span. AppState is required lazily so non-RN
40
+ // (Jest/SSR) environments don't need the native module.
41
+ let appStateSub;
42
+ try {
43
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
44
+ const { AppState } = require('react-native');
45
+ if (AppState && typeof AppState.addEventListener === 'function') {
46
+ appStateSub = AppState.addEventListener('change', (state) => {
47
+ if (state === 'background' || state === 'inactive') {
48
+ endCurrentSpan();
49
+ // Force the next foregrounded route to re-open a span.
50
+ currentName = null;
51
+ }
52
+ });
53
+ }
54
+ }
55
+ catch {
56
+ // No AppState available (test/SSR) — background-end is best-effort.
57
+ }
32
58
  return function uninstall() {
33
59
  unsub();
34
- if (currentSpan) {
35
- currentSpan.end();
36
- currentSpan = null;
37
- }
60
+ appStateSub?.remove?.();
61
+ endCurrentSpan();
38
62
  };
39
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"unhandledRejection.d.ts","sourceRoot":"","sources":["../../src/instrumentation/unhandledRejection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsBH,wBAAgB,wCAAwC,IAAI,MAAM,IAAI,CA8CrE"}
1
+ {"version":3,"file":"unhandledRejection.d.ts","sourceRoot":"","sources":["../../src/instrumentation/unhandledRejection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyBH,wBAAgB,wCAAwC,IAAI,MAAM,IAAI,CAoDrE"}