@lumin-monitor/react-native 0.1.0 → 0.4.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/CHANGELOG.md ADDED
@@ -0,0 +1,139 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@lumin-monitor/react-native` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.4.0] - 2026-05-26
9
+
10
+ ### Added
11
+
12
+ - **Mobile device classification.** The SDK now sends an `X-Lumin-Client`
13
+ header on every batch — `sdk=rn/0.4.0; os=ios; os_version=17.4` plus
14
+ optional `device=<model>` when the new `deviceInfo` init option is
15
+ provided. The Lumin API uses the header to fill the same `ua_os` /
16
+ `ua_device_type` columns the browser SDK has always populated via the
17
+ User-Agent header, so mobile sessions stop showing up as empty / desktop
18
+ in the sessions UI. A new `ua_device` column captures the device model.
19
+ - `init({ deviceInfo })` — pass an instance of `react-native-device-info`
20
+ (or any `DeviceInfoLike` shim) to include the device model. Opt-in;
21
+ omit it and the SDK still reports `os` + `os_version` from React Native's
22
+ built-in `Platform` constants. Not auto-detected — same opt-in policy as
23
+ `storage` (0.3.0), to keep Metro bundling clean.
24
+ - Exported `DeviceInfoLike` type for consumers writing their own shims.
25
+
26
+ ### Server compatibility
27
+
28
+ Requires Lumin API at or after the matching server release (which adds the
29
+ `X-Lumin-Client` parser and the `ua_device` column migration). Older
30
+ servers will ignore the header — events still ingest, mobile rows just
31
+ keep landing with empty `ua_*` columns.
32
+
33
+ ## [0.3.0] - 2026-05-26
34
+
35
+ ### Fixed
36
+
37
+ - **`init()` no longer throws under Metro / Expo** ([report]). The 0.1.0–0.2.0
38
+ releases used a runtime `require("react-native")` inside `detectAppState()`.
39
+ Metro's static dependency scanner doesn't see requires nested in function
40
+ bodies, so `react-native` was excluded from the bundle and the call threw
41
+ `Requiring unknown module "react-native"` at app startup. Replaced with a
42
+ top-level `import { AppState } from "react-native"`, which Metro picks up
43
+ and resolves through the consumer's already-installed peer dep. tsup keeps
44
+ `react-native` externalised so the import survives into the published
45
+ artifact untouched.
46
+
47
+ ### Changed (BREAKING)
48
+
49
+ - **AsyncStorage is no longer auto-detected.** The previous behaviour did a
50
+ runtime `require("@react-native-async-storage/async-storage")`, which has
51
+ the same Metro problem as the `react-native` require above — and forced
52
+ every consumer to declare an unused peer dep even when they didn't want
53
+ persistence. Apps that want persistence now import AsyncStorage themselves
54
+ and pass it through:
55
+
56
+ ```ts
57
+ import AsyncStorage from "@react-native-async-storage/async-storage";
58
+ import { init } from "@lumin-monitor/react-native";
59
+
60
+ const lumin = init({ apiKey, storage: AsyncStorage });
61
+ ```
62
+
63
+ Omitting `storage` falls back to in-memory ids (the same behaviour 0.2.0
64
+ exhibited when the optional peer dep wasn't installed). `@react-native-async-storage/async-storage`
65
+ has been removed from `peerDependencies` and `peerDependenciesMeta`; the
66
+ SDK itself no longer references the package.
67
+
68
+ ### Migration
69
+
70
+ - If you were on 0.2.0 and had `@react-native-async-storage/async-storage`
71
+ installed: import it and pass it as `storage` to `init()`. Two lines of
72
+ app code.
73
+ - If you didn't have it installed: nothing to change. You were already getting
74
+ in-memory ids (silently), which is still the default in 0.3.0.
75
+
76
+ [report]: https://github.com/tamso-labs/lumin/issues <!-- replace with the actual issue link when filed -->
77
+
78
+ ## [0.2.0] - 2026-05-26
79
+
80
+ ### Added
81
+
82
+ - `captureError(err, properties?)` — capture an exception as a Lumin event.
83
+ Accepts a real `Error` (preferred — preserves stack and constructor name as
84
+ `error_type`), a string, or a plain object with a `message` field. `null` /
85
+ `undefined` are silently ignored. The same `Error` instance captured twice
86
+ in one tick is deduped.
87
+ - Automatic capture of unhandled JS errors via `ErrorUtils.setGlobalHandler`.
88
+ Enabled by default; opt out with `captureUnhandledErrors: false`. The
89
+ handler **chains to the previous handler**, so RN's red-box, LogBox, and any
90
+ other error tool already installed still fires — the SDK never swallows the
91
+ throw.
92
+ - Fatal errors (`isFatal: true`) trigger an immediate `flush()` so the error
93
+ row has a chance to leave the device before the RN runtime tears down, and
94
+ are stamped with `properties: { fatal: true }`.
95
+ - `errorUtils` init option — override the auto-detected global ErrorUtils
96
+ (mainly for tests).
97
+ - New wire `EventType` value `"error"` plus optional `error_message`,
98
+ `error_stack`, and `error_type` fields on `WireEvent`.
99
+
100
+ ### Notes
101
+
102
+ - Native iOS / Android crashes are not captured. `ErrorUtils` is JS-only;
103
+ native crash reporting requires a native module (Crashlytics, Sentry-native,
104
+ Bugsnag's native bridge) which is out of scope for this SDK.
105
+ - Source maps are not symbolicated server-side. Stacks ship as the RN bundle's
106
+ minified frames.
107
+ - Requires Lumin API v0.x (this release) or newer — older servers will reject
108
+ `type: "error"` events with a 400.
109
+
110
+ ## [0.1.0] - 2026-05-26
111
+
112
+ Initial release. Mirrors `@lumin-monitor/browser` for React Native; same
113
+ `/v1/events` ingest, same `lmn_pub_*` API key kind, so a single Lumin project
114
+ can hold events from web and mobile and tie them to the same user via
115
+ `identify`.
116
+
117
+ ### Added
118
+
119
+ - `init(options)` returning a `LuminClient` with bound `screen` / `track` /
120
+ `identify` / `flush` / `close` / `getSessionId` / `getAnonymousId` methods.
121
+ - Synchronous `init()` with lazy AsyncStorage id hydration — buffered events
122
+ await hydration before the first flush, so apps can call `screen` /
123
+ `track` immediately after `init`.
124
+ - Persistent `anonymous_id` and `session_id` via
125
+ `@react-native-async-storage/async-storage` (optional peer dep); graceful
126
+ in-memory fallback when the peer dep is missing.
127
+ - Idle-based session rotation (`sessionIdleMs`, default 30 min) — matches the
128
+ GA / Mixpanel convention for mobile sessions.
129
+ - Auto-flush on `AppState` transition to `background` or `inactive`.
130
+ - `screen()` method (RN idiom); emits wire `type: "page"` for cross-platform
131
+ compatibility with the browser SDK.
132
+ - Optional `@lumin-monitor/react-native/react-navigation` subpath exporting
133
+ `useLuminScreenviews(client, navigationRef)` for React Navigation v6+ apps.
134
+ - Ships dual ESM + CJS bundle with full `.d.ts` types.
135
+
136
+ [0.4.0]: https://github.com/tamso-labs/lumin/releases/tag/sdk-react-native-v0.4.0
137
+ [0.3.0]: https://github.com/tamso-labs/lumin/releases/tag/sdk-react-native-v0.3.0
138
+ [0.2.0]: https://github.com/tamso-labs/lumin/releases/tag/sdk-react-native-v0.2.0
139
+ [0.1.0]: https://github.com/tamso-labs/lumin/releases/tag/sdk-react-native-v0.1.0
package/README.md CHANGED
@@ -13,26 +13,36 @@ the same user.
13
13
  ## Install
14
14
 
15
15
  ```sh
