@lumin-monitor/react-native 0.1.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/LICENSE +201 -0
- package/README.md +250 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/index.cjs +382 -0
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +358 -0
- package/dist/react-navigation.cjs +51 -0
- package/dist/react-navigation.d.cts +32 -0
- package/dist/react-navigation.d.ts +32 -0
- package/dist/react-navigation.js +28 -0
- package/dist/types-BeHbtR1J.d.cts +96 -0
- package/dist/types-BeHbtR1J.d.ts +96 -0
- package/package.json +84 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
4
|
+
|
|
5
|
+
// src/ids.ts
|
|
6
|
+
function uuidv4() {
|
|
7
|
+
const c = globalThis.crypto;
|
|
8
|
+
if (c && typeof c.randomUUID === "function") {
|
|
9
|
+
return c.randomUUID();
|
|
10
|
+
}
|
|
11
|
+
const bytes = new Array(16);
|
|
12
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
13
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
14
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
15
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
|
|
16
|
+
return hex.slice(0, 4).join("") + "-" + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 16).join("");
|
|
17
|
+
}
|
|
18
|
+
var ANON_KEY = "lumin.anonymous_id";
|
|
19
|
+
var SESSION_KEY = "lumin.session_id";
|
|
20
|
+
var SESSION_LAST_SEEN_KEY = "lumin.session_last_seen";
|
|
21
|
+
function detectAsyncStorage() {
|
|
22
|
+
try {
|
|
23
|
+
if (typeof __require === "undefined") return null;
|
|
24
|
+
const mod = __require("@react-native-async-storage/async-storage");
|
|
25
|
+
const candidate = mod?.default ?? mod;
|
|
26
|
+
if (candidate && typeof candidate.getItem === "function" && typeof candidate.setItem === "function") {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function memoryStorage() {
|
|
35
|
+
const m = /* @__PURE__ */ new Map();
|
|
36
|
+
return {
|
|
37
|
+
async getItem(k) {
|
|
38
|
+
return m.has(k) ? m.get(k) : null;
|
|
39
|
+
},
|
|
40
|
+
async setItem(k, v) {
|
|
41
|
+
m.set(k, v);
|
|
42
|
+
},
|
|
43
|
+
async removeItem(k) {
|
|
44
|
+
m.delete(k);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function readSafe(s, key) {
|
|
49
|
+
try {
|
|
50
|
+
return await s.getItem(key);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function writeSafe(s, key, value) {
|
|
56
|
+
try {
|
|
57
|
+
await s.setItem(key, value);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function getOrCreateAnonymousId(storage) {
|
|
62
|
+
const existing = await readSafe(storage, ANON_KEY);
|
|
63
|
+
if (existing) return existing;
|
|
64
|
+
const id = uuidv4();
|
|
65
|
+
await writeSafe(storage, ANON_KEY, id);
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
async function getOrCreateSessionId(storage, idleMs, now = Date.now()) {
|
|
69
|
+
const existingId = await readSafe(storage, SESSION_KEY);
|
|
70
|
+
const lastSeenStr = await readSafe(storage, SESSION_LAST_SEEN_KEY);
|
|
71
|
+
const lastSeen = lastSeenStr ? Number(lastSeenStr) : NaN;
|
|
72
|
+
const expired = !existingId || !Number.isFinite(lastSeen) || now - lastSeen > idleMs;
|
|
73
|
+
const id = expired ? uuidv4() : existingId;
|
|
74
|
+
if (expired) {
|
|
75
|
+
await writeSafe(storage, SESSION_KEY, id);
|
|
76
|
+
}
|
|
77
|
+
await writeSafe(storage, SESSION_LAST_SEEN_KEY, String(now));
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/client.ts
|
|
82
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
83
|
+
var DEFAULT_FLUSH_MS = 500;
|
|
84
|
+
var DEFAULT_SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
85
|
+
var SERVER_MAX_BATCH = 1e3;
|
|
86
|
+
var DEFAULT_ENDPOINT = "https://api.getlumin.dev";
|
|
87
|
+
var Client = class {
|
|
88
|
+
constructor(opts) {
|
|
89
|
+
this.userId = null;
|
|
90
|
+
this.cachedIds = null;
|
|
91
|
+
// Pending events queued before ids hydrate. `user_id` is stamped at enqueue
|
|
92
|
+
// time so a later identify() does not retroactively bind pre-identify events
|
|
93
|
+
// — matches the browser SDK's behaviour. Once `idsReady` resolves, the
|
|
94
|
+
// flushing path stamps `session_id` + `anonymous_id` and drains them.
|
|
95
|
+
this.pending = [];
|
|
96
|
+
this.buffer = [];
|
|
97
|
+
this.timer = null;
|
|
98
|
+
this.closed = false;
|
|
99
|
+
this.inflight = [];
|
|
100
|
+
this.appStateSub = null;
|
|
101
|
+
if (!opts.apiKey) throw new Error("lumin: apiKey required");
|
|
102
|
+
this.endpoint = validateEndpoint(opts.endpoint ?? DEFAULT_ENDPOINT);
|
|
103
|
+
this.apiKey = opts.apiKey;
|
|
104
|
+
this.batchSize = clampBatch(opts.batchSize ?? DEFAULT_BATCH_SIZE);
|
|
105
|
+
this.flushIntervalMs = Math.max(50, opts.flushIntervalMs ?? DEFAULT_FLUSH_MS);
|
|
106
|
+
this.sessionIdleMs = Math.max(1e3, opts.sessionIdleMs ?? DEFAULT_SESSION_IDLE_MS);
|
|
107
|
+
this.onError = opts.onError ?? ((err, dropped) => {
|
|
108
|
+
console.warn("[lumin] flush failed", { err, dropped });
|
|
109
|
+
});
|
|
110
|
+
this.fetchImpl = opts.fetch ?? fetch.bind(globalThis);
|
|
111
|
+
const resolvedStorage = opts.storage === null ? memoryStorage() : opts.storage ?? detectAsyncStorage() ?? memoryStorage();
|
|
112
|
+
this.storage = resolvedStorage;
|
|
113
|
+
this.appState = opts.appState === void 0 ? detectAppState() : opts.appState;
|
|
114
|
+
this.idsReady = this.hydrateIds();
|
|
115
|
+
this.installBackgroundFlush();
|
|
116
|
+
}
|
|
117
|
+
screen(name, properties) {
|
|
118
|
+
if (this.closed) return;
|
|
119
|
+
this.enqueue({
|
|
120
|
+
type: "page",
|
|
121
|
+
...name !== void 0 ? { name } : {},
|
|
122
|
+
...properties !== void 0 ? { properties } : {}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
track(name, properties) {
|
|
126
|
+
if (this.closed) return;
|
|
127
|
+
if (!name) {
|
|
128
|
+
this.onError(new Error("track requires a name"), 0);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.enqueue({ type: "track", name, ...properties !== void 0 ? { properties } : {} });
|
|
132
|
+
}
|
|
133
|
+
identify(userId, traits) {
|
|
134
|
+
if (this.closed) return;
|
|
135
|
+
if (!userId) {
|
|
136
|
+
this.onError(new Error("identify requires a userId"), 0);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.userId = userId;
|
|
140
|
+
this.enqueue({ type: "identify", ...traits !== void 0 ? { properties: traits } : {} });
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Drain the buffer to the server. Returns when the in-flight request
|
|
144
|
+
* settles (success or failure). Awaits id hydration first so a `flush()`
|
|
145
|
+
* called immediately after `init()` does not race the AsyncStorage read.
|
|
146
|
+
*/
|
|
147
|
+
async flush() {
|
|
148
|
+
await this.drainPending();
|
|
149
|
+
if (this.buffer.length === 0) return;
|
|
150
|
+
const batch = this.buffer;
|
|
151
|
+
this.buffer = [];
|
|
152
|
+
this.cancelTimer();
|
|
153
|
+
const p = this.send(batch);
|
|
154
|
+
this.inflight.push(p);
|
|
155
|
+
try {
|
|
156
|
+
await p;
|
|
157
|
+
} finally {
|
|
158
|
+
this.inflight = this.inflight.filter((q) => q !== p);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Stop accepting events, flush whatever's buffered, wait for in-flight
|
|
163
|
+
* requests to settle, and remove the AppState listener. Idempotent.
|
|
164
|
+
*/
|
|
165
|
+
async close() {
|
|
166
|
+
if (this.closed) return;
|
|
167
|
+
this.closed = true;
|
|
168
|
+
this.removeBackgroundFlush();
|
|
169
|
+
await this.flush();
|
|
170
|
+
await Promise.allSettled(this.inflight);
|
|
171
|
+
}
|
|
172
|
+
async getSessionId() {
|
|
173
|
+
const ids = await this.idsReady;
|
|
174
|
+
return ids.sessionId;
|
|
175
|
+
}
|
|
176
|
+
async getAnonymousId() {
|
|
177
|
+
const ids = await this.idsReady;
|
|
178
|
+
return ids.anonymousId;
|
|
179
|
+
}
|
|
180
|
+
// --- internals ---------------------------------------------------------
|
|
181
|
+
async hydrateIds() {
|
|
182
|
+
const anonymousId = await getOrCreateAnonymousId(this.storage);
|
|
183
|
+
const sessionId = await getOrCreateSessionId(this.storage, this.sessionIdleMs);
|
|
184
|
+
const ids = { anonymousId, sessionId };
|
|
185
|
+
this.cachedIds = ids;
|
|
186
|
+
return ids;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Refresh `cachedIds.sessionId` if the persisted lastSeenAt is older than
|
|
190
|
+
* `sessionIdleMs`. Called before each send so a long background pause →
|
|
191
|
+
* foreground burst opens a new session.
|
|
192
|
+
*/
|
|
193
|
+
async refreshSessionIfIdle() {
|
|
194
|
+
if (!this.cachedIds) return;
|
|
195
|
+
const sid = await getOrCreateSessionId(this.storage, this.sessionIdleMs);
|
|
196
|
+
this.cachedIds.sessionId = sid;
|
|
197
|
+
}
|
|
198
|
+
enqueue(partial) {
|
|
199
|
+
const withUser = {
|
|
200
|
+
ts: Date.now(),
|
|
201
|
+
...this.userId ? { user_id: this.userId } : {},
|
|
202
|
+
...partial
|
|
203
|
+
};
|
|
204
|
+
if (!this.cachedIds) {
|
|
205
|
+
this.pending.push(withUser);
|
|
206
|
+
this.scheduleTimer();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.buffer.push(this.stamp(withUser, this.cachedIds));
|
|
210
|
+
if (this.buffer.length >= this.batchSize) {
|
|
211
|
+
void this.flush();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.scheduleTimer();
|
|
215
|
+
}
|
|
216
|
+
stamp(partial, ids) {
|
|
217
|
+
return {
|
|
218
|
+
session_id: ids.sessionId,
|
|
219
|
+
anonymous_id: ids.anonymousId,
|
|
220
|
+
...partial
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async drainPending() {
|
|
224
|
+
if (this.pending.length === 0 && this.cachedIds) return;
|
|
225
|
+
const ids = await this.idsReady;
|
|
226
|
+
await this.refreshSessionIfIdle();
|
|
227
|
+
const effective = this.cachedIds ?? ids;
|
|
228
|
+
if (this.pending.length === 0) return;
|
|
229
|
+
const drained = this.pending;
|
|
230
|
+
this.pending = [];
|
|
231
|
+
for (const p of drained) this.buffer.push(this.stamp(p, effective));
|
|
232
|
+
}
|
|
233
|
+
scheduleTimer() {
|
|
234
|
+
if (this.timer !== null) return;
|
|
235
|
+
this.timer = setTimeout(() => {
|
|
236
|
+
this.timer = null;
|
|
237
|
+
void this.flush();
|
|
238
|
+
}, this.flushIntervalMs);
|
|
239
|
+
}
|
|
240
|
+
cancelTimer() {
|
|
241
|
+
if (this.timer !== null) {
|
|
242
|
+
clearTimeout(this.timer);
|
|
243
|
+
this.timer = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async send(batch) {
|
|
247
|
+
if (batch.length === 0) return;
|
|
248
|
+
if (batch.length > SERVER_MAX_BATCH) {
|
|
249
|
+
this.onError(
|
|
250
|
+
new Error(`batch size ${batch.length} exceeds server max ${SERVER_MAX_BATCH}`),
|
|
251
|
+
batch.length
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const body = JSON.stringify({ events: batch });
|
|
256
|
+
try {
|
|
257
|
+
const res = await this.fetchImpl(this.endpoint + "/v1/events", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: {
|
|
260
|
+
"Content-Type": "application/json",
|
|
261
|
+
Authorization: "Bearer " + this.apiKey
|
|
262
|
+
},
|
|
263
|
+
body
|
|
264
|
+
});
|
|
265
|
+
if (!res.ok) {
|
|
266
|
+
this.onError(new Error(`HTTP ${res.status}`), batch.length);
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.onError(err, batch.length);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Flush on app background/inactive. RN keeps the JS runtime alive for a
|
|
274
|
+
* short grace window after backgrounding (~30 s on iOS, similar on
|
|
275
|
+
* Android), which is enough for one batched POST to land. There is no
|
|
276
|
+
* `keepalive: true` equivalent on RN fetch — the OS-level grace window
|
|
277
|
+
* is the contract.
|
|
278
|
+
*/
|
|
279
|
+
installBackgroundFlush() {
|
|
280
|
+
if (!this.appState) return;
|
|
281
|
+
this.appStateSub = this.appState.addEventListener("change", (state) => {
|
|
282
|
+
if (state === "background" || state === "inactive") {
|
|
283
|
+
if (this.buffer.length === 0 && this.pending.length === 0) return;
|
|
284
|
+
void this.flush();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
removeBackgroundFlush() {
|
|
289
|
+
if (this.appStateSub) {
|
|
290
|
+
this.appStateSub.remove();
|
|
291
|
+
this.appStateSub = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
function clampBatch(n) {
|
|
296
|
+
if (!Number.isFinite(n) || n < 1) return 1;
|
|
297
|
+
if (n > SERVER_MAX_BATCH) return SERVER_MAX_BATCH;
|
|
298
|
+
return Math.floor(n);
|
|
299
|
+
}
|
|
300
|
+
function detectAppState() {
|
|
301
|
+
try {
|
|
302
|
+
if (typeof __require === "undefined") return null;
|
|
303
|
+
const rn = __require("react-native");
|
|
304
|
+
const candidate = rn?.AppState;
|
|
305
|
+
if (candidate && typeof candidate.addEventListener === "function") {
|
|
306
|
+
return candidate;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function validateEndpoint(raw) {
|
|
314
|
+
let url;
|
|
315
|
+
try {
|
|
316
|
+
url = new URL(raw);
|
|
317
|
+
} catch {
|
|
318
|
+
throw new Error(`lumin: endpoint is not a valid URL: ${raw}`);
|
|
319
|
+
}
|
|
320
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
321
|
+
throw new Error(`lumin: endpoint must be http(s); got ${url.protocol}//`);
|
|
322
|
+
}
|
|
323
|
+
const host = url.hostname;
|
|
324
|
+
const isLocalDev = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
|
|
325
|
+
if (url.protocol === "http:" && !isLocalDev) {
|
|
326
|
+
throw new Error(`lumin: endpoint must use https:// for non-local hosts (got ${raw})`);
|
|
327
|
+
}
|
|
328
|
+
if (url.pathname.replace(/\/+$/, "") !== "") {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`lumin: endpoint must be a base URL with no path; got pathname "${url.pathname}". Pass "https://api.getlumin.dev", not "https://api.getlumin.dev/v1/events".`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (url.search !== "" || url.hash !== "") {
|
|
334
|
+
throw new Error(`lumin: endpoint must not include a query string or fragment; got ${raw}`);
|
|
335
|
+
}
|
|
336
|
+
if (!url.hostname) {
|
|
337
|
+
throw new Error(`lumin: endpoint is missing a hostname: ${raw}`);
|
|
338
|
+
}
|
|
339
|
+
return `${url.protocol}//${url.host}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/index.ts
|
|
343
|
+
function init(opts) {
|
|
344
|
+
const c = new Client(opts);
|
|
345
|
+
return {
|
|
346
|
+
screen: c.screen.bind(c),
|
|
347
|
+
track: c.track.bind(c),
|
|
348
|
+
identify: c.identify.bind(c),
|
|
349
|
+
flush: c.flush.bind(c),
|
|
350
|
+
close: c.close.bind(c),
|
|
351
|
+
getSessionId: c.getSessionId.bind(c),
|
|
352
|
+
getAnonymousId: c.getAnonymousId.bind(c)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export {
|
|
356
|
+
Client,
|
|
357
|
+
init
|
|
358
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react-navigation.ts
|
|
21
|
+
var react_navigation_exports = {};
|
|
22
|
+
__export(react_navigation_exports, {
|
|
23
|
+
useLuminScreenviews: () => useLuminScreenviews
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(react_navigation_exports);
|
|
26
|
+
var import_react = require("react");
|
|
27
|
+
function useLuminScreenviews(client, navigationRef) {
|
|
28
|
+
const lastRoute = (0, import_react.useRef)(null);
|
|
29
|
+
(0, import_react.useEffect)(() => {
|
|
30
|
+
if (!client || !navigationRef) return;
|
|
31
|
+
const fire = () => {
|
|
32
|
+
const route = navigationRef.getCurrentRoute?.();
|
|
33
|
+
const name = route?.name;
|
|
34
|
+
if (!name) return;
|
|
35
|
+
if (lastRoute.current === name) return;
|
|
36
|
+
lastRoute.current = name;
|
|
37
|
+
client.screen(name);
|
|
38
|
+
};
|
|
39
|
+
if (navigationRef.isReady?.()) fire();
|
|
40
|
+
const unsubReady = navigationRef.addListener("ready", fire);
|
|
41
|
+
const unsubState = navigationRef.addListener("state", fire);
|
|
42
|
+
return () => {
|
|
43
|
+
unsubReady();
|
|
44
|
+
unsubState();
|
|
45
|
+
};
|
|
46
|
+
}, [client, navigationRef]);
|
|
47
|
+
}
|
|
48
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
49
|
+
0 && (module.exports = {
|
|
50
|
+
useLuminScreenviews
|
|
51
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { L as LuminClient } from './types-BeHbtR1J.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subset of @react-navigation/native's NavigationContainerRef the hook needs.
|
|
5
|
+
* Typed structurally so we do not pull react-navigation types into the SDK's
|
|
6
|
+
* public surface — the app already has them.
|
|
7
|
+
*/
|
|
8
|
+
interface NavigationRefLike {
|
|
9
|
+
isReady?(): boolean;
|
|
10
|
+
getCurrentRoute?(): {
|
|
11
|
+
name?: string;
|
|
12
|
+
params?: Record<string, unknown>;
|
|
13
|
+
} | undefined;
|
|
14
|
+
addListener(event: "state" | "ready", listener: () => void): () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Auto-fire `client.screen()` on every React Navigation route change. Drop
|
|
18
|
+
* into the component that renders `<NavigationContainer ref={navigationRef}>`
|
|
19
|
+
* once.
|
|
20
|
+
*
|
|
21
|
+
* The hook fires once per route name change. Same-name navigations (e.g.
|
|
22
|
+
* pushing a new instance of the same screen with different params) do not
|
|
23
|
+
* re-emit — params are usually filter state, not real screen views, and
|
|
24
|
+
* double-counting them inflates funnel metrics. If you want per-param
|
|
25
|
+
* tracking, call `screen()` manually from the route's effect.
|
|
26
|
+
*
|
|
27
|
+
* Pass `null` for `client` (e.g. when the SDK is disabled in dev) and the
|
|
28
|
+
* hook becomes a no-op.
|
|
29
|
+
*/
|
|
30
|
+
declare function useLuminScreenviews(client: LuminClient | null | undefined, navigationRef: NavigationRefLike | null | undefined): void;
|
|
31
|
+
|
|
32
|
+
export { type NavigationRefLike, useLuminScreenviews };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { L as LuminClient } from './types-BeHbtR1J.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subset of @react-navigation/native's NavigationContainerRef the hook needs.
|
|
5
|
+
* Typed structurally so we do not pull react-navigation types into the SDK's
|
|
6
|
+
* public surface — the app already has them.
|
|
7
|
+
*/
|
|
8
|
+
interface NavigationRefLike {
|
|
9
|
+
isReady?(): boolean;
|
|
10
|
+
getCurrentRoute?(): {
|
|
11
|
+
name?: string;
|
|
12
|
+
params?: Record<string, unknown>;
|
|
13
|
+
} | undefined;
|
|
14
|
+
addListener(event: "state" | "ready", listener: () => void): () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Auto-fire `client.screen()` on every React Navigation route change. Drop
|
|
18
|
+
* into the component that renders `<NavigationContainer ref={navigationRef}>`
|
|
19
|
+
* once.
|
|
20
|
+
*
|
|
21
|
+
* The hook fires once per route name change. Same-name navigations (e.g.
|
|
22
|
+
* pushing a new instance of the same screen with different params) do not
|
|
23
|
+
* re-emit — params are usually filter state, not real screen views, and
|
|
24
|
+
* double-counting them inflates funnel metrics. If you want per-param
|
|
25
|
+
* tracking, call `screen()` manually from the route's effect.
|
|
26
|
+
*
|
|
27
|
+
* Pass `null` for `client` (e.g. when the SDK is disabled in dev) and the
|
|
28
|
+
* hook becomes a no-op.
|
|
29
|
+
*/
|
|
30
|
+
declare function useLuminScreenviews(client: LuminClient | null | undefined, navigationRef: NavigationRefLike | null | undefined): void;
|
|
31
|
+
|
|
32
|
+
export { type NavigationRefLike, useLuminScreenviews };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "./chunk-3RG5ZIWI.js";
|
|
2
|
+
|
|
3
|
+
// src/react-navigation.ts
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
function useLuminScreenviews(client, navigationRef) {
|
|
6
|
+
const lastRoute = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!client || !navigationRef) return;
|
|
9
|
+
const fire = () => {
|
|
10
|
+
const route = navigationRef.getCurrentRoute?.();
|
|
11
|
+
const name = route?.name;
|
|
12
|
+
if (!name) return;
|
|
13
|
+
if (lastRoute.current === name) return;
|
|
14
|
+
lastRoute.current = name;
|
|
15
|
+
client.screen(name);
|
|
16
|
+
};
|
|
17
|
+
if (navigationRef.isReady?.()) fire();
|
|
18
|
+
const unsubReady = navigationRef.addListener("ready", fire);
|
|
19
|
+
const unsubState = navigationRef.addListener("state", fire);
|
|
20
|
+
return () => {
|
|
21
|
+
unsubReady();
|
|
22
|
+
unsubState();
|
|
23
|
+
};
|
|
24
|
+
}, [client, navigationRef]);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
useLuminScreenviews
|
|
28
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
type EventType = "page" | "track" | "identify";
|
|
2
|
+
interface WireEvent {
|
|
3
|
+
ts?: number;
|
|
4
|
+
session_id: string;
|
|
5
|
+
anonymous_id?: string;
|
|
6
|
+
user_id?: string;
|
|
7
|
+
type: EventType;
|
|
8
|
+
name?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
referrer?: string;
|
|
11
|
+
properties?: Record<string, unknown>;
|
|
12
|
+
trace_id?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Minimal AsyncStorage shape the SDK actually consumes. Matches both
|
|
16
|
+
* @react-native-async-storage/async-storage (community) and the legacy
|
|
17
|
+
* react-native built-in. Apps that ship a different storage backend can
|
|
18
|
+
* implement this directly.
|
|
19
|
+
*/
|
|
20
|
+
interface AsyncStorageLike {
|
|
21
|
+
getItem(key: string): Promise<string | null>;
|
|
22
|
+
setItem(key: string, value: string): Promise<void>;
|
|
23
|
+
removeItem?(key: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
interface InitOptions {
|
|
26
|
+
/** API key minted in the Lumin UI (settings → API keys). Must be `lmn_pub_*`. */
|
|
27
|
+
apiKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* Base URL of the Lumin ingest endpoint. Defaults to
|
|
30
|
+
* `https://api.getlumin.dev`. Override only for local development against
|
|
31
|
+
* a Lumin instance you are running yourself or for a customer-side
|
|
32
|
+
* same-origin proxy.
|
|
33
|
+
*
|
|
34
|
+
* Validation: must parse as a valid URL with no path beyond `/`. `https://`
|
|
35
|
+
* is required except when the hostname is `localhost`, `127.0.0.1`, or a
|
|
36
|
+
* `.localhost` subdomain (where `http://` is allowed). Invalid shapes
|
|
37
|
+
* throw synchronously from `init()` so misconfigurations surface at boot.
|
|
38
|
+
*/
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
/** Max events to buffer before forcing a flush. Default 50. */
|
|
41
|
+
batchSize?: number;
|
|
42
|
+
/** Max ms between flushes. Default 500. */
|
|
43
|
+
flushIntervalMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Session idle timeout. A new session_id is minted on the next event if
|
|
46
|
+
* the time since the last event exceeds this value. Default 30 min
|
|
47
|
+
* (matches the industry-standard analytics convention).
|
|
48
|
+
*/
|
|
49
|
+
sessionIdleMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Optional error sink. Called with the failed batch + reason so apps can
|
|
52
|
+
* surface drops. Default: console.warn.
|
|
53
|
+
*/
|
|
54
|
+
onError?: (err: unknown, dropped: number) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Override the global fetch. Tests use this to capture requests without
|
|
57
|
+
* spinning up a network stub.
|
|
58
|
+
*/
|
|
59
|
+
fetch?: typeof fetch;
|
|
60
|
+
/**
|
|
61
|
+
* Override the AsyncStorage implementation. The SDK auto-detects
|
|
62
|
+
* `@react-native-async-storage/async-storage` when the peer dep is
|
|
63
|
+
* installed; pass this only when you bring a different storage backend
|
|
64
|
+
* (e.g. an MMKV-backed shim) or to disable persistence in tests.
|
|
65
|
+
* Pass `null` explicitly to opt out of storage entirely (in-memory ids).
|
|
66
|
+
*/
|
|
67
|
+
storage?: AsyncStorageLike | null;
|
|
68
|
+
/**
|
|
69
|
+
* Override the React Native AppState module. Defaults to `react-native`'s
|
|
70
|
+
* AppState; tests pass a stub.
|
|
71
|
+
*/
|
|
72
|
+
appState?: AppStateLike | null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Minimal AppState shape the SDK consumes (subset of react-native's AppState).
|
|
76
|
+
* The SDK auto-detects `react-native` at runtime; pass this only for tests or
|
|
77
|
+
* non-RN environments.
|
|
78
|
+
*/
|
|
79
|
+
interface AppStateLike {
|
|
80
|
+
addEventListener(type: "change", listener: (state: string) => void): {
|
|
81
|
+
remove: () => void;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
interface LuminClient {
|
|
85
|
+
screen(name?: string, properties?: Record<string, unknown>): void;
|
|
86
|
+
track(name: string, properties?: Record<string, unknown>): void;
|
|
87
|
+
identify(userId: string, traits?: Record<string, unknown>): void;
|
|
88
|
+
flush(): Promise<void>;
|
|
89
|
+
close(): Promise<void>;
|
|
90
|
+
/** Current session id. Resolves once AsyncStorage hydration finishes. */
|
|
91
|
+
getSessionId(): Promise<string>;
|
|
92
|
+
/** Current anonymous id. Resolves once AsyncStorage hydration finishes. */
|
|
93
|
+
getAnonymousId(): Promise<string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type { AppStateLike as A, EventType as E, InitOptions as I, LuminClient as L, WireEvent as W, AsyncStorageLike as a };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
type EventType = "page" | "track" | "identify";
|
|
2
|
+
interface WireEvent {
|
|
3
|
+
ts?: number;
|
|
4
|
+
session_id: string;
|
|
5
|
+
anonymous_id?: string;
|
|
6
|
+
user_id?: string;
|
|
7
|
+
type: EventType;
|
|
8
|
+
name?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
referrer?: string;
|
|
11
|
+
properties?: Record<string, unknown>;
|
|
12
|
+
trace_id?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Minimal AsyncStorage shape the SDK actually consumes. Matches both
|
|
16
|
+
* @react-native-async-storage/async-storage (community) and the legacy
|
|
17
|
+
* react-native built-in. Apps that ship a different storage backend can
|
|
18
|
+
* implement this directly.
|
|
19
|
+
*/
|
|
20
|
+
interface AsyncStorageLike {
|
|
21
|
+
getItem(key: string): Promise<string | null>;
|
|
22
|
+
setItem(key: string, value: string): Promise<void>;
|
|
23
|
+
removeItem?(key: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
interface InitOptions {
|
|
26
|
+
/** API key minted in the Lumin UI (settings → API keys). Must be `lmn_pub_*`. */
|
|
27
|
+
apiKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* Base URL of the Lumin ingest endpoint. Defaults to
|
|
30
|
+
* `https://api.getlumin.dev`. Override only for local development against
|
|
31
|
+
* a Lumin instance you are running yourself or for a customer-side
|
|
32
|
+
* same-origin proxy.
|
|
33
|
+
*
|
|
34
|
+
* Validation: must parse as a valid URL with no path beyond `/`. `https://`
|
|
35
|
+
* is required except when the hostname is `localhost`, `127.0.0.1`, or a
|
|
36
|
+
* `.localhost` subdomain (where `http://` is allowed). Invalid shapes
|
|
37
|
+
* throw synchronously from `init()` so misconfigurations surface at boot.
|
|
38
|
+
*/
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
/** Max events to buffer before forcing a flush. Default 50. */
|
|
41
|
+
batchSize?: number;
|
|
42
|
+
/** Max ms between flushes. Default 500. */
|
|
43
|
+
flushIntervalMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Session idle timeout. A new session_id is minted on the next event if
|
|
46
|
+
* the time since the last event exceeds this value. Default 30 min
|
|
47
|
+
* (matches the industry-standard analytics convention).
|
|
48
|
+
*/
|
|
49
|
+
sessionIdleMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Optional error sink. Called with the failed batch + reason so apps can
|
|
52
|
+
* surface drops. Default: console.warn.
|
|
53
|
+
*/
|
|
54
|
+
onError?: (err: unknown, dropped: number) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Override the global fetch. Tests use this to capture requests without
|
|
57
|
+
* spinning up a network stub.
|
|
58
|
+
*/
|
|
59
|
+
fetch?: typeof fetch;
|
|
60
|
+
/**
|
|
61
|
+
* Override the AsyncStorage implementation. The SDK auto-detects
|
|
62
|
+
* `@react-native-async-storage/async-storage` when the peer dep is
|
|
63
|
+
* installed; pass this only when you bring a different storage backend
|
|
64
|
+
* (e.g. an MMKV-backed shim) or to disable persistence in tests.
|
|
65
|
+
* Pass `null` explicitly to opt out of storage entirely (in-memory ids).
|
|
66
|
+
*/
|
|
67
|
+
storage?: AsyncStorageLike | null;
|
|
68
|
+
/**
|
|
69
|
+
* Override the React Native AppState module. Defaults to `react-native`'s
|
|
70
|
+
* AppState; tests pass a stub.
|
|
71
|
+
*/
|
|
72
|
+
appState?: AppStateLike | null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Minimal AppState shape the SDK consumes (subset of react-native's AppState).
|
|
76
|
+
* The SDK auto-detects `react-native` at runtime; pass this only for tests or
|
|
77
|
+
* non-RN environments.
|
|
78
|
+
*/
|
|
79
|
+
interface AppStateLike {
|
|
80
|
+
addEventListener(type: "change", listener: (state: string) => void): {
|
|
81
|
+
remove: () => void;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
interface LuminClient {
|
|
85
|
+
screen(name?: string, properties?: Record<string, unknown>): void;
|
|
86
|
+
track(name: string, properties?: Record<string, unknown>): void;
|
|
87
|
+
identify(userId: string, traits?: Record<string, unknown>): void;
|
|
88
|
+
flush(): Promise<void>;
|
|
89
|
+
close(): Promise<void>;
|
|
90
|
+
/** Current session id. Resolves once AsyncStorage hydration finishes. */
|
|
91
|
+
getSessionId(): Promise<string>;
|
|
92
|
+
/** Current anonymous id. Resolves once AsyncStorage hydration finishes. */
|
|
93
|
+
getAnonymousId(): Promise<string>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type { AppStateLike as A, EventType as E, InitOptions as I, LuminClient as L, WireEvent as W, AsyncStorageLike as a };
|