@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 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 subject changes, and
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. 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.
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
- const result = callNative(nativeModule, "setNativeDiagnosticsContext", payload);
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 context failed with ${result.code}`);
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
- "LogBrew Apple native diagnostics context returned an invalid response"
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(context, ["schemaVersion", "session", "subject", "trace"], "context");
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 subject");
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 === 0x7f) {
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.20";
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
  }