@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.
- package/LogBrewReactNative.podspec +1 -0
- package/README.md +70 -7
- package/android/CMakeLists.txt +7 -0
- package/android/build.gradle +12 -0
- package/android/src/main/cpp/android_diagnostics.cpp +363 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +127 -156
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
- package/android-native-diagnostics.d.ts +29 -0
- package/android-native-diagnostics.js +176 -0
- package/fatal-replay.cjs +35 -3
- package/global-errors.d.cts +3 -3
- package/global-errors.d.ts +3 -3
- package/global-errors.native.js +1 -12
- package/index.cjs +27 -5
- package/index.native.d.ts +4 -0
- package/index.native.js +29 -4
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
- package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
- package/ios/LBRNFatalStoreModule.mm +34 -86
- package/package.json +17 -5
- package/persistent-delivery.native.js +4 -3
- package/resource-fetch.js +5 -463
- package/src/NativeLogBrewFatalStore.ts +6 -4
- package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
- package/ios/LBRNFatalRecordStore.h +0 -25
- 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
|
-
|
|
360
|
+
...(record.stackFrames.length === 0
|
|
361
|
+
? {}
|
|
362
|
+
: { stackFrames: record.stackFrames.map((frame) => Object.freeze({ ...frame })) }),
|
|
331
363
|
title: eventMessage
|
|
332
364
|
});
|
|
333
365
|
}
|
package/global-errors.d.cts
CHANGED
|
@@ -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
|
|
244
|
-
* Direct Node ESM/CJS callers can inject a compatible
|
|
245
|
-
*
|
|
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(
|
package/global-errors.d.ts
CHANGED
|
@@ -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
|
|
244
|
-
* Direct Node ESM/CJS callers can inject a compatible
|
|
245
|
-
*
|
|
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(
|
package/global-errors.native.js
CHANGED
|
@@ -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 :
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
105
|
+
eventID ?? nextEventID(),
|
|
95
106
|
timestamp: timestampProvider(),
|
|
96
|
-
span:
|
|
97
|
-
statusCode: (
|
|
98
|
-
durationMs:
|
|
99
|
-
error:
|
|
100
|
-
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.
|
|
117
|
-
|
|
118
|
-
|
|
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 {
|
|
@@ -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": "
|
|
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": "
|
|
30
|
-
"swift/logbrew-swift/Sources/LogBrew/Validation.swift": "
|
|
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": "
|
|
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",
|