@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.
@@ -1,634 +1,5 @@
1
- import { SdkError } from "@logbrew/sdk";
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 function createLogBrewReactNativeInstrumentation(client, {
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
- if (appState?.addEventListener) {
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.20"
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 context = try rawContext.map { try NativeCrashCorrelation.validated($0) }
148
- try capture.setCorrelationContext(context)
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")