@kitbase/analytics 0.1.6
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/README.md +475 -0
- package/dist/index.cjs +2430 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1242 -0
- package/dist/index.d.ts +1242 -0
- package/dist/index.js +2374 -0
- package/dist/index.js.map +1 -0
- package/dist/lite.js +1 -0
- package/package.json +65 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2430 @@
|
|
|
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
|
+
ClickTrackingPlugin: () => ClickTrackingPlugin,
|
|
36
|
+
FrustrationPlugin: () => FrustrationPlugin,
|
|
37
|
+
KitbaseAnalytics: () => KitbaseAnalytics2,
|
|
38
|
+
KitbaseError: () => KitbaseError,
|
|
39
|
+
OutboundLinksPlugin: () => OutboundLinksPlugin,
|
|
40
|
+
PageViewPlugin: () => PageViewPlugin,
|
|
41
|
+
ScrollDepthPlugin: () => ScrollDepthPlugin,
|
|
42
|
+
TimeoutError: () => TimeoutError,
|
|
43
|
+
ValidationError: () => ValidationError,
|
|
44
|
+
VisibilityPlugin: () => VisibilityPlugin,
|
|
45
|
+
WebVitalsPlugin: () => WebVitalsPlugin,
|
|
46
|
+
createDefaultPlugins: () => createDefaultPlugins,
|
|
47
|
+
detectBot: () => detectBot,
|
|
48
|
+
getInstance: () => getInstance,
|
|
49
|
+
getUserAgent: () => getUserAgent,
|
|
50
|
+
init: () => init,
|
|
51
|
+
isBot: () => isBot,
|
|
52
|
+
isUserAgentBot: () => isUserAgentBot
|
|
53
|
+
});
|
|
54
|
+
module.exports = __toCommonJS(index_exports);
|
|
55
|
+
|
|
56
|
+
// src/errors.ts
|
|
57
|
+
var KitbaseError = class _KitbaseError extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "KitbaseError";
|
|
61
|
+
Object.setPrototypeOf(this, _KitbaseError.prototype);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var AuthenticationError = class _AuthenticationError extends KitbaseError {
|
|
65
|
+
constructor(message = "Invalid API key") {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "AuthenticationError";
|
|
68
|
+
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var ApiError = class _ApiError extends KitbaseError {
|
|
72
|
+
statusCode;
|
|
73
|
+
response;
|
|
74
|
+
constructor(message, statusCode, response) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "ApiError";
|
|
77
|
+
this.statusCode = statusCode;
|
|
78
|
+
this.response = response;
|
|
79
|
+
Object.setPrototypeOf(this, _ApiError.prototype);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ValidationError = class _ValidationError extends KitbaseError {
|
|
83
|
+
field;
|
|
84
|
+
constructor(message, field) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "ValidationError";
|
|
87
|
+
this.field = field;
|
|
88
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var TimeoutError = class _TimeoutError extends KitbaseError {
|
|
92
|
+
constructor(message = "Request timed out") {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "TimeoutError";
|
|
95
|
+
Object.setPrototypeOf(this, _TimeoutError.prototype);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/botDetection.ts
|
|
100
|
+
var AUTOMATION_GLOBALS = [
|
|
101
|
+
"__webdriver_evaluate",
|
|
102
|
+
"__selenium_evaluate",
|
|
103
|
+
"__webdriver_script_function",
|
|
104
|
+
"__webdriver_unwrapped",
|
|
105
|
+
"__fxdriver_evaluate",
|
|
106
|
+
"__driver_evaluate",
|
|
107
|
+
"_Selenium_IDE_Recorder",
|
|
108
|
+
"_selenium",
|
|
109
|
+
"calledSelenium",
|
|
110
|
+
"$cdc_asdjflasutopfhvcZLmcfl_",
|
|
111
|
+
// Chrome DevTools Protocol marker
|
|
112
|
+
"__nightmare",
|
|
113
|
+
"domAutomation",
|
|
114
|
+
"domAutomationController"
|
|
115
|
+
];
|
|
116
|
+
var HEADLESS_PATTERNS = [
|
|
117
|
+
"headlesschrome",
|
|
118
|
+
"phantomjs",
|
|
119
|
+
"selenium",
|
|
120
|
+
"webdriver",
|
|
121
|
+
"puppeteer",
|
|
122
|
+
"playwright"
|
|
123
|
+
];
|
|
124
|
+
var HTTP_CLIENT_PATTERNS = [
|
|
125
|
+
"python",
|
|
126
|
+
"curl",
|
|
127
|
+
"wget",
|
|
128
|
+
"java/",
|
|
129
|
+
"go-http",
|
|
130
|
+
"node-fetch",
|
|
131
|
+
"axios",
|
|
132
|
+
"postman",
|
|
133
|
+
"insomnia",
|
|
134
|
+
"httpie",
|
|
135
|
+
"ruby",
|
|
136
|
+
"perl",
|
|
137
|
+
"scrapy",
|
|
138
|
+
"bot",
|
|
139
|
+
"spider",
|
|
140
|
+
"crawler",
|
|
141
|
+
"slurp",
|
|
142
|
+
"googlebot",
|
|
143
|
+
"bingbot",
|
|
144
|
+
"yandexbot",
|
|
145
|
+
"baiduspider",
|
|
146
|
+
"duckduckbot",
|
|
147
|
+
"facebookexternalhit",
|
|
148
|
+
"twitterbot",
|
|
149
|
+
"linkedinbot",
|
|
150
|
+
"whatsapp",
|
|
151
|
+
"telegram",
|
|
152
|
+
"discord",
|
|
153
|
+
"slack"
|
|
154
|
+
];
|
|
155
|
+
var DEFAULT_BOT_DETECTION_CONFIG = {
|
|
156
|
+
enabled: true,
|
|
157
|
+
checkWebdriver: true,
|
|
158
|
+
checkPhantomJS: true,
|
|
159
|
+
checkNightmare: true,
|
|
160
|
+
checkAutomationGlobals: true,
|
|
161
|
+
checkDocumentAttributes: true,
|
|
162
|
+
checkUserAgentHeadless: true,
|
|
163
|
+
checkUserAgentHttpClient: true
|
|
164
|
+
};
|
|
165
|
+
function isBrowser() {
|
|
166
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
167
|
+
}
|
|
168
|
+
function getWindowProperty(key) {
|
|
169
|
+
try {
|
|
170
|
+
return window[key];
|
|
171
|
+
} catch {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function checkWebdriver() {
|
|
176
|
+
if (!isBrowser()) return false;
|
|
177
|
+
try {
|
|
178
|
+
return window.navigator?.webdriver === true;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function checkPhantomJS() {
|
|
184
|
+
if (!isBrowser()) return false;
|
|
185
|
+
try {
|
|
186
|
+
return !!(getWindowProperty("callPhantom") || getWindowProperty("_phantom") || getWindowProperty("phantom"));
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function checkNightmare() {
|
|
192
|
+
if (!isBrowser()) return false;
|
|
193
|
+
try {
|
|
194
|
+
return !!getWindowProperty("__nightmare");
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function checkAutomationGlobals() {
|
|
200
|
+
if (!isBrowser()) return false;
|
|
201
|
+
try {
|
|
202
|
+
for (const global of AUTOMATION_GLOBALS) {
|
|
203
|
+
if (getWindowProperty(global) !== void 0) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function checkDocumentAttributes() {
|
|
213
|
+
if (!isBrowser()) return false;
|
|
214
|
+
try {
|
|
215
|
+
const docEl = document.documentElement;
|
|
216
|
+
if (!docEl) return false;
|
|
217
|
+
return !!(docEl.getAttribute("webdriver") || docEl.getAttribute("selenium") || docEl.getAttribute("driver"));
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function checkUserAgentHeadless() {
|
|
223
|
+
if (!isBrowser()) return false;
|
|
224
|
+
try {
|
|
225
|
+
const ua = window.navigator?.userAgent?.toLowerCase() || "";
|
|
226
|
+
if (!ua) return false;
|
|
227
|
+
for (const pattern of HEADLESS_PATTERNS) {
|
|
228
|
+
if (ua.includes(pattern)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function checkUserAgentHttpClient(additionalPatterns) {
|
|
238
|
+
if (!isBrowser()) return false;
|
|
239
|
+
try {
|
|
240
|
+
const ua = window.navigator?.userAgent?.toLowerCase() || "";
|
|
241
|
+
if (!ua) return false;
|
|
242
|
+
for (const pattern of HTTP_CLIENT_PATTERNS) {
|
|
243
|
+
if (ua.includes(pattern)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (additionalPatterns) {
|
|
248
|
+
for (const pattern of additionalPatterns) {
|
|
249
|
+
if (ua.includes(pattern.toLowerCase())) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function checkMissingUserAgent() {
|
|
260
|
+
if (!isBrowser()) return false;
|
|
261
|
+
try {
|
|
262
|
+
const ua = window.navigator?.userAgent;
|
|
263
|
+
return !ua || ua === "" || ua === "undefined" || ua.length < 10;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function checkInvalidEnvironment() {
|
|
269
|
+
if (!isBrowser()) return false;
|
|
270
|
+
try {
|
|
271
|
+
if (!window.navigator || !window.location || !window.document || typeof window.navigator !== "object" || typeof window.location !== "object" || typeof window.document !== "object") {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
} catch {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function detectBot(config = {}) {
|
|
280
|
+
const mergedConfig = { ...DEFAULT_BOT_DETECTION_CONFIG, ...config };
|
|
281
|
+
const checks = {
|
|
282
|
+
webdriver: mergedConfig.checkWebdriver ? checkWebdriver() : false,
|
|
283
|
+
phantomjs: mergedConfig.checkPhantomJS ? checkPhantomJS() : false,
|
|
284
|
+
nightmare: mergedConfig.checkNightmare ? checkNightmare() : false,
|
|
285
|
+
automationGlobals: mergedConfig.checkAutomationGlobals ? checkAutomationGlobals() : false,
|
|
286
|
+
documentAttributes: mergedConfig.checkDocumentAttributes ? checkDocumentAttributes() : false,
|
|
287
|
+
userAgentHeadless: mergedConfig.checkUserAgentHeadless ? checkUserAgentHeadless() : false,
|
|
288
|
+
userAgentHttpClient: mergedConfig.checkUserAgentHttpClient ? checkUserAgentHttpClient(config.additionalBotPatterns) : false,
|
|
289
|
+
missingUserAgent: checkMissingUserAgent(),
|
|
290
|
+
invalidEnvironment: checkInvalidEnvironment()
|
|
291
|
+
};
|
|
292
|
+
let reason;
|
|
293
|
+
if (checks.webdriver) {
|
|
294
|
+
reason = "WebDriver detected";
|
|
295
|
+
} else if (checks.phantomjs) {
|
|
296
|
+
reason = "PhantomJS detected";
|
|
297
|
+
} else if (checks.nightmare) {
|
|
298
|
+
reason = "Nightmare.js detected";
|
|
299
|
+
} else if (checks.automationGlobals) {
|
|
300
|
+
reason = "Automation tool globals detected";
|
|
301
|
+
} else if (checks.documentAttributes) {
|
|
302
|
+
reason = "Automation attributes on document element";
|
|
303
|
+
} else if (checks.userAgentHeadless) {
|
|
304
|
+
reason = "Headless browser user agent detected";
|
|
305
|
+
} else if (checks.userAgentHttpClient) {
|
|
306
|
+
reason = "HTTP client/bot user agent detected";
|
|
307
|
+
} else if (checks.missingUserAgent) {
|
|
308
|
+
reason = "Missing or invalid user agent";
|
|
309
|
+
} else if (checks.invalidEnvironment) {
|
|
310
|
+
reason = "Invalid browser environment";
|
|
311
|
+
}
|
|
312
|
+
const isBot2 = Object.values(checks).some(Boolean);
|
|
313
|
+
const result = {
|
|
314
|
+
isBot: isBot2,
|
|
315
|
+
reason,
|
|
316
|
+
checks
|
|
317
|
+
};
|
|
318
|
+
if (isBot2 && config.onBotDetected) {
|
|
319
|
+
try {
|
|
320
|
+
config.onBotDetected(result);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
function isBot(config = {}) {
|
|
327
|
+
return detectBot(config).isBot;
|
|
328
|
+
}
|
|
329
|
+
function isUserAgentBot(userAgent, additionalPatterns) {
|
|
330
|
+
if (!userAgent || userAgent.length < 10) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
const ua = userAgent.toLowerCase();
|
|
334
|
+
for (const pattern of HEADLESS_PATTERNS) {
|
|
335
|
+
if (ua.includes(pattern)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const pattern of HTTP_CLIENT_PATTERNS) {
|
|
340
|
+
if (ua.includes(pattern)) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (additionalPatterns) {
|
|
345
|
+
for (const pattern of additionalPatterns) {
|
|
346
|
+
if (ua.includes(pattern.toLowerCase())) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
function getUserAgent() {
|
|
354
|
+
if (!isBrowser()) return null;
|
|
355
|
+
try {
|
|
356
|
+
return window.navigator?.userAgent || null;
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/plugins/utils.ts
|
|
363
|
+
var CLICKABLE_SELECTOR = [
|
|
364
|
+
"a",
|
|
365
|
+
"button",
|
|
366
|
+
"input",
|
|
367
|
+
"select",
|
|
368
|
+
"textarea",
|
|
369
|
+
'[role="button"]',
|
|
370
|
+
'[role="link"]',
|
|
371
|
+
'[role="menuitem"]',
|
|
372
|
+
'[role="tab"]'
|
|
373
|
+
].join(", ");
|
|
374
|
+
function findClickableElement(event) {
|
|
375
|
+
const path = event.composedPath?.();
|
|
376
|
+
if (path) {
|
|
377
|
+
for (const node of path) {
|
|
378
|
+
if (!(node instanceof Element)) continue;
|
|
379
|
+
if (node === document.documentElement) break;
|
|
380
|
+
if (node.matches(CLICKABLE_SELECTOR)) {
|
|
381
|
+
const root = node.getRootNode();
|
|
382
|
+
if (root instanceof ShadowRoot && root.host instanceof Element) {
|
|
383
|
+
return root.host;
|
|
384
|
+
}
|
|
385
|
+
return node;
|
|
386
|
+
}
|
|
387
|
+
if (node.tagName.includes("-")) {
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const target = event.target;
|
|
393
|
+
if (!target?.closest) return null;
|
|
394
|
+
return target.closest(CLICKABLE_SELECTOR);
|
|
395
|
+
}
|
|
396
|
+
function buildCssSelector(el) {
|
|
397
|
+
if (el.id) return `#${el.id}`;
|
|
398
|
+
const tag = el.tagName.toLowerCase();
|
|
399
|
+
const classes = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
|
|
400
|
+
if (classes) return `${tag}${classes}`;
|
|
401
|
+
return tag;
|
|
402
|
+
}
|
|
403
|
+
function getRootDomain(hostname) {
|
|
404
|
+
const parts = hostname.replace(/^www\./, "").split(".");
|
|
405
|
+
if (parts.length >= 2) {
|
|
406
|
+
return parts.slice(-2).join(".");
|
|
407
|
+
}
|
|
408
|
+
return hostname;
|
|
409
|
+
}
|
|
410
|
+
function isSameRootDomain(host1, host2) {
|
|
411
|
+
return getRootDomain(host1) === getRootDomain(host2);
|
|
412
|
+
}
|
|
413
|
+
function getUtmParams() {
|
|
414
|
+
if (typeof window === "undefined") return {};
|
|
415
|
+
const params = new URLSearchParams(window.location.search);
|
|
416
|
+
const utmParams = {};
|
|
417
|
+
const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
|
418
|
+
for (const key of utmKeys) {
|
|
419
|
+
const value = params.get(key);
|
|
420
|
+
if (value) {
|
|
421
|
+
utmParams[`__${key}`] = value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return utmParams;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/client-base.ts
|
|
428
|
+
var DEFAULT_BASE_URL = "https://api.kitbase.dev";
|
|
429
|
+
var TIMEOUT = 3e4;
|
|
430
|
+
var ANALYTICS_CHANNEL = "__analytics";
|
|
431
|
+
var KitbaseAnalytics = class _KitbaseAnalytics {
|
|
432
|
+
sdkKey;
|
|
433
|
+
baseUrl;
|
|
434
|
+
// Super properties (memory-only, merged into all events)
|
|
435
|
+
superProperties = {};
|
|
436
|
+
// Time event tracking
|
|
437
|
+
timedEvents = /* @__PURE__ */ new Map();
|
|
438
|
+
// Debug mode
|
|
439
|
+
debugMode;
|
|
440
|
+
// Analytics config (stored for PluginContext)
|
|
441
|
+
analyticsConfig;
|
|
442
|
+
userId = null;
|
|
443
|
+
// Bot detection
|
|
444
|
+
botDetectionConfig;
|
|
445
|
+
botDetectionResult = null;
|
|
446
|
+
// Client-side session tracking
|
|
447
|
+
clientSessionId = null;
|
|
448
|
+
lastActivityAt = 0;
|
|
449
|
+
static SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
450
|
+
// 30 minutes
|
|
451
|
+
// Plugin system
|
|
452
|
+
_plugins = /* @__PURE__ */ new Map();
|
|
453
|
+
_pluginContext = null;
|
|
454
|
+
constructor(config, defaultPlugins) {
|
|
455
|
+
if (!config.sdkKey) {
|
|
456
|
+
throw new ValidationError("SDK key is required", "sdkKey");
|
|
457
|
+
}
|
|
458
|
+
this.sdkKey = config.sdkKey;
|
|
459
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
460
|
+
this.debugMode = config.debug ?? false;
|
|
461
|
+
this.analyticsConfig = config.analytics;
|
|
462
|
+
this.botDetectionConfig = {
|
|
463
|
+
...DEFAULT_BOT_DETECTION_CONFIG,
|
|
464
|
+
...config.botDetection
|
|
465
|
+
};
|
|
466
|
+
if (this.botDetectionConfig.enabled) {
|
|
467
|
+
this.botDetectionResult = detectBot(this.botDetectionConfig);
|
|
468
|
+
if (this.botDetectionResult.isBot) {
|
|
469
|
+
this.log("Bot detected", {
|
|
470
|
+
reason: this.botDetectionResult.reason,
|
|
471
|
+
checks: this.botDetectionResult.checks
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
this.log("Bot detection enabled, no bot detected");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (defaultPlugins) {
|
|
478
|
+
for (const plugin of defaultPlugins) {
|
|
479
|
+
this.use(plugin);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// ============================================================
|
|
484
|
+
// Plugin System
|
|
485
|
+
// ============================================================
|
|
486
|
+
/**
|
|
487
|
+
* Register a plugin
|
|
488
|
+
*
|
|
489
|
+
* @param plugin - The plugin instance to register
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```typescript
|
|
493
|
+
* kitbase.use(new WebVitalsPlugin());
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
use(plugin) {
|
|
497
|
+
if (this._plugins.has(plugin.name)) {
|
|
498
|
+
this.log(`Plugin "${plugin.name}" already registered`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const ctx = this.getPluginContext();
|
|
502
|
+
const result = plugin.setup(ctx);
|
|
503
|
+
if (result === false) {
|
|
504
|
+
this.log(`Plugin "${plugin.name}" declined to activate`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this._plugins.set(plugin.name, plugin);
|
|
508
|
+
const methods = plugin.methods;
|
|
509
|
+
if (methods) {
|
|
510
|
+
for (const [name, fn] of Object.entries(methods)) {
|
|
511
|
+
this[name] = fn;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
this.log(`Plugin "${plugin.name}" registered`);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Get the names of all registered plugins
|
|
518
|
+
*/
|
|
519
|
+
getPlugins() {
|
|
520
|
+
return Array.from(this._plugins.keys());
|
|
521
|
+
}
|
|
522
|
+
getPluginContext() {
|
|
523
|
+
if (this._pluginContext) return this._pluginContext;
|
|
524
|
+
this._pluginContext = {
|
|
525
|
+
track: (options) => this.track(options),
|
|
526
|
+
config: Object.freeze({
|
|
527
|
+
autoTrackPageViews: this.analyticsConfig?.autoTrackPageViews,
|
|
528
|
+
autoTrackOutboundLinks: this.analyticsConfig?.autoTrackOutboundLinks,
|
|
529
|
+
autoTrackClicks: this.analyticsConfig?.autoTrackClicks,
|
|
530
|
+
autoTrackScrollDepth: this.analyticsConfig?.autoTrackScrollDepth,
|
|
531
|
+
autoTrackVisibility: this.analyticsConfig?.autoTrackVisibility,
|
|
532
|
+
autoTrackWebVitals: this.analyticsConfig?.autoTrackWebVitals,
|
|
533
|
+
autoDetectFrustration: this.analyticsConfig?.autoDetectFrustration
|
|
534
|
+
}),
|
|
535
|
+
debug: this.debugMode,
|
|
536
|
+
log: (message, data) => this.log(message, data),
|
|
537
|
+
isBotBlockingActive: () => this.isBotBlockingActive(),
|
|
538
|
+
findClickableElement,
|
|
539
|
+
CLICKABLE_SELECTOR,
|
|
540
|
+
getRootDomain,
|
|
541
|
+
isSameRootDomain,
|
|
542
|
+
getUtmParams
|
|
543
|
+
};
|
|
544
|
+
return this._pluginContext;
|
|
545
|
+
}
|
|
546
|
+
// ============================================================
|
|
547
|
+
// Debug Mode
|
|
548
|
+
// ============================================================
|
|
549
|
+
/**
|
|
550
|
+
* Enable or disable debug mode
|
|
551
|
+
* When enabled, all SDK operations are logged to the console
|
|
552
|
+
*
|
|
553
|
+
* @param enabled - Whether to enable debug mode
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* ```typescript
|
|
557
|
+
* kitbase.setDebugMode(true);
|
|
558
|
+
* // All events and operations will now be logged
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
setDebugMode(enabled) {
|
|
562
|
+
this.debugMode = enabled;
|
|
563
|
+
this.log(`Debug mode ${enabled ? "enabled" : "disabled"}`);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Check if debug mode is enabled
|
|
567
|
+
*/
|
|
568
|
+
isDebugMode() {
|
|
569
|
+
return this.debugMode;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Internal logging function
|
|
573
|
+
*/
|
|
574
|
+
log(message, data) {
|
|
575
|
+
if (!this.debugMode) return;
|
|
576
|
+
const prefix = "[Kitbase]";
|
|
577
|
+
if (data !== void 0) {
|
|
578
|
+
console.log(prefix, message, data);
|
|
579
|
+
} else {
|
|
580
|
+
console.log(prefix, message);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ============================================================
|
|
584
|
+
// Super Properties
|
|
585
|
+
// ============================================================
|
|
586
|
+
/**
|
|
587
|
+
* Register super properties that will be included with every event
|
|
588
|
+
* These properties are stored in memory only and reset on page reload
|
|
589
|
+
*
|
|
590
|
+
* @param properties - Properties to register
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```typescript
|
|
594
|
+
* kitbase.register({
|
|
595
|
+
* app_version: '2.1.0',
|
|
596
|
+
* platform: 'web',
|
|
597
|
+
* environment: 'production',
|
|
598
|
+
* });
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
register(properties) {
|
|
602
|
+
this.superProperties = { ...this.superProperties, ...properties };
|
|
603
|
+
this.log("Super properties registered", properties);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Register super properties only if they haven't been set yet
|
|
607
|
+
* Useful for setting default values that shouldn't override existing ones
|
|
608
|
+
*
|
|
609
|
+
* @param properties - Properties to register if not already set
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* kitbase.registerOnce({ first_visit: new Date().toISOString() });
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
registerOnce(properties) {
|
|
617
|
+
const newProps = {};
|
|
618
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
619
|
+
if (!(key in this.superProperties)) {
|
|
620
|
+
newProps[key] = value;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (Object.keys(newProps).length > 0) {
|
|
624
|
+
this.superProperties = { ...this.superProperties, ...newProps };
|
|
625
|
+
this.log("Super properties registered (once)", newProps);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Remove a super property
|
|
630
|
+
*
|
|
631
|
+
* @param key - The property key to remove
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```typescript
|
|
635
|
+
* kitbase.unregister('platform');
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
unregister(key) {
|
|
639
|
+
if (key in this.superProperties) {
|
|
640
|
+
delete this.superProperties[key];
|
|
641
|
+
this.log("Super property removed", { key });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get all registered super properties
|
|
646
|
+
*
|
|
647
|
+
* @returns A copy of the current super properties
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```typescript
|
|
651
|
+
* const props = kitbase.getSuperProperties();
|
|
652
|
+
* console.log(props); // { app_version: '2.1.0', platform: 'web' }
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
getSuperProperties() {
|
|
656
|
+
return { ...this.superProperties };
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Clear all super properties
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* ```typescript
|
|
663
|
+
* kitbase.clearSuperProperties();
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
clearSuperProperties() {
|
|
667
|
+
this.superProperties = {};
|
|
668
|
+
this.log("Super properties cleared");
|
|
669
|
+
}
|
|
670
|
+
// ============================================================
|
|
671
|
+
// Time Events (Duration Tracking)
|
|
672
|
+
// ============================================================
|
|
673
|
+
/**
|
|
674
|
+
* Start timing an event
|
|
675
|
+
* When the same event is tracked later, a $duration property (in seconds)
|
|
676
|
+
* will automatically be included
|
|
677
|
+
*
|
|
678
|
+
* @param eventName - The name of the event to time
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* ```typescript
|
|
682
|
+
* kitbase.timeEvent('Video Watched');
|
|
683
|
+
* // ... user watches video ...
|
|
684
|
+
* await kitbase.track({
|
|
685
|
+
* channel: 'engagement',
|
|
686
|
+
* event: 'Video Watched',
|
|
687
|
+
* tags: { video_id: '123' }
|
|
688
|
+
* });
|
|
689
|
+
* // Event will include $duration: 45.2 (seconds)
|
|
690
|
+
* ```
|
|
691
|
+
*/
|
|
692
|
+
timeEvent(eventName) {
|
|
693
|
+
this.timedEvents.set(eventName, Date.now());
|
|
694
|
+
this.log("Timer started", { event: eventName });
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Cancel a timed event without tracking it
|
|
698
|
+
*
|
|
699
|
+
* @param eventName - The name of the event to cancel timing for
|
|
700
|
+
*
|
|
701
|
+
* @example
|
|
702
|
+
* ```typescript
|
|
703
|
+
* kitbase.timeEvent('Checkout Flow');
|
|
704
|
+
* // User abandons checkout
|
|
705
|
+
* kitbase.cancelTimeEvent('Checkout Flow');
|
|
706
|
+
* ```
|
|
707
|
+
*/
|
|
708
|
+
cancelTimeEvent(eventName) {
|
|
709
|
+
if (this.timedEvents.has(eventName)) {
|
|
710
|
+
this.timedEvents.delete(eventName);
|
|
711
|
+
this.log("Timer cancelled", { event: eventName });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Get all currently timed events
|
|
716
|
+
*
|
|
717
|
+
* @returns Array of event names that are currently being timed
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```typescript
|
|
721
|
+
* const timedEvents = kitbase.getTimedEvents();
|
|
722
|
+
* console.log(timedEvents); // ['Video Watched', 'Checkout Flow']
|
|
723
|
+
* ```
|
|
724
|
+
*/
|
|
725
|
+
getTimedEvents() {
|
|
726
|
+
return Array.from(this.timedEvents.keys());
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Get the duration of a timed event (without stopping it)
|
|
730
|
+
* @internal
|
|
731
|
+
*
|
|
732
|
+
* @param eventName - The name of the event
|
|
733
|
+
* @returns Duration in seconds, or null if not being timed
|
|
734
|
+
*/
|
|
735
|
+
getEventDuration(eventName) {
|
|
736
|
+
const startTime = this.timedEvents.get(eventName);
|
|
737
|
+
if (startTime === void 0) return null;
|
|
738
|
+
return (Date.now() - startTime) / 1e3;
|
|
739
|
+
}
|
|
740
|
+
// ============================================================
|
|
741
|
+
// Bot Detection
|
|
742
|
+
// ============================================================
|
|
743
|
+
/**
|
|
744
|
+
* Check if the current visitor is detected as a bot
|
|
745
|
+
*
|
|
746
|
+
* @internal This is an internal API and may change without notice.
|
|
747
|
+
* @returns true if bot detected, false otherwise
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* if (kitbase.isBot()) {
|
|
752
|
+
* console.log('Bot detected, tracking disabled');
|
|
753
|
+
* }
|
|
754
|
+
* ```
|
|
755
|
+
*/
|
|
756
|
+
isBot() {
|
|
757
|
+
if (!this.botDetectionConfig?.enabled) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
if (!this.botDetectionResult) {
|
|
761
|
+
this.botDetectionResult = detectBot(this.botDetectionConfig);
|
|
762
|
+
}
|
|
763
|
+
return this.botDetectionResult.isBot;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Get detailed bot detection result
|
|
767
|
+
*
|
|
768
|
+
* @internal This is an internal API and may change without notice.
|
|
769
|
+
* @returns Bot detection result with detailed checks, or null if detection not enabled
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```typescript
|
|
773
|
+
* const result = kitbase.getBotDetectionResult();
|
|
774
|
+
* if (result?.isBot) {
|
|
775
|
+
* console.log('Bot detected:', result.reason);
|
|
776
|
+
* console.log('Checks:', result.checks);
|
|
777
|
+
* }
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
getBotDetectionResult() {
|
|
781
|
+
if (!this.botDetectionConfig.enabled) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
if (!this.botDetectionResult) {
|
|
785
|
+
this.botDetectionResult = detectBot(this.botDetectionConfig);
|
|
786
|
+
}
|
|
787
|
+
return this.botDetectionResult;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Force re-run bot detection
|
|
791
|
+
* Useful if you want to check again after page state changes
|
|
792
|
+
*
|
|
793
|
+
* @internal This is an internal API and may change without notice.
|
|
794
|
+
* @returns Updated bot detection result
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```typescript
|
|
798
|
+
* const result = kitbase.redetectBot();
|
|
799
|
+
* console.log('Is bot:', result.isBot);
|
|
800
|
+
* ```
|
|
801
|
+
*/
|
|
802
|
+
redetectBot() {
|
|
803
|
+
this.botDetectionResult = detectBot(this.botDetectionConfig);
|
|
804
|
+
this.log("Bot detection re-run", {
|
|
805
|
+
isBot: this.botDetectionResult.isBot,
|
|
806
|
+
reason: this.botDetectionResult.reason
|
|
807
|
+
});
|
|
808
|
+
return this.botDetectionResult;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Check if bot blocking is currently active
|
|
812
|
+
* When bot detection is enabled and a bot is detected, all events are blocked.
|
|
813
|
+
*
|
|
814
|
+
* @internal This is an internal API and may change without notice.
|
|
815
|
+
* @returns true if bots are being blocked from tracking
|
|
816
|
+
*/
|
|
817
|
+
isBotBlockingActive() {
|
|
818
|
+
return this.botDetectionConfig?.enabled === true && this.isBot();
|
|
819
|
+
}
|
|
820
|
+
// ============================================================
|
|
821
|
+
// Client Session Tracking
|
|
822
|
+
// ============================================================
|
|
823
|
+
/**
|
|
824
|
+
* Get or create a client-side session ID.
|
|
825
|
+
* Rotates the session after 30 minutes of inactivity.
|
|
826
|
+
* @internal
|
|
827
|
+
*/
|
|
828
|
+
getClientSessionId() {
|
|
829
|
+
const now = Date.now();
|
|
830
|
+
if (!this.clientSessionId || this.lastActivityAt > 0 && now - this.lastActivityAt > _KitbaseAnalytics.SESSION_TIMEOUT_MS) {
|
|
831
|
+
this.clientSessionId = _KitbaseAnalytics.generateUUID();
|
|
832
|
+
this.log("New client session started", { sessionId: this.clientSessionId });
|
|
833
|
+
}
|
|
834
|
+
this.lastActivityAt = now;
|
|
835
|
+
return this.clientSessionId;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Generate a UUID v4, with fallback for environments where
|
|
839
|
+
* crypto.randomUUID() is not available (older WebViews, Ionic).
|
|
840
|
+
*/
|
|
841
|
+
static generateUUID() {
|
|
842
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
843
|
+
return crypto.randomUUID();
|
|
844
|
+
}
|
|
845
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
846
|
+
const bytes = new Uint8Array(16);
|
|
847
|
+
crypto.getRandomValues(bytes);
|
|
848
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
849
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
850
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
851
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
852
|
+
}
|
|
853
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
854
|
+
const r = Math.random() * 16 | 0;
|
|
855
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
856
|
+
return v.toString(16);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
// ============================================================
|
|
860
|
+
// Track Event
|
|
861
|
+
// ============================================================
|
|
862
|
+
/**
|
|
863
|
+
* Track an event
|
|
864
|
+
*
|
|
865
|
+
* Events are sent directly to the API. For offline queueing support,
|
|
866
|
+
* use the full Kitbase client instead.
|
|
867
|
+
*
|
|
868
|
+
* @param options - Event tracking options
|
|
869
|
+
* @returns Promise resolving to the track response, or void if tracking is blocked
|
|
870
|
+
* @throws {ValidationError} When required fields are missing
|
|
871
|
+
* @throws {AuthenticationError} When the API key is invalid
|
|
872
|
+
* @throws {ApiError} When the API returns an error
|
|
873
|
+
* @throws {TimeoutError} When the request times out
|
|
874
|
+
*/
|
|
875
|
+
async track(options) {
|
|
876
|
+
this.validateTrackOptions(options);
|
|
877
|
+
if (this.isBotBlockingActive()) {
|
|
878
|
+
this.log("Event skipped - bot detected", { event: options.event });
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
let duration;
|
|
882
|
+
const startTime = this.timedEvents.get(options.event);
|
|
883
|
+
if (startTime !== void 0) {
|
|
884
|
+
duration = (Date.now() - startTime) / 1e3;
|
|
885
|
+
this.timedEvents.delete(options.event);
|
|
886
|
+
this.log("Timer stopped", { event: options.event, duration });
|
|
887
|
+
}
|
|
888
|
+
const mergedTags = {
|
|
889
|
+
...this.superProperties,
|
|
890
|
+
...options.tags ?? {},
|
|
891
|
+
...duration !== void 0 ? { $duration: duration } : {}
|
|
892
|
+
};
|
|
893
|
+
const payload = {
|
|
894
|
+
channel: options.channel,
|
|
895
|
+
event: options.event,
|
|
896
|
+
client_timestamp: Date.now(),
|
|
897
|
+
client_session_id: this.getClientSessionId(),
|
|
898
|
+
...options.user_id && { user_id: options.user_id },
|
|
899
|
+
...options.icon && { icon: options.icon },
|
|
900
|
+
...options.notify !== void 0 && { notify: options.notify },
|
|
901
|
+
...options.description && { description: options.description },
|
|
902
|
+
...Object.keys(mergedTags).length > 0 && { tags: mergedTags }
|
|
903
|
+
};
|
|
904
|
+
this.log("Track", { event: options.event, payload });
|
|
905
|
+
const response = await this.sendRequest("/sdk/v1/logs", payload);
|
|
906
|
+
this.log("Event sent successfully", { id: response.id });
|
|
907
|
+
return response;
|
|
908
|
+
}
|
|
909
|
+
validateTrackOptions(options) {
|
|
910
|
+
if (!options.event) {
|
|
911
|
+
throw new ValidationError("Event is required", "event");
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Send a request to the API
|
|
916
|
+
*/
|
|
917
|
+
async sendRequest(endpoint, body) {
|
|
918
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
919
|
+
const controller = new AbortController();
|
|
920
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
|
921
|
+
try {
|
|
922
|
+
const response = await fetch(url, {
|
|
923
|
+
method: "POST",
|
|
924
|
+
headers: {
|
|
925
|
+
"Content-Type": "application/json",
|
|
926
|
+
"x-sdk-key": `${this.sdkKey}`
|
|
927
|
+
},
|
|
928
|
+
body: JSON.stringify(body),
|
|
929
|
+
signal: controller.signal
|
|
930
|
+
});
|
|
931
|
+
clearTimeout(timeoutId);
|
|
932
|
+
if (!response.ok) {
|
|
933
|
+
const errorBody = await this.parseResponseBody(response);
|
|
934
|
+
if (response.status === 401) {
|
|
935
|
+
throw new AuthenticationError();
|
|
936
|
+
}
|
|
937
|
+
throw new ApiError(
|
|
938
|
+
this.getErrorMessage(errorBody, response.statusText),
|
|
939
|
+
response.status,
|
|
940
|
+
errorBody
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return await response.json();
|
|
944
|
+
} catch (error) {
|
|
945
|
+
clearTimeout(timeoutId);
|
|
946
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
947
|
+
throw new TimeoutError();
|
|
948
|
+
}
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async parseResponseBody(response) {
|
|
953
|
+
try {
|
|
954
|
+
return await response.json();
|
|
955
|
+
} catch {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
getErrorMessage(body, fallback) {
|
|
960
|
+
if (body && typeof body === "object" && "message" in body) {
|
|
961
|
+
return String(body.message);
|
|
962
|
+
}
|
|
963
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
964
|
+
return String(body.error);
|
|
965
|
+
}
|
|
966
|
+
return fallback;
|
|
967
|
+
}
|
|
968
|
+
// ============================================================
|
|
969
|
+
// Analytics — Stub methods (overridden by plugins via methods)
|
|
970
|
+
// ============================================================
|
|
971
|
+
/**
|
|
972
|
+
* Track a page view
|
|
973
|
+
*
|
|
974
|
+
* @param options - Page view options
|
|
975
|
+
* @returns Promise resolving to the track response
|
|
976
|
+
*
|
|
977
|
+
* @example
|
|
978
|
+
* ```typescript
|
|
979
|
+
* // Track current page
|
|
980
|
+
* await kitbase.trackPageView();
|
|
981
|
+
*
|
|
982
|
+
* // Track with custom path
|
|
983
|
+
* await kitbase.trackPageView({ path: '/products/123', title: 'Product Details' });
|
|
984
|
+
* ```
|
|
985
|
+
*/
|
|
986
|
+
async trackPageView(options) {
|
|
987
|
+
this.log("trackPageView() called but page-view plugin is not registered");
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Track a click on an interactive element
|
|
991
|
+
*/
|
|
992
|
+
async trackClick(tags) {
|
|
993
|
+
this.log("trackClick() called but click-tracking plugin is not registered");
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Track an outbound link click
|
|
997
|
+
*
|
|
998
|
+
* @param options - Outbound link options
|
|
999
|
+
* @returns Promise resolving to the track response
|
|
1000
|
+
*
|
|
1001
|
+
* @example
|
|
1002
|
+
* ```typescript
|
|
1003
|
+
* await kitbase.trackOutboundLink({
|
|
1004
|
+
* url: 'https://example.com',
|
|
1005
|
+
* text: 'Visit Example',
|
|
1006
|
+
* });
|
|
1007
|
+
* ```
|
|
1008
|
+
*/
|
|
1009
|
+
async trackOutboundLink(options) {
|
|
1010
|
+
this.log("trackOutboundLink() called but outbound-links plugin is not registered");
|
|
1011
|
+
}
|
|
1012
|
+
// ============================================================
|
|
1013
|
+
// Analytics — Revenue & Identity (non-plugin)
|
|
1014
|
+
// ============================================================
|
|
1015
|
+
/**
|
|
1016
|
+
* Track a revenue event
|
|
1017
|
+
*
|
|
1018
|
+
* @param options - Revenue options
|
|
1019
|
+
* @returns Promise resolving to the track response
|
|
1020
|
+
*
|
|
1021
|
+
* @example
|
|
1022
|
+
* ```typescript
|
|
1023
|
+
* // Track a $19.99 purchase
|
|
1024
|
+
* await kitbase.trackRevenue({
|
|
1025
|
+
* amount: 1999,
|
|
1026
|
+
* currency: 'USD',
|
|
1027
|
+
* tags: { product_id: 'prod_123', plan: 'premium' },
|
|
1028
|
+
* });
|
|
1029
|
+
* ```
|
|
1030
|
+
*/
|
|
1031
|
+
async trackRevenue(options) {
|
|
1032
|
+
return this.track({
|
|
1033
|
+
channel: ANALYTICS_CHANNEL,
|
|
1034
|
+
event: "revenue",
|
|
1035
|
+
user_id: options.user_id ?? this.userId ?? void 0,
|
|
1036
|
+
tags: {
|
|
1037
|
+
__revenue: options.amount,
|
|
1038
|
+
__currency: options.currency ?? "USD",
|
|
1039
|
+
...options.tags ?? {}
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Identify a user
|
|
1045
|
+
* Sets the user identity on the server.
|
|
1046
|
+
* Call this when a user signs up or logs in.
|
|
1047
|
+
*
|
|
1048
|
+
* @param options - Identify options
|
|
1049
|
+
* @returns Promise that resolves when the identity is set
|
|
1050
|
+
*
|
|
1051
|
+
* @example
|
|
1052
|
+
* ```typescript
|
|
1053
|
+
* await kitbase.identify({
|
|
1054
|
+
* userId: 'user_123',
|
|
1055
|
+
* traits: { email: 'user@example.com', plan: 'premium' },
|
|
1056
|
+
* });
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
async identify(options) {
|
|
1060
|
+
this.userId = options.userId;
|
|
1061
|
+
if (options.traits) {
|
|
1062
|
+
this.register({
|
|
1063
|
+
__user_id: options.userId,
|
|
1064
|
+
...options.traits
|
|
1065
|
+
});
|
|
1066
|
+
} else {
|
|
1067
|
+
this.register({ __user_id: options.userId });
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
const response = await fetch(`${this.baseUrl}/sdk/v1/identify`, {
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
headers: {
|
|
1073
|
+
"Content-Type": "application/json",
|
|
1074
|
+
"x-sdk-key": this.sdkKey
|
|
1075
|
+
},
|
|
1076
|
+
body: JSON.stringify({
|
|
1077
|
+
user_id: options.userId,
|
|
1078
|
+
traits: options.traits
|
|
1079
|
+
})
|
|
1080
|
+
});
|
|
1081
|
+
if (!response.ok) {
|
|
1082
|
+
this.log("Identify API call failed", { status: response.status });
|
|
1083
|
+
} else {
|
|
1084
|
+
this.log("Identity set on server", {
|
|
1085
|
+
userId: options.userId
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
this.log("Failed to call identify endpoint", err);
|
|
1090
|
+
}
|
|
1091
|
+
this.log("User identified", { userId: options.userId });
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Get the current user ID (set via identify)
|
|
1095
|
+
*/
|
|
1096
|
+
getUserId() {
|
|
1097
|
+
return this.userId;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Reset the user identity
|
|
1101
|
+
* Call this when a user logs out
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```typescript
|
|
1105
|
+
* kitbase.reset();
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
reset() {
|
|
1109
|
+
this.userId = null;
|
|
1110
|
+
this.clientSessionId = null;
|
|
1111
|
+
this.lastActivityAt = 0;
|
|
1112
|
+
this.clearSuperProperties();
|
|
1113
|
+
this.log("User reset complete");
|
|
1114
|
+
}
|
|
1115
|
+
// ============================================================
|
|
1116
|
+
// Cleanup
|
|
1117
|
+
// ============================================================
|
|
1118
|
+
/**
|
|
1119
|
+
* Shutdown the client and cleanup resources
|
|
1120
|
+
* Call this when you're done using the client to stop timers and close connections
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* kitbase.shutdown();
|
|
1125
|
+
* ```
|
|
1126
|
+
*/
|
|
1127
|
+
shutdown() {
|
|
1128
|
+
this.log("Shutting down");
|
|
1129
|
+
this.timedEvents.clear();
|
|
1130
|
+
const pluginNames = Array.from(this._plugins.keys()).reverse();
|
|
1131
|
+
for (const name of pluginNames) {
|
|
1132
|
+
const plugin = this._plugins.get(name);
|
|
1133
|
+
try {
|
|
1134
|
+
plugin.teardown();
|
|
1135
|
+
this.log(`Plugin "${name}" torn down`);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
this.log(`Plugin "${name}" teardown failed`, err);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
this._plugins.clear();
|
|
1141
|
+
this._pluginContext = null;
|
|
1142
|
+
this.log("Shutdown complete");
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/queue/index.ts
|
|
1147
|
+
var import_dexie = __toESM(require("dexie"), 1);
|
|
1148
|
+
var DEFAULT_CONFIG = {
|
|
1149
|
+
enabled: false,
|
|
1150
|
+
maxQueueSize: 1e3,
|
|
1151
|
+
flushInterval: 3e4,
|
|
1152
|
+
flushBatchSize: 50,
|
|
1153
|
+
maxRetries: 3,
|
|
1154
|
+
retryBaseDelay: 1e3
|
|
1155
|
+
};
|
|
1156
|
+
function isIndexedDBAvailable() {
|
|
1157
|
+
try {
|
|
1158
|
+
return typeof window !== "undefined" && typeof window.indexedDB !== "undefined" && window.indexedDB !== null;
|
|
1159
|
+
} catch {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function isBrowser2() {
|
|
1164
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
1165
|
+
}
|
|
1166
|
+
var KitbaseQueueDB = class extends import_dexie.default {
|
|
1167
|
+
events;
|
|
1168
|
+
constructor(dbName) {
|
|
1169
|
+
super(dbName);
|
|
1170
|
+
this.version(1).stores({
|
|
1171
|
+
events: "++id, timestamp, retries, lastAttempt"
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
var MemoryQueue = class {
|
|
1176
|
+
queue = [];
|
|
1177
|
+
idCounter = 1;
|
|
1178
|
+
async enqueue(payload) {
|
|
1179
|
+
const event = {
|
|
1180
|
+
id: this.idCounter++,
|
|
1181
|
+
payload,
|
|
1182
|
+
timestamp: Date.now(),
|
|
1183
|
+
retries: 0
|
|
1184
|
+
};
|
|
1185
|
+
this.queue.push(event);
|
|
1186
|
+
return event.id;
|
|
1187
|
+
}
|
|
1188
|
+
async dequeue(count) {
|
|
1189
|
+
this.queue.sort((a, b) => a.timestamp - b.timestamp);
|
|
1190
|
+
return this.queue.slice(0, count);
|
|
1191
|
+
}
|
|
1192
|
+
async delete(ids) {
|
|
1193
|
+
this.queue = this.queue.filter((e) => !ids.includes(e.id));
|
|
1194
|
+
}
|
|
1195
|
+
async updateRetries(ids) {
|
|
1196
|
+
const now = Date.now();
|
|
1197
|
+
for (const event of this.queue) {
|
|
1198
|
+
if (ids.includes(event.id)) {
|
|
1199
|
+
event.retries++;
|
|
1200
|
+
event.lastAttempt = now;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async getStats() {
|
|
1205
|
+
const size = this.queue.length;
|
|
1206
|
+
const oldestEvent = size > 0 ? Math.min(...this.queue.map((e) => e.timestamp)) : void 0;
|
|
1207
|
+
return { size, oldestEvent };
|
|
1208
|
+
}
|
|
1209
|
+
async clear() {
|
|
1210
|
+
this.queue = [];
|
|
1211
|
+
}
|
|
1212
|
+
async enforceMaxSize(maxSize) {
|
|
1213
|
+
if (this.queue.length > maxSize) {
|
|
1214
|
+
this.queue.sort((a, b) => a.timestamp - b.timestamp);
|
|
1215
|
+
this.queue = this.queue.slice(-maxSize);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async getEventsExceedingRetries(maxRetries) {
|
|
1219
|
+
return this.queue.filter((e) => e.retries >= maxRetries).map((e) => e.id);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
var EventQueue = class {
|
|
1223
|
+
config;
|
|
1224
|
+
dbName;
|
|
1225
|
+
db = null;
|
|
1226
|
+
memoryQueue = null;
|
|
1227
|
+
flushTimer = null;
|
|
1228
|
+
isFlushing = false;
|
|
1229
|
+
sendEvents = null;
|
|
1230
|
+
useIndexedDB;
|
|
1231
|
+
debugMode = false;
|
|
1232
|
+
debugLogger = null;
|
|
1233
|
+
constructor(config = {}, dbName = "_ka_events") {
|
|
1234
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1235
|
+
this.dbName = dbName;
|
|
1236
|
+
this.useIndexedDB = isIndexedDBAvailable();
|
|
1237
|
+
if (this.useIndexedDB) {
|
|
1238
|
+
this.db = new KitbaseQueueDB(this.dbName);
|
|
1239
|
+
} else {
|
|
1240
|
+
this.memoryQueue = new MemoryQueue();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Set debug mode and logger
|
|
1245
|
+
*/
|
|
1246
|
+
setDebugMode(enabled, logger) {
|
|
1247
|
+
this.debugMode = enabled;
|
|
1248
|
+
this.debugLogger = logger ?? null;
|
|
1249
|
+
}
|
|
1250
|
+
log(message, data) {
|
|
1251
|
+
if (this.debugMode && this.debugLogger) {
|
|
1252
|
+
this.debugLogger(message, data);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Set the callback for sending events
|
|
1257
|
+
*/
|
|
1258
|
+
setSendCallback(callback) {
|
|
1259
|
+
this.sendEvents = callback;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Check if the queue storage is available
|
|
1263
|
+
*/
|
|
1264
|
+
isAvailable() {
|
|
1265
|
+
return this.useIndexedDB || this.memoryQueue !== null;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Get the storage type being used
|
|
1269
|
+
*/
|
|
1270
|
+
getStorageType() {
|
|
1271
|
+
return this.useIndexedDB ? "indexeddb" : "memory";
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Add an event to the queue
|
|
1275
|
+
*/
|
|
1276
|
+
async enqueue(payload) {
|
|
1277
|
+
const event = {
|
|
1278
|
+
payload,
|
|
1279
|
+
timestamp: Date.now(),
|
|
1280
|
+
retries: 0
|
|
1281
|
+
};
|
|
1282
|
+
if (this.useIndexedDB && this.db) {
|
|
1283
|
+
await this.db.events.add(event);
|
|
1284
|
+
this.log("Event queued to IndexedDB", payload);
|
|
1285
|
+
} else if (this.memoryQueue) {
|
|
1286
|
+
await this.memoryQueue.enqueue(payload);
|
|
1287
|
+
this.log("Event queued to memory", payload);
|
|
1288
|
+
}
|
|
1289
|
+
await this.enforceMaxQueueSize();
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get and remove the next batch of events to send
|
|
1293
|
+
*/
|
|
1294
|
+
async dequeue(count) {
|
|
1295
|
+
if (this.useIndexedDB && this.db) {
|
|
1296
|
+
return this.db.events.where("retries").below(this.config.maxRetries).sortBy("timestamp").then((events) => events.slice(0, count));
|
|
1297
|
+
} else if (this.memoryQueue) {
|
|
1298
|
+
const events = await this.memoryQueue.dequeue(count);
|
|
1299
|
+
return events.filter((e) => e.retries < this.config.maxRetries);
|
|
1300
|
+
}
|
|
1301
|
+
return [];
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Mark events as successfully sent (remove from queue)
|
|
1305
|
+
*/
|
|
1306
|
+
async markSent(ids) {
|
|
1307
|
+
if (ids.length === 0) return;
|
|
1308
|
+
if (this.useIndexedDB && this.db) {
|
|
1309
|
+
await this.db.events.bulkDelete(ids);
|
|
1310
|
+
this.log(`Removed ${ids.length} sent events from queue`);
|
|
1311
|
+
} else if (this.memoryQueue) {
|
|
1312
|
+
await this.memoryQueue.delete(ids);
|
|
1313
|
+
this.log(`Removed ${ids.length} sent events from memory queue`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Mark events as failed and increment retry count
|
|
1318
|
+
*/
|
|
1319
|
+
async markFailed(ids) {
|
|
1320
|
+
if (ids.length === 0) return;
|
|
1321
|
+
const now = Date.now();
|
|
1322
|
+
if (this.useIndexedDB && this.db) {
|
|
1323
|
+
await this.db.transaction("rw", this.db.events, async () => {
|
|
1324
|
+
for (const id of ids) {
|
|
1325
|
+
const event = await this.db.events.get(id);
|
|
1326
|
+
if (event) {
|
|
1327
|
+
await this.db.events.update(id, {
|
|
1328
|
+
retries: event.retries + 1,
|
|
1329
|
+
lastAttempt: now
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
this.log(`Marked ${ids.length} events as failed`);
|
|
1335
|
+
} else if (this.memoryQueue) {
|
|
1336
|
+
await this.memoryQueue.updateRetries(ids);
|
|
1337
|
+
this.log(`Marked ${ids.length} events as failed in memory queue`);
|
|
1338
|
+
}
|
|
1339
|
+
await this.removeExpiredRetries();
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Remove events that have exceeded max retry attempts
|
|
1343
|
+
*/
|
|
1344
|
+
async removeExpiredRetries() {
|
|
1345
|
+
if (this.useIndexedDB && this.db) {
|
|
1346
|
+
const expiredIds = await this.db.events.where("retries").aboveOrEqual(this.config.maxRetries).primaryKeys();
|
|
1347
|
+
if (expiredIds.length > 0) {
|
|
1348
|
+
await this.db.events.bulkDelete(expiredIds);
|
|
1349
|
+
this.log(`Removed ${expiredIds.length} events that exceeded max retries`);
|
|
1350
|
+
}
|
|
1351
|
+
} else if (this.memoryQueue) {
|
|
1352
|
+
const expiredIds = await this.memoryQueue.getEventsExceedingRetries(
|
|
1353
|
+
this.config.maxRetries
|
|
1354
|
+
);
|
|
1355
|
+
if (expiredIds.length > 0) {
|
|
1356
|
+
await this.memoryQueue.delete(expiredIds);
|
|
1357
|
+
this.log(`Removed ${expiredIds.length} events that exceeded max retries`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Enforce the maximum queue size by removing oldest events
|
|
1363
|
+
*/
|
|
1364
|
+
async enforceMaxQueueSize() {
|
|
1365
|
+
if (this.useIndexedDB && this.db) {
|
|
1366
|
+
const count = await this.db.events.count();
|
|
1367
|
+
if (count > this.config.maxQueueSize) {
|
|
1368
|
+
const excess = count - this.config.maxQueueSize;
|
|
1369
|
+
const oldestEvents = await this.db.events.orderBy("timestamp").limit(excess).primaryKeys();
|
|
1370
|
+
await this.db.events.bulkDelete(oldestEvents);
|
|
1371
|
+
this.log(`Removed ${excess} oldest events to enforce queue size limit`);
|
|
1372
|
+
}
|
|
1373
|
+
} else if (this.memoryQueue) {
|
|
1374
|
+
await this.memoryQueue.enforceMaxSize(this.config.maxQueueSize);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Get queue statistics
|
|
1379
|
+
*/
|
|
1380
|
+
async getStats() {
|
|
1381
|
+
if (this.useIndexedDB && this.db) {
|
|
1382
|
+
const size = await this.db.events.count();
|
|
1383
|
+
const oldestEvent = await this.db.events.orderBy("timestamp").first().then((e) => e?.timestamp);
|
|
1384
|
+
return { size, oldestEvent, isFlushing: this.isFlushing };
|
|
1385
|
+
} else if (this.memoryQueue) {
|
|
1386
|
+
const stats = await this.memoryQueue.getStats();
|
|
1387
|
+
return { ...stats, isFlushing: this.isFlushing };
|
|
1388
|
+
}
|
|
1389
|
+
return { size: 0, isFlushing: this.isFlushing };
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Clear all events from the queue
|
|
1393
|
+
*/
|
|
1394
|
+
async clear() {
|
|
1395
|
+
if (this.useIndexedDB && this.db) {
|
|
1396
|
+
await this.db.events.clear();
|
|
1397
|
+
this.log("Queue cleared (IndexedDB)");
|
|
1398
|
+
} else if (this.memoryQueue) {
|
|
1399
|
+
await this.memoryQueue.clear();
|
|
1400
|
+
this.log("Queue cleared (memory)");
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Start the automatic flush timer
|
|
1405
|
+
*/
|
|
1406
|
+
startFlushTimer() {
|
|
1407
|
+
if (this.flushTimer) return;
|
|
1408
|
+
this.flushTimer = setInterval(() => {
|
|
1409
|
+
this.flush().catch((err) => {
|
|
1410
|
+
this.log("Flush timer error", err);
|
|
1411
|
+
});
|
|
1412
|
+
}, this.config.flushInterval);
|
|
1413
|
+
if (isBrowser2()) {
|
|
1414
|
+
window.addEventListener("online", this.handleOnline);
|
|
1415
|
+
}
|
|
1416
|
+
this.log(`Flush timer started (interval: ${this.config.flushInterval}ms)`);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Stop the automatic flush timer
|
|
1420
|
+
*/
|
|
1421
|
+
stopFlushTimer() {
|
|
1422
|
+
if (this.flushTimer) {
|
|
1423
|
+
clearInterval(this.flushTimer);
|
|
1424
|
+
this.flushTimer = null;
|
|
1425
|
+
}
|
|
1426
|
+
if (isBrowser2()) {
|
|
1427
|
+
window.removeEventListener("online", this.handleOnline);
|
|
1428
|
+
}
|
|
1429
|
+
this.log("Flush timer stopped");
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Handle coming back online
|
|
1433
|
+
*/
|
|
1434
|
+
handleOnline = () => {
|
|
1435
|
+
this.log("Browser came online, triggering flush");
|
|
1436
|
+
this.flush().catch((err) => {
|
|
1437
|
+
this.log("Online flush error", err);
|
|
1438
|
+
});
|
|
1439
|
+
};
|
|
1440
|
+
/**
|
|
1441
|
+
* Check if we're currently online
|
|
1442
|
+
*/
|
|
1443
|
+
isOnline() {
|
|
1444
|
+
if (isBrowser2()) {
|
|
1445
|
+
return navigator.onLine;
|
|
1446
|
+
}
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Manually trigger a flush of queued events
|
|
1451
|
+
*/
|
|
1452
|
+
async flush() {
|
|
1453
|
+
if (this.isFlushing) {
|
|
1454
|
+
this.log("Flush already in progress, skipping");
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (!this.isOnline()) {
|
|
1458
|
+
this.log("Offline, skipping flush");
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (!this.sendEvents) {
|
|
1462
|
+
this.log("No send callback configured, skipping flush");
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
this.isFlushing = true;
|
|
1466
|
+
try {
|
|
1467
|
+
const stats = await this.getStats();
|
|
1468
|
+
if (stats.size === 0) {
|
|
1469
|
+
this.log("Queue is empty, nothing to flush");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
this.log(`Flushing queue (${stats.size} events)`);
|
|
1473
|
+
let processed = 0;
|
|
1474
|
+
while (true) {
|
|
1475
|
+
const events = await this.dequeue(this.config.flushBatchSize);
|
|
1476
|
+
if (events.length === 0) break;
|
|
1477
|
+
this.log(`Sending batch of ${events.length} events`);
|
|
1478
|
+
try {
|
|
1479
|
+
const sentIds = await this.sendEvents(events);
|
|
1480
|
+
await this.markSent(sentIds);
|
|
1481
|
+
const failedIds = events.filter((e) => !sentIds.includes(e.id)).map((e) => e.id);
|
|
1482
|
+
if (failedIds.length > 0) {
|
|
1483
|
+
await this.markFailed(failedIds);
|
|
1484
|
+
}
|
|
1485
|
+
processed += sentIds.length;
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
const allIds = events.map((e) => e.id);
|
|
1488
|
+
await this.markFailed(allIds);
|
|
1489
|
+
this.log("Batch send failed", error);
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
this.log(`Flush complete, sent ${processed} events`);
|
|
1494
|
+
} finally {
|
|
1495
|
+
this.isFlushing = false;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Close the database connection
|
|
1500
|
+
*/
|
|
1501
|
+
async close() {
|
|
1502
|
+
this.stopFlushTimer();
|
|
1503
|
+
if (this.db) {
|
|
1504
|
+
this.db.close();
|
|
1505
|
+
this.log("Database connection closed");
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
// src/plugins/page-view.ts
|
|
1511
|
+
var ANALYTICS_CHANNEL2 = "__analytics";
|
|
1512
|
+
var PageViewPlugin = class {
|
|
1513
|
+
name = "page-view";
|
|
1514
|
+
ctx;
|
|
1515
|
+
active = false;
|
|
1516
|
+
popstateListener = null;
|
|
1517
|
+
setup(ctx) {
|
|
1518
|
+
if (typeof window === "undefined") return false;
|
|
1519
|
+
this.ctx = ctx;
|
|
1520
|
+
this.active = true;
|
|
1521
|
+
Promise.resolve().then(() => {
|
|
1522
|
+
if (this.active) {
|
|
1523
|
+
this.trackPageView().catch((err) => ctx.log("Failed to track initial page view", err));
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
const originalPushState = history.pushState.bind(history);
|
|
1527
|
+
history.pushState = (...args) => {
|
|
1528
|
+
originalPushState(...args);
|
|
1529
|
+
if (this.active) {
|
|
1530
|
+
this.trackPageView().catch((err) => ctx.log("Failed to track page view (pushState)", err));
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
1534
|
+
history.replaceState = (...args) => {
|
|
1535
|
+
originalReplaceState(...args);
|
|
1536
|
+
};
|
|
1537
|
+
this.popstateListener = () => {
|
|
1538
|
+
if (this.active) {
|
|
1539
|
+
this.trackPageView().catch((err) => ctx.log("Failed to track page view (popstate)", err));
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
window.addEventListener("popstate", this.popstateListener);
|
|
1543
|
+
ctx.log("Auto page view tracking enabled");
|
|
1544
|
+
}
|
|
1545
|
+
teardown() {
|
|
1546
|
+
this.active = false;
|
|
1547
|
+
if (this.popstateListener) {
|
|
1548
|
+
window.removeEventListener("popstate", this.popstateListener);
|
|
1549
|
+
this.popstateListener = null;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
get methods() {
|
|
1553
|
+
return {
|
|
1554
|
+
trackPageView: (options) => this.trackPageView(options)
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
async trackPageView(options = {}) {
|
|
1558
|
+
const path = options.path ?? (typeof window !== "undefined" ? window.location.pathname : "");
|
|
1559
|
+
const title = options.title ?? (typeof document !== "undefined" ? document.title : "");
|
|
1560
|
+
const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
|
|
1561
|
+
return this.ctx.track({
|
|
1562
|
+
channel: ANALYTICS_CHANNEL2,
|
|
1563
|
+
event: "screen_view",
|
|
1564
|
+
tags: {
|
|
1565
|
+
__path: path,
|
|
1566
|
+
__title: title,
|
|
1567
|
+
__referrer: referrer,
|
|
1568
|
+
...getUtmParams(),
|
|
1569
|
+
...options.tags ?? {}
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
// src/plugins/outbound-links.ts
|
|
1576
|
+
var ANALYTICS_CHANNEL3 = "__analytics";
|
|
1577
|
+
var OutboundLinksPlugin = class {
|
|
1578
|
+
name = "outbound-links";
|
|
1579
|
+
ctx;
|
|
1580
|
+
clickListener = null;
|
|
1581
|
+
keydownListener = null;
|
|
1582
|
+
setup(ctx) {
|
|
1583
|
+
if (typeof window === "undefined") return false;
|
|
1584
|
+
this.ctx = ctx;
|
|
1585
|
+
this.clickListener = (event) => {
|
|
1586
|
+
const link = event.target?.closest?.("a");
|
|
1587
|
+
if (link) {
|
|
1588
|
+
this.handleLinkClick(link);
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
this.keydownListener = (event) => {
|
|
1592
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1593
|
+
const link = event.target?.closest?.("a");
|
|
1594
|
+
if (link) {
|
|
1595
|
+
this.handleLinkClick(link);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
document.addEventListener("click", this.clickListener);
|
|
1600
|
+
document.addEventListener("keydown", this.keydownListener);
|
|
1601
|
+
ctx.log("Outbound link tracking enabled");
|
|
1602
|
+
}
|
|
1603
|
+
teardown() {
|
|
1604
|
+
if (this.clickListener) {
|
|
1605
|
+
document.removeEventListener("click", this.clickListener);
|
|
1606
|
+
this.clickListener = null;
|
|
1607
|
+
}
|
|
1608
|
+
if (this.keydownListener) {
|
|
1609
|
+
document.removeEventListener("keydown", this.keydownListener);
|
|
1610
|
+
this.keydownListener = null;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
get methods() {
|
|
1614
|
+
return {
|
|
1615
|
+
trackOutboundLink: (options) => this.trackOutboundLink(options)
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
handleLinkClick(link) {
|
|
1619
|
+
if (!link.href) return;
|
|
1620
|
+
try {
|
|
1621
|
+
const linkUrl = new URL(link.href);
|
|
1622
|
+
if (linkUrl.protocol !== "http:" && linkUrl.protocol !== "https:") {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const currentHost = window.location.hostname;
|
|
1626
|
+
const linkHost = linkUrl.hostname;
|
|
1627
|
+
if (linkHost === currentHost) return;
|
|
1628
|
+
if (this.ctx.isSameRootDomain(currentHost, linkHost)) return;
|
|
1629
|
+
this.trackOutboundLink({
|
|
1630
|
+
url: link.href,
|
|
1631
|
+
text: link.textContent?.trim() || ""
|
|
1632
|
+
}).catch((err) => this.ctx.log("Failed to track outbound link", err));
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
async trackOutboundLink(options) {
|
|
1637
|
+
return this.ctx.track({
|
|
1638
|
+
channel: ANALYTICS_CHANNEL3,
|
|
1639
|
+
event: "outbound_link",
|
|
1640
|
+
tags: {
|
|
1641
|
+
__url: options.url,
|
|
1642
|
+
__text: options.text || ""
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
// src/plugins/click-tracking.ts
|
|
1649
|
+
var ANALYTICS_CHANNEL4 = "__analytics";
|
|
1650
|
+
var ClickTrackingPlugin = class {
|
|
1651
|
+
name = "click-tracking";
|
|
1652
|
+
ctx;
|
|
1653
|
+
clickListener = null;
|
|
1654
|
+
setup(ctx) {
|
|
1655
|
+
if (typeof window === "undefined") return false;
|
|
1656
|
+
this.ctx = ctx;
|
|
1657
|
+
this.clickListener = (event) => {
|
|
1658
|
+
const target = event.target;
|
|
1659
|
+
const annotated = target?.closest?.("[data-kb-track-click]");
|
|
1660
|
+
if (annotated) {
|
|
1661
|
+
const eventName = annotated.getAttribute("data-kb-track-click");
|
|
1662
|
+
if (eventName) {
|
|
1663
|
+
const channel = annotated.getAttribute("data-kb-click-channel") || "engagement";
|
|
1664
|
+
ctx.track({
|
|
1665
|
+
channel,
|
|
1666
|
+
event: eventName,
|
|
1667
|
+
tags: {
|
|
1668
|
+
__path: window.location.pathname
|
|
1669
|
+
}
|
|
1670
|
+
}).catch((err) => ctx.log("Failed to track data-attribute click", err));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const element = ctx.findClickableElement(event);
|
|
1675
|
+
if (!element) return;
|
|
1676
|
+
if (ctx.config.autoTrackOutboundLinks !== false) {
|
|
1677
|
+
const elHref = element.href || element.getAttribute("href") || "";
|
|
1678
|
+
if (elHref) {
|
|
1679
|
+
try {
|
|
1680
|
+
const linkUrl = new URL(elHref, window.location.origin);
|
|
1681
|
+
if ((linkUrl.protocol === "http:" || linkUrl.protocol === "https:") && linkUrl.hostname !== window.location.hostname && !ctx.isSameRootDomain(window.location.hostname, linkUrl.hostname)) {
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
const tag = element.tagName.toLowerCase();
|
|
1689
|
+
const id = element.id || "";
|
|
1690
|
+
const className = element.className && typeof element.className === "string" ? element.className : "";
|
|
1691
|
+
const text = (element.textContent || "").trim().slice(0, 100);
|
|
1692
|
+
const href = element.href || element.getAttribute("href") || "";
|
|
1693
|
+
const path = window.location.pathname;
|
|
1694
|
+
this.trackClick({ __tag: tag, __id: id, __class: className, __text: text, __href: href, __path: path }).catch(
|
|
1695
|
+
(err) => ctx.log("Failed to track click", err)
|
|
1696
|
+
);
|
|
1697
|
+
};
|
|
1698
|
+
document.addEventListener("click", this.clickListener);
|
|
1699
|
+
ctx.log("Click tracking enabled");
|
|
1700
|
+
}
|
|
1701
|
+
teardown() {
|
|
1702
|
+
if (this.clickListener) {
|
|
1703
|
+
document.removeEventListener("click", this.clickListener);
|
|
1704
|
+
this.clickListener = null;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
get methods() {
|
|
1708
|
+
return {
|
|
1709
|
+
trackClick: (tags) => this.trackClick(tags)
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
async trackClick(tags) {
|
|
1713
|
+
return this.ctx.track({
|
|
1714
|
+
channel: ANALYTICS_CHANNEL4,
|
|
1715
|
+
event: "click",
|
|
1716
|
+
tags
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// src/plugins/scroll-depth.ts
|
|
1722
|
+
var ANALYTICS_CHANNEL5 = "__analytics";
|
|
1723
|
+
var ScrollDepthPlugin = class {
|
|
1724
|
+
name = "scroll-depth";
|
|
1725
|
+
ctx;
|
|
1726
|
+
active = false;
|
|
1727
|
+
maxScrollDepth = 0;
|
|
1728
|
+
scrollListener = null;
|
|
1729
|
+
beforeUnloadListener = null;
|
|
1730
|
+
popstateListener = null;
|
|
1731
|
+
scrollRafScheduled = false;
|
|
1732
|
+
setup(ctx) {
|
|
1733
|
+
if (typeof window === "undefined") return false;
|
|
1734
|
+
this.ctx = ctx;
|
|
1735
|
+
this.active = true;
|
|
1736
|
+
this.scrollListener = () => {
|
|
1737
|
+
if (this.scrollRafScheduled) return;
|
|
1738
|
+
this.scrollRafScheduled = true;
|
|
1739
|
+
requestAnimationFrame(() => {
|
|
1740
|
+
this.scrollRafScheduled = false;
|
|
1741
|
+
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
1742
|
+
const viewportHeight = window.innerHeight;
|
|
1743
|
+
const documentHeight = Math.max(
|
|
1744
|
+
document.body.scrollHeight,
|
|
1745
|
+
document.documentElement.scrollHeight
|
|
1746
|
+
);
|
|
1747
|
+
if (documentHeight <= 0) return;
|
|
1748
|
+
const depth = Math.min(100, Math.round((scrollTop + viewportHeight) / documentHeight * 100));
|
|
1749
|
+
if (depth > this.maxScrollDepth) {
|
|
1750
|
+
this.maxScrollDepth = depth;
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
};
|
|
1754
|
+
this.beforeUnloadListener = () => {
|
|
1755
|
+
this.flushScrollDepth();
|
|
1756
|
+
};
|
|
1757
|
+
window.addEventListener("scroll", this.scrollListener, { passive: true });
|
|
1758
|
+
window.addEventListener("beforeunload", this.beforeUnloadListener);
|
|
1759
|
+
const originalPushState = history.pushState;
|
|
1760
|
+
const self = this;
|
|
1761
|
+
history.pushState = function(...args) {
|
|
1762
|
+
if (self.active) self.flushScrollDepth();
|
|
1763
|
+
return originalPushState.apply(this, args);
|
|
1764
|
+
};
|
|
1765
|
+
this.popstateListener = () => {
|
|
1766
|
+
if (this.active) this.flushScrollDepth();
|
|
1767
|
+
};
|
|
1768
|
+
window.addEventListener("popstate", this.popstateListener);
|
|
1769
|
+
ctx.log("Scroll depth tracking enabled");
|
|
1770
|
+
}
|
|
1771
|
+
teardown() {
|
|
1772
|
+
this.active = false;
|
|
1773
|
+
this.flushScrollDepth();
|
|
1774
|
+
if (this.scrollListener) {
|
|
1775
|
+
window.removeEventListener("scroll", this.scrollListener);
|
|
1776
|
+
this.scrollListener = null;
|
|
1777
|
+
}
|
|
1778
|
+
if (this.beforeUnloadListener) {
|
|
1779
|
+
window.removeEventListener("beforeunload", this.beforeUnloadListener);
|
|
1780
|
+
this.beforeUnloadListener = null;
|
|
1781
|
+
}
|
|
1782
|
+
if (this.popstateListener) {
|
|
1783
|
+
window.removeEventListener("popstate", this.popstateListener);
|
|
1784
|
+
this.popstateListener = null;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
flushScrollDepth() {
|
|
1788
|
+
if (this.maxScrollDepth > 0) {
|
|
1789
|
+
const path = typeof window !== "undefined" ? window.location.pathname : "";
|
|
1790
|
+
this.ctx.track({
|
|
1791
|
+
channel: ANALYTICS_CHANNEL5,
|
|
1792
|
+
event: "scroll_depth",
|
|
1793
|
+
tags: {
|
|
1794
|
+
__depth: this.maxScrollDepth,
|
|
1795
|
+
__path: path
|
|
1796
|
+
}
|
|
1797
|
+
}).catch((err) => this.ctx.log("Failed to track scroll depth", err));
|
|
1798
|
+
this.maxScrollDepth = 0;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
// src/plugins/visibility.ts
|
|
1804
|
+
var VisibilityPlugin = class {
|
|
1805
|
+
name = "visibility";
|
|
1806
|
+
ctx;
|
|
1807
|
+
active = false;
|
|
1808
|
+
visibilityObservers = /* @__PURE__ */ new Map();
|
|
1809
|
+
visibilityMutationObserver = null;
|
|
1810
|
+
visibilityData = /* @__PURE__ */ new Map();
|
|
1811
|
+
beforeUnloadListener = null;
|
|
1812
|
+
popstateListener = null;
|
|
1813
|
+
setup(ctx) {
|
|
1814
|
+
if (typeof window === "undefined") return false;
|
|
1815
|
+
if (typeof IntersectionObserver === "undefined" || typeof MutationObserver === "undefined") return false;
|
|
1816
|
+
this.ctx = ctx;
|
|
1817
|
+
this.active = true;
|
|
1818
|
+
this.scanForVisibilityElements();
|
|
1819
|
+
this.visibilityMutationObserver = new MutationObserver((mutations) => {
|
|
1820
|
+
for (const mutation of mutations) {
|
|
1821
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
1822
|
+
if (node instanceof Element) {
|
|
1823
|
+
this.observeVisibilityElement(node);
|
|
1824
|
+
for (const el of Array.from(node.querySelectorAll("[data-kb-track-visibility]"))) {
|
|
1825
|
+
this.observeVisibilityElement(el);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
for (const node of Array.from(mutation.removedNodes)) {
|
|
1830
|
+
if (node instanceof Element) {
|
|
1831
|
+
this.flushVisibilityForElement(node);
|
|
1832
|
+
for (const el of Array.from(node.querySelectorAll("[data-kb-track-visibility]"))) {
|
|
1833
|
+
this.flushVisibilityForElement(el);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
this.visibilityMutationObserver.observe(document.body, {
|
|
1840
|
+
childList: true,
|
|
1841
|
+
subtree: true
|
|
1842
|
+
});
|
|
1843
|
+
this.beforeUnloadListener = () => {
|
|
1844
|
+
this.flushAllVisibilityEvents();
|
|
1845
|
+
};
|
|
1846
|
+
window.addEventListener("beforeunload", this.beforeUnloadListener);
|
|
1847
|
+
const originalPushState = history.pushState;
|
|
1848
|
+
const self = this;
|
|
1849
|
+
history.pushState = function(...args) {
|
|
1850
|
+
if (self.active) self.flushAllVisibilityEvents();
|
|
1851
|
+
return originalPushState.apply(this, args);
|
|
1852
|
+
};
|
|
1853
|
+
this.popstateListener = () => {
|
|
1854
|
+
if (this.active) this.flushAllVisibilityEvents();
|
|
1855
|
+
};
|
|
1856
|
+
window.addEventListener("popstate", this.popstateListener);
|
|
1857
|
+
ctx.log("Visibility tracking enabled");
|
|
1858
|
+
}
|
|
1859
|
+
teardown() {
|
|
1860
|
+
this.active = false;
|
|
1861
|
+
this.flushAllVisibilityEvents();
|
|
1862
|
+
for (const observer of this.visibilityObservers.values()) {
|
|
1863
|
+
observer.disconnect();
|
|
1864
|
+
}
|
|
1865
|
+
this.visibilityObservers.clear();
|
|
1866
|
+
if (this.visibilityMutationObserver) {
|
|
1867
|
+
this.visibilityMutationObserver.disconnect();
|
|
1868
|
+
this.visibilityMutationObserver = null;
|
|
1869
|
+
}
|
|
1870
|
+
if (this.beforeUnloadListener) {
|
|
1871
|
+
window.removeEventListener("beforeunload", this.beforeUnloadListener);
|
|
1872
|
+
this.beforeUnloadListener = null;
|
|
1873
|
+
}
|
|
1874
|
+
if (this.popstateListener) {
|
|
1875
|
+
window.removeEventListener("popstate", this.popstateListener);
|
|
1876
|
+
this.popstateListener = null;
|
|
1877
|
+
}
|
|
1878
|
+
this.visibilityData.clear();
|
|
1879
|
+
}
|
|
1880
|
+
scanForVisibilityElements() {
|
|
1881
|
+
for (const el of Array.from(document.querySelectorAll("[data-kb-track-visibility]"))) {
|
|
1882
|
+
this.observeVisibilityElement(el);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
observeVisibilityElement(el) {
|
|
1886
|
+
const eventName = el.getAttribute("data-kb-track-visibility");
|
|
1887
|
+
if (!eventName || this.visibilityData.has(el)) return;
|
|
1888
|
+
const channel = el.getAttribute("data-kb-visibility-channel") || "engagement";
|
|
1889
|
+
const threshold = parseFloat(el.getAttribute("data-kb-visibility-threshold") || "0.5");
|
|
1890
|
+
const clampedThreshold = Math.max(0, Math.min(1, isNaN(threshold) ? 0.5 : threshold));
|
|
1891
|
+
this.visibilityData.set(el, {
|
|
1892
|
+
visibleSince: null,
|
|
1893
|
+
totalMs: 0,
|
|
1894
|
+
event: eventName,
|
|
1895
|
+
channel
|
|
1896
|
+
});
|
|
1897
|
+
const observer = this.getOrCreateObserver(clampedThreshold);
|
|
1898
|
+
observer.observe(el);
|
|
1899
|
+
}
|
|
1900
|
+
getOrCreateObserver(threshold) {
|
|
1901
|
+
const key = Math.round(threshold * 100);
|
|
1902
|
+
let observer = this.visibilityObservers.get(key);
|
|
1903
|
+
if (observer) return observer;
|
|
1904
|
+
observer = new IntersectionObserver(
|
|
1905
|
+
(entries) => {
|
|
1906
|
+
const now = Date.now();
|
|
1907
|
+
for (const entry of entries) {
|
|
1908
|
+
const data = this.visibilityData.get(entry.target);
|
|
1909
|
+
if (!data) continue;
|
|
1910
|
+
if (entry.isIntersecting) {
|
|
1911
|
+
data.visibleSince = now;
|
|
1912
|
+
} else if (data.visibleSince !== null) {
|
|
1913
|
+
data.totalMs += now - data.visibleSince;
|
|
1914
|
+
data.visibleSince = null;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
{ threshold }
|
|
1919
|
+
);
|
|
1920
|
+
this.visibilityObservers.set(key, observer);
|
|
1921
|
+
return observer;
|
|
1922
|
+
}
|
|
1923
|
+
flushVisibilityForElement(el) {
|
|
1924
|
+
const data = this.visibilityData.get(el);
|
|
1925
|
+
if (!data) return;
|
|
1926
|
+
if (data.visibleSince !== null) {
|
|
1927
|
+
data.totalMs += Date.now() - data.visibleSince;
|
|
1928
|
+
data.visibleSince = null;
|
|
1929
|
+
}
|
|
1930
|
+
if (data.totalMs > 0) {
|
|
1931
|
+
const durationMs = Math.round(data.totalMs);
|
|
1932
|
+
const durationSeconds = Math.round(durationMs / 1e3);
|
|
1933
|
+
this.ctx.track({
|
|
1934
|
+
channel: data.channel,
|
|
1935
|
+
event: "element_visible",
|
|
1936
|
+
tags: {
|
|
1937
|
+
__element_name: data.event,
|
|
1938
|
+
__duration_seconds: durationSeconds,
|
|
1939
|
+
__duration_ms: durationMs
|
|
1940
|
+
}
|
|
1941
|
+
}).catch((err) => this.ctx.log("Failed to track visibility event", err));
|
|
1942
|
+
}
|
|
1943
|
+
for (const observer of this.visibilityObservers.values()) {
|
|
1944
|
+
observer.unobserve(el);
|
|
1945
|
+
}
|
|
1946
|
+
this.visibilityData.delete(el);
|
|
1947
|
+
}
|
|
1948
|
+
flushAllVisibilityEvents() {
|
|
1949
|
+
for (const [, data] of this.visibilityData.entries()) {
|
|
1950
|
+
if (data.visibleSince !== null) {
|
|
1951
|
+
data.totalMs += Date.now() - data.visibleSince;
|
|
1952
|
+
data.visibleSince = null;
|
|
1953
|
+
}
|
|
1954
|
+
if (data.totalMs > 0) {
|
|
1955
|
+
const durationMs = Math.round(data.totalMs);
|
|
1956
|
+
const durationSeconds = Math.round(durationMs / 1e3);
|
|
1957
|
+
this.ctx.track({
|
|
1958
|
+
channel: data.channel,
|
|
1959
|
+
event: "element_visible",
|
|
1960
|
+
tags: {
|
|
1961
|
+
element_name: data.event,
|
|
1962
|
+
duration_seconds: durationSeconds,
|
|
1963
|
+
duration_ms: durationMs
|
|
1964
|
+
}
|
|
1965
|
+
}).catch((err) => this.ctx.log("Failed to track visibility event", err));
|
|
1966
|
+
}
|
|
1967
|
+
data.totalMs = 0;
|
|
1968
|
+
data.visibleSince = null;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
// src/plugins/web-vitals.ts
|
|
1974
|
+
var import_web_vitals = require("web-vitals");
|
|
1975
|
+
var ANALYTICS_CHANNEL6 = "__analytics";
|
|
1976
|
+
var WebVitalsPlugin = class {
|
|
1977
|
+
name = "web-vitals";
|
|
1978
|
+
ctx;
|
|
1979
|
+
sent = false;
|
|
1980
|
+
timeout = null;
|
|
1981
|
+
beforeUnloadListener = null;
|
|
1982
|
+
data = {
|
|
1983
|
+
lcp: null,
|
|
1984
|
+
cls: null,
|
|
1985
|
+
inp: null,
|
|
1986
|
+
fcp: null,
|
|
1987
|
+
ttfb: null
|
|
1988
|
+
};
|
|
1989
|
+
setup(ctx) {
|
|
1990
|
+
if (typeof window === "undefined") return false;
|
|
1991
|
+
this.ctx = ctx;
|
|
1992
|
+
const checkAndSend = () => {
|
|
1993
|
+
const { lcp, cls, inp, fcp, ttfb } = this.data;
|
|
1994
|
+
if (lcp !== null && cls !== null && inp !== null && fcp !== null && ttfb !== null) {
|
|
1995
|
+
this.sendWebVitals();
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
(0, import_web_vitals.onLCP)((metric) => {
|
|
1999
|
+
this.data.lcp = metric.value;
|
|
2000
|
+
ctx.log("Web Vital collected", { name: "LCP", value: metric.value });
|
|
2001
|
+
checkAndSend();
|
|
2002
|
+
});
|
|
2003
|
+
(0, import_web_vitals.onCLS)((metric) => {
|
|
2004
|
+
this.data.cls = metric.value;
|
|
2005
|
+
ctx.log("Web Vital collected", { name: "CLS", value: metric.value });
|
|
2006
|
+
checkAndSend();
|
|
2007
|
+
});
|
|
2008
|
+
(0, import_web_vitals.onINP)((metric) => {
|
|
2009
|
+
this.data.inp = metric.value;
|
|
2010
|
+
ctx.log("Web Vital collected", { name: "INP", value: metric.value });
|
|
2011
|
+
checkAndSend();
|
|
2012
|
+
});
|
|
2013
|
+
(0, import_web_vitals.onFCP)((metric) => {
|
|
2014
|
+
this.data.fcp = metric.value;
|
|
2015
|
+
ctx.log("Web Vital collected", { name: "FCP", value: metric.value });
|
|
2016
|
+
checkAndSend();
|
|
2017
|
+
});
|
|
2018
|
+
(0, import_web_vitals.onTTFB)((metric) => {
|
|
2019
|
+
this.data.ttfb = metric.value;
|
|
2020
|
+
ctx.log("Web Vital collected", { name: "TTFB", value: metric.value });
|
|
2021
|
+
checkAndSend();
|
|
2022
|
+
});
|
|
2023
|
+
this.timeout = setTimeout(() => {
|
|
2024
|
+
this.timeout = null;
|
|
2025
|
+
this.sendWebVitals();
|
|
2026
|
+
}, 3e4);
|
|
2027
|
+
this.beforeUnloadListener = () => {
|
|
2028
|
+
this.sendWebVitals();
|
|
2029
|
+
};
|
|
2030
|
+
window.addEventListener("beforeunload", this.beforeUnloadListener);
|
|
2031
|
+
ctx.log("Web Vitals tracking enabled");
|
|
2032
|
+
}
|
|
2033
|
+
teardown() {
|
|
2034
|
+
if (this.timeout !== null) {
|
|
2035
|
+
clearTimeout(this.timeout);
|
|
2036
|
+
this.timeout = null;
|
|
2037
|
+
}
|
|
2038
|
+
this.sendWebVitals();
|
|
2039
|
+
if (this.beforeUnloadListener) {
|
|
2040
|
+
window.removeEventListener("beforeunload", this.beforeUnloadListener);
|
|
2041
|
+
this.beforeUnloadListener = null;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
sendWebVitals() {
|
|
2045
|
+
if (this.sent) return;
|
|
2046
|
+
const { lcp, cls, inp, fcp, ttfb } = this.data;
|
|
2047
|
+
if (lcp === null && cls === null && inp === null && fcp === null && ttfb === null) return;
|
|
2048
|
+
this.sent = true;
|
|
2049
|
+
if (this.timeout !== null) {
|
|
2050
|
+
clearTimeout(this.timeout);
|
|
2051
|
+
this.timeout = null;
|
|
2052
|
+
}
|
|
2053
|
+
const tags = {};
|
|
2054
|
+
if (lcp !== null) tags.__lcp = lcp;
|
|
2055
|
+
if (cls !== null) tags.__cls = cls;
|
|
2056
|
+
if (inp !== null) tags.__inp = inp;
|
|
2057
|
+
if (fcp !== null) tags.__fcp = fcp;
|
|
2058
|
+
if (ttfb !== null) tags.__ttfb = ttfb;
|
|
2059
|
+
this.ctx.log("Sending Web Vitals", tags);
|
|
2060
|
+
this.ctx.track({
|
|
2061
|
+
channel: ANALYTICS_CHANNEL6,
|
|
2062
|
+
event: "web_vitals",
|
|
2063
|
+
tags
|
|
2064
|
+
}).catch((err) => this.ctx.log("Failed to track web vitals", err));
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
// src/plugins/frustration.ts
|
|
2069
|
+
var ANALYTICS_CHANNEL7 = "__analytics";
|
|
2070
|
+
var RAGE_CLICK_THRESHOLD = 3;
|
|
2071
|
+
var RAGE_CLICK_WINDOW_MS = 1e3;
|
|
2072
|
+
var RAGE_CLICK_RADIUS_PX = 30;
|
|
2073
|
+
var DEAD_CLICK_TIMEOUT_MS = 1e3;
|
|
2074
|
+
var FrustrationPlugin = class {
|
|
2075
|
+
name = "frustration";
|
|
2076
|
+
ctx;
|
|
2077
|
+
rageClickBuffer = [];
|
|
2078
|
+
deadClickObserver = null;
|
|
2079
|
+
deadClickTimeout = null;
|
|
2080
|
+
clickListener = null;
|
|
2081
|
+
setup(ctx) {
|
|
2082
|
+
if (typeof window === "undefined") return false;
|
|
2083
|
+
this.ctx = ctx;
|
|
2084
|
+
this.clickListener = (event) => {
|
|
2085
|
+
const target = event.target;
|
|
2086
|
+
if (!target) return;
|
|
2087
|
+
const now = Date.now();
|
|
2088
|
+
this.rageClickBuffer.push({ time: now, x: event.clientX, y: event.clientY, target });
|
|
2089
|
+
this.rageClickBuffer = this.rageClickBuffer.filter(
|
|
2090
|
+
(c) => now - c.time < RAGE_CLICK_WINDOW_MS
|
|
2091
|
+
);
|
|
2092
|
+
if (this.rageClickBuffer.length >= RAGE_CLICK_THRESHOLD) {
|
|
2093
|
+
const first = this.rageClickBuffer[0];
|
|
2094
|
+
const allNearby = this.rageClickBuffer.every(
|
|
2095
|
+
(c) => Math.hypot(c.x - first.x, c.y - first.y) < RAGE_CLICK_RADIUS_PX
|
|
2096
|
+
);
|
|
2097
|
+
if (allNearby) {
|
|
2098
|
+
const element = findClickableElement(event) || target;
|
|
2099
|
+
const clickCount = this.rageClickBuffer.length;
|
|
2100
|
+
this.rageClickBuffer = [];
|
|
2101
|
+
ctx.track({
|
|
2102
|
+
channel: ANALYTICS_CHANNEL7,
|
|
2103
|
+
event: "rage_click",
|
|
2104
|
+
tags: {
|
|
2105
|
+
__path: window.location.pathname,
|
|
2106
|
+
__tag: element.tagName.toLowerCase(),
|
|
2107
|
+
__id: element.id || "",
|
|
2108
|
+
__class: element.className && typeof element.className === "string" ? element.className : "",
|
|
2109
|
+
__text: (element.textContent || "").trim().slice(0, 100),
|
|
2110
|
+
__selector: buildCssSelector(element),
|
|
2111
|
+
__click_count: clickCount
|
|
2112
|
+
}
|
|
2113
|
+
}).catch((err) => ctx.log("Failed to track rage click", err));
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
const clickedElement = findClickableElement(event);
|
|
2118
|
+
if (!clickedElement) return;
|
|
2119
|
+
if (target.closest?.("a[href]")) return;
|
|
2120
|
+
if (clickedElement.tagName === "SELECT" || target.closest?.("select")) return;
|
|
2121
|
+
if (this.deadClickTimeout !== null) {
|
|
2122
|
+
clearTimeout(this.deadClickTimeout);
|
|
2123
|
+
this.deadClickTimeout = null;
|
|
2124
|
+
}
|
|
2125
|
+
if (this.deadClickObserver) {
|
|
2126
|
+
this.deadClickObserver.disconnect();
|
|
2127
|
+
}
|
|
2128
|
+
let mutationDetected = false;
|
|
2129
|
+
this.deadClickObserver = new MutationObserver(() => {
|
|
2130
|
+
mutationDetected = true;
|
|
2131
|
+
});
|
|
2132
|
+
this.deadClickObserver.observe(document.body, {
|
|
2133
|
+
childList: true,
|
|
2134
|
+
subtree: true,
|
|
2135
|
+
attributes: true,
|
|
2136
|
+
characterData: true
|
|
2137
|
+
});
|
|
2138
|
+
this.deadClickTimeout = setTimeout(() => {
|
|
2139
|
+
if (this.deadClickObserver) {
|
|
2140
|
+
this.deadClickObserver.disconnect();
|
|
2141
|
+
this.deadClickObserver = null;
|
|
2142
|
+
}
|
|
2143
|
+
if (!mutationDetected) {
|
|
2144
|
+
ctx.track({
|
|
2145
|
+
channel: ANALYTICS_CHANNEL7,
|
|
2146
|
+
event: "dead_click",
|
|
2147
|
+
tags: {
|
|
2148
|
+
__path: window.location.pathname,
|
|
2149
|
+
__tag: clickedElement.tagName.toLowerCase(),
|
|
2150
|
+
__id: clickedElement.id || "",
|
|
2151
|
+
__class: clickedElement.className && typeof clickedElement.className === "string" ? clickedElement.className : "",
|
|
2152
|
+
__text: (clickedElement.textContent || "").trim().slice(0, 100),
|
|
2153
|
+
__selector: buildCssSelector(clickedElement)
|
|
2154
|
+
}
|
|
2155
|
+
}).catch((err) => ctx.log("Failed to track dead click", err));
|
|
2156
|
+
}
|
|
2157
|
+
}, DEAD_CLICK_TIMEOUT_MS);
|
|
2158
|
+
};
|
|
2159
|
+
document.addEventListener("click", this.clickListener, true);
|
|
2160
|
+
ctx.log("Frustration signal detection enabled");
|
|
2161
|
+
}
|
|
2162
|
+
teardown() {
|
|
2163
|
+
if (this.clickListener) {
|
|
2164
|
+
document.removeEventListener("click", this.clickListener, true);
|
|
2165
|
+
this.clickListener = null;
|
|
2166
|
+
}
|
|
2167
|
+
if (this.deadClickObserver) {
|
|
2168
|
+
this.deadClickObserver.disconnect();
|
|
2169
|
+
this.deadClickObserver = null;
|
|
2170
|
+
}
|
|
2171
|
+
if (this.deadClickTimeout !== null) {
|
|
2172
|
+
clearTimeout(this.deadClickTimeout);
|
|
2173
|
+
this.deadClickTimeout = null;
|
|
2174
|
+
}
|
|
2175
|
+
this.rageClickBuffer = [];
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
// src/plugins/defaults.ts
|
|
2180
|
+
function createDefaultPlugins(config) {
|
|
2181
|
+
const plugins = [];
|
|
2182
|
+
if (config?.autoTrackPageViews !== false) {
|
|
2183
|
+
plugins.push(new PageViewPlugin());
|
|
2184
|
+
}
|
|
2185
|
+
if (config?.autoTrackOutboundLinks !== false) {
|
|
2186
|
+
plugins.push(new OutboundLinksPlugin());
|
|
2187
|
+
}
|
|
2188
|
+
if (config?.autoTrackClicks !== false) {
|
|
2189
|
+
plugins.push(new ClickTrackingPlugin());
|
|
2190
|
+
}
|
|
2191
|
+
if (config?.autoTrackScrollDepth !== false) {
|
|
2192
|
+
plugins.push(new ScrollDepthPlugin());
|
|
2193
|
+
}
|
|
2194
|
+
if (config?.autoTrackVisibility !== false) {
|
|
2195
|
+
plugins.push(new VisibilityPlugin());
|
|
2196
|
+
}
|
|
2197
|
+
if (config?.autoTrackWebVitals === true) {
|
|
2198
|
+
plugins.push(new WebVitalsPlugin());
|
|
2199
|
+
}
|
|
2200
|
+
if (config?.autoDetectFrustration !== false) {
|
|
2201
|
+
plugins.push(new FrustrationPlugin());
|
|
2202
|
+
}
|
|
2203
|
+
return plugins;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/client.ts
|
|
2207
|
+
var _instance = null;
|
|
2208
|
+
function init(config) {
|
|
2209
|
+
if (_instance) {
|
|
2210
|
+
_instance.shutdown();
|
|
2211
|
+
}
|
|
2212
|
+
_instance = new KitbaseAnalytics2(config);
|
|
2213
|
+
return _instance;
|
|
2214
|
+
}
|
|
2215
|
+
function getInstance() {
|
|
2216
|
+
return _instance;
|
|
2217
|
+
}
|
|
2218
|
+
var KitbaseAnalytics2 = class extends KitbaseAnalytics {
|
|
2219
|
+
// Offline queue
|
|
2220
|
+
queue = null;
|
|
2221
|
+
offlineEnabled;
|
|
2222
|
+
constructor(config) {
|
|
2223
|
+
super(config, createDefaultPlugins(config.analytics));
|
|
2224
|
+
this.offlineEnabled = config.offline?.enabled ?? false;
|
|
2225
|
+
if (this.offlineEnabled) {
|
|
2226
|
+
this.queue = new EventQueue(config.offline);
|
|
2227
|
+
this.queue.setDebugMode(this.debugMode, this.log.bind(this));
|
|
2228
|
+
this.queue.setSendCallback(this.sendQueuedEvents.bind(this));
|
|
2229
|
+
this.queue.startFlushTimer();
|
|
2230
|
+
this.log("Offline queueing enabled", {
|
|
2231
|
+
storageType: this.queue.getStorageType()
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
// ============================================================
|
|
2236
|
+
// Debug Mode Override
|
|
2237
|
+
// ============================================================
|
|
2238
|
+
/**
|
|
2239
|
+
* Enable or disable debug mode
|
|
2240
|
+
* When enabled, all SDK operations are logged to the console
|
|
2241
|
+
*
|
|
2242
|
+
* @param enabled - Whether to enable debug mode
|
|
2243
|
+
*
|
|
2244
|
+
* @example
|
|
2245
|
+
* ```typescript
|
|
2246
|
+
* kitbase.setDebugMode(true);
|
|
2247
|
+
* // All events and operations will now be logged
|
|
2248
|
+
* ```
|
|
2249
|
+
*/
|
|
2250
|
+
setDebugMode(enabled) {
|
|
2251
|
+
super.setDebugMode(enabled);
|
|
2252
|
+
if (this.queue) {
|
|
2253
|
+
this.queue.setDebugMode(enabled, this.log.bind(this));
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
// ============================================================
|
|
2257
|
+
// Offline Queue
|
|
2258
|
+
// ============================================================
|
|
2259
|
+
/**
|
|
2260
|
+
* Get offline queue statistics
|
|
2261
|
+
*
|
|
2262
|
+
* @returns Queue statistics including size and flush status
|
|
2263
|
+
*
|
|
2264
|
+
* @example
|
|
2265
|
+
* ```typescript
|
|
2266
|
+
* const stats = await kitbase.getQueueStats();
|
|
2267
|
+
* console.log(stats); // { size: 5, isFlushing: false }
|
|
2268
|
+
* ```
|
|
2269
|
+
*/
|
|
2270
|
+
async getQueueStats() {
|
|
2271
|
+
if (!this.queue) return null;
|
|
2272
|
+
return this.queue.getStats();
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Manually flush the offline queue
|
|
2276
|
+
* Events are automatically flushed on interval and when coming back online,
|
|
2277
|
+
* but this method can be used to trigger an immediate flush
|
|
2278
|
+
*
|
|
2279
|
+
* @example
|
|
2280
|
+
* ```typescript
|
|
2281
|
+
* await kitbase.flushQueue();
|
|
2282
|
+
* ```
|
|
2283
|
+
*/
|
|
2284
|
+
async flushQueue() {
|
|
2285
|
+
if (!this.queue) return;
|
|
2286
|
+
await this.queue.flush();
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Clear all events from the offline queue
|
|
2290
|
+
*
|
|
2291
|
+
* @example
|
|
2292
|
+
* ```typescript
|
|
2293
|
+
* await kitbase.clearQueue();
|
|
2294
|
+
* ```
|
|
2295
|
+
*/
|
|
2296
|
+
async clearQueue() {
|
|
2297
|
+
if (!this.queue) return;
|
|
2298
|
+
await this.queue.clear();
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Callback for the queue to send batched events via the batch endpoint.
|
|
2302
|
+
* Sends all events in a single HTTP request instead of individual POSTs.
|
|
2303
|
+
*/
|
|
2304
|
+
async sendQueuedEvents(events) {
|
|
2305
|
+
try {
|
|
2306
|
+
await this.sendRequest("/sdk/v1/logs/batch", {
|
|
2307
|
+
events: events.map((e) => e.payload)
|
|
2308
|
+
});
|
|
2309
|
+
return events.map((e) => e.id);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
this.log("Batch send failed", { count: events.length, error });
|
|
2312
|
+
return [];
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
// ============================================================
|
|
2316
|
+
// Track Event Override
|
|
2317
|
+
// ============================================================
|
|
2318
|
+
/**
|
|
2319
|
+
* Track an event
|
|
2320
|
+
*
|
|
2321
|
+
* When offline queueing is enabled, events are always written to the local
|
|
2322
|
+
* database first (write-ahead), then sent to the server. This ensures no
|
|
2323
|
+
* events are lost if the browser crashes or the network fails.
|
|
2324
|
+
*
|
|
2325
|
+
* @param options - Event tracking options
|
|
2326
|
+
* @returns Promise resolving to the track response, or void if tracking is blocked
|
|
2327
|
+
* @throws {ValidationError} When required fields are missing
|
|
2328
|
+
* @throws {AuthenticationError} When the API key is invalid (only when offline disabled)
|
|
2329
|
+
* @throws {ApiError} When the API returns an error (only when offline disabled)
|
|
2330
|
+
* @throws {TimeoutError} When the request times out (only when offline disabled)
|
|
2331
|
+
*/
|
|
2332
|
+
async track(options) {
|
|
2333
|
+
this.validateTrackOptions(options);
|
|
2334
|
+
if (this.isBotBlockingActive()) {
|
|
2335
|
+
this.log("Event skipped - bot detected", { event: options.event });
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
let duration;
|
|
2339
|
+
const startTime = this.timedEvents.get(options.event);
|
|
2340
|
+
if (startTime !== void 0) {
|
|
2341
|
+
duration = (Date.now() - startTime) / 1e3;
|
|
2342
|
+
this.timedEvents.delete(options.event);
|
|
2343
|
+
this.log("Timer stopped", { event: options.event, duration });
|
|
2344
|
+
}
|
|
2345
|
+
const mergedTags = {
|
|
2346
|
+
...this.superProperties,
|
|
2347
|
+
...options.tags ?? {},
|
|
2348
|
+
...duration !== void 0 ? { $duration: duration } : {}
|
|
2349
|
+
};
|
|
2350
|
+
const payload = {
|
|
2351
|
+
channel: options.channel,
|
|
2352
|
+
event: options.event,
|
|
2353
|
+
client_timestamp: Date.now(),
|
|
2354
|
+
client_session_id: this.getClientSessionId(),
|
|
2355
|
+
...options.user_id && { user_id: options.user_id },
|
|
2356
|
+
...options.icon && { icon: options.icon },
|
|
2357
|
+
...options.notify !== void 0 && { notify: options.notify },
|
|
2358
|
+
...options.description && { description: options.description },
|
|
2359
|
+
...Object.keys(mergedTags).length > 0 && { tags: mergedTags }
|
|
2360
|
+
};
|
|
2361
|
+
this.log("Track", { event: options.event, payload });
|
|
2362
|
+
if (this.queue) {
|
|
2363
|
+
await this.queue.enqueue(payload);
|
|
2364
|
+
this.log("Event persisted to queue");
|
|
2365
|
+
this.queue.flush().catch((err) => {
|
|
2366
|
+
this.log("Background flush failed", err);
|
|
2367
|
+
});
|
|
2368
|
+
return {
|
|
2369
|
+
id: `queued-${Date.now()}`,
|
|
2370
|
+
event: options.event,
|
|
2371
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
const response = await this.sendRequest("/sdk/v1/logs", payload);
|
|
2375
|
+
this.log("Event sent successfully", { id: response.id });
|
|
2376
|
+
return response;
|
|
2377
|
+
}
|
|
2378
|
+
validateTrackOptions(options) {
|
|
2379
|
+
if (!options.event) {
|
|
2380
|
+
throw new ValidationError("Event is required", "event");
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
// ============================================================
|
|
2384
|
+
// Cleanup
|
|
2385
|
+
// ============================================================
|
|
2386
|
+
/**
|
|
2387
|
+
* Shutdown the client and cleanup resources
|
|
2388
|
+
* Call this when you're done using the client to stop timers and close connections
|
|
2389
|
+
*
|
|
2390
|
+
* @example
|
|
2391
|
+
* ```typescript
|
|
2392
|
+
* await kitbase.shutdown();
|
|
2393
|
+
* ```
|
|
2394
|
+
*/
|
|
2395
|
+
async shutdown() {
|
|
2396
|
+
if (this.queue) {
|
|
2397
|
+
await this.queue.flush();
|
|
2398
|
+
}
|
|
2399
|
+
super.shutdown();
|
|
2400
|
+
if (this.queue) {
|
|
2401
|
+
await this.queue.flush();
|
|
2402
|
+
await this.queue.close();
|
|
2403
|
+
this.queue = null;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2407
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2408
|
+
0 && (module.exports = {
|
|
2409
|
+
ApiError,
|
|
2410
|
+
AuthenticationError,
|
|
2411
|
+
ClickTrackingPlugin,
|
|
2412
|
+
FrustrationPlugin,
|
|
2413
|
+
KitbaseAnalytics,
|
|
2414
|
+
KitbaseError,
|
|
2415
|
+
OutboundLinksPlugin,
|
|
2416
|
+
PageViewPlugin,
|
|
2417
|
+
ScrollDepthPlugin,
|
|
2418
|
+
TimeoutError,
|
|
2419
|
+
ValidationError,
|
|
2420
|
+
VisibilityPlugin,
|
|
2421
|
+
WebVitalsPlugin,
|
|
2422
|
+
createDefaultPlugins,
|
|
2423
|
+
detectBot,
|
|
2424
|
+
getInstance,
|
|
2425
|
+
getUserAgent,
|
|
2426
|
+
init,
|
|
2427
|
+
isBot,
|
|
2428
|
+
isUserAgentBot
|
|
2429
|
+
});
|
|
2430
|
+
//# sourceMappingURL=index.cjs.map
|