{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/transport.ts","../src/queue.ts","../src/session.ts","../src/spa.ts","../src/utm.ts","../src/validate.ts","../src/client.ts"],"sourcesContent":["/**\n * tgram-analytics JS SDK\n *\n * Lightweight, privacy-first analytics for websites and SPAs.\n * Zero dependencies. < 2 KB gzipped.\n *\n * ---\n *\n * ## Install via npm\n * ```bash\n * npm install tgram-analytics\n * ```\n *\n * ## Install via `<script>` tag\n * ```html\n * <script src=\"https://your-server.com/sdk/tga.min.js\"></script>\n * <!-- The global `TGA` object is now available. -->\n * ```\n *\n * ## Quick start (ESM / TypeScript)\n * ```ts\n * import TGA from \"tgram-analytics\";\n *\n * TGA.init(\"proj_xxx\", { serverUrl: \"https://analytics.example.com\" });\n *\n * // Pageviews are tracked automatically on load and SPA route changes.\n * // Track custom events manually:\n * TGA.track(\"purchase\", { amount: 49, plan: \"pro\" });\n * ```\n *\n * ## Quick start (`<script>` tag)\n * ```html\n * <script src=\"https://your-server.com/sdk/tga.min.js\"></script>\n * <script>\n *   TGA.init(\"proj_xxx\", { serverUrl: \"https://your-server.com\" });\n *   TGA.track(\"purchase\", { amount: 49 });\n * </script>\n * ```\n *\n * @module tgram-analytics\n */\n\nexport { TGAClient } from \"./client.js\";\nexport type { BatchOptions, EventProperties, TGAOptions } from \"./types.js\";\n\nimport { TGAClient } from \"./client.js\";\n\n/**\n * The global singleton SDK instance.\n *\n * Import this object and call `.init()` once at the top of your application.\n * Do **not** create a `new TGAClient()` — use this singleton instead.\n *\n * @example\n * import TGA from \"tgram-analytics\";\n * TGA.init(\"proj_xxx\", { serverUrl: \"https://analytics.example.com\" });\n */\nconst TGA = new TGAClient();\n\nexport default TGA;\n","/**\n * Automatic visitor context collection.\n *\n * Gathers device, browser, and environment information from standard browser\n * APIs and returns them as `$`-prefixed properties. These are merged into\n * `globalProperties` once on {@link TGAClient.init} so every event in the\n * session carries the visitor context automatically.\n *\n * **Collected properties:**\n * | Key            | Source                        | Example          |\n * |----------------|-------------------------------|------------------|\n * | `$os`          | User-Agent Client Hints / UA  | `\"macOS\"`        |\n * | `$browser`     | User-Agent Client Hints / UA  | `\"Chrome\"`       |\n * | `$language`    | `navigator.language`          | `\"en-US\"`        |\n * | `$screen`      | `screen.width` × `height`     | `\"1920x1080\"`    |\n * | `$viewport`    | `innerWidth` × `innerHeight`  | `\"1440x900\"`     |\n * | `$timezone`    | `Intl.DateTimeFormat`         | `\"Europe/Rome\"`  |\n * | `$device_type` | Screen width + touch heuristic| `\"desktop\"`      |\n */\n\n// ── UA Client Hints types (not yet in all TS libs) ──────────────────────────\n\ninterface NavigatorUABrandVersion {\n  brand: string;\n  version: string;\n}\n\ninterface NavigatorUAData {\n  brands: NavigatorUABrandVersion[];\n  mobile: boolean;\n  platform: string;\n}\n\n/**\n * Detects the operating system.\n *\n * Prefers the User-Agent Client Hints API (`navigator.userAgentData.platform`)\n * which returns clean values like `\"Windows\"`, `\"macOS\"`, `\"Android\"`.\n * Falls back to pattern-matching the legacy `navigator.userAgent` string.\n */\nfunction detectOS(uaData: NavigatorUAData | undefined, ua: string): string | undefined {\n  if (uaData?.platform) return uaData.platform;\n\n  if (/Windows/.test(ua)) return \"Windows\";\n  if (/iPhone|iPad|iPod/.test(ua)) return \"iOS\";\n  if (/Macintosh|Mac OS/.test(ua)) return \"macOS\";\n  if (/Android/.test(ua)) return \"Android\";\n  if (/Linux/.test(ua)) return \"Linux\";\n  if (/CrOS/.test(ua)) return \"ChromeOS\";\n\n  return undefined;\n}\n\n/**\n * Detects the browser name.\n *\n * From Client Hints, picks the first brand that is not the generic `\"Chromium\"`\n * engine and not a GREASE brand (those contain `\"Not\"` in the name).\n * Falls back to UA string matching.\n */\nfunction detectBrowser(uaData: NavigatorUAData | undefined, ua: string): string | undefined {\n  if (uaData?.brands?.length) {\n    const real = uaData.brands.find(\n      (b) => !b.brand.includes(\"Chromium\") && !b.brand.includes(\"Not\"),\n    );\n    if (real) return real.brand;\n  }\n\n  if (/Firefox\\//.test(ua)) return \"Firefox\";\n  if (/Edg\\//.test(ua)) return \"Edge\";\n  if (/OPR\\/|Opera\\//.test(ua)) return \"Opera\";\n  if (/Chrome\\//.test(ua)) return \"Chrome\";\n  if (/Safari\\//.test(ua) && !/Chrome/.test(ua)) return \"Safari\";\n\n  return undefined;\n}\n\n/**\n * Classifies the device as mobile, tablet, or desktop using a simple heuristic.\n *\n * - `navigator.userAgentData.mobile` is authoritative when available.\n * - Otherwise: screen width < 768 → mobile; < 1024 with touch → tablet.\n */\nfunction detectDeviceType(uaData: NavigatorUAData | undefined): string {\n  if (uaData?.mobile) return \"mobile\";\n\n  const width = typeof screen !== \"undefined\" ? screen.width : 1024;\n  const hasTouch = typeof window !== \"undefined\" && \"ontouchstart\" in window;\n\n  if (width < 768) return \"mobile\";\n  if (width < 1024 && hasTouch) return \"tablet\";\n  return \"desktop\";\n}\n\n/**\n * Collects visitor context from browser APIs.\n *\n * Returns an empty object when called outside a browser environment\n * (`window` is undefined), making it safe to call during SSR.\n *\n * Only keys with non-empty values are included in the returned object.\n *\n * @returns A plain object mapping `$`-prefixed key → value.\n *\n * @example\n * collectContext();\n * // => { $os: \"macOS\", $browser: \"Chrome\", $language: \"en-US\", ... }\n */\nexport function collectContext(): Record<string, string> {\n  if (typeof window === \"undefined\") return {};\n\n  const uaData = (navigator as Navigator & { userAgentData?: NavigatorUAData }).userAgentData;\n  const ua = typeof navigator !== \"undefined\" ? navigator.userAgent || \"\" : \"\";\n\n  const result: Record<string, string> = {};\n\n  const os = detectOS(uaData, ua);\n  if (os) result.$os = os;\n\n  const browser = detectBrowser(uaData, ua);\n  if (browser) result.$browser = browser;\n\n  if (typeof navigator !== \"undefined\" && navigator.language) {\n    result.$language = navigator.language;\n  }\n\n  if (typeof screen !== \"undefined\") {\n    result.$screen = `${screen.width}x${screen.height}`;\n  }\n\n  result.$viewport = `${window.innerWidth}x${window.innerHeight}`;\n\n  try {\n    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;\n    if (tz) result.$timezone = tz;\n  } catch {\n    // Intl may be unavailable in exotic environments.\n  }\n\n  result.$device_type = detectDeviceType(uaData);\n\n  return result;\n}\n","/**\n * Low-level event transport layer.\n *\n * **Design principle:** all sends are fire-and-forget. This module never\n * throws, never rejects, and never returns a meaningful value. Analytics\n * must never interrupt or break the host application.\n *\n * ## Transport selection\n *\n * | Situation | Mechanism |\n * |-----------|-----------|\n * | Page is being hidden / unloaded | `navigator.sendBeacon` |\n * | sendBeacon unavailable or refused | `fetch` with `keepalive: true` |\n * | fetch unavailable (very rare) | silent no-op |\n *\n * ### Why sendBeacon on page hide?\n * Regular `fetch` calls are cancelled when the browser navigates away.\n * `sendBeacon` queues the request at the OS level and guarantees delivery\n * even if the page is destroyed immediately after.\n *\n * ### Why keepalive on fetch?\n * `keepalive: true` tells the browser to keep the request alive even if the\n * page is unloaded — a reliable fallback when `sendBeacon` refuses (e.g.\n * when the payload exceeds the 64 KB beacon limit).\n *\n * ### Why mode: \"cors\"?\n * The browser includes the `Origin` header only on CORS requests. The server\n * reads that header to enforce the per-project `domain_allowlist`.\n */\n\n/**\n * Sends a JSON payload to the given URL, choosing the best available transport.\n *\n * This function is safe to call at any time, including during page unload.\n * Errors are silently swallowed — analytics must never break the host page.\n *\n * @param url     - The full endpoint URL, e.g. `\"https://srv.com/api/v1/track\"`.\n * @param payload - Any JSON-serialisable value (object, array, primitive).\n *\n * @example\n * send(\"https://analytics.example.com/api/v1/track\", {\n *   api_key: \"proj_xxx\",\n *   event_name: \"purchase\",\n *   session_id: \"550e8400-...\",\n *   properties: { amount: 49 },\n *   timestamp: \"2024-01-01T12:00:00.000Z\",\n * });\n */\nexport function send(url: string, payload: unknown): void {\n  const body = JSON.stringify(payload);\n\n  // During page hide, prefer Beacon for guaranteed delivery.\n  if (\n    typeof document !== \"undefined\" &&\n    document.visibilityState === \"hidden\" &&\n    typeof navigator !== \"undefined\" &&\n    typeof navigator.sendBeacon === \"function\"\n  ) {\n    // sendBeacon requires a Blob with the correct MIME type so that the server\n    // reads the body as JSON. Using text/plain would avoid a CORS preflight but\n    // the server requires application/json.\n    const blob = new Blob([body], { type: \"application/json\" });\n    const accepted = navigator.sendBeacon(url, blob);\n    if (accepted) return;\n    // sendBeacon can return false when the payload is too large (> 64 KB) or\n    // the browser is in a state that disallows queueing. Fall through to fetch.\n  }\n\n  // Normal path — fetch with keepalive so the request outlives navigations.\n  try {\n    fetch(url, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body,\n      keepalive: true, // keep alive across soft navigations\n      mode: \"cors\", // include Origin header for server-side allowlist check\n    }).catch(() => {\n      // Network failure or CORS rejection. Swallow silently.\n    });\n  } catch {\n    // fetch itself can throw synchronously in very unusual environments.\n    // Swallow silently — we must never throw from the analytics layer.\n  }\n}\n","/**\n * In-memory event batching queue.\n *\n * When batching is enabled (via `TGAOptions.batch`), events are buffered here\n * rather than being sent immediately. The queue flushes — sending all buffered\n * events — when either:\n * 1. The buffer reaches `maxSize` events, or\n * 2. `maxWait` milliseconds have passed since the first event was buffered.\n *\n * **Why batch?**\n * Batching is useful when you track many events in a short burst (e.g. user\n * interactions, scroll depth checkpoints). Instead of firing one `fetch` per\n * event, the queue coalesces them into a single burst sent on the next flush.\n *\n * **Note:** The server has no batch endpoint, so each event is still sent as\n * its own HTTP request when the queue flushes. The benefit is reducing the\n * number of requests fired during rapid event sequences.\n */\n\nimport { send } from \"./transport.js\";\n\n/** Required configuration for the {@link EventQueue}. */\nexport interface QueueConfig {\n  /**\n   * Maximum number of events to buffer before the queue is force-flushed.\n   * @default 10\n   */\n  maxSize: number;\n  /**\n   * Maximum milliseconds to wait before the queue is automatically flushed.\n   * @default 5000\n   */\n  maxWait: number;\n}\n\n/** A single event waiting to be sent. */\ninterface QueuedEvent {\n  /** API endpoint path, relative to serverUrl (e.g. `\"/api/v1/track\"`). */\n  endpoint: string;\n  /** JSON-serialisable request body. */\n  payload: unknown;\n}\n\n/**\n * A simple in-memory event queue that flushes on size or time threshold.\n *\n * @example\n * const queue = new EventQueue(\"https://analytics.example.com\", {\n *   maxSize: 10,\n *   maxWait: 5000,\n * });\n *\n * queue.push(\"/api/v1/track\", { api_key: \"proj_xxx\", event_name: \"click\", ... });\n * queue.flush(); // send all buffered events immediately\n */\nexport class EventQueue {\n  private readonly serverUrl: string;\n  private readonly config: QueueConfig;\n  private buffer: QueuedEvent[] = [];\n  private timer: ReturnType<typeof setTimeout> | null = null;\n\n  /**\n   * @param serverUrl - Base server URL (no trailing slash). Prepended to each\n   *                    endpoint path when building the full request URL.\n   * @param config    - Queue thresholds.\n   */\n  constructor(serverUrl: string, config: QueueConfig) {\n    this.serverUrl = serverUrl;\n    this.config = config;\n  }\n\n  /**\n   * Adds an event to the buffer.\n   *\n   * - If the buffer now equals `maxSize`, flushes immediately.\n   * - Otherwise, starts the auto-flush timer if it isn't already running.\n   *\n   * @param endpoint - API path relative to `serverUrl`, e.g. `\"/api/v1/track\"`.\n   * @param payload  - JSON-serialisable request body.\n   */\n  push(endpoint: string, payload: unknown): void {\n    this.buffer.push({ endpoint, payload });\n\n    if (this.buffer.length >= this.config.maxSize) {\n      // Buffer is full — flush now.\n      this.flush();\n    } else if (this.timer === null) {\n      // Start the countdown timer for the first event in a new batch.\n      this.timer = setTimeout(() => this.flush(), this.config.maxWait);\n    }\n  }\n\n  /**\n   * Sends all buffered events immediately and resets the buffer and timer.\n   *\n   * Safe to call when the buffer is empty — it is a no-op in that case.\n   * Also called automatically by the SDK on page hide / unload.\n   */\n  flush(): void {\n    if (this.timer !== null) {\n      clearTimeout(this.timer);\n      this.timer = null;\n    }\n\n    // Drain the buffer atomically so that events pushed during flush are not lost.\n    const events = this.buffer.splice(0);\n    for (const { endpoint, payload } of events) {\n      send(`${this.serverUrl}${endpoint}`, payload);\n    }\n  }\n}\n","/**\n * Session ID management.\n *\n * A \"session\" in tgram-analytics maps to a single browser tab.\n * We use `sessionStorage` so that:\n * - Each tab gets its own independent session ID.\n * - The ID is automatically cleared when the tab is closed.\n * - No cookies or `localStorage` are ever written (privacy-first).\n *\n * If `sessionStorage` is unavailable (e.g. sandboxed iframes, private\n * browsing with strict settings), the module silently falls back to\n * an in-memory ID that persists only for the current page load.\n */\n\n/** The key used to store the session ID in sessionStorage. */\nconst STORAGE_KEY = \"tga_sid\";\n\n/**\n * Returns the current session ID, creating and persisting a new UUID v4 if\n * one does not already exist in `sessionStorage`.\n *\n * @param customId - When provided, this value is returned as-is and\n *                   `sessionStorage` is not read or written. Use this when\n *                   you manage sessions server-side and want to pass an ID in.\n * @returns A UUID v4 string that identifies the current session.\n *\n * @example Auto-generated session\n * const sid = getOrCreateSessionId(); // \"550e8400-e29b-41d4-a716-446655440000\"\n *\n * @example Custom session ID\n * const sid = getOrCreateSessionId(\"my-server-session-id\");\n */\nexport function getOrCreateSessionId(customId?: string): string {\n  if (customId) return customId;\n\n  // Attempt to reuse an existing session from the same tab.\n  try {\n    const existing = sessionStorage.getItem(STORAGE_KEY);\n    if (existing) return existing;\n  } catch {\n    // sessionStorage may throw in sandboxed iframes or very strict\n    // private-browsing modes. Fall through to create a new in-memory ID.\n  }\n\n  const id = generateUUID();\n\n  try {\n    sessionStorage.setItem(STORAGE_KEY, id);\n  } catch {\n    // Write failed — the ID exists only for this call. Subsequent calls\n    // will generate a new ID, which is an acceptable degradation.\n  }\n\n  return id;\n}\n\n/**\n * Removes the session ID from `sessionStorage` so that the next call to\n * {@link getOrCreateSessionId} generates a fresh UUID.\n *\n * Called by `TGA.reset()` after a user logs out, ensuring subsequent events\n * are not attributed to the previous user's session.\n */\nexport function clearSessionId(): void {\n  try {\n    sessionStorage.removeItem(STORAGE_KEY);\n  } catch {\n    // Nothing to clear, or storage unavailable. Safe to ignore.\n  }\n}\n\n// ── UUID generation ──────────────────────────────────────────────────────────\n\n/**\n * Generates a random UUID v4 string using the Web Crypto API.\n *\n * Prefers `crypto.randomUUID()` (available in all modern browsers since 2021).\n * Falls back to a manual implementation using `crypto.getRandomValues()` for\n * older environments (e.g. Safari < 15.4, some older Chromium-based browsers).\n *\n * @returns A standard UUID v4 string,\n *          e.g. `\"550e8400-e29b-41d4-a716-446655440000\"`.\n */\nfunction generateUUID(): string {\n  // Fast path: native randomUUID (Chrome 92+, Firefox 95+, Safari 15.4+).\n  if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n    return crypto.randomUUID();\n  }\n\n  // Fallback: build UUID v4 manually via getRandomValues.\n  const bytes = new Uint8Array(16);\n  crypto.getRandomValues(bytes);\n\n  // Set the version nibble to 4 (bits 12-15 of byte 6).\n  bytes[6] = (bytes[6] & 0x0f) | 0x40;\n  // Set the variant bits to 0b10 (bits 6-7 of byte 8, RFC 4122 §4.1.1).\n  bytes[8] = (bytes[8] & 0x3f) | 0x80;\n\n  const hex = Array.from(bytes)\n    .map((b) => b.toString(16).padStart(2, \"0\"))\n    .join(\"\");\n\n  // Format as 8-4-4-4-12 UUID string.\n  return [\n    hex.slice(0, 8),\n    hex.slice(8, 12),\n    hex.slice(12, 16),\n    hex.slice(16, 20),\n    hex.slice(20),\n  ].join(\"-\");\n}\n","/**\n * SPA (Single-Page Application) route change detection.\n *\n * Traditional multi-page websites trigger a full page reload on every\n * navigation, so the SDK's `init()` call on each new page is enough to\n * track pageviews. SPAs, however, update the URL and render new content\n * **without** reloading the page, so we need to intercept the History API.\n *\n * ## How it works\n *\n * The browser's History API exposes three ways to change the URL without\n * reloading the page:\n * - `history.pushState(...)` — navigate to a new URL (creates a history entry)\n * - `history.replaceState(...)` — update the current URL in-place (no new entry)\n * - `popstate` event — fired on browser back/forward button clicks\n *\n * None of these emit a native `load` or `navigate` event, so we intercept\n * `pushState` and `replaceState` by wrapping them, and listen for `popstate`.\n *\n * ## Deduplication\n *\n * Some frameworks call `replaceState` repeatedly to sync query params without\n * actually navigating (e.g. updating a `?page=2` filter). To avoid counting\n * these as separate pageviews, consecutive navigations to the **same URL**\n * (path + search) are deduplicated.\n *\n * ## Cleanup\n *\n * The function returns a teardown callback. Always call it before re-running\n * `TGA.init()` (e.g. in tests or framework hot-reload scenarios) to restore\n * the original `history` methods and remove event listeners.\n */\n\n/**\n * Installs intercepts for SPA route changes and calls `onNavigate` after each\n * distinct URL change.\n *\n * @param onNavigate - Callback invoked after every distinct navigation.\n *                     Typically `() => TGA.pageview()`.\n * @returns A teardown function. Call it to remove all hooks and restore the\n *          original `history.pushState` / `history.replaceState`.\n *\n * @example\n * const teardown = installSpaListeners(() => TGA.pageview());\n *\n * // When the SDK is reset or the app is unmounted:\n * teardown();\n */\nexport function installSpaListeners(onNavigate: () => void): () => void {\n  const originalPushState = history.pushState.bind(history);\n  const originalReplaceState = history.replaceState.bind(history);\n\n  // Track the URL at install time so we can deduplicate same-URL navigations.\n  let lastUrl = currentUrl();\n\n  /**\n   * Called after every `pushState`, `replaceState`, or `popstate`.\n   * Skips the callback when the URL has not actually changed.\n   */\n  function handleNavigation(): void {\n    const next = currentUrl();\n    if (next === lastUrl) return; // same URL — nothing to track\n    lastUrl = next;\n    onNavigate();\n  }\n\n  // Wrap pushState — called by frameworks on every \"navigate to new route\".\n  history.pushState = (...args: Parameters<typeof history.pushState>) => {\n    originalPushState(...args);\n    handleNavigation();\n  };\n\n  // Wrap replaceState — called on in-place URL updates (query-string changes,\n  // hash updates, etc.).\n  history.replaceState = (...args: Parameters<typeof history.replaceState>) => {\n    originalReplaceState(...args);\n    handleNavigation();\n  };\n\n  // popstate fires when the user presses the browser Back or Forward button.\n  const onPopState = (): void => handleNavigation();\n  window.addEventListener(\"popstate\", onPopState);\n\n  // Return the teardown function so callers can clean up when needed.\n  return () => {\n    history.pushState = originalPushState;\n    history.replaceState = originalReplaceState;\n    window.removeEventListener(\"popstate\", onPopState);\n  };\n}\n\n/**\n * Returns the current URL as `pathname + search`, used for deduplication.\n * The hash fragment is intentionally excluded — hash changes alone should\n * not be counted as new pageviews.\n */\nfunction currentUrl(): string {\n  return window.location.pathname + window.location.search;\n}\n","/**\n * UTM parameter extraction.\n *\n * UTM (Urchin Tracking Module) parameters are query-string values added to\n * URLs by marketing tools to identify the source, medium, and campaign that\n * drove a visitor to the page.\n *\n * The SDK reads them once on initialisation from `window.location.search` and\n * merges them into `globalProperties` via `TGA.identify()`. This way, every\n * event in the session — not just the first pageview — carries the acquisition\n * channel information.\n *\n * **Standard UTM parameters:**\n * | Parameter      | Example value  | Meaning              |\n * |----------------|----------------|----------------------|\n * | utm_source     | `\"twitter\"`    | Traffic source       |\n * | utm_medium     | `\"social\"`     | Marketing medium     |\n * | utm_campaign   | `\"launch-2024\"`| Campaign name        |\n * | utm_term       | `\"analytics\"`  | Paid search keyword  |\n * | utm_content    | `\"banner-cta\"` | Ad variant / content |\n */\n\n/** The five standard UTM query-string keys. */\nconst UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\n\n/**\n * Reads UTM parameters from the current page URL (`window.location.search`).\n *\n * Only parameters that are present **and non-empty** are included in the\n * returned object. Non-UTM query parameters are ignored.\n *\n * Returns an empty object when:\n * - No UTM parameters are present in the URL.\n * - The function is called outside a browser environment (`window` undefined).\n *\n * @returns A plain object mapping UTM key → value for each present parameter.\n *\n * @example URL with source and medium\n * // Current URL: https://example.com/?utm_source=twitter&utm_medium=social\n * extractUtmParams();\n * // => { utm_source: \"twitter\", utm_medium: \"social\" }\n *\n * @example URL with no UTM params\n * // Current URL: https://example.com/pricing\n * extractUtmParams();\n * // => {}\n */\nexport function extractUtmParams(): Record<string, string> {\n  if (typeof window === \"undefined\") return {};\n\n  const params = new URLSearchParams(window.location.search);\n  const result: Record<string, string> = {};\n\n  for (const key of UTM_KEYS) {\n    const value = params.get(key);\n    if (value !== null && value !== \"\") {\n      result[key] = value;\n    }\n  }\n\n  return result;\n}\n","/**\n * Runtime validator for {@link EventProperties}.\n *\n * The compile-time type covers most consumers, but it can be bypassed\n * (`as any`, plain JavaScript callers, dynamic data from `JSON.parse`).\n * This module is the runtime safety net that fails loudly when an\n * unsupported shape is about to be sent to the server.\n *\n * Allowed value shapes:\n * - scalar:        `string | number | boolean | null`\n * - scalar array:  `(string | number | boolean | null)[]`\n *\n * Anything else (objects, nested arrays, `undefined`, functions, symbols,\n * `NaN`, `Infinity`) throws synchronously with a message that names the\n * bad key, the position in the array, and the calling method.\n */\n\nimport type { EventProperties } from \"./types.js\";\n\nfunction isScalar(v: unknown): boolean {\n  if (v === null) return true;\n  const t = typeof v;\n  if (t === \"string\" || t === \"boolean\") return true;\n  if (t === \"number\") {\n    // JSON.stringify silently turns NaN / ±Infinity into the literal `null`,\n    // which would corrupt downstream queries. Reject up front.\n    return Number.isFinite(v as number);\n  }\n  return false;\n}\n\nfunction describe(v: unknown): string {\n  if (v === null) return \"null\";\n  if (Array.isArray(v)) return \"array\";\n  if (typeof v === \"number\" && !Number.isFinite(v)) return String(v); // NaN / Infinity\n  return typeof v;\n}\n\n/**\n * Validates a properties object before it is enqueued or sent.\n *\n * @param props  - The properties to validate. Mutates nothing.\n * @param method - The public method name calling the validator\n *                 (`\"track\"` or `\"identify\"`). Included in error\n *                 messages to make debugging painless.\n *\n * @throws {Error} If any value (or array element) is not a JSON-safe scalar.\n */\nexport function validateProperties(props: EventProperties, method: string): void {\n  for (const key of Object.keys(props)) {\n    const value = (props as Record<string, unknown>)[key];\n\n    if (isScalar(value)) continue;\n\n    if (Array.isArray(value)) {\n      for (let i = 0; i < value.length; i++) {\n        const item = value[i];\n        if (isScalar(item)) continue;\n        throw new Error(\n          `[tgram-analytics] TGA.${method}(): properties[${JSON.stringify(key)}][${i}] must be a string, number, boolean, or null — got ${describe(item)}. Arrays may only contain scalar primitives; objects, nested arrays, undefined, NaN, and Infinity are not allowed.`,\n        );\n      }\n      continue;\n    }\n\n    throw new Error(\n      `[tgram-analytics] TGA.${method}(): properties[${JSON.stringify(key)}] must be a scalar (string, number, boolean, null) or an array of scalars — got ${describe(value)}.`,\n    );\n  }\n}\n","/**\n * Core SDK client.\n *\n * `TGAClient` implements all public SDK methods. It is instantiated once as a\n * module-level singleton in `index.ts` and exported as `TGA`. You almost never\n * need to import this class directly — use the singleton instead:\n *\n * @example\n * import TGA from \"tgram-analytics\";\n *\n * TGA.init(\"proj_xxx\", { serverUrl: \"https://analytics.example.com\" });\n * TGA.track(\"purchase\", { amount: 49 });\n */\n\nimport { collectContext } from \"./context.js\";\nimport { EventQueue, type QueueConfig } from \"./queue.js\";\nimport { clearSessionId, getOrCreateSessionId } from \"./session.js\";\nimport { installSpaListeners } from \"./spa.js\";\nimport { send } from \"./transport.js\";\nimport type { EventProperties, PageviewPayload, TGAOptions, TrackPayload } from \"./types.js\";\nimport { extractUtmParams } from \"./utm.js\";\nimport { validateProperties } from \"./validate.js\";\n\nexport class TGAClient {\n  private apiKey = \"\";\n  private serverUrl = \"\";\n  private sessionId = \"\";\n  private initialized = false;\n  private optedOut = false;\n  private globalProperties: EventProperties = {};\n  private queue: EventQueue | null = null;\n  private teardownSpa: (() => void) | null = null;\n\n  // ── Lifecycle ──────────────────────────────────────────────────────────────\n\n  /**\n   * Initialises the SDK. **Must be called once** before using any other method.\n   *\n   * What `init()` does:\n   * 1. Validates the API key and `serverUrl`.\n   * 2. Generates (or restores) a session ID from `sessionStorage`.\n   * 3. Checks Do Not Track — silently disables all tracking if enabled.\n   * 4. Reads UTM parameters from the current URL and stores them as global\n   *    properties so they appear on every event in this session.\n   * 5. Sends an initial pageview and installs SPA route-change listeners\n   *    (when `autoPageview` is `true`, which is the default).\n   * 6. Installs `visibilitychange` / `pagehide` listeners to flush any\n   *    pending queue when the user leaves the page.\n   *\n   * Calling `init()` a second time logs a warning and does nothing — it is\n   * not an error. Call {@link reset} first if you need a clean slate.\n   *\n   * @param apiKey  - Your project API key. Must start with `\"proj_\"`.\n   *                  Get it from the Telegram bot with `/projects`.\n   * @param options - Configuration options. See {@link TGAOptions}.\n   *\n   * @throws {Error} If `apiKey` is missing, does not start with `\"proj_\"`, or\n   *                 if `options.serverUrl` is not provided.\n   *\n   * @example\n   * TGA.init(\"proj_abc123\", {\n   *   serverUrl: \"https://analytics.example.com\",\n   * });\n   */\n  init(apiKey: string, options: TGAOptions): void {\n    if (this.initialized) {\n      console.warn(\n        \"[tgram-analytics] TGA.init() was already called. \" +\n          \"Call TGA.reset() first if you need a fresh session.\",\n      );\n      return;\n    }\n\n    // Fail loudly on misconfiguration — these errors should be caught in dev.\n    if (!apiKey || !apiKey.startsWith(\"proj_\")) {\n      throw new Error(\n        \"[tgram-analytics] Invalid API key. \" +\n          'API keys must start with \"proj_\". ' +\n          \"Find your key in the Telegram bot with /projects.\",\n      );\n    }\n    if (!options.serverUrl) {\n      throw new Error(\n        \"[tgram-analytics] options.serverUrl is required. \" +\n          'Example: \"https://analytics.example.com\"',\n      );\n    }\n\n    this.apiKey = apiKey;\n    // Strip trailing slash so we can always safely append endpoint paths.\n    this.serverUrl = options.serverUrl.replace(/\\/+$/, \"\");\n\n    // Warn when the server URL uses plain HTTP. All analytics data (including\n    // the API key) is transmitted in the request body — use HTTPS in production.\n    if (this.serverUrl.startsWith(\"http://\")) {\n      console.warn(\n        \"[tgram-analytics] serverUrl uses http:// — all data including your API key \" +\n          \"will be sent in cleartext. Use https:// in production.\",\n      );\n    }\n    this.sessionId = getOrCreateSessionId(options.sessionId);\n    this.initialized = true;\n\n    // ── Privacy: Do Not Track ──────────────────────────────────────────────\n    // When respectDNT is true (the default) and the browser signals DNT,\n    // we mark the client as opted-out and skip all further setup.\n    const respectDNT = options.respectDNT !== false;\n    if (respectDNT && typeof navigator !== \"undefined\" && navigator.doNotTrack === \"1\") {\n      this.optedOut = true;\n      return;\n    }\n\n    // ── Batching queue ─────────────────────────────────────────────────────\n    if (options.batch) {\n      const cfg: QueueConfig =\n        typeof options.batch === \"object\"\n          ? { maxSize: options.batch.maxSize ?? 10, maxWait: options.batch.maxWait ?? 5000 }\n          : { maxSize: 10, maxWait: 5000 };\n      this.queue = new EventQueue(this.serverUrl, cfg);\n    }\n\n    // ── UTM parameters ─────────────────────────────────────────────────────\n    // Capture acquisition channel data from the landing-page URL.\n    const utms = extractUtmParams();\n    if (Object.keys(utms).length > 0) {\n      this.globalProperties = { ...this.globalProperties, ...utms };\n    }\n\n    // ── Visitor context ─────────────────────────────────────────────────────\n    // Collect device, browser, and environment information once per session.\n    if (options.collectContext !== false) {\n      const ctx = collectContext();\n      if (Object.keys(ctx).length > 0) {\n        this.globalProperties = { ...this.globalProperties, ...ctx };\n      }\n    }\n\n    // ── Auto-pageview + SPA listeners ──────────────────────────────────────\n    const autoPageview = options.autoPageview !== false;\n    if (autoPageview && typeof window !== \"undefined\") {\n      this.pageview();\n      this.teardownSpa = installSpaListeners(() => this.pageview());\n    }\n\n    // ── Page-unload flush ──────────────────────────────────────────────────\n    // Ensure queued events are sent before the browser discards the page.\n    if (typeof document !== \"undefined\") {\n      document.addEventListener(\"visibilitychange\", () => {\n        if (document.visibilityState === \"hidden\") void this.flush();\n      });\n    }\n    if (typeof window !== \"undefined\") {\n      window.addEventListener(\"pagehide\", () => void this.flush());\n    }\n  }\n\n  // ── Tracking ───────────────────────────────────────────────────────────────\n\n  /**\n   * Tracks a custom named event.\n   *\n   * The event is sent to `POST /api/v1/track` on the server.\n   * Properties from {@link identify} are automatically merged in.\n   *\n   * @param eventName  - A string identifier for the event, e.g. `\"purchase\"`,\n   *                     `\"signup\"`, `\"button_click\"`. Use snake_case for\n   *                     consistency with the server's query patterns.\n   * @param properties - Optional key-value metadata for this specific event.\n   *                     Values must be strings, numbers, booleans, `null`, or\n   *                     arrays of those scalars. Nested arrays and object\n   *                     values are rejected synchronously.\n   *\n   * @throws {Error} When `properties` contains an unsupported value shape\n   *                 (e.g. nested object, nested array, `undefined`, `NaN`).\n   *                 The error names the bad key and is intended to surface\n   *                 developer mistakes in dev — production callers should\n   *                 never see this if types are honoured.\n   *\n   * @example Track a purchase\n   * TGA.track(\"purchase\", { amount: 49, currency: \"USD\", plan: \"pro\" });\n   *\n   * @example Track a multi-select answer (array property)\n   * TGA.track(\"onboarding_completed\", {\n   *   role: \"creator\",\n   *   interest_set: [\"vertical_to_horizontal\", \"unsure\"],\n   * });\n   *\n   * @example Track a signup with no extra properties\n   * TGA.track(\"signup\");\n   */\n  track(eventName: string, properties?: EventProperties): void {\n    if (!this.guardReady(\"track\")) return;\n\n    if (properties !== undefined) validateProperties(properties, \"track\");\n\n    const payload: TrackPayload = {\n      api_key: this.apiKey,\n      event_name: eventName,\n      session_id: this.sessionId,\n      // Per-event properties override global ones (identify() values).\n      properties: { ...this.globalProperties, ...properties },\n      timestamp: new Date().toISOString(),\n    };\n\n    this.dispatch(\"/api/v1/track\", payload);\n  }\n\n  /**\n   * Tracks a pageview event.\n   *\n   * You rarely need to call this manually — when `autoPageview` is `true`\n   * (the default), pageviews are sent automatically on init and on every\n   * SPA route change.\n   *\n   * The event is sent to `POST /api/v1/pageview` on the server.\n   *\n   * @param url      - The page URL or path to record. Defaults to\n   *                   `window.location.pathname + window.location.search`.\n   * @param referrer - The referring URL. Defaults to `document.referrer`.\n   *\n   * @example Manual pageview for a custom URL\n   * TGA.pageview(\"/virtual/checkout-step-2\");\n   *\n   * @example With explicit referrer\n   * TGA.pageview(\"/pricing\", \"https://twitter.com\");\n   */\n  pageview(url?: string, referrer?: string): void {\n    if (!this.guardReady(\"pageview\")) return;\n\n    const resolvedUrl =\n      url ??\n      (typeof window !== \"undefined\" ? window.location.pathname + window.location.search : \"\");\n\n    const resolvedReferrer =\n      referrer ?? (typeof document !== \"undefined\" ? document.referrer || null : null);\n\n    const payload: PageviewPayload = {\n      api_key: this.apiKey,\n      session_id: this.sessionId,\n      url: resolvedUrl,\n      referrer: resolvedReferrer,\n      timestamp: new Date().toISOString(),\n      properties: { ...this.globalProperties },\n    };\n\n    this.dispatch(\"/api/v1/pageview\", payload);\n  }\n\n  /**\n   * Attaches persistent properties to every subsequent {@link track} call.\n   *\n   * Properties set via `identify()` are merged into the `properties` field of\n   * each event. Per-event properties (passed directly to `track()`) take\n   * precedence over identified properties when keys conflict.\n   *\n   * Use `identify()` for attributes that apply to the whole session, such as\n   * the user's subscription plan, locale, or A/B test variant.\n   *\n   * @param properties - Key-value pairs to merge into global session\n   *                     properties. Values follow the same rules as\n   *                     {@link track}: scalars (string / number / boolean /\n   *                     null) or arrays of those scalars.\n   *\n   * @throws {Error} When `properties` contains an unsupported value shape.\n   *                 The error names the bad key.\n   *\n   * @example Single A/B variant\n   * TGA.identify({ plan: \"pro\", locale: \"en-US\", ab_variant: \"B\" });\n   * TGA.track(\"purchase\"); // => properties includes plan, locale, ab_variant\n   *\n   * @example Multi-bucket experiment membership (array property)\n   * TGA.identify({ ab_variants: [\"A\", \"B\"] });\n   */\n  identify(properties: EventProperties): void {\n    validateProperties(properties, \"identify\");\n    this.globalProperties = { ...this.globalProperties, ...properties };\n  }\n\n  // ── Privacy ────────────────────────────────────────────────────────────────\n\n  /**\n   * Opts the current user in or out of analytics.\n   *\n   * - Opting **out** immediately flushes any pending queue and silently\n   *   suppresses all future `track()` and `pageview()` calls.\n   * - Opting **in** re-enables tracking for the rest of the page session.\n   *\n   * **The opt-out state is not persisted.** If you want persistence across\n   * page loads, store the preference yourself (e.g. in `localStorage`) and\n   * call `TGA.opt(\"out\")` on every page load when the preference is set.\n   *\n   * @param status - `\"in\"` to enable tracking, `\"out\"` to disable it.\n   *\n   * @example Honour a consent flag stored in localStorage\n   * if (localStorage.getItem(\"analytics_consent\") === \"false\") {\n   *   TGA.opt(\"out\");\n   * }\n   */\n  opt(status: \"in\" | \"out\"): void {\n    if (status === \"out\") {\n      void this.flush(); // drain the queue before silencing further sends\n      this.optedOut = true;\n    } else {\n      this.optedOut = false;\n    }\n  }\n\n  // ── Queue / lifecycle ──────────────────────────────────────────────────────\n\n  /**\n   * Immediately sends all buffered events.\n   *\n   * This is a no-op when batching is disabled (the default). When batching is\n   * enabled, call `flush()` before programmatic navigations or logout to ensure\n   * no events are lost.\n   *\n   * @returns A Promise that resolves once the flush is complete.\n   *\n   * @example Flush before navigating away\n   * await TGA.flush();\n   * window.location.href = \"/thank-you\";\n   */\n  async flush(): Promise<void> {\n    this.queue?.flush();\n  }\n\n  /**\n   * Starts a new session.\n   *\n   * Clears the current session ID from `sessionStorage` and generates a fresh\n   * one. Also clears all properties set via {@link identify}.\n   *\n   * Call this after a user logs out to ensure subsequent events are not\n   * attributed to the previous user's session.\n   *\n   * @example\n   * function onLogout() {\n   *   TGA.reset();\n   *   // Now tracking continues under a new anonymous session.\n   * }\n   */\n  reset(): void {\n    // Remove SPA listeners installed by the previous init() so a subsequent\n    // init() call can install them fresh without duplicating them.\n    this.teardownSpa?.();\n    this.teardownSpa = null;\n\n    this.initialized = false;\n    clearSessionId();\n    this.sessionId = getOrCreateSessionId();\n    this.globalProperties = {};\n    this.optedOut = false;\n    this.queue = null;\n  }\n\n  // ── Internal helpers ───────────────────────────────────────────────────────\n\n  /**\n   * Guards all public tracking methods.\n   *\n   * Returns `false` (and logs a warning) when called before `init()`, so that\n   * misconfiguration fails visibly in development. Returns `false` silently\n   * when the user has opted out.\n   *\n   * @param method - Name of the calling method, used in the warning message.\n   * @returns `true` when it is safe to proceed with the send.\n   */\n  private guardReady(method: string): boolean {\n    if (!this.initialized) {\n      console.warn(\n        `[tgram-analytics] TGA.${method}() was called before TGA.init(). Initialise the SDK first.`,\n      );\n      return false;\n    }\n    return !this.optedOut;\n  }\n\n  /**\n   * Routes a payload to either the batching queue or the transport layer,\n   * depending on whether batching is enabled.\n   *\n   * @param endpoint - API path relative to `serverUrl` (e.g. `\"/api/v1/track\"`).\n   * @param payload  - JSON-serialisable request body.\n   */\n  private dispatch(endpoint: string, payload: unknown): void {\n    if (this.queue) {\n      this.queue.push(endpoint, payload);\n    } else {\n      send(`${this.serverUrl}${endpoint}`, payload);\n    }\n  }\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,eAAAE,EAAA,YAAAC,IAAA,eAAAC,EAAAJ,GCwCA,SAASK,EAASC,EAAqCC,EAAgC,CACrF,GAAID,GAAQ,SAAU,OAAOA,EAAO,SAEpC,GAAI,UAAU,KAAKC,CAAE,EAAG,MAAO,UAC/B,GAAI,mBAAmB,KAAKA,CAAE,EAAG,MAAO,MACxC,GAAI,mBAAmB,KAAKA,CAAE,EAAG,MAAO,QACxC,GAAI,UAAU,KAAKA,CAAE,EAAG,MAAO,UAC/B,GAAI,QAAQ,KAAKA,CAAE,EAAG,MAAO,QAC7B,GAAI,OAAO,KAAKA,CAAE,EAAG,MAAO,UAG9B,CASA,SAASC,EAAcF,EAAqCC,EAAgC,CAC1F,GAAID,GAAQ,QAAQ,OAAQ,CAC1B,IAAMG,EAAOH,EAAO,OAAO,KACxBI,GAAM,CAACA,EAAE,MAAM,SAAS,UAAU,GAAK,CAACA,EAAE,MAAM,SAAS,KAAK,CACjE,EACA,GAAID,EAAM,OAAOA,EAAK,KACxB,CAEA,GAAI,YAAY,KAAKF,CAAE,EAAG,MAAO,UACjC,GAAI,QAAQ,KAAKA,CAAE,EAAG,MAAO,OAC7B,GAAI,gBAAgB,KAAKA,CAAE,EAAG,MAAO,QACrC,GAAI,WAAW,KAAKA,CAAE,EAAG,MAAO,SAChC,GAAI,WAAW,KAAKA,CAAE,GAAK,CAAC,SAAS,KAAKA,CAAE,EAAG,MAAO,QAGxD,CAQA,SAASI,EAAiBL,EAA6C,CACrE,GAAIA,GAAQ,OAAQ,MAAO,SAE3B,IAAMM,EAAQ,OAAO,OAAW,IAAc,OAAO,MAAQ,KACvDC,EAAW,OAAO,OAAW,KAAe,iBAAkB,OAEpE,OAAID,EAAQ,IAAY,SACpBA,EAAQ,MAAQC,EAAiB,SAC9B,SACT,CAgBO,SAASC,GAAyC,CACvD,GAAI,OAAO,OAAW,IAAa,MAAO,CAAC,EAE3C,IAAMR,EAAU,UAA8D,cACxEC,EAAK,OAAO,UAAc,KAAc,UAAU,WAAa,GAE/DQ,EAAiC,CAAC,EAElCC,EAAKX,EAASC,EAAQC,CAAE,EAC1BS,IAAID,EAAO,IAAMC,GAErB,IAAMC,EAAUT,EAAcF,EAAQC,CAAE,EACpCU,IAASF,EAAO,SAAWE,GAE3B,OAAO,UAAc,KAAe,UAAU,WAChDF,EAAO,UAAY,UAAU,UAG3B,OAAO,OAAW,MACpBA,EAAO,QAAU,GAAG,OAAO,KAAK,IAAI,OAAO,MAAM,IAGnDA,EAAO,UAAY,GAAG,OAAO,UAAU,IAAI,OAAO,WAAW,GAE7D,GAAI,CACF,IAAMG,EAAK,KAAK,eAAe,EAAE,gBAAgB,EAAE,SAC/CA,IAAIH,EAAO,UAAYG,EAC7B,MAAQ,CAER,CAEA,OAAAH,EAAO,aAAeJ,EAAiBL,CAAM,EAEtCS,CACT,CC9FO,SAASI,EAAKC,EAAaC,EAAwB,CACxD,IAAMC,EAAO,KAAK,UAAUD,CAAO,EAGnC,GACE,OAAO,SAAa,KACpB,SAAS,kBAAoB,UAC7B,OAAO,UAAc,KACrB,OAAO,UAAU,YAAe,WAChC,CAIA,IAAME,EAAO,IAAI,KAAK,CAACD,CAAI,EAAG,CAAE,KAAM,kBAAmB,CAAC,EAE1D,GADiB,UAAU,WAAWF,EAAKG,CAAI,EACjC,MAGhB,CAGA,GAAI,CACF,MAAMH,EAAK,CACT,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAE,EACA,UAAW,GACX,KAAM,MACR,CAAC,EAAE,MAAM,IAAM,CAEf,CAAC,CACH,MAAQ,CAGR,CACF,CC5BO,IAAME,EAAN,KAAiB,CAWtB,YAAYC,EAAmBC,EAAqB,CARpD,KAAQ,OAAwB,CAAC,EACjC,KAAQ,MAA8C,KAQpD,KAAK,UAAYD,EACjB,KAAK,OAASC,CAChB,CAWA,KAAKC,EAAkBC,EAAwB,CAC7C,KAAK,OAAO,KAAK,CAAE,SAAAD,EAAU,QAAAC,CAAQ,CAAC,EAElC,KAAK,OAAO,QAAU,KAAK,OAAO,QAEpC,KAAK,MAAM,EACF,KAAK,QAAU,OAExB,KAAK,MAAQ,WAAW,IAAM,KAAK,MAAM,EAAG,KAAK,OAAO,OAAO,EAEnE,CAQA,OAAc,CACR,KAAK,QAAU,OACjB,aAAa,KAAK,KAAK,EACvB,KAAK,MAAQ,MAIf,IAAMC,EAAS,KAAK,OAAO,OAAO,CAAC,EACnC,OAAW,CAAE,SAAAF,EAAU,QAAAC,CAAQ,IAAKC,EAClCC,EAAK,GAAG,KAAK,SAAS,GAAGH,CAAQ,GAAIC,CAAO,CAEhD,CACF,EC/FA,IAAMG,EAAc,UAiBb,SAASC,EAAqBC,EAA2B,CAC9D,GAAIA,EAAU,OAAOA,EAGrB,GAAI,CACF,IAAMC,EAAW,eAAe,QAAQH,CAAW,EACnD,GAAIG,EAAU,OAAOA,CACvB,MAAQ,CAGR,CAEA,IAAMC,EAAKC,EAAa,EAExB,GAAI,CACF,eAAe,QAAQL,EAAaI,CAAE,CACxC,MAAQ,CAGR,CAEA,OAAOA,CACT,CASO,SAASE,GAAuB,CACrC,GAAI,CACF,eAAe,WAAWN,CAAW,CACvC,MAAQ,CAER,CACF,CAcA,SAASK,GAAuB,CAE9B,GAAI,OAAO,OAAW,KAAe,OAAO,OAAO,YAAe,WAChE,OAAO,OAAO,WAAW,EAI3B,IAAME,EAAQ,IAAI,WAAW,EAAE,EAC/B,OAAO,gBAAgBA,CAAK,EAG5BA,EAAM,CAAC,EAAKA,EAAM,CAAC,EAAI,GAAQ,GAE/BA,EAAM,CAAC,EAAKA,EAAM,CAAC,EAAI,GAAQ,IAE/B,IAAMC,EAAM,MAAM,KAAKD,CAAK,EACzB,IAAKE,GAAMA,EAAE,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,CAAC,EAC1C,KAAK,EAAE,EAGV,MAAO,CACLD,EAAI,MAAM,EAAG,CAAC,EACdA,EAAI,MAAM,EAAG,EAAE,EACfA,EAAI,MAAM,GAAI,EAAE,EAChBA,EAAI,MAAM,GAAI,EAAE,EAChBA,EAAI,MAAM,EAAE,CACd,EAAE,KAAK,GAAG,CACZ,CC9DO,SAASE,EAAoBC,EAAoC,CACtE,IAAMC,EAAoB,QAAQ,UAAU,KAAK,OAAO,EAClDC,EAAuB,QAAQ,aAAa,KAAK,OAAO,EAG1DC,EAAUC,EAAW,EAMzB,SAASC,GAAyB,CAChC,IAAMC,EAAOF,EAAW,EACpBE,IAASH,IACbA,EAAUG,EACVN,EAAW,EACb,CAGA,QAAQ,UAAY,IAAIO,IAA+C,CACrEN,EAAkB,GAAGM,CAAI,EACzBF,EAAiB,CACnB,EAIA,QAAQ,aAAe,IAAIE,IAAkD,CAC3EL,EAAqB,GAAGK,CAAI,EAC5BF,EAAiB,CACnB,EAGA,IAAMG,EAAa,IAAYH,EAAiB,EAChD,cAAO,iBAAiB,WAAYG,CAAU,EAGvC,IAAM,CACX,QAAQ,UAAYP,EACpB,QAAQ,aAAeC,EACvB,OAAO,oBAAoB,WAAYM,CAAU,CACnD,CACF,CAOA,SAASJ,GAAqB,CAC5B,OAAO,OAAO,SAAS,SAAW,OAAO,SAAS,MACpD,CC3EA,IAAMK,EAAW,CAAC,aAAc,aAAc,eAAgB,WAAY,aAAa,EAwBhF,SAASC,GAA2C,CACzD,GAAI,OAAO,OAAW,IAAa,MAAO,CAAC,EAE3C,IAAMC,EAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM,EACnDC,EAAiC,CAAC,EAExC,QAAWC,KAAOJ,EAAU,CAC1B,IAAMK,EAAQH,EAAO,IAAIE,CAAG,EACxBC,IAAU,MAAQA,IAAU,KAC9BF,EAAOC,CAAG,EAAIC,EAElB,CAEA,OAAOF,CACT,CC1CA,SAASG,EAASC,EAAqB,CACrC,GAAIA,IAAM,KAAM,MAAO,GACvB,IAAMC,EAAI,OAAOD,EACjB,OAAIC,IAAM,UAAYA,IAAM,UAAkB,GAC1CA,IAAM,SAGD,OAAO,SAASD,CAAW,EAE7B,EACT,CAEA,SAASE,EAASF,EAAoB,CACpC,OAAIA,IAAM,KAAa,OACnB,MAAM,QAAQA,CAAC,EAAU,QACzB,OAAOA,GAAM,UAAY,CAAC,OAAO,SAASA,CAAC,EAAU,OAAOA,CAAC,EAC1D,OAAOA,CAChB,CAYO,SAASG,EAAmBC,EAAwBC,EAAsB,CAC/E,QAAWC,KAAO,OAAO,KAAKF,CAAK,EAAG,CACpC,IAAMG,EAASH,EAAkCE,CAAG,EAEpD,GAAI,CAAAP,EAASQ,CAAK,EAElB,IAAI,MAAM,QAAQA,CAAK,EAAG,CACxB,QAASC,EAAI,EAAGA,EAAID,EAAM,OAAQC,IAAK,CACrC,IAAMC,EAAOF,EAAMC,CAAC,EACpB,GAAI,CAAAT,EAASU,CAAI,EACjB,MAAM,IAAI,MACR,yBAAyBJ,CAAM,kBAAkB,KAAK,UAAUC,CAAG,CAAC,KAAKE,CAAC,2DAAsDN,EAASO,CAAI,CAAC,oHAChJ,CACF,CACA,QACF,CAEA,MAAM,IAAI,MACR,yBAAyBJ,CAAM,kBAAkB,KAAK,UAAUC,CAAG,CAAC,wFAAmFJ,EAASK,CAAK,CAAC,GACxK,EACF,CACF,CC9CO,IAAMG,EAAN,KAAgB,CAAhB,cACL,KAAQ,OAAS,GACjB,KAAQ,UAAY,GACpB,KAAQ,UAAY,GACpB,KAAQ,YAAc,GACtB,KAAQ,SAAW,GACnB,KAAQ,iBAAoC,CAAC,EAC7C,KAAQ,MAA2B,KACnC,KAAQ,YAAmC,KAiC3C,KAAKC,EAAgBC,EAA2B,CAC9C,GAAI,KAAK,YAAa,CACpB,QAAQ,KACN,sGAEF,EACA,MACF,CAGA,GAAI,CAACD,GAAU,CAACA,EAAO,WAAW,OAAO,EACvC,MAAM,IAAI,MACR,wHAGF,EAEF,GAAI,CAACC,EAAQ,UACX,MAAM,IAAI,MACR,2FAEF,EAsBF,GAnBA,KAAK,OAASD,EAEd,KAAK,UAAYC,EAAQ,UAAU,QAAQ,OAAQ,EAAE,EAIjD,KAAK,UAAU,WAAW,SAAS,GACrC,QAAQ,KACN,wIAEF,EAEF,KAAK,UAAYC,EAAqBD,EAAQ,SAAS,EACvD,KAAK,YAAc,GAKAA,EAAQ,aAAe,IACxB,OAAO,UAAc,KAAe,UAAU,aAAe,IAAK,CAClF,KAAK,SAAW,GAChB,MACF,CAGA,GAAIA,EAAQ,MAAO,CACjB,IAAME,EACJ,OAAOF,EAAQ,OAAU,SACrB,CAAE,QAASA,EAAQ,MAAM,SAAW,GAAI,QAASA,EAAQ,MAAM,SAAW,GAAK,EAC/E,CAAE,QAAS,GAAI,QAAS,GAAK,EACnC,KAAK,MAAQ,IAAIG,EAAW,KAAK,UAAWD,CAAG,CACjD,CAIA,IAAME,EAAOC,EAAiB,EAO9B,GANI,OAAO,KAAKD,CAAI,EAAE,OAAS,IAC7B,KAAK,iBAAmB,CAAE,GAAG,KAAK,iBAAkB,GAAGA,CAAK,GAK1DJ,EAAQ,iBAAmB,GAAO,CACpC,IAAMM,EAAMC,EAAe,EACvB,OAAO,KAAKD,CAAG,EAAE,OAAS,IAC5B,KAAK,iBAAmB,CAAE,GAAG,KAAK,iBAAkB,GAAGA,CAAI,EAE/D,CAGqBN,EAAQ,eAAiB,IAC1B,OAAO,OAAW,MACpC,KAAK,SAAS,EACd,KAAK,YAAcQ,EAAoB,IAAM,KAAK,SAAS,CAAC,GAK1D,OAAO,SAAa,KACtB,SAAS,iBAAiB,mBAAoB,IAAM,CAC9C,SAAS,kBAAoB,UAAe,KAAK,MAAM,CAC7D,CAAC,EAEC,OAAO,OAAW,KACpB,OAAO,iBAAiB,WAAY,IAAG,CAAQ,KAAK,MAAM,EAAC,CAE/D,CAoCA,MAAMC,EAAmBC,EAAoC,CAC3D,GAAI,CAAC,KAAK,WAAW,OAAO,EAAG,OAE3BA,IAAe,QAAWC,EAAmBD,EAAY,OAAO,EAEpE,IAAME,EAAwB,CAC5B,QAAS,KAAK,OACd,WAAYH,EACZ,WAAY,KAAK,UAEjB,WAAY,CAAE,GAAG,KAAK,iBAAkB,GAAGC,CAAW,EACtD,UAAW,IAAI,KAAK,EAAE,YAAY,CACpC,EAEA,KAAK,SAAS,gBAAiBE,CAAO,CACxC,CAqBA,SAASC,EAAcC,EAAyB,CAC9C,GAAI,CAAC,KAAK,WAAW,UAAU,EAAG,OAElC,IAAMC,EACJF,IACC,OAAO,OAAW,IAAc,OAAO,SAAS,SAAW,OAAO,SAAS,OAAS,IAEjFG,EACJF,IAAa,OAAO,SAAa,KAAc,SAAS,UAAY,MAEhEF,EAA2B,CAC/B,QAAS,KAAK,OACd,WAAY,KAAK,UACjB,IAAKG,EACL,SAAUC,EACV,UAAW,IAAI,KAAK,EAAE,YAAY,EAClC,WAAY,CAAE,GAAG,KAAK,gBAAiB,CACzC,EAEA,KAAK,SAAS,mBAAoBJ,CAAO,CAC3C,CA2BA,SAASF,EAAmC,CAC1CC,EAAmBD,EAAY,UAAU,EACzC,KAAK,iBAAmB,CAAE,GAAG,KAAK,iBAAkB,GAAGA,CAAW,CACpE,CAsBA,IAAIO,EAA4B,CAC1BA,IAAW,OACR,KAAK,MAAM,EAChB,KAAK,SAAW,IAEhB,KAAK,SAAW,EAEpB,CAiBA,MAAM,OAAuB,CAC3B,KAAK,OAAO,MAAM,CACpB,CAiBA,OAAc,CAGZ,KAAK,cAAc,EACnB,KAAK,YAAc,KAEnB,KAAK,YAAc,GACnBC,EAAe,EACf,KAAK,UAAYjB,EAAqB,EACtC,KAAK,iBAAmB,CAAC,EACzB,KAAK,SAAW,GAChB,KAAK,MAAQ,IACf,CAcQ,WAAWkB,EAAyB,CAC1C,OAAK,KAAK,YAMH,CAAC,KAAK,UALX,QAAQ,KACN,yBAAyBA,CAAM,4DACjC,EACO,GAGX,CASQ,SAASC,EAAkBR,EAAwB,CACrD,KAAK,MACP,KAAK,MAAM,KAAKQ,EAAUR,CAAO,EAEjCS,EAAK,GAAG,KAAK,SAAS,GAAGD,CAAQ,GAAIR,CAAO,CAEhD,CACF,ER9UA,IAAMU,EAAM,IAAIC,EAETC,EAAQF","names":["index_exports","__export","TGAClient","index_default","__toCommonJS","detectOS","uaData","ua","detectBrowser","real","b","detectDeviceType","width","hasTouch","collectContext","result","os","browser","tz","send","url","payload","body","blob","EventQueue","serverUrl","config","endpoint","payload","events","send","STORAGE_KEY","getOrCreateSessionId","customId","existing","id","generateUUID","clearSessionId","bytes","hex","b","installSpaListeners","onNavigate","originalPushState","originalReplaceState","lastUrl","currentUrl","handleNavigation","next","args","onPopState","UTM_KEYS","extractUtmParams","params","result","key","value","isScalar","v","t","describe","validateProperties","props","method","key","value","i","item","TGAClient","apiKey","options","getOrCreateSessionId","cfg","EventQueue","utms","extractUtmParams","ctx","collectContext","installSpaListeners","eventName","properties","validateProperties","payload","url","referrer","resolvedUrl","resolvedReferrer","status","clearSessionId","method","endpoint","send","TGA","TGAClient","index_default"]}