16
- pnpm add @lumin-monitor/react-native @react-native-async-storage/async-storage
17
- # or: npm install / yarn add
16
+ pnpm add @lumin-monitor/react-native
17
+ # plus, if you want persistent ids across app launches (recommended):
18
+ pnpm add @react-native-async-storage/async-storage
19
+ # plus, if you want device-model tracking in the sessions UI:
20
+ pnpm add react-native-device-info
18
21
  ```
19
22
 
20
- `@react-native-async-storage/async-storage` is an optional peer dep. The
21
- SDK uses it to persist `anonymous_id` across app launches and to expire
22
- `session_id` after 30 minutes of inactivity. If it is not installed, the
23
- SDK falls back to in-memory ids that reset on every cold start — useful
24
- for prototypes, but you almost certainly want the peer dep in production.
23
+ `@react-native-async-storage/async-storage` is no longer auto-detected
24
+ (it broke Metro bundling see [CHANGELOG 0.3.0](./CHANGELOG.md)). Install
25
+ it and pass it to `init({ storage })` to persist `anonymous_id` across app
26
+ launches and let the 30-minute idle session window survive a cold start.
27
+ Omit `storage` (or pass `null`) to use in-memory ids that reset on every
28
+ cold start — fine for prototypes.
25
29
 
26
- React Navigation is also an optional peer dep, only needed if you import
30
+ `react-native-device-info` is also opt-in (same reason). Without it the
31
+ SDK still reports `os` + `os_version` via the `X-Lumin-Client` header —
32
+ see [Device classification](#device-classification).
33
+
34
+ React Navigation is an optional peer dep, only needed if you import
27
35
  `@lumin-monitor/react-native/react-navigation`.
28
36
 
29
37
  ## Quick start
30
38
 
31
39
  ```ts
