@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,17 @@
1
+ // Autolinking config for @dash0/mobile-react-native.
2
+ // Consumed by `react-native config` during build of host apps.
3
+
4
+ module.exports = {
5
+ dependency: {
6
+ platforms: {
7
+ ios: {
8
+ podspecPath: require('path').join(__dirname, 'Dash0Mobile.podspec'),
9
+ },
10
+ android: {
11
+ sourceDir: './android',
12
+ packageImportPath: 'import com.dash0.mobile.reactnative.Dash0MobilePackage;',
13
+ packageInstance: 'new Dash0MobilePackage()',
14
+ },
15
+ },
16
+ },
17
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * TurboModule codegen spec for @dash0/mobile-react-native.
3
+ *
4
+ * This file is intentionally NOT imported by `src/index.ts` — it exists
5
+ * solely to drive RN codegen (0.68+ / Fabric). The runtime code path goes
6
+ * through `require('react-native').NativeModules.Dash0Mobile` so we keep
7
+ * working on both old-arch and new-arch.
8
+ *
9
+ * Codegen constraints (from React Native docs):
10
+ * - methods must return `Promise<T>` or `void`
11
+ * - parameters must be primitives, `Object`, `Array`, or `{ readonly ... }`
12
+ * - no union types as parameters (codegen rejects)
13
+ *
14
+ * The shapes below are deliberately looser than `bridge/types.ts` (Object
15
+ * instead of discriminated unions) because codegen can't narrow on `kind`.
16
+ * The native side runtime-dispatches on the `kind` string.
17
+ */
18
+
19
+ import type { TurboModule } from 'react-native';
20
+ import { TurboModuleRegistry } from 'react-native';
21
+
22
+ export interface Spec extends TurboModule {
23
+ start(config: Object): Promise<void>;
24
+ emitBatch(payloads: Object[]): Promise<void>;
25
+ flushWindow(minutes: number): Promise<void>;
26
+ shutdown(): Promise<void>;
27
+ }
28
+
29
+ export default TurboModuleRegistry.getEnforcing<Spec>('Dash0Mobile');
@@ -0,0 +1,101 @@
1
+ import type { BridgePayload, NativeDash0MobileModule } from './types';
2
+
3
+ export const DEBOUNCE_MS = 50;
4
+ export const MAX_QUEUE = 10_000;
5
+ const RETRY_BASE_MS = 100;
6
+ const RETRY_MAX_ATTEMPTS = 5;
7
+
8
+ export class NativeBridge {
9
+ private readonly native: NativeDash0MobileModule;
10
+ private queue: BridgePayload[] = [];
11
+ private timer: ReturnType<typeof setTimeout> | null = null;
12
+ private inFlight: Promise<void> | null = null;
13
+
14
+ constructor(native: NativeDash0MobileModule) {
15
+ this.native = native;
16
+ }
17
+
18
+ emit(payload: BridgePayload): void {
19
+ this.queue.push(payload);
20
+ if (this.queue.length > MAX_QUEUE) {
21
+ this.queue.splice(0, this.queue.length - MAX_QUEUE);
22
+ }
23
+ if (this.timer === null) {
24
+ this.timer = setTimeout(() => {
25
+ this.timer = null;
26
+ void this.drain();
27
+ }, DEBOUNCE_MS);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Synchronous fast-path for FATAL-severity payloads. Cancels any pending
33
+ * debounce, drains the queue + the new payload in a single
34
+ * `native.emitBatch` call made SYNCHRONOUSLY on the current stack frame.
35
+ *
36
+ * Why not just `emit(); flush()`? Because `flush()` is `async`, and its
37
+ * `await this.drain()` places the call to `native.emitBatch` on the
38
+ * microtask queue. On a crash path (JS throw → ErrorUtils global handler
39
+ * → previous(error, isFatal) → RN fatal reporter → process exit) the
40
+ * handler's synchronous continuation wins the race against the
41
+ * microtask — the payload never crosses the bridge.
42
+ *
43
+ * This method is intentionally fire-and-forget: per the RN bridge
44
+ * contract, argument marshaling is synchronous, so the payload arrives
45
+ * on the native side by the time `native.emitBatch(...)` returns. The
46
+ * Promise resolution (native-side completion signal) is async and we
47
+ * don't await it — we only need the payload to cross the bridge before
48
+ * the process dies. `.catch(() => {})` prevents unhandledRejection
49
+ * warnings if the native side fails.
50
+ *
51
+ * See docs/superpowers/specs/2026-04-22-rn-fatal-bridge-bypass-design.md.
52
+ */
53
+ emitSync(payload: BridgePayload): void {
54
+ if (this.timer !== null) {
55
+ clearTimeout(this.timer);
56
+ this.timer = null;
57
+ }
58
+ const batch = this.queue.length > 0 ? [...this.queue, payload] : [payload];
59
+ this.queue = [];
60
+ this.native.emitBatch(batch).catch(() => {
61
+ // Swallow — on the crash path, the process is dying anyway and
62
+ // there's no caller alive to observe the error. On non-crash paths
63
+ // (if any caller uses emitSync for non-FATAL severity) the payload
64
+ // is lost on failure, which matches the implicit "best-effort"
65
+ // contract of the RN bridge under adverse conditions.
66
+ });
67
+ }
68
+
69
+ async flush(): Promise<void> {
70
+ if (this.timer !== null) {
71
+ clearTimeout(this.timer);
72
+ this.timer = null;
73
+ }
74
+ await this.drain();
75
+ }
76
+
77
+ private async drain(): Promise<void> {
78
+ if (this.queue.length === 0) return;
79
+ const batch = this.queue;
80
+ this.queue = [];
81
+ this.inFlight = this.sendWithRetry(batch);
82
+ try {
83
+ await this.inFlight;
84
+ } finally {
85
+ this.inFlight = null;
86
+ }
87
+ }
88
+
89
+ private async sendWithRetry(batch: BridgePayload[]): Promise<void> {
90
+ for (let attempt = 0; attempt < RETRY_MAX_ATTEMPTS; attempt++) {
91
+ try {
92
+ await this.native.emitBatch(batch);
93
+ return;
94
+ } catch {
95
+ if (attempt === RETRY_MAX_ATTEMPTS - 1) return;
96
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt);
97
+ await new Promise<void>(resolve => setTimeout(resolve, delay));
98
+ }
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,188 @@
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
+
11
+ export type SeverityNumber =
12
+ | 1 // TRACE
13
+ | 5 // DEBUG
14
+ | 9 // INFO
15
+ | 13 // WARN
16
+ | 17 // ERROR
17
+ | 21; // FATAL
18
+
19
+ export type SpanKind = 'INTERNAL' | 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER';
20
+
21
+ export type SpanStatus = 'UNSET' | 'OK' | 'ERROR';
22
+
23
+ export type AttrValue = string | number | boolean | null;
24
+ export type Attributes = Record<string, AttrValue>;
25
+
26
+ /**
27
+ * Per-trigger flags for the native screenshot module. Field names and
28
+ * defaults mirror Android's `ScreenshotConfig` and iOS's `ScreenshotConfig`
29
+ * one-to-one. All fields optional; absent fields fall through to native
30
+ * defaults.
31
+ */
32
+ export interface ScreenshotAutoCapture {
33
+ /** Master on/off. Required when passing an object form. Default `true` when present. */
34
+ enabled?: boolean;
35
+ /** Capture on every screen transition. Default `false` (high payload volume). */
36
+ captureOnScreenView?: boolean;
37
+ /** Capture when an uncaught exception occurs. Default `true`. */
38
+ captureOnError?: boolean;
39
+ /** Capture whenever a buffered-export policy fires (crash-recovery, ui-freeze, http-error). Default `true`. */
40
+ captureOnPolicyMatch?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Per-trigger flags for the native wireframe module. Field names and
45
+ * defaults mirror Android's `WireframeConfig` and iOS's `WireframeConfig`
46
+ * one-to-one. All fields optional; absent fields fall through to native
47
+ * defaults.
48
+ */
49
+ export interface WireframeAutoCapture {
50
+ /** Master on/off. Required when passing an object form. Default `true` when present. */
51
+ enabled?: boolean;
52
+ /** Capture on every screen transition. Default `true` (primary journey trigger). */
53
+ captureOnScreenView?: boolean;
54
+ /** Capture on every tap. Default `false` — taps rarely change the view hierarchy; dedup absorbs the case anyway. */
55
+ captureOnTap?: boolean;
56
+ /** Capture when an uncaught exception occurs. Default `true`. */
57
+ captureOnError?: boolean;
58
+ /** Capture whenever a buffered-export policy fires. Default `true`. */
59
+ captureOnPolicyMatch?: boolean;
60
+ /** Emit `ui.wireframe.ref` with `mobile.wireframe.id` instead of the full JSON when content matches the last capture. Default `true`. */
61
+ dedupeByContentHash?: boolean;
62
+ }
63
+
64
+ export interface StartConfig {
65
+ serviceName: string;
66
+ serviceVersion?: string;
67
+ endpoint: string;
68
+ authToken?: string;
69
+ dataset?: string;
70
+ bufferConfig?: {
71
+ ramEvents?: number;
72
+ diskBytes?: number;
73
+ };
74
+ enablePolicyPolling?: boolean;
75
+ /**
76
+ * Toggles for JS-side auto-instrumentation (fetch/XHR spans, JS error +
77
+ * unhandled rejection logs). Defaults to all-on. RN bridge uses the same
78
+ * flags to decide which native iOS/Android auto-capture suites to enable
79
+ * via `nativeAutoCapture`, so e.g. `autoCapture: { network: true }`
80
+ * enables the iOS `URLProtocol` swizzle in addition to the JS fetch/XHR
81
+ * wrappers. The default is OFF on the native side for `network`/`errors`
82
+ * — RN host apps typically don't want the native iOS URLProtocol swizzle
83
+ * or NSException handlers because they collide with RN's new-arch JS
84
+ * event loop (touch responder goes dormant).
85
+ *
86
+ * Lifecycle (`app.foreground` / `app.background` / `app.start`) is
87
+ * always-on via native instrumentation (Android ProcessLifecycleOwner,
88
+ * iOS NotificationCenter); there is no JS or per-flag knob for it.
89
+ */
90
+ autoCapture?: {
91
+ network?: boolean;
92
+ errors?: boolean;
93
+ /** Native iOS/Android-only capability — captures UI taps via swizzle/recognizer. Off by default on RN. */
94
+ tap?: boolean;
95
+ /** Native-only — scroll spans. Off by default on RN. */
96
+ scroll?: boolean;
97
+ /** Native-only — text input spans. Off by default on RN. */
98
+ textInput?: boolean;
99
+ /** Native-only — SwiftUI/Fragment screen tracking. Off by default on RN (use react-navigation helper instead). */
100
+ screen?: boolean;
101
+ /** Native-only — main-thread freeze detection. Off by default on RN. */
102
+ freeze?: boolean;
103
+ /** Native-only — app.start + jank + memory gauges. Off by default on RN. */
104
+ vitals?: boolean;
105
+ /** Native-only — periodic device health gauges (battery/thermal/network). Off by default on RN. */
106
+ deviceStats?: boolean;
107
+ /**
108
+ * Native-only — screenshot capture at journey boundaries + errors. Off
109
+ * by default on RN. Accepts either a bare boolean (`true` enables with
110
+ * native defaults) or an options object exposing the per-trigger flags
111
+ * mirrored from Android's `ScreenshotConfig` and iOS's `ScreenshotConfig`.
112
+ *
113
+ * NOTE: the per-trigger fields are forward-compatible. Today's bridge
114
+ * carries only the `enabled` bit through to native. Passing an object
115
+ * is equivalent to passing `true` until the bridge grows per-module
116
+ * options in a follow-up. Set the trigger flags natively (Android: in
117
+ * `MobileConfig.screenshotConfig`, iOS: in `MobileConfig`'s
118
+ * `screenshotConfig` param) until then.
119
+ */
120
+ screenshot?: boolean | ScreenshotAutoCapture;
121
+ /**
122
+ * Native-only — wireframe capture at screen transitions, optional taps,
123
+ * errors, and policy matches. Same shape rules as `screenshot` above.
124
+ */
125
+ wireframe?: boolean | WireframeAutoCapture;
126
+ };
127
+ /**
128
+ * Extra resource attributes merged into the native SDK's resource. Used by
129
+ * the RN bridge to inject `telemetry.distro.name` / `telemetry.distro.version`
130
+ * so Dash0 knows the telemetry came through the React Native distribution
131
+ * rather than direct Kotlin/Swift SDK usage. Caller-overridable for apps
132
+ * that want to add their own build/deployment tags.
133
+ */
134
+ extraResourceAttributes?: Record<string, string>;
135
+ }
136
+
137
+ export interface LogPayload {
138
+ kind: 'log';
139
+ name: string;
140
+ severity: SeverityNumber;
141
+ attributes: Attributes;
142
+ timeUnixNano: string; // string to preserve precision across bridge
143
+ }
144
+
145
+ export interface SpanStartPayload {
146
+ kind: 'spanStart';
147
+ spanId: string; // JS-generated; native echoes back to correlate
148
+ parentSpanId?: string;
149
+ name: string;
150
+ spanKind: SpanKind;
151
+ attributes: Attributes;
152
+ startTimeUnixNano: string;
153
+ }
154
+
155
+ export interface SpanEndPayload {
156
+ kind: 'spanEnd';
157
+ spanId: string;
158
+ status: SpanStatus;
159
+ statusMessage?: string;
160
+ attributes: Attributes;
161
+ endTimeUnixNano: string;
162
+ }
163
+
164
+ export interface MetricPayload {
165
+ kind: 'metric';
166
+ name: string;
167
+ instrumentType: 'counter' | 'histogram' | 'gauge';
168
+ value: number;
169
+ attributes: Attributes;
170
+ timeUnixNano: string;
171
+ }
172
+
173
+ export type BridgePayload =
174
+ | LogPayload
175
+ | SpanStartPayload
176
+ | SpanEndPayload
177
+ | MetricPayload;
178
+
179
+ export interface NativeDash0MobileModule {
180
+ start(config: StartConfig): Promise<void>;
181
+ emitBatch(payloads: BridgePayload[]): Promise<void>;
182
+ flushWindow(minutes: number): Promise<void>;
183
+ shutdown(): Promise<void>;
184
+ startJourney(name: string): Promise<string>;
185
+ endJourney(journeyId: string): Promise<void>;
186
+ captureScreenshot(trigger: string): Promise<void>;
187
+ captureWireframe(trigger: string): Promise<void>;
188
+ }