abmeter
Version:
ABMeter browser SDK — feature flags and A/B experiments with server-side pre-evaluated assignments
1 lines • 48.9 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/assignments-cache.ts","../src/api-error.ts","../src/async-submitter.ts","../src/dedup.ts","../src/http.ts","../src/user.ts","../src/error-safety.ts","../src/config.ts","../src/client.ts","../src/version.ts"],"sourcesContent":["export { configure, resolveParameter, getExposure, trackEvent, flush, reset, ready } from './client';\nexport { VERSION } from './version';\n\nexport type { ConfigInput, Config, Logger, ErrorCallback } from './config';\nexport type { User, UserInput } from './user';\nexport type { Assignment, AssignmentsPayload, ExposureMetadata } from './assignments-cache';\nexport type { ExposureRecord } from './client';\nexport { ApiError } from './api-error';\n","export const DEFAULT_BASE_URL = 'https://abmeter.ai';\nexport const DEFAULT_FLUSH_INTERVAL_MS = 1000;\n\n// Deliberately smaller than Ruby/Python's 100: those serve many users per\n// process, a browser serves one, so events are sparse. Small batches stay far\n// under the ~64 KiB keepalive/beacon quota and lose less on tab death.\nexport const BATCH_SIZE = 20;\nexport const MAX_SUBMIT_ATTEMPTS = 3;\nexport const MAX_RETRY_QUEUE = 1000;\n\nexport const DEDUP_WINDOW_MS = 10 * 60 * 1000;\nexport const DEDUP_MAX_KEYS = 1000;\n\n// fetch keepalive shares a ~64 KiB in-flight quota per page with sendBeacon;\n// chunk bodies to 80% of it rather than hoping a batch fits.\nexport const KEEPALIVE_BODY_LIMIT_BYTES = Math.floor(64 * 1024 * 0.8);\n\nexport const ASSIGNMENTS_STORAGE_PREFIX = 'abmeter:assignments:';\nexport const TRACK_ID_STORAGE_KEY = 'abmeter:track_id';\nexport const TRACK_ID_COOKIE_NAME = 'abmeter_track_id';\nexport const TRACK_ID_COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;\n\nexport const USER_ASSIGNMENTS_PATH = '/api/v1/user-assignments';\nexport const EXPOSURES_PATH = '/api/v1/exposures';\nexport const EVENTS_PATH = '/api/v1/events';\n","import { ASSIGNMENTS_STORAGE_PREFIX } from './constants';\nimport type { HttpClient } from './http';\nimport type { Logger } from './config';\nimport type { User } from './user';\n\nexport interface ExposureMetadata {\n exposable_type: 'Experiment';\n exposable_id: number;\n audience_id: number;\n}\n\nexport interface Assignment {\n value: unknown;\n parameter_id: number;\n space_id: number;\n /** null for feature-flag and default resolutions — never report those as exposures. */\n exposure: ExposureMetadata | null;\n}\n\nexport interface AssignmentsPayload {\n user: { user_id: string; email?: string | null };\n assignments: Record<string, Assignment>;\n}\n\ninterface StoredEntry {\n etag?: string;\n payload: AssignmentsPayload;\n}\n\n// The value map + hydration. Cache-then-network: hydrate synchronously from\n// localStorage (unblocks first paint), then refresh in the background with\n// If-None-Match. localStorage blocked → in-memory only; still works, just no\n// cross-session cache.\nexport class AssignmentsCache {\n private readonly http: HttpClient;\n private readonly user: User;\n private readonly logger: Logger;\n private readonly storage: Storage | null;\n private assignments: Record<string, Assignment> = {};\n private etag?: string;\n\n constructor({ http, user, logger }: { http: HttpClient; user: User; logger: Logger }) {\n this.http = http;\n this.user = user;\n this.logger = logger;\n this.storage = detectStorage();\n }\n\n hydrateFromStorage(): boolean {\n const raw = this.storageRead(this.storageKey());\n if (!raw) return false;\n\n try {\n const entry = JSON.parse(raw) as StoredEntry;\n if (!entry?.payload?.assignments) return false;\n this.assignments = entry.payload.assignments;\n this.etag = entry.etag;\n return true;\n } catch {\n return false;\n }\n }\n\n async refresh(): Promise<void> {\n const body: { user_id: string; email?: string } =\n this.user.email === undefined\n ? { user_id: this.user.userId }\n : { user_id: this.user.userId, email: this.user.email };\n\n const response = await this.http.fetchAssignments(body, this.etag);\n if (response.status === 304) return; // cache is current\n\n if (response.payload) {\n this.assignments = response.payload.assignments;\n this.etag = response.etag;\n this.persist({ etag: response.etag, payload: response.payload });\n }\n }\n\n resolveValue(slug: string): unknown {\n return this.assignments[slug]?.value;\n }\n\n resolveAssignment(slug: string): Assignment | undefined {\n return this.assignments[slug];\n }\n\n has(slug: string): boolean {\n return slug in this.assignments;\n }\n\n private persist(entry: StoredEntry): void {\n try {\n this.storage?.setItem(this.storageKey(), JSON.stringify(entry));\n } catch (error) {\n this.logger('assignments cache not persisted', error);\n }\n }\n\n private storageRead(key: string): string | null {\n try {\n return this.storage?.getItem(key) ?? null;\n } catch {\n return null;\n }\n }\n\n private storageKey(): string {\n return `${ASSIGNMENTS_STORAGE_PREFIX}${this.user.userId}`;\n }\n}\n\nfunction detectStorage(): Storage | null {\n try {\n const storage = globalThis.localStorage;\n const probe = 'abmeter:probe';\n storage.setItem(probe, '1');\n storage.removeItem(probe);\n return storage;\n } catch {\n return null;\n }\n}\n","interface ErrorBody {\n error?: unknown;\n details?: { failures?: unknown[]; invalid_count?: number };\n}\n\n// Same contract as the Ruby/Python SDKs: retryable (5xx/408/429) errors\n// re-queue, partial failures and other 4xx are permanent and drop.\nexport class ApiError extends Error {\n readonly status: number;\n readonly details: ErrorBody['details'];\n\n constructor(status: number, body: unknown) {\n const parsed: ErrorBody = typeof body === 'object' && body !== null ? (body as ErrorBody) : {};\n super(typeof parsed.error === 'string' ? parsed.error : `HTTP ${status}`);\n this.name = 'ApiError';\n this.status = status;\n this.details = parsed.details;\n }\n\n get retryable(): boolean {\n return this.status >= 500 || this.status === 408 || this.status === 429;\n }\n\n get partialFailure(): boolean {\n return this.status === 400 && Array.isArray(this.details?.failures) && this.details.failures.length > 0;\n }\n}\n","import { ApiError } from './api-error';\nimport {\n BATCH_SIZE,\n KEEPALIVE_BODY_LIMIT_BYTES,\n MAX_RETRY_QUEUE,\n MAX_SUBMIT_ATTEMPTS,\n} from './constants';\nimport type { ErrorCallback, Logger } from './config';\nimport type { HttpClient } from './http';\n\ntype Kind = 'exposure' | 'event';\n\ninterface QueueItem {\n kind: Kind;\n data: Record<string, unknown>;\n attempts: number;\n}\n\nexport interface SubmitterOptions {\n http: HttpClient;\n flushIntervalMs: number;\n batchSize?: number;\n logger: Logger;\n errorCallback?: ErrorCallback;\n}\n\n// Main-thread queue: setInterval + batch-size trigger, drained hard on\n// visibilitychange→hidden and pagehide (never unload/beforeunload — those\n// break bfcache and don't fire on mobile). The hidden drain uses keepalive\n// fetch with a sendBeacon fallback; a refused beacon is logged and dropped —\n// ad-blockers exist, loss is expected, never fatal.\nexport class AsyncSubmitter {\n private readonly http: HttpClient;\n private readonly flushIntervalMs: number;\n private readonly batchSize: number;\n private readonly logger: Logger;\n private readonly errorCallback?: ErrorCallback;\n\n private exposures: QueueItem[] = [];\n private events: QueueItem[] = [];\n private retryQueue: QueueItem[] = [];\n\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushChain: Promise<void> = Promise.resolve();\n private queuedFlush: Promise<void> | null = null;\n private readonly onVisibilityChange = (): void => {\n if (globalThis.document?.visibilityState === 'hidden') this.drainOnHide();\n };\n private readonly onPageHide = (): void => {\n this.drainOnHide();\n };\n\n constructor(options: SubmitterOptions) {\n this.http = options.http;\n this.flushIntervalMs = options.flushIntervalMs;\n this.batchSize = options.batchSize ?? BATCH_SIZE;\n this.logger = options.logger;\n this.errorCallback = options.errorCallback;\n }\n\n start(): void {\n if (this.timer === null) {\n this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);\n }\n globalThis.document?.addEventListener('visibilitychange', this.onVisibilityChange);\n globalThis.window?.addEventListener('pagehide', this.onPageHide);\n }\n\n /** Detach timer + listeners without draining. reset() is the draining teardown. */\n stop(): void {\n if (this.timer !== null) {\n clearInterval(this.timer);\n this.timer = null;\n }\n globalThis.document?.removeEventListener('visibilitychange', this.onVisibilityChange);\n globalThis.window?.removeEventListener('pagehide', this.onPageHide);\n }\n\n queueExposure(data: Record<string, unknown>): void {\n this.exposures.push({ kind: 'exposure', data, attempts: 0 });\n this.flushIfFull();\n }\n\n queueEvent(data: Record<string, unknown>): void {\n this.events.push({ kind: 'event', data, attempts: 0 });\n this.flushIfFull();\n }\n\n pending(): number {\n return this.exposures.length + this.events.length + this.retryQueue.length;\n }\n\n /**\n * Drain the queues over the network. One retry batch is attempted per flush;\n * items failing during this flush land in the retry queue and wait for the\n * next one, so a failing server can't spin this loop forever.\n *\n * Passes are serialized, never concurrent. flush() during a running pass\n * returns a promise for the NEXT pass — resolving early while a drain is\n * still in flight would break callers (SPA route changes, the canonical\n * app's pre-close barrier) that await flush() as proof nothing is queued.\n * Callers arriving in the same window share one queued pass.\n */\n flush(): Promise<void> {\n if (this.queuedFlush) return this.queuedFlush;\n\n const pass = this.flushChain.then(() => {\n this.queuedFlush = null;\n return this.drain();\n });\n this.queuedFlush = pass;\n this.flushChain = pass.catch(() => undefined);\n return pass;\n }\n\n private async drain(): Promise<void> {\n await this.retryOneBatch();\n while (this.exposures.length > 0 || this.events.length > 0) {\n if (this.exposures.length > 0) {\n await this.submitBatch('exposure', this.exposures.splice(0, this.batchSize));\n }\n if (this.events.length > 0) {\n await this.submitBatch('event', this.events.splice(0, this.batchSize));\n }\n }\n }\n\n /**\n * Drain fully and tear down. Terminates even against a failing server: every\n * failed batch either drops (validation, events on network error) or re-queues\n * with attempts++ and drops at MAX_SUBMIT_ATTEMPTS.\n */\n async reset(options: { force?: boolean } = {}): Promise<void> {\n this.stop();\n if (options.force) {\n const dropped = this.pending();\n this.exposures = [];\n this.events = [];\n this.retryQueue = [];\n if (dropped > 0) this.logger(`reset(force): dropped ${dropped} queued items`);\n return;\n }\n while (this.pending() > 0) {\n await this.flush();\n }\n }\n\n /**\n * Tab-death drain: chunked keepalive fetch (text/plain, CORS-simple) with a\n * sendBeacon fallback when the fetch fails. Fire-and-forget by necessity —\n * the page may be gone before any response arrives.\n */\n drainOnHide(): void {\n const items = [...this.retryQueue, ...this.exposures, ...this.events];\n this.retryQueue = [];\n this.exposures = [];\n this.events = [];\n\n for (const kind of ['exposure', 'event'] as const) {\n const rows = items.filter((item) => item.kind === kind).map((item) => item.data);\n for (const chunk of chunkForKeepalive(rows, this.batchSize)) {\n this.sendChunkOnHide(kind, chunk);\n }\n }\n }\n\n private sendChunkOnHide(kind: Kind, rows: unknown[]): void {\n const beaconFallback = (): void => {\n const accepted =\n kind === 'exposure' ? this.http.beaconExposures(rows) : this.http.beaconEvents(rows);\n if (!accepted) this.logger(`beacon refused; dropping ${rows.length} ${kind}s`);\n };\n\n try {\n const post =\n kind === 'exposure'\n ? this.http.postExposures(rows, { keepalive: true })\n : this.http.postEvents(rows, { keepalive: true });\n post.catch(beaconFallback);\n } catch {\n beaconFallback();\n }\n }\n\n private flushIfFull(): void {\n if (this.exposures.length + this.events.length >= this.batchSize) void this.flush();\n }\n\n private async retryOneBatch(): Promise<void> {\n if (this.retryQueue.length === 0) return;\n const batch = this.retryQueue.splice(0, this.batchSize);\n const byKind = new Map<Kind, QueueItem[]>();\n for (const item of batch) {\n const bucket = byKind.get(item.kind) ?? [];\n bucket.push(item);\n byKind.set(item.kind, bucket);\n }\n for (const [kind, items] of byKind) {\n await this.submitBatch(kind, items);\n }\n }\n\n private async submitBatch(kind: Kind, items: QueueItem[]): Promise<void> {\n const rows = items.map((item) => item.data);\n try {\n if (kind === 'exposure') {\n await this.http.postExposures(rows);\n } else {\n await this.http.postEvents(rows);\n }\n } catch (error) {\n this.handleSubmitError(kind, items, error);\n }\n }\n\n // The asymmetric retry contract shared with Ruby/Python: retryable API\n // error → re-queue (max attempts); validation/permanent → drop; unknown\n // network error → exposures re-queued (they feed the completeness gate),\n // events dropped.\n private handleSubmitError(kind: Kind, items: QueueItem[], error: unknown): void {\n if (error instanceof ApiError) {\n if (error.retryable) {\n this.logger(`retryable API error for ${items.length} ${kind}s`, error.message);\n this.requeue(items);\n } else if (error.partialFailure) {\n this.logger(`partial failure, dropping batch of ${items.length} ${kind}s`, error.message);\n } else {\n this.logger(`permanent API error, dropping ${items.length} ${kind}s`, error.message);\n }\n } else if (kind === 'exposure') {\n this.logger(`network error, re-queueing ${items.length} exposures`, error);\n this.requeue(items);\n } else {\n this.logger(`network error, dropping ${items.length} events`, error);\n }\n this.errorCallback?.(error);\n }\n\n private requeue(items: QueueItem[]): void {\n for (const item of items) {\n item.attempts += 1;\n if (item.attempts >= MAX_SUBMIT_ATTEMPTS) {\n this.logger(`max retries exceeded, dropping ${item.kind}`);\n } else if (this.retryQueue.length >= MAX_RETRY_QUEUE) {\n this.logger(`retry queue full, dropping ${item.kind}`);\n } else {\n this.retryQueue.push(item);\n }\n }\n }\n}\n\n// Split rows so each JSON body stays under the shared keepalive/beacon quota.\n// String length approximates bytes (payloads are ASCII-dominant); the 20%\n// headroom in the limit absorbs the difference.\nexport function chunkForKeepalive(rows: unknown[], batchSize: number): unknown[][] {\n const chunks: unknown[][] = [];\n let current: unknown[] = [];\n let currentBytes = 2; // []\n\n for (const row of rows) {\n const rowBytes = JSON.stringify(row).length + 1;\n const overflow = currentBytes + rowBytes > KEEPALIVE_BODY_LIMIT_BYTES || current.length >= batchSize;\n if (overflow && current.length > 0) {\n chunks.push(current);\n current = [];\n currentBytes = 2;\n }\n current.push(row);\n currentBytes += rowBytes;\n }\n if (current.length > 0) chunks.push(current);\n return chunks;\n}\n","import { DEDUP_MAX_KEYS, DEDUP_WINDOW_MS } from './constants';\n\n// Sliding-window exposure dedup: a Map used as an insertion-ordered LRU.\n// Re-sighting a key refreshes its slot; eviction drops the oldest entry once\n// the cap is hit, so a burst of distinct keys can't grow memory unbounded.\nexport class ExposureDedup {\n private readonly windowMs: number;\n private readonly maxKeys: number;\n private readonly now: () => number;\n private readonly entries = new Map<string, number>();\n\n constructor({\n windowMs = DEDUP_WINDOW_MS,\n maxKeys = DEDUP_MAX_KEYS,\n now = Date.now,\n }: { windowMs?: number; maxKeys?: number; now?: () => number } = {}) {\n this.windowMs = windowMs;\n this.maxKeys = maxKeys;\n this.now = now;\n }\n\n /** True when the key was already seen inside the window; records the sighting either way. */\n seenRecently(key: string): boolean {\n const timestamp = this.now();\n const seenAt = this.entries.get(key);\n const duplicate = seenAt !== undefined && timestamp - seenAt < this.windowMs;\n\n this.entries.delete(key);\n this.entries.set(key, duplicate ? (seenAt as number) : timestamp);\n this.evict();\n return duplicate;\n }\n\n private evict(): void {\n while (this.entries.size > this.maxKeys) {\n const oldest = this.entries.keys().next().value;\n if (oldest === undefined) return;\n this.entries.delete(oldest);\n }\n }\n}\n\nexport function exposureDedupKey(exposure: {\n user_id: string;\n exposable_id: number;\n audience_id: number;\n resolved_value: unknown;\n}): string {\n return `${exposure.user_id}|${exposure.exposable_id}|${exposure.audience_id}|${JSON.stringify(exposure.resolved_value)}`;\n}\n","import { ApiError } from './api-error';\nimport { EVENTS_PATH, EXPOSURES_PATH, USER_ASSIGNMENTS_PATH } from './constants';\nimport type { AssignmentsPayload } from './assignments-cache';\n\nexport interface AssignmentsResponse {\n status: 200 | 304;\n payload?: AssignmentsPayload;\n etag?: string;\n}\n\nexport interface PostOptions {\n /** Unload path: keepalive fetch + text/plain body (CORS-simple, no preflight). */\n keepalive?: boolean;\n}\n\n// fetch-based client for the three browser endpoints. Non-2xx responses throw\n// ApiError; network failures propagate the underlying TypeError so the\n// submitter can apply its asymmetric unknown-error contract.\nexport class HttpClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n\n constructor({ baseUrl, apiKey }: { baseUrl: string; apiKey: string }) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async fetchAssignments(\n body: { user_id: string; email?: string },\n etag?: string\n ): Promise<AssignmentsResponse> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n };\n if (etag) headers['If-None-Match'] = etag;\n\n const response = await fetch(`${this.baseUrl}${USER_ASSIGNMENTS_PATH}`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (response.status === 304) return { status: 304 };\n if (!response.ok) throw new ApiError(response.status, await parseBody(response));\n\n return {\n status: 200,\n payload: (await response.json()) as AssignmentsPayload,\n etag: response.headers.get('ETag') ?? undefined,\n };\n }\n\n async postExposures(rows: unknown[], options?: PostOptions): Promise<void> {\n await this.post(EXPOSURES_PATH, rows, options);\n }\n\n async postEvents(rows: unknown[], options?: PostOptions): Promise<void> {\n await this.post(EVENTS_PATH, rows, options);\n }\n\n /**\n * Last-resort transport for tab death: sendBeacon cannot set headers, so the\n * API key travels as a query token (accepted server-side only on the two\n * write endpoints). Returns false when the browser refuses the beacon.\n */\n beaconExposures(rows: unknown[]): boolean {\n return this.beacon(EXPOSURES_PATH, rows);\n }\n\n beaconEvents(rows: unknown[]): boolean {\n return this.beacon(EVENTS_PATH, rows);\n }\n\n private async post(path: string, rows: unknown[], options?: PostOptions): Promise<void> {\n const keepalive = options?.keepalive ?? false;\n const response = await fetch(`${this.baseUrl}${path}`, {\n method: 'POST',\n // text/plain on the keepalive path keeps the request CORS-simple (no\n // preflight — a preflight can't complete once the page is gone).\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': keepalive ? 'text/plain' : 'application/json',\n },\n body: JSON.stringify(rows),\n keepalive,\n });\n if (!response.ok) throw new ApiError(response.status, await parseBody(response));\n }\n\n private beacon(path: string, rows: unknown[]): boolean {\n const sendBeacon = globalThis.navigator?.sendBeacon?.bind(globalThis.navigator);\n if (!sendBeacon) return false;\n\n const url = `${this.baseUrl}${path}?api_key=${encodeURIComponent(this.apiKey)}`;\n const body = new Blob([JSON.stringify(rows)], { type: 'text/plain' });\n try {\n return sendBeacon(url, body);\n } catch {\n return false;\n }\n }\n}\n\nasync function parseBody(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n}\n","import {\n TRACK_ID_COOKIE_MAX_AGE_SECONDS,\n TRACK_ID_COOKIE_NAME,\n TRACK_ID_STORAGE_KEY,\n} from './constants';\n\nexport interface UserInput {\n userId?: string;\n email?: string;\n}\n\nexport interface User {\n userId: string;\n email?: string;\n}\n\n// When the caller supplies no id, identity is a generated track id: a random\n// UUID persisted in cookie + localStorage and sent as the user_id wire field.\n// ~122 bits of randomness make targeted impersonation of anonymous users\n// infeasible and keep real user ids out of browser traffic. Note: Safari ITP\n// caps JS-set cookies at ~7 days; long experiments should set the cookie\n// server-side.\nexport function ensureUser(input?: UserInput): User {\n const userId = input?.userId ?? loadOrCreateTrackId();\n return input?.email === undefined ? { userId } : { userId, email: input.email };\n}\n\nfunction loadOrCreateTrackId(): string {\n const existing = readCookie(TRACK_ID_COOKIE_NAME) ?? readStorage(TRACK_ID_STORAGE_KEY);\n const trackId = existing ?? generateUuid();\n persistTrackId(trackId);\n return trackId;\n}\n\nfunction persistTrackId(trackId: string): void {\n writeCookie(TRACK_ID_COOKIE_NAME, trackId);\n writeStorage(TRACK_ID_STORAGE_KEY, trackId);\n}\n\nfunction generateUuid(): string {\n const cryptoApi = globalThis.crypto;\n if (cryptoApi?.randomUUID) return cryptoApi.randomUUID();\n\n const bytes = new Uint8Array(16);\n if (cryptoApi?.getRandomValues) {\n cryptoApi.getRandomValues(bytes);\n } else {\n for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = ((bytes[6] as number) & 0x0f) | 0x40;\n bytes[8] = ((bytes[8] as number) & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\nfunction readCookie(name: string): string | null {\n if (typeof document === 'undefined') return null;\n const match = document.cookie\n .split(';')\n .map((part) => part.trim())\n .find((part) => part.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.slice(name.length + 1)) || null : null;\n}\n\nfunction writeCookie(name: string, value: string): void {\n if (typeof document === 'undefined') return;\n try {\n document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${TRACK_ID_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n } catch {\n // Cookies blocked — localStorage (or memory for the session) still holds the id.\n }\n}\n\nfunction readStorage(key: string): string | null {\n try {\n return globalThis.localStorage?.getItem(key) ?? null;\n } catch {\n return null;\n }\n}\n\nfunction writeStorage(key: string, value: string): void {\n try {\n globalThis.localStorage?.setItem(key, value);\n } catch {\n // localStorage blocked (Safari private mode, extensions) — cookie may still work.\n }\n}\n","import type { ErrorCallback, Logger } from './config';\n\nexport interface SafetyHandlers {\n logger?: Logger;\n errorCallback?: ErrorCallback;\n}\n\n// The error boundary around the public API: never let SDK internals crash the\n// host page, but never swallow silently either — every failure is logged and\n// forwarded to errorCallback so a salt-bug-class regression stays visible.\nexport function guard<R>(operation: string, fallback: R, handlers: SafetyHandlers, fn: () => R): R {\n try {\n return fn();\n } catch (error) {\n report(operation, error, handlers);\n return fallback;\n }\n}\n\nexport async function guardAsync<R>(\n operation: string,\n fallback: R,\n handlers: SafetyHandlers,\n fn: () => Promise<R>\n): Promise<R> {\n try {\n return await fn();\n } catch (error) {\n report(operation, error, handlers);\n return fallback;\n }\n}\n\nfunction report(operation: string, error: unknown, handlers: SafetyHandlers): void {\n if (handlers.logger) {\n handlers.logger(`${operation} failed`, error);\n } else {\n console.error(`[abmeter] ${operation} failed`, error);\n }\n try {\n handlers.errorCallback?.(error);\n } catch {\n // A throwing errorCallback must not take the page down with it.\n }\n}\n","import { DEFAULT_BASE_URL, DEFAULT_FLUSH_INTERVAL_MS } from './constants';\nimport type { UserInput } from './user';\n\nexport type Logger = (message: string, payload?: unknown) => void;\nexport type ErrorCallback = (error: unknown) => void;\n\nexport interface ConfigInput {\n apiKey: string;\n baseUrl?: string;\n user?: UserInput;\n /** Milliseconds between background flushes. */\n flushInterval?: number;\n logger?: Logger;\n errorCallback?: ErrorCallback;\n}\n\nexport interface Config {\n apiKey: string;\n baseUrl: string;\n flushIntervalMs: number;\n logger: Logger;\n errorCallback?: ErrorCallback;\n}\n\nconst PUBLISHABLE_KEY_PREFIX = 'pk_';\n\nexport function resolveConfig(input: ConfigInput): Config {\n if (!input || typeof input.apiKey !== 'string' || input.apiKey.length === 0) {\n throw new Error('abmeter.configure: apiKey is required');\n }\n\n // Refuse, don't warn: anything embedded in a browser bundle is readable by\n // anyone, so this SDK only accepts keys that are safe to expose. A secret\n // key here would grant full account access to every visitor.\n if (!input.apiKey.startsWith(PUBLISHABLE_KEY_PREFIX)) {\n throw new Error(\n 'abmeter.configure: apiKey must be a publishable key (pk_...) — mint one on the ' +\n 'Lab API Keys page. Secret keys (api-...) must never be shipped in browser code: ' +\n 'anyone can read them from your page source and gain full access to your ABMeter account.'\n );\n }\n\n return {\n apiKey: input.apiKey,\n baseUrl: (input.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, ''),\n flushIntervalMs: input.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS,\n logger: input.logger ?? defaultLogger,\n errorCallback: input.errorCallback,\n };\n}\n\nfunction defaultLogger(message: string, payload?: unknown): void {\n if (payload === undefined) {\n console.warn(`[abmeter] ${message}`);\n } else {\n console.warn(`[abmeter] ${message}`, payload);\n }\n}\n","import { AssignmentsCache } from './assignments-cache';\nimport { AsyncSubmitter } from './async-submitter';\nimport { ExposureDedup, exposureDedupKey } from './dedup';\nimport { HttpClient } from './http';\nimport { ensureUser } from './user';\nimport { guard, guardAsync } from './error-safety';\nimport { resolveConfig } from './config';\nimport type { Config, ConfigInput } from './config';\nimport type { User } from './user';\n\nexport interface ExposureRecord {\n parameter_id: number;\n space_id: number;\n resolved_value: unknown;\n user_id: string;\n exposable_type: 'Experiment';\n exposable_id: number;\n audience_id: number;\n resolved_at: string;\n}\n\ninterface ClientState {\n config: Config;\n user: User;\n cache: AssignmentsCache;\n dedup: ExposureDedup;\n submitter: AsyncSubmitter;\n refreshPromise: Promise<void>;\n}\n\nlet state: ClientState | null = null;\n\n/**\n * Initialize the SDK singleton. Cache-then-network: hydrates synchronously\n * from localStorage when a cached map exists, then refreshes in the background\n * (await ready() to know the refresh settled). Throws on misconfiguration —\n * a bad apiKey must be loud, not error-safe.\n */\nexport function configure(input: ConfigInput): void {\n const config = resolveConfig(input);\n const user = ensureUser(input.user);\n const http = new HttpClient({ baseUrl: config.baseUrl, apiKey: config.apiKey });\n const cache = new AssignmentsCache({ http, user, logger: config.logger });\n const submitter = new AsyncSubmitter({\n http,\n flushIntervalMs: config.flushIntervalMs,\n logger: config.logger,\n errorCallback: config.errorCallback,\n });\n\n // Reconfiguring detaches the previous submitter and lets it drain itself.\n // configure() is synchronous, so the drain cannot be awaited or reported —\n // but dropping the queue would lose exposures and events already collected,\n // a silent hole in the customer's results, while a stray background drain\n // only costs bandwidth. reset({ force: true }) is how a caller opts into\n // discarding, and await reset() is how one gets a guaranteed drain; neither\n // should be the accidental default here. Best-effort: reset() detaches the\n // unload listeners first, so a page dying mid-drain still loses the tail.\n if (state) {\n const previous = state.submitter;\n const pending = previous.pending();\n if (pending > 0) state.config.logger(`configure: draining ${pending} queued items from the previous configuration`);\n void previous.reset().catch(() => {\n // Already logged by the submitter; a failed drain must not break configure().\n });\n }\n\n cache.hydrateFromStorage();\n submitter.start();\n const refreshPromise = cache.refresh().catch((error) => {\n config.logger('assignments refresh failed', error);\n try {\n config.errorCallback?.(error);\n } catch {\n // errorCallback failures must not surface here\n }\n });\n\n state = { config, user, cache, dedup: new ExposureDedup(), submitter, refreshPromise };\n}\n\n/** Resolves when the background assignments refresh has settled (fetched or failed). */\nexport function ready(): Promise<void> {\n return state?.refreshPromise ?? Promise.resolve();\n}\n\n/**\n * Resolved value for this user, or undefined when unknown/unconfigured. Queues\n * an exposure lazily — only experiment resolutions carry exposure metadata,\n * and repeats inside the dedup window are not re-queued.\n */\nexport function resolveParameter(slug: string): unknown {\n return guard('resolveParameter', undefined, handlers(), () => {\n const current = requireState();\n const assignment = current.cache.resolveAssignment(slug);\n if (!assignment) {\n current.config.logger(`resolveParameter: unknown parameter '${slug}'`);\n return undefined;\n }\n\n const exposure = buildExposure(current, slug);\n if (exposure && !current.dedup.seenRecently(exposureDedupKey(exposure))) {\n current.submitter.queueExposure({ ...exposure });\n }\n return assignment.value;\n });\n}\n\n/** The exposure record resolveParameter would submit, or null — without queueing anything. */\nexport function getExposure(slug: string): ExposureRecord | null {\n return guard('getExposure', null, handlers(), () => buildExposure(requireState(), slug));\n}\n\n/**\n * Queue an event for the configured user.\n *\n * Deliberately takes no user id, unlike the server-side SDKs: there one\n * process serves every user, so each call must say who it is for, while a\n * page has exactly the one user configure() established. An override would\n * only ever detach the event — results attribute events to a visitor by\n * matching the id their exposure was recorded under, so an event under any\n * other id is stored, counted, and never joined.\n */\nexport function trackEvent(eventSlug: string, customFields?: Record<string, unknown>): void {\n guard('trackEvent', undefined, handlers(), () => {\n const current = requireState();\n current.submitter.queueEvent({\n event_slug: eventSlug,\n user_id: current.user.userId,\n occurred_at: new Date().toISOString(),\n custom_fields: customFields ?? {},\n });\n });\n}\n\n/** Drain the queue now (e.g. on SPA route changes). */\nexport function flush(): Promise<void> {\n return guardAsync('flush', undefined, handlers(), async () => {\n await state?.submitter.flush();\n });\n}\n\n/**\n * Drain fully and tear down timers/listeners; configure() again to restart.\n * force drops the queue instead of draining it.\n */\nexport function reset(options: { force?: boolean } = {}): Promise<void> {\n return guardAsync('reset', undefined, handlers(), async () => {\n const current = state;\n state = null;\n await current?.submitter.reset(options);\n });\n}\n\nfunction buildExposure(current: ClientState, slug: string): ExposureRecord | null {\n const assignment = current.cache.resolveAssignment(slug);\n if (!assignment?.exposure) return null;\n\n return {\n parameter_id: assignment.parameter_id,\n space_id: assignment.space_id,\n resolved_value: assignment.value,\n user_id: current.user.userId,\n exposable_type: assignment.exposure.exposable_type,\n exposable_id: assignment.exposure.exposable_id,\n audience_id: assignment.exposure.audience_id,\n resolved_at: new Date().toISOString(),\n };\n}\n\nfunction requireState(): ClientState {\n if (!state) throw new Error('abmeter is not configured — call abmeter.configure(...) first');\n return state;\n}\n\nfunction handlers(): { logger?: (message: string, payload?: unknown) => void; errorCallback?: (error: unknown) => void } {\n return state ? { logger: state.config.logger, errorCallback: state.config.errorCallback } : {};\n}\n","export const VERSION = '0.2.2';\n"],"mappings":"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","names":["handlers"]}