@logbrew/react-native 0.1.20 → 0.1.22
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 +22 -3
- package/apple-native-diagnostics.d.ts +5 -0
- package/apple-native-diagnostics.js +53 -6
- 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 +31 -3
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnosticEvidence.swift +2 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +3 -2
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +10 -2
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +29 -4
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +169 -38
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +27 -5
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +6 -6
- package/lifecycle.js +6 -120
- package/metadata.js +10 -181
- package/native-bridge.js +5 -120
- package/package.json +2 -2
- package/src/NativeLogBrewAppleDiagnostics.ts +3 -0
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.22"
|
|
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")
|
|
@@ -144,8 +149,11 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
|
144
149
|
return failure("native_diagnostics_not_installed")
|
|
145
150
|
}
|
|
146
151
|
do {
|
|
147
|
-
let
|
|
148
|
-
try capture.
|
|
152
|
+
let snapshot = try rawContext.map { try NativeCrashCorrelation.validated($0) }
|
|
153
|
+
try capture.setDiagnosticContext(
|
|
154
|
+
context: snapshot?.correlationContext,
|
|
155
|
+
impact: snapshot?.impact,
|
|
156
|
+
)
|
|
149
157
|
return ["status": rawContext == nil ? "cleared" : "updated"]
|
|
150
158
|
} catch let error as NativeCrashError {
|
|
151
159
|
return failure(error.code.rawValue)
|
|
@@ -154,6 +162,26 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
private func updateBreadcrumbs(_ rawSnapshot: NSDictionary?) -> NSDictionary {
|
|
166
|
+
lock.lock()
|
|
167
|
+
defer { lock.unlock() }
|
|
168
|
+
guard let capture else {
|
|
169
|
+
return failure("native_diagnostics_not_installed")
|
|
170
|
+
}
|
|
171
|
+
do {
|
|
172
|
+
let snapshot = try rawSnapshot.map { try NativeCrashBreadcrumbs.validated($0) }
|
|
173
|
+
try capture.setBreadcrumbs(
|
|
174
|
+
snapshot?.breadcrumbs,
|
|
175
|
+
truncated: snapshot?.truncated ?? false,
|
|
176
|
+
)
|
|
177
|
+
return ["status": rawSnapshot == nil ? "cleared" : "updated"]
|
|
178
|
+
} catch let error as NativeCrashError {
|
|
179
|
+
return failure(error.code.rawValue)
|
|
180
|
+
} catch {
|
|
181
|
+
return failure("native_diagnostics_failed")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
private func captureStatus(status: String) -> NSDictionary {
|
|
158
186
|
guard let capture else {
|
|
159
187
|
return failure("native_diagnostics_not_installed")
|