@logbrew/react-native 0.1.20 → 0.1.21
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 +10 -0
- package/apple-native-diagnostics.js +17 -3
- package/index.cjs +1 -1
- package/index.native.js +57 -4
- package/instrumentation.js +3 -632
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +5 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +26 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +3 -2
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +4 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +19 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +101 -4
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +23 -5
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -5
- package/metadata.js +10 -181
- package/package.json +2 -2
- package/src/NativeLogBrewAppleDiagnostics.ts +3 -0
package/README.md
CHANGED
|
@@ -347,6 +347,16 @@ clear it with `setLogBrewAppleNativeCrashContext(null)` on logout or session
|
|
|
347
347
|
end. The crash report keeps one atomic snapshot from the crashed process, so a
|
|
348
348
|
later app launch cannot replace it. Session and subject values must be opaque app-owned identifiers made from ASCII letters, numbers, `_`, or `-`. Do not use names, email addresses, IP addresses, or device identifiers. Resource context, tags, arbitrary fields, and values over the fixed 1 KiB snapshot limit fail before storage.
|
|
349
349
|
|
|
350
|
+
The iOS `createLogBrewReactNativeClient()` entry also mirrors its validated
|
|
351
|
+
breadcrumb history into native crash capture. Install Apple diagnostics first,
|
|
352
|
+
then use the normal screen, app-state, action, network, or `addBreadcrumb()`
|
|
353
|
+
helpers. A fatal crash retains the newest complete entries in oldest-to-newest
|
|
354
|
+
order. The snapshot keeps at most 64 entries and 64 KiB, drops oldest entries
|
|
355
|
+
when either limit is reached, and reports truncation. `clearBreadcrumbs()`
|
|
356
|
+
clears both the JavaScript history and its native crash snapshot. Missing,
|
|
357
|
+
corrupt, and captured breadcrumb state remains explicit on replay; corrupt
|
|
358
|
+
optional context never discards the crash.
|
|
359
|
+
|
|
350
360
|
Installation creates app-private, data-protected storage that iOS excludes from
|
|
351
361
|
device data archives. Pending reports are partitioned by project, so changing
|
|
352
362
|
the configured project does not replay an earlier project's records with the
|
|
@@ -48,17 +48,31 @@ export function setLogBrewAppleNativeCrashContext(context) {
|
|
|
48
48
|
requireApplePlatform();
|
|
49
49
|
const nativeModule = requireNativeModule();
|
|
50
50
|
const payload = context === null ? null : normalizeCorrelationContext(context);
|
|
51
|
-
|
|
51
|
+
return updateNativeSnapshot(nativeModule, "setNativeDiagnosticsContext", payload, "context");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function syncLogBrewAppleNativeCrashBreadcrumbs(snapshot) {
|
|
55
|
+
requireApplePlatform();
|
|
56
|
+
return updateNativeSnapshot(
|
|
57
|
+
requireNativeModule(),
|
|
58
|
+
"setNativeDiagnosticsBreadcrumbs",
|
|
59
|
+
snapshot,
|
|
60
|
+
"breadcrumbs"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function updateNativeSnapshot(nativeModule, method, payload, label) {
|
|
65
|
+
const result = callNative(nativeModule, method, payload);
|
|
52
66
|
const expectedStatus = payload === null ? "cleared" : "updated";
|
|
53
67
|
if (isErrorResult(result)) {
|
|
54
|
-
throw new SdkError(result.code, `LogBrew Apple native diagnostics
|
|
68
|
+
throw new SdkError(result.code, `LogBrew Apple native diagnostics ${label} failed with ${result.code}`);
|
|
55
69
|
}
|
|
56
70
|
if (!isPlainObject(result)
|
|
57
71
|
|| Object.keys(result).length !== 1
|
|
58
72
|
|| result.status !== expectedStatus) {
|
|
59
73
|
throw new SdkError(
|
|
60
74
|
"native_diagnostics_invalid_response",
|
|
61
|
-
|
|
75
|
+
`LogBrew Apple native diagnostics ${label} returned an invalid response`
|
|
62
76
|
);
|
|
63
77
|
}
|
|
64
78
|
return Object.freeze({ status: expectedStatus });
|
package/index.cjs
CHANGED
|
@@ -16,7 +16,7 @@ const {
|
|
|
16
16
|
} = require("./metadata.cjs");
|
|
17
17
|
|
|
18
18
|
const DEFAULT_SDK_NAME = "logbrew-react-native";
|
|
19
|
-
const DEFAULT_SDK_VERSION = "0.1.
|
|
19
|
+
const DEFAULT_SDK_VERSION = "0.1.21";
|
|
20
20
|
const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
|
|
21
21
|
const MAX_ACTION_NAME_LENGTH = 64;
|
|
22
22
|
const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
|
package/index.native.js
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
getLogBrewAppleNativeDiagnosticsStatus,
|
|
15
15
|
installLogBrewAppleNativeDiagnostics,
|
|
16
16
|
replayLogBrewAppleNativeDiagnostics,
|
|
17
|
-
setLogBrewAppleNativeCrashContext
|
|
17
|
+
setLogBrewAppleNativeCrashContext,
|
|
18
|
+
syncLogBrewAppleNativeCrashBreadcrumbs
|
|
18
19
|
} from "./apple-native-diagnostics.js";
|
|
19
20
|
import {
|
|
20
21
|
purgeReactNativePersistentQueue,
|
|
@@ -50,7 +51,7 @@ export function createLogBrewReactNativeClient(config = {}) {
|
|
|
50
51
|
) && input.persistentQueue !== undefined;
|
|
51
52
|
const authKey = clientKey ?? apiKey;
|
|
52
53
|
if (typeof authKey !== "string" || authKey.trim() === "") {
|
|
53
|
-
return createPlatformNeutralClient(input);
|
|
54
|
+
return bindAppleNativeCrashBreadcrumbs(createPlatformNeutralClient(input));
|
|
54
55
|
}
|
|
55
56
|
const resolved = resolveReactNativePersistentEventStore({
|
|
56
57
|
authKey,
|
|
@@ -61,20 +62,72 @@ export function createLogBrewReactNativeClient(config = {}) {
|
|
|
61
62
|
hasExplicitPersistentQueue
|
|
62
63
|
});
|
|
63
64
|
try {
|
|
64
|
-
return createPlatformNeutralClient({
|
|
65
|
+
return bindAppleNativeCrashBreadcrumbs(createPlatformNeutralClient({
|
|
65
66
|
...forwarded,
|
|
66
67
|
apiKey,
|
|
67
68
|
clientKey,
|
|
68
69
|
eventStore: resolved.eventStore,
|
|
69
70
|
maxQueueBytes,
|
|
70
71
|
maxQueueSize
|
|
71
|
-
});
|
|
72
|
+
}));
|
|
72
73
|
} catch (error) {
|
|
73
74
|
resolved.abort();
|
|
74
75
|
throw error;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function bindAppleNativeCrashBreadcrumbs(client) {
|
|
80
|
+
if (Platform?.OS !== "ios") {
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
83
|
+
const addBreadcrumb = client.addBreadcrumb.bind(client);
|
|
84
|
+
const clearBreadcrumbs = client.clearBreadcrumbs.bind(client);
|
|
85
|
+
client.addBreadcrumb = (...args) => updateAppleNativeBreadcrumbs(
|
|
86
|
+
client, () => addBreadcrumb(...args)
|
|
87
|
+
);
|
|
88
|
+
client.clearBreadcrumbs = () => updateAppleNativeBreadcrumbs(client, clearBreadcrumbs);
|
|
89
|
+
syncAppleNativeCrashBreadcrumbs(client);
|
|
90
|
+
return client;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function updateAppleNativeBreadcrumbs(client, update) {
|
|
94
|
+
const previous = [client.issueBreadcrumbs.slice(), client.issueBreadcrumbsTruncated];
|
|
95
|
+
const result = update();
|
|
96
|
+
try {
|
|
97
|
+
syncAppleNativeCrashBreadcrumbs(client);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
restoreBreadcrumbs(client, previous);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restoreBreadcrumbs(client, [breadcrumbs, truncated]) {
|
|
106
|
+
client.issueBreadcrumbs.splice(0, client.issueBreadcrumbs.length, ...breadcrumbs);
|
|
107
|
+
client.issueBreadcrumbsTruncated = truncated;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function syncAppleNativeCrashBreadcrumbs(client) {
|
|
111
|
+
const snapshot = client.issueBreadcrumbs.length === 0
|
|
112
|
+
? null
|
|
113
|
+
: {
|
|
114
|
+
breadcrumbs: client.issueBreadcrumbs.map(({ data, ...breadcrumb }) => ({
|
|
115
|
+
...breadcrumb,
|
|
116
|
+
...(data === undefined ? {} : { data: { ...data } })
|
|
117
|
+
})),
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
truncated: client.issueBreadcrumbsTruncated
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
syncLogBrewAppleNativeCrashBreadcrumbs(snapshot);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code !== "native_diagnostics_unavailable"
|
|
125
|
+
&& error?.code !== "native_diagnostics_not_installed") {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
export function purgeLogBrewReactNativePersistentQueue(config = {}) {
|
|
79
132
|
purgeReactNativePersistentQueue(config);
|
|
80
133
|
}
|
package/instrumentation.js
CHANGED
|
@@ -1,634 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
captureReactNativeResourceSpan,
|
|
4
|
-
createReactNavigationSpanListener,
|
|
5
|
-
createReactNativeTraceContext,
|
|
6
|
-
createReactNativeTraceHeaders,
|
|
7
|
-
getActiveLogBrewTrace,
|
|
8
|
-
shouldPropagateTraceparent
|
|
9
|
-
} from "./index.js";
|
|
10
|
-
import { createAppStateLifecycleSpanListener } from "./lifecycle.js";
|
|
11
|
-
import { createSafeReactNativeMetadata } from "./metadata.js";
|
|
12
|
-
import {
|
|
13
|
-
clearLogBrewNativeBridgeScope,
|
|
14
|
-
syncLogBrewNativeBridgeScope,
|
|
15
|
-
withLogBrewNativeBridgeScope
|
|
16
|
-
} from "./native-bridge.js";
|
|
17
|
-
import { createReactNativeResourceFetch } from "./resource-fetch.js";
|
|
1
|
+
import implementation from "./instrumentation.cjs";
|
|
18
2
|
|
|
19
|
-
export
|
|
20
|
-
appState,
|
|
21
|
-
captureInitialLifecycleState = false,
|
|
22
|
-
captureInitialNavigationRoute = false,
|
|
23
|
-
fetchImpl,
|
|
24
|
-
globalObject = globalThis,
|
|
25
|
-
includeRouteKey = false,
|
|
26
|
-
instrumentGlobalFetch = false,
|
|
27
|
-
instrumentGlobalXMLHttpRequest = false,
|
|
28
|
-
logger,
|
|
29
|
-
measureFetchResponseBodySize = false,
|
|
30
|
-
measureXhrResponseBodySize = false,
|
|
31
|
-
metadata = {},
|
|
32
|
-
metadataFactory,
|
|
33
|
-
nativeBridge,
|
|
34
|
-
navigation,
|
|
35
|
-
navigationContainer,
|
|
36
|
-
now = () => new Date().toISOString(),
|
|
37
|
-
nowMs = () => Date.now(),
|
|
38
|
-
onError,
|
|
39
|
-
platform,
|
|
40
|
-
randomValues,
|
|
41
|
-
routeTemplate,
|
|
42
|
-
routeTemplateFactory,
|
|
43
|
-
screen,
|
|
44
|
-
sessionId,
|
|
45
|
-
trace,
|
|
46
|
-
traceFlags = "01",
|
|
47
|
-
tracePropagationTargets = []
|
|
48
|
-
} = {}) {
|
|
49
|
-
requireClient(client);
|
|
50
|
-
const activeTrace = resolveInstrumentationTrace({ randomValues, trace, traceFlags });
|
|
51
|
-
const removers = [];
|
|
52
|
-
const resolvedNavigation = navigationContainer ?? navigation;
|
|
3
|
+
export const { createLogBrewReactNativeInstrumentation } = implementation;
|
|
53
4
|
|
|
54
|
-
|
|
55
|
-
removers.push(createAppStateLifecycleSpanListener(client, appState, {
|
|
56
|
-
captureInitialState: captureInitialLifecycleState,
|
|
57
|
-
metadata,
|
|
58
|
-
now,
|
|
59
|
-
nowMs,
|
|
60
|
-
onError,
|
|
61
|
-
platform,
|
|
62
|
-
screen,
|
|
63
|
-
sessionId,
|
|
64
|
-
trace: activeTrace
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (resolvedNavigation) {
|
|
69
|
-
removers.push(createReactNavigationSpanListener(client, resolvedNavigation, {
|
|
70
|
-
appState,
|
|
71
|
-
captureInitialRoute: captureInitialNavigationRoute,
|
|
72
|
-
includeRouteKey,
|
|
73
|
-
metadata,
|
|
74
|
-
now,
|
|
75
|
-
nowMs,
|
|
76
|
-
onError,
|
|
77
|
-
platform,
|
|
78
|
-
trace: activeTrace
|
|
79
|
-
}));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (nativeBridge) {
|
|
83
|
-
syncLogBrewNativeBridgeScope(nativeBridge, {
|
|
84
|
-
logger,
|
|
85
|
-
metadata,
|
|
86
|
-
screen,
|
|
87
|
-
sessionId,
|
|
88
|
-
source: "react-native.instrumentation",
|
|
89
|
-
trace: activeTrace
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const resourceFetch = createReactNativeResourceFetch(client, {
|
|
94
|
-
appState,
|
|
95
|
-
fetchImpl,
|
|
96
|
-
measureResponseBodySize: measureFetchResponseBodySize,
|
|
97
|
-
metadata,
|
|
98
|
-
metadataFactory,
|
|
99
|
-
now,
|
|
100
|
-
nowMs,
|
|
101
|
-
platform,
|
|
102
|
-
randomValues,
|
|
103
|
-
routeTemplate,
|
|
104
|
-
routeTemplateFactory,
|
|
105
|
-
screen,
|
|
106
|
-
sessionId,
|
|
107
|
-
trace: activeTrace,
|
|
108
|
-
traceFlags,
|
|
109
|
-
tracePropagationTargets
|
|
110
|
-
});
|
|
111
|
-
let globalFetch;
|
|
112
|
-
let globalXMLHttpRequest;
|
|
113
|
-
try {
|
|
114
|
-
globalFetch = instrumentGlobalFetch ? installGlobalFetchInstrumentation(client, {
|
|
115
|
-
appState,
|
|
116
|
-
globalObject,
|
|
117
|
-
measureFetchResponseBodySize,
|
|
118
|
-
metadata,
|
|
119
|
-
metadataFactory,
|
|
120
|
-
now,
|
|
121
|
-
nowMs,
|
|
122
|
-
platform,
|
|
123
|
-
randomValues,
|
|
124
|
-
routeTemplate,
|
|
125
|
-
routeTemplateFactory,
|
|
126
|
-
screen,
|
|
127
|
-
sessionId,
|
|
128
|
-
trace: activeTrace,
|
|
129
|
-
traceFlags,
|
|
130
|
-
tracePropagationTargets
|
|
131
|
-
}) : undefined;
|
|
132
|
-
if (globalFetch) {
|
|
133
|
-
removers.push(globalFetch.remove);
|
|
134
|
-
}
|
|
135
|
-
globalXMLHttpRequest = instrumentGlobalXMLHttpRequest ? installGlobalXMLHttpRequestInstrumentation(client, {
|
|
136
|
-
appState,
|
|
137
|
-
globalObject,
|
|
138
|
-
measureXhrResponseBodySize,
|
|
139
|
-
metadata,
|
|
140
|
-
metadataFactory,
|
|
141
|
-
now,
|
|
142
|
-
nowMs,
|
|
143
|
-
platform,
|
|
144
|
-
routeTemplate,
|
|
145
|
-
routeTemplateFactory,
|
|
146
|
-
screen,
|
|
147
|
-
sessionId,
|
|
148
|
-
trace: activeTrace,
|
|
149
|
-
tracePropagationTargets
|
|
150
|
-
}) : undefined;
|
|
151
|
-
if (globalXMLHttpRequest) {
|
|
152
|
-
removers.push(globalXMLHttpRequest.remove);
|
|
153
|
-
}
|
|
154
|
-
} catch (error) {
|
|
155
|
-
removeConfiguredInstrumentation({ nativeBridge, removers });
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let removed = false;
|
|
160
|
-
const remove = () => {
|
|
161
|
-
if (removed) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
removed = true;
|
|
165
|
-
removeConfiguredInstrumentation({ nativeBridge, removers });
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const handle = {
|
|
169
|
-
globalFetch,
|
|
170
|
-
globalXMLHttpRequest,
|
|
171
|
-
remove,
|
|
172
|
-
resourceFetch,
|
|
173
|
-
stop: remove,
|
|
174
|
-
syncNativeBridgeScope(options = {}) {
|
|
175
|
-
if (!nativeBridge) {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
return syncLogBrewNativeBridgeScope(nativeBridge, {
|
|
179
|
-
logger,
|
|
180
|
-
metadata,
|
|
181
|
-
screen,
|
|
182
|
-
sessionId,
|
|
183
|
-
source: "react-native.instrumentation",
|
|
184
|
-
trace: activeTrace,
|
|
185
|
-
...options
|
|
186
|
-
});
|
|
187
|
-
},
|
|
188
|
-
trace: activeTrace,
|
|
189
|
-
withNativeBridgeScope(callbackOrOptions, maybeCallback) {
|
|
190
|
-
if (!nativeBridge) {
|
|
191
|
-
throw new SdkError("configuration_error", "withNativeBridgeScope requires nativeBridge");
|
|
192
|
-
}
|
|
193
|
-
if (typeof callbackOrOptions === "function") {
|
|
194
|
-
return withLogBrewNativeBridgeScope(nativeBridge, {
|
|
195
|
-
logger,
|
|
196
|
-
metadata,
|
|
197
|
-
screen,
|
|
198
|
-
sessionId,
|
|
199
|
-
source: "react-native.instrumentation",
|
|
200
|
-
trace: activeTrace
|
|
201
|
-
}, callbackOrOptions);
|
|
202
|
-
}
|
|
203
|
-
return withLogBrewNativeBridgeScope(nativeBridge, {
|
|
204
|
-
logger,
|
|
205
|
-
metadata,
|
|
206
|
-
screen,
|
|
207
|
-
sessionId,
|
|
208
|
-
source: "react-native.instrumentation",
|
|
209
|
-
trace: activeTrace,
|
|
210
|
-
...(callbackOrOptions ?? {})
|
|
211
|
-
}, maybeCallback);
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
return Object.freeze(handle);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function resolveInstrumentationTrace({ randomValues, trace, traceFlags }) {
|
|
218
|
-
if (typeof trace === "string") {
|
|
219
|
-
return createReactNativeTraceContext({ randomValues, traceFlags, traceparent: trace });
|
|
220
|
-
}
|
|
221
|
-
return trace ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext({ randomValues, traceFlags });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function requireClient(client) {
|
|
225
|
-
if (!client) {
|
|
226
|
-
throw new SdkError("configuration_error", "createLogBrewReactNativeInstrumentation requires a client");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function removeConfiguredInstrumentation({ nativeBridge, removers }) {
|
|
231
|
-
if (nativeBridge) {
|
|
232
|
-
clearLogBrewNativeBridgeScope(nativeBridge);
|
|
233
|
-
}
|
|
234
|
-
for (const removeListener of removers.splice(0).reverse()) {
|
|
235
|
-
removeListener();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function installGlobalFetchInstrumentation(client, {
|
|
240
|
-
appState,
|
|
241
|
-
globalObject,
|
|
242
|
-
measureFetchResponseBodySize,
|
|
243
|
-
metadata,
|
|
244
|
-
metadataFactory,
|
|
245
|
-
now,
|
|
246
|
-
nowMs,
|
|
247
|
-
platform,
|
|
248
|
-
randomValues,
|
|
249
|
-
routeTemplate,
|
|
250
|
-
routeTemplateFactory,
|
|
251
|
-
screen,
|
|
252
|
-
sessionId,
|
|
253
|
-
trace,
|
|
254
|
-
traceFlags,
|
|
255
|
-
tracePropagationTargets
|
|
256
|
-
}) {
|
|
257
|
-
if ((typeof globalObject !== "object" && typeof globalObject !== "function") || globalObject === null) {
|
|
258
|
-
throw new SdkError("configuration_error", "instrumentGlobalFetch requires a globalObject");
|
|
259
|
-
}
|
|
260
|
-
const originalFetch = globalObject.fetch;
|
|
261
|
-
if (typeof originalFetch !== "function") {
|
|
262
|
-
throw new SdkError("configuration_error", "instrumentGlobalFetch requires globalObject.fetch");
|
|
263
|
-
}
|
|
264
|
-
const globalResourceFetch = createReactNativeResourceFetch(client, {
|
|
265
|
-
appState,
|
|
266
|
-
fetchImpl: (input, init) => originalFetch.call(globalObject, input, init),
|
|
267
|
-
measureResponseBodySize: measureFetchResponseBodySize,
|
|
268
|
-
metadata,
|
|
269
|
-
metadataFactory,
|
|
270
|
-
now,
|
|
271
|
-
nowMs,
|
|
272
|
-
platform,
|
|
273
|
-
randomValues,
|
|
274
|
-
routeTemplate,
|
|
275
|
-
routeTemplateFactory,
|
|
276
|
-
screen,
|
|
277
|
-
sessionId,
|
|
278
|
-
trace,
|
|
279
|
-
traceFlags,
|
|
280
|
-
tracePropagationTargets
|
|
281
|
-
});
|
|
282
|
-
let removed = false;
|
|
283
|
-
const wrappedFetch = (input, init) => globalResourceFetch(input, init);
|
|
284
|
-
try {
|
|
285
|
-
globalObject.fetch = wrappedFetch;
|
|
286
|
-
} catch {
|
|
287
|
-
throw new SdkError("configuration_error", "instrumentGlobalFetch could not patch globalObject.fetch");
|
|
288
|
-
}
|
|
289
|
-
if (globalObject.fetch !== wrappedFetch) {
|
|
290
|
-
throw new SdkError("configuration_error", "instrumentGlobalFetch could not patch globalObject.fetch");
|
|
291
|
-
}
|
|
292
|
-
const remove = () => {
|
|
293
|
-
if (removed) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
removed = true;
|
|
297
|
-
if (globalObject.fetch === wrappedFetch) {
|
|
298
|
-
globalObject.fetch = originalFetch;
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
return Object.freeze({
|
|
302
|
-
fetch: wrappedFetch,
|
|
303
|
-
remove,
|
|
304
|
-
stop: remove
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/* eslint-disable no-invalid-this */
|
|
309
|
-
function installGlobalXMLHttpRequestInstrumentation(client, {
|
|
310
|
-
appState,
|
|
311
|
-
globalObject,
|
|
312
|
-
measureXhrResponseBodySize,
|
|
313
|
-
metadata,
|
|
314
|
-
metadataFactory,
|
|
315
|
-
now,
|
|
316
|
-
nowMs,
|
|
317
|
-
platform,
|
|
318
|
-
routeTemplate,
|
|
319
|
-
routeTemplateFactory,
|
|
320
|
-
screen,
|
|
321
|
-
sessionId,
|
|
322
|
-
trace,
|
|
323
|
-
tracePropagationTargets
|
|
324
|
-
}) {
|
|
325
|
-
if ((typeof globalObject !== "object" && typeof globalObject !== "function") || globalObject === null) {
|
|
326
|
-
throw new SdkError("configuration_error", "instrumentGlobalXMLHttpRequest requires a globalObject");
|
|
327
|
-
}
|
|
328
|
-
const xhrType = globalObject.XMLHttpRequest;
|
|
329
|
-
if (typeof xhrType !== "function" || !xhrType.prototype) {
|
|
330
|
-
throw new SdkError("configuration_error", "instrumentGlobalXMLHttpRequest requires globalObject.XMLHttpRequest");
|
|
331
|
-
}
|
|
332
|
-
const prototype = xhrType.prototype;
|
|
333
|
-
const originalOpen = prototype.open;
|
|
334
|
-
const originalSend = prototype.send;
|
|
335
|
-
const originalSetRequestHeader = prototype.setRequestHeader;
|
|
336
|
-
if (typeof originalOpen !== "function" || typeof originalSend !== "function" || typeof originalSetRequestHeader !== "function") {
|
|
337
|
-
throw new SdkError("configuration_error", "instrumentGlobalXMLHttpRequest requires open, send, and setRequestHeader");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const contextKey = Symbol("logbrew.xhr");
|
|
341
|
-
const headersReceivedState = typeof xhrType.HEADERS_RECEIVED === "number" ? xhrType.HEADERS_RECEIVED : 2;
|
|
342
|
-
const doneState = typeof xhrType.DONE === "number" ? xhrType.DONE : 4;
|
|
343
|
-
const safeRouteTemplateFactory = routeTemplateFactory ?? defaultXhrRouteTemplateFactory;
|
|
344
|
-
|
|
345
|
-
function wrappedOpen(method, url) {
|
|
346
|
-
this[contextKey] = {
|
|
347
|
-
method: normalizeXhrMethod(method),
|
|
348
|
-
reported: false,
|
|
349
|
-
url: xhrUrl(url)
|
|
350
|
-
};
|
|
351
|
-
return originalOpen.apply(this, arguments);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function wrappedSend(body) {
|
|
355
|
-
const context = this[contextKey];
|
|
356
|
-
if (!context) {
|
|
357
|
-
return originalSend.apply(this, arguments);
|
|
358
|
-
}
|
|
359
|
-
context.body = xhrRequestBody(body);
|
|
360
|
-
context.startedAtMs = nowMs();
|
|
361
|
-
context.timestamp = now();
|
|
362
|
-
installXhrReadyStateTracker(this, context, {
|
|
363
|
-
appState,
|
|
364
|
-
client,
|
|
365
|
-
doneState,
|
|
366
|
-
headersReceivedState,
|
|
367
|
-
measureXhrResponseBodySize,
|
|
368
|
-
metadata,
|
|
369
|
-
metadataFactory,
|
|
370
|
-
nowMs,
|
|
371
|
-
platform,
|
|
372
|
-
routeTemplate,
|
|
373
|
-
routeTemplateFactory: safeRouteTemplateFactory,
|
|
374
|
-
screen,
|
|
375
|
-
sessionId,
|
|
376
|
-
trace
|
|
377
|
-
});
|
|
378
|
-
if (shouldPropagateTraceparent(context.url, tracePropagationTargets)) {
|
|
379
|
-
originalSetRequestHeader.call(this, "traceparent", createReactNativeTraceHeaders(trace).traceparent);
|
|
380
|
-
}
|
|
381
|
-
try {
|
|
382
|
-
return originalSend.apply(this, arguments);
|
|
383
|
-
} catch (error) {
|
|
384
|
-
captureXhrResourceSpan(this, context, {
|
|
385
|
-
appState,
|
|
386
|
-
client,
|
|
387
|
-
metadata: {
|
|
388
|
-
...metadata,
|
|
389
|
-
xhrErrorName: errorName(error),
|
|
390
|
-
xhrErrorValueType: typeof error
|
|
391
|
-
},
|
|
392
|
-
metadataFactory,
|
|
393
|
-
nowMs,
|
|
394
|
-
platform,
|
|
395
|
-
routeTemplate,
|
|
396
|
-
routeTemplateFactory: safeRouteTemplateFactory,
|
|
397
|
-
screen,
|
|
398
|
-
sessionId,
|
|
399
|
-
status: "error",
|
|
400
|
-
trace
|
|
401
|
-
});
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
prototype.open = wrappedOpen;
|
|
407
|
-
prototype.send = wrappedSend;
|
|
408
|
-
if (prototype.open !== wrappedOpen || prototype.send !== wrappedSend) {
|
|
409
|
-
throw new SdkError("configuration_error", "instrumentGlobalXMLHttpRequest could not patch XMLHttpRequest");
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
let removed = false;
|
|
413
|
-
const remove = () => {
|
|
414
|
-
if (removed) {
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
removed = true;
|
|
418
|
-
if (prototype.open === wrappedOpen) {
|
|
419
|
-
prototype.open = originalOpen;
|
|
420
|
-
}
|
|
421
|
-
if (prototype.send === wrappedSend) {
|
|
422
|
-
prototype.send = originalSend;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
return Object.freeze({
|
|
427
|
-
remove,
|
|
428
|
-
stop: remove
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function installXhrReadyStateTracker(xhr, context, options) {
|
|
433
|
-
const onReadyStateChange = () => {
|
|
434
|
-
if (xhr.readyState === options.headersReceivedState && context.responseStartAtMs === undefined) {
|
|
435
|
-
context.responseStartAtMs = options.nowMs();
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
if (xhr.readyState === options.doneState && !context.reported) {
|
|
439
|
-
captureXhrResourceSpan(xhr, context, options);
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
if (typeof xhr.addEventListener === "function") {
|
|
443
|
-
xhr.addEventListener("readystatechange", onReadyStateChange);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const originalOnReadyStateChange = xhr.onreadystatechange;
|
|
447
|
-
xhr.onreadystatechange = function logBrewOnReadyStateChange() {
|
|
448
|
-
onReadyStateChange();
|
|
449
|
-
if (typeof originalOnReadyStateChange === "function") {
|
|
450
|
-
return originalOnReadyStateChange.apply(this, arguments);
|
|
451
|
-
}
|
|
452
|
-
return undefined;
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function captureXhrResourceSpan(xhr, context, {
|
|
457
|
-
appState,
|
|
458
|
-
client,
|
|
459
|
-
measureXhrResponseBodySize,
|
|
460
|
-
metadata,
|
|
461
|
-
metadataFactory,
|
|
462
|
-
nowMs,
|
|
463
|
-
platform,
|
|
464
|
-
routeTemplate,
|
|
465
|
-
routeTemplateFactory,
|
|
466
|
-
screen,
|
|
467
|
-
sessionId,
|
|
468
|
-
status,
|
|
469
|
-
trace
|
|
470
|
-
}) {
|
|
471
|
-
if (context.reported) {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
context.reported = true;
|
|
475
|
-
const durationMs = elapsedMs(context.startedAtMs, nowMs);
|
|
476
|
-
const responseStartDurationMs = context.responseStartAtMs === undefined
|
|
477
|
-
? undefined
|
|
478
|
-
: elapsedMs(context.startedAtMs, () => context.responseStartAtMs);
|
|
479
|
-
const responseSizeBytes = xhrResponseSizeBytes(xhr, { measureXhrResponseBodySize });
|
|
480
|
-
const safeRouteTemplate = routeTemplate ?? routeTemplateFactory({ url: context.url });
|
|
481
|
-
const statusCode = xhrStatusCode(xhr);
|
|
482
|
-
captureReactNativeResourceSpan(client, {
|
|
483
|
-
appState,
|
|
484
|
-
durationMs,
|
|
485
|
-
metadata: {
|
|
486
|
-
...createSafeReactNativeMetadata(metadata, metadataFactory, {
|
|
487
|
-
durationMs,
|
|
488
|
-
init: context.body === undefined ? undefined : { body: context.body },
|
|
489
|
-
input: context.url,
|
|
490
|
-
method: context.method,
|
|
491
|
-
routeTemplate: safeRouteTemplate,
|
|
492
|
-
status: status ?? undefined,
|
|
493
|
-
statusCode,
|
|
494
|
-
url: context.url
|
|
495
|
-
}),
|
|
496
|
-
responseStartDurationMs,
|
|
497
|
-
transport: "xhr"
|
|
498
|
-
},
|
|
499
|
-
method: context.method,
|
|
500
|
-
platform,
|
|
501
|
-
routeTemplate: safeRouteTemplate,
|
|
502
|
-
screen,
|
|
503
|
-
responseSizeBytes,
|
|
504
|
-
sessionId,
|
|
505
|
-
status: status ?? undefined,
|
|
506
|
-
statusCode,
|
|
507
|
-
timestamp: context.timestamp,
|
|
508
|
-
trace
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function normalizeXhrMethod(method) {
|
|
513
|
-
return String(method ?? "GET").toUpperCase();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function xhrUrl(url) {
|
|
517
|
-
if (typeof url === "string") {
|
|
518
|
-
return url;
|
|
519
|
-
}
|
|
520
|
-
try {
|
|
521
|
-
return url.toString();
|
|
522
|
-
} catch {
|
|
523
|
-
return String(url);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function xhrRequestBody(body) {
|
|
528
|
-
if (typeof body === "string") {
|
|
529
|
-
return body;
|
|
530
|
-
}
|
|
531
|
-
if (body instanceof String) {
|
|
532
|
-
return body.toString();
|
|
533
|
-
}
|
|
534
|
-
return undefined;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function defaultXhrRouteTemplateFactory({ url }) {
|
|
538
|
-
const URLConstructor = globalThis.URL;
|
|
539
|
-
if (typeof URLConstructor === "function") {
|
|
540
|
-
try {
|
|
541
|
-
const parsedUrl = new URLConstructor(url, "https://logbrew.local");
|
|
542
|
-
return parsedUrl.pathname;
|
|
543
|
-
} catch {
|
|
544
|
-
// Fall back to query/hash stripping below for non-standard request keys.
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return String(url).split(/[?#]/u, 1)[0];
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function elapsedMs(startedAtMs, nowMs) {
|
|
551
|
-
const durationMs = nowMs() - startedAtMs;
|
|
552
|
-
return Number.isFinite(durationMs) ? Math.max(0, durationMs) : undefined;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function xhrStatusCode(xhr) {
|
|
556
|
-
return typeof xhr?.status === "number" && Number.isFinite(xhr.status) ? xhr.status : undefined;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function xhrResponseSizeBytes(xhr, { measureXhrResponseBodySize = false } = {}) {
|
|
560
|
-
if (typeof xhr?.getResponseHeader !== "function") {
|
|
561
|
-
return measureXhrResponseBodySize ? xhrResponseBodySizeBytes(xhr) : undefined;
|
|
562
|
-
}
|
|
563
|
-
const contentLength = Number.parseInt(String(xhr.getResponseHeader("Content-Length") ?? ""), 10);
|
|
564
|
-
if (Number.isFinite(contentLength) && contentLength >= 0) {
|
|
565
|
-
return contentLength;
|
|
566
|
-
}
|
|
567
|
-
return measureXhrResponseBodySize ? xhrResponseBodySizeBytes(xhr) : undefined;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function xhrResponseBodySizeBytes(xhr) {
|
|
571
|
-
const responseType = readXhrResponseValue(xhr, "responseType");
|
|
572
|
-
if (responseType === undefined || responseType === "" || responseType === "text") {
|
|
573
|
-
const responseText = readXhrResponseValue(xhr, "responseText");
|
|
574
|
-
if (typeof responseText === "string" || responseText instanceof String) {
|
|
575
|
-
return utf8ByteLength(responseText.toString());
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
const response = readXhrResponseValue(xhr, "response");
|
|
579
|
-
if (typeof response === "string" || response instanceof String) {
|
|
580
|
-
return utf8ByteLength(response.toString());
|
|
581
|
-
}
|
|
582
|
-
if (typeof ArrayBuffer === "function") {
|
|
583
|
-
if (response instanceof ArrayBuffer) {
|
|
584
|
-
return response.byteLength;
|
|
585
|
-
}
|
|
586
|
-
if (typeof ArrayBuffer.isView === "function" && ArrayBuffer.isView(response)) {
|
|
587
|
-
return response.byteLength;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
if (typeof response?.size === "number" && Number.isFinite(response.size) && response.size >= 0) {
|
|
591
|
-
return response.size;
|
|
592
|
-
}
|
|
593
|
-
return undefined;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function readXhrResponseValue(xhr, property) {
|
|
597
|
-
try {
|
|
598
|
-
return xhr?.[property];
|
|
599
|
-
} catch {
|
|
600
|
-
return undefined;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function utf8ByteLength(value) {
|
|
605
|
-
let bytes = 0;
|
|
606
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
607
|
-
const code = value.charCodeAt(index);
|
|
608
|
-
if (code <= 0x7f) {
|
|
609
|
-
bytes += 1;
|
|
610
|
-
} else if (code <= 0x7ff) {
|
|
611
|
-
bytes += 2;
|
|
612
|
-
} else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
|
|
613
|
-
const next = value.charCodeAt(index + 1);
|
|
614
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
615
|
-
bytes += 4;
|
|
616
|
-
index += 1;
|
|
617
|
-
} else {
|
|
618
|
-
bytes += 3;
|
|
619
|
-
}
|
|
620
|
-
} else {
|
|
621
|
-
bytes += 3;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return bytes;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function errorName(error) {
|
|
628
|
-
return typeof error?.name === "string" && error.name.trim() !== "" ? error.name : "Error";
|
|
629
|
-
}
|
|
630
|
-
/* eslint-enable no-invalid-this */
|
|
631
|
-
|
|
632
|
-
export default {
|
|
633
|
-
createLogBrewReactNativeInstrumentation
|
|
634
|
-
};
|
|
5
|
+
export default implementation.default;
|
|
@@ -45,6 +45,11 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setNativeDiagnosticsContext:(NSDictionary
|
|
|
45
45
|
return [LBRNAppleNativeDiagnostics setCorrelationContext:context];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setNativeDiagnosticsBreadcrumbs:(NSDictionary *)snapshot)
|
|
49
|
+
{
|
|
50
|
+
return [LBRNAppleNativeDiagnostics setBreadcrumbs:snapshot];
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(nativeDiagnosticsStatus)
|
|
49
54
|
{
|
|
50
55
|
return [LBRNAppleNativeDiagnostics status];
|
|
@@ -3,7 +3,7 @@ import Foundation
|
|
|
3
3
|
@objc(LBRNAppleNativeDiagnostics)
|
|
4
4
|
public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
5
5
|
private static let shared = LBRNAppleNativeDiagnostics()
|
|
6
|
-
private static let sdkVersion = "0.1.
|
|
6
|
+
private static let sdkVersion = "0.1.21"
|
|
7
7
|
|
|
8
8
|
private let lock = NSLock()
|
|
9
9
|
private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
|
|
@@ -44,6 +44,11 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
|
44
44
|
shared.updateCorrelationContext(rawContext)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
@objc(setBreadcrumbs:)
|
|
48
|
+
public static func setBreadcrumbs(_ rawSnapshot: NSDictionary?) -> NSDictionary {
|
|
49
|
+
shared.updateBreadcrumbs(rawSnapshot)
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
private func installOnMainThread(_ rawConfiguration: NSDictionary) -> NSDictionary {
|
|
48
53
|
guard let configuration = InstalledConfiguration(rawConfiguration) else {
|
|
49
54
|
return failure("native_diagnostics_invalid_configuration")
|
|
@@ -154,6 +159,26 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
|
|
162
|
+
private func updateBreadcrumbs(_ rawSnapshot: NSDictionary?) -> NSDictionary {
|
|
163
|
+
lock.lock()
|
|
164
|
+
defer { lock.unlock() }
|
|
165
|
+
guard let capture else {
|
|
166
|
+
return failure("native_diagnostics_not_installed")
|
|
167
|
+
}
|
|
168
|
+
do {
|
|
169
|
+
let snapshot = try rawSnapshot.map { try NativeCrashBreadcrumbs.validated($0) }
|
|
170
|
+
try capture.setBreadcrumbs(
|
|
171
|
+
snapshot?.breadcrumbs,
|
|
172
|
+
truncated: snapshot?.truncated ?? false,
|
|
173
|
+
)
|
|
174
|
+
return ["status": rawSnapshot == nil ? "cleared" : "updated"]
|
|
175
|
+
} catch let error as NativeCrashError {
|
|
176
|
+
return failure(error.code.rawValue)
|
|
177
|
+
} catch {
|
|
178
|
+
return failure("native_diagnostics_failed")
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
157
182
|
private func captureStatus(status: String) -> NSDictionary {
|
|
158
183
|
guard let capture else {
|
|
159
184
|
return failure("native_diagnostics_not_installed")
|
|
@@ -174,7 +174,7 @@ final class IssueBreadcrumbStore: @unchecked Sendable {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
let maximumIssueBreadcrumbs = 64
|
|
177
|
+
@_spi(CrashReplay) public let maximumIssueBreadcrumbs = 64
|
|
178
178
|
|
|
179
179
|
func normalizeIssueAttributes(_ value: IssueAttributes) throws -> IssueAttributes {
|
|
180
180
|
let exception = try value.exception.map(validateIssueException)
|
|
@@ -251,7 +251,8 @@ func validateIssueException(_ value: IssueException) throws -> IssueException {
|
|
|
251
251
|
return IssueException(type: exceptionType, mechanism: mechanism)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
@_spi(CrashReplay)
|
|
255
|
+
public func validateIssueBreadcrumb(_ value: IssueBreadcrumb) throws -> IssueBreadcrumb {
|
|
255
256
|
try requireTimestamp(value.timestamp)
|
|
256
257
|
guard validMachineKey(value.category, maximum: 64, separators: ["_", ".", ":", "-"]) else {
|
|
257
258
|
throw issueValidationError("issue breadcrumb category is invalid")
|
|
@@ -23,6 +23,7 @@ struct CrashReportSanitizer {
|
|
|
23
23
|
let nativeStackFrames = NativeStackFrameSanitizer().frames(from: rawReport)
|
|
24
24
|
let artifactIdentity = try NativeArtifactIdentityValue.persistedIdentity(in: rawReport)
|
|
25
25
|
let correlation = NativeCrashCorrelation.captured(in: rawReport)
|
|
26
|
+
let breadcrumbs = NativeCrashBreadcrumbs.captured(in: rawReport)
|
|
26
27
|
return NativeCrashRecord(
|
|
27
28
|
eventID: uuid.uuidString.lowercased(),
|
|
28
29
|
timestamp: timestamp.normalized,
|
|
@@ -35,6 +36,9 @@ struct CrashReportSanitizer {
|
|
|
35
36
|
correlation: correlation.0,
|
|
36
37
|
),
|
|
37
38
|
correlationState: correlation.1,
|
|
39
|
+
breadcrumbs: breadcrumbs.0?.breadcrumbs,
|
|
40
|
+
breadcrumbsTruncated: breadcrumbs.0?.truncated,
|
|
41
|
+
breadcrumbState: breadcrumbs.1,
|
|
38
42
|
hangState: nil,
|
|
39
43
|
hangDurationMs: nil,
|
|
40
44
|
source: .engine(reportID: reportID),
|
|
@@ -122,6 +122,25 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
|
|
|
122
122
|
)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/// Replaces the crash-time breadcrumb snapshot in one bounded write.
|
|
126
|
+
@nonobjc
|
|
127
|
+
public func setBreadcrumbs(
|
|
128
|
+
_ breadcrumbs: [IssueBreadcrumb]?,
|
|
129
|
+
truncated: Bool = false,
|
|
130
|
+
) throws {
|
|
131
|
+
lock.lock()
|
|
132
|
+
defer { lock.unlock() }
|
|
133
|
+
try verifyProcessLocked()
|
|
134
|
+
guard store != nil, lifecycle != .stopped else {
|
|
135
|
+
throw NativeCrashError(.notInstalled)
|
|
136
|
+
}
|
|
137
|
+
try verifyStorageLocked()
|
|
138
|
+
try driver.setUserInfo(
|
|
139
|
+
NativeCrashBreadcrumbs.encoded(breadcrumbs, truncated: truncated),
|
|
140
|
+
forKey: NativeCrashBreadcrumbs.reportKey,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
@objc(replayPendingReportsWithHandler:error:)
|
|
126
145
|
public func replayPendingReports(
|
|
127
146
|
_ handler: (NativeCrashRecord) -> Bool,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
3
|
import Foundation
|
|
4
4
|
|
|
5
|
-
enum
|
|
5
|
+
enum NativeCrashContextState: String {
|
|
6
6
|
case captured
|
|
7
7
|
case notCaptured = "not_captured"
|
|
8
8
|
case unavailable
|
|
@@ -22,14 +22,12 @@ enum NativeCrashCorrelation {
|
|
|
22
22
|
throw NativeCrashError(.invalidConfiguration)
|
|
23
23
|
}
|
|
24
24
|
return value
|
|
25
|
-
} catch let error as NativeCrashError {
|
|
26
|
-
throw error
|
|
27
25
|
} catch {
|
|
28
26
|
throw NativeCrashError(.invalidConfiguration)
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
static func captured(in rawReport: [String: Any]) -> (TelemetryContext?,
|
|
30
|
+
static func captured(in rawReport: [String: Any]) -> (TelemetryContext?, NativeCrashContextState) {
|
|
33
31
|
guard let user = rawReport["user"] as? [String: Any], let value = user[reportKey] else {
|
|
34
32
|
return (nil, .notCaptured)
|
|
35
33
|
}
|
|
@@ -75,3 +73,102 @@ enum NativeCrashCorrelation {
|
|
|
75
73
|
return encoder
|
|
76
74
|
}
|
|
77
75
|
}
|
|
76
|
+
|
|
77
|
+
struct NativeCrashBreadcrumbSnapshot: Codable, Equatable {
|
|
78
|
+
let schemaVersion: Int
|
|
79
|
+
let breadcrumbs: [IssueBreadcrumb]
|
|
80
|
+
let truncated: Bool
|
|
81
|
+
|
|
82
|
+
init(breadcrumbs: [IssueBreadcrumb], truncated: Bool) {
|
|
83
|
+
schemaVersion = 1
|
|
84
|
+
self.breadcrumbs = breadcrumbs
|
|
85
|
+
self.truncated = truncated
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
enum NativeCrashBreadcrumbs {
|
|
90
|
+
static let reportKey = "logbrew_native_breadcrumbs"
|
|
91
|
+
static let maximumBytes = 64 * 1024
|
|
92
|
+
|
|
93
|
+
static func encoded(_ values: [IssueBreadcrumb]?, truncated: Bool) throws -> String? {
|
|
94
|
+
guard let values, !values.isEmpty else {
|
|
95
|
+
return nil
|
|
96
|
+
}
|
|
97
|
+
do {
|
|
98
|
+
var breadcrumbs = try values.suffix(maximumIssueBreadcrumbs).map(validateIssueBreadcrumb)
|
|
99
|
+
var wasTruncated = truncated || breadcrumbs.count < values.count
|
|
100
|
+
while !breadcrumbs.isEmpty {
|
|
101
|
+
let data = try encoder().encode(NativeCrashBreadcrumbSnapshot(
|
|
102
|
+
breadcrumbs: breadcrumbs,
|
|
103
|
+
truncated: wasTruncated,
|
|
104
|
+
))
|
|
105
|
+
if data.count <= maximumBytes, let value = String(data: data, encoding: .utf8) {
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
breadcrumbs.removeFirst()
|
|
109
|
+
wasTruncated = true
|
|
110
|
+
}
|
|
111
|
+
} catch let error as NativeCrashError {
|
|
112
|
+
throw error
|
|
113
|
+
} catch {
|
|
114
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
115
|
+
}
|
|
116
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static func captured(
|
|
120
|
+
in rawReport: [String: Any],
|
|
121
|
+
) -> (NativeCrashBreadcrumbSnapshot?, NativeCrashContextState) {
|
|
122
|
+
guard let user = rawReport["user"] as? [String: Any], let value = user[reportKey] else {
|
|
123
|
+
return (nil, .notCaptured)
|
|
124
|
+
}
|
|
125
|
+
guard let value = value as? String,
|
|
126
|
+
let data = value.data(using: .utf8),
|
|
127
|
+
data.count <= maximumBytes,
|
|
128
|
+
let object = try? JSONSerialization.jsonObject(with: data),
|
|
129
|
+
let snapshot = try? validated(object)
|
|
130
|
+
else {
|
|
131
|
+
return (nil, .unavailable)
|
|
132
|
+
}
|
|
133
|
+
return (snapshot, .captured)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static func validated(_ object: Any) throws -> NativeCrashBreadcrumbSnapshot {
|
|
137
|
+
do {
|
|
138
|
+
guard JSONSerialization.isValidJSONObject(object) else {
|
|
139
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
140
|
+
}
|
|
141
|
+
let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
142
|
+
guard data.count <= maximumBytes else {
|
|
143
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
144
|
+
}
|
|
145
|
+
let decoded = try JSONDecoder().decode(NativeCrashBreadcrumbSnapshot.self, from: data)
|
|
146
|
+
guard decoded.schemaVersion == 1,
|
|
147
|
+
!decoded.breadcrumbs.isEmpty,
|
|
148
|
+
decoded.breadcrumbs.count <= maximumIssueBreadcrumbs
|
|
149
|
+
else {
|
|
150
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
151
|
+
}
|
|
152
|
+
let snapshot = try NativeCrashBreadcrumbSnapshot(
|
|
153
|
+
breadcrumbs: decoded.breadcrumbs.map(validateIssueBreadcrumb),
|
|
154
|
+
truncated: decoded.truncated,
|
|
155
|
+
)
|
|
156
|
+
let normalized = try encoder().encode(snapshot)
|
|
157
|
+
let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
|
|
158
|
+
guard object as? NSDictionary == normalizedObject as? NSDictionary else {
|
|
159
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
160
|
+
}
|
|
161
|
+
return snapshot
|
|
162
|
+
} catch let error as NativeCrashError {
|
|
163
|
+
throw error
|
|
164
|
+
} catch {
|
|
165
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static func encoder() -> JSONEncoder {
|
|
170
|
+
let encoder = JSONEncoder()
|
|
171
|
+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
|
172
|
+
return encoder
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -164,7 +164,10 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
164
164
|
private let nativeStackFrames: [NativeStackFrame]?
|
|
165
165
|
private let artifactIdentity: NativeArtifactIdentity?
|
|
166
166
|
private let context: TelemetryContext?
|
|
167
|
-
private let correlationState:
|
|
167
|
+
private let correlationState: NativeCrashContextState?
|
|
168
|
+
private let breadcrumbs: [IssueBreadcrumb]?
|
|
169
|
+
private let breadcrumbsTruncated: Bool?
|
|
170
|
+
private let breadcrumbState: NativeCrashContextState?
|
|
168
171
|
private let hangState: NativeHangIncidentState?
|
|
169
172
|
private let hangDurationMs: Double?
|
|
170
173
|
let source: NativeCrashRecordSource
|
|
@@ -178,7 +181,10 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
178
181
|
nativeStackFrames: [NativeStackFrame]?,
|
|
179
182
|
artifactIdentity: NativeArtifactIdentity?,
|
|
180
183
|
context: TelemetryContext?,
|
|
181
|
-
correlationState:
|
|
184
|
+
correlationState: NativeCrashContextState? = nil,
|
|
185
|
+
breadcrumbs: [IssueBreadcrumb]? = nil,
|
|
186
|
+
breadcrumbsTruncated: Bool? = nil,
|
|
187
|
+
breadcrumbState: NativeCrashContextState? = nil,
|
|
182
188
|
hangState: NativeHangIncidentState?,
|
|
183
189
|
hangDurationMs: Double? = nil,
|
|
184
190
|
source: NativeCrashRecordSource,
|
|
@@ -192,6 +198,9 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
192
198
|
self.artifactIdentity = artifactIdentity
|
|
193
199
|
self.context = context
|
|
194
200
|
self.correlationState = correlationState
|
|
201
|
+
self.breadcrumbs = breadcrumbs
|
|
202
|
+
self.breadcrumbsTruncated = breadcrumbsTruncated
|
|
203
|
+
self.breadcrumbState = breadcrumbState
|
|
195
204
|
self.hangState = hangState
|
|
196
205
|
self.hangDurationMs = hangDurationMs
|
|
197
206
|
self.source = source
|
|
@@ -234,6 +243,9 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
234
243
|
if let correlationState {
|
|
235
244
|
metadata["crash.correlation"] = .string(correlationState.rawValue)
|
|
236
245
|
}
|
|
246
|
+
if let breadcrumbState {
|
|
247
|
+
metadata["crash.breadcrumbs"] = .string(breadcrumbState.rawValue)
|
|
248
|
+
}
|
|
237
249
|
if let hangState {
|
|
238
250
|
metadata["crash.handled"] = .bool(hangState == .recovered)
|
|
239
251
|
}
|
|
@@ -245,6 +257,8 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
245
257
|
level: hangState == .recovered ? .error : .fatal,
|
|
246
258
|
exception: issueException,
|
|
247
259
|
exceptionChain: nativeCrashExceptionChain(for: issueException),
|
|
260
|
+
breadcrumbs: breadcrumbs,
|
|
261
|
+
breadcrumbsTruncated: breadcrumbsTruncated,
|
|
248
262
|
metadata: metadata,
|
|
249
263
|
context: context,
|
|
250
264
|
nativeStackFrames: nativeStackFrames,
|
|
@@ -287,12 +301,16 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
287
301
|
else {
|
|
288
302
|
return false
|
|
289
303
|
}
|
|
290
|
-
|
|
304
|
+
let legacyFields = ["exception", "exceptionChain", "breadcrumbs", "breadcrumbsTruncated", "context"]
|
|
305
|
+
for legacyField in legacyFields where actual[legacyField] == nil {
|
|
291
306
|
expected.removeValue(forKey: legacyField)
|
|
292
307
|
}
|
|
293
|
-
|
|
308
|
+
let actualMetadata = actual["metadata"] as? [String: Any]
|
|
309
|
+
if actualMetadata?["crash.correlation"] == nil || actualMetadata?["crash.breadcrumbs"] == nil {
|
|
294
310
|
var metadata = expected["metadata"] as? [String: Any]
|
|
295
|
-
|
|
311
|
+
for key in ["crash.correlation", "crash.breadcrumbs"] where actualMetadata?[key] == nil {
|
|
312
|
+
metadata?.removeValue(forKey: key)
|
|
313
|
+
}
|
|
296
314
|
expected["metadata"] = metadata
|
|
297
315
|
}
|
|
298
316
|
return actual as NSDictionary == expected as NSDictionary
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStoreRecovery.swift": "79fee27975b6571954bf0b3692a50e60bbbcf53b5ab47f653a474db5c56d68ba",
|
|
12
12
|
"swift/logbrew-swift/Sources/LogBrew/EventEncoding.swift": "b88b6b2d17f7d0cee9ac1a1f34ef35b192cd33d213ef3c9e1c2d21cbcc304a97",
|
|
13
13
|
"swift/logbrew-swift/Sources/LogBrew/IssueDiagnosticEvidence.swift": "e471f27fadaefe06971c1a98892aeb0774e840a2ce0cbb243ae9aabfa41b1c47",
|
|
14
|
-
"swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "
|
|
14
|
+
"swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "bbdfe6169961ff27b8f2cee63c7dcdeacf2e5cee6a3ca17f6a26b933f0adfc39",
|
|
15
15
|
"swift/logbrew-swift/Sources/LogBrew/IssueExceptionChain.swift": "10f81cbce95b47da0a44ef9e515b150e60d71dfcc8b04be24b037b1222a373a5",
|
|
16
16
|
"swift/logbrew-swift/Sources/LogBrew/LifecycleTrace.swift": "738cd3f129fac821a892404d1d42ee92bd806b6583d3dbb5379e88b14ab23c16",
|
|
17
17
|
"swift/logbrew-swift/Sources/LogBrew/LogBrewClient.swift": "186cdbda3754a811f17b82818ca8e7a1711b3b75123686cd17dc3b7441fa21cb",
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
"swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "0cdf08f92d6111c0dc2883b87837b81f69b5c2f9a8e5501780a114c23a741af7",
|
|
30
30
|
"swift/logbrew-swift/Sources/LogBrew/Validation.swift": "07ff5c2daeb829b1df4f50edb0b79727f9a81b1c9b5cde5f995f5ad6ce660aa7",
|
|
31
31
|
"swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "a94dfce653e3e7f381be42b6d2c4540f0be77e83777ecc53e06682cd2e569c5a",
|
|
32
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "
|
|
32
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "ccd0ef83f14096816765278e007f5e4ba0a40286ce00d708b38ca0b94959171e",
|
|
33
33
|
"swift/logbrew-swift/Sources/LogBrewCrash/CrashStorageDirectory.swift": "7cd566703cbf704dc99155451ee93dcace12c35c805abc09a6dcf23975cf43f9",
|
|
34
34
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeArtifactIdentity.swift": "3d785ad717dcf7302451531c18b5e1183983270a6cd78dd543a407e3facc6ae0",
|
|
35
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "
|
|
36
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "
|
|
35
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "c2f5d0e9a8953cb988633ebf60a06f3b7ddaa0c9828f99372969fdfb60b99bc4",
|
|
36
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "eef19c6e561bc03bb4c09736722afe57f63d557f9279b26353679bd53362c25d",
|
|
37
37
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "ce191e46298d91d87b379528d610caa88b924b15a8dd912b4a164b51eb172a7a",
|
|
38
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "
|
|
38
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "2d0f2e7cdce09dc09906cede5d83928e8bd0e70a8522ff55c999d19366d84730",
|
|
39
39
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashReplay.swift": "3eb3d93b32cf14de416f56fa30d6d65e6f268b5ec9d1a67081d7525c4efd4f80",
|
|
40
40
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "5a8150083f16d6b495c9c63434809dc5f11f13ebb23211f57151bad62040781b",
|
|
41
41
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdog.swift": "37b25904cd6576e20c40500be2c153156403b57ad7b4186a1b6b35b414145ecb",
|
package/metadata.js
CHANGED
|
@@ -1,181 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 sanitizeReactNativeIssueExceptionChain(exceptionChain) {
|
|
70
|
-
if (!exceptionChain || !Array.isArray(exceptionChain.entries)) {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
...exceptionChain,
|
|
75
|
-
entries: exceptionChain.entries.map((entry) => {
|
|
76
|
-
const stackFrames = sanitizeReactNativeIssueStackFrames(entry.stackFrames);
|
|
77
|
-
return {
|
|
78
|
-
...entry,
|
|
79
|
-
...(stackFrames ? { stackFrames } : {})
|
|
80
|
-
};
|
|
81
|
-
})
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function runtimeReactNativeDebugIdMap() {
|
|
86
|
-
try {
|
|
87
|
-
const registry = globalThis?.[REACT_NATIVE_DEBUG_ID_REGISTRY];
|
|
88
|
-
if (!registry || Array.isArray(registry) || typeof registry !== "object") {
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
const entries = Object.entries(registry);
|
|
92
|
-
if (entries.length === 0 || entries.length > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
const debugIdMap = Object.create(null);
|
|
96
|
-
let frameCount = 0;
|
|
97
|
-
for (const [stack, debugId] of entries) {
|
|
98
|
-
if (typeof debugId !== "string" || !SAFE_RELEASE_ARTIFACT_DEBUG_ID.test(debugId)) {
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
const normalizedDebugId = debugId.toLowerCase();
|
|
102
|
-
let stackFrameCount = 0;
|
|
103
|
-
for (const line of stack.split(/\r?\n/u)) {
|
|
104
|
-
const filename = runtimeStackFrameFilename(line);
|
|
105
|
-
if (!filename) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
frameCount += 1;
|
|
109
|
-
stackFrameCount += 1;
|
|
110
|
-
if (frameCount > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES) {
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
const existingDebugId = debugIdMap[filename];
|
|
114
|
-
if (existingDebugId && existingDebugId !== normalizedDebugId) {
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
debugIdMap[filename] = normalizedDebugId;
|
|
118
|
-
}
|
|
119
|
-
if (stackFrameCount === 0) {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return frameCount > 0 ? debugIdMap : undefined;
|
|
124
|
-
} catch {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function isSensitiveMetadataKey(key) {
|
|
130
|
-
return SENSITIVE_METADATA_FACTORY_KEY_RE.test(String(key).toLowerCase());
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function runtimeStackFrameFilename(rawLine) {
|
|
134
|
-
let location = typeof rawLine === "string" ? rawLine.trim() : "";
|
|
135
|
-
if (!location) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
if (location.startsWith("at ")) {
|
|
139
|
-
location = location.slice(3).trim();
|
|
140
|
-
if (location.endsWith(")") && location.includes("(")) {
|
|
141
|
-
location = location.slice(location.lastIndexOf("(") + 1, -1);
|
|
142
|
-
}
|
|
143
|
-
} else if (location.includes("@")) {
|
|
144
|
-
location = location.slice(location.lastIndexOf("@") + 1);
|
|
145
|
-
}
|
|
146
|
-
const parts = location.split(":");
|
|
147
|
-
if (parts.length < 3) {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
const columnText = parts.pop();
|
|
151
|
-
const lineText = parts.pop();
|
|
152
|
-
const filename = parts.join(":").trim();
|
|
153
|
-
if (!/^[1-9]\d*$/u.test(lineText) || !/^[1-9]\d*$/u.test(columnText)) {
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
const line = Number(lineText);
|
|
157
|
-
const column = Number(columnText);
|
|
158
|
-
return Number.isSafeInteger(line) && Number.isSafeInteger(column) && filename ? filename : undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function reactNativeCodePath(value) {
|
|
162
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
let path = value.trim();
|
|
166
|
-
const URLConstructor = globalThis.URL;
|
|
167
|
-
if (typeof URLConstructor === "function") {
|
|
168
|
-
try {
|
|
169
|
-
path = new URLConstructor(path).pathname || path;
|
|
170
|
-
} catch {
|
|
171
|
-
path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
|
|
175
|
-
}
|
|
176
|
-
if (/^[A-Za-z]:\//u.test(path) || /^\/(?:Users|home|private|tmp|var)\//u.test(path)) {
|
|
177
|
-
path = path.replace(/\/+$/u, "");
|
|
178
|
-
return path.slice(path.lastIndexOf("/") + 1) || undefined;
|
|
179
|
-
}
|
|
180
|
-
return path || undefined;
|
|
181
|
-
}
|
|
1
|
+
import implementation from "./metadata.cjs";
|
|
2
|
+
|
|
3
|
+
export const {
|
|
4
|
+
createSafeReactNativeMetadata,
|
|
5
|
+
runtimeReactNativeDebugIdMap,
|
|
6
|
+
safeReactNativeMetadataFactoryResult,
|
|
7
|
+
sanitizeReactNativeIssueExceptionChain,
|
|
8
|
+
sanitizeReactNativeIssueMetadata,
|
|
9
|
+
sanitizeReactNativeIssueStackFrames
|
|
10
|
+
} = implementation;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logbrew/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "React Native offline delivery, Apple native diagnostics, screen, error, trace, action, and network timeline helpers for LogBrew.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"url": "git+https://github.com/LogBrewCo/sdk.git"
|
|
241
241
|
},
|
|
242
242
|
"peerDependencies": {
|
|
243
|
-
"@logbrew/sdk": "^0.1.
|
|
243
|
+
"@logbrew/sdk": "^0.1.15",
|
|
244
244
|
"expo": ">=49",
|
|
245
245
|
"react": ">=18",
|
|
246
246
|
"react-native": ">=0.72"
|
|
@@ -9,6 +9,9 @@ export interface Spec extends TurboModule {
|
|
|
9
9
|
setNativeDiagnosticsContext(
|
|
10
10
|
context: CodegenTypes.UnsafeObject | null
|
|
11
11
|
): CodegenTypes.UnsafeObject;
|
|
12
|
+
setNativeDiagnosticsBreadcrumbs(
|
|
13
|
+
snapshot: CodegenTypes.UnsafeObject | null
|
|
14
|
+
): CodegenTypes.UnsafeObject;
|
|
12
15
|
nativeDiagnosticsStatus(): CodegenTypes.UnsafeObject;
|
|
13
16
|
}
|
|
14
17
|
|