@logbrew/react-native 0.1.19 → 0.1.21

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 CHANGED
@@ -309,7 +309,8 @@ before root registration, then start replay:
309
309
  ```js
310
310
  import {
311
311
  installLogBrewAppleNativeDiagnostics,
312
- replayLogBrewAppleNativeDiagnostics
312
+ replayLogBrewAppleNativeDiagnostics,
313
+ setLogBrewAppleNativeCrashContext
313
314
  } from "@logbrew/react-native/apple-native-diagnostics";
314
315
 
315
316
  const nativeStatus = installLogBrewAppleNativeDiagnostics({
@@ -322,6 +323,13 @@ const nativeStatus = installLogBrewAppleNativeDiagnostics({
322
323
  hangThresholdSeconds: 2
323
324
  });
324
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
+
325
333
  void replayLogBrewAppleNativeDiagnostics().catch((error) => {
326
334
  console.warn(error?.code ?? "native_diagnostics_failed");
327
335
  });
@@ -334,6 +342,21 @@ allowed, but only one integration may install native fatal capture in a given
334
342
  process. LogBrew cannot transfer or remove that ownership before process
335
343
  restart.
336
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
+
350
+ The iOS `createLogBrewReactNativeClient()` entry also mirrors its validated
351
+ breadcrumb history into native crash capture. Install Apple diagnostics first,
352
+ then use the normal screen, app-state, action, network, or `addBreadcrumb()`
353
+ helpers. A fatal crash retains the newest complete entries in oldest-to-newest
354
+ order. The snapshot keeps at most 64 entries and 64 KiB, drops oldest entries
355
+ when either limit is reached, and reports truncation. `clearBreadcrumbs()`
356
+ clears both the JavaScript history and its native crash snapshot. Missing,
357
+ corrupt, and captured breadcrumb state remains explicit on replay; corrupt
358
+ optional context never discards the crash.
359
+
337
360
  Installation creates app-private, data-protected storage that iOS excludes from
338
361
  device data archives. Pending reports are partitioned by project, so changing
339
362
  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,40 @@ 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
+ 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);
66
+ const expectedStatus = payload === null ? "cleared" : "updated";
67
+ if (isErrorResult(result)) {
68
+ throw new SdkError(result.code, `LogBrew Apple native diagnostics ${label} failed with ${result.code}`);
69
+ }
70
+ if (!isPlainObject(result)
71
+ || Object.keys(result).length !== 1
72
+ || result.status !== expectedStatus) {
73
+ throw new SdkError(
74
+ "native_diagnostics_invalid_response",
75
+ `LogBrew Apple native diagnostics ${label} returned an invalid response`
76
+ );
77
+ }
78
+ return Object.freeze({ status: expectedStatus });
79
+ }
80
+
47
81
  function normalizeConfiguration(configuration) {
48
82
  if (!configuration
49
83
  || Array.isArray(configuration)
@@ -109,6 +143,81 @@ function normalizeConfiguration(configuration) {
109
143
  return payload;
110
144
  }
111
145
 
146
+ function normalizeCorrelationContext(context) {
147
+ const source = exactObject(context, ["schemaVersion", "session", "subject", "trace"], "context");
148
+ if (source.schemaVersion !== 1) {
149
+ throw configurationError("context schemaVersion must be 1");
150
+ }
151
+ const output = { schemaVersion: 1 };
152
+ if (source.trace !== undefined) {
153
+ const trace = exactObject(
154
+ source.trace,
155
+ ["parentSpanId", "sampled", "spanId", "traceId"],
156
+ "context trace"
157
+ );
158
+ output.trace = { traceId: correlationHexId(trace.traceId, 32, "traceId") };
159
+ for (const [key, width] of [["spanId", 16], ["parentSpanId", 16]]) {
160
+ if (trace[key] !== undefined) {
161
+ output.trace[key] = correlationHexId(trace[key], width, key);
162
+ }
163
+ }
164
+ if (trace.sampled !== undefined) {
165
+ if (typeof trace.sampled !== "boolean") {
166
+ throw configurationError("context trace sampled must be a boolean");
167
+ }
168
+ output.trace.sampled = trace.sampled;
169
+ }
170
+ }
171
+ if (source.session !== undefined) {
172
+ const session = exactObject(source.session, ["id", "previousId"], "context session");
173
+ output.session = { id: opaqueCorrelationId(session.id, "session id") };
174
+ if (session.previousId !== undefined) {
175
+ output.session.previousId = opaqueCorrelationId(session.previousId, "session previousId");
176
+ if (output.session.previousId === output.session.id) {
177
+ throw configurationError("context session previousId must differ from id");
178
+ }
179
+ }
180
+ }
181
+ if (source.subject !== undefined) {
182
+ const subject = exactObject(source.subject, ["id", "kind"], "context subject");
183
+ if (subject.kind !== "anonymous" && subject.kind !== "user") {
184
+ throw configurationError("context subject kind must be anonymous or user");
185
+ }
186
+ output.subject = { id: opaqueCorrelationId(subject.id, "subject id"), kind: subject.kind };
187
+ }
188
+ if (Object.keys(output).length === 1) {
189
+ throw configurationError("context must include trace, session, or subject");
190
+ }
191
+ return output;
192
+ }
193
+
194
+ function exactObject(value, allowedKeys, name) {
195
+ if (!isPlainObject(value)) {
196
+ throw configurationError(`${name} must be an object`);
197
+ }
198
+ const unknown = Object.keys(value).filter((key) => !allowedKeys.includes(key)).sort();
199
+ if (unknown.length > 0) {
200
+ throw configurationError(`${name} contains unsupported fields`);
201
+ }
202
+ return value;
203
+ }
204
+
205
+ function correlationHexId(value, width, name) {
206
+ if (typeof value !== "string"
207
+ || !new RegExp(`^[0-9a-f]{${width}}$`, "iu").test(value)
208
+ || /^0+$/u.test(value)) {
209
+ throw configurationError(`context trace ${name} must be a non-zero ${width}-character hex value`);
210
+ }
211
+ return value.toLowerCase();
212
+ }
213
+
214
+ function opaqueCorrelationId(value, name) {
215
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/u.test(value)) {
216
+ throw configurationError(`context ${name} must be an opaque app-owned identifier`);
217
+ }
218
+ return value;
219
+ }
220
+
112
221
  function requireApplePlatform() {
113
222
  if (Platform?.OS !== "ios") {
114
223
  throw new SdkError(
@@ -280,7 +389,8 @@ function configurationError(message) {
280
389
  const logBrewAppleNativeDiagnostics = {
281
390
  getLogBrewAppleNativeDiagnosticsStatus,
282
391
  installLogBrewAppleNativeDiagnostics,
283
- replayLogBrewAppleNativeDiagnostics
392
+ replayLogBrewAppleNativeDiagnostics,
393
+ setLogBrewAppleNativeCrashContext
284
394
  };
285
395
 
286
396
  export default logBrewAppleNativeDiagnostics;
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";
19
+ const DEFAULT_SDK_VERSION = "0.1.21";
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;