@outlit/browser 1.1.0 → 1.3.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.
@@ -0,0 +1,1206 @@
1
+ // src/vue/plugin.ts
2
+ import { readonly, ref, shallowRef, watch } from "vue";
3
+
4
+ // src/tracker.ts
5
+ import {
6
+ DEFAULT_API_HOST,
7
+ buildBillingEvent,
8
+ buildCalendarEvent,
9
+ buildCustomEvent,
10
+ buildFormEvent,
11
+ buildIdentifyEvent,
12
+ buildIngestPayload,
13
+ buildPageviewEvent,
14
+ buildStageEvent
15
+ } from "@outlit/core";
16
+
17
+ // src/autocapture.ts
18
+ import { extractIdentityFromForm, sanitizeFormFields } from "@outlit/core";
19
+ var pageviewCallback = null;
20
+ var lastUrl = null;
21
+ function initPageviewTracking(callback) {
22
+ pageviewCallback = callback;
23
+ capturePageview();
24
+ setupSpaListeners();
25
+ }
26
+ function capturePageview() {
27
+ if (!pageviewCallback) return;
28
+ const url = window.location.href;
29
+ const referrer = document.referrer;
30
+ const title = document.title;
31
+ if (url === lastUrl) return;
32
+ lastUrl = url;
33
+ pageviewCallback(url, referrer, title);
34
+ }
35
+ function capturePageviewDelayed() {
36
+ setTimeout(capturePageview, 10);
37
+ }
38
+ function setupSpaListeners() {
39
+ window.addEventListener("popstate", () => {
40
+ capturePageviewDelayed();
41
+ });
42
+ const originalPushState = history.pushState;
43
+ const originalReplaceState = history.replaceState;
44
+ history.pushState = function(...args) {
45
+ originalPushState.apply(this, args);
46
+ capturePageviewDelayed();
47
+ };
48
+ history.replaceState = function(...args) {
49
+ originalReplaceState.apply(this, args);
50
+ capturePageviewDelayed();
51
+ };
52
+ }
53
+ var formCallback = null;
54
+ var formDenylist;
55
+ var identityCallback = null;
56
+ function initFormTracking(callback, denylist, onIdentity) {
57
+ formCallback = callback;
58
+ formDenylist = denylist;
59
+ identityCallback = onIdentity ?? null;
60
+ document.addEventListener("submit", handleFormSubmit, true);
61
+ }
62
+ function handleFormSubmit(event) {
63
+ if (!formCallback) return;
64
+ const form = event.target;
65
+ if (!(form instanceof HTMLFormElement)) return;
66
+ const url = window.location.href;
67
+ const formId = form.id || form.name || void 0;
68
+ const formData = new FormData(form);
69
+ const fields = {};
70
+ const inputTypes = /* @__PURE__ */ new Map();
71
+ const inputs = form.querySelectorAll("input, select, textarea");
72
+ for (const input of inputs) {
73
+ const name = input.getAttribute("name");
74
+ if (name && input instanceof HTMLInputElement) {
75
+ inputTypes.set(name, input.type);
76
+ }
77
+ }
78
+ formData.forEach((value, key) => {
79
+ if (typeof value === "string") {
80
+ fields[key] = value;
81
+ }
82
+ });
83
+ const sanitizedFields = sanitizeFormFields(fields, formDenylist);
84
+ if (identityCallback) {
85
+ const identity = extractIdentityFromForm(fields, inputTypes);
86
+ if (identity) {
87
+ identityCallback(identity);
88
+ }
89
+ }
90
+ if (sanitizedFields && Object.keys(sanitizedFields).length > 0) {
91
+ formCallback(url, formId, sanitizedFields);
92
+ }
93
+ }
94
+ function stopAutocapture() {
95
+ pageviewCallback = null;
96
+ formCallback = null;
97
+ identityCallback = null;
98
+ document.removeEventListener("submit", handleFormSubmit, true);
99
+ }
100
+
101
+ // src/embed-integrations.ts
102
+ var callbacks = null;
103
+ var isListening = false;
104
+ var calSetupAttempts = 0;
105
+ var calCallbackRegistered = false;
106
+ var lastBookingUid = null;
107
+ var CAL_MAX_RETRY_ATTEMPTS = 10;
108
+ var CAL_INITIAL_DELAY_MS = 200;
109
+ var CAL_MAX_DELAY_MS = 2e3;
110
+ function parseCalComBooking(data) {
111
+ const event = {
112
+ provider: "cal.com"
113
+ };
114
+ if (data.title) {
115
+ event.eventType = data.title;
116
+ const nameMatch = data.title.match(/between .+ and (.+)$/i);
117
+ if (nameMatch?.[1]) {
118
+ event.inviteeName = nameMatch[1].trim();
119
+ }
120
+ }
121
+ if (data.startTime) event.startTime = data.startTime;
122
+ if (data.endTime) event.endTime = data.endTime;
123
+ if (data.startTime && data.endTime) {
124
+ const start = new Date(data.startTime);
125
+ const end = new Date(data.endTime);
126
+ event.duration = Math.round((end.getTime() - start.getTime()) / 6e4);
127
+ }
128
+ if (data.isRecurring !== void 0) {
129
+ event.isRecurring = data.isRecurring;
130
+ }
131
+ return event;
132
+ }
133
+ function setupCalComListener() {
134
+ if (typeof window === "undefined") return;
135
+ if (calCallbackRegistered) return;
136
+ calSetupAttempts++;
137
+ if ("Cal" in window) {
138
+ const Cal = window.Cal;
139
+ if (typeof Cal === "function") {
140
+ try {
141
+ Cal("on", {
142
+ action: "bookingSuccessfulV2",
143
+ callback: handleCalComBooking
144
+ });
145
+ calCallbackRegistered = true;
146
+ return;
147
+ } catch (_e) {
148
+ }
149
+ }
150
+ }
151
+ if (calSetupAttempts < CAL_MAX_RETRY_ATTEMPTS) {
152
+ const delay = Math.min(CAL_INITIAL_DELAY_MS * calSetupAttempts, CAL_MAX_DELAY_MS);
153
+ setTimeout(setupCalComListener, delay);
154
+ }
155
+ }
156
+ function handleCalComBooking(e) {
157
+ if (!callbacks) return;
158
+ const data = e.detail?.data;
159
+ if (!data) return;
160
+ if (data.uid && data.uid === lastBookingUid) return;
161
+ lastBookingUid = data.uid || null;
162
+ const bookingEvent = parseCalComBooking(data);
163
+ callbacks.onCalendarBooked(bookingEvent);
164
+ }
165
+ function handlePostMessage(event) {
166
+ if (!callbacks) return;
167
+ if (isCalendlyEvent(event)) {
168
+ if (event.data.event === "calendly.event_scheduled") {
169
+ const bookingEvent = parseCalendlyBooking(event.data.payload);
170
+ callbacks.onCalendarBooked(bookingEvent);
171
+ }
172
+ return;
173
+ }
174
+ if (isCalComRawMessage(event)) {
175
+ const bookingData = extractCalComBookingFromMessage(event.data);
176
+ if (bookingData) {
177
+ if (bookingData.uid && bookingData.uid === lastBookingUid) return;
178
+ lastBookingUid = bookingData.uid || null;
179
+ const bookingEvent = parseCalComBooking(bookingData);
180
+ callbacks.onCalendarBooked(bookingEvent);
181
+ }
182
+ }
183
+ }
184
+ function isCalComRawMessage(event) {
185
+ if (!event.origin.includes("cal.com")) return false;
186
+ const data = event.data;
187
+ if (!data || typeof data !== "object") return false;
188
+ const messageType = data.type || data.action;
189
+ return messageType === "bookingSuccessfulV2" || messageType === "bookingSuccessful" || messageType === "booking_successful";
190
+ }
191
+ function extractCalComBookingFromMessage(data) {
192
+ if (!data || typeof data !== "object") return null;
193
+ const messageData = data;
194
+ if (messageData.data && typeof messageData.data === "object") {
195
+ return messageData.data;
196
+ }
197
+ if (messageData.booking && typeof messageData.booking === "object") {
198
+ return messageData.booking;
199
+ }
200
+ return null;
201
+ }
202
+ function isCalendlyEvent(e) {
203
+ return e.origin === "https://calendly.com" && e.data && typeof e.data.event === "string" && e.data.event.startsWith("calendly.");
204
+ }
205
+ function parseCalendlyBooking(_payload) {
206
+ return {
207
+ provider: "calendly"
208
+ };
209
+ }
210
+ function initCalendarTracking(cbs) {
211
+ if (isListening) return;
212
+ callbacks = cbs;
213
+ isListening = true;
214
+ calSetupAttempts = 0;
215
+ window.addEventListener("message", handlePostMessage);
216
+ setupCalComListener();
217
+ }
218
+ function stopCalendarTracking() {
219
+ if (!isListening) return;
220
+ window.removeEventListener("message", handlePostMessage);
221
+ callbacks = null;
222
+ isListening = false;
223
+ calCallbackRegistered = false;
224
+ calSetupAttempts = 0;
225
+ lastBookingUid = null;
226
+ }
227
+
228
+ // src/session-tracker.ts
229
+ import { buildEngagementEvent } from "@outlit/core";
230
+ var DEFAULT_IDLE_TIMEOUT = 3e4;
231
+ var SESSION_TIMEOUT = 30 * 60 * 1e3;
232
+ var TIME_UPDATE_INTERVAL = 1e3;
233
+ var MIN_SPURIOUS_THRESHOLD = 50;
234
+ var MIN_PAGE_TIME_FOR_ENGAGEMENT = 500;
235
+ var SESSION_ID_KEY = "outlit_session_id";
236
+ var SESSION_LAST_ACTIVITY_KEY = "outlit_session_last_activity";
237
+ var SessionTracker = class {
238
+ state;
239
+ options;
240
+ idleTimeout;
241
+ timeUpdateInterval = null;
242
+ boundHandleActivity;
243
+ boundHandleVisibilityChange;
244
+ constructor(options) {
245
+ this.options = options;
246
+ this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT;
247
+ this.state = this.createInitialState();
248
+ this.boundHandleActivity = this.handleActivity.bind(this);
249
+ this.boundHandleVisibilityChange = this.handleVisibilityChange.bind(this);
250
+ this.setupEventListeners();
251
+ this.startTimeUpdateInterval();
252
+ }
253
+ /**
254
+ * Get the current session ID.
255
+ */
256
+ getSessionId() {
257
+ return this.state.sessionId;
258
+ }
259
+ // ============================================
260
+ // PUBLIC METHODS
261
+ // ============================================
262
+ /**
263
+ * Emit an engagement event for the current page session.
264
+ * Called by Tracker on exit events and SPA navigation.
265
+ *
266
+ * This method:
267
+ * 1. Finalizes any pending active time
268
+ * 2. Creates and emits the engagement event (if meaningful)
269
+ * 3. Resets state for the next session
270
+ */
271
+ emitEngagement() {
272
+ if (this.state.hasEmittedEngagement) {
273
+ return;
274
+ }
275
+ this.state.hasEmittedEngagement = true;
276
+ this.updateActiveTime();
277
+ const totalTimeMs = Date.now() - this.state.pageEntryTime;
278
+ const isSpuriousEvent = this.state.activeTimeMs < MIN_SPURIOUS_THRESHOLD && totalTimeMs < MIN_SPURIOUS_THRESHOLD;
279
+ const isTooSoonAfterNavigation = totalTimeMs < MIN_PAGE_TIME_FOR_ENGAGEMENT;
280
+ if (!isSpuriousEvent && !isTooSoonAfterNavigation) {
281
+ const event = buildEngagementEvent({
282
+ url: this.state.currentUrl,
283
+ referrer: document.referrer,
284
+ activeTimeMs: this.state.activeTimeMs,
285
+ totalTimeMs,
286
+ sessionId: this.state.sessionId
287
+ });
288
+ this.options.onEngagement(event);
289
+ }
290
+ this.resetState();
291
+ }
292
+ /**
293
+ * Handle SPA navigation.
294
+ * Called by Tracker when a new pageview is detected.
295
+ *
296
+ * This method:
297
+ * 1. Emits engagement for the OLD page (using stored state)
298
+ * 2. Updates state for the NEW page
299
+ */
300
+ onNavigation(newUrl) {
301
+ this.emitEngagement();
302
+ this.state.currentUrl = newUrl;
303
+ this.state.currentPath = this.extractPath(newUrl);
304
+ this.state.pageEntryTime = Date.now();
305
+ this.state.activeTimeMs = 0;
306
+ this.state.lastActiveTime = Date.now();
307
+ this.state.isUserActive = true;
308
+ this.state.hasEmittedEngagement = false;
309
+ this.resetIdleTimer();
310
+ }
311
+ /**
312
+ * Stop session tracking and clean up.
313
+ */
314
+ stop() {
315
+ this.removeEventListeners();
316
+ if (this.timeUpdateInterval) {
317
+ clearInterval(this.timeUpdateInterval);
318
+ this.timeUpdateInterval = null;
319
+ }
320
+ if (this.state.idleTimeoutId) {
321
+ clearTimeout(this.state.idleTimeoutId);
322
+ this.state.idleTimeoutId = null;
323
+ }
324
+ }
325
+ // ============================================
326
+ // PRIVATE METHODS
327
+ // ============================================
328
+ createInitialState() {
329
+ const now = Date.now();
330
+ return {
331
+ currentUrl: typeof window !== "undefined" ? window.location.href : "",
332
+ currentPath: typeof window !== "undefined" ? window.location.pathname : "/",
333
+ pageEntryTime: now,
334
+ lastActiveTime: now,
335
+ activeTimeMs: 0,
336
+ isPageVisible: typeof document !== "undefined" ? document.visibilityState === "visible" : true,
337
+ isUserActive: true,
338
+ // Assume active on page load
339
+ idleTimeoutId: null,
340
+ sessionId: this.getOrCreateSessionId(),
341
+ hasEmittedEngagement: false
342
+ };
343
+ }
344
+ resetState() {
345
+ const now = Date.now();
346
+ this.state.pageEntryTime = now;
347
+ this.state.lastActiveTime = now;
348
+ this.state.activeTimeMs = 0;
349
+ this.state.isUserActive = true;
350
+ this.resetIdleTimer();
351
+ }
352
+ /**
353
+ * Get existing session ID from storage or create a new one.
354
+ * Session ID is reset if:
355
+ * - No existing session ID in storage
356
+ * - Last activity was more than 30 minutes ago
357
+ */
358
+ getOrCreateSessionId() {
359
+ if (typeof sessionStorage === "undefined") {
360
+ return this.generateSessionId();
361
+ }
362
+ try {
363
+ const existingSessionId = sessionStorage.getItem(SESSION_ID_KEY);
364
+ const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY);
365
+ const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0;
366
+ const now = Date.now();
367
+ if (existingSessionId && lastActivity && now - lastActivity < SESSION_TIMEOUT) {
368
+ this.updateSessionActivity();
369
+ return existingSessionId;
370
+ }
371
+ const newSessionId = this.generateSessionId();
372
+ sessionStorage.setItem(SESSION_ID_KEY, newSessionId);
373
+ sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString());
374
+ return newSessionId;
375
+ } catch {
376
+ return this.generateSessionId();
377
+ }
378
+ }
379
+ /**
380
+ * Generate a new session ID (UUID v4).
381
+ */
382
+ generateSessionId() {
383
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
384
+ return crypto.randomUUID();
385
+ }
386
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
387
+ const r = Math.random() * 16 | 0;
388
+ const v = c === "x" ? r : r & 3 | 8;
389
+ return v.toString(16);
390
+ });
391
+ }
392
+ /**
393
+ * Update the session's last activity timestamp.
394
+ */
395
+ updateSessionActivity() {
396
+ if (typeof sessionStorage === "undefined") return;
397
+ try {
398
+ sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, Date.now().toString());
399
+ } catch {
400
+ }
401
+ }
402
+ /**
403
+ * Check if the current session has expired and create a new one if needed.
404
+ * Called when user returns to the page after being away.
405
+ */
406
+ checkSessionExpiry() {
407
+ if (typeof sessionStorage === "undefined") return;
408
+ try {
409
+ const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY);
410
+ const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0;
411
+ const now = Date.now();
412
+ if (now - lastActivity >= SESSION_TIMEOUT) {
413
+ const newSessionId = this.generateSessionId();
414
+ sessionStorage.setItem(SESSION_ID_KEY, newSessionId);
415
+ this.state.sessionId = newSessionId;
416
+ }
417
+ sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString());
418
+ } catch {
419
+ }
420
+ }
421
+ setupEventListeners() {
422
+ if (typeof window === "undefined" || typeof document === "undefined") return;
423
+ const activityEvents = ["mousemove", "keydown", "click", "scroll", "touchstart"];
424
+ for (const event of activityEvents) {
425
+ document.addEventListener(event, this.boundHandleActivity, { passive: true });
426
+ }
427
+ document.addEventListener("visibilitychange", this.boundHandleVisibilityChange);
428
+ this.resetIdleTimer();
429
+ }
430
+ removeEventListeners() {
431
+ if (typeof window === "undefined" || typeof document === "undefined") return;
432
+ const activityEvents = ["mousemove", "keydown", "click", "scroll", "touchstart"];
433
+ for (const event of activityEvents) {
434
+ document.removeEventListener(event, this.boundHandleActivity);
435
+ }
436
+ document.removeEventListener("visibilitychange", this.boundHandleVisibilityChange);
437
+ }
438
+ /**
439
+ * Handle user activity events.
440
+ * Marks user as active and resets idle timer.
441
+ */
442
+ handleActivity() {
443
+ if (!this.state.isUserActive) {
444
+ this.checkSessionExpiry();
445
+ this.state.lastActiveTime = Date.now();
446
+ }
447
+ this.state.isUserActive = true;
448
+ this.resetIdleTimer();
449
+ this.updateSessionActivity();
450
+ }
451
+ /**
452
+ * Handle visibility change events.
453
+ * Pauses time accumulation when tab is hidden.
454
+ */
455
+ handleVisibilityChange() {
456
+ const wasVisible = this.state.isPageVisible;
457
+ const isNowVisible = document.visibilityState === "visible";
458
+ if (wasVisible && !isNowVisible) {
459
+ this.updateActiveTime();
460
+ }
461
+ this.state.isPageVisible = isNowVisible;
462
+ if (!wasVisible && isNowVisible) {
463
+ this.checkSessionExpiry();
464
+ this.state.lastActiveTime = Date.now();
465
+ this.state.hasEmittedEngagement = false;
466
+ this.updateSessionActivity();
467
+ }
468
+ }
469
+ /**
470
+ * Reset the idle timer.
471
+ * Called on activity and initialization.
472
+ */
473
+ resetIdleTimer() {
474
+ if (this.state.idleTimeoutId) {
475
+ clearTimeout(this.state.idleTimeoutId);
476
+ }
477
+ this.state.idleTimeoutId = setTimeout(() => {
478
+ this.updateActiveTime();
479
+ this.state.isUserActive = false;
480
+ }, this.idleTimeout);
481
+ }
482
+ /**
483
+ * Start the interval for updating active time.
484
+ */
485
+ startTimeUpdateInterval() {
486
+ if (this.timeUpdateInterval) return;
487
+ this.timeUpdateInterval = setInterval(() => {
488
+ this.updateActiveTime();
489
+ }, TIME_UPDATE_INTERVAL);
490
+ }
491
+ /**
492
+ * Update accumulated active time.
493
+ * Only accumulates when page is visible AND user is active.
494
+ */
495
+ updateActiveTime() {
496
+ if (this.state.isPageVisible && this.state.isUserActive) {
497
+ const now = Date.now();
498
+ this.state.activeTimeMs += now - this.state.lastActiveTime;
499
+ this.state.lastActiveTime = now;
500
+ }
501
+ }
502
+ /**
503
+ * Extract path from URL.
504
+ */
505
+ extractPath(url) {
506
+ try {
507
+ return new URL(url).pathname;
508
+ } catch {
509
+ return "/";
510
+ }
511
+ }
512
+ };
513
+ var sessionTrackerInstance = null;
514
+ function initSessionTracking(options) {
515
+ if (sessionTrackerInstance) {
516
+ console.warn("[Outlit] Session tracking already initialized");
517
+ return sessionTrackerInstance;
518
+ }
519
+ sessionTrackerInstance = new SessionTracker(options);
520
+ return sessionTrackerInstance;
521
+ }
522
+ function stopSessionTracking() {
523
+ if (sessionTrackerInstance) {
524
+ sessionTrackerInstance.stop();
525
+ sessionTrackerInstance = null;
526
+ }
527
+ }
528
+
529
+ // src/storage.ts
530
+ var VISITOR_ID_KEY = "outlit_visitor_id";
531
+ function generateVisitorId() {
532
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
533
+ return crypto.randomUUID();
534
+ }
535
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
536
+ const r = Math.random() * 16 | 0;
537
+ const v = c === "x" ? r : r & 3 | 8;
538
+ return v.toString(16);
539
+ });
540
+ }
541
+ function getOrCreateVisitorId() {
542
+ try {
543
+ const stored = localStorage.getItem(VISITOR_ID_KEY);
544
+ if (stored && isValidUuid(stored)) {
545
+ return stored;
546
+ }
547
+ } catch {
548
+ }
549
+ const cookieValue = getCookie(VISITOR_ID_KEY);
550
+ if (cookieValue && isValidUuid(cookieValue)) {
551
+ try {
552
+ localStorage.setItem(VISITOR_ID_KEY, cookieValue);
553
+ } catch {
554
+ }
555
+ return cookieValue;
556
+ }
557
+ const visitorId = generateVisitorId();
558
+ persistVisitorId(visitorId);
559
+ return visitorId;
560
+ }
561
+ function persistVisitorId(visitorId) {
562
+ try {
563
+ localStorage.setItem(VISITOR_ID_KEY, visitorId);
564
+ } catch {
565
+ }
566
+ setCookie(VISITOR_ID_KEY, visitorId, 365);
567
+ }
568
+ function isValidUuid(value) {
569
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
570
+ }
571
+ function getCookie(name) {
572
+ if (typeof document === "undefined") return null;
573
+ const value = `; ${document.cookie}`;
574
+ const parts = value.split(`; ${name}=`);
575
+ if (parts.length === 2) {
576
+ return parts.pop()?.split(";").shift() ?? null;
577
+ }
578
+ return null;
579
+ }
580
+ function getRootDomain() {
581
+ if (typeof window === "undefined") return null;
582
+ const hostname = window.location.hostname;
583
+ if (hostname === "localhost" || /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
584
+ return null;
585
+ }
586
+ const parts = hostname.split(".");
587
+ if (parts.length >= 2) {
588
+ const twoPartTlds = ["co.uk", "com.au", "co.nz", "org.uk", "net.au", "com.br"];
589
+ const lastTwo = parts.slice(-2).join(".");
590
+ if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {
591
+ return parts.slice(-3).join(".");
592
+ }
593
+ return parts.slice(-2).join(".");
594
+ }
595
+ return null;
596
+ }
597
+ function setCookie(name, value, days) {
598
+ if (typeof document === "undefined") return;
599
+ const expires = /* @__PURE__ */ new Date();
600
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1e3);
601
+ let cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
602
+ const rootDomain = getRootDomain();
603
+ if (rootDomain) {
604
+ cookie += `;domain=${rootDomain}`;
605
+ }
606
+ document.cookie = cookie;
607
+ }
608
+ var CONSENT_KEY = "outlit_consent";
609
+ function getConsentState() {
610
+ try {
611
+ const stored = localStorage.getItem(CONSENT_KEY);
612
+ if (stored === "1") return true;
613
+ if (stored === "0") return false;
614
+ } catch {
615
+ }
616
+ const cookieValue = getCookie(CONSENT_KEY);
617
+ if (cookieValue === "1") return true;
618
+ if (cookieValue === "0") return false;
619
+ return null;
620
+ }
621
+ function setConsentState(granted) {
622
+ const value = granted ? "1" : "0";
623
+ try {
624
+ localStorage.setItem(CONSENT_KEY, value);
625
+ } catch {
626
+ }
627
+ setCookie(CONSENT_KEY, value, 365);
628
+ }
629
+
630
+ // src/tracker.ts
631
+ var MAX_PENDING_STAGE_EVENTS = 10;
632
+ var Outlit = class {
633
+ publicKey;
634
+ apiHost;
635
+ visitorId = null;
636
+ eventQueue = [];
637
+ flushTimer = null;
638
+ flushInterval;
639
+ isInitialized = false;
640
+ isTrackingEnabled = false;
641
+ options;
642
+ hasHandledExit = false;
643
+ sessionTracker = null;
644
+ // User identity state for stage events
645
+ currentUser = null;
646
+ pendingUser = null;
647
+ pendingStageEvents = [];
648
+ constructor(options) {
649
+ this.publicKey = options.publicKey;
650
+ this.apiHost = options.apiHost ?? DEFAULT_API_HOST;
651
+ this.flushInterval = options.flushInterval ?? 5e3;
652
+ this.options = options;
653
+ if (typeof window !== "undefined") {
654
+ const handleExit = () => {
655
+ if (this.hasHandledExit) return;
656
+ this.hasHandledExit = true;
657
+ this.sessionTracker?.emitEngagement();
658
+ this.flush();
659
+ };
660
+ document.addEventListener("visibilitychange", () => {
661
+ if (document.visibilityState === "hidden") {
662
+ handleExit();
663
+ } else {
664
+ this.hasHandledExit = false;
665
+ }
666
+ });
667
+ window.addEventListener("pagehide", handleExit);
668
+ window.addEventListener("beforeunload", handleExit);
669
+ }
670
+ this.isInitialized = true;
671
+ const consent = getConsentState();
672
+ if (consent === true || consent === null && options.autoTrack !== false) {
673
+ this.enableTracking();
674
+ }
675
+ }
676
+ // ============================================
677
+ // PUBLIC API
678
+ // ============================================
679
+ /**
680
+ * Enable tracking. Call this after obtaining user consent.
681
+ * This will:
682
+ * - Generate/retrieve the visitor ID
683
+ * - Start automatic pageview and form tracking (if configured)
684
+ * - Begin sending events to the server
685
+ *
686
+ * If autoTrack is true (default), this is called automatically on init.
687
+ */
688
+ enableTracking() {
689
+ if (this.isTrackingEnabled) {
690
+ return;
691
+ }
692
+ this.visitorId = getOrCreateVisitorId();
693
+ this.startFlushTimer();
694
+ this.initSessionTracking();
695
+ if (this.options.trackPageviews !== false) {
696
+ this.initPageviewTracking();
697
+ }
698
+ if (this.options.trackForms !== false) {
699
+ this.initFormTracking(this.options.formFieldDenylist);
700
+ }
701
+ if (this.options.trackCalendarEmbeds !== false) {
702
+ this.initCalendarTracking();
703
+ }
704
+ this.isTrackingEnabled = true;
705
+ setConsentState(true);
706
+ if (this.pendingUser) {
707
+ this.applyUser(this.pendingUser);
708
+ this.pendingUser = null;
709
+ }
710
+ }
711
+ /**
712
+ * Disable tracking. Call this when a user revokes consent.
713
+ * This will:
714
+ * - Flush any pending events (captured while user had consent)
715
+ * - Stop the flush timer, pageview tracking, form tracking, and session tracking
716
+ * - Persist the opt-out decision so it's remembered across sessions
717
+ *
718
+ * The SDK instance remains usable — enableTracking() can be called again to re-enable.
719
+ */
720
+ async disableTracking() {
721
+ if (!this.isTrackingEnabled) {
722
+ setConsentState(false);
723
+ return;
724
+ }
725
+ if (this.flushTimer) {
726
+ clearInterval(this.flushTimer);
727
+ this.flushTimer = null;
728
+ }
729
+ stopAutocapture();
730
+ stopCalendarTracking();
731
+ stopSessionTracking();
732
+ await this.flush();
733
+ this.sessionTracker = null;
734
+ this.isTrackingEnabled = false;
735
+ setConsentState(false);
736
+ }
737
+ /**
738
+ * Check if tracking is currently enabled.
739
+ */
740
+ isEnabled() {
741
+ return this.isTrackingEnabled;
742
+ }
743
+ /**
744
+ * Track a custom event.
745
+ */
746
+ track(eventName, properties) {
747
+ if (!this.isTrackingEnabled) {
748
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
749
+ return;
750
+ }
751
+ const event = buildCustomEvent({
752
+ url: window.location.href,
753
+ referrer: document.referrer,
754
+ eventName,
755
+ properties
756
+ });
757
+ this.enqueue(event);
758
+ }
759
+ /**
760
+ * Identify the current visitor.
761
+ * Links the anonymous visitor to a known user.
762
+ *
763
+ * When email or userId is provided, also sets the current user identity
764
+ * for stage events (activate, engaged, inactive).
765
+ */
766
+ identify(options) {
767
+ if (!this.isTrackingEnabled) {
768
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
769
+ return;
770
+ }
771
+ if (options.email || options.userId) {
772
+ const hadNoUser = !this.currentUser;
773
+ this.currentUser = {
774
+ email: options.email,
775
+ userId: options.userId
776
+ };
777
+ if (hadNoUser) {
778
+ this.flushPendingStageEvents();
779
+ }
780
+ }
781
+ const event = buildIdentifyEvent({
782
+ url: window.location.href,
783
+ referrer: document.referrer,
784
+ email: options.email,
785
+ userId: options.userId,
786
+ traits: options.traits
787
+ });
788
+ this.enqueue(event);
789
+ }
790
+ /**
791
+ * Set the current user identity.
792
+ * This is useful for SPA applications where you know the user's identity
793
+ * after authentication. Calls identify() under the hood.
794
+ *
795
+ * If called before tracking is enabled, the identity is stored as pending
796
+ * and applied automatically when enableTracking() is called.
797
+ *
798
+ * Note: Both setUser() and identify() enable stage events. The difference is
799
+ * setUser() can be called before tracking is enabled (identity is queued),
800
+ * while identify() requires tracking to be enabled first.
801
+ */
802
+ setUser(identity) {
803
+ if (!identity.email && !identity.userId) {
804
+ console.warn("[Outlit] setUser requires at least email or userId");
805
+ return;
806
+ }
807
+ if (!this.isTrackingEnabled) {
808
+ this.pendingUser = identity;
809
+ return;
810
+ }
811
+ this.applyUser(identity);
812
+ }
813
+ /**
814
+ * Clear the current user identity.
815
+ * Call this when the user logs out.
816
+ */
817
+ clearUser() {
818
+ this.currentUser = null;
819
+ this.pendingUser = null;
820
+ this.pendingStageEvents = [];
821
+ }
822
+ /**
823
+ * Apply user identity and send identify event.
824
+ */
825
+ applyUser(identity) {
826
+ this.currentUser = identity;
827
+ this.identify({ email: identity.email, userId: identity.userId, traits: identity.traits });
828
+ this.flushPendingStageEvents();
829
+ }
830
+ /**
831
+ * Flush any pending stage events that were queued before user identity was set.
832
+ */
833
+ flushPendingStageEvents() {
834
+ if (this.pendingStageEvents.length === 0) return;
835
+ const events = [...this.pendingStageEvents];
836
+ this.pendingStageEvents = [];
837
+ for (const { stage, properties } of events) {
838
+ const event = buildStageEvent({
839
+ url: window.location.href,
840
+ referrer: document.referrer,
841
+ stage,
842
+ properties
843
+ });
844
+ this.enqueue(event);
845
+ }
846
+ }
847
+ /**
848
+ * User namespace methods for contact journey stages.
849
+ */
850
+ user = {
851
+ identify: (options) => this.identify(options),
852
+ activate: (properties) => this.sendStageEvent("activated", properties),
853
+ engaged: (properties) => this.sendStageEvent("engaged", properties),
854
+ inactive: (properties) => this.sendStageEvent("inactive", properties)
855
+ };
856
+ /**
857
+ * Customer namespace methods for billing status.
858
+ */
859
+ customer = {
860
+ trialing: (options) => this.sendBillingEvent("trialing", options),
861
+ paid: (options) => this.sendBillingEvent("paid", options),
862
+ churned: (options) => this.sendBillingEvent("churned", options)
863
+ };
864
+ /**
865
+ * Internal method to send a stage event.
866
+ */
867
+ sendStageEvent(stage, properties) {
868
+ if (!this.isTrackingEnabled) {
869
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
870
+ return;
871
+ }
872
+ if (!this.currentUser) {
873
+ if (this.pendingStageEvents.length >= MAX_PENDING_STAGE_EVENTS) {
874
+ console.warn(
875
+ `[Outlit] Pending stage event queue full (${MAX_PENDING_STAGE_EVENTS}). Call setUser() or identify() to flush queued events.`
876
+ );
877
+ return;
878
+ }
879
+ this.pendingStageEvents.push({ stage, properties });
880
+ return;
881
+ }
882
+ const event = buildStageEvent({
883
+ url: window.location.href,
884
+ referrer: document.referrer,
885
+ stage,
886
+ properties
887
+ });
888
+ this.enqueue(event);
889
+ }
890
+ sendBillingEvent(status, options) {
891
+ if (!this.isTrackingEnabled) {
892
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
893
+ return;
894
+ }
895
+ const event = buildBillingEvent({
896
+ url: window.location.href,
897
+ referrer: document.referrer,
898
+ status,
899
+ customerId: options.customerId,
900
+ stripeCustomerId: options.stripeCustomerId,
901
+ domain: options.domain,
902
+ properties: options.properties
903
+ });
904
+ this.enqueue(event);
905
+ }
906
+ /**
907
+ * Get the current visitor ID.
908
+ * Returns null if tracking is not enabled.
909
+ */
910
+ getVisitorId() {
911
+ return this.visitorId;
912
+ }
913
+ /**
914
+ * Manually flush the event queue.
915
+ */
916
+ async flush() {
917
+ if (this.eventQueue.length === 0) return;
918
+ const events = [...this.eventQueue];
919
+ this.eventQueue = [];
920
+ await this.sendEvents(events);
921
+ }
922
+ /**
923
+ * Shutdown the client.
924
+ */
925
+ async shutdown() {
926
+ if (this.flushTimer) {
927
+ clearInterval(this.flushTimer);
928
+ this.flushTimer = null;
929
+ }
930
+ stopAutocapture();
931
+ stopCalendarTracking();
932
+ stopSessionTracking();
933
+ this.sessionTracker = null;
934
+ await this.flush();
935
+ }
936
+ // ============================================
937
+ // INTERNAL METHODS
938
+ // ============================================
939
+ initSessionTracking() {
940
+ this.sessionTracker = initSessionTracking({
941
+ // Only emit engagement events when trackEngagement is enabled (default: true)
942
+ onEngagement: this.options.trackEngagement !== false ? (event) => this.enqueue(event) : () => {
943
+ },
944
+ idleTimeout: this.options.idleTimeout
945
+ });
946
+ }
947
+ initPageviewTracking() {
948
+ initPageviewTracking((url, referrer, title) => {
949
+ this.sessionTracker?.onNavigation(url);
950
+ const event = buildPageviewEvent({ url, referrer, title });
951
+ this.enqueue(event);
952
+ });
953
+ }
954
+ initFormTracking(denylist) {
955
+ const identityCallback2 = this.options.autoIdentify !== false ? (identity) => {
956
+ const traits = {};
957
+ if (identity.name) traits.name = identity.name;
958
+ if (identity.firstName) traits.firstName = identity.firstName;
959
+ if (identity.lastName) traits.lastName = identity.lastName;
960
+ this.identify({
961
+ email: identity.email,
962
+ traits: Object.keys(traits).length > 0 ? traits : void 0
963
+ });
964
+ } : void 0;
965
+ initFormTracking(
966
+ (url, formId, fields) => {
967
+ const event = buildFormEvent({
968
+ url,
969
+ referrer: document.referrer,
970
+ formId,
971
+ formFields: fields
972
+ });
973
+ this.enqueue(event);
974
+ },
975
+ denylist,
976
+ identityCallback2
977
+ );
978
+ }
979
+ initCalendarTracking() {
980
+ initCalendarTracking({
981
+ onCalendarBooked: (bookingEvent) => {
982
+ const event = buildCalendarEvent({
983
+ url: window.location.href,
984
+ referrer: document.referrer,
985
+ provider: bookingEvent.provider,
986
+ eventType: bookingEvent.eventType,
987
+ startTime: bookingEvent.startTime,
988
+ endTime: bookingEvent.endTime,
989
+ duration: bookingEvent.duration,
990
+ isRecurring: bookingEvent.isRecurring,
991
+ inviteeEmail: bookingEvent.inviteeEmail,
992
+ inviteeName: bookingEvent.inviteeName
993
+ });
994
+ this.enqueue(event);
995
+ }
996
+ });
997
+ }
998
+ enqueue(event) {
999
+ this.eventQueue.push(event);
1000
+ if (this.eventQueue.length >= 10) {
1001
+ this.flush();
1002
+ }
1003
+ }
1004
+ startFlushTimer() {
1005
+ if (this.flushTimer) return;
1006
+ this.flushTimer = setInterval(() => {
1007
+ this.flush();
1008
+ }, this.flushInterval);
1009
+ }
1010
+ async sendEvents(events) {
1011
+ if (events.length === 0) return;
1012
+ if (!this.visitorId) return;
1013
+ const userIdentity = this.currentUser ?? void 0;
1014
+ const sessionId = this.sessionTracker?.getSessionId();
1015
+ const payload = buildIngestPayload(this.visitorId, "client", events, userIdentity, sessionId);
1016
+ const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`;
1017
+ try {
1018
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1019
+ const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
1020
+ const sent = navigator.sendBeacon(url, blob);
1021
+ if (sent) return;
1022
+ console.warn(
1023
+ `[Outlit] sendBeacon failed for ${events.length} events, falling back to fetch`
1024
+ );
1025
+ }
1026
+ const response = await fetch(url, {
1027
+ method: "POST",
1028
+ headers: {
1029
+ "Content-Type": "application/json"
1030
+ },
1031
+ body: JSON.stringify(payload),
1032
+ keepalive: true
1033
+ });
1034
+ if (!response.ok) {
1035
+ console.warn(
1036
+ `[Outlit] Server returned ${response.status} when sending ${events.length} events`
1037
+ );
1038
+ }
1039
+ } catch (error) {
1040
+ console.warn(`[Outlit] Failed to send ${events.length} events:`, error);
1041
+ }
1042
+ }
1043
+ };
1044
+
1045
+ // src/vue/plugin.ts
1046
+ var OutlitKey = /* @__PURE__ */ Symbol("outlit");
1047
+ var OutlitPlugin = {
1048
+ install(app, options) {
1049
+ const outlitRef = shallowRef(null);
1050
+ const isInitialized = ref(false);
1051
+ const isTrackingEnabled = ref(false);
1052
+ const currentUser = ref(null);
1053
+ const {
1054
+ trackPageviews = true,
1055
+ trackForms = true,
1056
+ autoTrack = true,
1057
+ autoIdentify = true,
1058
+ ...rest
1059
+ } = options;
1060
+ outlitRef.value = new Outlit({
1061
+ ...rest,
1062
+ trackPageviews,
1063
+ trackForms,
1064
+ autoTrack,
1065
+ autoIdentify
1066
+ });
1067
+ isInitialized.value = true;
1068
+ isTrackingEnabled.value = outlitRef.value.isEnabled();
1069
+ const enableTracking = () => {
1070
+ if (outlitRef.value) {
1071
+ outlitRef.value.enableTracking();
1072
+ isTrackingEnabled.value = true;
1073
+ }
1074
+ };
1075
+ const disableTracking = () => {
1076
+ if (outlitRef.value) {
1077
+ outlitRef.value.disableTracking();
1078
+ isTrackingEnabled.value = false;
1079
+ }
1080
+ };
1081
+ const setUser = (user) => {
1082
+ currentUser.value = user;
1083
+ };
1084
+ watch(
1085
+ currentUser,
1086
+ (user) => {
1087
+ if (!outlitRef.value) return;
1088
+ if (user && (user.email || user.userId)) {
1089
+ outlitRef.value.setUser(user);
1090
+ } else {
1091
+ outlitRef.value.clearUser();
1092
+ }
1093
+ },
1094
+ { immediate: true }
1095
+ );
1096
+ app.provide(OutlitKey, {
1097
+ outlit: outlitRef,
1098
+ isInitialized: readonly(isInitialized),
1099
+ isTrackingEnabled: readonly(isTrackingEnabled),
1100
+ enableTracking,
1101
+ disableTracking,
1102
+ setUser
1103
+ });
1104
+ app.config.globalProperties.$outlit = outlitRef.value;
1105
+ }
1106
+ };
1107
+
1108
+ // src/vue/composables.ts
1109
+ import { inject, watch as watch2 } from "vue";
1110
+ function useOutlit() {
1111
+ const instance = inject(OutlitKey);
1112
+ if (!instance) {
1113
+ throw new Error("[Outlit] Not initialized. Make sure to install OutlitPlugin in your Vue app.");
1114
+ }
1115
+ const { outlit, isInitialized, isTrackingEnabled, enableTracking, disableTracking } = instance;
1116
+ const track = (eventName, properties) => {
1117
+ if (!outlit.value) {
1118
+ console.warn("[Outlit] Not initialized.");
1119
+ return;
1120
+ }
1121
+ outlit.value.track(eventName, properties);
1122
+ };
1123
+ const identify = (options) => {
1124
+ if (!outlit.value) {
1125
+ console.warn("[Outlit] Not initialized.");
1126
+ return;
1127
+ }
1128
+ outlit.value.identify(options);
1129
+ };
1130
+ const getVisitorId = () => {
1131
+ if (!outlit.value) return null;
1132
+ return outlit.value.getVisitorId();
1133
+ };
1134
+ const setUser = (identity) => {
1135
+ if (!outlit.value) {
1136
+ console.warn("[Outlit] Not initialized.");
1137
+ return;
1138
+ }
1139
+ outlit.value.setUser(identity);
1140
+ };
1141
+ const clearUser = () => {
1142
+ if (!outlit.value) {
1143
+ console.warn("[Outlit] Not initialized.");
1144
+ return;
1145
+ }
1146
+ outlit.value.clearUser();
1147
+ };
1148
+ return {
1149
+ track,
1150
+ identify,
1151
+ getVisitorId,
1152
+ setUser,
1153
+ clearUser,
1154
+ user: {
1155
+ identify: (options) => outlit.value?.user.identify(options),
1156
+ activate: (properties) => outlit.value?.user.activate(properties),
1157
+ engaged: (properties) => outlit.value?.user.engaged(properties),
1158
+ inactive: (properties) => outlit.value?.user.inactive(properties)
1159
+ },
1160
+ customer: {
1161
+ trialing: (options) => outlit.value?.customer.trialing(options),
1162
+ paid: (options) => outlit.value?.customer.paid(options),
1163
+ churned: (options) => outlit.value?.customer.churned(options)
1164
+ },
1165
+ isInitialized,
1166
+ isTrackingEnabled,
1167
+ enableTracking,
1168
+ disableTracking
1169
+ };
1170
+ }
1171
+ function useTrack() {
1172
+ const { track } = useOutlit();
1173
+ return track;
1174
+ }
1175
+ function useIdentify() {
1176
+ const { identify } = useOutlit();
1177
+ return identify;
1178
+ }
1179
+ function useOutlitUser(userRef) {
1180
+ const instance = inject(OutlitKey);
1181
+ if (!instance) {
1182
+ throw new Error("[Outlit] Not initialized. Make sure to install OutlitPlugin in your Vue app.");
1183
+ }
1184
+ const { outlit } = instance;
1185
+ watch2(
1186
+ userRef,
1187
+ (user) => {
1188
+ if (!outlit.value) return;
1189
+ if (user && (user.email || user.userId)) {
1190
+ outlit.value.setUser(user);
1191
+ } else {
1192
+ outlit.value.clearUser();
1193
+ }
1194
+ },
1195
+ { immediate: true, deep: true }
1196
+ );
1197
+ }
1198
+ export {
1199
+ OutlitKey,
1200
+ OutlitPlugin,
1201
+ useIdentify,
1202
+ useOutlit,
1203
+ useOutlitUser,
1204
+ useTrack
1205
+ };
1206
+ //# sourceMappingURL=index.mjs.map