@logbrew/react-native 0.1.0 → 0.1.2

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 (51) hide show
  1. package/README.md +431 -18
  2. package/apollo.cjs +301 -0
  3. package/apollo.d.cts +70 -0
  4. package/apollo.d.ts +70 -0
  5. package/apollo.js +294 -0
  6. package/examples/apollo-link-spans.mjs +149 -0
  7. package/examples/index.mjs +29 -1
  8. package/examples/instrumentation-kit.mjs +304 -0
  9. package/examples/lifecycle-spans.mjs +131 -0
  10. package/examples/native-bridge-scope.mjs +107 -0
  11. package/examples/navigation-resource-spans.mjs +156 -0
  12. package/examples/package.json +8 -1
  13. package/examples/real-user-smoke.mjs +106 -9
  14. package/examples/resource-fetch-spans.mjs +133 -0
  15. package/examples/trace-correlation.mjs +135 -0
  16. package/global-errors.cjs +366 -0
  17. package/global-errors.d.cts +63 -0
  18. package/global-errors.d.ts +63 -0
  19. package/global-errors.js +7 -0
  20. package/index.cjs +625 -111
  21. package/index.d.cts +236 -0
  22. package/index.d.ts +236 -0
  23. package/index.js +613 -95
  24. package/index.native.js +18 -0
  25. package/instrumentation.cjs +639 -0
  26. package/instrumentation.d.cts +84 -0
  27. package/instrumentation.d.ts +84 -0
  28. package/instrumentation.js +634 -0
  29. package/lifecycle.cjs +129 -0
  30. package/lifecycle.d.cts +50 -0
  31. package/lifecycle.d.ts +50 -0
  32. package/lifecycle.js +121 -0
  33. package/metadata.cjs +175 -0
  34. package/metadata.js +165 -0
  35. package/metro.cjs +310 -0
  36. package/metro.d.cts +37 -0
  37. package/metro.d.ts +37 -0
  38. package/metro.js +6 -0
  39. package/native-bridge.cjs +127 -0
  40. package/native-bridge.d.cts +60 -0
  41. package/native-bridge.d.ts +60 -0
  42. package/native-bridge.js +125 -0
  43. package/package.json +128 -4
  44. package/release-artifacts.cjs +344 -0
  45. package/release-artifacts.d.cts +55 -0
  46. package/release-artifacts.d.ts +53 -0
  47. package/release-artifacts.js +8 -0
  48. package/resource-fetch.cjs +469 -0
  49. package/resource-fetch.d.cts +60 -0
  50. package/resource-fetch.d.ts +60 -0
  51. package/resource-fetch.js +464 -0
