@barrysolomon/mobile-react-native 0.1.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.
Files changed (61) hide show
  1. package/Dash0Mobile.podspec +29 -0
  2. package/README.md +117 -0
  3. package/android/build.gradle +68 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/dash0/mobile/reactnative/BridgeCallSink.kt +67 -0
  6. package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobileModule.kt +198 -0
  7. package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobilePackage.kt +14 -0
  8. package/android/src/main/java/com/dash0/mobile/reactnative/OTelMobileCallSink.kt +208 -0
  9. package/android/src/test/java/com/dash0/mobile/reactnative/Dash0MobileModuleTest.kt +413 -0
  10. package/ios/BridgeCallSink.swift +83 -0
  11. package/ios/Dash0MobileBridgeDispatcher.swift +142 -0
  12. package/ios/OTelMobileCallSink.swift +262 -0
  13. package/ios/RCTDash0MobileModule.m +28 -0
  14. package/ios/RCTDash0MobileModule.swift +104 -0
  15. package/ios/Tests/Dash0MobileBridgeDispatcherTests.swift +341 -0
  16. package/lib/src/NativeDash0Mobile.d.ts +27 -0
  17. package/lib/src/NativeDash0Mobile.d.ts.map +1 -0
  18. package/lib/src/NativeDash0Mobile.js +19 -0
  19. package/lib/src/bridge/NativeBridge.d.ts +38 -0
  20. package/lib/src/bridge/NativeBridge.d.ts.map +1 -0
  21. package/lib/src/bridge/NativeBridge.js +95 -0
  22. package/lib/src/bridge/types.d.ts +166 -0
  23. package/lib/src/bridge/types.d.ts.map +1 -0
  24. package/lib/src/bridge/types.js +10 -0
  25. package/lib/src/index.d.ts +35 -0
  26. package/lib/src/index.d.ts.map +1 -0
  27. package/lib/src/index.js +408 -0
  28. package/lib/src/instrumentation/errors.d.ts +14 -0
  29. package/lib/src/instrumentation/errors.d.ts.map +1 -0
  30. package/lib/src/instrumentation/errors.js +65 -0
  31. package/lib/src/instrumentation/fetch.d.ts +16 -0
  32. package/lib/src/instrumentation/fetch.d.ts.map +1 -0
  33. package/lib/src/instrumentation/fetch.js +75 -0
  34. package/lib/src/instrumentation/navigation.d.ts +19 -0
  35. package/lib/src/instrumentation/navigation.d.ts.map +1 -0
  36. package/lib/src/instrumentation/navigation.js +39 -0
  37. package/lib/src/instrumentation/touch.d.ts +12 -0
  38. package/lib/src/instrumentation/touch.d.ts.map +1 -0
  39. package/lib/src/instrumentation/touch.js +18 -0
  40. package/lib/src/instrumentation/unhandledRejection.d.ts +9 -0
  41. package/lib/src/instrumentation/unhandledRejection.d.ts.map +1 -0
  42. package/lib/src/instrumentation/unhandledRejection.js +52 -0
  43. package/lib/src/instrumentation/xhr.d.ts +14 -0
  44. package/lib/src/instrumentation/xhr.d.ts.map +1 -0
  45. package/lib/src/instrumentation/xhr.js +88 -0
  46. package/lib/src/otel-compat.d.ts +67 -0
  47. package/lib/src/otel-compat.d.ts.map +1 -0
  48. package/lib/src/otel-compat.js +84 -0
  49. package/package.json +72 -0
  50. package/react-native.config.js +17 -0
  51. package/src/NativeDash0Mobile.ts +29 -0
  52. package/src/bridge/NativeBridge.ts +101 -0
  53. package/src/bridge/types.ts +188 -0
  54. package/src/index.ts +456 -0
  55. package/src/instrumentation/errors.ts +84 -0
  56. package/src/instrumentation/fetch.ts +93 -0
  57. package/src/instrumentation/navigation.ts +52 -0
  58. package/src/instrumentation/touch.ts +32 -0
  59. package/src/instrumentation/unhandledRejection.ts +75 -0
  60. package/src/instrumentation/xhr.ts +125 -0
  61. package/src/otel-compat.ts +159 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Bridge payload contract between the JS layer and the native modules.
