@aranova/tracking-react 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -778,10 +778,11 @@ interface SalesBusinessClient {
778
778
  * (`aranv_sk_…`), used **server-side only**, gets full read/list/update/delete.
779
779
  * Never ship a secret key in a browser bundle.
780
780
  *
781
- * Generic over the service-key union `TService`: bind the type emitted by
782
- * `@aranova/tracking-cli gen` for compile-time-checked `service` values.
781
+ * Generic over the service-key union `TService` and the manual-conversion key union
782
+ * `TConversion`: bind the types emitted by `@aranova/tracking-cli gen` (`AranovaService`
783
+ * and `AranovaConversion`) for compile-time-checked `service` / `trackConversion` keys.
783
784
  */
784
- interface SalesClient<TService extends string = string> {
785
+ interface SalesClient<TService extends string = string, TConversion extends string = string> {
785
786
  record(input: Omit<SaleInput, "currency" | "occurred_at" | "service" | "services"> & {
786
787
  service?: TService | null;
787
788
  services?: Array<{
@@ -812,7 +813,7 @@ interface SalesClient<TService extends string = string> {
812
813
  * write. No-ops when the goal isn't WEBPAGE-mapped (no resolved `send_to`) or the config
813
814
  * isn't wired. Consent-gated + de-duped. (Automatic event-goals fire themselves — no call.)
814
815
  */
815
- trackConversion(key: string, options?: {
816
+ trackConversion(key: TConversion, options?: {
816
817
  transactionId?: string | null;
817
818
  value?: number | null;
818
819
  currency?: SupportedCurrency;
@@ -839,7 +840,7 @@ interface SalesClient<TService extends string = string> {
839
840
  /** Business config (pk or sk). */
840
841
  business: SalesBusinessClient;
841
842
  }
842
- declare function createSalesClient<TService extends string = string>(config: SalesClientConfig): SalesClient<TService>;
843
+ declare function createSalesClient<TService extends string = string, TConversion extends string = string>(config: SalesClientConfig): SalesClient<TService, TConversion>;
843
844
 
844
845
  /**
845
846
  * Error thrown by the sales client (`createSalesClient`) on a
@@ -778,10 +778,11 @@ interface SalesBusinessClient {
778
778
  * (`aranv_sk_…`), used **server-side only**, gets full read/list/update/delete.
779
779
  * Never ship a secret key in a browser bundle.
780
780
  *
781
- * Generic over the service-key union `TService`: bind the type emitted by
782
- * `@aranova/tracking-cli gen` for compile-time-checked `service` values.
781
+ * Generic over the service-key union `TService` and the manual-conversion key union
782
+ * `TConversion`: bind the types emitted by `@aranova/tracking-cli gen` (`AranovaService`
783
+ * and `AranovaConversion`) for compile-time-checked `service` / `trackConversion` keys.
783
784
  */
784
- interface SalesClient<TService extends string = string> {
785
+ interface SalesClient<TService extends string = string, TConversion extends string = string> {
785
786
  record(input: Omit<SaleInput, "currency" | "occurred_at" | "service" | "services"> & {
786
787
  service?: TService | null;
787
788
  services?: Array<{
@@ -812,7 +813,7 @@ interface SalesClient<TService extends string = string> {
812
813
  * write. No-ops when the goal isn't WEBPAGE-mapped (no resolved `send_to`) or the config
813
814
  * isn't wired. Consent-gated + de-duped. (Automatic event-goals fire themselves — no call.)
814
815
  */
815
- trackConversion(key: string, options?: {
816
+ trackConversion(key: TConversion, options?: {
816
817
  transactionId?: string | null;
817
818
  value?: number | null;
818
819
  currency?: SupportedCurrency;
@@ -839,7 +840,7 @@ interface SalesClient<TService extends string = string> {
839
840
  /** Business config (pk or sk). */
840
841
  business: SalesBusinessClient;
841
842
  }
842
- declare function createSalesClient<TService extends string = string>(config: SalesClientConfig): SalesClient<TService>;
843
+ declare function createSalesClient<TService extends string = string, TConversion extends string = string>(config: SalesClientConfig): SalesClient<TService, TConversion>;
843
844
 
844
845
  /**
845
846
  * Error thrown by the sales client (`createSalesClient`) on a
package/dist/sales.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-7w7-Ud60.mjs';
1
+ export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-Bq7H-Vym.mjs';
2
2
  import 'zod';
package/dist/sales.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-7w7-Ud60.js';
1
+ export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-Bq7H-Vym.js';
2
2
  import 'zod';
package/dist/sales.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sales.ts","../../tracking-core/src/consent.ts","../../tracking-core/src/gtag.ts","../../tracking-core/src/resources/conversion-firing.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/session.ts","../../tracking-core/src/ingest.ts","../../tracking-core/src/resources/sales/errors.ts","../../tracking-core/src/resources/sales/transport.ts","../../tracking-core/src/resources/sales/money.ts","../../tracking-core/src/resources/sales/client.ts","../../tracking-core/src/resources/sales/schema.ts","../../tracking-core/src/resources/services.ts"],"sourcesContent":["// Server-safe sales SDK entry — `@aranova/tracking-react/sales`. React-free; the\n// recommended import for server/serverless modules (API routes, Node scripts).\n// The surface is authored once in tracking-core (see sales-public.ts); this file\n// exists only as the per-package tsup build entry.\nexport * from \"../../tracking-core/src/sales-public\";\n","import type { ConsentState } from \"./types\";\n\n/**\n * localStorage key for the visitor's consent choice.\n */\nexport const CONSENT_STATE_KEY = \"consent_state\";\n\n/**\n * localStorage key for the ISO timestamp when consent was last changed.\n */\nexport const CONSENT_TIMESTAMP_KEY = \"consent_timestamp\";\n\n/**\n * Google Consent Mode value sent to `gtag('consent', 'update', ...)`.\n */\nexport type GtagConsentValue = \"granted\" | \"denied\";\n\n/**\n * Consent Mode v2 payload sent to Google Ads when consent changes.\n */\nexport interface ConsentUpdatePayload {\n ad_storage: GtagConsentValue;\n ad_user_data: GtagConsentValue;\n ad_personalization: GtagConsentValue;\n analytics_storage: GtagConsentValue;\n}\n\ntype ConsentGrantedListener = () => void;\nconst grantedListeners = new Set<ConsentGrantedListener>();\n\n/**\n * Subscribe to consent being granted. Used to flush conversions that were queued while\n * consent was still pending (GAP28). Returns an unsubscribe function.\n */\nexport function onConsentGranted(listener: ConsentGrantedListener): () => void {\n grantedListeners.add(listener);\n return () => {\n grantedListeners.delete(listener);\n };\n}\n\n/**\n * Build the Google Consent Mode v2 update payload for a single consent state.\n */\nexport function buildConsentPayload(state: GtagConsentValue): ConsentUpdatePayload {\n return {\n ad_storage: state,\n ad_user_data: state,\n ad_personalization: state,\n analytics_storage: state,\n };\n}\n\n/**\n * Read the persisted visitor consent state from localStorage.\n *\n * Returns `pending` when called during SSR or before the visitor has made a\n * choice.\n */\nexport function getConsentState(): ConsentState {\n if (typeof window === \"undefined\") return \"pending\";\n\n try {\n const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);\n if (storedState === \"granted\" || storedState === \"denied\") return storedState;\n } catch {\n // localStorage blocked (sandboxed iframe / storage disabled) — treat as pending\n }\n\n return \"pending\";\n}\n\n/**\n * Persist a visitor consent choice and update Google Consent Mode when gtag is\n * loaded.\n */\nexport function setConsentState(state: GtagConsentValue): void {\n if (typeof window === \"undefined\") return;\n\n try {\n window.localStorage.setItem(CONSENT_STATE_KEY, state);\n window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, new Date().toISOString());\n } catch {\n // localStorage blocked — still push the live consent update below so the\n // visitor's click takes effect for this page load even if it can't persist\n }\n\n if (typeof window.gtag === \"function\")\n window.gtag(\"consent\", \"update\", buildConsentPayload(state));\n\n // Mirror the choice to the Meta Pixel so one banner gates both ad platforms.\n if (typeof window.fbq === \"function\")\n window.fbq(\"consent\", state === \"granted\" ? \"grant\" : \"revoke\");\n\n // Flush any conversions queued while consent was pending (GAP28).\n if (state === \"granted\") {\n for (const listener of grantedListeners) {\n try {\n listener();\n } catch {\n // a listener must never break the consent flow\n }\n }\n }\n}\n\n/**\n * Clear the stored consent choice so the banner re-appears on next render.\n *\n * Power a \"Cookie preferences\" link in a footer so visitors can change their\n * mind without losing access to your site:\n *\n * ```tsx\n * const { reset } = useConsent();\n * <button onClick={reset}>Cookie preferences</button>\n * ```\n *\n * Does NOT push an `update` to gtag — there's nothing to update because the\n * visitor hasn't chosen anything yet. The next `setConsentState()` call will\n * sync gtag once they re-choose.\n */\nexport function resetConsent(): void {\n if (typeof window === \"undefined\") return;\n\n try {\n window.localStorage.removeItem(CONSENT_STATE_KEY);\n window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);\n } catch {\n // localStorage blocked — nothing to clear\n }\n}\n\n/**\n * Re-apply a stored consent choice to Google Consent Mode.\n *\n * Useful after bootstrapping gtag on a page that already has a saved choice.\n */\nexport function restoreStoredConsent(): ConsentState {\n const consentState = getConsentState();\n\n if (consentState === \"granted\" || consentState === \"denied\") setConsentState(consentState);\n\n return consentState;\n}\n","// This module loads the Google tag (gtag.js) and today fires ONLY `gtag('config', …)`\n// plus Consent Mode — i.e. it builds Google Ads REMARKETING audiences once the visitor\n// accepts the consent banner. It does not fire `gtag('event','conversion')` yet, so\n// conversions are currently tracked OFFLINE via the backend writeback worker\n// (UPLOAD_CLICKS actions). Real-time on-site conversion firing through this tag is\n// planned (gap 28). See docs/google-ads-deployment/gtags-and-conversion-tracking.md.\n\nimport { buildConsentPayload, getConsentState, restoreStoredConsent } from \"./consent\";\nimport type { GtagEnvironmentMap } from \"./types\";\n\n/**\n * Google Tag Manager script host used for gtag loading.\n */\nexport const GTAG_SCRIPT_HOST = \"https://www.googletagmanager.com/gtag/js\";\n\n/**\n * Attribute used to mark scripts inserted by the tracking SDK.\n */\nexport const TRACKING_SCRIPT_ATTRIBUTE = \"data-aranova-tracking\";\n\nfunction getScriptMarker(id: string): string {\n return `aranova-${id}`;\n}\n\n/**\n * Pattern matching valid Google Ads / GA4 tag IDs.\n *\n * Accepted formats: `AW-123456789`, `G-XXXXXXXXXX`, `GT-XXXXXXX`, `DC-XXXXXXX`.\n * IDs that don't match are silently dropped to prevent script injection when\n * interpolated into inline `<script>` content.\n */\nconst GTAG_ID_PATTERN = /^[A-Z]{1,3}-[A-Za-z0-9_-]+$/;\n\nexport function isValidGtagId(id: string): boolean {\n return GTAG_ID_PATTERN.test(id);\n}\n\n/**\n * Create an inline script that initializes gtag with denied consent defaults.\n */\nexport function createConsentDefaultScript(): string {\n return `\nwindow.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\nwindow.gtag = gtag;\ngtag('consent', 'default', {\n ad_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n analytics_storage: 'denied',\n wait_for_update: 500,\n});\n`.trim();\n}\n\n/**\n * Create an inline script that initializes gtag for a Google Ads tag id.\n */\nexport function createGtagInitScript(gtagId: string): string {\n if (!isValidGtagId(gtagId)) return \"\";\n return `\nwindow.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\nwindow.gtag = gtag;\ngtag('js', new Date());\ngtag('config', '${gtagId}');\n`.trim();\n}\n\n/**\n * Create an inline script that restores a stored consent choice into gtag.\n */\nexport function createConsentRestoreScript(): string {\n // Guarded: window.localStorage access throws a SecurityError in sandboxed\n // iframes / storage-blocked contexts; an uncaught throw would abort the inline\n // <Script> and leave a prior consent choice un-applied.\n return `\ntry {\n var consentState = window.localStorage.getItem('consent_state');\n if (window.gtag && (consentState === 'granted' || consentState === 'denied')) {\n window.gtag('consent', 'update', {\n ad_storage: consentState,\n ad_user_data: consentState,\n ad_personalization: consentState,\n analytics_storage: consentState,\n });\n }\n} catch (e) {}\n`.trim();\n}\n\n/**\n * Ensure `window.gtag` and `window.dataLayer` exist and return the gtag shim.\n */\nexport function ensureGtagFunction(): NonNullable<Window[\"gtag\"]> {\n window.dataLayer = window.dataLayer || [];\n\n if (typeof window.gtag === \"function\") return window.gtag;\n\n // CRITICAL: push the `arguments` object, NOT a rest-param array. gtag.js only\n // treats the canonical `dataLayer.push(arguments)` form as a command — a plain\n // array pushed to the dataLayer is ignored. With the array form, every\n // gtag('config' | 'consent' | 'event') call silently no-ops: the account never\n // registers, consent mode never applies, and conversions never fire (verified\n // live — the array form sends zero network hits; the arguments form sends the\n // pagead/conversion ping). Mirror Google's canonical inline snippet exactly.\n function gtag(): void {\n // eslint-disable-next-line prefer-rest-params\n window.dataLayer?.push(arguments);\n }\n window.gtag = gtag;\n\n return window.gtag;\n}\n\nconst SEND_TO_RE = /^AW-[A-Za-z0-9]+\\/[A-Za-z0-9_-]+$/;\n\n/** A gtag `send_to` target, e.g. `AW-123456789/AbC-D_efg`. */\nexport function isValidSendTo(sendTo: string): boolean {\n return SEND_TO_RE.test(sendTo);\n}\n\nexport interface GtagConversionInput {\n /** `AW-<id>/<label>` — the conversion action's firing target. */\n sendTo: string;\n /** Conversion value in MAJOR units; omit to let Google apply the action's default. */\n value?: number | null;\n currency?: string | null;\n transactionId?: string | null;\n}\n\n/**\n * Fire a real-time on-site conversion — `gtag('event','conversion',{ send_to, … })`\n * (GAP28). Browser-gated and never throws: `record()` is isomorphic and runs server-side\n * under a secret key where `window`/`gtag` are absent, so this no-ops there. Consent\n * gating + de-dup are the caller's responsibility (see `fireConversionWithConsent`).\n * Returns whether the event was actually pushed.\n */\nexport function fireGtagConversion(input: GtagConversionInput): boolean {\n if (typeof window === \"undefined\" || typeof window.gtag !== \"function\") return false;\n if (!isValidSendTo(input.sendTo)) return false;\n const params: Record<string, unknown> = { send_to: input.sendTo };\n if (input.value != null) params.value = input.value;\n if (input.currency) params.currency = input.currency;\n if (input.transactionId) params.transaction_id = input.transactionId;\n try {\n window.gtag(\"event\", \"conversion\", params);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Apply denied-by-default Google Consent Mode state.\n */\nexport function applyDefaultConsentState(): void {\n const gtag = ensureGtagFunction();\n gtag(\"consent\", \"default\", {\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n analytics_storage: \"denied\",\n wait_for_update: 500,\n });\n}\n\n/**\n * Load the external gtag script once for the provided Google Ads tag id.\n */\nexport function loadGtagScript(gtagId: string): void {\n if (typeof document === \"undefined\") return;\n\n const marker = getScriptMarker(\"gtag-loader\");\n const existingScript = document.querySelector<HTMLScriptElement>(\n `script[${TRACKING_SCRIPT_ATTRIBUTE}=\"${marker}\"]`,\n );\n if (existingScript) return;\n\n const script = document.createElement(\"script\");\n script.async = true;\n script.src = `${GTAG_SCRIPT_HOST}?id=${encodeURIComponent(gtagId)}`;\n script.setAttribute(TRACKING_SCRIPT_ATTRIBUTE, marker);\n document.head.append(script);\n}\n\n/**\n * Send the initial `gtag('js')` and `gtag('config')` calls.\n */\nexport function initializeGtag(gtagId: string): void {\n const gtag = ensureGtagFunction();\n gtag(\"js\", new Date());\n gtag(\"config\", gtagId);\n}\n\n/**\n * Push the stored consent choice to gtag if one exists.\n */\nexport function restoreConsentState(): void {\n if (typeof window === \"undefined\") return;\n\n const consentState = getConsentState();\n if (consentState === \"granted\" || consentState === \"denied\")\n window.gtag?.(\"consent\", \"update\", buildConsentPayload(consentState));\n}\n\n/**\n * Load and initialize Google Ads tracking with Consent Mode support.\n *\n * Used by framework components and browser-script initialization.\n */\nexport function bootstrapGoogleAdsTracking(gtagId: string): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n if (!isValidGtagId(gtagId)) return;\n\n applyDefaultConsentState();\n loadGtagScript(gtagId);\n initializeGtag(gtagId);\n restoreStoredConsent();\n}\n\n/**\n * Load and initialize Google Ads tracking for ALL labelled gtag IDs.\n *\n * Every ID in the map gets a `gtag('config', ...)` call — gtag natively\n * supports multiple configured tags on the same page. The script loader\n * only runs once (for the first ID); subsequent IDs reuse the shared\n * `dataLayer`.\n */\nexport function bootstrapMultipleGtags(gtagIds: GtagEnvironmentMap): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n const ids = Object.values(gtagIds).filter(\n (id): id is string => typeof id === \"string\" && isValidGtagId(id),\n );\n if (ids.length === 0) return;\n\n applyDefaultConsentState();\n loadGtagScript(ids[0]!);\n\n // Call gtag('js') once, then gtag('config') per ID. initializeGtag\n // calls both, but gtag('js') should only fire once per page load.\n const gtag = ensureGtagFunction();\n gtag(\"js\", new Date());\n for (const id of ids) {\n gtag(\"config\", id);\n }\n\n restoreStoredConsent();\n}\n\n/**\n * Create an inline script that initializes gtag for multiple Google Ads tag ids.\n *\n * Used by the Next.js `GoogleAdsTracking` component when rendering `<Script>`\n * tags for a multi-gtag configuration.\n */\nexport function createGtagInitScriptMulti(gtagIds: string[]): string {\n const safeIds = gtagIds.filter(isValidGtagId);\n const configs = safeIds.map((id) => `gtag('config', '${id}');`).join(\"\\n\");\n return `\nwindow.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\nwindow.gtag = gtag;\ngtag('js', new Date());\n${configs}\n`.trim();\n}\n","// Consent-gated, de-duped orchestration around the raw `fireGtagConversion` (GAP28).\n//\n// granted → fire now (de-duped by transaction_id + send_to via sessionStorage)\n// pending → queue, flush automatically when consent is granted\n// denied → drop\n//\n// `fireGtagConversion` is itself browser-gated and never throws, so this is safe to call\n// from the isomorphic sales `record()` — server-side it simply no-ops.\n\nimport { getConsentState, onConsentGranted } from \"../consent\";\nimport { fireGtagConversion, type GtagConversionInput } from \"../gtag\";\n\nconst DEDUP_PREFIX = \"_aranova_conv_\";\n// Cap the queue so a visitor who never accepts the banner can't grow it unbounded; we drop\n// the oldest entry past the cap (the most recent conversions are the ones worth keeping).\nconst MAX_PENDING = 100;\nconst pendingQueue: GtagConversionInput[] = [];\n\nfunction dedupKey(input: GtagConversionInput): string {\n return `${DEDUP_PREFIX}${input.transactionId ?? \"\"}:${input.sendTo}`;\n}\n\n// Has this (transaction_id, send_to) already fired? Read-only — the key is written ONLY\n// after a successful fire (see `fireOnce`), so a fire that never happened (e.g. gtag not\n// ready yet) is never suppressed on retry. Best-effort: a blocked sessionStorage reads as\n// \"not fired\" so the conversion still gets a chance.\nfunction alreadyFired(input: GtagConversionInput): boolean {\n if (!input.transactionId || typeof window === \"undefined\") return false;\n try {\n return window.sessionStorage.getItem(dedupKey(input)) !== null;\n } catch {\n return false;\n }\n}\n\nfunction markFired(input: GtagConversionInput): void {\n if (!input.transactionId || typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(dedupKey(input), \"1\");\n } catch {\n // sessionStorage unavailable — Google's counting_type is the authoritative backstop\n }\n}\n\n// Fire once: skip if already fired, and only record the guard AFTER the fire succeeds.\nfunction fireOnce(input: GtagConversionInput): void {\n if (alreadyFired(input)) return;\n if (fireGtagConversion(input)) markFired(input);\n}\n\n/** Fire a conversion through the consent gate + de-dup guard. */\nexport function fireConversionWithConsent(input: GtagConversionInput): void {\n const state = getConsentState();\n if (state === \"denied\") return;\n if (state === \"pending\") {\n if (pendingQueue.length >= MAX_PENDING) pendingQueue.shift();\n pendingQueue.push(input);\n return;\n }\n fireOnce(input);\n}\n\n/** Flush conversions queued while consent was pending; called on consent-grant. */\nexport function flushPendingConversions(): void {\n if (getConsentState() !== \"granted\") return;\n while (pendingQueue.length > 0) {\n const input = pendingQueue.shift();\n if (input) fireOnce(input);\n }\n}\n\n// Subscribe once at module load so a grant flushes the queue regardless of how the queue\n// was filled (browser-only; no-ops under SSR where there is no consent flow).\nif (typeof window !== \"undefined\") onConsentGranted(flushPendingConversions);\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_view` event.\n *\n * The SDK emits this on initial load, SPA route changes, and bfcache restores.\n * Consumers do not call `trackEvent('page_view', ...)`; registering\n * `automatic: { page_view: {} }` enables the SDK-owned trigger.\n */\nexport const pageViewMetadataSchema = z\n .object({\n page: z\n .object({\n title: z.string().nullable(),\n path: z.string(),\n search: z.string(),\n hash: z.string(),\n })\n .strict(),\n referrer: z.string().nullable(),\n // `.nullable().optional()` — absent (undefined) OR explicit null OR a\n // real viewport object. Mirrors Pydantic's `_Viewport | None = None`\n // on the backend side so the drift test stays clean.\n viewport: z\n .object({\n w: z.number(),\n h: z.number(),\n })\n .strict()\n .nullable()\n .optional(),\n })\n .strict();\n\nexport type PageViewMetadata = z.infer<typeof pageViewMetadataSchema>;\n\n/**\n * Registration config for automatic `page_view`.\n *\n * `page_view` is required in every trigger registry and currently has no\n * options. Use `{ page_view: {} }`.\n */\nexport const pageViewConfigSchema = z.object({}).strict();\nexport type PageViewConfig = z.infer<typeof pageViewConfigSchema>;\n","// Visitor + session identity for the tracking SDK.\n//\n// Visitor: persistent localStorage UUID, never expires until the user clears\n// browser storage. Used for cross-session correlation.\n//\n// Session: rolling 30-minute idle window. Regenerated when more than\n// SESSION_IDLE_MS has passed since the last event. Matches the behavior of\n// GA4, PostHog, Mixpanel, etc., so analytics is comparable.\n\n/**\n * localStorage key for the persistent visitor id.\n */\nexport const VISITOR_STORAGE_KEY = \"aranova_tracking_visitor\";\n\n/**\n * localStorage key for the rolling session id state.\n */\nexport const SESSION_STORAGE_KEY = \"aranova_tracking_session\";\n\n/**\n * Idle window before a new session id is created.\n */\nexport const SESSION_IDLE_MS = 30 * 60 * 1000;\n\n/**\n * Serialized session state stored in localStorage.\n */\nexport interface StoredSession {\n /** Client-generated session UUID. */\n id: string;\n /** Unix timestamp in milliseconds for the most recent event/session touch. */\n last_event_at: number;\n}\n\nfunction safeUuid(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\")\n return crypto.randomUUID();\n // Fallback for ancient browsers — not cryptographically perfect but unique enough.\n return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage may be denied (private mode, blocked cookies, quota). Caller is\n // responsible for degrading gracefully.\n }\n}\n\n/**\n * Return the persistent visitor id for this browser profile.\n *\n * Creates and stores a new id when one does not already exist. During SSR,\n * returns an ephemeral id because browser storage is unavailable.\n */\nexport function getVisitorId(): string {\n if (typeof window === \"undefined\") return safeUuid();\n\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n if (existing && existing.length > 0) return existing;\n\n const fresh = safeUuid();\n writeLocalStorage(VISITOR_STORAGE_KEY, fresh);\n return fresh;\n}\n\n/**\n * Result from `getOrRotateSessionId()`.\n */\nexport interface SessionIdResult {\n /** Current session id. */\n id: string;\n /** Whether this call created a new session. */\n isNew: boolean;\n}\n\n/**\n * Return the current session id, rotating it after the idle window expires.\n *\n * Also refreshes `last_event_at` for active sessions.\n */\nexport function getOrRotateSessionId(now: number = Date.now()): SessionIdResult {\n if (typeof window === \"undefined\") return { id: safeUuid(), isNew: true };\n\n const raw = readLocalStorage(SESSION_STORAGE_KEY);\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredSession>;\n if (typeof parsed.id === \"string\" && typeof parsed.last_event_at === \"number\") {\n if (now - parsed.last_event_at <= SESSION_IDLE_MS) {\n const refreshed: StoredSession = { id: parsed.id, last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));\n return { id: parsed.id, isNew: false };\n }\n }\n } catch {\n // Fall through to a fresh session.\n }\n }\n\n const fresh: StoredSession = { id: safeUuid(), last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));\n return { id: fresh.id, isNew: true };\n}\n\n/**\n * Clear visitor and session identity from localStorage.\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(VISITOR_STORAGE_KEY);\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n // ignored\n }\n}\n","// Tracking ingest client. Queues events, debounce-flushes them to the\n// /tracking/events endpoint, and degrades silently on errors so a broken\n// network never breaks the host site.\n\nimport { getConsentState } from \"./consent\";\nimport { resetPageViewState } from \"./page-view\";\nimport type { PhoneConfig } from \"./phone-field\";\nimport { captureFbc, getFbcCookie, getFbpCookie } from \"./fbq\";\nimport {\n captureTrackingParamsFromLocation,\n createEmptyTrackingParams,\n getCookieValueFromDocument,\n getTrackingParamsFromCookieReader,\n mergeTrackingParams,\n} from \"./tracking\";\nimport type { TriggerRegistryConfig } from \"./events/registry\";\nimport { buildHeartbeatMetadata } from \"./heartbeat\";\nimport { getOrRotateSessionId, getVisitorId } from \"./session\";\nimport type {\n TrackingClientContext,\n TrackingEnvironment,\n TrackingInstallSurface,\n TrackingParams,\n TrackingSessionUpsertPayload,\n} from \"./types\";\n\n/**\n * Default debounce window before queued events are flushed.\n */\nexport const DEFAULT_FLUSH_INTERVAL_MS = 2000;\n\n/**\n * Default queue size that triggers an immediate flush.\n */\nexport const DEFAULT_MAX_QUEUE_SIZE = 10;\n\n/**\n * Hard server-side maximum event count per request body.\n */\nexport const HARD_MAX_BATCH = 50;\n\n/**\n * Header used to authenticate public tracking ingest requests.\n */\nexport const API_KEY_HEADER = \"X-Aranova-Api-Key\";\n\n/**\n * Identity headers stamped on every ingest request. Duplicate fields already\n * present in `session.context` but survive body-parse failures so the backend\n * can attribute 422s to the offending SDK install.\n */\nexport const SDK_VERSION_HEADER = \"X-Aranova-Sdk-Version\";\nexport const SDK_PACKAGE_HEADER = \"X-Aranova-Sdk-Package\";\nexport const SDK_SURFACE_HEADER = \"X-Aranova-Sdk-Surface\";\nexport const SDK_ENVIRONMENT_HEADER = \"X-Aranova-Sdk-Environment\";\n\n/**\n * Configuration for the low-level ingest client.\n *\n * Framework packages usually create this for you through `createTracking()`.\n */\nexport interface TrackingClientConfig {\n /** Public tracking API key issued for the business. */\n apiKey: string;\n /** Tracking endpoint base URL, usually ending in `/tracking`. */\n endpoint: string;\n /** SDK surface creating this client. */\n surface: TrackingInstallSurface;\n /** Package version reported in session context and heartbeat metadata. */\n sdkVersion?: string;\n /** Package name reported in session context and heartbeat metadata. */\n packageName?: string;\n /** Trigger registry so the heartbeat can report registered events. */\n triggers?: TriggerRegistryConfig;\n /** Override the default 2s debounce window. */\n flushIntervalMs?: number;\n /** Override the default 10-event batch trigger. */\n maxQueueSize?: number;\n /** Deployment environment label reported in session context. */\n environment?: TrackingEnvironment;\n /** All active gtag IDs, keyed by label. Included in session context. */\n activeGtagIds?: Record<string, string>;\n /** When true, swallow nothing — useful for tests. */\n debug?: boolean;\n /** Phone-field config, carried for parity; the React hook reads it via context. */\n phone?: PhoneConfig;\n}\n\n/**\n * Input accepted by the low-level stringly-typed client.\n *\n * Prefer the typed `trackEvent(eventName, metadata)` facade exposed by\n * `useTracking()` in React/Next integrations.\n */\nexport interface TrackEventInput {\n /** Event name to enqueue. */\n eventType: string;\n /** URL associated with the event. Defaults to the current page URL. */\n pageUrl?: string | null;\n /** Event-specific metadata. */\n metadata?: Record<string, unknown> | null;\n /** Timestamp override. Defaults to queue time. */\n occurredAt?: Date | string | null;\n}\n\ninterface QueuedEvent {\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n}\n\n/**\n * Low-level tracking client responsible for queueing and flushing events.\n */\nexport interface TrackingClient {\n /** Enqueue an event for batched delivery. */\n trackEvent: (input: TrackEventInput) => void;\n /** Flush queued events immediately. */\n flush: () => Promise<void>;\n /** Return the current rolling session id. */\n getSessionId: () => string;\n /** Return the persistent visitor id. */\n getVisitorId: () => string;\n /** Remove timers/listeners and prevent future flushes. */\n destroy: () => void;\n}\n\ninterface IngestRequestBody {\n session: TrackingSessionUpsertPayload;\n events: Array<{\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n }>;\n}\n\nfunction buildContext(\n surface: TrackingInstallSurface,\n sdkVersion: string | null,\n packageName: string | null,\n environment: TrackingEnvironment,\n activeGtagIds: Record<string, string> | null,\n): TrackingClientContext {\n return {\n surface,\n sdk_version: sdkVersion,\n package_name: packageName,\n site_origin: typeof window === \"undefined\" ? null : window.location.origin,\n page_title: typeof document === \"undefined\" ? null : document.title || null,\n referrer: typeof document === \"undefined\" ? null : document.referrer || null,\n environment,\n active_gtag_ids: activeGtagIds,\n };\n}\n\nfunction readTrackingParams(): TrackingParams {\n if (typeof window === \"undefined\") return createEmptyTrackingParams();\n // Capture from URL on every read so the first event in a session reflects the\n // landing-page params even if the cookie helper hasn't run yet.\n try {\n captureTrackingParamsFromLocation();\n } catch {\n // ignore\n }\n return getTrackingParamsFromCookieReader(getCookieValueFromDocument);\n}\n\nfunction consentSnapshot(): Record<string, unknown> | null {\n try {\n return { state: getConsentState() };\n } catch {\n return null;\n }\n}\n\ninterface IdentityHeaders {\n sdkVersion: string;\n packageName: string;\n surface: string;\n environment: string;\n}\n\nasync function postWithFetch(\n url: string,\n body: string,\n apiKey: string,\n identity: IdentityHeaders,\n keepalive: boolean,\n): Promise<void> {\n if (typeof fetch !== \"function\") return;\n try {\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n [API_KEY_HEADER]: apiKey,\n [SDK_VERSION_HEADER]: identity.sdkVersion,\n [SDK_PACKAGE_HEADER]: identity.packageName,\n [SDK_SURFACE_HEADER]: identity.surface,\n [SDK_ENVIRONMENT_HEADER]: identity.environment,\n },\n body,\n keepalive,\n // CORS is open on the tracking endpoint; never send cookies.\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch {\n // fire-and-forget — never throw to the host site\n }\n}\n\nlet globalClient: TrackingClient | null = null;\nlet globalClientKey: string | null = null;\n\nfunction clientConfigKey(config: TrackingClientConfig): string {\n return `${config.apiKey}@${config.endpoint}#${config.surface}`;\n}\n\n/**\n * Return a page-level singleton tracking client. Creating the client anew on\n * every component mount is wrong — React StrictMode double-mounts dev-only,\n * and destroying+recreating the client between the cleanup and re-run strips\n * away the pushState patch that SPA auto page view relies on. A singleton\n * survives all of that: the client lives for the entire page, and providers\n * just attach/detach auto page view against it.\n *\n * If `apiKey` / `endpoint` / `surface` change between calls, the previous\n * singleton is destroyed and a new one replaces it. This covers hot-config\n * changes without leaking state.\n */\nexport function getOrCreateTrackingClient(config: TrackingClientConfig): TrackingClient {\n const key = clientConfigKey(config);\n if (globalClient !== null && globalClientKey === key) {\n return globalClient;\n }\n if (globalClient !== null) {\n globalClient.destroy();\n }\n globalClient = createTrackingClient(config);\n globalClientKey = key;\n return globalClient;\n}\n\n/**\n * Tear down the singleton if any. Primarily an escape hatch for tests where\n * each test should see a fresh client; production code rarely needs this.\n * Also clears the in-session SPA referrer so the next test starts with a\n * fresh referrer chain.\n */\nexport function resetGlobalTrackingClient(): void {\n if (globalClient !== null) {\n globalClient.destroy();\n }\n globalClient = null;\n globalClientKey = null;\n resetPageViewState();\n}\n\n/**\n * Create a low-level ingest client.\n *\n * The client queues events, debounces network flushes, sends an SDK heartbeat\n * once per new session, and swallows network errors so analytics never break\n * the host site.\n */\nexport function createTrackingClient(config: TrackingClientConfig): TrackingClient {\n const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);\n const sdkVersion = config.sdkVersion ?? null;\n const packageName = config.packageName ?? null;\n // Default to 'production' so the wire payload always carries a valid enum\n // value. Sending null would fail the backend's strict enum validation.\n const environment: TrackingEnvironment = config.environment ?? \"production\";\n const activeGtagIds = config.activeGtagIds ?? null;\n const endpointBase = config.endpoint.replace(/\\/$/, \"\");\n const eventsUrl = `${endpointBase}/events`;\n const identityHeaders: IdentityHeaders = {\n sdkVersion: sdkVersion ?? \"\",\n packageName: packageName ?? \"\",\n surface: config.surface,\n environment,\n };\n\n let queue: QueuedEvent[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n let firstPage: string | null = null;\n // Attribution captured synchronously at client creation (before any SPA router\n // can strip the query string). Merged into every session payload as a fallback\n // so a blocked-cookie webview or a stripped URL still attributes.\n let initialParams = createEmptyTrackingParams();\n let destroyed = false;\n\n // Initialize identity early so the first POST has stable values.\n const visitorId = getVisitorId();\n const initialSession = getOrRotateSessionId();\n let sessionId = initialSession.id;\n\n if (typeof window !== \"undefined\") {\n firstPage = window.location.href;\n try {\n initialParams = captureTrackingParamsFromLocation();\n } catch {\n // ignore — never break the host site\n }\n try {\n captureFbc();\n } catch {\n // ignore\n }\n }\n\n // Queue an sdk_heartbeat event at the start of every new session.\n function enqueueHeartbeat(): void {\n const metadata = buildHeartbeatMetadata(\n config.surface,\n sdkVersion,\n packageName,\n config.triggers ?? null,\n activeGtagIds,\n );\n queue.push({\n event_type: \"sdk_heartbeat\",\n page_url: typeof window === \"undefined\" ? null : window.location.href,\n metadata: metadata as unknown as Record<string, unknown>,\n occurred_at: new Date().toISOString(),\n });\n }\n\n if (initialSession.isNew) {\n enqueueHeartbeat();\n }\n\n function buildSessionPayload(): TrackingSessionUpsertPayload {\n const rotated = getOrRotateSessionId();\n if (rotated.isNew && rotated.id !== sessionId) {\n // Session rotated mid-page (idle > 30min then user returned).\n enqueueHeartbeat();\n }\n sessionId = rotated.id;\n // Prefer the live cookie/localStorage read; fall back to the init-time\n // snapshot (covers a webview that blocked the cookie AND a router that\n // already stripped the landing URL by flush time).\n const params = mergeTrackingParams(readTrackingParams(), initialParams);\n const context = buildContext(\n config.surface,\n sdkVersion,\n packageName,\n environment,\n activeGtagIds,\n );\n return {\n session_id: sessionId,\n visitor_id: visitorId,\n gclid: params.gclid,\n fbclid: params.fbclid,\n fbc: getFbcCookie(),\n fbp: getFbpCookie(),\n utm_source: params.utm_source,\n utm_medium: params.utm_medium,\n utm_campaign: params.utm_campaign,\n utm_term: params.utm_term,\n utm_content: params.utm_content,\n first_page: firstPage,\n consent_state: consentSnapshot(),\n context,\n };\n }\n\n function scheduleFlush(): void {\n if (flushTimer !== null || destroyed) return;\n flushTimer = setTimeout(() => {\n flushTimer = null;\n void flush();\n }, flushIntervalMs);\n }\n\n function clearScheduledFlush(): void {\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n }\n\n async function flush(): Promise<void> {\n if (queue.length === 0) return;\n\n const events = queue.slice(0, HARD_MAX_BATCH);\n queue = queue.slice(events.length);\n clearScheduledFlush();\n\n const body: IngestRequestBody = {\n session: buildSessionPayload(),\n events,\n };\n\n const serialized = JSON.stringify(body);\n\n // Prefer fetch + keepalive for better error visibility. sendBeacon is the\n // pagehide fallback path because it survives navigation away.\n await postWithFetch(eventsUrl, serialized, config.apiKey, identityHeaders, false);\n }\n\n function trackEvent(input: TrackEventInput): void {\n if (destroyed) return;\n if (!input || typeof input.eventType !== \"string\" || input.eventType.length === 0) return;\n\n const occurredAt =\n input.occurredAt instanceof Date\n ? input.occurredAt.toISOString()\n : typeof input.occurredAt === \"string\"\n ? input.occurredAt\n : new Date().toISOString();\n\n queue.push({\n event_type: input.eventType,\n page_url: input.pageUrl ?? (typeof window === \"undefined\" ? null : window.location.href),\n metadata: input.metadata ?? null,\n occurred_at: occurredAt,\n });\n\n if (queue.length >= maxQueueSize) {\n void flush();\n } else {\n scheduleFlush();\n }\n }\n\n function flushOnUnload(): void {\n if (queue.length === 0) return;\n const events = queue.slice(0, HARD_MAX_BATCH);\n queue = queue.slice(events.length);\n clearScheduledFlush();\n\n const body: IngestRequestBody = {\n session: buildSessionPayload(),\n events,\n };\n const serialized = JSON.stringify(body);\n void postWithFetch(eventsUrl, serialized, config.apiKey, identityHeaders, true);\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", flushOnUnload);\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") flushOnUnload();\n });\n }\n\n return {\n trackEvent,\n flush,\n getSessionId: () => sessionId,\n getVisitorId: () => visitorId,\n destroy: () => {\n destroyed = true;\n // Fire any pending events through the keepalive path before tearing\n // down. Critical for React StrictMode in dev, where the provider's\n // first mount is immediately unmounted and its 2s debounce would\n // otherwise drop the initial page_view on the floor. Uses fetch\n // keepalive so the request survives the component tearing down.\n if (queue.length > 0) {\n flushOnUnload();\n }\n clearScheduledFlush();\n queue = [];\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"pagehide\", flushOnUnload);\n }\n },\n };\n}\n","/**\n * Error thrown by the sales client (`createSalesClient`) on a\n * non-2xx response. Unlike the fire-and-forget event queue (which swallows\n * failures), a sale is a transaction the caller must be able to react to.\n */\nexport class AranovaApiError extends Error {\n readonly status: number;\n readonly code: string | undefined;\n readonly requestId: string | undefined;\n\n constructor(message: string, options: { status: number; code?: string; requestId?: string }) {\n super(message);\n this.name = \"AranovaApiError\";\n this.status = options.status;\n this.code = options.code;\n this.requestId = options.requestId;\n }\n}\n","import {\n API_KEY_HEADER,\n SDK_ENVIRONMENT_HEADER,\n SDK_PACKAGE_HEADER,\n SDK_SURFACE_HEADER,\n SDK_VERSION_HEADER,\n} from \"../../ingest\";\nimport { AranovaApiError } from \"./errors\";\n\n/** Shared config for every sales HTTP helper. */\nexport interface SalesTransportConfig {\n /** Public (`aranv_pk_…`) or secret (`aranv_sk_…`) API key. */\n apiKey: string;\n /**\n * Base tracking endpoint, e.g. `https://aranovainternal-production.up.railway.app/tracking`.\n * The `/sales` path is appended by the helpers.\n */\n endpoint: string;\n /** Optional SDK identity headers (mirrors the event ingest client). */\n sdkVersion?: string;\n packageName?: string;\n surface?: string;\n environment?: string;\n}\n\nfunction identityHeaders(config: SalesTransportConfig): Record<string, string> {\n const headers: Record<string, string> = { [API_KEY_HEADER]: config.apiKey };\n if (config.sdkVersion) headers[SDK_VERSION_HEADER] = config.sdkVersion;\n if (config.packageName) headers[SDK_PACKAGE_HEADER] = config.packageName;\n if (config.surface) headers[SDK_SURFACE_HEADER] = config.surface;\n if (config.environment) headers[SDK_ENVIRONMENT_HEADER] = config.environment;\n return headers;\n}\n\nfunction joinUrl(endpoint: string, path: string): string {\n return `${endpoint.replace(/\\/$/, \"\")}${path}`;\n}\n\n/**\n * Single awaited request used by every sales helper. Unlike the event queue,\n * this surfaces failures: any non-2xx rejects with an {@link AranovaApiError}.\n * Returns `undefined` for 204 No Content.\n */\nexport async function salesRequest<T>(\n config: SalesTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n const headers = identityHeaders(config);\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await fetch(joinUrl(config.endpoint, path), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n if (!response.ok) {\n let detail: string | undefined;\n let code: string | undefined;\n try {\n const parsed: unknown = await response.json();\n if (parsed && typeof parsed === \"object\") {\n const record = parsed as Record<string, unknown>;\n if (typeof record.detail === \"string\") detail = record.detail;\n if (typeof record.code === \"string\") code = record.code;\n }\n } catch {\n // non-JSON error body — fall back to status text\n }\n throw new AranovaApiError(detail ?? response.statusText ?? \"Request failed\", {\n status: response.status,\n code,\n requestId: response.headers.get(\"x-request-id\") ?? undefined,\n });\n }\n\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n}\n","import type { SupportedCurrency } from \"./schema\";\n\n/**\n * Money conversion helpers.\n *\n * The API speaks **integer minor units (cents)** exclusively. These helpers move\n * between a human-facing major amount (dollars) and cents, and format cents for\n * display — so a consumer always has both representations without float math.\n */\n\n// Minor-unit exponent per supported currency (USD/CAD = 2 decimal places).\nconst MINOR_UNIT_EXPONENT: Record<SupportedCurrency, number> = {\n USD: 2,\n CAD: 2,\n};\n\nfunction exponentFor(currency: SupportedCurrency): number {\n return MINOR_UNIT_EXPONENT[currency] ?? 2;\n}\n\n/**\n * Convert a major amount (dollars `250.5`) to integer minor units (`25050`).\n *\n * Convenience only — the wire is always integer cents. Uses float multiply +\n * `Math.round`, so values that aren't exactly representable in binary float\n * (e.g. `1.005`) can round to the neighbouring cent. If you already hold an\n * exact cents integer, pass it straight through and skip this helper.\n */\nexport function toMinor(amount: number, currency: SupportedCurrency): number {\n return Math.round(amount * 10 ** exponentFor(currency));\n}\n\n/** Convert integer minor units (`25050`) to a major amount (`250.5`). */\nexport function fromMinor(cents: number, currency: SupportedCurrency): number {\n return cents / 10 ** exponentFor(currency);\n}\n\n/**\n * Format integer minor units as a localized currency string (e.g. `\"$250.50\"`).\n * Uses the built-in `Intl.NumberFormat` — no extra dependency.\n */\nexport function formatMoney(cents: number, currency: SupportedCurrency, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(\n fromMinor(cents, currency),\n );\n}\n\n/**\n * Format an ISO timestamp in a specific IANA time zone (e.g. `America/Toronto`),\n * so dashboards stop hand-rolling `Intl`. Defaults to a short date-time; pass\n * `opts` to override fields. The `timeZone` is always forced to the argument.\n */\nexport function formatDateInTz(\n iso: string,\n timeZone: string,\n opts?: Intl.DateTimeFormatOptions,\n locale?: string,\n): string {\n const date = new Date(iso);\n // Guard malformed input: Intl.format(Invalid Date) throws a RangeError.\n if (Number.isNaN(date.getTime())) return iso;\n return new Intl.DateTimeFormat(locale, {\n year: \"numeric\",\n month: \"short\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n ...opts,\n timeZone,\n }).format(date);\n}\n","import type {\n BusinessConfig,\n CustomerGetOptions,\n CustomerGetResult,\n CustomerKpis,\n CustomerListPage,\n CustomerListQuery,\n CustomerSummaryQuery,\n Sale,\n SaleCursorPage,\n SaleInput,\n SaleListQueryV2,\n SaleSummaryQueryV2,\n SaleSummaryV2,\n SaleUpdateInput,\n SupportedCurrency,\n} from \"./schema\";\nimport type { ConversionConfigStore } from \"../conversion-config\";\nimport { fireConversionWithConsent } from \"../conversion-firing\";\nimport { salesRequest, type SalesTransportConfig } from \"./transport\";\nimport { fromMinor } from \"./money\";\n\n/** Config for {@link createSalesClient}. */\nexport interface SalesClientConfig extends SalesTransportConfig {\n /** Applied when an individual `record()` call omits `currency`. */\n defaultCurrency?: SupportedCurrency;\n /**\n * GAP28: when present, `record()` ALSO fires a real-time on-site conversion\n * (`gtag('event','conversion')`) for any recorded service that has a `firing` send_to in\n * the resolved config. Consent-gated + de-duped; no-ops server-side. Wire it from\n * `resolveConversionConfig(...)`.\n */\n firing?: Pick<ConversionConfigStore, \"getFiring\">;\n}\n\n// Fire the on-site conversion(s) for a just-recorded sale. Per-service value = the sale's\n// amount when present, else the action's configured default from config (Google applies\n// its own default when neither is sent). transaction_id ties the gtag fire to the sale so\n// retries/reloads de-dup within the WEBPAGE action.\nfunction fireRecordedConversions(\n firing: Pick<ConversionConfigStore, \"getFiring\"> | undefined,\n input: { external_id?: string | null; amount_total_cents?: number | null },\n recorded: { service?: string | null; amount_cents: number | null }[],\n sale: Sale,\n currency: SupportedCurrency,\n): void {\n if (!firing) return;\n const txnBase = input.external_id ?? sale.id;\n for (const item of recorded) {\n if (!item.service) continue;\n const config = firing.getFiring(item.service);\n if (!config) continue;\n const cents = item.amount_cents ?? config.value_cents ?? null;\n fireConversionWithConsent({\n sendTo: config.send_to,\n value: cents != null ? fromMinor(cents, currency) : null,\n currency: config.currency ?? currency,\n transactionId: `${txnBase}:${item.service}`,\n });\n }\n}\n\n/** Phone-keyed customer rollups (sk-only; a public key gets a `403`). */\nexport interface SalesCustomersClient {\n list(query?: CustomerListQuery): Promise<CustomerListPage>;\n /** `id` is the customer's E.164 phone. */\n get(id: string, options?: CustomerGetOptions): Promise<CustomerGetResult>;\n summary(query?: CustomerSummaryQuery): Promise<CustomerKpis>;\n}\n\n/** Business config (low-sensitivity — accepts a public or secret key). */\nexport interface SalesBusinessClient {\n config(): Promise<BusinessConfig>;\n}\n\n/**\n * One isomorphic sales client — what a key may *do* is enforced by the backend,\n * not by hiding methods. A **public** key (`aranv_pk_…`) may `record` (the\n * backend rejects reads/CRUD from it with a `403`); a **secret** key\n * (`aranv_sk_…`), used **server-side only**, gets full read/list/update/delete.\n * Never ship a secret key in a browser bundle.\n *\n * Generic over the service-key union `TService`: bind the type emitted by\n * `@aranova/tracking-cli gen` for compile-time-checked `service` values.\n */\nexport interface SalesClient<TService extends string = string> {\n record(\n input: Omit<SaleInput, \"currency\" | \"occurred_at\" | \"service\" | \"services\"> & {\n service?: TService | null;\n // XOR with `service`: record multiple services in one sale, each priced\n // individually. `amount_total_cents` is then optional (the backend derives\n // it as the sum). Keys are checked against the codegen `TService` union.\n services?: Array<{ service: TService; amount_cents: number }>;\n currency?: SupportedCurrency;\n occurred_at?: string;\n },\n ): Promise<Sale>;\n /**\n * Record a revenue sale — the intent-revealing alias of {@link record} in the unified-goal\n * API. POSTs `/sales` and ALSO fires the on-site conversion when the sale-goal is\n * WEBPAGE-mapped. Use this for anything with real revenue; use {@link trackConversion} for a\n * non-revenue on-page event.\n */\n recordSale(\n input: Omit<SaleInput, \"currency\" | \"occurred_at\" | \"service\" | \"services\"> & {\n service?: TService | null;\n services?: Array<{ service: TService; amount_cents: number }>;\n currency?: SupportedCurrency;\n occurred_at?: string;\n },\n ): Promise<Sale>;\n /**\n * Fire ONLY the on-site conversion for an event-goal `key` — for MANUAL event-goals\n * (`form_submit`, `phone_click`, `cta_click`) the consumer triggers explicitly. No `/sales`\n * write. No-ops when the goal isn't WEBPAGE-mapped (no resolved `send_to`) or the config\n * isn't wired. Consent-gated + de-duped. (Automatic event-goals fire themselves — no call.)\n */\n trackConversion(\n key: string,\n options?: {\n transactionId?: string | null;\n value?: number | null;\n currency?: SupportedCurrency;\n },\n ): void;\n /** Keyset list with optional server sort + opt-in `total_count`. */\n list(query?: SaleListQueryV2): Promise<SaleCursorPage>;\n /**\n * Currency-grouped aggregations for the key's business. Additive v2 options:\n * calendar/custom ranges, IANA `timezone`, `granularity`, and `compare_to`.\n * Legacy `24h/7d/30d` keep their exact prior numbers. Secret key only.\n */\n summary(query: SaleSummaryQueryV2): Promise<SaleSummaryV2>;\n get(id: string): Promise<Sale>;\n update(\n id: string,\n patch: Omit<SaleUpdateInput, \"service\" | \"services\"> & {\n service?: TService | null;\n // Present ⇒ replaces the whole service set (XOR with `service`); the\n // backend recomputes `amount_total_cents` from the sum.\n services?: Array<{ service: TService; amount_cents: number }>;\n },\n ): Promise<Sale>;\n delete(id: string): Promise<void>;\n /** Phone-keyed customer rollups (sk-only). */\n customers: SalesCustomersClient;\n /** Business config (pk or sk). */\n business: SalesBusinessClient;\n}\n\nexport function createSalesClient<TService extends string = string>(\n config: SalesClientConfig,\n): SalesClient<TService> {\n type RecordInput = Parameters<SalesClient<TService>[\"record\"]>[0];\n\n async function record(input: RecordInput): Promise<Sale> {\n const currency = input.currency ?? config.defaultCurrency;\n if (!currency) {\n throw new Error(\n \"record: `currency` is required (pass it on the sale or set config.defaultCurrency)\",\n );\n }\n const body: SaleInput = {\n ...input,\n currency,\n occurred_at: input.occurred_at ?? new Date().toISOString(),\n };\n const sale = await salesRequest<Sale>(config, \"POST\", \"/sales\", body);\n // GAP28: fire the real-time on-site conversion(s) AFTER the sale is recorded, so the\n // gtag transaction_id and the offline path agree. Never throws (best-effort fire).\n const recorded = input.services?.length\n ? input.services.map((s) => ({\n service: s.service,\n amount_cents: s.amount_cents,\n }))\n : [\n {\n service: input.service,\n amount_cents: input.amount_total_cents ?? null,\n },\n ];\n fireRecordedConversions(config.firing, input, recorded, sale, currency);\n return sale;\n }\n\n return {\n record,\n // recordSale is the intent-revealing alias — same behavior, clearer call site.\n recordSale: record,\n\n trackConversion(key, options) {\n // Manual event-goal: fire the WEBPAGE conversion only (no /sales). No-op when the goal\n // isn't WEBPAGE-mapped or the config isn't wired.\n const firing = config.firing?.getFiring(key);\n if (!firing) return;\n const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;\n const cents = firing.value_cents ?? null;\n const value =\n options?.value ??\n (cents != null && currency ? fromMinor(cents, currency as SupportedCurrency) : null);\n fireConversionWithConsent({\n sendTo: firing.send_to,\n value,\n currency,\n transactionId: options?.transactionId ?? null,\n });\n },\n\n async list(query) {\n // Pagination + sort travel at the top level; everything else is a filter.\n const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};\n return salesRequest<SaleCursorPage>(config, \"POST\", \"/sales/query\", {\n filters,\n ...(limit !== undefined ? { limit } : {}),\n ...(cursor !== undefined ? { cursor } : {}),\n ...(sort !== undefined ? { sort } : {}),\n ...(order !== undefined ? { order } : {}),\n ...(want_total !== undefined ? { want_total } : {}),\n });\n },\n\n async summary(query) {\n const {\n range,\n include_categories,\n include_deleted_services,\n top_n,\n since,\n until,\n timezone,\n granularity,\n compare_to,\n ...filters\n } = query;\n return salesRequest<SaleSummaryV2>(config, \"POST\", \"/sales/summary\", {\n filters,\n ...(range !== undefined ? { range } : {}),\n ...(include_categories !== undefined ? { include_categories } : {}),\n ...(include_deleted_services !== undefined ? { include_deleted_services } : {}),\n ...(top_n !== undefined ? { top_n } : {}),\n ...(since !== undefined ? { since } : {}),\n ...(until !== undefined ? { until } : {}),\n ...(timezone !== undefined ? { timezone } : {}),\n ...(granularity !== undefined ? { granularity } : {}),\n ...(compare_to !== undefined ? { compare_to } : {}),\n });\n },\n\n async get(id) {\n return salesRequest<Sale>(config, \"GET\", `/sales/${id}`);\n },\n\n async update(id, patch) {\n return salesRequest<Sale>(config, \"PATCH\", `/sales/${id}`, patch);\n },\n\n async delete(id) {\n await salesRequest<void>(config, \"DELETE\", `/sales/${id}`);\n },\n\n customers: {\n async list(query) {\n const { segment, sort, order, cursor, limit, want_total, ...filters } = query ?? {};\n return salesRequest<CustomerListPage>(config, \"POST\", \"/customers/query\", {\n filters,\n ...(segment !== undefined ? { segment } : {}),\n ...(sort !== undefined ? { sort } : {}),\n ...(order !== undefined ? { order } : {}),\n ...(cursor !== undefined ? { cursor } : {}),\n ...(limit !== undefined ? { limit } : {}),\n ...(want_total !== undefined ? { want_total } : {}),\n });\n },\n async get(id, options) {\n const params = new URLSearchParams();\n if (options?.include_sales !== undefined)\n params.set(\"include_sales\", String(options.include_sales));\n if (options?.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options?.cursor != null) params.set(\"cursor\", options.cursor);\n const qs = params.toString();\n // `id` is the E.164 phone (`+1…`) — must be URL-encoded.\n return salesRequest<CustomerGetResult>(\n config,\n \"GET\",\n `/customers/${encodeURIComponent(id)}${qs ? `?${qs}` : \"\"}`,\n );\n },\n async summary(query) {\n const { range, since, until, timezone, compare_to, ...filters } = query ?? {};\n return salesRequest<CustomerKpis>(config, \"POST\", \"/customers/summary\", {\n filters,\n ...(range !== undefined ? { range } : {}),\n ...(since !== undefined ? { since } : {}),\n ...(until !== undefined ? { until } : {}),\n ...(timezone !== undefined ? { timezone } : {}),\n ...(compare_to !== undefined ? { compare_to } : {}),\n });\n },\n },\n\n business: {\n async config() {\n return salesRequest<BusinessConfig>(config, \"GET\", \"/business/config\");\n },\n },\n };\n}\n","import { z } from \"zod\";\n\n/**\n * Sales / Conversions wire schemas — the client-side source of truth.\n *\n * `saleCreateSchema` is mirrored by `SaleCreateSchema` in\n * `apps/api/src/schemas/tracking_sales.py` and enforced by the backend drift\n * test (the `resources` section of `events.schema.json`). Keep them in lockstep.\n *\n * Money is **integer minor units (cents)**; `quantity` is a decimal string;\n * `currency` is the required `SupportedCurrency` enum.\n */\n\nexport const SUPPORTED_CURRENCIES = [\"USD\", \"CAD\"] as const;\nexport type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number];\n\nconst TRACKING_ENVIRONMENTS = [\"production\", \"development\"] as const;\n\nconst currencySchema = z.enum(SUPPORTED_CURRENCIES);\nconst centsSchema = z.number().int().nonnegative();\nconst quantitySchema = z.string().regex(/^\\d+(\\.\\d{1,3})?$/);\n// Free-form JSON object — the sale's extensibility escape hatch. Mirrors the\n// Pydantic `dict[str, Any]` (`additionalProperties: true`) on the wire.\nconst metadataSchema = z.record(z.unknown());\n\nexport const saleItemSchema = z\n .object({\n external_item_id: z.string().nullable().optional(),\n name: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n quantity: quantitySchema,\n unit_price_cents: centsSchema,\n // Non-negativity validated on the wire — same contract as the other cents\n // fields — and backstopped by the DB CHECK.\n unit_cost_cents: centsSchema.nullable().optional(),\n })\n .strict();\n\n// One service covered by a sale, at its own price. `service` is the per-business\n// service key, validated against the taxonomy by the backend at write time.\nexport const saleServiceSchema = z\n .object({\n service: z.string(),\n amount_cents: centsSchema,\n })\n .strict();\n\n// Raw customer identity. Stored as-is on the backend for human display in\n// dashboards; hashing happens at writeback-upload time in the future Google\n// Ads reconciliation worker (see docs/tracking-package/conversion-writeback.md).\n// The SDK never hashes — pass values straight through.\nconst customerNameSchema = z.string().max(200);\nconst customerPhoneSchema = z.string().max(64);\nconst customerEmailSchema = z.string().max(320).email();\n\n// The singular `service` (priced by `amount_total_cents`) and the plural\n// `services` (each priced individually) are mutually exclusive. When `services`\n// is present, `amount_total_cents` is optional and the backend derives it as the\n// sum; otherwise it is required. Shared by create + update.\nfunction refineServiceXor(\n val: {\n service?: string | null;\n services?: ReadonlyArray<{ service: string; amount_cents: number }> | null;\n amount_total_cents?: number | null;\n },\n ctx: z.RefinementCtx,\n { requireAmount }: { requireAmount: boolean },\n): void {\n if (val.services != null) {\n if (val.service != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"pass either `service` or `services`, not both\",\n path: [\"services\"],\n });\n }\n if (val.services.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not be empty\",\n path: [\"services\"],\n });\n }\n const keys = val.services.map((s) => s.service);\n if (new Set(keys).size !== keys.length) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not list the same service more than once\",\n path: [\"services\"],\n });\n }\n if (val.amount_total_cents != null) {\n const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);\n if (val.amount_total_cents !== sum) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"amount_total_cents must equal the sum of the services amounts \" +\n \"(omit it to derive it automatically)\",\n path: [\"amount_total_cents\"],\n });\n }\n }\n } else if (requireAmount && val.amount_total_cents == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"amount_total_cents is required unless `services` is provided\",\n path: [\"amount_total_cents\"],\n });\n }\n}\n\nexport const saleCreateSchema = z\n .object({\n external_id: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema,\n // Optional only because the plural `services` form derives it from the sum\n // (see refineServiceXor); the singular/serviceless path still requires it.\n amount_total_cents: centsSchema.nullable().optional(),\n occurred_at: z.string().datetime(),\n environment: z.enum(TRACKING_ENVIRONMENTS).default(\"production\"),\n items: z.array(saleItemSchema).default([]),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));\n\nexport const saleUpdateSchema = z\n .object({\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema.optional(),\n amount_total_cents: centsSchema.optional(),\n occurred_at: z.string().datetime().optional(),\n items: z.array(saleItemSchema).optional(),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));\n\nexport type SaleItemInput = z.input<typeof saleItemSchema>;\nexport type SaleServiceInput = z.input<typeof saleServiceSchema>;\n// Use the INPUT type so fields with Zod defaults (environment, items) and\n// occurred_at are optional for callers — the sales client fills them in.\nexport type SaleInput = z.input<typeof saleCreateSchema>;\nexport type SaleUpdateInput = z.input<typeof saleUpdateSchema>;\n\n// ---------------------------------------------------------------------------\n// Read shapes (server responses; not validated on the client)\n// ---------------------------------------------------------------------------\n\nexport interface SaleItem {\n id: string;\n external_item_id: string | null;\n name: string | null;\n category: string | null;\n quantity: string;\n unit_price_cents: number;\n unit_cost_cents: number | null;\n}\n\nexport interface SaleService {\n id: string;\n service_id: string;\n service_key: string;\n service_label: string;\n amount_cents: number;\n}\n\nexport interface Sale {\n id: string;\n business_id: string;\n business_name?: string | null;\n external_id: string | null;\n currency: SupportedCurrency;\n amount_total_cents: number;\n description: string | null;\n // Singular service fields are populated only for single-service sales (kept\n // for backwards compatibility); `services` is always the full set.\n service_id: string | null;\n service_key?: string | null;\n service_label?: string | null;\n services: SaleService[];\n occurred_at: string;\n environment: (typeof TRACKING_ENVIRONMENTS)[number];\n metadata: Record<string, unknown> | null;\n customer_name: string | null;\n customer_phone: string | null;\n customer_email: string | null;\n created_at: string;\n updated_at: string;\n items: SaleItem[];\n}\n\nexport interface SaleListPage {\n items: Sale[];\n total: number;\n}\n\nexport interface SaleCursorPage {\n items: Sale[];\n next_cursor: string | null;\n /** Populated only when `want_total` was requested (a single indexed COUNT). */\n total_count?: number;\n has_more?: boolean;\n}\n\n/**\n * Sortable columns on the secret-key (keyset) and admin (offset) list endpoints.\n * `business_name` only applies to the cross-business admin list — it sorts the\n * joined `businesses.name` column.\n */\nexport type SaleSortField =\n | \"occurred_at\"\n | \"created_at\"\n | \"amount_total_cents\"\n | \"customer_name\"\n | \"business_name\";\n\nexport type SaleSortOrder = \"asc\" | \"desc\";\n\n/**\n * Comprehensive filter shape mirrored from the backend's `SaleQueryFilters`.\n *\n * `search` runs case-insensitively across `customer_name`, `customer_phone`,\n * `customer_email`, `description`, and `external_id` — the human-facing\n * columns. `service_id` is the resolved per-business service UUID (different\n * from the create-time `service` *key*).\n */\nexport interface SaleFilters {\n business_id?: string;\n external_id?: string;\n service_id?: string;\n /** E.164 phone — the canonical customer key; filters sales for one customer. */\n customer_phone?: string;\n currency?: SupportedCurrency;\n environment?: (typeof TRACKING_ENVIRONMENTS)[number];\n since?: string;\n until?: string;\n min_amount_cents?: number;\n max_amount_cents?: number;\n search?: string;\n}\n\n/**\n * Full query input for `SalesClient.list()` — filters + pagination.\n *\n * Pagination is **keyset (cursor)**: `next_cursor` returned by one page is\n * passed back as `cursor` on the next. `null` / undefined cursor = first page.\n *\n * Ordering on this endpoint is fixed at **`occurred_at DESC, id DESC`** — the\n * cursor encodes a position in that index, so a different sort would\n * invalidate cursors mid-pagination. For ad-hoc sorted reads use the\n * dashboard admin endpoint, which is offset-paginated.\n */\nexport interface SaleListQuery extends SaleFilters {\n limit?: number;\n cursor?: string | null;\n}\n\n/** Keyset-safe sort columns (NOT-NULL, indexed). */\nexport type SaleKeysetSortField = \"occurred_at\" | \"created_at\" | \"amount_total_cents\";\n\n/** v2 list query — adds server sort + opt-in total. */\nexport interface SaleListQueryV2 extends SaleListQuery {\n sort?: SaleKeysetSortField;\n order?: SaleSortOrder;\n /** Opt-in: a single indexed COUNT over the filtered set. */\n want_total?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Sales summary (aggregations). Read by `SalesClient.summary()`. Every monetary\n// figure is grouped **by currency** — USD and CAD cents are never summed into a\n// single total. Money stays integer cents; only `quantity` is a decimal string.\n// ---------------------------------------------------------------------------\n\nexport const TRACKING_RANGES = [\"24h\", \"7d\", \"30d\"] as const;\nexport type TrackingOverviewRange = (typeof TRACKING_RANGES)[number];\n\n/** Query input for `SalesClient.summary()` — filters + range + options. */\nexport interface SaleSummaryQuery extends SaleFilters {\n range: TrackingOverviewRange;\n /** Include the per-category line-item breakdown (extra join; default false). */\n include_categories?: boolean;\n /** Surface soft-deleted services individually (flagged \"(deleted)\") instead of\n * rolling them into a single \"Deleted services\" bucket. Default false. */\n include_deleted_services?: boolean;\n /** Cap on the by-service / by-category rows (1–50, default 10). */\n top_n?: number;\n}\n\nexport interface CurrencyRevenue {\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n average_order_value_cents: number;\n}\n\nexport interface SalesServiceBreakdown {\n service_id: string | null;\n service_key: string | null;\n /** \"Unassigned\" when the sale has no service. */\n service_label: string | null;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SalesCategoryBreakdown {\n category: string | null;\n currency: SupportedCurrency;\n /** sum(unit_price_cents * quantity) — advisory, not authoritative. */\n revenue_cents: number;\n /** Decimal string. */\n quantity: string;\n}\n\nexport interface SalesTrendPoint {\n bucket_start: string;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SaleSummary {\n range: TrackingOverviewRange;\n business_id: string | null;\n metrics: {\n sale_count: number;\n distinct_customers: number;\n by_currency: CurrencyRevenue[];\n };\n by_service: SalesServiceBreakdown[];\n by_currency: CurrencyRevenue[];\n by_category: SalesCategoryBreakdown[];\n series: SalesTrendPoint[];\n}\n\n// ---------------------------------------------------------------------------\n// v2 dashboard surface (additive, back-compat). Reads only → plain interfaces,\n// not drift-checked. Legacy `24h/7d/30d` callers get the same numbers; new\n// ranges/options layer on tz-correct calendar windows + comparisons.\n// ---------------------------------------------------------------------------\n\nexport const NAMED_RANGES = [\n \"today\",\n \"yesterday\",\n \"wtd\",\n \"mtd\",\n \"qtd\",\n \"ytd\",\n \"24h\",\n \"7d\",\n \"30d\",\n \"90d\",\n \"custom\",\n] as const;\n/** Superset of `TrackingOverviewRange` (`24h`/`7d`/`30d` stay valid). */\nexport type NamedRange = (typeof NAMED_RANGES)[number];\nexport type Granularity = \"hour\" | \"day\" | \"week\" | \"month\" | \"auto\";\nexport type CompareTo = \"previous_period\" | \"previous_year\" | \"none\";\n\nexport interface DistinctCustomersByCurrency {\n currency: SupportedCurrency;\n distinct_customers: number;\n}\n\nexport interface SummaryWindow {\n since: string;\n until: string;\n}\n\nexport interface SummaryCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n sale_count_delta: number;\n}\n\nexport interface SummaryDeltas {\n by_currency: SummaryCurrencyDelta[];\n sale_count_delta: number;\n distinct_customers_delta: number;\n}\n\nexport interface SaleSummaryPrevious {\n window: SummaryWindow;\n metrics: SaleSummaryV2[\"metrics\"];\n by_currency: CurrencyRevenue[];\n series: SalesTrendPoint[];\n}\n\n/** Query input for `SalesClient.summary()` v2 — widened range + tz/compare options. */\nexport interface SaleSummaryQueryV2 extends SaleFilters {\n range?: NamedRange;\n /** Required when `range === 'custom'`. */\n since?: string;\n until?: string;\n /** IANA tz, e.g. `America/Toronto`. Default = business tz. */\n timezone?: string;\n granularity?: Granularity;\n compare_to?: CompareTo;\n include_categories?: boolean;\n include_deleted_services?: boolean;\n top_n?: number;\n}\n\n/** Superset of `SaleSummary` (assignable to it). */\nexport interface SaleSummaryV2 extends Omit<SaleSummary, \"range\" | \"metrics\"> {\n range: NamedRange;\n metrics: SaleSummary[\"metrics\"] & {\n distinct_customers_by_currency: DistinctCustomersByCurrency[];\n };\n timezone: string | null;\n window: SummaryWindow | null;\n granularity: string | null;\n previous: SaleSummaryPrevious | null;\n deltas: SummaryDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Customers — phone-keyed (a customer IS their E.164 phone). `customer_id` is\n// the E.164 string. Money is per-currency, never summed.\n// ---------------------------------------------------------------------------\n\nexport type CustomerSegment = \"new\" | \"returning\" | \"repeat\" | \"lapsed\";\nexport type CustomerSortField = \"total_spent\" | \"last_purchase\" | \"purchases\" | \"first_purchase\";\n\nexport interface CustomerCurrencyTotal {\n currency: SupportedCurrency;\n revenue_cents: number;\n sale_count: number;\n average_order_value_cents: number;\n}\n\nexport interface CustomerSummary {\n /** The E.164 phone — the canonical customer id. */\n customer_id: string;\n display_name: string | null;\n email: string | null;\n phone: string;\n first_purchase_at: string;\n last_purchase_at: string;\n purchase_count: number;\n segment: CustomerSegment;\n totals: CustomerCurrencyTotal[];\n}\n\nexport type CustomerProfile = CustomerSummary;\n\nexport interface CustomerListQuery extends Omit<SaleFilters, \"external_id\"> {\n segment?: CustomerSegment;\n sort?: CustomerSortField;\n order?: SaleSortOrder;\n cursor?: string | null;\n limit?: number;\n want_total?: boolean;\n}\n\nexport interface CustomerListPage {\n items: CustomerSummary[];\n next_cursor: string | null;\n total_count: number | null;\n}\n\nexport interface CustomerGetOptions {\n include_sales?: boolean;\n limit?: number;\n cursor?: string | null;\n}\n\nexport interface CustomerGetResult {\n customer: CustomerProfile;\n /** Present only when `include_sales: true` (and a business-scoped key). */\n sales?: SaleCursorPage | null;\n}\n\nexport interface CustomerSummaryQuery extends SaleFilters {\n range?: NamedRange;\n since?: string;\n until?: string;\n timezone?: string;\n compare_to?: CompareTo;\n}\n\nexport interface CustomerSegmentCount {\n segment: CustomerSegment;\n count: number;\n}\n\nexport interface CustomerCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n}\n\nexport interface CustomerKpisDeltas {\n total_customers_delta: number;\n new_customers_delta: number;\n returning_customers_delta: number;\n repeat_rate_delta: number;\n ltv_by_currency: CustomerCurrencyDelta[];\n}\n\nexport interface CustomerKpisPrevious {\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n ltv_by_currency: CustomerCurrencyTotal[];\n}\n\nexport interface CustomerKpis {\n range: NamedRange;\n timezone: string;\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n by_segment: CustomerSegmentCount[];\n ltv_by_currency: CustomerCurrencyTotal[];\n /** Present only when `compare_to` is set. */\n previous?: CustomerKpisPrevious | null;\n deltas?: CustomerKpisDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Business config — supersets `fetchServices()`.\n// ---------------------------------------------------------------------------\n\nexport interface BusinessConfigService {\n key: string;\n label: string;\n archived: boolean;\n}\n\nexport interface BusinessConfigFeatures {\n customers: boolean;\n comparisons: boolean;\n retention: boolean;\n}\n\nexport interface BusinessConfig {\n business_id: string;\n display_name: string;\n timezone: string;\n primary_currency: SupportedCurrency;\n currencies: SupportedCurrency[];\n default_phone_country: string | null;\n services: BusinessConfigService[];\n features: BusinessConfigFeatures;\n}\n","import { salesRequest, type SalesTransportConfig } from \"./sales/transport\";\n\n/** A business's active service, as returned by `GET /tracking/services`. */\nexport interface PublicServiceItem {\n key: string;\n label: string;\n}\n\n/**\n * Fetch the caller's business's active service taxonomy. Accepts a public or\n * secret key (the taxonomy is low-sensitivity category names). Powers the\n * `@aranova/tracking-cli gen` codegen.\n */\nexport async function fetchServices(config: SalesTransportConfig): Promise<PublicServiceItem[]> {\n return salesRequest<PublicServiceItem[]>(config, \"GET\", \"/services\");\n}\n\n/** Labelled gtag map as returned by `GET /tracking/gtags` (`gtag_ids` field). */\nexport type GtagIdMap = Record<string, string>;\n\nexport type MetaPixelIdMap = Record<string, string>;\n\n/**\n * Render the `ARANOVA_GTAG_IDS` block — sorted labels, `{} as const` when empty\n * so the export (and consumer imports) always exist.\n *\n * KEEP IN SYNC with the byte-identical twin in\n * `packages/tracking-cli/src/gen.ts::renderGtagIdsSection`.\n */\nexport function renderGtagIdsSection(gtagIds: GtagIdMap): string {\n const labels = Object.keys(gtagIds).sort();\n const mapLiteral =\n labels.length === 0\n ? \"{}\"\n : `{\\n${labels\n .map((label) => ` ${JSON.stringify(label)}: ${JSON.stringify(gtagIds[label])},`)\n .join(\"\\n\")}\\n}`;\n\n return `// Google Ads tag IDs registered for this business (backend-driven — manage\n// them in the dashboard's Google Ads panel). ALL entries are loaded\n// simultaneously via gtag('config', ...).\nexport const ARANOVA_GTAG_IDS = ${mapLiteral} as const satisfies GtagEnvironmentMap;\n`;\n}\n\n/**\n * Render the `ARANOVA_META_PIXEL_IDS` block — sorted labels, `{} as const` when\n * empty so the export (and consumer imports) always exist.\n *\n * KEEP IN SYNC with the byte-identical twin in\n * `packages/tracking-cli/src/gen.ts::renderMetaPixelIdsSection`.\n */\nexport function renderMetaPixelIdsSection(metaPixelIds: MetaPixelIdMap): string {\n const labels = Object.keys(metaPixelIds).sort();\n const mapLiteral =\n labels.length === 0\n ? \"{}\"\n : `{\\n${labels\n .map((label) => ` ${JSON.stringify(label)}: ${JSON.stringify(metaPixelIds[label])},`)\n .join(\"\\n\")}\\n}`;\n\n return `// Meta Pixel IDs registered for this business (backend-driven — manage them in\n// the dashboard's Meta panel). ALL entries are loaded via fbq('init', ...).\nexport const ARANOVA_META_PIXEL_IDS = ${mapLiteral} as const satisfies MetaPixelEnvironmentMap;\n`;\n}\n\n/**\n * Render the generated TypeScript module: an `as const` array of `{ key, label }`\n * items (sorted by key → stable diffs), the `AranovaService` key union, a\n * `ARANOVA_SERVICE_LABELS` lookup typed against that union, the\n * `ARANOVA_GTAG_IDS` map, and the `ARANOVA_META_PIXEL_IDS` map.\n *\n * KEEP IN SYNC with the byte-identical twin in\n * `packages/tracking-cli/src/gen.ts::renderServicesModule`. The CLI cannot import\n * this (tracking-core is private/unpublished), so the two are independent copies;\n * the emitted format is a consumer-pinned contract. Each side's test asserts the\n * exact output for a fixed input — change both together.\n */\nexport function renderServicesModule(\n items: PublicServiceItem[],\n gtagIds: GtagIdMap = {},\n metaPixelIds: MetaPixelIdMap = {},\n sdkPackage = \"@aranova/tracking-react\",\n): string {\n const sorted = [...items].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));\n\n const itemsLiteral =\n sorted.length === 0\n ? \"[]\"\n : `[\\n${sorted\n .map((i) => ` { key: ${JSON.stringify(i.key)}, label: ${JSON.stringify(i.label)} },`)\n .join(\"\\n\")}\\n]`;\n\n const labelsLiteral =\n sorted.length === 0\n ? \"{}\"\n : `{\\n${sorted\n .map((i) => ` ${JSON.stringify(i.key)}: ${JSON.stringify(i.label)},`)\n .join(\"\\n\")}\\n}`;\n\n return `// AUTO-GENERATED by \\`@aranova/tracking-cli gen\\` — do not edit by hand.\n// Re-run the command to refresh after changing services in the dashboard.\n\nimport type { GtagEnvironmentMap, MetaPixelEnvironmentMap } from ${JSON.stringify(sdkPackage)};\n\nexport const ARANOVA_SERVICES = ${itemsLiteral} as const;\n\nexport type AranovaService = (typeof ARANOVA_SERVICES)[number][\"key\"];\n\nexport const ARANOVA_SERVICE_LABELS: Record<AranovaService, string> = ${labelsLiteral};\n\n${renderGtagIdsSection(gtagIds)}\n${renderMetaPixelIdsSection(metaPixelIds)}`;\n}\n"],"mappings":"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","names":["import_zod"]}
1
+ {"version":3,"sources":["../src/sales.ts","../../tracking-core/src/consent.ts","../../tracking-core/src/gtag.ts","../../tracking-core/src/resources/conversion-firing.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/session.ts","../../tracking-core/src/ingest.ts","../../tracking-core/src/resources/sales/errors.ts","../../tracking-core/src/resources/sales/transport.ts","../../tracking-core/src/resources/sales/money.ts","../../tracking-core/src/resources/sales/client.ts","../../tracking-core/src/resources/sales/schema.ts","../../tracking-core/src/resources/services.ts"],"sourcesContent":["// Server-safe sales SDK entry — `@aranova/tracking-react/sales`. React-free; the\n// recommended import for server/serverless modules (API routes, Node scripts).\n// The surface is authored once in tracking-core (see sales-public.ts); this file\n// exists only as the per-package tsup build entry.\nexport * from \"../../tracking-core/src/sales-public\";\n","import type { ConsentState } from \"./types\";\n\n/**\n * localStorage key for the visitor's consent choice.\n */\nexport const CONSENT_STATE_KEY = \"consent_state\";\n\n/**\n * localStorage key for the ISO timestamp when consent was last changed.\n */\nexport const CONSENT_TIMESTAMP_KEY = \"consent_timestamp\";\n\n/**\n * Google Consent Mode value sent to `gtag('consent', 'update', ...)`.\n */\nexport type GtagConsentValue = \"granted\" | \"denied\";\n\n/**\n * Consent Mode v2 payload sent to Google Ads when consent changes.\n */\nexport interface ConsentUpdatePayload {\n ad_storage: GtagConsentValue;\n ad_user_data: GtagConsentValue;\n ad_personalization: GtagConsentValue;\n analytics_storage: GtagConsentValue;\n}\n\ntype ConsentGrantedListener = () => void;\nconst grantedListeners = new Set<ConsentGrantedListener>();\n\n/**\n * Subscribe to consent being granted. Used to flush conversions that were queued while\n * consent was still pending (GAP28). Returns an unsubscribe function.\n */\nexport function onConsentGranted(listener: ConsentGrantedListener): () => void {\n grantedListeners.add(listener);\n return () => {\n grantedListeners.delete(listener);\n };\n}\n\n/**\n * Build the Google Consent Mode v2 update payload for a single consent state.\n */\nexport function buildConsentPayload(state: GtagConsentValue): ConsentUpdatePayload {\n return {\n ad_storage: state,\n ad_user_data: state,\n ad_personalization: state,\n analytics_storage: state,\n };\n}\n\n/**\n * Read the persisted visitor consent state from localStorage.\n *\n * Returns `pending` when called during SSR or before the visitor has made a\n * choice.\n */\nexport function getConsentState(): ConsentState {\n if (typeof window === \"undefined\") return \"pending\";\n\n try {\n const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);\n if (storedState === \"granted\" || storedState === \"denied\") return storedState;\n } catch {\n // localStorage blocked (sandboxed iframe / storage disabled) — treat as pending\n }\n\n return \"pending\";\n}\n\n/**\n * Persist a visitor consent choice and update Google Consent Mode when gtag is\n * loaded.\n */\nexport function setConsentState(state: GtagConsentValue): void {\n if (typeof window === \"undefined\") return;\n\n try {\n window.localStorage.setItem(CONSENT_STATE_KEY, state);\n window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, new Date().toISOString());\n } catch {\n // localStorage blocked — still push the live consent update below so the\n // visitor's click takes effect for this page load even if it can't persist\n }\n\n if (typeof window.gtag === \"function\")\n window.gtag(\"consent\", \"update\", buildConsentPayload(state));\n\n // Mirror the choice to the Meta Pixel so one banner gates both ad platforms.\n if (typeof window.fbq === \"function\")\n window.fbq(\"consent\", state === \"granted\" ? \"grant\" : \"revoke\");\n\n // Flush any conversions queued while consent was pending (GAP28).\n if (state === \"granted\") {\n for (const listener of grantedListeners) {\n try {\n listener();\n } catch {\n // a listener must never break the consent flow\n }\n }\n }\n}\n\n/**\n * Clear the stored consent choice so the banner re-appears on next render.\n *\n * Power a \"Cookie preferences\" link in a footer so visitors can change their\n * mind without losing access to your site:\n *\n * ```tsx\n * const { reset } = useConsent();\n * <button onClick={reset}>Cookie preferences</button>\n * ```\n *\n * Does NOT push an `update` to gtag — there's nothing to update because the\n * visitor hasn't chosen anything yet. The next `setConsentState()` call will\n * sync gtag once they re-choose.\n */\nexport function resetConsent(): void {\n if (typeof window === \"undefined\") return;\n\n try {\n window.localStorage.removeItem(CONSENT_STATE_KEY);\n window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);\n } catch {\n // localStorage blocked — nothing to clear\n }\n}\n\n/**\n * Re-apply a stored consent choice to Google Consent Mode.\n *\n * Useful after bootstrapping gtag on a page that already has a saved choice.\n */\nexport function restoreStoredConsent(): ConsentState {\n const consentState = getConsentState();\n\n if (consentState === \"granted\" || consentState === \"denied\") setConsentState(consentState);\n\n return consentState;\n}\n","// This module loads the Google tag (gtag.js) and today fires ONLY `gtag('config', …)`\n// plus Consent Mode — i.e. it builds Google Ads REMARKETING audiences once the visitor\n// accepts the consent banner. It does not fire `gtag('event','conversion')` yet, so\n// conversions are currently tracked OFFLINE via the backend writeback worker\n// (UPLOAD_CLICKS actions). Real-time on-site conversion firing through this tag is\n// planned (gap 28). See docs/google-ads-deployment/gtags-and-conversion-tracking.md.\n\nimport { buildConsentPayload, getConsentState, restoreStoredConsent } from \"./consent\";\nimport type { GtagEnvironmentMap } from \"./types\";\n\n/**\n * Google Tag Manager script host used for gtag loading.\n */\nexport const GTAG_SCRIPT_HOST = \"https://www.googletagmanager.com/gtag/js\";\n\n/**\n * Attribute used to mark scripts inserted by the tracking SDK.\n */\nexport const TRACKING_SCRIPT_ATTRIBUTE = \"data-aranova-tracking\";\n\nfunction getScriptMarker(id: string): string {\n return `aranova-${id}`;\n}\n\n/**\n * Pattern matching valid Google Ads / GA4 tag IDs.\n *\n * Accepted formats: `AW-123456789`, `G-XXXXXXXXXX`, `GT-XXXXXXX`, `DC-XXXXXXX`.\n * IDs that don't match are silently dropped to prevent script injection when\n * interpolated into inline `<script>` content.\n */\nconst GTAG_ID_PATTERN = /^[A-Z]{1,3}-[A-Za-z0-9_-]+$/;\n\nexport function isValidGtagId(id: string): boolean {\n return GTAG_ID_PATTERN.test(id);\n}\n\n/**\n * Create an inline script that initializes gtag with denied consent defaults.\n */\nexport function createConsentDefaultScript(): string {\n return `\nwindow.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\nwindow.gtag = gtag;\ngtag('consent', 'default', {\n ad_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n analytics_storage: 'denied',\n wait_for_update: 500,\n});\n`.trim();\n}\n\n/**\n * Create an inline script that initializes gtag for a Google Ads tag id.\n */\nexport function createGtagInitScript(gtagId: string): string {\n if (!isValidGtagId(gtagId)) return \"\";\n return `\nwindow.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\nwindow.gtag = gtag;\ngtag('js', new Date());\ngtag('config', '${gtagId}');\n`.trim();\n}\n\n/**\n * Create an inline script that restores a stored consent choice into gtag.\n */\nexport function createConsentRestoreScript(): string {\n // Guarded: window.localStorage access throws a SecurityError in sandboxed\n // iframes / storage-blocked contexts; an uncaught throw would abort the inline\n // <Script> and leave a prior consent choice un-applied.\n return `\ntry {\n var consentState = window.localStorage.getItem('consent_state');\n if (window.gtag && (consentState === 'granted' || consentState === 'denied')) {\n window.gtag('consent', 'update', {\n ad_storage: consentState,\n ad_user_data: consentState,\n ad_personalization: consentState,\n analytics_storage: consentState,\n });\n }\n} catch (e) {}\n`.trim();\n}\n\n/**\n * Ensure `window.gtag` and `window.dataLayer` exist and return the gtag shim.\n */\nexport function ensureGtagFunction(): NonNullable<Window[\"gtag\"]> {\n window.dataLayer = window.dataLayer || [];\n\n if (typeof window.gtag === \"function\") return window.gtag;\n\n // CRITICAL: push the `arguments` object, NOT a rest-param array. gtag.js only\n // treats the canonical `dataLayer.push(arguments)` form as a command — a plain\n // array pushed to the dataLayer is ignored. With the array form, every\n // gtag('config' | 'consent' | 'event') call silently no-ops: the account never\n // registers, consent mode never applies, and conversions never fire (verified\n // live — the array form sends zero network hits; the arguments form sends the\n // pagead/conversion ping). Mirror Google's canonical inline snippet exactly.\n function gtag(): void {\n // eslint-disable-next-line prefer-rest-params\n window.dataLayer?.push(arguments);\n }\n window.gtag = gtag;\n\n return window.gtag;\n}\n\nconst SEND_TO_RE = /^AW-[A-Za-z0-9]+\\/[A-Za-z0-9_-]+$/;\n\n/** A gtag `send_to` target, e.g. `AW-123456789/AbC-D_efg`. */\nexport function isValidSendTo(sendTo: string): boolean {\n return SEND_TO_RE.test(sendTo);\n}\n\nexport interface GtagConversionInput {\n /** `AW-<id>/<label>` — the conversion action's firing target. */\n sendTo: string;\n /** Conversion value in MAJOR units; omit to let Google apply the action's default. */\n value?: number | null;\n currency?: string | null;\n transactionId?: string | null;\n}\n\n/**\n * Fire a real-time on-site conversion — `gtag('event','conversion',{ send_to, … })`\n * (GAP28). Browser-gated and never throws: `record()` is isomorphic and runs server-side\n * under a secret key where `window`/`gtag` are absent, so this no-ops there. Consent\n * gating + de-dup are the caller's responsibility (see `fireConversionWithConsent`).\n * Returns whether the event was actually pushed.\n */\nexport function fireGtagConversion(input: GtagConversionInput): boolean {\n if (typeof window === \"undefined\" || typeof window.gtag !== \"function\") return false;\n if (!isValidSendTo(input.sendTo)) return false;\n const params: Record<string, unknown> = { send_to: input.sendTo };\n if (input.value != null) params.value = input.value;\n if (input.currency) params.currency = input.currency;\n if (input.transactionId) params.transaction_id = input.transactionId;\n try {\n window.gtag(\"event\", \"conversion\", params);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Apply denied-by-default Google Consent Mode state.\n */\nexport function applyDefaultConsentState(): void {\n const gtag = ensureGtagFunction();\n gtag(\"consent\", \"default\", {\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n analytics_storage: \"denied\",\n wait_for_update: 500,\n });\n}\n\n/**\n * Load the external gtag script once for the provided Google Ads tag id.\n */\nexport function loadGtagScript(gtagId: string): void {\n if (typeof document === \"undefined\") return;\n\n const marker = getScriptMarker(\"gtag-loader\");\n const existingScript = document.querySelector<HTMLScriptElement>(\n `script[${TRACKING_SCRIPT_ATTRIBUTE}=\"${marker}\"]`,\n );\n if (existingScript) return;\n\n const script = document.createElement(\"script\");\n script.async = true;\n script.src = `${GTAG_SCRIPT_HOST}?id=${encodeURIComponent(gtagId)}`;\n script.setAttribute(TRACKING_SCRIPT_ATTRIBUTE, marker);\n document.head.append(script);\n}\n\n/**\n * Send the initial `gtag('js')` and `gtag('config')` calls.\n */\nexport function initializeGtag(gtagId: string): void {\n const gtag = ensureGtagFunction();\n gtag(\"js\", new Date());\n gtag(\"config\", gtagId);\n}\n\n/**\n * Push the stored consent choice to gtag if one exists.\n */\nexport function restoreConsentState(): void {\n if (typeof window === \"undefined\") return;\n\n const consentState = getConsentState();\n if (consentState === \"granted\" || consentState === \"denied\")\n window.gtag?.(\"consent\", \"update\", buildConsentPayload(consentState));\n}\n\n/**\n * Load and initialize Google Ads tracking with Consent Mode support.\n *\n * Used by framework components and browser-script initialization.\n */\nexport function bootstrapGoogleAdsTracking(gtagId: string): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n if (!isValidGtagId(gtagId)) return;\n\n applyDefaultConsentState();\n loadGtagScript(gtagId);\n initializeGtag(gtagId);\n restoreStoredConsent();\n}\n\n/**\n * Load and initialize Google Ads tracking for ALL labelled gtag IDs.\n *\n * Every ID in the map gets a `gtag('config', ...)` call — gtag natively\n * supports multiple configured tags on the same page. The script loader\n * only runs once (for the first ID); subsequent IDs reuse the shared\n * `dataLayer`.\n */\nexport function bootstrapMultipleGtags(gtagIds: GtagEnvironmentMap): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n const ids = Object.values(gtagIds).filter(\n (id): id is string => typeof id === \"string\" && isValidGtagId(id),\n );\n if (ids.length === 0) return;\n\n applyDefaultConsentState();\n loadGtagScript(ids[0]!);\n\n // Call gtag('js') once, then gtag('config') per ID. initializeGtag\n // calls both, but gtag('js') should only fire once per page load.\n const gtag = ensureGtagFunction();\n gtag(\"js\", new Date());\n for (const id of ids) {\n gtag(\"config\", id);\n }\n\n restoreStoredConsent();\n}\n\n/**\n * Create an inline script that initializes gtag for multiple Google Ads tag ids.\n *\n * Used by the Next.js `GoogleAdsTracking` component when rendering `<Script>`\n * tags for a multi-gtag configuration.\n */\nexport function createGtagInitScriptMulti(gtagIds: string[]): string {\n const safeIds = gtagIds.filter(isValidGtagId);\n const configs = safeIds.map((id) => `gtag('config', '${id}');`).join(\"\\n\");\n return `\nwindow.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\nwindow.gtag = gtag;\ngtag('js', new Date());\n${configs}\n`.trim();\n}\n","// Consent-gated, de-duped orchestration around the raw `fireGtagConversion` (GAP28).\n//\n// granted → fire now (de-duped by transaction_id + send_to via sessionStorage)\n// pending → queue, flush automatically when consent is granted\n// denied → drop\n//\n// `fireGtagConversion` is itself browser-gated and never throws, so this is safe to call\n// from the isomorphic sales `record()` — server-side it simply no-ops.\n\nimport { getConsentState, onConsentGranted } from \"../consent\";\nimport { fireGtagConversion, type GtagConversionInput } from \"../gtag\";\n\nconst DEDUP_PREFIX = \"_aranova_conv_\";\n// Cap the queue so a visitor who never accepts the banner can't grow it unbounded; we drop\n// the oldest entry past the cap (the most recent conversions are the ones worth keeping).\nconst MAX_PENDING = 100;\nconst pendingQueue: GtagConversionInput[] = [];\n\nfunction dedupKey(input: GtagConversionInput): string {\n return `${DEDUP_PREFIX}${input.transactionId ?? \"\"}:${input.sendTo}`;\n}\n\n// Has this (transaction_id, send_to) already fired? Read-only — the key is written ONLY\n// after a successful fire (see `fireOnce`), so a fire that never happened (e.g. gtag not\n// ready yet) is never suppressed on retry. Best-effort: a blocked sessionStorage reads as\n// \"not fired\" so the conversion still gets a chance.\nfunction alreadyFired(input: GtagConversionInput): boolean {\n if (!input.transactionId || typeof window === \"undefined\") return false;\n try {\n return window.sessionStorage.getItem(dedupKey(input)) !== null;\n } catch {\n return false;\n }\n}\n\nfunction markFired(input: GtagConversionInput): void {\n if (!input.transactionId || typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(dedupKey(input), \"1\");\n } catch {\n // sessionStorage unavailable — Google's counting_type is the authoritative backstop\n }\n}\n\n// Fire once: skip if already fired, and only record the guard AFTER the fire succeeds.\nfunction fireOnce(input: GtagConversionInput): void {\n if (alreadyFired(input)) return;\n if (fireGtagConversion(input)) markFired(input);\n}\n\n/** Fire a conversion through the consent gate + de-dup guard. */\nexport function fireConversionWithConsent(input: GtagConversionInput): void {\n const state = getConsentState();\n if (state === \"denied\") return;\n if (state === \"pending\") {\n if (pendingQueue.length >= MAX_PENDING) pendingQueue.shift();\n pendingQueue.push(input);\n return;\n }\n fireOnce(input);\n}\n\n/** Flush conversions queued while consent was pending; called on consent-grant. */\nexport function flushPendingConversions(): void {\n if (getConsentState() !== \"granted\") return;\n while (pendingQueue.length > 0) {\n const input = pendingQueue.shift();\n if (input) fireOnce(input);\n }\n}\n\n// Subscribe once at module load so a grant flushes the queue regardless of how the queue\n// was filled (browser-only; no-ops under SSR where there is no consent flow).\nif (typeof window !== \"undefined\") onConsentGranted(flushPendingConversions);\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_view` event.\n *\n * The SDK emits this on initial load, SPA route changes, and bfcache restores.\n * Consumers do not call `trackEvent('page_view', ...)`; registering\n * `automatic: { page_view: {} }` enables the SDK-owned trigger.\n */\nexport const pageViewMetadataSchema = z\n .object({\n page: z\n .object({\n title: z.string().nullable(),\n path: z.string(),\n search: z.string(),\n hash: z.string(),\n })\n .strict(),\n referrer: z.string().nullable(),\n // `.nullable().optional()` — absent (undefined) OR explicit null OR a\n // real viewport object. Mirrors Pydantic's `_Viewport | None = None`\n // on the backend side so the drift test stays clean.\n viewport: z\n .object({\n w: z.number(),\n h: z.number(),\n })\n .strict()\n .nullable()\n .optional(),\n })\n .strict();\n\nexport type PageViewMetadata = z.infer<typeof pageViewMetadataSchema>;\n\n/**\n * Registration config for automatic `page_view`.\n *\n * `page_view` is required in every trigger registry and currently has no\n * options. Use `{ page_view: {} }`.\n */\nexport const pageViewConfigSchema = z.object({}).strict();\nexport type PageViewConfig = z.infer<typeof pageViewConfigSchema>;\n","// Visitor + session identity for the tracking SDK.\n//\n// Visitor: persistent localStorage UUID, never expires until the user clears\n// browser storage. Used for cross-session correlation.\n//\n// Session: rolling 30-minute idle window. Regenerated when more than\n// SESSION_IDLE_MS has passed since the last event. Matches the behavior of\n// GA4, PostHog, Mixpanel, etc., so analytics is comparable.\n\n/**\n * localStorage key for the persistent visitor id.\n */\nexport const VISITOR_STORAGE_KEY = \"aranova_tracking_visitor\";\n\n/**\n * localStorage key for the rolling session id state.\n */\nexport const SESSION_STORAGE_KEY = \"aranova_tracking_session\";\n\n/**\n * Idle window before a new session id is created.\n */\nexport const SESSION_IDLE_MS = 30 * 60 * 1000;\n\n/**\n * Serialized session state stored in localStorage.\n */\nexport interface StoredSession {\n /** Client-generated session UUID. */\n id: string;\n /** Unix timestamp in milliseconds for the most recent event/session touch. */\n last_event_at: number;\n}\n\nfunction safeUuid(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\")\n return crypto.randomUUID();\n // Fallback for ancient browsers — not cryptographically perfect but unique enough.\n return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage may be denied (private mode, blocked cookies, quota). Caller is\n // responsible for degrading gracefully.\n }\n}\n\n/**\n * Return the persistent visitor id for this browser profile.\n *\n * Creates and stores a new id when one does not already exist. During SSR,\n * returns an ephemeral id because browser storage is unavailable.\n */\nexport function getVisitorId(): string {\n if (typeof window === \"undefined\") return safeUuid();\n\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n if (existing && existing.length > 0) return existing;\n\n const fresh = safeUuid();\n writeLocalStorage(VISITOR_STORAGE_KEY, fresh);\n return fresh;\n}\n\n/**\n * Result from `getOrRotateSessionId()`.\n */\nexport interface SessionIdResult {\n /** Current session id. */\n id: string;\n /** Whether this call created a new session. */\n isNew: boolean;\n}\n\n/**\n * Return the current session id, rotating it after the idle window expires.\n *\n * Also refreshes `last_event_at` for active sessions.\n */\nexport function getOrRotateSessionId(now: number = Date.now()): SessionIdResult {\n if (typeof window === \"undefined\") return { id: safeUuid(), isNew: true };\n\n const raw = readLocalStorage(SESSION_STORAGE_KEY);\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredSession>;\n if (typeof parsed.id === \"string\" && typeof parsed.last_event_at === \"number\") {\n if (now - parsed.last_event_at <= SESSION_IDLE_MS) {\n const refreshed: StoredSession = { id: parsed.id, last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));\n return { id: parsed.id, isNew: false };\n }\n }\n } catch {\n // Fall through to a fresh session.\n }\n }\n\n const fresh: StoredSession = { id: safeUuid(), last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));\n return { id: fresh.id, isNew: true };\n}\n\n/**\n * Clear visitor and session identity from localStorage.\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(VISITOR_STORAGE_KEY);\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n // ignored\n }\n}\n","// Tracking ingest client. Queues events, debounce-flushes them to the\n// /tracking/events endpoint, and degrades silently on errors so a broken\n// network never breaks the host site.\n\nimport { getConsentState } from \"./consent\";\nimport { resetPageViewState } from \"./page-view\";\nimport type { PhoneConfig } from \"./phone-field\";\nimport { captureFbc, getFbcCookie, getFbpCookie } from \"./fbq\";\nimport {\n captureTrackingParamsFromLocation,\n createEmptyTrackingParams,\n getCookieValueFromDocument,\n getTrackingParamsFromCookieReader,\n mergeTrackingParams,\n} from \"./tracking\";\nimport type { TriggerRegistryConfig } from \"./events/registry\";\nimport { buildHeartbeatMetadata } from \"./heartbeat\";\nimport { getOrRotateSessionId, getVisitorId } from \"./session\";\nimport type {\n TrackingClientContext,\n TrackingEnvironment,\n TrackingInstallSurface,\n TrackingParams,\n TrackingSessionUpsertPayload,\n} from \"./types\";\n\n/**\n * Default debounce window before queued events are flushed.\n */\nexport const DEFAULT_FLUSH_INTERVAL_MS = 2000;\n\n/**\n * Default queue size that triggers an immediate flush.\n */\nexport const DEFAULT_MAX_QUEUE_SIZE = 10;\n\n/**\n * Hard server-side maximum event count per request body.\n */\nexport const HARD_MAX_BATCH = 50;\n\n/**\n * Header used to authenticate public tracking ingest requests.\n */\nexport const API_KEY_HEADER = \"X-Aranova-Api-Key\";\n\n/**\n * Identity headers stamped on every ingest request. Duplicate fields already\n * present in `session.context` but survive body-parse failures so the backend\n * can attribute 422s to the offending SDK install.\n */\nexport const SDK_VERSION_HEADER = \"X-Aranova-Sdk-Version\";\nexport const SDK_PACKAGE_HEADER = \"X-Aranova-Sdk-Package\";\nexport const SDK_SURFACE_HEADER = \"X-Aranova-Sdk-Surface\";\nexport const SDK_ENVIRONMENT_HEADER = \"X-Aranova-Sdk-Environment\";\n\n/**\n * Configuration for the low-level ingest client.\n *\n * Framework packages usually create this for you through `createTracking()`.\n */\nexport interface TrackingClientConfig {\n /** Public tracking API key issued for the business. */\n apiKey: string;\n /** Tracking endpoint base URL, usually ending in `/tracking`. */\n endpoint: string;\n /** SDK surface creating this client. */\n surface: TrackingInstallSurface;\n /** Package version reported in session context and heartbeat metadata. */\n sdkVersion?: string;\n /** Package name reported in session context and heartbeat metadata. */\n packageName?: string;\n /** Trigger registry so the heartbeat can report registered events. */\n triggers?: TriggerRegistryConfig;\n /** Override the default 2s debounce window. */\n flushIntervalMs?: number;\n /** Override the default 10-event batch trigger. */\n maxQueueSize?: number;\n /** Deployment environment label reported in session context. */\n environment?: TrackingEnvironment;\n /** All active gtag IDs, keyed by label. Included in session context. */\n activeGtagIds?: Record<string, string>;\n /** When true, swallow nothing — useful for tests. */\n debug?: boolean;\n /** Phone-field config, carried for parity; the React hook reads it via context. */\n phone?: PhoneConfig;\n}\n\n/**\n * Input accepted by the low-level stringly-typed client.\n *\n * Prefer the typed `trackEvent(eventName, metadata)` facade exposed by\n * `useTracking()` in React/Next integrations.\n */\nexport interface TrackEventInput {\n /** Event name to enqueue. */\n eventType: string;\n /** URL associated with the event. Defaults to the current page URL. */\n pageUrl?: string | null;\n /** Event-specific metadata. */\n metadata?: Record<string, unknown> | null;\n /** Timestamp override. Defaults to queue time. */\n occurredAt?: Date | string | null;\n}\n\ninterface QueuedEvent {\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n}\n\n/**\n * Low-level tracking client responsible for queueing and flushing events.\n */\nexport interface TrackingClient {\n /** Enqueue an event for batched delivery. */\n trackEvent: (input: TrackEventInput) => void;\n /** Flush queued events immediately. */\n flush: () => Promise<void>;\n /** Return the current rolling session id. */\n getSessionId: () => string;\n /** Return the persistent visitor id. */\n getVisitorId: () => string;\n /** Remove timers/listeners and prevent future flushes. */\n destroy: () => void;\n}\n\ninterface IngestRequestBody {\n session: TrackingSessionUpsertPayload;\n events: Array<{\n event_type: string;\n page_url: string | null;\n metadata: Record<string, unknown> | null;\n occurred_at: string | null;\n }>;\n}\n\nfunction buildContext(\n surface: TrackingInstallSurface,\n sdkVersion: string | null,\n packageName: string | null,\n environment: TrackingEnvironment,\n activeGtagIds: Record<string, string> | null,\n): TrackingClientContext {\n return {\n surface,\n sdk_version: sdkVersion,\n package_name: packageName,\n site_origin: typeof window === \"undefined\" ? null : window.location.origin,\n page_title: typeof document === \"undefined\" ? null : document.title || null,\n referrer: typeof document === \"undefined\" ? null : document.referrer || null,\n environment,\n active_gtag_ids: activeGtagIds,\n };\n}\n\nfunction readTrackingParams(): TrackingParams {\n if (typeof window === \"undefined\") return createEmptyTrackingParams();\n // Capture from URL on every read so the first event in a session reflects the\n // landing-page params even if the cookie helper hasn't run yet.\n try {\n captureTrackingParamsFromLocation();\n } catch {\n // ignore\n }\n return getTrackingParamsFromCookieReader(getCookieValueFromDocument);\n}\n\nfunction consentSnapshot(): Record<string, unknown> | null {\n try {\n return { state: getConsentState() };\n } catch {\n return null;\n }\n}\n\ninterface IdentityHeaders {\n sdkVersion: string;\n packageName: string;\n surface: string;\n environment: string;\n}\n\nasync function postWithFetch(\n url: string,\n body: string,\n apiKey: string,\n identity: IdentityHeaders,\n keepalive: boolean,\n): Promise<void> {\n if (typeof fetch !== \"function\") return;\n try {\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n [API_KEY_HEADER]: apiKey,\n [SDK_VERSION_HEADER]: identity.sdkVersion,\n [SDK_PACKAGE_HEADER]: identity.packageName,\n [SDK_SURFACE_HEADER]: identity.surface,\n [SDK_ENVIRONMENT_HEADER]: identity.environment,\n },\n body,\n keepalive,\n // CORS is open on the tracking endpoint; never send cookies.\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch {\n // fire-and-forget — never throw to the host site\n }\n}\n\nlet globalClient: TrackingClient | null = null;\nlet globalClientKey: string | null = null;\n\nfunction clientConfigKey(config: TrackingClientConfig): string {\n return `${config.apiKey}@${config.endpoint}#${config.surface}`;\n}\n\n/**\n * Return a page-level singleton tracking client. Creating the client anew on\n * every component mount is wrong — React StrictMode double-mounts dev-only,\n * and destroying+recreating the client between the cleanup and re-run strips\n * away the pushState patch that SPA auto page view relies on. A singleton\n * survives all of that: the client lives for the entire page, and providers\n * just attach/detach auto page view against it.\n *\n * If `apiKey` / `endpoint` / `surface` change between calls, the previous\n * singleton is destroyed and a new one replaces it. This covers hot-config\n * changes without leaking state.\n */\nexport function getOrCreateTrackingClient(config: TrackingClientConfig): TrackingClient {\n const key = clientConfigKey(config);\n if (globalClient !== null && globalClientKey === key) {\n return globalClient;\n }\n if (globalClient !== null) {\n globalClient.destroy();\n }\n globalClient = createTrackingClient(config);\n globalClientKey = key;\n return globalClient;\n}\n\n/**\n * Tear down the singleton if any. Primarily an escape hatch for tests where\n * each test should see a fresh client; production code rarely needs this.\n * Also clears the in-session SPA referrer so the next test starts with a\n * fresh referrer chain.\n */\nexport function resetGlobalTrackingClient(): void {\n if (globalClient !== null) {\n globalClient.destroy();\n }\n globalClient = null;\n globalClientKey = null;\n resetPageViewState();\n}\n\n/**\n * Create a low-level ingest client.\n *\n * The client queues events, debounces network flushes, sends an SDK heartbeat\n * once per new session, and swallows network errors so analytics never break\n * the host site.\n */\nexport function createTrackingClient(config: TrackingClientConfig): TrackingClient {\n const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);\n const sdkVersion = config.sdkVersion ?? null;\n const packageName = config.packageName ?? null;\n // Default to 'production' so the wire payload always carries a valid enum\n // value. Sending null would fail the backend's strict enum validation.\n const environment: TrackingEnvironment = config.environment ?? \"production\";\n const activeGtagIds = config.activeGtagIds ?? null;\n const endpointBase = config.endpoint.replace(/\\/$/, \"\");\n const eventsUrl = `${endpointBase}/events`;\n const identityHeaders: IdentityHeaders = {\n sdkVersion: sdkVersion ?? \"\",\n packageName: packageName ?? \"\",\n surface: config.surface,\n environment,\n };\n\n let queue: QueuedEvent[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n let firstPage: string | null = null;\n // Attribution captured synchronously at client creation (before any SPA router\n // can strip the query string). Merged into every session payload as a fallback\n // so a blocked-cookie webview or a stripped URL still attributes.\n let initialParams = createEmptyTrackingParams();\n let destroyed = false;\n\n // Initialize identity early so the first POST has stable values.\n const visitorId = getVisitorId();\n const initialSession = getOrRotateSessionId();\n let sessionId = initialSession.id;\n\n if (typeof window !== \"undefined\") {\n firstPage = window.location.href;\n try {\n initialParams = captureTrackingParamsFromLocation();\n } catch {\n // ignore — never break the host site\n }\n try {\n captureFbc();\n } catch {\n // ignore\n }\n }\n\n // Queue an sdk_heartbeat event at the start of every new session.\n function enqueueHeartbeat(): void {\n const metadata = buildHeartbeatMetadata(\n config.surface,\n sdkVersion,\n packageName,\n config.triggers ?? null,\n activeGtagIds,\n );\n queue.push({\n event_type: \"sdk_heartbeat\",\n page_url: typeof window === \"undefined\" ? null : window.location.href,\n metadata: metadata as unknown as Record<string, unknown>,\n occurred_at: new Date().toISOString(),\n });\n }\n\n if (initialSession.isNew) {\n enqueueHeartbeat();\n }\n\n function buildSessionPayload(): TrackingSessionUpsertPayload {\n const rotated = getOrRotateSessionId();\n if (rotated.isNew && rotated.id !== sessionId) {\n // Session rotated mid-page (idle > 30min then user returned).\n enqueueHeartbeat();\n }\n sessionId = rotated.id;\n // Prefer the live cookie/localStorage read; fall back to the init-time\n // snapshot (covers a webview that blocked the cookie AND a router that\n // already stripped the landing URL by flush time).\n const params = mergeTrackingParams(readTrackingParams(), initialParams);\n const context = buildContext(\n config.surface,\n sdkVersion,\n packageName,\n environment,\n activeGtagIds,\n );\n return {\n session_id: sessionId,\n visitor_id: visitorId,\n gclid: params.gclid,\n fbclid: params.fbclid,\n fbc: getFbcCookie(),\n fbp: getFbpCookie(),\n utm_source: params.utm_source,\n utm_medium: params.utm_medium,\n utm_campaign: params.utm_campaign,\n utm_term: params.utm_term,\n utm_content: params.utm_content,\n first_page: firstPage,\n consent_state: consentSnapshot(),\n context,\n };\n }\n\n function scheduleFlush(): void {\n if (flushTimer !== null || destroyed) return;\n flushTimer = setTimeout(() => {\n flushTimer = null;\n void flush();\n }, flushIntervalMs);\n }\n\n function clearScheduledFlush(): void {\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n }\n\n async function flush(): Promise<void> {\n if (queue.length === 0) return;\n\n const events = queue.slice(0, HARD_MAX_BATCH);\n queue = queue.slice(events.length);\n clearScheduledFlush();\n\n const body: IngestRequestBody = {\n session: buildSessionPayload(),\n events,\n };\n\n const serialized = JSON.stringify(body);\n\n // Prefer fetch + keepalive for better error visibility. sendBeacon is the\n // pagehide fallback path because it survives navigation away.\n await postWithFetch(eventsUrl, serialized, config.apiKey, identityHeaders, false);\n }\n\n function trackEvent(input: TrackEventInput): void {\n if (destroyed) return;\n if (!input || typeof input.eventType !== \"string\" || input.eventType.length === 0) return;\n\n const occurredAt =\n input.occurredAt instanceof Date\n ? input.occurredAt.toISOString()\n : typeof input.occurredAt === \"string\"\n ? input.occurredAt\n : new Date().toISOString();\n\n queue.push({\n event_type: input.eventType,\n page_url: input.pageUrl ?? (typeof window === \"undefined\" ? null : window.location.href),\n metadata: input.metadata ?? null,\n occurred_at: occurredAt,\n });\n\n if (queue.length >= maxQueueSize) {\n void flush();\n } else {\n scheduleFlush();\n }\n }\n\n function flushOnUnload(): void {\n if (queue.length === 0) return;\n const events = queue.slice(0, HARD_MAX_BATCH);\n queue = queue.slice(events.length);\n clearScheduledFlush();\n\n const body: IngestRequestBody = {\n session: buildSessionPayload(),\n events,\n };\n const serialized = JSON.stringify(body);\n void postWithFetch(eventsUrl, serialized, config.apiKey, identityHeaders, true);\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", flushOnUnload);\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"hidden\") flushOnUnload();\n });\n }\n\n return {\n trackEvent,\n flush,\n getSessionId: () => sessionId,\n getVisitorId: () => visitorId,\n destroy: () => {\n destroyed = true;\n // Fire any pending events through the keepalive path before tearing\n // down. Critical for React StrictMode in dev, where the provider's\n // first mount is immediately unmounted and its 2s debounce would\n // otherwise drop the initial page_view on the floor. Uses fetch\n // keepalive so the request survives the component tearing down.\n if (queue.length > 0) {\n flushOnUnload();\n }\n clearScheduledFlush();\n queue = [];\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"pagehide\", flushOnUnload);\n }\n },\n };\n}\n","/**\n * Error thrown by the sales client (`createSalesClient`) on a\n * non-2xx response. Unlike the fire-and-forget event queue (which swallows\n * failures), a sale is a transaction the caller must be able to react to.\n */\nexport class AranovaApiError extends Error {\n readonly status: number;\n readonly code: string | undefined;\n readonly requestId: string | undefined;\n\n constructor(message: string, options: { status: number; code?: string; requestId?: string }) {\n super(message);\n this.name = \"AranovaApiError\";\n this.status = options.status;\n this.code = options.code;\n this.requestId = options.requestId;\n }\n}\n","import {\n API_KEY_HEADER,\n SDK_ENVIRONMENT_HEADER,\n SDK_PACKAGE_HEADER,\n SDK_SURFACE_HEADER,\n SDK_VERSION_HEADER,\n} from \"../../ingest\";\nimport { AranovaApiError } from \"./errors\";\n\n/** Shared config for every sales HTTP helper. */\nexport interface SalesTransportConfig {\n /** Public (`aranv_pk_…`) or secret (`aranv_sk_…`) API key. */\n apiKey: string;\n /**\n * Base tracking endpoint, e.g. `https://aranovainternal-production.up.railway.app/tracking`.\n * The `/sales` path is appended by the helpers.\n */\n endpoint: string;\n /** Optional SDK identity headers (mirrors the event ingest client). */\n sdkVersion?: string;\n packageName?: string;\n surface?: string;\n environment?: string;\n}\n\nfunction identityHeaders(config: SalesTransportConfig): Record<string, string> {\n const headers: Record<string, string> = { [API_KEY_HEADER]: config.apiKey };\n if (config.sdkVersion) headers[SDK_VERSION_HEADER] = config.sdkVersion;\n if (config.packageName) headers[SDK_PACKAGE_HEADER] = config.packageName;\n if (config.surface) headers[SDK_SURFACE_HEADER] = config.surface;\n if (config.environment) headers[SDK_ENVIRONMENT_HEADER] = config.environment;\n return headers;\n}\n\nfunction joinUrl(endpoint: string, path: string): string {\n return `${endpoint.replace(/\\/$/, \"\")}${path}`;\n}\n\n/**\n * Single awaited request used by every sales helper. Unlike the event queue,\n * this surfaces failures: any non-2xx rejects with an {@link AranovaApiError}.\n * Returns `undefined` for 204 No Content.\n */\nexport async function salesRequest<T>(\n config: SalesTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n const headers = identityHeaders(config);\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n const response = await fetch(joinUrl(config.endpoint, path), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n\n if (!response.ok) {\n let detail: string | undefined;\n let code: string | undefined;\n try {\n const parsed: unknown = await response.json();\n if (parsed && typeof parsed === \"object\") {\n const record = parsed as Record<string, unknown>;\n if (typeof record.detail === \"string\") detail = record.detail;\n if (typeof record.code === \"string\") code = record.code;\n }\n } catch {\n // non-JSON error body — fall back to status text\n }\n throw new AranovaApiError(detail ?? response.statusText ?? \"Request failed\", {\n status: response.status,\n code,\n requestId: response.headers.get(\"x-request-id\") ?? undefined,\n });\n }\n\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n}\n","import type { SupportedCurrency } from \"./schema\";\n\n/**\n * Money conversion helpers.\n *\n * The API speaks **integer minor units (cents)** exclusively. These helpers move\n * between a human-facing major amount (dollars) and cents, and format cents for\n * display — so a consumer always has both representations without float math.\n */\n\n// Minor-unit exponent per supported currency (USD/CAD = 2 decimal places).\nconst MINOR_UNIT_EXPONENT: Record<SupportedCurrency, number> = {\n USD: 2,\n CAD: 2,\n};\n\nfunction exponentFor(currency: SupportedCurrency): number {\n return MINOR_UNIT_EXPONENT[currency] ?? 2;\n}\n\n/**\n * Convert a major amount (dollars `250.5`) to integer minor units (`25050`).\n *\n * Convenience only — the wire is always integer cents. Uses float multiply +\n * `Math.round`, so values that aren't exactly representable in binary float\n * (e.g. `1.005`) can round to the neighbouring cent. If you already hold an\n * exact cents integer, pass it straight through and skip this helper.\n */\nexport function toMinor(amount: number, currency: SupportedCurrency): number {\n return Math.round(amount * 10 ** exponentFor(currency));\n}\n\n/** Convert integer minor units (`25050`) to a major amount (`250.5`). */\nexport function fromMinor(cents: number, currency: SupportedCurrency): number {\n return cents / 10 ** exponentFor(currency);\n}\n\n/**\n * Format integer minor units as a localized currency string (e.g. `\"$250.50\"`).\n * Uses the built-in `Intl.NumberFormat` — no extra dependency.\n */\nexport function formatMoney(cents: number, currency: SupportedCurrency, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(\n fromMinor(cents, currency),\n );\n}\n\n/**\n * Format an ISO timestamp in a specific IANA time zone (e.g. `America/Toronto`),\n * so dashboards stop hand-rolling `Intl`. Defaults to a short date-time; pass\n * `opts` to override fields. The `timeZone` is always forced to the argument.\n */\nexport function formatDateInTz(\n iso: string,\n timeZone: string,\n opts?: Intl.DateTimeFormatOptions,\n locale?: string,\n): string {\n const date = new Date(iso);\n // Guard malformed input: Intl.format(Invalid Date) throws a RangeError.\n if (Number.isNaN(date.getTime())) return iso;\n return new Intl.DateTimeFormat(locale, {\n year: \"numeric\",\n month: \"short\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n ...opts,\n timeZone,\n }).format(date);\n}\n","import type {\n BusinessConfig,\n CustomerGetOptions,\n CustomerGetResult,\n CustomerKpis,\n CustomerListPage,\n CustomerListQuery,\n CustomerSummaryQuery,\n Sale,\n SaleCursorPage,\n SaleInput,\n SaleListQueryV2,\n SaleSummaryQueryV2,\n SaleSummaryV2,\n SaleUpdateInput,\n SupportedCurrency,\n} from \"./schema\";\nimport type { ConversionConfigStore } from \"../conversion-config\";\nimport { fireConversionWithConsent } from \"../conversion-firing\";\nimport { salesRequest, type SalesTransportConfig } from \"./transport\";\nimport { fromMinor } from \"./money\";\n\n/** Config for {@link createSalesClient}. */\nexport interface SalesClientConfig extends SalesTransportConfig {\n /** Applied when an individual `record()` call omits `currency`. */\n defaultCurrency?: SupportedCurrency;\n /**\n * GAP28: when present, `record()` ALSO fires a real-time on-site conversion\n * (`gtag('event','conversion')`) for any recorded service that has a `firing` send_to in\n * the resolved config. Consent-gated + de-duped; no-ops server-side. Wire it from\n * `resolveConversionConfig(...)`.\n */\n firing?: Pick<ConversionConfigStore, \"getFiring\">;\n}\n\n// Fire the on-site conversion(s) for a just-recorded sale. Per-service value = the sale's\n// amount when present, else the action's configured default from config (Google applies\n// its own default when neither is sent). transaction_id ties the gtag fire to the sale so\n// retries/reloads de-dup within the WEBPAGE action.\nfunction fireRecordedConversions(\n firing: Pick<ConversionConfigStore, \"getFiring\"> | undefined,\n input: { external_id?: string | null; amount_total_cents?: number | null },\n recorded: { service?: string | null; amount_cents: number | null }[],\n sale: Sale,\n currency: SupportedCurrency,\n): void {\n if (!firing) return;\n const txnBase = input.external_id ?? sale.id;\n for (const item of recorded) {\n if (!item.service) continue;\n const config = firing.getFiring(item.service);\n if (!config) continue;\n const cents = item.amount_cents ?? config.value_cents ?? null;\n fireConversionWithConsent({\n sendTo: config.send_to,\n value: cents != null ? fromMinor(cents, currency) : null,\n currency: config.currency ?? currency,\n transactionId: `${txnBase}:${item.service}`,\n });\n }\n}\n\n/** Phone-keyed customer rollups (sk-only; a public key gets a `403`). */\nexport interface SalesCustomersClient {\n list(query?: CustomerListQuery): Promise<CustomerListPage>;\n /** `id` is the customer's E.164 phone. */\n get(id: string, options?: CustomerGetOptions): Promise<CustomerGetResult>;\n summary(query?: CustomerSummaryQuery): Promise<CustomerKpis>;\n}\n\n/** Business config (low-sensitivity — accepts a public or secret key). */\nexport interface SalesBusinessClient {\n config(): Promise<BusinessConfig>;\n}\n\n/**\n * One isomorphic sales client — what a key may *do* is enforced by the backend,\n * not by hiding methods. A **public** key (`aranv_pk_…`) may `record` (the\n * backend rejects reads/CRUD from it with a `403`); a **secret** key\n * (`aranv_sk_…`), used **server-side only**, gets full read/list/update/delete.\n * Never ship a secret key in a browser bundle.\n *\n * Generic over the service-key union `TService` and the manual-conversion key union\n * `TConversion`: bind the types emitted by `@aranova/tracking-cli gen` (`AranovaService`\n * and `AranovaConversion`) for compile-time-checked `service` / `trackConversion` keys.\n */\nexport interface SalesClient<\n TService extends string = string,\n TConversion extends string = string,\n> {\n record(\n input: Omit<SaleInput, \"currency\" | \"occurred_at\" | \"service\" | \"services\"> & {\n service?: TService | null;\n // XOR with `service`: record multiple services in one sale, each priced\n // individually. `amount_total_cents` is then optional (the backend derives\n // it as the sum). Keys are checked against the codegen `TService` union.\n services?: Array<{ service: TService; amount_cents: number }>;\n currency?: SupportedCurrency;\n occurred_at?: string;\n },\n ): Promise<Sale>;\n /**\n * Record a revenue sale — the intent-revealing alias of {@link record} in the unified-goal\n * API. POSTs `/sales` and ALSO fires the on-site conversion when the sale-goal is\n * WEBPAGE-mapped. Use this for anything with real revenue; use {@link trackConversion} for a\n * non-revenue on-page event.\n */\n recordSale(\n input: Omit<SaleInput, \"currency\" | \"occurred_at\" | \"service\" | \"services\"> & {\n service?: TService | null;\n services?: Array<{ service: TService; amount_cents: number }>;\n currency?: SupportedCurrency;\n occurred_at?: string;\n },\n ): Promise<Sale>;\n /**\n * Fire ONLY the on-site conversion for an event-goal `key` — for MANUAL event-goals\n * (`form_submit`, `phone_click`, `cta_click`) the consumer triggers explicitly. No `/sales`\n * write. No-ops when the goal isn't WEBPAGE-mapped (no resolved `send_to`) or the config\n * isn't wired. Consent-gated + de-duped. (Automatic event-goals fire themselves — no call.)\n */\n trackConversion(\n key: TConversion,\n options?: {\n transactionId?: string | null;\n value?: number | null;\n currency?: SupportedCurrency;\n },\n ): void;\n /** Keyset list with optional server sort + opt-in `total_count`. */\n list(query?: SaleListQueryV2): Promise<SaleCursorPage>;\n /**\n * Currency-grouped aggregations for the key's business. Additive v2 options:\n * calendar/custom ranges, IANA `timezone`, `granularity`, and `compare_to`.\n * Legacy `24h/7d/30d` keep their exact prior numbers. Secret key only.\n */\n summary(query: SaleSummaryQueryV2): Promise<SaleSummaryV2>;\n get(id: string): Promise<Sale>;\n update(\n id: string,\n patch: Omit<SaleUpdateInput, \"service\" | \"services\"> & {\n service?: TService | null;\n // Present ⇒ replaces the whole service set (XOR with `service`); the\n // backend recomputes `amount_total_cents` from the sum.\n services?: Array<{ service: TService; amount_cents: number }>;\n },\n ): Promise<Sale>;\n delete(id: string): Promise<void>;\n /** Phone-keyed customer rollups (sk-only). */\n customers: SalesCustomersClient;\n /** Business config (pk or sk). */\n business: SalesBusinessClient;\n}\n\nexport function createSalesClient<\n TService extends string = string,\n TConversion extends string = string,\n>(config: SalesClientConfig): SalesClient<TService, TConversion> {\n type RecordInput = Parameters<SalesClient<TService, TConversion>[\"record\"]>[0];\n\n async function record(input: RecordInput): Promise<Sale> {\n const currency = input.currency ?? config.defaultCurrency;\n if (!currency) {\n throw new Error(\n \"record: `currency` is required (pass it on the sale or set config.defaultCurrency)\",\n );\n }\n const body: SaleInput = {\n ...input,\n currency,\n occurred_at: input.occurred_at ?? new Date().toISOString(),\n };\n const sale = await salesRequest<Sale>(config, \"POST\", \"/sales\", body);\n // GAP28: fire the real-time on-site conversion(s) AFTER the sale is recorded, so the\n // gtag transaction_id and the offline path agree. Never throws (best-effort fire).\n const recorded = input.services?.length\n ? input.services.map((s) => ({\n service: s.service,\n amount_cents: s.amount_cents,\n }))\n : [\n {\n service: input.service,\n amount_cents: input.amount_total_cents ?? null,\n },\n ];\n fireRecordedConversions(config.firing, input, recorded, sale, currency);\n return sale;\n }\n\n return {\n record,\n // recordSale is the intent-revealing alias — same behavior, clearer call site.\n recordSale: record,\n\n trackConversion(key, options) {\n // Manual event-goal: fire the WEBPAGE conversion only (no /sales). No-op when the goal\n // isn't WEBPAGE-mapped or the config isn't wired.\n const firing = config.firing?.getFiring(key);\n if (!firing) return;\n const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;\n const cents = firing.value_cents ?? null;\n const value =\n options?.value ??\n (cents != null && currency ? fromMinor(cents, currency as SupportedCurrency) : null);\n fireConversionWithConsent({\n sendTo: firing.send_to,\n value,\n currency,\n transactionId: options?.transactionId ?? null,\n });\n },\n\n async list(query) {\n // Pagination + sort travel at the top level; everything else is a filter.\n const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};\n return salesRequest<SaleCursorPage>(config, \"POST\", \"/sales/query\", {\n filters,\n ...(limit !== undefined ? { limit } : {}),\n ...(cursor !== undefined ? { cursor } : {}),\n ...(sort !== undefined ? { sort } : {}),\n ...(order !== undefined ? { order } : {}),\n ...(want_total !== undefined ? { want_total } : {}),\n });\n },\n\n async summary(query) {\n const {\n range,\n include_categories,\n include_deleted_services,\n top_n,\n since,\n until,\n timezone,\n granularity,\n compare_to,\n ...filters\n } = query;\n return salesRequest<SaleSummaryV2>(config, \"POST\", \"/sales/summary\", {\n filters,\n ...(range !== undefined ? { range } : {}),\n ...(include_categories !== undefined ? { include_categories } : {}),\n ...(include_deleted_services !== undefined ? { include_deleted_services } : {}),\n ...(top_n !== undefined ? { top_n } : {}),\n ...(since !== undefined ? { since } : {}),\n ...(until !== undefined ? { until } : {}),\n ...(timezone !== undefined ? { timezone } : {}),\n ...(granularity !== undefined ? { granularity } : {}),\n ...(compare_to !== undefined ? { compare_to } : {}),\n });\n },\n\n async get(id) {\n return salesRequest<Sale>(config, \"GET\", `/sales/${id}`);\n },\n\n async update(id, patch) {\n return salesRequest<Sale>(config, \"PATCH\", `/sales/${id}`, patch);\n },\n\n async delete(id) {\n await salesRequest<void>(config, \"DELETE\", `/sales/${id}`);\n },\n\n customers: {\n async list(query) {\n const { segment, sort, order, cursor, limit, want_total, ...filters } = query ?? {};\n return salesRequest<CustomerListPage>(config, \"POST\", \"/customers/query\", {\n filters,\n ...(segment !== undefined ? { segment } : {}),\n ...(sort !== undefined ? { sort } : {}),\n ...(order !== undefined ? { order } : {}),\n ...(cursor !== undefined ? { cursor } : {}),\n ...(limit !== undefined ? { limit } : {}),\n ...(want_total !== undefined ? { want_total } : {}),\n });\n },\n async get(id, options) {\n const params = new URLSearchParams();\n if (options?.include_sales !== undefined)\n params.set(\"include_sales\", String(options.include_sales));\n if (options?.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options?.cursor != null) params.set(\"cursor\", options.cursor);\n const qs = params.toString();\n // `id` is the E.164 phone (`+1…`) — must be URL-encoded.\n return salesRequest<CustomerGetResult>(\n config,\n \"GET\",\n `/customers/${encodeURIComponent(id)}${qs ? `?${qs}` : \"\"}`,\n );\n },\n async summary(query) {\n const { range, since, until, timezone, compare_to, ...filters } = query ?? {};\n return salesRequest<CustomerKpis>(config, \"POST\", \"/customers/summary\", {\n filters,\n ...(range !== undefined ? { range } : {}),\n ...(since !== undefined ? { since } : {}),\n ...(until !== undefined ? { until } : {}),\n ...(timezone !== undefined ? { timezone } : {}),\n ...(compare_to !== undefined ? { compare_to } : {}),\n });\n },\n },\n\n business: {\n async config() {\n return salesRequest<BusinessConfig>(config, \"GET\", \"/business/config\");\n },\n },\n };\n}\n","import { z } from \"zod\";\n\n/**\n * Sales / Conversions wire schemas — the client-side source of truth.\n *\n * `saleCreateSchema` is mirrored by `SaleCreateSchema` in\n * `apps/api/src/schemas/tracking_sales.py` and enforced by the backend drift\n * test (the `resources` section of `events.schema.json`). Keep them in lockstep.\n *\n * Money is **integer minor units (cents)**; `quantity` is a decimal string;\n * `currency` is the required `SupportedCurrency` enum.\n */\n\nexport const SUPPORTED_CURRENCIES = [\"USD\", \"CAD\"] as const;\nexport type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number];\n\nconst TRACKING_ENVIRONMENTS = [\"production\", \"development\"] as const;\n\nconst currencySchema = z.enum(SUPPORTED_CURRENCIES);\nconst centsSchema = z.number().int().nonnegative();\nconst quantitySchema = z.string().regex(/^\\d+(\\.\\d{1,3})?$/);\n// Free-form JSON object — the sale's extensibility escape hatch. Mirrors the\n// Pydantic `dict[str, Any]` (`additionalProperties: true`) on the wire.\nconst metadataSchema = z.record(z.unknown());\n\nexport const saleItemSchema = z\n .object({\n external_item_id: z.string().nullable().optional(),\n name: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n quantity: quantitySchema,\n unit_price_cents: centsSchema,\n // Non-negativity validated on the wire — same contract as the other cents\n // fields — and backstopped by the DB CHECK.\n unit_cost_cents: centsSchema.nullable().optional(),\n })\n .strict();\n\n// One service covered by a sale, at its own price. `service` is the per-business\n// service key, validated against the taxonomy by the backend at write time.\nexport const saleServiceSchema = z\n .object({\n service: z.string(),\n amount_cents: centsSchema,\n })\n .strict();\n\n// Raw customer identity. Stored as-is on the backend for human display in\n// dashboards; hashing happens at writeback-upload time in the future Google\n// Ads reconciliation worker (see docs/tracking-package/conversion-writeback.md).\n// The SDK never hashes — pass values straight through.\nconst customerNameSchema = z.string().max(200);\nconst customerPhoneSchema = z.string().max(64);\nconst customerEmailSchema = z.string().max(320).email();\n\n// The singular `service` (priced by `amount_total_cents`) and the plural\n// `services` (each priced individually) are mutually exclusive. When `services`\n// is present, `amount_total_cents` is optional and the backend derives it as the\n// sum; otherwise it is required. Shared by create + update.\nfunction refineServiceXor(\n val: {\n service?: string | null;\n services?: ReadonlyArray<{ service: string; amount_cents: number }> | null;\n amount_total_cents?: number | null;\n },\n ctx: z.RefinementCtx,\n { requireAmount }: { requireAmount: boolean },\n): void {\n if (val.services != null) {\n if (val.service != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"pass either `service` or `services`, not both\",\n path: [\"services\"],\n });\n }\n if (val.services.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not be empty\",\n path: [\"services\"],\n });\n }\n const keys = val.services.map((s) => s.service);\n if (new Set(keys).size !== keys.length) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not list the same service more than once\",\n path: [\"services\"],\n });\n }\n if (val.amount_total_cents != null) {\n const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);\n if (val.amount_total_cents !== sum) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"amount_total_cents must equal the sum of the services amounts \" +\n \"(omit it to derive it automatically)\",\n path: [\"amount_total_cents\"],\n });\n }\n }\n } else if (requireAmount && val.amount_total_cents == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"amount_total_cents is required unless `services` is provided\",\n path: [\"amount_total_cents\"],\n });\n }\n}\n\nexport const saleCreateSchema = z\n .object({\n external_id: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema,\n // Optional only because the plural `services` form derives it from the sum\n // (see refineServiceXor); the singular/serviceless path still requires it.\n amount_total_cents: centsSchema.nullable().optional(),\n occurred_at: z.string().datetime(),\n environment: z.enum(TRACKING_ENVIRONMENTS).default(\"production\"),\n items: z.array(saleItemSchema).default([]),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));\n\nexport const saleUpdateSchema = z\n .object({\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema.optional(),\n amount_total_cents: centsSchema.optional(),\n occurred_at: z.string().datetime().optional(),\n items: z.array(saleItemSchema).optional(),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));\n\nexport type SaleItemInput = z.input<typeof saleItemSchema>;\nexport type SaleServiceInput = z.input<typeof saleServiceSchema>;\n// Use the INPUT type so fields with Zod defaults (environment, items) and\n// occurred_at are optional for callers — the sales client fills them in.\nexport type SaleInput = z.input<typeof saleCreateSchema>;\nexport type SaleUpdateInput = z.input<typeof saleUpdateSchema>;\n\n// ---------------------------------------------------------------------------\n// Read shapes (server responses; not validated on the client)\n// ---------------------------------------------------------------------------\n\nexport interface SaleItem {\n id: string;\n external_item_id: string | null;\n name: string | null;\n category: string | null;\n quantity: string;\n unit_price_cents: number;\n unit_cost_cents: number | null;\n}\n\nexport interface SaleService {\n id: string;\n service_id: string;\n service_key: string;\n service_label: string;\n amount_cents: number;\n}\n\nexport interface Sale {\n id: string;\n business_id: string;\n business_name?: string | null;\n external_id: string | null;\n currency: SupportedCurrency;\n amount_total_cents: number;\n description: string | null;\n // Singular service fields are populated only for single-service sales (kept\n // for backwards compatibility); `services` is always the full set.\n service_id: string | null;\n service_key?: string | null;\n service_label?: string | null;\n services: SaleService[];\n occurred_at: string;\n environment: (typeof TRACKING_ENVIRONMENTS)[number];\n metadata: Record<string, unknown> | null;\n customer_name: string | null;\n customer_phone: string | null;\n customer_email: string | null;\n created_at: string;\n updated_at: string;\n items: SaleItem[];\n}\n\nexport interface SaleListPage {\n items: Sale[];\n total: number;\n}\n\nexport interface SaleCursorPage {\n items: Sale[];\n next_cursor: string | null;\n /** Populated only when `want_total` was requested (a single indexed COUNT). */\n total_count?: number;\n has_more?: boolean;\n}\n\n/**\n * Sortable columns on the secret-key (keyset) and admin (offset) list endpoints.\n * `business_name` only applies to the cross-business admin list — it sorts the\n * joined `businesses.name` column.\n */\nexport type SaleSortField =\n | \"occurred_at\"\n | \"created_at\"\n | \"amount_total_cents\"\n | \"customer_name\"\n | \"business_name\";\n\nexport type SaleSortOrder = \"asc\" | \"desc\";\n\n/**\n * Comprehensive filter shape mirrored from the backend's `SaleQueryFilters`.\n *\n * `search` runs case-insensitively across `customer_name`, `customer_phone`,\n * `customer_email`, `description`, and `external_id` — the human-facing\n * columns. `service_id` is the resolved per-business service UUID (different\n * from the create-time `service` *key*).\n */\nexport interface SaleFilters {\n business_id?: string;\n external_id?: string;\n service_id?: string;\n /** E.164 phone — the canonical customer key; filters sales for one customer. */\n customer_phone?: string;\n currency?: SupportedCurrency;\n environment?: (typeof TRACKING_ENVIRONMENTS)[number];\n since?: string;\n until?: string;\n min_amount_cents?: number;\n max_amount_cents?: number;\n search?: string;\n}\n\n/**\n * Full query input for `SalesClient.list()` — filters + pagination.\n *\n * Pagination is **keyset (cursor)**: `next_cursor` returned by one page is\n * passed back as `cursor` on the next. `null` / undefined cursor = first page.\n *\n * Ordering on this endpoint is fixed at **`occurred_at DESC, id DESC`** — the\n * cursor encodes a position in that index, so a different sort would\n * invalidate cursors mid-pagination. For ad-hoc sorted reads use the\n * dashboard admin endpoint, which is offset-paginated.\n */\nexport interface SaleListQuery extends SaleFilters {\n limit?: number;\n cursor?: string | null;\n}\n\n/** Keyset-safe sort columns (NOT-NULL, indexed). */\nexport type SaleKeysetSortField = \"occurred_at\" | \"created_at\" | \"amount_total_cents\";\n\n/** v2 list query — adds server sort + opt-in total. */\nexport interface SaleListQueryV2 extends SaleListQuery {\n sort?: SaleKeysetSortField;\n order?: SaleSortOrder;\n /** Opt-in: a single indexed COUNT over the filtered set. */\n want_total?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Sales summary (aggregations). Read by `SalesClient.summary()`. Every monetary\n// figure is grouped **by currency** — USD and CAD cents are never summed into a\n// single total. Money stays integer cents; only `quantity` is a decimal string.\n// ---------------------------------------------------------------------------\n\nexport const TRACKING_RANGES = [\"24h\", \"7d\", \"30d\"] as const;\nexport type TrackingOverviewRange = (typeof TRACKING_RANGES)[number];\n\n/** Query input for `SalesClient.summary()` — filters + range + options. */\nexport interface SaleSummaryQuery extends SaleFilters {\n range: TrackingOverviewRange;\n /** Include the per-category line-item breakdown (extra join; default false). */\n include_categories?: boolean;\n /** Surface soft-deleted services individually (flagged \"(deleted)\") instead of\n * rolling them into a single \"Deleted services\" bucket. Default false. */\n include_deleted_services?: boolean;\n /** Cap on the by-service / by-category rows (1–50, default 10). */\n top_n?: number;\n}\n\nexport interface CurrencyRevenue {\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n average_order_value_cents: number;\n}\n\nexport interface SalesServiceBreakdown {\n service_id: string | null;\n service_key: string | null;\n /** \"Unassigned\" when the sale has no service. */\n service_label: string | null;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SalesCategoryBreakdown {\n category: string | null;\n currency: SupportedCurrency;\n /** sum(unit_price_cents * quantity) — advisory, not authoritative. */\n revenue_cents: number;\n /** Decimal string. */\n quantity: string;\n}\n\nexport interface SalesTrendPoint {\n bucket_start: string;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SaleSummary {\n range: TrackingOverviewRange;\n business_id: string | null;\n metrics: {\n sale_count: number;\n distinct_customers: number;\n by_currency: CurrencyRevenue[];\n };\n by_service: SalesServiceBreakdown[];\n by_currency: CurrencyRevenue[];\n by_category: SalesCategoryBreakdown[];\n series: SalesTrendPoint[];\n}\n\n// ---------------------------------------------------------------------------\n// v2 dashboard surface (additive, back-compat). Reads only → plain interfaces,\n// not drift-checked. Legacy `24h/7d/30d` callers get the same numbers; new\n// ranges/options layer on tz-correct calendar windows + comparisons.\n// ---------------------------------------------------------------------------\n\nexport const NAMED_RANGES = [\n \"today\",\n \"yesterday\",\n \"wtd\",\n \"mtd\",\n \"qtd\",\n \"ytd\",\n \"24h\",\n \"7d\",\n \"30d\",\n \"90d\",\n \"custom\",\n] as const;\n/** Superset of `TrackingOverviewRange` (`24h`/`7d`/`30d` stay valid). */\nexport type NamedRange = (typeof NAMED_RANGES)[number];\nexport type Granularity = \"hour\" | \"day\" | \"week\" | \"month\" | \"auto\";\nexport type CompareTo = \"previous_period\" | \"previous_year\" | \"none\";\n\nexport interface DistinctCustomersByCurrency {\n currency: SupportedCurrency;\n distinct_customers: number;\n}\n\nexport interface SummaryWindow {\n since: string;\n until: string;\n}\n\nexport interface SummaryCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n sale_count_delta: number;\n}\n\nexport interface SummaryDeltas {\n by_currency: SummaryCurrencyDelta[];\n sale_count_delta: number;\n distinct_customers_delta: number;\n}\n\nexport interface SaleSummaryPrevious {\n window: SummaryWindow;\n metrics: SaleSummaryV2[\"metrics\"];\n by_currency: CurrencyRevenue[];\n series: SalesTrendPoint[];\n}\n\n/** Query input for `SalesClient.summary()` v2 — widened range + tz/compare options. */\nexport interface SaleSummaryQueryV2 extends SaleFilters {\n range?: NamedRange;\n /** Required when `range === 'custom'`. */\n since?: string;\n until?: string;\n /** IANA tz, e.g. `America/Toronto`. Default = business tz. */\n timezone?: string;\n granularity?: Granularity;\n compare_to?: CompareTo;\n include_categories?: boolean;\n include_deleted_services?: boolean;\n top_n?: number;\n}\n\n/** Superset of `SaleSummary` (assignable to it). */\nexport interface SaleSummaryV2 extends Omit<SaleSummary, \"range\" | \"metrics\"> {\n range: NamedRange;\n metrics: SaleSummary[\"metrics\"] & {\n distinct_customers_by_currency: DistinctCustomersByCurrency[];\n };\n timezone: string | null;\n window: SummaryWindow | null;\n granularity: string | null;\n previous: SaleSummaryPrevious | null;\n deltas: SummaryDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Customers — phone-keyed (a customer IS their E.164 phone). `customer_id` is\n// the E.164 string. Money is per-currency, never summed.\n// ---------------------------------------------------------------------------\n\nexport type CustomerSegment = \"new\" | \"returning\" | \"repeat\" | \"lapsed\";\nexport type CustomerSortField = \"total_spent\" | \"last_purchase\" | \"purchases\" | \"first_purchase\";\n\nexport interface CustomerCurrencyTotal {\n currency: SupportedCurrency;\n revenue_cents: number;\n sale_count: number;\n average_order_value_cents: number;\n}\n\nexport interface CustomerSummary {\n /** The E.164 phone — the canonical customer id. */\n customer_id: string;\n display_name: string | null;\n email: string | null;\n phone: string;\n first_purchase_at: string;\n last_purchase_at: string;\n purchase_count: number;\n segment: CustomerSegment;\n totals: CustomerCurrencyTotal[];\n}\n\nexport type CustomerProfile = CustomerSummary;\n\nexport interface CustomerListQuery extends Omit<SaleFilters, \"external_id\"> {\n segment?: CustomerSegment;\n sort?: CustomerSortField;\n order?: SaleSortOrder;\n cursor?: string | null;\n limit?: number;\n want_total?: boolean;\n}\n\nexport interface CustomerListPage {\n items: CustomerSummary[];\n next_cursor: string | null;\n total_count: number | null;\n}\n\nexport interface CustomerGetOptions {\n include_sales?: boolean;\n limit?: number;\n cursor?: string | null;\n}\n\nexport interface CustomerGetResult {\n customer: CustomerProfile;\n /** Present only when `include_sales: true` (and a business-scoped key). */\n sales?: SaleCursorPage | null;\n}\n\nexport interface CustomerSummaryQuery extends SaleFilters {\n range?: NamedRange;\n since?: string;\n until?: string;\n timezone?: string;\n compare_to?: CompareTo;\n}\n\nexport interface CustomerSegmentCount {\n segment: CustomerSegment;\n count: number;\n}\n\nexport interface CustomerCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n}\n\nexport interface CustomerKpisDeltas {\n total_customers_delta: number;\n new_customers_delta: number;\n returning_customers_delta: number;\n repeat_rate_delta: number;\n ltv_by_currency: CustomerCurrencyDelta[];\n}\n\nexport interface CustomerKpisPrevious {\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n ltv_by_currency: CustomerCurrencyTotal[];\n}\n\nexport interface CustomerKpis {\n range: NamedRange;\n timezone: string;\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n by_segment: CustomerSegmentCount[];\n ltv_by_currency: CustomerCurrencyTotal[];\n /** Present only when `compare_to` is set. */\n previous?: CustomerKpisPrevious | null;\n deltas?: CustomerKpisDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Business config — supersets `fetchServices()`.\n// ---------------------------------------------------------------------------\n\nexport interface BusinessConfigService {\n key: string;\n label: string;\n archived: boolean;\n}\n\nexport interface BusinessConfigFeatures {\n customers: boolean;\n comparisons: boolean;\n retention: boolean;\n}\n\nexport interface BusinessConfig {\n business_id: string;\n display_name: string;\n timezone: string;\n primary_currency: SupportedCurrency;\n currencies: SupportedCurrency[];\n default_phone_country: string | null;\n services: BusinessConfigService[];\n features: BusinessConfigFeatures;\n}\n","import { salesRequest, type SalesTransportConfig } from \"./sales/transport\";\n\n/** A business's active service, as returned by `GET /tracking/services`. */\nexport interface PublicServiceItem {\n key: string;\n label: string;\n}\n\n/**\n * Fetch the caller's business's active service taxonomy. Accepts a public or\n * secret key (the taxonomy is low-sensitivity category names). Powers the\n * `@aranova/tracking-cli gen` codegen.\n */\nexport async function fetchServices(config: SalesTransportConfig): Promise<PublicServiceItem[]> {\n return salesRequest<PublicServiceItem[]>(config, \"GET\", \"/services\");\n}\n"],"mappings":"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","names":["import_zod"]}