@tell-rs/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1268 @@
1
+ // ../core/src/constants.ts
2
+ var Events = {
3
+ // User Lifecycle
4
+ UserSignedUp: "User Signed Up",
5
+ UserSignedIn: "User Signed In",
6
+ UserSignedOut: "User Signed Out",
7
+ UserInvited: "User Invited",
8
+ UserOnboarded: "User Onboarded",
9
+ AuthenticationFailed: "Authentication Failed",
10
+ PasswordReset: "Password Reset",
11
+ TwoFactorEnabled: "Two Factor Enabled",
12
+ TwoFactorDisabled: "Two Factor Disabled",
13
+ // Revenue & Billing
14
+ OrderCompleted: "Order Completed",
15
+ OrderRefunded: "Order Refunded",
16
+ OrderCanceled: "Order Canceled",
17
+ PaymentFailed: "Payment Failed",
18
+ PaymentMethodAdded: "Payment Method Added",
19
+ PaymentMethodUpdated: "Payment Method Updated",
20
+ PaymentMethodRemoved: "Payment Method Removed",
21
+ // Subscription
22
+ SubscriptionStarted: "Subscription Started",
23
+ SubscriptionRenewed: "Subscription Renewed",
24
+ SubscriptionPaused: "Subscription Paused",
25
+ SubscriptionResumed: "Subscription Resumed",
26
+ SubscriptionChanged: "Subscription Changed",
27
+ SubscriptionCanceled: "Subscription Canceled",
28
+ // Trial
29
+ TrialStarted: "Trial Started",
30
+ TrialEndingSoon: "Trial Ending Soon",
31
+ TrialEnded: "Trial Ended",
32
+ TrialConverted: "Trial Converted",
33
+ // Shopping
34
+ CartViewed: "Cart Viewed",
35
+ CartUpdated: "Cart Updated",
36
+ CartAbandoned: "Cart Abandoned",
37
+ CheckoutStarted: "Checkout Started",
38
+ CheckoutCompleted: "Checkout Completed",
39
+ // Engagement
40
+ PageViewed: "Page Viewed",
41
+ FeatureUsed: "Feature Used",
42
+ SearchPerformed: "Search Performed",
43
+ FileUploaded: "File Uploaded",
44
+ NotificationSent: "Notification Sent",
45
+ NotificationClicked: "Notification Clicked",
46
+ // Communication
47
+ EmailSent: "Email Sent",
48
+ EmailOpened: "Email Opened",
49
+ EmailClicked: "Email Clicked",
50
+ EmailBounced: "Email Bounced",
51
+ EmailUnsubscribed: "Email Unsubscribed",
52
+ SupportTicketCreated: "Support Ticket Created",
53
+ SupportTicketResolved: "Support Ticket Resolved"
54
+ };
55
+
56
+ // ../core/src/errors.ts
57
+ var TellError = class extends Error {
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = "TellError";
61
+ }
62
+ };
63
+ var ConfigurationError = class extends TellError {
64
+ constructor(message) {
65
+ super(message);
66
+ this.name = "ConfigurationError";
67
+ }
68
+ };
69
+ var ValidationError = class extends TellError {
70
+ field;
71
+ constructor(field, message) {
72
+ super(`${field}: ${message}`);
73
+ this.name = "ValidationError";
74
+ this.field = field;
75
+ }
76
+ };
77
+ var NetworkError = class extends TellError {
78
+ statusCode;
79
+ constructor(message, statusCode) {
80
+ super(message);
81
+ this.name = "NetworkError";
82
+ this.statusCode = statusCode;
83
+ }
84
+ };
85
+ var ClosedError = class extends TellError {
86
+ constructor() {
87
+ super("Client is closed");
88
+ this.name = "ClosedError";
89
+ }
90
+ };
91
+ var SerializationError = class extends TellError {
92
+ constructor(message) {
93
+ super(message);
94
+ this.name = "SerializationError";
95
+ }
96
+ };
97
+
98
+ // ../core/src/validation.ts
99
+ var HEX_RE = /^[0-9a-fA-F]{32}$/;
100
+ var MAX_EVENT_NAME = 256;
101
+ var MAX_LOG_MESSAGE = 65536;
102
+ function validateApiKey(key) {
103
+ if (!key) {
104
+ throw new ConfigurationError("apiKey is required");
105
+ }
106
+ if (!HEX_RE.test(key)) {
107
+ throw new ConfigurationError(
108
+ "apiKey must be exactly 32 hex characters"
109
+ );
110
+ }
111
+ }
112
+ function validateEventName(name) {
113
+ if (typeof name !== "string" || name.length === 0) {
114
+ throw new ValidationError("eventName", "must be a non-empty string");
115
+ }
116
+ if (name.length > MAX_EVENT_NAME) {
117
+ throw new ValidationError(
118
+ "eventName",
119
+ `must be at most ${MAX_EVENT_NAME} characters, got ${name.length}`
120
+ );
121
+ }
122
+ }
123
+ function validateLogMessage(message) {
124
+ if (typeof message !== "string" || message.length === 0) {
125
+ throw new ValidationError("message", "must be a non-empty string");
126
+ }
127
+ if (message.length > MAX_LOG_MESSAGE) {
128
+ throw new ValidationError(
129
+ "message",
130
+ `must be at most ${MAX_LOG_MESSAGE} characters, got ${message.length}`
131
+ );
132
+ }
133
+ }
134
+ function validateUserId(id) {
135
+ if (typeof id !== "string" || id.length === 0) {
136
+ throw new ValidationError("userId", "must be a non-empty string");
137
+ }
138
+ }
139
+
140
+ // ../core/src/batcher.ts
141
+ var Batcher = class {
142
+ queue = [];
143
+ timer = null;
144
+ closed = false;
145
+ flushing = null;
146
+ config;
147
+ constructor(config) {
148
+ this.config = config;
149
+ this.timer = setInterval(() => {
150
+ if (this.queue.length > 0) {
151
+ this.flush().catch(() => {
152
+ });
153
+ }
154
+ }, config.interval);
155
+ if (this.timer && typeof this.timer.unref === "function") {
156
+ this.timer.unref();
157
+ }
158
+ }
159
+ add(item) {
160
+ if (this.closed) return;
161
+ if (this.queue.length >= this.config.maxQueueSize) {
162
+ this.queue.shift();
163
+ if (this.config.onOverflow) {
164
+ this.config.onOverflow();
165
+ }
166
+ }
167
+ this.queue.push(item);
168
+ if (this.queue.length >= this.config.size) {
169
+ this.flush().catch(() => {
170
+ });
171
+ }
172
+ }
173
+ async flush() {
174
+ if (this.flushing) {
175
+ return this.flushing;
176
+ }
177
+ this.flushing = this.doFlush();
178
+ try {
179
+ await this.flushing;
180
+ } finally {
181
+ this.flushing = null;
182
+ }
183
+ }
184
+ async close() {
185
+ this.closed = true;
186
+ if (this.timer !== null) {
187
+ clearInterval(this.timer);
188
+ this.timer = null;
189
+ }
190
+ await this.flush();
191
+ }
192
+ get pending() {
193
+ return this.queue.length;
194
+ }
195
+ drain() {
196
+ const items = this.queue;
197
+ this.queue = [];
198
+ return items;
199
+ }
200
+ halveBatchSize() {
201
+ this.config.size = Math.max(1, Math.floor(this.config.size / 2));
202
+ }
203
+ async doFlush() {
204
+ while (this.queue.length > 0) {
205
+ const batch = this.queue.slice(0, this.config.size);
206
+ try {
207
+ await this.config.send(batch);
208
+ this.queue.splice(0, batch.length);
209
+ } catch {
210
+ return;
211
+ }
212
+ }
213
+ }
214
+ };
215
+
216
+ // ../core/src/before-send.ts
217
+ function runBeforeSend(item, fns) {
218
+ const pipeline = Array.isArray(fns) ? fns : [fns];
219
+ let current = item;
220
+ for (const fn of pipeline) {
221
+ if (current === null) return null;
222
+ current = fn(current);
223
+ }
224
+ return current;
225
+ }
226
+
227
+ // src/config.ts
228
+ var DEFAULTS = {
229
+ endpoint: "https://collect.tell.app",
230
+ batchSize: 20,
231
+ flushInterval: 5e3,
232
+ maxRetries: 5,
233
+ closeTimeout: 5e3,
234
+ networkTimeout: 1e4,
235
+ logLevel: "error",
236
+ source: "browser",
237
+ disabled: false,
238
+ maxQueueSize: 1e3,
239
+ sessionTimeout: 18e5,
240
+ // 30 min
241
+ persistence: "localStorage",
242
+ respectDoNotTrack: false,
243
+ botDetection: true,
244
+ captureErrors: false
245
+ };
246
+ function resolveConfig(options) {
247
+ return { ...DEFAULTS, ...options };
248
+ }
249
+ function development(overrides) {
250
+ return {
251
+ endpoint: "http://localhost:8080",
252
+ batchSize: 5,
253
+ flushInterval: 2e3,
254
+ logLevel: "debug",
255
+ ...overrides
256
+ };
257
+ }
258
+ function production(overrides) {
259
+ return {
260
+ logLevel: "error",
261
+ ...overrides
262
+ };
263
+ }
264
+
265
+ // src/persistence.ts
266
+ var STORAGE_KEYS = {
267
+ DEVICE_ID: "tell_device_id",
268
+ USER_ID: "tell_user_id",
269
+ OPT_OUT: "tell_opt_out",
270
+ SUPER_PROPS: "tell_super_props"
271
+ };
272
+ var LocalStorageStorage = class {
273
+ get(key) {
274
+ try {
275
+ return localStorage.getItem(key);
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ set(key, value) {
281
+ try {
282
+ localStorage.setItem(key, value);
283
+ } catch {
284
+ }
285
+ }
286
+ remove(key) {
287
+ try {
288
+ localStorage.removeItem(key);
289
+ } catch {
290
+ }
291
+ }
292
+ };
293
+ var MemoryStorage = class {
294
+ data = /* @__PURE__ */ new Map();
295
+ get(key) {
296
+ return this.data.get(key) ?? null;
297
+ }
298
+ set(key, value) {
299
+ this.data.set(key, value);
300
+ }
301
+ remove(key) {
302
+ this.data.delete(key);
303
+ }
304
+ };
305
+ function createStorage(persistence) {
306
+ if (persistence === "localStorage" && typeof localStorage !== "undefined") {
307
+ try {
308
+ const testKey = "tell_test";
309
+ localStorage.setItem(testKey, "1");
310
+ localStorage.removeItem(testKey);
311
+ return new LocalStorageStorage();
312
+ } catch {
313
+ }
314
+ }
315
+ return new MemoryStorage();
316
+ }
317
+
318
+ // src/id.ts
319
+ function generateId() {
320
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
321
+ const bytes = new Uint8Array(16);
322
+ crypto.getRandomValues(bytes);
323
+ bytes[6] = bytes[6] & 15 | 64;
324
+ bytes[8] = bytes[8] & 63 | 128;
325
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
326
+ ""
327
+ );
328
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
329
+ }
330
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
331
+ const r = Math.random() * 16 | 0;
332
+ const v = c === "x" ? r : r & 3 | 8;
333
+ return v.toString(16);
334
+ });
335
+ }
336
+
337
+ // src/bot.ts
338
+ function isBot() {
339
+ if (typeof navigator === "undefined") return false;
340
+ if (navigator.webdriver === true) return true;
341
+ const ua = navigator.userAgent || "";
342
+ return /headless/i.test(ua);
343
+ }
344
+
345
+ // src/context.ts
346
+ function captureContext() {
347
+ const ctx = {};
348
+ if (typeof navigator !== "undefined") {
349
+ const ua = navigator.userAgent || "";
350
+ const parsed = parseUA(ua);
351
+ ctx.browser = parsed.browser;
352
+ ctx.browser_version = parsed.browserVersion;
353
+ ctx.os_name = parsed.os;
354
+ ctx.os_version = parsed.osVersion;
355
+ ctx.device_type = inferDeviceType(parsed.os);
356
+ ctx.locale = navigator.language;
357
+ if ("hardwareConcurrency" in navigator) {
358
+ ctx.cpu_cores = navigator.hardwareConcurrency;
359
+ }
360
+ if ("deviceMemory" in navigator) {
361
+ ctx.device_memory = navigator.deviceMemory;
362
+ }
363
+ if ("maxTouchPoints" in navigator) {
364
+ ctx.touch = navigator.maxTouchPoints > 0;
365
+ }
366
+ const conn = navigator.connection;
367
+ if (conn?.effectiveType) {
368
+ ctx.connection_type = conn.effectiveType;
369
+ }
370
+ }
371
+ if (typeof screen !== "undefined") {
372
+ ctx.screen_width = screen.width;
373
+ ctx.screen_height = screen.height;
374
+ }
375
+ if (typeof window !== "undefined") {
376
+ ctx.viewport_width = window.innerWidth;
377
+ ctx.viewport_height = window.innerHeight;
378
+ if (window.devicePixelRatio) {
379
+ ctx.device_pixel_ratio = window.devicePixelRatio;
380
+ }
381
+ }
382
+ if (typeof document !== "undefined") {
383
+ ctx.referrer = document.referrer || void 0;
384
+ if (ctx.referrer) {
385
+ try {
386
+ ctx.referrer_domain = new URL(ctx.referrer).hostname;
387
+ } catch {
388
+ }
389
+ }
390
+ ctx.title = document.title || void 0;
391
+ }
392
+ if (typeof location !== "undefined") {
393
+ ctx.url = location.href;
394
+ }
395
+ try {
396
+ ctx.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
397
+ } catch {
398
+ }
399
+ return ctx;
400
+ }
401
+ function inferDeviceType(os) {
402
+ if (os === "iOS" || os === "Android") return "mobile";
403
+ return "desktop";
404
+ }
405
+ function parseUA(ua) {
406
+ const result = {};
407
+ if (/Edg\/(\d+[\d.]*)/.test(ua)) {
408
+ result.browser = "Edge";
409
+ result.browserVersion = RegExp.$1;
410
+ } else if (/OPR\/(\d+[\d.]*)/.test(ua)) {
411
+ result.browser = "Opera";
412
+ result.browserVersion = RegExp.$1;
413
+ } else if (/Chrome\/(\d+[\d.]*)/.test(ua)) {
414
+ result.browser = "Chrome";
415
+ result.browserVersion = RegExp.$1;
416
+ } else if (/Safari\/[\d.]+/.test(ua) && /Version\/(\d+[\d.]*)/.test(ua)) {
417
+ result.browser = "Safari";
418
+ result.browserVersion = RegExp.$1;
419
+ } else if (/Firefox\/(\d+[\d.]*)/.test(ua)) {
420
+ result.browser = "Firefox";
421
+ result.browserVersion = RegExp.$1;
422
+ }
423
+ if (/Windows NT ([\d.]+)/.test(ua)) {
424
+ result.os = "Windows";
425
+ result.osVersion = RegExp.$1;
426
+ } else if (/Mac OS X ([\d_.]+)/.test(ua)) {
427
+ result.os = "macOS";
428
+ result.osVersion = RegExp.$1.replace(/_/g, ".");
429
+ } else if (/iPhone OS ([\d_]+)/.test(ua)) {
430
+ result.os = "iOS";
431
+ result.osVersion = RegExp.$1.replace(/_/g, ".");
432
+ } else if (/Android ([\d.]+)/.test(ua)) {
433
+ result.os = "Android";
434
+ result.osVersion = RegExp.$1;
435
+ } else if (/Linux/.test(ua)) {
436
+ result.os = "Linux";
437
+ }
438
+ return result;
439
+ }
440
+
441
+ // src/utm.ts
442
+ var UTM_KEYS = [
443
+ "utm_source",
444
+ "utm_medium",
445
+ "utm_campaign",
446
+ "utm_term",
447
+ "utm_content"
448
+ ];
449
+ function captureUtm() {
450
+ if (typeof window === "undefined" || !window.location?.search) return {};
451
+ const params = new URLSearchParams(window.location.search);
452
+ const utm = {};
453
+ for (const key of UTM_KEYS) {
454
+ const value = params.get(key);
455
+ if (value) {
456
+ utm[key] = value;
457
+ }
458
+ }
459
+ return utm;
460
+ }
461
+
462
+ // src/session.ts
463
+ var SessionManager = class {
464
+ _sessionId;
465
+ lastHiddenAt = 0;
466
+ lastActivityAt;
467
+ timeout;
468
+ onNewSession;
469
+ visibilityHandler;
470
+ checkTimer;
471
+ lastContextAt = {};
472
+ constructor(config) {
473
+ this.timeout = config.timeout;
474
+ this.onNewSession = config.onNewSession;
475
+ this._sessionId = generateId();
476
+ this.lastActivityAt = Date.now();
477
+ this.emitContext("session_start");
478
+ this.visibilityHandler = () => this.handleVisibility();
479
+ if (typeof document !== "undefined") {
480
+ document.addEventListener("visibilitychange", this.visibilityHandler);
481
+ }
482
+ this.checkTimer = setInterval(() => this.checkTimeout(), 6e4);
483
+ if (typeof this.checkTimer.unref === "function") {
484
+ this.checkTimer.unref();
485
+ }
486
+ }
487
+ get sessionId() {
488
+ return this._sessionId;
489
+ }
490
+ set sessionId(id) {
491
+ this._sessionId = id;
492
+ }
493
+ touch() {
494
+ this.lastActivityAt = Date.now();
495
+ }
496
+ destroy() {
497
+ if (typeof document !== "undefined") {
498
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
499
+ }
500
+ clearInterval(this.checkTimer);
501
+ }
502
+ handleVisibility() {
503
+ if (document.visibilityState === "hidden") {
504
+ this.lastHiddenAt = Date.now();
505
+ } else if (document.visibilityState === "visible") {
506
+ if (this.lastHiddenAt > 0 && Date.now() - this.lastHiddenAt > this.timeout) {
507
+ this.rotateSession("session_timeout");
508
+ } else if (this.lastHiddenAt > 0) {
509
+ this.emitContext("app_foreground");
510
+ }
511
+ this.lastHiddenAt = 0;
512
+ }
513
+ }
514
+ checkTimeout() {
515
+ if (Date.now() - this.lastActivityAt > this.timeout) {
516
+ this.rotateSession("session_timeout");
517
+ }
518
+ }
519
+ rotateSession(reason) {
520
+ this._sessionId = generateId();
521
+ this.lastActivityAt = Date.now();
522
+ this.emitContext(reason);
523
+ }
524
+ emitContext(reason) {
525
+ const now = Date.now();
526
+ const last = this.lastContextAt[reason] ?? 0;
527
+ if (now - last < 1e3) return;
528
+ this.lastContextAt[reason] = now;
529
+ this.onNewSession(reason, this._sessionId);
530
+ }
531
+ };
532
+
533
+ // src/transport.ts
534
+ var BrowserTransport = class {
535
+ endpoint;
536
+ apiKey;
537
+ maxRetries;
538
+ networkTimeout;
539
+ onError;
540
+ onPayloadTooLarge;
541
+ constructor(config) {
542
+ this.endpoint = config.endpoint;
543
+ this.apiKey = config.apiKey;
544
+ this.maxRetries = config.maxRetries;
545
+ this.networkTimeout = config.networkTimeout;
546
+ this.onError = config.onError;
547
+ this.onPayloadTooLarge = config.onPayloadTooLarge;
548
+ }
549
+ async sendEvents(events) {
550
+ if (events.length === 0) return;
551
+ const body = events.map((e) => JSON.stringify(e)).join("\n");
552
+ await this.send("/v1/events", body);
553
+ }
554
+ async sendLogs(logs) {
555
+ if (logs.length === 0) return;
556
+ const body = logs.map((l) => JSON.stringify(l)).join("\n");
557
+ await this.send("/v1/logs", body);
558
+ }
559
+ /** Best-effort flush via sendBeacon for page unload. */
560
+ beacon(events, logs) {
561
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
562
+ if (events.length > 0) {
563
+ const body = events.map((e) => JSON.stringify(e)).join("\n");
564
+ const blob = new Blob([body], { type: "application/x-ndjson" });
565
+ const url = `${this.endpoint}/v1/events?token=${encodeURIComponent(this.apiKey)}`;
566
+ navigator.sendBeacon(url, blob);
567
+ }
568
+ if (logs.length > 0) {
569
+ const body = logs.map((l) => JSON.stringify(l)).join("\n");
570
+ const blob = new Blob([body], { type: "application/x-ndjson" });
571
+ const url = `${this.endpoint}/v1/logs?token=${encodeURIComponent(this.apiKey)}`;
572
+ navigator.sendBeacon(url, blob);
573
+ }
574
+ }
575
+ async send(path, body) {
576
+ const url = `${this.endpoint}${path}`;
577
+ const headers = {
578
+ "Content-Type": "application/x-ndjson",
579
+ Authorization: `Bearer ${this.apiKey}`
580
+ };
581
+ let lastError;
582
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
583
+ if (attempt > 0) {
584
+ await this.backoff(attempt);
585
+ }
586
+ if (typeof navigator !== "undefined" && !navigator.onLine) {
587
+ lastError = new NetworkError("Browser is offline");
588
+ continue;
589
+ }
590
+ try {
591
+ const controller = new AbortController();
592
+ const timer = setTimeout(
593
+ () => controller.abort(),
594
+ this.networkTimeout
595
+ );
596
+ const response = await fetch(url, {
597
+ method: "POST",
598
+ headers,
599
+ body,
600
+ signal: controller.signal,
601
+ keepalive: true
602
+ });
603
+ clearTimeout(timer);
604
+ if (response.status === 202) {
605
+ return;
606
+ }
607
+ if (response.status === 207) {
608
+ if (this.onError) {
609
+ this.onError(
610
+ new NetworkError(
611
+ `Partial success: some items rejected`,
612
+ 207
613
+ )
614
+ );
615
+ }
616
+ return;
617
+ }
618
+ if (response.status === 413) {
619
+ if (this.onPayloadTooLarge) {
620
+ this.onPayloadTooLarge();
621
+ }
622
+ throw new NetworkError("Payload too large", 413);
623
+ }
624
+ if (response.status === 401) {
625
+ throw new NetworkError("Invalid API key", 401);
626
+ }
627
+ if (response.status >= 400 && response.status < 500) {
628
+ throw new NetworkError(
629
+ `HTTP ${response.status}: ${response.statusText}`,
630
+ response.status
631
+ );
632
+ }
633
+ lastError = new NetworkError(
634
+ `HTTP ${response.status}: ${response.statusText}`,
635
+ response.status
636
+ );
637
+ } catch (err) {
638
+ if (err instanceof NetworkError && err.statusCode === 413) {
639
+ throw err;
640
+ }
641
+ if (err instanceof NetworkError && err.statusCode && err.statusCode < 500) {
642
+ if (this.onError) this.onError(err);
643
+ return;
644
+ }
645
+ lastError = err instanceof Error ? err : new NetworkError(String(err));
646
+ }
647
+ }
648
+ if (lastError && this.onError) {
649
+ this.onError(lastError);
650
+ }
651
+ }
652
+ backoff(attempt) {
653
+ const base = 1e3 * Math.pow(1.5, attempt - 1);
654
+ const jitter = base * 0.2 * Math.random();
655
+ const delay = Math.min(base + jitter, 3e4);
656
+ return new Promise((resolve) => setTimeout(resolve, delay));
657
+ }
658
+ };
659
+
660
+ // src/queue.ts
661
+ var PreInitQueue = class {
662
+ items = [];
663
+ maxSize;
664
+ constructor(maxSize = 1e3) {
665
+ this.maxSize = maxSize;
666
+ }
667
+ push(call) {
668
+ if (this.items.length >= this.maxSize) {
669
+ this.items.shift();
670
+ }
671
+ this.items.push(call);
672
+ }
673
+ replay(target) {
674
+ const calls = this.items;
675
+ this.items = [];
676
+ for (const call of calls) {
677
+ try {
678
+ target[call.method](...call.args);
679
+ } catch {
680
+ }
681
+ }
682
+ }
683
+ get length() {
684
+ return this.items.length;
685
+ }
686
+ clear() {
687
+ this.items = [];
688
+ }
689
+ };
690
+
691
+ // src/index.ts
692
+ var configured = false;
693
+ var closed = false;
694
+ var _disabled = false;
695
+ var _optedOut = false;
696
+ var storage;
697
+ var transport;
698
+ var eventBatcher;
699
+ var logBatcher;
700
+ var sessionManager;
701
+ var resolvedConfig;
702
+ var _apiKey;
703
+ var deviceId;
704
+ var userId;
705
+ var superProperties = {};
706
+ var beforeSend;
707
+ var beforeSendLog;
708
+ var sdkLogLevel;
709
+ var queue = new PreInitQueue(1e3);
710
+ var unloadHandler = null;
711
+ var visibilityUnloadHandler = null;
712
+ var errorHandler = null;
713
+ var rejectionHandler = null;
714
+ var LOG_LEVELS = {
715
+ error: 0,
716
+ warn: 1,
717
+ info: 2,
718
+ debug: 3
719
+ };
720
+ function reportError(err) {
721
+ if (resolvedConfig?.onError && err instanceof Error) {
722
+ resolvedConfig.onError(err);
723
+ }
724
+ }
725
+ function sdkDebug(msg) {
726
+ if (sdkLogLevel >= LOG_LEVELS.debug) {
727
+ console.debug(`[Tell] ${msg}`);
728
+ }
729
+ }
730
+ function handleUnload() {
731
+ const events = eventBatcher.drain();
732
+ const logs = logBatcher.drain();
733
+ transport.beacon(events, logs);
734
+ }
735
+ function handleVisibilityUnload() {
736
+ if (document.visibilityState === "hidden") {
737
+ handleUnload();
738
+ }
739
+ }
740
+ function onNewSession(reason, sessionId) {
741
+ if (_disabled || _optedOut || closed) return;
742
+ const ctx = captureContext();
743
+ const event = {
744
+ type: "context",
745
+ device_id: deviceId,
746
+ session_id: sessionId,
747
+ user_id: userId,
748
+ timestamp: Date.now(),
749
+ context: { reason, ...ctx }
750
+ };
751
+ eventBatcher.add(event);
752
+ }
753
+ function persistSuperProps() {
754
+ storage.set(STORAGE_KEYS.SUPER_PROPS, JSON.stringify(superProperties));
755
+ }
756
+ function loadSuperProps() {
757
+ const raw = storage.get(STORAGE_KEYS.SUPER_PROPS);
758
+ if (!raw) return {};
759
+ try {
760
+ return JSON.parse(raw);
761
+ } catch {
762
+ storage.remove(STORAGE_KEYS.SUPER_PROPS);
763
+ return {};
764
+ }
765
+ }
766
+ var tell = {
767
+ // -----------------------------------------------------------------------
768
+ // Lifecycle
769
+ // -----------------------------------------------------------------------
770
+ configure(apiKey, options) {
771
+ if (configured) {
772
+ reportError(new ConfigurationError("Tell is already configured"));
773
+ return;
774
+ }
775
+ validateApiKey(apiKey);
776
+ _apiKey = apiKey;
777
+ resolvedConfig = resolveConfig(options);
778
+ sdkLogLevel = LOG_LEVELS[resolvedConfig.logLevel] ?? 0;
779
+ beforeSend = resolvedConfig.beforeSend;
780
+ beforeSendLog = resolvedConfig.beforeSendLog;
781
+ _disabled = resolvedConfig.disabled;
782
+ storage = createStorage(resolvedConfig.persistence);
783
+ deviceId = storage.get(STORAGE_KEYS.DEVICE_ID) ?? generateId();
784
+ storage.set(STORAGE_KEYS.DEVICE_ID, deviceId);
785
+ userId = storage.get(STORAGE_KEYS.USER_ID) ?? void 0;
786
+ _optedOut = storage.get(STORAGE_KEYS.OPT_OUT) === "1";
787
+ superProperties = loadSuperProps();
788
+ const utm = captureUtm();
789
+ if (Object.keys(utm).length > 0) {
790
+ Object.assign(superProperties, utm);
791
+ persistSuperProps();
792
+ }
793
+ if (resolvedConfig.botDetection && isBot()) {
794
+ _disabled = true;
795
+ sdkDebug("bot detected, disabling");
796
+ }
797
+ if (resolvedConfig.respectDoNotTrack && typeof navigator !== "undefined" && navigator.doNotTrack === "1") {
798
+ _disabled = true;
799
+ sdkDebug("Do Not Track enabled, disabling");
800
+ }
801
+ transport = new BrowserTransport({
802
+ endpoint: resolvedConfig.endpoint,
803
+ apiKey: _apiKey,
804
+ maxRetries: resolvedConfig.maxRetries,
805
+ networkTimeout: resolvedConfig.networkTimeout,
806
+ onError: resolvedConfig.onError,
807
+ onPayloadTooLarge: () => {
808
+ eventBatcher.halveBatchSize();
809
+ logBatcher.halveBatchSize();
810
+ sdkDebug("413 received, halved batch size");
811
+ }
812
+ });
813
+ eventBatcher = new Batcher({
814
+ size: resolvedConfig.batchSize,
815
+ interval: resolvedConfig.flushInterval,
816
+ maxQueueSize: resolvedConfig.maxQueueSize,
817
+ send: (items) => transport.sendEvents(items),
818
+ onOverflow: () => sdkDebug("event queue overflow, dropping oldest")
819
+ });
820
+ logBatcher = new Batcher({
821
+ size: resolvedConfig.batchSize,
822
+ interval: resolvedConfig.flushInterval,
823
+ maxQueueSize: resolvedConfig.maxQueueSize,
824
+ send: (items) => transport.sendLogs(items),
825
+ onOverflow: () => sdkDebug("log queue overflow, dropping oldest")
826
+ });
827
+ sessionManager = new SessionManager({
828
+ timeout: resolvedConfig.sessionTimeout,
829
+ onNewSession
830
+ });
831
+ if (typeof window !== "undefined") {
832
+ unloadHandler = handleUnload;
833
+ window.addEventListener("beforeunload", unloadHandler);
834
+ }
835
+ if (typeof document !== "undefined") {
836
+ visibilityUnloadHandler = handleVisibilityUnload;
837
+ document.addEventListener("visibilitychange", visibilityUnloadHandler);
838
+ }
839
+ if (resolvedConfig.captureErrors && typeof window !== "undefined") {
840
+ errorHandler = (event) => {
841
+ if (_disabled || _optedOut || closed) return;
842
+ const msg = event.message || "Unknown error";
843
+ const data = {};
844
+ if (event.filename) data.filename = event.filename;
845
+ if (event.lineno) data.lineno = event.lineno;
846
+ if (event.colno) data.colno = event.colno;
847
+ if (event.error?.stack) data.stack = event.error.stack;
848
+ tell.logError(msg, "browser", data);
849
+ };
850
+ rejectionHandler = (event) => {
851
+ if (_disabled || _optedOut || closed) return;
852
+ const reason = event.reason;
853
+ const msg = reason instanceof Error ? reason.message : String(reason ?? "Unhandled promise rejection");
854
+ const data = {};
855
+ if (reason instanceof Error && reason.stack) data.stack = reason.stack;
856
+ tell.logError(msg, "browser", data);
857
+ };
858
+ window.addEventListener("error", errorHandler);
859
+ window.addEventListener("unhandledrejection", rejectionHandler);
860
+ }
861
+ configured = true;
862
+ closed = false;
863
+ sdkDebug(
864
+ `configured (endpoint=${resolvedConfig.endpoint}, batch=${resolvedConfig.batchSize})`
865
+ );
866
+ queue.replay(tell);
867
+ },
868
+ // -----------------------------------------------------------------------
869
+ // Events
870
+ // -----------------------------------------------------------------------
871
+ track(eventName, properties) {
872
+ if (!configured) {
873
+ queue.push({ method: "track", args: [eventName, properties] });
874
+ return;
875
+ }
876
+ if (_disabled || _optedOut) return;
877
+ if (closed) {
878
+ reportError(new ClosedError());
879
+ return;
880
+ }
881
+ try {
882
+ validateEventName(eventName);
883
+ } catch (err) {
884
+ reportError(err);
885
+ return;
886
+ }
887
+ let event = {
888
+ type: "track",
889
+ event: eventName,
890
+ device_id: deviceId,
891
+ session_id: sessionManager.sessionId,
892
+ user_id: userId,
893
+ timestamp: Date.now(),
894
+ properties: { ...superProperties, ...properties }
895
+ };
896
+ if (beforeSend) {
897
+ event = runBeforeSend(event, beforeSend);
898
+ if (event === null) return;
899
+ }
900
+ sessionManager.touch();
901
+ eventBatcher.add(event);
902
+ },
903
+ identify(newUserId, traits) {
904
+ if (!configured) {
905
+ queue.push({ method: "identify", args: [newUserId, traits] });
906
+ return;
907
+ }
908
+ if (_disabled || _optedOut) return;
909
+ if (closed) {
910
+ reportError(new ClosedError());
911
+ return;
912
+ }
913
+ try {
914
+ validateUserId(newUserId);
915
+ } catch (err) {
916
+ reportError(err);
917
+ return;
918
+ }
919
+ userId = newUserId;
920
+ storage.set(STORAGE_KEYS.USER_ID, userId);
921
+ let event = {
922
+ type: "identify",
923
+ device_id: deviceId,
924
+ session_id: sessionManager.sessionId,
925
+ user_id: userId,
926
+ timestamp: Date.now(),
927
+ traits
928
+ };
929
+ if (beforeSend) {
930
+ event = runBeforeSend(event, beforeSend);
931
+ if (event === null) return;
932
+ }
933
+ sessionManager.touch();
934
+ eventBatcher.add(event);
935
+ },
936
+ group(groupId, properties) {
937
+ if (!configured) {
938
+ queue.push({ method: "group", args: [groupId, properties] });
939
+ return;
940
+ }
941
+ if (_disabled || _optedOut) return;
942
+ if (closed) {
943
+ reportError(new ClosedError());
944
+ return;
945
+ }
946
+ try {
947
+ if (!groupId) throw new ValidationError("groupId", "is required");
948
+ } catch (err) {
949
+ reportError(err);
950
+ return;
951
+ }
952
+ let event = {
953
+ type: "group",
954
+ device_id: deviceId,
955
+ session_id: sessionManager.sessionId,
956
+ user_id: userId,
957
+ group_id: groupId,
958
+ timestamp: Date.now(),
959
+ properties: { ...superProperties, ...properties }
960
+ };
961
+ if (beforeSend) {
962
+ event = runBeforeSend(event, beforeSend);
963
+ if (event === null) return;
964
+ }
965
+ sessionManager.touch();
966
+ eventBatcher.add(event);
967
+ },
968
+ revenue(amount, currency, orderId, properties) {
969
+ if (!configured) {
970
+ queue.push({ method: "revenue", args: [amount, currency, orderId, properties] });
971
+ return;
972
+ }
973
+ if (_disabled || _optedOut) return;
974
+ if (closed) {
975
+ reportError(new ClosedError());
976
+ return;
977
+ }
978
+ try {
979
+ if (amount <= 0) throw new ValidationError("amount", "must be positive");
980
+ if (!currency) throw new ValidationError("currency", "is required");
981
+ if (!orderId) throw new ValidationError("orderId", "is required");
982
+ } catch (err) {
983
+ reportError(err);
984
+ return;
985
+ }
986
+ let event = {
987
+ type: "track",
988
+ event: "Order Completed",
989
+ device_id: deviceId,
990
+ session_id: sessionManager.sessionId,
991
+ user_id: userId,
992
+ timestamp: Date.now(),
993
+ properties: {
994
+ ...superProperties,
995
+ ...properties,
996
+ order_id: orderId,
997
+ amount,
998
+ currency
999
+ }
1000
+ };
1001
+ if (beforeSend) {
1002
+ event = runBeforeSend(event, beforeSend);
1003
+ if (event === null) return;
1004
+ }
1005
+ sessionManager.touch();
1006
+ eventBatcher.add(event);
1007
+ },
1008
+ alias(previousId, newUserId) {
1009
+ if (!configured) {
1010
+ queue.push({ method: "alias", args: [previousId, newUserId] });
1011
+ return;
1012
+ }
1013
+ if (_disabled || _optedOut) return;
1014
+ if (closed) {
1015
+ reportError(new ClosedError());
1016
+ return;
1017
+ }
1018
+ try {
1019
+ if (!previousId)
1020
+ throw new ValidationError("previousId", "is required");
1021
+ validateUserId(newUserId);
1022
+ } catch (err) {
1023
+ reportError(err);
1024
+ return;
1025
+ }
1026
+ let event = {
1027
+ type: "alias",
1028
+ device_id: deviceId,
1029
+ session_id: sessionManager.sessionId,
1030
+ user_id: newUserId,
1031
+ timestamp: Date.now(),
1032
+ properties: { previous_id: previousId }
1033
+ };
1034
+ if (beforeSend) {
1035
+ event = runBeforeSend(event, beforeSend);
1036
+ if (event === null) return;
1037
+ }
1038
+ sessionManager.touch();
1039
+ eventBatcher.add(event);
1040
+ },
1041
+ // -----------------------------------------------------------------------
1042
+ // Logging
1043
+ // -----------------------------------------------------------------------
1044
+ log(level, message, service, data) {
1045
+ if (!configured) {
1046
+ queue.push({ method: "log", args: [level, message, service, data] });
1047
+ return;
1048
+ }
1049
+ if (_disabled || _optedOut) return;
1050
+ if (closed) {
1051
+ reportError(new ClosedError());
1052
+ return;
1053
+ }
1054
+ try {
1055
+ validateLogMessage(message);
1056
+ } catch (err) {
1057
+ reportError(err);
1058
+ return;
1059
+ }
1060
+ let logEntry = {
1061
+ level,
1062
+ message,
1063
+ source: resolvedConfig.source,
1064
+ service: service ?? "app",
1065
+ session_id: sessionManager.sessionId,
1066
+ timestamp: Date.now(),
1067
+ data
1068
+ };
1069
+ if (beforeSendLog) {
1070
+ logEntry = runBeforeSend(logEntry, beforeSendLog);
1071
+ if (logEntry === null) return;
1072
+ }
1073
+ logBatcher.add(logEntry);
1074
+ },
1075
+ logEmergency(message, service, data) {
1076
+ tell.log("emergency", message, service, data);
1077
+ },
1078
+ logAlert(message, service, data) {
1079
+ tell.log("alert", message, service, data);
1080
+ },
1081
+ logCritical(message, service, data) {
1082
+ tell.log("critical", message, service, data);
1083
+ },
1084
+ logError(message, service, data) {
1085
+ tell.log("error", message, service, data);
1086
+ },
1087
+ logWarning(message, service, data) {
1088
+ tell.log("warning", message, service, data);
1089
+ },
1090
+ logNotice(message, service, data) {
1091
+ tell.log("notice", message, service, data);
1092
+ },
1093
+ logInfo(message, service, data) {
1094
+ tell.log("info", message, service, data);
1095
+ },
1096
+ logDebug(message, service, data) {
1097
+ tell.log("debug", message, service, data);
1098
+ },
1099
+ logTrace(message, service, data) {
1100
+ tell.log("trace", message, service, data);
1101
+ },
1102
+ // -----------------------------------------------------------------------
1103
+ // Super Properties
1104
+ // -----------------------------------------------------------------------
1105
+ register(properties) {
1106
+ if (!configured) {
1107
+ queue.push({ method: "register", args: [properties] });
1108
+ return;
1109
+ }
1110
+ Object.assign(superProperties, properties);
1111
+ persistSuperProps();
1112
+ },
1113
+ unregister(key) {
1114
+ if (!configured) {
1115
+ queue.push({ method: "unregister", args: [key] });
1116
+ return;
1117
+ }
1118
+ delete superProperties[key];
1119
+ persistSuperProps();
1120
+ },
1121
+ // -----------------------------------------------------------------------
1122
+ // Privacy
1123
+ // -----------------------------------------------------------------------
1124
+ optOut() {
1125
+ if (!configured) {
1126
+ queue.push({ method: "optOut", args: [] });
1127
+ return;
1128
+ }
1129
+ _optedOut = true;
1130
+ storage.set(STORAGE_KEYS.OPT_OUT, "1");
1131
+ },
1132
+ optIn() {
1133
+ if (!configured) {
1134
+ queue.push({ method: "optIn", args: [] });
1135
+ return;
1136
+ }
1137
+ _optedOut = false;
1138
+ storage.remove(STORAGE_KEYS.OPT_OUT);
1139
+ },
1140
+ isOptedOut() {
1141
+ return _optedOut;
1142
+ },
1143
+ // -----------------------------------------------------------------------
1144
+ // Control
1145
+ // -----------------------------------------------------------------------
1146
+ enable() {
1147
+ _disabled = false;
1148
+ },
1149
+ disable() {
1150
+ _disabled = true;
1151
+ },
1152
+ // -----------------------------------------------------------------------
1153
+ // Lifecycle
1154
+ // -----------------------------------------------------------------------
1155
+ async flush() {
1156
+ if (!configured) return;
1157
+ await Promise.all([eventBatcher.flush(), logBatcher.flush()]);
1158
+ },
1159
+ async close() {
1160
+ if (!configured || closed) return;
1161
+ closed = true;
1162
+ if (sessionManager) sessionManager.destroy();
1163
+ if (typeof window !== "undefined" && unloadHandler) {
1164
+ window.removeEventListener("beforeunload", unloadHandler);
1165
+ unloadHandler = null;
1166
+ }
1167
+ if (typeof document !== "undefined" && visibilityUnloadHandler) {
1168
+ document.removeEventListener("visibilitychange", visibilityUnloadHandler);
1169
+ visibilityUnloadHandler = null;
1170
+ }
1171
+ if (typeof window !== "undefined") {
1172
+ if (errorHandler) {
1173
+ window.removeEventListener("error", errorHandler);
1174
+ errorHandler = null;
1175
+ }
1176
+ if (rejectionHandler) {
1177
+ window.removeEventListener("unhandledrejection", rejectionHandler);
1178
+ rejectionHandler = null;
1179
+ }
1180
+ }
1181
+ const work = Promise.all([eventBatcher.close(), logBatcher.close()]);
1182
+ const timeout = new Promise(
1183
+ (_, reject) => setTimeout(
1184
+ () => reject(new Error("close timed out")),
1185
+ resolvedConfig.closeTimeout
1186
+ )
1187
+ );
1188
+ try {
1189
+ await Promise.race([work, timeout]);
1190
+ } catch (err) {
1191
+ reportError(err);
1192
+ }
1193
+ },
1194
+ reset() {
1195
+ if (!configured) return;
1196
+ userId = void 0;
1197
+ deviceId = generateId();
1198
+ superProperties = {};
1199
+ storage.remove(STORAGE_KEYS.USER_ID);
1200
+ storage.set(STORAGE_KEYS.DEVICE_ID, deviceId);
1201
+ storage.remove(STORAGE_KEYS.SUPER_PROPS);
1202
+ if (sessionManager) {
1203
+ sessionManager.sessionId = generateId();
1204
+ }
1205
+ },
1206
+ // -----------------------------------------------------------------------
1207
+ // Testing helper
1208
+ // -----------------------------------------------------------------------
1209
+ _resetForTesting() {
1210
+ if (configured && !closed) {
1211
+ if (sessionManager) sessionManager.destroy();
1212
+ if (eventBatcher) {
1213
+ eventBatcher.drain();
1214
+ eventBatcher.close().catch(() => {
1215
+ });
1216
+ }
1217
+ if (logBatcher) {
1218
+ logBatcher.drain();
1219
+ logBatcher.close().catch(() => {
1220
+ });
1221
+ }
1222
+ if (typeof window !== "undefined" && unloadHandler) {
1223
+ window.removeEventListener("beforeunload", unloadHandler);
1224
+ }
1225
+ if (typeof document !== "undefined" && visibilityUnloadHandler) {
1226
+ document.removeEventListener(
1227
+ "visibilitychange",
1228
+ visibilityUnloadHandler
1229
+ );
1230
+ }
1231
+ if (typeof window !== "undefined") {
1232
+ if (errorHandler) window.removeEventListener("error", errorHandler);
1233
+ if (rejectionHandler)
1234
+ window.removeEventListener("unhandledrejection", rejectionHandler);
1235
+ }
1236
+ }
1237
+ configured = false;
1238
+ closed = false;
1239
+ _disabled = false;
1240
+ _optedOut = false;
1241
+ userId = void 0;
1242
+ deviceId = "";
1243
+ superProperties = {};
1244
+ beforeSend = void 0;
1245
+ beforeSendLog = void 0;
1246
+ sdkLogLevel = 0;
1247
+ unloadHandler = null;
1248
+ visibilityUnloadHandler = null;
1249
+ errorHandler = null;
1250
+ rejectionHandler = null;
1251
+ queue.clear();
1252
+ }
1253
+ };
1254
+ var index_default = tell;
1255
+ export {
1256
+ ClosedError,
1257
+ ConfigurationError,
1258
+ Events,
1259
+ NetworkError,
1260
+ SerializationError,
1261
+ TellError,
1262
+ ValidationError,
1263
+ index_default as default,
1264
+ development,
1265
+ production,
1266
+ tell
1267
+ };
1268
+ //# sourceMappingURL=index.js.map