@debugbundle/sdk-react-native 0.1.0

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 (67) hide show
  1. package/DebugBundleReactNative.podspec +15 -0
  2. package/README.md +124 -0
  3. package/android/build.gradle +37 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +281 -0
  6. package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativePackage.java +20 -0
  7. package/app.plugin.js +52 -0
  8. package/dist/NativeDebugBundleReactNative.d.ts +13 -0
  9. package/dist/NativeDebugBundleReactNative.d.ts.map +1 -0
  10. package/dist/NativeDebugBundleReactNative.js +2 -0
  11. package/dist/app-state.d.ts +9 -0
  12. package/dist/app-state.d.ts.map +1 -0
  13. package/dist/app-state.js +10 -0
  14. package/dist/client.d.ts +36 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +328 -0
  17. package/dist/console.d.ts +3 -0
  18. package/dist/console.d.ts.map +1 -0
  19. package/dist/console.js +35 -0
  20. package/dist/errors.d.ts +6 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +42 -0
  23. package/dist/index.d.ts +25 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +59 -0
  26. package/dist/native.d.ts +7 -0
  27. package/dist/native.d.ts.map +1 -0
  28. package/dist/native.js +59 -0
  29. package/dist/navigation.d.ts +13 -0
  30. package/dist/navigation.d.ts.map +1 -0
  31. package/dist/navigation.js +26 -0
  32. package/dist/network.d.ts +14 -0
  33. package/dist/network.d.ts.map +1 -0
  34. package/dist/network.js +179 -0
  35. package/dist/probes.d.ts +4 -0
  36. package/dist/probes.d.ts.map +1 -0
  37. package/dist/probes.js +8 -0
  38. package/dist/react.d.ts +21 -0
  39. package/dist/react.d.ts.map +1 -0
  40. package/dist/react.js +36 -0
  41. package/dist/redaction.d.ts +12 -0
  42. package/dist/redaction.d.ts.map +1 -0
  43. package/dist/redaction.js +132 -0
  44. package/dist/testing.d.ts +9 -0
  45. package/dist/testing.d.ts.map +1 -0
  46. package/dist/testing.js +36 -0
  47. package/dist/types.d.ts +152 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +1 -0
  50. package/ios/DebugBundleReactNative.swift +125 -0
  51. package/ios/DebugBundleReactNativeBridge.m +28 -0
  52. package/package.json +99 -0
  53. package/src/NativeDebugBundleReactNative.ts +14 -0
  54. package/src/app-state.ts +17 -0
  55. package/src/client.ts +385 -0
  56. package/src/console.ts +38 -0
  57. package/src/errors.ts +64 -0
  58. package/src/index.ts +82 -0
  59. package/src/native.ts +79 -0
  60. package/src/navigation.tsx +37 -0
  61. package/src/network.ts +240 -0
  62. package/src/probes.ts +11 -0
  63. package/src/react-native-shim.d.ts +14 -0
  64. package/src/react.tsx +60 -0
  65. package/src/redaction.ts +165 -0
  66. package/src/testing.ts +45 -0
  67. package/src/types.ts +173 -0
