@logbrew/react-native 0.1.19 → 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 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,11 @@ 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
+
337
350
  Installation creates app-private, data-protected storage that iOS excludes from
338
351
  device data archives. Pending reports are partitioned by project, so changing
339
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/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.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;