3
+ *
4
+ * This is the stable seam. Any change here is a cross-repo breaking change
5
+ * (JS package + Android module + iOS module must all move together).
6
+ *
7
+ * Values crossing the RN bridge must be JSON-serializable primitives.
8
+ * No Date, no Map, no Symbol, no functions.
9
+ */
10
+ export type SeverityNumber = 1 | 5 | 9 | 13 | 17 | 21;
11
+ export type SpanKind = 'INTERNAL' | 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER';
12
+ export type SpanStatus = 'UNSET' | 'OK' | 'ERROR';
13
+ export type AttrValue = string | number | boolean | null;
14
+ export type Attributes = Record<string, AttrValue>;
15
+ /**
16
+ * Per-trigger flags for the native screenshot module. Field names and
17
+ * defaults mirror Android's `ScreenshotConfig` and iOS's `ScreenshotConfig`
18
+ * one-to-one. All fields optional; absent fields fall through to native
19
+ * defaults.
20
+ */
21
+ export interface ScreenshotAutoCapture {
22
+ /** Master on/off. Required when passing an object form. Default `true` when present. */
23
+ enabled?: boolean;
24
+ /** Capture on every screen transition. Default `false` (high payload volume). */
25
+ captureOnScreenView?: boolean;
26
+ /** Capture when an uncaught exception occurs. Default `true`. */
27
+ captureOnError?: boolean;
28
+ /** Capture whenever a buffered-export policy fires (crash-recovery, ui-freeze, http-error). Default `true`. */
29
+ captureOnPolicyMatch?: boolean;
30
+ }
31
+ /**
32
+ * Per-trigger flags for the native wireframe module. Field names and
33
+ * defaults mirror Android's `WireframeConfig` and iOS's `WireframeConfig`
34
+ * one-to-one. All fields optional; absent fields fall through to native
35
+ * defaults.
36
+ */
37
+ export interface WireframeAutoCapture {
38
+ /** Master on/off. Required when passing an object form. Default `true` when present. */
39
+ enabled?: boolean;
40
+ /** Capture on every screen transition. Default `true` (primary journey trigger). */
41
+ captureOnScreenView?: boolean;
42
+ /** Capture on every tap. Default `false` — taps rarely change the view hierarchy; dedup absorbs the case anyway. */
43
+ captureOnTap?: boolean;
44
+ /** Capture when an uncaught exception occurs. Default `true`. */
45
+ captureOnError?: boolean;
46
+ /** Capture whenever a buffered-export policy fires. Default `true`. */
47
+ captureOnPolicyMatch?: boolean;
48
+ /** Emit `ui.wireframe.ref` with `mobile.wireframe.id` instead of the full JSON when content matches the last capture. Default `true`. */
49
+ dedupeByContentHash?: boolean;
50
+ }
51
+ export interface StartConfig {
52
+ serviceName: string;
53
+ serviceVersion?: string;
54
+ endpoint: string;
55
+ authToken?: string;
56
+ dataset?: string;
57
+ bufferConfig?: {
58
+ ramEvents?: number;
59
+ diskBytes?: number;
60
+ };
61
+ enablePolicyPolling?: boolean;
62
+ /**
63
+ * Toggles for JS-side auto-instrumentation (fetch/XHR spans, JS error +
64
+ * unhandled rejection logs). Defaults to all-on. RN bridge uses the same
65
+ * flags to decide which native iOS/Android auto-capture suites to enable
66
+ * via `nativeAutoCapture`, so e.g. `autoCapture: { network: true }`
67
+ * enables the iOS `URLProtocol` swizzle in addition to the JS fetch/XHR
68
+ * wrappers. The default is OFF on the native side for `network`/`errors`
69
+ * — RN host apps typically don't want the native iOS URLProtocol swizzle
70
+ * or NSException handlers because they collide with RN's new-arch JS
71
+ * event loop (touch responder goes dormant).
72
+ *
73
+ * Lifecycle (`app.foreground` / `app.background` / `app.start`) is
74
+ * always-on via native instrumentation (Android ProcessLifecycleOwner,
75
+ * iOS NotificationCenter); there is no JS or per-flag knob for it.
76
+ */
77
+ autoCapture?: {
78
+ network?: boolean;
79
+ errors?: boolean;
80
+ /** Native iOS/Android-only capability — captures UI taps via swizzle/recognizer. Off by default on RN. */
81
+ tap?: boolean;
82
+ /** Native-only — scroll spans. Off by default on RN. */
83
+ scroll?: boolean;
84
+ /** Native-only — text input spans. Off by default on RN. */
85
+ textInput?: boolean;
86
+ /** Native-only — SwiftUI/Fragment screen tracking. Off by default on RN (use react-navigation helper instead). */
87
+ screen?: boolean;
88
+ /** Native-only — main-thread freeze detection. Off by default on RN. */
89
+ freeze?: boolean;
90
+ /** Native-only — app.start + jank + memory gauges. Off by default on RN. */
91
+ vitals?: boolean;
92
+ /** Native-only — periodic device health gauges (battery/thermal/network). Off by default on RN. */
93
+ deviceStats?: boolean;
94
+ /**
95
+ * Native-only — screenshot capture at journey boundaries + errors. Off
96
+ * by default on RN. Accepts either a bare boolean (`true` enables with
97
+ * native defaults) or an options object exposing the per-trigger flags
98
+ * mirrored from Android's `ScreenshotConfig` and iOS's `ScreenshotConfig`.
99
+ *
100
+ * NOTE: the per-trigger fields are forward-compatible. Today's bridge
101
+ * carries only the `enabled` bit through to native. Passing an object
102
+ * is equivalent to passing `true` until the bridge grows per-module
103
+ * options in a follow-up. Set the trigger flags natively (Android: in
104
+ * `MobileConfig.screenshotConfig`, iOS: in `MobileConfig`'s
105
+ * `screenshotConfig` param) until then.
106
+ */
107
+ screenshot?: boolean | ScreenshotAutoCapture;
108
+ /**
109
+ * Native-only — wireframe capture at screen transitions, optional taps,
110
+ * errors, and policy matches. Same shape rules as `screenshot` above.
111
+ */
112
+ wireframe?: boolean | WireframeAutoCapture;
113
+ };
114
+ /**
115
+ * Extra resource attributes merged into the native SDK's resource. Used by
116
+ * the RN bridge to inject `telemetry.distro.name` / `telemetry.distro.version`
117
+ * so Dash0 knows the telemetry came through the React Native distribution
118
+ * rather than direct Kotlin/Swift SDK usage. Caller-overridable for apps
119
+ * that want to add their own build/deployment tags.
120
+ */
121
+ extraResourceAttributes?: Record<string, string>;
122
+ }
123
+ export interface LogPayload {
124
+ kind: 'log';
125
+ name: string;
126
+ severity: SeverityNumber;
127
+ attributes: Attributes;
128
+ timeUnixNano: string;
129
+ }
130
+ export interface SpanStartPayload {
131
+ kind: 'spanStart';
132
+ spanId: string;
133
+ parentSpanId?: string;
134
+ name: string;
135
+ spanKind: SpanKind;
136
+ attributes: Attributes;
137
+ startTimeUnixNano: string;
138
+ }
139
+ export interface SpanEndPayload {
140
+ kind: 'spanEnd';
141
+ spanId: string;
142
+ status: SpanStatus;
143
+ statusMessage?: string;
144
+ attributes: Attributes;
145
+ endTimeUnixNano: string;
146
+ }
147
+ export interface MetricPayload {
148
+ kind: 'metric';
149
+ name: string;
150
+ instrumentType: 'counter' | 'histogram' | 'gauge';
151
+ value: number;
152
+ attributes: Attributes;
153
+ timeUnixNano: string;
154
+ }
155
+ export type BridgePayload = LogPayload | SpanStartPayload | SpanEndPayload | MetricPayload;
156
+ export interface NativeDash0MobileModule {
157
+ start(config: StartConfig): Promise<void>;
158
+ emitBatch(payloads: BridgePayload[]): Promise<void>;
159
+ flushWindow(minutes: number): Promise<void>;
160
+ shutdown(): Promise<void>;
161
+ startJourney(name: string): Promise<string>;
162
+ endJourney(journeyId: string): Promise<void>;
163
+ captureScreenshot(trigger: string): Promise<void>;
164
+ captureWireframe(trigger: string): Promise<void>;
165
+ }
166
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/bridge/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Bridge payload contract between the JS layer and the native modules.
3
+ *
4
+ * This is the stable seam. Any change here is a cross-repo breaking change
5
+ * (JS package + Android module + iOS module must all move together).
6
+ *
7
+ * Values crossing the RN bridge must be JSON-serializable primitives.
8
+ * No Date, no Map, no Symbol, no functions.
9
+ */
10
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @dash0/mobile-react-native — public API
3
+ *
4
+ * Thin JS facade. All buffering, policy, OTLP export, crash recovery happen
5
+ * in the native modules. This file only marshals calls onto a NativeBridge.
6
+ *
7
+ * See docs/epics/REACT_NATIVE_EPIC.md.
8
+ */
9
+ import type { Attributes, NativeDash0MobileModule, SeverityNumber, SpanKind, StartConfig } from './bridge/types';
10
+ export type { Attributes, SeverityNumber, StartConfig } from './bridge/types';
11
+ export { installReactNavigationInstrumentation } from './instrumentation/navigation';
12
+ export { withTapTelemetry } from './instrumentation/touch';
13
+ export { otel } from './otel-compat';
14
+ export interface SpanHandle {
15
+ setAttribute(key: string, value: string | number | boolean): void;
16
+ setStatus(status: 'OK' | 'ERROR', message?: string): void;
17
+ end(): void;
18
+ }
19
+ export declare function __resetTimestampAnchorForTests__(): void;
20
+ export declare const Dash0Mobile: {
21
+ start(config: StartConfig): Promise<void>;
22
+ log(name: string, attributes?: Attributes, severity?: SeverityNumber): void;
23
+ startSpan(name: string, attributes?: Attributes, spanKind?: SpanKind): SpanHandle;
24
+ span<T>(name: string, fn: (handle: SpanHandle) => Promise<T> | T, attributes?: Attributes): Promise<T>;
25
+ recordMetric(name: string, value: number, instrumentType?: "counter" | "histogram" | "gauge", attributes?: Attributes): void;
26
+ flushWindow(minutes: number): Promise<void>;
27
+ startJourney(name: string): Promise<string | null>;
28
+ endJourney(journeyId: string): Promise<void>;
29
+ captureScreenshot(trigger?: string): Promise<void>;
30
+ captureWireframe(trigger?: string): Promise<void>;
31
+ shutdown(): Promise<void>;
32
+ };
33
+ export declare function __setNativeForTesting(native: NativeDash0MobileModule | null): void;
34
+ export declare function __resetForTesting(): void;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,408 @@
1
+ /**
2
+ * @dash0/mobile-react-native — public API
3
+ *
4
+ * Thin JS facade. All buffering, policy, OTLP export, crash recovery happen
5
+ * in the native modules. This file only marshals calls onto a NativeBridge.
6
+ *
7
+ * See docs/epics/REACT_NATIVE_EPIC.md.
8
+ */
9
+ import { NativeBridge } from './bridge/NativeBridge';
10
+ import { installFetchInstrumentation } from './instrumentation/fetch';
11
+ import { installXhrInstrumentation } from './instrumentation/xhr';
12
+ import { installErrorInstrumentation } from './instrumentation/errors';
13
+ import { installUnhandledRejectionInstrumentation } from './instrumentation/unhandledRejection';
14
+ export { installReactNavigationInstrumentation } from './instrumentation/navigation';
15
+ export { withTapTelemetry } from './instrumentation/touch';
16
+ export { otel } from './otel-compat';
17
+ let injectedNative = null;
18
+ let bridge = null;
19
+ let started = false;
20
+ let autoInstrUninstallers = [];
21
+ // Ambient parent-span stack. Each `startSpan` pushes the new spanId; each
22
+ // `.end()` pops. Children emitted while an outer span is live pick up the
23
+ // top of the stack as their `parentSpanId`, so Dash0 renders proper
24
+ // waterfalls without callers threading parent handles manually.
25
+ //
26
+ // Limitation: a single global stack is only correct for strictly-nested
27
+ // (LIFO) span work — sufficient for `Dash0Mobile.span('x', async () => { ... })`
28
+ // and sequential `startSpan/.end()` pairs. Concurrent async span trees would
29
+ // need AsyncLocalStorage / Hermes async-hooks; RN doesn't expose those
30
+ // cleanly, so we accept the limitation and document it on the public API.
31
+ const spanStack = [];
32
+ /// Anchor pair used by `nowUnixNano` to combine an absolute-epoch
33
+ /// base (from `Date.now()`) with a sub-ms monotonic delta (from
34
+ /// `performance.now()`). Initialized lazily on the first timestamp
35
+ /// request so we don't pay the cost for consumers who never emit.
36
+ ///
37
+ /// Why the anchor pattern: `performance.now()` gives sub-ms precision
38
+ /// but returns time-since-an-arbitrary-origin, not wall-clock epoch.
39
+ /// `Date.now()` gives wall-clock but only ms resolution. Capturing
40
+ /// both once, then deriving each timestamp as `epochAnchor + perfDelta`
41
+ /// yields sub-ms wall-clock nanoseconds without per-call drift.
42
+ let _epochAnchorMs = null;
43
+ let _perfAnchorMs = null;
44
+ function perfNow() {
45
+ // RN 0.85 new-arch ships `global.performance.now` via JSI. Older
46
+ // runtimes (Hermes < 0.12, some test harnesses) may not. Fall back
47
+ // gracefully — callers still get correct ms-resolution nanoseconds
48
+ // in that case, just not the sub-ms fidelity.
49
+ const g = globalThis;
50
+ if (typeof g.performance?.now === 'function') {
51
+ return g.performance.now();
52
+ }
53
+ return Date.now();
54
+ }
55
+ function nowUnixNano() {
56
+ // Initialize anchor lazily. The offset between the two reads is at
57
+ // most one JS tick — negligible for observability purposes.
58
+ if (_epochAnchorMs === null || _perfAnchorMs === null) {
59
+ _epochAnchorMs = Date.now();
60
+ _perfAnchorMs = perfNow();
61
+ }
62
+ const elapsedMs = perfNow() - _perfAnchorMs;
63
+ // Sub-ms fraction; convert to nanoseconds as an integer.
64
+ const extraNanos = Math.floor(elapsedMs * 1000000);
65
+ // BigInt is required for correctness. Unix nanoseconds in 2026
66
+ // are ~1.77e18, well past JS `Number.MAX_SAFE_INTEGER` (2^53 ≈
67
+ // 9.01e15). The previous `Date.now() * 1_000_000` implementation
68
+ // lost precision on every call (rounding to ~128-nanosecond bands),
69
+ // which collapsed nested child spans' start+end timestamps to
70
+ // identical values and surfaced as `duration=0ms` in Dash0.
71
+ const totalNanos = BigInt(_epochAnchorMs) * 1000000n + BigInt(extraNanos);
72
+ return totalNanos.toString();
73
+ }
74
+ /// Test-only: reset the anchors so each test case starts fresh.
75
+ /// Not exported from the public entry point — accessed in Jest via
76
+ /// the internal require path.
77
+ export function __resetTimestampAnchorForTests__() {
78
+ _epochAnchorMs = null;
79
+ _perfAnchorMs = null;
80
+ }
81
+ function randomSpanId() {
82
+ let id = '';
83
+ for (let i = 0; i < 16; i++) {
84
+ id += Math.floor(Math.random() * 16).toString(16);
85
+ }
86
+ return id;
87
+ }
88
+ function resolveNative() {
89
+ if (injectedNative)
90
+ return injectedNative;
91
+ // Production path: lazily require react-native to avoid pulling it into
92
+ // tests that stub at the unit level. If RN isn't present we return null
93
+ // and pre-start calls become no-ops.
94
+ try {
95
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
96
+ const rn = require('react-native');
97
+ return rn?.NativeModules?.Dash0Mobile ?? null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ // Version of this RN bridge package. Sent as `telemetry.distro.version` so
104
+ // Dash0 can distinguish RN-originated telemetry from direct native SDK
105
+ // callers and correlate issues to a specific bridge release. Keep this in
106
+ // sync with package.json on each release.
107
+ const DISTRO_NAME = 'dash0-react-native';
108
+ const DISTRO_VERSION = '0.1.0-alpha';
109
+ function resolveReactNativeVersion() {
110
+ try {
111
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
112
+ const rn = require('react-native/package.json');
113
+ return typeof rn?.version === 'string' ? rn.version : undefined;
114
+ }
115
+ catch {
116
+ return undefined;
117
+ }
118
+ }
119
+ export const Dash0Mobile = {
120
+ async start(config) {
121
+ const native = resolveNative();
122
+ if (!native) {
123
+ // No native module wired. Stay quiet — caller is likely in a non-RN
124
+ // environment (Jest, SSR) and shouldn't crash.
125
+ started = false;
126
+ return;
127
+ }
128
+ bridge = new NativeBridge(native);
129
+ // Inject RN distribution attributes as OTel-spec resource attributes.
130
+ // Caller-provided extras win (lets apps override for testing/tagging).
131
+ const rnVersion = resolveReactNativeVersion();
132
+ // Translate the `autoCapture` flags into a native-capability token list the
133
+ // bridge (iOS/Android) can use to build `AutoCaptureOptions`. Native
134
+ // default is OFF for everything — callers must explicitly set a flag to
135
+ // `true` to enable the native suite on that capability. This avoids the
136
+ // iOS URLProtocol / NSException / signal-handler swizzles by default,
137
+ // which collide with RN's new-arch JS event loop.
138
+ const nativeAutoCapture = buildNativeAutoCaptureTokens(config.autoCapture);
139
+ const mergedConfig = {
140
+ ...config,
141
+ extraResourceAttributes: {
142
+ 'telemetry.distro.name': DISTRO_NAME,
143
+ 'telemetry.distro.version': DISTRO_VERSION,
144
+ ...(rnVersion ? { 'app.framework': 'react-native', 'app.framework.version': rnVersion } : {}),
145
+ ...(config.extraResourceAttributes ?? {}),
146
+ },
147
+ nativeAutoCapture,
148
+ };
149
+ await native.start(mergedConfig);
150
+ started = true;
151
+ const auto = config.autoCapture ?? {};
152
+ if (auto.network !== false) {
153
+ const collectorHost = hostFromEndpoint(config.endpoint);
154
+ const ignoredHosts = collectorHost ? [collectorHost] : [];
155
+ // On React Native, `fetch` is implemented on top of XHR, so every
156
+ // fetch() call also fires the XHR instrumentation — installing both
157
+ // produces two spans per request. XHR is the authoritative layer
158
+ // because it also captures direct XHR callers (axios, Apollo HTTP
159
+ // link, legacy code) that don't go through fetch. In non-RN JS
160
+ // environments (future web/SSR use) fetch is native and independent
161
+ // of XHR, so both shims are needed.
162
+ if (!isReactNative()) {
163
+ autoInstrUninstallers.push(installFetchInstrumentation({ ignoredHosts }));
164
+ }
165
+ autoInstrUninstallers.push(installXhrInstrumentation({ ignoredHosts }));
166
+ }
167
+ if (auto.errors !== false) {
168
+ autoInstrUninstallers.push(installErrorInstrumentation());
169
+ autoInstrUninstallers.push(installUnhandledRejectionInstrumentation());
170
+ }
171
+ // Lifecycle (`app.foreground` / `app.background` / `app.start`) is now
172
+ // emitted by native instrumentation on both platforms — Android via
173
+ // ProcessLifecycleOwner, iOS via NotificationCenter with applicationState
174
+ // late-init synthesis. The previous JS-side AppState shim was deleted
175
+ // because RN 0.85 new-arch made it unreliable (TurboModule init race).
176
+ },
177
+ log(name, attributes = {}, severity = 9) {
178
+ if (!started || !bridge)
179
+ return;
180
+ const payload = {
181
+ kind: 'log',
182
+ name,
183
+ severity,
184
+ attributes,
185
+ timeUnixNano: nowUnixNano(),
186
+ };
187
+ // FATAL-severity logs bypass the 50ms debounce via `emitSync`, which
188
+ // calls `native.emitBatch` without any `await`. The payload crosses
189
+ // the RN bridge in the current stack frame — critical on crash paths
190
+ // because any microtask boundary (including the `await` inside
191
+ // `flush()`) loses the race against the handler's continuation into
192
+ // RN's fatal reporter. Once the payload is on the native side, the
193
+ // iOS SDK's willTerminate auto-forceFlush (commit 1a69c7e) persists
194
+ // it to disk and attempts OTLP export before process exit.
195
+ if (severity >= 21) {
196
+ bridge.emitSync(payload);
197
+ }
198
+ else {
199
+ bridge.emit(payload);
200
+ }
201
+ },
202
+ startSpan(name, attributes = {}, spanKind = 'INTERNAL') {
203
+ const spanId = randomSpanId();
204
+ const parentSpanId = spanStack.length > 0 ? spanStack[spanStack.length - 1] : undefined;
205
+ const startPayload = {
206
+ kind: 'spanStart',
207
+ spanId,
208
+ parentSpanId,
209
+ name,
210
+ spanKind,
211
+ attributes: { ...attributes },
212
+ startTimeUnixNano: nowUnixNano(),
213
+ };
214
+ const active = {
215
+ payload: startPayload,
216
+ attrs: {},
217
+ status: 'UNSET',
218
+ ended: false,
219
+ };
220
+ spanStack.push(spanId);
221
+ if (started && bridge) {
222
+ bridge.emit(startPayload);
223
+ }
224
+ return {
225
+ setAttribute(key, value) {
226
+ active.attrs[key] = value;
227
+ },
228
+ setStatus(status, message) {
229
+ active.status = status;
230
+ active.statusMessage = message;
231
+ },
232
+ end() {
233
+ if (active.ended)
234
+ return;
235
+ active.ended = true;
236
+ // Pop this spanId off the stack. Tolerate out-of-order ends by
237
+ // scanning from the top — the common case is LIFO, but mis-ordered
238
+ // ends (e.g. a long-lived outer span that ends after an inner
239
+ // one that was itself popped correctly) shouldn't corrupt the stack.
240
+ const idx = spanStack.lastIndexOf(spanId);
241
+ if (idx >= 0)
242
+ spanStack.splice(idx, 1);
243
+ if (!started || !bridge)
244
+ return;
245
+ bridge.emit({
246
+ kind: 'spanEnd',
247
+ spanId,
248
+ status: active.status === 'UNSET' ? 'OK' : active.status,
249
+ statusMessage: active.statusMessage,
250
+ attributes: active.attrs,
251
+ endTimeUnixNano: nowUnixNano(),
252
+ });
253
+ },
254
+ };
255
+ },
256
+ async span(name, fn, attributes) {
257
+ const handle = this.startSpan(name, attributes);
258
+ try {
259
+ const result = await fn(handle);
260
+ handle.setStatus('OK');
261
+ return result;
262
+ }
263
+ catch (err) {
264
+ handle.setStatus('ERROR', err instanceof Error ? err.message : String(err));
265
+ throw err;
266
+ }
267
+ finally {
268
+ handle.end();
269
+ }
270
+ },
271
+ recordMetric(name, value, instrumentType = 'counter', attributes = {}) {
272
+ if (!started || !bridge)
273
+ return;
274
+ bridge.emit({
275
+ kind: 'metric',
276
+ name,
277
+ instrumentType,
278
+ value,
279
+ attributes,
280
+ timeUnixNano: nowUnixNano(),
281
+ });
282
+ },
283
+ async flushWindow(minutes) {
284
+ if (!started || !bridge)
285
+ return;
286
+ await bridge.flush();
287
+ const native = resolveNative();
288
+ if (native)
289
+ await native.flushWindow(minutes);
290
+ },
291
+ // ─── User Journey API (UJ-020) ──────────────────────────────────────────
292
+ // Thin passthrough to native `OTelMobile.startJourney/endJourney`. The
293
+ // native side creates the journey span and triggers screenshot + wireframe
294
+ // captures at start/end boundaries.
295
+ async startJourney(name) {
296
+ if (!started)
297
+ return null;
298
+ const native = resolveNative();
299
+ if (!native)
300
+ return null;
301
+ return native.startJourney(name);
302
+ },
303
+ async endJourney(journeyId) {
304
+ if (!started)
305
+ return;
306
+ const native = resolveNative();
307
+ if (!native)
308
+ return;
309
+ await native.endJourney(journeyId);
310
+ },
311
+ async captureScreenshot(trigger = 'manual') {
312
+ if (!started)
313
+ return;
314
+ const native = resolveNative();
315
+ if (!native)
316
+ return;
317
+ await native.captureScreenshot(trigger);
318
+ },
319
+ async captureWireframe(trigger = 'manual') {
320
+ if (!started)
321
+ return;
322
+ const native = resolveNative();
323
+ if (!native)
324
+ return;
325
+ await native.captureWireframe(trigger);
326
+ },
327
+ async shutdown() {
328
+ for (const uninstall of autoInstrUninstallers) {
329
+ try {
330
+ uninstall();
331
+ }
332
+ catch {
333
+ // Swallow — shutdown should never throw.
334
+ }
335
+ }
336
+ autoInstrUninstallers = [];
337
+ if (!bridge)
338
+ return;
339
+ await bridge.flush();
340
+ const native = resolveNative();
341
+ if (native)
342
+ await native.shutdown();
343
+ started = false;
344
+ bridge = null;
345
+ },
346
+ };
347
+ const ENDPOINT_HOST_RE = /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i;
348
+ function hostFromEndpoint(endpoint) {
349
+ const match = ENDPOINT_HOST_RE.exec(endpoint);
350
+ return match ? match[1].toLowerCase() : null;
351
+ }
352
+ // React Native sets `navigator.product === 'ReactNative'` — the standard
353
+ // public signal for code that wants to differ by JS environment. This is
354
+ // the same check used by Sentry, Datadog, and RN itself internally. Jest
355
+ // / Node / SSR won't match, so test and non-RN environments still
356
+ // exercise both network shims.
357
+ function isReactNative() {
358
+ const nav = globalThis.navigator;
359
+ return nav?.product === 'ReactNative';
360
+ }
361
+ const NATIVE_AUTO_CAPTURE_FLAGS = [
362
+ ['network', 'network'],
363
+ ['errors', 'errors'],
364
+ ['tap', 'tap'],
365
+ ['scroll', 'scroll'],
366
+ ['textInput', 'textInput'],
367
+ ['screen', 'screen'],
368
+ ['freeze', 'freeze'],
369
+ ['vitals', 'vitals'],
370
+ ['deviceStats', 'deviceStats'],
371
+ ['screenshot', 'screenshot'],
372
+ ['wireframe', 'wireframe'],
373
+ ];
374
+ function buildNativeAutoCaptureTokens(ac) {
375
+ if (!ac)
376
+ return [];
377
+ const out = [];
378
+ for (const [flag, token] of NATIVE_AUTO_CAPTURE_FLAGS) {
379
+ const value = ac[flag];
380
+ // Object form (screenshot / wireframe per-trigger): treat as enabled
381
+ // unless `enabled` is explicitly `false`. The bridge protocol carries
382
+ // only this on/off bit today — per-trigger fields are forward-compatible
383
+ // and currently must be configured natively. See ScreenshotAutoCapture /
384
+ // WireframeAutoCapture doc comments in bridge/types.ts.
385
+ if (value === true) {
386
+ out.push(token);
387
+ }
388
+ else if (typeof value === 'object' && value !== null) {
389
+ const enabled = value.enabled;
390
+ if (enabled !== false)
391
+ out.push(token);
392
+ }
393
+ }
394
+ return out;
395
+ }
396
+ // ─── test hooks ──────────────────────────────────────────────────────────────
397
+ // These are intentionally exported but prefixed __ to signal "not for app use."
398
+ // Native-module tests inject a mock here; a CI lint rule should ban callers
399
+ // outside __tests__/ from importing them.
400
+ export function __setNativeForTesting(native) {
401
+ injectedNative = native;
402
+ }
403
+ export function __resetForTesting() {
404
+ injectedNative = null;
405
+ bridge = null;
406
+ started = false;
407
+ spanStack.length = 0;
408
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Error auto-instrumentation for React Native.
3
+ *
4
+ * Chains our handler into RN's `ErrorUtils.setGlobalHandler`, emits an
5
+ * `app.error` log with OTel `exception.*` semconv attributes, then delegates
6
+ * to the previous handler so Sentry/Bugsnag/redbox keep working.
7
+ *
8
+ * Deduplication: identical (type + message) errors inside a 5-minute window
9
+ * collapse to a single log, matching the Android ErrorInstrumentation
10
+ * policy (loops that throw on every frame shouldn't DoS the collector).
11
+ */
12
+ export declare const DEDUPE_WINDOW_MS: number;
13
+ export declare function installErrorInstrumentation(): () => void;
14
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +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"}