@flareapp/react-native 2.6.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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @flareapp/react-native
2
+
3
+ React Native SDK for [Flare](https://flareapp.io) error tracking by Spatie.
4
+
5
+ Pure JavaScript — works on both Expo (managed) and bare React Native with no
6
+ required native module.
7
+
8
+ ## Requirements
9
+
10
+ React Native **0.79+** (Expo SDK 53+). The package relies on Metro's package
11
+ exports support, which is on by default from RN 0.79. It ships a Metro-targeted
12
+ CJS build via the `"react-native"` export condition, and the `FlareErrorBoundary`
13
+ imports `@flareapp/react/inject` (an export-map subpath) — both need package
14
+ exports enabled.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @flareapp/react-native @flareapp/react
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { flare, FlareErrorBoundary } from '@flareapp/react-native';
26
+
27
+ flare.light('your-project-key');
28
+ flare.setUser({ id: 1, email: 'user@example.com' });
29
+ ```
30
+
31
+ Wrap your app in the boundary to capture React render errors:
32
+
33
+ ```tsx
34
+ <FlareErrorBoundary>
35
+ <App />
36
+ </FlareErrorBoundary>
37
+ ```
38
+
39
+ ## What it captures
40
+
41
+ - Uncaught JS errors (via `ErrorUtils`)
42
+ - React render errors (via `FlareErrorBoundary`)
43
+ - Unhandled promise rejections (best-effort; uses the active JS engine's hook —
44
+ Hermes or JSC)
45
+
46
+ Device/app context is collected from React Native core, enriched with
47
+ `expo-device` / `expo-application` when present. Note: on iOS, Expo's `app.id`
48
+ (Android package name) is not available as a sync constant, so that attribute is
49
+ Android-only.
50
+
51
+ Delivery is best-effort. Reports are sent with `fetch`, which React Native does
52
+ not back with `keepalive`, so a report fired during a fatal JS crash (or while
53
+ the app is being backgrounded/suspended) may not finish sending. This applies to
54
+ JS-fatal errors too, not only native crashes.
55
+
56
+ ## Not yet included
57
+
58
+ - Native crash capture (requires a native module)
59
+ - Metro sourcemap upload (planned as a separate package)
60
+ - Automatic breadcrumbs (use `flare.glow(...)` manually for now)
package/dist/index.cjs ADDED
@@ -0,0 +1,484 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _flareapp_core = require("@flareapp/core");
3
+ let react_native = require("react-native");
4
+ let _flareapp_react_inject = require("@flareapp/react/inject");
5
+ let react = require("react");
6
+
7
+ //#region src/context/expo.ts
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.
22
+ */
23
+ function loadExpoModules() {
24
+ const mods = {};
25
+ if (typeof require === "undefined") return mods;
26
+ try {
27
+ mods.device = require("expo-device");
28
+ } catch {}
29
+ try {
30
+ mods.application = require("expo-application");
31
+ } catch {}
32
+ return mods;
33
+ }
34
+ const DEVICE_TYPE_LABELS = {
35
+ 1: "phone",
36
+ 2: "tablet",
37
+ 3: "desktop",
38
+ 4: "tv"
39
+ };
40
+ /**
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).
44
+ */
45
+ function projectExpoContext(expo) {
46
+ const attrs = {};
47
+ const device = expo.device;
48
+ if (device) {
49
+ if (device.modelName != null) attrs["device.model.name"] = device.modelName;
50
+ if (device.osName != null) attrs["os.name"] = device.osName;
51
+ if (device.osVersion != null) attrs["os.version"] = device.osVersion;
52
+ if (device.deviceType != null) {
53
+ const label = DEVICE_TYPE_LABELS[device.deviceType];
54
+ if (label) attrs["device.type"] = label;
55
+ }
56
+ }
57
+ const application = expo.application;
58
+ if (application) {
59
+ if (application.nativeApplicationVersion != null) attrs["app.version"] = application.nativeApplicationVersion;
60
+ if (application.applicationId != null) attrs["app.id"] = application.applicationId;
61
+ }
62
+ return attrs;
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/context/collectReactNative.ts
67
+ /**
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.
79
+ *
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.
84
+ */
85
+ function makeReactNativeContextCollector(expo = loadExpoModules()) {
86
+ const expoAttrs = projectExpoContext(expo);
87
+ return (_config) => {
88
+ const screen = react_native.Dimensions.get("window");
89
+ const attrs = {
90
+ "os.name": react_native.Platform.OS,
91
+ "os.version": String(react_native.Platform.Version),
92
+ "device.screen.width": screen.width,
93
+ "device.screen.height": screen.height,
94
+ "device.screen.scale": screen.scale,
95
+ ...expoAttrs
96
+ };
97
+ if (attrs["device.model.name"] == null) {
98
+ const model = nativeModelName();
99
+ if (model) attrs["device.model.name"] = model;
100
+ }
101
+ const device = buildDeviceContext(attrs);
102
+ if (Object.keys(device).length > 0) attrs["context.device"] = device;
103
+ return attrs;
104
+ };
105
+ }
106
+ /**
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`).
111
+ */
112
+ function nativeModelName() {
113
+ if (react_native.Platform.OS !== "android") return void 0;
114
+ const constants = react_native.Platform.constants;
115
+ if (!constants?.Model) return void 0;
116
+ const maker = constants.Manufacturer ?? constants.Brand;
117
+ return maker ? `${maker} ${constants.Model}` : constants.Model;
118
+ }
119
+ /**
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`.
123
+ */
124
+ function buildDeviceContext(attrs) {
125
+ const device = {};
126
+ if (attrs["device.model.name"] != null) device.model = attrs["device.model.name"];
127
+ const os = [attrs["os.name"], attrs["os.version"]].filter((v) => v != null).join(" ");
128
+ if (os) device.OS = os;
129
+ const width = attrs["device.screen.width"];
130
+ const height = attrs["device.screen.height"];
131
+ const scale = attrs["device.screen.scale"];
132
+ if (width != null && height != null) device.screen = scale != null ? `${width} × ${height} @ ${scale}x` : `${width} × ${height}`;
133
+ if (attrs["app.version"] != null) device.appVersion = attrs["app.version"];
134
+ if (attrs["app.id"] != null) device.appId = attrs["app.id"];
135
+ return device;
136
+ }
137
+
138
+ //#endregion
139
+ //#region src/handlers/appStateFlush.ts
140
+ /**
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).
146
+ *
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`).
151
+ */
152
+ function installAppStateFlush(getFlush) {
153
+ const subscription = react_native.AppState.addEventListener("change", (state) => {
154
+ if (state === "background") getFlush()?.();
155
+ });
156
+ return () => subscription.remove();
157
+ }
158
+
159
+ //#endregion
160
+ //#region src/devMode.ts
161
+ /** True only in a React Native dev bundle. Safe (false) everywhere else. */
162
+ function inDevMode() {
163
+ return typeof __DEV__ !== "undefined" && __DEV__ === true;
164
+ }
165
+
166
+ //#endregion
167
+ //#region src/handlers/globalErrorHandler.ts
168
+ function getErrorUtils() {
169
+ return globalThis.ErrorUtils;
170
+ }
171
+ /**
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.
188
+ *
189
+ * Returns an uninstaller that restores the previous handler.
190
+ */
191
+ function installGlobalErrorHandler(report, onFatal) {
192
+ const errorUtils = getErrorUtils();
193
+ if (!errorUtils) return () => {};
194
+ const previous = errorUtils.getGlobalHandler();
195
+ let handlingFatal = false;
196
+ const handler = (error, isFatal) => {
197
+ try {
198
+ report((0, _flareapp_core.convertToError)(error), Boolean(isFatal));
199
+ } catch {}
200
+ if (isFatal && onFatal && !inDevMode() && !handlingFatal) {
201
+ handlingFatal = true;
202
+ onFatal().catch(() => {}).then(() => {
203
+ try {
204
+ previous?.(error, isFatal);
205
+ } finally {
206
+ handlingFatal = false;
207
+ }
208
+ });
209
+ return;
210
+ }
211
+ previous?.(error, isFatal);
212
+ };
213
+ errorUtils.setGlobalHandler(handler);
214
+ return () => {
215
+ errorUtils.setGlobalHandler(previous ?? (() => {}));
216
+ };
217
+ }
218
+
219
+ //#endregion
220
+ //#region src/handlers/rejectionTracking.ts
221
+ /**
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.
234
+ */
235
+ function resolveRejectionEnabler(deps = {}) {
236
+ const hermes = deps.hermes !== void 0 ? deps.hermes : globalThis.HermesInternal;
237
+ 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 {
240
+ const tracker = req("promise/setimmediate/rejection-tracking");
241
+ if (tracker && typeof tracker.enable === "function") return (options) => tracker.enable(options);
242
+ } catch {}
243
+ return null;
244
+ }
245
+ /**
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`.
252
+ *
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).
265
+ */
266
+ function installRejectionTracking(reporter, deps = {}) {
267
+ const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
268
+ if (!enable) {
269
+ if (inDevMode()) console.debug("[flare] No promise-rejection hook for this JS engine; rejections not captured.");
270
+ return () => {};
271
+ }
272
+ try {
273
+ enable({
274
+ allRejections: true,
275
+ onUnhandled: (_id, error) => {
276
+ if (inDevMode()) console.warn("[flare] Unhandled promise rejection:", error);
277
+ (0, _flareapp_core.routeRejection)(reporter, error);
278
+ },
279
+ onHandled: () => {}
280
+ });
281
+ } catch {
282
+ if (inDevMode()) console.debug("[flare] Promise-rejection hook threw on enable; rejections not captured.");
283
+ return () => {};
284
+ }
285
+ return () => {
286
+ try {
287
+ enable({
288
+ allRejections: true,
289
+ onUnhandled: () => {},
290
+ onHandled: () => {}
291
+ });
292
+ } catch {}
293
+ };
294
+ }
295
+
296
+ //#endregion
297
+ //#region src/ReactNativeFlushScheduler.ts
298
+ /**
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.
308
+ */
309
+ var ReactNativeFlushScheduler = class {
310
+ flushFn = null;
311
+ register(flush) {
312
+ this.flushFn = flush;
313
+ }
314
+ getFlush() {
315
+ const flush = this.flushFn;
316
+ if (!flush) return void 0;
317
+ return () => {
318
+ flush();
319
+ };
320
+ }
321
+ };
322
+
323
+ //#endregion
324
+ //#region src/Flare.ts
325
+ const RN_SDK_NAME = "@flareapp/react-native";
326
+ const RN_SDK_VERSION = "2.6.0";
327
+ const RN_FRAMEWORK_NAME = "React Native";
328
+ const FATAL_FLUSH_TIMEOUT_MS = 2e3;
329
+ /**
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).
343
+ */
344
+ var ReactNativeFlare = class extends _flareapp_core.Flare {
345
+ scheduler;
346
+ rejectionDeps;
347
+ installed = false;
348
+ uninstallers = [];
349
+ /**
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.
354
+ */
355
+ constructor(rejectionDeps = {}) {
356
+ const scheduler = new ReactNativeFlushScheduler();
357
+ const collector = makeReactNativeContextCollector();
358
+ super(new _flareapp_core.Api(), collector, new _flareapp_core.NullFileReader(), new _flareapp_core.GlobalScopeProvider(), scheduler);
359
+ this.scheduler = scheduler;
360
+ this.rejectionDeps = rejectionDeps;
361
+ this.setSdkInfo({
362
+ name: RN_SDK_NAME,
363
+ version: RN_SDK_VERSION
364
+ });
365
+ this.setFramework({ name: RN_FRAMEWORK_NAME });
366
+ }
367
+ /**
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).
373
+ */
374
+ setFramework(framework) {
375
+ return super.setFramework({
376
+ ...framework,
377
+ name: RN_FRAMEWORK_NAME
378
+ });
379
+ }
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
+ */
386
+ light(key, debug) {
387
+ super.light(key, debug);
388
+ this.install();
389
+ return this;
390
+ }
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
+ */
396
+ removeHandlers() {
397
+ for (const uninstall of this.uninstallers) try {
398
+ uninstall();
399
+ } catch {}
400
+ this.uninstallers = [];
401
+ this.installed = false;
402
+ }
403
+ install() {
404
+ if (this.installed) return;
405
+ this.installed = true;
406
+ this.uninstallers.push(installGlobalErrorHandler((error, isFatal) => {
407
+ this.reportSilently(error, { "error.fatal": isFatal });
408
+ }, () => this.flush(FATAL_FLUSH_TIMEOUT_MS)), installRejectionTracking({
409
+ reportSilently: (error) => this.reportSilently(error),
410
+ reportUnhandledRejection: (message) => this.reportUnhandledRejection(message)
411
+ }, this.rejectionDeps), installAppStateFlush(() => this.scheduler.getFlush()));
412
+ }
413
+ };
414
+
415
+ //#endregion
416
+ //#region src/singleton.ts
417
+ const flare = new ReactNativeFlare();
418
+
419
+ //#endregion
420
+ //#region src/FlareErrorBoundary.ts
421
+ /**
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.
431
+ */
432
+ function FlareErrorBoundary(props) {
433
+ return (0, react.createElement)(_flareapp_react_inject.FlareErrorBoundary, {
434
+ ...props,
435
+ flare
436
+ });
437
+ }
438
+
439
+ //#endregion
440
+ Object.defineProperty(exports, 'DEFAULT_URL_DENYLIST', {
441
+ enumerable: true,
442
+ get: function () {
443
+ return _flareapp_core.DEFAULT_URL_DENYLIST;
444
+ }
445
+ });
446
+ Object.defineProperty(exports, 'Flare', {
447
+ enumerable: true,
448
+ get: function () {
449
+ return _flareapp_core.Flare;
450
+ }
451
+ });
452
+ exports.FlareErrorBoundary = FlareErrorBoundary;
453
+ Object.defineProperty(exports, 'GlobalScopeProvider', {
454
+ enumerable: true,
455
+ get: function () {
456
+ return _flareapp_core.GlobalScopeProvider;
457
+ }
458
+ });
459
+ Object.defineProperty(exports, 'NullFileReader', {
460
+ enumerable: true,
461
+ get: function () {
462
+ return _flareapp_core.NullFileReader;
463
+ }
464
+ });
465
+ exports.ReactNativeFlare = ReactNativeFlare;
466
+ Object.defineProperty(exports, 'convertToError', {
467
+ enumerable: true,
468
+ get: function () {
469
+ return _flareapp_core.convertToError;
470
+ }
471
+ });
472
+ exports.flare = flare;
473
+ Object.defineProperty(exports, 'redactUrlQuery', {
474
+ enumerable: true,
475
+ get: function () {
476
+ return _flareapp_core.redactUrlQuery;
477
+ }
478
+ });
479
+ Object.defineProperty(exports, 'resolveDenylist', {
480
+ enumerable: true,
481
+ get: function () {
482
+ return _flareapp_core.resolveDenylist;
483
+ }
484
+ });
@@ -0,0 +1,85 @@
1
+ import { AttributeValue, Attributes, Config, ContextCollector, DEFAULT_URL_DENYLIST, FileReader, Flare, Flare as Flare$1, FlushFn, FlushScheduler, Framework, Framework as Framework$1, GlobalScopeProvider, Glow, MessageLevel, NullFileReader, Report, ScopeProvider, SdkInfo, StackFrame, User, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
2
+ import { FlareErrorBoundaryProps } from "@flareapp/react/inject";
3
+ import { ReactElement } from "react";
4
+
5
+ //#region src/handlers/rejectionTracking.d.ts
6
+ type EnableOptions = {
7
+ allRejections?: boolean;
8
+ onUnhandled?: (id: number, error: unknown) => void;
9
+ onHandled?: (id: number) => void;
10
+ };
11
+ type RejectionEnabler = (options: EnableOptions) => void;
12
+ type RejectionDeps = {
13
+ enable?: RejectionEnabler | null;
14
+ };
15
+ //#endregion
16
+ //#region src/Flare.d.ts
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).
31
+ */
32
+ declare class ReactNativeFlare extends Flare$1 {
33
+ private readonly scheduler;
34
+ private readonly rejectionDeps;
35
+ private installed;
36
+ private uninstallers;
37
+ /**
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.
42
+ */
43
+ constructor(rejectionDeps?: RejectionDeps);
44
+ /**
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).
50
+ */
51
+ 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
+ */
58
+ 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
+ */
64
+ removeHandlers(): void;
65
+ private install;
66
+ }
67
+ //#endregion
68
+ //#region src/singleton.d.ts
69
+ declare const flare: ReactNativeFlare;
70
+ //#endregion
71
+ //#region src/FlareErrorBoundary.d.ts
72
+ /**
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.
82
+ */
83
+ declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
84
+ //#endregion
85
+ export { type AttributeValue, type Attributes, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type FileReader, Flare, FlareErrorBoundary, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type MessageLevel, NullFileReader, ReactNativeFlare, type Report, type ScopeProvider, type SdkInfo, type StackFrame, type User, convertToError, flare, redactUrlQuery, resolveDenylist };
@@ -0,0 +1,85 @@
1
+ import { AttributeValue, Attributes, Config, ContextCollector, DEFAULT_URL_DENYLIST, FileReader, Flare, Flare as Flare$1, FlushFn, FlushScheduler, Framework, Framework as Framework$1, GlobalScopeProvider, Glow, MessageLevel, NullFileReader, Report, ScopeProvider, SdkInfo, StackFrame, User, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
2
+ import { FlareErrorBoundaryProps } from "@flareapp/react/inject";
3
+ import { ReactElement } from "react";
4
+
5
+ //#region src/handlers/rejectionTracking.d.ts
6
+ type EnableOptions = {
7
+ allRejections?: boolean;
8
+ onUnhandled?: (id: number, error: unknown) => void;
9
+ onHandled?: (id: number) => void;
10
+ };
11
+ type RejectionEnabler = (options: EnableOptions) => void;
12
+ type RejectionDeps = {
13
+ enable?: RejectionEnabler | null;
14
+ };
15
+ //#endregion
16
+ //#region src/Flare.d.ts
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).
31
+ */
32
+ declare class ReactNativeFlare extends Flare$1 {
33
+ private readonly scheduler;
34
+ private readonly rejectionDeps;
35
+ private installed;
36
+ private uninstallers;
37
+ /**
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.
42
+ */
43
+ constructor(rejectionDeps?: RejectionDeps);
44
+ /**
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).
50
+ */
51
+ 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
+ */
58
+ 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
+ */
64
+ removeHandlers(): void;
65
+ private install;
66
+ }
67
+ //#endregion
68
+ //#region src/singleton.d.ts
69
+ declare const flare: ReactNativeFlare;
70
+ //#endregion
71
+ //#region src/FlareErrorBoundary.d.ts
72
+ /**
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.
82
+ */
83
+ declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
84
+ //#endregion
85
+ export { type AttributeValue, type Attributes, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type FileReader, Flare, FlareErrorBoundary, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type MessageLevel, NullFileReader, ReactNativeFlare, type Report, type ScopeProvider, type SdkInfo, type StackFrame, type User, convertToError, flare, redactUrlQuery, resolveDenylist };
package/dist/index.mjs ADDED
@@ -0,0 +1,444 @@
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";
3
+ import { AppState, Dimensions, Platform } from "react-native";
4
+ import { FlareErrorBoundary as FlareErrorBoundary$1 } from "@flareapp/react/inject";
5
+ import { createElement } from "react";
6
+
7
+ //#region \0rolldown/runtime.js
8
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
9
+
10
+ //#endregion
11
+ //#region src/context/expo.ts
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.
26
+ */
27
+ function loadExpoModules() {
28
+ const mods = {};
29
+ if (typeof __require === "undefined") return mods;
30
+ try {
31
+ mods.device = __require("expo-device");
32
+ } catch {}
33
+ try {
34
+ mods.application = __require("expo-application");
35
+ } catch {}
36
+ return mods;
37
+ }
38
+ const DEVICE_TYPE_LABELS = {
39
+ 1: "phone",
40
+ 2: "tablet",
41
+ 3: "desktop",
42
+ 4: "tv"
43
+ };
44
+ /**
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).
48
+ */
49
+ function projectExpoContext(expo) {
50
+ const attrs = {};
51
+ const device = expo.device;
52
+ if (device) {
53
+ if (device.modelName != null) attrs["device.model.name"] = device.modelName;
54
+ if (device.osName != null) attrs["os.name"] = device.osName;
55
+ if (device.osVersion != null) attrs["os.version"] = device.osVersion;
56
+ if (device.deviceType != null) {
57
+ const label = DEVICE_TYPE_LABELS[device.deviceType];
58
+ if (label) attrs["device.type"] = label;
59
+ }
60
+ }
61
+ const application = expo.application;
62
+ if (application) {
63
+ if (application.nativeApplicationVersion != null) attrs["app.version"] = application.nativeApplicationVersion;
64
+ if (application.applicationId != null) attrs["app.id"] = application.applicationId;
65
+ }
66
+ return attrs;
67
+ }
68
+
69
+ //#endregion
70
+ //#region src/context/collectReactNative.ts
71
+ /**
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.
83
+ *
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.
88
+ */
89
+ function makeReactNativeContextCollector(expo = loadExpoModules()) {
90
+ const expoAttrs = projectExpoContext(expo);
91
+ return (_config) => {
92
+ const screen = Dimensions.get("window");
93
+ const attrs = {
94
+ "os.name": Platform.OS,
95
+ "os.version": String(Platform.Version),
96
+ "device.screen.width": screen.width,
97
+ "device.screen.height": screen.height,
98
+ "device.screen.scale": screen.scale,
99
+ ...expoAttrs
100
+ };
101
+ if (attrs["device.model.name"] == null) {
102
+ const model = nativeModelName();
103
+ if (model) attrs["device.model.name"] = model;
104
+ }
105
+ const device = buildDeviceContext(attrs);
106
+ if (Object.keys(device).length > 0) attrs["context.device"] = device;
107
+ return attrs;
108
+ };
109
+ }
110
+ /**
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`).
115
+ */
116
+ function nativeModelName() {
117
+ if (Platform.OS !== "android") return void 0;
118
+ const constants = Platform.constants;
119
+ if (!constants?.Model) return void 0;
120
+ const maker = constants.Manufacturer ?? constants.Brand;
121
+ return maker ? `${maker} ${constants.Model}` : constants.Model;
122
+ }
123
+ /**
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`.
127
+ */
128
+ function buildDeviceContext(attrs) {
129
+ const device = {};
130
+ if (attrs["device.model.name"] != null) device.model = attrs["device.model.name"];
131
+ const os = [attrs["os.name"], attrs["os.version"]].filter((v) => v != null).join(" ");
132
+ if (os) device.OS = os;
133
+ const width = attrs["device.screen.width"];
134
+ const height = attrs["device.screen.height"];
135
+ const scale = attrs["device.screen.scale"];
136
+ if (width != null && height != null) device.screen = scale != null ? `${width} × ${height} @ ${scale}x` : `${width} × ${height}`;
137
+ if (attrs["app.version"] != null) device.appVersion = attrs["app.version"];
138
+ if (attrs["app.id"] != null) device.appId = attrs["app.id"];
139
+ return device;
140
+ }
141
+
142
+ //#endregion
143
+ //#region src/handlers/appStateFlush.ts
144
+ /**
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).
150
+ *
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`).
155
+ */
156
+ function installAppStateFlush(getFlush) {
157
+ const subscription = AppState.addEventListener("change", (state) => {
158
+ if (state === "background") getFlush()?.();
159
+ });
160
+ return () => subscription.remove();
161
+ }
162
+
163
+ //#endregion
164
+ //#region src/devMode.ts
165
+ /** True only in a React Native dev bundle. Safe (false) everywhere else. */
166
+ function inDevMode() {
167
+ return typeof __DEV__ !== "undefined" && __DEV__ === true;
168
+ }
169
+
170
+ //#endregion
171
+ //#region src/handlers/globalErrorHandler.ts
172
+ function getErrorUtils() {
173
+ return globalThis.ErrorUtils;
174
+ }
175
+ /**
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.
192
+ *
193
+ * Returns an uninstaller that restores the previous handler.
194
+ */
195
+ function installGlobalErrorHandler(report, onFatal) {
196
+ const errorUtils = getErrorUtils();
197
+ if (!errorUtils) return () => {};
198
+ const previous = errorUtils.getGlobalHandler();
199
+ let handlingFatal = false;
200
+ const handler = (error, isFatal) => {
201
+ try {
202
+ report(convertToError$1(error), Boolean(isFatal));
203
+ } catch {}
204
+ if (isFatal && onFatal && !inDevMode() && !handlingFatal) {
205
+ handlingFatal = true;
206
+ onFatal().catch(() => {}).then(() => {
207
+ try {
208
+ previous?.(error, isFatal);
209
+ } finally {
210
+ handlingFatal = false;
211
+ }
212
+ });
213
+ return;
214
+ }
215
+ previous?.(error, isFatal);
216
+ };
217
+ errorUtils.setGlobalHandler(handler);
218
+ return () => {
219
+ errorUtils.setGlobalHandler(previous ?? (() => {}));
220
+ };
221
+ }
222
+
223
+ //#endregion
224
+ //#region src/handlers/rejectionTracking.ts
225
+ /**
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.
238
+ */
239
+ function resolveRejectionEnabler(deps = {}) {
240
+ const hermes = deps.hermes !== void 0 ? deps.hermes : globalThis.HermesInternal;
241
+ 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 {
244
+ const tracker = req("promise/setimmediate/rejection-tracking");
245
+ if (tracker && typeof tracker.enable === "function") return (options) => tracker.enable(options);
246
+ } catch {}
247
+ return null;
248
+ }
249
+ /**
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`.
256
+ *
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).
269
+ */
270
+ function installRejectionTracking(reporter, deps = {}) {
271
+ const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
272
+ if (!enable) {
273
+ if (inDevMode()) console.debug("[flare] No promise-rejection hook for this JS engine; rejections not captured.");
274
+ return () => {};
275
+ }
276
+ try {
277
+ enable({
278
+ allRejections: true,
279
+ onUnhandled: (_id, error) => {
280
+ if (inDevMode()) console.warn("[flare] Unhandled promise rejection:", error);
281
+ routeRejection(reporter, error);
282
+ },
283
+ onHandled: () => {}
284
+ });
285
+ } catch {
286
+ if (inDevMode()) console.debug("[flare] Promise-rejection hook threw on enable; rejections not captured.");
287
+ return () => {};
288
+ }
289
+ return () => {
290
+ try {
291
+ enable({
292
+ allRejections: true,
293
+ onUnhandled: () => {},
294
+ onHandled: () => {}
295
+ });
296
+ } catch {}
297
+ };
298
+ }
299
+
300
+ //#endregion
301
+ //#region src/ReactNativeFlushScheduler.ts
302
+ /**
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.
312
+ */
313
+ var ReactNativeFlushScheduler = class {
314
+ flushFn = null;
315
+ register(flush) {
316
+ this.flushFn = flush;
317
+ }
318
+ getFlush() {
319
+ const flush = this.flushFn;
320
+ if (!flush) return void 0;
321
+ return () => {
322
+ flush();
323
+ };
324
+ }
325
+ };
326
+
327
+ //#endregion
328
+ //#region src/Flare.ts
329
+ const RN_SDK_NAME = "@flareapp/react-native";
330
+ const RN_SDK_VERSION = "2.6.0";
331
+ const RN_FRAMEWORK_NAME = "React Native";
332
+ const FATAL_FLUSH_TIMEOUT_MS = 2e3;
333
+ /**
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).
347
+ */
348
+ var ReactNativeFlare = class extends Flare$1 {
349
+ scheduler;
350
+ rejectionDeps;
351
+ installed = false;
352
+ uninstallers = [];
353
+ /**
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.
358
+ */
359
+ constructor(rejectionDeps = {}) {
360
+ const scheduler = new ReactNativeFlushScheduler();
361
+ const collector = makeReactNativeContextCollector();
362
+ super(new Api(), collector, new NullFileReader$1(), new GlobalScopeProvider$1(), scheduler);
363
+ this.scheduler = scheduler;
364
+ this.rejectionDeps = rejectionDeps;
365
+ this.setSdkInfo({
366
+ name: RN_SDK_NAME,
367
+ version: RN_SDK_VERSION
368
+ });
369
+ this.setFramework({ name: RN_FRAMEWORK_NAME });
370
+ }
371
+ /**
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).
377
+ */
378
+ setFramework(framework) {
379
+ return super.setFramework({
380
+ ...framework,
381
+ name: RN_FRAMEWORK_NAME
382
+ });
383
+ }
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
+ */
390
+ light(key, debug) {
391
+ super.light(key, debug);
392
+ this.install();
393
+ return this;
394
+ }
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
+ */
400
+ removeHandlers() {
401
+ for (const uninstall of this.uninstallers) try {
402
+ uninstall();
403
+ } catch {}
404
+ this.uninstallers = [];
405
+ this.installed = false;
406
+ }
407
+ install() {
408
+ if (this.installed) return;
409
+ this.installed = true;
410
+ this.uninstallers.push(installGlobalErrorHandler((error, isFatal) => {
411
+ this.reportSilently(error, { "error.fatal": isFatal });
412
+ }, () => this.flush(FATAL_FLUSH_TIMEOUT_MS)), installRejectionTracking({
413
+ reportSilently: (error) => this.reportSilently(error),
414
+ reportUnhandledRejection: (message) => this.reportUnhandledRejection(message)
415
+ }, this.rejectionDeps), installAppStateFlush(() => this.scheduler.getFlush()));
416
+ }
417
+ };
418
+
419
+ //#endregion
420
+ //#region src/singleton.ts
421
+ const flare = new ReactNativeFlare();
422
+
423
+ //#endregion
424
+ //#region src/FlareErrorBoundary.ts
425
+ /**
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.
435
+ */
436
+ function FlareErrorBoundary(props) {
437
+ return createElement(FlareErrorBoundary$1, {
438
+ ...props,
439
+ flare
440
+ });
441
+ }
442
+
443
+ //#endregion
444
+ export { DEFAULT_URL_DENYLIST, Flare, FlareErrorBoundary, GlobalScopeProvider, NullFileReader, ReactNativeFlare, convertToError, flare, redactUrlQuery, resolveDenylist };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@flareapp/react-native",
3
+ "version": "2.6.0",
4
+ "description": "React Native SDK for flareapp.io",
5
+ "homepage": "https://flareapp.io",
6
+ "bugs": {
7
+ "url": "https://github.com/spatie/flare-client-js/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/spatie/flare-client-js.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Spatie",
16
+ "email": "info@spatie.be"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "main": "./dist/index.cjs",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.cts",
24
+ "exports": {
25
+ ".": {
26
+ "react-native": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ },
30
+ "import": {
31
+ "types": "./dist/index.d.mts",
32
+ "default": "./dist/index.mjs"
33
+ },
34
+ "require": {
35
+ "types": "./dist/index.d.cts",
36
+ "default": "./dist/index.cjs"
37
+ }
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "scripts": {
42
+ "prepublishOnly": "npm run build",
43
+ "build": "tsdown src/index.ts --format cjs,esm --dts --env.FLARE_JS_CLIENT_VERSION=$(node -p \"require('./package.json').version\") --clean",
44
+ "test": "vitest run",
45
+ "typescript": "tsc --noEmit",
46
+ "release": "release-it"
47
+ },
48
+ "dependencies": {
49
+ "@flareapp/core": "2.6.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@flareapp/react": "^2.5.0",
53
+ "react": "^18.0.0||^19.0.0",
54
+ "react-native": ">=0.79.0"
55
+ },
56
+ "devDependencies": {
57
+ "@flareapp/js": "file:../js",
58
+ "@flareapp/react": "file:../react",
59
+ "@types/react": "^19.0.0",
60
+ "react": "^19.0.0",
61
+ "react-native": "^0.79.0",
62
+ "tsdown": "^0.20.3",
63
+ "typescript": "^5.7.0",
64
+ "vitest": "^4.0.18"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ }
69
+ }