@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 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
- logbrew_react_native_path = File.dirname(`node --print "require.resolve('@logbrew/react-native')"`)
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
- " logbrew_react_native_path = File.dirname(`node --print \"require.resolve('@logbrew/react-native')\"`)",
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
- return withPodfile(config, (podfileConfig) => {
14
- podfileConfig.modResults.contents = modifyPodfile(
15
- podfileConfig.modResults.contents,
16
- options?.appleNativeDiagnostics !== false
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.18";
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;