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