40
+ import AsyncStorage from "@react-native-async-storage/async-storage";
32
41
  import { init } from "@lumin-monitor/react-native";
33
42
 
34
43
  export const lumin = init({
35
44
  apiKey: process.env.EXPO_PUBLIC_LUMIN_BROWSER_API_KEY!,
45
+ storage: AsyncStorage,
36
46
  });
37
47
 
38
48
  // Bound methods, safe to destructure:
@@ -72,8 +82,11 @@ hydration to complete before sending.
72
82
  | `sessionIdleMs` | `1800000` (30 min) | A new session id is minted on the next event after this much inactivity. |
73
83
  | `onError` | `console.warn` | Called as `(err, droppedCount)` when a batch fails. |
74
84
  | `fetch` | global `fetch` | Override for tests. |
75
- | `storage` | auto-detect AsyncStorage | Pass a custom `AsyncStorageLike`, or `null` to opt out (in-memory ids). |
85
+ | `storage` | `null` (in-memory ids) | Pass `AsyncStorage` for persistence across launches. See *Install* above. |
76
86
  | `appState` | auto-detect react-native | Pass `null` to disable the background-flush listener. |
87
+ | `captureUnhandledErrors` | `true` | Install an `ErrorUtils.setGlobalHandler` chain. See *Error capture* below. |
88
+ | `errorUtils` | auto-detect global ErrorUtils | Override the ErrorUtils-like object the SDK installs into. |
89
+ | `deviceInfo` | `null` (no device model) | Pass `DeviceInfo` from `react-native-device-info` to include the device model in the `X-Lumin-Client` header. See *Device classification* below. |
77
90
 
78
91
  ### `screen(name?, properties?)`
79
92
 
@@ -115,6 +128,50 @@ identify("user_abc123", { plan: "indie", signedUpAt: "2026-05-01" });
115
128
  Re-call on every app launch while the user is signed in. It is cheap
116
129
  and ensures a cold start still binds the session.
