@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.
- package/README.md +431 -18
- package/apollo.cjs +301 -0
- package/apollo.d.cts +70 -0
- package/apollo.d.ts +70 -0
- package/apollo.js +294 -0
- package/examples/apollo-link-spans.mjs +149 -0
- package/examples/index.mjs +29 -1
- package/examples/instrumentation-kit.mjs +304 -0
- package/examples/lifecycle-spans.mjs +131 -0
- package/examples/native-bridge-scope.mjs +107 -0
- package/examples/navigation-resource-spans.mjs +156 -0
- package/examples/package.json +8 -1
- package/examples/real-user-smoke.mjs +106 -9
- package/examples/resource-fetch-spans.mjs +133 -0
- package/examples/trace-correlation.mjs +135 -0
- package/global-errors.cjs +366 -0
- package/global-errors.d.cts +63 -0
- package/global-errors.d.ts +63 -0
- package/global-errors.js +7 -0
- package/index.cjs +625 -111
- package/index.d.cts +236 -0
- package/index.d.ts +236 -0
- package/index.js +613 -95
- package/index.native.js +18 -0
- package/instrumentation.cjs +639 -0
- package/instrumentation.d.cts +84 -0
- package/instrumentation.d.ts +84 -0
- package/instrumentation.js +634 -0
- package/lifecycle.cjs +129 -0
- package/lifecycle.d.cts +50 -0
- package/lifecycle.d.ts +50 -0
- package/lifecycle.js +121 -0
- package/metadata.cjs +175 -0
- package/metadata.js +165 -0
- package/metro.cjs +310 -0
- package/metro.d.cts +37 -0
- package/metro.d.ts +37 -0
- package/metro.js +6 -0
- package/native-bridge.cjs +127 -0
- package/native-bridge.d.cts +60 -0
- package/native-bridge.d.ts +60 -0
- package/native-bridge.js +125 -0
- package/package.json +128 -4
- package/release-artifacts.cjs +344 -0
- package/release-artifacts.d.cts +55 -0
- package/release-artifacts.d.ts +53 -0
- package/release-artifacts.js +8 -0
- package/resource-fetch.cjs +469 -0
- package/resource-fetch.d.cts +60 -0
- package/resource-fetch.d.ts +60 -0
- 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
|
+
};
|
package/lifecycle.d.cts
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.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
|
+
}
|