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