@outlit/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/tracker.ts","../src/autocapture.ts","../src/storage.ts"],"sourcesContent":["// Main exports for npm package\nexport {\n Tracker,\n init,\n getInstance,\n track,\n identify,\n enableTracking,\n isTrackingEnabled,\n} from \"./tracker\"\nexport type { TrackerOptions } from \"./tracker\"\n\n// Re-export useful types from core\nexport type {\n BrowserTrackOptions,\n BrowserIdentifyOptions,\n TrackerConfig,\n UtmParams,\n} from \"@outlit/core\"\n\n// Default export for simple import\nimport {\n Tracker,\n enableTracking,\n getInstance,\n identify,\n init,\n isTrackingEnabled,\n track,\n} from \"./tracker\"\n\nexport default {\n init,\n track,\n identify,\n getInstance,\n Tracker,\n enableTracking,\n isTrackingEnabled,\n}\n","import {\n type BrowserIdentifyOptions,\n type BrowserTrackOptions,\n DEFAULT_API_HOST,\n type TrackerConfig,\n type TrackerEvent,\n buildCustomEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n} from \"@outlit/core\"\nimport { initFormTracking, initPageviewTracking, stopAutocapture } from \"./autocapture\"\nimport { getOrCreateVisitorId } from \"./storage\"\n\n// ============================================\n// TRACKER CLASS\n// ============================================\n\nexport interface TrackerOptions 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\nexport class Tracker {\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: TrackerOptions\n\n constructor(options: TrackerOptions) {\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 beforeunload handler\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeunload\", () => {\n this.flush()\n })\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 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 this.isTrackingEnabled = true\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 identify(options: BrowserIdentifyOptions): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\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 * 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 tracker.\n */\n async shutdown(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n stopAutocapture()\n await this.flush()\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private initPageviewTracking(): void {\n initPageviewTracking((url, referrer, title) => {\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 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 const payload = buildIngestPayload(this.visitorId, \"pixel\", events)\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: Tracker | null = null\n\n/**\n * Initialize the Outlit tracker.\n * Should be called once at app startup.\n */\nexport function init(options: TrackerOptions): Tracker {\n if (instance) {\n console.warn(\"[Outlit] Tracker already initialized\")\n return instance\n }\n\n instance = new Tracker(options)\n return instance\n}\n\n/**\n * Get the tracker instance.\n * Throws if not initialized.\n */\nexport function getInstance(): Tracker {\n if (!instance) {\n throw new Error(\"[Outlit] Tracker 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","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 * Set up listeners for SPA navigation.\n */\nfunction setupSpaListeners(): void {\n // Listen for popstate (browser back/forward)\n window.addEventListener(\"popstate\", () => {\n capturePageview()\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 capturePageview()\n }\n\n history.replaceState = function (...args) {\n originalReplaceState.apply(this, args)\n capturePageview()\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// 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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,eAWO;;;ACXP,kBAAoF;AAQpF,IAAI,mBAA4C;AAChD,IAAI,UAAyB;AAMtB,SAAS,qBAAqB,UAAkC;AACrE,qBAAmB;AAGnB,kBAAgB;AAGhB,oBAAkB;AACpB;AAKA,SAAS,kBAAwB;AAC/B,MAAI,CAAC,iBAAkB;AAEvB,QAAM,MAAM,OAAO,SAAS;AAC5B,QAAM,WAAW,SAAS;AAC1B,QAAM,QAAQ,SAAS;AAGvB,MAAI,QAAQ,QAAS;AACrB,YAAU;AAEV,mBAAiB,KAAK,UAAU,KAAK;AACvC;AAKA,SAAS,oBAA0B;AAEjC,SAAO,iBAAiB,YAAY,MAAM;AACxC,oBAAgB;AAAA,EAClB,CAAC;AAGD,QAAM,oBAAoB,QAAQ;AAClC,QAAM,uBAAuB,QAAQ;AAErC,UAAQ,YAAY,YAAa,MAAM;AACrC,sBAAkB,MAAM,MAAM,IAAI;AAClC,oBAAgB;AAAA,EAClB;AAEA,UAAQ,eAAe,YAAa,MAAM;AACxC,yBAAqB,MAAM,MAAM,IAAI;AACrC,oBAAgB;AAAA,EAClB;AACF;AAcA,IAAI,eAAoC;AACxC,IAAI;AACJ,IAAI,mBAA4C;AAUzC,SAAS,iBACd,UACA,UACA,YACM;AACN,iBAAe;AACf,iBAAe;AACf,qBAAmB,cAAc;AAGjC,WAAS,iBAAiB,UAAU,kBAAkB,IAAI;AAC5D;AAKA,SAAS,iBAAiB,OAAoB;AAC5C,MAAI,CAAC,aAAc;AAEnB,QAAM,OAAO,MAAM;AACnB,MAAI,EAAE,gBAAgB,iBAAkB;AAExC,QAAM,MAAM,OAAO,SAAS;AAC5B,QAAM,SAAS,KAAK,MAAM,KAAK,QAAQ;AAGvC,QAAM,WAAW,IAAI,SAAS,IAAI;AAClC,QAAM,SAAiC,CAAC;AACxC,QAAM,aAAa,oBAAI,IAAoB;AAG3C,QAAM,SAAS,KAAK,iBAAiB,yBAAyB;AAC9D,aAAW,SAAS,QAAQ;AAC1B,UAAM,OAAO,MAAM,aAAa,MAAM;AACtC,QAAI,QAAQ,iBAAiB,kBAAkB;AAC7C,iBAAW,IAAI,MAAM,MAAM,IAAI;AAAA,IACjC;AAAA,EACF;AAEA,WAAS,QAAQ,CAAC,OAAO,QAAQ;AAE/B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF,CAAC;AAGD,QAAM,sBAAkB,gCAAmB,QAAQ,YAAY;AAI/D,MAAI,kBAAkB;AACpB,UAAM,eAAW,qCAAwB,QAAQ,UAAU;AAC3D,QAAI,UAAU;AACZ,uBAAiB,QAAQ;AAAA,IAC3B;AAAA,EACF;AAGA,MAAI,mBAAmB,OAAO,KAAK,eAAe,EAAE,SAAS,GAAG;AAC9D,iBAAa,KAAK,QAAQ,eAAe;AAAA,EAC3C;AACF;AASO,SAAS,kBAAwB;AACtC,qBAAmB;AACnB,iBAAe;AACf,qBAAmB;AACnB,WAAS,oBAAoB,UAAU,kBAAkB,IAAI;AAC/D;;;AClKA,IAAM,iBAAiB;AAMhB,SAAS,oBAA4B;AAC1C,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY;AACtD,WAAO,OAAO,WAAW;AAAA,EAC3B;AAGA,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,UAAM,IAAI,MAAM,MAAM,IAAK,IAAI,IAAO;AACtC,WAAO,EAAE,SAAS,EAAE;AAAA,EACtB,CAAC;AACH;AAMO,SAAS,uBAA+B;AAE7C,MAAI;AACF,UAAM,SAAS,aAAa,QAAQ,cAAc;AAClD,QAAI,UAAU,YAAY,MAAM,GAAG;AACjC,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,QAAM,cAAc,UAAU,cAAc;AAC5C,MAAI,eAAe,YAAY,WAAW,GAAG;AAE3C,QAAI;AACF,mBAAa,QAAQ,gBAAgB,WAAW;AAAA,IAClD,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,kBAAkB;AACpC,mBAAiB,SAAS;AAC1B,SAAO;AACT;AAKA,SAAS,iBAAiB,WAAyB;AAEjD,MAAI;AACF,iBAAa,QAAQ,gBAAgB,SAAS;AAAA,EAChD,QAAQ;AAAA,EAER;AAGA,YAAU,gBAAgB,WAAW,GAAG;AAC1C;AAKA,SAAS,YAAY,OAAwB;AAC3C,SAAO,yEAAyE,KAAK,KAAK;AAC5F;AAMA,SAAS,UAAU,MAA6B;AAC9C,MAAI,OAAO,aAAa,YAAa,QAAO;AAE5C,QAAM,QAAQ,KAAK,SAAS,MAAM;AAClC,QAAM,QAAQ,MAAM,MAAM,KAAK,IAAI,GAAG;AACtC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,MAAM,IAAI,GAAG,MAAM,GAAG,EAAE,MAAM,KAAK;AAAA,EAC5C;AACA,SAAO;AACT;AAQA,SAAS,gBAA+B;AACtC,MAAI,OAAO,WAAW,YAAa,QAAO;AAE1C,QAAM,WAAW,OAAO,SAAS;AAGjC,MAAI,aAAa,eAAe,0BAA0B,KAAK,QAAQ,GAAG;AACxE,WAAO;AAAA,EACT;AAGA,QAAM,QAAQ,SAAS,MAAM,GAAG;AAIhC,MAAI,MAAM,UAAU,GAAG;AAErB,UAAM,cAAc,CAAC,SAAS,UAAU,SAAS,UAAU,UAAU,QAAQ;AAC7E,UAAM,UAAU,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAExC,QAAI,YAAY,SAAS,OAAO,KAAK,MAAM,UAAU,GAAG;AAEtD,aAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAAA,IACjC;AAGA,WAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAAA,EACjC;AAEA,SAAO;AACT;AAEA,SAAS,UAAU,MAAc,OAAe,MAAoB;AAClE,MAAI,OAAO,aAAa,YAAa;AAErC,QAAM,UAAU,oBAAI,KAAK;AACzB,UAAQ,QAAQ,QAAQ,QAAQ,IAAI,OAAO,KAAK,KAAK,KAAK,GAAI;AAG9D,MAAI,SAAS,GAAG,IAAI,IAAI,KAAK,YAAY,QAAQ,YAAY,CAAC;AAG9D,QAAM,aAAa,cAAc;AACjC,MAAI,YAAY;AACd,cAAU,WAAW,UAAU;AAAA,EACjC;AAEA,WAAS,SAAS;AACpB;;;AF5GO,IAAM,UAAN,MAAc;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAA2B;AAAA,EAC3B,aAA6B,CAAC;AAAA,EAC9B,aAAoD;AAAA,EACpD;AAAA,EACA,gBAAgB;AAAA,EAChB,oBAAoB;AAAA,EACpB;AAAA,EAER,YAAY,SAAyB;AACnC,SAAK,YAAY,QAAQ;AACzB,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,UAAU;AAGf,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,gBAAgB,MAAM;AAC5C,aAAK,MAAM;AAAA,MACb,CAAC;AAAA,IACH;AAEA,SAAK,gBAAgB;AAGrB,QAAI,QAAQ,cAAc,OAAO;AAC/B,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,iBAAuB;AACrB,QAAI,KAAK,mBAAmB;AAC1B;AAAA,IACF;AAGA,SAAK,YAAY,qBAAqB;AAGtC,SAAK,gBAAgB;AAGrB,QAAI,KAAK,QAAQ,mBAAmB,OAAO;AACzC,WAAK,qBAAqB;AAAA,IAC5B;AAEA,QAAI,KAAK,QAAQ,eAAe,OAAO;AACrC,WAAK,iBAAiB,KAAK,QAAQ,iBAAiB;AAAA,IACtD;AAEA,SAAK,oBAAoB;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAmB,YAAsD;AAC7E,QAAI,CAAC,KAAK,mBAAmB;AAC3B,cAAQ,KAAK,6DAA6D;AAC1E;AAAA,IACF;AAEA,UAAM,YAAQ,+BAAiB;AAAA,MAC7B,KAAK,OAAO,SAAS;AAAA,MACrB,UAAU,SAAS;AAAA,MACnB;AAAA,MACA;AAAA,IACF,CAAC;AACD,SAAK,QAAQ,KAAK;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,SAAuC;AAC9C,QAAI,CAAC,KAAK,mBAAmB;AAC3B,cAAQ,KAAK,6DAA6D;AAC1E;AAAA,IACF;AAEA,UAAM,YAAQ,iCAAmB;AAAA,MAC/B,KAAK,OAAO,SAAS;AAAA,MACrB,UAAU,SAAS;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AACD,SAAK,QAAQ,KAAK;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,KAAK,WAAW,WAAW,EAAG;AAElC,UAAM,SAAS,CAAC,GAAG,KAAK,UAAU;AAClC,SAAK,aAAa,CAAC;AAEnB,UAAM,KAAK,WAAW,MAAM;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAA0B;AAC9B,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AACA,oBAAgB;AAChB,UAAM,KAAK,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMQ,uBAA6B;AACnC,yBAAqB,CAAC,KAAK,UAAU,UAAU;AAC7C,YAAM,YAAQ,iCAAmB,EAAE,KAAK,UAAU,MAAM,CAAC;AACzD,WAAK,QAAQ,KAAK;AAAA,IACpB,CAAC;AAAA,EACH;AAAA,EAEQ,iBAAiB,UAA2B;AAElD,UAAMC,oBACJ,KAAK,QAAQ,iBAAiB,QAC1B,CAAC,aAAsF;AAErF,YAAM,SAAiC,CAAC;AACxC,UAAI,SAAS,KAAM,QAAO,OAAO,SAAS;AAC1C,UAAI,SAAS,UAAW,QAAO,YAAY,SAAS;AACpD,UAAI,SAAS,SAAU,QAAO,WAAW,SAAS;AAElD,WAAK,SAAS;AAAA,QACZ,OAAO,SAAS;AAAA,QAChB,QAAQ,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AAAA,MACpD,CAAC;AAAA,IACH,IACA;AAEN;AAAA,MACE,CAAC,KAAK,QAAQ,WAAW;AACvB,cAAM,YAAQ,6BAAe;AAAA,UAC3B;AAAA,UACA,UAAU,SAAS;AAAA,UACnB;AAAA,UACA,YAAY;AAAA,QACd,CAAC;AACD,aAAK,QAAQ,KAAK;AAAA,MACpB;AAAA,MACA;AAAA,MACAA;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,QAAQ,OAA2B;AACzC,SAAK,WAAW,KAAK,KAAK;AAG1B,QAAI,KAAK,WAAW,UAAU,IAAI;AAChC,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,KAAK,WAAY;AAErB,SAAK,aAAa,YAAY,MAAM;AAClC,WAAK,MAAM;AAAA,IACb,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA,EAEA,MAAc,WAAW,QAAuC;AAC9D,QAAI,OAAO,WAAW,EAAG;AACzB,QAAI,CAAC,KAAK,UAAW;AAErB,UAAM,cAAU,iCAAmB,KAAK,WAAW,SAAS,MAAM;AAClE,UAAM,MAAM,GAAG,KAAK,OAAO,aAAa,KAAK,SAAS;AAEtD,QAAI;AAEF,UAAI,OAAO,cAAc,eAAe,UAAU,YAAY;AAC5D,cAAM,OAAO,IAAI,KAAK,CAAC,KAAK,UAAU,OAAO,CAAC,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAC7E,cAAM,OAAO,UAAU,WAAW,KAAK,IAAI;AAC3C,YAAI,KAAM;AAAA,MACZ;AAGA,YAAM,MAAM,KAAK;AAAA,QACf,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,OAAO;AAAA,QAC5B,WAAW;AAAA,MACb,CAAC;AAAA,IACH,SAAS,OAAO;AAEd,cAAQ,KAAK,mCAAmC,KAAK;AAAA,IACvD;AAAA,EACF;AACF;AAMA,IAAI,WAA2B;AAMxB,SAAS,KAAK,SAAkC;AACrD,MAAI,UAAU;AACZ,YAAQ,KAAK,sCAAsC;AACnD,WAAO;AAAA,EACT;AAEA,aAAW,IAAI,QAAQ,OAAO;AAC9B,SAAO;AACT;AAMO,SAAS,cAAuB;AACrC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AACA,SAAO;AACT;AAMO,SAAS,MAAM,WAAmB,YAAsD;AAC7F,cAAY,EAAE,MAAM,WAAW,UAAU;AAC3C;AAMO,SAAS,SAAS,SAAuC;AAC9D,cAAY,EAAE,SAAS,OAAO;AAChC;AAOO,SAAS,iBAAuB;AACrC,cAAY,EAAE,eAAe;AAC/B;AAMO,SAAS,oBAA6B;AAC3C,SAAO,YAAY,EAAE,UAAU;AACjC;;;ADnTA,IAAO,cAAQ;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":["import_core","identityCallback"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,409 @@
1
+ // src/tracker.ts
2
+ import {
3
+ DEFAULT_API_HOST,
4
+ buildCustomEvent,
5
+ buildFormEvent,
6
+ buildIdentifyEvent,
7
+ buildIngestPayload,
8
+ buildPageviewEvent
9
+ } from "@outlit/core";
10
+
11
+ // src/autocapture.ts
12
+ import { extractIdentityFromForm, sanitizeFormFields } from "@outlit/core";
13
+ var pageviewCallback = null;
14
+ var lastUrl = null;
15
+ function initPageviewTracking(callback) {
16
+ pageviewCallback = callback;
17
+ capturePageview();
18
+ setupSpaListeners();
19
+ }
20
+ function capturePageview() {
21
+ if (!pageviewCallback) return;
22
+ const url = window.location.href;
23
+ const referrer = document.referrer;
24
+ const title = document.title;
25
+ if (url === lastUrl) return;
26
+ lastUrl = url;
27
+ pageviewCallback(url, referrer, title);
28
+ }
29
+ function setupSpaListeners() {
30
+ window.addEventListener("popstate", () => {
31
+ capturePageview();
32
+ });
33
+ const originalPushState = history.pushState;
34
+ const originalReplaceState = history.replaceState;
35
+ history.pushState = function(...args) {
36
+ originalPushState.apply(this, args);
37
+ capturePageview();
38
+ };
39
+ history.replaceState = function(...args) {
40
+ originalReplaceState.apply(this, args);
41
+ capturePageview();
42
+ };
43
+ }
44
+ var formCallback = null;
45
+ var formDenylist;
46
+ var identityCallback = null;
47
+ function initFormTracking(callback, denylist, onIdentity) {
48
+ formCallback = callback;
49
+ formDenylist = denylist;
50
+ identityCallback = onIdentity ?? null;
51
+ document.addEventListener("submit", handleFormSubmit, true);
52
+ }
53
+ function handleFormSubmit(event) {
54
+ if (!formCallback) return;
55
+ const form = event.target;
56
+ if (!(form instanceof HTMLFormElement)) return;
57
+ const url = window.location.href;
58
+ const formId = form.id || form.name || void 0;
59
+ const formData = new FormData(form);
60
+ const fields = {};
61
+ const inputTypes = /* @__PURE__ */ new Map();
62
+ const inputs = form.querySelectorAll("input, select, textarea");
63
+ for (const input of inputs) {
64
+ const name = input.getAttribute("name");
65
+ if (name && input instanceof HTMLInputElement) {
66
+ inputTypes.set(name, input.type);
67
+ }
68
+ }
69
+ formData.forEach((value, key) => {
70
+ if (typeof value === "string") {
71
+ fields[key] = value;
72
+ }
73
+ });
74
+ const sanitizedFields = sanitizeFormFields(fields, formDenylist);
75
+ if (identityCallback) {
76
+ const identity = extractIdentityFromForm(fields, inputTypes);
77
+ if (identity) {
78
+ identityCallback(identity);
79
+ }
80
+ }
81
+ if (sanitizedFields && Object.keys(sanitizedFields).length > 0) {
82
+ formCallback(url, formId, sanitizedFields);
83
+ }
84
+ }
85
+ function stopAutocapture() {
86
+ pageviewCallback = null;
87
+ formCallback = null;
88
+ identityCallback = null;
89
+ document.removeEventListener("submit", handleFormSubmit, true);
90
+ }
91
+
92
+ // src/storage.ts
93
+ var VISITOR_ID_KEY = "outlit_visitor_id";
94
+ function generateVisitorId() {
95
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
96
+ return crypto.randomUUID();
97
+ }
98
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
99
+ const r = Math.random() * 16 | 0;
100
+ const v = c === "x" ? r : r & 3 | 8;
101
+ return v.toString(16);
102
+ });
103
+ }
104
+ function getOrCreateVisitorId() {
105
+ try {
106
+ const stored = localStorage.getItem(VISITOR_ID_KEY);
107
+ if (stored && isValidUuid(stored)) {
108
+ return stored;
109
+ }
110
+ } catch {
111
+ }
112
+ const cookieValue = getCookie(VISITOR_ID_KEY);
113
+ if (cookieValue && isValidUuid(cookieValue)) {
114
+ try {
115
+ localStorage.setItem(VISITOR_ID_KEY, cookieValue);
116
+ } catch {
117
+ }
118
+ return cookieValue;
119
+ }
120
+ const visitorId = generateVisitorId();
121
+ persistVisitorId(visitorId);
122
+ return visitorId;
123
+ }
124
+ function persistVisitorId(visitorId) {
125
+ try {
126
+ localStorage.setItem(VISITOR_ID_KEY, visitorId);
127
+ } catch {
128
+ }
129
+ setCookie(VISITOR_ID_KEY, visitorId, 365);
130
+ }
131
+ function isValidUuid(value) {
132
+ 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);
133
+ }
134
+ function getCookie(name) {
135
+ if (typeof document === "undefined") return null;
136
+ const value = `; ${document.cookie}`;
137
+ const parts = value.split(`; ${name}=`);
138
+ if (parts.length === 2) {
139
+ return parts.pop()?.split(";").shift() ?? null;
140
+ }
141
+ return null;
142
+ }
143
+ function getRootDomain() {
144
+ if (typeof window === "undefined") return null;
145
+ const hostname = window.location.hostname;
146
+ if (hostname === "localhost" || /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
147
+ return null;
148
+ }
149
+ const parts = hostname.split(".");
150
+ if (parts.length >= 2) {
151
+ const twoPartTlds = ["co.uk", "com.au", "co.nz", "org.uk", "net.au", "com.br"];
152
+ const lastTwo = parts.slice(-2).join(".");
153
+ if (twoPartTlds.includes(lastTwo) && parts.length >= 3) {
154
+ return parts.slice(-3).join(".");
155
+ }
156
+ return parts.slice(-2).join(".");
157
+ }
158
+ return null;
159
+ }
160
+ function setCookie(name, value, days) {
161
+ if (typeof document === "undefined") return;
162
+ const expires = /* @__PURE__ */ new Date();
163
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1e3);
164
+ let cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
165
+ const rootDomain = getRootDomain();
166
+ if (rootDomain) {
167
+ cookie += `;domain=${rootDomain}`;
168
+ }
169
+ document.cookie = cookie;
170
+ }
171
+
172
+ // src/tracker.ts
173
+ var Tracker = class {
174
+ publicKey;
175
+ apiHost;
176
+ visitorId = null;
177
+ eventQueue = [];
178
+ flushTimer = null;
179
+ flushInterval;
180
+ isInitialized = false;
181
+ isTrackingEnabled = false;
182
+ options;
183
+ constructor(options) {
184
+ this.publicKey = options.publicKey;
185
+ this.apiHost = options.apiHost ?? DEFAULT_API_HOST;
186
+ this.flushInterval = options.flushInterval ?? 5e3;
187
+ this.options = options;
188
+ if (typeof window !== "undefined") {
189
+ window.addEventListener("beforeunload", () => {
190
+ this.flush();
191
+ });
192
+ }
193
+ this.isInitialized = true;
194
+ if (options.autoTrack !== false) {
195
+ this.enableTracking();
196
+ }
197
+ }
198
+ // ============================================
199
+ // PUBLIC API
200
+ // ============================================
201
+ /**
202
+ * Enable tracking. Call this after obtaining user consent.
203
+ * This will:
204
+ * - Generate/retrieve the visitor ID
205
+ * - Start automatic pageview and form tracking (if configured)
206
+ * - Begin sending events to the server
207
+ *
208
+ * If autoTrack is true (default), this is called automatically on init.
209
+ */
210
+ enableTracking() {
211
+ if (this.isTrackingEnabled) {
212
+ return;
213
+ }
214
+ this.visitorId = getOrCreateVisitorId();
215
+ this.startFlushTimer();
216
+ if (this.options.trackPageviews !== false) {
217
+ this.initPageviewTracking();
218
+ }
219
+ if (this.options.trackForms !== false) {
220
+ this.initFormTracking(this.options.formFieldDenylist);
221
+ }
222
+ this.isTrackingEnabled = true;
223
+ }
224
+ /**
225
+ * Check if tracking is currently enabled.
226
+ */
227
+ isEnabled() {
228
+ return this.isTrackingEnabled;
229
+ }
230
+ /**
231
+ * Track a custom event.
232
+ */
233
+ track(eventName, properties) {
234
+ if (!this.isTrackingEnabled) {
235
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
236
+ return;
237
+ }
238
+ const event = buildCustomEvent({
239
+ url: window.location.href,
240
+ referrer: document.referrer,
241
+ eventName,
242
+ properties
243
+ });
244
+ this.enqueue(event);
245
+ }
246
+ /**
247
+ * Identify the current visitor.
248
+ * Links the anonymous visitor to a known user.
249
+ */
250
+ identify(options) {
251
+ if (!this.isTrackingEnabled) {
252
+ console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");
253
+ return;
254
+ }
255
+ const event = buildIdentifyEvent({
256
+ url: window.location.href,
257
+ referrer: document.referrer,
258
+ email: options.email,
259
+ userId: options.userId,
260
+ traits: options.traits
261
+ });
262
+ this.enqueue(event);
263
+ }
264
+ /**
265
+ * Get the current visitor ID.
266
+ * Returns null if tracking is not enabled.
267
+ */
268
+ getVisitorId() {
269
+ return this.visitorId;
270
+ }
271
+ /**
272
+ * Manually flush the event queue.
273
+ */
274
+ async flush() {
275
+ if (this.eventQueue.length === 0) return;
276
+ const events = [...this.eventQueue];
277
+ this.eventQueue = [];
278
+ await this.sendEvents(events);
279
+ }
280
+ /**
281
+ * Shutdown the tracker.
282
+ */
283
+ async shutdown() {
284
+ if (this.flushTimer) {
285
+ clearInterval(this.flushTimer);
286
+ this.flushTimer = null;
287
+ }
288
+ stopAutocapture();
289
+ await this.flush();
290
+ }
291
+ // ============================================
292
+ // INTERNAL METHODS
293
+ // ============================================
294
+ initPageviewTracking() {
295
+ initPageviewTracking((url, referrer, title) => {
296
+ const event = buildPageviewEvent({ url, referrer, title });
297
+ this.enqueue(event);
298
+ });
299
+ }
300
+ initFormTracking(denylist) {
301
+ const identityCallback2 = this.options.autoIdentify !== false ? (identity) => {
302
+ const traits = {};
303
+ if (identity.name) traits.name = identity.name;
304
+ if (identity.firstName) traits.firstName = identity.firstName;
305
+ if (identity.lastName) traits.lastName = identity.lastName;
306
+ this.identify({
307
+ email: identity.email,
308
+ traits: Object.keys(traits).length > 0 ? traits : void 0
309
+ });
310
+ } : void 0;
311
+ initFormTracking(
312
+ (url, formId, fields) => {
313
+ const event = buildFormEvent({
314
+ url,
315
+ referrer: document.referrer,
316
+ formId,
317
+ formFields: fields
318
+ });
319
+ this.enqueue(event);
320
+ },
321
+ denylist,
322
+ identityCallback2
323
+ );
324
+ }
325
+ enqueue(event) {
326
+ this.eventQueue.push(event);
327
+ if (this.eventQueue.length >= 10) {
328
+ this.flush();
329
+ }
330
+ }
331
+ startFlushTimer() {
332
+ if (this.flushTimer) return;
333
+ this.flushTimer = setInterval(() => {
334
+ this.flush();
335
+ }, this.flushInterval);
336
+ }
337
+ async sendEvents(events) {
338
+ if (events.length === 0) return;
339
+ if (!this.visitorId) return;
340
+ const payload = buildIngestPayload(this.visitorId, "pixel", events);
341
+ const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`;
342
+ try {
343
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
344
+ const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
345
+ const sent = navigator.sendBeacon(url, blob);
346
+ if (sent) return;
347
+ }
348
+ await fetch(url, {
349
+ method: "POST",
350
+ headers: {
351
+ "Content-Type": "application/json"
352
+ },
353
+ body: JSON.stringify(payload),
354
+ keepalive: true
355
+ });
356
+ } catch (error) {
357
+ console.warn("[Outlit] Failed to send events:", error);
358
+ }
359
+ }
360
+ };
361
+ var instance = null;
362
+ function init(options) {
363
+ if (instance) {
364
+ console.warn("[Outlit] Tracker already initialized");
365
+ return instance;
366
+ }
367
+ instance = new Tracker(options);
368
+ return instance;
369
+ }
370
+ function getInstance() {
371
+ if (!instance) {
372
+ throw new Error("[Outlit] Tracker not initialized. Call init() first.");
373
+ }
374
+ return instance;
375
+ }
376
+ function track(eventName, properties) {
377
+ getInstance().track(eventName, properties);
378
+ }
379
+ function identify(options) {
380
+ getInstance().identify(options);
381
+ }
382
+ function enableTracking() {
383
+ getInstance().enableTracking();
384
+ }
385
+ function isTrackingEnabled() {
386
+ return getInstance().isEnabled();
387
+ }
388
+
389
+ // src/index.ts
390
+ var src_default = {
391
+ init,
392
+ track,
393
+ identify,
394
+ getInstance,
395
+ Tracker,
396
+ enableTracking,
397
+ isTrackingEnabled
398
+ };
399
+ export {
400
+ Tracker,
401
+ src_default as default,
402
+ enableTracking,
403
+ getInstance,
404
+ identify,
405
+ init,
406
+ isTrackingEnabled,
407
+ track
408
+ };
409
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tracker.ts","../src/autocapture.ts","../src/storage.ts","../src/index.ts"],"sourcesContent":["import {\n type BrowserIdentifyOptions,\n type BrowserTrackOptions,\n DEFAULT_API_HOST,\n type TrackerConfig,\n type TrackerEvent,\n buildCustomEvent,\n buildFormEvent,\n buildIdentifyEvent,\n buildIngestPayload,\n buildPageviewEvent,\n} from \"@outlit/core\"\nimport { initFormTracking, initPageviewTracking, stopAutocapture } from \"./autocapture\"\nimport { getOrCreateVisitorId } from \"./storage\"\n\n// ============================================\n// TRACKER CLASS\n// ============================================\n\nexport interface TrackerOptions 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\nexport class Tracker {\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: TrackerOptions\n\n constructor(options: TrackerOptions) {\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 beforeunload handler\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeunload\", () => {\n this.flush()\n })\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 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 this.isTrackingEnabled = true\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 identify(options: BrowserIdentifyOptions): void {\n if (!this.isTrackingEnabled) {\n console.warn(\"[Outlit] Tracking not enabled. Call enableTracking() first.\")\n return\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 * 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 tracker.\n */\n async shutdown(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n stopAutocapture()\n await this.flush()\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private initPageviewTracking(): void {\n initPageviewTracking((url, referrer, title) => {\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 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 const payload = buildIngestPayload(this.visitorId, \"pixel\", events)\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: Tracker | null = null\n\n/**\n * Initialize the Outlit tracker.\n * Should be called once at app startup.\n */\nexport function init(options: TrackerOptions): Tracker {\n if (instance) {\n console.warn(\"[Outlit] Tracker already initialized\")\n return instance\n }\n\n instance = new Tracker(options)\n return instance\n}\n\n/**\n * Get the tracker instance.\n * Throws if not initialized.\n */\nexport function getInstance(): Tracker {\n if (!instance) {\n throw new Error(\"[Outlit] Tracker 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","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 * Set up listeners for SPA navigation.\n */\nfunction setupSpaListeners(): void {\n // Listen for popstate (browser back/forward)\n window.addEventListener(\"popstate\", () => {\n capturePageview()\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 capturePageview()\n }\n\n history.replaceState = function (...args) {\n originalReplaceState.apply(this, args)\n capturePageview()\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// 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","// Main exports for npm package\nexport {\n Tracker,\n init,\n getInstance,\n track,\n identify,\n enableTracking,\n isTrackingEnabled,\n} from \"./tracker\"\nexport type { TrackerOptions } from \"./tracker\"\n\n// Re-export useful types from core\nexport type {\n BrowserTrackOptions,\n BrowserIdentifyOptions,\n TrackerConfig,\n UtmParams,\n} from \"@outlit/core\"\n\n// Default export for simple import\nimport {\n Tracker,\n enableTracking,\n getInstance,\n identify,\n init,\n isTrackingEnabled,\n track,\n} from \"./tracker\"\n\nexport default {\n init,\n track,\n identify,\n getInstance,\n Tracker,\n enableTracking,\n isTrackingEnabled,\n}\n"],"mappings":"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","names":["identityCallback"]}
@@ -0,0 +1,2 @@
1
+ "use strict";var OutlitTracker=(()=>{var k=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var U=(e,t)=>{for(var i in t)k(e,i,{get:t[i],enumerable:!0})},V=(e,t,i,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of B(t))!M.call(e,r)&&r!==i&&k(e,r,{get:()=>t[r],enumerable:!(n=R(t,r))||n.enumerable});return e};var H=e=>V(k({},"__esModule",{value:!0}),e);var ut={};U(ut,{outlit:()=>w});var _="https://app.outlit.ai",Q=["password","passwd","pass","pwd","token","secret","api_key","apikey","api-key","credit_card","creditcard","credit-card","cc_number","ccnumber","card_number","cardnumber","cvv","cvc","ssn","social_security","socialsecurity","bank_account","bankaccount","routing_number","routingnumber"];function f(e){try{let i=new URL(e).searchParams,n={};return i.has("utm_source")&&(n.source=i.get("utm_source")??void 0),i.has("utm_medium")&&(n.medium=i.get("utm_medium")??void 0),i.has("utm_campaign")&&(n.campaign=i.get("utm_campaign")??void 0),i.has("utm_term")&&(n.term=i.get("utm_term")??void 0),i.has("utm_content")&&(n.content=i.get("utm_content")??void 0),Object.keys(n).length>0?n:void 0}catch{return}}function m(e){try{return new URL(e).pathname}catch{return"/"}}function K(e,t){let i=e.toLowerCase().replace(/[-_\s]/g,"");return t.some(n=>{let r=n.toLowerCase().replace(/[-_\s]/g,"");return i.includes(r)})}function G(e){let t=e.replace(/[\s-]/g,"");return!!(/^\d{13,19}$/.test(t)||/^\d{9}$/.test(t)||/^\d{3}-\d{2}-\d{4}$/.test(e))}function I(e,t){if(!e)return;let i=t??Q,n={};for(let[r,a]of Object.entries(e))K(r,i)||G(a)||(n[r]=a);return Object.keys(n).length>0?n:void 0}function T(e){return!e||typeof e!="string"?!1:/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.trim())}var J=[/^e?-?mail$/i,/^email[_-]?address$/i,/^user[_-]?email$/i,/^work[_-]?email$/i,/^contact[_-]?email$/i,/^primary[_-]?email$/i,/^business[_-]?email$/i],X=[/^name$/i,/^full[_-]?name$/i,/^your[_-]?name$/i,/^customer[_-]?name$/i,/^contact[_-]?name$/i,/^display[_-]?name$/i],Y=[/^first[_-]?name$/i,/^firstname$/i,/^first$/i,/^fname$/i,/^given[_-]?name$/i,/^forename$/i],Z=[/^last[_-]?name$/i,/^lastname$/i,/^last$/i,/^lname$/i,/^surname$/i,/^family[_-]?name$/i];function d(e,t){let i=e.trim();return t.some(n=>n.test(i))}function W(e,t){if(t){for(let[i,n]of t.entries())if(n==="email"){let r=e[i];if(r&&T(r))return r.trim()}}for(let[i,n]of Object.entries(e))if(d(i,J)&&T(n))return n.trim();for(let i of Object.values(e))if(T(i))return i.trim()}function tt(e){let t,i,n;for(let[a,s]of Object.entries(e)){let l=s?.trim();l&&(!t&&d(a,X)&&(t=l),!i&&d(a,Y)&&(i=l),!n&&d(a,Z)&&(n=l))}let r={};return t?r.name=t:i&&n?(r.name=`${i} ${n}`,r.firstName=i,r.lastName=n):i?r.firstName=i:n&&(r.lastName=n),r}function x(e,t){let i=W(e,t);if(!i)return;let n=tt(e);return{email:i,...n}}function E(e){let{url:t,referrer:i,timestamp:n,title:r}=e;return{type:"pageview",timestamp:n??Date.now(),url:t,path:m(t),referrer:i,utm:f(t),title:r}}function O(e){let{url:t,referrer:i,timestamp:n,formId:r,formFields:a}=e;return{type:"form",timestamp:n??Date.now(),url:t,path:m(t),referrer:i,utm:f(t),formId:r,formFields:a}}function S(e){let{url:t,referrer:i,timestamp:n,email:r,userId:a,traits:s}=e;return{type:"identify",timestamp:n??Date.now(),url:t,path:m(t),referrer:i,utm:f(t),email:r,userId:a,traits:s}}function F(e){let{url:t,referrer:i,timestamp:n,eventName:r,properties:a}=e;return{type:"custom",timestamp:n??Date.now(),url:t,path:m(t),referrer:i,utm:f(t),eventName:r,properties:a}}function $(e,t,i){return{visitorId:e,source:t,events:i}}var g=null,N=null;function P(e){g=e,p(),et()}function p(){if(!g)return;let e=window.location.href,t=document.referrer,i=document.title;e!==N&&(N=e,g(e,t,i))}function et(){window.addEventListener("popstate",()=>{p()});let e=history.pushState,t=history.replaceState;history.pushState=function(...i){e.apply(this,i),p()},history.replaceState=function(...i){t.apply(this,i),p()}}var h=null,C,v=null;function A(e,t,i){h=e,C=t,v=i??null,document.addEventListener("submit",D,!0)}function D(e){if(!h)return;let t=e.target;if(!(t instanceof HTMLFormElement))return;let i=window.location.href,n=t.id||t.name||void 0,r=new FormData(t),a={},s=new Map,l=t.querySelectorAll("input, select, textarea");for(let o of l){let c=o.getAttribute("name");c&&o instanceof HTMLInputElement&&s.set(c,o.type)}r.forEach((o,c)=>{typeof o=="string"&&(a[c]=o)});let b=I(a,C);if(v){let o=x(a,s);o&&v(o)}b&&Object.keys(b).length>0&&h(i,n,b)}function L(){g=null,h=null,v=null,document.removeEventListener("submit",D,!0)}var u="outlit_visitor_id";function it(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function j(){try{let i=localStorage.getItem(u);if(i&&z(i))return i}catch{}let e=rt(u);if(e&&z(e)){try{localStorage.setItem(u,e)}catch{}return e}let t=it();return nt(t),t}function nt(e){try{localStorage.setItem(u,e)}catch{}st(u,e,365)}function z(e){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(e)}function rt(e){if(typeof document>"u")return null;let i=`; ${document.cookie}`.split(`; ${e}=`);return i.length===2?i.pop()?.split(";").shift()??null:null}function at(){if(typeof window>"u")return null;let e=window.location.hostname;if(e==="localhost"||/^(\d{1,3}\.){3}\d{1,3}$/.test(e))return null;let t=e.split(".");if(t.length>=2){let i=["co.uk","com.au","co.nz","org.uk","net.au","com.br"],n=t.slice(-2).join(".");return i.includes(n)&&t.length>=3?t.slice(-3).join("."):t.slice(-2).join(".")}return null}function st(e,t,i){if(typeof document>"u")return;let n=new Date;n.setTime(n.getTime()+i*24*60*60*1e3);let r=`${e}=${t};expires=${n.toUTCString()};path=/;SameSite=Lax`,a=at();a&&(r+=`;domain=${a}`),document.cookie=r}var y=class{publicKey;apiHost;visitorId=null;eventQueue=[];flushTimer=null;flushInterval;isInitialized=!1;isTrackingEnabled=!1;options;constructor(t){this.publicKey=t.publicKey,this.apiHost=t.apiHost??_,this.flushInterval=t.flushInterval??5e3,this.options=t,typeof window<"u"&&window.addEventListener("beforeunload",()=>{this.flush()}),this.isInitialized=!0,t.autoTrack!==!1&&this.enableTracking()}enableTracking(){this.isTrackingEnabled||(this.visitorId=j(),this.startFlushTimer(),this.options.trackPageviews!==!1&&this.initPageviewTracking(),this.options.trackForms!==!1&&this.initFormTracking(this.options.formFieldDenylist),this.isTrackingEnabled=!0)}isEnabled(){return this.isTrackingEnabled}track(t,i){if(!this.isTrackingEnabled){console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");return}let n=F({url:window.location.href,referrer:document.referrer,eventName:t,properties:i});this.enqueue(n)}identify(t){if(!this.isTrackingEnabled){console.warn("[Outlit] Tracking not enabled. Call enableTracking() first.");return}let i=S({url:window.location.href,referrer:document.referrer,email:t.email,userId:t.userId,traits:t.traits});this.enqueue(i)}getVisitorId(){return this.visitorId}async flush(){if(this.eventQueue.length===0)return;let t=[...this.eventQueue];this.eventQueue=[],await this.sendEvents(t)}async shutdown(){this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null),L(),await this.flush()}initPageviewTracking(){P((t,i,n)=>{let r=E({url:t,referrer:i,title:n});this.enqueue(r)})}initFormTracking(t){let i=this.options.autoIdentify!==!1?n=>{let r={};n.name&&(r.name=n.name),n.firstName&&(r.firstName=n.firstName),n.lastName&&(r.lastName=n.lastName),this.identify({email:n.email,traits:Object.keys(r).length>0?r:void 0})}:void 0;A((n,r,a)=>{let s=O({url:n,referrer:document.referrer,formId:r,formFields:a});this.enqueue(s)},t,i)}enqueue(t){this.eventQueue.push(t),this.eventQueue.length>=10&&this.flush()}startFlushTimer(){this.flushTimer||(this.flushTimer=setInterval(()=>{this.flush()},this.flushInterval))}async sendEvents(t){if(t.length===0||!this.visitorId)return;let i=$(this.visitorId,"pixel",t),n=`${this.apiHost}/api/i/v1/${this.publicKey}/events`;try{if(typeof navigator<"u"&&navigator.sendBeacon){let r=new Blob([JSON.stringify(i)],{type:"application/json"});if(navigator.sendBeacon(n,r))return}await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i),keepalive:!0})}catch(r){console.warn("[Outlit] Failed to send events:",r)}}};var ot=typeof window<"u"?window.outlit:void 0,lt=ot?._q||[],w={_initialized:!1,_instance:null,_queue:[],_loaded:!0,init(e){if(this._initialized){console.warn("[Outlit] Already initialized");return}this._instance=new y(e),this._initialized=!0;for(let[t,i]of lt){let n=this;t in n&&typeof n[t]=="function"&&n[t](...i)}for(;this._queue.length>0;)this._queue.shift()?.()},track(e,t){if(!this._initialized||!this._instance){this._queue.push(()=>this.track(e,t));return}this._instance.track(e,t)},identify(e){if(!this._initialized||!this._instance){this._queue.push(()=>this.identify(e));return}this._instance.identify(e)},getVisitorId(){return this._instance?this._instance.getVisitorId():null},enableTracking(){if(!this._initialized||!this._instance){this._queue.push(()=>this.enableTracking());return}this._instance.enableTracking()},isTrackingEnabled(){return this._instance?this._instance.isEnabled():!1}};function q(){let e=document.currentScript;if(e||(e=document.querySelector("script[data-public-key]")),!e){console.warn("[Outlit] No script tag found with data-public-key attribute");return}let t=e.getAttribute("data-public-key");if(!t){console.warn("[Outlit] Missing data-public-key attribute on script tag");return}let i=e.getAttribute("data-api-host")??void 0,n=e.getAttribute("data-track-pageviews")!=="false",r=e.getAttribute("data-track-forms")!=="false",a=e.getAttribute("data-auto-track")!=="false",s=e.getAttribute("data-auto-identify")!=="false";w.init({publicKey:t,apiHost:i,trackPageviews:n,trackForms:r,autoTrack:a,autoIdentify:s})}typeof window<"u"&&(window.outlit=w,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",q):q());return H(ut);})();
2
+ //# sourceMappingURL=outlit.global.js.map