@flareapp/react-native 2.6.0 → 2.8.0
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/dist/index.cjs +79 -155
- package/dist/index.d.cts +13 -42
- package/dist/index.d.mts +13 -42
- package/dist/index.mjs +80 -156
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -6,19 +6,11 @@ let react = require("react");
|
|
|
6
6
|
|
|
7
7
|
//#region src/context/expo.ts
|
|
8
8
|
/**
|
|
9
|
-
* Lazy, synchronous Expo load. The `require(...)` calls
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* build error. Aliasing `require` to a local (`const req = require; req('pkg')`)
|
|
15
|
-
* would defeat that static collection — Metro never adds the module to this
|
|
16
|
-
* file's dependency map, so the require would fail to resolve even when the
|
|
17
|
-
* package IS installed. So do NOT reintroduce an alias here.
|
|
18
|
-
*
|
|
19
|
-
* The `typeof require` guard keeps non-Metro/ESM environments (e.g. some test
|
|
20
|
-
* runners, where `require` is undefined) safe; under Metro `require` always
|
|
21
|
-
* exists in the `react-native`/CJS build this package ships.
|
|
9
|
+
* Lazy, synchronous Expo load. The `require(...)` calls MUST be direct string literals: Metro statically
|
|
10
|
+
* collects only literal `require('pkg')` calls, treating those inside a try/catch as optional deps
|
|
11
|
+
* (`allowOptionalDependencies` is on by default), so a missing package degrades to a caught throw not a
|
|
12
|
+
* build error. Do NOT alias `require` to a local; that defeats the static collection and the module never
|
|
13
|
+
* resolves even when installed. The `typeof require` guard keeps non-Metro/ESM envs (some test runners) safe.
|
|
22
14
|
*/
|
|
23
15
|
function loadExpoModules() {
|
|
24
16
|
const mods = {};
|
|
@@ -31,6 +23,7 @@ function loadExpoModules() {
|
|
|
31
23
|
} catch {}
|
|
32
24
|
return mods;
|
|
33
25
|
}
|
|
26
|
+
/** Maps Expo's `DeviceType` enum (UNKNOWN=0, PHONE=1, TABLET=2, DESKTOP=3, TV=4) to a label. */
|
|
34
27
|
const DEVICE_TYPE_LABELS = {
|
|
35
28
|
1: "phone",
|
|
36
29
|
2: "tablet",
|
|
@@ -38,9 +31,8 @@ const DEVICE_TYPE_LABELS = {
|
|
|
38
31
|
4: "tv"
|
|
39
32
|
};
|
|
40
33
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* getters are intentionally not used (the context collector is synchronous).
|
|
34
|
+
* Turn the synchronous Expo constants into report attributes. Only present (non-null) fields are emitted.
|
|
35
|
+
* Async Expo getters are not used, since the context collector is synchronous.
|
|
44
36
|
*/
|
|
45
37
|
function projectExpoContext(expo) {
|
|
46
38
|
const attrs = {};
|
|
@@ -65,22 +57,11 @@ function projectExpoContext(expo) {
|
|
|
65
57
|
//#endregion
|
|
66
58
|
//#region src/context/collectReactNative.ts
|
|
67
59
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* Sources, layered:
|
|
72
|
-
* 1. RN core (`Platform`, `Dimensions`) — read on every call.
|
|
73
|
-
* 2. Expo constants — resolved once (injected for tests, else `loadExpoModules()`)
|
|
74
|
-
* and projected; absent on bare RN.
|
|
75
|
-
*
|
|
76
|
-
* The authenticated user is NOT projected here: `Flare.setUser` (inherited from
|
|
77
|
-
* core) writes the `user.*` identity keys straight to the active scope, the same
|
|
78
|
-
* model node and electron use.
|
|
60
|
+
* Two layers: RN core (`Platform`, `Dimensions`) read per call, and Expo constants resolved once and
|
|
61
|
+
* absent on bare RN. Expo's `osName`/`osVersion` override the RN values where present.
|
|
79
62
|
*
|
|
80
|
-
* `Platform.Version` is a string on iOS
|
|
81
|
-
*
|
|
82
|
-
* (NOT `os.type`, which conventionally means the kernel family); when Expo is
|
|
83
|
-
* present its `osName`/`osVersion` overwrite these coarser values.
|
|
63
|
+
* `Platform.Version` is a string on iOS and a number on Android, hence the stringify. `Platform.OS` maps
|
|
64
|
+
* to `os.name`, not `os.type`, which is the kernel family.
|
|
84
65
|
*/
|
|
85
66
|
function makeReactNativeContextCollector(expo = loadExpoModules()) {
|
|
86
67
|
const expoAttrs = projectExpoContext(expo);
|
|
@@ -104,22 +85,19 @@ function makeReactNativeContextCollector(expo = loadExpoModules()) {
|
|
|
104
85
|
};
|
|
105
86
|
}
|
|
106
87
|
/**
|
|
107
|
-
* Native device model from `Platform.constants
|
|
108
|
-
* `Model`/`Manufacturer`/`Brand`; iOS core
|
|
109
|
-
* needs `expo-device` or a native module), so iOS returns undefined. Prefixes the
|
|
110
|
-
* model with its maker when available (e.g. `Google Pixel 7`).
|
|
88
|
+
* Native device model from `Platform.constants`, maker-prefixed when available (e.g. `Google Pixel 7`).
|
|
89
|
+
* Android exposes `Model`/`Manufacturer`/`Brand`; iOS core surfaces none (needs `expo-device`), so undefined.
|
|
111
90
|
*/
|
|
112
91
|
function nativeModelName() {
|
|
113
|
-
if (react_native.Platform.OS !== "android") return
|
|
92
|
+
if (react_native.Platform.OS !== "android") return;
|
|
114
93
|
const constants = react_native.Platform.constants;
|
|
115
|
-
if (!constants?.Model) return
|
|
94
|
+
if (!constants?.Model) return;
|
|
116
95
|
const maker = constants.Manufacturer ?? constants.Brand;
|
|
117
96
|
return maker ? `${maker} ${constants.Model}` : constants.Model;
|
|
118
97
|
}
|
|
119
98
|
/**
|
|
120
|
-
* Build a human-readable `context.device` group from the
|
|
121
|
-
*
|
|
122
|
-
* naturally omits `model` / `appVersion` / `appId`.
|
|
99
|
+
* Build a human-readable `context.device` group from the semantic attributes. Only present fields are
|
|
100
|
+
* included, so bare RN (no Expo) omits `model`/`appVersion`/`appId`.
|
|
123
101
|
*/
|
|
124
102
|
function buildDeviceContext(attrs) {
|
|
125
103
|
const device = {};
|
|
@@ -138,16 +116,13 @@ function buildDeviceContext(attrs) {
|
|
|
138
116
|
//#endregion
|
|
139
117
|
//#region src/handlers/appStateFlush.ts
|
|
140
118
|
/**
|
|
141
|
-
* Flush the log buffer when the app
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* (not every blur).
|
|
119
|
+
* Flush the log buffer when the app backgrounds. Gate on `background` only: iOS fires `inactive` on every
|
|
120
|
+
* transient interruption (app-switcher peek, Control Center, incoming call), so gating avoids flooding the
|
|
121
|
+
* network. Mirrors the browser scheduler gating on `hidden`, not every blur. Delivery is best-effort (see
|
|
122
|
+
* `ReactNativeFlushScheduler`).
|
|
146
123
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* Returns an uninstaller that removes the listener via the subscription handle
|
|
150
|
-
* (modern RN API — do NOT use the removed `AppState.removeEventListener`).
|
|
124
|
+
* Returns an uninstaller that removes the listener via the subscription handle (modern RN API; do NOT use
|
|
125
|
+
* the removed `AppState.removeEventListener`).
|
|
151
126
|
*/
|
|
152
127
|
function installAppStateFlush(getFlush) {
|
|
153
128
|
const subscription = react_native.AppState.addEventListener("change", (state) => {
|
|
@@ -169,24 +144,14 @@ function getErrorUtils() {
|
|
|
169
144
|
return globalThis.ErrorUtils;
|
|
170
145
|
}
|
|
171
146
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* (red box in dev, process crash in prod) is preserved — we observe, we do not
|
|
175
|
-
* swallow. No-op when `ErrorUtils` is unavailable.
|
|
176
|
-
*
|
|
177
|
-
* Fatal delivery (`onFatal`). On a FATAL error in a PRODUCTION bundle the
|
|
178
|
-
* previous handler is what tears the app down, and our report is an async
|
|
179
|
-
* `fetch` the OS would kill the instant the app dies — so a bare report almost
|
|
180
|
-
* never sends. When `onFatal` is supplied we DEFER the previous handler until
|
|
181
|
-
* `onFatal()` settles; it drains the transport via core's `flush(timeoutMs)`,
|
|
182
|
-
* buying the report time to send before the crash. RN does not crash on its own
|
|
183
|
-
* (the default handler triggers it), so deferring the delegate genuinely delays
|
|
184
|
-
* the crash. This mirrors Sentry's React Native SDK. It is skipped in `__DEV__`
|
|
185
|
-
* (don't fight the red box / debugger) and guarded by a re-entrancy latch, so a
|
|
186
|
-
* second fatal arriving mid-flush delegates immediately rather than racing two
|
|
187
|
-
* shutdowns.
|
|
147
|
+
* Wraps RN's `ErrorUtils` global handler: observe, do not swallow. The wrapper reports and then delegates
|
|
148
|
+
* to the previous handler, so RN's own behaviour (red box in dev, crash in prod) is preserved.
|
|
188
149
|
*
|
|
189
|
-
*
|
|
150
|
+
* `onFatal` exists because a production fatal tears the app down while our report is still an async fetch
|
|
151
|
+
* the OS kills, so a bare report rarely sends. With it, the previous handler is deferred until the
|
|
152
|
+
* transport drains. Skipped in `__DEV__` so it does not fight the red box, and it only runs for the
|
|
153
|
+
* first fatal, so a second one mid-flush hands straight over instead of starting a second shutdown.
|
|
154
|
+
* Mirrors Sentry's RN SDK.
|
|
190
155
|
*/
|
|
191
156
|
function installGlobalErrorHandler(report, onFatal) {
|
|
192
157
|
const errorUtils = getErrorUtils();
|
|
@@ -218,50 +183,44 @@ function installGlobalErrorHandler(report, onFatal) {
|
|
|
218
183
|
|
|
219
184
|
//#endregion
|
|
220
185
|
//#region src/handlers/rejectionTracking.ts
|
|
186
|
+
/** An injected `null` means "engine absent" and must win over the live global, so this tests for
|
|
187
|
+
* `undefined` rather than falsiness. */
|
|
188
|
+
function resolveHermes(deps) {
|
|
189
|
+
if (deps.hermes !== void 0) return deps.hermes;
|
|
190
|
+
return globalThis.HermesInternal;
|
|
191
|
+
}
|
|
192
|
+
/** Same `undefined`-not-falsy rule as `resolveHermes`. */
|
|
193
|
+
function resolveRequire(deps) {
|
|
194
|
+
if (deps.requirePolyfill !== void 0) return deps.requirePolyfill;
|
|
195
|
+
if (typeof require === "undefined") return null;
|
|
196
|
+
return require;
|
|
197
|
+
}
|
|
221
198
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* `promise` npm polyfill is NOT the runtime Promise on Hermes, so the
|
|
227
|
-
* polyfill's `rejection-tracking.enable()` would hook unused objects and
|
|
228
|
-
* silently never fire — we must use the Hermes hook here.
|
|
229
|
-
* - JSC / non-Hermes: RN polyfills `global.Promise` with the `promise` package,
|
|
230
|
-
* so `promise/setimmediate/rejection-tracking.enable()` is the real hook.
|
|
231
|
-
*
|
|
232
|
-
* Returns null when neither is reachable. Exported (with injectable deps) for
|
|
233
|
-
* direct unit testing of the ordering; not re-exported from the package entry.
|
|
199
|
+
* The rejection enabler for the active JS engine, null when neither is reachable. Order matters: on
|
|
200
|
+
* Hermes the `promise` npm polyfill is not the runtime Promise, so its `rejection-tracking.enable()`
|
|
201
|
+
* would hook unused objects and never fire. On JSC, RN does polyfill `global.Promise` with that package,
|
|
202
|
+
* making it the real hook. Exported with injectable deps so the ordering is unit-testable.
|
|
234
203
|
*/
|
|
235
204
|
function resolveRejectionEnabler(deps = {}) {
|
|
236
|
-
const hermes = deps
|
|
205
|
+
const hermes = resolveHermes(deps);
|
|
237
206
|
if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
|
|
238
|
-
const req = deps
|
|
239
|
-
if (req)
|
|
207
|
+
const req = resolveRequire(deps);
|
|
208
|
+
if (!req) return null;
|
|
209
|
+
try {
|
|
240
210
|
const tracker = req("promise/setimmediate/rejection-tracking");
|
|
241
211
|
if (tracker && typeof tracker.enable === "function") return (options) => tracker.enable(options);
|
|
242
212
|
} catch {}
|
|
243
213
|
return null;
|
|
244
214
|
}
|
|
245
215
|
/**
|
|
246
|
-
* Best-effort capture of unhandled
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* polyfill on JSC. Reasons are routed exactly like the browser
|
|
250
|
-
* `unhandledrejection` handler (core's `routeRejection`), so Error reasons keep
|
|
251
|
-
* their stack via `reportSilently`.
|
|
216
|
+
* Best-effort, engine-aware capture of unhandled rejections. RN routes these through the engine's tracker
|
|
217
|
+
* rather than `window.onunhandledrejection`. With no engine hook reachable this is a no-op; uncaught
|
|
218
|
+
* throws still arrive via ErrorUtils.
|
|
252
219
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* Chaining caveat: enabling REPLACES the engine's current callbacks (RN
|
|
259
|
-
* registers its own dev warning) and neither engine exposes a getter for the
|
|
260
|
-
* previous ones, so we cannot truly chain RN's default. To avoid swallowing that
|
|
261
|
-
* developer signal, `onUnhandled` re-emits a `console.warn` in dev (`__DEV__`).
|
|
262
|
-
*
|
|
263
|
-
* Returns an uninstaller that re-enables with no-op callbacks (no clean disable
|
|
264
|
-
* exists on either engine).
|
|
220
|
+
* Enabling REPLACES the engine's current callbacks, including RN's own dev warning, and neither engine
|
|
221
|
+
* exposes a getter for the previous ones, so chaining is impossible. `onUnhandled` re-emits a
|
|
222
|
+
* `console.warn` in dev to avoid swallowing that signal. For the same reason the uninstaller re-enables
|
|
223
|
+
* with no-op callbacks: neither engine offers a clean disable.
|
|
265
224
|
*/
|
|
266
225
|
function installRejectionTracking(reporter, deps = {}) {
|
|
267
226
|
const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
|
|
@@ -296,15 +255,9 @@ function installRejectionTracking(reporter, deps = {}) {
|
|
|
296
255
|
//#endregion
|
|
297
256
|
//#region src/ReactNativeFlushScheduler.ts
|
|
298
257
|
/**
|
|
299
|
-
* Passive
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* background) is wired separately in `Flare.install()` so it stays symmetric
|
|
303
|
-
* with handler teardown.
|
|
304
|
-
*
|
|
305
|
-
* Deliberately calls `flush()` WITHOUT `{ keepalive: true }`: RN's fetch (over
|
|
306
|
-
* XMLHttpRequest) does not reliably honor keepalive, so a backgrounding flush is
|
|
307
|
-
* best-effort and may be dropped if the OS suspends the app mid-request.
|
|
258
|
+
* Passive: the AppState -> background trigger is wired separately in `Flare.install()`, to stay symmetric
|
|
259
|
+
* with handler teardown. Flushes without `{ keepalive: true }` because RN's fetch runs over XMLHttpRequest
|
|
260
|
+
* and does not reliably honour it, so a backgrounding flush is best-effort.
|
|
308
261
|
*/
|
|
309
262
|
var ReactNativeFlushScheduler = class {
|
|
310
263
|
flushFn = null;
|
|
@@ -313,7 +266,7 @@ var ReactNativeFlushScheduler = class {
|
|
|
313
266
|
}
|
|
314
267
|
getFlush() {
|
|
315
268
|
const flush = this.flushFn;
|
|
316
|
-
if (!flush) return
|
|
269
|
+
if (!flush) return;
|
|
317
270
|
return () => {
|
|
318
271
|
flush();
|
|
319
272
|
};
|
|
@@ -323,23 +276,13 @@ var ReactNativeFlushScheduler = class {
|
|
|
323
276
|
//#endregion
|
|
324
277
|
//#region src/Flare.ts
|
|
325
278
|
const RN_SDK_NAME = "@flareapp/react-native";
|
|
326
|
-
const RN_SDK_VERSION = "2.
|
|
327
|
-
|
|
279
|
+
const RN_SDK_VERSION = "2.8.0";
|
|
280
|
+
/** How long a fatal JS crash holds the app open to drain the transport before RN's default handler runs. */
|
|
328
281
|
const FATAL_FLUSH_TIMEOUT_MS = 2e3;
|
|
329
282
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* - core `Api` (fetch is native in RN),
|
|
334
|
-
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
335
|
-
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
336
|
-
* follow-up),
|
|
337
|
-
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
338
|
-
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
339
|
-
*
|
|
340
|
-
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
341
|
-
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
342
|
-
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
283
|
+
* The RN `Flare` singleton, exposed as `flare` from the package root. Uses `NullFileReader` because there
|
|
284
|
+
* are no runtime snippets on a device (sourcemaps are a Metro follow-up) and `GlobalScopeProvider` because
|
|
285
|
+
* RN has a single app scope.
|
|
343
286
|
*/
|
|
344
287
|
var ReactNativeFlare = class extends _flareapp_core.Flare {
|
|
345
288
|
scheduler;
|
|
@@ -347,10 +290,8 @@ var ReactNativeFlare = class extends _flareapp_core.Flare {
|
|
|
347
290
|
installed = false;
|
|
348
291
|
uninstallers = [];
|
|
349
292
|
/**
|
|
350
|
-
* @param rejectionDeps test seam
|
|
351
|
-
*
|
|
352
|
-
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
353
|
-
* tracker as a leaking side effect.
|
|
293
|
+
* @param rejectionDeps test seam. Default `{}` resolves the active engine's tracker (Hermes or JSC);
|
|
294
|
+
* tests pass `{ enable: null }` so `light()` installs no leaking global tracker.
|
|
354
295
|
*/
|
|
355
296
|
constructor(rejectionDeps = {}) {
|
|
356
297
|
const scheduler = new ReactNativeFlushScheduler();
|
|
@@ -362,37 +303,25 @@ var ReactNativeFlare = class extends _flareapp_core.Flare {
|
|
|
362
303
|
name: RN_SDK_NAME,
|
|
363
304
|
version: RN_SDK_VERSION
|
|
364
305
|
});
|
|
365
|
-
this.setFramework({ name:
|
|
306
|
+
this.setFramework({ name: _flareapp_core.FrameworkName.ReactNative });
|
|
366
307
|
}
|
|
367
308
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
371
|
-
* name here while preserving whatever version the caller supplied (the React
|
|
372
|
-
* renderer version when the boundary tags it).
|
|
309
|
+
* The wrapped `@flareapp/react` boundary tags every flare it injects as `react`, which is wrong here,
|
|
310
|
+
* so coerce the name while keeping whatever version the caller supplied.
|
|
373
311
|
*/
|
|
374
312
|
setFramework(framework) {
|
|
375
313
|
return super.setFramework({
|
|
376
314
|
...framework,
|
|
377
|
-
name:
|
|
315
|
+
name: _flareapp_core.FrameworkName.ReactNative
|
|
378
316
|
});
|
|
379
317
|
}
|
|
380
|
-
/**
|
|
381
|
-
* Set the API key (and optional debug flag), then install the global
|
|
382
|
-
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
383
|
-
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
384
|
-
* idempotent).
|
|
385
|
-
*/
|
|
318
|
+
/** Idempotent: a second `light()` must not double-wrap `ErrorUtils`, which node's reconcile gets for free. */
|
|
386
319
|
light(key, debug) {
|
|
387
320
|
super.light(key, debug);
|
|
388
321
|
this.install();
|
|
389
322
|
return this;
|
|
390
323
|
}
|
|
391
|
-
/**
|
|
392
|
-
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
393
|
-
* and clear the install guard so a later `light()` re-installs. For tests
|
|
394
|
-
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
395
|
-
*/
|
|
324
|
+
/** For tests and manual teardown. Clears the install guard, so a later `light()` re-installs. */
|
|
396
325
|
removeHandlers() {
|
|
397
326
|
for (const uninstall of this.uninstallers) try {
|
|
398
327
|
uninstall();
|
|
@@ -419,15 +348,10 @@ const flare = new ReactNativeFlare();
|
|
|
419
348
|
//#endregion
|
|
420
349
|
//#region src/FlareErrorBoundary.ts
|
|
421
350
|
/**
|
|
422
|
-
* React Native error boundary
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
427
|
-
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
428
|
-
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
429
|
-
* the boundary only calls a core-level method (`reportSilently`), which
|
|
430
|
-
* `ReactNativeFlare` inherits.
|
|
351
|
+
* React Native error boundary: a thin wrapper over `@flareapp/react`'s `/inject` boundary that injects the
|
|
352
|
+
* RN `flare` singleton. `flare` is applied after `{...props}` so a consumer cannot override it. The
|
|
353
|
+
* `as unknown as Flare` cast is needed because the prop is typed against `@flareapp/js/browser`'s `Flare`
|
|
354
|
+
* (a superset); safe at runtime since the boundary only calls `reportSilently`, which RN inherits.
|
|
431
355
|
*/
|
|
432
356
|
function FlareErrorBoundary(props) {
|
|
433
357
|
return (0, react.createElement)(_flareapp_react_inject.FlareErrorBoundary, {
|
package/dist/index.d.cts
CHANGED
|
@@ -15,19 +15,9 @@ type RejectionDeps = {
|
|
|
15
15
|
//#endregion
|
|
16
16
|
//#region src/Flare.d.ts
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* - core `Api` (fetch is native in RN),
|
|
22
|
-
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
23
|
-
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
24
|
-
* follow-up),
|
|
25
|
-
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
26
|
-
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
27
|
-
*
|
|
28
|
-
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
29
|
-
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
30
|
-
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
18
|
+
* The RN `Flare` singleton, exposed as `flare` from the package root. Uses `NullFileReader` because there
|
|
19
|
+
* are no runtime snippets on a device (sourcemaps are a Metro follow-up) and `GlobalScopeProvider` because
|
|
20
|
+
* RN has a single app scope.
|
|
31
21
|
*/
|
|
32
22
|
declare class ReactNativeFlare extends Flare$1 {
|
|
33
23
|
private readonly scheduler;
|
|
@@ -35,32 +25,18 @@ declare class ReactNativeFlare extends Flare$1 {
|
|
|
35
25
|
private installed;
|
|
36
26
|
private uninstallers;
|
|
37
27
|
/**
|
|
38
|
-
* @param rejectionDeps test seam
|
|
39
|
-
*
|
|
40
|
-
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
41
|
-
* tracker as a leaking side effect.
|
|
28
|
+
* @param rejectionDeps test seam. Default `{}` resolves the active engine's tracker (Hermes or JSC);
|
|
29
|
+
* tests pass `{ enable: null }` so `light()` installs no leaking global tracker.
|
|
42
30
|
*/
|
|
43
31
|
constructor(rejectionDeps?: RejectionDeps);
|
|
44
32
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
48
|
-
* name here while preserving whatever version the caller supplied (the React
|
|
49
|
-
* renderer version when the boundary tags it).
|
|
33
|
+
* The wrapped `@flareapp/react` boundary tags every flare it injects as `react`, which is wrong here,
|
|
34
|
+
* so coerce the name while keeping whatever version the caller supplied.
|
|
50
35
|
*/
|
|
51
36
|
setFramework(framework: Framework$1): this;
|
|
52
|
-
/**
|
|
53
|
-
* Set the API key (and optional debug flag), then install the global
|
|
54
|
-
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
55
|
-
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
56
|
-
* idempotent).
|
|
57
|
-
*/
|
|
37
|
+
/** Idempotent: a second `light()` must not double-wrap `ErrorUtils`, which node's reconcile gets for free. */
|
|
58
38
|
light(key?: string, debug?: boolean): this;
|
|
59
|
-
/**
|
|
60
|
-
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
61
|
-
* and clear the install guard so a later `light()` re-installs. For tests
|
|
62
|
-
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
63
|
-
*/
|
|
39
|
+
/** For tests and manual teardown. Clears the install guard, so a later `light()` re-installs. */
|
|
64
40
|
removeHandlers(): void;
|
|
65
41
|
private install;
|
|
66
42
|
}
|
|
@@ -70,15 +46,10 @@ declare const flare: ReactNativeFlare;
|
|
|
70
46
|
//#endregion
|
|
71
47
|
//#region src/FlareErrorBoundary.d.ts
|
|
72
48
|
/**
|
|
73
|
-
* React Native error boundary
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
78
|
-
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
79
|
-
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
80
|
-
* the boundary only calls a core-level method (`reportSilently`), which
|
|
81
|
-
* `ReactNativeFlare` inherits.
|
|
49
|
+
* React Native error boundary: a thin wrapper over `@flareapp/react`'s `/inject` boundary that injects the
|
|
50
|
+
* RN `flare` singleton. `flare` is applied after `{...props}` so a consumer cannot override it. The
|
|
51
|
+
* `as unknown as Flare` cast is needed because the prop is typed against `@flareapp/js/browser`'s `Flare`
|
|
52
|
+
* (a superset); safe at runtime since the boundary only calls `reportSilently`, which RN inherits.
|
|
82
53
|
*/
|
|
83
54
|
declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
|
|
84
55
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -15,19 +15,9 @@ type RejectionDeps = {
|
|
|
15
15
|
//#endregion
|
|
16
16
|
//#region src/Flare.d.ts
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* - core `Api` (fetch is native in RN),
|
|
22
|
-
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
23
|
-
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
24
|
-
* follow-up),
|
|
25
|
-
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
26
|
-
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
27
|
-
*
|
|
28
|
-
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
29
|
-
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
30
|
-
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
18
|
+
* The RN `Flare` singleton, exposed as `flare` from the package root. Uses `NullFileReader` because there
|
|
19
|
+
* are no runtime snippets on a device (sourcemaps are a Metro follow-up) and `GlobalScopeProvider` because
|
|
20
|
+
* RN has a single app scope.
|
|
31
21
|
*/
|
|
32
22
|
declare class ReactNativeFlare extends Flare$1 {
|
|
33
23
|
private readonly scheduler;
|
|
@@ -35,32 +25,18 @@ declare class ReactNativeFlare extends Flare$1 {
|
|
|
35
25
|
private installed;
|
|
36
26
|
private uninstallers;
|
|
37
27
|
/**
|
|
38
|
-
* @param rejectionDeps test seam
|
|
39
|
-
*
|
|
40
|
-
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
41
|
-
* tracker as a leaking side effect.
|
|
28
|
+
* @param rejectionDeps test seam. Default `{}` resolves the active engine's tracker (Hermes or JSC);
|
|
29
|
+
* tests pass `{ enable: null }` so `light()` installs no leaking global tracker.
|
|
42
30
|
*/
|
|
43
31
|
constructor(rejectionDeps?: RejectionDeps);
|
|
44
32
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
48
|
-
* name here while preserving whatever version the caller supplied (the React
|
|
49
|
-
* renderer version when the boundary tags it).
|
|
33
|
+
* The wrapped `@flareapp/react` boundary tags every flare it injects as `react`, which is wrong here,
|
|
34
|
+
* so coerce the name while keeping whatever version the caller supplied.
|
|
50
35
|
*/
|
|
51
36
|
setFramework(framework: Framework$1): this;
|
|
52
|
-
/**
|
|
53
|
-
* Set the API key (and optional debug flag), then install the global
|
|
54
|
-
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
55
|
-
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
56
|
-
* idempotent).
|
|
57
|
-
*/
|
|
37
|
+
/** Idempotent: a second `light()` must not double-wrap `ErrorUtils`, which node's reconcile gets for free. */
|
|
58
38
|
light(key?: string, debug?: boolean): this;
|
|
59
|
-
/**
|
|
60
|
-
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
61
|
-
* and clear the install guard so a later `light()` re-installs. For tests
|
|
62
|
-
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
63
|
-
*/
|
|
39
|
+
/** For tests and manual teardown. Clears the install guard, so a later `light()` re-installs. */
|
|
64
40
|
removeHandlers(): void;
|
|
65
41
|
private install;
|
|
66
42
|
}
|
|
@@ -70,15 +46,10 @@ declare const flare: ReactNativeFlare;
|
|
|
70
46
|
//#endregion
|
|
71
47
|
//#region src/FlareErrorBoundary.d.ts
|
|
72
48
|
/**
|
|
73
|
-
* React Native error boundary
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
78
|
-
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
79
|
-
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
80
|
-
* the boundary only calls a core-level method (`reportSilently`), which
|
|
81
|
-
* `ReactNativeFlare` inherits.
|
|
49
|
+
* React Native error boundary: a thin wrapper over `@flareapp/react`'s `/inject` boundary that injects the
|
|
50
|
+
* RN `flare` singleton. `flare` is applied after `{...props}` so a consumer cannot override it. The
|
|
51
|
+
* `as unknown as Flare` cast is needed because the prop is typed against `@flareapp/js/browser`'s `Flare`
|
|
52
|
+
* (a superset); safe at runtime since the boundary only calls `reportSilently`, which RN inherits.
|
|
82
53
|
*/
|
|
83
54
|
declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
|
|
84
55
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { Api, DEFAULT_URL_DENYLIST, Flare, Flare as Flare$1, GlobalScopeProvider, GlobalScopeProvider as GlobalScopeProvider$1, NullFileReader, NullFileReader as NullFileReader$1, convertToError, convertToError as convertToError$1, redactUrlQuery, resolveDenylist, routeRejection } from "@flareapp/core";
|
|
2
|
+
import { Api, DEFAULT_URL_DENYLIST, Flare, Flare as Flare$1, FrameworkName, GlobalScopeProvider, GlobalScopeProvider as GlobalScopeProvider$1, NullFileReader, NullFileReader as NullFileReader$1, convertToError, convertToError as convertToError$1, redactUrlQuery, resolveDenylist, routeRejection } from "@flareapp/core";
|
|
3
3
|
import { AppState, Dimensions, Platform } from "react-native";
|
|
4
4
|
import { FlareErrorBoundary as FlareErrorBoundary$1 } from "@flareapp/react/inject";
|
|
5
5
|
import { createElement } from "react";
|
|
@@ -10,19 +10,11 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region src/context/expo.ts
|
|
12
12
|
/**
|
|
13
|
-
* Lazy, synchronous Expo load. The `require(...)` calls
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* build error. Aliasing `require` to a local (`const req = require; req('pkg')`)
|
|
19
|
-
* would defeat that static collection — Metro never adds the module to this
|
|
20
|
-
* file's dependency map, so the require would fail to resolve even when the
|
|
21
|
-
* package IS installed. So do NOT reintroduce an alias here.
|
|
22
|
-
*
|
|
23
|
-
* The `typeof require` guard keeps non-Metro/ESM environments (e.g. some test
|
|
24
|
-
* runners, where `require` is undefined) safe; under Metro `require` always
|
|
25
|
-
* exists in the `react-native`/CJS build this package ships.
|
|
13
|
+
* Lazy, synchronous Expo load. The `require(...)` calls MUST be direct string literals: Metro statically
|
|
14
|
+
* collects only literal `require('pkg')` calls, treating those inside a try/catch as optional deps
|
|
15
|
+
* (`allowOptionalDependencies` is on by default), so a missing package degrades to a caught throw not a
|
|
16
|
+
* build error. Do NOT alias `require` to a local; that defeats the static collection and the module never
|
|
17
|
+
* resolves even when installed. The `typeof require` guard keeps non-Metro/ESM envs (some test runners) safe.
|
|
26
18
|
*/
|
|
27
19
|
function loadExpoModules() {
|
|
28
20
|
const mods = {};
|
|
@@ -35,6 +27,7 @@ function loadExpoModules() {
|
|
|
35
27
|
} catch {}
|
|
36
28
|
return mods;
|
|
37
29
|
}
|
|
30
|
+
/** Maps Expo's `DeviceType` enum (UNKNOWN=0, PHONE=1, TABLET=2, DESKTOP=3, TV=4) to a label. */
|
|
38
31
|
const DEVICE_TYPE_LABELS = {
|
|
39
32
|
1: "phone",
|
|
40
33
|
2: "tablet",
|
|
@@ -42,9 +35,8 @@ const DEVICE_TYPE_LABELS = {
|
|
|
42
35
|
4: "tv"
|
|
43
36
|
};
|
|
44
37
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* getters are intentionally not used (the context collector is synchronous).
|
|
38
|
+
* Turn the synchronous Expo constants into report attributes. Only present (non-null) fields are emitted.
|
|
39
|
+
* Async Expo getters are not used, since the context collector is synchronous.
|
|
48
40
|
*/
|
|
49
41
|
function projectExpoContext(expo) {
|
|
50
42
|
const attrs = {};
|
|
@@ -69,22 +61,11 @@ function projectExpoContext(expo) {
|
|
|
69
61
|
//#endregion
|
|
70
62
|
//#region src/context/collectReactNative.ts
|
|
71
63
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* Sources, layered:
|
|
76
|
-
* 1. RN core (`Platform`, `Dimensions`) — read on every call.
|
|
77
|
-
* 2. Expo constants — resolved once (injected for tests, else `loadExpoModules()`)
|
|
78
|
-
* and projected; absent on bare RN.
|
|
79
|
-
*
|
|
80
|
-
* The authenticated user is NOT projected here: `Flare.setUser` (inherited from
|
|
81
|
-
* core) writes the `user.*` identity keys straight to the active scope, the same
|
|
82
|
-
* model node and electron use.
|
|
64
|
+
* Two layers: RN core (`Platform`, `Dimensions`) read per call, and Expo constants resolved once and
|
|
65
|
+
* absent on bare RN. Expo's `osName`/`osVersion` override the RN values where present.
|
|
83
66
|
*
|
|
84
|
-
* `Platform.Version` is a string on iOS
|
|
85
|
-
*
|
|
86
|
-
* (NOT `os.type`, which conventionally means the kernel family); when Expo is
|
|
87
|
-
* present its `osName`/`osVersion` overwrite these coarser values.
|
|
67
|
+
* `Platform.Version` is a string on iOS and a number on Android, hence the stringify. `Platform.OS` maps
|
|
68
|
+
* to `os.name`, not `os.type`, which is the kernel family.
|
|
88
69
|
*/
|
|
89
70
|
function makeReactNativeContextCollector(expo = loadExpoModules()) {
|
|
90
71
|
const expoAttrs = projectExpoContext(expo);
|
|
@@ -108,22 +89,19 @@ function makeReactNativeContextCollector(expo = loadExpoModules()) {
|
|
|
108
89
|
};
|
|
109
90
|
}
|
|
110
91
|
/**
|
|
111
|
-
* Native device model from `Platform.constants
|
|
112
|
-
* `Model`/`Manufacturer`/`Brand`; iOS core
|
|
113
|
-
* needs `expo-device` or a native module), so iOS returns undefined. Prefixes the
|
|
114
|
-
* model with its maker when available (e.g. `Google Pixel 7`).
|
|
92
|
+
* Native device model from `Platform.constants`, maker-prefixed when available (e.g. `Google Pixel 7`).
|
|
93
|
+
* Android exposes `Model`/`Manufacturer`/`Brand`; iOS core surfaces none (needs `expo-device`), so undefined.
|
|
115
94
|
*/
|
|
116
95
|
function nativeModelName() {
|
|
117
|
-
if (Platform.OS !== "android") return
|
|
96
|
+
if (Platform.OS !== "android") return;
|
|
118
97
|
const constants = Platform.constants;
|
|
119
|
-
if (!constants?.Model) return
|
|
98
|
+
if (!constants?.Model) return;
|
|
120
99
|
const maker = constants.Manufacturer ?? constants.Brand;
|
|
121
100
|
return maker ? `${maker} ${constants.Model}` : constants.Model;
|
|
122
101
|
}
|
|
123
102
|
/**
|
|
124
|
-
* Build a human-readable `context.device` group from the
|
|
125
|
-
*
|
|
126
|
-
* naturally omits `model` / `appVersion` / `appId`.
|
|
103
|
+
* Build a human-readable `context.device` group from the semantic attributes. Only present fields are
|
|
104
|
+
* included, so bare RN (no Expo) omits `model`/`appVersion`/`appId`.
|
|
127
105
|
*/
|
|
128
106
|
function buildDeviceContext(attrs) {
|
|
129
107
|
const device = {};
|
|
@@ -142,16 +120,13 @@ function buildDeviceContext(attrs) {
|
|
|
142
120
|
//#endregion
|
|
143
121
|
//#region src/handlers/appStateFlush.ts
|
|
144
122
|
/**
|
|
145
|
-
* Flush the log buffer when the app
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* (not every blur).
|
|
123
|
+
* Flush the log buffer when the app backgrounds. Gate on `background` only: iOS fires `inactive` on every
|
|
124
|
+
* transient interruption (app-switcher peek, Control Center, incoming call), so gating avoids flooding the
|
|
125
|
+
* network. Mirrors the browser scheduler gating on `hidden`, not every blur. Delivery is best-effort (see
|
|
126
|
+
* `ReactNativeFlushScheduler`).
|
|
150
127
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* Returns an uninstaller that removes the listener via the subscription handle
|
|
154
|
-
* (modern RN API — do NOT use the removed `AppState.removeEventListener`).
|
|
128
|
+
* Returns an uninstaller that removes the listener via the subscription handle (modern RN API; do NOT use
|
|
129
|
+
* the removed `AppState.removeEventListener`).
|
|
155
130
|
*/
|
|
156
131
|
function installAppStateFlush(getFlush) {
|
|
157
132
|
const subscription = AppState.addEventListener("change", (state) => {
|
|
@@ -173,24 +148,14 @@ function getErrorUtils() {
|
|
|
173
148
|
return globalThis.ErrorUtils;
|
|
174
149
|
}
|
|
175
150
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* (red box in dev, process crash in prod) is preserved — we observe, we do not
|
|
179
|
-
* swallow. No-op when `ErrorUtils` is unavailable.
|
|
180
|
-
*
|
|
181
|
-
* Fatal delivery (`onFatal`). On a FATAL error in a PRODUCTION bundle the
|
|
182
|
-
* previous handler is what tears the app down, and our report is an async
|
|
183
|
-
* `fetch` the OS would kill the instant the app dies — so a bare report almost
|
|
184
|
-
* never sends. When `onFatal` is supplied we DEFER the previous handler until
|
|
185
|
-
* `onFatal()` settles; it drains the transport via core's `flush(timeoutMs)`,
|
|
186
|
-
* buying the report time to send before the crash. RN does not crash on its own
|
|
187
|
-
* (the default handler triggers it), so deferring the delegate genuinely delays
|
|
188
|
-
* the crash. This mirrors Sentry's React Native SDK. It is skipped in `__DEV__`
|
|
189
|
-
* (don't fight the red box / debugger) and guarded by a re-entrancy latch, so a
|
|
190
|
-
* second fatal arriving mid-flush delegates immediately rather than racing two
|
|
191
|
-
* shutdowns.
|
|
151
|
+
* Wraps RN's `ErrorUtils` global handler: observe, do not swallow. The wrapper reports and then delegates
|
|
152
|
+
* to the previous handler, so RN's own behaviour (red box in dev, crash in prod) is preserved.
|
|
192
153
|
*
|
|
193
|
-
*
|
|
154
|
+
* `onFatal` exists because a production fatal tears the app down while our report is still an async fetch
|
|
155
|
+
* the OS kills, so a bare report rarely sends. With it, the previous handler is deferred until the
|
|
156
|
+
* transport drains. Skipped in `__DEV__` so it does not fight the red box, and it only runs for the
|
|
157
|
+
* first fatal, so a second one mid-flush hands straight over instead of starting a second shutdown.
|
|
158
|
+
* Mirrors Sentry's RN SDK.
|
|
194
159
|
*/
|
|
195
160
|
function installGlobalErrorHandler(report, onFatal) {
|
|
196
161
|
const errorUtils = getErrorUtils();
|
|
@@ -222,50 +187,44 @@ function installGlobalErrorHandler(report, onFatal) {
|
|
|
222
187
|
|
|
223
188
|
//#endregion
|
|
224
189
|
//#region src/handlers/rejectionTracking.ts
|
|
190
|
+
/** An injected `null` means "engine absent" and must win over the live global, so this tests for
|
|
191
|
+
* `undefined` rather than falsiness. */
|
|
192
|
+
function resolveHermes(deps) {
|
|
193
|
+
if (deps.hermes !== void 0) return deps.hermes;
|
|
194
|
+
return globalThis.HermesInternal;
|
|
195
|
+
}
|
|
196
|
+
/** Same `undefined`-not-falsy rule as `resolveHermes`. */
|
|
197
|
+
function resolveRequire(deps) {
|
|
198
|
+
if (deps.requirePolyfill !== void 0) return deps.requirePolyfill;
|
|
199
|
+
if (typeof __require === "undefined") return null;
|
|
200
|
+
return __require;
|
|
201
|
+
}
|
|
225
202
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* `promise` npm polyfill is NOT the runtime Promise on Hermes, so the
|
|
231
|
-
* polyfill's `rejection-tracking.enable()` would hook unused objects and
|
|
232
|
-
* silently never fire — we must use the Hermes hook here.
|
|
233
|
-
* - JSC / non-Hermes: RN polyfills `global.Promise` with the `promise` package,
|
|
234
|
-
* so `promise/setimmediate/rejection-tracking.enable()` is the real hook.
|
|
235
|
-
*
|
|
236
|
-
* Returns null when neither is reachable. Exported (with injectable deps) for
|
|
237
|
-
* direct unit testing of the ordering; not re-exported from the package entry.
|
|
203
|
+
* The rejection enabler for the active JS engine, null when neither is reachable. Order matters: on
|
|
204
|
+
* Hermes the `promise` npm polyfill is not the runtime Promise, so its `rejection-tracking.enable()`
|
|
205
|
+
* would hook unused objects and never fire. On JSC, RN does polyfill `global.Promise` with that package,
|
|
206
|
+
* making it the real hook. Exported with injectable deps so the ordering is unit-testable.
|
|
238
207
|
*/
|
|
239
208
|
function resolveRejectionEnabler(deps = {}) {
|
|
240
|
-
const hermes = deps
|
|
209
|
+
const hermes = resolveHermes(deps);
|
|
241
210
|
if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
|
|
242
|
-
const req = deps
|
|
243
|
-
if (req)
|
|
211
|
+
const req = resolveRequire(deps);
|
|
212
|
+
if (!req) return null;
|
|
213
|
+
try {
|
|
244
214
|
const tracker = req("promise/setimmediate/rejection-tracking");
|
|
245
215
|
if (tracker && typeof tracker.enable === "function") return (options) => tracker.enable(options);
|
|
246
216
|
} catch {}
|
|
247
217
|
return null;
|
|
248
218
|
}
|
|
249
219
|
/**
|
|
250
|
-
* Best-effort capture of unhandled
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* polyfill on JSC. Reasons are routed exactly like the browser
|
|
254
|
-
* `unhandledrejection` handler (core's `routeRejection`), so Error reasons keep
|
|
255
|
-
* their stack via `reportSilently`.
|
|
220
|
+
* Best-effort, engine-aware capture of unhandled rejections. RN routes these through the engine's tracker
|
|
221
|
+
* rather than `window.onunhandledrejection`. With no engine hook reachable this is a no-op; uncaught
|
|
222
|
+
* throws still arrive via ErrorUtils.
|
|
256
223
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* Chaining caveat: enabling REPLACES the engine's current callbacks (RN
|
|
263
|
-
* registers its own dev warning) and neither engine exposes a getter for the
|
|
264
|
-
* previous ones, so we cannot truly chain RN's default. To avoid swallowing that
|
|
265
|
-
* developer signal, `onUnhandled` re-emits a `console.warn` in dev (`__DEV__`).
|
|
266
|
-
*
|
|
267
|
-
* Returns an uninstaller that re-enables with no-op callbacks (no clean disable
|
|
268
|
-
* exists on either engine).
|
|
224
|
+
* Enabling REPLACES the engine's current callbacks, including RN's own dev warning, and neither engine
|
|
225
|
+
* exposes a getter for the previous ones, so chaining is impossible. `onUnhandled` re-emits a
|
|
226
|
+
* `console.warn` in dev to avoid swallowing that signal. For the same reason the uninstaller re-enables
|
|
227
|
+
* with no-op callbacks: neither engine offers a clean disable.
|
|
269
228
|
*/
|
|
270
229
|
function installRejectionTracking(reporter, deps = {}) {
|
|
271
230
|
const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
|
|
@@ -300,15 +259,9 @@ function installRejectionTracking(reporter, deps = {}) {
|
|
|
300
259
|
//#endregion
|
|
301
260
|
//#region src/ReactNativeFlushScheduler.ts
|
|
302
261
|
/**
|
|
303
|
-
* Passive
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* background) is wired separately in `Flare.install()` so it stays symmetric
|
|
307
|
-
* with handler teardown.
|
|
308
|
-
*
|
|
309
|
-
* Deliberately calls `flush()` WITHOUT `{ keepalive: true }`: RN's fetch (over
|
|
310
|
-
* XMLHttpRequest) does not reliably honor keepalive, so a backgrounding flush is
|
|
311
|
-
* best-effort and may be dropped if the OS suspends the app mid-request.
|
|
262
|
+
* Passive: the AppState -> background trigger is wired separately in `Flare.install()`, to stay symmetric
|
|
263
|
+
* with handler teardown. Flushes without `{ keepalive: true }` because RN's fetch runs over XMLHttpRequest
|
|
264
|
+
* and does not reliably honour it, so a backgrounding flush is best-effort.
|
|
312
265
|
*/
|
|
313
266
|
var ReactNativeFlushScheduler = class {
|
|
314
267
|
flushFn = null;
|
|
@@ -317,7 +270,7 @@ var ReactNativeFlushScheduler = class {
|
|
|
317
270
|
}
|
|
318
271
|
getFlush() {
|
|
319
272
|
const flush = this.flushFn;
|
|
320
|
-
if (!flush) return
|
|
273
|
+
if (!flush) return;
|
|
321
274
|
return () => {
|
|
322
275
|
flush();
|
|
323
276
|
};
|
|
@@ -327,23 +280,13 @@ var ReactNativeFlushScheduler = class {
|
|
|
327
280
|
//#endregion
|
|
328
281
|
//#region src/Flare.ts
|
|
329
282
|
const RN_SDK_NAME = "@flareapp/react-native";
|
|
330
|
-
const RN_SDK_VERSION = "2.
|
|
331
|
-
|
|
283
|
+
const RN_SDK_VERSION = "2.8.0";
|
|
284
|
+
/** How long a fatal JS crash holds the app open to drain the transport before RN's default handler runs. */
|
|
332
285
|
const FATAL_FLUSH_TIMEOUT_MS = 2e3;
|
|
333
286
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* - core `Api` (fetch is native in RN),
|
|
338
|
-
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
339
|
-
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
340
|
-
* follow-up),
|
|
341
|
-
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
342
|
-
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
343
|
-
*
|
|
344
|
-
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
345
|
-
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
346
|
-
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
287
|
+
* The RN `Flare` singleton, exposed as `flare` from the package root. Uses `NullFileReader` because there
|
|
288
|
+
* are no runtime snippets on a device (sourcemaps are a Metro follow-up) and `GlobalScopeProvider` because
|
|
289
|
+
* RN has a single app scope.
|
|
347
290
|
*/
|
|
348
291
|
var ReactNativeFlare = class extends Flare$1 {
|
|
349
292
|
scheduler;
|
|
@@ -351,10 +294,8 @@ var ReactNativeFlare = class extends Flare$1 {
|
|
|
351
294
|
installed = false;
|
|
352
295
|
uninstallers = [];
|
|
353
296
|
/**
|
|
354
|
-
* @param rejectionDeps test seam
|
|
355
|
-
*
|
|
356
|
-
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
357
|
-
* tracker as a leaking side effect.
|
|
297
|
+
* @param rejectionDeps test seam. Default `{}` resolves the active engine's tracker (Hermes or JSC);
|
|
298
|
+
* tests pass `{ enable: null }` so `light()` installs no leaking global tracker.
|
|
358
299
|
*/
|
|
359
300
|
constructor(rejectionDeps = {}) {
|
|
360
301
|
const scheduler = new ReactNativeFlushScheduler();
|
|
@@ -366,37 +307,25 @@ var ReactNativeFlare = class extends Flare$1 {
|
|
|
366
307
|
name: RN_SDK_NAME,
|
|
367
308
|
version: RN_SDK_VERSION
|
|
368
309
|
});
|
|
369
|
-
this.setFramework({ name:
|
|
310
|
+
this.setFramework({ name: FrameworkName.ReactNative });
|
|
370
311
|
}
|
|
371
312
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
375
|
-
* name here while preserving whatever version the caller supplied (the React
|
|
376
|
-
* renderer version when the boundary tags it).
|
|
313
|
+
* The wrapped `@flareapp/react` boundary tags every flare it injects as `react`, which is wrong here,
|
|
314
|
+
* so coerce the name while keeping whatever version the caller supplied.
|
|
377
315
|
*/
|
|
378
316
|
setFramework(framework) {
|
|
379
317
|
return super.setFramework({
|
|
380
318
|
...framework,
|
|
381
|
-
name:
|
|
319
|
+
name: FrameworkName.ReactNative
|
|
382
320
|
});
|
|
383
321
|
}
|
|
384
|
-
/**
|
|
385
|
-
* Set the API key (and optional debug flag), then install the global
|
|
386
|
-
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
387
|
-
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
388
|
-
* idempotent).
|
|
389
|
-
*/
|
|
322
|
+
/** Idempotent: a second `light()` must not double-wrap `ErrorUtils`, which node's reconcile gets for free. */
|
|
390
323
|
light(key, debug) {
|
|
391
324
|
super.light(key, debug);
|
|
392
325
|
this.install();
|
|
393
326
|
return this;
|
|
394
327
|
}
|
|
395
|
-
/**
|
|
396
|
-
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
397
|
-
* and clear the install guard so a later `light()` re-installs. For tests
|
|
398
|
-
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
399
|
-
*/
|
|
328
|
+
/** For tests and manual teardown. Clears the install guard, so a later `light()` re-installs. */
|
|
400
329
|
removeHandlers() {
|
|
401
330
|
for (const uninstall of this.uninstallers) try {
|
|
402
331
|
uninstall();
|
|
@@ -423,15 +352,10 @@ const flare = new ReactNativeFlare();
|
|
|
423
352
|
//#endregion
|
|
424
353
|
//#region src/FlareErrorBoundary.ts
|
|
425
354
|
/**
|
|
426
|
-
* React Native error boundary
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
431
|
-
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
432
|
-
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
433
|
-
* the boundary only calls a core-level method (`reportSilently`), which
|
|
434
|
-
* `ReactNativeFlare` inherits.
|
|
355
|
+
* React Native error boundary: a thin wrapper over `@flareapp/react`'s `/inject` boundary that injects the
|
|
356
|
+
* RN `flare` singleton. `flare` is applied after `{...props}` so a consumer cannot override it. The
|
|
357
|
+
* `as unknown as Flare` cast is needed because the prop is typed against `@flareapp/js/browser`'s `Flare`
|
|
358
|
+
* (a superset); safe at runtime since the boundary only calls `reportSilently`, which RN inherits.
|
|
435
359
|
*/
|
|
436
360
|
function FlareErrorBoundary(props) {
|
|
437
361
|
return createElement(FlareErrorBoundary$1, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flareapp/react-native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "React Native SDK for flareapp.io",
|
|
5
5
|
"homepage": "https://flareapp.io",
|
|
6
6
|
"bugs": {
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"name": "Spatie",
|
|
16
16
|
"email": "info@spatie.be"
|
|
17
17
|
},
|
|
18
|
+
"contributors": [
|
|
19
|
+
"Dries Heyninck <dries@spatie.be>"
|
|
20
|
+
],
|
|
18
21
|
"files": [
|
|
19
22
|
"dist"
|
|
20
23
|
],
|
|
@@ -46,7 +49,7 @@
|
|
|
46
49
|
"release": "release-it"
|
|
47
50
|
},
|
|
48
51
|
"dependencies": {
|
|
49
|
-
"@flareapp/core": "2.
|
|
52
|
+
"@flareapp/core": "2.8.0"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
52
55
|
"@flareapp/react": "^2.5.0",
|
|
@@ -56,6 +59,7 @@
|
|
|
56
59
|
"devDependencies": {
|
|
57
60
|
"@flareapp/js": "file:../js",
|
|
58
61
|
"@flareapp/react": "file:../react",
|
|
62
|
+
"@flareapp/test-helpers": "*",
|
|
59
63
|
"@types/react": "^19.0.0",
|
|
60
64
|
"react": "^19.0.0",
|
|
61
65
|
"react-native": "^0.79.0",
|