export declare const TRACKER_SOURCE = "/* TeleportHQ first-party analytics tracker. Anonymous, cookieless by default. */\nconst SERVER_URL = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_URL\nconst PUBLIC_KEY = process.env.NEXT_PUBLIC_TELEPORT_ANALYTICS_KEY\n\nconst HEARTBEAT_INTERVAL_MS = 30000\nconst FLUSH_INTERVAL_MS = 5000\nconst FLUSH_AT_QUEUE_SIZE = 10\nconst MAX_BATCH = 25\nconst RETRY_DELAYS_MS = [1000, 5000, 15000]\nconst SESSION_WINDOW_MS = 30 * 60 * 1000\n\nlet initialized = false\nlet disabled = false\nlet forbiddenCount = 0\n\nlet sessionId = null\nlet visitorId = null\nlet pageLoadId = null\nlet seq = 0\nlet currentPath = null\nlet initialReferrer = null\nlet utm = null\n\nlet visibleSince = null\nlet visibleAccumMs = 0\nlet maxScrollPct = 0\n\nlet queue = []\nlet flushTimer = null\nlet heartbeatTimer = null\nlet retryAttempt = 0\n\nfunction uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID()\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction hasConsent() {\n try {\n return window.localStorage.getItem('cookieConsent') === 'accepted'\n } catch (e) {\n return false\n }\n}\n\nfunction isTrackingPossible() {\n if (disabled || typeof window === 'undefined') {\n return false\n }\n if (!SERVER_URL || !PUBLIC_KEY) {\n return false\n }\n // Fail safe: a misconfigured deploy can leave the unresolved\n // \"teleporthq.secrets.*\" placeholder (or any non-absolute value) baked into\n // these build-time vars. Never fire requests at a non-absolute URL \u2014 that\n // would point every beacon at the host site's own origin instead of the\n // analytics API.\n if (SERVER_URL.indexOf('http') !== 0 || PUBLIC_KEY.indexOf('teleporthq.secrets.') === 0) {\n return false\n }\n const host = window.location.hostname\n if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0') {\n return false\n }\n if (typeof navigator !== 'undefined' && navigator.webdriver) {\n return false\n }\n return true\n}\n\nfunction setupIdentity() {\n if (!hasConsent()) {\n // Consentless mode: nothing is stored on the device. The session id only\n // lives in JS memory; the server stitches sessions via its anonymized\n // daily visitor hash.\n sessionId = uuid()\n visitorId = null\n return\n }\n\n try {\n visitorId = window.localStorage.getItem('tp_aid')\n if (!visitorId) {\n visitorId = uuid()\n window.localStorage.setItem('tp_aid', visitorId)\n }\n\n const now = Date.now()\n const storedSession = window.sessionStorage.getItem('tp_sid')\n const storedAt = Number(window.sessionStorage.getItem('tp_sid_t') || 0)\n\n if (storedSession && now - storedAt < SESSION_WINDOW_MS) {\n sessionId = storedSession\n } else {\n sessionId = uuid()\n }\n window.sessionStorage.setItem('tp_sid', sessionId)\n window.sessionStorage.setItem('tp_sid_t', String(now))\n } catch (e) {\n sessionId = uuid()\n visitorId = null\n }\n}\n\nfunction touchSession() {\n if (visitorId === null) {\n return\n }\n try {\n window.sessionStorage.setItem('tp_sid_t', String(Date.now()))\n } catch (e) {\n /* storage unavailable */\n }\n}\n\nfunction parseUtm() {\n try {\n const params = new URLSearchParams(window.location.search)\n const read = (key) => {\n const value = params.get(key)\n return value ? value.slice(0, 255) : null\n }\n const parsed = {\n source: read('utm_source'),\n medium: read('utm_medium'),\n campaign: read('utm_campaign'),\n term: read('utm_term'),\n content: read('utm_content'),\n }\n const hasAny = Object.keys(parsed).some((key) => parsed[key])\n return hasAny ? parsed : null\n } catch (e) {\n return null\n }\n}\n\nfunction endpoint(suffix) {\n return SERVER_URL.replace(/\\/$/, '') + '/events/' + PUBLIC_KEY + (suffix || '')\n}\n\nfunction markForbidden(status) {\n if (status === 403 || status === 401) {\n forbiddenCount += 1\n if (forbiddenCount >= 3) {\n // Analytics disabled server-side \u2014 go silent for this page lifetime\n disabled = true\n queue = []\n if (flushTimer) clearInterval(flushTimer)\n if (heartbeatTimer) clearInterval(heartbeatTimer)\n }\n } else {\n forbiddenCount = 0\n }\n}\n\nfunction sendBatch(events, useBeacon) {\n if (events.length === 0) {\n return Promise.resolve(true)\n }\n\n const body = JSON.stringify({ events: events })\n const url = endpoint('/batch')\n\n // text/plain is a CORS-safelisted content type, so the request skips the\n // preflight. That preflight is what makes an application/json beacon fail on\n // page unload (the browser can't complete OPTIONS while the page is dying),\n // and it also doubles every normal batch into OPTIONS+POST. The server reads\n // the JSON body regardless of this content type.\n if (useBeacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n try {\n const blob = new Blob([body], { type: 'text/plain;charset=UTF-8' })\n return Promise.resolve(navigator.sendBeacon(url, blob))\n } catch (e) {\n /* fall through to fetch */\n }\n }\n\n return fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: body,\n keepalive: true,\n })\n .then((response) => {\n markForbidden(response.status)\n return response.status < 400\n })\n .catch(() => false)\n}\n\nfunction flush(useBeacon) {\n if (queue.length === 0 || disabled) {\n return\n }\n\n const batch = queue.slice(0, MAX_BATCH)\n queue = queue.slice(batch.length)\n\n sendBatch(batch, useBeacon).then((ok) => {\n if (ok) {\n retryAttempt = 0\n return\n }\n if (retryAttempt < RETRY_DELAYS_MS.length && !disabled) {\n // Re-queue once per backoff step, then drop \u2014 analytics is lossy-tolerant\n queue = batch.concat(queue)\n const delay = RETRY_DELAYS_MS[retryAttempt]\n retryAttempt += 1\n setTimeout(() => flush(false), delay)\n }\n })\n}\n\nfunction enqueue(event) {\n if (disabled) {\n return\n }\n queue.push(event)\n touchSession()\n if (queue.length >= FLUSH_AT_QUEUE_SIZE) {\n flush(false)\n }\n}\n\nfunction baseEvent(type) {\n seq += 1\n return {\n type: type,\n pageLoadId: pageLoadId,\n seq: seq - 1,\n sessionId: sessionId,\n visitorId: visitorId,\n path: currentPath,\n clientTs: Date.now(),\n }\n}\n\nfunction visibleTimeMs() {\n let total = visibleAccumMs\n if (visibleSince !== null) {\n total += Date.now() - visibleSince\n }\n return Math.max(0, Math.round(total))\n}\n\nfunction resetPageMetrics() {\n visibleAccumMs = 0\n visibleSince = document.visibilityState === 'visible' ? Date.now() : null\n maxScrollPct = 0\n}\n\nfunction currentScrollPct() {\n try {\n const doc = document.documentElement\n const scrollable = doc.scrollHeight - window.innerHeight\n if (scrollable <= 0) {\n return 100\n }\n return Math.min(100, Math.round((window.scrollY / scrollable) * 100))\n } catch (e) {\n return 0\n }\n}\n\nfunction pathnameOf(url) {\n if (typeof url !== 'string' || !url) {\n return null\n }\n return url.split('?')[0].split('#')[0] || '/'\n}\n\n// True when a route event resolves to the page we are already tracking. The\n// pages-router emits routeChangeStart/Complete for the SAME path during initial\n// hydration (and when a link points at the current page), which would otherwise\n// double-count the pageview and emit a bogus 0ms page_leave.\nfunction samePathAsCurrent(url) {\n const next = pathnameOf(url)\n return currentPath !== null && next !== null && next === currentPath\n}\n\nfunction trackPageview(isFirstLoad) {\n if (!isTrackingPossible()) {\n return\n }\n\n const newPath = window.location.pathname || '/'\n // Defensive same-path guard (the route handlers also guard on the router's\n // destination url): never emit a second pageview for the page already shown.\n if (!isFirstLoad && currentPath !== null && newPath === currentPath) {\n return\n }\n\n pageLoadId = uuid()\n seq = 0\n currentPath = newPath\n resetPageMetrics()\n\n const event = baseEvent('pageview')\n event.referrer = isFirstLoad ? (document.referrer || null) : null\n event.utm = isFirstLoad ? utm : null\n event.screenW = window.screen && window.screen.width ? window.screen.width : null\n enqueue(event)\n}\n\nfunction trackLeave(useBeacon) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n const event = baseEvent('page_leave')\n event.timeOnPageMs = visibleTimeMs()\n event.maxScrollPct = maxScrollPct\n enqueue(event)\n\n if (useBeacon) {\n flush(true)\n }\n}\n\nfunction sendHeartbeat() {\n if (!isTrackingPossible() || document.visibilityState !== 'visible' || !pageLoadId) {\n return\n }\n\n // Heartbeats go direct (never queued) \u2014 a stale heartbeat is worthless.\n // text/plain keeps this preflight-free too (see sendBatch).\n fetch(endpoint('/heartbeat'), {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify({\n sessionId: sessionId,\n visitorId: visitorId,\n pageLoadId: pageLoadId,\n path: currentPath,\n }),\n keepalive: true,\n })\n .then((response) => markForbidden(response.status))\n .catch(() => undefined)\n}\n\nfunction onClickCapture(domEvent) {\n if (!isTrackingPossible() || !pageLoadId) {\n return\n }\n\n try {\n const target = domEvent.target && domEvent.target.closest\n ? domEvent.target.closest('a, button, [role=\"button\"], input[type=\"submit\"], [data-tp-event]')\n : null\n if (!target) {\n return\n }\n\n const namedAttr = target.getAttribute('data-tp-event')\n let href = null\n if (target.tagName === 'A' && target.getAttribute('href')) {\n const raw = target.getAttribute('href')\n if (raw.indexOf('http') === 0) {\n try {\n const parsed = new URL(raw)\n href = parsed.origin === window.location.origin ? parsed.pathname : raw.slice(0, 512)\n } catch (e) {\n href = raw.slice(0, 512)\n }\n } else {\n href = raw.split('?')[0].split('#')[0].slice(0, 512) || null\n }\n }\n\n const event = baseEvent('click')\n event.click = {\n tag: target.tagName ? target.tagName.toLowerCase().slice(0, 16) : null,\n id: target.id ? target.id.slice(0, 64) : null,\n text: target.textContent ? target.textContent.trim().slice(0, 64) : null,\n href: href,\n name: namedAttr && /^[a-zA-Z0-9_-]{1,64}$/.test(namedAttr) ? namedAttr : null,\n }\n enqueue(event)\n } catch (e) {\n /* never break the host page */\n }\n}\n\nexport function initTeleportAnalytics() {\n if (initialized || typeof window === 'undefined') {\n return\n }\n initialized = true\n\n if (!isTrackingPossible()) {\n return\n }\n\n setupIdentity()\n utm = parseUtm()\n\n document.addEventListener('click', onClickCapture, true)\n\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n visibleSince = Date.now()\n sendHeartbeat()\n } else {\n if (visibleSince !== null) {\n visibleAccumMs += Date.now() - visibleSince\n visibleSince = null\n }\n flush(true)\n }\n })\n\n window.addEventListener(\n 'scroll',\n () => {\n const pct = currentScrollPct()\n if (pct > maxScrollPct) {\n maxScrollPct = pct\n }\n },\n { passive: true }\n )\n\n // pagehide covers tab close, navigation away and most mobile terminations.\n // When even this never fires (power loss, process kill), the server's\n // heartbeat window + session finalizer close the visit.\n window.addEventListener('pagehide', () => {\n trackLeave(true)\n })\n\n flushTimer = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS)\n\n trackPageview(true)\n sendHeartbeat()\n}\n\nexport function trackRouteLeave(url) {\n // A same-path routeChangeStart (hydration) is not a real leave \u2014 skip it so we\n // don't enqueue a 0ms page_leave. Genuine end-of-visit goes through pagehide\n // (trackLeave(true)), which is intentionally unconditional.\n if (samePathAsCurrent(url)) {\n return\n }\n trackLeave(false)\n}\n\nexport function trackRouteChange(url) {\n if (samePathAsCurrent(url)) {\n return\n }\n trackPageview(false)\n sendHeartbeat()\n}\n\n// \u2500\u2500 Commerce funnel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// The five steps of a storefront purchase, in the GA4 vocabulary a merchant\n// already knows. They are emitted as their own event TYPE rather than as named\n// clicks: a page view and a purchase are not clicks, and mixing them into the\n// clicks report would make both harder to read.\n//\n// A FIXED allowlist, not free text. The name becomes a grouping dimension in\n// the funnel builder and the daily roll-up, so an unbounded set would let a\n// typo (or a compromised page) fan the table out into thousands of one-row\n// groups \u2014 and a mistyped step is a step that silently never matches.\nconst COMMERCE_EVENTS = [\n 'view_item',\n 'add_to_cart',\n 'remove_from_cart',\n 'begin_checkout',\n 'purchase',\n]\n\n// Orders already counted, so a shopper refreshing the confirmation page (or\n// coming back to it from their history) cannot inflate the conversion number.\n// Kept in localStorage because the whole point is to survive a reload; capped\n// so a busy device cannot grow it without bound.\nconst PURCHASE_LEDGER_KEY = 'tp_purchases'\nconst PURCHASE_LEDGER_MAX = 50\n\nfunction readPurchaseLedger() {\n try {\n const raw = window.localStorage.getItem(PURCHASE_LEDGER_KEY)\n const parsed = raw ? JSON.parse(raw) : []\n return Array.isArray(parsed) ? parsed : []\n } catch (e) {\n // Private mode, disabled storage, corrupted value: an empty ledger means a\n // duplicate purchase MIGHT be counted, which is a far better failure than\n // throwing inside a checkout.\n return []\n }\n}\n\n// True when this order has been counted before. Recording and checking are one\n// function so no caller can test without also marking.\nfunction claimPurchase(orderId) {\n if (!orderId) {\n // Without an id there is nothing to dedupe on. Counting it is the honest\n // choice: dropping every anonymous purchase would under-report far more\n // than the occasional refresh over-reports.\n return true\n }\n const ledger = readPurchaseLedger()\n if (ledger.indexOf(orderId) !== -1) {\n return false\n }\n ledger.push(orderId)\n try {\n window.localStorage.setItem(\n PURCHASE_LEDGER_KEY,\n JSON.stringify(ledger.slice(-PURCHASE_LEDGER_MAX))\n )\n } catch (e) {\n /* the event still goes out; only the dedupe is lost */\n }\n return true\n}\n\n// Mirror to GA4 / GTM when the merchant configured one. The dataLayer contract\n// is GA4's own, so a merchant's existing Ads and GA4 funnels work with no\n// tagging work \u2014 which is the entire reason to duplicate the event.\nfunction pushToDataLayer(name, detail) {\n try {\n if (typeof window === 'undefined' || !Array.isArray(window.dataLayer)) {\n return\n }\n const payload = { event: name }\n if (detail && detail.currency) {\n payload.currency = detail.currency\n }\n if (detail && detail.value != null) {\n payload.value = detail.value\n }\n if (detail && detail.orderId) {\n payload.transaction_id = detail.orderId\n }\n if (detail && detail.items) {\n payload.items = detail.items\n }\n window.dataLayer.push({ ecommerce: null })\n window.dataLayer.push(payload)\n } catch (e) {\n /* never break the host page */\n }\n}\n\n// The one entry point. Safe to call before the tracker has initialised, after\n// it has disabled itself, and on a page where analytics was never enabled \u2014\n// every one of those is a silent no-op rather than a throw, because the callers\n// are checkout and add-to-cart handlers where an exception costs a sale.\nexport function trackCommerceEvent(name, detail) {\n try {\n if (COMMERCE_EVENTS.indexOf(name) === -1) {\n return\n }\n // The dataLayer mirror runs even when our own tracker is off: the merchant's\n // GA4 property is theirs, and it should not depend on our analytics being\n // enabled.\n pushToDataLayer(name, detail)\n\n if (!initialized || !isTrackingPossible()) {\n return\n }\n if (name === 'purchase' && !claimPurchase(detail && detail.orderId)) {\n return\n }\n\n const event = baseEvent('commerce')\n event.name = name\n enqueue(event)\n // Checkout ends in a redirect to a payment provider, which can tear the page\n // down before the 5s flush timer fires. \"purchase\" and \"begin_checkout\" are\n // the two events worth losing nothing of.\n if (name === 'purchase' || name === 'begin_checkout') {\n flush(true)\n }\n } catch (e) {\n /* never break a checkout */\n }\n}\n\n// \u2500\u2500 View-based funnel steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// \"view_item\", \"begin_checkout\" and \"purchase\" are things a page IS, not things\n// a shopper clicks \u2014 so instead of threading an emit node through three\n// different page-load workflows, the page declares what it represents and this\n// reports it.\n//\n// The contract is one attribute set on the page's content wrapper:\n//\n// data-tq-commerce-view=\"view_item\" which step this page is\n// data-tq-commerce-id=\"\" what it is about\n// data-tq-commerce-value=\"49.99\" optional monetary value\n// data-tq-commerce-currency=\"USD\" optional currency\n//\n// Declarative on purpose: a builder that adds a new page type gets analytics by\n// naming the step, with no analytics code to write and nothing to keep in sync.\nconst COMMERCE_VIEW_ATTR = 'data-tq-commerce-view'\n\n// One report per PAGE LOAD, so a re-render (a variant click, a filter change)\n// cannot re-count the same view. Reset by the route-change hook below.\nlet reportedViewKey = null\n\nfunction readCommerceViewElement() {\n try {\n return document.querySelector('[' + COMMERCE_VIEW_ATTR + ']')\n } catch (e) {\n return null\n }\n}\n\nexport function trackCommerceView() {\n try {\n const element = readCommerceViewElement()\n if (!element) {\n return\n }\n const name = element.getAttribute(COMMERCE_VIEW_ATTR)\n if (COMMERCE_EVENTS.indexOf(name) === -1) {\n return\n }\n const id = element.getAttribute('data-tq-commerce-id') || null\n // The path alone is not enough: a details page keeps its route while the\n // shopper switches variant, and two different products share one route\n // template. The id is what makes a second view a genuinely different one.\n const key = name + '|' + (id || currentPath || '')\n if (reportedViewKey === key) {\n return\n }\n reportedViewKey = key\n\n const rawValue = element.getAttribute('data-tq-commerce-value')\n const value = rawValue != null && rawValue !== '' ? Number(rawValue) : null\n trackCommerceEvent(name, {\n orderId: name === 'purchase' ? id : null,\n currency: element.getAttribute('data-tq-commerce-currency') || null,\n value: value != null && isFinite(value) ? value : null,\n })\n } catch (e) {\n /* never break the host page */\n }\n}\n\n// Called by the tracker component after a client-side navigation. The marker\n// element only exists once the new page has painted, so the caller defers it.\nexport function resetCommerceView() {\n reportedViewKey = null\n}\n"; //# sourceMappingURL=tracker-source.d.ts.map