@aranova/tracking-next 0.21.0 → 0.22.0

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.
Files changed (41) hide show
  1. package/dist/blog-server.js +12 -0
  2. package/dist/blog-server.js.map +1 -1
  3. package/dist/blog-server.mjs +12 -0
  4. package/dist/blog-server.mjs.map +1 -1
  5. package/dist/blog.js +7 -0
  6. package/dist/blog.js.map +1 -1
  7. package/dist/blog.mjs +7 -0
  8. package/dist/blog.mjs.map +1 -1
  9. package/dist/calendar-server.d.mts +1 -1
  10. package/dist/calendar-server.d.ts +1 -1
  11. package/dist/calendar-server.js.map +1 -1
  12. package/dist/calendar-server.mjs.map +1 -1
  13. package/dist/calendar.d.mts +2 -2
  14. package/dist/calendar.d.ts +2 -2
  15. package/dist/calendar.js +7 -0
  16. package/dist/calendar.js.map +1 -1
  17. package/dist/calendar.mjs +7 -0
  18. package/dist/calendar.mjs.map +1 -1
  19. package/dist/index.d.mts +6 -6
  20. package/dist/index.d.ts +6 -6
  21. package/dist/index.js +56 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +56 -4
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/{phone-utils-B2vHJkNz.d.mts → phone-utils-BhWfNuPS.d.mts} +8 -0
  26. package/dist/{phone-utils-B2vHJkNz.d.ts → phone-utils-BhWfNuPS.d.ts} +8 -0
  27. package/dist/phone.d.mts +1 -1
  28. package/dist/phone.d.ts +1 -1
  29. package/dist/sales.d.mts +1 -1
  30. package/dist/sales.d.ts +1 -1
  31. package/dist/sales.js +7 -0
  32. package/dist/sales.js.map +1 -1
  33. package/dist/sales.mjs +7 -0
  34. package/dist/sales.mjs.map +1 -1
  35. package/dist/server.d.mts +1 -1
  36. package/dist/server.d.ts +1 -1
  37. package/package.json +1 -1
  38. package/dist/{sales-CDjXRdmd.d.ts → sales-C7tPnqbq.d.ts} +4 -4
  39. package/dist/{sales-B3BnYncU.d.mts → sales-H_6bHrL-.d.mts} +4 -4
  40. package/dist/{transport-DsOY6kcW.d.ts → transport-C-_rFSR4.d.ts} +8 -8
  41. package/dist/{transport-Bku8zftU.d.mts → transport-yUF5Wize.d.mts} +8 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../../tracking-core/src/resources/http/errors.ts","../../tracking-core/src/phone.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/session.ts","../../tracking-core/src/ingest.ts","../../tracking-core/src/resources/http/request.ts","../../tracking-core/src/resources/calendar/transport.ts","../../tracking-core/src/resources/intl/date.ts","../../tracking-core/src/resources/calendar/time.ts","../../tracking-core/src/resources/calendar/read-client.ts","../../tracking-core/src/resources/calendar/schema.ts","../../tracking-core/src/resources/calendar/slots.ts"],"sourcesContent":["/**\n * Thrown on a non-2xx from any awaited Aranova API call.\n *\n * Lives here rather than inside one resource because more than one resource\n * needs it, and a duplicate class would be a genuine hazard: a name star-exported\n * from two modules is silently dropped unless both resolve to the same binding.\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","// Framework-agnostic phone-number utilities — isomorphic (browser + Node/RSC),\n// zero React. Bundled `libphonenumber-js` (standard metadata) so clients add no\n// dependency. The transmitted value is ALWAYS E.164; display is the only knob.\n\nimport { AsYouType, parsePhoneNumberFromString, type CountryCode } from \"libphonenumber-js\";\n\nexport type { CountryCode };\n\n/**\n * How a phone number is shown in the UI. The transmitted value is always E.164\n * and is deliberately NOT part of this — only the display format is configurable.\n * A function form covers the long tail (`(parsed) => string`).\n */\nexport type PhoneDisplayFormat =\n | \"national\"\n | \"international\"\n | \"e164\"\n | ((parsed: ParsedPhone) => string);\n\nexport interface ParsedPhone {\n /** E.164 (`\"+14165550199\"`) or `null` when the input isn't a valid number. This is what gets transmitted. */\n e164: string | null;\n /** National display form (`\"(416) 555-0199\"`); empty string when unparseable. */\n national: string;\n /** International display form (`\"+1 416 555 0199\"`); empty string when unparseable. */\n international: string;\n /** ISO-3166 country resolved by libphonenumber, or `null`. */\n country: CountryCode | null;\n isValid: boolean;\n}\n\n/** Region assumed for numbers typed without a country code. */\nexport const DEFAULT_PHONE_COUNTRY: CountryCode = \"CA\";\n\n/** Parse a raw/display string into every representation at once (one parse → display + wire never drift). */\nexport function parsePhone(raw: string, country?: CountryCode): ParsedPhone {\n const region = country ?? DEFAULT_PHONE_COUNTRY;\n const parsed = parsePhoneNumberFromString(raw ?? \"\", region);\n if (!parsed) {\n return { e164: null, national: \"\", international: \"\", country: region, isValid: false };\n }\n const isValid = parsed.isValid();\n return {\n // E.164 is only surfaced for a *valid* number — a possible-but-invalid input\n // (e.g. too few digits) still parses but must not be transmitted.\n e164: isValid ? parsed.number : null,\n national: parsed.formatNational(),\n international: parsed.formatInternational(),\n country: parsed.country ?? region,\n isValid,\n };\n}\n\n/** Normalize any raw/display value to E.164, or `null` if it isn't a valid number. */\nexport function toE164(raw: string, country?: CountryCode): string | null {\n return parsePhone(raw, country).e164;\n}\n\n/** Format a value for display. Defaults to `'national'`. Never affects the wire value. */\nexport function formatPhone(\n value: string,\n format: PhoneDisplayFormat = \"national\",\n country?: CountryCode,\n): string {\n const parsed = parsePhone(value, country);\n if (typeof format === \"function\") return format(parsed);\n switch (format) {\n case \"international\":\n return parsed.international || value;\n case \"e164\":\n return parsed.e164 ?? value;\n case \"national\":\n default:\n return parsed.national || value;\n }\n}\n\n/** Live, incremental formatting for an `<input>` as the user types (`AsYouType`). */\nexport function formatPhoneAsTyped(raw: string, country?: CountryCode): string {\n return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? \"\");\n}\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\nimport { clearLandingRecord } from \"./landing\";\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, including the\n * session-scoped landing record (a landing must never outlive its session).\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n clearLandingRecord();\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 { getConsentChoice, 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 { buildLandingPayloadFields, getOrCaptureLandingParams } from \"./landing\";\nimport { getOrRotateSessionId, getVisitorId } from \"./session\";\nimport { stashUserDataFromFormFields } from \"./user-data\";\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 (fetch, non-keepalive). */\n flush: () => Promise<void>;\n /**\n * Flush queued events through the keepalive transport so the request\n * survives document unload. Use from `pagehide`/`visibilitychange:hidden`\n * handlers — a plain `flush()` there is aborted by the browser on unload.\n */\n flushBeacon: () => 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 // Effective consent + provenance so the dashboard can tell default-granted\n // (opt-out model, no interaction) apart from an explicit choice.\n const choice = getConsentChoice();\n return {\n state: choice.state,\n source: choice.source,\n updated_at: choice.updatedAt,\n expires_at: choice.expiresAt,\n };\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// Global/delegated captures (document/window listeners: page_exit, scroll_depth,\n// time_on_site, form_start, cta_click, phone_click, bfcache restore) must attach ONCE\n// per page-singleton client — NOT once per React provider mount. A page can mount several\n// <TrackingProvider>s (a supported island pattern: a global provider for page-level\n// triggers plus per-component providers so `useSearchParams()` doesn't opt the whole tree\n// out of static rendering). Since the client is a singleton but each provider runs its own\n// attach effect, N mounts would stack N document listeners and every interaction would emit\n// its event N times. We ref-count attaches against the singleton client's identity: the\n// first mount attaches, later mounts are no-ops, and the last unmount detaches.\ninterface ClientCaptureEntry {\n detach: () => void;\n refCount: number;\n}\nconst clientCaptureRegistry = new WeakMap<TrackingClient, ClientCaptureEntry>();\n\n/**\n * Attach a set of global/delegated captures against a singleton `client` exactly once,\n * ref-counted across provider mounts. `build` performs the actual `document`/`window`\n * listener attachment and returns a single detacher for all of them; it runs only on the\n * first mount for a given client. The returned release decrements the ref-count and runs\n * `build`'s detacher when the last holder releases. Release is idempotent — React\n * StrictMode invokes an effect's cleanup twice in dev, and a double release must not\n * double-decrement.\n */\nexport function attachClientCapturesOnce(\n client: TrackingClient,\n build: () => () => void,\n): () => void {\n let entry = clientCaptureRegistry.get(client);\n if (entry === undefined) {\n entry = { detach: build(), refCount: 0 };\n clientCaptureRegistry.set(client, entry);\n }\n entry.refCount += 1;\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = clientCaptureRegistry.get(client);\n if (current === undefined) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n current.detach();\n clientCaptureRegistry.delete(client);\n }\n };\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 // Force-detach any captures still attached to this client so their document/window\n // listeners can't leak into the next test even if a provider didn't unmount.\n const entry = clientCaptureRegistry.get(globalClient);\n if (entry !== undefined) {\n entry.detach();\n clientCaptureRegistry.delete(globalClient);\n }\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 // Pin this session's LANDING params now, before any SPA router strips the\n // query string (reuses the stored record when the session already has one).\n getOrCaptureLandingParams(sessionId);\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 wbraid: params.wbraid,\n gbraid: params.gbraid,\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 // Landing params for the CURRENT session id — captured on the spot when\n // the session just rotated (the current URL is the rotated session's\n // landing), reused from the stored record otherwise. Keys are omitted\n // entirely when the landing isn't observable (SSR).\n ...buildLandingPayloadFields(sessionId),\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 // Enhanced conversions: a form submit is the one moment the visitor's own\n // email/phone pass through the SDK — stash them (memory only) so the\n // conversion that fires next carries user_data. NOT done for phone_click:\n // its metadata holds the BUSINESS's number, not the visitor's. An explicit\n // consent decline also skips the stash entirely — egress is already gated,\n // but a decliner's identifiers shouldn't sit in page memory either.\n if (input.eventType === \"form_submit\" && getConsentState() !== \"denied\") {\n try {\n const fields = (input.metadata as { form?: { fields?: unknown } } | null)?.form?.fields;\n if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);\n } catch {\n // user-data capture must never break ingest\n }\n }\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 flushBeacon: flushOnUnload,\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","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 awaited (non fire-and-forget) API helper. */\nexport interface ApiTransportConfig {\n /** Public (`aranv_pk_…`) or secret (`aranv_sk_…`) API key. */\n apiKey: string;\n /** Base tracking endpoint, e.g. `https://aranovainternal-production.up.railway.app/tracking`. */\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: ApiTransportConfig): 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. Unlike the event queue, this surfaces failures: any\n * non-2xx rejects with an {@link AranovaApiError}. Returns `undefined` for 204.\n */\nexport async function apiRequest<T>(\n config: ApiTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<T> {\n const headers = { ...identityHeaders(config), ...extraHeaders };\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 { ApiTransportConfig } from \"../http/request\";\nimport { apiRequest } from \"../http/request\";\n\nexport type CalendarTransportConfig = ApiTransportConfig;\n\n/** Prefixes `/calendar` so callers pass resource-relative paths. */\nexport async function calendarRequest<T>(\n config: CalendarTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n return apiRequest<T>(config, method, `/calendar${path}`, body);\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 { formatDateInTz } from \"../intl/date\";\n\nexport { formatDateInTz };\n\n/** The visitor's own zone, so a booking page defaults to their local times. */\nexport function resolveBrowserTimeZone(): string {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\";\n } catch {\n return \"UTC\";\n }\n}\n\n/** Parts of an instant as seen in a specific zone. */\nfunction partsInTz(date: Date, timeZone: string): Record<string, number> {\n const formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n const out: Record<string, number> = {};\n for (const part of formatter.formatToParts(date)) {\n if (part.type !== \"literal\") out[part.type] = Number(part.value);\n }\n // `hour` comes back as 24 at midnight under hour12:false in some engines.\n if (out.hour === 24) out.hour = 0;\n return out;\n}\n\n/** Offset in minutes between UTC and `timeZone` at a given instant. */\nexport function offsetMinutesAt(date: Date, timeZone: string): number {\n const p = partsInTz(date, timeZone);\n const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);\n return (asUtc - Math.floor(date.getTime() / 1000) * 1000) / 60_000;\n}\n\n/**\n * The instant matching a wall-clock time in `timeZone`.\n *\n * Two passes because the offset itself depends on the instant: guess with the\n * offset at the naive time, then re-solve with the offset that guess landed in.\n * That second pass is what makes DST-transition days come out right.\n */\nexport function instantFromWallClock(\n timeZone: string,\n year: number,\n month: number,\n day: number,\n hour = 0,\n minute = 0,\n): Date {\n const naive = Date.UTC(year, month - 1, day, hour, minute);\n const firstGuess = new Date(naive - offsetMinutesAt(new Date(naive), timeZone) * 60_000);\n const corrected = naive - offsetMinutesAt(firstGuess, timeZone) * 60_000;\n return new Date(corrected);\n}\n\n/** `YYYY-MM-DD` for an instant as seen in `timeZone`. */\nexport function localDateKey(iso: string, timeZone: string): string {\n const p = partsInTz(new Date(iso), timeZone);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${p.year}-${pad(p.month)}-${pad(p.day)}`;\n}\n\nexport function startOfDayInTz(dateKey: string, timeZone: string): Date {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 0, 0);\n}\n\nexport function endOfDayInTz(dateKey: string, timeZone: string): Date {\n const start = startOfDayInTz(dateKey, timeZone);\n // Add a calendar day rather than 24h: DST days are 23 or 25 hours long.\n const p = partsInTz(start, timeZone);\n return instantFromWallClock(timeZone, p.year, p.month, p.day + 1, 0, 0);\n}\n\nexport function durationMinutes(startIso: string, endIso: string): number {\n return Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 60_000);\n}\n\n/** `10:00 – 10:30` in the given zone. */\nexport function formatSlotRange(\n startIso: string,\n endIso: string,\n timeZone: string,\n locale?: string,\n): string {\n const opts: Intl.DateTimeFormatOptions = { hour: \"2-digit\", minute: \"2-digit\", timeZone };\n const fmt = new Intl.DateTimeFormat(locale, opts);\n return `${fmt.format(new Date(startIso))} – ${fmt.format(new Date(endIso))}`;\n}\n","import { AranovaApiError } from \"../http/errors\";\nimport type { BusyResponse, CalendarErrorCode, CalendarSummary } from \"./schema\";\nimport type { CalendarTransportConfig } from \"./transport\";\nimport { calendarRequest } from \"./transport\";\nimport { resolveBrowserTimeZone } from \"./time\";\n\n/**\n * The browser-side calendar client — reads only.\n *\n * There is deliberately no write verb on this type. A public key ships in a JS\n * bundle and its Origin allowlist is a browser-enforced header a script forges\n * trivially; a booking is not a database row but an event on a real person's\n * calendar that mails their contacts. Writes live in\n * `@aranova/tracking-next/calendar-server`, which is `server-only`.\n */\nexport interface CalendarReadClientConfig extends CalendarTransportConfig {\n /** Falls back to the visitor's own zone. */\n defaultTimeZone?: string;\n /** Required when the key is not already scoped to one business. */\n businessId?: string;\n}\n\nexport interface BusyQuery {\n /**\n * ISO 8601 instants WITH an offset — the API rejects a naive or date-only\n * value with a 400. `startOfDayInTz`/`endOfDayInTz` turn a picked date into\n * the right pair for a given zone.\n */\n since: string;\n until: string;\n timeZone?: string;\n}\n\nexport interface CalendarReadClient<TCalendarKey extends string = string> {\n /** Labels, timezones and status — everything a widget needs before booking. */\n listCalendars(): Promise<CalendarSummary[]>;\n /** Busy intervals only. Never the contents of the grantee's events. */\n busy(calendarKey: TCalendarKey, query: BusyQuery): Promise<BusyResponse>;\n}\n\nfunction withBusiness(config: CalendarReadClientConfig, path: string): string {\n if (!config.businessId) return path;\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;\n}\n\nexport function createCalendarReadClient<TCalendarKey extends string = string>(\n config: CalendarReadClientConfig,\n): CalendarReadClient<TCalendarKey> {\n return {\n async listCalendars() {\n return calendarRequest<CalendarSummary[]>(config, \"GET\", withBusiness(config, \"/calendars\"));\n },\n async busy(calendarKey, query) {\n const params = new URLSearchParams({\n since: query.since,\n until: query.until,\n });\n const timeZone = query.timeZone ?? config.defaultTimeZone ?? resolveBrowserTimeZone();\n params.set(\"time_zone\", timeZone);\n return calendarRequest<BusyResponse>(\n config,\n \"GET\",\n withBusiness(config, `/${encodeURIComponent(calendarKey)}/busy?${params.toString()}`),\n );\n },\n };\n}\n\n/** Narrow an unknown error to a specific calendar failure code. */\nexport function isCalendarError(\n error: unknown,\n code?: CalendarErrorCode,\n): error is AranovaApiError {\n if (!(error instanceof AranovaApiError)) return false;\n return code === undefined || error.code === code;\n}\n","import { z } from \"zod\";\n\n// Mirrored by `apps/api/src/schemas/tracking_calendar.py` and pinned by the\n// event-schema drift test. Keep both sides in step.\n//\n// The location union is a plain `z.union`, deliberately: a discriminated union\n// emits `oneOf` + discriminator metadata on the Python side and `anyOf` here,\n// which would fail the drift snapshot. Both sides stay plain.\n\n/** Bumped when the durable `metadata` payload changes shape. */\nexport const CALENDAR_METADATA_SCHEMA_VERSION = 1;\n\nexport const MAX_ATTENDEES = 20;\nexport const MAX_REMINDER_OVERRIDES = 5;\n/** Google's ceiling for `reminders.overrides[].minutes`: four weeks. */\nexport const MAX_REMINDER_MINUTES = 40_320;\nexport const MAX_BOOKING_MINUTES = 1_440;\n\nexport const BOOKING_SOURCES = [\"website\", \"phone\", \"walk_in\", \"import\", \"operator\"] as const;\nexport const BOOKING_STATUSES = [\"pending\", \"confirmed\", \"cancelled\", \"no_show\"] as const;\nexport const GOOGLE_SYNC_STATES = [\"queued\", \"synced\", \"uncertain\", \"failed\", \"drifted\"] as const;\nexport const CALENDAR_SYNC_STATUSES = [\n \"pending_invite\",\n \"awaiting_calendar_selection\",\n \"connected\",\n \"needs_reconnect\",\n \"revoked\",\n \"disabled\",\n] as const;\n\n/** Every failure the calendar API can hand back, so a site can branch on it. */\nexport const CALENDAR_ERROR_CODES = [\n \"slot_unavailable\",\n \"calendar_not_connected\",\n \"calendar_disconnected\",\n \"booking_not_found\",\n \"outside_booking_window\",\n \"rate_limited\",\n \"google_error\",\n] as const;\nexport type CalendarErrorCode = (typeof CALENDAR_ERROR_CODES)[number];\n\nexport type BookingStatus = (typeof BOOKING_STATUSES)[number];\nexport type GoogleSyncState = (typeof GOOGLE_SYNC_STATES)[number];\nexport type CalendarSyncStatus = (typeof CALENDAR_SYNC_STATUSES)[number];\nexport type JsonScalar = string | number | boolean | null;\n\nconst isoInstant = z.string().datetime({ offset: true });\n\n/**\n * Does the runtime actually know this zone?\n *\n * `Intl.DateTimeFormat` rather than `Intl.supportedValuesOf(\"timeZone\")`: the\n * latter returns canonical zones only and omits live aliases (`Asia/Calcutta`,\n * `US/Pacific`, `Europe/Kiev`) that the API's `ZoneInfo` accepts. Validating\n * against it would reject values the server stores happily.\n */\nfunction isKnownTimeZone(timeZone: string): boolean {\n try {\n new Intl.DateTimeFormat(undefined, { timeZone });\n return true;\n } catch {\n return false;\n }\n}\n\n// The refinement is invisible to `zod-to-json-schema` — unrepresentable\n// constraints are dropped — so `events.schema.json` and the drift test against\n// the Pydantic mirror are unaffected while both sides gain the real check.\nconst ianaTz = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[A-Za-z0-9_+\\-/]+$/)\n .refine(isKnownTimeZone, { message: \"unknown IANA time zone\" });\n\nexport const bookingAttendeeSchema = z\n .object({\n email: z.string().max(320).email(),\n display_name: z.string().max(200).nullable().optional(),\n optional: z.boolean().default(false),\n })\n .strict();\n\nexport const bookingReminderSchema = z\n .object({\n method: z.enum([\"email\", \"popup\"]),\n minutes_before: z.number().int().min(0).max(MAX_REMINDER_MINUTES),\n })\n .strict();\n\nexport const bookingRemindersSchema = z\n .object({\n use_calendar_defaults: z.boolean().default(true),\n overrides: z.array(bookingReminderSchema).max(MAX_REMINDER_OVERRIDES).default([]),\n })\n .strict();\n\nexport const bookingLocationSchema = z.union([\n z.object({ kind: z.literal(\"google_meet\") }).strict(),\n z.object({ kind: z.literal(\"physical\"), address: z.string().max(1024) }).strict(),\n z.object({ kind: z.literal(\"phone\"), phone: z.string().max(64) }).strict(),\n z.object({ kind: z.literal(\"custom\"), text: z.string().max(1024) }).strict(),\n]);\n\n/**\n * The nullable form, flattened.\n *\n * `bookingLocationSchema.nullable()` would nest one `anyOf` inside another,\n * while Pydantic's `BookingLocation | None` flattens to a single list — the\n * drift snapshot compares structure, so both sides emit the flat shape.\n */\nexport const bookingLocationNullableSchema = z.union([...bookingLocationSchema.options, z.null()]);\n\nexport const bookingCustomerSchema = z\n .object({\n // Required: Google needs an address to send the invite that confirms the\n // booking, and that invite is the whole confirmation mechanism.\n email: z.string().max(320).email(),\n name: z.string().max(200).nullable().optional(),\n phone: z.string().max(64).nullable().optional(),\n sms_consent: z.boolean().default(false),\n })\n .strict();\n\nconst jsonScalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);\n\n/**\n * Named attribution keys plus a flat escape hatch.\n *\n * `custom` is flat and scalar-only on purpose: it mirrors trivially in Pydantic,\n * stays JSONB-queryable, and bounds what a caller can push. Only `custom` is\n * ever mirrored to Google's `extendedProperties`; the named keys stay in our\n * database.\n */\nexport const bookingMetadataSchema = z\n .object({\n schema_version: z.number().int().default(CALENDAR_METADATA_SCHEMA_VERSION),\n source: z.enum(BOOKING_SOURCES).nullable().optional(),\n service: z.string().max(64).nullable().optional(),\n visitor_id: z.string().max(64).nullable().optional(),\n session_id: z.string().max(64).nullable().optional(),\n gclid: z.string().max(256).nullable().optional(),\n fbclid: z.string().max(256).nullable().optional(),\n utm_source: z.string().max(128).nullable().optional(),\n utm_medium: z.string().max(128).nullable().optional(),\n utm_campaign: z.string().max(128).nullable().optional(),\n utm_term: z.string().max(128).nullable().optional(),\n utm_content: z.string().max(128).nullable().optional(),\n landing_page: z.string().max(2048).nullable().optional(),\n referrer: z.string().max(2048).nullable().optional(),\n notes: z.string().max(2000).nullable().optional(),\n custom: z.record(jsonScalar).nullable().optional(),\n })\n .strict();\n\nfunction refineEndXor(\n value: { start_at: string; end_at?: string | null; duration_minutes?: number | null },\n ctx: z.RefinementCtx,\n): void {\n const hasEnd = value.end_at !== undefined && value.end_at !== null;\n const hasDuration = value.duration_minutes !== undefined && value.duration_minutes !== null;\n if (hasEnd === hasDuration) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"provide exactly one of end_at or duration_minutes\",\n path: [\"end_at\"],\n });\n return;\n }\n if (hasEnd && new Date(value.end_at as string) <= new Date(value.start_at)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"end_at must be after start_at\",\n path: [\"end_at\"],\n });\n }\n}\n\nexport const bookingCreateSchema = z\n .object({\n calendar_key: z.string().min(1).max(64),\n // Free-form: appointment types are the client site's own vocabulary, not\n // configuration the dashboard holds.\n appointment_type: z.string().max(64).nullable().optional(),\n start_at: isoInstant,\n end_at: isoInstant.nullable().optional(),\n duration_minutes: z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),\n /** The booker's zone — distinct from the business's and the calendar's. */\n time_zone: ianaTz,\n title: z.string().max(300).nullable().optional(),\n description: z.string().max(8000).nullable().optional(),\n location: bookingLocationNullableSchema.optional(),\n customer: bookingCustomerSchema,\n attendees: z.array(bookingAttendeeSchema).max(MAX_ATTENDEES).default([]),\n send_invites: z.boolean().default(true),\n reminders: bookingRemindersSchema.nullable().optional(),\n metadata: bookingMetadataSchema.nullable().optional(),\n idempotency_key: z.string().max(128).nullable().optional(),\n buffer_before_minutes: z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),\n buffer_after_minutes: z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),\n })\n .strict()\n .superRefine(refineEndXor);\n\nexport const bookingRescheduleSchema = z\n .object({\n start_at: isoInstant,\n end_at: isoInstant.nullable().optional(),\n duration_minutes: z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),\n time_zone: ianaTz,\n notify_attendees: z.boolean().default(true),\n reason: z.string().max(500).nullable().optional(),\n })\n .strict()\n .superRefine(refineEndXor);\n\nexport const bookingCancelSchema = z\n .object({\n reason: z.string().max(500).nullable().optional(),\n notify_attendees: z.boolean().default(true),\n })\n .strict();\n\n// `z.input` so Zod defaults stay optional for callers.\nexport type BookingCreateInput = z.input<typeof bookingCreateSchema>;\nexport type BookingRescheduleInput = z.input<typeof bookingRescheduleSchema>;\nexport type BookingCancelInput = z.input<typeof bookingCancelSchema>;\nexport type BookingAttendee = z.input<typeof bookingAttendeeSchema>;\nexport type BookingReminders = z.input<typeof bookingRemindersSchema>;\nexport type BookingLocation = z.input<typeof bookingLocationSchema>;\nexport type BookingCustomer = z.input<typeof bookingCustomerSchema>;\nexport type BookingMetadata<\n TCustom extends Record<string, JsonScalar> = Record<string, JsonScalar>,\n> = Omit<z.input<typeof bookingMetadataSchema>, \"custom\"> & { custom?: TCustom | null };\n\n// ---------------------------------------------------------------------------\n// Read shapes — server responses, trusted and therefore not re-validated here.\n// ---------------------------------------------------------------------------\n\nexport interface CalendarSummary {\n calendar_key: string;\n label: string;\n time_zone: string;\n status: CalendarSyncStatus;\n /** Secret-key only. A public key gets `null`: whose Google account is behind\n * a calendar is a third party's identity, not the client site's business. */\n google_account_email?: string | null;\n granted_scopes?: string[] | null;\n connected_at?: string | null;\n}\n\nexport interface BusyInterval {\n start_at: string;\n end_at: string;\n source: \"google\" | \"booking\";\n}\n\nexport interface BusyResponse {\n calendar_key: string;\n time_zone: string;\n window: { since: string; until: string };\n busy: BusyInterval[];\n generated_at: string;\n}\n\nexport interface Booking {\n id: string;\n calendar_key: string;\n status: BookingStatus;\n google_sync_state: GoogleSyncState;\n start_at: string;\n end_at: string;\n time_zone: string;\n appointment_type: string | null;\n title: string;\n customer: BookingCustomer;\n google_html_link: string | null;\n meet_url: string | null;\n /** Returned only on create — the capability credential for manage links. */\n manage_token?: string | null;\n}\n","import type { BusyInterval } from \"./schema\";\nimport { instantFromWallClock, offsetMinutesAt } from \"./time\";\n\n/**\n * Slot computation lives in the client site, not in our database.\n *\n * Opening hours, slot length, notice and buffers differ per client and per use\n * case; modelling them as dashboard config would mean every new client needs\n * data entry before their site works. The API answers only \"when is this person\n * busy\" — this pure helper turns that plus the site's own hours into offerable\n * slots.\n */\n\nexport interface OpeningInterval {\n /** Local wall-clock `HH:MM` in the calendar's zone. */\n start: string;\n end: string;\n}\n\nexport interface OpeningHours {\n /** 0 = Sunday … 6 = Saturday. A missing weekday is closed. */\n weekly: Partial<Record<number, OpeningInterval[]>>;\n /** `YYYY-MM-DD` → intervals, replacing that day's weekly rule entirely. */\n overrides?: Record<string, OpeningInterval[]>;\n /** `YYYY-MM-DD` dates that are closed regardless of the weekly rule. */\n closedDates?: string[];\n}\n\nexport interface ComputeSlotsOptions {\n busy: BusyInterval[];\n openingHours: OpeningHours;\n timeZone: string;\n durationMinutes: number;\n granularityMinutes?: number;\n minNoticeMinutes?: number;\n bufferBeforeMinutes?: number;\n bufferAfterMinutes?: number;\n windowDays?: number;\n now?: Date;\n}\n\nexport interface Slot {\n start_at: string;\n end_at: string;\n}\n\nexport interface ComputeSlotsResult {\n slots: Slot[];\n nextAvailableAt: string | null;\n}\n\nfunction parseHhMm(value: string): [number, number] {\n const [hour, minute] = value.split(\":\").map(Number);\n return [hour || 0, minute || 0];\n}\n\nfunction dateKeyOf(date: Date, timeZone: string): string {\n const fmt = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n return fmt.format(date);\n}\n\n/** Weekday of a `YYYY-MM-DD` local date, read at noon UTC so no offset can\n * shift it across a day boundary. */\nfunction weekdayFromDateKey(dateKey: string): number {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();\n}\n\n/** Midday of the local date containing `at` — a stable anchor to step from,\n * immune to a midnight DST jump. */\nfunction startOfLocalDay(at: Date, timeZone: string): Date {\n const [year, month, day] = dateKeyOf(at, timeZone).split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 12, 0);\n}\n\nfunction overlaps(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && aEnd > bStart;\n}\n\nexport function computeSlots(options: ComputeSlotsOptions): ComputeSlotsResult {\n const {\n busy,\n openingHours,\n timeZone,\n durationMinutes,\n granularityMinutes = 15,\n minNoticeMinutes = 0,\n bufferBeforeMinutes = 0,\n bufferAfterMinutes = 0,\n windowDays = 30,\n now = new Date(),\n } = options;\n\n const earliest = now.getTime() + minNoticeMinutes * 60_000;\n const busyRanges = busy.map((interval) => [\n new Date(interval.start_at).getTime(),\n new Date(interval.end_at).getTime(),\n ]);\n\n const slots: Slot[] = [];\n // Advance by CALENDAR days, not by 86_400_000 ms. A DST day is 23 or 25 hours\n // long, so fixed-millisecond stepping skips a local date entirely at spring\n // forward and visits one twice at fall back — in America/Toronto, adding 24h\n // from 2026-03-08T04:30Z jumps straight from the 7th to the 9th.\n let cursor = startOfLocalDay(now, timeZone);\n for (let dayOffset = 0; dayOffset < windowDays; dayOffset += 1) {\n const dateKey = dateKeyOf(cursor, timeZone);\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n // `Date.UTC` normalises day 32 into the next month, so this needs no\n // month-length handling.\n cursor = instantFromWallClock(timeZone, year, month, day + 1, 12, 0);\n\n if (openingHours.closedDates?.includes(dateKey)) continue;\n\n const intervals =\n openingHours.overrides?.[dateKey] ?? openingHours.weekly[weekdayFromDateKey(dateKey)] ?? [];\n for (const interval of intervals) {\n const [openHour, openMinute] = parseHhMm(interval.start);\n const [closeHour, closeMinute] = parseHhMm(interval.end);\n const opensAt = instantFromWallClock(timeZone, year, month, day, openHour, openMinute);\n const closesAt = instantFromWallClock(timeZone, year, month, day, closeHour, closeMinute);\n\n for (\n let start = opensAt.getTime();\n start + durationMinutes * 60_000 <= closesAt.getTime();\n start += granularityMinutes * 60_000\n ) {\n const end = start + durationMinutes * 60_000;\n if (start < earliest) continue;\n\n // A slot is unavailable if its BUFFERED span touches anything busy —\n // the buffers are what keep a gap between appointments.\n const blockedStart = start - bufferBeforeMinutes * 60_000;\n const blockedEnd = end + bufferAfterMinutes * 60_000;\n const taken = busyRanges.some(([busyStart, busyEnd]) =>\n overlaps(blockedStart, blockedEnd, busyStart, busyEnd),\n );\n if (taken) continue;\n\n slots.push({\n start_at: new Date(start).toISOString(),\n end_at: new Date(end).toISOString(),\n });\n }\n }\n }\n\n slots.sort((a, b) => a.start_at.localeCompare(b.start_at));\n return { slots, nextAvailableAt: slots[0]?.start_at ?? null };\n}\n\n/** Group slots by local date, the shape every date-picker UI wants. */\nexport function groupSlotsByDay(\n slots: Slot[],\n timeZone: string,\n): { date: string; slots: Slot[] }[] {\n const byDay = new Map<string, Slot[]>();\n for (const slot of slots) {\n const key = dateKeyOf(new Date(slot.start_at), timeZone);\n const bucket = byDay.get(key);\n if (bucket) bucket.push(slot);\n else byDay.set(key, [slot]);\n }\n return [...byDay.entries()]\n .map(([date, daySlots]) => ({ date, slots: daySlots }))\n .sort((a, b) => a.date.localeCompare(b.date));\n}\n\nexport { offsetMinutesAt };\n"],"mappings":"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","names":["z","durationMinutes"]}
