@logbrew/react-native 0.1.21 → 0.1.23
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 +20 -5
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +16 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +5 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +5 -0
- package/apple-native-diagnostics.d.ts +5 -0
- package/apple-native-diagnostics.js +36 -3
- package/index.cjs +27 -5
- package/index.native.js +8 -1
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +6 -3
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnosticEvidence.swift +2 -1
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +6 -2
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +20 -14
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +113 -79
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +5 -1
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -5
- package/ios/LBRNFatalStoreModule.mm +19 -0
- package/lifecycle.js +6 -120
- package/native-bridge.js +5 -120
- package/package.json +1 -1
- package/resource-fetch.js +5 -463
- package/src/NativeLogBrewFatalStore.ts +1 -0
package/README.md
CHANGED
|
@@ -327,7 +327,11 @@ setLogBrewAppleNativeCrashContext({
|
|
|
327
327
|
schemaVersion: 1,
|
|
328
328
|
trace: { traceId: trace.traceId, spanId: trace.spanId, sampled: trace.sampled },
|
|
329
329
|
session: { id: "session_123" },
|
|
330
|
-
subject: { id: "subject_456", kind: "user" }
|
|
330
|
+
subject: { id: "subject_456", kind: "user" },
|
|
331
|
+
impact: {
|
|
332
|
+
failedAction: "checkout.submit",
|
|
333
|
+
userVisibleOutcome: "The order was not confirmed."
|
|
334
|
+
}
|
|
331
335
|
});
|
|
332
336
|
|
|
333
337
|
void replayLogBrewAppleNativeDiagnostics().catch((error) => {
|
|
@@ -342,10 +346,15 @@ allowed, but only one integration may install native fatal capture in a given
|
|
|
342
346
|
process. LogBrew cannot transfer or remove that ownership before process
|
|
343
347
|
restart.
|
|
344
348
|
|
|
345
|
-
Update this snapshot when the active trace, session, or
|
|
349
|
+
Update this snapshot when the active trace, session, subject, or failed action changes, and
|
|
346
350
|
clear it with `setLogBrewAppleNativeCrashContext(null)` on logout or session
|
|
347
351
|
end. The crash report keeps one atomic snapshot from the crashed process, so a
|
|
348
|
-
later app launch cannot replace it.
|
|
352
|
+
later app launch cannot replace it. `impact` is explicit app knowledge. Use a
|
|
353
|
+
stable action name and an optional safe user-visible result. Never include raw
|
|
354
|
+
input, request data, authentication values, or identity. Session and subject values must
|
|
355
|
+
be opaque app-owned identifiers made from ASCII letters, numbers, `_`, or `-`.
|
|
356
|
+
Resource context, tags, arbitrary fields, and values over the fixed 4 KiB
|
|
357
|
+
snapshot limit fail before storage.
|
|
349
358
|
|
|
350
359
|
The iOS `createLogBrewReactNativeClient()` entry also mirrors its validated
|
|
351
360
|
breadcrumb history into native crash capture. Install Apple diagnostics first,
|
|
@@ -579,11 +588,17 @@ export function App({ client }) {
|
|
|
579
588
|
|
|
580
589
|
Screen views carry the versioned `screen_view` analytics classification, and explicit product actions carry `interaction`. The SDK uses the app-owned screen name as a bounded surface and does not inspect view hierarchies, selectors, or input values. Caller metadata cannot replace the reserved classification. See the repository [product analytics capture contract](../../docs/product-analytics-contract.md).
|
|
581
590
|
|
|
582
|
-
The package ships a `react-native` entry that
|
|
591
|
+
The package ships a `react-native` entry that binds platform state and linked native services for Metro, while the default server entry accepts runtime dependencies explicitly. That keeps mobile setup explicit without treating a server process as a native runtime.
|
|
583
592
|
|
|
584
593
|
## Trace Propagation
|
|
585
594
|
|
|
586
|
-
Use an active trace when one product operation should connect screen views, logs, handled errors, actions, network milestones, explicit spans, and outbound request headers. `createReactNativeTraceContext()` continues a valid W3C `traceparent` with a fresh local span ID and falls back to a local root when the incoming value is missing or malformed
|
|
595
|
+
Use an active trace when one product operation should connect screen views, logs, handled errors, actions, network milestones, explicit spans, and outbound request headers. `createReactNativeTraceContext()` continues a valid W3C `traceparent` with a fresh local span ID and falls back to a local root when the incoming value is missing or malformed.
|
|
596
|
+
|
|
597
|
+
The React Native entry creates trace and span IDs with the linked platform's
|
|
598
|
+
cryptographic random source, or Expo Crypto in a managed runtime. It fails
|
|
599
|
+
closed when neither source exists and never falls back to `Math.random` or
|
|
600
|
+
predictable bytes. Callers may still inject `randomValues` when deterministic
|
|
601
|
+
input control is required.
|
|
587
602
|
|
|
588
603
|
```js
|
|
589
604
|
import {
|
|
@@ -12,6 +12,7 @@ import java.io.File;
|
|
|
12
12
|
import java.nio.charset.StandardCharsets;
|
|
13
13
|
import java.security.MessageDigest;
|
|
14
14
|
import java.security.NoSuchAlgorithmException;
|
|
15
|
+
import java.security.SecureRandom;
|
|
15
16
|
import java.util.ArrayList;
|
|
16
17
|
import java.util.Arrays;
|
|
17
18
|
import java.util.HashMap;
|
|
@@ -22,6 +23,7 @@ import java.util.Set;
|
|
|
22
23
|
|
|
23
24
|
final class FatalStoreModuleImpl {
|
|
24
25
|
static final String NAME = "LogBrewFatalStore";
|
|
26
|
+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
|
25
27
|
|
|
26
28
|
private static final Set<String> RECORD_KEYS =
|
|
27
29
|
new HashSet<>(
|
|
@@ -51,6 +53,20 @@ final class FatalStoreModuleImpl {
|
|
|
51
53
|
new AndroidParentDirectorySync());
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
String secureRandomHex(double length) {
|
|
57
|
+
Integer byteCount = integer(length);
|
|
58
|
+
if (byteCount == null || byteCount < 1 || byteCount > 64) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
byte[] bytes = new byte[byteCount];
|
|
62
|
+
SECURE_RANDOM.nextBytes(bytes);
|
|
63
|
+
StringBuilder output = new StringBuilder(byteCount * 2);
|
|
64
|
+
for (byte value : bytes) {
|
|
65
|
+
output.append(String.format(java.util.Locale.ROOT, "%02x", value & 0xff));
|
|
66
|
+
}
|
|
67
|
+
return output.toString();
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
WritableMap loadEventRecords(String queueKey) {
|
|
55
71
|
try {
|
|
56
72
|
EventRecordStore eventStore = eventStore(queueKey);
|
|
@@ -17,6 +17,11 @@ final class FatalStoreModule extends NativeLogBrewFatalStoreSpec {
|
|
|
17
17
|
return FatalStoreModuleImpl.NAME;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
@Override
|
|
21
|
+
public String secureRandomHex(double length) {
|
|
22
|
+
return implementation.secureRandomHex(length);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
@Override
|
|
21
26
|
public WritableMap writeFatalRecord(ReadableMap record) {
|
|
22
27
|
return implementation.writeFatalRecord(record);
|
|
@@ -19,6 +19,11 @@ final class FatalStoreModule extends ReactContextBaseJavaModule {
|
|
|
19
19
|
return FatalStoreModuleImpl.NAME;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
23
|
+
public String secureRandomHex(double length) {
|
|
24
|
+
return implementation.secureRandomHex(length);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
23
28
|
public WritableMap writeFatalRecord(ReadableMap record) {
|
|
24
29
|
return implementation.writeFatalRecord(record);
|
|
@@ -36,6 +36,11 @@ export type LogBrewAppleNativeDiagnosticsReplayResult = Readonly<{
|
|
|
36
36
|
|
|
37
37
|
export type LogBrewAppleNativeCrashContext = {
|
|
38
38
|
schemaVersion: 1;
|
|
39
|
+
/** App-reported failed action and optional user-visible result. Never include raw input or identity. */
|
|
40
|
+
impact?: {
|
|
41
|
+
failedAction: string;
|
|
42
|
+
userVisibleOutcome?: string;
|
|
43
|
+
};
|
|
39
44
|
trace?: TelemetryTraceContext;
|
|
40
45
|
session?: TelemetrySessionContext;
|
|
41
46
|
subject?: TelemetrySubjectContext;
|
|
@@ -144,7 +144,11 @@ function normalizeConfiguration(configuration) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function normalizeCorrelationContext(context) {
|
|
147
|
-
const source = exactObject(
|
|
147
|
+
const source = exactObject(
|
|
148
|
+
context,
|
|
149
|
+
["impact", "schemaVersion", "session", "subject", "trace"],
|
|
150
|
+
"context"
|
|
151
|
+
);
|
|
148
152
|
if (source.schemaVersion !== 1) {
|
|
149
153
|
throw configurationError("context schemaVersion must be 1");
|
|
150
154
|
}
|
|
@@ -185,8 +189,26 @@ function normalizeCorrelationContext(context) {
|
|
|
185
189
|
}
|
|
186
190
|
output.subject = { id: opaqueCorrelationId(subject.id, "subject id"), kind: subject.kind };
|
|
187
191
|
}
|
|
192
|
+
if (source.impact !== undefined) {
|
|
193
|
+
const impact = exactObject(
|
|
194
|
+
source.impact,
|
|
195
|
+
["failedAction", "userVisibleOutcome"],
|
|
196
|
+
"context impact"
|
|
197
|
+
);
|
|
198
|
+
output.impact = {
|
|
199
|
+
failedAction: diagnosticText(impact.failedAction, 256, true, "failedAction")
|
|
200
|
+
};
|
|
201
|
+
if (impact.userVisibleOutcome !== undefined) {
|
|
202
|
+
output.impact.userVisibleOutcome = diagnosticText(
|
|
203
|
+
impact.userVisibleOutcome,
|
|
204
|
+
512,
|
|
205
|
+
false,
|
|
206
|
+
"userVisibleOutcome"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
188
210
|
if (Object.keys(output).length === 1) {
|
|
189
|
-
throw configurationError("context must include trace, session, or
|
|
211
|
+
throw configurationError("context must include trace, session, subject, or impact");
|
|
190
212
|
}
|
|
191
213
|
return output;
|
|
192
214
|
}
|
|
@@ -218,6 +240,17 @@ function opaqueCorrelationId(value, name) {
|
|
|
218
240
|
return value;
|
|
219
241
|
}
|
|
220
242
|
|
|
243
|
+
function diagnosticText(value, maximum, rejectLocationText, name) {
|
|
244
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
245
|
+
if (normalized.length === 0
|
|
246
|
+
|| Array.from(normalized).length > maximum
|
|
247
|
+
|| hasControlCharacter(normalized)
|
|
248
|
+
|| (rejectLocationText && /[?#]/u.test(normalized))) {
|
|
249
|
+
throw configurationError(`context impact ${name} is invalid`);
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
|
|
221
254
|
function requireApplePlatform() {
|
|
222
255
|
if (Platform?.OS !== "ios") {
|
|
223
256
|
throw new SdkError(
|
|
@@ -366,7 +399,7 @@ function exactDeliveryEndpoint(value) {
|
|
|
366
399
|
function hasControlCharacter(value) {
|
|
367
400
|
for (const scalar of value) {
|
|
368
401
|
const codePoint = scalar.codePointAt(0);
|
|
369
|
-
if (codePoint <= 0x1f || codePoint
|
|
402
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
370
403
|
return true;
|
|
371
404
|
}
|
|
372
405
|
}
|
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.23";
|
|
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.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,
|
|
@@ -34,6 +34,13 @@ export {
|
|
|
34
34
|
|
|
35
35
|
export * from "./index.js";
|
|
36
36
|
|
|
37
|
+
const nativeRuntime = TurboModuleRegistry?.get?.("LogBrewFatalStore")
|
|
38
|
+
?? NativeModules?.LogBrewFatalStore;
|
|
39
|
+
if (typeof nativeRuntime?.secureRandomHex === "function") {
|
|
40
|
+
globalThis[Symbol.for("co.logbrew.react-native.secure-random-hex")] =
|
|
41
|
+
nativeRuntime.secureRandomHex.bind(nativeRuntime);
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export function createLogBrewReactNativeClient(config = {}) {
|
|
38
45
|
const input = config !== null && typeof config === "object" ? config : {};
|
|
39
46
|
const {
|
|
@@ -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.23"
|
|
7
7
|
|
|
8
8
|
private let lock = NSLock()
|
|
9
9
|
private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
|
|
@@ -149,8 +149,11 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
|
149
149
|
return failure("native_diagnostics_not_installed")
|
|
150
150
|
}
|
|
151
151
|
do {
|
|
152
|
-
let
|
|
153
|
-
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
|
+
)
|
|
154
157
|
return ["status": rawContext == nil ? "cleared" : "updated"]
|
|
155
158
|
} catch let error as NativeCrashError {
|
|
156
159
|
return failure(error.code.rawValue)
|
|
@@ -77,7 +77,8 @@ public struct IssueDiagnosticEvidence: Codable, Equatable, Sendable {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
@_spi(CrashReplay)
|
|
81
|
+
public func validateIssueDiagnosticEvidence(_ value: IssueDiagnosticEvidence) throws -> IssueDiagnosticEvidence {
|
|
81
82
|
let cause = try value.likelyRootCause.map {
|
|
82
83
|
try issueText($0, label: "issue evidence likelyRootCause", maximum: 1024)
|
|
83
84
|
}
|
|
@@ -23,6 +23,9 @@ struct CrashReportSanitizer {
|
|
|
23
23
|
let nativeStackFrames = NativeStackFrameSanitizer().frames(from: rawReport)
|
|
24
24
|
let artifactIdentity = try NativeArtifactIdentityValue.persistedIdentity(in: rawReport)
|
|
25
25
|
let correlation = NativeCrashCorrelation.captured(in: rawReport)
|
|
26
|
+
let correlationState = correlation.0?.correlationContext == nil && correlation.1 == .captured
|
|
27
|
+
? NativeCrashContextState.notCaptured
|
|
28
|
+
: correlation.1
|
|
26
29
|
let breadcrumbs = NativeCrashBreadcrumbs.captured(in: rawReport)
|
|
27
30
|
return NativeCrashRecord(
|
|
28
31
|
eventID: uuid.uuidString.lowercased(),
|
|
@@ -33,9 +36,10 @@ struct CrashReportSanitizer {
|
|
|
33
36
|
context: nativeCrashContext(
|
|
34
37
|
system: rawReport["system"] as? [String: Any],
|
|
35
38
|
artifactIdentity: artifactIdentity,
|
|
36
|
-
correlation: correlation.0,
|
|
39
|
+
correlation: correlation.0?.correlationContext,
|
|
37
40
|
),
|
|
38
|
-
correlationState:
|
|
41
|
+
correlationState: correlationState,
|
|
42
|
+
impact: correlation.0?.impact,
|
|
39
43
|
breadcrumbs: breadcrumbs.0?.breadcrumbs,
|
|
40
44
|
breadcrumbsTruncated: breadcrumbs.0?.truncated,
|
|
41
45
|
breadcrumbState: breadcrumbs.1,
|
|
@@ -109,17 +109,17 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
|
|
|
109
109
|
/// Replaces the crash-time trace, session, and opaque subject snapshot in one bounded write.
|
|
110
110
|
@nonobjc
|
|
111
111
|
public func setCorrelationContext(_ context: TelemetryContext?) throws {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
try setDiagnosticContext(context: context, impact: nil)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@nonobjc
|
|
116
|
+
func setDiagnosticContext(
|
|
117
|
+
context: TelemetryContext?,
|
|
118
|
+
impact: IssueImpactEvidence?,
|
|
119
|
+
) throws {
|
|
120
|
+
try setSnapshot(forKey: NativeCrashCorrelation.reportKey) {
|
|
121
|
+
try NativeCrashCorrelation.encoded(context: context, impact: impact)
|
|
117
122
|
}
|
|
118
|
-
try verifyStorageLocked()
|
|
119
|
-
try driver.setUserInfo(
|
|
120
|
-
NativeCrashCorrelation.encoded(context),
|
|
121
|
-
forKey: NativeCrashCorrelation.reportKey,
|
|
122
|
-
)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/// Replaces the crash-time breadcrumb snapshot in one bounded write.
|
|
@@ -127,6 +127,15 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
|
|
|
127
127
|
public func setBreadcrumbs(
|
|
128
128
|
_ breadcrumbs: [IssueBreadcrumb]?,
|
|
129
129
|
truncated: Bool = false,
|
|
130
|
+
) throws {
|
|
131
|
+
try setSnapshot(forKey: NativeCrashBreadcrumbs.reportKey) {
|
|
132
|
+
try NativeCrashBreadcrumbs.encoded(breadcrumbs, truncated: truncated)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func setSnapshot(
|
|
137
|
+
forKey key: String,
|
|
138
|
+
encoded: () throws -> String?,
|
|
130
139
|
) throws {
|
|
131
140
|
lock.lock()
|
|
132
141
|
defer { lock.unlock() }
|
|
@@ -135,10 +144,7 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
|
|
|
135
144
|
throw NativeCrashError(.notInstalled)
|
|
136
145
|
}
|
|
137
146
|
try verifyStorageLocked()
|
|
138
|
-
try driver.setUserInfo(
|
|
139
|
-
NativeCrashBreadcrumbs.encoded(breadcrumbs, truncated: truncated),
|
|
140
|
-
forKey: NativeCrashBreadcrumbs.reportKey,
|
|
141
|
-
)
|
|
147
|
+
try driver.setUserInfo(encoded(), forKey: key)
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
@objc(replayPendingReportsWithHandler:error:)
|
|
@@ -8,16 +8,60 @@ enum NativeCrashContextState: String {
|
|
|
8
8
|
case unavailable
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
struct NativeCrashDiagnosticSnapshot: Codable, Equatable {
|
|
12
|
+
let schemaVersion: Int
|
|
13
|
+
let trace: TelemetryTraceContext?
|
|
14
|
+
let session: TelemetrySessionContext?
|
|
15
|
+
let subject: TelemetrySubjectContext?
|
|
16
|
+
let impact: IssueImpactEvidence?
|
|
17
|
+
|
|
18
|
+
init(context: TelemetryContext?, impact: IssueImpactEvidence?) throws {
|
|
19
|
+
let context = try context.map(validateNativeCrashCorrelationContext)
|
|
20
|
+
let impact = try impact.map { value in
|
|
21
|
+
guard value.affectedUserSegment == nil,
|
|
22
|
+
let value = try validateIssueDiagnosticEvidence(
|
|
23
|
+
IssueDiagnosticEvidence(impact: value),
|
|
24
|
+
).impact,
|
|
25
|
+
value.failedAction != nil
|
|
26
|
+
else {
|
|
27
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
guard context != nil || impact != nil else {
|
|
32
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
33
|
+
}
|
|
34
|
+
schemaVersion = 1
|
|
35
|
+
trace = context?.trace
|
|
36
|
+
session = context?.session
|
|
37
|
+
subject = context?.subject
|
|
38
|
+
self.impact = impact
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var correlationContext: TelemetryContext? {
|
|
42
|
+
trace == nil && session == nil && subject == nil
|
|
43
|
+
? nil
|
|
44
|
+
: TelemetryContext(trace: trace, session: session, subject: subject)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
enum NativeCrashCorrelation {
|
|
12
50
|
static let reportKey = "logbrew_native_correlation"
|
|
13
|
-
private static let maximumBytes = 1024
|
|
51
|
+
private static let maximumBytes = 4 * 1024
|
|
14
52
|
|
|
15
|
-
static func encoded(
|
|
16
|
-
|
|
53
|
+
static func encoded(
|
|
54
|
+
context: TelemetryContext?,
|
|
55
|
+
impact: IssueImpactEvidence? = nil,
|
|
56
|
+
) throws -> String? {
|
|
57
|
+
guard context != nil || impact != nil else {
|
|
17
58
|
return nil
|
|
18
59
|
}
|
|
19
60
|
do {
|
|
20
|
-
let data = try
|
|
61
|
+
let data = try nativeCrashEncoded(NativeCrashDiagnosticSnapshot(
|
|
62
|
+
context: context,
|
|
63
|
+
impact: impact,
|
|
64
|
+
))
|
|
21
65
|
guard data.count <= maximumBytes, let value = String(data: data, encoding: .utf8) else {
|
|
22
66
|
throw NativeCrashError(.invalidConfiguration)
|
|
23
67
|
}
|
|
@@ -27,51 +71,23 @@ enum NativeCrashCorrelation {
|
|
|
27
71
|
}
|
|
28
72
|
}
|
|
29
73
|
|
|
30
|
-
static func captured(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
guard let value = value as? String,
|
|
35
|
-
let data = value.data(using: .utf8),
|
|
36
|
-
data.count <= maximumBytes,
|
|
37
|
-
let object = try? JSONSerialization.jsonObject(with: data),
|
|
38
|
-
let context = try? validated(object)
|
|
39
|
-
else {
|
|
40
|
-
return (nil, .unavailable)
|
|
41
|
-
}
|
|
42
|
-
return (context, .captured)
|
|
74
|
+
static func captured(
|
|
75
|
+
in rawReport: [String: Any],
|
|
76
|
+
) -> (NativeCrashDiagnosticSnapshot?, NativeCrashContextState) {
|
|
77
|
+
nativeCrashCaptured(in: rawReport, key: reportKey, maximumBytes: maximumBytes, validate: validated)
|
|
43
78
|
}
|
|
44
79
|
|
|
45
|
-
static func validated(_ object: Any) throws ->
|
|
46
|
-
|
|
47
|
-
guard
|
|
48
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
49
|
-
}
|
|
50
|
-
let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
51
|
-
guard data.count <= maximumBytes else {
|
|
80
|
+
static func validated(_ object: Any) throws -> NativeCrashDiagnosticSnapshot {
|
|
81
|
+
try nativeCrashValidated(object, maximumBytes: maximumBytes) { decoded in
|
|
82
|
+
guard decoded.schemaVersion == 1 else {
|
|
52
83
|
throw NativeCrashError(.invalidConfiguration)
|
|
53
84
|
}
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
return try NativeCrashDiagnosticSnapshot(
|
|
86
|
+
context: decoded.correlationContext,
|
|
87
|
+
impact: decoded.impact,
|
|
56
88
|
)
|
|
57
|
-
let normalized = try encoder().encode(context)
|
|
58
|
-
let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
|
|
59
|
-
guard object as? NSDictionary == normalizedObject as? NSDictionary else {
|
|
60
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
61
|
-
}
|
|
62
|
-
return context
|
|
63
|
-
} catch let error as NativeCrashError {
|
|
64
|
-
throw error
|
|
65
|
-
} catch {
|
|
66
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
67
89
|
}
|
|
68
90
|
}
|
|
69
|
-
|
|
70
|
-
private static func encoder() -> JSONEncoder {
|
|
71
|
-
let encoder = JSONEncoder()
|
|
72
|
-
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
|
73
|
-
return encoder
|
|
74
|
-
}
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
struct NativeCrashBreadcrumbSnapshot: Codable, Equatable {
|
|
@@ -98,7 +114,7 @@ enum NativeCrashBreadcrumbs {
|
|
|
98
114
|
var breadcrumbs = try values.suffix(maximumIssueBreadcrumbs).map(validateIssueBreadcrumb)
|
|
99
115
|
var wasTruncated = truncated || breadcrumbs.count < values.count
|
|
100
116
|
while !breadcrumbs.isEmpty {
|
|
101
|
-
let data = try
|
|
117
|
+
let data = try nativeCrashEncoded(NativeCrashBreadcrumbSnapshot(
|
|
102
118
|
breadcrumbs: breadcrumbs,
|
|
103
119
|
truncated: wasTruncated,
|
|
104
120
|
))
|
|
@@ -119,56 +135,74 @@ enum NativeCrashBreadcrumbs {
|
|
|
119
135
|
static func captured(
|
|
120
136
|
in rawReport: [String: Any],
|
|
121
137
|
) -> (NativeCrashBreadcrumbSnapshot?, NativeCrashContextState) {
|
|
122
|
-
|
|
123
|
-
return (nil, .notCaptured)
|
|
124
|
-
}
|
|
125
|
-
guard let value = value as? String,
|
|
126
|
-
let data = value.data(using: .utf8),
|
|
127
|
-
data.count <= maximumBytes,
|
|
128
|
-
let object = try? JSONSerialization.jsonObject(with: data),
|
|
129
|
-
let snapshot = try? validated(object)
|
|
130
|
-
else {
|
|
131
|
-
return (nil, .unavailable)
|
|
132
|
-
}
|
|
133
|
-
return (snapshot, .captured)
|
|
138
|
+
nativeCrashCaptured(in: rawReport, key: reportKey, maximumBytes: maximumBytes, validate: validated)
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
static func validated(_ object: Any) throws -> NativeCrashBreadcrumbSnapshot {
|
|
137
|
-
|
|
138
|
-
guard JSONSerialization.isValidJSONObject(object) else {
|
|
139
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
140
|
-
}
|
|
141
|
-
let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
142
|
-
guard data.count <= maximumBytes else {
|
|
143
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
144
|
-
}
|
|
145
|
-
let decoded = try JSONDecoder().decode(NativeCrashBreadcrumbSnapshot.self, from: data)
|
|
142
|
+
try nativeCrashValidated(object, maximumBytes: maximumBytes) { decoded in
|
|
146
143
|
guard decoded.schemaVersion == 1,
|
|
147
144
|
!decoded.breadcrumbs.isEmpty,
|
|
148
145
|
decoded.breadcrumbs.count <= maximumIssueBreadcrumbs
|
|
149
146
|
else {
|
|
150
147
|
throw NativeCrashError(.invalidConfiguration)
|
|
151
148
|
}
|
|
152
|
-
|
|
149
|
+
return try NativeCrashBreadcrumbSnapshot(
|
|
153
150
|
breadcrumbs: decoded.breadcrumbs.map(validateIssueBreadcrumb),
|
|
154
151
|
truncated: decoded.truncated,
|
|
155
152
|
)
|
|
156
|
-
let normalized = try encoder().encode(snapshot)
|
|
157
|
-
let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
|
|
158
|
-
guard object as? NSDictionary == normalizedObject as? NSDictionary else {
|
|
159
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
160
|
-
}
|
|
161
|
-
return snapshot
|
|
162
|
-
} catch let error as NativeCrashError {
|
|
163
|
-
throw error
|
|
164
|
-
} catch {
|
|
165
|
-
throw NativeCrashError(.invalidConfiguration)
|
|
166
153
|
}
|
|
167
154
|
}
|
|
155
|
+
}
|
|
168
156
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
157
|
+
private func nativeCrashCaptured<Value>(
|
|
158
|
+
in rawReport: [String: Any],
|
|
159
|
+
key: String,
|
|
160
|
+
maximumBytes: Int,
|
|
161
|
+
validate: (Any) throws -> Value,
|
|
162
|
+
) -> (Value?, NativeCrashContextState) {
|
|
163
|
+
guard let user = rawReport["user"] as? [String: Any], let rawValue = user[key] else {
|
|
164
|
+
return (nil, .notCaptured)
|
|
165
|
+
}
|
|
166
|
+
guard let value = rawValue as? String,
|
|
167
|
+
let data = value.data(using: .utf8),
|
|
168
|
+
data.count <= maximumBytes,
|
|
169
|
+
let object = try? JSONSerialization.jsonObject(with: data),
|
|
170
|
+
let snapshot = try? validate(object)
|
|
171
|
+
else {
|
|
172
|
+
return (nil, .unavailable)
|
|
173
|
+
}
|
|
174
|
+
return (snapshot, .captured)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private func nativeCrashValidated<Value: Codable>(
|
|
178
|
+
_ object: Any,
|
|
179
|
+
maximumBytes: Int,
|
|
180
|
+
normalize: (Value) throws -> Value,
|
|
181
|
+
) throws -> Value {
|
|
182
|
+
do {
|
|
183
|
+
guard JSONSerialization.isValidJSONObject(object) else {
|
|
184
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
185
|
+
}
|
|
186
|
+
let source = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
187
|
+
guard source.count <= maximumBytes else {
|
|
188
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
189
|
+
}
|
|
190
|
+
let value = try normalize(JSONDecoder().decode(Value.self, from: source))
|
|
191
|
+
let normalized = try nativeCrashEncoded(value)
|
|
192
|
+
let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
|
|
193
|
+
guard object as? NSDictionary == normalizedObject as? NSDictionary else {
|
|
194
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
195
|
+
}
|
|
196
|
+
return value
|
|
197
|
+
} catch let error as NativeCrashError {
|
|
198
|
+
throw error
|
|
199
|
+
} catch {
|
|
200
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
173
201
|
}
|
|
174
202
|
}
|
|
203
|
+
|
|
204
|
+
private func nativeCrashEncoded(_ value: some Encodable) throws -> Data {
|
|
205
|
+
let encoder = JSONEncoder()
|
|
206
|
+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
|
207
|
+
return try encoder.encode(value)
|
|
208
|
+
}
|