@logbrew/react-native 0.1.18 → 0.1.20
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 +20 -2
- package/apple-native-diagnostics.d.ts +20 -0
- package/apple-native-diagnostics.js +97 -1
- package/expo.cjs +8 -14
- package/index.cjs +1 -1
- package/index.js +36 -1207
- package/index.native.d.ts +1 -0
- package/index.native.js +6 -3
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +5 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +23 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +16 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +5 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +22 -10
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +16 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +77 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +11 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +6 -5
- package/package.json +1 -1
- package/src/NativeLogBrewAppleDiagnostics.ts +3 -0
package/README.md
CHANGED
|
@@ -293,10 +293,15 @@ module. For a bare React Native app, add the optional subspec inside the app
|
|
|
293
293
|
target in `ios/Podfile`, then run `pod install`:
|
|
294
294
|
|
|
295
295
|
```ruby
|
|
296
|
-
|
|
296
|
+
require 'pathname'
|
|
297
|
+
logbrew_react_native_path = Pathname(File.dirname(`bun --print "require.resolve('@logbrew/react-native')"`)).relative_path_from(Pathname(__dir__)).to_s
|
|
297
298
|
pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path
|
|
298
299
|
```
|
|
299
300
|
|
|
301
|
+
Keep the resolved package path relative to the Podfile directory. React Native
|
|
302
|
+
autolinking declares the core pod with that relative source; CocoaPods rejects
|
|
303
|
+
the same pod when the optional subspec uses an absolute source.
|
|
304
|
+
|
|
300
305
|
The default `LogBrewReactNative/Core` subspec remains unchanged and supports
|
|
301
306
|
iOS 13. `AppleNativeDiagnostics` requires iOS 15. Install it on the main thread
|
|
302
307
|
before root registration, then start replay:
|
|
@@ -304,7 +309,8 @@ before root registration, then start replay:
|
|
|
304
309
|
```js
|
|
305
310
|
import {
|
|
306
311
|
installLogBrewAppleNativeDiagnostics,
|
|
307
|
-
replayLogBrewAppleNativeDiagnostics
|
|
312
|
+
replayLogBrewAppleNativeDiagnostics,
|
|
313
|
+
setLogBrewAppleNativeCrashContext
|
|
308
314
|
} from "@logbrew/react-native/apple-native-diagnostics";
|
|
309
315
|
|
|
310
316
|
const nativeStatus = installLogBrewAppleNativeDiagnostics({
|
|
@@ -317,6 +323,13 @@ const nativeStatus = installLogBrewAppleNativeDiagnostics({
|
|
|
317
323
|
hangThresholdSeconds: 2
|
|
318
324
|
});
|
|
319
325
|
|
|
326
|
+
setLogBrewAppleNativeCrashContext({
|
|
327
|
+
schemaVersion: 1,
|
|
328
|
+
trace: { traceId: trace.traceId, spanId: trace.spanId, sampled: trace.sampled },
|
|
329
|
+
session: { id: "session_123" },
|
|
330
|
+
subject: { id: "subject_456", kind: "user" }
|
|
331
|
+
});
|
|
332
|
+
|
|
320
333
|
void replayLogBrewAppleNativeDiagnostics().catch((error) => {
|
|
321
334
|
console.warn(error?.code ?? "native_diagnostics_failed");
|
|
322
335
|
});
|
|
@@ -329,6 +342,11 @@ allowed, but only one integration may install native fatal capture in a given
|
|
|
329
342
|
process. LogBrew cannot transfer or remove that ownership before process
|
|
330
343
|
restart.
|
|
331
344
|
|
|
345
|
+
Update this snapshot when the active trace, session, or subject changes, and
|
|
346
|
+
clear it with `setLogBrewAppleNativeCrashContext(null)` on logout or session
|
|
347
|
+
end. The crash report keeps one atomic snapshot from the crashed process, so a
|
|
348
|
+
later app launch cannot replace it. Session and subject values must be opaque app-owned identifiers made from ASCII letters, numbers, `_`, or `-`. Do not use names, email addresses, IP addresses, or device identifiers. Resource context, tags, arbitrary fields, and values over the fixed 1 KiB snapshot limit fail before storage.
|
|
349
|
+
|
|
332
350
|
Installation creates app-private, data-protected storage that iOS excludes from
|
|
333
351
|
device data archives. Pending reports are partitioned by project, so changing
|
|
334
352
|
the configured project does not replay an earlier project's records with the
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TelemetrySessionContext, TelemetrySubjectContext, TelemetryTraceContext } from "@logbrew/sdk";
|
|
2
|
+
|
|
1
3
|
type LogBrewAppleNativeDiagnosticsAuth =
|
|
2
4
|
| { apiKey: string; clientKey?: never }
|
|
3
5
|
| { clientKey: string; apiKey?: never };
|
|
@@ -32,6 +34,19 @@ export type LogBrewAppleNativeDiagnosticsReplayResult = Readonly<{
|
|
|
32
34
|
pending: number;
|
|
33
35
|
}>;
|
|
34
36
|
|
|
37
|
+
export type LogBrewAppleNativeCrashContext = {
|
|
38
|
+
schemaVersion: 1;
|
|
39
|
+
trace?: TelemetryTraceContext;
|
|
40
|
+
session?: TelemetrySessionContext;
|
|
41
|
+
subject?: TelemetrySubjectContext;
|
|
42
|
+
resource?: never;
|
|
43
|
+
tags?: never;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type LogBrewAppleNativeCrashContextResult = Readonly<{
|
|
47
|
+
status: "cleared" | "updated";
|
|
48
|
+
}>;
|
|
49
|
+
|
|
35
50
|
export declare function installLogBrewAppleNativeDiagnostics(
|
|
36
51
|
configuration: LogBrewAppleNativeDiagnosticsConfiguration
|
|
37
52
|
): LogBrewAppleNativeDiagnosticsStatus;
|
|
@@ -43,10 +58,15 @@ export declare function replayLogBrewAppleNativeDiagnostics(): Promise<
|
|
|
43
58
|
export declare function getLogBrewAppleNativeDiagnosticsStatus():
|
|
44
59
|
LogBrewAppleNativeDiagnosticsStatus;
|
|
45
60
|
|
|
61
|
+
export declare function setLogBrewAppleNativeCrashContext(
|
|
62
|
+
context: LogBrewAppleNativeCrashContext | null
|
|
63
|
+
): LogBrewAppleNativeCrashContextResult;
|
|
64
|
+
|
|
46
65
|
declare const defaultExport: {
|
|
47
66
|
getLogBrewAppleNativeDiagnosticsStatus: typeof getLogBrewAppleNativeDiagnosticsStatus;
|
|
48
67
|
installLogBrewAppleNativeDiagnostics: typeof installLogBrewAppleNativeDiagnostics;
|
|
49
68
|
replayLogBrewAppleNativeDiagnostics: typeof replayLogBrewAppleNativeDiagnostics;
|
|
69
|
+
setLogBrewAppleNativeCrashContext: typeof setLogBrewAppleNativeCrashContext;
|
|
50
70
|
};
|
|
51
71
|
|
|
52
72
|
export default defaultExport;
|
|
@@ -44,6 +44,26 @@ export function getLogBrewAppleNativeDiagnosticsStatus() {
|
|
|
44
44
|
return requireStatusResult("status", result, new Set(["not_installed", "ready"]));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function setLogBrewAppleNativeCrashContext(context) {
|
|
48
|
+
requireApplePlatform();
|
|
49
|
+
const nativeModule = requireNativeModule();
|
|
50
|
+
const payload = context === null ? null : normalizeCorrelationContext(context);
|
|
51
|
+
const result = callNative(nativeModule, "setNativeDiagnosticsContext", payload);
|
|
52
|
+
const expectedStatus = payload === null ? "cleared" : "updated";
|
|
53
|
+
if (isErrorResult(result)) {
|
|
54
|
+
throw new SdkError(result.code, `LogBrew Apple native diagnostics context failed with ${result.code}`);
|
|
55
|
+
}
|
|
56
|
+
if (!isPlainObject(result)
|
|
57
|
+
|| Object.keys(result).length !== 1
|
|
58
|
+
|| result.status !== expectedStatus) {
|
|
59
|
+
throw new SdkError(
|
|
60
|
+
"native_diagnostics_invalid_response",
|
|
61
|
+
"LogBrew Apple native diagnostics context returned an invalid response"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze({ status: expectedStatus });
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
function normalizeConfiguration(configuration) {
|
|
48
68
|
if (!configuration
|
|
49
69
|
|| Array.isArray(configuration)
|
|
@@ -109,6 +129,81 @@ function normalizeConfiguration(configuration) {
|
|
|
109
129
|
return payload;
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
function normalizeCorrelationContext(context) {
|
|
133
|
+
const source = exactObject(context, ["schemaVersion", "session", "subject", "trace"], "context");
|
|
134
|
+
if (source.schemaVersion !== 1) {
|
|
135
|
+
throw configurationError("context schemaVersion must be 1");
|
|
136
|
+
}
|
|
137
|
+
const output = { schemaVersion: 1 };
|
|
138
|
+
if (source.trace !== undefined) {
|
|
139
|
+
const trace = exactObject(
|
|
140
|
+
source.trace,
|
|
141
|
+
["parentSpanId", "sampled", "spanId", "traceId"],
|
|
142
|
+
"context trace"
|
|
143
|
+
);
|
|
144
|
+
output.trace = { traceId: correlationHexId(trace.traceId, 32, "traceId") };
|
|
145
|
+
for (const [key, width] of [["spanId", 16], ["parentSpanId", 16]]) {
|
|
146
|
+
if (trace[key] !== undefined) {
|
|
147
|
+
output.trace[key] = correlationHexId(trace[key], width, key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (trace.sampled !== undefined) {
|
|
151
|
+
if (typeof trace.sampled !== "boolean") {
|
|
152
|
+
throw configurationError("context trace sampled must be a boolean");
|
|
153
|
+
}
|
|
154
|
+
output.trace.sampled = trace.sampled;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (source.session !== undefined) {
|
|
158
|
+
const session = exactObject(source.session, ["id", "previousId"], "context session");
|
|
159
|
+
output.session = { id: opaqueCorrelationId(session.id, "session id") };
|
|
160
|
+
if (session.previousId !== undefined) {
|
|
161
|
+
output.session.previousId = opaqueCorrelationId(session.previousId, "session previousId");
|
|
162
|
+
if (output.session.previousId === output.session.id) {
|
|
163
|
+
throw configurationError("context session previousId must differ from id");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (source.subject !== undefined) {
|
|
168
|
+
const subject = exactObject(source.subject, ["id", "kind"], "context subject");
|
|
169
|
+
if (subject.kind !== "anonymous" && subject.kind !== "user") {
|
|
170
|
+
throw configurationError("context subject kind must be anonymous or user");
|
|
171
|
+
}
|
|
172
|
+
output.subject = { id: opaqueCorrelationId(subject.id, "subject id"), kind: subject.kind };
|
|
173
|
+
}
|
|
174
|
+
if (Object.keys(output).length === 1) {
|
|
175
|
+
throw configurationError("context must include trace, session, or subject");
|
|
176
|
+
}
|
|
177
|
+
return output;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function exactObject(value, allowedKeys, name) {
|
|
181
|
+
if (!isPlainObject(value)) {
|
|
182
|
+
throw configurationError(`${name} must be an object`);
|
|
183
|
+
}
|
|
184
|
+
const unknown = Object.keys(value).filter((key) => !allowedKeys.includes(key)).sort();
|
|
185
|
+
if (unknown.length > 0) {
|
|
186
|
+
throw configurationError(`${name} contains unsupported fields`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function correlationHexId(value, width, name) {
|
|
192
|
+
if (typeof value !== "string"
|
|
193
|
+
|| !new RegExp(`^[0-9a-f]{${width}}$`, "iu").test(value)
|
|
194
|
+
|| /^0+$/u.test(value)) {
|
|
195
|
+
throw configurationError(`context trace ${name} must be a non-zero ${width}-character hex value`);
|
|
196
|
+
}
|
|
197
|
+
return value.toLowerCase();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function opaqueCorrelationId(value, name) {
|
|
201
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/u.test(value)) {
|
|
202
|
+
throw configurationError(`context ${name} must be an opaque app-owned identifier`);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
112
207
|
function requireApplePlatform() {
|
|
113
208
|
if (Platform?.OS !== "ios") {
|
|
114
209
|
throw new SdkError(
|
|
@@ -280,7 +375,8 @@ function configurationError(message) {
|
|
|
280
375
|
const logBrewAppleNativeDiagnostics = {
|
|
281
376
|
getLogBrewAppleNativeDiagnosticsStatus,
|
|
282
377
|
installLogBrewAppleNativeDiagnostics,
|
|
283
|
-
replayLogBrewAppleNativeDiagnostics
|
|
378
|
+
replayLogBrewAppleNativeDiagnostics,
|
|
379
|
+
setLogBrewAppleNativeCrashContext
|
|
284
380
|
};
|
|
285
381
|
|
|
286
382
|
export default logBrewAppleNativeDiagnostics;
|
package/expo.cjs
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
const PLUGIN_NAME = "@logbrew/react-native/expo";
|
|
2
2
|
const START_MARKER = " # @generated by @logbrew/react-native/expo";
|
|
3
3
|
const END_MARKER = " # @end @logbrew/react-native/expo";
|
|
4
|
+
const MANAGED_BLOCK = / # @generated by @logbrew\/react-native\/expo[\s\S]*? # @end @logbrew\/react-native\/expo\n?/gu;
|
|
4
5
|
const POD_BLOCK = [
|
|
5
6
|
START_MARKER,
|
|
6
|
-
"
|
|
7
|
+
" require 'pathname'",
|
|
8
|
+
" logbrew_react_native_path = Pathname(File.dirname(`bun --print \"require.resolve('@logbrew/react-native')\"`)).relative_path_from(Pathname(__dir__)).to_s",
|
|
7
9
|
" pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path",
|
|
8
10
|
END_MARKER
|
|
9
11
|
].join("\n");
|
|
10
12
|
|
|
11
13
|
function withLogBrewReactNative(config, options = {}) {
|
|
12
14
|
const { withPodfile } = require("expo/config-plugins");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
15
|
+
const enabled = options?.appleNativeDiagnostics !== false;
|
|
16
|
+
return withPodfile(config, podfileConfig => {
|
|
17
|
+
const podfile = podfileConfig.modResults;
|
|
18
|
+
podfile.contents = modifyPodfile(podfile.contents, enabled);
|
|
18
19
|
return podfileConfig;
|
|
19
20
|
});
|
|
20
21
|
}
|
|
@@ -23,10 +24,7 @@ function modifyPodfile(contents, enabled = true) {
|
|
|
23
24
|
if (typeof contents !== "string") {
|
|
24
25
|
throw new TypeError(`${PLUGIN_NAME} expected Podfile text`);
|
|
25
26
|
}
|
|
26
|
-
const withoutManagedBlock = contents.replace(
|
|
27
|
-
new RegExp(`${escapePattern(START_MARKER)}[\\s\\S]*?${escapePattern(END_MARKER)}\\n?`, "gu"),
|
|
28
|
-
""
|
|
29
|
-
);
|
|
27
|
+
const withoutManagedBlock = contents.replace(MANAGED_BLOCK, "");
|
|
30
28
|
if (!enabled) {
|
|
31
29
|
return withoutManagedBlock;
|
|
32
30
|
}
|
|
@@ -37,10 +35,6 @@ function modifyPodfile(contents, enabled = true) {
|
|
|
37
35
|
return withoutManagedBlock.replace(targetPattern, `$1\n${POD_BLOCK}`);
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
function escapePattern(value) {
|
|
41
|
-
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
38
|
module.exports = withLogBrewReactNative;
|
|
45
39
|
module.exports.modifyPodfile = modifyPodfile;
|
|
46
40
|
module.exports.withLogBrewReactNative = withLogBrewReactNative;
|
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.20";
|
|
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;
|