@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.
- package/Dash0Mobile.podspec +29 -0
- package/README.md +117 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/BridgeCallSink.kt +67 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobileModule.kt +198 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobilePackage.kt +14 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/OTelMobileCallSink.kt +208 -0
- package/android/src/test/java/com/dash0/mobile/reactnative/Dash0MobileModuleTest.kt +413 -0
- package/ios/BridgeCallSink.swift +83 -0
- package/ios/Dash0MobileBridgeDispatcher.swift +142 -0
- package/ios/OTelMobileCallSink.swift +262 -0
- package/ios/RCTDash0MobileModule.m +28 -0
- package/ios/RCTDash0MobileModule.swift +104 -0
- package/ios/Tests/Dash0MobileBridgeDispatcherTests.swift +341 -0
- package/lib/src/NativeDash0Mobile.d.ts +27 -0
- package/lib/src/NativeDash0Mobile.d.ts.map +1 -0
- package/lib/src/NativeDash0Mobile.js +19 -0
- package/lib/src/bridge/NativeBridge.d.ts +38 -0
- package/lib/src/bridge/NativeBridge.d.ts.map +1 -0
- package/lib/src/bridge/NativeBridge.js +95 -0
- package/lib/src/bridge/types.d.ts +166 -0
- package/lib/src/bridge/types.d.ts.map +1 -0
- package/lib/src/bridge/types.js +10 -0
- package/lib/src/index.d.ts +35 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +408 -0
- package/lib/src/instrumentation/errors.d.ts +14 -0
- package/lib/src/instrumentation/errors.d.ts.map +1 -0
- package/lib/src/instrumentation/errors.js +65 -0
- package/lib/src/instrumentation/fetch.d.ts +16 -0
- package/lib/src/instrumentation/fetch.d.ts.map +1 -0
- package/lib/src/instrumentation/fetch.js +75 -0
- package/lib/src/instrumentation/navigation.d.ts +19 -0
- package/lib/src/instrumentation/navigation.d.ts.map +1 -0
- package/lib/src/instrumentation/navigation.js +39 -0
- package/lib/src/instrumentation/touch.d.ts +12 -0
- package/lib/src/instrumentation/touch.d.ts.map +1 -0
- package/lib/src/instrumentation/touch.js +18 -0
- package/lib/src/instrumentation/unhandledRejection.d.ts +9 -0
- package/lib/src/instrumentation/unhandledRejection.d.ts.map +1 -0
- package/lib/src/instrumentation/unhandledRejection.js +52 -0
- package/lib/src/instrumentation/xhr.d.ts +14 -0
- package/lib/src/instrumentation/xhr.d.ts.map +1 -0
- package/lib/src/instrumentation/xhr.js +88 -0
- package/lib/src/otel-compat.d.ts +67 -0
- package/lib/src/otel-compat.d.ts.map +1 -0
- package/lib/src/otel-compat.js +84 -0
- package/package.json +72 -0
- package/react-native.config.js +17 -0
- package/src/NativeDash0Mobile.ts +29 -0
- package/src/bridge/NativeBridge.ts +101 -0
- package/src/bridge/types.ts +188 -0
- package/src/index.ts +456 -0
- package/src/instrumentation/errors.ts +84 -0
- package/src/instrumentation/fetch.ts +93 -0
- package/src/instrumentation/navigation.ts +52 -0
- package/src/instrumentation/touch.ts +32 -0
- package/src/instrumentation/unhandledRejection.ts +75 -0
- package/src/instrumentation/xhr.ts +125 -0
- 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
|
+
}
|