package/src/console.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { DebugBundleClient, DebugBundleLogLevel } from "./types.js";
2
+
3
+ let restoreConsole: (() => void) | null = null;
4
+
5
+ export function captureDebugBundleConsole(client: Pick<DebugBundleClient, "captureLog">): () => void {
6
+ if (restoreConsole) {
7
+ return restoreConsole;
8
+ }
9
+ const originalWarn = console.warn;
10
+ const originalError = console.error;
11
+ let inCapture = false;
12
+ console.warn = (...args: unknown[]) => {
13
+ capture("warning", args);
14
+ originalWarn(...args);
15
+ };
16
+ console.error = (...args: unknown[]) => {
17
+ capture("error", args);
18
+ originalError(...args);
19
+ };
20
+ restoreConsole = () => {
21
+ console.warn = originalWarn;
22
+ console.error = originalError;
23
+ restoreConsole = null;
24
+ };
25
+ return restoreConsole;
26
+
27
+ function capture(level: DebugBundleLogLevel, args: unknown[]): void {
28
+ if (inCapture) {
29
+ return;
30
+ }
31
+ inCapture = true;
32
+ try {
33
+ client.captureLog(args.map(String).join(" "), level, { source: "console" });
34
+ } finally {
35
+ inCapture = false;
36
+ }
37
+ }
38
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { DebugBundleClient } from "./types.js";
2
+
3
+ export interface ErrorHandlerInstallOptions {
4
+ captureUnhandledRejections?: boolean;
5
+ }
6
+
7
+ let restoreHandlers: (() => void) | null = null;
8
+
9
+ export function installDebugBundleErrorHandlers(
10
+ client: Pick<DebugBundleClient, "captureException">,
11
+ options: ErrorHandlerInstallOptions = {}
12
+ ): () => void {
13
+ if (restoreHandlers) {
14
+ return restoreHandlers;
15
+ }
16
+ const restores: Array<() => void> = [];
17
+ const errorUtils = (globalThis as { ErrorUtils?: ReactNativeErrorUtils }).ErrorUtils;
18
+
19
+ if (errorUtils?.setGlobalHandler) {
20
+ const previousHandler = errorUtils.getGlobalHandler?.();
21
+ errorUtils.setGlobalHandler((error, isFatal) => {
22
+ client.captureException(error, {
23
+ source: "react-native-global-error",
24
+ fatal: Boolean(isFatal)
25
+ });
26
+ previousHandler?.(error, isFatal);
27
+ });
28
+ restores.push(() => {
29
+ if (previousHandler) {
30
+ errorUtils.setGlobalHandler?.(previousHandler);
31
+ }
32
+ });
33
+ }
34
+
35
+ if (options.captureUnhandledRejections !== false && hasAddEventListener(globalThis)) {
36
+ const listener = (event: PromiseRejectionEvent) => {
37
+ client.captureException(event.reason ?? new Error("Unhandled promise rejection"), {
38
+ source: "unhandledrejection"
39
+ });
40
+ };
41
+ globalThis.addEventListener("unhandledrejection", listener);
42
+ restores.push(() => globalThis.removeEventListener("unhandledrejection", listener));
43
+ }
44
+
45
+ restoreHandlers = () => {
46
+ for (const restore of restores.reverse()) {
47
+ restore();
48
+ }
49
+ restoreHandlers = null;
50
+ };
51
+ return restoreHandlers;
52
+ }
53
+
54
+ interface ReactNativeErrorUtils {
55
+ getGlobalHandler?: () => ((error: unknown, isFatal?: boolean) => void) | undefined;
56
+ setGlobalHandler?: (handler: (error: unknown, isFatal?: boolean) => void) => void;
57
+ }
58
+
59
+ function hasAddEventListener(value: typeof globalThis): value is typeof globalThis & {
60
+ addEventListener: (type: "unhandledrejection", listener: (event: PromiseRejectionEvent) => void) => void;
61
+ removeEventListener: (type: "unhandledrejection", listener: (event: PromiseRejectionEvent) => void) => void;
62
+ } {
63
+ return "addEventListener" in value && "removeEventListener" in value;
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { DebugBundleReactNativeClient, createDebugBundleClient } from "./client.js";
2
+ import { captureDebugBundleConsole } from "./console.js";
3
+ import { installDebugBundleErrorHandlers } from "./errors.js";
4
+ import type {
5
+ DebugBundleCaptureContext,
6
+ DebugBundleClient,
7
+ DebugBundleConfig,
8
+ DebugBundleLogLevel,
9
+ DebugBundleProbeOptions,
10
+ DebugBundleRequestInfo,
11
+ DebugBundleResponseInfo,
12
+ DebugBundleStatus
13
+ } from "./types.js";
14
+
15
+ const singleton = new DebugBundleReactNativeClient();
16
+
17
+ export const DebugBundle = {
18
+ init(config: DebugBundleConfig): void {
19
+ singleton.init(config);
20
+ if (config.captureErrors !== false) {
21
+ installDebugBundleErrorHandlers(singleton, {
22
+ captureUnhandledRejections: config.captureUnhandledRejections !== false
23
+ });
24
+ }
25
+ if (config.captureConsole === true) {
26
+ captureDebugBundleConsole(singleton);
27
+ }
28
+ },
29
+ captureException(error: unknown, context?: DebugBundleCaptureContext): void {
30
+ singleton.captureException(error, context);
31
+ },
32
+ captureError(error: unknown, context?: DebugBundleCaptureContext): void {
33
+ singleton.captureError(error, context);
34
+ },
35
+ captureLog(message: string, level?: DebugBundleLogLevel, context?: DebugBundleCaptureContext): void {
36
+ singleton.captureLog(message, level, context);
37
+ },
38
+ captureRequest(request: DebugBundleRequestInfo, response: DebugBundleResponseInfo, context?: DebugBundleCaptureContext): void {
39
+ singleton.captureRequest(request, response, context);
40
+ },
41
+ captureMessage(message: string, level?: DebugBundleLogLevel, context?: DebugBundleCaptureContext): void {
42
+ singleton.captureMessage(message, level, context);
43
+ },
44
+ setContext(key: string, value: unknown): void {
45
+ singleton.setContext(key, value);
46
+ },
47
+ probe(label: string, data: unknown | (() => unknown), options?: DebugBundleProbeOptions): void {
48
+ singleton.probe(label, data, options);
49
+ },
50
+ flush(): Promise<void> {
51
+ return singleton.flush();
52
+ },
53
+ activateProbeTriggerToken(token: string): Promise<boolean> {
54
+ return singleton.activateProbeTriggerToken(token);
55
+ },
56
+ recordBreadcrumb(type: string, data?: Record<string, unknown>): void {
57
+ singleton.recordBreadcrumb(type, data);
58
+ },
59
+ recordScreen(screenName: string, previousScreen?: string | null, source?: string): void {
60
+ singleton.recordScreen(screenName, previousScreen, source);
61
+ },
62
+ get status(): DebugBundleStatus {
63
+ return singleton.status;
64
+ },
65
+ get lastEventAt(): number | null {
66
+ return singleton.lastEventAt;
67
+ }
68
+ };
69
+
70
+ export { createDebugBundleClient, DebugBundleReactNativeClient };
71
+ export { captureDebugBundleConsole };
72
+ export { installDebugBundleErrorHandlers };
73
+ export type {
74
+ DebugBundleCaptureContext,
75
+ DebugBundleClient,
76
+ DebugBundleConfig,
77
+ DebugBundleLogLevel,
78
+ DebugBundleProbeOptions,
79
+ DebugBundleRequestInfo,
80
+ DebugBundleResponseInfo,
81
+ DebugBundleStatus
82
+ };
package/src/native.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type { NativeDebugBundleModule, NativeDebugBundleState } from "./types.js";
2
+
3
+ let overrideModule: NativeDebugBundleModule | null | undefined;
4
+ let cachedReactNativeModule: NativeDebugBundleModule | null | undefined;
5
+
6
+ export function getNativeModule(): NativeDebugBundleModule | null {
7
+ if (overrideModule !== undefined) {
8
+ return overrideModule;
9
+ }
10
+ const globalValue = globalThis as typeof globalThis & {
11
+ __DebugBundleReactNativeModule?: NativeDebugBundleModule;
12
+ nativeFabricUIManager?: unknown;
13
+ };
14
+ if (globalValue.__DebugBundleReactNativeModule) {
15
+ return globalValue.__DebugBundleReactNativeModule;
16
+ }
17
+ if (cachedReactNativeModule !== undefined) {
18
+ return cachedReactNativeModule;
19
+ }
20
+ cachedReactNativeModule = resolveReactNativeModule();
21
+ return cachedReactNativeModule;
22
+ }
23
+
24
+ export function setNativeModuleForTesting(module: NativeDebugBundleModule | null | undefined): void {
25
+ overrideModule = module;
26
+ }
27
+
28
+ export function degradedNativeState(reason: string): NativeDebugBundleState {
29
+ return {
30
+ status: "degraded",
31
+ lastEventAt: null,
32
+ device: null,
33
+ nativeModuleAvailable: false,
34
+ degradedReason: reason
35
+ };
36
+ }
37
+
38
+ export async function safeNativeCall<T>(operation: () => Promise<T> | T, fallback: T): Promise<T> {
39
+ try {
40
+ return await operation();
41
+ } catch {
42
+ return fallback;
43
+ }
44
+ }
45
+
46
+ export function resetNativeModuleCacheForTesting(): void {
47
+ cachedReactNativeModule = undefined;
48
+ }
49
+
50
+ function resolveReactNativeModule(): NativeDebugBundleModule | null {
51
+ const required = requireReactNative();
52
+ const turboModule = required?.TurboModuleRegistry?.get?.("DebugBundleReactNative") as NativeDebugBundleModule | null | undefined;
53
+ if (turboModule) {
54
+ return turboModule;
55
+ }
56
+ return (required?.NativeModules?.DebugBundleReactNative as NativeDebugBundleModule | undefined) ?? null;
57
+ }
58
+
59
+ function requireReactNative():
60
+ | {
61
+ NativeModules?: Record<string, unknown>;
62
+ TurboModuleRegistry?: { get?: (name: string) => unknown };
63
+ }
64
+ | null {
65
+ try {
66
+ const maybeRequire = Function("return typeof require === 'function' ? require : null")() as
67
+ | ((specifier: string) => unknown)
68
+ | null;
69
+ if (!maybeRequire) {
70
+ return null;
71
+ }
72
+ return maybeRequire("react-native") as {
73
+ NativeModules?: Record<string, unknown>;
74
+ TurboModuleRegistry?: { get?: (name: string) => unknown };
75
+ };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
@@ -0,0 +1,37 @@
1
+ import { DebugBundle } from "./index.js";
2
+ import type { DebugBundleClient } from "./types.js";
3
+
4
+ export interface DebugBundleNavigationRef {
5
+ current?: unknown;
6
+ getCurrentRoute?: () => { name?: string; key?: string; params?: unknown } | undefined;
7
+ }
8
+
9
+ let previousRouteName: string | null = null;
10
+
11
+ export function createDebugBundleNavigationRef(): DebugBundleNavigationRef {
12
+ return { current: undefined };
13
+ }
14
+
15
+ export function onDebugBundleNavigationReady(ref: DebugBundleNavigationRef, client: DebugBundleClient = DebugBundle): void {
16
+ previousRouteName = routeName(ref);
17
+ if (previousRouteName) {
18
+ client.recordScreen(previousRouteName, null, "react-navigation");
19
+ }
20
+ }
21
+
22
+ export function onDebugBundleNavigationStateChange(ref: DebugBundleNavigationRef, client: DebugBundleClient = DebugBundle): void {
23
+ const currentRouteName = routeName(ref);
24
+ if (!currentRouteName || currentRouteName === previousRouteName) {
25
+ return;
26
+ }
27
+ client.recordScreen(currentRouteName, previousRouteName, "react-navigation");
28
+ previousRouteName = currentRouteName;
29
+ }
30
+
31
+ function routeName(ref: DebugBundleNavigationRef): string | null {
32
+ const route = ref.getCurrentRoute?.();
33
+ if (!route?.name) {
34
+ return null;
35
+ }
36
+ return route.name.replace(/[^a-zA-Z0-9_.:/-]/g, "_").slice(0, 120);
37
+ }
package/src/network.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { DebugBundle } from "./index.js";
2
+ import { sanitizeHeaders } from "./redaction.js";
3
+ import type { DebugBundleClient, DebugBundleConfig, DebugBundleNetworkFilter } from "./types.js";
4
+
5
+ const TRACE_HEADER = "X-DebugBundle-Trace-Id";
6
+ let fetchRestore: (() => void) | null = null;
7
+ let xhrRestore: (() => void) | null = null;
8
+
9
+ export interface NetworkInstrumentationOptions {
10
+ client?: DebugBundleClient;
11
+ tracePropagationTargets?: string[];
12
+ captureNetwork?: boolean;
13
+ networkFilter?: DebugBundleNetworkFilter;
14
+ headerAllowlist?: string[];
15
+ redactFields?: string[];
16
+ }
17
+
18
+ export function instrumentDebugBundleNetwork(options: NetworkInstrumentationOptions = {}): () => void {
19
+ const client = options.client ?? DebugBundle;
20
+ const fetchTarget = globalThis as typeof globalThis & { fetch?: typeof fetch; XMLHttpRequest?: typeof XMLHttpRequest };
21
+ if (fetchTarget.fetch && !fetchRestore) {
22
+ const originalFetch = fetchTarget.fetch.bind(globalThis);
23
+ fetchTarget.fetch = createInstrumentedFetch(originalFetch, client, options) as typeof fetch;
24
+ fetchRestore = () => {
25
+ fetchTarget.fetch = originalFetch;
26
+ fetchRestore = null;
27
+ };
28
+ }
29
+ if (fetchTarget.XMLHttpRequest && !xhrRestore) {
30
+ xhrRestore = instrumentXMLHttpRequest(fetchTarget.XMLHttpRequest, client, options);
31
+ }
32
+ return () => {
33
+ fetchRestore?.();
34
+ xhrRestore?.();
35
+ };
36
+ }
37
+
38
+ export function createInstrumentedFetch(
39
+ originalFetch: typeof fetch,
40
+ client: Pick<DebugBundleClient, "captureRequest">,
41
+ options: NetworkInstrumentationOptions
42
+ ): typeof fetch {
43
+ return (async (input: RequestInfo | URL, init?: RequestInit) => {
44
+ const startedAt = Date.now();
45
+ const url = requestUrl(input);
46
+ const method = requestMethod(input, init);
47
+ const traceId = shouldInjectTrace(url, options.tracePropagationTargets ?? []) ? existingTraceHeader(input, init) ?? generateTraceId() : null;
48
+ const nextInit = traceId ? withTraceHeader(init, traceId) : init;
49
+ try {
50
+ const response = await originalFetch(input, nextInit);
51
+ maybeCaptureNetwork(client, options, url, method, traceId, response.status, Date.now() - startedAt, headersToRecord(nextInit?.headers));
52
+ return response;
53
+ } catch (error) {
54
+ maybeCaptureNetwork(client, options, url, method, traceId, 0, Date.now() - startedAt, headersToRecord(nextInit?.headers));
55
+ throw error;
56
+ }
57
+ }) as typeof fetch;
58
+ }
59
+
60
+ export function shouldInjectTrace(url: string, targets: string[]): boolean {
61
+ if (isRelativeUrl(url)) {
62
+ return true;
63
+ }
64
+ return targets.some((target) => matchesTarget(url, target));
65
+ }
66
+
67
+ function instrumentXMLHttpRequest(
68
+ OriginalXHR: typeof XMLHttpRequest,
69
+ client: DebugBundleClient,
70
+ options: NetworkInstrumentationOptions
71
+ ): () => void {
72
+ const proto = OriginalXHR.prototype as XMLHttpRequest & {
73
+ __debugBundleWrapped?: boolean;
74
+ __debugBundleUrl?: string;
75
+ __debugBundleMethod?: string;
76
+ __debugBundleStartedAt?: number;
77
+ __debugBundleTraceId?: string | null;
78
+ };
79
+ if (proto.__debugBundleWrapped) {
80
+ return () => undefined;
81
+ }
82
+ const originalOpen = proto.open;
83
+ const originalSend = proto.send;
84
+ const originalSetRequestHeader = proto.setRequestHeader;
85
+ proto.open = function open(method: string, url: string | URL) {
86
+ this.__debugBundleMethod = method;
87
+ this.__debugBundleUrl = String(url);
88
+ return originalOpen.apply(this, arguments as unknown as Parameters<typeof originalOpen>);
89
+ };
90
+ proto.send = function send() {
91
+ const url = this.__debugBundleUrl ?? "";
92
+ const traceId = shouldInjectTrace(url, options.tracePropagationTargets ?? []) ? generateTraceId() : null;
93
+ this.__debugBundleTraceId = traceId;
94
+ this.__debugBundleStartedAt = Date.now();
95
+ if (traceId) {
96
+ originalSetRequestHeader.call(this, TRACE_HEADER, traceId);
97
+ }
98
+ this.addEventListener("loadend", () => {
99
+ maybeCaptureNetwork(
100
+ client,
101
+ options,
102
+ url,
103
+ this.__debugBundleMethod ?? "GET",
104
+ this.__debugBundleTraceId ?? null,
105
+ this.status,
106
+ Date.now() - (this.__debugBundleStartedAt ?? Date.now()),
107
+ traceId ? { [TRACE_HEADER]: traceId } : {}
108
+ );
109
+ });
110
+ return originalSend.apply(this, arguments as unknown as Parameters<typeof originalSend>);
111
+ };
112
+ proto.__debugBundleWrapped = true;
113
+ return () => {
114
+ proto.open = originalOpen;
115
+ proto.send = originalSend;
116
+ proto.__debugBundleWrapped = false;
117
+ };
118
+ }
119
+
120
+ function maybeCaptureNetwork(
121
+ client: Pick<DebugBundleClient, "captureRequest">,
122
+ options: NetworkInstrumentationOptions,
123
+ url: string,
124
+ method: string,
125
+ traceId: string | null,
126
+ statusCode: number,
127
+ durationMillis: number,
128
+ headers: Record<string, string>
129
+ ): void {
130
+ if (options.captureNetwork === false || !matchesNetworkFilter(url, statusCode, durationMillis, options.networkFilter)) {
131
+ return;
132
+ }
133
+ const sanitizeOptions = options.redactFields ? { redactFields: options.redactFields } : {};
134
+ client.captureRequest(
135
+ {
136
+ method,
137
+ url,
138
+ headers: sanitizeHeaders(headers, options.headerAllowlist, sanitizeOptions),
139
+ ...(traceId ? { traceId } : {})
140
+ },
141
+ { statusCode, durationMillis },
142
+ traceId ? { trace_id: traceId } : undefined
143
+ );
144
+ }
145
+
146
+ function matchesNetworkFilter(url: string, statusCode: number, durationMillis: number, filter: DebugBundleNetworkFilter = {}): boolean {
147
+ if (filter.urlDenylist?.some((entry) => url.includes(entry))) {
148
+ return false;
149
+ }
150
+ if (filter.urlAllowlist && filter.urlAllowlist.length > 0 && !filter.urlAllowlist.some((entry) => url.includes(entry))) {
151
+ return false;
152
+ }
153
+ if (filter.statusCodes && filter.statusCodes.length > 0 && !filter.statusCodes.includes(statusCode)) {
154
+ return false;
155
+ }
156
+ if (filter.minResponseTime !== undefined && durationMillis < filter.minResponseTime) {
157
+ return false;
158
+ }
159
+ return statusCode === 0 || statusCode >= 400 || filter.statusCodes !== undefined || filter.minResponseTime !== undefined;
160
+ }
161
+
162
+ function requestUrl(input: RequestInfo | URL): string {
163
+ if (typeof input === "string") {
164
+ return input;
165
+ }
166
+ if (input instanceof URL) {
167
+ return input.toString();
168
+ }
169
+ return "url" in input ? input.url : String(input);
170
+ }
171
+
172
+ function requestMethod(input: RequestInfo | URL, init?: RequestInit): string {
173
+ if (init?.method) {
174
+ return init.method.toUpperCase();
175
+ }
176
+ if (typeof input === "object" && "method" in input && typeof input.method === "string") {
177
+ return input.method.toUpperCase();
178
+ }
179
+ return "GET";
180
+ }
181
+
182
+ function withTraceHeader(init: RequestInit | undefined, traceId: string): RequestInit {
183
+ const headers = new Headers(init?.headers);
184
+ if (!headers.has(TRACE_HEADER)) {
185
+ headers.set(TRACE_HEADER, traceId);
186
+ }
187
+ return { ...init, headers };
188
+ }
189
+
190
+ function existingTraceHeader(input: RequestInfo | URL, init?: RequestInit): string | null {
191
+ const fromInit = init?.headers ? new Headers(init.headers).get(TRACE_HEADER) : null;
192
+ if (fromInit) {
193
+ return fromInit;
194
+ }
195
+ if (typeof input === "object" && "headers" in input) {
196
+ return new Headers(input.headers).get(TRACE_HEADER);
197
+ }
198
+ return null;
199
+ }
200
+
201
+ function headersToRecord(headers: HeadersInit | undefined): Record<string, string> {
202
+ const output: Record<string, string> = {};
203
+ if (!headers) {
204
+ return output;
205
+ }
206
+ new Headers(headers).forEach((value, key) => {
207
+ output[key] = value;
208
+ });
209
+ return output;
210
+ }
211
+
212
+ function isRelativeUrl(url: string): boolean {
213
+ return url.startsWith("/") && !url.startsWith("//");
214
+ }
215
+
216
+ function matchesTarget(url: string, target: string): boolean {
217
+ if (!target || target.includes("*")) {
218
+ return false;
219
+ }
220
+ try {
221
+ const parsedUrl = new URL(url);
222
+ const parsedTarget = new URL(target);
223
+ return parsedUrl.origin === parsedTarget.origin && parsedUrl.pathname.startsWith(parsedTarget.pathname.replace(/\/$/, ""));
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ function generateTraceId(): string {
230
+ if (globalThis.crypto?.randomUUID) {
231
+ return globalThis.crypto.randomUUID();
232
+ }
233
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (marker) => {
234
+ const random = Math.trunc(Math.random() * 16);
235
+ const value = marker === "x" ? random : (random & 0x3) | 0x8;
236
+ return value.toString(16);
237
+ });
238
+ }
239
+
240
+ export type { DebugBundleConfig };
package/src/probes.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { DebugBundle } from "./index.js";
2
+ import type { DebugBundleClient, DebugBundleProbeOptions } from "./types.js";
3
+
4
+ export function probe(label: string, data: unknown | (() => unknown), options?: DebugBundleProbeOptions): void {
5
+ DebugBundle.probe(label, data, options);
6
+ }
7
+
8
+ export function activateDebugBundleProbeTrigger(token: string, client: DebugBundleClient = DebugBundle): boolean {
9
+ void client.activateProbeTriggerToken(token);
10
+ return Boolean(token);
11
+ }
@@ -0,0 +1,14 @@
1
+ declare module "react-native" {
2
+ export interface TurboModule {}
3
+
4
+ export const TurboModuleRegistry: {
5
+ get<T extends TurboModule>(name: string): T | null;
6
+ getEnforcing<T extends TurboModule>(name: string): T;
7
+ };
8
+
9
+ export const NativeModules: Record<string, unknown>;
10
+ }
11
+
12
+ declare module "react-native/Libraries/Types/CodegenTypes" {
13
+ export type UnsafeObject = object;
14
+ }
package/src/react.tsx ADDED
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+ import { DebugBundle } from "./index.js";
3
+ import type { DebugBundleClient } from "./types.js";
4
+
5
+ export interface DebugBundleErrorBoundaryProps {
6
+ children: React.ReactNode;
7
+ client?: DebugBundleClient;
8
+ fallback?: React.ReactNode | ((error: Error) => React.ReactNode);
9
+ context?: Record<string, unknown>;
10
+ rethrow?: boolean;
11
+ }
12
+
13
+ interface DebugBundleErrorBoundaryState {
14
+ error: Error | null;
15
+ }
16
+
17
+ export class DebugBundleErrorBoundary extends React.Component<
18
+ DebugBundleErrorBoundaryProps,
19
+ DebugBundleErrorBoundaryState
20
+ > {
21
+ state: DebugBundleErrorBoundaryState = { error: null };
22
+
23
+ static getDerivedStateFromError(error: Error): DebugBundleErrorBoundaryState {
24
+ return { error };
25
+ }
26
+
27
+ componentDidCatch(error: Error, info: React.ErrorInfo): void {
28
+ const client = this.props.client ?? DebugBundle;
29
+ client.captureException(error, {
30
+ ...this.props.context,
31
+ component_stack: info.componentStack,
32
+ source: "react-error-boundary"
33
+ });
34
+ if (this.props.rethrow) {
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ render(): React.ReactNode {
40
+ if (!this.state.error) {
41
+ return this.props.children;
42
+ }
43
+ if (typeof this.props.fallback === "function") {
44
+ return this.props.fallback(this.state.error);
45
+ }
46
+ return this.props.fallback ?? null;
47
+ }
48
+ }
49
+
50
+ export function useDebugBundleAction(
51
+ actionName: string,
52
+ client: DebugBundleClient = DebugBundle
53
+ ): (data?: Record<string, unknown>) => void {
54
+ return (data?: Record<string, unknown>) => {
55
+ client.recordBreadcrumb("action", {
56
+ action_name: actionName,
57
+ ...(data ?? {})
58
+ });
59
+ };
60
+ }