package/lifecycle.cjs ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ const { SdkError } = require("@logbrew/sdk");
4
+ const {
5
+ createReactNativeSpanAttributes,
6
+ createReactNativeTraceContext,
7
+ getActiveLogBrewTrace,
8
+ getReactNativeContext
9
+ } = require("./index.cjs");
10
+
11
+ function createReactNativeLifecycleSpanEvent({
12
+ durationMs, fromState, id, idFactory = defaultLifecycleSpanEventId, metadata = {}, name,
13
+ now = () => new Date().toISOString(), platform, appState, screen, sessionId, state, status = "ok",
14
+ timestamp, toState, trace
15
+ } = {}) {
16
+ const safeFromState = normalizeLifecycleState(fromState);
17
+ const safeToState = normalizeLifecycleState(toState ?? state);
18
+ const transition = [safeFromState, safeToState].filter(Boolean).join("->");
19
+ const spanName = name ?? `app_state:${transition || safeToState || safeFromState || "change"}`;
20
+ const activeTrace = trace ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext();
21
+ return {
22
+ id: id ?? idFactory({ fromState: safeFromState, screen, toState: safeToState }),
23
+ timestamp: timestamp ?? now(),
24
+ attributes: createReactNativeSpanAttributes({
25
+ name: spanName,
26
+ status,
27
+ durationMs,
28
+ trace: activeTrace,
29
+ metadata: {
30
+ ...getReactNativeContext({ platform, appState }),
31
+ source: "react-native.lifecycle",
32
+ appState: safeToState,
33
+ durationMs,
34
+ fromAppState: safeFromState,
35
+ screen,
36
+ sessionId,
37
+ toAppState: safeToState,
38
+ ...metadata
39
+ }
40
+ })
41
+ };
42
+ }
43
+
44
+ function captureReactNativeLifecycleSpan(client, input = {}) {
45
+ requireClient(client);
46
+ const event = createReactNativeLifecycleSpanEvent(input);
47
+ client.span(event.id, event.timestamp, event.attributes);
48
+ return event;
49
+ }
50
+
51
+ function createAppStateLifecycleSpanListener(client, appState, {
52
+ captureInitialState = false, metadata = {}, now = () => new Date().toISOString(), nowMs = () => Date.now(),
53
+ onError, platform, screen, sessionId, trace
54
+ } = {}) {
55
+ requireClient(client);
56
+ if (!appState || typeof appState.addEventListener !== "function") {
57
+ throw new SdkError("configuration_error", "createAppStateLifecycleSpanListener requires AppState.addEventListener");
58
+ }
59
+
60
+ let previousState = normalizeLifecycleState(appState.currentState);
61
+ let previousChangedAtMs = nowMs();
62
+ if (captureInitialState && previousState !== undefined) {
63
+ captureReactNativeLifecycleSpan(client, {
64
+ appState, metadata, now, platform, screen, sessionId, toState: previousState, trace
65
+ });
66
+ }
67
+
68
+ const subscription = appState.addEventListener("change", (nextState) => {
69
+ try {
70
+ const safeNextState = normalizeLifecycleState(nextState);
71
+ if (safeNextState === undefined) {
72
+ return;
73
+ }
74
+ const changedAtMs = nowMs();
75
+ const durationMs = previousState === undefined ? undefined : Math.max(0, changedAtMs - previousChangedAtMs);
76
+ captureReactNativeLifecycleSpan(client, {
77
+ appState, durationMs, fromState: previousState, metadata, now, platform, screen, sessionId,
78
+ timestamp: now(), toState: safeNextState, trace
79
+ });
80
+ previousState = safeNextState;
81
+ previousChangedAtMs = changedAtMs;
82
+ } catch (error) {
83
+ if (typeof onError === "function") {
84
+ onError(error);
85
+ } else {
86
+ throw error;
87
+ }
88
+ }
89
+ });
90
+
91
+ return subscriptionRemover(subscription);
92
+ }
93
+
94
+ function requireClient(client) {
95
+ if (!client) {
96
+ throw new SdkError("configuration_error", "LogBrew React Native lifecycle helpers require a client");
97
+ }
98
+ }
99
+
100
+ function defaultLifecycleSpanEventId({ fromState, screen, toState }) {
101
+ return `evt_native_lifecycle_${slugify([screen, fromState, toState].filter(Boolean).join("_") || "app_state")}`;
102
+ }
103
+
104
+ function normalizeLifecycleState(state) {
105
+ return typeof state === "string" && state.trim() !== "" ? state.trim() : undefined;
106
+ }
107
+
108
+ function subscriptionRemover(subscription) {
109
+ if (typeof subscription === "function") {
110
+ return subscription;
111
+ }
112
+ if (subscription && typeof subscription.remove === "function") {
113
+ return () => subscription.remove();
114
+ }
115
+ return () => {};
116
+ }
117
+
118
+ function slugify(value) {
119
+ return String(value)
120
+ .toLowerCase()
121
+ .replace(/[^a-z0-9]+/g, "_")
122
+ .replace(/^_+|_+$/g, "") || "event";
123
+ }
124
+
125
+ module.exports = {
126
+ captureReactNativeLifecycleSpan,
127
+ createAppStateLifecycleSpanListener,
128
+ createReactNativeLifecycleSpanEvent
129
+ };
@@ -0,0 +1,50 @@
1
+ import type { LogBrewClient, Metadata, SpanAttributes } from "@logbrew/sdk";
2
+ import type {
3
+ ReactNativeAppStateLike,
4
+ ReactNativeContextOptions,
5
+ ReactNativeSpanEvent
6
+ } from "./index.cjs";
7
+
8
+ export type ReactNativeLifecycleSpanInput = ReactNativeContextOptions & {
9
+ durationMs?: number;
10
+ fromState?: string;
11
+ id?: string;
12
+ idFactory?: (context: ReactNativeLifecycleSpanIdFactoryContext) => string;
13
+ name?: string;
14
+ now?: () => string;
15
+ screen?: string;
16
+ sessionId?: string;
17
+ state?: string;
18
+ status?: SpanAttributes["status"];
19
+ timestamp?: string;
20
+ toState?: string;
21
+ };
22
+
23
+ export type ReactNativeLifecycleSpanIdFactoryContext = {
24
+ fromState?: string;
25
+ screen?: string;
26
+ toState?: string;
27
+ };
28
+
29
+ export type AppStateLifecycleSpanListenerOptions = ReactNativeContextOptions & {
30
+ captureInitialState?: boolean;
31
+ metadata?: Metadata;
32
+ now?: () => string;
33
+ nowMs?: () => number;
34
+ onError?: (error: unknown) => void;
35
+ screen?: string;
36
+ sessionId?: string;
37
+ };
38
+
39
+ export declare function createReactNativeLifecycleSpanEvent(
40
+ input?: ReactNativeLifecycleSpanInput
41
+ ): ReactNativeSpanEvent;
42
+ export declare function captureReactNativeLifecycleSpan(
43
+ client: LogBrewClient,
44
+ input?: ReactNativeLifecycleSpanInput
45
+ ): ReactNativeSpanEvent;
46
+ export declare function createAppStateLifecycleSpanListener(
47
+ client: LogBrewClient,
48
+ appState: ReactNativeAppStateLike,
49
+ options?: AppStateLifecycleSpanListenerOptions
50
+ ): () => void;
package/lifecycle.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { LogBrewClient, Metadata, SpanAttributes } from "@logbrew/sdk";
2
+ import type {
3
+ ReactNativeAppStateLike,
4
+ ReactNativeContextOptions,
5
+ ReactNativeSpanEvent
6
+ } from "./index.js";
7
+
8
+ export type ReactNativeLifecycleSpanInput = ReactNativeContextOptions & {
9
+ durationMs?: number;
10
+ fromState?: string;
11
+ id?: string;
12
+ idFactory?: (context: ReactNativeLifecycleSpanIdFactoryContext) => string;
13
+ name?: string;
14
+ now?: () => string;
15
+ screen?: string;
16
+ sessionId?: string;
17
+ state?: string;
18
+ status?: SpanAttributes["status"];
19
+ timestamp?: string;
20
+ toState?: string;
21
+ };
22
+
23
+ export type ReactNativeLifecycleSpanIdFactoryContext = {
24
+ fromState?: string;
25
+ screen?: string;
26
+ toState?: string;
27
+ };
28
+
29
+ export type AppStateLifecycleSpanListenerOptions = ReactNativeContextOptions & {
30
+ captureInitialState?: boolean;
31
+ metadata?: Metadata;
32
+ now?: () => string;
33
+ nowMs?: () => number;
34
+ onError?: (error: unknown) => void;
35
+ screen?: string;
36
+ sessionId?: string;
37
+ };
38
+
39
+ export declare function createReactNativeLifecycleSpanEvent(
40
+ input?: ReactNativeLifecycleSpanInput
41
+ ): ReactNativeSpanEvent;
42
+ export declare function captureReactNativeLifecycleSpan(
43
+ client: LogBrewClient,
44
+ input?: ReactNativeLifecycleSpanInput
45
+ ): ReactNativeSpanEvent;
46
+ export declare function createAppStateLifecycleSpanListener(
47
+ client: LogBrewClient,
48
+ appState: ReactNativeAppStateLike,
49
+ options?: AppStateLifecycleSpanListenerOptions
50
+ ): () => void;
package/lifecycle.js ADDED
@@ -0,0 +1,121 @@
1
+ import { SdkError } from "@logbrew/sdk";
2
+ import {
3
+ createReactNativeSpanAttributes,
4
+ createReactNativeTraceContext,
5
+ getActiveLogBrewTrace,
6
+ getReactNativeContext
7
+ } from "./index.js";
8
+
9
+ export function createReactNativeLifecycleSpanEvent({
10
+ durationMs, fromState, id, idFactory = defaultLifecycleSpanEventId, metadata = {}, name,
11
+ now = () => new Date().toISOString(), platform, appState, screen, sessionId, state, status = "ok",
12
+ timestamp, toState, trace
13
+ } = {}) {
14
+ const safeFromState = normalizeLifecycleState(fromState);
15
+ const safeToState = normalizeLifecycleState(toState ?? state);
16
+ const transition = [safeFromState, safeToState].filter(Boolean).join("->");
17
+ const spanName = name ?? `app_state:${transition || safeToState || safeFromState || "change"}`;
18
+ const activeTrace = trace ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext();
19
+ return {
20
+ id: id ?? idFactory({ fromState: safeFromState, screen, toState: safeToState }),
21
+ timestamp: timestamp ?? now(),
22
+ attributes: createReactNativeSpanAttributes({
23
+ name: spanName,
24
+ status,
25
+ durationMs,
26
+ trace: activeTrace,
27
+ metadata: {
28
+ ...getReactNativeContext({ platform, appState }),
29
+ source: "react-native.lifecycle",
30
+ appState: safeToState,
31
+ durationMs,
32
+ fromAppState: safeFromState,
33
+ screen,
34
+ sessionId,
35
+ toAppState: safeToState,
36
+ ...metadata
37
+ }
38
+ })
39
+ };
40
+ }
41
+
42
+ export function captureReactNativeLifecycleSpan(client, input = {}) {
43
+ requireClient(client);
44
+ const event = createReactNativeLifecycleSpanEvent(input);
45
+ client.span(event.id, event.timestamp, event.attributes);
46
+ return event;
47
+ }
48
+
49
+ export function createAppStateLifecycleSpanListener(client, appState, {
50
+ captureInitialState = false, metadata = {}, now = () => new Date().toISOString(), nowMs = () => Date.now(),
51
+ onError, platform, screen, sessionId, trace
52
+ } = {}) {
53
+ requireClient(client);
54
+ if (!appState || typeof appState.addEventListener !== "function") {
55
+ throw new SdkError("configuration_error", "createAppStateLifecycleSpanListener requires AppState.addEventListener");
56
+ }
57
+
58
+ let previousState = normalizeLifecycleState(appState.currentState);
59
+ let previousChangedAtMs = nowMs();
60
+ if (captureInitialState && previousState !== undefined) {
61
+ captureReactNativeLifecycleSpan(client, {
62
+ appState, metadata, now, platform, screen, sessionId, toState: previousState, trace
63
+ });
64
+ }
65
+
66
+ const subscription = appState.addEventListener("change", (nextState) => {
67
+ try {
68
+ const safeNextState = normalizeLifecycleState(nextState);
69
+ if (safeNextState === undefined) {
70
+ return;
71
+ }
72
+ const changedAtMs = nowMs();
73
+ const durationMs = previousState === undefined ? undefined : Math.max(0, changedAtMs - previousChangedAtMs);
74
+ captureReactNativeLifecycleSpan(client, {
75
+ appState, durationMs, fromState: previousState, metadata, now, platform, screen, sessionId,
76
+ timestamp: now(), toState: safeNextState, trace
77
+ });
78
+ previousState = safeNextState;
79
+ previousChangedAtMs = changedAtMs;
80
+ } catch (error) {
81
+ if (typeof onError === "function") {
82
+ onError(error);
83
+ } else {
84
+ throw error;
85
+ }
86
+ }
87
+ });
88
+
89
+ return subscriptionRemover(subscription);
90
+ }
91
+
92
+ function requireClient(client) {
93
+ if (!client) {
94
+ throw new SdkError("configuration_error", "LogBrew React Native lifecycle helpers require a client");
95
+ }
96
+ }
97
+
98
+ function defaultLifecycleSpanEventId({ fromState, screen, toState }) {
99
+ return `evt_native_lifecycle_${slugify([screen, fromState, toState].filter(Boolean).join("_") || "app_state")}`;
100
+ }
101
+
102
+ function normalizeLifecycleState(state) {
103
+ return typeof state === "string" && state.trim() !== "" ? state.trim() : undefined;
104
+ }
105
+
106
+ function subscriptionRemover(subscription) {
107
+ if (typeof subscription === "function") {
108
+ return subscription;
109
+ }
110
+ if (subscription && typeof subscription.remove === "function") {
111
+ return () => subscription.remove();
112
+ }
113
+ return () => {};
114
+ }
115
+
116
+ function slugify(value) {
117
+ return String(value)
118
+ .toLowerCase()
119
+ .replace(/[^a-z0-9]+/g, "_")
120
+ .replace(/^_+|_+$/g, "") || "event";
121
+ }
package/metadata.cjs ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+
3
+ const SENSITIVE_METADATA_FACTORY_KEY_RE = new RegExp([
4
+ "body",
5
+ "payload",
6
+ "variable",
7
+ "header",
8
+ "authorization",
9
+ "cookie",
10
+ "to\u006ben",
11
+ "sec\u0072et",
12
+ "pass\u0077ord"
13
+ ].join("|"), "u");
14
+ const REACT_NATIVE_DEBUG_ID_REGISTRY = Symbol.for("@logbrew/react-native/debug-ids");
15
+ const SAFE_RELEASE_ARTIFACT_DEBUG_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
16
+ const MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES = 64;
17
+ const MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES = 128;
18
+
19
+ function createSafeReactNativeMetadata(metadata, metadataFactory, context) {
20
+ if (typeof metadataFactory !== "function") {
21
+ return metadata;
22
+ }
23
+ return {
24
+ ...metadata,
25
+ ...safeReactNativeMetadataFactoryResult(metadataFactory(context))
26
+ };
27
+ }
28
+
29
+ function safeReactNativeMetadataFactoryResult(candidate) {
30
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
31
+ return {};
32
+ }
33
+ const metadata = {};
34
+ for (const [key, value] of Object.entries(candidate)) {
35
+ if (isSensitiveMetadataKey(key)) {
36
+ continue;
37
+ }
38
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
39
+ metadata[key] = value;
40
+ }
41
+ }
42
+ return metadata;
43
+ }
44
+
45
+ function sanitizeReactNativeIssueMetadata(metadata, compactMetadata) {
46
+ const next = { ...metadata };
47
+ for (const key of ["errorFrameFile", "releaseArtifactCodeFile"]) {
48
+ const path = reactNativeCodePath(next[key]);
49
+ if (path) {
50
+ next[key] = path;
51
+ }
52
+ }
53
+ const match = typeof next.issueGroupingKey === "string" ? next.issueGroupingKey.match(/^([^:]+):([^:]+):(.+)$/u) : null;
54
+ const path = match ? reactNativeCodePath(match[3]) : undefined;
55
+ if (path) {
56
+ next.issueGroupingKey = `${match[1]}:${match[2]}:${path}`;
57
+ }
58
+ return compactMetadata(next);
59
+ }
60
+
61
+ function sanitizeReactNativeIssueStackFrames(stackFrames) {
62
+ if (!Array.isArray(stackFrames)) {
63
+ return undefined;
64
+ }
65
+ return stackFrames.map((frame) => ({
66
+ ...frame,
67
+ filename: reactNativeCodePath(frame.filename) ?? frame.filename
68
+ }));
69
+ }
70
+
71
+ function runtimeReactNativeDebugIdMap() {
72
+ try {
73
+ const registry = globalThis?.[REACT_NATIVE_DEBUG_ID_REGISTRY];
74
+ if (!registry || Array.isArray(registry) || typeof registry !== "object") {
75
+ return undefined;
76
+ }
77
+ const entries = Object.entries(registry);
78
+ if (entries.length === 0 || entries.length > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES) {
79
+ return undefined;
80
+ }
81
+ const debugIdMap = Object.create(null);
82
+ let frameCount = 0;
83
+ for (const [stack, debugId] of entries) {
84
+ if (typeof debugId !== "string" || !SAFE_RELEASE_ARTIFACT_DEBUG_ID.test(debugId)) {
85
+ return undefined;
86
+ }
87
+ const normalizedDebugId = debugId.toLowerCase();
88
+ let stackFrameCount = 0;
89
+ for (const line of stack.split(/\r?\n/u)) {
90
+ const filename = runtimeStackFrameFilename(line);
91
+ if (!filename) {
92
+ continue;
93
+ }
94
+ frameCount += 1;
95
+ stackFrameCount += 1;
96
+ if (frameCount > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES) {
97
+ return undefined;
98
+ }
99
+ const existingDebugId = debugIdMap[filename];
100
+ if (existingDebugId && existingDebugId !== normalizedDebugId) {
101
+ return undefined;
102
+ }
103
+ debugIdMap[filename] = normalizedDebugId;
104
+ }
105
+ if (stackFrameCount === 0) {
106
+ return undefined;
107
+ }
108
+ }
109
+ return frameCount > 0 ? debugIdMap : undefined;
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ function isSensitiveMetadataKey(key) {
116
+ return SENSITIVE_METADATA_FACTORY_KEY_RE.test(String(key).toLowerCase());
117
+ }
118
+
119
+ function runtimeStackFrameFilename(rawLine) {
120
+ let location = typeof rawLine === "string" ? rawLine.trim() : "";
121
+ if (!location) {
122
+ return undefined;
123
+ }
124
+ if (location.startsWith("at ")) {
125
+ location = location.slice(3).trim();
126
+ if (location.endsWith(")") && location.includes("(")) {
127
+ location = location.slice(location.lastIndexOf("(") + 1, -1);
128
+ }
129
+ } else if (location.includes("@")) {
130
+ location = location.slice(location.lastIndexOf("@") + 1);
131
+ }
132
+ const parts = location.split(":");
133
+ if (parts.length < 3) {
134
+ return undefined;
135
+ }
136
+ const columnText = parts.pop();
137
+ const lineText = parts.pop();
138
+ const filename = parts.join(":").trim();
139
+ if (!/^[1-9]\d*$/u.test(lineText) || !/^[1-9]\d*$/u.test(columnText)) {
140
+ return undefined;
141
+ }
142
+ const line = Number(lineText);
143
+ const column = Number(columnText);
144
+ return Number.isSafeInteger(line) && Number.isSafeInteger(column) && filename ? filename : undefined;
145
+ }
146
+
147
+ function reactNativeCodePath(value) {
148
+ if (typeof value !== "string" || value.trim() === "") {
149
+ return undefined;
150
+ }
151
+ let path = value.trim();
152
+ const URLConstructor = globalThis.URL;
153
+ if (typeof URLConstructor === "function") {
154
+ try {
155
+ path = new URLConstructor(path).pathname || path;
156
+ } catch {
157
+ path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
158
+ }
159
+ } else {
160
+ path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
161
+ }
162
+ if (/^[A-Za-z]:\//u.test(path) || /^\/(?:Users|home|private|tmp|var)\//u.test(path)) {
163
+ path = path.replace(/\/+$/u, "");
164
+ return path.slice(path.lastIndexOf("/") + 1) || undefined;
165
+ }
166
+ return path || undefined;
167
+ }
168
+
169
+ module.exports = {
170
+ createSafeReactNativeMetadata,
171
+ runtimeReactNativeDebugIdMap,
172
+ safeReactNativeMetadataFactoryResult,
173
+ sanitizeReactNativeIssueMetadata,
174
+ sanitizeReactNativeIssueStackFrames
175
+ };
package/metadata.js ADDED
@@ -0,0 +1,165 @@
1
+ const SENSITIVE_METADATA_FACTORY_KEY_RE = new RegExp([
2
+ "body",
3
+ "payload",
4
+ "variable",
5
+ "header",
6
+ "authorization",
7
+ "cookie",
8
+ "to\u006ben",
9
+ "sec\u0072et",
10
+ "pass\u0077ord"
11
+ ].join("|"), "u");
12
+ const REACT_NATIVE_DEBUG_ID_REGISTRY = Symbol.for("@logbrew/react-native/debug-ids");
13
+ const SAFE_RELEASE_ARTIFACT_DEBUG_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
14
+ const MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES = 64;
15
+ const MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES = 128;
16
+
17
+ export function createSafeReactNativeMetadata(metadata, metadataFactory, context) {
18
+ if (typeof metadataFactory !== "function") {
19
+ return metadata;
20
+ }
21
+ return {
22
+ ...metadata,
23
+ ...safeReactNativeMetadataFactoryResult(metadataFactory(context))
24
+ };
25
+ }
26
+
27
+ export function safeReactNativeMetadataFactoryResult(candidate) {
28
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
29
+ return {};
30
+ }
31
+ const metadata = {};
32
+ for (const [key, value] of Object.entries(candidate)) {
33
+ if (isSensitiveMetadataKey(key)) {
34
+ continue;
35
+ }
36
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
37
+ metadata[key] = value;
38
+ }
39
+ }
40
+ return metadata;
41
+ }
42
+
43
+ export function sanitizeReactNativeIssueMetadata(metadata, compactMetadata) {
44
+ const next = { ...metadata };
45
+ for (const key of ["errorFrameFile", "releaseArtifactCodeFile"]) {
46
+ const path = reactNativeCodePath(next[key]);
47
+ if (path) {
48
+ next[key] = path;
49
+ }
50
+ }
51
+ const match = typeof next.issueGroupingKey === "string" ? next.issueGroupingKey.match(/^([^:]+):([^:]+):(.+)$/u) : null;
52
+ const path = match ? reactNativeCodePath(match[3]) : undefined;
53
+ if (path) {
54
+ next.issueGroupingKey = `${match[1]}:${match[2]}:${path}`;
55
+ }
56
+ return compactMetadata(next);
57
+ }
58
+
59
+ export function sanitizeReactNativeIssueStackFrames(stackFrames) {
60
+ if (!Array.isArray(stackFrames)) {
61
+ return undefined;
62
+ }
63
+ return stackFrames.map((frame) => ({
64
+ ...frame,
65
+ filename: reactNativeCodePath(frame.filename) ?? frame.filename
66
+ }));
67
+ }
68
+
69
+ export function runtimeReactNativeDebugIdMap() {
70
+ try {
71
+ const registry = globalThis?.[REACT_NATIVE_DEBUG_ID_REGISTRY];
72
+ if (!registry || Array.isArray(registry) || typeof registry !== "object") {
73
+ return undefined;
74
+ }
75
+ const entries = Object.entries(registry);
76
+ if (entries.length === 0 || entries.length > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES) {
77
+ return undefined;
78
+ }
79
+ const debugIdMap = Object.create(null);
80
+ let frameCount = 0;
81
+ for (const [stack, debugId] of entries) {
82
+ if (typeof debugId !== "string" || !SAFE_RELEASE_ARTIFACT_DEBUG_ID.test(debugId)) {
83
+ return undefined;
84
+ }
85
+ const normalizedDebugId = debugId.toLowerCase();
86
+ let stackFrameCount = 0;
87
+ for (const line of stack.split(/\r?\n/u)) {
88
+ const filename = runtimeStackFrameFilename(line);
89
+ if (!filename) {
90
+ continue;
91
+ }
92
+ frameCount += 1;
93
+ stackFrameCount += 1;
94
+ if (frameCount > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES) {
95
+ return undefined;
96
+ }
97
+ const existingDebugId = debugIdMap[filename];
98
+ if (existingDebugId && existingDebugId !== normalizedDebugId) {
99
+ return undefined;
100
+ }
101
+ debugIdMap[filename] = normalizedDebugId;
102
+ }
103
+ if (stackFrameCount === 0) {
104
+ return undefined;
105
+ }
106
+ }
107
+ return frameCount > 0 ? debugIdMap : undefined;
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ function isSensitiveMetadataKey(key) {
114
+ return SENSITIVE_METADATA_FACTORY_KEY_RE.test(String(key).toLowerCase());
115
+ }
116
+
117
+ function runtimeStackFrameFilename(rawLine) {
118
+ let location = typeof rawLine === "string" ? rawLine.trim() : "";
119
+ if (!location) {
120
+ return undefined;
121
+ }
122
+ if (location.startsWith("at ")) {
123
+ location = location.slice(3).trim();
124
+ if (location.endsWith(")") && location.includes("(")) {
125
+ location = location.slice(location.lastIndexOf("(") + 1, -1);
126
+ }
127
+ } else if (location.includes("@")) {
128
+ location = location.slice(location.lastIndexOf("@") + 1);
129
+ }
130
+ const parts = location.split(":");
131
+ if (parts.length < 3) {
132
+ return undefined;
133
+ }
134
+ const columnText = parts.pop();
135
+ const lineText = parts.pop();
136
+ const filename = parts.join(":").trim();
137
+ if (!/^[1-9]\d*$/u.test(lineText) || !/^[1-9]\d*$/u.test(columnText)) {
138
+ return undefined;
139
+ }
140
+ const line = Number(lineText);
141
+ const column = Number(columnText);
142
+ return Number.isSafeInteger(line) && Number.isSafeInteger(column) && filename ? filename : undefined;
143
+ }
144
+
145
+ function reactNativeCodePath(value) {
146
+ if (typeof value !== "string" || value.trim() === "") {
147
+ return undefined;
148
+ }
149
+ let path = value.trim();
150
+ const URLConstructor = globalThis.URL;
151
+ if (typeof URLConstructor === "function") {
152
+ try {
153
+ path = new URLConstructor(path).pathname || path;
154
+ } catch {
155
+ path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
156
+ }
157
+ } else {
158
+ path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
159
+ }
160
+ if (/^[A-Za-z]:\//u.test(path) || /^\/(?:Users|home|private|tmp|var)\//u.test(path)) {
161
+ path = path.replace(/\/+$/u, "");
162
+ return path.slice(path.lastIndexOf("/") + 1) || undefined;
163
+ }
164
+ return path || undefined;
165
+ }