/** * VTilt Types * * Type definitions for the VTilt tracking SDK. * Following PostHog's patterns where applicable. */ import type { PersonProfilesMode } from "./constants"; import type { BubbleConfig } from "./utils/globals"; export interface VTiltConfig { /** Project identifier (required) */ token: string; /** * API host for all SDK requests (events, config, recordings, chat). * Set this to your own domain when using a reverse proxy. * If not set, SDK uses relative URLs (same-origin). */ api_host?: string; /** UI host for dashboard links */ ui_host?: string | null; /** * CDN host for loading extension scripts (recorder.js, etc.) * If set, scripts load from {script_host}/{script}.js (no /dist/) * If not set, falls back to {api_host}/dist/{script}.js */ script_host?: string; /** Instance name (for multiple instances) */ name?: string; /** Domain to track (auto-detected if not provided) - used in event properties */ domain?: string; /** Storage method for session data */ storage?: PersistenceMethod; /** Persistence method for user data */ persistence?: PersistenceMethod; /** Persistence name prefix */ persistence_name?: string; /** * Enable cross-subdomain cookies. * When true, cookies are shared across subdomains (e.g., app.example.com and www.example.com). * Auto-detects false for platforms like herokuapp.com, vercel.app, netlify.app. * @default true (except for excluded platforms) */ cross_subdomain_cookie?: boolean; /** * Person profiles mode: * - 'always': Always create person profiles (default) * - 'identified_only': Only create when user is identified * - 'never': Never create person profiles */ person_profiles?: PersonProfilesMode; /** * Enable autocapture for automatic DOM event tracking. * Can be a boolean or an object with detailed configuration. * When true, captures clicks, form submissions, and input changes. */ autocapture?: boolean | AutocaptureOptions; /** * Enable web vitals tracking. * Can be a boolean or an object with detailed configuration. */ capture_performance?: boolean | CapturePerformanceConfig; /** Enable page view tracking */ capture_pageview?: boolean | "auto"; /** Enable page leave tracking */ capture_pageleave?: boolean | "if_capture_pageview"; /** Enable rage click detection (rapid clicks in same area) */ rageclick?: boolean; /** Disable compression */ disable_compression?: boolean; /** Whether to stringify payload before sending */ stringifyPayload?: boolean; /** Properties to exclude from events */ property_denylist?: string[]; /** Mask text in autocapture */ mask_all_text?: boolean; /** Mask all element attributes */ mask_all_element_attributes?: boolean; /** Respect Do Not Track browser setting */ respect_dnt?: boolean; /** * When false (default), events are not sent when the browser looks like a bot * (known crawler user agents, `navigator.webdriver`, or blocked CH brands). * Set to true to disable bot filtering (PostHog: `opt_out_useragent_filter`). */ opt_out_useragent_filter?: boolean; /** * Extra user-agent substrings to treat as bots (case-insensitive), merged with * the built-in list aligned with PostHog (PostHog: `custom_blocked_useragents`). */ custom_blocked_useragents?: string[]; /** Opt users out by default */ opt_out_capturing_by_default?: boolean; /** * When true, non-essential events are blocked until setConsent() is called. * Essential events ($identify, $alias, $set) are always allowed. * Use with vt.setConsent({ analytics: true, marketing: true, advertising: true }). */ require_consent?: boolean; /** Session recording configuration. Set `{ enabled: false }` to disable. */ session_recording?: SessionRecordingOptions; /** Chat widget configuration. Set `{ enabled: false }` to disable. */ chat?: ChatWidgetConfig; /** Outbound messaging (banners/posts). Set `{ enabled: true }` to opt in. */ outbound?: OutboundWidgetConfig; /** * Google Tag Gateway feature config (merged `ga4_gtag` + `google_ads_gtag` * rows). Property name is the stable SDK config key; populated from `/decide` * or set for SSR/tests. Enablement is driven by the presence of tag IDs from * the dashboard — disable it by turning the gtag destination off there. */ google_tag?: GoogleTagClientConfig; /** * Console log level for SDK output prefixed with `[vTilt]`. * * Levels (each includes the levels above it): * - 'none' — silence everything (escape hatch for shared kiosks etc.) * - 'error' — SDK errors only * - 'warn' — errors + warnings (default) * - 'info' — + lifecycle events (init, autocapture started, replay started) * - 'debug' — + per-event trace (every captured event, autocapture skips, requests) * * Default is 'warn' so SDK errors and warnings always surface without opt-in * (matches Amplitude/PostHog/Sentry). * * NOT to be confused with the per-event `$debug` flag — that is a separate * marker on the event payload itself, controlled by `debug: true` (below) * or the `?vtilt_debug=1` URL parameter, and consumed by the Debug View in * the dashboard. See the public docs for details. * * Setting `log_level` (or `debug: true`) at init time pins the level — it * cannot be overridden by remote config / Default Configuration. Leave it * unset to let project admins control verbosity from the dashboard. */ log_level?: "none" | "error" | "warn" | "info" | "debug"; /** * PostHog-style shorthand: when true, equivalent to `log_level: 'debug'`. * Ignored if `log_level` is also set. * * Setting this to true ALSO marks every captured event with `$debug: true` * so the dashboard's Debug View highlights it. The `?vtilt_debug=1` URL * parameter sets the event flag without raising the console log level. */ debug?: boolean; /** Global attributes added to all events */ globalAttributes?: Record; /** Bootstrap data for initialization (server-side rendering) */ bootstrap?: { distinctID?: string; isIdentifiedID?: boolean; featureFlags?: Record; /** Remote config from /decide endpoint - use this for SSR to avoid async fetch delay */ remoteConfig?: RemoteConfig; }; /** Before send hook for modifying events */ before_send?: (event: CaptureResult) => CaptureResult | null; /** Loaded callback */ loaded?: (vtilt: any) => void; /** @internal Set by RemoteConfigManager after a fresh /decide response (or fetch failure). */ __remote_config_loaded?: boolean; } export interface EventPayload { [key: string]: any; } export interface CaptureResult { uuid: string; event: string; properties: Properties; $set?: Properties; $set_once?: Properties; timestamp?: string; } export interface CaptureOptions { /** Override timestamp */ timestamp?: Date; /** Properties to $set on person */ $set?: Properties; /** Properties to $set_once on person */ $set_once?: Properties; /** Send immediately (skip batching) */ send_instantly?: boolean; } export interface TrackingEvent { timestamp: string; event: string; distinct_id: string; anonymous_id?: string; payload: EventPayload; } export type Property = string | number | boolean | null | undefined | Date | any[] | Record; export interface Properties { [key: string]: Property; } export interface PropertyOperations { $set?: Properties; $set_once?: Properties; $unset?: string[]; } export interface SessionData { value: string; expiry: number; } /** * Persistence method for user/session data * Following PostHog's approach: * - 'localStorage+cookie': Stores limited data in cookies, rest in localStorage (default) * - 'cookie': Stores all data in cookies * - 'localStorage': Stores all data in localStorage * - 'sessionStorage': Stores all data in sessionStorage * - 'memory': Stores all data in memory only (no persistence) */ export type PersistenceMethod = "localStorage+cookie" | "cookie" | "localStorage" | "sessionStorage" | "memory"; /** User identity state */ export interface UserIdentity { /** Current distinct ID (null if anonymous) */ distinct_id: string | null; /** Anonymous ID (always present) */ anonymous_id: string; /** Device ID (persists across sessions) */ device_id: string; /** User properties */ properties: Properties; /** Identity state */ user_state: "anonymous" | "identified"; } export interface UserProperties { [key: string]: any; } /** Batched identity state change — applied atomically with a single save. */ export interface IdentityUpdate { distinct_id?: string; user_state?: "anonymous" | "identified"; device_id?: string; properties_set?: Properties; properties_set_once?: Properties; } /** Supported Web Vitals metrics */ export type SupportedWebVitalsMetric = "LCP" | "CLS" | "FCP" | "INP" | "TTFB"; /** All supported Web Vitals metrics */ export declare const ALL_WEB_VITALS_METRICS: SupportedWebVitalsMetric[]; /** Default Web Vitals metrics (matches PostHog defaults) */ export declare const DEFAULT_WEB_VITALS_METRICS: SupportedWebVitalsMetric[]; /** * Web Vitals capture configuration */ export interface CapturePerformanceConfig { /** Enable or disable web vitals capture */ web_vitals?: boolean; /** Which metrics to capture (default: LCP, CLS, FCP, INP) */ web_vitals_allowed_metrics?: SupportedWebVitalsMetric[]; /** Delay before flushing metrics in ms (default: 5000) */ web_vitals_delayed_flush_ms?: number; /** * Maximum allowed metric value in ms (default: 900000 = 15 minutes) * Values above this are considered anomalies and ignored. * Set to 0 to disable this check. */ __web_vitals_max_value?: number; } export interface WebVitalMetric { name: string; value: number; delta: number; rating: "good" | "needs-improvement" | "poor"; id: string; navigationType: string; /** Timestamp when the metric was captured (added internally) */ timestamp?: number; /** Attribution data from web-vitals library */ attribution?: Record; } export interface GeolocationData { country?: string; locale?: string; } export interface GroupsConfig { [groupType: string]: string; } export interface FeatureFlagsConfig { [flagKey: string]: boolean | string; } export type SessionIdChangedCallback = (newSessionId: string, previousSessionId: string | null, changeInfo: { reason: "timeout" | "new_session" | "reset"; }) => void; export interface RequestOptions { method?: "POST" | "GET"; headers?: Record; timeout?: number; retry?: boolean; } /** * Autocapture configuration options * Controls automatic DOM event tracking (clicks, form submissions, input changes) */ export interface AutocaptureOptions { /** * Enable autocapture (default: true when autocapture config is present) */ enabled?: boolean; /** * URL patterns to allow autocapture on (default: all URLs) * Supports strings (exact match) and RegExp patterns */ url_allowlist?: (string | RegExp)[]; /** * URL patterns to exclude from autocapture * Supports strings (exact match) and RegExp patterns */ url_ignorelist?: (string | RegExp)[]; /** * DOM events to capture (default: ['click', 'change', 'submit']) */ dom_event_allowlist?: ("click" | "change" | "submit")[]; /** * Element tags to capture (default: ['a', 'button', 'form', 'input', 'select', 'textarea', 'label']) */ element_allowlist?: string[]; /** * CSS selectors to allow for capture (elements matching these are always captured) * Example: ['[data-track]', '.track-click'] */ css_selector_allowlist?: string[]; /** * CSS selectors to exclude from autocapture. * If any element in the tree matches, the event is NOT captured. * * By default, common cookie consent banner patterns are excluded: * - Elements with 'cookie', 'consent', 'gdpr', 'privacy' in id/class * - Known consent management platforms (CookieBot, OneTrust, Iubenda, etc.) * * Set to empty array [] to disable default filtering. * * Example: ['#my-custom-banner', '[data-no-track]'] */ css_selector_ignorelist?: string[]; /** * Element attributes to exclude from capture * Example: ['data-secret', 'aria-label'] */ element_attribute_ignorelist?: string[]; /** * Capture text content from copy/cut events (default: false) */ capture_copied_text?: boolean; /** * Mask all text content in captured elements (use placeholder). * Overrides VTiltConfig.mask_all_text for autocapture specifically. */ mask_all_text?: boolean; /** * Mask all element attributes (only safe attributes or none). * Overrides VTiltConfig.mask_all_element_attributes for autocapture specifically. */ mask_all_element_attributes?: boolean; /** * Capture form field values on change events (default: false) * * When enabled, captures $el_value and $selected_text properties: * - $el_value: The programmatic value (input.value, option.value, checkbox checked state) * - $selected_text: Human-readable selection (option text, checkbox label) * * Protected by multiple privacy layers: * - Password and hidden inputs are never captured * - Fields with sensitive names (cc, pass, ssn, etc.) are skipped * - Credit card and SSN patterns in values are filtered out * - Elements with vt-sensitive or vt-no-capture classes are excluded * * Use case: Tracking search keywords, filter selections, etc. * * @default false */ capture_element_values?: boolean; /** * Scroll depth autocapture — two independently opt-in modes (both default off). */ scroll_depth?: { milestones?: boolean | { thresholds?: number[]; }; pageleave?: boolean; scroll_root_selector?: string | string[]; }; } /** Mask options for input elements in session recording */ export interface SessionRecordingMaskInputOptions { color?: boolean; date?: boolean; "datetime-local"?: boolean; email?: boolean; month?: boolean; number?: boolean; range?: boolean; search?: boolean; tel?: boolean; text?: boolean; time?: boolean; url?: boolean; week?: boolean; textarea?: boolean; select?: boolean; password?: boolean; } /** Session recording configuration */ export interface SessionRecordingOptions { /** Enable session recording */ enabled?: boolean; /** Sample rate (0-1, where 1 = 100%) */ sampleRate?: number; /** Minimum session duration in ms before sending */ minimumDurationMs?: number; /** Session idle threshold in ms (default: 5 minutes) */ sessionIdleThresholdMs?: number; /** Full snapshot interval in ms (default: 5 minutes) */ fullSnapshotIntervalMs?: number; /** Enable console log capture */ captureConsole?: boolean; /** Enable network request capture */ captureNetwork?: boolean; /** Canvas recording settings */ captureCanvas?: { recordCanvas?: boolean; canvasFps?: number; canvasQuality?: number; }; /** Block class for elements to hide (default: 'vt-no-capture') */ blockClass?: string; /** Block selector for elements to hide */ blockSelector?: string; /** * When `true`, skip built-in `blockSelector` entries for common invisible * template nodes (screen-reader-only, Webflow CMS placeholders, etc.). */ skipDefaultInvisibleBlocking?: boolean; /** Ignore class for input masking (default: 'vt-ignore-input') */ ignoreClass?: string; /** Mask text class (default: 'vt-mask') */ maskTextClass?: string; /** Mask text selector */ maskTextSelector?: string; /** Mask all inputs (default: true) */ maskAllInputs?: boolean; /** Mask input options */ maskInputOptions?: SessionRecordingMaskInputOptions; /** Masking configuration */ masking?: { maskAllInputs?: boolean; maskTextSelector?: string; blockSelector?: string; }; /** Record headers in network requests */ recordHeaders?: boolean; /** Record body in network requests */ recordBody?: boolean; /** Gzip-compress the request body before sending (default: true) */ compressEvents?: boolean; /** Internal: Mutation throttler refill rate */ __mutationThrottlerRefillRate?: number; /** Internal: Mutation throttler bucket size */ __mutationThrottlerBucketSize?: number; } /** Outbound messaging (banners/posts/carousels) widget configuration. */ export interface OutboundWidgetConfig { /** Opt-in for v1 — the banner only renders when explicitly enabled. */ enabled?: boolean; } /** Chat widget configuration */ export interface ChatWidgetConfig { /** Enable/disable chat widget (default: auto from dashboard) */ enabled?: boolean; /** Auto-fetch settings from dashboard (default: true) */ autoConfig?: boolean; /** Widget position (default: 'bottom-right') */ position?: "bottom-right" | "bottom-left"; /** Widget header/greeting message */ greeting?: string; /** Widget primary color */ color?: string; /** Start in AI mode (default: true) */ aiMode?: boolean; /** AI greeting message (first message from AI) */ aiGreeting?: string; /** Preload widget script on idle vs on-demand */ preload?: boolean; /** Offline message shown when business is unavailable */ offlineMessage?: string; /** Collect email when offline */ collectEmailOffline?: boolean; /** Bubble appearance and behavior */ bubble?: BubbleConfig; } export type GoogleConsentValue = "follow_advertising" | "follow_analytics" | "granted" | "denied"; export interface GoogleConsentOverride { ad_storage?: GoogleConsentValue; ad_user_data?: GoogleConsentValue; ad_personalization?: GoogleConsentValue; analytics_storage?: GoogleConsentValue; } export interface GoogleAdsConversionMapping { event_name: string; send_to: string; value_param_path?: string; currency_param_path?: string; default_currency?: string; send_transaction_id?: boolean; transaction_id_param_path?: string; } /** * How gtag.js is loaded — mirrors the server-side `proxy_mode` destination * setting: * * - `proxied` (default) — route through `api_host/gt/*`. `api_host` is the * SDK-level proxy setting; self-hosted customers point it at their own * domain (which must implement the `/gt/*` spec). * - `direct` — no proxy; gtag.js loads straight from * `https://www.googletagmanager.com/gtag/js`. */ export type GoogleTagProxyMode = "proxied" | "direct"; /** * Configuration for the Google Tag Gateway SDK feature. Ships under the * `googleTag` key of /decide when at least one `ga4_gtag` or `google_ads_gtag` destination is * enabled for the project. */ export interface GoogleTagClientConfig { destinationId: string; /** * Proxy mode. When absent the SDK falls back to `proxied` so older * `/decide` responses and the zero-config default remain compatible. */ proxyMode?: GoogleTagProxyMode; tagIds: string[]; conversions: GoogleAdsConversionMapping[]; enhancedConversions: boolean; conversionLinker: boolean; linkerDomains: string[]; capturePageview: boolean; debugMode: boolean; consentOverride?: GoogleConsentOverride; /** * Master switch for forwarding every `vt.capture()` event to GA4 via * `gtag('event', name, params)`. Defaults to `true` when absent. When * `false`, only events that match an explicit row in `eventMappings` — or * an Ads `conversions` mapping — fire; everything else is left to the * Google tag's own behavior (page_view, enhanced measurement, etc.). */ autoForward?: boolean; /** * Whether to fire the direct Ads conversion beacon * (`gtag('event', 'conversion', {send_to: 'AW-.../...'})`) for rows in * `conversions`. Defaults to `true` when absent. Turn off when GA4 is * linked to Google Ads in the Ads account — in that mode the GA4 event * (marked as a conversion in GA4) propagates to Ads automatically and * the direct beacon would duplicate it. Has no effect when `conversions` * is empty. */ sendAdsConversions?: boolean; /** * Admin-configured event renames and param remappings. Mirrors the * generic `event_mappings` configured on the destination. Applied after * the filter check and before firing `gtag('event', ...)`. */ eventMappings?: GoogleTagEventMapping[]; /** * Admin-configured include/exclude lists. Mirrors the generic * `event_filter` on the destination. Evaluated before any mapping. */ eventFilter?: { include?: string[]; exclude?: string[]; }; } /** * Rename an event and optionally remap payload paths to gtag param paths. * Structurally identical to the server-side `EventMapping` type but lives * in the browser package to avoid cross-package imports. Source paths use * dotted notation into the captured payload (e.g. `order.total`); dest * paths use dotted notation into the outgoing gtag `params` object. */ export interface GoogleTagEventMapping { source: string; destination: string; param_mappings?: GoogleTagParamMapping[]; } export interface GoogleTagParamMapping { source: string; destination: string; } export interface RemoteConfig { sessionRecording?: { enabled?: boolean; sampleRate?: number; minimumDurationMs?: number; /** Full DOM snapshot interval in ms (default 300000 = 5 min). Higher = less data. */ fullSnapshotIntervalMs?: number; maskAllInputs?: boolean; maskAllText?: boolean; captureConsole?: boolean; captureCanvas?: { recordCanvas?: boolean; canvasFps?: number; canvasQuality?: number; }; }; chat?: { enabled?: boolean; widgetPosition?: "bottom-right" | "bottom-left"; widgetColor?: string; bubbleDraggable?: boolean; bubbleVisible?: boolean; }; chatTracking?: { trackUserMessages?: boolean; trackAgentMessages?: boolean; }; analytics?: { capturePageview?: boolean; capturePageleave?: boolean; capturePerformance?: boolean; autocapture?: boolean; scrollDepthMilestones?: boolean; scrollDepthPageleave?: boolean; }; privacy?: { respectDnt?: boolean; requireConsent?: boolean; ipAnonymization?: boolean; }; /** * Diagnostics — runtime-tunable console verbosity. Applied by the SDK only * when the integrator did NOT pass `log_level` / `debug` to `vt.init()`. * Set from the dashboard's Default Configuration → Diagnostics tab. */ diagnostics?: { defaultLogLevel?: "none" | "error" | "warn" | "info" | "debug"; }; featureFlags?: FeatureFlagsConfig; /** Whether to send elements as chain string (PostHog compatibility) */ elementsChainAsString?: boolean; /** Server-side autocapture opt-out */ autocapture_opt_out?: boolean; /** * Google Tag Gateway config (absent when no client Google gtag destination is * enabled for the project). Drives the gtag.js proxy loader, Consent Mode * v2 bridge, and event → conversion mappings. */ googleTag?: GoogleTagClientConfig; /** * Outbound surfaces (banner). `enabled` is derived server-side from whether * the project has an active message, so operators publish from the dashboard * instead of editing `vt.init`. Off means there is nothing to show, so the * lazy `outbound.js` bundle is never fetched. */ outbound?: { enabled?: boolean; }; }