117
130
 
131
+ ### `captureError(err, properties?)`
132
+
133
+ Capture an error. The auto-installed global handler catches uncaught JS
134
+ throws (and rejected promises that bubble up to RN); call this directly
135
+ from `try/catch` blocks where you'd otherwise swallow the failure.
136
+
137
+ ```ts
138
+ try {
139
+ await applyDiscount(code);
140
+ } catch (err) {
141
+ captureError(err, { code, step: "checkout" });
142
+ showToast("Discount didn't apply");
143
+ }
144
+ ```
145
+
146
+ Accepts a real `Error` (preferred — preserves stack and constructor name
147
+ as `error_type`), or any value that will be stringified for the message.
148
+ `null` / `undefined` are silently ignored. The same `Error` object
149
+ captured twice in the same tick is deduped.
150
+
151
+ ### Auto error capture
152
+
153
+ When `captureUnhandledErrors` is true (the default), the SDK installs a
154
+ handler via `ErrorUtils.setGlobalHandler` that **chains to the previous
155
+ handler**, so RN's red-box, the LogBox, and any other error tool already
156
+ installed still fire. The SDK does not swallow the throw.
157
+
158
+ Fatal errors (`isFatal: true`) trigger an immediate `flush()` so the
159
+ error row has a chance to leave the device before the RN runtime tears
160
+ down. Best-effort: the OS may suspend the JS thread before the request
161
+ lands, but the standard ~30 s background grace window covers the
162
+ common case.
163
+
164
+ **What is NOT captured**:
165
+ - **Native iOS / Android crashes.** RN's `ErrorUtils` is JS-only. Native
166
+ crashes need a native module (Crashlytics, Sentry-native, Bugsnag's
167
+ native bridge) — out of scope for this SDK.
168
+ - **Source-mapped stacks.** Stacks ship as the minified RN bundle frames
169
+ ship them. Symbolication against the bundle's source maps is a v2
170
+ concern.
171
+
172
+ Opt out with `captureUnhandledErrors: false` if another tool already
173
+ owns the global handler; manual `captureError(err)` still works.
174
+
118
175
  ### `flush(): Promise<void>`
119
176
 
120
177
  Force a flush of any buffered events. The SDK auto-flushes on AppState
@@ -161,6 +218,45 @@ tracking, call `screen()` manually from the route's effect.
161
218
  Peer deps: `react >= 18`, `@react-navigation/native >= 6`. Both are
162
219
  optional — the subpath only loads them when imported.
163
220
 
