@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 +139 -0
- package/README.md +105 -9
- package/dist/index.cjs +135 -29
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +134 -32
- package/dist/react-navigation.d.cts +1 -1
- package/dist/react-navigation.d.ts +1 -1
- package/dist/react-navigation.js +0 -2
- package/dist/types-B6rqS9Vp.d.cts +169 -0
- package/dist/types-B6rqS9Vp.d.ts +169 -0
- package/package.json +3 -6
- package/dist/chunk-3RG5ZIWI.js +0 -10
- package/dist/types-BeHbtR1J.d.cts +0 -96
- package/dist/types-BeHbtR1J.d.ts +0 -96
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
|
|
17
|
-
#
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
-
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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-
|
|
2
|
-
export { A as AppStateLike, a as AsyncStorageLike, E as EventType, W as WireEvent } from './types-
|
|
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-
|
|
2
|
-
export { A as AppStateLike, a as AsyncStorageLike, E as EventType, W as WireEvent } from './types-
|
|
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
|
/**
|