@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,65 @@
|
|
|
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
|
+
import { Dash0Mobile } from '../index';
|
|
13
|
+
export const DEDUPE_WINDOW_MS = 5 * 60 * 1000;
|
|
14
|
+
const SEVERITY_ERROR = 17;
|
|
15
|
+
const SEVERITY_FATAL = 21;
|
|
16
|
+
function resolveErrorUtils() {
|
|
17
|
+
const g = globalThis;
|
|
18
|
+
return g.ErrorUtils ?? null;
|
|
19
|
+
}
|
|
20
|
+
function dedupeKey(err) {
|
|
21
|
+
const type = err?.name ?? 'Error';
|
|
22
|
+
const message = err?.message ?? '';
|
|
23
|
+
return `${type}::${message}`;
|
|
24
|
+
}
|
|
25
|
+
export function installErrorInstrumentation() {
|
|
26
|
+
const errorUtils = resolveErrorUtils();
|
|
27
|
+
if (!errorUtils) {
|
|
28
|
+
// Non-RN environment — caller is probably in Jest/SSR. Return a no-op
|
|
29
|
+
// uninstaller so callers don't need to gate on platform.
|
|
30
|
+
return () => { };
|
|
31
|
+
}
|
|
32
|
+
const previous = errorUtils.getGlobalHandler();
|
|
33
|
+
const recentlySeen = new Map();
|
|
34
|
+
const handler = (error, isFatal) => {
|
|
35
|
+
const key = dedupeKey(error);
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const lastAt = recentlySeen.get(key);
|
|
38
|
+
if (lastAt === undefined || now - lastAt >= DEDUPE_WINDOW_MS) {
|
|
39
|
+
recentlySeen.set(key, now);
|
|
40
|
+
Dash0Mobile.log('app.error', {
|
|
41
|
+
'exception.type': error?.name ?? 'Error',
|
|
42
|
+
'exception.message': error?.message ?? String(error),
|
|
43
|
+
'exception.stacktrace': error?.stack ?? '',
|
|
44
|
+
}, isFatal ? SEVERITY_FATAL : SEVERITY_ERROR);
|
|
45
|
+
}
|
|
46
|
+
// Always chain — our capture must never swallow crashes from other
|
|
47
|
+
// reporters or the default redbox.
|
|
48
|
+
try {
|
|
49
|
+
previous(error, isFatal);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// The previous handler throwing would normally be catastrophic, but
|
|
53
|
+
// we've already emitted the log — nothing safe to do here.
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
errorUtils.setGlobalHandler(handler);
|
|
57
|
+
return function uninstall() {
|
|
58
|
+
// Best-effort restore. If someone chained on top of us we can't cleanly
|
|
59
|
+
// unwind — restore `previous` and accept the break (matches how Sentry
|
|
60
|
+
// handles the same race).
|
|
61
|
+
if (errorUtils.getGlobalHandler() === handler) {
|
|
62
|
+
errorUtils.setGlobalHandler(previous);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetch() auto-instrumentation for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the global fetch so every HTTP call produces a CLIENT span with
|
|
5
|
+
* OTel HTTP semconv v1.23+ attributes. Matches attribute names emitted by
|
|
6
|
+
* the iOS URLProtocol interceptor and the Android OkHttp interceptor so a
|
|
7
|
+
* single Dash0 filter surfaces network spans across all three platforms.
|
|
8
|
+
*
|
|
9
|
+
* Self-capture avoidance: pass the collector host via `ignoredHosts` so
|
|
10
|
+
* OTLP export requests don't generate spans that then need to be exported.
|
|
11
|
+
*/
|
|
12
|
+
export interface FetchInstrumentationConfig {
|
|
13
|
+
ignoredHosts?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function installFetchInstrumentation(config?: FetchInstrumentationConfig): () => void;
|
|
16
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/instrumentation/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAoBD,wBAAgB,2BAA2B,CACzC,MAAM,GAAE,0BAA+B,GACtC,MAAM,IAAI,CAsDZ"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetch() auto-instrumentation for React Native.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the global fetch so every HTTP call produces a CLIENT span with
|
|
5
|
+
* OTel HTTP semconv v1.23+ attributes. Matches attribute names emitted by
|
|
6
|
+
* the iOS URLProtocol interceptor and the Android OkHttp interceptor so a
|
|
7
|
+
* single Dash0 filter surfaces network spans across all three platforms.
|
|
8
|
+
*
|
|
9
|
+
* Self-capture avoidance: pass the collector host via `ignoredHosts` so
|
|
10
|
+
* OTLP export requests don't generate spans that then need to be exported.
|
|
11
|
+
*/
|
|
12
|
+
import { Dash0Mobile } from '../index';
|
|
13
|
+
function hostFromUrl(url) {
|
|
14
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i.exec(url);
|
|
15
|
+
return match ? match[1].toLowerCase() : null;
|
|
16
|
+
}
|
|
17
|
+
function resolveUrl(input) {
|
|
18
|
+
if (typeof input === 'string')
|
|
19
|
+
return input;
|
|
20
|
+
if (input instanceof URL)
|
|
21
|
+
return input.toString();
|
|
22
|
+
return input.url;
|
|
23
|
+
}
|
|
24
|
+
function resolveMethod(input, init) {
|
|
25
|
+
const m = init?.method ?? input?.method ?? 'GET';
|
|
26
|
+
return m.toUpperCase();
|
|
27
|
+
}
|
|
28
|
+
export function installFetchInstrumentation(config = {}) {
|
|
29
|
+
const original = globalThis.fetch;
|
|
30
|
+
const ignored = new Set((config.ignoredHosts ?? []).map(h => h.toLowerCase()));
|
|
31
|
+
const wrapped = async (input, init) => {
|
|
32
|
+
const url = resolveUrl(input);
|
|
33
|
+
const host = hostFromUrl(url);
|
|
34
|
+
if (host && ignored.has(host)) {
|
|
35
|
+
return original(input, init);
|
|
36
|
+
}
|
|
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
|
+
try {
|
|
44
|
+
const response = await original(input, init);
|
|
45
|
+
const status = response?.status;
|
|
46
|
+
if (typeof status === 'number') {
|
|
47
|
+
handle.setAttribute('http.response.status_code', status);
|
|
48
|
+
if (status >= 400) {
|
|
49
|
+
handle.setStatus('ERROR', `HTTP ${status}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
handle.setStatus('OK');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
handle.setStatus('OK');
|
|
57
|
+
}
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
handle.setStatus('ERROR', message);
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
handle.end();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
globalThis.fetch = wrapped;
|
|
70
|
+
return function uninstall() {
|
|
71
|
+
if (globalThis.fetch === wrapped) {
|
|
72
|
+
globalThis.fetch = original;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Navigation auto-instrumentation (opt-in).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const navRef = useNavigationContainerRef();
|
|
6
|
+
* useEffect(() => installReactNavigationInstrumentation(navRef), [navRef]);
|
|
7
|
+
*
|
|
8
|
+
* Emits `ui.screen_view` logs and `page.<name>` spans keyed by the current
|
|
9
|
+
* route. Consecutive emissions for the same route are suppressed.
|
|
10
|
+
*/
|
|
11
|
+
interface NavRefLike {
|
|
12
|
+
addListener(type: string, listener: () => void): () => void;
|
|
13
|
+
getCurrentRoute(): {
|
|
14
|
+
name: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare function installReactNavigationInstrumentation(navRef: NavRefLike | null | undefined): () => void;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +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,CA8BZ"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Navigation auto-instrumentation (opt-in).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const navRef = useNavigationContainerRef();
|
|
6
|
+
* useEffect(() => installReactNavigationInstrumentation(navRef), [navRef]);
|
|
7
|
+
*
|
|
8
|
+
* Emits `ui.screen_view` logs and `page.<name>` spans keyed by the current
|
|
9
|
+
* route. Consecutive emissions for the same route are suppressed.
|
|
10
|
+
*/
|
|
11
|
+
import { Dash0Mobile } from '../index';
|
|
12
|
+
export function installReactNavigationInstrumentation(navRef) {
|
|
13
|
+
if (!navRef)
|
|
14
|
+
return () => { };
|
|
15
|
+
let currentName = null;
|
|
16
|
+
let currentSpan = null;
|
|
17
|
+
const onState = () => {
|
|
18
|
+
const route = navRef.getCurrentRoute();
|
|
19
|
+
if (!route)
|
|
20
|
+
return;
|
|
21
|
+
if (route.name === currentName)
|
|
22
|
+
return;
|
|
23
|
+
if (currentSpan) {
|
|
24
|
+
currentSpan.end();
|
|
25
|
+
currentSpan = null;
|
|
26
|
+
}
|
|
27
|
+
currentName = route.name;
|
|
28
|
+
Dash0Mobile.log('ui.screen_view', { 'screen.name': route.name }, 9);
|
|
29
|
+
currentSpan = Dash0Mobile.startSpan(`page.${route.name}`);
|
|
30
|
+
};
|
|
31
|
+
const unsub = navRef.addListener('state', onState);
|
|
32
|
+
return function uninstall() {
|
|
33
|
+
unsub();
|
|
34
|
+
if (currentSpan) {
|
|
35
|
+
currentSpan.end();
|
|
36
|
+
currentSpan = null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
import type { Attributes } from '../bridge/types';
|
|
9
|
+
type Handler<Args extends unknown[], R> = (...args: Args) => R;
|
|
10
|
+
export declare function withTapTelemetry<Args extends unknown[], R>(target: string, handler?: Handler<Args, R>, extraAttrs?: Attributes): Handler<Args, R | undefined>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=touch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"touch.d.ts","sourceRoot":"","sources":["../../../src/instrumentation/touch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,KAAK,OAAO,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,CAAC;AAE/D,wBAAgB,gBAAgB,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EACxD,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,EAC1B,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC,CAY9B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
import { Dash0Mobile } from '../index';
|
|
9
|
+
const SEVERITY_INFO = 9;
|
|
10
|
+
export function withTapTelemetry(target, handler, extraAttrs) {
|
|
11
|
+
return function wrappedOnPress(...args) {
|
|
12
|
+
Dash0Mobile.log('ui.tap', {
|
|
13
|
+
'ui.target': target,
|
|
14
|
+
...(extraAttrs ?? {}),
|
|
15
|
+
}, SEVERITY_INFO);
|
|
16
|
+
return handler ? handler(...args) : undefined;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export declare function installUnhandledRejectionInstrumentation(): () => void;
|
|
9
|
+
//# sourceMappingURL=unhandledRejection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unhandledRejection.d.ts","sourceRoot":"","sources":["../../../src/instrumentation/unhandledRejection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsBH,wBAAgB,wCAAwC,IAAI,MAAM,IAAI,CA8CrE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
import { Dash0Mobile } from '../index';
|
|
9
|
+
const DEDUPE_WINDOW_MS = 5 * 60 * 1000;
|
|
10
|
+
const SEVERITY_ERROR = 17;
|
|
11
|
+
function dedupeKey(reason) {
|
|
12
|
+
if (reason instanceof Error) {
|
|
13
|
+
return `${reason.name ?? 'Error'}::${reason.message ?? ''}`;
|
|
14
|
+
}
|
|
15
|
+
return `UnhandledRejection::${String(reason)}`;
|
|
16
|
+
}
|
|
17
|
+
export function installUnhandledRejectionInstrumentation() {
|
|
18
|
+
const g = globalThis;
|
|
19
|
+
if (typeof g.addEventListener !== 'function' || typeof g.removeEventListener !== 'function') {
|
|
20
|
+
// Environment doesn't expose a global event target (most bare Node contexts).
|
|
21
|
+
return () => { };
|
|
22
|
+
}
|
|
23
|
+
const recentlySeen = new Map();
|
|
24
|
+
const handler = (ev) => {
|
|
25
|
+
const reason = ev?.reason;
|
|
26
|
+
const key = dedupeKey(reason);
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const lastAt = recentlySeen.get(key);
|
|
29
|
+
if (lastAt !== undefined && now - lastAt < DEDUPE_WINDOW_MS)
|
|
30
|
+
return;
|
|
31
|
+
recentlySeen.set(key, now);
|
|
32
|
+
if (reason instanceof Error) {
|
|
33
|
+
Dash0Mobile.log('app.error', {
|
|
34
|
+
'exception.type': reason.name ?? 'Error',
|
|
35
|
+
'exception.message': reason.message ?? String(reason),
|
|
36
|
+
'exception.stacktrace': reason.stack ?? '',
|
|
37
|
+
'exception.escaped': true,
|
|
38
|
+
}, SEVERITY_ERROR);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
Dash0Mobile.log('app.error', {
|
|
42
|
+
'exception.type': 'UnhandledRejection',
|
|
43
|
+
'exception.message': String(reason),
|
|
44
|
+
'exception.escaped': true,
|
|
45
|
+
}, SEVERITY_ERROR);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
g.addEventListener('unhandledrejection', handler);
|
|
49
|
+
return function uninstall() {
|
|
50
|
+
g.removeEventListener?.('unhandledrejection', handler);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export interface XhrInstrumentationConfig {
|
|
11
|
+
ignoredHosts?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function installXhrInstrumentation(config?: XhrInstrumentationConfig): () => void;
|
|
14
|
+
//# sourceMappingURL=xhr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xhr.d.ts","sourceRoot":"","sources":["../../../src/instrumentation/xhr.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,wBAAwB;IACvC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAWD,wBAAgB,yBAAyB,CACvC,MAAM,GAAE,wBAA6B,GACpC,MAAM,IAAI,CAgGZ"}
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
import { Dash0Mobile } from '../index';
|
|
11
|
+
const HOST_PATTERN = /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i;
|
|
12
|
+
function hostFromUrl(url) {
|
|
13
|
+
const match = url.match(HOST_PATTERN);
|
|
14
|
+
return match ? match[1].toLowerCase() : null;
|
|
15
|
+
}
|
|
16
|
+
export function installXhrInstrumentation(config = {}) {
|
|
17
|
+
const OriginalXHR = globalThis.XMLHttpRequest;
|
|
18
|
+
if (!OriginalXHR)
|
|
19
|
+
return () => { };
|
|
20
|
+
const ignored = new Set((config.ignoredHosts ?? []).map(h => h.toLowerCase()));
|
|
21
|
+
const wrapped = new Proxy(OriginalXHR, {
|
|
22
|
+
construct(target, args, newTarget) {
|
|
23
|
+
const instance = Reflect.construct(target, args, newTarget);
|
|
24
|
+
const originalOpen = instance.open.bind(instance);
|
|
25
|
+
instance.open = function open(method, url, ...rest) {
|
|
26
|
+
instance.__dash0_method = method.toUpperCase();
|
|
27
|
+
instance.__dash0_url = typeof url === 'string' ? url : url.toString();
|
|
28
|
+
// @ts-expect-error — forwarding variadic
|
|
29
|
+
return originalOpen(method, url, ...rest);
|
|
30
|
+
};
|
|
31
|
+
const originalSend = instance.send.bind(instance);
|
|
32
|
+
instance.send = function send(body) {
|
|
33
|
+
const url = instance.__dash0_url ?? '';
|
|
34
|
+
const host = hostFromUrl(url);
|
|
35
|
+
if (!host || !ignored.has(host)) {
|
|
36
|
+
const method = instance.__dash0_method ?? 'GET';
|
|
37
|
+
instance.__dash0_span = Dash0Mobile.startSpan(`${method} ${host ?? 'unknown'}`, {
|
|
38
|
+
'http.request.method': method,
|
|
39
|
+
'url.full': url,
|
|
40
|
+
...(host ? { 'server.address': host } : {}),
|
|
41
|
+
}, 'CLIENT');
|
|
42
|
+
const onError = () => {
|
|
43
|
+
instance.__dash0_failed = true;
|
|
44
|
+
};
|
|
45
|
+
const onLoadEnd = () => {
|
|
46
|
+
const span = instance.__dash0_span;
|
|
47
|
+
if (!span)
|
|
48
|
+
return;
|
|
49
|
+
if (instance.__dash0_failed) {
|
|
50
|
+
span.setStatus('ERROR', 'network error');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const status = instance.status;
|
|
54
|
+
if (typeof status === 'number' && status > 0) {
|
|
55
|
+
span.setAttribute('http.response.status_code', status);
|
|
56
|
+
if (status >= 400) {
|
|
57
|
+
span.setStatus('ERROR', `HTTP ${status}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
span.setStatus('OK');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
span.setStatus('OK');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
span.end();
|
|
68
|
+
instance.__dash0_span = undefined;
|
|
69
|
+
};
|
|
70
|
+
instance.addEventListener('error', onError);
|
|
71
|
+
instance.addEventListener('loadend', onLoadEnd);
|
|
72
|
+
}
|
|
73
|
+
return originalSend(body);
|
|
74
|
+
};
|
|
75
|
+
return instance;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
globalThis.XMLHttpRequest =
|
|
79
|
+
wrapped;
|
|
80
|
+
return function uninstall() {
|
|
81
|
+
const current = globalThis
|
|
82
|
+
.XMLHttpRequest;
|
|
83
|
+
if (current === wrapped) {
|
|
84
|
+
globalThis.XMLHttpRequest =
|
|
85
|
+
OriginalXHR;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
import type { Attributes, SeverityNumber } from './bridge/types';
|
|
14
|
+
interface SpanStatus {
|
|
15
|
+
code: number;
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
interface SpanOptions {
|
|
19
|
+
kind?: number;
|
|
20
|
+
attributes?: Attributes;
|
|
21
|
+
}
|
|
22
|
+
interface CompatSpan {
|
|
23
|
+
setAttribute(key: string, value: string | number | boolean): void;
|
|
24
|
+
setStatus(status: SpanStatus): void;
|
|
25
|
+
end(): void;
|
|
26
|
+
}
|
|
27
|
+
interface CompatTracer {
|
|
28
|
+
startSpan(name: string, options?: SpanOptions): CompatSpan;
|
|
29
|
+
}
|
|
30
|
+
declare function getTracer(_name: string, _version?: string): CompatTracer;
|
|
31
|
+
interface LogRecord {
|
|
32
|
+
body?: string;
|
|
33
|
+
severityNumber?: SeverityNumber;
|
|
34
|
+
attributes?: Attributes;
|
|
35
|
+
}
|
|
36
|
+
interface CompatLogger {
|
|
37
|
+
emit(record: LogRecord): void;
|
|
38
|
+
}
|
|
39
|
+
declare function getLogger(_name: string, _version?: string): CompatLogger;
|
|
40
|
+
interface CompatCounter {
|
|
41
|
+
add(value: number, attributes?: Attributes): void;
|
|
42
|
+
}
|
|
43
|
+
interface CompatHistogram {
|
|
44
|
+
record(value: number, attributes?: Attributes): void;
|
|
45
|
+
}
|
|
46
|
+
interface CompatGauge {
|
|
47
|
+
record(value: number, attributes?: Attributes): void;
|
|
48
|
+
}
|
|
49
|
+
interface CompatMeter {
|
|
50
|
+
createCounter(name: string): CompatCounter;
|
|
51
|
+
createHistogram(name: string): CompatHistogram;
|
|
52
|
+
createGauge(name: string): CompatGauge;
|
|
53
|
+
}
|
|
54
|
+
declare function getMeter(_name: string, _version?: string): CompatMeter;
|
|
55
|
+
export declare const otel: {
|
|
56
|
+
trace: {
|
|
57
|
+
getTracer: typeof getTracer;
|
|
58
|
+
};
|
|
59
|
+
logs: {
|
|
60
|
+
getLogger: typeof getLogger;
|
|
61
|
+
};
|
|
62
|
+
metrics: {
|
|
63
|
+
getMeter: typeof getMeter;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export type { CompatSpan, CompatTracer, CompatLogger, CompatMeter, CompatCounter, CompatHistogram, CompatGauge, };
|
|
67
|
+
//# sourceMappingURL=otel-compat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel-compat.d.ts","sourceRoot":"","sources":["../../src/otel-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAY,MAAM,gBAAgB,CAAC;AAI3E,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,WAAW;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,UAAU;IAClB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAClE,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,GAAG,IAAI,IAAI,CAAC;CACb;AAED,UAAU,YAAY;IACpB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;CAC5D;AA4BD,iBAAS,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAQjE;AAID,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,YAAY;IACpB,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;CAC/B;AAED,iBAAS,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAOjE;AAID,UAAU,aAAa;IACrB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CACnD;AAED,UAAU,eAAe;IACvB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CACtD;AAED,UAAU,WAAW;IACnB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CACtD;AAED,UAAU,WAAW;IACnB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC;IAC3C,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAC;IAC/C,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CACxC;AAED,iBAAS,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,CAwB/D;AAID,eAAO,MAAM,IAAI;;;;;;;;;;CAIhB,CAAC;AAEF,YAAY,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,aAAa,EACb,eAAe,EACf,WAAW,GACZ,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
import { Dash0Mobile } from './index';
|
|
14
|
+
const OTEL_SPAN_KIND_TO_OURS = {
|
|
15
|
+
0: 'INTERNAL',
|
|
16
|
+
1: 'SERVER',
|
|
17
|
+
2: 'CLIENT',
|
|
18
|
+
3: 'PRODUCER',
|
|
19
|
+
4: 'CONSUMER',
|
|
20
|
+
};
|
|
21
|
+
function mapStatus(code) {
|
|
22
|
+
return code === 2 ? 'ERROR' : 'OK';
|
|
23
|
+
}
|
|
24
|
+
function wrapSpan(handle) {
|
|
25
|
+
return {
|
|
26
|
+
setAttribute(key, value) {
|
|
27
|
+
handle.setAttribute(key, value);
|
|
28
|
+
},
|
|
29
|
+
setStatus(status) {
|
|
30
|
+
handle.setStatus(mapStatus(status.code), status.message);
|
|
31
|
+
},
|
|
32
|
+
end() {
|
|
33
|
+
handle.end();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function getTracer(_name, _version) {
|
|
38
|
+
return {
|
|
39
|
+
startSpan(name, options) {
|
|
40
|
+
const kind = OTEL_SPAN_KIND_TO_OURS[options?.kind ?? 0] ?? 'INTERNAL';
|
|
41
|
+
const handle = Dash0Mobile.startSpan(name, options?.attributes, kind);
|
|
42
|
+
return wrapSpan(handle);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function getLogger(_name, _version) {
|
|
47
|
+
return {
|
|
48
|
+
emit(record) {
|
|
49
|
+
const name = record.body ?? 'log';
|
|
50
|
+
Dash0Mobile.log(name, record.attributes ?? {}, record.severityNumber ?? 9);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function getMeter(_name, _version) {
|
|
55
|
+
return {
|
|
56
|
+
createCounter(name) {
|
|
57
|
+
return {
|
|
58
|
+
add(value, attributes) {
|
|
59
|
+
Dash0Mobile.recordMetric(name, value, 'counter', attributes ?? {});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
createHistogram(name) {
|
|
64
|
+
return {
|
|
65
|
+
record(value, attributes) {
|
|
66
|
+
Dash0Mobile.recordMetric(name, value, 'histogram', attributes ?? {});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
createGauge(name) {
|
|
71
|
+
return {
|
|
72
|
+
record(value, attributes) {
|
|
73
|
+
Dash0Mobile.recordMetric(name, value, 'gauge', attributes ?? {});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// ─── Namespace ──────────────────────────────────────────────────────────────
|
|
80
|
+
export const otel = {
|
|
81
|
+
trace: { getTracer },
|
|
82
|
+
logs: { getLogger },
|
|
83
|
+
metrics: { getMeter },
|
|
84
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@barrysolomon/mobile-react-native",
|
|
3
|
+
"version": "0.1.0-alpha",
|
|
4
|
+
"description": "Dash0 Mobile Observability SDK for React Native (JS/TS bridge). For native iOS use the Swift Package at https://github.com/barrysolomon/mobile-otel. For native Android use io.opentelemetry.android:mobile on GitHub Packages.",
|
|
5
|
+
"main": "lib/commonjs/index.js",
|
|
6
|
+
"module": "lib/module/index.js",
|
|
7
|
+
"types": "lib/typescript/src/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"lib",
|
|
13
|
+
"android",
|
|
14
|
+
"ios",
|
|
15
|
+
"cpp",
|
|
16
|
+
"*.podspec",
|
|
17
|
+
"react-native.config.js",
|
|
18
|
+
"!**/__tests__",
|
|
19
|
+
"!**/.*"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:watch": "jest --watch",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"publish:alpha": "npm publish --tag alpha"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"react-native",
|
|
36
|
+
"opentelemetry",
|
|
37
|
+
"observability",
|
|
38
|
+
"dash0",
|
|
39
|
+
"mobile",
|
|
40
|
+
"rum"
|
|
41
|
+
],
|
|
42
|
+
"repository": "https://github.com/barrysolomon/mobile-otel",
|
|
43
|
+
"license": "Apache-2.0",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=18.0.0",
|
|
46
|
+
"react-native": ">=0.72.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/jest": "^29.5.12",
|
|
50
|
+
"@types/react": "^18.2.0",
|
|
51
|
+
"@types/react-native": "^0.73.0",
|
|
52
|
+
"jest": "^29.7.0",
|
|
53
|
+
"react": "18.3.1",
|
|
54
|
+
"react-native": "0.76.0",
|
|
55
|
+
"ts-jest": "^29.1.2",
|
|
56
|
+
"typescript": "^5.3.3"
|
|
57
|
+
},
|
|
58
|
+
"jest": {
|
|
59
|
+
"preset": "ts-jest",
|
|
60
|
+
"testEnvironment": "node",
|
|
61
|
+
"testMatch": [
|
|
62
|
+
"<rootDir>/__tests__/**/*.test.(ts|tsx)"
|
|
63
|
+
],
|
|
64
|
+
"moduleFileExtensions": [
|
|
65
|
+
"ts",
|
|
66
|
+
"tsx",
|
|
67
|
+
"js",
|
|
68
|
+
"jsx",
|
|
69
|
+
"json"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|