@logbrew/react-native 0.1.0 → 0.1.2

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.
Files changed (51) hide show
  1. package/README.md +431 -18
  2. package/apollo.cjs +301 -0
  3. package/apollo.d.cts +70 -0
  4. package/apollo.d.ts +70 -0
  5. package/apollo.js +294 -0
  6. package/examples/apollo-link-spans.mjs +149 -0
  7. package/examples/index.mjs +29 -1
  8. package/examples/instrumentation-kit.mjs +304 -0
  9. package/examples/lifecycle-spans.mjs +131 -0
  10. package/examples/native-bridge-scope.mjs +107 -0
  11. package/examples/navigation-resource-spans.mjs +156 -0
  12. package/examples/package.json +8 -1
  13. package/examples/real-user-smoke.mjs +106 -9
  14. package/examples/resource-fetch-spans.mjs +133 -0
  15. package/examples/trace-correlation.mjs +135 -0
  16. package/global-errors.cjs +366 -0
  17. package/global-errors.d.cts +63 -0
  18. package/global-errors.d.ts +63 -0
  19. package/global-errors.js +7 -0
  20. package/index.cjs +625 -111
  21. package/index.d.cts +236 -0
  22. package/index.d.ts +236 -0
  23. package/index.js +613 -95
  24. package/index.native.js +18 -0
  25. package/instrumentation.cjs +639 -0
  26. package/instrumentation.d.cts +84 -0
  27. package/instrumentation.d.ts +84 -0
  28. package/instrumentation.js +634 -0
  29. package/lifecycle.cjs +129 -0
  30. package/lifecycle.d.cts +50 -0
  31. package/lifecycle.d.ts +50 -0
  32. package/lifecycle.js +121 -0
  33. package/metadata.cjs +175 -0
  34. package/metadata.js +165 -0
  35. package/metro.cjs +310 -0
  36. package/metro.d.cts +37 -0
  37. package/metro.d.ts +37 -0
  38. package/metro.js +6 -0
  39. package/native-bridge.cjs +127 -0
  40. package/native-bridge.d.cts +60 -0
  41. package/native-bridge.d.ts +60 -0
  42. package/native-bridge.js +125 -0
  43. package/package.json +128 -4
  44. package/release-artifacts.cjs +344 -0
  45. package/release-artifacts.d.cts +55 -0
  46. package/release-artifacts.d.ts +53 -0
  47. package/release-artifacts.js +8 -0
  48. package/resource-fetch.cjs +469 -0
  49. package/resource-fetch.d.cts +60 -0
  50. package/resource-fetch.d.ts +60 -0
  51. package/resource-fetch.js +464 -0
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+
3
+ const { createReactNativeErrorEvent } = require("./index.cjs");
4
+
5
+ const AUTOMATIC_ERROR_MESSAGE = "React Native global JavaScript report";
6
+ const MAX_STACK_BYTES = 16 * 1024;
7
+ const MAX_STACK_FRAMES = 32;
8
+ const SAFE_ERROR_NAMES = new Set([
9
+ "Error",
10
+ "EvalError",
11
+ "RangeError",
12
+ "ReferenceError",
13
+ "SyntaxError",
14
+ "TypeError",
15
+ "URIError"
16
+ ]);
17
+ const installations = new WeakMap();
18
+ let nextEventSequence = 0;
19
+
20
+ function installLogBrewReactNativeGlobalErrorHandler({
21
+ client,
22
+ errorUtils = globalThis?.ErrorUtils,
23
+ onDiagnostic
24
+ } = {}) {
25
+ const issue = safeFunction(client, "issue");
26
+ const getGlobalHandler = safeFunction(errorUtils, "getGlobalHandler");
27
+ const setGlobalHandler = safeFunction(errorUtils, "setGlobalHandler");
28
+ if (!issue || !getGlobalHandler || !setGlobalHandler || !isObjectLike(errorUtils)) {
29
+ emitDiagnostic(onDiagnostic, "handler_unavailable");
30
+ return inactiveInstallation();
31
+ }
32
+
33
+ const existing = installations.get(errorUtils);
34
+ if (existing?.health().active) {
35
+ return existing;
36
+ }
37
+
38
+ let previousHandler;
39
+ try {
40
+ previousHandler = getGlobalHandler.call(errorUtils);
41
+ } catch {
42
+ emitDiagnostic(onDiagnostic, "handler_unavailable");
43
+ return inactiveInstallation();
44
+ }
45
+ if (typeof previousHandler !== "function") {
46
+ emitDiagnostic(onDiagnostic, "handler_unavailable");
47
+ return inactiveInstallation();
48
+ }
49
+
50
+ const state = {
51
+ active: true,
52
+ capturedEvents: 0,
53
+ handling: false,
54
+ lastOutcome: "idle",
55
+ suppressedEvents: 0
56
+ };
57
+ const capturedErrors = new WeakSet();
58
+ const handler = (error, isFatal) => {
59
+ if (!state.active) {
60
+ previousHandler(error, isFatal);
61
+ return;
62
+ }
63
+ if (state.handling) {
64
+ recordSuppression(state, onDiagnostic, "recursive_capture_suppressed");
65
+ return;
66
+ }
67
+ state.handling = true;
68
+ try {
69
+ if (isFatal === true) {
70
+ state.lastOutcome = "fatal_unsupported";
71
+ emitDiagnostic(onDiagnostic, "fatal_capture_requires_sync_store");
72
+ } else if (isObjectLike(error) && capturedErrors.has(error)) {
73
+ recordSuppression(state, onDiagnostic, "duplicate_capture_suppressed");
74
+ } else {
75
+ captureNonfatalError(client, issue, error, capturedErrors, state, onDiagnostic);
76
+ }
77
+ } finally {
78
+ try {
79
+ if (typeof previousHandler === "function") {
80
+ previousHandler(error, isFatal);
81
+ }
82
+ } finally {
83
+ state.handling = false;
84
+ }
85
+ }
86
+ };
87
+
88
+ const installation = Object.freeze({
89
+ health() {
90
+ return healthSnapshot(state);
91
+ },
92
+ remove() {
93
+ if (!state.active) {
94
+ return false;
95
+ }
96
+ let ownsHandler;
97
+ try {
98
+ ownsHandler = getGlobalHandler.call(errorUtils) === handler;
99
+ } catch {
100
+ return false;
101
+ }
102
+ if (!ownsHandler) {
103
+ state.active = false;
104
+ state.lastOutcome = "handler_replaced";
105
+ installations.delete(errorUtils);
106
+ return false;
107
+ }
108
+ try {
109
+ setGlobalHandler.call(errorUtils, previousHandler);
110
+ } catch {
111
+ state.lastOutcome = "remove_failed";
112
+ return false;
113
+ }
114
+ state.active = false;
115
+ state.lastOutcome = "removed";
116
+ installations.delete(errorUtils);
117
+ return true;
118
+ }
119
+ });
120
+
121
+ try {
122
+ setGlobalHandler.call(errorUtils, handler);
123
+ } catch {
124
+ state.active = false;
125
+ try {
126
+ if (getGlobalHandler.call(errorUtils) === handler) {
127
+ setGlobalHandler.call(errorUtils, previousHandler);
128
+ }
129
+ } catch {
130
+ // An inactive wrapper only delegates if a platform setter failed after mutation.
131
+ }
132
+ emitDiagnostic(onDiagnostic, "handler_unavailable");
133
+ return inactiveInstallation();
134
+ }
135
+ installations.set(errorUtils, installation);
136
+ return installation;
137
+ }
138
+
139
+ function captureNonfatalError(client, issue, error, capturedErrors, state, onDiagnostic) {
140
+ const tracksIdentity = isObjectLike(error);
141
+ if (tracksIdentity) {
142
+ capturedErrors.add(error);
143
+ }
144
+ try {
145
+ const event = createAutomaticErrorEvent(error);
146
+ issue.call(client, event.id, event.timestamp, event.attributes);
147
+ state.capturedEvents = incrementBounded(state.capturedEvents);
148
+ state.lastOutcome = "captured";
149
+ } catch {
150
+ if (tracksIdentity) {
151
+ capturedErrors.delete(error);
152
+ }
153
+ state.lastOutcome = "capture_failed";
154
+ emitDiagnostic(onDiagnostic, "capture_failed");
155
+ }
156
+ }
157
+
158
+ function createAutomaticErrorEvent(error) {
159
+ const sanitizedError = new Error(AUTOMATIC_ERROR_MESSAGE);
160
+ sanitizedError.name = safeErrorName(error);
161
+ const stack = safeStack(error);
162
+ if (stack) {
163
+ sanitizedError.stack = stack;
164
+ } else {
165
+ delete sanitizedError.stack;
166
+ }
167
+ const event = createReactNativeErrorEvent(sanitizedError, {
168
+ id: nextEventId(),
169
+ includeStack: false
170
+ });
171
+ return {
172
+ ...event,
173
+ attributes: {
174
+ ...event.attributes,
175
+ title: AUTOMATIC_ERROR_MESSAGE,
176
+ message: AUTOMATIC_ERROR_MESSAGE,
177
+ metadata: {
178
+ ...event.attributes.metadata,
179
+ automatic: true,
180
+ fatal: false,
181
+ handled: true,
182
+ mechanism: "react_native_error_utils",
183
+ source: "react-native.global_error"
184
+ }
185
+ }
186
+ };
187
+ }
188
+
189
+ function safeStack(error) {
190
+ const stack = safeReadProperty(error, "stack");
191
+ const candidate = typeof stack === "string"
192
+ ? stack.slice(0, MAX_STACK_BYTES)
193
+ : "";
194
+ const frames = [];
195
+ for (const line of candidate.split(/\r?\n/u)) {
196
+ const frame = safeStackFrame(line);
197
+ if (frame) {
198
+ frames.push(` at frame (${frame})`);
199
+ if (frames.length === MAX_STACK_FRAMES) {
200
+ break;
201
+ }
202
+ }
203
+ }
204
+ return frames.length === 0
205
+ ? undefined
206
+ : `${safeErrorName(error)}: ${AUTOMATIC_ERROR_MESSAGE}\n${frames.join("\n")}`;
207
+ }
208
+
209
+ function safeStackFrame(line) {
210
+ let location = typeof line === "string" ? line.trim() : "";
211
+ if (!location) {
212
+ return undefined;
213
+ }
214
+ if (location.startsWith("at ")) {
215
+ location = location.slice(3).trim();
216
+ if (location.endsWith(")") && location.includes("(")) {
217
+ location = location.slice(location.lastIndexOf("(") + 1, -1);
218
+ }
219
+ } else if (location.includes("@")) {
220
+ location = location.slice(location.lastIndexOf("@") + 1);
221
+ }
222
+ const parts = location.split(":");
223
+ if (parts.length < 3) {
224
+ return undefined;
225
+ }
226
+ const column = positiveInteger(parts.pop());
227
+ const lineNumber = positiveInteger(parts.pop());
228
+ const filename = safeStackFilename(parts.join(":"));
229
+ if (!filename || lineNumber === undefined || column === undefined) {
230
+ return undefined;
231
+ }
232
+ return `${filename}:${lineNumber}:${column}`;
233
+ }
234
+
235
+ function safeStackFilename(value) {
236
+ let filename = String(value ?? "").trim().replace(/\\/gu, "/");
237
+ if (!filename || filename.length > 2048 || hasControlCharacter(filename)) {
238
+ return undefined;
239
+ }
240
+ try {
241
+ if (/^[a-z][a-z0-9+.-]*:/iu.test(filename)) {
242
+ const parsed = new URL(filename);
243
+ if (!["app:", "http:", "https:"].includes(parsed.protocol)
244
+ || parsed.username
245
+ || urlAuthority(filename).includes("@")) {
246
+ return undefined;
247
+ }
248
+ filename = parsed.pathname;
249
+ } else {
250
+ filename = filename.split(/[?#]/u, 1)[0];
251
+ if (filename.startsWith("/") || /^[A-Za-z]:\//u.test(filename)) {
252
+ return undefined;
253
+ }
254
+ }
255
+ } catch {
256
+ return undefined;
257
+ }
258
+ return filename && filename.length <= 2048 && !hasControlCharacter(filename)
259
+ ? filename
260
+ : undefined;
261
+ }
262
+
263
+ function urlAuthority(value) {
264
+ const schemeMarker = value.indexOf("://");
265
+ if (schemeMarker < 0) {
266
+ return "";
267
+ }
268
+ const start = schemeMarker + 3;
269
+ const end = value.indexOf("/", start);
270
+ return value.slice(start, end < 0 ? value.length : end);
271
+ }
272
+
273
+ function safeErrorName(error) {
274
+ const name = safeReadProperty(error, "name");
275
+ const candidate = typeof name === "string" ? name : "Error";
276
+ return SAFE_ERROR_NAMES.has(candidate) ? candidate : "Error";
277
+ }
278
+
279
+ function safeReadProperty(value, property) {
280
+ if (!isObjectLike(value)) {
281
+ return undefined;
282
+ }
283
+ try {
284
+ return value[property];
285
+ } catch {
286
+ return undefined;
287
+ }
288
+ }
289
+
290
+ function safeFunction(value, property) {
291
+ const candidate = safeReadProperty(value, property);
292
+ return typeof candidate === "function" ? candidate : undefined;
293
+ }
294
+
295
+ function hasControlCharacter(value) {
296
+ return Array.from(value).some((character) => {
297
+ const code = character.codePointAt(0);
298
+ return code !== undefined && (code <= 31 || code === 127);
299
+ });
300
+ }
301
+
302
+ function positiveInteger(value) {
303
+ if (!/^[1-9][0-9]*$/u.test(String(value))) {
304
+ return undefined;
305
+ }
306
+ const number = Number(value);
307
+ return Number.isSafeInteger(number) && number <= 2147483647 ? number : undefined;
308
+ }
309
+
310
+ function nextEventId() {
311
+ nextEventSequence = incrementBounded(nextEventSequence);
312
+ return `evt_rn_global_${Date.now().toString(36)}_${nextEventSequence.toString(36)}`;
313
+ }
314
+
315
+ function incrementBounded(value) {
316
+ return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
317
+ }
318
+
319
+ function recordSuppression(state, onDiagnostic, code) {
320
+ state.suppressedEvents = incrementBounded(state.suppressedEvents);
321
+ state.lastOutcome = code === "recursive_capture_suppressed" ? "recursive_suppressed" : "duplicate_suppressed";
322
+ emitDiagnostic(onDiagnostic, code);
323
+ }
324
+
325
+ function emitDiagnostic(onDiagnostic, code) {
326
+ if (typeof onDiagnostic !== "function") {
327
+ return;
328
+ }
329
+ try {
330
+ onDiagnostic(Object.freeze({ code }));
331
+ } catch {
332
+ // Diagnostics must never interfere with application error handling.
333
+ }
334
+ }
335
+
336
+ function inactiveInstallation() {
337
+ const snapshot = Object.freeze({
338
+ active: false,
339
+ capturedEvents: 0,
340
+ lastOutcome: "unavailable",
341
+ suppressedEvents: 0
342
+ });
343
+ return Object.freeze({
344
+ health: () => snapshot,
345
+ remove: () => false
346
+ });
347
+ }
348
+
349
+ function healthSnapshot(state) {
350
+ return Object.freeze({
351
+ active: state.active,
352
+ capturedEvents: state.capturedEvents,
353
+ lastOutcome: state.lastOutcome,
354
+ suppressedEvents: state.suppressedEvents
355
+ });
356
+ }
357
+
358
+ function isObjectLike(value) {
359
+ return value !== null && (typeof value === "object" || typeof value === "function");
360
+ }
361
+
362
+ const defaultExport = {
363
+ installLogBrewReactNativeGlobalErrorHandler
364
+ };
365
+
366
+ module.exports = { ...defaultExport, default: defaultExport };
@@ -0,0 +1,63 @@
1
+ import type { LogBrewClient } from "@logbrew/sdk";
2
+
3
+ export type ReactNativeGlobalErrorHandler = (error: unknown, isFatal?: boolean) => void;
4
+
5
+ export type ReactNativeErrorUtilsLike = {
6
+ getGlobalHandler(): ReactNativeGlobalErrorHandler | undefined;
7
+ setGlobalHandler(handler: ReactNativeGlobalErrorHandler | undefined): void;
8
+ };
9
+
10
+ export type ReactNativeGlobalErrorDiagnosticCode =
11
+ | "capture_failed"
12
+ | "duplicate_capture_suppressed"
13
+ | "fatal_capture_requires_sync_store"
14
+ | "handler_unavailable"
15
+ | "recursive_capture_suppressed";
16
+
17
+ export type ReactNativeGlobalErrorDiagnostic = Readonly<{
18
+ code: ReactNativeGlobalErrorDiagnosticCode;
19
+ }>;
20
+
21
+ export type ReactNativeGlobalErrorHealth = Readonly<{
22
+ active: boolean;
23
+ capturedEvents: number;
24
+ lastOutcome:
25
+ | "capture_failed"
26
+ | "captured"
27
+ | "duplicate_suppressed"
28
+ | "fatal_unsupported"
29
+ | "handler_replaced"
30
+ | "idle"
31
+ | "recursive_suppressed"
32
+ | "remove_failed"
33
+ | "removed"
34
+ | "unavailable";
35
+ suppressedEvents: number;
36
+ }>;
37
+
38
+ export type InstallLogBrewReactNativeGlobalErrorHandlerOptions = {
39
+ client: Pick<LogBrewClient, "issue">;
40
+ errorUtils?: ReactNativeErrorUtilsLike;
41
+ onDiagnostic?: (diagnostic: ReactNativeGlobalErrorDiagnostic) => void;
42
+ };
43
+
44
+ export type LogBrewReactNativeGlobalErrorHandlerInstallation = Readonly<{
45
+ health(): ReactNativeGlobalErrorHealth;
46
+ remove(): boolean;
47
+ }>;
48
+
49
+ /**
50
+ * Install reversible automatic capture for nonfatal React Native global JavaScript errors.
51
+ *
52
+ * Fatal errors are chained without capture until a synchronous native durable handoff is
53
+ * available. This helper does not install Promise rejection handling.
54
+ */
55
+ export declare function installLogBrewReactNativeGlobalErrorHandler(
56
+ options: InstallLogBrewReactNativeGlobalErrorHandlerOptions
57
+ ): LogBrewReactNativeGlobalErrorHandlerInstallation;
58
+
59
+ declare const logBrewReactNativeGlobalErrors: {
60
+ installLogBrewReactNativeGlobalErrorHandler: typeof installLogBrewReactNativeGlobalErrorHandler;
61
+ };
62
+
63
+ export default logBrewReactNativeGlobalErrors;
@@ -0,0 +1,63 @@
1
+ import type { LogBrewClient } from "@logbrew/sdk";
2
+
3
+ export type ReactNativeGlobalErrorHandler = (error: unknown, isFatal?: boolean) => void;
4
+
5
+ export type ReactNativeErrorUtilsLike = {
6
+ getGlobalHandler(): ReactNativeGlobalErrorHandler | undefined;
7
+ setGlobalHandler(handler: ReactNativeGlobalErrorHandler | undefined): void;
8
+ };
9
+
10
+ export type ReactNativeGlobalErrorDiagnosticCode =
11
+ | "capture_failed"
12
+ | "duplicate_capture_suppressed"
13
+ | "fatal_capture_requires_sync_store"
14
+ | "handler_unavailable"
15
+ | "recursive_capture_suppressed";
16
+
17
+ export type ReactNativeGlobalErrorDiagnostic = Readonly<{
18
+ code: ReactNativeGlobalErrorDiagnosticCode;
19
+ }>;
20
+
21
+ export type ReactNativeGlobalErrorHealth = Readonly<{
22
+ active: boolean;
23
+ capturedEvents: number;
24
+ lastOutcome:
25
+ | "capture_failed"
26
+ | "captured"
27
+ | "duplicate_suppressed"
28
+ | "fatal_unsupported"
29
+ | "handler_replaced"
30
+ | "idle"
31
+ | "recursive_suppressed"
32
+ | "remove_failed"
33
+ | "removed"
34
+ | "unavailable";
35
+ suppressedEvents: number;
36
+ }>;
37
+
38
+ export type InstallLogBrewReactNativeGlobalErrorHandlerOptions = {
39
+ client: Pick<LogBrewClient, "issue">;
40
+ errorUtils?: ReactNativeErrorUtilsLike;
41
+ onDiagnostic?: (diagnostic: ReactNativeGlobalErrorDiagnostic) => void;
42
+ };
43
+
44
+ export type LogBrewReactNativeGlobalErrorHandlerInstallation = Readonly<{
45
+ health(): ReactNativeGlobalErrorHealth;
46
+ remove(): boolean;
47
+ }>;
48
+
49
+ /**
50
+ * Install reversible automatic capture for nonfatal React Native global JavaScript errors.
51
+ *
52
+ * Fatal errors are chained without capture until a synchronous native durable handoff is
53
+ * available. This helper does not install Promise rejection handling.
54
+ */
55
+ export declare function installLogBrewReactNativeGlobalErrorHandler(
56
+ options: InstallLogBrewReactNativeGlobalErrorHandlerOptions
57
+ ): LogBrewReactNativeGlobalErrorHandlerInstallation;
58
+
59
+ declare const logBrewReactNativeGlobalErrors: {
60
+ installLogBrewReactNativeGlobalErrorHandler: typeof installLogBrewReactNativeGlobalErrorHandler;
61
+ };
62
+
63
+ export default logBrewReactNativeGlobalErrors;
@@ -0,0 +1,7 @@
1
+ import implementation from "./global-errors.cjs";
2
+
3
+ export const {
4
+ installLogBrewReactNativeGlobalErrorHandler
5
+ } = implementation;
6
+
7
+ export default implementation;