/** * Datalyr Web SDK * Modern attribution tracking for web applications */ import type { DatalyrConfig, EventProperties, UserTraits, PageProperties, SessionData, Attribution, TouchPoint, ConsentConfig, NetworkStatus, ErrorInfo } from './types'; export * from './types'; declare class Datalyr { private config; private identity; private session; private attribution; private queue; private fingerprint; private cookies; private container?; private pendingCheckoutChampPixels; private autoIdentify?; private explicitConfigKeys; private superProperties; private userProperties; private optedOut; private consent; private initialized; private errors; private MAX_ERRORS; private originalPushState?; private originalReplaceState?; private popstateHandler?; private hashchangeHandler?; private unloadHandler?; private visibilityHandler?; private shopifyConsentHandler?; private shopifyConsentUnresolvable; private outboundDisposer?; private stripeLinksDisposer?; private stripeSessionWatcher?; private inAppHandoffTimer; private inAppHandoffWrite; private inAppHandoffReported; private lastSpaPath; private initialPageViewReady; private initialPageViewSent; private initializationPromise; constructor(); /** * Return the workspace configured for this instance, or null before init(). * * CDN bootstrap uses this public accessor instead of reaching into the * private runtime config when deciding whether an existing global belongs * to the requested workspace. */ getWorkspaceId(): string | null; /** * Initialize the SDK */ init(config: DatalyrConfig): void; /** * Complete async initialization (encryption, user properties, container, page view) * * FIXED (ISSUE-01): Separated async initialization to prevent race conditions * Encryption must complete before first events are tracked */ private initializeAsync; /** * Wait for async initialization to complete * FIXED (ISSUE-01): Public method to await full initialization */ ready(): Promise; /** * Track an event */ track(eventName: string, properties?: EventProperties): void; /** * Track an app download click and redirect to the app store. * Fires a $app_download_click event with full attribution data, * then redirects the user to the appropriate store URL. * For Android, encodes attribution params into the Play Store referrer * so the mobile SDK can retrieve them deterministically after install. */ trackAppDownloadClick(options: { targetPlatform: 'ios' | 'android'; appStoreUrl: string; }): Promise; /** * Identify a user */ /** * Stable fingerprint of an identity for redundant-emit suppression (WEB-20). * * Keys are sorted and each value is JSON-serialized, so `{a,b}` and `{b,a}` * collapse to one identity while a genuinely changed nested trait does not. * (The mobile SDKs use `String(value)`, which flattens every object to * `[object Object]` and can swallow a real change; web traits are plain JSON, * so it can afford to be exact.) * * Hashed rather than stored raw so the key does not become another copy of the * user's traits at rest. Being honest about the strength: a 32-bit * non-cryptographic digest sitting beside a readable `dl_dl_anonymous_id` is * NOT protection against a determined reader — it defeats casual inspection * and bulk scraping, nothing more. The PII that matters is handled properly * (encrypted `dl_user_id_pii`, encrypted `dl_user_traits`). 32 bits also means * a ~1-in-4.3e9 chance per identity that a genuine change is suppressed; * acceptable for change detection, which is all this is. */ private identityFingerprint; identify(userId: string, traits?: UserTraits): void; /** * WEB-27: queue health, including events the SDK gave up on. * * `getStats()` existed on EventQueue but was unreachable — `queue` is private * and nothing exposed it, so the observability fix could not actually be * observed. A non-zero `droppedEvents` means data was lost. */ getQueueStats(): { queued: number; offline: number; droppedEvents: number; lastDropStatus: number | null; backoffUntil: number; } | null; /** * Track a page view */ page(properties?: PageProperties): void; /** * Track a screen view (for SPAs). * Fires a `pageview` event with a `screen` property, consistent with * the React Native and iOS SDKs. */ screen(screenName: string, properties?: Record): void; /** * Associate user with a group/account */ group(groupId: string, traits?: Record): void; /** * Alias one ID to another */ alias(userId: string, previousId?: string): void; /** * Reset the current user */ reset(): void; /** * Get the current anonymous ID */ getAnonymousId(): string; /** * Get the current visitor ID (alias for getAnonymousId). * Matches the `visitor_id` terminology used in the dashboard, schema, * and integration guides — pass this to Stripe's `client_reference_id` * or webhook metadata for first-party attribution. */ getVisitorId(): string; /** * Returns a metadata bundle ready to pass to Stripe so the webhook can * deterministically link the payment back to this browser session (90%+ * attribution match vs. 70-85% email-only). * * Usage (Stripe Checkout Session): * stripe.checkout.sessions.create({ * ...datalyr.getStripeMetadata(), * line_items: [...], * }) * * Usage (PaymentIntent / Subscription / Customer): * stripe.paymentIntents.create({ * amount, currency, * metadata: datalyr.getStripeMetadata().metadata, * }) * * The server-side Datalyr webhook reads `client_reference_id` on Checkout * and `metadata.visitor_id` on every other Stripe object. */ getStripeMetadata(): { client_reference_id: string; metadata: { visitor_id: string; }; }; /** * Returns a metadata object ready to attach to a Whop checkout configuration. * Whop inherits checkout-configuration metadata onto payments and memberships * (https://docs.whop.com/api-reference/checkout-configurations/), so Datalyr * can read `metadata.visitor_id` off every resulting webhook event. * * Usage: * whop.checkoutConfigurations.create({ * plan_id, * metadata: datalyr.getWhopCheckoutMetadata(), * }) */ getWhopCheckoutMetadata(): { visitor_id: string; }; /** * Get the current user ID */ getUserId(): string | null; /** * Get the distinct ID */ getDistinctId(): string; /** * Get the current session ID */ getSessionId(): string; /** * Start a new session manually */ startNewSession(): string; /** * Get session data */ getSessionData(): SessionData | null; /** * Get current attribution data */ getAttribution(): Attribution; /** * Get customer journey */ getJourney(): TouchPoint[]; /** * Set attribution manually */ setAttribution(attribution: Partial): void; /** * Opt out of tracking */ /** * X-1 (consent): stop the Stripe Payment Link + CheckoutChamp outbound-link decorators from * stamping `client_reference_id`/`prefilled_email` onto hrefs once consent/marketing is * withdrawn. Their MutationObservers otherwise keep rewriting links after a CMP or Shopify * decline — and the server still reads the stamped `client_reference_id` — so attribution * continues post-withdrawal. Idempotent; decorators re-init on the next load if consent returns * (same convention as pixels/auto-identify). */ private disposeMarketingLinkDecorators; optOut(): void; /** * Opt in to tracking */ optIn(): void; /** * Check if user has opted out */ isOptedOut(): boolean; /** * Set consent preferences */ setConsent(consent: ConsentConfig): void; /** * Manually flush the event queue */ flush(): Promise; /** * Set super properties */ setSuperProperties(properties: Record): void; /** * Unset a super property */ unsetSuperProperty(propertyName: string): void; /** * Get super properties */ getSuperProperties(): Record; /** * Stamp Datalyr attribution signals into the Shopify cart as cart attributes. * Cart attributes become `order.note_attributes` with the same names, which the * server-side order webhook reads to recover the browser visitor_id + Meta click * signals (_fbc/_fbp/fbclid) — enabling accurate Meta CAPI attribution for orders. * * Opt-in (config.shopifyCartAttributes), best-effort, runs once during init. * Merges via Shopify's /cart/update.js (does not clobber other cart attributes). */ private syncShopifyCartAttributes; /** Strip the retired unsigned Checkout Champ bridge parameters. */ private restoreFromURL; /** * Checkout Champ Meta Pixel ⇄ CAPI deduplication. * * On a CC thank-you / upsell page, fire the BROWSER Meta Pixel Purchase with the * exact same event_id the CC webhook stamps server-side, so Meta collapses the * browser Pixel event and the server-side CAPI event into one (dedup key = * event_name + event_id). This gives the EMQ lift of a matched browser+server * event without double-counting conversions in Ads Manager. * * PIXEL-ONLY by design. We do NOT enqueue a server event here: the CC Export * Profile webhook (webhooks/platforms/checkoutchamp.js) already ingests the * purchase and fires CAPI. Calling track() here would create a second server * event (source='web') AND double-fire CAPI — defeating the whole point. * * The event_id MUST stay byte-identical to the server formula: * webhooks/platforms/checkoutchamp.js:250 * generateEventId('checkoutchamp', `${event_type}_${order_id}`) * webhooks/core/ingest.js:122 → `${platform}_${webhookEventId}` * ⇒ `checkoutchamp_purchase_` * where is the value CC posts to the webhook via its [orderId] macro. * We read the browser-side counterpart from CC's own client-side order object: * JSON.parse(sessionStorage.getItem('orderData')).orderId * (CC docs: referenced in custom scripts as `orderDataTmp.orderId`). * * ASSUMPTION TO VERIFY ON A REAL CC TEST ORDER: that sessionStorage * orderData.orderId === the [orderId] CC sends to the postback. If a merchant's * CC plan exposes a different id client-side, the two event_ids won't match and * Meta will show duplicates — caught by the test order in the setup checklist. * * v1 scope: the primary order only. Per-upsell Pixel dedup (each upsell is its * own order_id + parent_order_id server-side) needs the upsell sessionStorage * shape confirmed on a live funnel first — tracked as a follow-up. */ private fireCheckoutChampPurchasePixel; /** Checkout Champ identity bridging remains disabled pending signed tokens. */ private syncOutboundLinkParams; /** * Stripe Payment Link auto-decoration (D1). Structurally mirrors * syncOutboundLinkParams: stampAll on init, debounced MutationObserver for * SPA-rendered anchors, capture-phase click re-stamp + queue flush, disposer * wired into destroy()/pagehide. * * Match: exact-host equality against buy.stripe.com + config.stripeLinkDomains * via the URL API — NEVER substring (a[href*=...] would match * evil.com/buy.stripe.com paths) and NEVER checkout.stripe.com (Checkout * Session URLs ignore these params — the session is already created). * * Stamp (URL API preserves existing query + hash): * - client_reference_id= only if the param is absent (merchant-set * wins) and the id passes the Stripe format guard (Stripe silently DROPS * invalid values — don't write garbage into merchant links). * - prefilled_email= only if identified email exists, the param is * absent, locked_prefilled_email is absent, and privacyMode !== 'strict'. * - /: client-reference-id attribute * if absent. * * The decorated link's checkout.session.completed arrives with * client_reference_id, which stripe-connect.js already reads — zero server * changes. window.open(paymentLink) is not covered (use getVisitorId() * manually); iframe/shadow-DOM links are unreachable from document. */ /** * Observe Stripe Checkout Session ids and report each one once. * * The event is what carries the pairing: the server stores * (stripe_session_id -> visitor_id) and, when checkout.session.completed * arrives without a client_reference_id, joins on session.id and feeds the * SAME cacheCustomerVisitor path the Payment Link flow already uses — so * every later invoice for that customer inherits the visitor too. * * Consent is re-checked at emit time, not just at init: a visitor can * withdraw between page load and checkout, and this pairing is exactly the * kind of identity link that must stop when they do. */ /** * Keep a fresh handoff token in the address bar while running inside an in-app browser. * Uses the ORIGINAL replaceState (not our SPA wrapper) and re-seeds lastSpaPath, so the * rewrite can never manufacture a pageview. Re-checks shouldTrack() on every write: a * consent withdrawal mid-session removes the token instead of refreshing it. */ /** * Bring the handoff in line with the CURRENT consent state. Called at init and from every * path that changes whether tracking is allowed (optIn/optOut/setConsent/Shopify consent/ * reset): a mid-session grant starts the writer (on Shopify the Customer Privacy API * resolves after init, so an init-only check would leave the feature dead there), and a * withdrawal or reset rewrites the URL NOW instead of leaving the old id for up to 30s. */ private syncInAppHandoff; private startInAppHandoff; private startStripeSessionCapture; private syncStripePaymentLinks; /** * Create event payload */ private createEventPayload; /** * Check if we should track */ private shouldTrack; /** * Whether marketing/third-party pixels are allowed. Withdrawing `marketing` or `sale` * (CCPA "do not sell") consent via setConsent() blocks loading the Meta/Google/TikTok * pixels (which share data). No consent set = allowed (default). */ private consentAllowsMarketing; /** * Shopify Customer Privacy API handle (9.A.1), or null when it doesn't apply. * * X-2 (LEGAL): gated on RUNTIME detection of `window.Shopify.customerPrivacy`, NOT on * `config.platform === 'shopify'`. A Shopify merchant who installs via a plain