@flareapp/react-native 2.7.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 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 are DIRECT string
10
- * literals on purpose: Metro statically collects only literal `require('pkg')`
11
- * calls and treats those inside a try/catch as OPTIONAL dependencies
12
- * (`allowOptionalDependencies` is on by default for the React Native CLI and
13
- * Expo), so a missing package degrades to a caught runtime throw instead of a
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
- * Project the synchronous Expo constants into report attributes. Only the
42
- * fields that are present (non-null, non-undefined) are emitted. Async Expo
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
- * Build the React Native `ContextCollector` that core's `Flare` calls on every
69
- * report. Synchronous, matching `ContextCollector = (config) => Attributes`.
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 but a number on Android, so it is
81
- * stringified for the `os.version` attribute. `Platform.OS` maps to `os.name`
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`. Android exposes
108
- * `Model`/`Manufacturer`/`Brand`; iOS core does not surface a device model (it
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 void 0;
92
+ if (react_native.Platform.OS !== "android") return;
114
93
  const constants = react_native.Platform.constants;
115
- if (!constants?.Model) return void 0;
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 collected semantic
121
- * attributes. Only present fields are included, so the bare app (no Expo)
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 moves to the background. iOS fires
142
- * `inactive` on every transient interruption (app-switcher peek, Control Center,
143
- * incoming call), so we gate on `background` ONLY to avoid flooding the network
144
- * with redundant flushes. This mirrors the browser scheduler gating on `hidden`
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
- * Delivery is best-effort: see `ReactNativeFlushScheduler`.
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
- * Wrap RN's `ErrorUtils` global handler. The wrapper reports the error and then
173
- * delegates to the previously-registered handler, so React Native's own behavior
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
- * Returns an uninstaller that restores the previous handler.
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
- * Resolve the promise-rejection enabler for the ACTIVE JS engine.
223
- *
224
- * - Hermes (RN's default engine since 0.70) tracks rejections on its native
225
- * Promise via `global.HermesInternal.enablePromiseRejectionTracker`. The
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.hermes !== void 0 ? deps.hermes : globalThis.HermesInternal;
205
+ const hermes = resolveHermes(deps);
237
206
  if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
238
- const req = deps.requirePolyfill !== void 0 ? deps.requirePolyfill : typeof require !== "undefined" ? require : null;
239
- if (req) try {
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 promise rejections, engine-aware. RN routes
247
- * these through its engine's tracker (NOT `window.onunhandledrejection`):
248
- * `HermesInternal.enablePromiseRejectionTracker` on Hermes, the `promise`
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
- * Engine-dependent, NOT version-dependent: if no engine hook is reachable this
254
- * is a no-op (uncaught throws via ErrorUtils still work) and emits a dev-only
255
- * debug line; it must never crash. The `enable(...)` invocation is wrapped so a
256
- * throwing engine hook degrades to no-op instead of propagating.
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 flush scheduler for React Native. Core's `Logger` calls `register`
300
- * once during construction; this stores the flush callback and exposes a plain,
301
- * argument-less caller via `getFlush()`. The actual trigger (AppState ->
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 void 0;
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.7.0";
327
- const RN_FRAMEWORK_NAME = "React Native";
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
- * React Native `Flare` singleton (exposed as `flare` from the package root).
331
- *
332
- * Subclasses core's `Flare`, injecting the RN seams:
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 for the rejection hook. Defaults to `{}`
351
- * (resolve the active engine's tracker Hermes or JSC). Tests pass
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: RN_FRAMEWORK_NAME });
306
+ this.setFramework({ name: _flareapp_core.FrameworkName.ReactNative });
366
307
  }
367
308
  /**
368
- * Force the framework identity to "React Native". The wrapped
369
- * `@flareapp/react` boundary tags every flare it injects as `React` (via
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: RN_FRAMEWORK_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. A thin wrapper over `@flareapp/react`'s
423
- * `/inject` boundary that injects the RN `flare` singleton.
424
- *
425
- * `flare` is applied AFTER `{...props}` so a consumer cannot override the
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
- * React Native `Flare` singleton (exposed as `flare` from the package root).
19
- *
20
- * Subclasses core's `Flare`, injecting the RN seams:
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 for the rejection hook. Defaults to `{}`
39
- * (resolve the active engine's tracker Hermes or JSC). Tests pass
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
- * Force the framework identity to "React Native". The wrapped
46
- * `@flareapp/react` boundary tags every flare it injects as `React` (via
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. A thin wrapper over `@flareapp/react`'s
74
- * `/inject` boundary that injects the RN `flare` singleton.
75
- *
76
- * `flare` is applied AFTER `{...props}` so a consumer cannot override the
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
- * React Native `Flare` singleton (exposed as `flare` from the package root).
19
- *
20
- * Subclasses core's `Flare`, injecting the RN seams:
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 for the rejection hook. Defaults to `{}`
39
- * (resolve the active engine's tracker Hermes or JSC). Tests pass
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
- * Force the framework identity to "React Native". The wrapped
46
- * `@flareapp/react` boundary tags every flare it injects as `React` (via
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. A thin wrapper over `@flareapp/react`'s
74
- * `/inject` boundary that injects the RN `flare` singleton.
75
- *
76
- * `flare` is applied AFTER `{...props}` so a consumer cannot override the
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 are DIRECT string
14
- * literals on purpose: Metro statically collects only literal `require('pkg')`
15
- * calls and treats those inside a try/catch as OPTIONAL dependencies
16
- * (`allowOptionalDependencies` is on by default for the React Native CLI and
17
- * Expo), so a missing package degrades to a caught runtime throw instead of a
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
- * Project the synchronous Expo constants into report attributes. Only the
46
- * fields that are present (non-null, non-undefined) are emitted. Async Expo
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
- * Build the React Native `ContextCollector` that core's `Flare` calls on every
73
- * report. Synchronous, matching `ContextCollector = (config) => Attributes`.
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 but a number on Android, so it is
85
- * stringified for the `os.version` attribute. `Platform.OS` maps to `os.name`
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`. Android exposes
112
- * `Model`/`Manufacturer`/`Brand`; iOS core does not surface a device model (it
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 void 0;
96
+ if (Platform.OS !== "android") return;
118
97
  const constants = Platform.constants;
119
- if (!constants?.Model) return void 0;
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 collected semantic
125
- * attributes. Only present fields are included, so the bare app (no Expo)
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 moves to the background. iOS fires
146
- * `inactive` on every transient interruption (app-switcher peek, Control Center,
147
- * incoming call), so we gate on `background` ONLY to avoid flooding the network
148
- * with redundant flushes. This mirrors the browser scheduler gating on `hidden`
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
- * Delivery is best-effort: see `ReactNativeFlushScheduler`.
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
- * Wrap RN's `ErrorUtils` global handler. The wrapper reports the error and then
177
- * delegates to the previously-registered handler, so React Native's own behavior
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
- * Returns an uninstaller that restores the previous handler.
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
- * Resolve the promise-rejection enabler for the ACTIVE JS engine.
227
- *
228
- * - Hermes (RN's default engine since 0.70) tracks rejections on its native
229
- * Promise via `global.HermesInternal.enablePromiseRejectionTracker`. The
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.hermes !== void 0 ? deps.hermes : globalThis.HermesInternal;
209
+ const hermes = resolveHermes(deps);
241
210
  if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
242
- const req = deps.requirePolyfill !== void 0 ? deps.requirePolyfill : typeof __require !== "undefined" ? __require : null;
243
- if (req) try {
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 promise rejections, engine-aware. RN routes
251
- * these through its engine's tracker (NOT `window.onunhandledrejection`):
252
- * `HermesInternal.enablePromiseRejectionTracker` on Hermes, the `promise`
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
- * Engine-dependent, NOT version-dependent: if no engine hook is reachable this
258
- * is a no-op (uncaught throws via ErrorUtils still work) and emits a dev-only
259
- * debug line; it must never crash. The `enable(...)` invocation is wrapped so a
260
- * throwing engine hook degrades to no-op instead of propagating.
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 flush scheduler for React Native. Core's `Logger` calls `register`
304
- * once during construction; this stores the flush callback and exposes a plain,
305
- * argument-less caller via `getFlush()`. The actual trigger (AppState ->
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 void 0;
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.7.0";
331
- const RN_FRAMEWORK_NAME = "React Native";
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
- * React Native `Flare` singleton (exposed as `flare` from the package root).
335
- *
336
- * Subclasses core's `Flare`, injecting the RN seams:
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 for the rejection hook. Defaults to `{}`
355
- * (resolve the active engine's tracker Hermes or JSC). Tests pass
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: RN_FRAMEWORK_NAME });
310
+ this.setFramework({ name: FrameworkName.ReactNative });
370
311
  }
371
312
  /**
372
- * Force the framework identity to "React Native". The wrapped
373
- * `@flareapp/react` boundary tags every flare it injects as `React` (via
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: RN_FRAMEWORK_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. A thin wrapper over `@flareapp/react`'s
427
- * `/inject` boundary that injects the RN `flare` singleton.
428
- *
429
- * `flare` is applied AFTER `{...props}` so a consumer cannot override the
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.7.0",
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.7.0"
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",