@logbrew/react-native 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LogBrewReactNative.podspec +1 -0
  2. package/README.md +70 -7
  3. package/android/CMakeLists.txt +7 -0
  4. package/android/build.gradle +12 -0
  5. package/android/src/main/cpp/android_diagnostics.cpp +363 -0
  6. package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
  7. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
  8. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
  9. package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
  10. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
  11. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +127 -156
  12. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
  13. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
  14. package/android-native-diagnostics.d.ts +29 -0
  15. package/android-native-diagnostics.js +176 -0
  16. package/fatal-replay.cjs +35 -3
  17. package/global-errors.d.cts +3 -3
  18. package/global-errors.d.ts +3 -3
  19. package/global-errors.native.js +1 -12
  20. package/index.cjs +27 -5
  21. package/index.native.d.ts +4 -0
  22. package/index.native.js +29 -4
  23. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
  29. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
  30. package/ios/LBRNFatalStoreModule.mm +34 -86
  31. package/package.json +17 -5
  32. package/persistent-delivery.native.js +4 -3
  33. package/resource-fetch.js +5 -463
  34. package/src/NativeLogBrewFatalStore.ts +6 -4
  35. package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
  36. package/ios/LBRNFatalRecordStore.h +0 -25
  37. package/ios/LBRNFatalRecordStore.m +0 -542
package/fatal-replay.cjs CHANGED
@@ -5,6 +5,7 @@ const MAX_ADMITTED_FATAL_IDS = 256;
5
5
  const MAX_FATAL_FILENAME_BYTES = 512;
6
6
  const MAX_FATAL_STACK_BYTES = 16 * 1024;
7
7
  const MAX_FATAL_STACK_FRAMES = 24;
8
+ const DURABLE_QUEUE = Symbol.for("co.logbrew.react-native.durable-event-queue");
8
9
 
9
10
  const admittedFatalIdsByClient = new WeakMap();
10
11
  let nextFatalSequence = 0;
