@outlit/browser 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/dist/outlit.global.js +1 -1
- package/dist/outlit.global.js.map +1 -1
- package/dist/react/index.js +6 -6
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +6 -6
- package/dist/react/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/script.ts","../../core/src/types.ts","../../core/src/utils.ts","../../core/src/payload.ts","../src/autocapture.ts","../src/embed-integrations.ts","../src/session-tracker.ts","../src/storage.ts","../src/tracker.ts"],"sourcesContent":["/**\n * IIFE entry point for CDN script tag usage.\n *\n * Usage (with stub snippet - recommended):\n * <script>\n * !function(w,d,src,key,auto){\n * if(w.outlit&&w.outlit._loaded)return;\n * w.outlit=w.outlit||{_q:[]};\n * [\"init\",\"track\",\"identify\",\"enableTracking\",\"isTrackingEnabled\",\"getVisitorId\",\"setUser\",\"clearUser\",\"activate\",\"engaged\",\"paid\",\"churned\"].forEach(function(m){\n * w.outlit[m]=w.outlit[m]||function(){w.outlit._q.push([m,[].slice.call(arguments)])};\n * });\n * var s=d.createElement(\"script\");s.async=1;s.src=src;\n * s.dataset.publicKey=key;if(auto!==undefined)s.dataset.autoTrack=auto;\n * (d.body||d.head).appendChild(s);\n * }(window,document,\"https://cdn.outlit.ai/outlit.js\",\"pk_xxx\");\n * </script>\n *\n * Usage (simple script tag):\n * <script src=\"https://cdn.outlit.ai/outlit.js\" data-public-key=\"pk_xxx\" async></script>\n *\n * Usage (with consent management):\n * Pass `false` as last param to stub, or use data-auto-track=\"false\" on script tag\n */\n\nimport type { BrowserIdentifyOptions, BrowserTrackOptions } from \"@outlit/core\"\nimport { Outlit, type OutlitOptions, type UserIdentity } from \"./tracker\"\n\n// ============================================\n// TYPES\n// ============================================\n\n// Stub queue format: [methodName, arguments]\ntype StubQueueItem = [string, unknown[]]\n\ninterface OutlitStub {\n _q?: StubQueueItem[]\n [key: string]: unknown\n}\n\ninterface OutlitGlobal {\n _initialized: boolean\n _instance: Outlit | null\n _queue: Array<() => void>\n init: (options: OutlitOptions) => void\n track: (eventName: string, properties?: BrowserTrackOptions[\"properties\"]) => void\n identify: (options: BrowserIdentifyOptions) => void\n getVisitorId: () => string | null\n enableTracking: () => void\n isTrackingEnabled: () => boolean\n setUser: (identity: UserIdentity) => void\n clearUser: () => void\n activate: (properties?: Record<string, string | number | boolean | null>) => void\n engaged: (properties?: Record<string, string | number | boolean | null>) => void\n paid: (properties?: Record<string, string | number | boolean | null>) => void\n churned: (properties?: Record<string, string | number | boolean | null>) => void\n}\n\n// ============================================\n// GLOBAL API\n// ============================================\n\n// Check for existing stub with queued calls\nconst existingStub =\n typeof window !== \"undefined\" ? (window as { outlit?: OutlitStub }).outlit : undefined\nconst stubQueue: StubQueueItem[] = existingStub?._q || []\n\n// Create global object with queuing support\nconst outlit: OutlitGlobal & { _loaded?: boolean } = {\n _initialized: false,\n _instance: null,\n _queue: [],\n _loaded: true, // Marks that the real SDK has loaded (for double-load protection)\n\n init(options: OutlitOptions) {\n if (this._initialized) {\n console.warn(\"[Outlit] Already initialized\")\n return\n }\n\n this._instance = new Outlit(options)\n this._initialized = true\n\n // Process calls queued by the stub snippet (before SDK loaded)\n for (const [method, args] of stubQueue) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const self = this as any\n if (method in self && typeof self[method] === \"function\") {\n self[method](...args)\n }\n }\n\n // Process calls queued after SDK loaded but before init\n while (this._queue.length > 0) {\n const fn = this._queue.shift()\n fn?.()\n }\n },\n\n track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]) {\n if (!this._initialized || !this._instance) {\n // Queue the call for after initialization\n this._queue.push(() => this.track(eventName, properties))\n return\n }\n this._instance.track(eventName, properties)\n },\n\n identify(options: BrowserIdentifyOptions) {\n if (!this._initialized || !this._instance) {\n // Queue the call for after initialization\n this._queue.push(() => this.identify(options))\n return\n }\n this._instance.identify(options)\n },\n\n getVisitorId() {\n if (!this._instance) return null\n return this._instance.getVisitorId()\n },\n\n /**\n * Enable tracking after user consent.\n * Call this in your consent management tool's callback.\n */\n enableTracking() {\n if (!this._initialized || !this._instance) {\n // Queue the call for after initialization\n this._queue.push(() => this.enableTracking())\n return\n }\n this._instance.enableTracking()\n },\n\n /**\n * Check if tracking is currently enabled.\n */\n isTrackingEnabled() {\n if (!this._instance) return false\n return this._instance.isEnabled()\n },\n\n /**\n * Set the user identity for attribution.\n */\n setUser(identity: UserIdentity) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.setUser(identity))\n return\n }\n this._instance.setUser(identity)\n },\n\n /**\n * Clear the current user identity (logout).\n */\n clearUser() {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.clearUser())\n return\n }\n this._instance.clearUser()\n },\n\n /**\n * Mark the current user as activated.\n */\n activate(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.activate(properties))\n return\n }\n this._instance.activate(properties)\n },\n\n /**\n * Mark the current user as engaged.\n */\n engaged(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.engaged(properties))\n return\n }\n this._instance.engaged(properties)\n },\n\n /**\n * Mark the current user as paid.\n */\n paid(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.paid(properties))\n return\n }\n this._instance.paid(properties)\n },\n\n /**\n * Mark the current user as churned.\n */\n churned(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.churned(properties))\n return\n }\n this._instance.churned(properties)\n },\n}\n\n// ============================================\n// AUTO-INITIALIZATION\n// ============================================\n\n/**\n * Auto-initialize from script tag attributes.\n */\nfunction autoInit(): void {\n // Find the script tag - currentScript is only available during synchronous execution\n // When called from DOMContentLoaded, we need to fall back to querySelector\n let script = document.currentScript as HTMLScriptElement | null\n\n if (!script) {\n // Fallback: find script tag with data-public-key attribute\n script = document.querySelector(\"script[data-public-key]\") as HTMLScriptElement | null\n }\n\n if (!script) {\n console.warn(\"[Outlit] No script tag found with data-public-key attribute\")\n return\n }\n\n const publicKey = script.getAttribute(\"data-public-key\")\n if (!publicKey) {\n console.warn(\"[Outlit] Missing data-public-key attribute on script tag\")\n return\n }\n\n // Get optional attributes\n const apiHost = script.getAttribute(\"data-api-host\") ?? undefined\n const trackPageviews = script.getAttribute(\"data-track-pageviews\") !== \"false\"\n const trackForms = script.getAttribute(\"data-track-forms\") !== \"false\"\n const autoTrack = script.getAttribute(\"data-auto-track\") !== \"false\"\n const autoIdentify = script.getAttribute(\"data-auto-identify\") !== \"false\"\n const trackCalendarEmbeds = script.getAttribute(\"data-track-calendar-embeds\") !== \"false\"\n\n // Initialize\n outlit.init({\n publicKey,\n apiHost,\n trackPageviews,\n trackForms,\n autoTrack,\n autoIdentify,\n trackCalendarEmbeds,\n })\n}\n\n// ============================================\n// EXPOSE GLOBAL & AUTO-INIT\n// ============================================\n\n// Expose on window\nif (typeof window !== \"undefined\") {\n // @ts-expect-error - Adding to window\n window.outlit = outlit\n\n // Auto-initialize when DOM is ready\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", autoInit)\n } else {\n // DOM is already ready\n autoInit()\n }\n}\n\n// Also export for module usage if needed\nexport { outlit }\n","// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType =\n | \"pageview\"\n | \"form\"\n | \"identify\"\n | \"custom\"\n | \"calendar\"\n | \"engagement\"\n | \"stage\"\n\n// Only explicit stages - discovered/signed_up are inferred from identify calls\nexport type ExplicitJourneyStage = \"activated\" | \"engaged\" | \"paid\" | \"churned\"\n\nexport type CalendarProvider = \"cal.com\" | \"calendly\" | \"unknown\"\n\nexport type SourceType = \"client\" | \"server\" | \"integration\"\n\n// ============================================\n// UTM PARAMETERS\n// ============================================\n\nexport interface UtmParams {\n source?: string\n medium?: string\n campaign?: string\n term?: string\n content?: string\n}\n\n// ============================================\n// TRACKER CONFIGURATION\n// ============================================\n\nexport interface TrackerConfig {\n publicKey: string\n apiHost?: string // default: 'https://app.outlit.ai'\n}\n\n// ============================================\n// BROWSER-SPECIFIC TYPES (anonymous allowed)\n// visitorId is auto-managed by the browser SDK\n// ============================================\n\nexport interface BrowserTrackOptions {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BrowserIdentifyOptions {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES (identity required)\n// No anonymous tracking - must identify the user\n// ============================================\n\nexport interface ServerTrackOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n type: EventType\n timestamp: number // Unix timestamp in milliseconds\n url: string\n path: string\n referrer?: string\n utm?: UtmParams\n}\n\nexport interface PageviewEvent extends BaseEvent {\n type: \"pageview\"\n title?: string\n}\n\nexport interface FormEvent extends BaseEvent {\n type: \"form\"\n formId?: string\n formFields?: Record<string, string>\n}\n\nexport interface IdentifyEvent extends BaseEvent {\n type: \"identify\"\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface CalendarEvent extends BaseEvent {\n type: \"calendar\"\n provider: CalendarProvider\n eventType?: string // e.g., \"30 Minute Meeting\"\n startTime?: string // ISO timestamp\n endTime?: string // ISO timestamp\n duration?: number // Duration in minutes\n isRecurring?: boolean\n /** Available when identity is passed via webhooks or manual integration */\n inviteeEmail?: string\n inviteeName?: string\n}\n\nexport interface EngagementEvent extends BaseEvent {\n type: \"engagement\"\n /** Time in milliseconds the user was actively engaged (visible tab + user interactions) */\n activeTimeMs: number\n /** Total wall-clock time in milliseconds on the page */\n totalTimeMs: number\n /** Session ID for grouping engagement events. Resets after 30 min of inactivity or tab close. */\n sessionId: string\n}\n\nexport interface StageEvent extends BaseEvent {\n type: \"stage\"\n /** The journey stage to set (only explicit stages, discovered/signed_up are inferred) */\n stage: ExplicitJourneyStage\n /** Optional properties for context */\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport type TrackerEvent =\n | PageviewEvent\n | FormEvent\n | IdentifyEvent\n | CustomEvent\n | CalendarEvent\n | EngagementEvent\n | StageEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\n/**\n * User identity for payload-level resolution.\n * Used by browser SDK when user is logged in (via setUser).\n */\nexport interface PayloadUserIdentity {\n email?: string\n userId?: string\n}\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n /**\n * User identity for this batch of events.\n * When present, the server can resolve directly to CustomerContact\n * instead of relying on anonymous visitor flow.\n *\n * This is set by the browser SDK when setUser() has been called,\n * allowing immediate identity resolution for SPA/React apps.\n */\n userIdentity?: PayloadUserIdentity\n}\n\n// ============================================\n// API RESPONSE\n// ============================================\n\nexport interface IngestResponse {\n success: boolean\n processed: number\n errors?: Array<{\n index: number\n message: string\n }>\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\n// Re-export for convenience\nexport type { PayloadUserIdentity as UserIdentity }\n\nexport const DEFAULT_DENIED_FORM_FIELDS = [\n \"password\",\n \"passwd\",\n \"pass\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"api_key\",\n \"apikey\",\n \"api-key\",\n \"credit_card\",\n \"creditcard\",\n \"credit-card\",\n \"cc_number\",\n \"ccnumber\",\n \"card_number\",\n \"cardnumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"social_security\",\n \"socialsecurity\",\n \"bank_account\",\n \"bankaccount\",\n \"routing_number\",\n \"routingnumber\",\n]\n","import { DEFAULT_DENIED_FORM_FIELDS, type UtmParams } from \"./types\"\n\n// ============================================\n// UTM EXTRACTION\n// ============================================\n\n/**\n * Extract UTM parameters from a URL.\n */\nexport function extractUtmParams(url: string): UtmParams | undefined {\n try {\n const urlObj = new URL(url)\n const params = urlObj.searchParams\n\n const utm: UtmParams = {}\n\n if (params.has(\"utm_source\")) utm.source = params.get(\"utm_source\") ?? undefined\n if (params.has(\"utm_medium\")) utm.medium = params.get(\"utm_medium\") ?? undefined\n if (params.has(\"utm_campaign\")) utm.campaign = params.get(\"utm_campaign\") ?? undefined\n if (params.has(\"utm_term\")) utm.term = params.get(\"utm_term\") ?? undefined\n if (params.has(\"utm_content\")) utm.content = params.get(\"utm_content\") ?? undefined\n\n return Object.keys(utm).length > 0 ? utm : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Extract path from a URL.\n */\nexport function extractPathFromUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n return urlObj.pathname\n } catch {\n return \"/\"\n }\n}\n\n// ============================================\n// FORM FIELD SANITIZATION\n// ============================================\n\n/**\n * Check if a field name should be denied (case-insensitive).\n */\nexport function isFieldDenied(fieldName: string, denylist: string[]): boolean {\n const normalizedName = fieldName.toLowerCase().replace(/[-_\\s]/g, \"\")\n return denylist.some((denied) => {\n const normalizedDenied = denied.toLowerCase().replace(/[-_\\s]/g, \"\")\n return normalizedName.includes(normalizedDenied)\n })\n}\n\n/**\n * Check if a value looks like sensitive data (e.g., credit card number).\n */\nfunction looksLikeSensitiveValue(value: string): boolean {\n // Remove spaces and dashes\n const cleaned = value.replace(/[\\s-]/g, \"\")\n\n // Check for credit card patterns (13-19 digits)\n if (/^\\d{13,19}$/.test(cleaned)) {\n return true\n }\n\n // Check for SSN pattern (9 digits)\n if (/^\\d{9}$/.test(cleaned) || /^\\d{3}-\\d{2}-\\d{4}$/.test(value)) {\n return true\n }\n\n return false\n}\n\n/**\n * Sanitize form fields by removing sensitive data.\n * Returns a new object with denied fields removed.\n */\nexport function sanitizeFormFields(\n fields: Record<string, string> | undefined,\n customDenylist?: string[],\n): Record<string, string> | undefined {\n if (!fields) return undefined\n\n const denylist = customDenylist ?? DEFAULT_DENIED_FORM_FIELDS\n const sanitized: Record<string, string> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n if (!isFieldDenied(key, denylist)) {\n // Also check for credit card patterns in values\n if (!looksLikeSensitiveValue(value)) {\n sanitized[key] = value\n }\n }\n }\n\n return Object.keys(sanitized).length > 0 ? sanitized : undefined\n}\n\n// ============================================\n// VISITOR ID DERIVATION (for server SDK)\n// ============================================\n\n/**\n * Derive a deterministic visitor ID from email and/or userId.\n * This is used by the server SDK to create consistent IDs for API compatibility.\n *\n * Uses a simple hash to create a UUID-like string that will be consistent\n * for the same email/userId combination.\n */\nexport function deriveVisitorIdFromIdentity(email?: string, userId?: string): string {\n const identity = [email?.toLowerCase(), userId].filter(Boolean).join(\"|\")\n if (!identity) {\n throw new Error(\"Either email or userId must be provided\")\n }\n\n // Simple hash function to create a deterministic UUID-like string\n let hash = 0\n for (let i = 0; i < identity.length; i++) {\n const char = identity.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Convert to hex and format as UUID-like string\n const hex = Math.abs(hash).toString(16).padStart(8, \"0\")\n const part1 = hex.slice(0, 8)\n const part2 = identity.length.toString(16).padStart(4, \"0\")\n const part3 = \"4000\" // Version 4 UUID marker\n const part4 = (((hash >>> 16) & 0x0fff) | 0x8000).toString(16)\n const part5 = Math.abs(hash * 31)\n .toString(16)\n .padStart(12, \"0\")\n .slice(0, 12)\n\n return `${part1}-${part2}-${part3}-${part4}-${part5}`\n}\n\n// ============================================\n// VALIDATION\n// ============================================\n\n/**\n * Validate that at least one identity field is provided.\n * Used by the server SDK to enforce identity requirements.\n */\nexport function validateServerIdentity(email?: string, userId?: string): void {\n if (!email && !userId) {\n throw new Error(\n \"Server SDK requires either email or userId for all track/identify calls. \" +\n \"Anonymous tracking is only supported in the browser SDK.\",\n )\n }\n}\n\n// ============================================\n// AUTO-IDENTIFY: EMAIL & NAME EXTRACTION\n// ============================================\n\n/**\n * Validate that a string looks like a valid email address.\n */\nexport function isValidEmail(value: string): boolean {\n if (!value || typeof value !== \"string\") return false\n // Basic email regex - intentionally permissive to avoid false negatives\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(value.trim())\n}\n\n/**\n * Email field name patterns (case-insensitive, normalized).\n * Order matters - more specific patterns first.\n */\nconst EMAIL_FIELD_PATTERNS = [\n /^e?-?mail$/i,\n /^email[_-]?address$/i,\n /^user[_-]?email$/i,\n /^work[_-]?email$/i,\n /^contact[_-]?email$/i,\n /^primary[_-]?email$/i,\n /^business[_-]?email$/i,\n]\n\n/**\n * Full name field patterns.\n */\nconst FULL_NAME_PATTERNS = [\n /^name$/i,\n /^full[_-]?name$/i,\n /^your[_-]?name$/i,\n /^customer[_-]?name$/i,\n /^contact[_-]?name$/i,\n /^display[_-]?name$/i,\n]\n\n/**\n * First name field patterns.\n */\nconst FIRST_NAME_PATTERNS = [\n /^first[_-]?name$/i,\n /^firstname$/i,\n /^first$/i,\n /^fname$/i,\n /^given[_-]?name$/i,\n /^forename$/i,\n]\n\n/**\n * Last name field patterns.\n */\nconst LAST_NAME_PATTERNS = [\n /^last[_-]?name$/i,\n /^lastname$/i,\n /^last$/i,\n /^lname$/i,\n /^surname$/i,\n /^family[_-]?name$/i,\n]\n\n/**\n * Check if a field name matches any of the given patterns.\n */\nfunction matchesPatterns(fieldName: string, patterns: RegExp[]): boolean {\n const normalized = fieldName.trim()\n return patterns.some((pattern) => pattern.test(normalized))\n}\n\n/**\n * Find an email value from form fields.\n *\n * Priority:\n * 1. Fields with input type=\"email\" (if inputTypes map provided)\n * 2. Field names matching email patterns\n * 3. Any field with a value that looks like an email\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns The email value if found, undefined otherwise\n */\nexport function findEmailField(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): string | undefined {\n // Priority 1: Check fields with type=\"email\"\n if (inputTypes) {\n for (const [fieldName, inputType] of inputTypes.entries()) {\n if (inputType === \"email\") {\n const value = fields[fieldName]\n if (value && isValidEmail(value)) {\n return value.trim()\n }\n }\n }\n }\n\n // Priority 2: Check field names matching email patterns\n for (const [fieldName, value] of Object.entries(fields)) {\n if (matchesPatterns(fieldName, EMAIL_FIELD_PATTERNS) && isValidEmail(value)) {\n return value.trim()\n }\n }\n\n // Priority 3: Any field with email-like value (fallback)\n for (const value of Object.values(fields)) {\n if (isValidEmail(value)) {\n return value.trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Extract name fields from form data.\n *\n * Looks for:\n * - Full name fields (name, full_name, etc.)\n * - First name fields (first_name, fname, etc.)\n * - Last name fields (last_name, lname, etc.)\n *\n * If only first/last names are found, combines them into a full name.\n *\n * @param fields - Form field key-value pairs\n * @returns Object with name, firstName, and/or lastName if found\n */\nexport function findNameFields(fields: Record<string, string>): {\n name?: string\n firstName?: string\n lastName?: string\n} {\n let fullName: string | undefined\n let firstName: string | undefined\n let lastName: string | undefined\n\n for (const [fieldName, value] of Object.entries(fields)) {\n const trimmedValue = value?.trim()\n if (!trimmedValue) continue\n\n // Check for full name\n if (!fullName && matchesPatterns(fieldName, FULL_NAME_PATTERNS)) {\n fullName = trimmedValue\n }\n\n // Check for first name\n if (!firstName && matchesPatterns(fieldName, FIRST_NAME_PATTERNS)) {\n firstName = trimmedValue\n }\n\n // Check for last name\n if (!lastName && matchesPatterns(fieldName, LAST_NAME_PATTERNS)) {\n lastName = trimmedValue\n }\n }\n\n const result: { name?: string; firstName?: string; lastName?: string } = {}\n\n // If we have a full name, use it\n if (fullName) {\n result.name = fullName\n }\n // If we have first and last, combine them\n else if (firstName && lastName) {\n result.name = `${firstName} ${lastName}`\n result.firstName = firstName\n result.lastName = lastName\n }\n // If we only have first name\n else if (firstName) {\n result.firstName = firstName\n }\n // If we only have last name\n else if (lastName) {\n result.lastName = lastName\n }\n\n return result\n}\n\n/**\n * Identity extracted from a form submission.\n */\nexport interface ExtractedIdentity {\n email: string\n name?: string\n firstName?: string\n lastName?: string\n}\n\n/**\n * Extract identity information (email + name) from form fields.\n *\n * Returns undefined if no valid email is found (email is required for identification).\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns Extracted identity with email and optional name fields, or undefined\n */\nexport function extractIdentityFromForm(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): ExtractedIdentity | undefined {\n const email = findEmailField(fields, inputTypes)\n\n // Email is required for identification\n if (!email) {\n return undefined\n }\n\n const nameFields = findNameFields(fields)\n\n return {\n email,\n ...nameFields,\n }\n}\n","import type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n EngagementEvent,\n ExplicitJourneyStage,\n FormEvent,\n IdentifyEvent,\n IngestPayload,\n PageviewEvent,\n PayloadUserIdentity,\n SourceType,\n StageEvent,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\n// ============================================\n// EVENT BUILDERS\n// ============================================\n\ninterface BaseEventParams {\n url: string\n referrer?: string\n timestamp?: number\n}\n\n/**\n * Build a pageview event.\n */\nexport function buildPageviewEvent(params: BaseEventParams & { title?: string }): PageviewEvent {\n const { url, referrer, timestamp, title } = params\n return {\n type: \"pageview\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n title,\n }\n}\n\n/**\n * Build a form event.\n */\nexport function buildFormEvent(\n params: BaseEventParams & {\n formId?: string\n formFields?: Record<string, string>\n },\n): FormEvent {\n const { url, referrer, timestamp, formId, formFields } = params\n return {\n type: \"form\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n formId,\n formFields,\n }\n}\n\n/**\n * Build an identify event.\n */\nexport function buildIdentifyEvent(\n params: BaseEventParams & {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n },\n): IdentifyEvent {\n const { url, referrer, timestamp, email, userId, traits } = params\n return {\n type: \"identify\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n email,\n userId,\n traits,\n }\n}\n\n/**\n * Build a custom event.\n */\nexport function buildCustomEvent(\n params: BaseEventParams & {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n },\n): CustomEvent {\n const { url, referrer, timestamp, eventName, properties } = params\n return {\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n properties,\n }\n}\n\n/**\n * Build a calendar booking event.\n */\nexport function buildCalendarEvent(\n params: BaseEventParams & {\n provider: CalendarProvider\n eventType?: string\n startTime?: string\n endTime?: string\n duration?: number\n isRecurring?: boolean\n inviteeEmail?: string\n inviteeName?: string\n },\n): CalendarEvent {\n const {\n url,\n referrer,\n timestamp,\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n } = params\n return {\n type: \"calendar\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n }\n}\n\n/**\n * Build an engagement event.\n * Captures active time on page for session analytics.\n */\nexport function buildEngagementEvent(\n params: BaseEventParams & {\n activeTimeMs: number\n totalTimeMs: number\n sessionId: string\n },\n): EngagementEvent {\n const { url, referrer, timestamp, activeTimeMs, totalTimeMs, sessionId } = params\n return {\n type: \"engagement\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n activeTimeMs,\n totalTimeMs,\n sessionId,\n }\n}\n\n/**\n * Build a stage event.\n * Used to explicitly set customer journey stage (activated, engaged, paid, churned).\n * discovered/signed_up stages are inferred from identify calls.\n */\nexport function buildStageEvent(\n params: BaseEventParams & {\n stage: ExplicitJourneyStage\n properties?: Record<string, string | number | boolean | null>\n },\n): StageEvent {\n const { url, referrer, timestamp, stage, properties } = params\n return {\n type: \"stage\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n stage,\n properties,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n *\n * @param visitorId - The anonymous visitor ID from browser cookie/storage\n * @param source - The event source (client, server, integration)\n * @param events - Array of events to send\n * @param userIdentity - Optional user identity for immediate resolution (from setUser in SPA)\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n userIdentity?: PayloadUserIdentity,\n): IngestPayload {\n const payload: IngestPayload = {\n visitorId,\n source,\n events,\n }\n\n // Only include userIdentity if it has actual values\n if (userIdentity && (userIdentity.email || userIdentity.userId)) {\n payload.userIdentity = {\n ...(userIdentity.email && { email: userIdentity.email }),\n ...(userIdentity.userId && { userId: userIdentity.userId }),\n }\n }\n\n return payload\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = 100\n\n/**\n * Split events into batches of MAX_BATCH_SIZE.\n */\nexport function batchEvents(events: TrackerEvent[]): TrackerEvent[][] {\n const batches: TrackerEvent[][] = []\n for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {\n batches.push(events.slice(i, i + MAX_BATCH_SIZE))\n }\n return batches\n}\n","import { type ExtractedIdentity, extractIdentityFromForm, sanitizeFormFields } from \"@outlit/core\"\n\n// ============================================\n// PAGEVIEW TRACKING\n// ============================================\n\ntype PageviewCallback = (url: string, referrer: string, title: string) => void\n\nlet pageviewCallback: PageviewCallback | null = null\nlet lastUrl: string | null = null\n\n/**\n * Initialize automatic pageview tracking.\n * Captures initial pageview and listens for SPA navigation.\n */\nexport function initPageviewTracking(callback: PageviewCallback): void {\n pageviewCallback = callback\n\n // Capture initial pageview\n capturePageview()\n\n // Listen for SPA navigation\n setupSpaListeners()\n}\n\n/**\n * Capture a pageview event.\n */\nfunction capturePageview(): void {\n if (!pageviewCallback) return\n\n const url = window.location.href\n const referrer = document.referrer\n const title = document.title\n\n // Avoid duplicate pageviews for the same URL\n if (url === lastUrl) return\n lastUrl = url\n\n pageviewCallback(url, referrer, title)\n}\n\n/**\n * Capture pageview after a small delay.\n * SPAs update document.title asynchronously after navigation,\n * so we need to wait for the title to be updated.\n *\n * Different frameworks update titles at different times:\n * - React: after commit phase (typically after rAF)\n * - Next.js: after route change completes\n * - Framer: after page transition completes\n *\n * A small delay (10ms) reliably captures the updated title\n * while being imperceptible to users.\n */\nfunction capturePageviewDelayed(): void {\n setTimeout(capturePageview, 10)\n}\n\n/**\n * Set up listeners for SPA navigation.\n */\nfunction setupSpaListeners(): void {\n // Listen for popstate (browser back/forward)\n window.addEventListener(\"popstate\", () => {\n capturePageviewDelayed()\n })\n\n // Monkey-patch pushState and replaceState\n const originalPushState = history.pushState\n const originalReplaceState = history.replaceState\n\n history.pushState = function (...args) {\n originalPushState.apply(this, args)\n capturePageviewDelayed()\n }\n\n history.replaceState = function (...args) {\n originalReplaceState.apply(this, args)\n capturePageviewDelayed()\n }\n}\n\n// ============================================\n// FORM TRACKING\n// ============================================\n\ntype FormCallback = (\n url: string,\n formId: string | undefined,\n fields: Record<string, string>,\n) => void\n\ntype IdentityCallback = (identity: ExtractedIdentity) => void\n\nlet formCallback: FormCallback | null = null\nlet formDenylist: string[] | undefined\nlet identityCallback: IdentityCallback | null = null\n\n/**\n * Initialize automatic form tracking.\n * Captures form submissions with field sanitization.\n *\n * @param callback - Called when a form is submitted with sanitized fields\n * @param denylist - Optional list of field names to exclude\n * @param onIdentity - Optional callback for auto-identification when email is found\n */\nexport function initFormTracking(\n callback: FormCallback,\n denylist?: string[],\n onIdentity?: IdentityCallback,\n): void {\n formCallback = callback\n formDenylist = denylist\n identityCallback = onIdentity ?? null\n\n // Listen for form submissions\n document.addEventListener(\"submit\", handleFormSubmit, true)\n}\n\n/**\n * Handle form submission events.\n */\nfunction handleFormSubmit(event: Event): void {\n if (!formCallback) return\n\n const form = event.target as HTMLFormElement\n if (!(form instanceof HTMLFormElement)) return\n\n const url = window.location.href\n const formId = form.id || form.name || undefined\n\n // Extract form fields and input types\n const formData = new FormData(form)\n const fields: Record<string, string> = {}\n const inputTypes = new Map<string, string>()\n\n // Get input types for better email detection\n const inputs = form.querySelectorAll(\"input, select, textarea\")\n for (const input of inputs) {\n const name = input.getAttribute(\"name\")\n if (name && input instanceof HTMLInputElement) {\n inputTypes.set(name, input.type)\n }\n }\n\n formData.forEach((value, key) => {\n // Only capture string values, skip files\n if (typeof value === \"string\") {\n fields[key] = value\n }\n })\n\n // Sanitize fields to remove sensitive data\n const sanitizedFields = sanitizeFormFields(fields, formDenylist)\n\n // Auto-identify if callback is set and we find identity fields\n // Use unsanitized fields for identity extraction (email might be in there)\n if (identityCallback) {\n const identity = extractIdentityFromForm(fields, inputTypes)\n if (identity) {\n identityCallback(identity)\n }\n }\n\n // Emit form event (with sanitized fields)\n if (sanitizedFields && Object.keys(sanitizedFields).length > 0) {\n formCallback(url, formId, sanitizedFields)\n }\n}\n\n// ============================================\n// CLEANUP\n// ============================================\n\n/**\n * Stop all autocapture tracking.\n */\nexport function stopAutocapture(): void {\n pageviewCallback = null\n formCallback = null\n identityCallback = null\n document.removeEventListener(\"submit\", handleFormSubmit, true)\n}\n","/**\n * Third-party Embed Integrations\n *\n * This module handles automatic tracking of booking events from\n * third-party calendar embeds like Cal.com and Calendly.\n *\n * Cal.com Integration:\n * - Hooks into Cal() API for booking events via Cal(\"on\", { action: \"bookingSuccessfulV2\" })\n * - Captures booking details: event type, time, duration, invitee name (from title)\n *\n * IMPORTANT - Email Limitation:\n * Cal.com does NOT expose invitee email in their client-side events for privacy.\n * The email IS in the success page URL, but:\n * 1. iframe.src attribute doesn't update (Cal.com uses client-side routing)\n * 2. contentWindow.location.href is blocked by cross-origin policy\n *\n * To get email from Cal.com bookings, you need SERVER-SIDE WEBHOOKS:\n * 1. Set up Cal.com webhook: Settings → Developer → Webhooks\n * 2. Point it to your server endpoint\n * 3. Server calls Outlit identify() API with the email from webhook payload\n */\n\nimport type { CalendarProvider } from \"@outlit/core\"\n\n// ============================================\n// TYPES\n// ============================================\n\nexport interface CalendarBookingEvent {\n provider: CalendarProvider\n eventType?: string\n startTime?: string\n endTime?: string\n duration?: number\n isRecurring?: boolean\n inviteeEmail?: string\n inviteeName?: string\n}\n\nexport interface CalendarIntegrationCallbacks {\n onCalendarBooked: (event: CalendarBookingEvent) => void\n onIdentity?: (identity: { email: string; name?: string }) => void\n}\n\n// ============================================\n// CAL.COM TYPES\n// ============================================\n\ninterface CalComBookingData {\n uid?: string\n title?: string\n startTime?: string\n endTime?: string\n eventTypeId?: number\n status?: string\n paymentRequired?: boolean\n isRecurring?: boolean\n}\n\ninterface CalComEventDetail {\n data: CalComBookingData\n type: string\n namespace: string\n}\n\n// Cal.com's Cal() function type\ntype CalFunction = {\n (\n method: \"on\",\n options: { action: string; callback: (e: { detail: CalComEventDetail }) => void },\n ): void\n (method: string, ...args: unknown[]): void\n loaded?: boolean\n q?: unknown[][]\n ns?: Record<string, unknown>\n}\n\n// ============================================\n// STATE\n// ============================================\n\nlet callbacks: CalendarIntegrationCallbacks | null = null\nlet isListening = false\nlet calSetupAttempts = 0\nlet calCallbackRegistered = false\nlet lastBookingUid: string | null = null // Prevent duplicate events\n\n// ============================================\n// CONFIG\n// ============================================\n\nconst CAL_MAX_RETRY_ATTEMPTS = 10 // Max attempts to find Cal() API\nconst CAL_INITIAL_DELAY_MS = 200 // Start with short delay\nconst CAL_MAX_DELAY_MS = 2000 // Cap retry delay at 2s\n\n// ============================================\n// CAL.COM BOOKING PARSER\n// ============================================\n\nfunction parseCalComBooking(data: CalComBookingData): CalendarBookingEvent {\n const event: CalendarBookingEvent = {\n provider: \"cal.com\",\n }\n\n if (data.title) {\n event.eventType = data.title\n // Extract invitee name from title: \"Meeting between Host and Guest\"\n const nameMatch = data.title.match(/between .+ and (.+)$/i)\n if (nameMatch?.[1]) {\n event.inviteeName = nameMatch[1].trim()\n }\n }\n\n if (data.startTime) event.startTime = data.startTime\n if (data.endTime) event.endTime = data.endTime\n\n if (data.startTime && data.endTime) {\n const start = new Date(data.startTime)\n const end = new Date(data.endTime)\n event.duration = Math.round((end.getTime() - start.getTime()) / 60000)\n }\n\n if (data.isRecurring !== undefined) {\n event.isRecurring = data.isRecurring\n }\n\n // Note: Email is NOT available from Cal.com client-side events\n // Use server-side webhooks to get email for identify()\n\n return event\n}\n\n// ============================================\n// CAL.COM API INTEGRATION\n// ============================================\n\n/**\n * Set up listener for Cal.com's Cal() API.\n * Registers callback via Cal(\"on\", ...) with retries.\n */\nfunction setupCalComListener(): void {\n if (typeof window === \"undefined\") return\n if (calCallbackRegistered) return\n\n calSetupAttempts++\n\n // Check if Cal() API exists\n if (\"Cal\" in window) {\n const Cal = (window as unknown as { Cal: CalFunction }).Cal\n\n if (typeof Cal === \"function\") {\n try {\n Cal(\"on\", {\n action: \"bookingSuccessfulV2\",\n callback: handleCalComBooking,\n })\n calCallbackRegistered = true\n return\n } catch (_e) {\n // Registration failed, will retry\n }\n }\n }\n\n // Cal() not ready yet, retry with backoff\n if (calSetupAttempts < CAL_MAX_RETRY_ATTEMPTS) {\n const delay = Math.min(CAL_INITIAL_DELAY_MS * calSetupAttempts, CAL_MAX_DELAY_MS)\n setTimeout(setupCalComListener, delay)\n }\n}\n\nfunction handleCalComBooking(e: { detail: CalComEventDetail }): void {\n if (!callbacks) return\n\n const data = e.detail?.data\n if (!data) return\n\n // Prevent duplicate events for the same booking\n if (data.uid && data.uid === lastBookingUid) return\n lastBookingUid = data.uid || null\n\n const bookingEvent = parseCalComBooking(data)\n callbacks.onCalendarBooked(bookingEvent)\n}\n\n// ============================================\n// POSTMESSAGE HANDLER (FALLBACK)\n// ============================================\n\nfunction handlePostMessage(event: MessageEvent): void {\n if (!callbacks) return\n\n // Check for Calendly events\n if (isCalendlyEvent(event)) {\n if (event.data.event === \"calendly.event_scheduled\") {\n const bookingEvent = parseCalendlyBooking(event.data.payload)\n callbacks.onCalendarBooked(bookingEvent)\n }\n return\n }\n\n // Check for Cal.com postMessages (fallback if Cal() API not available)\n if (isCalComRawMessage(event)) {\n const bookingData = extractCalComBookingFromMessage(event.data)\n if (bookingData) {\n // Prevent duplicates\n if (bookingData.uid && bookingData.uid === lastBookingUid) return\n lastBookingUid = bookingData.uid || null\n\n const bookingEvent = parseCalComBooking(bookingData)\n callbacks.onCalendarBooked(bookingEvent)\n }\n }\n}\n\nfunction isCalComRawMessage(event: MessageEvent): boolean {\n if (!event.origin.includes(\"cal.com\")) return false\n\n const data = event.data\n if (!data || typeof data !== \"object\") return false\n\n // Cal.com sends type: 'bookingSuccessfulV2' for successful bookings\n const messageType = data.type || data.action\n return (\n messageType === \"bookingSuccessfulV2\" ||\n messageType === \"bookingSuccessful\" ||\n messageType === \"booking_successful\"\n )\n}\n\nfunction extractCalComBookingFromMessage(data: unknown): CalComBookingData | null {\n if (!data || typeof data !== \"object\") return null\n\n const messageData = data as Record<string, unknown>\n\n // Cal.com sends: { originator: 'CAL', type: 'bookingSuccessfulV2', data: { uid, title, ... } }\n if (messageData.data && typeof messageData.data === \"object\") {\n return messageData.data as CalComBookingData\n }\n\n if (messageData.booking && typeof messageData.booking === \"object\") {\n return messageData.booking as CalComBookingData\n }\n\n return null\n}\n\n// ============================================\n// CALENDLY INTEGRATION\n// ============================================\n\ninterface CalendlyPayload {\n event?: { uri?: string }\n invitee?: { uri?: string }\n}\n\ninterface CalendlyMessageData {\n event: string\n payload: CalendlyPayload\n}\n\nfunction isCalendlyEvent(e: MessageEvent): e is MessageEvent<CalendlyMessageData> {\n return (\n e.origin === \"https://calendly.com\" &&\n e.data &&\n typeof e.data.event === \"string\" &&\n e.data.event.startsWith(\"calendly.\")\n )\n}\n\nfunction parseCalendlyBooking(_payload: CalendlyPayload): CalendarBookingEvent {\n return {\n provider: \"calendly\",\n }\n}\n\n// ============================================\n// PUBLIC API\n// ============================================\n\n/**\n * Initialize calendar embed tracking.\n */\nexport function initCalendarTracking(cbs: CalendarIntegrationCallbacks): void {\n if (isListening) return\n\n callbacks = cbs\n isListening = true\n calSetupAttempts = 0\n\n // Listen for postMessage events (Calendly, fallback for Cal.com)\n window.addEventListener(\"message\", handlePostMessage)\n\n // Set up Cal.com API listener\n setupCalComListener()\n}\n\n/**\n * Stop calendar embed tracking.\n */\nexport function stopCalendarTracking(): void {\n if (!isListening) return\n\n window.removeEventListener(\"message\", handlePostMessage)\n\n callbacks = null\n isListening = false\n calCallbackRegistered = false\n calSetupAttempts = 0\n lastBookingUid = null\n}\n\n/**\n * Check if calendar tracking is active.\n */\nexport function isCalendarTrackingActive(): boolean {\n return isListening\n}\n","import { type EngagementEvent, buildEngagementEvent } from \"@outlit/core\"\n\n// ============================================\n// SESSION TRACKER\n// ============================================\n\n/**\n * Default idle timeout in milliseconds (30 seconds).\n * After this period of no user interaction, the user is considered idle.\n */\nconst DEFAULT_IDLE_TIMEOUT = 30000\n\n/**\n * Session timeout in milliseconds (30 minutes).\n * After this period of inactivity, a new session ID is generated.\n */\nconst SESSION_TIMEOUT = 30 * 60 * 1000\n\n/**\n * Interval for updating active time (1 second).\n */\nconst TIME_UPDATE_INTERVAL = 1000\n\n/**\n * Minimum threshold to consider an engagement event spurious (50ms).\n * Events with BOTH activeTimeMs < 50ms AND totalTimeMs < 50ms are likely\n * caused by browser visibility quirks during page load and should be skipped.\n * This prevents emitting events with activeTimeMs=1ms while still allowing\n * legitimate events where the user was on the page for a meaningful duration.\n */\nconst MIN_SPURIOUS_THRESHOLD = 50\n\n/**\n * Minimum time on a page before engagement can be emitted (500ms).\n * This prevents spurious engagement events when visibility toggles occur\n * shortly after SPA navigation (e.g., Framer page transitions).\n * Without this, navigating from /terms to / and then quickly toggling\n * visibility would incorrectly emit engagement for \"/\" immediately.\n */\nconst MIN_PAGE_TIME_FOR_ENGAGEMENT = 500\n\n/**\n * Storage keys for session data in sessionStorage.\n */\nconst SESSION_ID_KEY = \"outlit_session_id\"\nconst SESSION_LAST_ACTIVITY_KEY = \"outlit_session_last_activity\"\n\n// ============================================\n// TYPES\n// ============================================\n\ninterface SessionState {\n /** Full URL when the session started (used for engagement event) */\n currentUrl: string\n /** Path portion of the URL */\n currentPath: string\n /** Timestamp when user entered the current page */\n pageEntryTime: number\n /** Last timestamp when we recorded activity */\n lastActiveTime: number\n /** Accumulated active time in milliseconds */\n activeTimeMs: number\n /** Is the tab currently visible? */\n isPageVisible: boolean\n /** Has the user interacted recently (within idle timeout)? */\n isUserActive: boolean\n /** Timeout ID for idle detection */\n idleTimeoutId: ReturnType<typeof setTimeout> | null\n /** Session ID for grouping engagement events */\n sessionId: string\n /** Whether we've already emitted an engagement event for this page session */\n hasEmittedEngagement: boolean\n}\n\nexport interface SessionTrackerOptions {\n /** Callback to emit engagement events (typically Tracker.enqueue) */\n onEngagement: (event: EngagementEvent) => void\n /** Idle timeout in milliseconds. Default: 30000 (30s) */\n idleTimeout?: number\n}\n\n// ============================================\n// SESSION TRACKER CLASS\n// ============================================\n\nexport class SessionTracker {\n private state: SessionState\n private options: SessionTrackerOptions\n private idleTimeout: number\n private timeUpdateInterval: ReturnType<typeof setInterval> | null = null\n private boundHandleActivity: () => void\n private boundHandleVisibilityChange: () => void\n\n constructor(options: SessionTrackerOptions) {\n this.options = options\n this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT\n\n // Initialize state for current page (including session ID)\n this.state = this.createInitialState()\n\n // Bind event handlers\n this.boundHandleActivity = this.handleActivity.bind(this)\n this.boundHandleVisibilityChange = this.handleVisibilityChange.bind(this)\n\n // Set up event listeners\n this.setupEventListeners()\n\n // Start time update interval\n this.startTimeUpdateInterval()\n }\n\n /**\n * Get the current session ID.\n */\n getSessionId(): string {\n return this.state.sessionId\n }\n\n // ============================================\n // PUBLIC METHODS\n // ============================================\n\n /**\n * Emit an engagement event for the current page session.\n * Called by Tracker on exit events and SPA navigation.\n *\n * This method:\n * 1. Finalizes any pending active time\n * 2. Creates and emits the engagement event (if meaningful)\n * 3. Resets state for the next session\n */\n emitEngagement(): void {\n // 1. Check if we've already handled exit for this page session (deduplication)\n // This prevents duplicate events from multiple exit handlers (visibilitychange,\n // pagehide, beforeunload) firing for the same page exit.\n if (this.state.hasEmittedEngagement) {\n return\n }\n\n // 2. Mark as handled FIRST to prevent any concurrent/reentrant calls\n this.state.hasEmittedEngagement = true\n\n // 3. Finalize any pending active time\n this.updateActiveTime()\n\n // 4. Create and emit event (only if we have meaningful data)\n const totalTimeMs = Date.now() - this.state.pageEntryTime\n\n // Skip spurious events caused by browser visibility quirks during page load.\n // These occur when a hidden→visible transition happens almost immediately after\n // SDK initialization, resulting in activeTimeMs ≈ 1ms and totalTimeMs ≈ 5ms.\n // We only skip if BOTH values are below the threshold - legitimate events will\n // have at least one meaningful value (user was on page for some duration).\n const isSpuriousEvent =\n this.state.activeTimeMs < MIN_SPURIOUS_THRESHOLD && totalTimeMs < MIN_SPURIOUS_THRESHOLD\n\n // Skip events if user hasn't been on the page long enough.\n // This prevents spurious engagement events when visibility toggles occur\n // shortly after SPA navigation (e.g., during Framer page transitions).\n // Without this check, navigating then quickly toggling visibility would\n // incorrectly emit engagement for the new page immediately.\n const isTooSoonAfterNavigation = totalTimeMs < MIN_PAGE_TIME_FOR_ENGAGEMENT\n\n if (!isSpuriousEvent && !isTooSoonAfterNavigation) {\n const event = buildEngagementEvent({\n url: this.state.currentUrl,\n referrer: document.referrer,\n activeTimeMs: this.state.activeTimeMs,\n totalTimeMs,\n sessionId: this.state.sessionId,\n })\n this.options.onEngagement(event)\n }\n\n // 5. Reset state for next engagement period (preserves sessionId and hasEmittedEngagement)\n this.resetState()\n }\n\n /**\n * Handle SPA navigation.\n * Called by Tracker when a new pageview is detected.\n *\n * This method:\n * 1. Emits engagement for the OLD page (using stored state)\n * 2. Updates state for the NEW page\n */\n onNavigation(newUrl: string): void {\n // Emit engagement for OLD page (uses state.currentUrl, not window.location)\n this.emitEngagement()\n\n // Update state for NEW page\n this.state.currentUrl = newUrl\n this.state.currentPath = this.extractPath(newUrl)\n this.state.pageEntryTime = Date.now()\n this.state.activeTimeMs = 0\n this.state.lastActiveTime = Date.now()\n this.state.isUserActive = true\n // Reset the engagement flag for the new page session\n this.state.hasEmittedEngagement = false\n\n // Reset idle timer\n this.resetIdleTimer()\n }\n\n /**\n * Stop session tracking and clean up.\n */\n stop(): void {\n // Remove event listeners\n this.removeEventListeners()\n\n // Clear intervals and timeouts\n if (this.timeUpdateInterval) {\n clearInterval(this.timeUpdateInterval)\n this.timeUpdateInterval = null\n }\n\n if (this.state.idleTimeoutId) {\n clearTimeout(this.state.idleTimeoutId)\n this.state.idleTimeoutId = null\n }\n }\n\n // ============================================\n // PRIVATE METHODS\n // ============================================\n\n private createInitialState(): SessionState {\n const now = Date.now()\n return {\n currentUrl: typeof window !== \"undefined\" ? window.location.href : \"\",\n currentPath: typeof window !== \"undefined\" ? window.location.pathname : \"/\",\n pageEntryTime: now,\n lastActiveTime: now,\n activeTimeMs: 0,\n isPageVisible:\n typeof document !== \"undefined\" ? document.visibilityState === \"visible\" : true,\n isUserActive: true, // Assume active on page load\n idleTimeoutId: null,\n sessionId: this.getOrCreateSessionId(),\n hasEmittedEngagement: false,\n }\n }\n\n private resetState(): void {\n const now = Date.now()\n this.state.pageEntryTime = now\n this.state.lastActiveTime = now\n this.state.activeTimeMs = 0\n this.state.isUserActive = true\n // Note: hasEmittedEngagement is NOT reset here - it's only reset in onNavigation\n // when a new page session begins. This prevents duplicate events on the same page.\n // Note: sessionId is preserved across page navigations within the same session\n\n // Reset idle timer\n this.resetIdleTimer()\n }\n\n /**\n * Get existing session ID from storage or create a new one.\n * Session ID is reset if:\n * - No existing session ID in storage\n * - Last activity was more than 30 minutes ago\n */\n private getOrCreateSessionId(): string {\n if (typeof sessionStorage === \"undefined\") {\n return this.generateSessionId()\n }\n\n try {\n const existingSessionId = sessionStorage.getItem(SESSION_ID_KEY)\n const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY)\n const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0\n const now = Date.now()\n\n // Check if session has expired (30 minutes of inactivity)\n if (existingSessionId && lastActivity && now - lastActivity < SESSION_TIMEOUT) {\n // Session is still valid, update last activity\n this.updateSessionActivity()\n return existingSessionId\n }\n\n // Create new session\n const newSessionId = this.generateSessionId()\n sessionStorage.setItem(SESSION_ID_KEY, newSessionId)\n sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString())\n return newSessionId\n } catch {\n // sessionStorage might throw in private browsing mode\n return this.generateSessionId()\n }\n }\n\n /**\n * Generate a new session ID (UUID v4).\n */\n private generateSessionId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n\n // Fallback for older browsers\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n }\n\n /**\n * Update the session's last activity timestamp.\n */\n private updateSessionActivity(): void {\n if (typeof sessionStorage === \"undefined\") return\n\n try {\n sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, Date.now().toString())\n } catch {\n // Ignore storage errors\n }\n }\n\n /**\n * Check if the current session has expired and create a new one if needed.\n * Called when user returns to the page after being away.\n */\n private checkSessionExpiry(): void {\n if (typeof sessionStorage === \"undefined\") return\n\n try {\n const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY)\n const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0\n const now = Date.now()\n\n if (now - lastActivity >= SESSION_TIMEOUT) {\n // Session expired, create new one\n const newSessionId = this.generateSessionId()\n sessionStorage.setItem(SESSION_ID_KEY, newSessionId)\n this.state.sessionId = newSessionId\n }\n\n // Update last activity\n sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString())\n } catch {\n // Ignore storage errors\n }\n }\n\n private setupEventListeners(): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n\n // Activity events - explicit user interactions only\n // Do NOT use media events (timeupdate, play, etc.) to avoid video loop false positives\n const activityEvents = [\"mousemove\", \"keydown\", \"click\", \"scroll\", \"touchstart\"]\n for (const event of activityEvents) {\n document.addEventListener(event, this.boundHandleActivity, { passive: true })\n }\n\n // Visibility change - pause/resume time accumulation\n document.addEventListener(\"visibilitychange\", this.boundHandleVisibilityChange)\n\n // Start idle timer\n this.resetIdleTimer()\n }\n\n private removeEventListeners(): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n\n const activityEvents = [\"mousemove\", \"keydown\", \"click\", \"scroll\", \"touchstart\"]\n for (const event of activityEvents) {\n document.removeEventListener(event, this.boundHandleActivity)\n }\n\n document.removeEventListener(\"visibilitychange\", this.boundHandleVisibilityChange)\n }\n\n /**\n * Handle user activity events.\n * Marks user as active and resets idle timer.\n */\n private handleActivity(): void {\n // If user was idle, check if session expired while idle\n if (!this.state.isUserActive) {\n this.checkSessionExpiry()\n this.state.lastActiveTime = Date.now()\n }\n\n this.state.isUserActive = true\n this.resetIdleTimer()\n\n // Update session activity timestamp (throttled - every activity event)\n this.updateSessionActivity()\n }\n\n /**\n * Handle visibility change events.\n * Pauses time accumulation when tab is hidden.\n */\n private handleVisibilityChange(): void {\n const wasVisible = this.state.isPageVisible\n const isNowVisible = document.visibilityState === \"visible\"\n\n if (wasVisible && !isNowVisible) {\n // Tab is being hidden - capture any pending active time BEFORE updating state\n // (updateActiveTime checks isPageVisible, so we must call it first)\n this.updateActiveTime()\n }\n\n // Update state after capturing time\n this.state.isPageVisible = isNowVisible\n\n if (!wasVisible && isNowVisible) {\n // Tab is becoming visible - check if session expired while away\n this.checkSessionExpiry()\n // Reset last active time\n this.state.lastActiveTime = Date.now()\n // Reset engagement flag to allow new engagement event on next exit\n this.state.hasEmittedEngagement = false\n }\n }\n\n /**\n * Reset the idle timer.\n * Called on activity and initialization.\n */\n private resetIdleTimer(): void {\n if (this.state.idleTimeoutId) {\n clearTimeout(this.state.idleTimeoutId)\n }\n\n this.state.idleTimeoutId = setTimeout(() => {\n // Finalize active time before marking as idle\n this.updateActiveTime()\n this.state.isUserActive = false\n }, this.idleTimeout)\n }\n\n /**\n * Start the interval for updating active time.\n */\n private startTimeUpdateInterval(): void {\n if (this.timeUpdateInterval) return\n\n this.timeUpdateInterval = setInterval(() => {\n this.updateActiveTime()\n }, TIME_UPDATE_INTERVAL)\n }\n\n /**\n * Update accumulated active time.\n * Only accumulates when page is visible AND user is active.\n */\n private updateActiveTime(): void {\n if (this.state.isPageVisible && this.state.isUserActive) {\n const now = Date.now()\n this.state.activeTimeMs += now - this.state.lastActiveTime\n this.state.lastActiveTime = now\n }\n }\n\n /**\n * Extract path from URL.\n */\n private extractPath(url: string): string {\n try {\n return new URL(url).pathname\n } catch {\n return \"/\"\n }\n }\n}\n\n// ============================================\n// MODULE-LEVEL FUNCTIONS\n// ============================================\n\nlet sessionTrackerInstance: SessionTracker | null = null\n\n/**\n * Initialize session tracking.\n * @returns The SessionTracker instance\n */\nexport function initSessionTracking(options: SessionTrackerOptions): SessionTracker {\n if (sessionTrackerInstance) {\n console.warn(\"[Outlit] Session tracking already initialized\")\n return sessionTrackerInstance\n }\n\n sessionTrackerInstance = new SessionTracker(options)\n return sessionTrackerInstance\n}\n\n/**\n * Stop session tracking and clean up.\n */\nexport function stopSessionTracking(): void {\n if (sessionTrackerInstance) {\n sessionTrackerInstance.stop()\n sessionTrackerInstance = null\n }\n}\n","// ============================================\n// VISITOR ID STORAGE\n// ============================================\n\nconst VISITOR_ID_KEY = \"outlit_visitor_id\"\n\n/**\n * Generate a UUID v4.\n * Uses crypto.randomUUID if available, otherwise falls back to manual generation.\n */\nexport function generateVisitorId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n\n // Fallback for older browsers\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\n/**\n * Get the visitor ID from storage, generating a new one if needed.\n * Tries localStorage first, falls back to cookie.\n */\nexport function getOrCreateVisitorId(): string {\n // Try localStorage first\n try {\n const stored = localStorage.getItem(VISITOR_ID_KEY)\n if (stored && isValidUuid(stored)) {\n return stored\n }\n } catch {\n // localStorage not available\n }\n\n // Try cookie fallback\n const cookieValue = getCookie(VISITOR_ID_KEY)\n if (cookieValue && isValidUuid(cookieValue)) {\n // Also store in localStorage for consistency\n try {\n localStorage.setItem(VISITOR_ID_KEY, cookieValue)\n } catch {\n // Ignore\n }\n return cookieValue\n }\n\n // Generate new visitor ID\n const visitorId = generateVisitorId()\n persistVisitorId(visitorId)\n return visitorId\n}\n\n/**\n * Persist visitor ID to both localStorage and cookie.\n */\nfunction persistVisitorId(visitorId: string): void {\n // Store in localStorage\n try {\n localStorage.setItem(VISITOR_ID_KEY, visitorId)\n } catch {\n // localStorage not available\n }\n\n // Also store in cookie for cross-subdomain support\n setCookie(VISITOR_ID_KEY, visitorId, 365) // 1 year\n}\n\n/**\n * Basic UUID validation.\n */\nfunction isValidUuid(value: string): boolean {\n 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)\n}\n\n// ============================================\n// COOKIE HELPERS\n// ============================================\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null\n\n const value = `; ${document.cookie}`\n const parts = value.split(`; ${name}=`)\n if (parts.length === 2) {\n return parts.pop()?.split(\";\").shift() ?? null\n }\n return null\n}\n\n/**\n * Get the root domain for cross-subdomain cookie sharing.\n * e.g., \"www.example.com\" → \"example.com\"\n * \"app.staging.example.com\" → \"example.com\"\n * \"localhost\" → null (no domain attribute needed)\n */\nfunction getRootDomain(): string | null {\n if (typeof window === \"undefined\") return null\n\n const hostname = window.location.hostname\n\n // Don't set domain for localhost or IP addresses\n if (hostname === \"localhost\" || /^(\\d{1,3}\\.){3}\\d{1,3}$/.test(hostname)) {\n return null\n }\n\n // Split hostname into parts\n const parts = hostname.split(\".\")\n\n // For simple domains like \"example.com\", return \".example.com\"\n // For subdomains like \"www.example.com\" or \"app.example.com\", return \".example.com\"\n if (parts.length >= 2) {\n // Handle common TLDs with two parts (e.g., .co.uk, .com.au)\n const twoPartTlds = [\"co.uk\", \"com.au\", \"co.nz\", \"org.uk\", \"net.au\", \"com.br\"]\n const lastTwo = parts.slice(-2).join(\".\")\n\n if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {\n // e.g., \"www.example.co.uk\" → \"example.co.uk\"\n return parts.slice(-3).join(\".\")\n }\n\n // Standard case: \"www.example.com\" → \"example.com\"\n return parts.slice(-2).join(\".\")\n }\n\n return null\n}\n\nfunction setCookie(name: string, value: string, days: number): void {\n if (typeof document === \"undefined\") return\n\n const expires = new Date()\n expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)\n\n // Build cookie string\n let cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`\n\n // Add domain for cross-subdomain support\n const rootDomain = getRootDomain()\n if (rootDomain) {\n cookie += `;domain=${rootDomain}`\n }\n\n document.cookie = cookie\n}\n","import {\n type BrowserIdentifyOptions,\n type BrowserTrackOptions,\n DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type TrackerConfig,\n type TrackerEvent,\n buildCalendarEvent,\n buildCustomEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n buildStageEvent,\n} from \"@outlit/core\"\nimport { initFormTracking, initPageviewTracking, stopAutocapture } from \"./autocapture\"\nimport {\n type CalendarBookingEvent,\n initCalendarTracking,\n stopCalendarTracking,\n} from \"./embed-integrations\"\nimport { type SessionTracker, initSessionTracking, stopSessionTracking } from \"./session-tracker\"\nimport { getOrCreateVisitorId } from \"./storage\"\n\n// ============================================\n// OUTLIT CLIENT\n// ============================================\n\nexport interface OutlitOptions extends TrackerConfig {\n /**\n * Automatically start tracking on init.\n * Set to false if you need to wait for user consent before tracking.\n * Call enableTracking() to start tracking after consent is obtained.\n * @default true\n */\n autoTrack?: boolean\n trackPageviews?: boolean\n trackForms?: boolean\n formFieldDenylist?: string[]\n flushInterval?: number\n /**\n * Automatically identify users when they submit forms with email fields.\n * Extracts email and name (first/last) from form fields using heuristics.\n * @default true\n */\n autoIdentify?: boolean\n /**\n * Track booking events from calendar embeds (Cal.com, Calendly).\n * When enabled, fires a \"calendar_booked\" custom event when bookings are detected.\n *\n * NOTE: Due to privacy restrictions in Cal.com and Calendly, their postMessage\n * events do NOT include PII (email, name). Auto-identify is NOT possible with\n * these embeds using client-side tracking alone.\n *\n * For auto-identify with calendar bookings, use server-side webhooks.\n * @default true\n */\n trackCalendarEmbeds?: boolean\n /**\n * Track engagement metrics (active time on page).\n * When enabled, emits \"engagement\" events on page exit and SPA navigation\n * capturing how long users actively engaged with each page.\n * @default true\n */\n trackEngagement?: boolean\n /**\n * Idle timeout in milliseconds for engagement tracking.\n * After this period of no user interaction, the user is considered idle\n * and active time stops accumulating.\n * @default 30000 (30 seconds)\n */\n idleTimeout?: number\n}\n\nexport interface UserIdentity {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport class Outlit {\n private publicKey: string\n private apiHost: string\n private visitorId: string | null = null\n private eventQueue: TrackerEvent[] = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isInitialized = false\n private isTrackingEnabled = false\n private options: OutlitOptions\n private hasHandledExit = false\n private sessionTracker: SessionTracker | null = null\n // User identity state for stage events\n private currentUser: UserIdentity | null = null\n private pendingUser: UserIdentity | null = null\n\n constructor(options: OutlitOptions) {\n this.publicKey = options.publicKey\n this.apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 5000\n this.options = options\n\n // Set up exit handlers for reliable flushing\n // Uses multiple events because beforeunload is unreliable on mobile\n if (typeof window !== \"undefined\") {\n const handleExit = () => {\n if (this.hasHandledExit) return\n this.hasHandledExit = true\n\n // 1. Emit engagement event for current page (if session tracking enabled)\n this.sessionTracker?.emitEngagement()\n\n // 2. Flush the queue (now includes engagement event)\n this.flush()\n }\n\n // visibilitychange is most reliable - fires when tab is hidden\n document.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n handleExit()\n } else {\n // Reset when user returns to allow next exit to flush\n this.hasHandledExit = false\n }\n })\n\n // pagehide is reliable and bfcache-friendly\n window.addEventListener(\"pagehide\", handleExit)\n\n // beforeunload as fallback for older browsers\n window.addEventListener(\"beforeunload\", handleExit)\n }\n\n this.isInitialized = true\n\n // Start tracking immediately unless autoTrack is explicitly false\n if (options.autoTrack !== false) {\n this.enableTracking()\n }\n }\n\n // ============================================\n // PUBLIC API\n // ============================================\n\n /**\n * Enable tracking. Call this after obtaining user consent.\n * This will:\n * - Generate/retrieve the visitor ID\n * - Start automatic pageview and form tracking (if configured)\n * - Begin sending events to the server\n *\n * If autoTrack is true (default), this is called automatically on init.\n */\n enableTracking(): void {\n if (this.isTrackingEnabled) {\n return // Already enabled\n }\n\n // Now we can generate/retrieve the visitor ID (sets cookies/localStorage)\n this.visitorId = getOrCreateVisitorId()\n\n // Start the flush timer\n this.startFlushTimer()\n\n // Initialize session/engagement tracking if enabled (before pageview tracking)\n if (this.options.trackEngagement !== false) {\n this.initSessionTracking()\n }\n\n // Initialize autocapture if enabled\n if (this.options.trackPageviews !== false) {\n this.initPageviewTracking()\n }\n\n if (this.options.trackForms !== false) {\n this.initFormTracking(this.options.formFieldDenylist)\n }\n\n // Initialize calendar embed tracking if enabled\n if (this.options.trackCalendarEmbeds !== false) {\n this.initCalendarTracking()\n }\n\n this.isTrackingEnabled = true\n\n // Apply any pending user identity that was set before tracking was enabled\n if (this.pendingUser) {\n this.applyUser(this.pendingUser)\n this.pendingUser = null\n }\n }\n\n /**\n * Check if tracking is currently enabled.\n */\n isEnabled(): boolean {\n return this.isTrackingEnabled\n }\n\n /**\n * Track a custom event.\n */\n track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n const event = buildCustomEvent({\n url: window.location.href,\n referrer: document.referrer,\n eventName,\n properties,\n })\n this.enqueue(event)\n }\n\n /**\n * Identify the current visitor.\n * Links the anonymous visitor to a known user.\n *\n * When email or userId is provided, also sets the current user identity\n * for stage events (activate, engaged, paid).\n */\n identify(options: BrowserIdentifyOptions): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n // Update currentUser if email or userId is provided\n // This enables stage events after identify() is called\n if (options.email || options.userId) {\n this.currentUser = {\n email: options.email,\n userId: options.userId,\n }\n }\n\n const event = buildIdentifyEvent({\n url: window.location.href,\n referrer: document.referrer,\n email: options.email,\n userId: options.userId,\n traits: options.traits,\n })\n this.enqueue(event)\n }\n\n /**\n * Set the current user identity.\n * This is useful for SPA applications where you know the user's identity\n * after authentication. Calls identify() under the hood.\n *\n * If called before tracking is enabled, the identity is stored as pending\n * and applied automatically when enableTracking() is called.\n *\n * Note: Both setUser() and identify() enable stage events. The difference is\n * setUser() can be called before tracking is enabled (identity is queued),\n * while identify() requires tracking to be enabled first.\n */\n setUser(identity: UserIdentity): void {\n if (!identity.email && !identity.userId) {\n console.warn(\"[Outlit] setUser requires at least email or userId\")\n return\n }\n\n if (!this.isTrackingEnabled) {\n this.pendingUser = identity\n return\n }\n\n this.applyUser(identity)\n }\n\n /**\n * Clear the current user identity.\n * Call this when the user logs out.\n */\n clearUser(): void {\n this.currentUser = null\n this.pendingUser = null\n }\n\n /**\n * Apply user identity and send identify event.\n */\n private applyUser(identity: UserIdentity): void {\n this.currentUser = identity\n this.identify({ email: identity.email, userId: identity.userId, traits: identity.traits })\n }\n\n /**\n * Mark the current user as activated.\n * This is typically called after a user completes onboarding or a key activation milestone.\n * Requires the user to be identified (via setUser or identify with userId).\n */\n activate(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"activated\", properties)\n }\n\n /**\n * Mark the current user as engaged.\n * This is typically called when a user reaches a usage milestone.\n * Can also be computed automatically by the engagement cron.\n */\n engaged(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"engaged\", properties)\n }\n\n /**\n * Mark the current user as paid.\n * This is typically called after a successful payment/subscription.\n * Can also be triggered by Stripe integration.\n */\n paid(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"paid\", properties)\n }\n\n /**\n * Mark the current user as churned.\n * This is typically called when a subscription is cancelled.\n * Can also be triggered by Stripe integration.\n */\n churned(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"churned\", properties)\n }\n\n /**\n * Internal method to send a stage event.\n */\n private sendStageEvent(\n stage: ExplicitJourneyStage,\n properties?: Record<string, string | number | boolean | null>,\n ): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n if (!this.currentUser) {\n console.warn(\n `[Outlit] Cannot call ${stage}() without setting user identity. Call setUser() or identify() first.`,\n )\n return\n }\n\n const event = buildStageEvent({\n url: window.location.href,\n referrer: document.referrer,\n stage,\n properties,\n })\n this.enqueue(event)\n }\n\n /**\n * Get the current visitor ID.\n * Returns null if tracking is not enabled.\n */\n getVisitorId(): string | null {\n return this.visitorId\n }\n\n /**\n * Manually flush the event queue.\n */\n async flush(): Promise<void> {\n if (this.eventQueue.length === 0) return\n\n const events = [...this.eventQueue]\n this.eventQueue = []\n\n await this.sendEvents(events)\n }\n\n /**\n * Shutdown the client.\n */\n async shutdown(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n stopAutocapture()\n stopCalendarTracking()\n stopSessionTracking()\n this.sessionTracker = null\n await this.flush()\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private initSessionTracking(): void {\n this.sessionTracker = initSessionTracking({\n onEngagement: (event) => {\n this.enqueue(event)\n },\n idleTimeout: this.options.idleTimeout,\n })\n }\n\n private initPageviewTracking(): void {\n initPageviewTracking((url, referrer, title) => {\n // Notify session tracker FIRST (emits engagement for OLD page using stored state)\n // This must happen before enqueueing the new pageview\n this.sessionTracker?.onNavigation(url)\n\n // Then enqueue pageview for NEW page\n const event = buildPageviewEvent({ url, referrer, title })\n this.enqueue(event)\n })\n }\n\n private initFormTracking(denylist?: string[]): void {\n // Create identity callback if autoIdentify is enabled (default: true)\n const identityCallback =\n this.options.autoIdentify !== false\n ? (identity: { email: string; name?: string; firstName?: string; lastName?: string }) => {\n // Build traits from extracted name fields\n const traits: Record<string, string> = {}\n if (identity.name) traits.name = identity.name\n if (identity.firstName) traits.firstName = identity.firstName\n if (identity.lastName) traits.lastName = identity.lastName\n\n this.identify({\n email: identity.email,\n traits: Object.keys(traits).length > 0 ? traits : undefined,\n })\n }\n : undefined\n\n initFormTracking(\n (url, formId, fields) => {\n const event = buildFormEvent({\n url,\n referrer: document.referrer,\n formId,\n formFields: fields,\n })\n this.enqueue(event)\n },\n denylist,\n identityCallback,\n )\n }\n\n private initCalendarTracking(): void {\n initCalendarTracking({\n onCalendarBooked: (bookingEvent: CalendarBookingEvent) => {\n // Track the calendar booking as a first-class calendar event\n // Note: Email is NOT available from Cal.com/Calendly client-side events\n // Use server-side webhooks for identify()\n const event = buildCalendarEvent({\n url: window.location.href,\n referrer: document.referrer,\n provider: bookingEvent.provider,\n eventType: bookingEvent.eventType,\n startTime: bookingEvent.startTime,\n endTime: bookingEvent.endTime,\n duration: bookingEvent.duration,\n isRecurring: bookingEvent.isRecurring,\n inviteeEmail: bookingEvent.inviteeEmail,\n inviteeName: bookingEvent.inviteeName,\n })\n this.enqueue(event)\n },\n })\n }\n\n private enqueue(event: TrackerEvent): void {\n this.eventQueue.push(event)\n\n // Flush immediately if queue is getting large\n if (this.eventQueue.length >= 10) {\n this.flush()\n }\n }\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush()\n }, this.flushInterval)\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (!this.visitorId) return // Can't send without a visitor ID\n\n // Include current user identity in payload for direct resolution\n // This allows the server to resolve identity immediately for SPA apps\n // instead of waiting for the anonymous visitor flow\n const userIdentity = this.currentUser ?? undefined\n const payload = buildIngestPayload(this.visitorId, \"client\", events, userIdentity)\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n try {\n // Use sendBeacon for better reliability on page unload\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n const blob = new Blob([JSON.stringify(payload)], { type: \"application/json\" })\n const sent = navigator.sendBeacon(url, blob)\n if (sent) return\n }\n\n // Fallback to fetch\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n keepalive: true,\n })\n } catch (error) {\n // Silently fail - we don't want to break the user's site\n console.warn(\"[Outlit] Failed to send events:\", error)\n }\n }\n}\n\n// ============================================\n// SINGLETON INSTANCE\n// ============================================\n\nlet instance: Outlit | null = null\n\n/**\n * Initialize the Outlit client.\n * Should be called once at app startup.\n */\nexport function init(options: OutlitOptions): Outlit {\n if (instance) {\n console.warn(\"[Outlit] Already initialized\")\n return instance\n }\n\n instance = new Outlit(options)\n return instance\n}\n\n/**\n * Get the Outlit instance.\n * Throws if not initialized.\n */\nexport function getInstance(): Outlit {\n if (!instance) {\n throw new Error(\"[Outlit] Not initialized. Call init() first.\")\n }\n return instance\n}\n\n/**\n * Track a custom event.\n * Convenience method that uses the singleton instance.\n */\nexport function track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]): void {\n getInstance().track(eventName, properties)\n}\n\n/**\n * Identify the current visitor.\n * Convenience method that uses the singleton instance.\n */\nexport function identify(options: BrowserIdentifyOptions): void {\n getInstance().identify(options)\n}\n\n/**\n * Enable tracking after consent is obtained.\n * Call this in your consent management tool's callback.\n * Convenience method that uses the singleton instance.\n */\nexport function enableTracking(): void {\n getInstance().enableTracking()\n}\n\n/**\n * Check if tracking is currently enabled.\n * Convenience method that uses the singleton instance.\n */\nexport function isTrackingEnabled(): boolean {\n return getInstance().isEnabled()\n}\n\n/**\n * Set the current user identity.\n * Convenience method that uses the singleton instance.\n */\nexport function setUser(identity: UserIdentity): void {\n getInstance().setUser(identity)\n}\n\n/**\n * Clear the current user identity (on logout).\n * Convenience method that uses the singleton instance.\n */\nexport function clearUser(): void {\n getInstance().clearUser()\n}\n\n/**\n * Mark the current user as activated.\n * Convenience method that uses the singleton instance.\n */\nexport function activate(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().activate(properties)\n}\n\n/**\n * Mark the current user as engaged.\n * Convenience method that uses the singleton instance.\n */\nexport function engaged(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().engaged(properties)\n}\n\n/**\n * Mark the current user as paid.\n * Convenience method that uses the singleton instance.\n */\nexport function paid(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().paid(properties)\n}\n\n/**\n * Mark the current user as churned.\n * Convenience method that uses the singleton instance.\n */\nexport function churned(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().churned(properties)\n}\n"],"mappings":"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","names":["script_exports","__export","outlit","DEFAULT_API_HOST","DEFAULT_DENIED_FORM_FIELDS","extractUtmParams","url","params","utm","extractPathFromUrl","isFieldDenied","fieldName","denylist","normalizedName","denied","normalizedDenied","looksLikeSensitiveValue","value","cleaned","sanitizeFormFields","fields","customDenylist","sanitized","key","isValidEmail","value","EMAIL_FIELD_PATTERNS","FULL_NAME_PATTERNS","FIRST_NAME_PATTERNS","LAST_NAME_PATTERNS","matchesPatterns","fieldName","patterns","normalized","pattern","findEmailField","fields","inputTypes","inputType","findNameFields","fullName","firstName","lastName","trimmedValue","result","extractIdentityFromForm","email","nameFields","buildPageviewEvent","params","url","referrer","timestamp","title","extractPathFromUrl","extractUtmParams","buildFormEvent","formId","formFields","buildIdentifyEvent","userId","traits","buildCustomEvent","eventName","properties","buildCalendarEvent","provider","eventType","startTime","endTime","duration","isRecurring","inviteeEmail","inviteeName","buildEngagementEvent","activeTimeMs","totalTimeMs","sessionId","buildStageEvent","stage","buildIngestPayload","visitorId","source","events","userIdentity","payload","pageviewCallback","lastUrl","initPageviewTracking","callback","capturePageview","setupSpaListeners","url","referrer","title","capturePageviewDelayed","originalPushState","originalReplaceState","args","formCallback","formDenylist","identityCallback","initFormTracking","denylist","onIdentity","handleFormSubmit","event","form","formId","formData","fields","inputTypes","inputs","input","name","value","key","sanitizedFields","sanitizeFormFields","identity","extractIdentityFromForm","stopAutocapture","callbacks","isListening","calSetupAttempts","calCallbackRegistered","lastBookingUid","CAL_MAX_RETRY_ATTEMPTS","CAL_INITIAL_DELAY_MS","CAL_MAX_DELAY_MS","parseCalComBooking","data","event","nameMatch","start","end","setupCalComListener","Cal","handleCalComBooking","delay","e","bookingEvent","handlePostMessage","isCalendlyEvent","parseCalendlyBooking","isCalComRawMessage","bookingData","extractCalComBookingFromMessage","messageType","messageData","_payload","initCalendarTracking","cbs","stopCalendarTracking","DEFAULT_IDLE_TIMEOUT","SESSION_TIMEOUT","TIME_UPDATE_INTERVAL","MIN_SPURIOUS_THRESHOLD","MIN_PAGE_TIME_FOR_ENGAGEMENT","SESSION_ID_KEY","SESSION_LAST_ACTIVITY_KEY","SessionTracker","options","totalTimeMs","isSpuriousEvent","isTooSoonAfterNavigation","event","buildEngagementEvent","newUrl","now","existingSessionId","lastActivityStr","lastActivity","newSessionId","c","r","activityEvents","wasVisible","isNowVisible","url","sessionTrackerInstance","initSessionTracking","stopSessionTracking","VISITOR_ID_KEY","generateVisitorId","c","r","getOrCreateVisitorId","stored","isValidUuid","cookieValue","getCookie","visitorId","persistVisitorId","setCookie","value","name","parts","getRootDomain","hostname","twoPartTlds","lastTwo","days","expires","cookie","rootDomain","Outlit","options","DEFAULT_API_HOST","handleExit","getOrCreateVisitorId","eventName","properties","event","buildCustomEvent","buildIdentifyEvent","identity","stage","buildStageEvent","events","stopAutocapture","stopCalendarTracking","stopSessionTracking","initSessionTracking","initPageviewTracking","url","referrer","title","buildPageviewEvent","denylist","identityCallback","traits","initFormTracking","formId","fields","buildFormEvent","initCalendarTracking","bookingEvent","buildCalendarEvent","userIdentity","payload","buildIngestPayload","blob","error","existingStub","stubQueue","outlit","options","Outlit","method","args","self","eventName","properties","identity","autoInit","script","publicKey","apiHost","trackPageviews","trackForms","autoTrack","autoIdentify","trackCalendarEmbeds"]}
|
|
1
|
+
{"version":3,"sources":["../src/script.ts","../../core/src/types.ts","../../core/src/utils.ts","../../core/src/payload.ts","../src/autocapture.ts","../src/embed-integrations.ts","../src/session-tracker.ts","../src/storage.ts","../src/tracker.ts"],"sourcesContent":["/**\n * IIFE entry point for CDN script tag usage.\n *\n * Usage (with stub snippet - recommended):\n * <script>\n * !function(w,d,src,key,auto){\n * if(w.outlit&&w.outlit._loaded)return;\n * w.outlit=w.outlit||{_q:[]};\n * [\"init\",\"track\",\"identify\",\"enableTracking\",\"isTrackingEnabled\",\"getVisitorId\",\"setUser\",\"clearUser\",\"activate\",\"engaged\",\"paid\",\"churned\"].forEach(function(m){\n * w.outlit[m]=w.outlit[m]||function(){w.outlit._q.push([m,[].slice.call(arguments)])};\n * });\n * var s=d.createElement(\"script\");s.async=1;s.src=src;\n * s.dataset.publicKey=key;if(auto!==undefined)s.dataset.autoTrack=auto;\n * (d.body||d.head).appendChild(s);\n * }(window,document,\"https://cdn.outlit.ai/outlit.js\",\"pk_xxx\");\n * </script>\n *\n * Usage (simple script tag):\n * <script src=\"https://cdn.outlit.ai/outlit.js\" data-public-key=\"pk_xxx\" async></script>\n *\n * Usage (with consent management):\n * Pass `false` as last param to stub, or use data-auto-track=\"false\" on script tag\n */\n\nimport type { BrowserIdentifyOptions, BrowserTrackOptions } from \"@outlit/core\"\nimport { Outlit, type OutlitOptions, type UserIdentity } from \"./tracker\"\n\n// ============================================\n// TYPES\n// ============================================\n\n// Stub queue format: [methodName, arguments]\ntype StubQueueItem = [string, unknown[]]\n\ninterface OutlitStub {\n _q?: StubQueueItem[]\n [key: string]: unknown\n}\n\ninterface OutlitGlobal {\n _initialized: boolean\n _instance: Outlit | null\n _queue: Array<() => void>\n init: (options: OutlitOptions) => void\n track: (eventName: string, properties?: BrowserTrackOptions[\"properties\"]) => void\n identify: (options: BrowserIdentifyOptions) => void\n getVisitorId: () => string | null\n enableTracking: () => void\n isTrackingEnabled: () => boolean\n setUser: (identity: UserIdentity) => void\n clearUser: () => void\n activate: (properties?: Record<string, string | number | boolean | null>) => void\n engaged: (properties?: Record<string, string | number | boolean | null>) => void\n paid: (properties?: Record<string, string | number | boolean | null>) => void\n churned: (properties?: Record<string, string | number | boolean | null>) => void\n}\n\n// ============================================\n// GLOBAL API\n// ============================================\n\n// Check for existing stub with queued calls\nconst existingStub =\n typeof window !== \"undefined\" ? (window as { outlit?: OutlitStub }).outlit : undefined\nconst stubQueue: StubQueueItem[] = existingStub?._q || []\n\n// Create global object with queuing support\nconst outlit: OutlitGlobal & { _loaded?: boolean } = {\n _initialized: false,\n _instance: null,\n _queue: [],\n _loaded: true, // Marks that the real SDK has loaded (for double-load protection)\n\n init(options: OutlitOptions) {\n if (this._initialized) {\n console.warn(\"[Outlit] Already initialized\")\n return\n }\n\n this._instance = new Outlit(options)\n this._initialized = true\n\n // Process calls queued by the stub snippet (before SDK loaded)\n for (const [method, args] of stubQueue) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const self = this as any\n if (method in self && typeof self[method] === \"function\") {\n self[method](...args)\n }\n }\n\n // Process calls queued after SDK loaded but before init\n while (this._queue.length > 0) {\n const fn = this._queue.shift()\n fn?.()\n }\n },\n\n track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]) {\n if (!this._initialized || !this._instance) {\n // Queue the call for after initialization\n this._queue.push(() => this.track(eventName, properties))\n return\n }\n this._instance.track(eventName, properties)\n },\n\n identify(options: BrowserIdentifyOptions) {\n if (!this._initialized || !this._instance) {\n // Queue the call for after initialization\n this._queue.push(() => this.identify(options))\n return\n }\n this._instance.identify(options)\n },\n\n getVisitorId() {\n if (!this._instance) return null\n return this._instance.getVisitorId()\n },\n\n /**\n * Enable tracking after user consent.\n * Call this in your consent management tool's callback.\n */\n enableTracking() {\n if (!this._initialized || !this._instance) {\n // Queue the call for after initialization\n this._queue.push(() => this.enableTracking())\n return\n }\n this._instance.enableTracking()\n },\n\n /**\n * Check if tracking is currently enabled.\n */\n isTrackingEnabled() {\n if (!this._instance) return false\n return this._instance.isEnabled()\n },\n\n /**\n * Set the user identity for attribution.\n */\n setUser(identity: UserIdentity) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.setUser(identity))\n return\n }\n this._instance.setUser(identity)\n },\n\n /**\n * Clear the current user identity (logout).\n */\n clearUser() {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.clearUser())\n return\n }\n this._instance.clearUser()\n },\n\n /**\n * Mark the current user as activated.\n */\n activate(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.activate(properties))\n return\n }\n this._instance.activate(properties)\n },\n\n /**\n * Mark the current user as engaged.\n */\n engaged(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.engaged(properties))\n return\n }\n this._instance.engaged(properties)\n },\n\n /**\n * Mark the current user as paid.\n */\n paid(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.paid(properties))\n return\n }\n this._instance.paid(properties)\n },\n\n /**\n * Mark the current user as churned.\n */\n churned(properties?: Record<string, string | number | boolean | null>) {\n if (!this._initialized || !this._instance) {\n this._queue.push(() => this.churned(properties))\n return\n }\n this._instance.churned(properties)\n },\n}\n\n// ============================================\n// AUTO-INITIALIZATION\n// ============================================\n\n/**\n * Auto-initialize from script tag attributes.\n */\nfunction autoInit(): void {\n // Find the script tag - currentScript is only available during synchronous execution\n // When called from DOMContentLoaded, we need to fall back to querySelector\n let script = document.currentScript as HTMLScriptElement | null\n\n if (!script) {\n // Fallback: find script tag with data-public-key attribute\n script = document.querySelector(\"script[data-public-key]\") as HTMLScriptElement | null\n }\n\n if (!script) {\n console.warn(\"[Outlit] No script tag found with data-public-key attribute\")\n return\n }\n\n const publicKey = script.getAttribute(\"data-public-key\")\n if (!publicKey) {\n console.warn(\"[Outlit] Missing data-public-key attribute on script tag\")\n return\n }\n\n // Get optional attributes\n const apiHost = script.getAttribute(\"data-api-host\") ?? undefined\n const trackPageviews = script.getAttribute(\"data-track-pageviews\") !== \"false\"\n const trackForms = script.getAttribute(\"data-track-forms\") !== \"false\"\n const autoTrack = script.getAttribute(\"data-auto-track\") !== \"false\"\n const autoIdentify = script.getAttribute(\"data-auto-identify\") !== \"false\"\n const trackCalendarEmbeds = script.getAttribute(\"data-track-calendar-embeds\") !== \"false\"\n\n // Initialize\n outlit.init({\n publicKey,\n apiHost,\n trackPageviews,\n trackForms,\n autoTrack,\n autoIdentify,\n trackCalendarEmbeds,\n })\n}\n\n// ============================================\n// EXPOSE GLOBAL & AUTO-INIT\n// ============================================\n\n// Expose on window\nif (typeof window !== \"undefined\") {\n // @ts-expect-error - Adding to window\n window.outlit = outlit\n\n // Auto-initialize when DOM is ready\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", autoInit)\n } else {\n // DOM is already ready\n autoInit()\n }\n}\n\n// Also export for module usage if needed\nexport { outlit }\n","// ============================================\n// EVENT TYPES\n// ============================================\n\nexport type EventType =\n | \"pageview\"\n | \"form\"\n | \"identify\"\n | \"custom\"\n | \"calendar\"\n | \"engagement\"\n | \"stage\"\n\n// Only explicit stages - discovered/signed_up are inferred from identify calls\nexport type ExplicitJourneyStage = \"activated\" | \"engaged\" | \"paid\" | \"churned\"\n\nexport type CalendarProvider = \"cal.com\" | \"calendly\" | \"unknown\"\n\nexport type SourceType = \"client\" | \"server\" | \"integration\"\n\n// ============================================\n// UTM PARAMETERS\n// ============================================\n\nexport interface UtmParams {\n source?: string\n medium?: string\n campaign?: string\n term?: string\n content?: string\n}\n\n// ============================================\n// TRACKER CONFIGURATION\n// ============================================\n\nexport interface TrackerConfig {\n publicKey: string\n apiHost?: string // default: 'https://app.outlit.ai'\n}\n\n// ============================================\n// BROWSER-SPECIFIC TYPES (anonymous allowed)\n// visitorId is auto-managed by the browser SDK\n// ============================================\n\nexport interface BrowserTrackOptions {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface BrowserIdentifyOptions {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// SERVER-SPECIFIC TYPES (identity required)\n// No anonymous tracking - must identify the user\n// ============================================\n\nexport interface ServerTrackOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n timestamp?: number\n}\n\nexport interface ServerIdentifyOptions {\n email?: string // At least one of email/userId required\n userId?: string // At least one of email/userId required\n traits?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// INTERNAL EVENT TYPES\n// These are the full event objects sent to the API\n// ============================================\n\ninterface BaseEvent {\n type: EventType\n timestamp: number // Unix timestamp in milliseconds\n url: string\n path: string\n referrer?: string\n utm?: UtmParams\n}\n\nexport interface PageviewEvent extends BaseEvent {\n type: \"pageview\"\n title?: string\n}\n\nexport interface FormEvent extends BaseEvent {\n type: \"form\"\n formId?: string\n formFields?: Record<string, string>\n}\n\nexport interface IdentifyEvent extends BaseEvent {\n type: \"identify\"\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport interface CustomEvent extends BaseEvent {\n type: \"custom\"\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport interface CalendarEvent extends BaseEvent {\n type: \"calendar\"\n provider: CalendarProvider\n eventType?: string // e.g., \"30 Minute Meeting\"\n startTime?: string // ISO timestamp\n endTime?: string // ISO timestamp\n duration?: number // Duration in minutes\n isRecurring?: boolean\n /** Available when identity is passed via webhooks or manual integration */\n inviteeEmail?: string\n inviteeName?: string\n}\n\nexport interface EngagementEvent extends BaseEvent {\n type: \"engagement\"\n /** Time in milliseconds the user was actively engaged (visible tab + user interactions) */\n activeTimeMs: number\n /** Total wall-clock time in milliseconds on the page */\n totalTimeMs: number\n /** Session ID for grouping engagement events. Resets after 30 min of inactivity or tab close. */\n sessionId: string\n}\n\nexport interface StageEvent extends BaseEvent {\n type: \"stage\"\n /** The journey stage to set (only explicit stages, discovered/signed_up are inferred) */\n stage: ExplicitJourneyStage\n /** Optional properties for context */\n properties?: Record<string, string | number | boolean | null>\n}\n\nexport type TrackerEvent =\n | PageviewEvent\n | FormEvent\n | IdentifyEvent\n | CustomEvent\n | CalendarEvent\n | EngagementEvent\n | StageEvent\n\n// ============================================\n// INGEST PAYLOAD\n// This is what gets sent to the API\n// ============================================\n\n/**\n * User identity for payload-level resolution.\n * Used by browser SDK when user is logged in (via setUser).\n */\nexport interface PayloadUserIdentity {\n email?: string\n userId?: string\n}\n\nexport interface IngestPayload {\n visitorId?: string // Required for pixel, optional for server\n source: SourceType\n events: TrackerEvent[]\n /**\n * Session ID for grouping all events in this batch.\n * Only present for browser (client) source events.\n * Used to correlate pageviews, forms, custom events, and engagement\n * within the same browsing session.\n */\n sessionId?: string\n /**\n * User identity for this batch of events.\n * When present, the server can resolve directly to CustomerContact\n * instead of relying on anonymous visitor flow.\n *\n * This is set by the browser SDK when setUser() has been called,\n * allowing immediate identity resolution for SPA/React apps.\n */\n userIdentity?: PayloadUserIdentity\n}\n\n// ============================================\n// API RESPONSE\n// ============================================\n\nexport interface IngestResponse {\n success: boolean\n processed: number\n errors?: Array<{\n index: number\n message: string\n }>\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nexport const DEFAULT_API_HOST = \"https://app.outlit.ai\"\n\n// Re-export for convenience\nexport type { PayloadUserIdentity as UserIdentity }\n\nexport const DEFAULT_DENIED_FORM_FIELDS = [\n \"password\",\n \"passwd\",\n \"pass\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"api_key\",\n \"apikey\",\n \"api-key\",\n \"credit_card\",\n \"creditcard\",\n \"credit-card\",\n \"cc_number\",\n \"ccnumber\",\n \"card_number\",\n \"cardnumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"social_security\",\n \"socialsecurity\",\n \"bank_account\",\n \"bankaccount\",\n \"routing_number\",\n \"routingnumber\",\n]\n","import { DEFAULT_DENIED_FORM_FIELDS, type UtmParams } from \"./types\"\n\n// ============================================\n// UTM EXTRACTION\n// ============================================\n\n/**\n * Extract UTM parameters from a URL.\n */\nexport function extractUtmParams(url: string): UtmParams | undefined {\n try {\n const urlObj = new URL(url)\n const params = urlObj.searchParams\n\n const utm: UtmParams = {}\n\n if (params.has(\"utm_source\")) utm.source = params.get(\"utm_source\") ?? undefined\n if (params.has(\"utm_medium\")) utm.medium = params.get(\"utm_medium\") ?? undefined\n if (params.has(\"utm_campaign\")) utm.campaign = params.get(\"utm_campaign\") ?? undefined\n if (params.has(\"utm_term\")) utm.term = params.get(\"utm_term\") ?? undefined\n if (params.has(\"utm_content\")) utm.content = params.get(\"utm_content\") ?? undefined\n\n return Object.keys(utm).length > 0 ? utm : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Extract path from a URL.\n */\nexport function extractPathFromUrl(url: string): string {\n try {\n const urlObj = new URL(url)\n return urlObj.pathname\n } catch {\n return \"/\"\n }\n}\n\n// ============================================\n// FORM FIELD SANITIZATION\n// ============================================\n\n/**\n * Check if a field name should be denied (case-insensitive).\n */\nexport function isFieldDenied(fieldName: string, denylist: string[]): boolean {\n const normalizedName = fieldName.toLowerCase().replace(/[-_\\s]/g, \"\")\n return denylist.some((denied) => {\n const normalizedDenied = denied.toLowerCase().replace(/[-_\\s]/g, \"\")\n return normalizedName.includes(normalizedDenied)\n })\n}\n\n/**\n * Check if a value looks like sensitive data (e.g., credit card number).\n */\nfunction looksLikeSensitiveValue(value: string): boolean {\n // Remove spaces and dashes\n const cleaned = value.replace(/[\\s-]/g, \"\")\n\n // Check for credit card patterns (13-19 digits)\n if (/^\\d{13,19}$/.test(cleaned)) {\n return true\n }\n\n // Check for SSN pattern (9 digits)\n if (/^\\d{9}$/.test(cleaned) || /^\\d{3}-\\d{2}-\\d{4}$/.test(value)) {\n return true\n }\n\n return false\n}\n\n/**\n * Sanitize form fields by removing sensitive data.\n * Returns a new object with denied fields removed.\n */\nexport function sanitizeFormFields(\n fields: Record<string, string> | undefined,\n customDenylist?: string[],\n): Record<string, string> | undefined {\n if (!fields) return undefined\n\n const denylist = customDenylist ?? DEFAULT_DENIED_FORM_FIELDS\n const sanitized: Record<string, string> = {}\n\n for (const [key, value] of Object.entries(fields)) {\n if (!isFieldDenied(key, denylist)) {\n // Also check for credit card patterns in values\n if (!looksLikeSensitiveValue(value)) {\n sanitized[key] = value\n }\n }\n }\n\n return Object.keys(sanitized).length > 0 ? sanitized : undefined\n}\n\n// ============================================\n// VISITOR ID DERIVATION (for server SDK)\n// ============================================\n\n/**\n * Derive a deterministic visitor ID from email and/or userId.\n * This is used by the server SDK to create consistent IDs for API compatibility.\n *\n * Uses a simple hash to create a UUID-like string that will be consistent\n * for the same email/userId combination.\n */\nexport function deriveVisitorIdFromIdentity(email?: string, userId?: string): string {\n const identity = [email?.toLowerCase(), userId].filter(Boolean).join(\"|\")\n if (!identity) {\n throw new Error(\"Either email or userId must be provided\")\n }\n\n // Simple hash function to create a deterministic UUID-like string\n let hash = 0\n for (let i = 0; i < identity.length; i++) {\n const char = identity.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Convert to hex and format as UUID-like string\n const hex = Math.abs(hash).toString(16).padStart(8, \"0\")\n const part1 = hex.slice(0, 8)\n const part2 = identity.length.toString(16).padStart(4, \"0\")\n const part3 = \"4000\" // Version 4 UUID marker\n const part4 = (((hash >>> 16) & 0x0fff) | 0x8000).toString(16)\n const part5 = Math.abs(hash * 31)\n .toString(16)\n .padStart(12, \"0\")\n .slice(0, 12)\n\n return `${part1}-${part2}-${part3}-${part4}-${part5}`\n}\n\n// ============================================\n// VALIDATION\n// ============================================\n\n/**\n * Validate that at least one identity field is provided.\n * Used by the server SDK to enforce identity requirements.\n */\nexport function validateServerIdentity(email?: string, userId?: string): void {\n if (!email && !userId) {\n throw new Error(\n \"Server SDK requires either email or userId for all track/identify calls. \" +\n \"Anonymous tracking is only supported in the browser SDK.\",\n )\n }\n}\n\n// ============================================\n// AUTO-IDENTIFY: EMAIL & NAME EXTRACTION\n// ============================================\n\n/**\n * Validate that a string looks like a valid email address.\n */\nexport function isValidEmail(value: string): boolean {\n if (!value || typeof value !== \"string\") return false\n // Basic email regex - intentionally permissive to avoid false negatives\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return emailRegex.test(value.trim())\n}\n\n/**\n * Email field name patterns (case-insensitive, normalized).\n * Order matters - more specific patterns first.\n */\nconst EMAIL_FIELD_PATTERNS = [\n /^e?-?mail$/i,\n /^email[_-]?address$/i,\n /^user[_-]?email$/i,\n /^work[_-]?email$/i,\n /^contact[_-]?email$/i,\n /^primary[_-]?email$/i,\n /^business[_-]?email$/i,\n]\n\n/**\n * Full name field patterns.\n */\nconst FULL_NAME_PATTERNS = [\n /^name$/i,\n /^full[_-]?name$/i,\n /^your[_-]?name$/i,\n /^customer[_-]?name$/i,\n /^contact[_-]?name$/i,\n /^display[_-]?name$/i,\n]\n\n/**\n * First name field patterns.\n */\nconst FIRST_NAME_PATTERNS = [\n /^first[_-]?name$/i,\n /^firstname$/i,\n /^first$/i,\n /^fname$/i,\n /^given[_-]?name$/i,\n /^forename$/i,\n]\n\n/**\n * Last name field patterns.\n */\nconst LAST_NAME_PATTERNS = [\n /^last[_-]?name$/i,\n /^lastname$/i,\n /^last$/i,\n /^lname$/i,\n /^surname$/i,\n /^family[_-]?name$/i,\n]\n\n/**\n * Check if a field name matches any of the given patterns.\n */\nfunction matchesPatterns(fieldName: string, patterns: RegExp[]): boolean {\n const normalized = fieldName.trim()\n return patterns.some((pattern) => pattern.test(normalized))\n}\n\n/**\n * Find an email value from form fields.\n *\n * Priority:\n * 1. Fields with input type=\"email\" (if inputTypes map provided)\n * 2. Field names matching email patterns\n * 3. Any field with a value that looks like an email\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns The email value if found, undefined otherwise\n */\nexport function findEmailField(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): string | undefined {\n // Priority 1: Check fields with type=\"email\"\n if (inputTypes) {\n for (const [fieldName, inputType] of inputTypes.entries()) {\n if (inputType === \"email\") {\n const value = fields[fieldName]\n if (value && isValidEmail(value)) {\n return value.trim()\n }\n }\n }\n }\n\n // Priority 2: Check field names matching email patterns\n for (const [fieldName, value] of Object.entries(fields)) {\n if (matchesPatterns(fieldName, EMAIL_FIELD_PATTERNS) && isValidEmail(value)) {\n return value.trim()\n }\n }\n\n // Priority 3: Any field with email-like value (fallback)\n for (const value of Object.values(fields)) {\n if (isValidEmail(value)) {\n return value.trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Extract name fields from form data.\n *\n * Looks for:\n * - Full name fields (name, full_name, etc.)\n * - First name fields (first_name, fname, etc.)\n * - Last name fields (last_name, lname, etc.)\n *\n * If only first/last names are found, combines them into a full name.\n *\n * @param fields - Form field key-value pairs\n * @returns Object with name, firstName, and/or lastName if found\n */\nexport function findNameFields(fields: Record<string, string>): {\n name?: string\n firstName?: string\n lastName?: string\n} {\n let fullName: string | undefined\n let firstName: string | undefined\n let lastName: string | undefined\n\n for (const [fieldName, value] of Object.entries(fields)) {\n const trimmedValue = value?.trim()\n if (!trimmedValue) continue\n\n // Check for full name\n if (!fullName && matchesPatterns(fieldName, FULL_NAME_PATTERNS)) {\n fullName = trimmedValue\n }\n\n // Check for first name\n if (!firstName && matchesPatterns(fieldName, FIRST_NAME_PATTERNS)) {\n firstName = trimmedValue\n }\n\n // Check for last name\n if (!lastName && matchesPatterns(fieldName, LAST_NAME_PATTERNS)) {\n lastName = trimmedValue\n }\n }\n\n const result: { name?: string; firstName?: string; lastName?: string } = {}\n\n // If we have a full name, use it\n if (fullName) {\n result.name = fullName\n }\n // If we have first and last, combine them\n else if (firstName && lastName) {\n result.name = `${firstName} ${lastName}`\n result.firstName = firstName\n result.lastName = lastName\n }\n // If we only have first name\n else if (firstName) {\n result.firstName = firstName\n }\n // If we only have last name\n else if (lastName) {\n result.lastName = lastName\n }\n\n return result\n}\n\n/**\n * Identity extracted from a form submission.\n */\nexport interface ExtractedIdentity {\n email: string\n name?: string\n firstName?: string\n lastName?: string\n}\n\n/**\n * Extract identity information (email + name) from form fields.\n *\n * Returns undefined if no valid email is found (email is required for identification).\n *\n * @param fields - Form field key-value pairs\n * @param inputTypes - Optional map of field names to input types\n * @returns Extracted identity with email and optional name fields, or undefined\n */\nexport function extractIdentityFromForm(\n fields: Record<string, string>,\n inputTypes?: Map<string, string>,\n): ExtractedIdentity | undefined {\n const email = findEmailField(fields, inputTypes)\n\n // Email is required for identification\n if (!email) {\n return undefined\n }\n\n const nameFields = findNameFields(fields)\n\n return {\n email,\n ...nameFields,\n }\n}\n","import type {\n CalendarEvent,\n CalendarProvider,\n CustomEvent,\n EngagementEvent,\n ExplicitJourneyStage,\n FormEvent,\n IdentifyEvent,\n IngestPayload,\n PageviewEvent,\n PayloadUserIdentity,\n SourceType,\n StageEvent,\n TrackerEvent,\n UtmParams,\n} from \"./types\"\nimport { extractPathFromUrl, extractUtmParams } from \"./utils\"\n\n// ============================================\n// EVENT BUILDERS\n// ============================================\n\ninterface BaseEventParams {\n url: string\n referrer?: string\n timestamp?: number\n}\n\n/**\n * Build a pageview event.\n */\nexport function buildPageviewEvent(params: BaseEventParams & { title?: string }): PageviewEvent {\n const { url, referrer, timestamp, title } = params\n return {\n type: \"pageview\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n title,\n }\n}\n\n/**\n * Build a form event.\n */\nexport function buildFormEvent(\n params: BaseEventParams & {\n formId?: string\n formFields?: Record<string, string>\n },\n): FormEvent {\n const { url, referrer, timestamp, formId, formFields } = params\n return {\n type: \"form\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n formId,\n formFields,\n }\n}\n\n/**\n * Build an identify event.\n */\nexport function buildIdentifyEvent(\n params: BaseEventParams & {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n },\n): IdentifyEvent {\n const { url, referrer, timestamp, email, userId, traits } = params\n return {\n type: \"identify\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n email,\n userId,\n traits,\n }\n}\n\n/**\n * Build a custom event.\n */\nexport function buildCustomEvent(\n params: BaseEventParams & {\n eventName: string\n properties?: Record<string, string | number | boolean | null>\n },\n): CustomEvent {\n const { url, referrer, timestamp, eventName, properties } = params\n return {\n type: \"custom\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n eventName,\n properties,\n }\n}\n\n/**\n * Build a calendar booking event.\n */\nexport function buildCalendarEvent(\n params: BaseEventParams & {\n provider: CalendarProvider\n eventType?: string\n startTime?: string\n endTime?: string\n duration?: number\n isRecurring?: boolean\n inviteeEmail?: string\n inviteeName?: string\n },\n): CalendarEvent {\n const {\n url,\n referrer,\n timestamp,\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n } = params\n return {\n type: \"calendar\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n provider,\n eventType,\n startTime,\n endTime,\n duration,\n isRecurring,\n inviteeEmail,\n inviteeName,\n }\n}\n\n/**\n * Build an engagement event.\n * Captures active time on page for session analytics.\n */\nexport function buildEngagementEvent(\n params: BaseEventParams & {\n activeTimeMs: number\n totalTimeMs: number\n sessionId: string\n },\n): EngagementEvent {\n const { url, referrer, timestamp, activeTimeMs, totalTimeMs, sessionId } = params\n return {\n type: \"engagement\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n activeTimeMs,\n totalTimeMs,\n sessionId,\n }\n}\n\n/**\n * Build a stage event.\n * Used to explicitly set customer journey stage (activated, engaged, paid, churned).\n * discovered/signed_up stages are inferred from identify calls.\n */\nexport function buildStageEvent(\n params: BaseEventParams & {\n stage: ExplicitJourneyStage\n properties?: Record<string, string | number | boolean | null>\n },\n): StageEvent {\n const { url, referrer, timestamp, stage, properties } = params\n return {\n type: \"stage\",\n timestamp: timestamp ?? Date.now(),\n url,\n path: extractPathFromUrl(url),\n referrer,\n utm: extractUtmParams(url),\n stage,\n properties,\n }\n}\n\n// ============================================\n// PAYLOAD BUILDER\n// ============================================\n\n/**\n * Build an ingest payload from events.\n *\n * @param visitorId - The anonymous visitor ID from browser cookie/storage\n * @param source - The event source (client, server, integration)\n * @param events - Array of events to send\n * @param userIdentity - Optional user identity for immediate resolution (from setUser in SPA)\n * @param sessionId - Optional session ID for grouping events (browser SDK only)\n */\nexport function buildIngestPayload(\n visitorId: string,\n source: SourceType,\n events: TrackerEvent[],\n userIdentity?: PayloadUserIdentity,\n sessionId?: string,\n): IngestPayload {\n const payload: IngestPayload = {\n visitorId,\n source,\n events,\n }\n\n // Only include sessionId if provided (browser SDK only)\n if (sessionId) {\n payload.sessionId = sessionId\n }\n\n // Only include userIdentity if it has actual values\n if (userIdentity && (userIdentity.email || userIdentity.userId)) {\n payload.userIdentity = {\n ...(userIdentity.email && { email: userIdentity.email }),\n ...(userIdentity.userId && { userId: userIdentity.userId }),\n }\n }\n\n return payload\n}\n\n// ============================================\n// BATCH HELPERS\n// ============================================\n\n/**\n * Maximum number of events in a single batch.\n */\nexport const MAX_BATCH_SIZE = 100\n\n/**\n * Split events into batches of MAX_BATCH_SIZE.\n */\nexport function batchEvents(events: TrackerEvent[]): TrackerEvent[][] {\n const batches: TrackerEvent[][] = []\n for (let i = 0; i < events.length; i += MAX_BATCH_SIZE) {\n batches.push(events.slice(i, i + MAX_BATCH_SIZE))\n }\n return batches\n}\n","import { type ExtractedIdentity, extractIdentityFromForm, sanitizeFormFields } from \"@outlit/core\"\n\n// ============================================\n// PAGEVIEW TRACKING\n// ============================================\n\ntype PageviewCallback = (url: string, referrer: string, title: string) => void\n\nlet pageviewCallback: PageviewCallback | null = null\nlet lastUrl: string | null = null\n\n/**\n * Initialize automatic pageview tracking.\n * Captures initial pageview and listens for SPA navigation.\n */\nexport function initPageviewTracking(callback: PageviewCallback): void {\n pageviewCallback = callback\n\n // Capture initial pageview\n capturePageview()\n\n // Listen for SPA navigation\n setupSpaListeners()\n}\n\n/**\n * Capture a pageview event.\n */\nfunction capturePageview(): void {\n if (!pageviewCallback) return\n\n const url = window.location.href\n const referrer = document.referrer\n const title = document.title\n\n // Avoid duplicate pageviews for the same URL\n if (url === lastUrl) return\n lastUrl = url\n\n pageviewCallback(url, referrer, title)\n}\n\n/**\n * Capture pageview after a small delay.\n * SPAs update document.title asynchronously after navigation,\n * so we need to wait for the title to be updated.\n *\n * Different frameworks update titles at different times:\n * - React: after commit phase (typically after rAF)\n * - Next.js: after route change completes\n * - Framer: after page transition completes\n *\n * A small delay (10ms) reliably captures the updated title\n * while being imperceptible to users.\n */\nfunction capturePageviewDelayed(): void {\n setTimeout(capturePageview, 10)\n}\n\n/**\n * Set up listeners for SPA navigation.\n */\nfunction setupSpaListeners(): void {\n // Listen for popstate (browser back/forward)\n window.addEventListener(\"popstate\", () => {\n capturePageviewDelayed()\n })\n\n // Monkey-patch pushState and replaceState\n const originalPushState = history.pushState\n const originalReplaceState = history.replaceState\n\n history.pushState = function (...args) {\n originalPushState.apply(this, args)\n capturePageviewDelayed()\n }\n\n history.replaceState = function (...args) {\n originalReplaceState.apply(this, args)\n capturePageviewDelayed()\n }\n}\n\n// ============================================\n// FORM TRACKING\n// ============================================\n\ntype FormCallback = (\n url: string,\n formId: string | undefined,\n fields: Record<string, string>,\n) => void\n\ntype IdentityCallback = (identity: ExtractedIdentity) => void\n\nlet formCallback: FormCallback | null = null\nlet formDenylist: string[] | undefined\nlet identityCallback: IdentityCallback | null = null\n\n/**\n * Initialize automatic form tracking.\n * Captures form submissions with field sanitization.\n *\n * @param callback - Called when a form is submitted with sanitized fields\n * @param denylist - Optional list of field names to exclude\n * @param onIdentity - Optional callback for auto-identification when email is found\n */\nexport function initFormTracking(\n callback: FormCallback,\n denylist?: string[],\n onIdentity?: IdentityCallback,\n): void {\n formCallback = callback\n formDenylist = denylist\n identityCallback = onIdentity ?? null\n\n // Listen for form submissions\n document.addEventListener(\"submit\", handleFormSubmit, true)\n}\n\n/**\n * Handle form submission events.\n */\nfunction handleFormSubmit(event: Event): void {\n if (!formCallback) return\n\n const form = event.target as HTMLFormElement\n if (!(form instanceof HTMLFormElement)) return\n\n const url = window.location.href\n const formId = form.id || form.name || undefined\n\n // Extract form fields and input types\n const formData = new FormData(form)\n const fields: Record<string, string> = {}\n const inputTypes = new Map<string, string>()\n\n // Get input types for better email detection\n const inputs = form.querySelectorAll(\"input, select, textarea\")\n for (const input of inputs) {\n const name = input.getAttribute(\"name\")\n if (name && input instanceof HTMLInputElement) {\n inputTypes.set(name, input.type)\n }\n }\n\n formData.forEach((value, key) => {\n // Only capture string values, skip files\n if (typeof value === \"string\") {\n fields[key] = value\n }\n })\n\n // Sanitize fields to remove sensitive data\n const sanitizedFields = sanitizeFormFields(fields, formDenylist)\n\n // Auto-identify if callback is set and we find identity fields\n // Use unsanitized fields for identity extraction (email might be in there)\n if (identityCallback) {\n const identity = extractIdentityFromForm(fields, inputTypes)\n if (identity) {\n identityCallback(identity)\n }\n }\n\n // Emit form event (with sanitized fields)\n if (sanitizedFields && Object.keys(sanitizedFields).length > 0) {\n formCallback(url, formId, sanitizedFields)\n }\n}\n\n// ============================================\n// CLEANUP\n// ============================================\n\n/**\n * Stop all autocapture tracking.\n */\nexport function stopAutocapture(): void {\n pageviewCallback = null\n formCallback = null\n identityCallback = null\n document.removeEventListener(\"submit\", handleFormSubmit, true)\n}\n","/**\n * Third-party Embed Integrations\n *\n * This module handles automatic tracking of booking events from\n * third-party calendar embeds like Cal.com and Calendly.\n *\n * Cal.com Integration:\n * - Hooks into Cal() API for booking events via Cal(\"on\", { action: \"bookingSuccessfulV2\" })\n * - Captures booking details: event type, time, duration, invitee name (from title)\n *\n * IMPORTANT - Email Limitation:\n * Cal.com does NOT expose invitee email in their client-side events for privacy.\n * The email IS in the success page URL, but:\n * 1. iframe.src attribute doesn't update (Cal.com uses client-side routing)\n * 2. contentWindow.location.href is blocked by cross-origin policy\n *\n * To get email from Cal.com bookings, you need SERVER-SIDE WEBHOOKS:\n * 1. Set up Cal.com webhook: Settings → Developer → Webhooks\n * 2. Point it to your server endpoint\n * 3. Server calls Outlit identify() API with the email from webhook payload\n */\n\nimport type { CalendarProvider } from \"@outlit/core\"\n\n// ============================================\n// TYPES\n// ============================================\n\nexport interface CalendarBookingEvent {\n provider: CalendarProvider\n eventType?: string\n startTime?: string\n endTime?: string\n duration?: number\n isRecurring?: boolean\n inviteeEmail?: string\n inviteeName?: string\n}\n\nexport interface CalendarIntegrationCallbacks {\n onCalendarBooked: (event: CalendarBookingEvent) => void\n onIdentity?: (identity: { email: string; name?: string }) => void\n}\n\n// ============================================\n// CAL.COM TYPES\n// ============================================\n\ninterface CalComBookingData {\n uid?: string\n title?: string\n startTime?: string\n endTime?: string\n eventTypeId?: number\n status?: string\n paymentRequired?: boolean\n isRecurring?: boolean\n}\n\ninterface CalComEventDetail {\n data: CalComBookingData\n type: string\n namespace: string\n}\n\n// Cal.com's Cal() function type\ntype CalFunction = {\n (\n method: \"on\",\n options: { action: string; callback: (e: { detail: CalComEventDetail }) => void },\n ): void\n (method: string, ...args: unknown[]): void\n loaded?: boolean\n q?: unknown[][]\n ns?: Record<string, unknown>\n}\n\n// ============================================\n// STATE\n// ============================================\n\nlet callbacks: CalendarIntegrationCallbacks | null = null\nlet isListening = false\nlet calSetupAttempts = 0\nlet calCallbackRegistered = false\nlet lastBookingUid: string | null = null // Prevent duplicate events\n\n// ============================================\n// CONFIG\n// ============================================\n\nconst CAL_MAX_RETRY_ATTEMPTS = 10 // Max attempts to find Cal() API\nconst CAL_INITIAL_DELAY_MS = 200 // Start with short delay\nconst CAL_MAX_DELAY_MS = 2000 // Cap retry delay at 2s\n\n// ============================================\n// CAL.COM BOOKING PARSER\n// ============================================\n\nfunction parseCalComBooking(data: CalComBookingData): CalendarBookingEvent {\n const event: CalendarBookingEvent = {\n provider: \"cal.com\",\n }\n\n if (data.title) {\n event.eventType = data.title\n // Extract invitee name from title: \"Meeting between Host and Guest\"\n const nameMatch = data.title.match(/between .+ and (.+)$/i)\n if (nameMatch?.[1]) {\n event.inviteeName = nameMatch[1].trim()\n }\n }\n\n if (data.startTime) event.startTime = data.startTime\n if (data.endTime) event.endTime = data.endTime\n\n if (data.startTime && data.endTime) {\n const start = new Date(data.startTime)\n const end = new Date(data.endTime)\n event.duration = Math.round((end.getTime() - start.getTime()) / 60000)\n }\n\n if (data.isRecurring !== undefined) {\n event.isRecurring = data.isRecurring\n }\n\n // Note: Email is NOT available from Cal.com client-side events\n // Use server-side webhooks to get email for identify()\n\n return event\n}\n\n// ============================================\n// CAL.COM API INTEGRATION\n// ============================================\n\n/**\n * Set up listener for Cal.com's Cal() API.\n * Registers callback via Cal(\"on\", ...) with retries.\n */\nfunction setupCalComListener(): void {\n if (typeof window === \"undefined\") return\n if (calCallbackRegistered) return\n\n calSetupAttempts++\n\n // Check if Cal() API exists\n if (\"Cal\" in window) {\n const Cal = (window as unknown as { Cal: CalFunction }).Cal\n\n if (typeof Cal === \"function\") {\n try {\n Cal(\"on\", {\n action: \"bookingSuccessfulV2\",\n callback: handleCalComBooking,\n })\n calCallbackRegistered = true\n return\n } catch (_e) {\n // Registration failed, will retry\n }\n }\n }\n\n // Cal() not ready yet, retry with backoff\n if (calSetupAttempts < CAL_MAX_RETRY_ATTEMPTS) {\n const delay = Math.min(CAL_INITIAL_DELAY_MS * calSetupAttempts, CAL_MAX_DELAY_MS)\n setTimeout(setupCalComListener, delay)\n }\n}\n\nfunction handleCalComBooking(e: { detail: CalComEventDetail }): void {\n if (!callbacks) return\n\n const data = e.detail?.data\n if (!data) return\n\n // Prevent duplicate events for the same booking\n if (data.uid && data.uid === lastBookingUid) return\n lastBookingUid = data.uid || null\n\n const bookingEvent = parseCalComBooking(data)\n callbacks.onCalendarBooked(bookingEvent)\n}\n\n// ============================================\n// POSTMESSAGE HANDLER (FALLBACK)\n// ============================================\n\nfunction handlePostMessage(event: MessageEvent): void {\n if (!callbacks) return\n\n // Check for Calendly events\n if (isCalendlyEvent(event)) {\n if (event.data.event === \"calendly.event_scheduled\") {\n const bookingEvent = parseCalendlyBooking(event.data.payload)\n callbacks.onCalendarBooked(bookingEvent)\n }\n return\n }\n\n // Check for Cal.com postMessages (fallback if Cal() API not available)\n if (isCalComRawMessage(event)) {\n const bookingData = extractCalComBookingFromMessage(event.data)\n if (bookingData) {\n // Prevent duplicates\n if (bookingData.uid && bookingData.uid === lastBookingUid) return\n lastBookingUid = bookingData.uid || null\n\n const bookingEvent = parseCalComBooking(bookingData)\n callbacks.onCalendarBooked(bookingEvent)\n }\n }\n}\n\nfunction isCalComRawMessage(event: MessageEvent): boolean {\n if (!event.origin.includes(\"cal.com\")) return false\n\n const data = event.data\n if (!data || typeof data !== \"object\") return false\n\n // Cal.com sends type: 'bookingSuccessfulV2' for successful bookings\n const messageType = data.type || data.action\n return (\n messageType === \"bookingSuccessfulV2\" ||\n messageType === \"bookingSuccessful\" ||\n messageType === \"booking_successful\"\n )\n}\n\nfunction extractCalComBookingFromMessage(data: unknown): CalComBookingData | null {\n if (!data || typeof data !== \"object\") return null\n\n const messageData = data as Record<string, unknown>\n\n // Cal.com sends: { originator: 'CAL', type: 'bookingSuccessfulV2', data: { uid, title, ... } }\n if (messageData.data && typeof messageData.data === \"object\") {\n return messageData.data as CalComBookingData\n }\n\n if (messageData.booking && typeof messageData.booking === \"object\") {\n return messageData.booking as CalComBookingData\n }\n\n return null\n}\n\n// ============================================\n// CALENDLY INTEGRATION\n// ============================================\n\ninterface CalendlyPayload {\n event?: { uri?: string }\n invitee?: { uri?: string }\n}\n\ninterface CalendlyMessageData {\n event: string\n payload: CalendlyPayload\n}\n\nfunction isCalendlyEvent(e: MessageEvent): e is MessageEvent<CalendlyMessageData> {\n return (\n e.origin === \"https://calendly.com\" &&\n e.data &&\n typeof e.data.event === \"string\" &&\n e.data.event.startsWith(\"calendly.\")\n )\n}\n\nfunction parseCalendlyBooking(_payload: CalendlyPayload): CalendarBookingEvent {\n return {\n provider: \"calendly\",\n }\n}\n\n// ============================================\n// PUBLIC API\n// ============================================\n\n/**\n * Initialize calendar embed tracking.\n */\nexport function initCalendarTracking(cbs: CalendarIntegrationCallbacks): void {\n if (isListening) return\n\n callbacks = cbs\n isListening = true\n calSetupAttempts = 0\n\n // Listen for postMessage events (Calendly, fallback for Cal.com)\n window.addEventListener(\"message\", handlePostMessage)\n\n // Set up Cal.com API listener\n setupCalComListener()\n}\n\n/**\n * Stop calendar embed tracking.\n */\nexport function stopCalendarTracking(): void {\n if (!isListening) return\n\n window.removeEventListener(\"message\", handlePostMessage)\n\n callbacks = null\n isListening = false\n calCallbackRegistered = false\n calSetupAttempts = 0\n lastBookingUid = null\n}\n\n/**\n * Check if calendar tracking is active.\n */\nexport function isCalendarTrackingActive(): boolean {\n return isListening\n}\n","import { type EngagementEvent, buildEngagementEvent } from \"@outlit/core\"\n\n// ============================================\n// SESSION TRACKER\n// ============================================\n\n/**\n * Default idle timeout in milliseconds (30 seconds).\n * After this period of no user interaction, the user is considered idle.\n */\nconst DEFAULT_IDLE_TIMEOUT = 30000\n\n/**\n * Session timeout in milliseconds (30 minutes).\n * After this period of inactivity, a new session ID is generated.\n */\nconst SESSION_TIMEOUT = 30 * 60 * 1000\n\n/**\n * Interval for updating active time (1 second).\n */\nconst TIME_UPDATE_INTERVAL = 1000\n\n/**\n * Minimum threshold to consider an engagement event spurious (50ms).\n * Events with BOTH activeTimeMs < 50ms AND totalTimeMs < 50ms are likely\n * caused by browser visibility quirks during page load and should be skipped.\n * This prevents emitting events with activeTimeMs=1ms while still allowing\n * legitimate events where the user was on the page for a meaningful duration.\n */\nconst MIN_SPURIOUS_THRESHOLD = 50\n\n/**\n * Minimum time on a page before engagement can be emitted (500ms).\n * This prevents spurious engagement events when visibility toggles occur\n * shortly after SPA navigation (e.g., Framer page transitions).\n * Without this, navigating from /terms to / and then quickly toggling\n * visibility would incorrectly emit engagement for \"/\" immediately.\n */\nconst MIN_PAGE_TIME_FOR_ENGAGEMENT = 500\n\n/**\n * Storage keys for session data in sessionStorage.\n */\nconst SESSION_ID_KEY = \"outlit_session_id\"\nconst SESSION_LAST_ACTIVITY_KEY = \"outlit_session_last_activity\"\n\n// ============================================\n// TYPES\n// ============================================\n\ninterface SessionState {\n /** Full URL when the session started (used for engagement event) */\n currentUrl: string\n /** Path portion of the URL */\n currentPath: string\n /** Timestamp when user entered the current page */\n pageEntryTime: number\n /** Last timestamp when we recorded activity */\n lastActiveTime: number\n /** Accumulated active time in milliseconds */\n activeTimeMs: number\n /** Is the tab currently visible? */\n isPageVisible: boolean\n /** Has the user interacted recently (within idle timeout)? */\n isUserActive: boolean\n /** Timeout ID for idle detection */\n idleTimeoutId: ReturnType<typeof setTimeout> | null\n /** Session ID for grouping engagement events */\n sessionId: string\n /** Whether we've already emitted an engagement event for this page session */\n hasEmittedEngagement: boolean\n}\n\nexport interface SessionTrackerOptions {\n /** Callback to emit engagement events (typically Tracker.enqueue) */\n onEngagement: (event: EngagementEvent) => void\n /** Idle timeout in milliseconds. Default: 30000 (30s) */\n idleTimeout?: number\n}\n\n// ============================================\n// SESSION TRACKER CLASS\n// ============================================\n\nexport class SessionTracker {\n private state: SessionState\n private options: SessionTrackerOptions\n private idleTimeout: number\n private timeUpdateInterval: ReturnType<typeof setInterval> | null = null\n private boundHandleActivity: () => void\n private boundHandleVisibilityChange: () => void\n\n constructor(options: SessionTrackerOptions) {\n this.options = options\n this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT\n\n // Initialize state for current page (including session ID)\n this.state = this.createInitialState()\n\n // Bind event handlers\n this.boundHandleActivity = this.handleActivity.bind(this)\n this.boundHandleVisibilityChange = this.handleVisibilityChange.bind(this)\n\n // Set up event listeners\n this.setupEventListeners()\n\n // Start time update interval\n this.startTimeUpdateInterval()\n }\n\n /**\n * Get the current session ID.\n */\n getSessionId(): string {\n return this.state.sessionId\n }\n\n // ============================================\n // PUBLIC METHODS\n // ============================================\n\n /**\n * Emit an engagement event for the current page session.\n * Called by Tracker on exit events and SPA navigation.\n *\n * This method:\n * 1. Finalizes any pending active time\n * 2. Creates and emits the engagement event (if meaningful)\n * 3. Resets state for the next session\n */\n emitEngagement(): void {\n // 1. Check if we've already handled exit for this page session (deduplication)\n // This prevents duplicate events from multiple exit handlers (visibilitychange,\n // pagehide, beforeunload) firing for the same page exit.\n if (this.state.hasEmittedEngagement) {\n return\n }\n\n // 2. Mark as handled FIRST to prevent any concurrent/reentrant calls\n this.state.hasEmittedEngagement = true\n\n // 3. Finalize any pending active time\n this.updateActiveTime()\n\n // 4. Create and emit event (only if we have meaningful data)\n const totalTimeMs = Date.now() - this.state.pageEntryTime\n\n // Skip spurious events caused by browser visibility quirks during page load.\n // These occur when a hidden→visible transition happens almost immediately after\n // SDK initialization, resulting in activeTimeMs ≈ 1ms and totalTimeMs ≈ 5ms.\n // We only skip if BOTH values are below the threshold - legitimate events will\n // have at least one meaningful value (user was on page for some duration).\n const isSpuriousEvent =\n this.state.activeTimeMs < MIN_SPURIOUS_THRESHOLD && totalTimeMs < MIN_SPURIOUS_THRESHOLD\n\n // Skip events if user hasn't been on the page long enough.\n // This prevents spurious engagement events when visibility toggles occur\n // shortly after SPA navigation (e.g., during Framer page transitions).\n // Without this check, navigating then quickly toggling visibility would\n // incorrectly emit engagement for the new page immediately.\n const isTooSoonAfterNavigation = totalTimeMs < MIN_PAGE_TIME_FOR_ENGAGEMENT\n\n if (!isSpuriousEvent && !isTooSoonAfterNavigation) {\n const event = buildEngagementEvent({\n url: this.state.currentUrl,\n referrer: document.referrer,\n activeTimeMs: this.state.activeTimeMs,\n totalTimeMs,\n sessionId: this.state.sessionId,\n })\n this.options.onEngagement(event)\n }\n\n // 5. Reset state for next engagement period (preserves sessionId and hasEmittedEngagement)\n this.resetState()\n }\n\n /**\n * Handle SPA navigation.\n * Called by Tracker when a new pageview is detected.\n *\n * This method:\n * 1. Emits engagement for the OLD page (using stored state)\n * 2. Updates state for the NEW page\n */\n onNavigation(newUrl: string): void {\n // Emit engagement for OLD page (uses state.currentUrl, not window.location)\n this.emitEngagement()\n\n // Update state for NEW page\n this.state.currentUrl = newUrl\n this.state.currentPath = this.extractPath(newUrl)\n this.state.pageEntryTime = Date.now()\n this.state.activeTimeMs = 0\n this.state.lastActiveTime = Date.now()\n this.state.isUserActive = true\n // Reset the engagement flag for the new page session\n this.state.hasEmittedEngagement = false\n\n // Reset idle timer\n this.resetIdleTimer()\n }\n\n /**\n * Stop session tracking and clean up.\n */\n stop(): void {\n // Remove event listeners\n this.removeEventListeners()\n\n // Clear intervals and timeouts\n if (this.timeUpdateInterval) {\n clearInterval(this.timeUpdateInterval)\n this.timeUpdateInterval = null\n }\n\n if (this.state.idleTimeoutId) {\n clearTimeout(this.state.idleTimeoutId)\n this.state.idleTimeoutId = null\n }\n }\n\n // ============================================\n // PRIVATE METHODS\n // ============================================\n\n private createInitialState(): SessionState {\n const now = Date.now()\n return {\n currentUrl: typeof window !== \"undefined\" ? window.location.href : \"\",\n currentPath: typeof window !== \"undefined\" ? window.location.pathname : \"/\",\n pageEntryTime: now,\n lastActiveTime: now,\n activeTimeMs: 0,\n isPageVisible:\n typeof document !== \"undefined\" ? document.visibilityState === \"visible\" : true,\n isUserActive: true, // Assume active on page load\n idleTimeoutId: null,\n sessionId: this.getOrCreateSessionId(),\n hasEmittedEngagement: false,\n }\n }\n\n private resetState(): void {\n const now = Date.now()\n this.state.pageEntryTime = now\n this.state.lastActiveTime = now\n this.state.activeTimeMs = 0\n this.state.isUserActive = true\n // Note: hasEmittedEngagement is NOT reset here - it's only reset in onNavigation\n // when a new page session begins. This prevents duplicate events on the same page.\n // Note: sessionId is preserved across page navigations within the same session\n\n // Reset idle timer\n this.resetIdleTimer()\n }\n\n /**\n * Get existing session ID from storage or create a new one.\n * Session ID is reset if:\n * - No existing session ID in storage\n * - Last activity was more than 30 minutes ago\n */\n private getOrCreateSessionId(): string {\n if (typeof sessionStorage === \"undefined\") {\n return this.generateSessionId()\n }\n\n try {\n const existingSessionId = sessionStorage.getItem(SESSION_ID_KEY)\n const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY)\n const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0\n const now = Date.now()\n\n // Check if session has expired (30 minutes of inactivity)\n if (existingSessionId && lastActivity && now - lastActivity < SESSION_TIMEOUT) {\n // Session is still valid, update last activity\n this.updateSessionActivity()\n return existingSessionId\n }\n\n // Create new session\n const newSessionId = this.generateSessionId()\n sessionStorage.setItem(SESSION_ID_KEY, newSessionId)\n sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString())\n return newSessionId\n } catch {\n // sessionStorage might throw in private browsing mode\n return this.generateSessionId()\n }\n }\n\n /**\n * Generate a new session ID (UUID v4).\n */\n private generateSessionId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n\n // Fallback for older browsers\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n }\n\n /**\n * Update the session's last activity timestamp.\n */\n private updateSessionActivity(): void {\n if (typeof sessionStorage === \"undefined\") return\n\n try {\n sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, Date.now().toString())\n } catch {\n // Ignore storage errors\n }\n }\n\n /**\n * Check if the current session has expired and create a new one if needed.\n * Called when user returns to the page after being away.\n */\n private checkSessionExpiry(): void {\n if (typeof sessionStorage === \"undefined\") return\n\n try {\n const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY)\n const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0\n const now = Date.now()\n\n if (now - lastActivity >= SESSION_TIMEOUT) {\n // Session expired, create new one\n const newSessionId = this.generateSessionId()\n sessionStorage.setItem(SESSION_ID_KEY, newSessionId)\n this.state.sessionId = newSessionId\n }\n\n // Update last activity\n sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString())\n } catch {\n // Ignore storage errors\n }\n }\n\n private setupEventListeners(): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n\n // Activity events - explicit user interactions only\n // Do NOT use media events (timeupdate, play, etc.) to avoid video loop false positives\n const activityEvents = [\"mousemove\", \"keydown\", \"click\", \"scroll\", \"touchstart\"]\n for (const event of activityEvents) {\n document.addEventListener(event, this.boundHandleActivity, { passive: true })\n }\n\n // Visibility change - pause/resume time accumulation\n document.addEventListener(\"visibilitychange\", this.boundHandleVisibilityChange)\n\n // Start idle timer\n this.resetIdleTimer()\n }\n\n private removeEventListeners(): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n\n const activityEvents = [\"mousemove\", \"keydown\", \"click\", \"scroll\", \"touchstart\"]\n for (const event of activityEvents) {\n document.removeEventListener(event, this.boundHandleActivity)\n }\n\n document.removeEventListener(\"visibilitychange\", this.boundHandleVisibilityChange)\n }\n\n /**\n * Handle user activity events.\n * Marks user as active and resets idle timer.\n */\n private handleActivity(): void {\n // If user was idle, check if session expired while idle\n if (!this.state.isUserActive) {\n this.checkSessionExpiry()\n this.state.lastActiveTime = Date.now()\n }\n\n this.state.isUserActive = true\n this.resetIdleTimer()\n\n // Update session activity timestamp (throttled - every activity event)\n this.updateSessionActivity()\n }\n\n /**\n * Handle visibility change events.\n * Pauses time accumulation when tab is hidden.\n */\n private handleVisibilityChange(): void {\n const wasVisible = this.state.isPageVisible\n const isNowVisible = document.visibilityState === \"visible\"\n\n if (wasVisible && !isNowVisible) {\n // Tab is being hidden - capture any pending active time BEFORE updating state\n // (updateActiveTime checks isPageVisible, so we must call it first)\n this.updateActiveTime()\n }\n\n // Update state after capturing time\n this.state.isPageVisible = isNowVisible\n\n if (!wasVisible && isNowVisible) {\n // Tab is becoming visible - check if session expired while away\n this.checkSessionExpiry()\n // Reset last active time\n this.state.lastActiveTime = Date.now()\n // Reset engagement flag to allow new engagement event on next exit\n this.state.hasEmittedEngagement = false\n // Keep session alive when returning to tab\n this.updateSessionActivity()\n }\n }\n\n /**\n * Reset the idle timer.\n * Called on activity and initialization.\n */\n private resetIdleTimer(): void {\n if (this.state.idleTimeoutId) {\n clearTimeout(this.state.idleTimeoutId)\n }\n\n this.state.idleTimeoutId = setTimeout(() => {\n // Finalize active time before marking as idle\n this.updateActiveTime()\n this.state.isUserActive = false\n }, this.idleTimeout)\n }\n\n /**\n * Start the interval for updating active time.\n */\n private startTimeUpdateInterval(): void {\n if (this.timeUpdateInterval) return\n\n this.timeUpdateInterval = setInterval(() => {\n this.updateActiveTime()\n }, TIME_UPDATE_INTERVAL)\n }\n\n /**\n * Update accumulated active time.\n * Only accumulates when page is visible AND user is active.\n */\n private updateActiveTime(): void {\n if (this.state.isPageVisible && this.state.isUserActive) {\n const now = Date.now()\n this.state.activeTimeMs += now - this.state.lastActiveTime\n this.state.lastActiveTime = now\n }\n }\n\n /**\n * Extract path from URL.\n */\n private extractPath(url: string): string {\n try {\n return new URL(url).pathname\n } catch {\n return \"/\"\n }\n }\n}\n\n// ============================================\n// MODULE-LEVEL FUNCTIONS\n// ============================================\n\nlet sessionTrackerInstance: SessionTracker | null = null\n\n/**\n * Initialize session tracking.\n * @returns The SessionTracker instance\n */\nexport function initSessionTracking(options: SessionTrackerOptions): SessionTracker {\n if (sessionTrackerInstance) {\n console.warn(\"[Outlit] Session tracking already initialized\")\n return sessionTrackerInstance\n }\n\n sessionTrackerInstance = new SessionTracker(options)\n return sessionTrackerInstance\n}\n\n/**\n * Stop session tracking and clean up.\n */\nexport function stopSessionTracking(): void {\n if (sessionTrackerInstance) {\n sessionTrackerInstance.stop()\n sessionTrackerInstance = null\n }\n}\n","// ============================================\n// VISITOR ID STORAGE\n// ============================================\n\nconst VISITOR_ID_KEY = \"outlit_visitor_id\"\n\n/**\n * Generate a UUID v4.\n * Uses crypto.randomUUID if available, otherwise falls back to manual generation.\n */\nexport function generateVisitorId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n\n // Fallback for older browsers\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\n/**\n * Get the visitor ID from storage, generating a new one if needed.\n * Tries localStorage first, falls back to cookie.\n */\nexport function getOrCreateVisitorId(): string {\n // Try localStorage first\n try {\n const stored = localStorage.getItem(VISITOR_ID_KEY)\n if (stored && isValidUuid(stored)) {\n return stored\n }\n } catch {\n // localStorage not available\n }\n\n // Try cookie fallback\n const cookieValue = getCookie(VISITOR_ID_KEY)\n if (cookieValue && isValidUuid(cookieValue)) {\n // Also store in localStorage for consistency\n try {\n localStorage.setItem(VISITOR_ID_KEY, cookieValue)\n } catch {\n // Ignore\n }\n return cookieValue\n }\n\n // Generate new visitor ID\n const visitorId = generateVisitorId()\n persistVisitorId(visitorId)\n return visitorId\n}\n\n/**\n * Persist visitor ID to both localStorage and cookie.\n */\nfunction persistVisitorId(visitorId: string): void {\n // Store in localStorage\n try {\n localStorage.setItem(VISITOR_ID_KEY, visitorId)\n } catch {\n // localStorage not available\n }\n\n // Also store in cookie for cross-subdomain support\n setCookie(VISITOR_ID_KEY, visitorId, 365) // 1 year\n}\n\n/**\n * Basic UUID validation.\n */\nfunction isValidUuid(value: string): boolean {\n 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)\n}\n\n// ============================================\n// COOKIE HELPERS\n// ============================================\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") return null\n\n const value = `; ${document.cookie}`\n const parts = value.split(`; ${name}=`)\n if (parts.length === 2) {\n return parts.pop()?.split(\";\").shift() ?? null\n }\n return null\n}\n\n/**\n * Get the root domain for cross-subdomain cookie sharing.\n * e.g., \"www.example.com\" → \"example.com\"\n * \"app.staging.example.com\" → \"example.com\"\n * \"localhost\" → null (no domain attribute needed)\n */\nfunction getRootDomain(): string | null {\n if (typeof window === \"undefined\") return null\n\n const hostname = window.location.hostname\n\n // Don't set domain for localhost or IP addresses\n if (hostname === \"localhost\" || /^(\\d{1,3}\\.){3}\\d{1,3}$/.test(hostname)) {\n return null\n }\n\n // Split hostname into parts\n const parts = hostname.split(\".\")\n\n // For simple domains like \"example.com\", return \".example.com\"\n // For subdomains like \"www.example.com\" or \"app.example.com\", return \".example.com\"\n if (parts.length >= 2) {\n // Handle common TLDs with two parts (e.g., .co.uk, .com.au)\n const twoPartTlds = [\"co.uk\", \"com.au\", \"co.nz\", \"org.uk\", \"net.au\", \"com.br\"]\n const lastTwo = parts.slice(-2).join(\".\")\n\n if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {\n // e.g., \"www.example.co.uk\" → \"example.co.uk\"\n return parts.slice(-3).join(\".\")\n }\n\n // Standard case: \"www.example.com\" → \"example.com\"\n return parts.slice(-2).join(\".\")\n }\n\n return null\n}\n\nfunction setCookie(name: string, value: string, days: number): void {\n if (typeof document === \"undefined\") return\n\n const expires = new Date()\n expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)\n\n // Build cookie string\n let cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`\n\n // Add domain for cross-subdomain support\n const rootDomain = getRootDomain()\n if (rootDomain) {\n cookie += `;domain=${rootDomain}`\n }\n\n document.cookie = cookie\n}\n","import {\n type BrowserIdentifyOptions,\n type BrowserTrackOptions,\n DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type TrackerConfig,\n type TrackerEvent,\n buildCalendarEvent,\n buildCustomEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n buildStageEvent,\n} from \"@outlit/core\"\nimport { initFormTracking, initPageviewTracking, stopAutocapture } from \"./autocapture\"\nimport {\n type CalendarBookingEvent,\n initCalendarTracking,\n stopCalendarTracking,\n} from \"./embed-integrations\"\nimport { type SessionTracker, initSessionTracking, stopSessionTracking } from \"./session-tracker\"\nimport { getOrCreateVisitorId } from \"./storage\"\n\n// ============================================\n// OUTLIT CLIENT\n// ============================================\n\nexport interface OutlitOptions extends TrackerConfig {\n /**\n * Automatically start tracking on init.\n * Set to false if you need to wait for user consent before tracking.\n * Call enableTracking() to start tracking after consent is obtained.\n * @default true\n */\n autoTrack?: boolean\n trackPageviews?: boolean\n trackForms?: boolean\n formFieldDenylist?: string[]\n flushInterval?: number\n /**\n * Automatically identify users when they submit forms with email fields.\n * Extracts email and name (first/last) from form fields using heuristics.\n * @default true\n */\n autoIdentify?: boolean\n /**\n * Track booking events from calendar embeds (Cal.com, Calendly).\n * When enabled, fires a \"calendar_booked\" custom event when bookings are detected.\n *\n * NOTE: Due to privacy restrictions in Cal.com and Calendly, their postMessage\n * events do NOT include PII (email, name). Auto-identify is NOT possible with\n * these embeds using client-side tracking alone.\n *\n * For auto-identify with calendar bookings, use server-side webhooks.\n * @default true\n */\n trackCalendarEmbeds?: boolean\n /**\n * Track engagement metrics (active time on page).\n * When enabled, emits \"engagement\" events on page exit and SPA navigation\n * capturing how long users actively engaged with each page.\n * @default true\n */\n trackEngagement?: boolean\n /**\n * Idle timeout in milliseconds for engagement tracking.\n * After this period of no user interaction, the user is considered idle\n * and active time stops accumulating.\n * @default 30000 (30 seconds)\n */\n idleTimeout?: number\n}\n\nexport interface UserIdentity {\n email?: string\n userId?: string\n traits?: Record<string, string | number | boolean | null>\n}\n\nexport class Outlit {\n private publicKey: string\n private apiHost: string\n private visitorId: string | null = null\n private eventQueue: TrackerEvent[] = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isInitialized = false\n private isTrackingEnabled = false\n private options: OutlitOptions\n private hasHandledExit = false\n private sessionTracker: SessionTracker | null = null\n // User identity state for stage events\n private currentUser: UserIdentity | null = null\n private pendingUser: UserIdentity | null = null\n\n constructor(options: OutlitOptions) {\n this.publicKey = options.publicKey\n this.apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 5000\n this.options = options\n\n // Set up exit handlers for reliable flushing\n // Uses multiple events because beforeunload is unreliable on mobile\n if (typeof window !== \"undefined\") {\n const handleExit = () => {\n if (this.hasHandledExit) return\n this.hasHandledExit = true\n\n // 1. Emit engagement event for current page (if session tracking enabled)\n this.sessionTracker?.emitEngagement()\n\n // 2. Flush the queue (now includes engagement event)\n this.flush()\n }\n\n // visibilitychange is most reliable - fires when tab is hidden\n document.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") {\n handleExit()\n } else {\n // Reset when user returns to allow next exit to flush\n this.hasHandledExit = false\n }\n })\n\n // pagehide is reliable and bfcache-friendly\n window.addEventListener(\"pagehide\", handleExit)\n\n // beforeunload as fallback for older browsers\n window.addEventListener(\"beforeunload\", handleExit)\n }\n\n this.isInitialized = true\n\n // Start tracking immediately unless autoTrack is explicitly false\n if (options.autoTrack !== false) {\n this.enableTracking()\n }\n }\n\n // ============================================\n // PUBLIC API\n // ============================================\n\n /**\n * Enable tracking. Call this after obtaining user consent.\n * This will:\n * - Generate/retrieve the visitor ID\n * - Start automatic pageview and form tracking (if configured)\n * - Begin sending events to the server\n *\n * If autoTrack is true (default), this is called automatically on init.\n */\n enableTracking(): void {\n if (this.isTrackingEnabled) {\n return // Already enabled\n }\n\n // Now we can generate/retrieve the visitor ID (sets cookies/localStorage)\n this.visitorId = getOrCreateVisitorId()\n\n // Start the flush timer\n this.startFlushTimer()\n\n // Always initialize session tracking for session ID management\n // Engagement events are only emitted when trackEngagement is enabled\n this.initSessionTracking()\n\n // Initialize autocapture if enabled\n if (this.options.trackPageviews !== false) {\n this.initPageviewTracking()\n }\n\n if (this.options.trackForms !== false) {\n this.initFormTracking(this.options.formFieldDenylist)\n }\n\n // Initialize calendar embed tracking if enabled\n if (this.options.trackCalendarEmbeds !== false) {\n this.initCalendarTracking()\n }\n\n this.isTrackingEnabled = true\n\n // Apply any pending user identity that was set before tracking was enabled\n if (this.pendingUser) {\n this.applyUser(this.pendingUser)\n this.pendingUser = null\n }\n }\n\n /**\n * Check if tracking is currently enabled.\n */\n isEnabled(): boolean {\n return this.isTrackingEnabled\n }\n\n /**\n * Track a custom event.\n */\n track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n const event = buildCustomEvent({\n url: window.location.href,\n referrer: document.referrer,\n eventName,\n properties,\n })\n this.enqueue(event)\n }\n\n /**\n * Identify the current visitor.\n * Links the anonymous visitor to a known user.\n *\n * When email or userId is provided, also sets the current user identity\n * for stage events (activate, engaged, paid).\n */\n identify(options: BrowserIdentifyOptions): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n // Update currentUser if email or userId is provided\n // This enables stage events after identify() is called\n if (options.email || options.userId) {\n this.currentUser = {\n email: options.email,\n userId: options.userId,\n }\n }\n\n const event = buildIdentifyEvent({\n url: window.location.href,\n referrer: document.referrer,\n email: options.email,\n userId: options.userId,\n traits: options.traits,\n })\n this.enqueue(event)\n }\n\n /**\n * Set the current user identity.\n * This is useful for SPA applications where you know the user's identity\n * after authentication. Calls identify() under the hood.\n *\n * If called before tracking is enabled, the identity is stored as pending\n * and applied automatically when enableTracking() is called.\n *\n * Note: Both setUser() and identify() enable stage events. The difference is\n * setUser() can be called before tracking is enabled (identity is queued),\n * while identify() requires tracking to be enabled first.\n */\n setUser(identity: UserIdentity): void {\n if (!identity.email && !identity.userId) {\n console.warn(\"[Outlit] setUser requires at least email or userId\")\n return\n }\n\n if (!this.isTrackingEnabled) {\n this.pendingUser = identity\n return\n }\n\n this.applyUser(identity)\n }\n\n /**\n * Clear the current user identity.\n * Call this when the user logs out.\n */\n clearUser(): void {\n this.currentUser = null\n this.pendingUser = null\n }\n\n /**\n * Apply user identity and send identify event.\n */\n private applyUser(identity: UserIdentity): void {\n this.currentUser = identity\n this.identify({ email: identity.email, userId: identity.userId, traits: identity.traits })\n }\n\n /**\n * Mark the current user as activated.\n * This is typically called after a user completes onboarding or a key activation milestone.\n * Requires the user to be identified (via setUser or identify with userId).\n */\n activate(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"activated\", properties)\n }\n\n /**\n * Mark the current user as engaged.\n * This is typically called when a user reaches a usage milestone.\n * Can also be computed automatically by the engagement cron.\n */\n engaged(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"engaged\", properties)\n }\n\n /**\n * Mark the current user as paid.\n * This is typically called after a successful payment/subscription.\n * Can also be triggered by Stripe integration.\n */\n paid(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"paid\", properties)\n }\n\n /**\n * Mark the current user as churned.\n * This is typically called when a subscription is cancelled.\n * Can also be triggered by Stripe integration.\n */\n churned(properties?: Record<string, string | number | boolean | null>): void {\n this.sendStageEvent(\"churned\", properties)\n }\n\n /**\n * Internal method to send a stage event.\n */\n private sendStageEvent(\n stage: ExplicitJourneyStage,\n properties?: Record<string, string | number | boolean | null>,\n ): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\n }\n\n if (!this.currentUser) {\n console.warn(\n `[Outlit] Cannot call ${stage}() without setting user identity. Call setUser() or identify() first.`,\n )\n return\n }\n\n const event = buildStageEvent({\n url: window.location.href,\n referrer: document.referrer,\n stage,\n properties,\n })\n this.enqueue(event)\n }\n\n /**\n * Get the current visitor ID.\n * Returns null if tracking is not enabled.\n */\n getVisitorId(): string | null {\n return this.visitorId\n }\n\n /**\n * Manually flush the event queue.\n */\n async flush(): Promise<void> {\n if (this.eventQueue.length === 0) return\n\n const events = [...this.eventQueue]\n this.eventQueue = []\n\n await this.sendEvents(events)\n }\n\n /**\n * Shutdown the client.\n */\n async shutdown(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n stopAutocapture()\n stopCalendarTracking()\n stopSessionTracking()\n this.sessionTracker = null\n await this.flush()\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private initSessionTracking(): void {\n this.sessionTracker = initSessionTracking({\n // Only emit engagement events when trackEngagement is enabled (default: true)\n onEngagement:\n this.options.trackEngagement !== false ? (event) => this.enqueue(event) : () => {},\n idleTimeout: this.options.idleTimeout,\n })\n }\n\n private initPageviewTracking(): void {\n initPageviewTracking((url, referrer, title) => {\n // Notify session tracker FIRST (emits engagement for OLD page using stored state)\n // This must happen before enqueueing the new pageview\n this.sessionTracker?.onNavigation(url)\n\n // Then enqueue pageview for NEW page\n const event = buildPageviewEvent({ url, referrer, title })\n this.enqueue(event)\n })\n }\n\n private initFormTracking(denylist?: string[]): void {\n // Create identity callback if autoIdentify is enabled (default: true)\n const identityCallback =\n this.options.autoIdentify !== false\n ? (identity: { email: string; name?: string; firstName?: string; lastName?: string }) => {\n // Build traits from extracted name fields\n const traits: Record<string, string> = {}\n if (identity.name) traits.name = identity.name\n if (identity.firstName) traits.firstName = identity.firstName\n if (identity.lastName) traits.lastName = identity.lastName\n\n this.identify({\n email: identity.email,\n traits: Object.keys(traits).length > 0 ? traits : undefined,\n })\n }\n : undefined\n\n initFormTracking(\n (url, formId, fields) => {\n const event = buildFormEvent({\n url,\n referrer: document.referrer,\n formId,\n formFields: fields,\n })\n this.enqueue(event)\n },\n denylist,\n identityCallback,\n )\n }\n\n private initCalendarTracking(): void {\n initCalendarTracking({\n onCalendarBooked: (bookingEvent: CalendarBookingEvent) => {\n // Track the calendar booking as a first-class calendar event\n // Note: Email is NOT available from Cal.com/Calendly client-side events\n // Use server-side webhooks for identify()\n const event = buildCalendarEvent({\n url: window.location.href,\n referrer: document.referrer,\n provider: bookingEvent.provider,\n eventType: bookingEvent.eventType,\n startTime: bookingEvent.startTime,\n endTime: bookingEvent.endTime,\n duration: bookingEvent.duration,\n isRecurring: bookingEvent.isRecurring,\n inviteeEmail: bookingEvent.inviteeEmail,\n inviteeName: bookingEvent.inviteeName,\n })\n this.enqueue(event)\n },\n })\n }\n\n private enqueue(event: TrackerEvent): void {\n this.eventQueue.push(event)\n\n // Flush immediately if queue is getting large\n if (this.eventQueue.length >= 10) {\n this.flush()\n }\n }\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush()\n }, this.flushInterval)\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (!this.visitorId) return // Can't send without a visitor ID\n\n // Include current user identity in payload for direct resolution\n // This allows the server to resolve identity immediately for SPA apps\n // instead of waiting for the anonymous visitor flow\n const userIdentity = this.currentUser ?? undefined\n // Include session ID for grouping all events in this batch\n const sessionId = this.sessionTracker?.getSessionId()\n const payload = buildIngestPayload(this.visitorId, \"client\", events, userIdentity, sessionId)\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n try {\n // Use sendBeacon for better reliability on page unload\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n const blob = new Blob([JSON.stringify(payload)], { type: \"application/json\" })\n const sent = navigator.sendBeacon(url, blob)\n if (sent) return\n }\n\n // Fallback to fetch\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n keepalive: true,\n })\n } catch (error) {\n // Silently fail - we don't want to break the user's site\n console.warn(\"[Outlit] Failed to send events:\", error)\n }\n }\n}\n\n// ============================================\n// SINGLETON INSTANCE\n// ============================================\n\nlet instance: Outlit | null = null\n\n/**\n * Initialize the Outlit client.\n * Should be called once at app startup.\n */\nexport function init(options: OutlitOptions): Outlit {\n if (instance) {\n console.warn(\"[Outlit] Already initialized\")\n return instance\n }\n\n instance = new Outlit(options)\n return instance\n}\n\n/**\n * Get the Outlit instance.\n * Throws if not initialized.\n */\nexport function getInstance(): Outlit {\n if (!instance) {\n throw new Error(\"[Outlit] Not initialized. Call init() first.\")\n }\n return instance\n}\n\n/**\n * Track a custom event.\n * Convenience method that uses the singleton instance.\n */\nexport function track(eventName: string, properties?: BrowserTrackOptions[\"properties\"]): void {\n getInstance().track(eventName, properties)\n}\n\n/**\n * Identify the current visitor.\n * Convenience method that uses the singleton instance.\n */\nexport function identify(options: BrowserIdentifyOptions): void {\n getInstance().identify(options)\n}\n\n/**\n * Enable tracking after consent is obtained.\n * Call this in your consent management tool's callback.\n * Convenience method that uses the singleton instance.\n */\nexport function enableTracking(): void {\n getInstance().enableTracking()\n}\n\n/**\n * Check if tracking is currently enabled.\n * Convenience method that uses the singleton instance.\n */\nexport function isTrackingEnabled(): boolean {\n return getInstance().isEnabled()\n}\n\n/**\n * Set the current user identity.\n * Convenience method that uses the singleton instance.\n */\nexport function setUser(identity: UserIdentity): void {\n getInstance().setUser(identity)\n}\n\n/**\n * Clear the current user identity (on logout).\n * Convenience method that uses the singleton instance.\n */\nexport function clearUser(): void {\n getInstance().clearUser()\n}\n\n/**\n * Mark the current user as activated.\n * Convenience method that uses the singleton instance.\n */\nexport function activate(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().activate(properties)\n}\n\n/**\n * Mark the current user as engaged.\n * Convenience method that uses the singleton instance.\n */\nexport function engaged(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().engaged(properties)\n}\n\n/**\n * Mark the current user as paid.\n * Convenience method that uses the singleton instance.\n */\nexport function paid(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().paid(properties)\n}\n\n/**\n * Mark the current user as churned.\n * Convenience method that uses the singleton instance.\n */\nexport function churned(properties?: Record<string, string | number | boolean | null>): void {\n getInstance().churned(properties)\n}\n"],"mappings":"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","names":["script_exports","__export","outlit","DEFAULT_API_HOST","DEFAULT_DENIED_FORM_FIELDS","extractUtmParams","url","params","utm","extractPathFromUrl","isFieldDenied","fieldName","denylist","normalizedName","denied","normalizedDenied","looksLikeSensitiveValue","value","cleaned","sanitizeFormFields","fields","customDenylist","sanitized","key","isValidEmail","value","EMAIL_FIELD_PATTERNS","FULL_NAME_PATTERNS","FIRST_NAME_PATTERNS","LAST_NAME_PATTERNS","matchesPatterns","fieldName","patterns","normalized","pattern","findEmailField","fields","inputTypes","inputType","findNameFields","fullName","firstName","lastName","trimmedValue","result","extractIdentityFromForm","email","nameFields","buildPageviewEvent","params","url","referrer","timestamp","title","extractPathFromUrl","extractUtmParams","buildFormEvent","formId","formFields","buildIdentifyEvent","userId","traits","buildCustomEvent","eventName","properties","buildCalendarEvent","provider","eventType","startTime","endTime","duration","isRecurring","inviteeEmail","inviteeName","buildEngagementEvent","activeTimeMs","totalTimeMs","sessionId","buildStageEvent","stage","buildIngestPayload","visitorId","source","events","userIdentity","payload","pageviewCallback","lastUrl","initPageviewTracking","callback","capturePageview","setupSpaListeners","url","referrer","title","capturePageviewDelayed","originalPushState","originalReplaceState","args","formCallback","formDenylist","identityCallback","initFormTracking","denylist","onIdentity","handleFormSubmit","event","form","formId","formData","fields","inputTypes","inputs","input","name","value","key","sanitizedFields","sanitizeFormFields","identity","extractIdentityFromForm","stopAutocapture","callbacks","isListening","calSetupAttempts","calCallbackRegistered","lastBookingUid","CAL_MAX_RETRY_ATTEMPTS","CAL_INITIAL_DELAY_MS","CAL_MAX_DELAY_MS","parseCalComBooking","data","event","nameMatch","start","end","setupCalComListener","Cal","handleCalComBooking","delay","e","bookingEvent","handlePostMessage","isCalendlyEvent","parseCalendlyBooking","isCalComRawMessage","bookingData","extractCalComBookingFromMessage","messageType","messageData","_payload","initCalendarTracking","cbs","stopCalendarTracking","DEFAULT_IDLE_TIMEOUT","SESSION_TIMEOUT","TIME_UPDATE_INTERVAL","MIN_SPURIOUS_THRESHOLD","MIN_PAGE_TIME_FOR_ENGAGEMENT","SESSION_ID_KEY","SESSION_LAST_ACTIVITY_KEY","SessionTracker","options","totalTimeMs","isSpuriousEvent","isTooSoonAfterNavigation","event","buildEngagementEvent","newUrl","now","existingSessionId","lastActivityStr","lastActivity","newSessionId","c","r","activityEvents","wasVisible","isNowVisible","url","sessionTrackerInstance","initSessionTracking","stopSessionTracking","VISITOR_ID_KEY","generateVisitorId","c","r","getOrCreateVisitorId","stored","isValidUuid","cookieValue","getCookie","visitorId","persistVisitorId","setCookie","value","name","parts","getRootDomain","hostname","twoPartTlds","lastTwo","days","expires","cookie","rootDomain","Outlit","options","DEFAULT_API_HOST","handleExit","getOrCreateVisitorId","eventName","properties","event","buildCustomEvent","buildIdentifyEvent","identity","stage","buildStageEvent","events","stopAutocapture","stopCalendarTracking","stopSessionTracking","initSessionTracking","initPageviewTracking","url","referrer","title","buildPageviewEvent","denylist","identityCallback","traits","initFormTracking","formId","fields","buildFormEvent","initCalendarTracking","bookingEvent","buildCalendarEvent","userIdentity","sessionId","payload","buildIngestPayload","blob","error","existingStub","stubQueue","outlit","options","Outlit","method","args","self","eventName","properties","identity","autoInit","script","publicKey","apiHost","trackPageviews","trackForms","autoTrack","autoIdentify","trackCalendarEmbeds"]}
|