@barrysolomon/mobile-react-native 0.1.1-alpha → 0.2.0-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.
- package/Dash0Mobile.podspec +4 -0
- package/README.md +27 -0
- package/android/build.gradle +73 -6
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/android/gradle.properties +27 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle +62 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/BridgeCallSink.kt +42 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobileModule.kt +35 -7
- package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobilePackage.kt +71 -2
- package/android/src/main/java/com/dash0/mobile/reactnative/NetworkInstrumentation.kt +28 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/OTelMobileCallSink.kt +61 -1
- package/android/src/main/java/com/dash0/mobile/reactnative/OTelNetworkInterceptor.kt +206 -0
- package/android/src/test/java/com/dash0/mobile/reactnative/Dash0MobileModuleTest.kt +81 -6
- package/android/src/test/java/com/dash0/mobile/reactnative/OTelNetworkInterceptorTest.kt +381 -0
- package/ios/BoundedLiveSpanStore.swift +138 -0
- package/ios/BridgeCallSink.swift +49 -1
- package/ios/Dash0MobileBridgeDispatcher.swift +23 -1
- package/ios/OTelMobileCallSink.swift +205 -34
- package/ios/RCTDash0MobileModule.swift +10 -2
- package/ios/Tests/BoundedLiveSpanStoreTests.swift +143 -0
- package/ios/Tests/Dash0MobileBridgeDispatcherTests.swift +63 -0
- package/ios/Tests/OTelMobileCallSinkTests.swift +243 -0
- package/lib/bridge/types.d.ts +35 -0
- package/lib/bridge/types.d.ts.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +41 -1
- package/lib/instrumentation/errors.d.ts.map +1 -1
- package/lib/instrumentation/errors.js +13 -2
- package/lib/instrumentation/fetch.d.ts.map +1 -1
- package/lib/instrumentation/fetch.js +58 -22
- package/lib/instrumentation/navigation.d.ts.map +1 -1
- package/lib/instrumentation/navigation.js +32 -8
- package/lib/instrumentation/unhandledRejection.d.ts.map +1 -1
- package/lib/instrumentation/unhandledRejection.js +13 -3
- package/lib/instrumentation/xhr.d.ts.map +1 -1
- package/lib/instrumentation/xhr.js +63 -36
- package/lib/redact.d.ts +30 -0
- package/lib/redact.d.ts.map +1 -0
- package/lib/redact.js +67 -0
- package/package.json +1 -1
- package/src/bridge/types.ts +36 -0
- package/src/index.ts +43 -2
- package/src/instrumentation/errors.ts +12 -2
- package/src/instrumentation/fetch.ts +60 -27
- package/src/instrumentation/navigation.ts +40 -8
- package/src/instrumentation/unhandledRejection.ts +12 -3
- package/src/instrumentation/xhr.ts +65 -40
- 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
|
package/lib/bridge/types.d.ts
CHANGED
|
@@ -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';
|
package/lib/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -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
|
-
|
|
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;
|
|
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;
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
88
|
+
try {
|
|
89
|
+
handle.end();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
67
94
|
}
|
|
68
95
|
};
|
|
69
|
-
|
|
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 (
|
|
72
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"unhandledRejection.d.ts","sourceRoot":"","sources":["../../src/instrumentation/unhandledRejection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyBH,wBAAgB,wCAAwC,IAAI,MAAM,IAAI,CAoDrE"}
|