@logbrew/react-native 0.1.5 → 0.1.6
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 +32 -2
- package/global-errors.cjs +4 -0
- package/global-errors.d.cts +64 -0
- package/global-errors.d.ts +64 -0
- package/global-errors.js +1 -0
- package/global-errors.native.js +6 -0
- package/package.json +3 -2
- package/promise-rejections.cjs +271 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
React Native helpers for the public LogBrew JavaScript SDK.
|
|
8
8
|
|
|
9
|
-
This package is intentionally thin. It keeps all event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`, while adding mobile-friendly helpers for screen views, app-state changes, product actions, API milestones, handled JavaScript errors, provider/hook usage, active W3C trace correlation, explicit W3C trace propagation, opt-in lifecycle spans, opt-in resource fetch spans, opt-in reversible global fetch spans, app-owned native bridge scope sync, and reversible instrumentation setup.
|
|
9
|
+
This package is intentionally thin. It keeps all event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`, while adding mobile-friendly helpers for screen views, app-state changes, product actions, API milestones, handled JavaScript errors, app-owned Promise rejection reports, provider/hook usage, active W3C trace correlation, explicit W3C trace propagation, opt-in lifecycle spans, opt-in resource fetch spans, opt-in reversible global fetch spans, app-owned native bridge scope sync, and reversible instrumentation setup.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -128,7 +128,37 @@ Installation is idempotent for the active React Native `ErrorUtils` object. The
|
|
|
128
128
|
|
|
129
129
|
The React Native conditional export obtains LogBrew's synchronous native fatal store through the supported TurboModule or `NativeModules` seam. Before chaining a fatal report, it writes one bounded record to app-private storage that is excluded from operating-system archives. On a later installation it performs stable-ID at-least-once replay, and acknowledgement happens only after local queue admission is observable through the SDK queue counters. Filtered, dropped, unknown-admission, persistence-failed, and acknowledgement-failed records are retained. A failed acknowledgement is retried without admitting the same ID twice in one JavaScript runtime. Use `fatalHealth()` for frozen bounded counters and status, or `discardPendingFatalRecord()` for an explicit rollback discard. The Node ESM and CommonJS entries never import React Native; non-React-Native callers must inject `fatalStore` explicitly.
|
|
130
130
|
|
|
131
|
-
Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This integration does not claim mathematically exactly-once delivery, backend-visible deduplication, native crash capture, Promise rejection ownership, ANR or hang detection, general offline queueing, or symbolication.
|
|
131
|
+
Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This integration does not claim mathematically exactly-once delivery, backend-visible deduplication, native crash capture, automatic Promise rejection tracker ownership, ANR or hang detection, general offline queueing, or symbolication.
|
|
132
|
+
|
|
133
|
+
### App-owned Promise rejection reports
|
|
134
|
+
|
|
135
|
+
Use the Promise rejection callbacks when your app or framework already owns rejection tracking:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import {
|
|
139
|
+
createLogBrewReactNativePromiseRejectionHandlers
|
|
140
|
+
} from "@logbrew/react-native/global-errors";
|
|
141
|
+
|
|
142
|
+
const logBrewPromiseRejections =
|
|
143
|
+
createLogBrewReactNativePromiseRejectionHandlers({
|
|
144
|
+
client,
|
|
145
|
+
onDiagnostic({ code }) {
|
|
146
|
+
console.warn(`LogBrew Promise rejection handler: ${code}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const promiseRejectionTrackerOptions = {
|
|
151
|
+
allRejections: true,
|
|
152
|
+
onUnhandled: logBrewPromiseRejections.onUnhandled,
|
|
153
|
+
onHandled: logBrewPromiseRejections.onHandled
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Pass promiseRejectionTrackerOptions to the tracker your app already owns.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The callbacks match the common `(id, rejection)` and `(id)` tracker shapes, but LogBrew does not install, replace, or patch Promise, Hermes, JavaScriptCore, or a global rejection tracker. If another integration already has callbacks, keep that integration as the tracker owner and call both callback sets from the app-owned composition point.
|
|
160
|
+
|
|
161
|
+
`onUnhandled()` emits a fixed-content issue and deliberately does not inspect or send the rejection value, raw runtime rejection ID, error message, stack, Promise, or arbitrary metadata. Numeric IDs and bounded strings are retained only in local memory for duplicate suppression and `onHandled()` health. The set defaults to 128 entries and can be configured from 1 to 1024 with `maxTrackedRejections`; old entries are evicted. Missing or unsafe IDs still produce an untracked privacy-safe report. `onHandled()` updates local health only and cannot retract an issue that was already queued. Use `health()` for frozen counters and the last bounded outcome. Capture and diagnostic failures never escape these callbacks.
|
|
132
162
|
|
|
133
163
|
When you prepare React Native release artifacts, wrap the app-owned Metro config once. Production bundles and source maps receive one matching Debug ID, while development and hot-reload serialization remain unchanged:
|
|
134
164
|
|
package/global-errors.cjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { createReactNativeErrorEvent } = require("./index.cjs");
|
|
4
4
|
const { createFatalController } = require("./fatal-replay.cjs");
|
|
5
|
+
const {
|
|
6
|
+
createLogBrewReactNativePromiseRejectionHandlers
|
|
7
|
+
} = require("./promise-rejections.cjs");
|
|
5
8
|
|
|
6
9
|
const AUTOMATIC_ERROR_MESSAGE = "React Native global JavaScript report";
|
|
7
10
|
const MAX_STACK_BYTES = 16 * 1024;
|
|
@@ -408,6 +411,7 @@ function isObjectLike(value) {
|
|
|
408
411
|
}
|
|
409
412
|
|
|
410
413
|
const defaultExport = {
|
|
414
|
+
createLogBrewReactNativePromiseRejectionHandlers,
|
|
411
415
|
installLogBrewReactNativeGlobalErrorHandler
|
|
412
416
|
};
|
|
413
417
|
|
package/global-errors.d.cts
CHANGED
|
@@ -7,6 +7,19 @@ export type ReactNativeErrorUtilsLike = {
|
|
|
7
7
|
setGlobalHandler(handler: ReactNativeGlobalErrorHandler | undefined): void;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type ReactNativePromiseRejectionDiagnosticCode =
|
|
11
|
+
| "promise_rejection_capture_failed"
|
|
12
|
+
| "promise_rejection_configuration_invalid"
|
|
13
|
+
| "promise_rejection_duplicate_suppressed"
|
|
14
|
+
| "promise_rejection_handler_unavailable"
|
|
15
|
+
| "promise_rejection_id_unavailable"
|
|
16
|
+
| "promise_rejection_recursive_capture_suppressed"
|
|
17
|
+
| "promise_rejection_tracking_evicted";
|
|
18
|
+
|
|
19
|
+
export type ReactNativePromiseRejectionDiagnostic = Readonly<{
|
|
20
|
+
code: ReactNativePromiseRejectionDiagnosticCode;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
10
23
|
export type ReactNativeGlobalErrorDiagnosticCode =
|
|
11
24
|
| "capture_failed"
|
|
12
25
|
| "duplicate_capture_suppressed"
|
|
@@ -44,6 +57,45 @@ export type ReactNativeGlobalErrorHealth = Readonly<{
|
|
|
44
57
|
suppressedEvents: number;
|
|
45
58
|
}>;
|
|
46
59
|
|
|
60
|
+
export type ReactNativePromiseRejectionHealth = Readonly<{
|
|
61
|
+
available: boolean;
|
|
62
|
+
capturedEvents: number;
|
|
63
|
+
evictedRejections: number;
|
|
64
|
+
handledLaterEvents: number;
|
|
65
|
+
lastOutcome:
|
|
66
|
+
| "capture_failed"
|
|
67
|
+
| "captured"
|
|
68
|
+
| "captured_untracked"
|
|
69
|
+
| "captured_with_eviction"
|
|
70
|
+
| "duplicate_suppressed"
|
|
71
|
+
| "handled_later"
|
|
72
|
+
| "handled_unknown"
|
|
73
|
+
| "idle"
|
|
74
|
+
| "recursive_suppressed"
|
|
75
|
+
| "unavailable";
|
|
76
|
+
maxTrackedRejections: number;
|
|
77
|
+
suppressedEvents: number;
|
|
78
|
+
trackedRejections: number;
|
|
79
|
+
unknownHandledEvents: number;
|
|
80
|
+
untrackedEvents: number;
|
|
81
|
+
}>;
|
|
82
|
+
|
|
83
|
+
export type CreateLogBrewReactNativePromiseRejectionHandlersOptions = {
|
|
84
|
+
client: Pick<LogBrewClient, "issue">;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum number of safe runtime rejection identifiers retained for duplicate
|
|
87
|
+
* suppression and later-handled health. Defaults to 128 and must be 1-1024.
|
|
88
|
+
*/
|
|
89
|
+
maxTrackedRejections?: number;
|
|
90
|
+
onDiagnostic?: (diagnostic: ReactNativePromiseRejectionDiagnostic) => void;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type LogBrewReactNativePromiseRejectionHandlers = Readonly<{
|
|
94
|
+
health(): ReactNativePromiseRejectionHealth;
|
|
95
|
+
onHandled(runtimeRejectionId: unknown): void;
|
|
96
|
+
onUnhandled(runtimeRejectionId: unknown, rejection?: unknown): void;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
47
99
|
export type ReactNativeFatalRecord = Readonly<{
|
|
48
100
|
corruptRecords: number;
|
|
49
101
|
droppedRecords: number;
|
|
@@ -112,6 +164,17 @@ export type LogBrewReactNativeGlobalErrorHandlerInstallation = Readonly<{
|
|
|
112
164
|
remove(): boolean;
|
|
113
165
|
}>;
|
|
114
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Create privacy-safe callbacks for a Promise rejection tracker owned by the app.
|
|
169
|
+
*
|
|
170
|
+
* The callbacks do not install or patch Promise, Hermes, JavaScriptCore, or globals.
|
|
171
|
+
* Automatic reports never inspect or emit the rejection value or runtime identifier.
|
|
172
|
+
* Tracking is bounded and suppresses duplicate callbacks only while an identifier is retained.
|
|
173
|
+
*/
|
|
174
|
+
export declare function createLogBrewReactNativePromiseRejectionHandlers(
|
|
175
|
+
options: CreateLogBrewReactNativePromiseRejectionHandlersOptions
|
|
176
|
+
): LogBrewReactNativePromiseRejectionHandlers;
|
|
177
|
+
|
|
115
178
|
/**
|
|
116
179
|
* Install reversible automatic capture for React Native global JavaScript errors.
|
|
117
180
|
*
|
|
@@ -125,6 +188,7 @@ export declare function installLogBrewReactNativeGlobalErrorHandler(
|
|
|
125
188
|
): LogBrewReactNativeGlobalErrorHandlerInstallation;
|
|
126
189
|
|
|
127
190
|
declare const logBrewReactNativeGlobalErrors: {
|
|
191
|
+
createLogBrewReactNativePromiseRejectionHandlers: typeof createLogBrewReactNativePromiseRejectionHandlers;
|
|
128
192
|
installLogBrewReactNativeGlobalErrorHandler: typeof installLogBrewReactNativeGlobalErrorHandler;
|
|
129
193
|
};
|
|
130
194
|
|
package/global-errors.d.ts
CHANGED
|
@@ -7,6 +7,19 @@ export type ReactNativeErrorUtilsLike = {
|
|
|
7
7
|
setGlobalHandler(handler: ReactNativeGlobalErrorHandler | undefined): void;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type ReactNativePromiseRejectionDiagnosticCode =
|
|
11
|
+
| "promise_rejection_capture_failed"
|
|
12
|
+
| "promise_rejection_configuration_invalid"
|
|
13
|
+
| "promise_rejection_duplicate_suppressed"
|
|
14
|
+
| "promise_rejection_handler_unavailable"
|
|
15
|
+
| "promise_rejection_id_unavailable"
|
|
16
|
+
| "promise_rejection_recursive_capture_suppressed"
|
|
17
|
+
| "promise_rejection_tracking_evicted";
|
|
18
|
+
|
|
19
|
+
export type ReactNativePromiseRejectionDiagnostic = Readonly<{
|
|
20
|
+
code: ReactNativePromiseRejectionDiagnosticCode;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
10
23
|
export type ReactNativeGlobalErrorDiagnosticCode =
|
|
11
24
|
| "capture_failed"
|
|
12
25
|
| "duplicate_capture_suppressed"
|
|
@@ -44,6 +57,45 @@ export type ReactNativeGlobalErrorHealth = Readonly<{
|
|
|
44
57
|
suppressedEvents: number;
|
|
45
58
|
}>;
|
|
46
59
|
|
|
60
|
+
export type ReactNativePromiseRejectionHealth = Readonly<{
|
|
61
|
+
available: boolean;
|
|
62
|
+
capturedEvents: number;
|
|
63
|
+
evictedRejections: number;
|
|
64
|
+
handledLaterEvents: number;
|
|
65
|
+
lastOutcome:
|
|
66
|
+
| "capture_failed"
|
|
67
|
+
| "captured"
|
|
68
|
+
| "captured_untracked"
|
|
69
|
+
| "captured_with_eviction"
|
|
70
|
+
| "duplicate_suppressed"
|
|
71
|
+
| "handled_later"
|
|
72
|
+
| "handled_unknown"
|
|
73
|
+
| "idle"
|
|
74
|
+
| "recursive_suppressed"
|
|
75
|
+
| "unavailable";
|
|
76
|
+
maxTrackedRejections: number;
|
|
77
|
+
suppressedEvents: number;
|
|
78
|
+
trackedRejections: number;
|
|
79
|
+
unknownHandledEvents: number;
|
|
80
|
+
untrackedEvents: number;
|
|
81
|
+
}>;
|
|
82
|
+
|
|
83
|
+
export type CreateLogBrewReactNativePromiseRejectionHandlersOptions = {
|
|
84
|
+
client: Pick<LogBrewClient, "issue">;
|
|
85
|
+
/**
|
|
86
|
+
* Maximum number of safe runtime rejection identifiers retained for duplicate
|
|
87
|
+
* suppression and later-handled health. Defaults to 128 and must be 1-1024.
|
|
88
|
+
*/
|
|
89
|
+
maxTrackedRejections?: number;
|
|
90
|
+
onDiagnostic?: (diagnostic: ReactNativePromiseRejectionDiagnostic) => void;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type LogBrewReactNativePromiseRejectionHandlers = Readonly<{
|
|
94
|
+
health(): ReactNativePromiseRejectionHealth;
|
|
95
|
+
onHandled(runtimeRejectionId: unknown): void;
|
|
96
|
+
onUnhandled(runtimeRejectionId: unknown, rejection?: unknown): void;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
47
99
|
export type ReactNativeFatalRecord = Readonly<{
|
|
48
100
|
corruptRecords: number;
|
|
49
101
|
droppedRecords: number;
|
|
@@ -112,6 +164,17 @@ export type LogBrewReactNativeGlobalErrorHandlerInstallation = Readonly<{
|
|
|
112
164
|
remove(): boolean;
|
|
113
165
|
}>;
|
|
114
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Create privacy-safe callbacks for a Promise rejection tracker owned by the app.
|
|
169
|
+
*
|
|
170
|
+
* The callbacks do not install or patch Promise, Hermes, JavaScriptCore, or globals.
|
|
171
|
+
* Automatic reports never inspect or emit the rejection value or runtime identifier.
|
|
172
|
+
* Tracking is bounded and suppresses duplicate callbacks only while an identifier is retained.
|
|
173
|
+
*/
|
|
174
|
+
export declare function createLogBrewReactNativePromiseRejectionHandlers(
|
|
175
|
+
options: CreateLogBrewReactNativePromiseRejectionHandlersOptions
|
|
176
|
+
): LogBrewReactNativePromiseRejectionHandlers;
|
|
177
|
+
|
|
115
178
|
/**
|
|
116
179
|
* Install reversible automatic capture for React Native global JavaScript errors.
|
|
117
180
|
*
|
|
@@ -125,6 +188,7 @@ export declare function installLogBrewReactNativeGlobalErrorHandler(
|
|
|
125
188
|
): LogBrewReactNativeGlobalErrorHandlerInstallation;
|
|
126
189
|
|
|
127
190
|
declare const logBrewReactNativeGlobalErrors: {
|
|
191
|
+
createLogBrewReactNativePromiseRejectionHandlers: typeof createLogBrewReactNativePromiseRejectionHandlers;
|
|
128
192
|
installLogBrewReactNativeGlobalErrorHandler: typeof installLogBrewReactNativeGlobalErrorHandler;
|
|
129
193
|
};
|
|
130
194
|
|
package/global-errors.js
CHANGED
package/global-errors.native.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { NativeModules, TurboModuleRegistry } from "react-native";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
createLogBrewReactNativePromiseRejectionHandlers,
|
|
4
5
|
installLogBrewReactNativeGlobalErrorHandler as installPlatformNeutralHandler
|
|
5
6
|
} from "./global-errors.js";
|
|
6
7
|
|
|
8
|
+
export {
|
|
9
|
+
createLogBrewReactNativePromiseRejectionHandlers
|
|
10
|
+
};
|
|
11
|
+
|
|
7
12
|
function defaultFatalStore() {
|
|
8
13
|
try {
|
|
9
14
|
return TurboModuleRegistry?.get?.("LogBrewFatalStore")
|
|
@@ -33,6 +38,7 @@ export function installLogBrewReactNativeGlobalErrorHandler(options = {}) {
|
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const logBrewReactNativeGlobalErrors = {
|
|
41
|
+
createLogBrewReactNativePromiseRejectionHandlers,
|
|
36
42
|
installLogBrewReactNativeGlobalErrorHandler
|
|
37
43
|
};
|
|
38
44
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logbrew/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "React Native screen, error, trace, action, and network timeline helpers for LogBrew.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
"lifecycle.d.ts",
|
|
137
137
|
"lifecycle.d.cts",
|
|
138
138
|
"global-errors.cjs",
|
|
139
|
+
"promise-rejections.cjs",
|
|
139
140
|
"fatal-replay.cjs",
|
|
140
141
|
"global-errors.js",
|
|
141
142
|
"global-errors.native.js",
|
|
@@ -207,6 +208,6 @@
|
|
|
207
208
|
"react-native": ">=0.72"
|
|
208
209
|
},
|
|
209
210
|
"scripts": {
|
|
210
|
-
"test": "node --check index.js && node --check index.cjs && node --check index.native.js && node --check apollo.js && node --check apollo.cjs && node --check instrumentation.js && node --check instrumentation.cjs && node --check lifecycle.js && node --check lifecycle.cjs && node --check global-errors.js && node --check global-errors.native.js && node --check global-errors.cjs && node --check fatal-replay.cjs && node --check metadata.js && node --check metadata.cjs && node --check native-bridge.js && node --check native-bridge.cjs && node --check resource-fetch.js && node --check resource-fetch.cjs && node --check release-artifacts.js && node --check release-artifacts.cjs && node --check metro.js && node --check metro.cjs && node --check examples/index.mjs && node --check examples/apollo-link-spans.mjs && node --check examples/instrumentation-kit.mjs && node --check examples/lifecycle-spans.mjs && node --check examples/native-bridge-scope.mjs && node --check examples/navigation-resource-spans.mjs && node --check examples/readme-example.mjs && node --check examples/real-user-smoke.mjs && node --check examples/resource-fetch-spans.mjs && node --check examples/trace-correlation.mjs && node --test"
|
|
211
|
+
"test": "node --check index.js && node --check index.cjs && node --check index.native.js && node --check apollo.js && node --check apollo.cjs && node --check instrumentation.js && node --check instrumentation.cjs && node --check lifecycle.js && node --check lifecycle.cjs && node --check global-errors.js && node --check global-errors.native.js && node --check global-errors.cjs && node --check promise-rejections.cjs && node --check fatal-replay.cjs && node --check metadata.js && node --check metadata.cjs && node --check native-bridge.js && node --check native-bridge.cjs && node --check resource-fetch.js && node --check resource-fetch.cjs && node --check release-artifacts.js && node --check release-artifacts.cjs && node --check metro.js && node --check metro.cjs && node --check examples/index.mjs && node --check examples/apollo-link-spans.mjs && node --check examples/instrumentation-kit.mjs && node --check examples/lifecycle-spans.mjs && node --check examples/native-bridge-scope.mjs && node --check examples/navigation-resource-spans.mjs && node --check examples/readme-example.mjs && node --check examples/real-user-smoke.mjs && node --check examples/resource-fetch-spans.mjs && node --check examples/trace-correlation.mjs && node --test"
|
|
211
212
|
}
|
|
212
213
|
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { createReactNativeErrorEvent } = require("./index.cjs");
|
|
4
|
+
|
|
5
|
+
const AUTOMATIC_MESSAGE = "Unhandled Promise rejection";
|
|
6
|
+
const DEFAULT_MAX_TRACKED_REJECTIONS = 128;
|
|
7
|
+
const MAX_TRACKED_REJECTIONS = 1024;
|
|
8
|
+
const MAX_STRING_ID_LENGTH = 128;
|
|
9
|
+
let nextEventSequence = 0;
|
|
10
|
+
|
|
11
|
+
function createLogBrewReactNativePromiseRejectionHandlers(options = {}) {
|
|
12
|
+
const input = isObjectLike(options) ? options : {};
|
|
13
|
+
const client = safeReadProperty(input, "client");
|
|
14
|
+
const issue = safeFunction(client, "issue");
|
|
15
|
+
const onDiagnostic = safeFunction(input, "onDiagnostic");
|
|
16
|
+
if (!issue) {
|
|
17
|
+
emitDiagnostic(onDiagnostic, "promise_rejection_handler_unavailable");
|
|
18
|
+
return inactiveHandlers();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configuredMaximum = safeReadProperty(input, "maxTrackedRejections");
|
|
22
|
+
const maxTrackedRejections = validMaximum(configuredMaximum)
|
|
23
|
+
? configuredMaximum
|
|
24
|
+
: DEFAULT_MAX_TRACKED_REJECTIONS;
|
|
25
|
+
if (configuredMaximum !== undefined && maxTrackedRejections !== configuredMaximum) {
|
|
26
|
+
emitDiagnostic(onDiagnostic, "promise_rejection_configuration_invalid");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tracked = new Map();
|
|
30
|
+
const state = {
|
|
31
|
+
capturedEvents: 0,
|
|
32
|
+
emittingDiagnostic: false,
|
|
33
|
+
evictedRejections: 0,
|
|
34
|
+
handledLaterEvents: 0,
|
|
35
|
+
handling: false,
|
|
36
|
+
lastOutcome: "idle",
|
|
37
|
+
suppressedEvents: 0,
|
|
38
|
+
unknownHandledEvents: 0,
|
|
39
|
+
untrackedEvents: 0
|
|
40
|
+
};
|
|
41
|
+
let emittedEvictionDiagnostic = false;
|
|
42
|
+
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
health() {
|
|
45
|
+
return healthSnapshot(state, tracked.size, maxTrackedRejections);
|
|
46
|
+
},
|
|
47
|
+
onHandled(runtimeRejectionId) {
|
|
48
|
+
const key = rejectionKey(runtimeRejectionId);
|
|
49
|
+
const record = key === undefined ? undefined : tracked.get(key);
|
|
50
|
+
if (!record) {
|
|
51
|
+
state.unknownHandledEvents = incrementBounded(state.unknownHandledEvents);
|
|
52
|
+
state.lastOutcome = "handled_unknown";
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (record.handled) {
|
|
56
|
+
recordSuppression(
|
|
57
|
+
state,
|
|
58
|
+
onDiagnostic,
|
|
59
|
+
"promise_rejection_duplicate_suppressed",
|
|
60
|
+
"duplicate_suppressed"
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
record.handled = true;
|
|
65
|
+
state.handledLaterEvents = incrementBounded(state.handledLaterEvents);
|
|
66
|
+
state.lastOutcome = "handled_later";
|
|
67
|
+
},
|
|
68
|
+
onUnhandled(runtimeRejectionId) {
|
|
69
|
+
if (state.handling) {
|
|
70
|
+
recordSuppression(
|
|
71
|
+
state,
|
|
72
|
+
onDiagnostic,
|
|
73
|
+
"promise_rejection_recursive_capture_suppressed",
|
|
74
|
+
"recursive_suppressed"
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const key = rejectionKey(runtimeRejectionId);
|
|
80
|
+
if (key !== undefined && tracked.has(key)) {
|
|
81
|
+
recordSuppression(
|
|
82
|
+
state,
|
|
83
|
+
onDiagnostic,
|
|
84
|
+
"promise_rejection_duplicate_suppressed",
|
|
85
|
+
"duplicate_suppressed"
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
state.handling = true;
|
|
91
|
+
try {
|
|
92
|
+
if (key === undefined) {
|
|
93
|
+
emitStateDiagnostic(state, onDiagnostic, "promise_rejection_id_unavailable");
|
|
94
|
+
}
|
|
95
|
+
const event = createEvent();
|
|
96
|
+
issue.call(client, event.id, event.timestamp, event.attributes);
|
|
97
|
+
state.capturedEvents = incrementBounded(state.capturedEvents);
|
|
98
|
+
|
|
99
|
+
if (key === undefined) {
|
|
100
|
+
state.untrackedEvents = incrementBounded(state.untrackedEvents);
|
|
101
|
+
state.lastOutcome = "captured_untracked";
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let evicted = false;
|
|
106
|
+
if (tracked.size === maxTrackedRejections) {
|
|
107
|
+
tracked.delete(tracked.keys().next().value);
|
|
108
|
+
state.evictedRejections = incrementBounded(state.evictedRejections);
|
|
109
|
+
evicted = true;
|
|
110
|
+
if (!emittedEvictionDiagnostic) {
|
|
111
|
+
emittedEvictionDiagnostic = true;
|
|
112
|
+
emitStateDiagnostic(state, onDiagnostic, "promise_rejection_tracking_evicted");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
tracked.set(key, { handled: false });
|
|
116
|
+
state.lastOutcome = evicted ? "captured_with_eviction" : "captured";
|
|
117
|
+
} catch {
|
|
118
|
+
state.lastOutcome = "capture_failed";
|
|
119
|
+
emitStateDiagnostic(state, onDiagnostic, "promise_rejection_capture_failed");
|
|
120
|
+
} finally {
|
|
121
|
+
state.handling = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createEvent() {
|
|
128
|
+
const error = new Error(AUTOMATIC_MESSAGE);
|
|
129
|
+
delete error.stack;
|
|
130
|
+
const event = createReactNativeErrorEvent(error, {
|
|
131
|
+
id: nextEventId(),
|
|
132
|
+
includeStack: false
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
...event,
|
|
136
|
+
attributes: {
|
|
137
|
+
...event.attributes,
|
|
138
|
+
title: AUTOMATIC_MESSAGE,
|
|
139
|
+
message: AUTOMATIC_MESSAGE,
|
|
140
|
+
metadata: {
|
|
141
|
+
...event.attributes.metadata,
|
|
142
|
+
automatic: true,
|
|
143
|
+
fatal: false,
|
|
144
|
+
handled: false,
|
|
145
|
+
mechanism: "app_owned_promise_rejection_tracker",
|
|
146
|
+
source: "react-native.promise_rejection"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function rejectionKey(value) {
|
|
153
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
|
|
154
|
+
return `number:${value}`;
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "string"
|
|
157
|
+
&& value.length > 0
|
|
158
|
+
&& value.length <= MAX_STRING_ID_LENGTH
|
|
159
|
+
&& !hasControlCharacter(value)) {
|
|
160
|
+
return `string:${value}`;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validMaximum(value) {
|
|
166
|
+
return Number.isInteger(value) && value >= 1 && value <= MAX_TRACKED_REJECTIONS;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nextEventId() {
|
|
170
|
+
nextEventSequence = incrementBounded(nextEventSequence);
|
|
171
|
+
return `evt_rn_promise_${Date.now().toString(36)}_${nextEventSequence.toString(36)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function incrementBounded(value) {
|
|
175
|
+
return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function recordSuppression(state, onDiagnostic, code, outcome) {
|
|
179
|
+
state.suppressedEvents = incrementBounded(state.suppressedEvents);
|
|
180
|
+
state.lastOutcome = outcome;
|
|
181
|
+
emitStateDiagnostic(state, onDiagnostic, code);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function emitStateDiagnostic(state, onDiagnostic, code) {
|
|
185
|
+
if (state.emittingDiagnostic) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
state.emittingDiagnostic = true;
|
|
189
|
+
try {
|
|
190
|
+
emitDiagnostic(onDiagnostic, code);
|
|
191
|
+
} finally {
|
|
192
|
+
state.emittingDiagnostic = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function emitDiagnostic(onDiagnostic, code) {
|
|
197
|
+
if (typeof onDiagnostic !== "function") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
onDiagnostic(Object.freeze({ code }));
|
|
202
|
+
} catch {
|
|
203
|
+
// Diagnostics must never interfere with application rejection handling.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function inactiveHandlers() {
|
|
208
|
+
const snapshot = Object.freeze({
|
|
209
|
+
available: false,
|
|
210
|
+
capturedEvents: 0,
|
|
211
|
+
evictedRejections: 0,
|
|
212
|
+
handledLaterEvents: 0,
|
|
213
|
+
lastOutcome: "unavailable",
|
|
214
|
+
maxTrackedRejections: 0,
|
|
215
|
+
suppressedEvents: 0,
|
|
216
|
+
trackedRejections: 0,
|
|
217
|
+
unknownHandledEvents: 0,
|
|
218
|
+
untrackedEvents: 0
|
|
219
|
+
});
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
health: () => snapshot,
|
|
222
|
+
onHandled: () => {},
|
|
223
|
+
onUnhandled: () => {}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function healthSnapshot(state, trackedRejections, maxTrackedRejections) {
|
|
228
|
+
return Object.freeze({
|
|
229
|
+
available: true,
|
|
230
|
+
capturedEvents: state.capturedEvents,
|
|
231
|
+
evictedRejections: state.evictedRejections,
|
|
232
|
+
handledLaterEvents: state.handledLaterEvents,
|
|
233
|
+
lastOutcome: state.lastOutcome,
|
|
234
|
+
maxTrackedRejections,
|
|
235
|
+
suppressedEvents: state.suppressedEvents,
|
|
236
|
+
trackedRejections,
|
|
237
|
+
unknownHandledEvents: state.unknownHandledEvents,
|
|
238
|
+
untrackedEvents: state.untrackedEvents
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function safeReadProperty(value, property) {
|
|
243
|
+
if (!isObjectLike(value)) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
return value[property];
|
|
248
|
+
} catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function safeFunction(value, property) {
|
|
254
|
+
const candidate = safeReadProperty(value, property);
|
|
255
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasControlCharacter(value) {
|
|
259
|
+
return Array.from(value).some((character) => {
|
|
260
|
+
const code = character.codePointAt(0);
|
|
261
|
+
return code !== undefined && (code <= 31 || code === 127);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isObjectLike(value) {
|
|
266
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
createLogBrewReactNativePromiseRejectionHandlers
|
|
271
|
+
};
|