@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.
- package/dist/vue/index.d.mts +198 -0
- package/dist/vue/index.d.ts +198 -0
- package/dist/vue/index.js +1171 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/vue/index.mjs +1149 -0
- package/dist/vue/index.mjs.map +1 -0
- package/package.json +20 -5
|
@@ -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
|