@logbrew/react-native 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/apple-native-diagnostics.d.ts +5 -0
- package/apple-native-diagnostics.js +53 -6
- package/index.cjs +1 -1
- package/index.native.js +57 -4
- package/instrumentation.js +3 -632
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +5 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +31 -3
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnosticEvidence.swift +2 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +3 -2
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +10 -2
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +29 -4
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +169 -38
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +27 -5
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +6 -6
- package/lifecycle.js +6 -120
- package/metadata.js +10 -181
- package/native-bridge.js +5 -120
- package/package.json +2 -2
- package/src/NativeLogBrewAppleDiagnostics.ts +3 -0
package/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,25 @@ 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.
|
|
358
|
+
|
|
359
|
+
The iOS `createLogBrewReactNativeClient()` entry also mirrors its validated
|
|
360
|
+
breadcrumb history into native crash capture. Install Apple diagnostics first,
|
|
361
|
+
then use the normal screen, app-state, action, network, or `addBreadcrumb()`
|
|
362
|
+
helpers. A fatal crash retains the newest complete entries in oldest-to-newest
|
|
363
|
+
order. The snapshot keeps at most 64 entries and 64 KiB, drops oldest entries
|
|
364
|
+
when either limit is reached, and reports truncation. `clearBreadcrumbs()`
|
|
365
|
+
clears both the JavaScript history and its native crash snapshot. Missing,
|
|
366
|
+
corrupt, and captured breadcrumb state remains explicit on replay; corrupt
|
|
367
|
+
optional context never discards the crash.
|
|
349
368
|
|
|
350
369
|
Installation creates app-private, data-protected storage that iOS excludes from
|
|
351
370
|
device data archives. Pending reports are partitioned by project, so changing
|
|
@@ -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;
|
|
@@ -48,17 +48,31 @@ export function setLogBrewAppleNativeCrashContext(context) {
|
|
|
48
48
|
requireApplePlatform();
|
|
49
49
|
const nativeModule = requireNativeModule();
|
|
50
50
|
const payload = context === null ? null : normalizeCorrelationContext(context);
|
|
51
|
-
|
|
51
|
+
return updateNativeSnapshot(nativeModule, "setNativeDiagnosticsContext", payload, "context");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function syncLogBrewAppleNativeCrashBreadcrumbs(snapshot) {
|
|
55
|
+
requireApplePlatform();
|
|
56
|
+
return updateNativeSnapshot(
|
|
57
|
+
requireNativeModule(),
|
|
58
|
+
"setNativeDiagnosticsBreadcrumbs",
|
|
59
|
+
snapshot,
|
|
60
|
+
"breadcrumbs"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function updateNativeSnapshot(nativeModule, method, payload, label) {
|
|
65
|
+
const result = callNative(nativeModule, method, payload);
|
|
52
66
|
const expectedStatus = payload === null ? "cleared" : "updated";
|
|
53
67
|
if (isErrorResult(result)) {
|
|
54
|
-
throw new SdkError(result.code, `LogBrew Apple native diagnostics
|
|
68
|
+
throw new SdkError(result.code, `LogBrew Apple native diagnostics ${label} failed with ${result.code}`);
|
|
55
69
|
}
|
|
56
70
|
if (!isPlainObject(result)
|
|
57
71
|
|| Object.keys(result).length !== 1
|
|
58
72
|
|| result.status !== expectedStatus) {
|
|
59
73
|
throw new SdkError(
|
|
60
74
|
"native_diagnostics_invalid_response",
|
|
61
|
-
|
|
75
|
+
`LogBrew Apple native diagnostics ${label} returned an invalid response`
|
|
62
76
|
);
|
|
63
77
|
}
|
|
64
78
|
return Object.freeze({ status: expectedStatus });
|
|
@@ -130,7 +144,11 @@ function normalizeConfiguration(configuration) {
|
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
function normalizeCorrelationContext(context) {
|
|
133
|
-
const source = exactObject(
|
|
147
|
+
const source = exactObject(
|
|
148
|
+
context,
|
|
149
|
+
["impact", "schemaVersion", "session", "subject", "trace"],
|
|
150
|
+
"context"
|
|
151
|
+
);
|
|
134
152
|
if (source.schemaVersion !== 1) {
|
|
135
153
|
throw configurationError("context schemaVersion must be 1");
|
|
136
154
|
}
|
|
@@ -171,8 +189,26 @@ function normalizeCorrelationContext(context) {
|
|
|
171
189
|
}
|
|
172
190
|
output.subject = { id: opaqueCorrelationId(subject.id, "subject id"), kind: subject.kind };
|
|
173
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
|
+
}
|
|
174
210
|
if (Object.keys(output).length === 1) {
|
|
175
|
-
throw configurationError("context must include trace, session, or
|
|
211
|
+
throw configurationError("context must include trace, session, subject, or impact");
|
|
176
212
|
}
|
|
177
213
|
return output;
|
|
178
214
|
}
|
|
@@ -204,6 +240,17 @@ function opaqueCorrelationId(value, name) {
|
|
|
204
240
|
return value;
|
|
205
241
|
}
|
|
206
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
|
+
|
|
207
254
|
function requireApplePlatform() {
|
|
208
255
|
if (Platform?.OS !== "ios") {
|
|
209
256
|
throw new SdkError(
|
|
@@ -352,7 +399,7 @@ function exactDeliveryEndpoint(value) {
|
|
|
352
399
|
function hasControlCharacter(value) {
|
|
353
400
|
for (const scalar of value) {
|
|
354
401
|
const codePoint = scalar.codePointAt(0);
|
|
355
|
-
if (codePoint <= 0x1f || codePoint
|
|
402
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
356
403
|
return true;
|
|
357
404
|
}
|
|
358
405
|
}
|
package/index.cjs
CHANGED
|
@@ -16,7 +16,7 @@ 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.22";
|
|
20
20
|
const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
|
|
21
21
|
const MAX_ACTION_NAME_LENGTH = 64;
|
|
22
22
|
const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
|
package/index.native.js
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
getLogBrewAppleNativeDiagnosticsStatus,
|
|
15
15
|
installLogBrewAppleNativeDiagnostics,
|
|
16
16
|
replayLogBrewAppleNativeDiagnostics,
|
|
17
|
-
setLogBrewAppleNativeCrashContext
|
|
17
|
+
setLogBrewAppleNativeCrashContext,
|
|
18
|
+
syncLogBrewAppleNativeCrashBreadcrumbs
|
|
18
19
|
} from "./apple-native-diagnostics.js";
|
|
19
20
|
import {
|
|
20
21
|
purgeReactNativePersistentQueue,
|
|
@@ -50,7 +51,7 @@ export function createLogBrewReactNativeClient(config = {}) {
|
|
|
50
51
|
) && input.persistentQueue !== undefined;
|
|
51
52
|
const authKey = clientKey ?? apiKey;
|
|
52
53
|
if (typeof authKey !== "string" || authKey.trim() === "") {
|
|
53
|
-
return createPlatformNeutralClient(input);
|
|
54
|
+
return bindAppleNativeCrashBreadcrumbs(createPlatformNeutralClient(input));
|
|
54
55
|
}
|
|
55
56
|
const resolved = resolveReactNativePersistentEventStore({
|
|
56
57
|
authKey,
|
|
@@ -61,20 +62,72 @@ export function createLogBrewReactNativeClient(config = {}) {
|
|
|
61
62
|
hasExplicitPersistentQueue
|
|
62
63
|
});
|
|
63
64
|
try {
|
|
64
|
-
return createPlatformNeutralClient({
|
|
65
|
+
return bindAppleNativeCrashBreadcrumbs(createPlatformNeutralClient({
|
|
65
66
|
...forwarded,
|
|
66
67
|
apiKey,
|
|
67
68
|
clientKey,
|
|
68
69
|
eventStore: resolved.eventStore,
|
|
69
70
|
maxQueueBytes,
|
|
70
71
|
maxQueueSize
|
|
71
|
-
});
|
|
72
|
+
}));
|
|
72
73
|
} catch (error) {
|
|
73
74
|
resolved.abort();
|
|
74
75
|
throw error;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function bindAppleNativeCrashBreadcrumbs(client) {
|
|
80
|
+
if (Platform?.OS !== "ios") {
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
83
|
+
const addBreadcrumb = client.addBreadcrumb.bind(client);
|
|
84
|
+
const clearBreadcrumbs = client.clearBreadcrumbs.bind(client);
|
|
85
|
+
client.addBreadcrumb = (...args) => updateAppleNativeBreadcrumbs(
|
|
86
|
+
client, () => addBreadcrumb(...args)
|
|
87
|
+
);
|
|
88
|
+
client.clearBreadcrumbs = () => updateAppleNativeBreadcrumbs(client, clearBreadcrumbs);
|
|
89
|
+
syncAppleNativeCrashBreadcrumbs(client);
|
|
90
|
+
return client;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function updateAppleNativeBreadcrumbs(client, update) {
|
|
94
|
+
const previous = [client.issueBreadcrumbs.slice(), client.issueBreadcrumbsTruncated];
|
|
95
|
+
const result = update();
|
|
96
|
+
try {
|
|
97
|
+
syncAppleNativeCrashBreadcrumbs(client);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
restoreBreadcrumbs(client, previous);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restoreBreadcrumbs(client, [breadcrumbs, truncated]) {
|
|
106
|
+
client.issueBreadcrumbs.splice(0, client.issueBreadcrumbs.length, ...breadcrumbs);
|
|
107
|
+
client.issueBreadcrumbsTruncated = truncated;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function syncAppleNativeCrashBreadcrumbs(client) {
|
|
111
|
+
const snapshot = client.issueBreadcrumbs.length === 0
|
|
112
|
+
? null
|
|
113
|
+
: {
|
|
114
|
+
breadcrumbs: client.issueBreadcrumbs.map(({ data, ...breadcrumb }) => ({
|
|
115
|
+
...breadcrumb,
|
|
116
|
+
...(data === undefined ? {} : { data: { ...data } })
|
|
117
|
+
})),
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
truncated: client.issueBreadcrumbsTruncated
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
syncLogBrewAppleNativeCrashBreadcrumbs(snapshot);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code !== "native_diagnostics_unavailable"
|
|
125
|
+
&& error?.code !== "native_diagnostics_not_installed") {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
export function purgeLogBrewReactNativePersistentQueue(config = {}) {
|
|
79
132
|
purgeReactNativePersistentQueue(config);
|
|
80
133
|
}
|