1
+ {"version":3,"sources":["../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/http/errors.ts","../../tracking-core/src/phone.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/session.ts","../../tracking-core/src/ingest.ts","../../tracking-core/src/resources/http/request.ts","../../tracking-core/src/resources/calendar/transport.ts","../../tracking-core/src/resources/intl/date.ts","../../tracking-core/src/resources/calendar/time.ts","../../tracking-core/src/resources/calendar/read-client.ts","../../tracking-core/src/resources/calendar/schema.ts","../../tracking-core/src/resources/calendar/slots.ts"],"sourcesContent":["// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","/**\n * Thrown on a non-2xx from any awaited Aranova API call.\n *\n * Lives here rather than inside one resource because more than one resource\n * needs it, and a duplicate class would be a genuine hazard: a name star-exported\n * from two modules is silently dropped unless both resolve to the same binding.\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","// Framework-agnostic phone-number utilities — isomorphic (browser + Node/RSC),\n// zero React. Bundled `libphonenumber-js` (standard metadata) so clients add no\n// dependency. The transmitted value is ALWAYS E.164; display is the only knob.\n\nimport { AsYouType, parsePhoneNumberFromString, type CountryCode } from \"libphonenumber-js\";\n\nexport type { CountryCode };\n\n/**\n * How a phone number is shown in the UI. The transmitted value is always E.164\n * and is deliberately NOT part of this — only the display format is configurable.\n * A function form covers the long tail (`(parsed) => string`).\n */\nexport type PhoneDisplayFormat =\n | \"national\"\n | \"international\"\n | \"e164\"\n | ((parsed: ParsedPhone) => string);\n\nexport interface ParsedPhone {\n /** E.164 (`\"+14165550199\"`) or `null` when the input isn't a valid number. This is what gets transmitted. */\n e164: string | null;\n /** National display form (`\"(416) 555-0199\"`); empty string when unparseable. */\n national: string;\n /** International display form (`\"+1 416 555 0199\"`); empty string when unparseable. */\n international: string;\n /** ISO-3166 country resolved by libphonenumber, or `null`. */\n country: CountryCode | null;\n isValid: boolean;\n}\n\n/** Region assumed for numbers typed without a country code. */\nexport const DEFAULT_PHONE_COUNTRY: CountryCode = \"CA\";\n\n/** Parse a raw/display string into every representation at once (one parse → display + wire never drift). */\nexport function parsePhone(raw: string, country?: CountryCode): ParsedPhone {\n const region = country ?? DEFAULT_PHONE_COUNTRY;\n const parsed = parsePhoneNumberFromString(raw ?? \"\", region);\n if (!parsed) {\n return { e164: null, national: \"\", international: \"\", country: region, isValid: false };\n }\n const isValid = parsed.isValid();\n return {\n // E.164 is only surfaced for a *valid* number — a possible-but-invalid input\n // (e.g. too few digits) still parses but must not be transmitted.\n e164: isValid ? parsed.number : null,\n national: parsed.formatNational(),\n international: parsed.formatInternational(),\n country: parsed.country ?? region,\n isValid,\n };\n}\n\n/** Normalize any raw/display value to E.164, or `null` if it isn't a valid number. */\nexport function toE164(raw: string, country?: CountryCode): string | null {\n return parsePhone(raw, country).e164;\n}\n\n/** Format a value for display. Defaults to `'national'`. Never affects the wire value. */\nexport function formatPhone(\n value: string,\n format: PhoneDisplayFormat = \"national\",\n country?: CountryCode,\n): string {\n const parsed = parsePhone(value, country);\n if (typeof format === \"function\") return format(parsed);\n switch (format) {\n case \"international\":\n return parsed.international || value;\n case \"e164\":\n return parsed.e164 ?? value;\n case \"national\":\n default:\n return parsed.national || value;\n }\n}\n\n/** Live, incremental formatting for an `<input>` as the user types (`AsYouType`). */\nexport function formatPhoneAsTyped(raw: string, country?: CountryCode): string {\n return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? \"\");\n}\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\nimport { clearLandingRecord } from \"./landing\";\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, including the\n * session-scoped landing record (a landing must never outlive its session).\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n clearLandingRecord();\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 { getRegisteredCapabilities } from \"./capabilities\";\nimport { getConsentChoice, 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 { buildLandingPayloadFields, getOrCaptureLandingParams } from \"./landing\";\nimport { getOrRotateSessionId, getVisitorId } from \"./session\";\nimport { stashUserDataFromFormFields } from \"./user-data\";\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 (fetch, non-keepalive). */\n flush: () => Promise<void>;\n /**\n * Flush queued events through the keepalive transport so the request\n * survives document unload. Use from `pagehide`/`visibilitychange:hidden`\n * handlers — a plain `flush()` there is aborted by the browser on unload.\n */\n flushBeacon: () => 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 // Rebuilt per flush (this runs inside the payload builder), so a client\n // constructed later on a deeper route still gets reported.\n capabilities: getRegisteredCapabilities(),\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 // Effective consent + provenance so the dashboard can tell default-granted\n // (opt-out model, no interaction) apart from an explicit choice.\n const choice = getConsentChoice();\n return {\n state: choice.state,\n source: choice.source,\n updated_at: choice.updatedAt,\n expires_at: choice.expiresAt,\n };\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// Global/delegated captures (document/window listeners: page_exit, scroll_depth,\n// time_on_site, form_start, cta_click, phone_click, bfcache restore) must attach ONCE\n// per page-singleton client — NOT once per React provider mount. A page can mount several\n// <TrackingProvider>s (a supported island pattern: a global provider for page-level\n// triggers plus per-component providers so `useSearchParams()` doesn't opt the whole tree\n// out of static rendering). Since the client is a singleton but each provider runs its own\n// attach effect, N mounts would stack N document listeners and every interaction would emit\n// its event N times. We ref-count attaches against the singleton client's identity: the\n// first mount attaches, later mounts are no-ops, and the last unmount detaches.\ninterface ClientCaptureEntry {\n detach: () => void;\n refCount: number;\n}\nconst clientCaptureRegistry = new WeakMap<TrackingClient, ClientCaptureEntry>();\n\n/**\n * Attach a set of global/delegated captures against a singleton `client` exactly once,\n * ref-counted across provider mounts. `build` performs the actual `document`/`window`\n * listener attachment and returns a single detacher for all of them; it runs only on the\n * first mount for a given client. The returned release decrements the ref-count and runs\n * `build`'s detacher when the last holder releases. Release is idempotent — React\n * StrictMode invokes an effect's cleanup twice in dev, and a double release must not\n * double-decrement.\n */\nexport function attachClientCapturesOnce(\n client: TrackingClient,\n build: () => () => void,\n): () => void {\n let entry = clientCaptureRegistry.get(client);\n if (entry === undefined) {\n entry = { detach: build(), refCount: 0 };\n clientCaptureRegistry.set(client, entry);\n }\n entry.refCount += 1;\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = clientCaptureRegistry.get(client);\n if (current === undefined) return;\n current.refCount -= 1;\n if (current.refCount <= 0) {\n current.detach();\n clientCaptureRegistry.delete(client);\n }\n };\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 // Force-detach any captures still attached to this client so their document/window\n // listeners can't leak into the next test even if a provider didn't unmount.\n const entry = clientCaptureRegistry.get(globalClient);\n if (entry !== undefined) {\n entry.detach();\n clientCaptureRegistry.delete(globalClient);\n }\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 // Pin this session's LANDING params now, before any SPA router strips the\n // query string (reuses the stored record when the session already has one).\n getOrCaptureLandingParams(sessionId);\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 wbraid: params.wbraid,\n gbraid: params.gbraid,\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 // Landing params for the CURRENT session id — captured on the spot when\n // the session just rotated (the current URL is the rotated session's\n // landing), reused from the stored record otherwise. Keys are omitted\n // entirely when the landing isn't observable (SSR).\n ...buildLandingPayloadFields(sessionId),\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 // keepalive=false: this runs on a live page, where a plain fetch completes\n // normally and its failure is observable. The unload path (flushOnUnload)\n // passes true instead — pagehide tears the document down and aborts a plain\n // fetch mid-flight, while keepalive hands the request to the browser to\n // finish after teardown.\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 // Enhanced conversions: a form submit is the one moment the visitor's own\n // email/phone pass through the SDK — stash them (memory only) so the\n // conversion that fires next carries user_data. NOT done for phone_click:\n // its metadata holds the BUSINESS's number, not the visitor's. An explicit\n // consent decline also skips the stash entirely — egress is already gated,\n // but a decliner's identifiers shouldn't sit in page memory either.\n if (input.eventType === \"form_submit\" && getConsentState() !== \"denied\") {\n try {\n const fields = (input.metadata as { form?: { fields?: unknown } } | null)?.form?.fields;\n if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);\n } catch {\n // user-data capture must never break ingest\n }\n }\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 flushBeacon: flushOnUnload,\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","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 awaited (non fire-and-forget) API helper. */\nexport interface ApiTransportConfig {\n /** Public (`aranv_pk_…`) or secret (`aranv_sk_…`) API key. */\n apiKey: string;\n /** Base tracking endpoint, e.g. `https://aranovainternal-production.up.railway.app/tracking`. */\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: ApiTransportConfig): 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. Unlike the event queue, this surfaces failures: any\n * non-2xx rejects with an {@link AranovaApiError}. Returns `undefined` for 204.\n */\nexport async function apiRequest<T>(\n config: ApiTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<T> {\n const headers = { ...identityHeaders(config), ...extraHeaders };\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 { ApiTransportConfig } from \"../http/request\";\nimport { apiRequest } from \"../http/request\";\n\nexport type CalendarTransportConfig = ApiTransportConfig;\n\n/** Prefixes `/calendar` so callers pass resource-relative paths. */\nexport async function calendarRequest<T>(\n config: CalendarTransportConfig,\n method: string,\n path: string,\n body?: unknown,\n): Promise<T> {\n return apiRequest<T>(config, method, `/calendar${path}`, body);\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 { formatDateInTz } from \"../intl/date\";\n\nexport { formatDateInTz };\n\n/** The visitor's own zone, so a booking page defaults to their local times. */\nexport function resolveBrowserTimeZone(): string {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || \"UTC\";\n } catch {\n return \"UTC\";\n }\n}\n\n/** Parts of an instant as seen in a specific zone. */\nfunction partsInTz(date: Date, timeZone: string): Record<string, number> {\n const formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hour12: false,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n const out: Record<string, number> = {};\n for (const part of formatter.formatToParts(date)) {\n if (part.type !== \"literal\") out[part.type] = Number(part.value);\n }\n // `hour` comes back as 24 at midnight under hour12:false in some engines.\n if (out.hour === 24) out.hour = 0;\n return out;\n}\n\n/** Offset in minutes between UTC and `timeZone` at a given instant. */\nexport function offsetMinutesAt(date: Date, timeZone: string): number {\n const p = partsInTz(date, timeZone);\n const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);\n return (asUtc - Math.floor(date.getTime() / 1000) * 1000) / 60_000;\n}\n\n/**\n * The instant matching a wall-clock time in `timeZone`.\n *\n * Two passes because the offset itself depends on the instant: guess with the\n * offset at the naive time, then re-solve with the offset that guess landed in.\n * That second pass is what makes DST-transition days come out right.\n */\nexport function instantFromWallClock(\n timeZone: string,\n year: number,\n month: number,\n day: number,\n hour = 0,\n minute = 0,\n): Date {\n const naive = Date.UTC(year, month - 1, day, hour, minute);\n const firstGuess = new Date(naive - offsetMinutesAt(new Date(naive), timeZone) * 60_000);\n const corrected = naive - offsetMinutesAt(firstGuess, timeZone) * 60_000;\n return new Date(corrected);\n}\n\n/** `YYYY-MM-DD` for an instant as seen in `timeZone`. */\nexport function localDateKey(iso: string, timeZone: string): string {\n const p = partsInTz(new Date(iso), timeZone);\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${p.year}-${pad(p.month)}-${pad(p.day)}`;\n}\n\nexport function startOfDayInTz(dateKey: string, timeZone: string): Date {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 0, 0);\n}\n\nexport function endOfDayInTz(dateKey: string, timeZone: string): Date {\n const start = startOfDayInTz(dateKey, timeZone);\n // Add a calendar day rather than 24h: DST days are 23 or 25 hours long.\n const p = partsInTz(start, timeZone);\n return instantFromWallClock(timeZone, p.year, p.month, p.day + 1, 0, 0);\n}\n\nexport function durationMinutes(startIso: string, endIso: string): number {\n return Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 60_000);\n}\n\n/** `10:00 – 10:30` in the given zone. */\nexport function formatSlotRange(\n startIso: string,\n endIso: string,\n timeZone: string,\n locale?: string,\n): string {\n const opts: Intl.DateTimeFormatOptions = { hour: \"2-digit\", minute: \"2-digit\", timeZone };\n const fmt = new Intl.DateTimeFormat(locale, opts);\n return `${fmt.format(new Date(startIso))} – ${fmt.format(new Date(endIso))}`;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport { AranovaApiError } from \"../http/errors\";\nimport type { BusyResponse, CalendarErrorCode, CalendarSummary } from \"./schema\";\nimport type { CalendarTransportConfig } from \"./transport\";\nimport { calendarRequest } from \"./transport\";\nimport { resolveBrowserTimeZone } from \"./time\";\n\n/**\n * The browser-side calendar client — reads only.\n *\n * There is deliberately no write verb on this type. A public key ships in a JS\n * bundle and its Origin allowlist is a browser-enforced header a script forges\n * trivially; a booking is not a database row but an event on a real person's\n * calendar that mails their contacts. Writes live in\n * `@aranova/tracking-next/calendar-server`, which is `server-only`.\n */\nexport interface CalendarReadClientConfig extends CalendarTransportConfig {\n /** Falls back to the visitor's own zone. */\n defaultTimeZone?: string;\n /** Required when the key is not already scoped to one business. */\n businessId?: string;\n}\n\nexport interface BusyQuery {\n /**\n * ISO 8601 instants WITH an offset — the API rejects a naive or date-only\n * value with a 400. `startOfDayInTz`/`endOfDayInTz` turn a picked date into\n * the right pair for a given zone.\n */\n since: string;\n until: string;\n timeZone?: string;\n}\n\nexport interface CalendarReadClient<TCalendarKey extends string = string> {\n /** Labels, timezones and status — everything a widget needs before booking. */\n listCalendars(): Promise<CalendarSummary[]>;\n /** Busy intervals only. Never the contents of the grantee's events. */\n busy(calendarKey: TCalendarKey, query: BusyQuery): Promise<BusyResponse>;\n}\n\nfunction withBusiness(config: CalendarReadClientConfig, path: string): string {\n if (!config.businessId) return path;\n const separator = path.includes(\"?\") ? \"&\" : \"?\";\n return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;\n}\n\nexport function createCalendarReadClient<TCalendarKey extends string = string>(\n config: CalendarReadClientConfig,\n): CalendarReadClient<TCalendarKey> {\n registerCapability(\"calendar_read\");\n return {\n async listCalendars() {\n return calendarRequest<CalendarSummary[]>(config, \"GET\", withBusiness(config, \"/calendars\"));\n },\n async busy(calendarKey, query) {\n const params = new URLSearchParams({\n since: query.since,\n until: query.until,\n });\n const timeZone = query.timeZone ?? config.defaultTimeZone ?? resolveBrowserTimeZone();\n params.set(\"time_zone\", timeZone);\n return calendarRequest<BusyResponse>(\n config,\n \"GET\",\n withBusiness(config, `/${encodeURIComponent(calendarKey)}/busy?${params.toString()}`),\n );\n },\n };\n}\n\n/** Narrow an unknown error to a specific calendar failure code. */\nexport function isCalendarError(\n error: unknown,\n code?: CalendarErrorCode,\n): error is AranovaApiError {\n if (!(error instanceof AranovaApiError)) return false;\n return code === undefined || error.code === code;\n}\n","import { z } from \"zod\";\n\n// Mirrored by `apps/api/src/schemas/tracking_calendar.py` and pinned by the\n// event-schema drift test. Keep both sides in step.\n//\n// The location union is a plain `z.union`, deliberately: a discriminated union\n// emits `oneOf` + discriminator metadata on the Python side and `anyOf` here,\n// which would fail the drift snapshot. Both sides stay plain.\n\n/** Bumped when the durable `metadata` payload changes shape. */\nexport const CALENDAR_METADATA_SCHEMA_VERSION = 1;\n\nexport const MAX_ATTENDEES = 20;\nexport const MAX_REMINDER_OVERRIDES = 5;\n/** Google's ceiling for `reminders.overrides[].minutes`: four weeks. */\nexport const MAX_REMINDER_MINUTES = 40_320;\nexport const MAX_BOOKING_MINUTES = 1_440;\n\nexport const BOOKING_SOURCES = [\"website\", \"phone\", \"walk_in\", \"import\", \"operator\"] as const;\nexport const BOOKING_STATUSES = [\"pending\", \"confirmed\", \"cancelled\", \"no_show\"] as const;\nexport const GOOGLE_SYNC_STATES = [\"queued\", \"synced\", \"uncertain\", \"failed\", \"drifted\"] as const;\nexport const CALENDAR_SYNC_STATUSES = [\n \"pending_invite\",\n \"awaiting_calendar_selection\",\n \"connected\",\n \"needs_reconnect\",\n \"revoked\",\n \"disabled\",\n] as const;\n\n/** Every failure the calendar API can hand back, so a site can branch on it. */\nexport const CALENDAR_ERROR_CODES = [\n \"slot_unavailable\",\n \"calendar_not_connected\",\n \"calendar_disconnected\",\n \"booking_not_found\",\n \"outside_booking_window\",\n \"rate_limited\",\n \"google_error\",\n] as const;\nexport type CalendarErrorCode = (typeof CALENDAR_ERROR_CODES)[number];\n\nexport type BookingStatus = (typeof BOOKING_STATUSES)[number];\nexport type GoogleSyncState = (typeof GOOGLE_SYNC_STATES)[number];\nexport type CalendarSyncStatus = (typeof CALENDAR_SYNC_STATUSES)[number];\nexport type JsonScalar = string | number | boolean | null;\n\nconst isoInstant = z.string().datetime({ offset: true });\n\n/**\n * Does the runtime actually know this zone?\n *\n * `Intl.DateTimeFormat` rather than `Intl.supportedValuesOf(\"timeZone\")`: the\n * latter returns canonical zones only and omits live aliases (`Asia/Calcutta`,\n * `US/Pacific`, `Europe/Kiev`) that the API's `ZoneInfo` accepts. Validating\n * against it would reject values the server stores happily.\n */\nfunction isKnownTimeZone(timeZone: string): boolean {\n try {\n new Intl.DateTimeFormat(undefined, { timeZone });\n return true;\n } catch {\n return false;\n }\n}\n\n// The refinement is invisible to `zod-to-json-schema` — unrepresentable\n// constraints are dropped — so `events.schema.json` and the drift test against\n// the Pydantic mirror are unaffected while both sides gain the real check.\nconst ianaTz = z\n .string()\n .min(1)\n .max(64)\n .regex(/^[A-Za-z0-9_+\\-/]+$/)\n .refine(isKnownTimeZone, { message: \"unknown IANA time zone\" });\n\nexport const bookingAttendeeSchema = z\n .object({\n email: z.string().max(320).email(),\n display_name: z.string().max(200).nullable().optional(),\n optional: z.boolean().default(false),\n })\n .strict();\n\nexport const bookingReminderSchema = z\n .object({\n method: z.enum([\"email\", \"popup\"]),\n minutes_before: z.number().int().min(0).max(MAX_REMINDER_MINUTES),\n })\n .strict();\n\nexport const bookingRemindersSchema = z\n .object({\n use_calendar_defaults: z.boolean().default(true),\n overrides: z.array(bookingReminderSchema).max(MAX_REMINDER_OVERRIDES).default([]),\n })\n .strict();\n\nexport const bookingLocationSchema = z.union([\n z.object({ kind: z.literal(\"google_meet\") }).strict(),\n z.object({ kind: z.literal(\"physical\"), address: z.string().max(1024) }).strict(),\n z.object({ kind: z.literal(\"phone\"), phone: z.string().max(64) }).strict(),\n z.object({ kind: z.literal(\"custom\"), text: z.string().max(1024) }).strict(),\n]);\n\n/**\n * The nullable form, flattened.\n *\n * `bookingLocationSchema.nullable()` would nest one `anyOf` inside another,\n * while Pydantic's `BookingLocation | None` flattens to a single list — the\n * drift snapshot compares structure, so both sides emit the flat shape.\n */\nexport const bookingLocationNullableSchema = z.union([...bookingLocationSchema.options, z.null()]);\n\nexport const bookingCustomerSchema = z\n .object({\n // Required: Google needs an address to send the invite that confirms the\n // booking, and that invite is the whole confirmation mechanism.\n email: z.string().max(320).email(),\n name: z.string().max(200).nullable().optional(),\n phone: z.string().max(64).nullable().optional(),\n sms_consent: z.boolean().default(false),\n })\n .strict();\n\nconst jsonScalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);\n\n/**\n * Named attribution keys plus a flat escape hatch.\n *\n * `custom` is flat and scalar-only on purpose: it mirrors trivially in Pydantic,\n * stays JSONB-queryable, and bounds what a caller can push. Only `custom` is\n * ever mirrored to Google's `extendedProperties`; the named keys stay in our\n * database.\n */\nexport const bookingMetadataSchema = z\n .object({\n schema_version: z.number().int().default(CALENDAR_METADATA_SCHEMA_VERSION),\n source: z.enum(BOOKING_SOURCES).nullable().optional(),\n service: z.string().max(64).nullable().optional(),\n visitor_id: z.string().max(64).nullable().optional(),\n session_id: z.string().max(64).nullable().optional(),\n gclid: z.string().max(256).nullable().optional(),\n fbclid: z.string().max(256).nullable().optional(),\n utm_source: z.string().max(128).nullable().optional(),\n utm_medium: z.string().max(128).nullable().optional(),\n utm_campaign: z.string().max(128).nullable().optional(),\n utm_term: z.string().max(128).nullable().optional(),\n utm_content: z.string().max(128).nullable().optional(),\n landing_page: z.string().max(2048).nullable().optional(),\n referrer: z.string().max(2048).nullable().optional(),\n notes: z.string().max(2000).nullable().optional(),\n custom: z.record(jsonScalar).nullable().optional(),\n })\n .strict();\n\nfunction refineEndXor(\n value: { start_at: string; end_at?: string | null; duration_minutes?: number | null },\n ctx: z.RefinementCtx,\n): void {\n const hasEnd = value.end_at !== undefined && value.end_at !== null;\n const hasDuration = value.duration_minutes !== undefined && value.duration_minutes !== null;\n if (hasEnd === hasDuration) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"provide exactly one of end_at or duration_minutes\",\n path: [\"end_at\"],\n });\n return;\n }\n if (hasEnd && new Date(value.end_at as string) <= new Date(value.start_at)) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"end_at must be after start_at\",\n path: [\"end_at\"],\n });\n }\n}\n\nexport const bookingCreateSchema = z\n .object({\n calendar_key: z.string().min(1).max(64),\n // Free-form: appointment types are the client site's own vocabulary, not\n // configuration the dashboard holds.\n appointment_type: z.string().max(64).nullable().optional(),\n start_at: isoInstant,\n end_at: isoInstant.nullable().optional(),\n duration_minutes: z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),\n /** The booker's zone — distinct from the business's and the calendar's. */\n time_zone: ianaTz,\n title: z.string().max(300).nullable().optional(),\n description: z.string().max(8000).nullable().optional(),\n location: bookingLocationNullableSchema.optional(),\n customer: bookingCustomerSchema,\n attendees: z.array(bookingAttendeeSchema).max(MAX_ATTENDEES).default([]),\n send_invites: z.boolean().default(true),\n reminders: bookingRemindersSchema.nullable().optional(),\n metadata: bookingMetadataSchema.nullable().optional(),\n idempotency_key: z.string().max(128).nullable().optional(),\n buffer_before_minutes: z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),\n buffer_after_minutes: z.number().int().min(0).max(MAX_BOOKING_MINUTES).default(0),\n })\n .strict()\n .superRefine(refineEndXor);\n\nexport const bookingRescheduleSchema = z\n .object({\n start_at: isoInstant,\n end_at: isoInstant.nullable().optional(),\n duration_minutes: z.number().int().positive().max(MAX_BOOKING_MINUTES).nullable().optional(),\n time_zone: ianaTz,\n notify_attendees: z.boolean().default(true),\n reason: z.string().max(500).nullable().optional(),\n })\n .strict()\n .superRefine(refineEndXor);\n\nexport const bookingCancelSchema = z\n .object({\n reason: z.string().max(500).nullable().optional(),\n notify_attendees: z.boolean().default(true),\n })\n .strict();\n\n// `z.input` so Zod defaults stay optional for callers.\nexport type BookingCreateInput = z.input<typeof bookingCreateSchema>;\nexport type BookingRescheduleInput = z.input<typeof bookingRescheduleSchema>;\nexport type BookingCancelInput = z.input<typeof bookingCancelSchema>;\nexport type BookingAttendee = z.input<typeof bookingAttendeeSchema>;\nexport type BookingReminders = z.input<typeof bookingRemindersSchema>;\nexport type BookingLocation = z.input<typeof bookingLocationSchema>;\nexport type BookingCustomer = z.input<typeof bookingCustomerSchema>;\nexport type BookingMetadata<\n TCustom extends Record<string, JsonScalar> = Record<string, JsonScalar>,\n> = Omit<z.input<typeof bookingMetadataSchema>, \"custom\"> & { custom?: TCustom | null };\n\n// ---------------------------------------------------------------------------\n// Read shapes — server responses, trusted and therefore not re-validated here.\n// ---------------------------------------------------------------------------\n\nexport interface CalendarSummary {\n calendar_key: string;\n label: string;\n time_zone: string;\n status: CalendarSyncStatus;\n /** Secret-key only. A public key gets `null`: whose Google account is behind\n * a calendar is a third party's identity, not the client site's business. */\n google_account_email?: string | null;\n granted_scopes?: string[] | null;\n connected_at?: string | null;\n}\n\nexport interface BusyInterval {\n start_at: string;\n end_at: string;\n source: \"google\" | \"booking\";\n}\n\nexport interface BusyResponse {\n calendar_key: string;\n time_zone: string;\n window: { since: string; until: string };\n busy: BusyInterval[];\n generated_at: string;\n}\n\nexport interface Booking {\n id: string;\n calendar_key: string;\n status: BookingStatus;\n google_sync_state: GoogleSyncState;\n start_at: string;\n end_at: string;\n time_zone: string;\n appointment_type: string | null;\n title: string;\n customer: BookingCustomer;\n google_html_link: string | null;\n meet_url: string | null;\n /** Returned only on create — the capability credential for manage links. */\n manage_token?: string | null;\n}\n","import type { BusyInterval } from \"./schema\";\nimport { instantFromWallClock, offsetMinutesAt } from \"./time\";\n\n/**\n * Slot computation lives in the client site, not in our database.\n *\n * Opening hours, slot length, notice and buffers differ per client and per use\n * case; modelling them as dashboard config would mean every new client needs\n * data entry before their site works. The API answers only \"when is this person\n * busy\" — this pure helper turns that plus the site's own hours into offerable\n * slots.\n */\n\nexport interface OpeningInterval {\n /** Local wall-clock `HH:MM` in the calendar's zone. */\n start: string;\n end: string;\n}\n\nexport interface OpeningHours {\n /** 0 = Sunday … 6 = Saturday. A missing weekday is closed. */\n weekly: Partial<Record<number, OpeningInterval[]>>;\n /** `YYYY-MM-DD` → intervals, replacing that day's weekly rule entirely. */\n overrides?: Record<string, OpeningInterval[]>;\n /** `YYYY-MM-DD` dates that are closed regardless of the weekly rule. */\n closedDates?: string[];\n}\n\nexport interface ComputeSlotsOptions {\n busy: BusyInterval[];\n openingHours: OpeningHours;\n timeZone: string;\n durationMinutes: number;\n granularityMinutes?: number;\n minNoticeMinutes?: number;\n bufferBeforeMinutes?: number;\n bufferAfterMinutes?: number;\n windowDays?: number;\n now?: Date;\n}\n\nexport interface Slot {\n start_at: string;\n end_at: string;\n}\n\nexport interface ComputeSlotsResult {\n slots: Slot[];\n nextAvailableAt: string | null;\n}\n\nfunction parseHhMm(value: string): [number, number] {\n const [hour, minute] = value.split(\":\").map(Number);\n return [hour || 0, minute || 0];\n}\n\nfunction dateKeyOf(date: Date, timeZone: string): string {\n const fmt = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n return fmt.format(date);\n}\n\n/** Weekday of a `YYYY-MM-DD` local date, read at noon UTC so no offset can\n * shift it across a day boundary. */\nfunction weekdayFromDateKey(dateKey: string): number {\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();\n}\n\n/** Midday of the local date containing `at` — a stable anchor to step from,\n * immune to a midnight DST jump. */\nfunction startOfLocalDay(at: Date, timeZone: string): Date {\n const [year, month, day] = dateKeyOf(at, timeZone).split(\"-\").map(Number);\n return instantFromWallClock(timeZone, year, month, day, 12, 0);\n}\n\nfunction overlaps(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && aEnd > bStart;\n}\n\nexport function computeSlots(options: ComputeSlotsOptions): ComputeSlotsResult {\n const {\n busy,\n openingHours,\n timeZone,\n durationMinutes,\n granularityMinutes = 15,\n minNoticeMinutes = 0,\n bufferBeforeMinutes = 0,\n bufferAfterMinutes = 0,\n windowDays = 30,\n now = new Date(),\n } = options;\n\n const earliest = now.getTime() + minNoticeMinutes * 60_000;\n const busyRanges = busy.map((interval) => [\n new Date(interval.start_at).getTime(),\n new Date(interval.end_at).getTime(),\n ]);\n\n const slots: Slot[] = [];\n // Advance by CALENDAR days, not by 86_400_000 ms. A DST day is 23 or 25 hours\n // long, so fixed-millisecond stepping skips a local date entirely at spring\n // forward and visits one twice at fall back — in America/Toronto, adding 24h\n // from 2026-03-08T04:30Z jumps straight from the 7th to the 9th.\n let cursor = startOfLocalDay(now, timeZone);\n for (let dayOffset = 0; dayOffset < windowDays; dayOffset += 1) {\n const dateKey = dateKeyOf(cursor, timeZone);\n const [year, month, day] = dateKey.split(\"-\").map(Number);\n // `Date.UTC` normalises day 32 into the next month, so this needs no\n // month-length handling.\n cursor = instantFromWallClock(timeZone, year, month, day + 1, 12, 0);\n\n if (openingHours.closedDates?.includes(dateKey)) continue;\n\n const intervals =\n openingHours.overrides?.[dateKey] ?? openingHours.weekly[weekdayFromDateKey(dateKey)] ?? [];\n for (const interval of intervals) {\n const [openHour, openMinute] = parseHhMm(interval.start);\n const [closeHour, closeMinute] = parseHhMm(interval.end);\n const opensAt = instantFromWallClock(timeZone, year, month, day, openHour, openMinute);\n const closesAt = instantFromWallClock(timeZone, year, month, day, closeHour, closeMinute);\n\n for (\n let start = opensAt.getTime();\n start + durationMinutes * 60_000 <= closesAt.getTime();\n start += granularityMinutes * 60_000\n ) {\n const end = start + durationMinutes * 60_000;\n if (start < earliest) continue;\n\n // A slot is unavailable if its BUFFERED span touches anything busy —\n // the buffers are what keep a gap between appointments.\n const blockedStart = start - bufferBeforeMinutes * 60_000;\n const blockedEnd = end + bufferAfterMinutes * 60_000;\n const taken = busyRanges.some(([busyStart, busyEnd]) =>\n overlaps(blockedStart, blockedEnd, busyStart, busyEnd),\n );\n if (taken) continue;\n\n slots.push({\n start_at: new Date(start).toISOString(),\n end_at: new Date(end).toISOString(),\n });\n }\n }\n }\n\n slots.sort((a, b) => a.start_at.localeCompare(b.start_at));\n return { slots, nextAvailableAt: slots[0]?.start_at ?? null };\n}\n\n/** Group slots by local date, the shape every date-picker UI wants. */\nexport function groupSlotsByDay(\n slots: Slot[],\n timeZone: string,\n): { date: string; slots: Slot[] }[] {\n const byDay = new Map<string, Slot[]>();\n for (const slot of slots) {\n const key = dateKeyOf(new Date(slot.start_at), timeZone);\n const bucket = byDay.get(key);\n if (bucket) bucket.push(slot);\n else byDay.set(key, [slot]);\n }\n return [...byDay.entries()]\n .map(([date, daySlots]) => ({ date, slots: daySlots }))\n .sort((a, b) => a.date.localeCompare(b.date));\n}\n\nexport { offsetMinutesAt };\n"],"mappings":"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","names":["z","durationMinutes"]}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { T as TrackingParams, a as TrackingInstallSurface, b as TrackingEnvironment, c as TrackingClientContext, d as TrackingEventCreatePayload, e as TrackingSessionUpsertPayload, F as FormSubmitConfig, f as FormSubmitMetadata, C as ConsentState, g as ConsentChoiceState, h as ConsentSource, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, P as PhoneConfig, i as ParsedPhone, j as PhoneDisplayFormat } from './phone-utils-B2vHJkNz.mjs';
2
- export { k as ConsentChoice, D as DEFAULT_DECLINE_TTL_DAYS, l as DEFAULT_PHONE_COUNTRY, J as JsonValue, S as SetConsentOptions, m as TrackedField, n as TrackingInitConfig, o as formSubmitConfigSchema, p as formSubmitMetadataSchema, q as formatPhone, r as formatPhoneAsTyped, s as getConsentChoice, t as getConsentState, u as jsonValueSchema, v as onConsentChange, w as optIn, x as optOut, y as parsePhone, z as phoneField, A as resetConsent, B as setConsentState, E as toE164 } from './phone-utils-B2vHJkNz.mjs';
3
- import { T as TrackingConfigReference, C as ConversionConfig } from './sales-B3BnYncU.mjs';
4
- export { B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, d as ConversionConfigStore, e as ConversionUserData, f as CurrencyRevenue, g as CustomerCurrencyDelta, h as CustomerCurrencyTotal, i as CustomerGetOptions, j as CustomerGetResult, k as CustomerKpis, l as CustomerKpisDeltas, m as CustomerKpisPrevious, n as CustomerListPage, o as CustomerListQuery, p as CustomerProfile, q as CustomerSegment, r as CustomerSegmentCount, s as CustomerSortField, t as CustomerSummary, u as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, v as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, w as Sale, x as SaleCursorPage, y as SaleFilters, z as SaleInput, A 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, U as SaleSummaryQuery, V as SaleSummaryQueryV2, W as SaleSummaryV2, X as SaleUpdateInput, Y as SalesBusinessClient, Z as SalesCategoryBreakdown, _ as SalesClient, $ as SalesClientConfig, a0 as SalesCustomersClient, a1 as SalesServiceBreakdown, a2 as SalesTransportConfig, a3 as SalesTrendPoint, a4 as SummaryCurrencyDelta, a5 as SummaryDeltas, a6 as SummaryWindow, a7 as SupportedCurrency, a8 as TRACKING_RANGES, a9 as TrackingOverviewRange, aa as clearStashedUserData, ab as createSalesClient, ac as fetchServices, ad as formatMoney, ae as fromMinor, af as getTrackingConfigRuntime, ag as resolveConversionConfig, ah as saleCreateSchema, ai as saleItemSchema, aj as saleServiceSchema, ak as saleUpdateSchema, al as salesRequest, am as stashUserData, an as toMinor } from './sales-B3BnYncU.mjs';
1
+ import { T as TrackingParams, a as TrackingInstallSurface, b as TrackingEnvironment, c as TrackingClientContext, d as TrackingEventCreatePayload, e as TrackingSessionUpsertPayload, F as FormSubmitConfig, f as FormSubmitMetadata, C as ConsentState, g as ConsentChoiceState, h as ConsentSource, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, P as PhoneConfig, i as ParsedPhone, j as PhoneDisplayFormat } from './phone-utils-BhWfNuPS.mjs';
2
+ export { k as ConsentChoice, D as DEFAULT_DECLINE_TTL_DAYS, l as DEFAULT_PHONE_COUNTRY, J as JsonValue, S as SetConsentOptions, m as TrackedField, n as TrackingInitConfig, o as formSubmitConfigSchema, p as formSubmitMetadataSchema, q as formatPhone, r as formatPhoneAsTyped, s as getConsentChoice, t as getConsentState, u as jsonValueSchema, v as onConsentChange, w as optIn, x as optOut, y as parsePhone, z as phoneField, A as resetConsent, B as setConsentState, E as toE164 } from './phone-utils-BhWfNuPS.mjs';
3
+ import { T as TrackingConfigReference, C as ConversionConfig } from './sales-H_6bHrL-.mjs';
4
+ export { B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, d as ConversionConfigStore, e as ConversionUserData, f as CurrencyRevenue, g as CustomerCurrencyDelta, h as CustomerCurrencyTotal, i as CustomerGetOptions, j as CustomerGetResult, k as CustomerKpis, l as CustomerKpisDeltas, m as CustomerKpisPrevious, n as CustomerListPage, o as CustomerListQuery, p as CustomerProfile, q as CustomerSegment, r as CustomerSegmentCount, s as CustomerSortField, t as CustomerSummary, u as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, v as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, w as Sale, x as SaleCursorPage, y as SaleFilters, z as SaleInput, A 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, U as SaleSummaryQuery, V as SaleSummaryQueryV2, W as SaleSummaryV2, X as SaleUpdateInput, Y as SalesBusinessClient, Z as SalesCategoryBreakdown, _ as SalesClient, $ as SalesClientConfig, a0 as SalesCustomersClient, a1 as SalesServiceBreakdown, a2 as SalesTransportConfig, a3 as SalesTrendPoint, a4 as SummaryCurrencyDelta, a5 as SummaryDeltas, a6 as SummaryWindow, a7 as SupportedCurrency, a8 as TRACKING_RANGES, a9 as TrackingOverviewRange, aa as clearStashedUserData, ab as createSalesClient, ac as fetchServices, ad as formatMoney, ae as fromMinor, af as getTrackingConfigRuntime, ag as resolveConversionConfig, ah as saleCreateSchema, ai as saleItemSchema, aj as saleServiceSchema, ak as saleUpdateSchema, al as salesRequest, am as stashUserData, an as toMinor } from './sales-H_6bHrL-.mjs';
5
5
  import { z } from 'zod';
