@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 +1260 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +726 -0
- package/dist/index.d.ts +726 -0
- package/dist/index.js +1218 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
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
|