@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/dist/index.cjs ADDED
@@ -0,0 +1,382 @@
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/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Client: () => Client,
24
+ init: () => init
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/ids.ts
29
+ function uuidv4() {
30
+ const c = globalThis.crypto;
31
+ if (c && typeof c.randomUUID === "function") {
32
+ return c.randomUUID();
33
+ }
34
+ const bytes = new Array(16);
35
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
36
+ bytes[6] = bytes[6] & 15 | 64;
37
+ bytes[8] = bytes[8] & 63 | 128;
38
+ const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
39
+ 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("");
40
+ }
41
+ var ANON_KEY = "lumin.anonymous_id";
42
+ var SESSION_KEY = "lumin.session_id";
43
+ 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
+ function memoryStorage() {
58
+ const m = /* @__PURE__ */ new Map();
59
+ return {
60
+ async getItem(k) {
61
+ return m.has(k) ? m.get(k) : null;
62
+ },
63
+ async setItem(k, v) {
64
+ m.set(k, v);
65
+ },
66
+ async removeItem(k) {
67
+ m.delete(k);
68
+ }
69
+ };
70
+ }
71
+ async function readSafe(s, key) {
72
+ try {
73
+ return await s.getItem(key);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ async function writeSafe(s, key, value) {
79
+ try {
80
+ await s.setItem(key, value);
81
+ } catch {
82
+ }
83
+ }
84
+ async function getOrCreateAnonymousId(storage) {
85
+ const existing = await readSafe(storage, ANON_KEY);
86
+ if (existing) return existing;
87
+ const id = uuidv4();
88
+ await writeSafe(storage, ANON_KEY, id);
89
+ return id;
90
+ }
91
+ async function getOrCreateSessionId(storage, idleMs, now = Date.now()) {
92
+ const existingId = await readSafe(storage, SESSION_KEY);
93
+ const lastSeenStr = await readSafe(storage, SESSION_LAST_SEEN_KEY);
94
+ const lastSeen = lastSeenStr ? Number(lastSeenStr) : NaN;
95
+ const expired = !existingId || !Number.isFinite(lastSeen) || now - lastSeen > idleMs;
96
+ const id = expired ? uuidv4() : existingId;
97
+ if (expired) {
98
+ await writeSafe(storage, SESSION_KEY, id);
99
+ }
100
+ await writeSafe(storage, SESSION_LAST_SEEN_KEY, String(now));
101
+ return id;
102
+ }
103
+
104
+ // src/client.ts
105
+ var DEFAULT_BATCH_SIZE = 50;
106
+ var DEFAULT_FLUSH_MS = 500;
107
+ var DEFAULT_SESSION_IDLE_MS = 30 * 60 * 1e3;
108
+ var SERVER_MAX_BATCH = 1e3;
109
+ var DEFAULT_ENDPOINT = "https://api.getlumin.dev";
110
+ var Client = class {
111
+ constructor(opts) {
112
+ this.userId = null;
113
+ this.cachedIds = null;
114
+ // Pending events queued before ids hydrate. `user_id` is stamped at enqueue
115
+ // time so a later identify() does not retroactively bind pre-identify events
116
+ // — matches the browser SDK's behaviour. Once `idsReady` resolves, the
117
+ // flushing path stamps `session_id` + `anonymous_id` and drains them.
118
+ this.pending = [];
119
+ this.buffer = [];
120
+ this.timer = null;
121
+ this.closed = false;
122
+ this.inflight = [];
123
+ this.appStateSub = null;
124
+ if (!opts.apiKey) throw new Error("lumin: apiKey required");
125
+ this.endpoint = validateEndpoint(opts.endpoint ?? DEFAULT_ENDPOINT);
126
+ this.apiKey = opts.apiKey;
127
+ this.batchSize = clampBatch(opts.batchSize ?? DEFAULT_BATCH_SIZE);
128
+ this.flushIntervalMs = Math.max(50, opts.flushIntervalMs ?? DEFAULT_FLUSH_MS);
129
+ this.sessionIdleMs = Math.max(1e3, opts.sessionIdleMs ?? DEFAULT_SESSION_IDLE_MS);
130
+ this.onError = opts.onError ?? ((err, dropped) => {
131
+ console.warn("[lumin] flush failed", { err, dropped });
132
+ });
133
+ this.fetchImpl = opts.fetch ?? fetch.bind(globalThis);
134
+ const resolvedStorage = opts.storage === null ? memoryStorage() : opts.storage ?? detectAsyncStorage() ?? memoryStorage();
135
+ this.storage = resolvedStorage;
136
+ this.appState = opts.appState === void 0 ? detectAppState() : opts.appState;
137
+ this.idsReady = this.hydrateIds();
138
+ this.installBackgroundFlush();
139
+ }
140
+ screen(name, properties) {
141
+ if (this.closed) return;
142
+ this.enqueue({
143
+ type: "page",
144
+ ...name !== void 0 ? { name } : {},
145
+ ...properties !== void 0 ? { properties } : {}
146
+ });
147
+ }
148
+ track(name, properties) {
149
+ if (this.closed) return;
150
+ if (!name) {
151
+ this.onError(new Error("track requires a name"), 0);
152
+ return;
153
+ }
154
+ this.enqueue({ type: "track", name, ...properties !== void 0 ? { properties } : {} });
155
+ }
156
+ identify(userId, traits) {
157
+ if (this.closed) return;
158
+ if (!userId) {
159
+ this.onError(new Error("identify requires a userId"), 0);
160
+ return;
161
+ }
162
+ this.userId = userId;
163
+ this.enqueue({ type: "identify", ...traits !== void 0 ? { properties: traits } : {} });
164
+ }
165
+ /**
166
+ * Drain the buffer to the server. Returns when the in-flight request
167
+ * settles (success or failure). Awaits id hydration first so a `flush()`
168
+ * called immediately after `init()` does not race the AsyncStorage read.
169
+ */
170
+ async flush() {
171
+ await this.drainPending();
172
+ if (this.buffer.length === 0) return;
173
+ const batch = this.buffer;
174
+ this.buffer = [];
175
+ this.cancelTimer();
176
+ const p = this.send(batch);
177
+ this.inflight.push(p);
178
+ try {
179
+ await p;
180
+ } finally {
181
+ this.inflight = this.inflight.filter((q) => q !== p);
182
+ }
183
+ }
184
+ /**
185
+ * Stop accepting events, flush whatever's buffered, wait for in-flight
186
+ * requests to settle, and remove the AppState listener. Idempotent.
187
+ */
188
+ async close() {
189
+ if (this.closed) return;
190
+ this.closed = true;
191
+ this.removeBackgroundFlush();
192
+ await this.flush();
193
+ await Promise.allSettled(this.inflight);
194
+ }
195
+ async getSessionId() {
196
+ const ids = await this.idsReady;
197
+ return ids.sessionId;
198
+ }
199
+ async getAnonymousId() {
200
+ const ids = await this.idsReady;
201
+ return ids.anonymousId;
202
+ }
203
+ // --- internals ---------------------------------------------------------
204
+ async hydrateIds() {
205
+ const anonymousId = await getOrCreateAnonymousId(this.storage);
206
+ const sessionId = await getOrCreateSessionId(this.storage, this.sessionIdleMs);
207
+ const ids = { anonymousId, sessionId };
208
+ this.cachedIds = ids;
209
+ return ids;
210
+ }
211
+ /**
212
+ * Refresh `cachedIds.sessionId` if the persisted lastSeenAt is older than
213
+ * `sessionIdleMs`. Called before each send so a long background pause →
214
+ * foreground burst opens a new session.
215
+ */
216
+ async refreshSessionIfIdle() {
217
+ if (!this.cachedIds) return;
218
+ const sid = await getOrCreateSessionId(this.storage, this.sessionIdleMs);
219
+ this.cachedIds.sessionId = sid;
220
+ }
221
+ enqueue(partial) {
222
+ const withUser = {
223
+ ts: Date.now(),
224
+ ...this.userId ? { user_id: this.userId } : {},
225
+ ...partial
226
+ };
227
+ if (!this.cachedIds) {
228
+ this.pending.push(withUser);
229
+ this.scheduleTimer();
230
+ return;
231
+ }
232
+ this.buffer.push(this.stamp(withUser, this.cachedIds));
233
+ if (this.buffer.length >= this.batchSize) {
234
+ void this.flush();
235
+ return;
236
+ }
237
+ this.scheduleTimer();
238
+ }
239
+ stamp(partial, ids) {
240
+ return {
241
+ session_id: ids.sessionId,
242
+ anonymous_id: ids.anonymousId,
243
+ ...partial
244
+ };
245
+ }
246
+ async drainPending() {
247
+ if (this.pending.length === 0 && this.cachedIds) return;
248
+ const ids = await this.idsReady;
249
+ await this.refreshSessionIfIdle();
250
+ const effective = this.cachedIds ?? ids;
251
+ if (this.pending.length === 0) return;
252
+ const drained = this.pending;
253
+ this.pending = [];
254
+ for (const p of drained) this.buffer.push(this.stamp(p, effective));
255
+ }
256
+ scheduleTimer() {
257
+ if (this.timer !== null) return;
258
+ this.timer = setTimeout(() => {
259
+ this.timer = null;
260
+ void this.flush();
261
+ }, this.flushIntervalMs);
262
+ }
263
+ cancelTimer() {
264
+ if (this.timer !== null) {
265
+ clearTimeout(this.timer);
266
+ this.timer = null;
267
+ }
268
+ }
269
+ async send(batch) {
270
+ if (batch.length === 0) return;
271
+ if (batch.length > SERVER_MAX_BATCH) {
272
+ this.onError(
273
+ new Error(`batch size ${batch.length} exceeds server max ${SERVER_MAX_BATCH}`),
274
+ batch.length
275
+ );
276
+ return;
277
+ }
278
+ const body = JSON.stringify({ events: batch });
279
+ try {
280
+ const res = await this.fetchImpl(this.endpoint + "/v1/events", {
281
+ method: "POST",
282
+ headers: {
283
+ "Content-Type": "application/json",
284
+ Authorization: "Bearer " + this.apiKey
285
+ },
286
+ body
287
+ });
288
+ if (!res.ok) {
289
+ this.onError(new Error(`HTTP ${res.status}`), batch.length);
290
+ }
291
+ } catch (err) {
292
+ this.onError(err, batch.length);
293
+ }
294
+ }
295
+ /**
296
+ * Flush on app background/inactive. RN keeps the JS runtime alive for a
297
+ * short grace window after backgrounding (~30 s on iOS, similar on
298
+ * Android), which is enough for one batched POST to land. There is no
299
+ * `keepalive: true` equivalent on RN fetch — the OS-level grace window
300
+ * is the contract.
301
+ */
302
+ installBackgroundFlush() {
303
+ if (!this.appState) return;
304
+ this.appStateSub = this.appState.addEventListener("change", (state) => {
305
+ if (state === "background" || state === "inactive") {
306
+ if (this.buffer.length === 0 && this.pending.length === 0) return;
307
+ void this.flush();
308
+ }
309
+ });
310
+ }
311
+ removeBackgroundFlush() {
312
+ if (this.appStateSub) {
313
+ this.appStateSub.remove();
314
+ this.appStateSub = null;
315
+ }
316
+ }
317
+ };
318
+ function clampBatch(n) {
319
+ if (!Number.isFinite(n) || n < 1) return 1;
320
+ if (n > SERVER_MAX_BATCH) return SERVER_MAX_BATCH;
321
+ return Math.floor(n);
322
+ }
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;
330
+ }
331
+ return null;
332
+ } catch {
333
+ return null;
334
+ }
335
+ }
336
+ function validateEndpoint(raw) {
337
+ let url;
338
+ try {
339
+ url = new URL(raw);
340
+ } catch {
341
+ throw new Error(`lumin: endpoint is not a valid URL: ${raw}`);
342
+ }
343
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
344
+ throw new Error(`lumin: endpoint must be http(s); got ${url.protocol}//`);
345
+ }
346
+ const host = url.hostname;
347
+ const isLocalDev = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost");
348
+ if (url.protocol === "http:" && !isLocalDev) {
349
+ throw new Error(`lumin: endpoint must use https:// for non-local hosts (got ${raw})`);
350
+ }
351
+ if (url.pathname.replace(/\/+$/, "") !== "") {
352
+ throw new Error(
353
+ `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".`
354
+ );
355
+ }
356
+ if (url.search !== "" || url.hash !== "") {
357
+ throw new Error(`lumin: endpoint must not include a query string or fragment; got ${raw}`);
358
+ }
359
+ if (!url.hostname) {
360
+ throw new Error(`lumin: endpoint is missing a hostname: ${raw}`);
361
+ }
362
+ return `${url.protocol}//${url.host}`;
363
+ }
364
+
365
+ // src/index.ts
366
+ function init(opts) {
367
+ const c = new Client(opts);
368
+ return {
369
+ screen: c.screen.bind(c),
370
+ track: c.track.bind(c),
371
+ identify: c.identify.bind(c),
372
+ flush: c.flush.bind(c),
373
+ close: c.close.bind(c),
374
+ getSessionId: c.getSessionId.bind(c),
375
+ getAnonymousId: c.getAnonymousId.bind(c)
376
+ };
377
+ }
378
+ // Annotate the CommonJS export names for ESM import in node:
379
+ 0 && (module.exports = {
380
+ Client,
381
+ init
382
+ });
@@ -0,0 +1,79 @@
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';
3
+
4
+ declare class Client implements LuminClient {
5
+ private readonly endpoint;
6
+ private readonly apiKey;
7
+ private readonly batchSize;
8
+ private readonly flushIntervalMs;
9
+ private readonly sessionIdleMs;
10
+ private readonly onError;
11
+ private readonly fetchImpl;
12
+ private readonly storage;
13
+ private readonly appState;
14
+ private userId;
15
+ private idsReady;
16
+ private cachedIds;
17
+ private pending;
18
+ private buffer;
19
+ private timer;
20
+ private closed;
21
+ private inflight;
22
+ private appStateSub;
23
+ constructor(opts: InitOptions);
24
+ screen(name?: string, properties?: Record<string, unknown>): void;
25
+ track(name: string, properties?: Record<string, unknown>): void;
26
+ identify(userId: string, traits?: Record<string, unknown>): void;
27
+ /**
28
+ * Drain the buffer to the server. Returns when the in-flight request
29
+ * settles (success or failure). Awaits id hydration first so a `flush()`
30
+ * called immediately after `init()` does not race the AsyncStorage read.
31
+ */
32
+ flush(): Promise<void>;
33
+ /**
34
+ * Stop accepting events, flush whatever's buffered, wait for in-flight
35
+ * requests to settle, and remove the AppState listener. Idempotent.
36
+ */
37
+ close(): Promise<void>;
38
+ getSessionId(): Promise<string>;
39
+ getAnonymousId(): Promise<string>;
40
+ private hydrateIds;
41
+ /**
42
+ * Refresh `cachedIds.sessionId` if the persisted lastSeenAt is older than
43
+ * `sessionIdleMs`. Called before each send so a long background pause →
44
+ * foreground burst opens a new session.
45
+ */
46
+ private refreshSessionIfIdle;
47
+ private enqueue;
48
+ private stamp;
49
+ private drainPending;
50
+ private scheduleTimer;
51
+ private cancelTimer;
52
+ private send;
53
+ /**
54
+ * Flush on app background/inactive. RN keeps the JS runtime alive for a
55
+ * short grace window after backgrounding (~30 s on iOS, similar on
56
+ * Android), which is enough for one batched POST to land. There is no
57
+ * `keepalive: true` equivalent on RN fetch — the OS-level grace window
58
+ * is the contract.
59
+ */
60
+ private installBackgroundFlush;
61
+ private removeBackgroundFlush;
62
+ }
63
+
64
+ /**
65
+ * Initialise the Lumin React Native SDK. Returns a client whose `screen`,
66
+ * `track`, `identify`, `flush`, and `close` methods are bound — safe to
67
+ * destructure.
68
+ *
69
+ * `init` is synchronous: the returned client buffers events while it reads
70
+ * AsyncStorage for the persistent anonymous and session ids in the
71
+ * background. Callers can `screen()` / `track()` / `identify()` immediately;
72
+ * the first flush waits for hydration to complete.
73
+ *
74
+ * Calling `init` multiple times produces independent clients; apps that
75
+ * want a single shared instance should pin one at the module level.
76
+ */
77
+ declare function init(opts: InitOptions): LuminClient;
78
+
79
+ export { Client, InitOptions, LuminClient, init };
@@ -0,0 +1,79 @@
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';
3
+
4
+ declare class Client implements LuminClient {
5
+ private readonly endpoint;
6
+ private readonly apiKey;
7
+ private readonly batchSize;
8
+ private readonly flushIntervalMs;
9
+ private readonly sessionIdleMs;
10
+ private readonly onError;
11
+ private readonly fetchImpl;
12
+ private readonly storage;
13
+ private readonly appState;
14
+ private userId;
15
+ private idsReady;
16
+ private cachedIds;
17
+ private pending;
18
+ private buffer;
19
+ private timer;
20
+ private closed;
21
+ private inflight;
22
+ private appStateSub;
23
+ constructor(opts: InitOptions);
24
+ screen(name?: string, properties?: Record<string, unknown>): void;
25
+ track(name: string, properties?: Record<string, unknown>): void;
26
+ identify(userId: string, traits?: Record<string, unknown>): void;
27
+ /**
28
+ * Drain the buffer to the server. Returns when the in-flight request
29
+ * settles (success or failure). Awaits id hydration first so a `flush()`
30
+ * called immediately after `init()` does not race the AsyncStorage read.
31
+ */
32
+ flush(): Promise<void>;
33
+ /**
34
+ * Stop accepting events, flush whatever's buffered, wait for in-flight
35
+ * requests to settle, and remove the AppState listener. Idempotent.
36
+ */
37
+ close(): Promise<void>;
38
+ getSessionId(): Promise<string>;
39
+ getAnonymousId(): Promise<string>;
40
+ private hydrateIds;
41
+ /**
42
+ * Refresh `cachedIds.sessionId` if the persisted lastSeenAt is older than
43
+ * `sessionIdleMs`. Called before each send so a long background pause →
44
+ * foreground burst opens a new session.
45
+ */
46
+ private refreshSessionIfIdle;
47
+ private enqueue;
48
+ private stamp;
49
+ private drainPending;
50
+ private scheduleTimer;
51
+ private cancelTimer;
52
+ private send;
53
+ /**
54
+ * Flush on app background/inactive. RN keeps the JS runtime alive for a
55
+ * short grace window after backgrounding (~30 s on iOS, similar on
56
+ * Android), which is enough for one batched POST to land. There is no
57
+ * `keepalive: true` equivalent on RN fetch — the OS-level grace window
58
+ * is the contract.
59
+ */
60
+ private installBackgroundFlush;
61
+ private removeBackgroundFlush;
62
+ }
63
+
64
+ /**
65
+ * Initialise the Lumin React Native SDK. Returns a client whose `screen`,
66
+ * `track`, `identify`, `flush`, and `close` methods are bound — safe to
67
+ * destructure.
68
+ *
69
+ * `init` is synchronous: the returned client buffers events while it reads
70
+ * AsyncStorage for the persistent anonymous and session ids in the
71
+ * background. Callers can `screen()` / `track()` / `identify()` immediately;
72
+ * the first flush waits for hydration to complete.
73
+ *
74
+ * Calling `init` multiple times produces independent clients; apps that
75
+ * want a single shared instance should pin one at the module level.
76
+ */
77
+ declare function init(opts: InitOptions): LuminClient;
78
+
79
+ export { Client, InitOptions, LuminClient, init };