@arsel.sa/web-sdk 1.0.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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/arsel.js +600 -0
- package/dist/arsel.js.map +1 -0
- package/dist/arsel.umd.cjs +2 -0
- package/dist/arsel.umd.cjs.map +1 -0
- package/dist/events.d.ts +23 -0
- package/dist/index.d.ts +81 -0
- package/dist/push.d.ts +39 -0
- package/dist/session.d.ts +21 -0
- package/dist/store.d.ts +52 -0
- package/dist/transport.d.ts +22 -0
- package/dist/types.d.ts +68 -0
- package/dist/version.d.ts +5 -0
- package/package.json +60 -0
- package/sw/arsel-sw.js +278 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arsel.js","sources":["../src/store.ts","../src/version.ts","../src/transport.ts","../src/events.ts","../src/push.ts","../src/session.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared durable state, in IndexedDB.\n *\n * IndexedDB rather than `localStorage` for one reason that decides it: the\n * service worker has to read the device secret and the client key to report a\n * `delivered` engagement when **no tab is open at all**, and `localStorage` is\n * unreachable from a worker. Splitting state across both stores would put the\n * two halves of one fact in two places.\n */\n\nconst DB_NAME = 'arsel';\nconst DB_VERSION = 1;\nconst KV_STORE = 'kv';\nconst EVENT_STORE = 'events';\n\nexport interface PendingEvent {\n /** Monotonic within a tab; the auto-increment key doubles as delivery order. */\n id?: number;\n body: string;\n /**\n * Minted once at enqueue and persisted, so a retry of the same event carries\n * the same key and the backend's 24h idempotency window can dedupe it.\n */\n idempotencyKey: string;\n createdAtMs: number;\n}\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openDb(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(KV_STORE)) db.createObjectStore(KV_STORE);\n if (!db.objectStoreNames.contains(EVENT_STORE)) {\n db.createObjectStore(EVENT_STORE, { keyPath: 'id', autoIncrement: true });\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n return dbPromise;\n}\n\nfunction tx<T>(\n store: string,\n mode: IDBTransactionMode,\n run: (s: IDBObjectStore) => IDBRequest<T>,\n): Promise<T> {\n return openDb().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const request = run(db.transaction(store, mode).objectStore(store));\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n }),\n );\n}\n\nexport function get<T>(key: string): Promise<T | null> {\n return tx<T | undefined>(KV_STORE, 'readonly', (s) => s.get(key)).then(\n (v) => v ?? null,\n );\n}\n\nexport function set(key: string, value: unknown): Promise<unknown> {\n return tx(KV_STORE, 'readwrite', (s) => s.put(value, key));\n}\n\nexport function remove(key: string): Promise<unknown> {\n return tx(KV_STORE, 'readwrite', (s) => s.delete(key));\n}\n\nexport function addEvent(body: string, idempotencyKey: string): Promise<unknown> {\n return tx(EVENT_STORE, 'readwrite', (s) =>\n s.add({ body, idempotencyKey, createdAtMs: Date.now() }),\n );\n}\n\nexport function allEvents(): Promise<PendingEvent[]> {\n return tx<PendingEvent[]>(EVENT_STORE, 'readonly', (s) => s.getAll());\n}\n\nexport function removeEvent(id: number): Promise<unknown> {\n return tx(EVENT_STORE, 'readwrite', (s) => s.delete(id));\n}\n\nexport function countEvents(): Promise<number> {\n return tx<number>(EVENT_STORE, 'readonly', (s) => s.count());\n}\n\nexport const KEYS = {\n clientKey: 'client_key',\n baseUrl: 'base_url',\n anonymousId: 'anonymous_id',\n externalId: 'external_id',\n email: 'email',\n phoneNumber: 'phone_number',\n installationId: 'installation_id',\n deviceSecret: 'device_secret',\n vapidKeyVersion: 'vapid_key_version',\n endpoint: 'endpoint',\n sessionStartedAt: 'session_started_at',\n backgroundedAt: 'backgrounded_at',\n lastResponseCode: 'last_response_code',\n lastResponsePath: 'last_response_path',\n lastResponseAtMs: 'last_response_at',\n} as const;\n\n/**\n * Person-shaped identity, minted on first use. Distinct from the installation\n * id, which names the browser profile: this one names whoever is using it and\n * is rotated by `reset()`, so a shared computer does not hand the next person\n * the previous one's history.\n */\nexport async function anonymousId(): Promise<string> {\n const existing = await get<string>(KEYS.anonymousId);\n if (existing) return existing;\n const minted = crypto.randomUUID();\n await set(KEYS.anonymousId, minted);\n return minted;\n}\n\nexport async function rotateAnonymousId(): Promise<string> {\n const minted = crypto.randomUUID();\n await set(KEYS.anonymousId, minted);\n return minted;\n}\n","/**\n * In its own module so transport can stamp `X-Arsel-SDK` without importing the\n * public surface (index → events → transport would be a cycle).\n */\nexport const SDK_VERSION = '1.0.0';\n","import { KEYS, set } from './store';\nimport { SDK_VERSION } from './version';\n\n/** Identifies the SDK build on every request; the backend records it. */\nconst SDK_HEADER = { 'X-Arsel-SDK': `web/${SDK_VERSION}` };\n\nexport const RESULT = {\n success: 'success',\n retryable: 'retryable',\n permanent: 'permanent',\n reauth: 'reauth',\n} as const;\n\nexport type Result = (typeof RESULT)[keyof typeof RESULT];\n\nexport interface Response<T = unknown> {\n result: Result;\n code: number;\n body: T | null;\n}\n\n/** No status line at all — DNS failure, offline, TLS error. */\nexport const CODE_NO_RESPONSE = -1;\n\n/**\n * Status → retry policy. Pure, and the same table the Android SDK uses: this is\n * the decision that separates a browser that eventually delivers from one that\n * gives up forever.\n */\nexport function classify(code: number, authenticated: boolean): Result {\n if (code >= 200 && code <= 299) return RESULT.success;\n if (code === 408 || code === 429 || (code >= 500 && code <= 599)) {\n return RESULT.retryable;\n }\n // On an authed route these all mean one thing behind the backend's\n // deliberately opaque 404: this secret is no longer accepted.\n if (authenticated && (code === 401 || code === 403 || code === 404)) {\n return RESULT.reauth;\n }\n // An unknown push key and an org whose channel is not switched on yet both\n // answer a bare 404. The second is ordinary onboarding, so giving up\n // permanently here would strand a browser that would have worked tomorrow.\n if (code === 404) return RESULT.retryable;\n return RESULT.permanent;\n}\n\nasync function record(path: string, code: number): Promise<void> {\n // Diagnostics only; nothing branches on it. Failures here must never mask the\n // request's own outcome.\n try {\n await Promise.all([\n set(KEYS.lastResponseCode, code),\n set(KEYS.lastResponsePath, path),\n set(KEYS.lastResponseAtMs, Date.now()),\n ]);\n } catch {\n /* diagnostics are best-effort */\n }\n}\n\nexport async function post<T = unknown>(\n baseUrl: string,\n path: string,\n body: unknown,\n headers: Record<string, string> = {},\n authenticated = false,\n): Promise<Response<T>> {\n let response: globalThis.Response;\n try {\n response = await fetch(baseUrl + path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...SDK_HEADER, ...headers },\n body: JSON.stringify(body),\n // The SDK never relies on cookies, and sending them would attach a\n // first-party session to a cross-origin call for no reason.\n credentials: 'omit',\n keepalive: true,\n });\n } catch {\n await record(path, CODE_NO_RESPONSE);\n return { result: RESULT.retryable, code: CODE_NO_RESPONSE, body: null };\n }\n\n await record(path, response.status);\n const result = classify(response.status, authenticated);\n let parsed: T | null = null;\n try {\n parsed = (await response.json()) as T;\n } catch {\n /* an empty or non-JSON body is fine — the status is what matters */\n }\n return { result, code: response.status, body: parsed };\n}\n\nexport async function getJson<T>(\n baseUrl: string,\n path: string,\n): Promise<T | null> {\n try {\n const response = await fetch(baseUrl + path, {\n credentials: 'omit',\n headers: SDK_HEADER,\n });\n await record(path, response.status);\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n await record(path, CODE_NO_RESPONSE);\n return null;\n }\n}\n","import {\n KEYS,\n addEvent,\n allEvents,\n anonymousId,\n get,\n removeEvent,\n} from './store';\nimport type { PendingEvent } from './store';\nimport { RESULT, post } from './transport';\nimport type { EventProperties } from './types';\n\nexport const EVENTS_PATH = '/v1/events/send';\n\n/** Reserved prefix, so an SDK event can never collide with a customer's. */\nexport const RESERVED_PREFIX = 'arsel.';\nexport const EVENT_SESSION_START = `${RESERVED_PREFIX}session_start`;\nexport const EVENT_SESSION_END = `${RESERVED_PREFIX}session_end`;\nexport const EVENT_IDENTIFY = `${RESERVED_PREFIX}identify`;\n\n// Mirror `IngestEventDto`'s @Length caps, applied here so the 400 never happens.\nconst MAX_EVENT_NAME = 80;\nconst MAX_ANONYMOUS_ID = 128;\nconst MAX_EXTERNAL_ID = 255;\n\n/** The batch ceiling `POST /v1/events/send` accepts in an `{ events: [...] }` body. */\nexport const MAX_BATCH = 50;\n\nconst MAX_DATA_DEPTH = 8;\nconst MAX_DATA_BYTES = 64 * 1024;\n\n/**\n * The backend accepts arbitrary nested JSON, so JSON-safe values pass through\n * unchanged. Dates become ISO strings, non-finite numbers become strings (they\n * are not valid JSON), and functions/symbols/undefined are dropped. The depth\n * cap also terminates cycles.\n */\nfunction sanitizeValue(value: unknown, depth: number): unknown {\n if (value === null) return null;\n const type = typeof value;\n if (type === 'string' || type === 'boolean') return value;\n if (type === 'number') return Number.isFinite(value) ? value : String(value);\n if (type === 'bigint') return String(value);\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? undefined : value.toISOString();\n }\n if (type !== 'object' || depth >= MAX_DATA_DEPTH) return undefined;\n if (Array.isArray(value)) {\n // undefined has no JSON form inside an array; null keeps the indices stable.\n return value.map((item) => sanitizeValue(item, depth + 1) ?? null);\n }\n const out: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n if (!key) continue;\n const clean = sanitizeValue(item, depth + 1);\n if (clean !== undefined) out[key] = clean;\n }\n return out;\n}\n\n/** `data` must be wire-safe JSON, bounded so one huge property cannot 413 the drain. */\nfunction sanitize(properties: EventProperties): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n let bytes = 2;\n let dropped = false;\n for (const [key, raw] of Object.entries(properties ?? {})) {\n if (!key) continue;\n const value = sanitizeValue(raw, 0);\n if (value === undefined) continue;\n const cost = key.length + JSON.stringify(value).length + 6;\n if (bytes + cost > MAX_DATA_BYTES) {\n dropped = true;\n continue;\n }\n bytes += cost;\n out[key] = value;\n }\n if (dropped) {\n console.warn(\n `[arsel] event data exceeded ${MAX_DATA_BYTES} serialized bytes — oversized properties were dropped`,\n );\n }\n return out;\n}\n\nasync function buildBody(\n name: string,\n properties: EventProperties,\n timestampMs: number,\n): Promise<string> {\n const [anon, externalId, email, phoneNumber] = await Promise.all([\n anonymousId(),\n get<string>(KEYS.externalId),\n get<string>(KEYS.email),\n get<string>(KEYS.phoneNumber),\n ]);\n\n const body: Record<string, unknown> = {\n event: name.slice(0, MAX_EVENT_NAME),\n // Always sent, even once identified: it is what the backend merges FROM.\n anonymous_id: anon.slice(0, MAX_ANONYMOUS_ID),\n data: sanitize(properties),\n timestamp: new Date(timestampMs).toISOString(),\n };\n if (externalId) body.external_id = externalId.slice(0, MAX_EXTERNAL_ID);\n if (email) body.email = email;\n if (phoneNumber) body.phone_number = phoneNumber;\n return JSON.stringify(body);\n}\n\n/**\n * Persist first, then send. A tab closed mid-flight would otherwise lose the\n * event outright, and page-lifetime-only buffering is the single most common\n * way a web analytics SDK silently undercounts.\n */\nexport async function enqueue(\n name: string,\n properties: EventProperties,\n timestampMs = Date.now(),\n): Promise<void> {\n await addEvent(await buildBody(name, properties, timestampMs), crypto.randomUUID());\n // Fire-and-forget: track() must not make the caller wait on the network.\n void flush().catch(() => {});\n}\n\nlet inFlight: Promise<void> | null = null;\n\n/**\n * Deliver everything queued, oldest first.\n *\n * Serialised, because two concurrent drains would both read the same rows and\n * send each event twice. A caller arriving mid-drain gets the in-flight promise\n * rather than a silent no-op, so `await flush()` actually means \"delivered\".\n */\nexport function flush(): Promise<void> {\n if (inFlight) return inFlight;\n inFlight = drain().finally(() => {\n inFlight = null;\n });\n return inFlight;\n}\n\n/**\n * Deterministic per batch: an identical batch resent after a timeout carries\n * the same key and dedupes inside the backend's 24h window.\n */\nfunction batchKey(batch: PendingEvent[]): string {\n const first = batch[0]!.idempotencyKey;\n if (batch.length === 1) return first;\n return `${first}:${batch[batch.length - 1]!.idempotencyKey}:${batch.length}`;\n}\n\n/**\n * A permanent rejection is dropped, but a drop that takes identifiers with it\n * must not be silent — one bad identifier would otherwise 400 every subsequent\n * event with no visible cause.\n */\nfunction reportPermanentDrop(\n items: Record<string, unknown>[],\n code: number,\n body: unknown,\n): void {\n const carried = items.some(\n (item) => item.external_id || item.email || item.phone_number,\n );\n if (!carried) return;\n console.error(\n `[arsel] ${items.length} event(s) carrying identifiers were rejected permanently ` +\n `(HTTP ${code}) and dropped. Response: ${JSON.stringify(body)}`,\n );\n}\n\nasync function drain(): Promise<void> {\n const [clientKey, baseUrl] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n ]);\n if (!clientKey || !baseUrl) return;\n\n // Re-reads until the queue is empty: anything enqueued while we were on the\n // network would otherwise sit there until the next unrelated trigger.\n for (;;) {\n const pending = await allEvents();\n if (pending.length === 0) return;\n\n for (let i = 0; i < pending.length; i += MAX_BATCH) {\n const batch = pending.slice(i, i + MAX_BATCH);\n const items = batch.map(\n (event) => JSON.parse(event.body) as Record<string, unknown>,\n );\n const response = await post(\n baseUrl,\n EVENTS_PATH,\n batch.length === 1 ? items[0] : { events: items },\n {\n Authorization: `Bearer ${clientKey}`,\n 'Idempotency-Key': batchKey(batch),\n },\n );\n // Stop at the first retryable failure rather than skipping past it — a\n // later batch overtaking an earlier one would reorder a user's history.\n // The whole batch retries under the same key, so a request that landed\n // but timed out on the way back dedupes instead of double-counting.\n if (response.result === RESULT.retryable) return;\n // Success or permanent: either way it is settled. A 4xx will never\n // succeed on a retry, and holding it would wedge everything behind it.\n if (response.result !== RESULT.success) {\n reportPermanentDrop(items, response.code, response.body);\n }\n for (const event of batch) {\n if (event.id !== undefined) await removeEvent(event.id);\n }\n }\n }\n}\n","import { KEYS, anonymousId, get, set } from './store';\nimport { RESULT, getJson, post } from './transport';\nimport type { WebPushConfig } from './types';\n\n/**\n * VAPID keys arrive base64url; `applicationServerKey` wants raw bytes.\n *\n * Returns the ArrayBuffer rather than the view: TS models `BufferSource` as\n * requiring a non-shared buffer, and a `Uint8Array` is only assignable when its\n * backing buffer is statically known not to be a `SharedArrayBuffer`.\n */\nfunction decodeBase64Url(value: string): ArrayBuffer {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/');\n const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);\n return bytes.buffer;\n}\n\nexport function isSupported(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n typeof PushManager !== 'undefined' &&\n typeof Notification !== 'undefined'\n );\n}\n\nfunction configPath(clientKey: string): string {\n return `/api/v1/orgs/${clientKey}/push/web/config`;\n}\n\nfunction subscriptionsPath(clientKey: string): string {\n return `/api/v1/orgs/${clientKey}/push/subscriptions`;\n}\n\nconst ACTIVATION_TIMEOUT_MS = 10_000;\n\n/**\n * How this page gets a service worker, decided once by `init()`:\n * - `none` — events-only integration; every push entry point fails fast.\n * - `managed` — the SDK registered `serviceWorkerPath` and holds the result.\n * - `external` — the customer's own worker `importScripts` arsel-sw.js; the SDK\n * registers nothing and uses whatever controls the page.\n */\nlet mode: 'none' | 'managed' | 'external' = 'none';\nlet managedRegistration: ServiceWorkerRegistration | null = null;\nlet registrationError: string | null = null;\n\nexport function useExternal(): void {\n mode = 'external';\n}\n\nexport async function register(path: string): Promise<void> {\n if (!isSupported()) return;\n mode = 'managed';\n try {\n // No explicit scope: the default is the path's directory, and forcing '/'\n // throws SecurityError for any worker served from a subpath.\n managedRegistration = await navigator.serviceWorker.register(path);\n registrationError = null;\n } catch (error) {\n managedRegistration = null;\n registrationError =\n `service worker registration failed for '${path}': ${String(error)}. ` +\n 'Check that the file is deployed and served with a JavaScript content type.';\n }\n}\n\n/**\n * Resolve once the registration has an active worker, or fail with a reason —\n * never `navigator.serviceWorker.ready`, which hangs forever when registration\n * failed and turns every caller into a silent no-op.\n */\nfunction waitForActive(\n registration: ServiceWorkerRegistration,\n timeoutMs: number,\n): Promise<ServiceWorkerRegistration> {\n if (registration.active) return Promise.resolve(registration);\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new Error(\n `service worker did not activate within ${timeoutMs}ms — ` +\n 'check the worker script for load errors in DevTools → Application → Service Workers',\n ),\n );\n }, timeoutMs);\n const watch = (worker: ServiceWorker): void => {\n worker.addEventListener('statechange', () => {\n if (worker.state === 'activated') {\n clearTimeout(timer);\n resolve(registration);\n } else if (worker.state === 'redundant') {\n clearTimeout(timer);\n reject(\n new Error(\n 'service worker became redundant before activating — its script likely failed to parse',\n ),\n );\n }\n });\n };\n const worker = registration.installing ?? registration.waiting;\n if (worker) watch(worker);\n else {\n registration.addEventListener('updatefound', () => {\n if (registration.installing) watch(registration.installing);\n });\n }\n });\n}\n\nfunction withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n message: string,\n): Promise<T> {\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n setTimeout(() => reject(new Error(message)), timeoutMs);\n }),\n ]);\n}\n\nasync function activeRegistration(\n timeoutMs = ACTIVATION_TIMEOUT_MS,\n): Promise<ServiceWorkerRegistration> {\n if (mode === 'none') {\n throw new Error(\n \"push is not configured — pass serviceWorkerPath to init(), or serviceWorker: 'external' \" +\n 'if your own service worker imports arsel-sw.js',\n );\n }\n if (mode === 'external') {\n return withTimeout(\n navigator.serviceWorker.ready,\n timeoutMs,\n `no service worker became active within ${timeoutMs}ms — with serviceWorker: 'external', ` +\n 'your own worker must be registered and must importScripts arsel-sw.js',\n );\n }\n if (registrationError) throw new Error(registrationError);\n if (!managedRegistration) {\n throw new Error('service worker registration has not completed');\n }\n return waitForActive(managedRegistration, timeoutMs);\n}\n\n/**\n * Everything that can be verified *before* the permission prompt, so a broken\n * worker setup fails with a reason instead of burning the origin's one real\n * prompt on a subscription that can never settle.\n *\n * Returns an error message, or null when push can proceed.\n */\nexport async function preflight(\n timeoutMs = ACTIVATION_TIMEOUT_MS,\n): Promise<string | null> {\n try {\n await activeRegistration(timeoutMs);\n return null;\n } catch (error) {\n return error instanceof Error ? error.message : String(error);\n }\n}\n\n/**\n * Subscribe this browser and register it with Arsel.\n *\n * Only ever called from a user gesture path — `subscribe()` triggers the\n * permission prompt, and a prompt fired on page load is what gets an origin\n * permanently blocked in Chrome's abusive-notification heuristics.\n */\nexport async function subscribe(): Promise<boolean> {\n const [clientKey, baseUrl] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n ]);\n if (!clientKey || !baseUrl || !isSupported()) return false;\n\n const config = await getJson<WebPushConfig>(baseUrl, configPath(clientKey));\n if (!config?.vapidPublicKey) return false;\n\n let registration: ServiceWorkerRegistration;\n try {\n registration = await activeRegistration();\n } catch (error) {\n console.error(`[arsel] ${error instanceof Error ? error.message : error}`);\n return false;\n }\n const subscription = await registration.pushManager.subscribe({\n // Non-negotiable: the Push API only accepts a userVisibleOnly subscription\n // in Chrome, and silent push is what the restriction exists to prevent.\n userVisibleOnly: true,\n applicationServerKey: decodeBase64Url(config.vapidPublicKey),\n });\n\n return sendSubscription(subscription, config.keyVersion);\n}\n\nasync function sendSubscription(\n subscription: PushSubscription,\n keyVersion: number,\n): Promise<boolean> {\n const [clientKey, baseUrl, anon] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n anonymousId(),\n ]);\n if (!clientKey || !baseUrl) return false;\n\n let installationId = await get<string>(KEYS.installationId);\n if (!installationId) {\n installationId = crypto.randomUUID();\n await set(KEYS.installationId, installationId);\n }\n\n // Posted VERBATIM. Reshaping this object is exactly where an SDK drops the\n // `auth` secret, and every send then fails encryption with no obvious cause.\n const json = subscription.toJSON();\n\n const response = await post<{ deviceSecret?: string }>(\n baseUrl,\n subscriptionsPath(clientKey),\n {\n installationId,\n platform: 'web',\n endpoint: json.endpoint,\n keys: json.keys,\n // The same id the events API sends — it is what binds this subscription\n // to the contact the browser's events resolve to.\n anonymousId: anon,\n enablementStatus:\n Notification.permission === 'granted' ? 'AUTHORIZED' : 'DENIED',\n deviceTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n deviceLocale: navigator.language,\n },\n );\n\n if (response.result !== RESULT.success) return false;\n\n // Issued exactly once, on the call that mints it. Losing it strands this\n // browser without a way to authenticate anything ever again.\n if (response.body?.deviceSecret) {\n await set(KEYS.deviceSecret, response.body.deviceSecret);\n }\n await set(KEYS.vapidKeyVersion, keyVersion);\n await set(KEYS.endpoint, json.endpoint ?? null);\n return true;\n}\n\n/**\n * Durable opt-out, mirroring the Android SDK: the backend revocation is\n * non-resurrectable — a later register for this installation will not undo it.\n * Distinct from `reset()`, which never touches push.\n */\nexport async function optOut(): Promise<boolean> {\n const [clientKey, baseUrl, installationId, deviceSecret] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n get<string>(KEYS.installationId),\n get<string>(KEYS.deviceSecret),\n ]);\n if (!clientKey || !baseUrl) return false;\n if (!installationId || !deviceSecret) {\n // Never registered — there is nothing server-side to revoke.\n return false;\n }\n\n const response = await post(\n baseUrl,\n `${subscriptionsPath(clientKey)}/unsubscribe`,\n { installationId },\n { 'X-Arsel-Device-Auth': deviceSecret },\n true,\n );\n return response.result === RESULT.success;\n}\n\n/**\n * Re-subscribe silently when the org has rotated its VAPID keypair.\n *\n * Rotation invalidates every live subscription: the browser's endpoint was\n * issued against the old application server key, and pushes to it are refused.\n * Nothing surfaces that to the user, so without this an entire cohort goes\n * quietly unreachable and the org's reach decays with no error anywhere.\n *\n * Safe to call on every init — it is a no-op unless the version actually moved,\n * and it never prompts: permission is already granted at this point, so\n * `subscribe()` resolves without user interaction.\n */\nexport async function reconcile(): Promise<void> {\n if (mode === 'none') return;\n if (!isSupported() || Notification.permission !== 'granted') return;\n\n const [clientKey, baseUrl] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n ]);\n if (!clientKey || !baseUrl) return;\n\n const config = await getJson<WebPushConfig>(baseUrl, configPath(clientKey));\n if (!config?.vapidPublicKey) return;\n\n let registration: ServiceWorkerRegistration;\n try {\n registration = await activeRegistration();\n } catch {\n // Opportunistic maintenance; a broken worker surfaces on promptForPush().\n return;\n }\n const existing = await registration.pushManager.getSubscription();\n const knownVersion = await get<number>(KEYS.vapidKeyVersion);\n\n if (existing && knownVersion === config.keyVersion) return;\n\n // The old subscription must go first: `subscribe()` with a different\n // applicationServerKey throws InvalidStateError while one is still live.\n if (existing) await existing.unsubscribe().catch(() => false);\n\n const fresh = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: decodeBase64Url(config.vapidPublicKey),\n });\n await sendSubscription(fresh, config.keyVersion);\n}\n\nexport async function isSubscribed(): Promise<boolean> {\n if (!isSupported()) return false;\n try {\n // Never `.ready` — it hangs forever when nothing is registered.\n const registration =\n mode === 'managed'\n ? managedRegistration\n : await navigator.serviceWorker.getRegistration();\n if (!registration) return false;\n return (await registration.pushManager.getSubscription()) !== null;\n } catch {\n return false;\n }\n}\n","import { EVENT_SESSION_END, EVENT_SESSION_START, enqueue } from './events';\nimport { KEYS, get, set } from './store';\n\n/**\n * A session ends after this long away — the industry-standard 30 minutes.\n * Shorter gaps read a tab switch or a phone unlocking as a session boundary\n * and inflate every count.\n */\nexport const SESSION_GAP_MS = 30 * 60_000;\n\n/**\n * Sessions from visibility transitions, with no timers.\n *\n * A session ends after {@link SESSION_GAP_MS} hidden, but that is only\n * *discovered* on the next visible — so the end event is emitted then,\n * backdated to when the page actually went away. A timer would have to survive\n * the tab being frozen or discarded, which is precisely when it matters.\n *\n * The consequence is the standard one: someone who closes the tab and never\n * returns produces no `session_end`. An open-but-unclosed session beats a\n * fabricated end time.\n */\nexport async function onVisible(now = Date.now()): Promise<void> {\n const openedAt = (await get<number>(KEYS.sessionStartedAt)) ?? 0;\n const hiddenAt = (await get<number>(KEYS.backgroundedAt)) ?? 0;\n\n if (openedAt !== 0) {\n if (hiddenAt === 0 || now - hiddenAt < SESSION_GAP_MS) return;\n await enqueue(\n EVENT_SESSION_END,\n { duration_seconds: Math.round((hiddenAt - openedAt) / 1000) },\n hiddenAt,\n );\n }\n\n await set(KEYS.sessionStartedAt, now);\n await set(KEYS.backgroundedAt, 0);\n await enqueue(EVENT_SESSION_START, {}, now);\n}\n\nexport async function onHidden(now = Date.now()): Promise<void> {\n // Recorded, not emitted: whether this is the end of a session or a two-second\n // tab switch is not knowable yet.\n if (((await get<number>(KEYS.sessionStartedAt)) ?? 0) === 0) return;\n await set(KEYS.backgroundedAt, now);\n}\n\nexport function attach(): void {\n if (typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', () => {\n void (document.visibilityState === 'visible' ? onVisible() : onHidden());\n });\n // `visibilitychange` does not fire on a real close, and `unload` is not\n // reliable on mobile. `pagehide` is the one the browser actually guarantees.\n window.addEventListener('pagehide', () => void onHidden());\n}\n","import {\n EVENT_IDENTIFY,\n RESERVED_PREFIX,\n enqueue,\n flush,\n} from './events';\nimport * as push from './push';\nimport * as session from './session';\nimport {\n KEYS,\n anonymousId,\n countEvents,\n get,\n remove,\n rotateAnonymousId,\n set,\n} from './store';\nimport type {\n ArselConfig,\n ArselDiagnostics,\n ArselIdentity,\n EventProperties,\n} from './types';\nimport { SDK_VERSION } from './version';\n\nexport type {\n ArselConfig,\n ArselDiagnostics,\n ArselIdentity,\n EventProperties,\n} from './types';\nexport { SDK_VERSION } from './version';\n\nlet ready: Promise<void> | null = null;\nlet debug = false;\n\nfunction log(message: string): void {\n if (debug) console.info(`[arsel] ${message}`);\n}\n\nconst E164 = /^\\+[1-9]\\d{6,14}$/;\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * Bounded, memory-only holding pen for `track()` calls made before `init()`.\n * Drained into the durable queue the moment init supplies config.\n */\nconst PRE_INIT_BUFFER_MAX = 100;\ninterface BufferedEvent {\n name: string;\n properties: EventProperties;\n timestampMs: number;\n}\nlet preInitBuffer: BufferedEvent[] = [];\nlet preInitOverflowWarned = false;\n\nfunction attachFlushTriggers(): void {\n // Without these, events stranded by an offline spell or a backgrounded tab\n // wait for the next track() to happen to fire.\n if (typeof window !== 'undefined') {\n window.addEventListener('online', () => void flush().catch(() => {}));\n }\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') void flush().catch(() => {});\n });\n }\n}\n\n/**\n * Start the SDK. Idempotent — a second call returns the first one's promise, so\n * a framework that mounts twice does not mint two identities.\n *\n * Deliberately does **not** request notification permission. A prompt on page\n * load is what gets an origin permanently blocked by Chrome's abusive-\n * notification heuristics; call {@link promptForPush} from a user gesture.\n */\nexport function init(config: ArselConfig): Promise<void> {\n if (ready) return ready;\n ready = (async () => {\n if (!config.clientKey) throw new Error('Arsel: clientKey is required');\n const localhostHttp = /^http:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?(\\/|$)/.test(\n config.baseUrl ?? '',\n );\n if (!config.baseUrl?.startsWith('https://') && !localhostHttp) {\n throw new Error('Arsel: baseUrl must be HTTPS (http is allowed for localhost only)');\n }\n debug = config.debug ?? false;\n\n const baseUrl = config.baseUrl.replace(/\\/+$/, '');\n await Promise.all([\n set(KEYS.clientKey, config.clientKey),\n set(KEYS.baseUrl, baseUrl),\n ]);\n await anonymousId();\n\n // Anything tracked before init() moves into the durable queue, in order,\n // with its original timestamps.\n const buffered = preInitBuffer;\n preInitBuffer = [];\n for (const event of buffered) {\n await enqueue(event.name, event.properties, event.timestampMs);\n }\n\n // Registration is opt-in: unconditionally registering would clobber a\n // PWA's own worker on the same scope and 404-spam events-only sites.\n if (config.serviceWorker === 'external') push.useExternal();\n else if (config.serviceWorkerPath) await push.register(config.serviceWorkerPath);\n\n session.attach();\n attachFlushTriggers();\n // A background or prerendered page has not been *seen* — the session opens\n // on the visibilitychange (or prerenderingchange → visible) that follows.\n const doc = typeof document !== 'undefined'\n ? (document as Document & { prerendering?: boolean })\n : undefined;\n if (doc?.visibilityState === 'visible' && doc.prerendering !== true) {\n await session.onVisible();\n }\n\n // Anything stranded by a previous page's close goes out now.\n void flush();\n // Silent: it only acts if the org rotated its keys, and permission is\n // already granted in that case.\n void push.reconcile();\n log(`initialized (sdk ${SDK_VERSION})`);\n })();\n return ready;\n}\n\n/**\n * Record something the user did.\n *\n * Needs no notification permission and no push subscription. An event tracked\n * before {@link identify} attaches to the anonymous identity and is merged\n * forward when the user is identified. Events tracked before {@link init} are\n * buffered (up to 100) and enqueued when init supplies the configuration.\n *\n * Names beginning `arsel.` are reserved for the SDK and ignored.\n */\nexport async function track(\n name: string,\n properties: EventProperties = {},\n): Promise<void> {\n const trimmed = name?.trim();\n if (!trimmed) {\n log('track() called with a blank event name — ignoring');\n return;\n }\n if (trimmed.startsWith(RESERVED_PREFIX)) {\n log(`'${RESERVED_PREFIX}' is reserved for the SDK — ignoring '${trimmed}'`);\n return;\n }\n if (!ready) {\n if (preInitBuffer.length >= PRE_INIT_BUFFER_MAX) {\n if (!preInitOverflowWarned) {\n preInitOverflowWarned = true;\n console.warn(\n `[arsel] more than ${PRE_INIT_BUFFER_MAX} events tracked before init() — dropping the rest. Call init() earlier.`,\n );\n }\n return;\n }\n preInitBuffer.push({ name: trimmed, properties, timestampMs: Date.now() });\n return;\n }\n await ready;\n await enqueue(trimmed, properties);\n}\n\n/**\n * The person using this browser, as an id you already have.\n *\n * Everything tracked beforehand under the anonymous identity is merged onto the\n * contact this resolves to. Prefer `externalId` alone: it binds a contact\n * without putting an email address into page script, and it survives the user\n * changing their address.\n */\nexport async function identify(identity: ArselIdentity): Promise<void> {\n await ready;\n let { email, phoneNumber } = identity ?? {};\n const externalId = identity?.externalId;\n\n // Rejected here rather than stored: one malformed phone number would ride\n // every subsequent event, 400 each one, and silently drop them all.\n if (email && !EMAIL_SHAPE.test(email)) {\n console.error(`[arsel] identify(): '${email}' is not a valid email address — ignored`);\n email = undefined;\n }\n if (phoneNumber && !E164.test(phoneNumber)) {\n console.error(\n `[arsel] identify(): '${phoneNumber}' is not E.164 (e.g. +966501234567) — ignored`,\n );\n phoneNumber = undefined;\n }\n\n if (!externalId && !email && !phoneNumber) {\n log('identify() needs at least one of externalId, email or phoneNumber');\n return;\n }\n if (externalId) await set(KEYS.externalId, externalId);\n if (email) await set(KEYS.email, email);\n if (phoneNumber) await set(KEYS.phoneNumber, phoneNumber);\n\n // Sent immediately rather than waiting for the next track(): the merge is\n // what the caller asked for, and deferring it leaves the two contacts split\n // until something unrelated happens to fire.\n await enqueue(EVENT_IDENTIFY, {});\n}\n\n/**\n * Logout. Rotates the anonymous identity so the next person on this browser\n * does not inherit the previous one's history, and forgets the identifiers.\n *\n * Deliberately does **not** unsubscribe from push: the backend's opt-out is\n * durable and non-resurrectable, so a logout that called it would permanently\n * kill push on a shared machine for everyone who used it afterwards. That\n * opt-out exists as {@link optOut}, for the user who asks to stop receiving\n * notifications.\n */\nexport async function reset(): Promise<void> {\n await ready;\n await Promise.all([\n remove(KEYS.externalId),\n remove(KEYS.email),\n remove(KEYS.phoneNumber),\n remove(KEYS.sessionStartedAt),\n ]);\n await rotateAnonymousId();\n log('identity reset');\n}\n\n/**\n * Durable push opt-out for this browser — \"stop sending me notifications\", not\n * logout. The revocation is server-side and non-resurrectable: a later\n * registration of the same installation does not undo it. Re-opt-in is an\n * explicit, separate act on the backend.\n *\n * Returns false when this browser never completed a push registration (there\n * is nothing to revoke) or the request failed.\n */\nexport async function optOut(): Promise<boolean> {\n await ready;\n const revoked = await push.optOut();\n log(revoked ? 'push opt-out recorded' : 'push opt-out not sent — no registration to revoke');\n return revoked;\n}\n\n/**\n * Ask for notification permission and subscribe. **Call this from a click**, not\n * on page load.\n *\n * Returns false when the user declines, the browser has no support, or the org\n * does not have the web channel switched on. Declining is a normal outcome, not an\n * error: the contact and its events exist either way.\n */\nexport async function promptForPush(): Promise<boolean> {\n await ready;\n if (!push.isSupported()) return false;\n // Verified BEFORE prompting: a broken worker setup would otherwise burn the\n // origin's one real permission prompt on a subscription that can never settle.\n const problem = await push.preflight();\n if (problem) {\n console.error(`[arsel] promptForPush(): ${problem}`);\n return false;\n }\n const permission = await Notification.requestPermission();\n if (permission !== 'granted') {\n log(`notification permission: ${permission}`);\n return false;\n }\n return push.subscribe();\n}\n\n/** The identity events carry before login. Rotated by {@link reset}. */\nexport async function getAnonymousId(): Promise<string> {\n await ready;\n return anonymousId();\n}\n\n/** A snapshot safe to paste into a support ticket — no secrets in it. */\nexport async function diagnostics(): Promise<ArselDiagnostics> {\n const [\n anon,\n externalId,\n email,\n phoneNumber,\n installationId,\n deviceSecret,\n keyVersion,\n pendingEvents,\n subscribed,\n lastResponseCode,\n lastResponsePath,\n lastResponseAtMs,\n ] = await Promise.all([\n get<string>(KEYS.anonymousId),\n get<string>(KEYS.externalId),\n get<string>(KEYS.email),\n get<string>(KEYS.phoneNumber),\n get<string>(KEYS.installationId),\n get<string>(KEYS.deviceSecret),\n get<number>(KEYS.vapidKeyVersion),\n countEvents(),\n push.isSubscribed(),\n get<number>(KEYS.lastResponseCode),\n get<string>(KEYS.lastResponsePath),\n get<number>(KEYS.lastResponseAtMs),\n ]);\n\n return {\n sdkVersion: SDK_VERSION,\n initialized: ready !== null,\n anonymousId: anon,\n hasAssertedIdentity: Boolean(externalId || email || phoneNumber),\n installationId,\n hasDeviceSecret: Boolean(deviceSecret),\n pendingEvents,\n permission: push.isSupported() ? Notification.permission : 'unsupported',\n isSubscribed: subscribed,\n vapidKeyVersion: keyVersion,\n lastResponseCode,\n lastResponsePath,\n lastResponseAtMs,\n };\n}\n\n/** Deliver anything queued now, rather than on the next natural drain. */\nexport async function flushNow(): Promise<void> {\n await ready;\n await flush();\n}\n\nexport const Arsel = {\n init,\n track,\n identify,\n reset,\n optOut,\n promptForPush,\n getAnonymousId,\n diagnostics,\n flushNow,\n SDK_VERSION,\n};\n\nexport default Arsel;\n"],"names":["DB_NAME","EVENT_STORE","dbPromise","openDb","resolve","reject","request","db","tx","store","mode","run","get","key","s","v","set","value","remove","addEvent","body","idempotencyKey","allEvents","removeEvent","id","countEvents","KEYS","anonymousId","existing","minted","rotateAnonymousId","SDK_VERSION","SDK_HEADER","RESULT","CODE_NO_RESPONSE","classify","code","authenticated","record","path","post","baseUrl","headers","response","result","parsed","getJson","EVENTS_PATH","RESERVED_PREFIX","EVENT_SESSION_START","EVENT_SESSION_END","EVENT_IDENTIFY","MAX_EVENT_NAME","MAX_ANONYMOUS_ID","MAX_EXTERNAL_ID","MAX_BATCH","MAX_DATA_DEPTH","MAX_DATA_BYTES","sanitizeValue","depth","type","item","out","clean","sanitize","properties","bytes","dropped","raw","cost","buildBody","name","timestampMs","anon","externalId","email","phoneNumber","enqueue","flush","inFlight","drain","batchKey","batch","first","reportPermanentDrop","items","clientKey","pending","event","decodeBase64Url","padded","binary","i","isSupported","configPath","subscriptionsPath","ACTIVATION_TIMEOUT_MS","managedRegistration","registrationError","useExternal","register","error","waitForActive","registration","timeoutMs","timer","watch","worker","withTimeout","promise","message","_","activeRegistration","preflight","subscribe","config","subscription","sendSubscription","keyVersion","_a","installationId","json","optOut","deviceSecret","reconcile","knownVersion","fresh","isSubscribed","SESSION_GAP_MS","onVisible","now","openedAt","hiddenAt","onHidden","attach","ready","debug","log","E164","EMAIL_SHAPE","PRE_INIT_BUFFER_MAX","preInitBuffer","preInitOverflowWarned","attachFlushTriggers","init","localhostHttp","buffered","push.useExternal","push.register","session.attach","doc","session.onVisible","push.reconcile","track","trimmed","identify","identity","reset","revoked","push.optOut","promptForPush","push.isSupported","problem","push.preflight","permission","push.subscribe","getAnonymousId","diagnostics","pendingEvents","subscribed","lastResponseCode","lastResponsePath","lastResponseAtMs","push.isSubscribed","flushNow","Arsel"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(u,v){typeof exports=="object"&&typeof module<"u"?v(exports):typeof define=="function"&&define.amd?define(["exports"],v):(u=typeof globalThis<"u"?globalThis:u||self,v(u.Arsel={}))})(this,(function(u){"use strict";const v="arsel",m="events";let N=null;function oe(){return N||(N=new Promise((e,t)=>{const n=indexedDB.open(v,1);n.onupgradeneeded=()=>{const i=n.result;i.objectStoreNames.contains("kv")||i.createObjectStore("kv"),i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id",autoIncrement:!0})},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)}),N)}function y(e,t,n){return oe().then(i=>new Promise((s,a)=>{const o=n(i.transaction(e,t).objectStore(e));o.onsuccess=()=>s(o.result),o.onerror=()=>a(o.error)}))}function c(e){return y("kv","readonly",t=>t.get(e)).then(t=>t??null)}function d(e,t){return y("kv","readwrite",n=>n.put(t,e))}function k(e){return y("kv","readwrite",t=>t.delete(e))}function ce(e,t){return y(m,"readwrite",n=>n.add({body:e,idempotencyKey:t,createdAtMs:Date.now()}))}function le(){return y(m,"readonly",e=>e.getAll())}function ue(e){return y(m,"readwrite",t=>t.delete(e))}function de(){return y(m,"readonly",e=>e.count())}const r={clientKey:"client_key",baseUrl:"base_url",anonymousId:"anonymous_id",externalId:"external_id",email:"email",phoneNumber:"phone_number",installationId:"installation_id",deviceSecret:"device_secret",vapidKeyVersion:"vapid_key_version",endpoint:"endpoint",sessionStartedAt:"session_started_at",backgroundedAt:"backgrounded_at",lastResponseCode:"last_response_code",lastResponsePath:"last_response_path",lastResponseAtMs:"last_response_at"};async function T(){const e=await c(r.anonymousId);if(e)return e;const t=crypto.randomUUID();return await d(r.anonymousId,t),t}async function fe(){const e=crypto.randomUUID();return await d(r.anonymousId,e),e}const g="1.0.0",M={"X-Arsel-SDK":`web/${g}`},p={success:"success",retryable:"retryable",permanent:"permanent",reauth:"reauth"},R=-1;function pe(e,t){return e>=200&&e<=299?p.success:e===408||e===429||e>=500&&e<=599?p.retryable:t&&(e===401||e===403||e===404)?p.reauth:e===404?p.retryable:p.permanent}async function P(e,t){try{await Promise.all([d(r.lastResponseCode,t),d(r.lastResponsePath,e),d(r.lastResponseAtMs,Date.now())])}catch{}}async function $(e,t,n,i={},s=!1){let a;try{a=await fetch(e+t,{method:"POST",headers:{"Content-Type":"application/json",...M,...i},body:JSON.stringify(n),credentials:"omit",keepalive:!0})}catch{return await P(t,R),{result:p.retryable,code:R,body:null}}await P(t,a.status);const o=pe(a.status,s);let l=null;try{l=await a.json()}catch{}return{result:o,code:a.status,body:l}}async function j(e,t){try{const n=await fetch(e+t,{credentials:"omit",headers:M});return await P(t,n.status),n.ok?await n.json():null}catch{return await P(t,R),null}}const ye="/v1/events/send",S="arsel.",we=`${S}session_start`,me=`${S}session_end`,be=`${S}identify`,he=80,ve=128,ge=255,x=50,Se=8,W=64*1024;function V(e,t){if(e===null)return null;const n=typeof e;if(n==="string"||n==="boolean")return e;if(n==="number")return Number.isFinite(e)?e:String(e);if(n==="bigint")return String(e);if(e instanceof Date)return Number.isNaN(e.getTime())?void 0:e.toISOString();if(n!=="object"||t>=Se)return;if(Array.isArray(e))return e.map(s=>V(s,t+1)??null);const i={};for(const[s,a]of Object.entries(e)){if(!s)continue;const o=V(a,t+1);o!==void 0&&(i[s]=o)}return i}function Ee(e){const t={};let n=2,i=!1;for(const[s,a]of Object.entries(e??{})){if(!s)continue;const o=V(a,0);if(o===void 0)continue;const l=s.length+JSON.stringify(o).length+6;if(n+l>W){i=!0;continue}n+=l,t[s]=o}return i&&console.warn(`[arsel] event data exceeded ${W} serialized bytes — oversized properties were dropped`),t}async function _e(e,t,n){const[i,s,a,o]=await Promise.all([T(),c(r.externalId),c(r.email),c(r.phoneNumber)]),l={event:e.slice(0,he),anonymous_id:i.slice(0,ve),data:Ee(t),timestamp:new Date(n).toISOString()};return s&&(l.external_id=s.slice(0,ge)),a&&(l.email=a),o&&(l.phone_number=o),JSON.stringify(l)}async function E(e,t,n=Date.now()){await ce(await _e(e,t,n),crypto.randomUUID()),A().catch(()=>{})}let _=null;function A(){return _||(_=Ne().finally(()=>{_=null}),_)}function Ae(e){const t=e[0].idempotencyKey;return e.length===1?t:`${t}:${e[e.length-1].idempotencyKey}:${e.length}`}function Ie(e,t,n){e.some(s=>s.external_id||s.email||s.phone_number)&&console.error(`[arsel] ${e.length} event(s) carrying identifiers were rejected permanently (HTTP ${t}) and dropped. Response: ${JSON.stringify(n)}`)}async function Ne(){const[e,t]=await Promise.all([c(r.clientKey),c(r.baseUrl)]);if(!(!e||!t))for(;;){const n=await le();if(n.length===0)return;for(let i=0;i<n.length;i+=x){const s=n.slice(i,i+x),a=s.map(l=>JSON.parse(l.body)),o=await $(t,ye,s.length===1?a[0]:{events:a},{Authorization:`Bearer ${e}`,"Idempotency-Key":Ae(s)});if(o.result===p.retryable)return;o.result!==p.success&&Ie(a,o.code,o.body);for(const l of s)l.id!==void 0&&await ue(l.id)}}}function B(e){const t=e.replace(/-/g,"+").replace(/_/g,"/"),n=atob(t+"=".repeat((4-t.length%4)%4)),i=new Uint8Array(n.length);for(let s=0;s<n.length;s+=1)i[s]=n.charCodeAt(s);return i.buffer}function b(){return typeof navigator<"u"&&"serviceWorker"in navigator&&typeof PushManager<"u"&&typeof Notification<"u"}function F(e){return`/api/v1/orgs/${e}/push/web/config`}function C(e){return`/api/v1/orgs/${e}/push/subscriptions`}const X=1e4;let h="none",I=null,O=null;function ke(){h="external"}async function Te(e){if(b()){h="managed";try{I=await navigator.serviceWorker.register(e),O=null}catch(t){I=null,O=`service worker registration failed for '${e}': ${String(t)}. Check that the file is deployed and served with a JavaScript content type.`}}}function Pe(e,t){return e.active?Promise.resolve(e):new Promise((n,i)=>{const s=setTimeout(()=>{i(new Error(`service worker did not activate within ${t}ms — check the worker script for load errors in DevTools → Application → Service Workers`))},t),a=l=>{l.addEventListener("statechange",()=>{l.state==="activated"?(clearTimeout(s),n(e)):l.state==="redundant"&&(clearTimeout(s),i(new Error("service worker became redundant before activating — its script likely failed to parse")))})},o=e.installing??e.waiting;o?a(o):e.addEventListener("updatefound",()=>{e.installing&&a(e.installing)})})}function Oe(e,t,n){return Promise.race([e,new Promise((i,s)=>{setTimeout(()=>s(new Error(n)),t)})])}async function U(e=X){if(h==="none")throw new Error("push is not configured — pass serviceWorkerPath to init(), or serviceWorker: 'external' if your own service worker imports arsel-sw.js");if(h==="external")return Oe(navigator.serviceWorker.ready,e,`no service worker became active within ${e}ms — with serviceWorker: 'external', your own worker must be registered and must importScripts arsel-sw.js`);if(O)throw new Error(O);if(!I)throw new Error("service worker registration has not completed");return Pe(I,e)}async function Ke(e=X){try{return await U(e),null}catch(t){return t instanceof Error?t.message:String(t)}}async function De(){const[e,t]=await Promise.all([c(r.clientKey),c(r.baseUrl)]);if(!e||!t||!b())return!1;const n=await j(t,F(e));if(!(n!=null&&n.vapidPublicKey))return!1;let i;try{i=await U()}catch(a){return console.error(`[arsel] ${a instanceof Error?a.message:a}`),!1}const s=await i.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:B(n.vapidPublicKey)});return H(s,n.keyVersion)}async function H(e,t){var D;const[n,i,s]=await Promise.all([c(r.clientKey),c(r.baseUrl),T()]);if(!n||!i)return!1;let a=await c(r.installationId);a||(a=crypto.randomUUID(),await d(r.installationId,a));const o=e.toJSON(),l=await $(i,C(n),{installationId:a,platform:"web",endpoint:o.endpoint,keys:o.keys,anonymousId:s,enablementStatus:Notification.permission==="granted"?"AUTHORIZED":"DENIED",deviceTimezone:Intl.DateTimeFormat().resolvedOptions().timeZone,deviceLocale:navigator.language});return l.result!==p.success?!1:((D=l.body)!=null&&D.deviceSecret&&await d(r.deviceSecret,l.body.deviceSecret),await d(r.vapidKeyVersion,t),await d(r.endpoint,o.endpoint??null),!0)}async function Re(){const[e,t,n,i]=await Promise.all([c(r.clientKey),c(r.baseUrl),c(r.installationId),c(r.deviceSecret)]);return!e||!t||!n||!i?!1:(await $(t,`${C(e)}/unsubscribe`,{installationId:n},{"X-Arsel-Device-Auth":i},!0)).result===p.success}async function $e(){if(h==="none"||!b()||Notification.permission!=="granted")return;const[e,t]=await Promise.all([c(r.clientKey),c(r.baseUrl)]);if(!e||!t)return;const n=await j(t,F(e));if(!(n!=null&&n.vapidPublicKey))return;let i;try{i=await U()}catch{return}const s=await i.pushManager.getSubscription(),a=await c(r.vapidKeyVersion);if(s&&a===n.keyVersion)return;s&&await s.unsubscribe().catch(()=>!1);const o=await i.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:B(n.vapidPublicKey)});await H(o,n.keyVersion)}async function Ve(){if(!b())return!1;try{const e=h==="managed"?I:await navigator.serviceWorker.getRegistration();return e?await e.pushManager.getSubscription()!==null:!1}catch{return!1}}const Ue=30*6e4;async function L(e=Date.now()){const t=await c(r.sessionStartedAt)??0,n=await c(r.backgroundedAt)??0;if(t!==0){if(n===0||e-n<Ue)return;await E(me,{duration_seconds:Math.round((n-t)/1e3)},n)}await d(r.sessionStartedAt,e),await d(r.backgroundedAt,0),await E(we,{},e)}async function z(e=Date.now()){(await c(r.sessionStartedAt)??0)!==0&&await d(r.backgroundedAt,e)}function Me(){typeof document>"u"||(document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"?L():z()}),window.addEventListener("pagehide",()=>void z()))}let f=null,J=!1;function w(e){J&&console.info(`[arsel] ${e}`)}const je=/^\+[1-9]\d{6,14}$/,xe=/^[^\s@]+@[^\s@]+\.[^\s@]+$/,q=100;let K=[],Y=!1;function We(){typeof window<"u"&&window.addEventListener("online",()=>void A().catch(()=>{})),typeof document<"u"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&A().catch(()=>{})})}function Z(e){return f||(f=(async()=>{var a;if(!e.clientKey)throw new Error("Arsel: clientKey is required");const t=/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(e.baseUrl??"");if(!((a=e.baseUrl)!=null&&a.startsWith("https://"))&&!t)throw new Error("Arsel: baseUrl must be HTTPS (http is allowed for localhost only)");J=e.debug??!1;const n=e.baseUrl.replace(/\/+$/,"");await Promise.all([d(r.clientKey,e.clientKey),d(r.baseUrl,n)]),await T();const i=K;K=[];for(const o of i)await E(o.name,o.properties,o.timestampMs);e.serviceWorker==="external"?ke():e.serviceWorkerPath&&await Te(e.serviceWorkerPath),Me(),We();const s=typeof document<"u"?document:void 0;(s==null?void 0:s.visibilityState)==="visible"&&s.prerendering!==!0&&await L(),A(),$e(),w(`initialized (sdk ${g})`)})(),f)}async function G(e,t={}){const n=e==null?void 0:e.trim();if(!n){w("track() called with a blank event name — ignoring");return}if(n.startsWith(S)){w(`'${S}' is reserved for the SDK — ignoring '${n}'`);return}if(!f){if(K.length>=q){Y||(Y=!0,console.warn(`[arsel] more than ${q} events tracked before init() — dropping the rest. Call init() earlier.`));return}K.push({name:n,properties:t,timestampMs:Date.now()});return}await f,await E(n,t)}async function Q(e){await f;let{email:t,phoneNumber:n}=e??{};const i=e==null?void 0:e.externalId;if(t&&!xe.test(t)&&(console.error(`[arsel] identify(): '${t}' is not a valid email address — ignored`),t=void 0),n&&!je.test(n)&&(console.error(`[arsel] identify(): '${n}' is not E.164 (e.g. +966501234567) — ignored`),n=void 0),!i&&!t&&!n){w("identify() needs at least one of externalId, email or phoneNumber");return}i&&await d(r.externalId,i),t&&await d(r.email,t),n&&await d(r.phoneNumber,n),await E(be,{})}async function ee(){await f,await Promise.all([k(r.externalId),k(r.email),k(r.phoneNumber),k(r.sessionStartedAt)]),await fe(),w("identity reset")}async function te(){await f;const e=await Re();return w(e?"push opt-out recorded":"push opt-out not sent — no registration to revoke"),e}async function ne(){if(await f,!b())return!1;const e=await Ke();if(e)return console.error(`[arsel] promptForPush(): ${e}`),!1;const t=await Notification.requestPermission();return t!=="granted"?(w(`notification permission: ${t}`),!1):De()}async function re(){return await f,T()}async function ie(){const[e,t,n,i,s,a,o,l,D,Be,Fe,Ce]=await Promise.all([c(r.anonymousId),c(r.externalId),c(r.email),c(r.phoneNumber),c(r.installationId),c(r.deviceSecret),c(r.vapidKeyVersion),de(),Ve(),c(r.lastResponseCode),c(r.lastResponsePath),c(r.lastResponseAtMs)]);return{sdkVersion:g,initialized:f!==null,anonymousId:e,hasAssertedIdentity:!!(t||n||i),installationId:s,hasDeviceSecret:!!a,pendingEvents:l,permission:b()?Notification.permission:"unsupported",isSubscribed:D,vapidKeyVersion:o,lastResponseCode:Be,lastResponsePath:Fe,lastResponseAtMs:Ce}}async function se(){await f,await A()}const ae={init:Z,track:G,identify:Q,reset:ee,optOut:te,promptForPush:ne,getAnonymousId:re,diagnostics:ie,flushNow:se,SDK_VERSION:g};u.Arsel=ae,u.SDK_VERSION=g,u.default=ae,u.diagnostics=ie,u.flushNow=se,u.getAnonymousId=re,u.identify=Q,u.init=Z,u.optOut=te,u.promptForPush=ne,u.reset=ee,u.track=G,Object.defineProperties(u,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
2
|
+
//# sourceMappingURL=arsel.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arsel.umd.cjs","sources":["../src/store.ts","../src/version.ts","../src/transport.ts","../src/events.ts","../src/push.ts","../src/session.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared durable state, in IndexedDB.\n *\n * IndexedDB rather than `localStorage` for one reason that decides it: the\n * service worker has to read the device secret and the client key to report a\n * `delivered` engagement when **no tab is open at all**, and `localStorage` is\n * unreachable from a worker. Splitting state across both stores would put the\n * two halves of one fact in two places.\n */\n\nconst DB_NAME = 'arsel';\nconst DB_VERSION = 1;\nconst KV_STORE = 'kv';\nconst EVENT_STORE = 'events';\n\nexport interface PendingEvent {\n /** Monotonic within a tab; the auto-increment key doubles as delivery order. */\n id?: number;\n body: string;\n /**\n * Minted once at enqueue and persisted, so a retry of the same event carries\n * the same key and the backend's 24h idempotency window can dedupe it.\n */\n idempotencyKey: string;\n createdAtMs: number;\n}\n\nlet dbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openDb(): Promise<IDBDatabase> {\n if (dbPromise) return dbPromise;\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(KV_STORE)) db.createObjectStore(KV_STORE);\n if (!db.objectStoreNames.contains(EVENT_STORE)) {\n db.createObjectStore(EVENT_STORE, { keyPath: 'id', autoIncrement: true });\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n return dbPromise;\n}\n\nfunction tx<T>(\n store: string,\n mode: IDBTransactionMode,\n run: (s: IDBObjectStore) => IDBRequest<T>,\n): Promise<T> {\n return openDb().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const request = run(db.transaction(store, mode).objectStore(store));\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n }),\n );\n}\n\nexport function get<T>(key: string): Promise<T | null> {\n return tx<T | undefined>(KV_STORE, 'readonly', (s) => s.get(key)).then(\n (v) => v ?? null,\n );\n}\n\nexport function set(key: string, value: unknown): Promise<unknown> {\n return tx(KV_STORE, 'readwrite', (s) => s.put(value, key));\n}\n\nexport function remove(key: string): Promise<unknown> {\n return tx(KV_STORE, 'readwrite', (s) => s.delete(key));\n}\n\nexport function addEvent(body: string, idempotencyKey: string): Promise<unknown> {\n return tx(EVENT_STORE, 'readwrite', (s) =>\n s.add({ body, idempotencyKey, createdAtMs: Date.now() }),\n );\n}\n\nexport function allEvents(): Promise<PendingEvent[]> {\n return tx<PendingEvent[]>(EVENT_STORE, 'readonly', (s) => s.getAll());\n}\n\nexport function removeEvent(id: number): Promise<unknown> {\n return tx(EVENT_STORE, 'readwrite', (s) => s.delete(id));\n}\n\nexport function countEvents(): Promise<number> {\n return tx<number>(EVENT_STORE, 'readonly', (s) => s.count());\n}\n\nexport const KEYS = {\n clientKey: 'client_key',\n baseUrl: 'base_url',\n anonymousId: 'anonymous_id',\n externalId: 'external_id',\n email: 'email',\n phoneNumber: 'phone_number',\n installationId: 'installation_id',\n deviceSecret: 'device_secret',\n vapidKeyVersion: 'vapid_key_version',\n endpoint: 'endpoint',\n sessionStartedAt: 'session_started_at',\n backgroundedAt: 'backgrounded_at',\n lastResponseCode: 'last_response_code',\n lastResponsePath: 'last_response_path',\n lastResponseAtMs: 'last_response_at',\n} as const;\n\n/**\n * Person-shaped identity, minted on first use. Distinct from the installation\n * id, which names the browser profile: this one names whoever is using it and\n * is rotated by `reset()`, so a shared computer does not hand the next person\n * the previous one's history.\n */\nexport async function anonymousId(): Promise<string> {\n const existing = await get<string>(KEYS.anonymousId);\n if (existing) return existing;\n const minted = crypto.randomUUID();\n await set(KEYS.anonymousId, minted);\n return minted;\n}\n\nexport async function rotateAnonymousId(): Promise<string> {\n const minted = crypto.randomUUID();\n await set(KEYS.anonymousId, minted);\n return minted;\n}\n","/**\n * In its own module so transport can stamp `X-Arsel-SDK` without importing the\n * public surface (index → events → transport would be a cycle).\n */\nexport const SDK_VERSION = '1.0.0';\n","import { KEYS, set } from './store';\nimport { SDK_VERSION } from './version';\n\n/** Identifies the SDK build on every request; the backend records it. */\nconst SDK_HEADER = { 'X-Arsel-SDK': `web/${SDK_VERSION}` };\n\nexport const RESULT = {\n success: 'success',\n retryable: 'retryable',\n permanent: 'permanent',\n reauth: 'reauth',\n} as const;\n\nexport type Result = (typeof RESULT)[keyof typeof RESULT];\n\nexport interface Response<T = unknown> {\n result: Result;\n code: number;\n body: T | null;\n}\n\n/** No status line at all — DNS failure, offline, TLS error. */\nexport const CODE_NO_RESPONSE = -1;\n\n/**\n * Status → retry policy. Pure, and the same table the Android SDK uses: this is\n * the decision that separates a browser that eventually delivers from one that\n * gives up forever.\n */\nexport function classify(code: number, authenticated: boolean): Result {\n if (code >= 200 && code <= 299) return RESULT.success;\n if (code === 408 || code === 429 || (code >= 500 && code <= 599)) {\n return RESULT.retryable;\n }\n // On an authed route these all mean one thing behind the backend's\n // deliberately opaque 404: this secret is no longer accepted.\n if (authenticated && (code === 401 || code === 403 || code === 404)) {\n return RESULT.reauth;\n }\n // An unknown push key and an org whose channel is not switched on yet both\n // answer a bare 404. The second is ordinary onboarding, so giving up\n // permanently here would strand a browser that would have worked tomorrow.\n if (code === 404) return RESULT.retryable;\n return RESULT.permanent;\n}\n\nasync function record(path: string, code: number): Promise<void> {\n // Diagnostics only; nothing branches on it. Failures here must never mask the\n // request's own outcome.\n try {\n await Promise.all([\n set(KEYS.lastResponseCode, code),\n set(KEYS.lastResponsePath, path),\n set(KEYS.lastResponseAtMs, Date.now()),\n ]);\n } catch {\n /* diagnostics are best-effort */\n }\n}\n\nexport async function post<T = unknown>(\n baseUrl: string,\n path: string,\n body: unknown,\n headers: Record<string, string> = {},\n authenticated = false,\n): Promise<Response<T>> {\n let response: globalThis.Response;\n try {\n response = await fetch(baseUrl + path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...SDK_HEADER, ...headers },\n body: JSON.stringify(body),\n // The SDK never relies on cookies, and sending them would attach a\n // first-party session to a cross-origin call for no reason.\n credentials: 'omit',\n keepalive: true,\n });\n } catch {\n await record(path, CODE_NO_RESPONSE);\n return { result: RESULT.retryable, code: CODE_NO_RESPONSE, body: null };\n }\n\n await record(path, response.status);\n const result = classify(response.status, authenticated);\n let parsed: T | null = null;\n try {\n parsed = (await response.json()) as T;\n } catch {\n /* an empty or non-JSON body is fine — the status is what matters */\n }\n return { result, code: response.status, body: parsed };\n}\n\nexport async function getJson<T>(\n baseUrl: string,\n path: string,\n): Promise<T | null> {\n try {\n const response = await fetch(baseUrl + path, {\n credentials: 'omit',\n headers: SDK_HEADER,\n });\n await record(path, response.status);\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n await record(path, CODE_NO_RESPONSE);\n return null;\n }\n}\n","import {\n KEYS,\n addEvent,\n allEvents,\n anonymousId,\n get,\n removeEvent,\n} from './store';\nimport type { PendingEvent } from './store';\nimport { RESULT, post } from './transport';\nimport type { EventProperties } from './types';\n\nexport const EVENTS_PATH = '/v1/events/send';\n\n/** Reserved prefix, so an SDK event can never collide with a customer's. */\nexport const RESERVED_PREFIX = 'arsel.';\nexport const EVENT_SESSION_START = `${RESERVED_PREFIX}session_start`;\nexport const EVENT_SESSION_END = `${RESERVED_PREFIX}session_end`;\nexport const EVENT_IDENTIFY = `${RESERVED_PREFIX}identify`;\n\n// Mirror `IngestEventDto`'s @Length caps, applied here so the 400 never happens.\nconst MAX_EVENT_NAME = 80;\nconst MAX_ANONYMOUS_ID = 128;\nconst MAX_EXTERNAL_ID = 255;\n\n/** The batch ceiling `POST /v1/events/send` accepts in an `{ events: [...] }` body. */\nexport const MAX_BATCH = 50;\n\nconst MAX_DATA_DEPTH = 8;\nconst MAX_DATA_BYTES = 64 * 1024;\n\n/**\n * The backend accepts arbitrary nested JSON, so JSON-safe values pass through\n * unchanged. Dates become ISO strings, non-finite numbers become strings (they\n * are not valid JSON), and functions/symbols/undefined are dropped. The depth\n * cap also terminates cycles.\n */\nfunction sanitizeValue(value: unknown, depth: number): unknown {\n if (value === null) return null;\n const type = typeof value;\n if (type === 'string' || type === 'boolean') return value;\n if (type === 'number') return Number.isFinite(value) ? value : String(value);\n if (type === 'bigint') return String(value);\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? undefined : value.toISOString();\n }\n if (type !== 'object' || depth >= MAX_DATA_DEPTH) return undefined;\n if (Array.isArray(value)) {\n // undefined has no JSON form inside an array; null keeps the indices stable.\n return value.map((item) => sanitizeValue(item, depth + 1) ?? null);\n }\n const out: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n if (!key) continue;\n const clean = sanitizeValue(item, depth + 1);\n if (clean !== undefined) out[key] = clean;\n }\n return out;\n}\n\n/** `data` must be wire-safe JSON, bounded so one huge property cannot 413 the drain. */\nfunction sanitize(properties: EventProperties): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n let bytes = 2;\n let dropped = false;\n for (const [key, raw] of Object.entries(properties ?? {})) {\n if (!key) continue;\n const value = sanitizeValue(raw, 0);\n if (value === undefined) continue;\n const cost = key.length + JSON.stringify(value).length + 6;\n if (bytes + cost > MAX_DATA_BYTES) {\n dropped = true;\n continue;\n }\n bytes += cost;\n out[key] = value;\n }\n if (dropped) {\n console.warn(\n `[arsel] event data exceeded ${MAX_DATA_BYTES} serialized bytes — oversized properties were dropped`,\n );\n }\n return out;\n}\n\nasync function buildBody(\n name: string,\n properties: EventProperties,\n timestampMs: number,\n): Promise<string> {\n const [anon, externalId, email, phoneNumber] = await Promise.all([\n anonymousId(),\n get<string>(KEYS.externalId),\n get<string>(KEYS.email),\n get<string>(KEYS.phoneNumber),\n ]);\n\n const body: Record<string, unknown> = {\n event: name.slice(0, MAX_EVENT_NAME),\n // Always sent, even once identified: it is what the backend merges FROM.\n anonymous_id: anon.slice(0, MAX_ANONYMOUS_ID),\n data: sanitize(properties),\n timestamp: new Date(timestampMs).toISOString(),\n };\n if (externalId) body.external_id = externalId.slice(0, MAX_EXTERNAL_ID);\n if (email) body.email = email;\n if (phoneNumber) body.phone_number = phoneNumber;\n return JSON.stringify(body);\n}\n\n/**\n * Persist first, then send. A tab closed mid-flight would otherwise lose the\n * event outright, and page-lifetime-only buffering is the single most common\n * way a web analytics SDK silently undercounts.\n */\nexport async function enqueue(\n name: string,\n properties: EventProperties,\n timestampMs = Date.now(),\n): Promise<void> {\n await addEvent(await buildBody(name, properties, timestampMs), crypto.randomUUID());\n // Fire-and-forget: track() must not make the caller wait on the network.\n void flush().catch(() => {});\n}\n\nlet inFlight: Promise<void> | null = null;\n\n/**\n * Deliver everything queued, oldest first.\n *\n * Serialised, because two concurrent drains would both read the same rows and\n * send each event twice. A caller arriving mid-drain gets the in-flight promise\n * rather than a silent no-op, so `await flush()` actually means \"delivered\".\n */\nexport function flush(): Promise<void> {\n if (inFlight) return inFlight;\n inFlight = drain().finally(() => {\n inFlight = null;\n });\n return inFlight;\n}\n\n/**\n * Deterministic per batch: an identical batch resent after a timeout carries\n * the same key and dedupes inside the backend's 24h window.\n */\nfunction batchKey(batch: PendingEvent[]): string {\n const first = batch[0]!.idempotencyKey;\n if (batch.length === 1) return first;\n return `${first}:${batch[batch.length - 1]!.idempotencyKey}:${batch.length}`;\n}\n\n/**\n * A permanent rejection is dropped, but a drop that takes identifiers with it\n * must not be silent — one bad identifier would otherwise 400 every subsequent\n * event with no visible cause.\n */\nfunction reportPermanentDrop(\n items: Record<string, unknown>[],\n code: number,\n body: unknown,\n): void {\n const carried = items.some(\n (item) => item.external_id || item.email || item.phone_number,\n );\n if (!carried) return;\n console.error(\n `[arsel] ${items.length} event(s) carrying identifiers were rejected permanently ` +\n `(HTTP ${code}) and dropped. Response: ${JSON.stringify(body)}`,\n );\n}\n\nasync function drain(): Promise<void> {\n const [clientKey, baseUrl] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n ]);\n if (!clientKey || !baseUrl) return;\n\n // Re-reads until the queue is empty: anything enqueued while we were on the\n // network would otherwise sit there until the next unrelated trigger.\n for (;;) {\n const pending = await allEvents();\n if (pending.length === 0) return;\n\n for (let i = 0; i < pending.length; i += MAX_BATCH) {\n const batch = pending.slice(i, i + MAX_BATCH);\n const items = batch.map(\n (event) => JSON.parse(event.body) as Record<string, unknown>,\n );\n const response = await post(\n baseUrl,\n EVENTS_PATH,\n batch.length === 1 ? items[0] : { events: items },\n {\n Authorization: `Bearer ${clientKey}`,\n 'Idempotency-Key': batchKey(batch),\n },\n );\n // Stop at the first retryable failure rather than skipping past it — a\n // later batch overtaking an earlier one would reorder a user's history.\n // The whole batch retries under the same key, so a request that landed\n // but timed out on the way back dedupes instead of double-counting.\n if (response.result === RESULT.retryable) return;\n // Success or permanent: either way it is settled. A 4xx will never\n // succeed on a retry, and holding it would wedge everything behind it.\n if (response.result !== RESULT.success) {\n reportPermanentDrop(items, response.code, response.body);\n }\n for (const event of batch) {\n if (event.id !== undefined) await removeEvent(event.id);\n }\n }\n }\n}\n","import { KEYS, anonymousId, get, set } from './store';\nimport { RESULT, getJson, post } from './transport';\nimport type { WebPushConfig } from './types';\n\n/**\n * VAPID keys arrive base64url; `applicationServerKey` wants raw bytes.\n *\n * Returns the ArrayBuffer rather than the view: TS models `BufferSource` as\n * requiring a non-shared buffer, and a `Uint8Array` is only assignable when its\n * backing buffer is statically known not to be a `SharedArrayBuffer`.\n */\nfunction decodeBase64Url(value: string): ArrayBuffer {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/');\n const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);\n return bytes.buffer;\n}\n\nexport function isSupported(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n typeof PushManager !== 'undefined' &&\n typeof Notification !== 'undefined'\n );\n}\n\nfunction configPath(clientKey: string): string {\n return `/api/v1/orgs/${clientKey}/push/web/config`;\n}\n\nfunction subscriptionsPath(clientKey: string): string {\n return `/api/v1/orgs/${clientKey}/push/subscriptions`;\n}\n\nconst ACTIVATION_TIMEOUT_MS = 10_000;\n\n/**\n * How this page gets a service worker, decided once by `init()`:\n * - `none` — events-only integration; every push entry point fails fast.\n * - `managed` — the SDK registered `serviceWorkerPath` and holds the result.\n * - `external` — the customer's own worker `importScripts` arsel-sw.js; the SDK\n * registers nothing and uses whatever controls the page.\n */\nlet mode: 'none' | 'managed' | 'external' = 'none';\nlet managedRegistration: ServiceWorkerRegistration | null = null;\nlet registrationError: string | null = null;\n\nexport function useExternal(): void {\n mode = 'external';\n}\n\nexport async function register(path: string): Promise<void> {\n if (!isSupported()) return;\n mode = 'managed';\n try {\n // No explicit scope: the default is the path's directory, and forcing '/'\n // throws SecurityError for any worker served from a subpath.\n managedRegistration = await navigator.serviceWorker.register(path);\n registrationError = null;\n } catch (error) {\n managedRegistration = null;\n registrationError =\n `service worker registration failed for '${path}': ${String(error)}. ` +\n 'Check that the file is deployed and served with a JavaScript content type.';\n }\n}\n\n/**\n * Resolve once the registration has an active worker, or fail with a reason —\n * never `navigator.serviceWorker.ready`, which hangs forever when registration\n * failed and turns every caller into a silent no-op.\n */\nfunction waitForActive(\n registration: ServiceWorkerRegistration,\n timeoutMs: number,\n): Promise<ServiceWorkerRegistration> {\n if (registration.active) return Promise.resolve(registration);\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new Error(\n `service worker did not activate within ${timeoutMs}ms — ` +\n 'check the worker script for load errors in DevTools → Application → Service Workers',\n ),\n );\n }, timeoutMs);\n const watch = (worker: ServiceWorker): void => {\n worker.addEventListener('statechange', () => {\n if (worker.state === 'activated') {\n clearTimeout(timer);\n resolve(registration);\n } else if (worker.state === 'redundant') {\n clearTimeout(timer);\n reject(\n new Error(\n 'service worker became redundant before activating — its script likely failed to parse',\n ),\n );\n }\n });\n };\n const worker = registration.installing ?? registration.waiting;\n if (worker) watch(worker);\n else {\n registration.addEventListener('updatefound', () => {\n if (registration.installing) watch(registration.installing);\n });\n }\n });\n}\n\nfunction withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n message: string,\n): Promise<T> {\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n setTimeout(() => reject(new Error(message)), timeoutMs);\n }),\n ]);\n}\n\nasync function activeRegistration(\n timeoutMs = ACTIVATION_TIMEOUT_MS,\n): Promise<ServiceWorkerRegistration> {\n if (mode === 'none') {\n throw new Error(\n \"push is not configured — pass serviceWorkerPath to init(), or serviceWorker: 'external' \" +\n 'if your own service worker imports arsel-sw.js',\n );\n }\n if (mode === 'external') {\n return withTimeout(\n navigator.serviceWorker.ready,\n timeoutMs,\n `no service worker became active within ${timeoutMs}ms — with serviceWorker: 'external', ` +\n 'your own worker must be registered and must importScripts arsel-sw.js',\n );\n }\n if (registrationError) throw new Error(registrationError);\n if (!managedRegistration) {\n throw new Error('service worker registration has not completed');\n }\n return waitForActive(managedRegistration, timeoutMs);\n}\n\n/**\n * Everything that can be verified *before* the permission prompt, so a broken\n * worker setup fails with a reason instead of burning the origin's one real\n * prompt on a subscription that can never settle.\n *\n * Returns an error message, or null when push can proceed.\n */\nexport async function preflight(\n timeoutMs = ACTIVATION_TIMEOUT_MS,\n): Promise<string | null> {\n try {\n await activeRegistration(timeoutMs);\n return null;\n } catch (error) {\n return error instanceof Error ? error.message : String(error);\n }\n}\n\n/**\n * Subscribe this browser and register it with Arsel.\n *\n * Only ever called from a user gesture path — `subscribe()` triggers the\n * permission prompt, and a prompt fired on page load is what gets an origin\n * permanently blocked in Chrome's abusive-notification heuristics.\n */\nexport async function subscribe(): Promise<boolean> {\n const [clientKey, baseUrl] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n ]);\n if (!clientKey || !baseUrl || !isSupported()) return false;\n\n const config = await getJson<WebPushConfig>(baseUrl, configPath(clientKey));\n if (!config?.vapidPublicKey) return false;\n\n let registration: ServiceWorkerRegistration;\n try {\n registration = await activeRegistration();\n } catch (error) {\n console.error(`[arsel] ${error instanceof Error ? error.message : error}`);\n return false;\n }\n const subscription = await registration.pushManager.subscribe({\n // Non-negotiable: the Push API only accepts a userVisibleOnly subscription\n // in Chrome, and silent push is what the restriction exists to prevent.\n userVisibleOnly: true,\n applicationServerKey: decodeBase64Url(config.vapidPublicKey),\n });\n\n return sendSubscription(subscription, config.keyVersion);\n}\n\nasync function sendSubscription(\n subscription: PushSubscription,\n keyVersion: number,\n): Promise<boolean> {\n const [clientKey, baseUrl, anon] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n anonymousId(),\n ]);\n if (!clientKey || !baseUrl) return false;\n\n let installationId = await get<string>(KEYS.installationId);\n if (!installationId) {\n installationId = crypto.randomUUID();\n await set(KEYS.installationId, installationId);\n }\n\n // Posted VERBATIM. Reshaping this object is exactly where an SDK drops the\n // `auth` secret, and every send then fails encryption with no obvious cause.\n const json = subscription.toJSON();\n\n const response = await post<{ deviceSecret?: string }>(\n baseUrl,\n subscriptionsPath(clientKey),\n {\n installationId,\n platform: 'web',\n endpoint: json.endpoint,\n keys: json.keys,\n // The same id the events API sends — it is what binds this subscription\n // to the contact the browser's events resolve to.\n anonymousId: anon,\n enablementStatus:\n Notification.permission === 'granted' ? 'AUTHORIZED' : 'DENIED',\n deviceTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n deviceLocale: navigator.language,\n },\n );\n\n if (response.result !== RESULT.success) return false;\n\n // Issued exactly once, on the call that mints it. Losing it strands this\n // browser without a way to authenticate anything ever again.\n if (response.body?.deviceSecret) {\n await set(KEYS.deviceSecret, response.body.deviceSecret);\n }\n await set(KEYS.vapidKeyVersion, keyVersion);\n await set(KEYS.endpoint, json.endpoint ?? null);\n return true;\n}\n\n/**\n * Durable opt-out, mirroring the Android SDK: the backend revocation is\n * non-resurrectable — a later register for this installation will not undo it.\n * Distinct from `reset()`, which never touches push.\n */\nexport async function optOut(): Promise<boolean> {\n const [clientKey, baseUrl, installationId, deviceSecret] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n get<string>(KEYS.installationId),\n get<string>(KEYS.deviceSecret),\n ]);\n if (!clientKey || !baseUrl) return false;\n if (!installationId || !deviceSecret) {\n // Never registered — there is nothing server-side to revoke.\n return false;\n }\n\n const response = await post(\n baseUrl,\n `${subscriptionsPath(clientKey)}/unsubscribe`,\n { installationId },\n { 'X-Arsel-Device-Auth': deviceSecret },\n true,\n );\n return response.result === RESULT.success;\n}\n\n/**\n * Re-subscribe silently when the org has rotated its VAPID keypair.\n *\n * Rotation invalidates every live subscription: the browser's endpoint was\n * issued against the old application server key, and pushes to it are refused.\n * Nothing surfaces that to the user, so without this an entire cohort goes\n * quietly unreachable and the org's reach decays with no error anywhere.\n *\n * Safe to call on every init — it is a no-op unless the version actually moved,\n * and it never prompts: permission is already granted at this point, so\n * `subscribe()` resolves without user interaction.\n */\nexport async function reconcile(): Promise<void> {\n if (mode === 'none') return;\n if (!isSupported() || Notification.permission !== 'granted') return;\n\n const [clientKey, baseUrl] = await Promise.all([\n get<string>(KEYS.clientKey),\n get<string>(KEYS.baseUrl),\n ]);\n if (!clientKey || !baseUrl) return;\n\n const config = await getJson<WebPushConfig>(baseUrl, configPath(clientKey));\n if (!config?.vapidPublicKey) return;\n\n let registration: ServiceWorkerRegistration;\n try {\n registration = await activeRegistration();\n } catch {\n // Opportunistic maintenance; a broken worker surfaces on promptForPush().\n return;\n }\n const existing = await registration.pushManager.getSubscription();\n const knownVersion = await get<number>(KEYS.vapidKeyVersion);\n\n if (existing && knownVersion === config.keyVersion) return;\n\n // The old subscription must go first: `subscribe()` with a different\n // applicationServerKey throws InvalidStateError while one is still live.\n if (existing) await existing.unsubscribe().catch(() => false);\n\n const fresh = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: decodeBase64Url(config.vapidPublicKey),\n });\n await sendSubscription(fresh, config.keyVersion);\n}\n\nexport async function isSubscribed(): Promise<boolean> {\n if (!isSupported()) return false;\n try {\n // Never `.ready` — it hangs forever when nothing is registered.\n const registration =\n mode === 'managed'\n ? managedRegistration\n : await navigator.serviceWorker.getRegistration();\n if (!registration) return false;\n return (await registration.pushManager.getSubscription()) !== null;\n } catch {\n return false;\n }\n}\n","import { EVENT_SESSION_END, EVENT_SESSION_START, enqueue } from './events';\nimport { KEYS, get, set } from './store';\n\n/**\n * A session ends after this long away — the industry-standard 30 minutes.\n * Shorter gaps read a tab switch or a phone unlocking as a session boundary\n * and inflate every count.\n */\nexport const SESSION_GAP_MS = 30 * 60_000;\n\n/**\n * Sessions from visibility transitions, with no timers.\n *\n * A session ends after {@link SESSION_GAP_MS} hidden, but that is only\n * *discovered* on the next visible — so the end event is emitted then,\n * backdated to when the page actually went away. A timer would have to survive\n * the tab being frozen or discarded, which is precisely when it matters.\n *\n * The consequence is the standard one: someone who closes the tab and never\n * returns produces no `session_end`. An open-but-unclosed session beats a\n * fabricated end time.\n */\nexport async function onVisible(now = Date.now()): Promise<void> {\n const openedAt = (await get<number>(KEYS.sessionStartedAt)) ?? 0;\n const hiddenAt = (await get<number>(KEYS.backgroundedAt)) ?? 0;\n\n if (openedAt !== 0) {\n if (hiddenAt === 0 || now - hiddenAt < SESSION_GAP_MS) return;\n await enqueue(\n EVENT_SESSION_END,\n { duration_seconds: Math.round((hiddenAt - openedAt) / 1000) },\n hiddenAt,\n );\n }\n\n await set(KEYS.sessionStartedAt, now);\n await set(KEYS.backgroundedAt, 0);\n await enqueue(EVENT_SESSION_START, {}, now);\n}\n\nexport async function onHidden(now = Date.now()): Promise<void> {\n // Recorded, not emitted: whether this is the end of a session or a two-second\n // tab switch is not knowable yet.\n if (((await get<number>(KEYS.sessionStartedAt)) ?? 0) === 0) return;\n await set(KEYS.backgroundedAt, now);\n}\n\nexport function attach(): void {\n if (typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', () => {\n void (document.visibilityState === 'visible' ? onVisible() : onHidden());\n });\n // `visibilitychange` does not fire on a real close, and `unload` is not\n // reliable on mobile. `pagehide` is the one the browser actually guarantees.\n window.addEventListener('pagehide', () => void onHidden());\n}\n","import {\n EVENT_IDENTIFY,\n RESERVED_PREFIX,\n enqueue,\n flush,\n} from './events';\nimport * as push from './push';\nimport * as session from './session';\nimport {\n KEYS,\n anonymousId,\n countEvents,\n get,\n remove,\n rotateAnonymousId,\n set,\n} from './store';\nimport type {\n ArselConfig,\n ArselDiagnostics,\n ArselIdentity,\n EventProperties,\n} from './types';\nimport { SDK_VERSION } from './version';\n\nexport type {\n ArselConfig,\n ArselDiagnostics,\n ArselIdentity,\n EventProperties,\n} from './types';\nexport { SDK_VERSION } from './version';\n\nlet ready: Promise<void> | null = null;\nlet debug = false;\n\nfunction log(message: string): void {\n if (debug) console.info(`[arsel] ${message}`);\n}\n\nconst E164 = /^\\+[1-9]\\d{6,14}$/;\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * Bounded, memory-only holding pen for `track()` calls made before `init()`.\n * Drained into the durable queue the moment init supplies config.\n */\nconst PRE_INIT_BUFFER_MAX = 100;\ninterface BufferedEvent {\n name: string;\n properties: EventProperties;\n timestampMs: number;\n}\nlet preInitBuffer: BufferedEvent[] = [];\nlet preInitOverflowWarned = false;\n\nfunction attachFlushTriggers(): void {\n // Without these, events stranded by an offline spell or a backgrounded tab\n // wait for the next track() to happen to fire.\n if (typeof window !== 'undefined') {\n window.addEventListener('online', () => void flush().catch(() => {}));\n }\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') void flush().catch(() => {});\n });\n }\n}\n\n/**\n * Start the SDK. Idempotent — a second call returns the first one's promise, so\n * a framework that mounts twice does not mint two identities.\n *\n * Deliberately does **not** request notification permission. A prompt on page\n * load is what gets an origin permanently blocked by Chrome's abusive-\n * notification heuristics; call {@link promptForPush} from a user gesture.\n */\nexport function init(config: ArselConfig): Promise<void> {\n if (ready) return ready;\n ready = (async () => {\n if (!config.clientKey) throw new Error('Arsel: clientKey is required');\n const localhostHttp = /^http:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?(\\/|$)/.test(\n config.baseUrl ?? '',\n );\n if (!config.baseUrl?.startsWith('https://') && !localhostHttp) {\n throw new Error('Arsel: baseUrl must be HTTPS (http is allowed for localhost only)');\n }\n debug = config.debug ?? false;\n\n const baseUrl = config.baseUrl.replace(/\\/+$/, '');\n await Promise.all([\n set(KEYS.clientKey, config.clientKey),\n set(KEYS.baseUrl, baseUrl),\n ]);\n await anonymousId();\n\n // Anything tracked before init() moves into the durable queue, in order,\n // with its original timestamps.\n const buffered = preInitBuffer;\n preInitBuffer = [];\n for (const event of buffered) {\n await enqueue(event.name, event.properties, event.timestampMs);\n }\n\n // Registration is opt-in: unconditionally registering would clobber a\n // PWA's own worker on the same scope and 404-spam events-only sites.\n if (config.serviceWorker === 'external') push.useExternal();\n else if (config.serviceWorkerPath) await push.register(config.serviceWorkerPath);\n\n session.attach();\n attachFlushTriggers();\n // A background or prerendered page has not been *seen* — the session opens\n // on the visibilitychange (or prerenderingchange → visible) that follows.\n const doc = typeof document !== 'undefined'\n ? (document as Document & { prerendering?: boolean })\n : undefined;\n if (doc?.visibilityState === 'visible' && doc.prerendering !== true) {\n await session.onVisible();\n }\n\n // Anything stranded by a previous page's close goes out now.\n void flush();\n // Silent: it only acts if the org rotated its keys, and permission is\n // already granted in that case.\n void push.reconcile();\n log(`initialized (sdk ${SDK_VERSION})`);\n })();\n return ready;\n}\n\n/**\n * Record something the user did.\n *\n * Needs no notification permission and no push subscription. An event tracked\n * before {@link identify} attaches to the anonymous identity and is merged\n * forward when the user is identified. Events tracked before {@link init} are\n * buffered (up to 100) and enqueued when init supplies the configuration.\n *\n * Names beginning `arsel.` are reserved for the SDK and ignored.\n */\nexport async function track(\n name: string,\n properties: EventProperties = {},\n): Promise<void> {\n const trimmed = name?.trim();\n if (!trimmed) {\n log('track() called with a blank event name — ignoring');\n return;\n }\n if (trimmed.startsWith(RESERVED_PREFIX)) {\n log(`'${RESERVED_PREFIX}' is reserved for the SDK — ignoring '${trimmed}'`);\n return;\n }\n if (!ready) {\n if (preInitBuffer.length >= PRE_INIT_BUFFER_MAX) {\n if (!preInitOverflowWarned) {\n preInitOverflowWarned = true;\n console.warn(\n `[arsel] more than ${PRE_INIT_BUFFER_MAX} events tracked before init() — dropping the rest. Call init() earlier.`,\n );\n }\n return;\n }\n preInitBuffer.push({ name: trimmed, properties, timestampMs: Date.now() });\n return;\n }\n await ready;\n await enqueue(trimmed, properties);\n}\n\n/**\n * The person using this browser, as an id you already have.\n *\n * Everything tracked beforehand under the anonymous identity is merged onto the\n * contact this resolves to. Prefer `externalId` alone: it binds a contact\n * without putting an email address into page script, and it survives the user\n * changing their address.\n */\nexport async function identify(identity: ArselIdentity): Promise<void> {\n await ready;\n let { email, phoneNumber } = identity ?? {};\n const externalId = identity?.externalId;\n\n // Rejected here rather than stored: one malformed phone number would ride\n // every subsequent event, 400 each one, and silently drop them all.\n if (email && !EMAIL_SHAPE.test(email)) {\n console.error(`[arsel] identify(): '${email}' is not a valid email address — ignored`);\n email = undefined;\n }\n if (phoneNumber && !E164.test(phoneNumber)) {\n console.error(\n `[arsel] identify(): '${phoneNumber}' is not E.164 (e.g. +966501234567) — ignored`,\n );\n phoneNumber = undefined;\n }\n\n if (!externalId && !email && !phoneNumber) {\n log('identify() needs at least one of externalId, email or phoneNumber');\n return;\n }\n if (externalId) await set(KEYS.externalId, externalId);\n if (email) await set(KEYS.email, email);\n if (phoneNumber) await set(KEYS.phoneNumber, phoneNumber);\n\n // Sent immediately rather than waiting for the next track(): the merge is\n // what the caller asked for, and deferring it leaves the two contacts split\n // until something unrelated happens to fire.\n await enqueue(EVENT_IDENTIFY, {});\n}\n\n/**\n * Logout. Rotates the anonymous identity so the next person on this browser\n * does not inherit the previous one's history, and forgets the identifiers.\n *\n * Deliberately does **not** unsubscribe from push: the backend's opt-out is\n * durable and non-resurrectable, so a logout that called it would permanently\n * kill push on a shared machine for everyone who used it afterwards. That\n * opt-out exists as {@link optOut}, for the user who asks to stop receiving\n * notifications.\n */\nexport async function reset(): Promise<void> {\n await ready;\n await Promise.all([\n remove(KEYS.externalId),\n remove(KEYS.email),\n remove(KEYS.phoneNumber),\n remove(KEYS.sessionStartedAt),\n ]);\n await rotateAnonymousId();\n log('identity reset');\n}\n\n/**\n * Durable push opt-out for this browser — \"stop sending me notifications\", not\n * logout. The revocation is server-side and non-resurrectable: a later\n * registration of the same installation does not undo it. Re-opt-in is an\n * explicit, separate act on the backend.\n *\n * Returns false when this browser never completed a push registration (there\n * is nothing to revoke) or the request failed.\n */\nexport async function optOut(): Promise<boolean> {\n await ready;\n const revoked = await push.optOut();\n log(revoked ? 'push opt-out recorded' : 'push opt-out not sent — no registration to revoke');\n return revoked;\n}\n\n/**\n * Ask for notification permission and subscribe. **Call this from a click**, not\n * on page load.\n *\n * Returns false when the user declines, the browser has no support, or the org\n * does not have the web channel switched on. Declining is a normal outcome, not an\n * error: the contact and its events exist either way.\n */\nexport async function promptForPush(): Promise<boolean> {\n await ready;\n if (!push.isSupported()) return false;\n // Verified BEFORE prompting: a broken worker setup would otherwise burn the\n // origin's one real permission prompt on a subscription that can never settle.\n const problem = await push.preflight();\n if (problem) {\n console.error(`[arsel] promptForPush(): ${problem}`);\n return false;\n }\n const permission = await Notification.requestPermission();\n if (permission !== 'granted') {\n log(`notification permission: ${permission}`);\n return false;\n }\n return push.subscribe();\n}\n\n/** The identity events carry before login. Rotated by {@link reset}. */\nexport async function getAnonymousId(): Promise<string> {\n await ready;\n return anonymousId();\n}\n\n/** A snapshot safe to paste into a support ticket — no secrets in it. */\nexport async function diagnostics(): Promise<ArselDiagnostics> {\n const [\n anon,\n externalId,\n email,\n phoneNumber,\n installationId,\n deviceSecret,\n keyVersion,\n pendingEvents,\n subscribed,\n lastResponseCode,\n lastResponsePath,\n lastResponseAtMs,\n ] = await Promise.all([\n get<string>(KEYS.anonymousId),\n get<string>(KEYS.externalId),\n get<string>(KEYS.email),\n get<string>(KEYS.phoneNumber),\n get<string>(KEYS.installationId),\n get<string>(KEYS.deviceSecret),\n get<number>(KEYS.vapidKeyVersion),\n countEvents(),\n push.isSubscribed(),\n get<number>(KEYS.lastResponseCode),\n get<string>(KEYS.lastResponsePath),\n get<number>(KEYS.lastResponseAtMs),\n ]);\n\n return {\n sdkVersion: SDK_VERSION,\n initialized: ready !== null,\n anonymousId: anon,\n hasAssertedIdentity: Boolean(externalId || email || phoneNumber),\n installationId,\n hasDeviceSecret: Boolean(deviceSecret),\n pendingEvents,\n permission: push.isSupported() ? Notification.permission : 'unsupported',\n isSubscribed: subscribed,\n vapidKeyVersion: keyVersion,\n lastResponseCode,\n lastResponsePath,\n lastResponseAtMs,\n };\n}\n\n/** Deliver anything queued now, rather than on the next natural drain. */\nexport async function flushNow(): Promise<void> {\n await ready;\n await flush();\n}\n\nexport const Arsel = {\n init,\n track,\n identify,\n reset,\n optOut,\n promptForPush,\n getAnonymousId,\n diagnostics,\n flushNow,\n SDK_VERSION,\n};\n\nexport default Arsel;\n"],"names":["DB_NAME","EVENT_STORE","dbPromise","openDb","resolve","reject","request","db","tx","store","mode","run","get","key","s","v","set","value","remove","addEvent","body","idempotencyKey","allEvents","removeEvent","id","countEvents","KEYS","anonymousId","existing","minted","rotateAnonymousId","SDK_VERSION","SDK_HEADER","RESULT","CODE_NO_RESPONSE","classify","code","authenticated","record","path","post","baseUrl","headers","response","result","parsed","getJson","EVENTS_PATH","RESERVED_PREFIX","EVENT_SESSION_START","EVENT_SESSION_END","EVENT_IDENTIFY","MAX_EVENT_NAME","MAX_ANONYMOUS_ID","MAX_EXTERNAL_ID","MAX_BATCH","MAX_DATA_DEPTH","MAX_DATA_BYTES","sanitizeValue","depth","type","item","out","clean","sanitize","properties","bytes","dropped","raw","cost","buildBody","name","timestampMs","anon","externalId","email","phoneNumber","enqueue","flush","inFlight","drain","batchKey","batch","first","reportPermanentDrop","items","clientKey","pending","event","decodeBase64Url","padded","binary","i","isSupported","configPath","subscriptionsPath","ACTIVATION_TIMEOUT_MS","managedRegistration","registrationError","useExternal","register","error","waitForActive","registration","timeoutMs","timer","watch","worker","withTimeout","promise","message","_","activeRegistration","preflight","subscribe","config","subscription","sendSubscription","keyVersion","installationId","json","_a","optOut","deviceSecret","reconcile","knownVersion","fresh","isSubscribed","SESSION_GAP_MS","onVisible","now","openedAt","hiddenAt","onHidden","attach","ready","debug","log","E164","EMAIL_SHAPE","PRE_INIT_BUFFER_MAX","preInitBuffer","preInitOverflowWarned","attachFlushTriggers","init","localhostHttp","buffered","push.useExternal","push.register","session.attach","doc","session.onVisible","push.reconcile","track","trimmed","identify","identity","reset","revoked","push.optOut","promptForPush","push.isSupported","problem","push.preflight","permission","push.subscribe","getAnonymousId","diagnostics","pendingEvents","subscribed","lastResponseCode","lastResponsePath","lastResponseAtMs","push.isSubscribed","flushNow","Arsel"],"mappings":"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"}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EventProperties } from './types';
|
|
2
|
+
export declare const EVENTS_PATH = "/v1/events/send";
|
|
3
|
+
/** Reserved prefix, so an SDK event can never collide with a customer's. */
|
|
4
|
+
export declare const RESERVED_PREFIX = "arsel.";
|
|
5
|
+
export declare const EVENT_SESSION_START = "arsel.session_start";
|
|
6
|
+
export declare const EVENT_SESSION_END = "arsel.session_end";
|
|
7
|
+
export declare const EVENT_IDENTIFY = "arsel.identify";
|
|
8
|
+
/** The batch ceiling `POST /v1/events/send` accepts in an `{ events: [...] }` body. */
|
|
9
|
+
export declare const MAX_BATCH = 50;
|
|
10
|
+
/**
|
|
11
|
+
* Persist first, then send. A tab closed mid-flight would otherwise lose the
|
|
12
|
+
* event outright, and page-lifetime-only buffering is the single most common
|
|
13
|
+
* way a web analytics SDK silently undercounts.
|
|
14
|
+
*/
|
|
15
|
+
export declare function enqueue(name: string, properties: EventProperties, timestampMs?: number): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Deliver everything queued, oldest first.
|
|
18
|
+
*
|
|
19
|
+
* Serialised, because two concurrent drains would both read the same rows and
|
|
20
|
+
* send each event twice. A caller arriving mid-drain gets the in-flight promise
|
|
21
|
+
* rather than a silent no-op, so `await flush()` actually means "delivered".
|
|
22
|
+
*/
|
|
23
|
+
export declare function flush(): Promise<void>;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ArselConfig, ArselDiagnostics, ArselIdentity, EventProperties } from './types';
|
|
2
|
+
export type { ArselConfig, ArselDiagnostics, ArselIdentity, EventProperties, } from './types';
|
|
3
|
+
export { SDK_VERSION } from './version';
|
|
4
|
+
/**
|
|
5
|
+
* Start the SDK. Idempotent — a second call returns the first one's promise, so
|
|
6
|
+
* a framework that mounts twice does not mint two identities.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately does **not** request notification permission. A prompt on page
|
|
9
|
+
* load is what gets an origin permanently blocked by Chrome's abusive-
|
|
10
|
+
* notification heuristics; call {@link promptForPush} from a user gesture.
|
|
11
|
+
*/
|
|
12
|
+
export declare function init(config: ArselConfig): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Record something the user did.
|
|
15
|
+
*
|
|
16
|
+
* Needs no notification permission and no push subscription. An event tracked
|
|
17
|
+
* before {@link identify} attaches to the anonymous identity and is merged
|
|
18
|
+
* forward when the user is identified. Events tracked before {@link init} are
|
|
19
|
+
* buffered (up to 100) and enqueued when init supplies the configuration.
|
|
20
|
+
*
|
|
21
|
+
* Names beginning `arsel.` are reserved for the SDK and ignored.
|
|
22
|
+
*/
|
|
23
|
+
export declare function track(name: string, properties?: EventProperties): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* The person using this browser, as an id you already have.
|
|
26
|
+
*
|
|
27
|
+
* Everything tracked beforehand under the anonymous identity is merged onto the
|
|
28
|
+
* contact this resolves to. Prefer `externalId` alone: it binds a contact
|
|
29
|
+
* without putting an email address into page script, and it survives the user
|
|
30
|
+
* changing their address.
|
|
31
|
+
*/
|
|
32
|
+
export declare function identify(identity: ArselIdentity): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Logout. Rotates the anonymous identity so the next person on this browser
|
|
35
|
+
* does not inherit the previous one's history, and forgets the identifiers.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately does **not** unsubscribe from push: the backend's opt-out is
|
|
38
|
+
* durable and non-resurrectable, so a logout that called it would permanently
|
|
39
|
+
* kill push on a shared machine for everyone who used it afterwards. That
|
|
40
|
+
* opt-out exists as {@link optOut}, for the user who asks to stop receiving
|
|
41
|
+
* notifications.
|
|
42
|
+
*/
|
|
43
|
+
export declare function reset(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Durable push opt-out for this browser — "stop sending me notifications", not
|
|
46
|
+
* logout. The revocation is server-side and non-resurrectable: a later
|
|
47
|
+
* registration of the same installation does not undo it. Re-opt-in is an
|
|
48
|
+
* explicit, separate act on the backend.
|
|
49
|
+
*
|
|
50
|
+
* Returns false when this browser never completed a push registration (there
|
|
51
|
+
* is nothing to revoke) or the request failed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function optOut(): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Ask for notification permission and subscribe. **Call this from a click**, not
|
|
56
|
+
* on page load.
|
|
57
|
+
*
|
|
58
|
+
* Returns false when the user declines, the browser has no support, or the org
|
|
59
|
+
* does not have the web channel switched on. Declining is a normal outcome, not an
|
|
60
|
+
* error: the contact and its events exist either way.
|
|
61
|
+
*/
|
|
62
|
+
export declare function promptForPush(): Promise<boolean>;
|
|
63
|
+
/** The identity events carry before login. Rotated by {@link reset}. */
|
|
64
|
+
export declare function getAnonymousId(): Promise<string>;
|
|
65
|
+
/** A snapshot safe to paste into a support ticket — no secrets in it. */
|
|
66
|
+
export declare function diagnostics(): Promise<ArselDiagnostics>;
|
|
67
|
+
/** Deliver anything queued now, rather than on the next natural drain. */
|
|
68
|
+
export declare function flushNow(): Promise<void>;
|
|
69
|
+
export declare const Arsel: {
|
|
70
|
+
init: typeof init;
|
|
71
|
+
track: typeof track;
|
|
72
|
+
identify: typeof identify;
|
|
73
|
+
reset: typeof reset;
|
|
74
|
+
optOut: typeof optOut;
|
|
75
|
+
promptForPush: typeof promptForPush;
|
|
76
|
+
getAnonymousId: typeof getAnonymousId;
|
|
77
|
+
diagnostics: typeof diagnostics;
|
|
78
|
+
flushNow: typeof flushNow;
|
|
79
|
+
SDK_VERSION: string;
|
|
80
|
+
};
|
|
81
|
+
export default Arsel;
|
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare function isSupported(): boolean;
|
|
2
|
+
export declare function useExternal(): void;
|
|
3
|
+
export declare function register(path: string): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Everything that can be verified *before* the permission prompt, so a broken
|
|
6
|
+
* worker setup fails with a reason instead of burning the origin's one real
|
|
7
|
+
* prompt on a subscription that can never settle.
|
|
8
|
+
*
|
|
9
|
+
* Returns an error message, or null when push can proceed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function preflight(timeoutMs?: number): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe this browser and register it with Arsel.
|
|
14
|
+
*
|
|
15
|
+
* Only ever called from a user gesture path — `subscribe()` triggers the
|
|
16
|
+
* permission prompt, and a prompt fired on page load is what gets an origin
|
|
17
|
+
* permanently blocked in Chrome's abusive-notification heuristics.
|
|
18
|
+
*/
|
|
19
|
+
export declare function subscribe(): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Durable opt-out, mirroring the Android SDK: the backend revocation is
|
|
22
|
+
* non-resurrectable — a later register for this installation will not undo it.
|
|
23
|
+
* Distinct from `reset()`, which never touches push.
|
|
24
|
+
*/
|
|
25
|
+
export declare function optOut(): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Re-subscribe silently when the org has rotated its VAPID keypair.
|
|
28
|
+
*
|
|
29
|
+
* Rotation invalidates every live subscription: the browser's endpoint was
|
|
30
|
+
* issued against the old application server key, and pushes to it are refused.
|
|
31
|
+
* Nothing surfaces that to the user, so without this an entire cohort goes
|
|
32
|
+
* quietly unreachable and the org's reach decays with no error anywhere.
|
|
33
|
+
*
|
|
34
|
+
* Safe to call on every init — it is a no-op unless the version actually moved,
|
|
35
|
+
* and it never prompts: permission is already granted at this point, so
|
|
36
|
+
* `subscribe()` resolves without user interaction.
|
|
37
|
+
*/
|
|
38
|
+
export declare function reconcile(): Promise<void>;
|
|
39
|
+
export declare function isSubscribed(): Promise<boolean>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session ends after this long away — the industry-standard 30 minutes.
|
|
3
|
+
* Shorter gaps read a tab switch or a phone unlocking as a session boundary
|
|
4
|
+
* and inflate every count.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SESSION_GAP_MS: number;
|
|
7
|
+
/**
|
|
8
|
+
* Sessions from visibility transitions, with no timers.
|
|
9
|
+
*
|
|
10
|
+
* A session ends after {@link SESSION_GAP_MS} hidden, but that is only
|
|
11
|
+
* *discovered* on the next visible — so the end event is emitted then,
|
|
12
|
+
* backdated to when the page actually went away. A timer would have to survive
|
|
13
|
+
* the tab being frozen or discarded, which is precisely when it matters.
|
|
14
|
+
*
|
|
15
|
+
* The consequence is the standard one: someone who closes the tab and never
|
|
16
|
+
* returns produces no `session_end`. An open-but-unclosed session beats a
|
|
17
|
+
* fabricated end time.
|
|
18
|
+
*/
|
|
19
|
+
export declare function onVisible(now?: number): Promise<void>;
|
|
20
|
+
export declare function onHidden(now?: number): Promise<void>;
|
|
21
|
+
export declare function attach(): void;
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared durable state, in IndexedDB.
|
|
3
|
+
*
|
|
4
|
+
* IndexedDB rather than `localStorage` for one reason that decides it: the
|
|
5
|
+
* service worker has to read the device secret and the client key to report a
|
|
6
|
+
* `delivered` engagement when **no tab is open at all**, and `localStorage` is
|
|
7
|
+
* unreachable from a worker. Splitting state across both stores would put the
|
|
8
|
+
* two halves of one fact in two places.
|
|
9
|
+
*/
|
|
10
|
+
export interface PendingEvent {
|
|
11
|
+
/** Monotonic within a tab; the auto-increment key doubles as delivery order. */
|
|
12
|
+
id?: number;
|
|
13
|
+
body: string;
|
|
14
|
+
/**
|
|
15
|
+
* Minted once at enqueue and persisted, so a retry of the same event carries
|
|
16
|
+
* the same key and the backend's 24h idempotency window can dedupe it.
|
|
17
|
+
*/
|
|
18
|
+
idempotencyKey: string;
|
|
19
|
+
createdAtMs: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function get<T>(key: string): Promise<T | null>;
|
|
22
|
+
export declare function set(key: string, value: unknown): Promise<unknown>;
|
|
23
|
+
export declare function remove(key: string): Promise<unknown>;
|
|
24
|
+
export declare function addEvent(body: string, idempotencyKey: string): Promise<unknown>;
|
|
25
|
+
export declare function allEvents(): Promise<PendingEvent[]>;
|
|
26
|
+
export declare function removeEvent(id: number): Promise<unknown>;
|
|
27
|
+
export declare function countEvents(): Promise<number>;
|
|
28
|
+
export declare const KEYS: {
|
|
29
|
+
readonly clientKey: "client_key";
|
|
30
|
+
readonly baseUrl: "base_url";
|
|
31
|
+
readonly anonymousId: "anonymous_id";
|
|
32
|
+
readonly externalId: "external_id";
|
|
33
|
+
readonly email: "email";
|
|
34
|
+
readonly phoneNumber: "phone_number";
|
|
35
|
+
readonly installationId: "installation_id";
|
|
36
|
+
readonly deviceSecret: "device_secret";
|
|
37
|
+
readonly vapidKeyVersion: "vapid_key_version";
|
|
38
|
+
readonly endpoint: "endpoint";
|
|
39
|
+
readonly sessionStartedAt: "session_started_at";
|
|
40
|
+
readonly backgroundedAt: "backgrounded_at";
|
|
41
|
+
readonly lastResponseCode: "last_response_code";
|
|
42
|
+
readonly lastResponsePath: "last_response_path";
|
|
43
|
+
readonly lastResponseAtMs: "last_response_at";
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Person-shaped identity, minted on first use. Distinct from the installation
|
|
47
|
+
* id, which names the browser profile: this one names whoever is using it and
|
|
48
|
+
* is rotated by `reset()`, so a shared computer does not hand the next person
|
|
49
|
+
* the previous one's history.
|
|
50
|
+
*/
|
|
51
|
+
export declare function anonymousId(): Promise<string>;
|
|
52
|
+
export declare function rotateAnonymousId(): Promise<string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const RESULT: {
|
|
2
|
+
readonly success: "success";
|
|
3
|
+
readonly retryable: "retryable";
|
|
4
|
+
readonly permanent: "permanent";
|
|
5
|
+
readonly reauth: "reauth";
|
|
6
|
+
};
|
|
7
|
+
export type Result = (typeof RESULT)[keyof typeof RESULT];
|
|
8
|
+
export interface Response<T = unknown> {
|
|
9
|
+
result: Result;
|
|
10
|
+
code: number;
|
|
11
|
+
body: T | null;
|
|
12
|
+
}
|
|
13
|
+
/** No status line at all — DNS failure, offline, TLS error. */
|
|
14
|
+
export declare const CODE_NO_RESPONSE = -1;
|
|
15
|
+
/**
|
|
16
|
+
* Status → retry policy. Pure, and the same table the Android SDK uses: this is
|
|
17
|
+
* the decision that separates a browser that eventually delivers from one that
|
|
18
|
+
* gives up forever.
|
|
19
|
+
*/
|
|
20
|
+
export declare function classify(code: number, authenticated: boolean): Result;
|
|
21
|
+
export declare function post<T = unknown>(baseUrl: string, path: string, body: unknown, headers?: Record<string, string>, authenticated?: boolean): Promise<Response<T>>;
|
|
22
|
+
export declare function getJson<T>(baseUrl: string, path: string): Promise<T | null>;
|