@flareapp/react-native 2.10.0 → 2.12.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
@@ -5,13 +5,6 @@ let _flareapp_react_inject = require("@flareapp/react/inject");
5
5
  let react = require("react");
6
6
 
7
7
  //#region src/context/expo.ts
8
- /**
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.
14
- */
15
8
  function loadExpoModules() {
16
9
  const mods = {};
17
10
  if (typeof require === "undefined") return mods;
@@ -23,71 +16,67 @@ function loadExpoModules() {
23
16
  } catch {}
24
17
  return mods;
25
18
  }
26
- /** Maps Expo's `DeviceType` enum (UNKNOWN=0, PHONE=1, TABLET=2, DESKTOP=3, TV=4) to a label. */
27
19
  const DEVICE_TYPE_LABELS = {
28
20
  1: "phone",
29
21
  2: "tablet",
30
22
  3: "desktop",
31
23
  4: "tv"
32
24
  };
33
- /**
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.
36
- */
37
- function projectExpoContext(expo) {
38
- const attrs = {};
25
+ function expoToDeviceInfo(expo) {
26
+ const info = {};
39
27
  const device = expo.device;
40
28
  if (device) {
41
- if (device.modelName != null) attrs["device.model.name"] = device.modelName;
42
- if (device.osName != null) attrs["os.name"] = device.osName;
43
- if (device.osVersion != null) attrs["os.version"] = device.osVersion;
44
- if (device.deviceType != null) {
45
- const label = DEVICE_TYPE_LABELS[device.deviceType];
46
- if (label) attrs["device.type"] = label;
47
- }
29
+ const os = {};
30
+ if (device.osName != null) os.name = device.osName;
31
+ if (device.osVersion != null) os.version = device.osVersion;
32
+ if (Object.keys(os).length > 0) info.os = os;
33
+ const target = {};
34
+ if (device.modelName != null) target.model = device.modelName;
35
+ if (device.deviceType != null && DEVICE_TYPE_LABELS[device.deviceType]) target.type = DEVICE_TYPE_LABELS[device.deviceType];
36
+ if (Object.keys(target).length > 0) info.device = target;
48
37
  }
49
38
  const application = expo.application;
50
39
  if (application) {
51
- if (application.nativeApplicationVersion != null) attrs["app.version"] = application.nativeApplicationVersion;
52
- if (application.applicationId != null) attrs["app.id"] = application.applicationId;
40
+ const app = {};
41
+ if (application.nativeApplicationVersion != null) app.version = application.nativeApplicationVersion;
42
+ if (application.applicationId != null) app.id = application.applicationId;
43
+ if (Object.keys(app).length > 0) info.app = app;
53
44
  }
54
- return attrs;
45
+ return info;
55
46
  }
56
47
 
57
48
  //#endregion
58
- //#region src/context/collectReactNative.ts
59
- /**
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.
62
- *
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.
65
- */
66
- function makeReactNativeContextCollector(expo = loadExpoModules()) {
67
- const expoAttrs = projectExpoContext(expo);
68
- return (_config) => {
49
+ //#region src/context/deviceInfo.ts
50
+ var ReactNativeDeviceInfoProvider = class {
51
+ expoInfo;
52
+ constructor(expo = loadExpoModules()) {
53
+ this.expoInfo = expoToDeviceInfo(expo);
54
+ }
55
+ collect() {
69
56
  const screen = react_native.Dimensions.get("window");
70
- const attrs = {
71
- "os.name": react_native.Platform.OS,
72
- "os.version": String(react_native.Platform.Version),
73
- "device.screen.width": screen.width,
74
- "device.screen.height": screen.height,
75
- "device.screen.scale": screen.scale,
76
- ...expoAttrs
57
+ const device = {
58
+ screen: {
59
+ width: screen.width,
60
+ height: screen.height,
61
+ scale: screen.scale
62
+ },
63
+ ...this.expoInfo.device
77
64
  };
78
- if (attrs["device.model.name"] == null) {
65
+ if (device.model == null) {
79
66
  const model = nativeModelName();
80
- if (model) attrs["device.model.name"] = model;
67
+ if (model != null) device.model = model;
81
68
  }
82
- const device = buildDeviceContext(attrs);
83
- if (Object.keys(device).length > 0) attrs["context.device"] = device;
84
- return attrs;
85
- };
86
- }
87
- /**
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.
90
- */
69
+ const info = {
70
+ os: {
71
+ name: this.expoInfo.os?.name ?? react_native.Platform.OS,
72
+ version: this.expoInfo.os?.version ?? String(react_native.Platform.Version)
73
+ },
74
+ device
75
+ };
76
+ if (this.expoInfo.app) info.app = this.expoInfo.app;
77
+ return info;
78
+ }
79
+ };
91
80
  function nativeModelName() {
92
81
  if (react_native.Platform.OS !== "android") return;
93
82
  const constants = react_native.Platform.constants;
@@ -95,35 +84,16 @@ function nativeModelName() {
95
84
  const maker = constants.Manufacturer ?? constants.Brand;
96
85
  return maker ? `${maker} ${constants.Model}` : constants.Model;
97
86
  }
98
- /**
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`.
101
- */
102
- function buildDeviceContext(attrs) {
103
- const device = {};
104
- if (attrs["device.model.name"] != null) device.model = attrs["device.model.name"];
105
- const os = [attrs["os.name"], attrs["os.version"]].filter((v) => v != null).join(" ");
106
- if (os) device.OS = os;
107
- const width = attrs["device.screen.width"];
108
- const height = attrs["device.screen.height"];
109
- const scale = attrs["device.screen.scale"];
110
- if (width != null && height != null) device.screen = scale != null ? `${width} × ${height} @ ${scale}x` : `${width} × ${height}`;
111
- if (attrs["app.version"] != null) device.appVersion = attrs["app.version"];
112
- if (attrs["app.id"] != null) device.appId = attrs["app.id"];
113
- return device;
87
+
88
+ //#endregion
89
+ //#region src/context/collectReactNative.ts
90
+ function makeReactNativeContextCollector(expo = loadExpoModules()) {
91
+ const provider = new ReactNativeDeviceInfoProvider(expo);
92
+ return (_config) => (0, _flareapp_core.deviceInfoToAttributes)(provider.collect());
114
93
  }
115
94
 
116
95
  //#endregion
117
96
  //#region src/handlers/appStateFlush.ts
118
- /**
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`).
123
- *
124
- * Returns an uninstaller that removes the listener via the subscription handle (modern RN API; do NOT use
125
- * the removed `AppState.removeEventListener`).
126
- */
127
97
  function installAppStateFlush(getFlush) {
128
98
  const subscription = react_native.AppState.addEventListener("change", (state) => {
129
99
  if (state === "background") getFlush()?.();
@@ -133,7 +103,6 @@ function installAppStateFlush(getFlush) {
133
103
 
134
104
  //#endregion
135
105
  //#region src/devMode.ts
136
- /** True only in a React Native dev bundle. Safe (false) everywhere else. */
137
106
  function inDevMode() {
138
107
  return typeof __DEV__ !== "undefined" && __DEV__ === true;
139
108
  }
@@ -143,16 +112,6 @@ function inDevMode() {
143
112
  function getErrorUtils() {
144
113
  return globalThis.ErrorUtils;
145
114
  }
146
- /**
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.
149
- *
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.
155
- */
156
115
  function installGlobalErrorHandler(report, onFatal) {
157
116
  const errorUtils = getErrorUtils();
158
117
  if (!errorUtils) return () => {};
@@ -183,24 +142,15 @@ function installGlobalErrorHandler(report, onFatal) {
183
142
 
184
143
  //#endregion
185
144
  //#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
145
  function resolveHermes(deps) {
189
146
  if (deps.hermes !== void 0) return deps.hermes;
190
147
  return globalThis.HermesInternal;
191
148
  }
192
- /** Same `undefined`-not-falsy rule as `resolveHermes`. */
193
149
  function resolveRequire(deps) {
194
150
  if (deps.requirePolyfill !== void 0) return deps.requirePolyfill;
195
151
  if (typeof require === "undefined") return null;
196
152
  return require;
197
153
  }
198
- /**
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.
203
- */
204
154
  function resolveRejectionEnabler(deps = {}) {
205
155
  const hermes = resolveHermes(deps);
206
156
  if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
@@ -212,16 +162,6 @@ function resolveRejectionEnabler(deps = {}) {
212
162
  } catch {}
213
163
  return null;
214
164
  }
215
- /**
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.
219
- *
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.
224
- */
225
165
  function installRejectionTracking(reporter, deps = {}) {
226
166
  const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
227
167
  if (!enable) {
@@ -254,11 +194,6 @@ function installRejectionTracking(reporter, deps = {}) {
254
194
 
255
195
  //#endregion
256
196
  //#region src/ReactNativeFlushScheduler.ts
257
- /**
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.
261
- */
262
197
  var ReactNativeFlushScheduler = class {
263
198
  flushFn = null;
264
199
  register(flush) {
@@ -276,13 +211,11 @@ var ReactNativeFlushScheduler = class {
276
211
  //#endregion
277
212
  //#region src/Flare.ts
278
213
  const RN_SDK_NAME = "@flareapp/react-native";
279
- const RN_SDK_VERSION = "2.10.0";
280
- /** How long a fatal JS crash holds the app open to drain the transport before RN's default handler runs. */
214
+ const RN_SDK_VERSION = "2.12.0";
281
215
  const FATAL_FLUSH_TIMEOUT_MS = 2e3;
282
216
  /**
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.
217
+ * The RN `Flare` singleton, exposed as `flare`. Uses `NullFileReader` (no runtime snippets on device) and
218
+ * `GlobalScopeProvider` (RN has a single app scope).
286
219
  */
287
220
  var ReactNativeFlare = class extends _flareapp_core.Flare {
288
221
  scheduler;
@@ -348,10 +281,9 @@ const flare = new ReactNativeFlare();
348
281
  //#endregion
349
282
  //#region src/FlareErrorBoundary.ts
350
283
  /**
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.
284
+ * Wraps `@flareapp/react`'s `/inject` boundary with the RN `flare` singleton, applied after `{...props}`
285
+ * so callers can't override it. The `Flare` cast is safe: the boundary only calls `reportSilently`, which
286
+ * RN implements too.
355
287
  */
356
288
  function FlareErrorBoundary(props) {
357
289
  return (0, react.createElement)(_flareapp_react_inject.FlareErrorBoundary, {
package/dist/index.d.cts CHANGED
@@ -15,9 +15,8 @@ type RejectionDeps = {
15
15
  //#endregion
16
16
  //#region src/Flare.d.ts
17
17
  /**
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.
18
+ * The RN `Flare` singleton, exposed as `flare`. Uses `NullFileReader` (no runtime snippets on device) and
19
+ * `GlobalScopeProvider` (RN has a single app scope).
21
20
  */
22
21
  declare class ReactNativeFlare extends Flare$1 {
23
22
  private readonly scheduler;
@@ -46,10 +45,9 @@ declare const flare: ReactNativeFlare;
46
45
  //#endregion
47
46
  //#region src/FlareErrorBoundary.d.ts
48
47
  /**
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.
48
+ * Wraps `@flareapp/react`'s `/inject` boundary with the RN `flare` singleton, applied after `{...props}`
49
+ * so callers can't override it. The `Flare` cast is safe: the boundary only calls `reportSilently`, which
50
+ * RN implements too.
53
51
  */
54
52
  declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
55
53
  //#endregion
package/dist/index.d.mts CHANGED
@@ -15,9 +15,8 @@ type RejectionDeps = {
15
15
  //#endregion
16
16
  //#region src/Flare.d.ts
17
17
  /**
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.
18
+ * The RN `Flare` singleton, exposed as `flare`. Uses `NullFileReader` (no runtime snippets on device) and
19
+ * `GlobalScopeProvider` (RN has a single app scope).
21
20
  */
22
21
  declare class ReactNativeFlare extends Flare$1 {
23
22
  private readonly scheduler;
@@ -46,10 +45,9 @@ declare const flare: ReactNativeFlare;
46
45
  //#endregion
47
46
  //#region src/FlareErrorBoundary.d.ts
48
47
  /**
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.
48
+ * Wraps `@flareapp/react`'s `/inject` boundary with the RN `flare` singleton, applied after `{...props}`
49
+ * so callers can't override it. The `Flare` cast is safe: the boundary only calls `reportSilently`, which
50
+ * RN implements too.
53
51
  */
54
52
  declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
55
53
  //#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, FrameworkName, 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, deviceInfoToAttributes, 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";
@@ -9,13 +9,6 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
9
9
 
10
10
  //#endregion
11
11
  //#region src/context/expo.ts
12
- /**
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.
18
- */
19
12
  function loadExpoModules() {
20
13
  const mods = {};
21
14
  if (typeof __require === "undefined") return mods;
@@ -27,71 +20,67 @@ function loadExpoModules() {
27
20
  } catch {}
28
21
  return mods;
29
22
  }
30
- /** Maps Expo's `DeviceType` enum (UNKNOWN=0, PHONE=1, TABLET=2, DESKTOP=3, TV=4) to a label. */
31
23
  const DEVICE_TYPE_LABELS = {
32
24
  1: "phone",
33
25
  2: "tablet",
34
26
  3: "desktop",
35
27
  4: "tv"
36
28
  };
37
- /**
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.
40
- */
41
- function projectExpoContext(expo) {
42
- const attrs = {};
29
+ function expoToDeviceInfo(expo) {
30
+ const info = {};
43
31
  const device = expo.device;
44
32
  if (device) {
45
- if (device.modelName != null) attrs["device.model.name"] = device.modelName;
46
- if (device.osName != null) attrs["os.name"] = device.osName;
47
- if (device.osVersion != null) attrs["os.version"] = device.osVersion;
48
- if (device.deviceType != null) {
49
- const label = DEVICE_TYPE_LABELS[device.deviceType];
50
- if (label) attrs["device.type"] = label;
51
- }
33
+ const os = {};
34
+ if (device.osName != null) os.name = device.osName;
35
+ if (device.osVersion != null) os.version = device.osVersion;
36
+ if (Object.keys(os).length > 0) info.os = os;
37
+ const target = {};
38
+ if (device.modelName != null) target.model = device.modelName;
39
+ if (device.deviceType != null && DEVICE_TYPE_LABELS[device.deviceType]) target.type = DEVICE_TYPE_LABELS[device.deviceType];
40
+ if (Object.keys(target).length > 0) info.device = target;
52
41
  }
53
42
  const application = expo.application;
54
43
  if (application) {
55
- if (application.nativeApplicationVersion != null) attrs["app.version"] = application.nativeApplicationVersion;
56
- if (application.applicationId != null) attrs["app.id"] = application.applicationId;
44
+ const app = {};
45
+ if (application.nativeApplicationVersion != null) app.version = application.nativeApplicationVersion;
46
+ if (application.applicationId != null) app.id = application.applicationId;
47
+ if (Object.keys(app).length > 0) info.app = app;
57
48
  }
58
- return attrs;
49
+ return info;
59
50
  }
60
51
 
61
52
  //#endregion
62
- //#region src/context/collectReactNative.ts
63
- /**
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.
66
- *
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.
69
- */
70
- function makeReactNativeContextCollector(expo = loadExpoModules()) {
71
- const expoAttrs = projectExpoContext(expo);
72
- return (_config) => {
53
+ //#region src/context/deviceInfo.ts
54
+ var ReactNativeDeviceInfoProvider = class {
55
+ expoInfo;
56
+ constructor(expo = loadExpoModules()) {
57
+ this.expoInfo = expoToDeviceInfo(expo);
58
+ }
59
+ collect() {
73
60
  const screen = Dimensions.get("window");
74
- const attrs = {
75
- "os.name": Platform.OS,
76
- "os.version": String(Platform.Version),
77
- "device.screen.width": screen.width,
78
- "device.screen.height": screen.height,
79
- "device.screen.scale": screen.scale,
80
- ...expoAttrs
61
+ const device = {
62
+ screen: {
63
+ width: screen.width,
64
+ height: screen.height,
65
+ scale: screen.scale
66
+ },
67
+ ...this.expoInfo.device
81
68
  };
82
- if (attrs["device.model.name"] == null) {
69
+ if (device.model == null) {
83
70
  const model = nativeModelName();
84
- if (model) attrs["device.model.name"] = model;
71
+ if (model != null) device.model = model;
85
72
  }
86
- const device = buildDeviceContext(attrs);
87
- if (Object.keys(device).length > 0) attrs["context.device"] = device;
88
- return attrs;
89
- };
90
- }
91
- /**
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.
94
- */
73
+ const info = {
74
+ os: {
75
+ name: this.expoInfo.os?.name ?? Platform.OS,
76
+ version: this.expoInfo.os?.version ?? String(Platform.Version)
77
+ },
78
+ device
79
+ };
80
+ if (this.expoInfo.app) info.app = this.expoInfo.app;
81
+ return info;
82
+ }
83
+ };
95
84
  function nativeModelName() {
96
85
  if (Platform.OS !== "android") return;
97
86
  const constants = Platform.constants;
@@ -99,35 +88,16 @@ function nativeModelName() {
99
88
  const maker = constants.Manufacturer ?? constants.Brand;
100
89
  return maker ? `${maker} ${constants.Model}` : constants.Model;
101
90
  }
102
- /**
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`.
105
- */
106
- function buildDeviceContext(attrs) {
107
- const device = {};
108
- if (attrs["device.model.name"] != null) device.model = attrs["device.model.name"];
109
- const os = [attrs["os.name"], attrs["os.version"]].filter((v) => v != null).join(" ");
110
- if (os) device.OS = os;
111
- const width = attrs["device.screen.width"];
112
- const height = attrs["device.screen.height"];
113
- const scale = attrs["device.screen.scale"];
114
- if (width != null && height != null) device.screen = scale != null ? `${width} × ${height} @ ${scale}x` : `${width} × ${height}`;
115
- if (attrs["app.version"] != null) device.appVersion = attrs["app.version"];
116
- if (attrs["app.id"] != null) device.appId = attrs["app.id"];
117
- return device;
91
+
92
+ //#endregion
93
+ //#region src/context/collectReactNative.ts
94
+ function makeReactNativeContextCollector(expo = loadExpoModules()) {
95
+ const provider = new ReactNativeDeviceInfoProvider(expo);
96
+ return (_config) => deviceInfoToAttributes(provider.collect());
118
97
  }
119
98
 
120
99
  //#endregion
121
100
  //#region src/handlers/appStateFlush.ts
122
- /**
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`).
127
- *
128
- * Returns an uninstaller that removes the listener via the subscription handle (modern RN API; do NOT use
129
- * the removed `AppState.removeEventListener`).
130
- */
131
101
  function installAppStateFlush(getFlush) {
132
102
  const subscription = AppState.addEventListener("change", (state) => {
133
103
  if (state === "background") getFlush()?.();
@@ -137,7 +107,6 @@ function installAppStateFlush(getFlush) {
137
107
 
138
108
  //#endregion
139
109
  //#region src/devMode.ts
140
- /** True only in a React Native dev bundle. Safe (false) everywhere else. */
141
110
  function inDevMode() {
142
111
  return typeof __DEV__ !== "undefined" && __DEV__ === true;
143
112
  }
@@ -147,16 +116,6 @@ function inDevMode() {
147
116
  function getErrorUtils() {
148
117
  return globalThis.ErrorUtils;
149
118
  }
150
- /**
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.
153
- *
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.
159
- */
160
119
  function installGlobalErrorHandler(report, onFatal) {
161
120
  const errorUtils = getErrorUtils();
162
121
  if (!errorUtils) return () => {};
@@ -187,24 +146,15 @@ function installGlobalErrorHandler(report, onFatal) {
187
146
 
188
147
  //#endregion
189
148
  //#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
149
  function resolveHermes(deps) {
193
150
  if (deps.hermes !== void 0) return deps.hermes;
194
151
  return globalThis.HermesInternal;
195
152
  }
196
- /** Same `undefined`-not-falsy rule as `resolveHermes`. */
197
153
  function resolveRequire(deps) {
198
154
  if (deps.requirePolyfill !== void 0) return deps.requirePolyfill;
199
155
  if (typeof __require === "undefined") return null;
200
156
  return __require;
201
157
  }
202
- /**
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.
207
- */
208
158
  function resolveRejectionEnabler(deps = {}) {
209
159
  const hermes = resolveHermes(deps);
210
160
  if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
@@ -216,16 +166,6 @@ function resolveRejectionEnabler(deps = {}) {
216
166
  } catch {}
217
167
  return null;
218
168
  }
219
- /**
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.
223
- *
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.
228
- */
229
169
  function installRejectionTracking(reporter, deps = {}) {
230
170
  const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
231
171
  if (!enable) {
@@ -258,11 +198,6 @@ function installRejectionTracking(reporter, deps = {}) {
258
198
 
259
199
  //#endregion
260
200
  //#region src/ReactNativeFlushScheduler.ts
261
- /**
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.
265
- */
266
201
  var ReactNativeFlushScheduler = class {
267
202
  flushFn = null;
268
203
  register(flush) {
@@ -280,13 +215,11 @@ var ReactNativeFlushScheduler = class {
280
215
  //#endregion
281
216
  //#region src/Flare.ts
282
217
  const RN_SDK_NAME = "@flareapp/react-native";
283
- const RN_SDK_VERSION = "2.10.0";
284
- /** How long a fatal JS crash holds the app open to drain the transport before RN's default handler runs. */
218
+ const RN_SDK_VERSION = "2.12.0";
285
219
  const FATAL_FLUSH_TIMEOUT_MS = 2e3;
286
220
  /**
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.
221
+ * The RN `Flare` singleton, exposed as `flare`. Uses `NullFileReader` (no runtime snippets on device) and
222
+ * `GlobalScopeProvider` (RN has a single app scope).
290
223
  */
291
224
  var ReactNativeFlare = class extends Flare$1 {
292
225
  scheduler;
@@ -352,10 +285,9 @@ const flare = new ReactNativeFlare();
352
285
  //#endregion
353
286
  //#region src/FlareErrorBoundary.ts
354
287
  /**
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.
288
+ * Wraps `@flareapp/react`'s `/inject` boundary with the RN `flare` singleton, applied after `{...props}`
289
+ * so callers can't override it. The `Flare` cast is safe: the boundary only calls `reportSilently`, which
290
+ * RN implements too.
359
291
  */
360
292
  function FlareErrorBoundary(props) {
361
293
  return createElement(FlareErrorBoundary$1, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flareapp/react-native",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "React Native SDK for flareapp.io",
5
5
  "homepage": "https://flareapp.io",
6
6
  "bugs": {
@@ -49,7 +49,7 @@
49
49
  "release": "release-it"
50
50
  },
51
51
  "dependencies": {
52
- "@flareapp/core": "2.10.0"
52
+ "@flareapp/core": "2.12.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@flareapp/react": "^2.5.0",