@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,32 @@
1
+ /**
2
+ * Touch telemetry helpers (opt-in).
3
+ *
4
+ * `withTapTelemetry(target, handler?, extraAttrs?)` returns a press handler
5
+ * that emits a `ui.tap` log before forwarding the call. Attribute names
6
+ * match Android's `TapInstrumentation` so cross-platform dashboards line up.
7
+ */
8
+
9
+ import { Dash0Mobile } from '../index';
10
+ import type { Attributes } from '../bridge/types';
11
+
12
+ const SEVERITY_INFO = 9 as const;
13
+
14
+ type Handler<Args extends unknown[], R> = (...args: Args) => R;
15
+
16
+ export function withTapTelemetry<Args extends unknown[], R>(
17
+ target: string,
18
+ handler?: Handler<Args, R>,
19
+ extraAttrs?: Attributes,
20
+ ): Handler<Args, R | undefined> {
21
+ return function wrappedOnPress(...args: Args): R | undefined {
22
+ Dash0Mobile.log(
23
+ 'ui.tap',
24
+ {
25
+ 'ui.target': target,
26
+ ...(extraAttrs ?? {}),
27
+ },
28
+ SEVERITY_INFO,
29
+ );
30
+ return handler ? handler(...args) : undefined;
31
+ };
32
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Unhandled promise rejection auto-instrumentation for React Native.
3
+ *
4
+ * Shares a dedupe window with the sync error handler so a thrown Error and
5
+ * its rejected-promise twin collapse to one log — matches Android's
6
+ * ErrorInstrumentation dedupe semantics.
7
+ */
8
+
9
+ import { Dash0Mobile } from '../index';
10
+
11
+ const DEDUPE_WINDOW_MS = 5 * 60 * 1000;
12
+ const SEVERITY_ERROR = 17 as const;
13
+
14
+ type RejectionEvent = { reason: unknown };
15
+ type RejectionListener = (ev: RejectionEvent) => void;
16
+
17
+ interface GlobalWithEventTarget {
18
+ addEventListener?: (type: string, listener: RejectionListener) => void;
19
+ removeEventListener?: (type: string, listener: RejectionListener) => void;
20
+ }
21
+
22
+ function dedupeKey(reason: unknown): string {
23
+ if (reason instanceof Error) {
24
+ return `${reason.name ?? 'Error'}::${reason.message ?? ''}`;
25
+ }
26
+ return `UnhandledRejection::${String(reason)}`;
27
+ }
28
+
29
+ export function installUnhandledRejectionInstrumentation(): () => void {
30
+ const g = globalThis as unknown as GlobalWithEventTarget;
31
+ if (typeof g.addEventListener !== 'function' || typeof g.removeEventListener !== 'function') {
32
+ // Environment doesn't expose a global event target (most bare Node contexts).
33
+ return () => {};
34
+ }
35
+
36
+ const recentlySeen = new Map<string, number>();
37
+
38
+ const handler: RejectionListener = (ev) => {
39
+ const reason = ev?.reason;
40
+ const key = dedupeKey(reason);
41
+ const now = Date.now();
42
+ const lastAt = recentlySeen.get(key);
43
+ if (lastAt !== undefined && now - lastAt < DEDUPE_WINDOW_MS) return;
44
+ recentlySeen.set(key, now);
45
+
46
+ if (reason instanceof Error) {
47
+ Dash0Mobile.log(
48
+ 'app.error',
49
+ {
50
+ 'exception.type': reason.name ?? 'Error',
51
+ 'exception.message': reason.message ?? String(reason),
52
+ 'exception.stacktrace': reason.stack ?? '',
53
+ 'exception.escaped': true,
54
+ },
55
+ SEVERITY_ERROR,
56
+ );
57
+ } else {
58
+ Dash0Mobile.log(
59
+ 'app.error',
60
+ {
61
+ 'exception.type': 'UnhandledRejection',
62
+ 'exception.message': String(reason),
63
+ 'exception.escaped': true,
64
+ },
65
+ SEVERITY_ERROR,
66
+ );
67
+ }
68
+ };
69
+
70
+ g.addEventListener('unhandledrejection', handler);
71
+
72
+ return function uninstall() {
73
+ g.removeEventListener?.('unhandledrejection', handler);
74
+ };
75
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * XMLHttpRequest auto-instrumentation for React Native.
3
+ *
4
+ * RN's fetch is itself XHR-backed, so libraries that use XHR directly
5
+ * (axios, apollo) bypass the fetch wrapper. This module replaces the
6
+ * global `XMLHttpRequest` constructor with a Proxy that captures a
7
+ * CLIENT span per request. Attribute shape matches the fetch wrapper so
8
+ * Dash0 queries see a single consistent HTTP span schema.
9
+ */
10
+
11
+ import { Dash0Mobile } from '../index';
12
+ import type { SpanHandle } from '../index';
13
+
14
+ export interface XhrInstrumentationConfig {
15
+ ignoredHosts?: readonly string[];
16
+ }
17
+
18
+ const HOST_PATTERN = /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i;
19
+
20
+ function hostFromUrl(url: string): string | null {
21
+ const match = url.match(HOST_PATTERN);
22
+ return match ? match[1].toLowerCase() : null;
23
+ }
24
+
25
+ type XhrCtor = typeof globalThis.XMLHttpRequest;
26
+
27
+ export function installXhrInstrumentation(
28
+ config: XhrInstrumentationConfig = {},
29
+ ): () => void {
30
+ const OriginalXHR: XhrCtor | undefined = (
31
+ globalThis as unknown as { XMLHttpRequest?: XhrCtor }
32
+ ).XMLHttpRequest;
33
+
34
+ if (!OriginalXHR) return () => {};
35
+
36
+ const ignored = new Set(
37
+ (config.ignoredHosts ?? []).map(h => h.toLowerCase()),
38
+ );
39
+
40
+ const wrapped = new Proxy(OriginalXHR, {
41
+ construct(target, args, newTarget) {
42
+ const instance = Reflect.construct(target, args, newTarget) as XMLHttpRequest & {
43
+ __dash0_method?: string;
44
+ __dash0_url?: string;
45
+ __dash0_span?: SpanHandle;
46
+ __dash0_failed?: boolean;
47
+ };
48
+
49
+ const originalOpen = instance.open.bind(instance);
50
+ (instance as unknown as { open: XMLHttpRequest['open'] }).open = function open(
51
+ method: string,
52
+ url: string | URL,
53
+ ...rest: unknown[]
54
+ ) {
55
+ instance.__dash0_method = method.toUpperCase();
56
+ instance.__dash0_url = typeof url === 'string' ? url : url.toString();
57
+ // @ts-expect-error — forwarding variadic
58
+ return originalOpen(method, url, ...rest);
59
+ };
60
+
61
+ const originalSend = instance.send.bind(instance);
62
+ (instance as unknown as { send: XMLHttpRequest['send'] }).send = function send(
63
+ body?: Document | XMLHttpRequestBodyInit | null,
64
+ ) {
65
+ const url = instance.__dash0_url ?? '';
66
+ const host = hostFromUrl(url);
67
+ if (!host || !ignored.has(host)) {
68
+ const method = instance.__dash0_method ?? 'GET';
69
+ instance.__dash0_span = Dash0Mobile.startSpan(
70
+ `${method} ${host ?? 'unknown'}`,
71
+ {
72
+ 'http.request.method': method,
73
+ 'url.full': url,
74
+ ...(host ? { 'server.address': host } : {}),
75
+ },
76
+ 'CLIENT',
77
+ );
78
+
79
+ const onError = () => {
80
+ instance.__dash0_failed = true;
81
+ };
82
+ const onLoadEnd = () => {
83
+ const span = instance.__dash0_span;
84
+ if (!span) return;
85
+ if (instance.__dash0_failed) {
86
+ span.setStatus('ERROR', 'network error');
87
+ } else {
88
+ const status = instance.status;
89
+ if (typeof status === 'number' && status > 0) {
90
+ span.setAttribute('http.response.status_code', status);
91
+ if (status >= 400) {
92
+ span.setStatus('ERROR', `HTTP ${status}`);
93
+ } else {
94
+ span.setStatus('OK');
95
+ }
96
+ } else {
97
+ span.setStatus('OK');
98
+ }
99
+ }
100
+ span.end();
101
+ instance.__dash0_span = undefined;
102
+ };
103
+
104
+ instance.addEventListener('error', onError);
105
+ instance.addEventListener('loadend', onLoadEnd);
106
+ }
107
+ return originalSend(body);
108
+ };
109
+
110
+ return instance;
111
+ },
112
+ });
113
+
114
+ (globalThis as unknown as { XMLHttpRequest: XhrCtor }).XMLHttpRequest =
115
+ wrapped as XhrCtor;
116
+
117
+ return function uninstall() {
118
+ const current = (globalThis as unknown as { XMLHttpRequest?: XhrCtor })
119
+ .XMLHttpRequest;
120
+ if (current === wrapped) {
121
+ (globalThis as unknown as { XMLHttpRequest?: XhrCtor }).XMLHttpRequest =
122
+ OriginalXHR;
123
+ }
124
+ };
125
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Minimal @opentelemetry/api compatibility surface.
3
+ *
4
+ * Exposes `otel.trace.getTracer`, `otel.logs.getLogger`, `otel.metrics.getMeter`
5
+ * backed by our bridge. Scope is the subset of the API third-party JS libraries
6
+ * actually use at runtime — this is NOT a drop-in for the full upstream API.
7
+ *
8
+ * Why a shim instead of the real package: adding `@opentelemetry/api` as a
9
+ * runtime dep pulls version-matching and global-registration concerns into
10
+ * every RN app that uses us. The shim keeps our bundle small and our bridge
11
+ * the sole source of truth.
12
+ */
13
+
14
+ import { Dash0Mobile } from './index';
15
+ import type { SpanHandle } from './index';
16
+ import type { Attributes, SeverityNumber, SpanKind } from './bridge/types';
17
+
18
+ // ─── Tracer ──────────────────────────────────────────────────────────────────
19
+
20
+ interface SpanStatus {
21
+ code: number; // 0 UNSET, 1 OK, 2 ERROR
22
+ message?: string;
23
+ }
24
+
25
+ interface SpanOptions {
26
+ kind?: number; // OTel SpanKind enum (0-5). We only map 0 INTERNAL / 3 CLIENT / 4 PRODUCER / 5 CONSUMER.
27
+ attributes?: Attributes;
28
+ }
29
+
30
+ interface CompatSpan {
31
+ setAttribute(key: string, value: string | number | boolean): void;
32
+ setStatus(status: SpanStatus): void;
33
+ end(): void;
34
+ }
35
+
36
+ interface CompatTracer {
37
+ startSpan(name: string, options?: SpanOptions): CompatSpan;
38
+ }
39
+
40
+ const OTEL_SPAN_KIND_TO_OURS: Record<number, SpanKind> = {
41
+ 0: 'INTERNAL',
42
+ 1: 'SERVER',
43
+ 2: 'CLIENT',
44
+ 3: 'PRODUCER',
45
+ 4: 'CONSUMER',
46
+ };
47
+
48
+ function mapStatus(code: number): 'OK' | 'ERROR' {
49
+ return code === 2 ? 'ERROR' : 'OK';
50
+ }
51
+
52
+ function wrapSpan(handle: SpanHandle): CompatSpan {
53
+ return {
54
+ setAttribute(key, value) {
55
+ handle.setAttribute(key, value);
56
+ },
57
+ setStatus(status) {
58
+ handle.setStatus(mapStatus(status.code), status.message);
59
+ },
60
+ end() {
61
+ handle.end();
62
+ },
63
+ };
64
+ }
65
+
66
+ function getTracer(_name: string, _version?: string): CompatTracer {
67
+ return {
68
+ startSpan(name, options) {
69
+ const kind = OTEL_SPAN_KIND_TO_OURS[options?.kind ?? 0] ?? 'INTERNAL';
70
+ const handle = Dash0Mobile.startSpan(name, options?.attributes, kind);
71
+ return wrapSpan(handle);
72
+ },
73
+ };
74
+ }
75
+
76
+ // ─── Logger ──────────────────────────────────────────────────────────────────
77
+
78
+ interface LogRecord {
79
+ body?: string;
80
+ severityNumber?: SeverityNumber;
81
+ attributes?: Attributes;
82
+ }
83
+
84
+ interface CompatLogger {
85
+ emit(record: LogRecord): void;
86
+ }
87
+
88
+ function getLogger(_name: string, _version?: string): CompatLogger {
89
+ return {
90
+ emit(record) {
91
+ const name = record.body ?? 'log';
92
+ Dash0Mobile.log(name, record.attributes ?? {}, record.severityNumber ?? 9);
93
+ },
94
+ };
95
+ }
96
+
97
+ // ─── Meter ──────────────────────────────────────────────────────────────────
98
+
99
+ interface CompatCounter {
100
+ add(value: number, attributes?: Attributes): void;
101
+ }
102
+
103
+ interface CompatHistogram {
104
+ record(value: number, attributes?: Attributes): void;
105
+ }
106
+
107
+ interface CompatGauge {
108
+ record(value: number, attributes?: Attributes): void;
109
+ }
110
+
111
+ interface CompatMeter {
112
+ createCounter(name: string): CompatCounter;
113
+ createHistogram(name: string): CompatHistogram;
114
+ createGauge(name: string): CompatGauge;
115
+ }
116
+
117
+ function getMeter(_name: string, _version?: string): CompatMeter {
118
+ return {
119
+ createCounter(name) {
120
+ return {
121
+ add(value, attributes) {
122
+ Dash0Mobile.recordMetric(name, value, 'counter', attributes ?? {});
123
+ },
124
+ };
125
+ },
126
+ createHistogram(name) {
127
+ return {
128
+ record(value, attributes) {
129
+ Dash0Mobile.recordMetric(name, value, 'histogram', attributes ?? {});
130
+ },
131
+ };
132
+ },
133
+ createGauge(name) {
134
+ return {
135
+ record(value, attributes) {
136
+ Dash0Mobile.recordMetric(name, value, 'gauge', attributes ?? {});
137
+ },
138
+ };
139
+ },
140
+ };
141
+ }
142
+
143
+ // ─── Namespace ──────────────────────────────────────────────────────────────
144
+
145
+ export const otel = {
146
+ trace: { getTracer },
147
+ logs: { getLogger },
148
+ metrics: { getMeter },
149
+ };
150
+
151
+ export type {
152
+ CompatSpan,
153
+ CompatTracer,
154
+ CompatLogger,
155
+ CompatMeter,
156
+ CompatCounter,
157
+ CompatHistogram,
158
+ CompatGauge,
159
+ };