@@ -18,12 +19,13 @@ function createFatalController({
18
19
  sanitizers
19
20
  }) {
20
21
  const methods = fatalStoreMethods(fatalStore);
22
+ const directAdmission = !methods && safeReadProperty(client, DURABLE_QUEUE) === true;
21
23
  const state = {
22
24
  acknowledgedRecords: 0,
23
- available: methods !== undefined,
25
+ available: methods !== undefined || directAdmission,
24
26
  corruptRecords: 0,
25
27
  droppedRecords: 0,
26
- lastOutcome: methods ? "idle" : "unavailable",
28
+ lastOutcome: methods || directAdmission ? "idle" : "unavailable",
27
29
  replayedRecords: 0,
28
30
  storedRecords: 0
29
31
  };
@@ -37,6 +39,9 @@ function createFatalController({
37
39
  return fatalHealthSnapshot(state);
38
40
  },
39
41
  replay() {
42
+ if (!methods) {
43
+ return;
44
+ }
40
45
  replayPendingFatal({
41
46
  client,
42
47
  eventMessage,
@@ -49,6 +54,11 @@ function createFatalController({
49
54
  });
50
55
  },
51
56
  store(error) {
57
+ if (!methods) {
58
+ return directAdmission
59
+ ? admitFatalError({ client, error, eventMessage, issue, onDiagnostic, sanitizers, state })
60
+ : false;
61
+ }
52
62
  return storeFatalError({
53
63
  error,
54
64
  fatalStore,
@@ -61,6 +71,26 @@ function createFatalController({
61
71
  };
62
72
  }
63
73
 
74
+ function admitFatalError({ client, error, eventMessage, issue, onDiagnostic, sanitizers, state }) {
75
+ const record = createFatalRecord(error, sanitizers);
76
+ const before = admissionSnapshot(client);
77
+ try {
78
+ issue.call(client, record.id, record.timestamp, fatalEventAttributes(record, eventMessage));
79
+ } catch {
80
+ state.lastOutcome = "storage_error";
81
+ emitDiagnostic(onDiagnostic, "fatal_store_failed");
82
+ return false;
83
+ }
84
+ if (!wasAdmitted(before, admissionSnapshot(client))) {
85
+ state.lastOutcome = "storage_error";
86
+ emitDiagnostic(onDiagnostic, "fatal_store_failed");
87
+ return false;
88
+ }
89
+ state.storedRecords = incrementBounded(state.storedRecords);
90
+ state.lastOutcome = "stored";
91
+ return true;
92
+ }
93
+
64
94
  function fatalStoreMethods(fatalStore) {
65
95
  if (!isObjectLike(fatalStore)) {
66
96
  return undefined;
@@ -327,7 +357,9 @@ function fatalEventAttributes(record, eventMessage) {
327
357
  replayed: true,
328
358
  source: "react-native.global_error"
329
359
  }),
330
- stackFrames: record.stackFrames.map((frame) => Object.freeze({ ...frame })),
360
+ ...(record.stackFrames.length === 0
361
+ ? {}
362
+ : { stackFrames: record.stackFrames.map((frame) => Object.freeze({ ...frame })) }),
331
363
  title: eventMessage
332
364
  });
333
365
  }
@@ -240,9 +240,9 @@ export declare function installLogBrewReactNativePromiseRejectionTracker(
240
240
  /**
241
241
  * Install reversible automatic capture for React Native global JavaScript errors.
242
242
  *
243
- * The React Native conditional export injects its synchronous native fatal store by default.
244
- * Direct Node ESM/CJS callers can inject a compatible store explicitly. Fatal replay is
245
- * stable-ID at-least-once and acknowledges only after observable local queue admission.
243
+ * The React Native conditional export admits fatal reports into its persistent event queue
244
+ * before the previous handler runs. Direct Node ESM/CJS callers can inject a compatible
245
+ * fatal store explicitly.
246
246
  * This helper does not claim local exactly-once delivery or install Promise rejection handling.
247
247
  */
248
248
  export declare function installLogBrewReactNativeGlobalErrorHandler(
@@ -240,9 +240,9 @@ export declare function installLogBrewReactNativePromiseRejectionTracker(
240
240
  /**
241
241
  * Install reversible automatic capture for React Native global JavaScript errors.
242
242
  *
243
- * The React Native conditional export injects its synchronous native fatal store by default.
244
- * Direct Node ESM/CJS callers can inject a compatible store explicitly. Fatal replay is
245
- * stable-ID at-least-once and acknowledges only after observable local queue admission.
243
+ * The React Native conditional export admits fatal reports into its persistent event queue
244
+ * before the previous handler runs. Direct Node ESM/CJS callers can inject a compatible
245
+ * fatal store explicitly.
246
246
  * This helper does not claim local exactly-once delivery or install Promise rejection handling.
247
247
  */
248
248
  export declare function installLogBrewReactNativeGlobalErrorHandler(
@@ -1,5 +1,3 @@
1
- import { NativeModules, TurboModuleRegistry } from "react-native";
2
-
3
1
  import {
4
2
  createLogBrewReactNativePromiseRejectionHandlers,
5
3
  installLogBrewReactNativeGlobalErrorHandler as installPlatformNeutralHandler,
@@ -12,15 +10,6 @@ export {
12
10
 
13
11
  const hermesTrackerAdapters = new WeakMap();
14
12
 
15
- function defaultFatalStore() {
16
- try {
17
- return TurboModuleRegistry?.get?.("LogBrewFatalStore")
18
- ?? NativeModules?.LogBrewFatalStore;
19
- } catch {
20
- return undefined;
21
- }
22
- }
23
-
24
13
  function defaultPromiseRejectionTracker() {
25
14
  let hermes;
26
15
  let enable;
@@ -70,7 +59,7 @@ export function installLogBrewReactNativeGlobalErrorHandler(options = {}) {
70
59
  }
71
60
  return installPlatformNeutralHandler({
72
61
  ...forwarded,
73
- fatalStore: hasInjectedStore ? forwarded.fatalStore : defaultFatalStore()
62
+ fatalStore: hasInjectedStore ? forwarded.fatalStore : undefined
74
63
  });
75
64
  }
76
65
 
package/index.cjs CHANGED
@@ -16,8 +16,9 @@ const {
16
16
  } = require("./metadata.cjs");
17
17
 
18
18
  const DEFAULT_SDK_NAME = "logbrew-react-native";
19
- const DEFAULT_SDK_VERSION = "0.1.22";
19
+ const DEFAULT_SDK_VERSION = "0.1.24";
20
20
  const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
21
+ const NATIVE_RANDOM_HEX = Symbol.for("co.logbrew.react-native.secure-random-hex");
21
22
  const MAX_ACTION_NAME_LENGTH = 64;
22
23
  const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
23
24
  const SCREEN_ACTION_PREFIX = "screen:";
@@ -913,11 +914,32 @@ function boundedBreadcrumbMessage(value) {
913
914
  }
914
915
 
915
916
  function defaultRandomValues(length) {
916
- if (!globalThis.crypto || typeof globalThis.crypto.getRandomValues !== "function") {
917
- throw new SdkError("configuration_error", "createReactNativeTraceparent requires crypto.getRandomValues or randomValues");
918
- }
919
917
  const bytes = new Uint8Array(length);
920
- return globalThis.crypto.getRandomValues(bytes);
918
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
919
+ return globalThis.crypto.getRandomValues(bytes);
920
+ }
921
+ if (typeof globalThis.expo?.modules?.ExpoCrypto?.getRandomValues === "function") {
922
+ globalThis.expo.modules.ExpoCrypto.getRandomValues(bytes);
923
+ return bytes;
924
+ }
925
+ const nativeRandom = globalThis[NATIVE_RANDOM_HEX];
926
+ if (typeof nativeRandom !== "function") {
927
+ throw secureRandomError();
928
+ }
929
+ let nativeHex;
930
+ try {
931
+ nativeHex = nativeRandom(length);
932
+ } catch {
933
+ throw secureRandomError();
934
+ }
935
+ if (typeof nativeHex !== "string" || nativeHex.length !== length * 2 || !/^[0-9a-f]+$/iu.test(nativeHex)) {
936
+ throw secureRandomError();
937
+ }
938
+ return Uint8Array.from({ length }, (_, index) => Number.parseInt(nativeHex.slice(index * 2, index * 2 + 2), 16));
939
+ }
940
+
941
+ function secureRandomError() {
942
+ return new SdkError("configuration_error", "createReactNativeTraceparent requires secure random values");
921
943
  }
922
944
 
923
945
  function headersWithTraceparent(headers, traceparent) {
package/index.native.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { CreateLogBrewReactNativeClientConfig } from "./index.js";
3
3
 
4
4
  export * from "./index.js";
5
5
  export * from "./apple-native-diagnostics.js";
6
+ export * from "./android-native-diagnostics.js";
6
7
  export {
7
8
  installLogBrewReactNativeGlobalErrorHandler,
8
9
  installLogBrewReactNativePromiseRejectionTracker
@@ -43,10 +44,13 @@ declare const defaultExport: Omit<
43
44
  createLogBrewReactNativeClient: typeof createLogBrewReactNativeClient;
44
45
  createDefaultLogBrewReactNativeClient: typeof createDefaultLogBrewReactNativeClient;
45
46
  getLogBrewAppleNativeDiagnosticsStatus: typeof import("./apple-native-diagnostics.js").getLogBrewAppleNativeDiagnosticsStatus;
47
+ getLogBrewAndroidNativeDiagnosticsStatus: typeof import("./android-native-diagnostics.js").getLogBrewAndroidNativeDiagnosticsStatus;
46
48
  installLogBrewAppleNativeDiagnostics: typeof import("./apple-native-diagnostics.js").installLogBrewAppleNativeDiagnostics;
49
+ installLogBrewAndroidNativeDiagnostics: typeof import("./android-native-diagnostics.js").installLogBrewAndroidNativeDiagnostics;
47
50
  purgeLogBrewReactNativePersistentQueue: typeof purgeLogBrewReactNativePersistentQueue;
48
51
  replayLogBrewAppleNativeDiagnostics: typeof import("./apple-native-diagnostics.js").replayLogBrewAppleNativeDiagnostics;
49
52
  setLogBrewAppleNativeCrashContext: typeof import("./apple-native-diagnostics.js").setLogBrewAppleNativeCrashContext;
53
+ uninstallLogBrewAndroidNativeDiagnostics: typeof import("./android-native-diagnostics.js").uninstallLogBrewAndroidNativeDiagnostics;
50
54
  };
51
55
 
52
56
  export default defaultExport;
package/index.native.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AppState, Platform } from "react-native";
1
+ import { AppState, NativeModules, Platform, TurboModuleRegistry } from "react-native";
2
2
  import {
3
3
  captureAppStateChange,
4
4
  captureReactNativeAction,
@@ -17,6 +17,11 @@ import {
17
17
  setLogBrewAppleNativeCrashContext,
18
18
  syncLogBrewAppleNativeCrashBreadcrumbs
19
19
  } from "./apple-native-diagnostics.js";
20
+ import {
21
+ getLogBrewAndroidNativeDiagnosticsStatus,
22
+ installLogBrewAndroidNativeDiagnostics,
23
+ uninstallLogBrewAndroidNativeDiagnostics
24
+ } from "./android-native-diagnostics.js";
20
25
  import {
21
26
  purgeReactNativePersistentQueue,
22
27
  resolveReactNativePersistentEventStore
@@ -31,9 +36,22 @@ export {
31
36
  replayLogBrewAppleNativeDiagnostics,
32
37
  setLogBrewAppleNativeCrashContext
33
38
  };
39
+ export {
40
+ getLogBrewAndroidNativeDiagnosticsStatus,
41
+ installLogBrewAndroidNativeDiagnostics,
42
+ uninstallLogBrewAndroidNativeDiagnostics
43
+ };
34
44
 
35
45
  export * from "./index.js";
36
46
 
47
+ const nativeRuntime = TurboModuleRegistry?.get?.("LogBrewFatalStore")
48
+ ?? NativeModules?.LogBrewFatalStore;
49
+ if (typeof nativeRuntime?.secureRandomHex === "function") {
50
+ globalThis[Symbol.for("co.logbrew.react-native.secure-random-hex")] =
51
+ nativeRuntime.secureRandomHex.bind(nativeRuntime);
52
+ }
53
+ const DURABLE_QUEUE = Symbol.for("co.logbrew.react-native.durable-event-queue");
54
+
37
55
  export function createLogBrewReactNativeClient(config = {}) {
38
56
  const input = config !== null && typeof config === "object" ? config : {};
39
57
  const {
@@ -62,14 +80,18 @@ export function createLogBrewReactNativeClient(config = {}) {
62
80
  hasExplicitPersistentQueue
63
81
  });
64
82
  try {
65
- return bindAppleNativeCrashBreadcrumbs(createPlatformNeutralClient({
83
+ const client = createPlatformNeutralClient({
66
84
  ...forwarded,
67
85
  apiKey,
68
86
  clientKey,
69
87
  eventStore: resolved.eventStore,
70
88
  maxQueueBytes,
71
89
  maxQueueSize
72
- }));
90
+ });
91
+ if (resolved.durable) {
92
+ Object.defineProperty(client, DURABLE_QUEUE, { value: true });
93
+ }
94
+ return bindAppleNativeCrashBreadcrumbs(client);
73
95
  } catch (error) {
74
96
  resolved.abort();
75
97
  throw error;
@@ -198,11 +220,14 @@ const defaultExport = {
198
220
  createDefaultLogBrewReactNativeClient,
199
221
  createLogBrewReactNativeClient,
200
222
  getDefaultReactNativeContext,
223
+ getLogBrewAndroidNativeDiagnosticsStatus,
201
224
  getLogBrewAppleNativeDiagnosticsStatus,
202
225
  installLogBrewAppleNativeDiagnostics,
226
+ installLogBrewAndroidNativeDiagnostics,
203
227
  purgeLogBrewReactNativePersistentQueue,
204
228
  replayLogBrewAppleNativeDiagnostics,
205
- setLogBrewAppleNativeCrashContext
229
+ setLogBrewAppleNativeCrashContext,
230
+ uninstallLogBrewAndroidNativeDiagnostics
206
231
  };
207
232
 
208
233
  export default defaultExport;
@@ -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.22"
6
+ private static let sdkVersion = "0.1.24"
7
7
 
8
8
  private let lock = NSLock()
9
9
  private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
@@ -14,9 +14,7 @@ public enum LogBrewLoggerLevel: String, Sendable {
14
14
 
15
15
  var logBrewLevel: LogLevel {
16
16
  switch self {
17
- case .trace, .debug:
18
- .info
19
- case .info, .notice:
17
+ case .trace, .debug, .info, .notice:
20
18
  .info
21
19
  case .warning:
22
20
  .warning
@@ -73,7 +71,7 @@ public final class LogBrewLogger {
73
71
  baseMetadata = metadata ?? [:]
74
72
  self.transport = transport
75
73
  self.flushOnLog = flushOnLog
76
- self.timestampProvider = timestampProvider ?? Self.defaultTimestamp
74
+ self.timestampProvider = timestampProvider ?? currentTelemetryTimestamp
77
75
  self.onError = onError
78
76
  }
79
77
 
@@ -246,12 +244,6 @@ public final class LogBrewLogger {
246
244
  }
247
245
  return merged
248
246
  }
249
-
250
- private static func defaultTimestamp() -> String {
251
- let formatter = ISO8601DateFormatter()
252
- formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
253
- return formatter.string(from: Date())
254
- }
255
247
  }
256
248
 
257
249
  private struct LogBrewLoggerCall {
@@ -0,0 +1,56 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import Foundation
4
+
5
+ public extension LogBrewClient {
6
+ /// Runs one app-owned operation under a root or child trace and records its span.
7
+ @discardableResult
8
+ func withOperation<Result>(
9
+ _ name: String,
10
+ metadata: Metadata? = nil,
11
+ onOperationError: ((any Error) -> Void)? = nil,
12
+ onCaptureError: ((any Error) -> Void)? = nil,
13
+ operation: () async throws -> Result,
14
+ ) async throws -> Result {
15
+ let operationName = name.trimmingCharacters(in: .whitespacesAndNewlines)
16
+ try requireNonEmpty("operation name", operationName)
17
+ let context = LogBrewTrace.current.map(LogBrewTrace.childContext(of:)) ?? LogBrewTrace
18
+ .continueOrCreateContext(fromTraceparent: nil)
19
+ let startedAtMs = ProcessInfo.processInfo.systemUptime * 1000
20
+ func finish(_ error: (any Error)? = nil) {
21
+ do {
22
+ var evidence = metadata ?? [:]
23
+ evidence["source"] = .string("swift.operation")
24
+ if let error {
25
+ evidence["errorType"] = .string(String(reflecting: type(of: error)))
26
+ }
27
+ try span(
28
+ "swift_operation_span_\(context.spanId)",
29
+ timestamp: currentTelemetryTimestamp(),
30
+ attributes: LogBrewTrace.spanAttributes(
31
+ name: operationName,
32
+ status: error == nil ? .ok : .error,
33
+ durationMs: max(0, ProcessInfo.processInfo.systemUptime * 1000 - startedAtMs),
34
+ metadata: evidence,
35
+ context: context,
36
+ ),
37
+ )
38
+ } catch {
39
+ onCaptureError?(error)
40
+ }
41
+ }
42
+
43
+ return try await LogBrewTrace.withContext(context) {
44
+ do {
45
+ let result = try await operation()
46
+ finish()
47
+ return result
48
+ } catch {
49
+ let operationError = error
50
+ onOperationError?(operationError)
51
+ finish(operationError)
52
+ throw operationError
53
+ }
54
+ }
55
+ }
56
+ }
@@ -23,7 +23,6 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
23
23
  nowMsProvider: (@Sendable () -> Double)? = nil,
24
24
  onCaptureError: (@Sendable (any Error) -> Void)? = nil,
25
25
  ) throws {
26
- let loader = LogBrewURLSessionDataLoader(session: session)
27
26
  try self.init(
28
27
  client: client,
29
28
  eventIDPrefix: eventIDPrefix,
@@ -31,7 +30,7 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
31
30
  nowMsProvider: nowMsProvider,
32
31
  onCaptureError: onCaptureError,
33
32
  dataLoader: { request in
34
- try await loader.data(for: request)
33
+ try await session.data(for: request)
35
34
  },
36
35
  )
37
36
  }
@@ -48,7 +47,7 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
48
47
  try requireNonEmpty("URLSession tracer eventIDPrefix", normalizedPrefix)
49
48
  self.client = client
50
49
  self.eventIDPrefix = normalizedPrefix
51
- self.timestampProvider = timestampProvider ?? Self.defaultTimestamp
50
+ self.timestampProvider = timestampProvider ?? currentTelemetryTimestamp
52
51
  self.nowMsProvider = nowMsProvider ?? Self.defaultNowMs
53
52
  self.onCaptureError = onCaptureError
54
53
  self.dataLoader = dataLoader
@@ -60,45 +59,63 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
60
59
  routeTemplate: String? = nil,
61
60
  eventID: String? = nil,
62
61
  metadata: Metadata? = nil,
62
+ onRequestError: (@Sendable (any Error) throws -> Void)? = nil,
63
63
  ) async throws -> (Data, URLResponse) {
64
64
  let span = try LogBrewTrace.startURLSessionSpan(for: request, routeTemplate: routeTemplate)
65
65
  let startedAtMs = nowMsProvider()
66
66
 
67
67
  do {
68
68
  let (data, response) = try await dataLoader(span.request)
69
- captureSpan(URLSessionTraceCapture(
70
- eventID: eventID,
71
- span: span,
72
- response: response,
73
- durationMs: durationSince(startedAtMs),
74
- error: nil,
75
- metadata: metadata,
76
- ))
69
+ let durationMs = durationSince(startedAtMs)
70
+ captureSpan(eventID, span: span, response: response, durationMs: durationMs, metadata: metadata)
77
71
  return (data, response)
78
72
  } catch {
79
- captureSpan(URLSessionTraceCapture(
80
- eventID: eventID,
81
- span: span,
82
- response: nil,
83
- durationMs: durationSince(startedAtMs),
84
- error: error,
85
- metadata: metadata,
86
- ))
73
+ let durationMs = durationSince(startedAtMs)
74
+ captureSpan(eventID, span: span, durationMs: durationMs, error: error, metadata: metadata)
75
+ captureRequestError(error, context: span.traceContext, handler: onRequestError)
87
76
  throw error
88
77
  }
89
78
  }
90
79
 
91
- private func captureSpan(_ capture: URLSessionTraceCapture) {
92
- do {
80
+ private func captureRequestError(
81
+ _ requestError: any Error,
82
+ context: LogBrewTraceContext,
83
+ handler: (@Sendable (any Error) throws -> Void)?,
84
+ ) {
85
+ guard let handler else {
86
+ return
87
+ }
88
+ capture {
89
+ try LogBrewTrace.withContext(context) {
90
+ try handler(requestError)
91
+ }
92
+ }
93
+ }
94
+
95
+ private func captureSpan(
96
+ _ eventID: String?,
97
+ span: LogBrewURLSessionSpan,
98
+ response: URLResponse? = nil,
99
+ durationMs: Double,
100
+ error: (any Error)? = nil,
101
+ metadata: Metadata?,
102
+ ) {
103
+ capture {
93
104
  try client.captureURLSessionSpan(
94
- capture.eventID ?? nextEventID(),
105
+ eventID ?? nextEventID(),
95
106
  timestamp: timestampProvider(),
96
- span: capture.span,
97
- statusCode: (capture.response as? HTTPURLResponse)?.statusCode,
98
- durationMs: capture.durationMs,
99
- error: capture.error,
100
- metadata: capture.metadata,
107
+ span: span,
108
+ statusCode: (response as? HTTPURLResponse)?.statusCode,
109
+ durationMs: durationMs,
110
+ error: error,
111
+ metadata: metadata,
101
112
  )
113
+ }
114
+ }
115
+
116
+ private func capture(_ operation: () throws -> Void) {
117
+ do {
118
+ try operation()
102
119
  } catch {
103
120
  onCaptureError?(error)
104
121
  }
@@ -113,39 +130,14 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
113
130
  }
114
131
 
115
132
  private func nextEventID() -> String {
116
- lock.lock()
117
- defer {
118
- lock.unlock()
133
+ lock.withLock {
134
+ let eventID = "\(eventIDPrefix)_\(nextEventSequence)"
135
+ nextEventSequence += 1
136
+ return eventID
119
137
  }
120
- let eventID = "\(eventIDPrefix)_\(nextEventSequence)"
121
- nextEventSequence += 1
122
- return eventID
123
- }
124
-
125
- private static func defaultTimestamp() -> String {
126
- let formatter = ISO8601DateFormatter()
127
- formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
128
- return formatter.string(from: Date())
129
138
  }
130
139
 
131
140
  private static func defaultNowMs() -> Double {
132
141
  ProcessInfo.processInfo.systemUptime * 1000
133
142
  }
134
143
  }
135
-
136
- private struct URLSessionTraceCapture {
137
- let eventID: String?
138
- let span: LogBrewURLSessionSpan
139
- let response: URLResponse?
140
- let durationMs: Double
141
- let error: (any Error)?
142
- let metadata: Metadata?
143
- }
144
-
145
- private struct LogBrewURLSessionDataLoader: @unchecked Sendable {
146
- let session: URLSession
147
-
148
- func data(for request: URLRequest) async throws -> (Data, URLResponse) {
149
- try await session.data(for: request)
150
- }
151
- }
@@ -2,6 +2,12 @@
2
2
  // Edit the canonical Swift source, then regenerate this package boundary.
3
3
  import Foundation
4
4
 
5
+ func currentTelemetryTimestamp() -> String {
6
+ let formatter = ISO8601DateFormatter()
7
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
8
+ return formatter.string(from: Date())
9
+ }
10
+
5
11
  func validateRelease(_ attributes: ReleaseAttributes) throws -> ReleaseAttributes {
6
12
  try requireNonEmpty("release version", attributes.version)
7
13
  if let commit = attributes.commit {
@@ -43,7 +43,6 @@ struct NativeCrashDiagnosticSnapshot: Codable, Equatable {
43
43
  ? nil
44
44
  : TelemetryContext(trace: trace, session: session, subject: subject)
45
45
  }
46
-
47
46
  }
48
47
 
49
48
  enum NativeCrashCorrelation {
@@ -15,10 +15,11 @@
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",
18
- "swift/logbrew-swift/Sources/LogBrew/LogBrewLogger.swift": "d3266353a5f5139a2a1ca1a86bacce793518eb1489bf67ed9e3720b57b1c29b2",
18
+ "swift/logbrew-swift/Sources/LogBrew/LogBrewLogger.swift": "96e440f411c6276056e10c5feca82eb24fce9824632c7dbcedfd99b49d8332fc",
19
19
  "swift/logbrew-swift/Sources/LogBrew/LogBrewTrace.swift": "f67edf4ca464e6a6eb1c7a22cd4997928cd27b6c2d07535855a18a053d8de924",
20
20
  "swift/logbrew-swift/Sources/LogBrew/Metadata.swift": "65840ebb3f3b79fbfc1f5faa3026c2583319e89ff26061676dc552c03ee15dbe",
21
21
  "swift/logbrew-swift/Sources/LogBrew/NativeStackFrame.swift": "ecbd418969ec4a31503d9916a8bf27f22e6863356c6fabe457c1e61027a8d805",
22
+ "swift/logbrew-swift/Sources/LogBrew/OperationTrace.swift": "d7383d1926c7276cc97f03537e4587e20e4495f80226b5113e8ef10397f9c702",
22
23
  "swift/logbrew-swift/Sources/LogBrew/ProductTimeline.swift": "209906aa0e8f096a347e35832840522710eadfb530053253cd73c748518e362d",
23
24
  "swift/logbrew-swift/Sources/LogBrew/PublicTypes.swift": "a21aee058f716ec9d41d54ad032b2c7a4d64a4b99d749a9fa2be3fd2df5c6568",
24
25
  "swift/logbrew-swift/Sources/LogBrew/TelemetryContext.swift": "ce3ac93a62a345f8c9a8f4c69ce84efc991bf02a688f49bad5e68edcff2f6e08",
@@ -26,14 +27,14 @@
26
27
  "swift/logbrew-swift/Sources/LogBrew/TraceEvidence.swift": "40aaa42976164c53d9f4d9e9e30d236534238da43bbe90832a673c8809ad54b6",
27
28
  "swift/logbrew-swift/Sources/LogBrew/Transport.swift": "73eec81aaebec4bc922b2a372429c88fcd3e85606a7862cd08e1795eb2a8e6cd",
28
29
  "swift/logbrew-swift/Sources/LogBrew/URLSessionTrace.swift": "d0076c72671716c41bd7498d0509e453b8182c418156e169196fee9f26fc66b1",
29
- "swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "0cdf08f92d6111c0dc2883b87837b81f69b5c2f9a8e5501780a114c23a741af7",
30
- "swift/logbrew-swift/Sources/LogBrew/Validation.swift": "07ff5c2daeb829b1df4f50edb0b79727f9a81b1c9b5cde5f995f5ad6ce660aa7",
30
+ "swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "8f45867e5ec48fc7be8f4c7a015e887ed7f49f3a0ffb11bd850e1bb7e937f9a0",
31
+ "swift/logbrew-swift/Sources/LogBrew/Validation.swift": "4ce069077dcca7899b15733e9aee921392da1e2136996ae3d4eb37bd73a5af07",
31
32
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "a94dfce653e3e7f381be42b6d2c4540f0be77e83777ecc53e06682cd2e569c5a",
32
33
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "a56a2f6c6c7f1db900804a37df51aebba7b83dce14fa7816de5753c33409e4a5",
33
34
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashStorageDirectory.swift": "7cd566703cbf704dc99155451ee93dcace12c35c805abc09a6dcf23975cf43f9",
34
35
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeArtifactIdentity.swift": "3d785ad717dcf7302451531c18b5e1183983270a6cd78dd543a407e3facc6ae0",
35
36
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "0b773a63987b6c694b044e56c0f81878f4f5e71eae3fdd126744cba3cf15aac1",
36
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "e5a7c25448c83ca2ae149c650ecfc72c5193741291a6ff9623eabadad8a9c440",
37
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "001cba0fc6fa0d03ac058170b3e998b7db415a1b1038acaaec481183f26e8f0d",
37
38
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "ce191e46298d91d87b379528d610caa88b924b15a8dd912b4a164b51eb172a7a",
38
39
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "70aa9700efd26d03b9656f1df96876de0b9eb769c03512ec53f667696773954f",
39
40
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashReplay.swift": "3eb3d93b32cf14de416f56fa30d6d65e6f268b5ec9d1a67081d7525c4efd4f80",