6
6
  import * as src from 'src';
7
7
  export { A as AranovaApiError, f as formatDateInTz } from './date-BHetnL7s.mjs';
@@ -10,8 +10,8 @@ import { ReactNode, CSSProperties, InputHTMLAttributes, ChangeEvent, FocusEvent
10
10
  import * as react_jsx_runtime from 'react/jsx-runtime';
11
11
  import { CalendarReadClient, ComputeSlotsOptions, OpeningHours, Slot } from './calendar.mjs';
12
12
  export { BusyQuery, CalendarReadClientConfig, ComputeSlotsResult, OpeningInterval, computeSlots, createCalendarReadClient, durationMinutes, endOfDayInTz, formatSlotRange, groupSlotsByDay, instantFromWallClock, isCalendarError, localDateKey, offsetMinutesAt, resolveBrowserTimeZone, startOfDayInTz } from './calendar.mjs';
13
- import { a as Booking, B as BookingCreateInput, d as BusyInterval } from './transport-Bku8zftU.mjs';
14
- export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, g as BookingAttendee, c as BookingCancelInput, h as BookingCustomer, i as BookingLocation, j as BookingMetadata, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, m as BusyResponse, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, q as CalendarErrorCode, r as CalendarSummary, s as CalendarSyncStatus, C as CalendarTransportConfig, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-Bku8zftU.mjs';
13
+ import { a as Booking, B as BookingCreateInput, d as BusyInterval } from './transport-yUF5Wize.mjs';
14
+ export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, g as BookingAttendee, c as BookingCancelInput, h as BookingCustomer, i as BookingLocation, j as BookingMetadata, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, m as BusyResponse, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, q as CalendarErrorCode, r as CalendarSummary, s as CalendarSyncStatus, C as CalendarTransportConfig, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-yUF5Wize.mjs';
15
15
  import { CountryCode } from 'libphonenumber-js';
16
16
  export { CountryCode } from 'libphonenumber-js';
17
17
  import './request-Doq36Rt0.mjs';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { T as TrackingParams, a as TrackingInstallSurface, b as TrackingEnvironment, c as TrackingClientContext, d as TrackingEventCreatePayload, e as TrackingSessionUpsertPayload, F as FormSubmitConfig, f as FormSubmitMetadata, C as ConsentState, g as ConsentChoiceState, h as ConsentSource, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, P as PhoneConfig, i as ParsedPhone, j as PhoneDisplayFormat } from './phone-utils-B2vHJkNz.js';
2
- export { k as ConsentChoice, D as DEFAULT_DECLINE_TTL_DAYS, l as DEFAULT_PHONE_COUNTRY, J as JsonValue, S as SetConsentOptions, m as TrackedField, n as TrackingInitConfig, o as formSubmitConfigSchema, p as formSubmitMetadataSchema, q as formatPhone, r as formatPhoneAsTyped, s as getConsentChoice, t as getConsentState, u as jsonValueSchema, v as onConsentChange, w as optIn, x as optOut, y as parsePhone, z as phoneField, A as resetConsent, B as setConsentState, E as toE164 } from './phone-utils-B2vHJkNz.js';
3
- import { T as TrackingConfigReference, C as ConversionConfig } from './sales-CDjXRdmd.js';
4
- export { B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, d as ConversionConfigStore, e as ConversionUserData, f as CurrencyRevenue, g as CustomerCurrencyDelta, h as CustomerCurrencyTotal, i as CustomerGetOptions, j as CustomerGetResult, k as CustomerKpis, l as CustomerKpisDeltas, m as CustomerKpisPrevious, n as CustomerListPage, o as CustomerListQuery, p as CustomerProfile, q as CustomerSegment, r as CustomerSegmentCount, s as CustomerSortField, t as CustomerSummary, u as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, v as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, w as Sale, x as SaleCursorPage, y as SaleFilters, z as SaleInput, A 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, U as SaleSummaryQuery, V as SaleSummaryQueryV2, W as SaleSummaryV2, X as SaleUpdateInput, Y as SalesBusinessClient, Z as SalesCategoryBreakdown, _ as SalesClient, $ as SalesClientConfig, a0 as SalesCustomersClient, a1 as SalesServiceBreakdown, a2 as SalesTransportConfig, a3 as SalesTrendPoint, a4 as SummaryCurrencyDelta, a5 as SummaryDeltas, a6 as SummaryWindow, a7 as SupportedCurrency, a8 as TRACKING_RANGES, a9 as TrackingOverviewRange, aa as clearStashedUserData, ab as createSalesClient, ac as fetchServices, ad as formatMoney, ae as fromMinor, af as getTrackingConfigRuntime, ag as resolveConversionConfig, ah as saleCreateSchema, ai as saleItemSchema, aj as saleServiceSchema, ak as saleUpdateSchema, al as salesRequest, am as stashUserData, an as toMinor } from './sales-CDjXRdmd.js';
1
+ import { T as TrackingParams, a as TrackingInstallSurface, b as TrackingEnvironment, c as TrackingClientContext, d as TrackingEventCreatePayload, e as TrackingSessionUpsertPayload, F as FormSubmitConfig, f as FormSubmitMetadata, C as ConsentState, g as ConsentChoiceState, h as ConsentSource, G as GtagEnvironmentMap, M as MetaPixelEnvironmentMap, P as PhoneConfig, i as ParsedPhone, j as PhoneDisplayFormat } from './phone-utils-BhWfNuPS.js';
2
+ export { k as ConsentChoice, D as DEFAULT_DECLINE_TTL_DAYS, l as DEFAULT_PHONE_COUNTRY, J as JsonValue, S as SetConsentOptions, m as TrackedField, n as TrackingInitConfig, o as formSubmitConfigSchema, p as formSubmitMetadataSchema, q as formatPhone, r as formatPhoneAsTyped, s as getConsentChoice, t as getConsentState, u as jsonValueSchema, v as onConsentChange, w as optIn, x as optOut, y as parsePhone, z as phoneField, A as resetConsent, B as setConsentState, E as toE164 } from './phone-utils-BhWfNuPS.js';
3
+ import { T as TrackingConfigReference, C as ConversionConfig } from './sales-C7tPnqbq.js';
4
+ export { B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, d as ConversionConfigStore, e as ConversionUserData, f as CurrencyRevenue, g as CustomerCurrencyDelta, h as CustomerCurrencyTotal, i as CustomerGetOptions, j as CustomerGetResult, k as CustomerKpis, l as CustomerKpisDeltas, m as CustomerKpisPrevious, n as CustomerListPage, o as CustomerListQuery, p as CustomerProfile, q as CustomerSegment, r as CustomerSegmentCount, s as CustomerSortField, t as CustomerSummary, u as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, v as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, w as Sale, x as SaleCursorPage, y as SaleFilters, z as SaleInput, A 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, U as SaleSummaryQuery, V as SaleSummaryQueryV2, W as SaleSummaryV2, X as SaleUpdateInput, Y as SalesBusinessClient, Z as SalesCategoryBreakdown, _ as SalesClient, $ as SalesClientConfig, a0 as SalesCustomersClient, a1 as SalesServiceBreakdown, a2 as SalesTransportConfig, a3 as SalesTrendPoint, a4 as SummaryCurrencyDelta, a5 as SummaryDeltas, a6 as SummaryWindow, a7 as SupportedCurrency, a8 as TRACKING_RANGES, a9 as TrackingOverviewRange, aa as clearStashedUserData, ab as createSalesClient, ac as fetchServices, ad as formatMoney, ae as fromMinor, af as getTrackingConfigRuntime, ag as resolveConversionConfig, ah as saleCreateSchema, ai as saleItemSchema, aj as saleServiceSchema, ak as saleUpdateSchema, al as salesRequest, am as stashUserData, an as toMinor } from './sales-C7tPnqbq.js';
5
5
  import { z } from 'zod';
6
6
  import * as src from 'src';
7
7
  export { A as AranovaApiError, f as formatDateInTz } from './date-BHetnL7s.js';
@@ -10,8 +10,8 @@ import { ReactNode, CSSProperties, InputHTMLAttributes, ChangeEvent, FocusEvent
10
10
  import * as react_jsx_runtime from 'react/jsx-runtime';
11
11
  import { CalendarReadClient, ComputeSlotsOptions, OpeningHours, Slot } from './calendar.js';
12
12
  export { BusyQuery, CalendarReadClientConfig, ComputeSlotsResult, OpeningInterval, computeSlots, createCalendarReadClient, durationMinutes, endOfDayInTz, formatSlotRange, groupSlotsByDay, instantFromWallClock, isCalendarError, localDateKey, offsetMinutesAt, resolveBrowserTimeZone, startOfDayInTz } from './calendar.js';
13
- import { a as Booking, B as BookingCreateInput, d as BusyInterval } from './transport-DsOY6kcW.js';
14
- export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, g as BookingAttendee, c as BookingCancelInput, h as BookingCustomer, i as BookingLocation, j as BookingMetadata, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, m as BusyResponse, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, q as CalendarErrorCode, r as CalendarSummary, s as CalendarSyncStatus, C as CalendarTransportConfig, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-DsOY6kcW.js';
13
+ import { a as Booking, B as BookingCreateInput, d as BusyInterval } from './transport-C-_rFSR4.js';
14
+ export { e as BOOKING_SOURCES, f as BOOKING_STATUSES, g as BookingAttendee, c as BookingCancelInput, h as BookingCustomer, i as BookingLocation, j as BookingMetadata, k as BookingReminders, b as BookingRescheduleInput, l as BookingStatus, m as BusyResponse, n as CALENDAR_ERROR_CODES, o as CALENDAR_METADATA_SCHEMA_VERSION, p as CALENDAR_SYNC_STATUSES, q as CalendarErrorCode, r as CalendarSummary, s as CalendarSyncStatus, C as CalendarTransportConfig, G as GOOGLE_SYNC_STATES, t as GoogleSyncState, J as JsonScalar, M as MAX_ATTENDEES, u as MAX_BOOKING_MINUTES, v as MAX_REMINDER_MINUTES, w as MAX_REMINDER_OVERRIDES, x as bookingAttendeeSchema, y as bookingCancelSchema, z as bookingCreateSchema, A as bookingCustomerSchema, D as bookingLocationNullableSchema, E as bookingLocationSchema, F as bookingMetadataSchema, H as bookingReminderSchema, I as bookingRemindersSchema, K as bookingRescheduleSchema } from './transport-C-_rFSR4.js';
15
15
  import { CountryCode } from 'libphonenumber-js';
16
16
  export { CountryCode } from 'libphonenumber-js';
17
17
  import './request-Doq36Rt0.js';
package/dist/index.js CHANGED
@@ -382,6 +382,43 @@ var aranovaConsent = (function () {
382
382
  `.trim();
383
383
  }
384
384
 
385
+ // ../tracking-core/src/capabilities.ts
386
+ var TRACKING_CAPABILITIES = [
387
+ "ad_tags_google",
388
+ "ad_tags_meta",
389
+ "base_tracking",
390
+ "blog_rendering",
391
+ "calendar_read",
392
+ "calendar_write",
393
+ "consent_controls",
394
+ "conversion_goals_auto",
395
+ "conversion_goals_manual",
396
+ "cta_click_capture",
397
+ "form_capture",
398
+ "phone_click_capture",
399
+ "phone_fields"
400
+ ];
401
+ var CAPABILITY_DOM_ATTRIBUTE = "data-aranova-capability";
402
+ var registered = /* @__PURE__ */ new Set();
403
+ function registerCapability(capability) {
404
+ registered.add(capability);
405
+ }
406
+ function getRegisteredCapabilities() {
407
+ const all = new Set(registered);
408
+ for (const marker of readDomMarkers()) all.add(marker);
409
+ return Array.from(all).sort();
410
+ }
411
+ function readDomMarkers() {
412
+ if (typeof document === "undefined") return [];
413
+ const known = new Set(TRACKING_CAPABILITIES);
414
+ const found = [];
415
+ for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {
416
+ const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);
417
+ if (value && known.has(value)) found.push(value);
418
+ }
419
+ return found;
420
+ }
421
+
385
422
  // ../tracking-core/src/tracking.ts
386
423
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
387
424
  var TRACKING_PARAM_KEYS = [
@@ -726,7 +763,8 @@ function createTrackingClientContext(surface, input = {}) {
726
763
  page_title: input.pageTitle ?? (typeof document === "undefined" ? null : document.title || null),
727
764
  referrer: input.referrer ?? (typeof document === "undefined" ? null : document.referrer || null),
728
765
  environment: input.environment ?? "production",
729
- active_gtag_ids: input.activeGtagIds ?? null
766
+ active_gtag_ids: input.activeGtagIds ?? null,
767
+ capabilities: getRegisteredCapabilities()
730
768
  };
731
769
  }
732
770
  function createTrackingSessionUpsertPayload(trackingParams, input, context) {
@@ -1720,7 +1758,10 @@ function buildContext(surface, sdkVersion, packageName, environment, activeGtagI
1720
1758
  page_title: typeof document === "undefined" ? null : document.title || null,
1721
1759
  referrer: typeof document === "undefined" ? null : document.referrer || null,
1722
1760
  environment,
1723
- active_gtag_ids: activeGtagIds
1761
+ active_gtag_ids: activeGtagIds,
1762
+ // Rebuilt per flush (this runs inside the payload builder), so a client
1763
+ // constructed later on a deeper route still gets reported.
1764
+ capabilities: getRegisteredCapabilities()
1724
1765
  };
1725
1766
  }
1726
1767
  function readTrackingParams() {
@@ -2313,8 +2354,9 @@ function getEventDefinition(name) {
2313
2354
  }
2314
2355
 
2315
2356
  // ../tracking-core/src/ingest-typed.ts
2316
- function createTypedClient(raw, _registry, options = {}) {
2357
+ function createTypedClient(raw, registry, options = {}) {
2317
2358
  const debug = options.debug ?? false;
2359
+ if (registry.manual?.form_submit) registerCapability("form_capture");
2318
2360
  return {
2319
2361
  trackEvent(eventType, metadata, opts) {
2320
2362
  if (debug) {
@@ -2721,6 +2763,7 @@ function attachCtaClickCapture(client, config) {
2721
2763
  return () => {
2722
2764
  };
2723
2765
  }
2766
+ registerCapability("cta_click_capture");
2724
2767
  const selector = autoCapture.selector ?? DEFAULT_CTA_SELECTOR;
2725
2768
  function onClick(event) {
2726
2769
  const target = event.target;
@@ -2777,6 +2820,7 @@ function attachPhoneClickCapture(client, config) {
2777
2820
  return () => {
2778
2821
  };
2779
2822
  }
2823
+ registerCapability("phone_click_capture");
2780
2824
  const selector = autoCapture.selector ?? DEFAULT_TEL_SELECTOR;
2781
2825
  function onClick(event) {
2782
2826
  const target = event.target;
@@ -2893,6 +2937,7 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2893
2937
  }
2894
2938
  }
2895
2939
  function createSalesClient(config) {
2940
+ if (config.firing) registerCapability("conversion_goals_manual");
2896
2941
  async function record(input) {
2897
2942
  const currency = input.currency ?? config.defaultCurrency;
2898
2943
  if (!currency) {
@@ -3192,6 +3237,7 @@ var DEFAULT_CHOICE2 = {
3192
3237
  expiresAt: null
3193
3238
  };
3194
3239
  function useCookiePreferences(options) {
3240
+ registerCapability("consent_controls");
3195
3241
  const [choice, setChoice] = (0, import_react.useState)(DEFAULT_CHOICE2);
3196
3242
  const ttlDays = options?.declineTtlDays;
3197
3243
  (0, import_react.useEffect)(() => {
@@ -3459,6 +3505,8 @@ function AdPlatformTracking({
3459
3505
  }, [trackingConfigKey, standalonePageView]);
3460
3506
  const gtagAll = trackingConfig ? [] : gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3461
3507
  const metaAll = metaPixelIds ? Object.values(metaPixelIds).filter((id) => id != null) : metaPixelId ? [metaPixelId] : [];
3508
+ if (trackingConfig || gtagAll.length > 0) registerCapability("ad_tags_google");
3509
+ if (metaAll.length > 0) registerCapability("ad_tags_meta");
3462
3510
  const gtagLoaderId = gtagAll[0];
3463
3511
  const gtagInitScript = gtagAll.length === 1 ? createGtagInitScript(gtagAll[0]) : createGtagInitScriptMulti(gtagAll);
3464
3512
  const metaInitScript = metaAll.length === 1 ? createMetaPixelInitScript(metaAll[0]) : createMetaPixelInitScriptMulti(metaAll);
@@ -3507,7 +3555,7 @@ function GoogleAdsTracking(props) {
3507
3555
  var import_navigation3 = require("next/navigation");
3508
3556
 
3509
3557
  // package.json
3510
- var version = "0.21.0";
3558
+ var version = "0.22.0";
3511
3559
 
3512
3560
  // src/factory.tsx
3513
3561
  var import_react5 = require("react");
@@ -3534,6 +3582,7 @@ function usePhoneConfig() {
3534
3582
  };
3535
3583
  }
3536
3584
  function usePhoneField(opts = {}) {
3585
+ registerCapability("phone_fields");
3537
3586
  const cfg = usePhoneConfig();
3538
3587
  const country = opts.country ?? cfg.defaultCountry;
3539
3588
  const display = opts.display ?? cfg.display;
@@ -3620,6 +3669,8 @@ function createTracking(options) {
3620
3669
  conversionConfig,
3621
3670
  trackingConfig
3622
3671
  } = options;
3672
+ registerCapability("base_tracking");
3673
+ if (trackingConfig) registerCapability("conversion_goals_auto");
3623
3674
  if (!apiKey || !endpoint) {
3624
3675
  if (apiKey || endpoint) {
3625
3676
  console.warn(
@@ -3861,6 +3912,7 @@ function withBusiness(config, path) {
3861
3912
  return `${path}${separator}business_id=${encodeURIComponent(config.businessId)}`;
3862
3913
  }
3863
3914
  function createCalendarReadClient(config) {
3915
+ registerCapability("calendar_read");
3864
3916
  return {
3865
3917
  async listCalendars() {
3866
3918
  return calendarRequest(config, "GET", withBusiness(config, "/calendars"));