@outlit/browser 1.1.0 → 1.2.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,1149 @@
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
+
609
+ // src/tracker.ts
610
+ var MAX_PENDING_STAGE_EVENTS = 10;
611
+ var Outlit = class {
612
+ publicKey;
613
+ apiHost;
614
+ visitorId = null;
615
+ eventQueue = [];
616
+ flushTimer = null;
617
+ flushInterval;
618
+ isInitialized = false;
619
+ isTrackingEnabled = false;
620
+ options;
621
+ hasHandledExit = false;
622
+ sessionTracker = null;
623
+ // User identity state for stage events
624
+ currentUser = null;
625
+ pendingUser = null;
626
+ pendingStageEvents = [];
627
+ constructor(options) {
628
+ this.publicKey = options.publicKey;
629
+ this.apiHost = options.apiHost ?? DEFAULT_API_HOST;
630
+ this.flushInterval = options.flushInterval ?? 5e3;
631
+ this.options = options;
632
+ if (typeof window !== "undefined") {
633
+ const handleExit = () => {
634
+ if (this.hasHandledExit) return;
635
+ this.hasHandledExit = true;
636
+ this.sessionTracker?.emitEngagement();
637
+ this.flush();
638
+ };
639
+ document.addEventListener("visibilitychange", () => {
640
+ if (document.visibilityState === "hidden") {
641
+ handleExit();
642
+ } else {
643
+ this.hasHandledExit = false;
644
+ }
645
+ });
646
+ window.addEventListener("pagehide", handleExit);
647
+ window.addEventListener("beforeunload", handleExit);
648
+ }
649
+ this.isInitialized = true;
650
+ if (options.autoTrack !== false) {
651
+ this.enableTracking();
652
+ }
653
+ }
654
+ // ============================================
655
+ // PUBLIC API
656
+ // ============================================
657
+ /**
658
+ * Enable tracking. Call this after obtaining user consent.
659
+ * This will:
660
+ * - Generate/retrieve the visitor ID
661
+ * - Start automatic pageview and form tracking (if configured)
662
+ * - Begin sending events to the server
663
+ *
664
+ * If autoTrack is true (default), this is called automatically on init.
665
+ */
666
+ enableTracking() {
667
+ if (this.isTrackingEnabled) {
668
+ return;
669
+ }
670
+ this.visitorId = getOrCreateVisitorId();
671
+ this.startFlushTimer();
672
+ this.initSessionTracking();
673
+ if (this.options.trackPageviews !== false) {
674
+ this.initPageviewTracking();
675
+ }
676
+ if (this.options.trackForms !== false) {
677
+ this.initFormTracking(this.options.formFieldDenylist);
678
+ }
679
+ if (this.options.trackCalendarEmbeds !== false) {
680
+ this.initCalendarTracking();
681
+ }
682
+ this.isTrackingEnabled = true;
683
+ if (this.pendingUser) {
684
+ this.applyUser(this.pendingUser);
685
+ this.pendingUser = null;
686
+ }
687
+ }
688
+ /**
689
+ * Check if tracking is currently enabled.
690
+ */
691
+ isEnabled() {
692
+ return this.isTrackingEnabled;
693
+ }
694
+ /**
695
+ * Track a custom event.
696
+ */
697
+ track(eventName, properties) {
698
+ if (!this.isTrackingEnabled) {
699
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
700
+ return;
701
+ }
702
+ const event = buildCustomEvent({
703
+ url: window.location.href,
704
+ referrer: document.referrer,
705
+ eventName,
706
+ properties
707
+ });
708
+ this.enqueue(event);
709
+ }
710
+ /**
711
+ * Identify the current visitor.
712
+ * Links the anonymous visitor to a known user.
713
+ *
714
+ * When email or userId is provided, also sets the current user identity
715
+ * for stage events (activate, engaged, inactive).
716
+ */
717
+ identify(options) {
718
+ if (!this.isTrackingEnabled) {
719
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
720
+ return;
721
+ }
722
+ if (options.email || options.userId) {
723
+ const hadNoUser = !this.currentUser;
724
+ this.currentUser = {
725
+ email: options.email,
726
+ userId: options.userId
727
+ };
728
+ if (hadNoUser) {
729
+ this.flushPendingStageEvents();
730
+ }
731
+ }
732
+ const event = buildIdentifyEvent({
733
+ url: window.location.href,
734
+ referrer: document.referrer,
735
+ email: options.email,
736
+ userId: options.userId,
737
+ traits: options.traits
738
+ });
739
+ this.enqueue(event);
740
+ }
741
+ /**
742
+ * Set the current user identity.
743
+ * This is useful for SPA applications where you know the user's identity
744
+ * after authentication. Calls identify() under the hood.
745
+ *
746
+ * If called before tracking is enabled, the identity is stored as pending
747
+ * and applied automatically when enableTracking() is called.
748
+ *
749
+ * Note: Both setUser() and identify() enable stage events. The difference is
750
+ * setUser() can be called before tracking is enabled (identity is queued),
751
+ * while identify() requires tracking to be enabled first.
752
+ */
753
+ setUser(identity) {
754
+ if (!identity.email && !identity.userId) {
755
+ console.warn("[Outlit] setUser requires at least email or userId");
756
+ return;
757
+ }
758
+ if (!this.isTrackingEnabled) {
759
+ this.pendingUser = identity;
760
+ return;
761
+ }
762
+ this.applyUser(identity);
763
+ }
764
+ /**
765
+ * Clear the current user identity.
766
+ * Call this when the user logs out.
767
+ */
768
+ clearUser() {
769
+ this.currentUser = null;
770
+ this.pendingUser = null;
771
+ this.pendingStageEvents = [];
772
+ }
773
+ /**
774
+ * Apply user identity and send identify event.
775
+ */
776
+ applyUser(identity) {
777
+ this.currentUser = identity;
778
+ this.identify({ email: identity.email, userId: identity.userId, traits: identity.traits });
779
+ this.flushPendingStageEvents();
780
+ }
781
+ /**
782
+ * Flush any pending stage events that were queued before user identity was set.
783
+ */
784
+ flushPendingStageEvents() {
785
+ if (this.pendingStageEvents.length === 0) return;
786
+ const events = [...this.pendingStageEvents];
787
+ this.pendingStageEvents = [];
788
+ for (const { stage, properties } of events) {
789
+ const event = buildStageEvent({
790
+ url: window.location.href,
791
+ referrer: document.referrer,
792
+ stage,
793
+ properties
794
+ });
795
+ this.enqueue(event);
796
+ }
797
+ }
798
+ /**
799
+ * User namespace methods for contact journey stages.
800
+ */
801
+ user = {
802
+ identify: (options) => this.identify(options),
803
+ activate: (properties) => this.sendStageEvent("activated", properties),
804
+ engaged: (properties) => this.sendStageEvent("engaged", properties),
805
+ inactive: (properties) => this.sendStageEvent("inactive", properties)
806
+ };
807
+ /**
808
+ * Customer namespace methods for billing status.
809
+ */
810
+ customer = {
811
+ trialing: (options) => this.sendBillingEvent("trialing", options),
812
+ paid: (options) => this.sendBillingEvent("paid", options),
813
+ churned: (options) => this.sendBillingEvent("churned", options)
814
+ };
815
+ /**
816
+ * Internal method to send a stage event.
817
+ */
818
+ sendStageEvent(stage, properties) {
819
+ if (!this.isTrackingEnabled) {
820
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
821
+ return;
822
+ }
823
+ if (!this.currentUser) {
824
+ if (this.pendingStageEvents.length >= MAX_PENDING_STAGE_EVENTS) {
825
+ console.warn(
826
+ `[Outlit] Pending stage event queue full (${MAX_PENDING_STAGE_EVENTS}). Call setUser() or identify() to flush queued events.`
827
+ );
828
+ return;
829
+ }
830
+ this.pendingStageEvents.push({ stage, properties });
831
+ return;
832
+ }
833
+ const event = buildStageEvent({
834
+ url: window.location.href,
835
+ referrer: document.referrer,
836
+ stage,
837
+ properties
838
+ });
839
+ this.enqueue(event);
840
+ }
841
+ sendBillingEvent(status, options) {
842
+ if (!this.isTrackingEnabled) {
843
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
844
+ return;
845
+ }
846
+ const event = buildBillingEvent({
847
+ url: window.location.href,
848
+ referrer: document.referrer,
849
+ status,
850
+ customerId: options.customerId,
851
+ stripeCustomerId: options.stripeCustomerId,
852
+ domain: options.domain,
853
+ properties: options.properties
854
+ });
855
+ this.enqueue(event);
856
+ }
857
+ /**
858
+ * Get the current visitor ID.
859
+ * Returns null if tracking is not enabled.
860
+ */
861
+ getVisitorId() {
862
+ return this.visitorId;
863
+ }
864
+ /**
865
+ * Manually flush the event queue.
866
+ */
867
+ async flush() {
868
+ if (this.eventQueue.length === 0) return;
869
+ const events = [...this.eventQueue];
870
+ this.eventQueue = [];
871
+ await this.sendEvents(events);
872
+ }
873
+ /**
874
+ * Shutdown the client.
875
+ */
876
+ async shutdown() {
877
+ if (this.flushTimer) {
878
+ clearInterval(this.flushTimer);
879
+ this.flushTimer = null;
880
+ }
881
+ stopAutocapture();
882
+ stopCalendarTracking();
883
+ stopSessionTracking();
884
+ this.sessionTracker = null;
885
+ await this.flush();
886
+ }
887
+ // ============================================
888
+ // INTERNAL METHODS
889
+ // ============================================
890
+ initSessionTracking() {
891
+ this.sessionTracker = initSessionTracking({
892
+ // Only emit engagement events when trackEngagement is enabled (default: true)
893
+ onEngagement: this.options.trackEngagement !== false ? (event) => this.enqueue(event) : () => {
894
+ },
895
+ idleTimeout: this.options.idleTimeout
896
+ });
897
+ }
898
+ initPageviewTracking() {
899
+ initPageviewTracking((url, referrer, title) => {
900
+ this.sessionTracker?.onNavigation(url);
901
+ const event = buildPageviewEvent({ url, referrer, title });
902
+ this.enqueue(event);
903
+ });
904
+ }
905
+ initFormTracking(denylist) {
906
+ const identityCallback2 = this.options.autoIdentify !== false ? (identity) => {
907
+ const traits = {};
908
+ if (identity.name) traits.name = identity.name;
909
+ if (identity.firstName) traits.firstName = identity.firstName;
910
+ if (identity.lastName) traits.lastName = identity.lastName;
911
+ this.identify({
912
+ email: identity.email,
913
+ traits: Object.keys(traits).length > 0 ? traits : void 0
914
+ });
915
+ } : void 0;
916
+ initFormTracking(
917
+ (url, formId, fields) => {
918
+ const event = buildFormEvent({
919
+ url,
920
+ referrer: document.referrer,
921
+ formId,
922
+ formFields: fields
923
+ });
924
+ this.enqueue(event);
925
+ },
926
+ denylist,
927
+ identityCallback2
928
+ );
929
+ }
930
+ initCalendarTracking() {
931
+ initCalendarTracking({
932
+ onCalendarBooked: (bookingEvent) => {
933
+ const event = buildCalendarEvent({
934
+ url: window.location.href,
935
+ referrer: document.referrer,
936
+ provider: bookingEvent.provider,
937
+ eventType: bookingEvent.eventType,
938
+ startTime: bookingEvent.startTime,
939
+ endTime: bookingEvent.endTime,
940
+ duration: bookingEvent.duration,
941
+ isRecurring: bookingEvent.isRecurring,
942
+ inviteeEmail: bookingEvent.inviteeEmail,
943
+ inviteeName: bookingEvent.inviteeName
944
+ });
945
+ this.enqueue(event);
946
+ }
947
+ });
948
+ }
949
+ enqueue(event) {
950
+ this.eventQueue.push(event);
951
+ if (this.eventQueue.length >= 10) {
952
+ this.flush();
953
+ }
954
+ }
955
+ startFlushTimer() {
956
+ if (this.flushTimer) return;
957
+ this.flushTimer = setInterval(() => {
958
+ this.flush();
959
+ }, this.flushInterval);
960
+ }
961
+ async sendEvents(events) {
962
+ if (events.length === 0) return;
963
+ if (!this.visitorId) return;
964
+ const userIdentity = this.currentUser ?? void 0;
965
+ const sessionId = this.sessionTracker?.getSessionId();
966
+ const payload = buildIngestPayload(this.visitorId, "client", events, userIdentity, sessionId);
967
+ const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`;
968
+ try {
969
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
970
+ const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
971
+ const sent = navigator.sendBeacon(url, blob);
972
+ if (sent) return;
973
+ console.warn(
974
+ `[Outlit] sendBeacon failed for ${events.length} events, falling back to fetch`
975
+ );
976
+ }
977
+ const response = await fetch(url, {
978
+ method: "POST",
979
+ headers: {
980
+ "Content-Type": "application/json"
981
+ },
982
+ body: JSON.stringify(payload),
983
+ keepalive: true
984
+ });
985
+ if (!response.ok) {
986
+ console.warn(
987
+ `[Outlit] Server returned ${response.status} when sending ${events.length} events`
988
+ );
989
+ }
990
+ } catch (error) {
991
+ console.warn(`[Outlit] Failed to send ${events.length} events:`, error);
992
+ }
993
+ }
994
+ };
995
+
996
+ // src/vue/plugin.ts
997
+ var OutlitKey = /* @__PURE__ */ Symbol("outlit");
998
+ var OutlitPlugin = {
999
+ install(app, options) {
1000
+ const outlitRef = shallowRef(null);
1001
+ const isInitialized = ref(false);
1002
+ const isTrackingEnabled = ref(false);
1003
+ const currentUser = ref(null);
1004
+ const {
1005
+ trackPageviews = true,
1006
+ trackForms = true,
1007
+ autoTrack = true,
1008
+ autoIdentify = true,
1009
+ ...rest
1010
+ } = options;
1011
+ outlitRef.value = new Outlit({
1012
+ ...rest,
1013
+ trackPageviews,
1014
+ trackForms,
1015
+ autoTrack,
1016
+ autoIdentify
1017
+ });
1018
+ isInitialized.value = true;
1019
+ isTrackingEnabled.value = outlitRef.value.isEnabled();
1020
+ const enableTracking = () => {
1021
+ if (outlitRef.value) {
1022
+ outlitRef.value.enableTracking();
1023
+ isTrackingEnabled.value = true;
1024
+ }
1025
+ };
1026
+ const setUser = (user) => {
1027
+ currentUser.value = user;
1028
+ };
1029
+ watch(
1030
+ currentUser,
1031
+ (user) => {
1032
+ if (!outlitRef.value) return;
1033
+ if (user && (user.email || user.userId)) {
1034
+ outlitRef.value.setUser(user);
1035
+ } else {
1036
+ outlitRef.value.clearUser();
1037
+ }
1038
+ },
1039
+ { immediate: true }
1040
+ );
1041
+ app.provide(OutlitKey, {
1042
+ outlit: outlitRef,
1043
+ isInitialized: readonly(isInitialized),
1044
+ isTrackingEnabled: readonly(isTrackingEnabled),
1045
+ enableTracking,
1046
+ setUser
1047
+ });
1048
+ app.config.globalProperties.$outlit = outlitRef.value;
1049
+ }
1050
+ };
1051
+
1052
+ // src/vue/composables.ts
1053
+ import { inject, watch as watch2 } from "vue";
1054
+ function useOutlit() {
1055
+ const instance = inject(OutlitKey);
1056
+ if (!instance) {
1057
+ throw new Error("[Outlit] Not initialized. Make sure to install OutlitPlugin in your Vue app.");
1058
+ }
1059
+ const { outlit, isInitialized, isTrackingEnabled, enableTracking } = instance;
1060
+ const track = (eventName, properties) => {
1061
+ if (!outlit.value) {
1062
+ console.warn("[Outlit] Not initialized.");
1063
+ return;
1064
+ }
1065
+ outlit.value.track(eventName, properties);
1066
+ };
1067
+ const identify = (options) => {
1068
+ if (!outlit.value) {
1069
+ console.warn("[Outlit] Not initialized.");
1070
+ return;
1071
+ }
1072
+ outlit.value.identify(options);
1073
+ };
1074
+ const getVisitorId = () => {
1075
+ if (!outlit.value) return null;
1076
+ return outlit.value.getVisitorId();
1077
+ };
1078
+ const setUser = (identity) => {
1079
+ if (!outlit.value) {
1080
+ console.warn("[Outlit] Not initialized.");
1081
+ return;
1082
+ }
1083
+ outlit.value.setUser(identity);
1084
+ };
1085
+ const clearUser = () => {
1086
+ if (!outlit.value) {
1087
+ console.warn("[Outlit] Not initialized.");
1088
+ return;
1089
+ }
1090
+ outlit.value.clearUser();
1091
+ };
1092
+ return {
1093
+ track,
1094
+ identify,
1095
+ getVisitorId,
1096
+ setUser,
1097
+ clearUser,
1098
+ user: {
1099
+ identify: (options) => outlit.value?.user.identify(options),
1100
+ activate: (properties) => outlit.value?.user.activate(properties),
1101
+ engaged: (properties) => outlit.value?.user.engaged(properties),
1102
+ inactive: (properties) => outlit.value?.user.inactive(properties)
1103
+ },
1104
+ customer: {
1105
+ trialing: (options) => outlit.value?.customer.trialing(options),
1106
+ paid: (options) => outlit.value?.customer.paid(options),
1107
+ churned: (options) => outlit.value?.customer.churned(options)
1108
+ },
1109
+ isInitialized,
1110
+ isTrackingEnabled,
1111
+ enableTracking
1112
+ };
1113
+ }
1114
+ function useTrack() {
1115
+ const { track } = useOutlit();
1116
+ return track;
1117
+ }
1118
+ function useIdentify() {
1119
+ const { identify } = useOutlit();
1120
+ return identify;
1121
+ }
1122
+ function useOutlitUser(userRef) {
1123
+ const instance = inject(OutlitKey);
1124
+ if (!instance) {
1125
+ throw new Error("[Outlit] Not initialized. Make sure to install OutlitPlugin in your Vue app.");
1126
+ }
1127
+ const { outlit } = instance;
1128
+ watch2(
1129
+ userRef,
1130
+ (user) => {
1131
+ if (!outlit.value) return;
1132
+ if (user && (user.email || user.userId)) {
1133
+ outlit.value.setUser(user);
1134
+ } else {
1135
+ outlit.value.clearUser();
1136
+ }
1137
+ },
1138
+ { immediate: true, deep: true }
1139
+ );
1140
+ }
1141
+ export {
1142
+ OutlitKey,
1143
+ OutlitPlugin,
1144
+ useIdentify,
1145
+ useOutlit,
1146
+ useOutlitUser,
1147
+ useTrack
1148
+ };
1149
+ //# sourceMappingURL=index.mjs.map