221
+ ## Device classification
222
+
223
+ Browser SDKs send a meaningful `User-Agent`; the Lumin server parses it
224
+ into `ua_browser` / `ua_os` / `ua_device_type` columns that the sessions
225
+ UI groups by. React Native's `fetch` sends `okhttp/…` or `CFNetwork/…`,
226
+ which carries no device info, so the SDK ships an `X-Lumin-Client` header
227
+ on every batch instead:
228
+
229
+ ```
230
+ X-Lumin-Client: sdk=rn/0.4.0; os=ios; os_version=17.4
231
+ ```
232
+
233
+ `os` and `os_version` come from React Native's built-in `Platform`
234
+ constants — no peer dep required. The server folds the header into the
235
+ same `ua_os` / `ua_device_type` columns the browser SDK has always used,
236
+ so mobile sessions show up in the UI alongside web sessions.
237
+
238
+ For the device model (`iPhone15,3`, `Pixel 7`), install
239
+ [`react-native-device-info`](https://www.npmjs.com/package/react-native-device-info)
240
+ and pass it through:
241
+
242
+ ```ts
243
+ import DeviceInfo from "react-native-device-info";
244
+ import AsyncStorage from "@react-native-async-storage/async-storage";
245
+ import { init } from "@lumin-monitor/react-native";
246
+
247
+ export const lumin = init({
248
+ apiKey: process.env.EXPO_PUBLIC_LUMIN_BROWSER_API_KEY!,
249
+ storage: AsyncStorage,
250
+ deviceInfo: DeviceInfo, // populates X-Lumin-Client: device=<model>
251
+ });
252
+ ```
253
+
254
+ The SDK never `require()`s `react-native-device-info` — same opt-in
255
+ policy as `storage`, to keep Metro bundling clean (see CHANGELOG 0.3.0).
256
+ If the installed version of `react-native-device-info` ever changes its
257
+ shape, ship a tiny adapter that satisfies `DeviceInfoLike`
258
+ (`{ getModel(): string }`).
259
+
164
260
  ## Session semantics
165
261
 
166
262
  Sessions are **idle-based**, not tied to a single foreground period:
package/dist/index.cjs CHANGED
@@ -25,6 +25,9 @@ __export(src_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(src_exports);
27
27
 
28
+ // src/client.ts
29
+ var import_react_native = require("react-native");
30
+
28
31
  // src/ids.ts
29
32
  function uuidv4() {
30
33
  const c = globalThis.crypto;
@@ -41,19 +44,6 @@ function uuidv4() {
41
44
  var ANON_KEY = "lumin.anonymous_id";
42
45
  var SESSION_KEY = "lumin.session_id";
43
46
  var SESSION_LAST_SEEN_KEY = "lumin.session_last_seen";
44
- function detectAsyncStorage() {
45
- try {
46
- if (typeof require === "undefined") return null;
47
- const mod = require("@react-native-async-storage/async-storage");
48
- const candidate = mod?.default ?? mod;
49
- if (candidate && typeof candidate.getItem === "function" && typeof candidate.setItem === "function") {
50
- return candidate;
51
- }
52
- return null;
53
- } catch {
54
- return null;
55
- }
56
- }
57
47
  function memoryStorage() {
58
48
  const m = /* @__PURE__ */ new Map();
59
49
  return {
@@ -107,8 +97,14 @@ var DEFAULT_FLUSH_MS = 500;
107
97
  var DEFAULT_SESSION_IDLE_MS = 30 * 60 * 1e3;
108
98
  var SERVER_MAX_BATCH = 1e3;
109
99
  var DEFAULT_ENDPOINT = "https://api.getlumin.dev";
100
+ var SDK_VERSION = "0.4.0";
110
101
  var Client = class {
111
102
  constructor(opts) {
103
+ this.prevErrorHandler = void 0;
104
+ this.errorHandlerInstalled = false;
105
+ // Dedup the same Error captured twice (e.g. via captureError + the global
106
+ // handler firing on the same throw). WeakSet so we don't pin GC.
107
+ this.recentErrors = /* @__PURE__ */ new WeakSet();
112
108
  this.userId = null;
113
109
  this.cachedIds = null;
114
110
  // Pending events queued before ids hydrate. `user_id` is stamped at enqueue
@@ -131,11 +127,15 @@ var Client = class {
131
127
  console.warn("[lumin] flush failed", { err, dropped });
132
128
  });
133
129
  this.fetchImpl = opts.fetch ?? fetch.bind(globalThis);
134
- const resolvedStorage = opts.storage === null ? memoryStorage() : opts.storage ?? detectAsyncStorage() ?? memoryStorage();
135
- this.storage = resolvedStorage;
130
+ this.storage = opts.storage ?? memoryStorage();
136
131
  this.appState = opts.appState === void 0 ? detectAppState() : opts.appState;
132
+ this.errorUtils = opts.errorUtils === void 0 ? detectErrorUtils() : opts.errorUtils;
133
+ this.clientHeader = buildClientHeader(opts.deviceInfo ?? null);
137
134
  this.idsReady = this.hydrateIds();
138
135
  this.installBackgroundFlush();
136
+ if (opts.captureUnhandledErrors !== false) {
137
+ this.installErrorHandler();
138
+ }
139
139
  }
140
140
  screen(name, properties) {
141
141
  if (this.closed) return;
@@ -153,6 +153,22 @@ var Client = class {
153
153
  }
154
154
  this.enqueue({ type: "track", name, ...properties !== void 0 ? { properties } : {} });
155
155
  }
156
+ captureError(err, properties) {
157
+ if (this.closed) return;
158
+ const e = normalizeError(err);
159
+ if (!e) return;
160
+ if (typeof err === "object" && err !== null) {
161
+ if (this.recentErrors.has(err)) return;
162
+ this.recentErrors.add(err);
163
+ }
164
+ this.enqueue({
165
+ type: "error",
166
+ ...properties !== void 0 ? { properties } : {},
167
+ error_message: e.message,
168
+ ...e.stack ? { error_stack: e.stack } : {},
169
+ ...e.type ? { error_type: e.type } : {}
170
+ });
171
+ }
156
172
  identify(userId, traits) {
157
173
  if (this.closed) return;
158
174
  if (!userId) {
@@ -189,6 +205,7 @@ var Client = class {
189
205
  if (this.closed) return;
190
206
  this.closed = true;
191
207
  this.removeBackgroundFlush();
208
+ this.restoreErrorHandler();
192
209
  await this.flush();
193
210
  await Promise.allSettled(this.inflight);
194
211
  }
@@ -276,13 +293,15 @@ var Client = class {
276
293
  return;
277
294
  }
278
295
  const body = JSON.stringify({ events: batch });
296
+ const headers = {
297
+ "Content-Type": "application/json",
298
+ Authorization: "Bearer " + this.apiKey
299
+ };
300
+ if (this.clientHeader) headers["X-Lumin-Client"] = this.clientHeader;
279
301
  try {
280
302
  const res = await this.fetchImpl(this.endpoint + "/v1/events", {
281
303
  method: "POST",
282
- headers: {
283
- "Content-Type": "application/json",
284
- Authorization: "Bearer " + this.apiKey
285
- },
304
+ headers,
286
305
  body
287
306
  });
288
307
  if (!res.ok) {
@@ -314,24 +333,110 @@ var Client = class {
314
333
  this.appStateSub = null;
315
334
  }
316
335
  }
336
+ // --- error handler -----------------------------------------------------
337
+ /**
338
+ * Install a global JS error handler via ErrorUtils.setGlobalHandler that
339
+ * captures + chains to whatever handler was already in place. RN's
340
+ * red-box and any other observability tool that installed before us
341
+ * still fires — we do not swallow the throw.
342
+ */
343
+ installErrorHandler() {
344
+ if (!this.errorUtils) return;
345
+ this.prevErrorHandler = this.errorUtils.getGlobalHandler?.();
346
+ const handler = (err, isFatal) => {
347
+ try {
348
+ this.captureError(err, isFatal ? { fatal: true } : void 0);
349
+ if (isFatal) void this.flush();
350
+ } catch {
351
+ }
352
+ if (this.prevErrorHandler) {
353
+ this.prevErrorHandler(err, isFatal);
354
+ }
355
+ };
356
+ this.errorUtils.setGlobalHandler(handler);
357
+ this.errorHandlerInstalled = true;
358
+ }
359
+ restoreErrorHandler() {
360
+ if (!this.errorHandlerInstalled || !this.errorUtils) return;
361
+ this.errorUtils.setGlobalHandler(
362
+ this.prevErrorHandler ?? defaultNoopErrorHandler
363
+ );
364
+ this.errorHandlerInstalled = false;
365
+ }
317
366
  };
367
+ function defaultNoopErrorHandler() {
368
+ }
369
+ function detectErrorUtils() {
370
+ const g = globalThis;
371
+ const eu = g.ErrorUtils;
372
+ if (eu && typeof eu.setGlobalHandler === "function") {
373
+ return eu;
374
+ }
375
+ return null;
376
+ }
377
+ function normalizeError(raw) {
378
+ if (raw == null) return null;
379
+ if (raw instanceof Error) {
380
+ return {
381
+ message: raw.message || raw.name || "Error",
382
+ ...raw.stack ? { stack: raw.stack } : {},
383
+ type: raw.name || raw.constructor?.name
384
+ };
385
+ }
386
+ if (typeof raw === "string") {
387
+ return { message: raw };
388
+ }
389
+ if (typeof raw === "object") {
390
+ const obj = raw;
391
+ const message = typeof obj.message === "string" ? obj.message : safeStringify(raw);
392
+ return {
393
+ message,
394
+ ...typeof obj.stack === "string" ? { stack: obj.stack } : {},
395
+ ...typeof obj.name === "string" ? { type: obj.name } : {}
396
+ };
397
+ }
398
+ return { message: String(raw) };
399
+ }
400
+ function safeStringify(v) {
401
+ try {
402
+ return JSON.stringify(v) ?? String(v);
403
+ } catch {
404
+ return String(v);
405
+ }
406
+ }
318
407
  function clampBatch(n) {
319
408
  if (!Number.isFinite(n) || n < 1) return 1;
320
409
  if (n > SERVER_MAX_BATCH) return SERVER_MAX_BATCH;
321
410
  return Math.floor(n);
322
411
  }
323
- function detectAppState() {
324
- try {
325
- if (typeof require === "undefined") return null;
326
- const rn = require("react-native");
327
- const candidate = rn?.AppState;
328
- if (candidate && typeof candidate.addEventListener === "function") {
329
- return candidate;
412
+ function buildClientHeader(deviceInfo) {
413
+ const parts = [`sdk=rn/${SDK_VERSION}`];
414
+ const platform = import_react_native.Platform;
415
+ const osRaw = platform?.OS;
416
+ if (typeof osRaw === "string" && osRaw) {
417
+ parts.push(`os=${encodeURIComponent(osRaw)}`);
418
+ } else {
419
+ return "";
420
+ }
421
+ const versionRaw = platform?.Version;
422
+ if (versionRaw !== void 0 && versionRaw !== null && versionRaw !== "") {
423
+ parts.push(`os_version=${encodeURIComponent(String(versionRaw))}`);
424
+ }
425
+ if (deviceInfo && typeof deviceInfo.getModel === "function") {
426
+ try {
427
+ const model = deviceInfo.getModel();
428
+ if (model) parts.push(`device=${encodeURIComponent(model)}`);
429
+ } catch {
330
430
  }
331
- return null;
332
- } catch {
333
- return null;
334
431
  }
432
+ return parts.join("; ");
433
+ }
434
+ function detectAppState() {
435
+ const candidate = import_react_native.AppState;
436
+ if (candidate && typeof candidate.addEventListener === "function") {
437
+ return candidate;
438
+ }
439
+ return null;
335
440
  }
336
441
  function validateEndpoint(raw) {
337
442
  let url;
@@ -369,6 +474,7 @@ function init(opts) {
369
474
  screen: c.screen.bind(c),
370
475
  track: c.track.bind(c),
371
476
  identify: c.identify.bind(c),
477
+ captureError: c.captureError.bind(c),
372
478
  flush: c.flush.bind(c),
373
479
  close: c.close.bind(c),
374
480
  getSessionId: c.getSessionId.bind(c),
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { L as LuminClient, I as InitOptions } from './types-BeHbtR1J.cjs';
2
- export { A as AppStateLike, a as AsyncStorageLike, E as EventType, W as WireEvent } from './types-BeHbtR1J.cjs';
1
+ import { L as LuminClient, I as InitOptions } from './types-B6rqS9Vp.cjs';
2
+ export { A as AppStateLike, a as AsyncStorageLike, D as DeviceInfoLike, E as ErrorUtilsLike, b as EventType, W as WireEvent } from './types-B6rqS9Vp.cjs';
3
3
 
4
4
  declare class Client implements LuminClient {
5
5
  private readonly endpoint;
@@ -11,6 +11,11 @@ declare class Client implements LuminClient {
11
11
  private readonly fetchImpl;
12
12
  private readonly storage;
13
13
  private readonly appState;
14
+ private readonly errorUtils;
15
+ private readonly clientHeader;
16
+ private prevErrorHandler;
17
+ private errorHandlerInstalled;
18
+ private readonly recentErrors;
14
19
  private userId;
15
20
  private idsReady;
16
21
  private cachedIds;
@@ -23,6 +28,7 @@ declare class Client implements LuminClient {
23
28
  constructor(opts: InitOptions);
24
29
  screen(name?: string, properties?: Record<string, unknown>): void;
25
30
  track(name: string, properties?: Record<string, unknown>): void;
31
+ captureError(err: unknown, properties?: Record<string, unknown>): void;
26
32
  identify(userId: string, traits?: Record<string, unknown>): void;
27
33
  /**
28
34
  * Drain the buffer to the server. Returns when the in-flight request
@@ -59,6 +65,14 @@ declare class Client implements LuminClient {
59
65
  */
60
66
  private installBackgroundFlush;
61
67
  private removeBackgroundFlush;
68
+ /**
69
+ * Install a global JS error handler via ErrorUtils.setGlobalHandler that
70
+ * captures + chains to whatever handler was already in place. RN's
71
+ * red-box and any other observability tool that installed before us
72
+ * still fires — we do not swallow the throw.
73
+ */
74
+ private installErrorHandler;
75
+ private restoreErrorHandler;
62
76
  }
63
77
 
64
78
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { L as LuminClient, I as InitOptions } from './types-BeHbtR1J.js';
2
- export { A as AppStateLike, a as AsyncStorageLike, E as EventType, W as WireEvent } from './types-BeHbtR1J.js';
1
+ import { L as LuminClient, I as InitOptions } from './types-B6rqS9Vp.js';
2
+ export { A as AppStateLike, a as AsyncStorageLike, D as DeviceInfoLike, E as ErrorUtilsLike, b as EventType, W as WireEvent } from './types-B6rqS9Vp.js';
3
3
 
4
4
  declare class Client implements LuminClient {
5
5
  private readonly endpoint;
@@ -11,6 +11,11 @@ declare class Client implements LuminClient {
11
11
  private readonly fetchImpl;
12
12
  private readonly storage;
13
13
  private readonly appState;
14
+ private readonly errorUtils;
15
+ private readonly clientHeader;
16
+ private prevErrorHandler;
17
+ private errorHandlerInstalled;
18
+ private readonly recentErrors;
14
19
  private userId;
15
20
  private idsReady;
16
21
  private cachedIds;
@@ -23,6 +28,7 @@ declare class Client implements LuminClient {
23
28
  constructor(opts: InitOptions);
24
29
  screen(name?: string, properties?: Record<string, unknown>): void;
25
30
  track(name: string, properties?: Record<string, unknown>): void;
31
+ captureError(err: unknown, properties?: Record<string, unknown>): void;
26
32
  identify(userId: string, traits?: Record<string, unknown>): void;
27
33
  /**
28
34
  * Drain the buffer to the server. Returns when the in-flight request
@@ -59,6 +65,14 @@ declare class Client implements LuminClient {
59
65
  */
60
66
  private installBackgroundFlush;
61
67
  private removeBackgroundFlush;
68
+ /**
69
+ * Install a global JS error handler via ErrorUtils.setGlobalHandler that
70
+ * captures + chains to whatever handler was already in place. RN's
71
+ * red-box and any other observability tool that installed before us
72
+ * still fires — we do not swallow the throw.
73
+ */
74
+ private installErrorHandler;
75
+ private restoreErrorHandler;
62
76
  }
63
77
 
64
78
  /**