@kitbase/events 0.1.1

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.js ADDED
@@ -0,0 +1,1218 @@
1
+ // src/client.ts
2
+ import { v4 as uuidv4 } from "uuid";
3
+
4
+ // src/errors.ts
5
+ var KitbaseError = class _KitbaseError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "KitbaseError";
9
+ Object.setPrototypeOf(this, _KitbaseError.prototype);
10
+ }
11
+ };
12
+ var AuthenticationError = class _AuthenticationError extends KitbaseError {
13
+ constructor(message = "Invalid API key") {
14
+ super(message);
15
+ this.name = "AuthenticationError";
16
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
17
+ }
18
+ };
19
+ var ApiError = class _ApiError extends KitbaseError {
20
+ statusCode;
21
+ response;
22
+ constructor(message, statusCode, response) {
23
+ super(message);
24
+ this.name = "ApiError";
25
+ this.statusCode = statusCode;
26
+ this.response = response;
27
+ Object.setPrototypeOf(this, _ApiError.prototype);
28
+ }
29
+ };
30
+ var ValidationError = class _ValidationError extends KitbaseError {
31
+ field;
32
+ constructor(message, field) {
33
+ super(message);
34
+ this.name = "ValidationError";
35
+ this.field = field;
36
+ Object.setPrototypeOf(this, _ValidationError.prototype);
37
+ }
38
+ };
39
+ var TimeoutError = class _TimeoutError extends KitbaseError {
40
+ constructor(message = "Request timed out") {
41
+ super(message);
42
+ this.name = "TimeoutError";
43
+ Object.setPrototypeOf(this, _TimeoutError.prototype);
44
+ }
45
+ };
46
+
47
+ // src/queue/index.ts
48
+ import Dexie from "dexie";
49
+ var DEFAULT_CONFIG = {
50
+ enabled: false,
51
+ maxQueueSize: 1e3,
52
+ flushInterval: 3e4,
53
+ flushBatchSize: 50,
54
+ maxRetries: 3,
55
+ retryBaseDelay: 1e3
56
+ };
57
+ function isIndexedDBAvailable() {
58
+ try {
59
+ return typeof window !== "undefined" && typeof window.indexedDB !== "undefined" && window.indexedDB !== null;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function isBrowser() {
65
+ return typeof window !== "undefined" && typeof document !== "undefined";
66
+ }
67
+ var KitbaseQueueDB = class extends Dexie {
68
+ events;
69
+ constructor(dbName) {
70
+ super(dbName);
71
+ this.version(1).stores({
72
+ events: "++id, timestamp, retries, lastAttempt"
73
+ });
74
+ }
75
+ };
76
+ var MemoryQueue = class {
77
+ queue = [];
78
+ idCounter = 1;
79
+ async enqueue(payload) {
80
+ const event = {
81
+ id: this.idCounter++,
82
+ payload,
83
+ timestamp: Date.now(),
84
+ retries: 0
85
+ };
86
+ this.queue.push(event);
87
+ return event.id;
88
+ }
89
+ async dequeue(count) {
90
+ this.queue.sort((a, b) => a.timestamp - b.timestamp);
91
+ return this.queue.slice(0, count);
92
+ }
93
+ async delete(ids) {
94
+ this.queue = this.queue.filter((e) => !ids.includes(e.id));
95
+ }
96
+ async updateRetries(ids) {
97
+ const now = Date.now();
98
+ for (const event of this.queue) {
99
+ if (ids.includes(event.id)) {
100
+ event.retries++;
101
+ event.lastAttempt = now;
102
+ }
103
+ }
104
+ }
105
+ async getStats() {
106
+ const size = this.queue.length;
107
+ const oldestEvent = size > 0 ? Math.min(...this.queue.map((e) => e.timestamp)) : void 0;
108
+ return { size, oldestEvent };
109
+ }
110
+ async clear() {
111
+ this.queue = [];
112
+ }
113
+ async enforceMaxSize(maxSize) {
114
+ if (this.queue.length > maxSize) {
115
+ this.queue.sort((a, b) => a.timestamp - b.timestamp);
116
+ this.queue = this.queue.slice(-maxSize);
117
+ }
118
+ }
119
+ async getEventsExceedingRetries(maxRetries) {
120
+ return this.queue.filter((e) => e.retries >= maxRetries).map((e) => e.id);
121
+ }
122
+ };
123
+ var EventQueue = class {
124
+ config;
125
+ dbName;
126
+ db = null;
127
+ memoryQueue = null;
128
+ flushTimer = null;
129
+ isFlushing = false;
130
+ sendEvents = null;
131
+ useIndexedDB;
132
+ debugMode = false;
133
+ debugLogger = null;
134
+ constructor(config = {}, dbName = "kitbase-events") {
135
+ this.config = { ...DEFAULT_CONFIG, ...config };
136
+ this.dbName = dbName;
137
+ this.useIndexedDB = isIndexedDBAvailable();
138
+ if (this.useIndexedDB) {
139
+ this.db = new KitbaseQueueDB(this.dbName);
140
+ } else {
141
+ this.memoryQueue = new MemoryQueue();
142
+ }
143
+ }
144
+ /**
145
+ * Set debug mode and logger
146
+ */
147
+ setDebugMode(enabled, logger) {
148
+ this.debugMode = enabled;
149
+ this.debugLogger = logger ?? null;
150
+ }
151
+ log(message, data) {
152
+ if (this.debugMode && this.debugLogger) {
153
+ this.debugLogger(message, data);
154
+ }
155
+ }
156
+ /**
157
+ * Set the callback for sending events
158
+ */
159
+ setSendCallback(callback) {
160
+ this.sendEvents = callback;
161
+ }
162
+ /**
163
+ * Check if the queue storage is available
164
+ */
165
+ isAvailable() {
166
+ return this.useIndexedDB || this.memoryQueue !== null;
167
+ }
168
+ /**
169
+ * Get the storage type being used
170
+ */
171
+ getStorageType() {
172
+ return this.useIndexedDB ? "indexeddb" : "memory";
173
+ }
174
+ /**
175
+ * Add an event to the queue
176
+ */
177
+ async enqueue(payload) {
178
+ const event = {
179
+ payload,
180
+ timestamp: Date.now(),
181
+ retries: 0
182
+ };
183
+ if (this.useIndexedDB && this.db) {
184
+ await this.db.events.add(event);
185
+ this.log("Event queued to IndexedDB", payload);
186
+ } else if (this.memoryQueue) {
187
+ await this.memoryQueue.enqueue(payload);
188
+ this.log("Event queued to memory", payload);
189
+ }
190
+ await this.enforceMaxQueueSize();
191
+ }
192
+ /**
193
+ * Get and remove the next batch of events to send
194
+ */
195
+ async dequeue(count) {
196
+ if (this.useIndexedDB && this.db) {
197
+ return this.db.events.where("retries").below(this.config.maxRetries).sortBy("timestamp").then((events) => events.slice(0, count));
198
+ } else if (this.memoryQueue) {
199
+ const events = await this.memoryQueue.dequeue(count);
200
+ return events.filter((e) => e.retries < this.config.maxRetries);
201
+ }
202
+ return [];
203
+ }
204
+ /**
205
+ * Mark events as successfully sent (remove from queue)
206
+ */
207
+ async markSent(ids) {
208
+ if (ids.length === 0) return;
209
+ if (this.useIndexedDB && this.db) {
210
+ await this.db.events.bulkDelete(ids);
211
+ this.log(`Removed ${ids.length} sent events from queue`);
212
+ } else if (this.memoryQueue) {
213
+ await this.memoryQueue.delete(ids);
214
+ this.log(`Removed ${ids.length} sent events from memory queue`);
215
+ }
216
+ }
217
+ /**
218
+ * Mark events as failed and increment retry count
219
+ */
220
+ async markFailed(ids) {
221
+ if (ids.length === 0) return;
222
+ const now = Date.now();
223
+ if (this.useIndexedDB && this.db) {
224
+ await this.db.transaction("rw", this.db.events, async () => {
225
+ for (const id of ids) {
226
+ const event = await this.db.events.get(id);
227
+ if (event) {
228
+ await this.db.events.update(id, {
229
+ retries: event.retries + 1,
230
+ lastAttempt: now
231
+ });
232
+ }
233
+ }
234
+ });
235
+ this.log(`Marked ${ids.length} events as failed`);
236
+ } else if (this.memoryQueue) {
237
+ await this.memoryQueue.updateRetries(ids);
238
+ this.log(`Marked ${ids.length} events as failed in memory queue`);
239
+ }
240
+ await this.removeExpiredRetries();
241
+ }
242
+ /**
243
+ * Remove events that have exceeded max retry attempts
244
+ */
245
+ async removeExpiredRetries() {
246
+ if (this.useIndexedDB && this.db) {
247
+ const expiredIds = await this.db.events.where("retries").aboveOrEqual(this.config.maxRetries).primaryKeys();
248
+ if (expiredIds.length > 0) {
249
+ await this.db.events.bulkDelete(expiredIds);
250
+ this.log(`Removed ${expiredIds.length} events that exceeded max retries`);
251
+ }
252
+ } else if (this.memoryQueue) {
253
+ const expiredIds = await this.memoryQueue.getEventsExceedingRetries(
254
+ this.config.maxRetries
255
+ );
256
+ if (expiredIds.length > 0) {
257
+ await this.memoryQueue.delete(expiredIds);
258
+ this.log(`Removed ${expiredIds.length} events that exceeded max retries`);
259
+ }
260
+ }
261
+ }
262
+ /**
263
+ * Enforce the maximum queue size by removing oldest events
264
+ */
265
+ async enforceMaxQueueSize() {
266
+ if (this.useIndexedDB && this.db) {
267
+ const count = await this.db.events.count();
268
+ if (count > this.config.maxQueueSize) {
269
+ const excess = count - this.config.maxQueueSize;
270
+ const oldestEvents = await this.db.events.orderBy("timestamp").limit(excess).primaryKeys();
271
+ await this.db.events.bulkDelete(oldestEvents);
272
+ this.log(`Removed ${excess} oldest events to enforce queue size limit`);
273
+ }
274
+ } else if (this.memoryQueue) {
275
+ await this.memoryQueue.enforceMaxSize(this.config.maxQueueSize);
276
+ }
277
+ }
278
+ /**
279
+ * Get queue statistics
280
+ */
281
+ async getStats() {
282
+ if (this.useIndexedDB && this.db) {
283
+ const size = await this.db.events.count();
284
+ const oldestEvent = await this.db.events.orderBy("timestamp").first().then((e) => e?.timestamp);
285
+ return { size, oldestEvent, isFlushing: this.isFlushing };
286
+ } else if (this.memoryQueue) {
287
+ const stats = await this.memoryQueue.getStats();
288
+ return { ...stats, isFlushing: this.isFlushing };
289
+ }
290
+ return { size: 0, isFlushing: this.isFlushing };
291
+ }
292
+ /**
293
+ * Clear all events from the queue
294
+ */
295
+ async clear() {
296
+ if (this.useIndexedDB && this.db) {
297
+ await this.db.events.clear();
298
+ this.log("Queue cleared (IndexedDB)");
299
+ } else if (this.memoryQueue) {
300
+ await this.memoryQueue.clear();
301
+ this.log("Queue cleared (memory)");
302
+ }
303
+ }
304
+ /**
305
+ * Start the automatic flush timer
306
+ */
307
+ startFlushTimer() {
308
+ if (this.flushTimer) return;
309
+ this.flushTimer = setInterval(() => {
310
+ this.flush().catch((err) => {
311
+ this.log("Flush timer error", err);
312
+ });
313
+ }, this.config.flushInterval);
314
+ if (isBrowser()) {
315
+ window.addEventListener("online", this.handleOnline);
316
+ }
317
+ this.log(`Flush timer started (interval: ${this.config.flushInterval}ms)`);
318
+ }
319
+ /**
320
+ * Stop the automatic flush timer
321
+ */
322
+ stopFlushTimer() {
323
+ if (this.flushTimer) {
324
+ clearInterval(this.flushTimer);
325
+ this.flushTimer = null;
326
+ }
327
+ if (isBrowser()) {
328
+ window.removeEventListener("online", this.handleOnline);
329
+ }
330
+ this.log("Flush timer stopped");
331
+ }
332
+ /**
333
+ * Handle coming back online
334
+ */
335
+ handleOnline = () => {
336
+ this.log("Browser came online, triggering flush");
337
+ this.flush().catch((err) => {
338
+ this.log("Online flush error", err);
339
+ });
340
+ };
341
+ /**
342
+ * Check if we're currently online
343
+ */
344
+ isOnline() {
345
+ if (isBrowser()) {
346
+ return navigator.onLine;
347
+ }
348
+ return true;
349
+ }
350
+ /**
351
+ * Manually trigger a flush of queued events
352
+ */
353
+ async flush() {
354
+ if (this.isFlushing) {
355
+ this.log("Flush already in progress, skipping");
356
+ return;
357
+ }
358
+ if (!this.isOnline()) {
359
+ this.log("Offline, skipping flush");
360
+ return;
361
+ }
362
+ if (!this.sendEvents) {
363
+ this.log("No send callback configured, skipping flush");
364
+ return;
365
+ }
366
+ this.isFlushing = true;
367
+ try {
368
+ const stats = await this.getStats();
369
+ if (stats.size === 0) {
370
+ this.log("Queue is empty, nothing to flush");
371
+ return;
372
+ }
373
+ this.log(`Flushing queue (${stats.size} events)`);
374
+ let processed = 0;
375
+ while (true) {
376
+ const events = await this.dequeue(this.config.flushBatchSize);
377
+ if (events.length === 0) break;
378
+ this.log(`Sending batch of ${events.length} events`);
379
+ try {
380
+ const sentIds = await this.sendEvents(events);
381
+ await this.markSent(sentIds);
382
+ const failedIds = events.filter((e) => !sentIds.includes(e.id)).map((e) => e.id);
383
+ if (failedIds.length > 0) {
384
+ await this.markFailed(failedIds);
385
+ }
386
+ processed += sentIds.length;
387
+ } catch (error) {
388
+ const allIds = events.map((e) => e.id);
389
+ await this.markFailed(allIds);
390
+ this.log("Batch send failed", error);
391
+ break;
392
+ }
393
+ }
394
+ this.log(`Flush complete, sent ${processed} events`);
395
+ } finally {
396
+ this.isFlushing = false;
397
+ }
398
+ }
399
+ /**
400
+ * Close the database connection
401
+ */
402
+ async close() {
403
+ this.stopFlushTimer();
404
+ if (this.db) {
405
+ this.db.close();
406
+ this.log("Database connection closed");
407
+ }
408
+ }
409
+ };
410
+
411
+ // src/client.ts
412
+ var DEFAULT_BASE_URL = "https://api.kitbase.dev";
413
+ var TIMEOUT = 3e4;
414
+ var DEFAULT_STORAGE_KEY = "kitbase_anonymous_id";
415
+ var DEFAULT_SESSION_STORAGE_KEY = "kitbase_session";
416
+ var DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1e3;
417
+ var ANALYTICS_CHANNEL = "__analytics";
418
+ var MemoryStorage = class {
419
+ data = /* @__PURE__ */ new Map();
420
+ getItem(key) {
421
+ return this.data.get(key) ?? null;
422
+ }
423
+ setItem(key, value) {
424
+ this.data.set(key, value);
425
+ }
426
+ removeItem(key) {
427
+ this.data.delete(key);
428
+ }
429
+ };
430
+ function getDefaultStorage() {
431
+ if (typeof window !== "undefined" && window.localStorage) {
432
+ return window.localStorage;
433
+ }
434
+ return new MemoryStorage();
435
+ }
436
+ var Kitbase = class {
437
+ token;
438
+ baseUrl;
439
+ storage;
440
+ storageKey;
441
+ anonymousId = null;
442
+ // Super properties (memory-only, merged into all events)
443
+ superProperties = {};
444
+ // Time event tracking
445
+ timedEvents = /* @__PURE__ */ new Map();
446
+ // Debug mode
447
+ debugMode;
448
+ // Offline queue
449
+ queue = null;
450
+ offlineEnabled;
451
+ // Analytics & Session tracking
452
+ session = null;
453
+ sessionTimeout;
454
+ sessionStorageKey;
455
+ analyticsEnabled;
456
+ autoTrackPageViews;
457
+ userId = null;
458
+ unloadListenerAdded = false;
459
+ constructor(config) {
460
+ if (!config.token) {
461
+ throw new ValidationError("API token is required", "token");
462
+ }
463
+ this.token = config.token;
464
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
465
+ this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;
466
+ this.debugMode = config.debug ?? false;
467
+ if (config.storage === null) {
468
+ this.storage = null;
469
+ } else {
470
+ this.storage = config.storage ?? getDefaultStorage();
471
+ }
472
+ this.initializeAnonymousId();
473
+ this.sessionTimeout = config.analytics?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
474
+ this.sessionStorageKey = config.analytics?.sessionStorageKey ?? DEFAULT_SESSION_STORAGE_KEY;
475
+ this.analyticsEnabled = config.analytics?.autoTrackSessions ?? true;
476
+ this.autoTrackPageViews = config.analytics?.autoTrackPageViews ?? false;
477
+ if (this.analyticsEnabled) {
478
+ this.loadSession();
479
+ this.setupUnloadListener();
480
+ if (this.autoTrackPageViews && typeof window !== "undefined") {
481
+ this.enableAutoPageViews();
482
+ }
483
+ }
484
+ this.offlineEnabled = config.offline?.enabled ?? false;
485
+ if (this.offlineEnabled) {
486
+ this.queue = new EventQueue(config.offline);
487
+ this.queue.setDebugMode(this.debugMode, this.log.bind(this));
488
+ this.queue.setSendCallback(this.sendQueuedEvents.bind(this));
489
+ this.queue.startFlushTimer();
490
+ this.log("Offline queueing enabled", {
491
+ storageType: this.queue.getStorageType()
492
+ });
493
+ }
494
+ }
495
+ /**
496
+ * Initialize the anonymous ID from storage or generate a new one
497
+ */
498
+ initializeAnonymousId() {
499
+ if (this.storage) {
500
+ const stored = this.storage.getItem(this.storageKey);
501
+ if (stored) {
502
+ this.anonymousId = stored;
503
+ return;
504
+ }
505
+ }
506
+ this.anonymousId = uuidv4();
507
+ if (this.storage && this.anonymousId) {
508
+ this.storage.setItem(this.storageKey, this.anonymousId);
509
+ }
510
+ }
511
+ /**
512
+ * Get the current anonymous ID
513
+ */
514
+ getAnonymousId() {
515
+ return this.anonymousId;
516
+ }
517
+ // ============================================================
518
+ // Debug Mode
519
+ // ============================================================
520
+ /**
521
+ * Enable or disable debug mode
522
+ * When enabled, all SDK operations are logged to the console
523
+ *
524
+ * @param enabled - Whether to enable debug mode
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * kitbase.setDebugMode(true);
529
+ * // All events and operations will now be logged
530
+ * ```
531
+ */
532
+ setDebugMode(enabled) {
533
+ this.debugMode = enabled;
534
+ if (this.queue) {
535
+ this.queue.setDebugMode(enabled, this.log.bind(this));
536
+ }
537
+ this.log(`Debug mode ${enabled ? "enabled" : "disabled"}`);
538
+ }
539
+ /**
540
+ * Check if debug mode is enabled
541
+ */
542
+ isDebugMode() {
543
+ return this.debugMode;
544
+ }
545
+ /**
546
+ * Internal logging function
547
+ */
548
+ log(message, data) {
549
+ if (!this.debugMode) return;
550
+ const prefix = "[Kitbase]";
551
+ if (data !== void 0) {
552
+ console.log(prefix, message, data);
553
+ } else {
554
+ console.log(prefix, message);
555
+ }
556
+ }
557
+ // ============================================================
558
+ // Super Properties
559
+ // ============================================================
560
+ /**
561
+ * Register super properties that will be included with every event
562
+ * These properties are stored in memory only and reset on page reload
563
+ *
564
+ * @param properties - Properties to register
565
+ *
566
+ * @example
567
+ * ```typescript
568
+ * kitbase.register({
569
+ * app_version: '2.1.0',
570
+ * platform: 'web',
571
+ * environment: 'production',
572
+ * });
573
+ * ```
574
+ */
575
+ register(properties) {
576
+ this.superProperties = { ...this.superProperties, ...properties };
577
+ this.log("Super properties registered", properties);
578
+ }
579
+ /**
580
+ * Register super properties only if they haven't been set yet
581
+ * Useful for setting default values that shouldn't override existing ones
582
+ *
583
+ * @param properties - Properties to register if not already set
584
+ *
585
+ * @example
586
+ * ```typescript
587
+ * kitbase.registerOnce({ first_visit: new Date().toISOString() });
588
+ * ```
589
+ */
590
+ registerOnce(properties) {
591
+ const newProps = {};
592
+ for (const [key, value] of Object.entries(properties)) {
593
+ if (!(key in this.superProperties)) {
594
+ newProps[key] = value;
595
+ }
596
+ }
597
+ if (Object.keys(newProps).length > 0) {
598
+ this.superProperties = { ...this.superProperties, ...newProps };
599
+ this.log("Super properties registered (once)", newProps);
600
+ }
601
+ }
602
+ /**
603
+ * Remove a super property
604
+ *
605
+ * @param key - The property key to remove
606
+ *
607
+ * @example
608
+ * ```typescript
609
+ * kitbase.unregister('platform');
610
+ * ```
611
+ */
612
+ unregister(key) {
613
+ if (key in this.superProperties) {
614
+ delete this.superProperties[key];
615
+ this.log("Super property removed", { key });
616
+ }
617
+ }
618
+ /**
619
+ * Get all registered super properties
620
+ *
621
+ * @returns A copy of the current super properties
622
+ *
623
+ * @example
624
+ * ```typescript
625
+ * const props = kitbase.getSuperProperties();
626
+ * console.log(props); // { app_version: '2.1.0', platform: 'web' }
627
+ * ```
628
+ */
629
+ getSuperProperties() {
630
+ return { ...this.superProperties };
631
+ }
632
+ /**
633
+ * Clear all super properties
634
+ *
635
+ * @example
636
+ * ```typescript
637
+ * kitbase.clearSuperProperties();
638
+ * ```
639
+ */
640
+ clearSuperProperties() {
641
+ this.superProperties = {};
642
+ this.log("Super properties cleared");
643
+ }
644
+ // ============================================================
645
+ // Time Events (Duration Tracking)
646
+ // ============================================================
647
+ /**
648
+ * Start timing an event
649
+ * When the same event is tracked later, a $duration property (in seconds)
650
+ * will automatically be included
651
+ *
652
+ * @param eventName - The name of the event to time
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * kitbase.timeEvent('Video Watched');
657
+ * // ... user watches video ...
658
+ * await kitbase.track({
659
+ * channel: 'engagement',
660
+ * event: 'Video Watched',
661
+ * tags: { video_id: '123' }
662
+ * });
663
+ * // Event will include $duration: 45.2 (seconds)
664
+ * ```
665
+ */
666
+ timeEvent(eventName) {
667
+ this.timedEvents.set(eventName, Date.now());
668
+ this.log("Timer started", { event: eventName });
669
+ }
670
+ /**
671
+ * Cancel a timed event without tracking it
672
+ *
673
+ * @param eventName - The name of the event to cancel timing for
674
+ *
675
+ * @example
676
+ * ```typescript
677
+ * kitbase.timeEvent('Checkout Flow');
678
+ * // User abandons checkout
679
+ * kitbase.cancelTimeEvent('Checkout Flow');
680
+ * ```
681
+ */
682
+ cancelTimeEvent(eventName) {
683
+ if (this.timedEvents.has(eventName)) {
684
+ this.timedEvents.delete(eventName);
685
+ this.log("Timer cancelled", { event: eventName });
686
+ }
687
+ }
688
+ /**
689
+ * Get all currently timed events
690
+ *
691
+ * @returns Array of event names that are currently being timed
692
+ *
693
+ * @example
694
+ * ```typescript
695
+ * const timedEvents = kitbase.getTimedEvents();
696
+ * console.log(timedEvents); // ['Video Watched', 'Checkout Flow']
697
+ * ```
698
+ */
699
+ getTimedEvents() {
700
+ return Array.from(this.timedEvents.keys());
701
+ }
702
+ /**
703
+ * Get the duration of a timed event (without stopping it)
704
+ *
705
+ * @param eventName - The name of the event
706
+ * @returns Duration in seconds, or null if not being timed
707
+ */
708
+ getEventDuration(eventName) {
709
+ const startTime = this.timedEvents.get(eventName);
710
+ if (startTime === void 0) return null;
711
+ return (Date.now() - startTime) / 1e3;
712
+ }
713
+ // ============================================================
714
+ // Offline Queue
715
+ // ============================================================
716
+ /**
717
+ * Get offline queue statistics
718
+ *
719
+ * @returns Queue statistics including size and flush status
720
+ *
721
+ * @example
722
+ * ```typescript
723
+ * const stats = await kitbase.getQueueStats();
724
+ * console.log(stats); // { size: 5, isFlushing: false }
725
+ * ```
726
+ */
727
+ async getQueueStats() {
728
+ if (!this.queue) return null;
729
+ return this.queue.getStats();
730
+ }
731
+ /**
732
+ * Manually flush the offline queue
733
+ * Events are automatically flushed on interval and when coming back online,
734
+ * but this method can be used to trigger an immediate flush
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * await kitbase.flushQueue();
739
+ * ```
740
+ */
741
+ async flushQueue() {
742
+ if (!this.queue) return;
743
+ await this.queue.flush();
744
+ }
745
+ /**
746
+ * Clear all events from the offline queue
747
+ *
748
+ * @example
749
+ * ```typescript
750
+ * await kitbase.clearQueue();
751
+ * ```
752
+ */
753
+ async clearQueue() {
754
+ if (!this.queue) return;
755
+ await this.queue.clear();
756
+ }
757
+ /**
758
+ * Callback for the queue to send batched events
759
+ */
760
+ async sendQueuedEvents(events) {
761
+ const sentIds = [];
762
+ for (const event of events) {
763
+ try {
764
+ await this.sendRequest("/sdk/v1/logs", event.payload);
765
+ sentIds.push(event.id);
766
+ } catch (error) {
767
+ this.log("Failed to send queued event", { id: event.id, error });
768
+ }
769
+ }
770
+ return sentIds;
771
+ }
772
+ // ============================================================
773
+ // Track Event
774
+ // ============================================================
775
+ /**
776
+ * Track an event
777
+ *
778
+ * When offline queueing is enabled, events are always written to the local
779
+ * database first (write-ahead), then sent to the server. This ensures no
780
+ * events are lost if the browser crashes or the network fails.
781
+ *
782
+ * @param options - Event tracking options
783
+ * @returns Promise resolving to the track response
784
+ * @throws {ValidationError} When required fields are missing
785
+ * @throws {AuthenticationError} When the API key is invalid (only when offline disabled)
786
+ * @throws {ApiError} When the API returns an error (only when offline disabled)
787
+ * @throws {TimeoutError} When the request times out (only when offline disabled)
788
+ */
789
+ async track(options) {
790
+ this.validateTrackOptions(options);
791
+ let duration;
792
+ const startTime = this.timedEvents.get(options.event);
793
+ if (startTime !== void 0) {
794
+ duration = (Date.now() - startTime) / 1e3;
795
+ this.timedEvents.delete(options.event);
796
+ this.log("Timer stopped", { event: options.event, duration });
797
+ }
798
+ const includeAnonymousId = options.includeAnonymousId !== false;
799
+ const mergedTags = {
800
+ ...this.superProperties,
801
+ ...options.tags ?? {},
802
+ ...duration !== void 0 ? { $duration: duration } : {}
803
+ };
804
+ const payload = {
805
+ channel: options.channel,
806
+ event: options.event,
807
+ timestamp: Date.now(),
808
+ ...options.user_id && { user_id: options.user_id },
809
+ ...includeAnonymousId && this.anonymousId && { anonymous_id: this.anonymousId },
810
+ ...options.icon && { icon: options.icon },
811
+ ...options.notify !== void 0 && { notify: options.notify },
812
+ ...options.description && { description: options.description },
813
+ ...Object.keys(mergedTags).length > 0 && { tags: mergedTags }
814
+ };
815
+ this.log("Track", { event: options.event, payload });
816
+ if (this.queue) {
817
+ await this.queue.enqueue(payload);
818
+ this.log("Event persisted to queue");
819
+ this.queue.flush().catch((err) => {
820
+ this.log("Background flush failed", err);
821
+ });
822
+ return {
823
+ id: `queued-${Date.now()}`,
824
+ event: options.event,
825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
826
+ };
827
+ }
828
+ const response = await this.sendRequest("/sdk/v1/logs", payload);
829
+ this.log("Event sent successfully", { id: response.id });
830
+ return response;
831
+ }
832
+ validateTrackOptions(options) {
833
+ if (!options.event) {
834
+ throw new ValidationError("Event is required", "event");
835
+ }
836
+ }
837
+ /**
838
+ * Send a request to the API
839
+ */
840
+ async sendRequest(endpoint, body) {
841
+ const url = `${this.baseUrl}${endpoint}`;
842
+ const controller = new AbortController();
843
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
844
+ try {
845
+ const response = await fetch(url, {
846
+ method: "POST",
847
+ headers: {
848
+ "Content-Type": "application/json",
849
+ "x-sdk-key": `${this.token}`
850
+ },
851
+ body: JSON.stringify(body),
852
+ signal: controller.signal
853
+ });
854
+ clearTimeout(timeoutId);
855
+ if (!response.ok) {
856
+ const errorBody = await this.parseResponseBody(response);
857
+ if (response.status === 401) {
858
+ throw new AuthenticationError();
859
+ }
860
+ throw new ApiError(
861
+ this.getErrorMessage(errorBody, response.statusText),
862
+ response.status,
863
+ errorBody
864
+ );
865
+ }
866
+ return await response.json();
867
+ } catch (error) {
868
+ clearTimeout(timeoutId);
869
+ if (error instanceof Error && error.name === "AbortError") {
870
+ throw new TimeoutError();
871
+ }
872
+ throw error;
873
+ }
874
+ }
875
+ async parseResponseBody(response) {
876
+ try {
877
+ return await response.json();
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
882
+ getErrorMessage(body, fallback) {
883
+ if (body && typeof body === "object" && "message" in body) {
884
+ return String(body.message);
885
+ }
886
+ if (body && typeof body === "object" && "error" in body) {
887
+ return String(body.error);
888
+ }
889
+ return fallback;
890
+ }
891
+ // ============================================================
892
+ // Analytics & Session Management
893
+ // ============================================================
894
+ /**
895
+ * Load session from storage
896
+ */
897
+ loadSession() {
898
+ if (!this.storage) return;
899
+ try {
900
+ const stored = this.storage.getItem(this.sessionStorageKey);
901
+ if (stored) {
902
+ const session = JSON.parse(stored);
903
+ const now = Date.now();
904
+ if (now - session.lastActivityAt < this.sessionTimeout) {
905
+ this.session = session;
906
+ this.log("Session restored", { sessionId: session.id });
907
+ } else {
908
+ this.storage.removeItem(this.sessionStorageKey);
909
+ this.log("Session expired, removed from storage");
910
+ }
911
+ }
912
+ } catch (error) {
913
+ this.log("Failed to load session from storage", error);
914
+ }
915
+ }
916
+ /**
917
+ * Save session to storage
918
+ */
919
+ saveSession() {
920
+ if (!this.storage || !this.session) return;
921
+ try {
922
+ this.storage.setItem(this.sessionStorageKey, JSON.stringify(this.session));
923
+ } catch (error) {
924
+ this.log("Failed to save session to storage", error);
925
+ }
926
+ }
927
+ /**
928
+ * Get or create a session
929
+ */
930
+ getOrCreateSession() {
931
+ const now = Date.now();
932
+ if (this.session && now - this.session.lastActivityAt > this.sessionTimeout) {
933
+ this.endSession();
934
+ this.session = null;
935
+ }
936
+ if (!this.session) {
937
+ const referrer = typeof document !== "undefined" ? document.referrer : void 0;
938
+ const path = typeof window !== "undefined" ? window.location.pathname : void 0;
939
+ this.session = {
940
+ id: uuidv4(),
941
+ startedAt: now,
942
+ lastActivityAt: now,
943
+ screenViewCount: 0,
944
+ entryPath: path,
945
+ entryReferrer: referrer
946
+ };
947
+ this.log("New session created", { sessionId: this.session.id });
948
+ this.trackSessionStart();
949
+ }
950
+ this.session.lastActivityAt = now;
951
+ this.saveSession();
952
+ return this.session;
953
+ }
954
+ /**
955
+ * Get the current session ID (or null if no active session)
956
+ */
957
+ getSessionId() {
958
+ return this.session?.id ?? null;
959
+ }
960
+ /**
961
+ * Get the current session data
962
+ */
963
+ getSession() {
964
+ return this.session ? { ...this.session } : null;
965
+ }
966
+ /**
967
+ * Track session start event
968
+ */
969
+ trackSessionStart() {
970
+ if (!this.session) return;
971
+ const utmParams = this.getUtmParams();
972
+ this.track({
973
+ channel: ANALYTICS_CHANNEL,
974
+ event: "session_start",
975
+ tags: {
976
+ __session_id: this.session.id,
977
+ __entry_path: this.session.entryPath ?? "",
978
+ __referrer: this.session.entryReferrer ?? "",
979
+ ...utmParams
980
+ }
981
+ }).catch((err) => this.log("Failed to track session_start", err));
982
+ }
983
+ /**
984
+ * End the current session (clears local state only - server calculates metrics)
985
+ */
986
+ endSession() {
987
+ if (!this.session) return;
988
+ this.log("Session ended", { sessionId: this.session.id });
989
+ if (this.storage) {
990
+ this.storage.removeItem(this.sessionStorageKey);
991
+ }
992
+ this.session = null;
993
+ }
994
+ /**
995
+ * Setup listeners for session lifecycle management
996
+ */
997
+ setupUnloadListener() {
998
+ if (typeof window === "undefined" || this.unloadListenerAdded) return;
999
+ document.addEventListener("visibilitychange", () => {
1000
+ if (document.visibilityState === "hidden") {
1001
+ this.saveSession();
1002
+ this.log("Page hidden, session state saved");
1003
+ }
1004
+ });
1005
+ window.addEventListener("pagehide", () => {
1006
+ this.endSession();
1007
+ this.log("Page unloading, session ended locally");
1008
+ });
1009
+ this.unloadListenerAdded = true;
1010
+ this.log("Session lifecycle listeners added");
1011
+ }
1012
+ /**
1013
+ * Get UTM parameters from URL
1014
+ */
1015
+ getUtmParams() {
1016
+ if (typeof window === "undefined") return {};
1017
+ const params = new URLSearchParams(window.location.search);
1018
+ const utmParams = {};
1019
+ const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
1020
+ for (const key of utmKeys) {
1021
+ const value = params.get(key);
1022
+ if (value) {
1023
+ utmParams[`__${key}`] = value;
1024
+ }
1025
+ }
1026
+ return utmParams;
1027
+ }
1028
+ /**
1029
+ * Track a page view
1030
+ *
1031
+ * @param options - Page view options
1032
+ * @returns Promise resolving to the track response
1033
+ *
1034
+ * @example
1035
+ * ```typescript
1036
+ * // Track current page
1037
+ * await kitbase.trackPageView();
1038
+ *
1039
+ * // Track with custom path
1040
+ * await kitbase.trackPageView({ path: '/products/123', title: 'Product Details' });
1041
+ * ```
1042
+ */
1043
+ async trackPageView(options = {}) {
1044
+ const session = this.getOrCreateSession();
1045
+ session.screenViewCount++;
1046
+ this.saveSession();
1047
+ const path = options.path ?? (typeof window !== "undefined" ? window.location.pathname : "");
1048
+ const title = options.title ?? (typeof document !== "undefined" ? document.title : "");
1049
+ const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
1050
+ return this.track({
1051
+ channel: ANALYTICS_CHANNEL,
1052
+ event: "screen_view",
1053
+ tags: {
1054
+ __session_id: session.id,
1055
+ __path: path,
1056
+ __title: title,
1057
+ __referrer: referrer,
1058
+ ...this.getUtmParams(),
1059
+ ...options.tags ?? {}
1060
+ }
1061
+ });
1062
+ }
1063
+ /**
1064
+ * Enable automatic page view tracking
1065
+ * Intercepts browser history changes (pushState, replaceState, popstate)
1066
+ *
1067
+ * @example
1068
+ * ```typescript
1069
+ * kitbase.enableAutoPageViews();
1070
+ * // Now all route changes will automatically be tracked
1071
+ * ```
1072
+ */
1073
+ enableAutoPageViews() {
1074
+ if (typeof window === "undefined") {
1075
+ this.log("Auto page views not available in non-browser environment");
1076
+ return;
1077
+ }
1078
+ this.trackPageView().catch((err) => this.log("Failed to track initial page view", err));
1079
+ const originalPushState = history.pushState.bind(history);
1080
+ history.pushState = (...args) => {
1081
+ originalPushState(...args);
1082
+ this.trackPageView().catch((err) => this.log("Failed to track page view (pushState)", err));
1083
+ };
1084
+ const originalReplaceState = history.replaceState.bind(history);
1085
+ history.replaceState = (...args) => {
1086
+ originalReplaceState(...args);
1087
+ };
1088
+ window.addEventListener("popstate", () => {
1089
+ this.trackPageView().catch((err) => this.log("Failed to track page view (popstate)", err));
1090
+ });
1091
+ this.log("Auto page view tracking enabled");
1092
+ }
1093
+ /**
1094
+ * Track a revenue event
1095
+ *
1096
+ * @param options - Revenue options
1097
+ * @returns Promise resolving to the track response
1098
+ *
1099
+ * @example
1100
+ * ```typescript
1101
+ * // Track a $19.99 purchase
1102
+ * await kitbase.trackRevenue({
1103
+ * amount: 1999,
1104
+ * currency: 'USD',
1105
+ * tags: { product_id: 'prod_123', plan: 'premium' },
1106
+ * });
1107
+ * ```
1108
+ */
1109
+ async trackRevenue(options) {
1110
+ const session = this.getOrCreateSession();
1111
+ return this.track({
1112
+ channel: ANALYTICS_CHANNEL,
1113
+ event: "revenue",
1114
+ user_id: options.user_id ?? this.userId ?? void 0,
1115
+ tags: {
1116
+ __session_id: session.id,
1117
+ __revenue: options.amount,
1118
+ __currency: options.currency ?? "USD",
1119
+ ...options.tags ?? {}
1120
+ }
1121
+ });
1122
+ }
1123
+ /**
1124
+ * Identify a user
1125
+ * Links the current anonymous ID to a user ID for future events
1126
+ *
1127
+ * @param options - Identify options
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * kitbase.identify({
1132
+ * userId: 'user_123',
1133
+ * traits: { email: 'user@example.com', plan: 'premium' },
1134
+ * });
1135
+ * ```
1136
+ */
1137
+ identify(options) {
1138
+ this.userId = options.userId;
1139
+ if (options.traits) {
1140
+ this.register({
1141
+ __user_id: options.userId,
1142
+ ...options.traits
1143
+ });
1144
+ } else {
1145
+ this.register({ __user_id: options.userId });
1146
+ }
1147
+ this.track({
1148
+ channel: ANALYTICS_CHANNEL,
1149
+ event: "identify",
1150
+ user_id: options.userId,
1151
+ tags: {
1152
+ __session_id: this.session?.id ?? "",
1153
+ __anonymous_id: this.anonymousId ?? "",
1154
+ ...options.traits ?? {}
1155
+ }
1156
+ }).catch((err) => this.log("Failed to track identify", err));
1157
+ this.log("User identified", { userId: options.userId });
1158
+ }
1159
+ /**
1160
+ * Get the current user ID (set via identify)
1161
+ */
1162
+ getUserId() {
1163
+ return this.userId;
1164
+ }
1165
+ /**
1166
+ * Reset the user identity and session
1167
+ * Call this when a user logs out
1168
+ *
1169
+ * @example
1170
+ * ```typescript
1171
+ * kitbase.reset();
1172
+ * ```
1173
+ */
1174
+ reset() {
1175
+ if (this.session) {
1176
+ this.endSession();
1177
+ this.session = null;
1178
+ }
1179
+ this.userId = null;
1180
+ this.anonymousId = uuidv4();
1181
+ if (this.storage) {
1182
+ this.storage.setItem(this.storageKey, this.anonymousId);
1183
+ }
1184
+ this.clearSuperProperties();
1185
+ this.log("User reset complete");
1186
+ }
1187
+ // ============================================================
1188
+ // Cleanup
1189
+ // ============================================================
1190
+ /**
1191
+ * Shutdown the client and cleanup resources
1192
+ * Call this when you're done using the client to stop timers and close connections
1193
+ *
1194
+ * @example
1195
+ * ```typescript
1196
+ * await kitbase.shutdown();
1197
+ * ```
1198
+ */
1199
+ async shutdown() {
1200
+ this.log("Shutting down");
1201
+ if (this.queue) {
1202
+ await this.queue.flush();
1203
+ await this.queue.close();
1204
+ this.queue = null;
1205
+ }
1206
+ this.timedEvents.clear();
1207
+ this.log("Shutdown complete");
1208
+ }
1209
+ };
1210
+ export {
1211
+ ApiError,
1212
+ AuthenticationError,
1213
+ Kitbase,
1214
+ KitbaseError,
1215
+ TimeoutError,
1216
+ ValidationError
1217
+ };
1218
+ //# sourceMappingURL=index.js.map