{"version":3,"sources":["../src/OfflineStore.ts","../src/EventDispatcher.ts","../src/collectors/ListenState.ts","../src/internal/uuid.ts","../src/wire.ts","../src/collectors/BaseCollector.ts","../src/collectors/session/complete.ts","../src/collectors/session/track_change.ts","../src/collectors/track/play_buffer.ts","../src/collectors/track/play_complete.ts","../src/collectors/track/play_idle.ts","../src/collectors/track/play_pause.ts","../src/collectors/track/play_progress.ts","../src/collectors/thresholds.ts","../src/collectors/track/play_replay.ts","../src/collectors/track/play_seek.ts","../src/collectors/track/play_skip.ts","../src/collectors/track/play_start.ts","../src/collectors/track/play_visibility.ts","../src/collectors/track/play_volume.ts","../src/TelemetryManager.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * IndexedDB-backed durable queue for outgoing wire events.\n *\n * Plan §5 item 5: FIFO eviction at 5000 events. Drop oldest, increment a\n * `dropped_events` counter that piggybacks on the next successful dispatch.\n * Never block playback on storage failure.\n *\n * Hand-rolled (no `idb`/`idb-keyval` dep). The surface is small: append,\n * peek a batch, delete by id range, count. We avoid transactions across\n * multiple object stores; everything lives in one store keyed by an\n * auto-incrementing integer that's also the FIFO ordering.\n */\n\nimport type { WireEvent } from './wire'\n\nconst DB_NAME = 'delistening-telemetry'\nconst DB_VERSION = 1\nconst STORE = 'events'\nconst META = 'meta'\n\nconst META_KEY_DROPPED = 'dropped_events'\n\nexport interface QueuedRecord {\n id: number\n event: WireEvent\n}\n\nexport interface OfflineStoreOptions {\n /** Override db name. Useful for tests to keep DBs isolated. */\n dbName?: string\n /** FIFO eviction threshold. Default: 5000. */\n maxEvents?: number\n}\n\nexport class OfflineStore {\n private readonly dbName: string\n private readonly maxEvents: number\n private dbPromise: Promise | null = null\n\n constructor(options: OfflineStoreOptions = {}) {\n this.dbName = options.dbName ?? DB_NAME\n this.maxEvents = options.maxEvents ?? 5000\n }\n\n /**\n * Append a wire event to the queue. If the queue exceeds `maxEvents`,\n * drop the oldest event and bump the dropped_events counter.\n */\n async append(event: WireEvent): Promise {\n const db = await this.open()\n return new Promise((resolve, reject) => {\n const tx = db.transaction([STORE, META], 'readwrite')\n const store = tx.objectStore(STORE)\n store.add(event)\n const count = store.count()\n count.onsuccess = (): void => {\n const n = count.result\n if (n > this.maxEvents) {\n // Drop the oldest.\n const cursorReq = store.openCursor()\n cursorReq.onsuccess = (): void => {\n const cursor = cursorReq.result\n if (cursor) {\n cursor.delete()\n const meta = tx.objectStore(META)\n const dropReq = meta.get(META_KEY_DROPPED)\n dropReq.onsuccess = (): void => {\n const prev = (dropReq.result as number | undefined) ?? 0\n meta.put(prev + 1, META_KEY_DROPPED)\n }\n }\n }\n }\n }\n tx.oncomplete = (): void => resolve()\n tx.onerror = (): void => reject(tx.error)\n tx.onabort = (): void => reject(tx.error)\n })\n }\n\n /** Peek up to `n` oldest queued events. Does not delete. */\n async peek(n: number): Promise {\n const db = await this.open()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly')\n const store = tx.objectStore(STORE)\n const result: QueuedRecord[] = []\n const cursorReq = store.openCursor()\n cursorReq.onsuccess = (): void => {\n const cursor = cursorReq.result\n if (cursor && result.length < n) {\n result.push({ id: cursor.primaryKey as number, event: cursor.value as WireEvent })\n cursor.continue()\n } else {\n resolve(result)\n }\n }\n cursorReq.onerror = (): void => reject(cursorReq.error)\n })\n }\n\n /** Delete the records with the given ids. */\n async deleteRecords(ids: number[]): Promise {\n if (ids.length === 0) return\n const db = await this.open()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readwrite')\n const store = tx.objectStore(STORE)\n for (const id of ids) store.delete(id)\n tx.oncomplete = (): void => resolve()\n tx.onerror = (): void => reject(tx.error)\n })\n }\n\n async count(): Promise {\n const db = await this.open()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, 'readonly')\n const req = tx.objectStore(STORE).count()\n req.onsuccess = (): void => resolve(req.result)\n req.onerror = (): void => reject(req.error)\n })\n }\n\n /** Read and zero the dropped_events counter. */\n async drainDroppedCount(): Promise {\n const db = await this.open()\n return new Promise((resolve, reject) => {\n const tx = db.transaction(META, 'readwrite')\n const meta = tx.objectStore(META)\n const req = meta.get(META_KEY_DROPPED)\n req.onsuccess = (): void => {\n const prev = (req.result as number | undefined) ?? 0\n meta.put(0, META_KEY_DROPPED)\n resolve(prev)\n }\n req.onerror = (): void => reject(req.error)\n })\n }\n\n private open(): Promise {\n if (this.dbPromise) return this.dbPromise\n this.dbPromise = new Promise((resolve, reject) => {\n if (typeof indexedDB === 'undefined') {\n reject(new Error('OfflineStore: indexedDB is not available in this environment'))\n return\n }\n const req = indexedDB.open(this.dbName, DB_VERSION)\n req.onupgradeneeded = (): void => {\n const db = req.result\n if (!db.objectStoreNames.contains(STORE)) {\n db.createObjectStore(STORE, { autoIncrement: true })\n }\n if (!db.objectStoreNames.contains(META)) {\n db.createObjectStore(META)\n }\n }\n req.onsuccess = (): void => resolve(req.result)\n req.onerror = (): void => {\n // Don't cache the rejected promise: clear it so the next call\n // re-attempts the open instead of failing forever on a transient error.\n this.dbPromise = null\n reject(req.error)\n }\n })\n return this.dbPromise\n }\n}\n","import { OfflineStore } from './OfflineStore'\nimport type { WireEvent } from './wire'\n\n/**\n * Batches WireEvents and POSTs them to the ingestion endpoint.\n *\n * Plan §5:\n * - Item 5: never block playback. All writes go through OfflineStore so\n * events survive reloads.\n * - Item 6: exponential backoff with jitter, max 60s, NO retry budget cap.\n * Stop only on permanent 4xx (other than 408/429).\n *\n * Flush triggers:\n * - 5s interval while there are queued events\n * - immediate flush when the page is hidden or unloaded (`pagehide`, or\n * `visibilitychange` to hidden), as a keepalive fetch that carries the\n * bearer. Not `sendBeacon`: it can't send the Authorization header, so\n * ingestion 401'd every beacon while the SDK deleted the batch anyway.\n * - `flush()` called manually (e.g. by integration tests)\n */\n\nexport interface EventDispatcherOptions {\n endpoint: string\n /** Override the offline store (tests use an in-memory IDB). */\n offline?: OfflineStore\n /** Flush interval in ms. Default: 5000. */\n flushIntervalMs?: number\n /** Max events per POST body. Default: 50. */\n batchSize?: number\n /**\n * Custom fetch implementation. Tests use a mock; production uses\n * `globalThis.fetch`. We don't accept `null` — pass a stub if needed.\n */\n fetchImpl?: typeof fetch\n /** Optional `Authorization` header value. */\n authToken?: string\n /**\n * Called when ingestion answers 401 — the bearer has expired (the token\n * lives an hour; the persistent player outlives it). Returns a fresh token\n * or null when none can be had. With a refresher the batch is retried once\n * with the new token instead of being dropped; without one a 401 stays a\n * permanent 4xx (dropped), as before.\n */\n refreshAuthToken?: () => Promise\n}\n\nexport class EventDispatcher {\n private readonly endpoint: string\n private readonly offline: OfflineStore\n private readonly flushIntervalMs: number\n private readonly batchSize: number\n private readonly fetchImpl: typeof fetch\n private readonly refreshAuthToken?: () => Promise\n private authToken: string | undefined\n\n private timer: ReturnType | null = null\n private inFlight: Promise | null = null\n private retryDelayMs = 1000\n private nextRetryAt = 0\n private pageHideHandler: (() => void) | null = null\n private readonly exitHooks: Array<() => Promise | void> = []\n private static readonly EXIT_BATCHES = 2\n /** Browsers refuse keepalive bodies over 64 KB; stay clear of it. */\n private static readonly EXIT_BODY_BYTES = 60_000\n private visibilityHandler: (() => void) | null = null\n\n constructor(options: EventDispatcherOptions) {\n if (!options.endpoint) {\n throw new Error('EventDispatcher: endpoint is required')\n }\n this.endpoint = options.endpoint\n this.offline = options.offline ?? new OfflineStore()\n this.flushIntervalMs = options.flushIntervalMs ?? 5000\n this.batchSize = options.batchSize ?? 50\n this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis)\n if (options.authToken !== undefined) {\n this.authToken = options.authToken\n }\n if (options.refreshAuthToken !== undefined) {\n this.refreshAuthToken = options.refreshAuthToken\n }\n }\n\n /**\n * Swap the bearer mid-session — the player calls this whenever the server\n * hands it a fresh token (every track start), so a long session never\n * outlives its credential.\n */\n setAuthToken(token: string | undefined): void {\n this.authToken = token || undefined\n }\n\n /**\n * Run `hook` when the page is going away (pagehide), before the final\n * flush — so an event it enqueues (the \"left the page\" pause) goes out in\n * that same flush instead of waiting for a next page load whose new\n * session would get it refused.\n */\n onExit(hook: () => Promise | void): void {\n this.exitHooks.push(hook)\n }\n\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => {\n void this.flush()\n }, this.flushIntervalMs)\n if (typeof window !== 'undefined') {\n // pagehide fires on reloads, tab closes and bfcache entries (where\n // beforeunload often doesn't, notably on mobile); a hidden tab may\n // never come back, so it flushes too.\n this.pageHideHandler = (): void => {\n void (async () => {\n for (const hook of this.exitHooks) {\n try {\n await hook()\n } catch {\n // An exit hook must never cost the flush of what's queued.\n }\n }\n // Drain up to two batches: the exit pause is the newest event, so\n // a backlog of more than one batch would otherwise strand it. Not\n // more — the browser caps in-flight keepalive bodies at 64 KB.\n for (let i = 0; i < EventDispatcher.EXIT_BATCHES; i++) {\n if ((await this.flushNow(true)) === 0) break\n }\n })()\n }\n window.addEventListener('pagehide', this.pageHideHandler)\n if (typeof document !== 'undefined') {\n this.visibilityHandler = (): void => {\n if (document.visibilityState === 'hidden') void this.flushNow(true)\n }\n document.addEventListener('visibilitychange', this.visibilityHandler)\n }\n }\n }\n\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n if (this.pageHideHandler && typeof window !== 'undefined') {\n window.removeEventListener('pagehide', this.pageHideHandler)\n this.pageHideHandler = null\n }\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler)\n this.visibilityHandler = null\n }\n }\n\n /** Enqueue an event. Always goes through the offline store. */\n async enqueue(event: WireEvent): Promise {\n await this.offline.append(event)\n }\n\n /**\n * Try to flush queued events. No-op if a flush is already in progress or\n * we're inside a backoff window. Returns the number of events successfully\n * dispatched.\n */\n async flush(): Promise {\n if (Date.now() < this.nextRetryAt) return 0\n return this.flushNow()\n }\n\n /**\n * Flush ignoring the backoff window — the page is going away, so this is\n * the last chance. Sends as a keepalive fetch with the bearer (see\n * `send()`), and deletes the batch only once ingestion has answered: a\n * batch the browser never delivered stays queued. Re-sending it on the\n * next load is harmless — the new page's session gets it refused rather\n * than counted twice, and reads count distinct event_ids.\n */\n async flushNow(exiting = false): Promise {\n // A flush already running (the visibilitychange one, when the tab is\n // being closed) used to make this return 0 — stranding the exit pause\n // the pagehide hook had just queued. Wait for it, then send ours.\n while (this.inFlight) await this.inFlight.catch(() => 0)\n this.inFlight = this.flushOnce(exiting)\n try {\n return await this.inFlight\n } finally {\n this.inFlight = null\n }\n }\n\n private async flushOnce(exiting: boolean): Promise {\n const queued = await this.offline.peek(this.batchSize)\n if (queued.length === 0) return 0\n // One identity per batch. Ingestion refuses a WHOLE batch (403) when\n // any event's session or listener differs from the token's, and a\n // refused batch is dropped as a permanent 4xx — so events left over\n // from a previous visit (a sign-in or browser restart since) took the\n // new page's events down with them. Send the first identity's events\n // alone; the rest go next, on their own.\n const first = queued[0]!.event\n let batch = queued.filter(\n (r) => r.event.session_id === first.session_id && r.event.listener_uuid === first.listener_uuid,\n )\n if (exiting) batch = this.fitKeepalive(batch)\n const ok = await this.post(\n batch.map((r) => r.event),\n exiting,\n )\n if (ok) {\n await this.offline.deleteRecords(batch.map((r) => r.id))\n this.retryDelayMs = 1000 // reset backoff\n return batch.length\n }\n this.scheduleRetry()\n return 0\n }\n\n /** Trim an exit batch from the end until its body fits a keepalive request. */\n private fitKeepalive(batch: T[]): T[] {\n let out = batch\n while (out.length > 1 && byteLength(JSON.stringify({ events: out.map((r) => r.event) })) > EventDispatcher.EXIT_BODY_BYTES) {\n out = out.slice(0, Math.ceil(out.length / 2))\n }\n return out\n }\n\n /**\n * POST a batch. Returns true on success, false on transient failure (will\n * retry). 4xx (other than 408/429) returns true so we don't loop on bad\n * payloads.\n */\n private async post(events: WireEvent[], keepalive = false): Promise {\n const dropped = await this.offline.drainDroppedCount().catch(() => 0)\n const body = JSON.stringify({\n events,\n ...(dropped > 0 ? { meta: { dropped_events: dropped } } : {}),\n })\n const status = await this.send(body, keepalive)\n if (status !== 401 || !this.refreshAuthToken) return this.outcome(status)\n // Expired bearer: refresh once and retry the same body. A refresh that\n // yields nothing keeps the batch (transient — the next flush, after\n // backoff, asks again) rather than dropping plays on the floor.\n const fresh = await this.refreshAuthToken().catch(() => null)\n if (!fresh) return false\n this.setAuthToken(fresh)\n const retried = await this.send(body, keepalive)\n return retried === 401 ? false : this.outcome(retried)\n }\n\n /** One POST. Returns the HTTP status, or 0 for a network failure. */\n private async send(body: string, keepalive = false): Promise {\n try {\n const headers: Record = { 'Content-Type': 'application/json' }\n if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`\n const res = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers,\n body,\n // Only when the page is leaving: the browser refuses keepalive bodies\n // over 64 KB, and a refused send was retried forever, stalling every\n // batch behind it. An ordinary flush has no such limit.\n keepalive,\n })\n return res.status\n } catch {\n return 0\n }\n }\n\n /**\n * Map a status to \"done with this batch\": true on success or a permanent\n * 4xx (dropped so we never loop on a bad payload), false on anything\n * transient (network, 408, 429, 5xx) so it stays queued for retry.\n */\n private outcome(status: number): boolean {\n if (status >= 200 && status < 300) return true\n if (status === 408 || status === 429) return false\n if (status >= 400 && status < 500) return true\n return false\n }\n\n /**\n * Schedule the next retry with exponential backoff + jitter.\n * Max 60s. No cap on total retry count — events are durable in OfflineStore.\n */\n private scheduleRetry(): void {\n this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000)\n const jitter = Math.random() * 0.3 * this.retryDelayMs\n this.nextRetryAt = Date.now() + this.retryDelayMs + jitter\n }\n}\n\nfunction byteLength(text: string): number {\n return new TextEncoder().encode(text).length\n}\n","/**\n * One listen's accounting, shared by every collector of a TelemetryManager.\n *\n * A listen begins on a track's first `engine:play`, and again on a HAND\n * replay (a manual restart, or playing the track again after it ended): each\n * listen gets its own `played_at` and its own seconds-heard counter, so a\n * replay's completion and drop-off are measured on their own instead of\n * piling onto the first listen (where completion read above 100%). A loop\n * (repeat-one / ×N) needs nothing here — the queue resets the track, which\n * resets this state through `resetForNewTrack()`.\n *\n * Shared rather than per-collector: before, only the progress collector\n * advanced its own counter, so every pause, complete, skip and replay row\n * reported zero seconds heard.\n */\nexport class ListenState {\n /** ISO start of the current listen, or null before the first play. */\n playedAt: string | null = null\n /** Media seconds actually heard in this listen (seeks excluded). */\n secondsHeard = 0\n /** Last media position the progress tick saw, for the next delta. */\n private lastPosition: number | null = null\n\n /** Called on every `engine:play`; starts the first listen of a track. */\n markPlaying(): void {\n if (this.playedAt === null) this.begin()\n }\n\n /** Start a new listen now: fresh `played_at`, nothing heard yet. */\n begin(): void {\n this.playedAt = new Date().toISOString()\n this.secondsHeard = 0\n this.lastPosition = null\n }\n\n /**\n * Advance by the media time played since the last tick. A backward jump,\n * or a forward one of more than a few seconds (a seek), adds nothing: only\n * time the listener actually heard counts.\n */\n advance(position: number): void {\n if (this.lastPosition !== null) {\n const delta = position - this.lastPosition\n if (delta > 0 && delta <= ListenState.MAX_TICK_SECONDS) this.secondsHeard += delta\n } else {\n // First tick of a listen: the engine ticks once a second.\n this.secondsHeard += Math.min(Math.max(position, 0), 1)\n }\n this.lastPosition = position\n }\n\n /** The playhead moved while nothing could be heard (muted): no seconds. */\n silent(position: number): void {\n this.lastPosition = position\n }\n\n /** The track changed (or detached): no listen in progress. */\n reset(): void {\n this.playedAt = null\n this.secondsHeard = 0\n this.lastPosition = null\n }\n\n /** Engine ticks once a second; a gap above this is a seek, not listening. */\n private static readonly MAX_TICK_SECONDS = 5\n}\n","/**\n * RFC 4122 v4 UUID for client-minted `event_id`s. Prefers the platform\n * `crypto.randomUUID`, falling back to `getRandomValues` (and last-ditch\n * Math.random) so the SDK works in older webviews and test environments.\n *\n * Kept internal to @delistening/telemetry — the player package has its own\n * copy for its own ids; we don't reach across the package boundary for this.\n */\nexport function cryptoRandomUuid(): string {\n // biome-ignore lint/suspicious/noExplicitAny: optional API detection\n const c = (globalThis as any).crypto as Crypto | undefined\n if (c && typeof c.randomUUID === 'function') return c.randomUUID()\n // Fallback: RFC 4122 v4 over getRandomValues.\n const bytes = new Uint8Array(16)\n if (c && typeof c.getRandomValues === 'function') {\n c.getRandomValues(bytes)\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256)\n }\n // biome-ignore lint/style/noNonNullAssertion: in-bounds by construction\n bytes[6] = (bytes[6]! & 0x0f) | 0x40\n // biome-ignore lint/style/noNonNullAssertion: in-bounds by construction\n bytes[8] = (bytes[8]! & 0x3f) | 0x80\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'))\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex\n .slice(8, 10)\n .join('')}-${hex.slice(10, 16).join('')}`\n}\n","/**\n * Wire format types for the telemetry SDK. The CANONICAL contract is the JSON\n * Schema at `shared/schemas/play_event.wire.schema.json` — the same document the\n * Rust ingestion service validates every event against. These TypeScript types\n * mirror it; `tests/wire_schema.contract.test.ts` validates real collector\n * output against the schema so the two cannot drift silently (a divergence\n * would surface as a 400 from ingestion in production).\n *\n * The post-ingestion schema in `shared/schemas/play_event.schema.json` carries\n * server-enriched fields (fraud_verdict, account_trust_level, country_code,\n * engagement_contribution, etc.) that the client must NOT send — they are\n * computed by ingestion. The wire format below is the strict subset the client\n * is responsible for.\n *\n * Cross-field invariants the client enforces:\n * - `event_name` ∈ TrackEventName ∪ SessionEventName\n * - `played_at` ≤ `client_emitted_at` (start of playback is never after emit)\n * - `listener_uuid === null` is allowed; ingestion classifies as preview_play\n * - `track_uuid` MUST be a UUID; we don't validate format on the client\n *\n * Cross-field invariants ingestion enforces (NOT client's job, but documented\n * here so the schema stays accurate):\n * - listener_uuid==null ⇒ play_type==preview_play (see shared/schemas/README.md:61)\n */\n\nexport type TrackEventName =\n | 'play:start'\n | 'play:progress'\n | 'play:pause'\n | 'play:complete'\n | 'play:seek'\n | 'play:skip'\n | 'play:replay'\n | 'play:visibility'\n | 'play:volume'\n | 'play:idle'\n | 'play:buffer'\n // App-level navigation intent (not derived from an engine signal): a listener\n // jumped to a track section via the structure strip. event_data carries\n // { section_label, target_seconds, origin }. Emitted via TelemetryManager.track().\n | 'nav:section'\n\nexport type SessionEventName = 'session:track_change' | 'session:complete'\n\nexport type EventName = TrackEventName | SessionEventName\n\nexport type DeviceType = 'web' | 'mobile_web' | 'ios_app' | 'android_app'\n\n/**\n * One wire event. Telemetry batches these into arrays and POSTs the array as\n * a single body to `/events/plays`.\n */\nexport interface WireEvent {\n /** MAJOR.MINOR version of the wire format, e.g. \"1.0\". */\n schema_version: string\n /** Client-side UUID. Used for idempotency at ingestion. */\n event_id: string\n /** Collector-emitted event name. See vocabularies above. */\n event_name: EventName\n /** UUID of the track this event is about. */\n track_uuid: string\n /** Optional: artist UUID (denormalized for query locality). */\n artist_uuid?: string\n /** SessionManager-minted session_id. */\n session_id: string\n /** Authenticated listener UUID, or null for anonymous. */\n listener_uuid: string | null\n /** ISO-8601 UTC time the track started playing. Captured at engine:play. */\n played_at: string\n /** ISO-8601 UTC time the collector emitted this event. */\n client_emitted_at: string\n /** Seconds of playback accumulated for this track so far. */\n duration_played: number\n /** 0-1 ratio of duration_played / track duration. -1 if duration unknown. */\n completion_rate: number\n /** Device classification. Web defaults to 'web'; mobile UA → 'mobile_web'. */\n device_type: DeviceType\n /** Document referrer host, if same-origin permitted. */\n referrer_domain?: string\n utm_source?: string\n utm_campaign?: string\n utm_medium?: string\n share_token?: string\n share_source?: string\n /**\n * Scheme-and-host origin of the site embedding the player (e.g. \"https://kirawaves.com\").\n * Only present for plays inside an embed iframe; omitted for plays on delistening.app.\n */\n embed_origin?: string\n /**\n * Collector-specific payload. Keep keys snake_case to match the rest of the\n * wire format. Examples: { from, to } for play:seek, { volume_from, volume_to }\n * for play:volume, { hidden } for play:visibility, { trigger } for play:replay.\n */\n event_data: Record\n}\n\n/** Current wire-format version. */\nexport const WIRE_SCHEMA_VERSION = '1.1' as const\n\n/**\n * Detect device_type from the runtime environment. Conservative — anything\n * that isn't clearly mobile-web ends up as `'web'`. Native apps don't use\n * this package at all; they set their own value.\n */\nexport function detectDeviceType(): DeviceType {\n if (typeof navigator === 'undefined') return 'web'\n // Mobile detection is a tar pit. We use the userAgentData hint when\n // available and fall back to a single conservative regex.\n // biome-ignore lint/suspicious/noExplicitAny: experimental API\n const uaData = (navigator as any).userAgentData as { mobile?: boolean } | undefined\n if (uaData?.mobile === true) return 'mobile_web'\n if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) return 'mobile_web'\n return 'web'\n}\n","import type { EventBus, PlayerEvents, SessionManager, TrackRef } from '@delistening/player'\nimport { cryptoRandomUuid } from '../internal/uuid'\nimport type { EventName, WireEvent } from '../wire'\nimport { WIRE_SCHEMA_VERSION, detectDeviceType } from '../wire'\nimport type { ListenState } from './ListenState'\n\n/**\n * Context shared by every collector while attached.\n *\n * `track` is the track UUID + artist UUID context for events the collector\n * emits. It can change underneath the collector when the queue moves —\n * collectors observe `queue:track_change` to update local state, but the\n * context object itself is provided by TelemetryManager so that out-of-band\n * collectors (session-level) can read it consistently.\n */\nexport interface CollectorContext {\n bus: EventBus\n session: SessionManager\n audio: HTMLAudioElement\n /** Returns the currently-attached track, or null between attaches. */\n getCurrentTrack(): TrackRef | null\n /** Sink for collector-emitted wire events. TelemetryManager wires this to dispatcher. */\n emit(event: WireEvent): void\n /** The current listen, shared by every collector (see ListenState). */\n listen: ListenState\n}\n\n/**\n * Optional metadata a collector may stash on each event (UTM, share token,\n * listener_uuid). Collectors don't gather these themselves — TelemetryManager\n * collects them once at attach time and lets every collector see the same\n * snapshot via context.\n */\nexport interface CollectorMetadata {\n listenerUuid: string | null\n referrerDomain?: string\n utmSource?: string\n utmCampaign?: string\n utmMedium?: string\n shareToken?: string\n shareSource?: string\n /** Embedding site origin, e.g. \"https://kirawaves.com\". Absent for non-embed plays. */\n embedOrigin?: string\n}\n\n/**\n * Base class for all collectors. Provides:\n * - `attach(ctx, metadata)` lifecycle\n * - `detach()` lifecycle\n * - `emit(name, eventData)` builder that fills in the boilerplate\n *\n * Collectors should NOT subscribe in their constructor. Subscribe in\n * `onAttach()` so detach() is symmetric. The `subscriptions` array is the\n * convention — push your unsubscribe fns there and the base will clean up.\n */\nexport abstract class BaseCollector {\n protected ctx!: CollectorContext\n protected metadata!: CollectorMetadata\n protected readonly subscriptions: Array<() => void> = []\n private attached = false\n\n /** Collector name — used for diagnostics only, not the wire. */\n abstract readonly name: string\n\n attach(ctx: CollectorContext, metadata: CollectorMetadata): void {\n if (this.attached) return\n this.attached = true\n this.ctx = ctx\n this.metadata = metadata\n this.subscriptions.push(\n ctx.bus.on('engine:play', () => {\n ctx.listen.markPlaying()\n }),\n )\n this.onAttach()\n }\n\n detach(): void {\n if (!this.attached) return\n this.attached = false\n for (const off of this.subscriptions) off()\n this.subscriptions.length = 0\n this.onDetach()\n }\n\n /** Subscribe to bus events here. Called once after `attach()`. */\n protected abstract onAttach(): void\n\n /** Optional cleanup hook. Subscriptions are already torn down. */\n protected onDetach(): void {\n /* default no-op */\n }\n\n /**\n * Build a wire event from a collector emission. Collectors call this with\n * just the event name and the collector-specific `event_data`; the rest of\n * the fields are filled in from context + metadata.\n */\n protected build(name: EventName, eventData: Record = {}): WireEvent {\n const track = this.ctx.getCurrentTrack()\n if (!track) {\n throw new Error(`${this.name}: cannot build event with no current track`)\n }\n const duration = this.ctx.audio.duration\n const listen = this.ctx.listen\n const background = typeof document !== 'undefined' ? document.hidden === true : false\n return {\n schema_version: WIRE_SCHEMA_VERSION,\n event_id: cryptoRandomUuid(),\n event_name: name,\n track_uuid: track.trackUuid,\n ...(track.artistUuid ? { artist_uuid: track.artistUuid } : {}),\n session_id: this.ctx.session.getSessionId(),\n listener_uuid: this.metadata.listenerUuid,\n played_at: listen.playedAt ?? new Date().toISOString(),\n client_emitted_at: new Date().toISOString(),\n // Whole seconds on the wire: the schema types duration_played as an\n // integer (play_event.wire.schema.json). Round here so the contract\n // holds even if a future collector accumulates sub-second deltas;\n // completion_rate keeps the precise value below.\n duration_played: Math.round(listen.secondsHeard),\n // Capped at 1: replaying a part (the hook, three times) hears more\n // seconds than the track is long, and ingestion rejects any event\n // above 1 — which silently dropped the rest of that listen,\n // completion included.\n completion_rate:\n Number.isFinite(duration) && duration > 0\n ? Math.min(1, listen.secondsHeard / duration)\n : -1,\n device_type: detectDeviceType(),\n ...(this.metadata.referrerDomain ? { referrer_domain: this.metadata.referrerDomain } : {}),\n ...(this.metadata.utmSource ? { utm_source: this.metadata.utmSource } : {}),\n ...(this.metadata.utmCampaign ? { utm_campaign: this.metadata.utmCampaign } : {}),\n ...(this.metadata.utmMedium ? { utm_medium: this.metadata.utmMedium } : {}),\n ...(this.metadata.shareToken ? { share_token: this.metadata.shareToken } : {}),\n ...(this.metadata.shareSource ? { share_source: this.metadata.shareSource } : {}),\n ...(this.metadata.embedOrigin ? { embed_origin: this.metadata.embedOrigin } : {}),\n // Whether the tab was in the background when this fired. Listening\n // in the background is listening: it's recorded and shown to the\n // artist next to foreground listening, never subtracted. Carried in\n // event_data so an ingestion that predates it ignores it rather than\n // rejecting the batch.\n event_data: { ...eventData, background },\n }\n }\n\n /** Convenience: build + emit in one step. */\n protected emit(name: EventName, eventData: Record = {}): void {\n this.ctx.emit(this.build(name, eventData))\n }\n\n /**\n * Called by TelemetryManager when the queue moves, BEFORE the current\n * track and the shared listen state switch to the new track — the last\n * moment a collector can still report on the track that is ending.\n */\n beforeTrackChange(_change: PlayerEvents['queue:track_change']): void {\n /* default no-op */\n }\n\n /** Called by TelemetryManager when the current track changes. */\n resetForNewTrack(): void {\n // Shared state: every collector resets it on the same track change,\n // which is idempotent.\n this.ctx?.listen.reset()\n this.onTrackReset()\n }\n\n protected onTrackReset(): void {\n /* default no-op */\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `session:complete` when the queue is exhausted — the last track in an\n * album/playlist (or a single shared track) finishes and there is nothing to\n * advance to. PlayerController turns the final `engine:ended` into a\n * `next('auto')`, and QueueManager raises `queue:complete { lastTrack }` when\n * that advance runs off the end of the queue.\n *\n * This is the queue \"listen-through\" marker: paired with `play:start` it lets\n * analytics compute full-album / full-track completion rate. The envelope's\n * `track_uuid` already carries the last track — TelemetryManager only repoints\n * `currentTrack` on `queue:track_change`, which `queue:complete` does NOT emit —\n * so the payload is empty, the same shape `play:complete` uses.\n *\n * Scope note: this is the *queue*-completion stop event. An explicit per-tab\n * session teardown (tab close) is a separate concern — a closed tab relies on\n * the ingestion-side 30s TTL by design, and there is no client bus event for it\n * today.\n */\nexport class SessionCompleteCollector extends BaseCollector {\n readonly name = 'SessionCompleteCollector'\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('queue:complete', () => {\n this.emit('session:complete')\n }),\n )\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `session:track_change` whenever the queue moves between tracks.\n * `event_data.reason` carries the QueueChangeReason ('next' | 'prev' | 'jump'\n * | 'auto') so analytics can distinguish manual navigation from album flow.\n *\n * Carries both the from-track and to-track UUIDs so downstream can do \"after\n * Track 1, X% of listeners went to Track Y\" without joining adjacent rows.\n */\nexport class SessionTrackChangeCollector extends BaseCollector {\n readonly name = 'SessionTrackChangeCollector'\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('queue:track_change', (e) => {\n // Skip the initial load (from === null) — that's not a \"change\", that's\n // session start. PlayStartCollector handles session-start semantics.\n if (e.from === null) return\n this.emit('session:track_change', {\n from_track_uuid: e.from.trackUuid,\n to_track_uuid: e.to.trackUuid,\n reason: e.reason,\n })\n }),\n )\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:buffer` on a buffer stall — an `engine:waiting` (playback blocked\n * on network) followed by an `engine:playing` (recovery) — carrying\n * `{ stall_duration_ms }`. Network-quality metric.\n *\n * A short floor (`MIN_STALL_MS`) filters out the sub-200ms `waiting`/`playing`\n * flutter browsers emit around gapless transitions and seeks, which aren't real\n * network stalls. An `engine:playing` with no pending `engine:waiting` is inert.\n */\nexport class PlayBufferCollector extends BaseCollector {\n readonly name = 'PlayBufferCollector'\n\n /** Stalls shorter than this are gapless-transition / seek artefacts, not network. */\n private static readonly MIN_STALL_MS = 200\n\n /** Monotonic `at` of the pending `engine:waiting`, or null when not stalled. */\n private waitingAt: number | null = null\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:waiting', (e) => {\n this.waitingAt = e.at\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:playing', (e) => {\n if (this.waitingAt === null) return\n const stall = e.at - this.waitingAt\n this.waitingAt = null\n if (stall < PlayBufferCollector.MIN_STALL_MS) return\n this.emit('play:buffer', { stall_duration_ms: Math.round(stall) })\n }),\n )\n }\n\n protected override onTrackReset(): void {\n this.waitingAt = null\n }\n}\n","import type { PlayerEvents } from '@delistening/player'\nimport { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:complete` on natural end-of-track. Distinct from a manual seek\n * to the end — `engine:ended` only fires when the audio element runs out of\n * source naturally.\n *\n * On a natural end that advances the queue (next track, repeat-one, ×N) the\n * player's own `engine:ended` handler runs first — it was subscribed before\n * telemetry attached — and moves to the next track synchronously. By the\n * time this collector's `engine:ended` handler ran, the context already\n * pointed at the NEXT track with a fresh listen: the finished track got no\n * complete, and the next one got a complete with 0 seconds heard. So the\n * complete for an auto-advance is sent from beforeTrackChange, while the\n * finished track is still current, and the late `engine:ended` is skipped.\n * The last track of a queue (no advance) completes from `engine:ended`.\n */\nexport class PlayCompleteCollector extends BaseCollector {\n readonly name = 'PlayCompleteCollector'\n /** Set when the auto-advance hook already completed the track `engine:ended` is about to report. */\n private completedOnAdvance = false\n /** This track's complete was sent (either path) — never twice, whichever handler runs first. */\n private completed = false\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:ended', () => {\n if (this.completedOnAdvance) {\n this.completedOnAdvance = false\n return\n }\n if (this.completed) return\n this.completed = true\n this.emit('play:complete')\n }),\n )\n }\n\n override beforeTrackChange(change: PlayerEvents['queue:track_change']): void {\n // 'auto' only comes from the player's engine:ended handler (a natural end).\n if (change.reason !== 'auto' || change.from === null || this.completed) return\n this.emit('play:complete')\n this.completedOnAdvance = true\n }\n\n protected override onTrackReset(): void {\n this.completed = false\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:idle` when playback continues for `idleMs` without ANY user\n * interaction — no seek, pause, volume change, rate change, or tab switch.\n * Passive-listening / bot-pattern signal: a human queue-and-walk-away looks\n * different from a script holding a tab open.\n *\n * A countdown is armed on `engine:play` and re-armed on every interaction; if\n * it ever elapses while playing, `play:idle` fires with `{ idle_ms }` and the\n * countdown re-arms, so a very long passive listen yields one event per window.\n * Pausing or ending playback (also the bookend of any interaction) stops the\n * countdown — idle only counts time the track is actually advancing.\n *\n * A loop (repeat-one, a queued ×N) is not an interaction: the queue restarts\n * the track by itself, with a seek to the top and a fresh play. The countdown\n * carries across the loop from where it was, so a fan who loops for an hour\n * without touching anything reads as idle like any other walk-away listen.\n *\n * `idleMs` defaults to 5 minutes; the threshold is a starting guess and is\n * overridable so it can be tuned against real-world data (and driven fast in\n * tests).\n */\nexport class PlayIdleCollector extends BaseCollector {\n readonly name = 'PlayIdleCollector'\n private readonly idleMs: number\n private playing = false\n private timer: ReturnType | null = null\n /** When the countdown last started from zero (the last real interaction). */\n private idleSince = 0\n /** A same-track auto-advance just looped the track; its play and seek aren't the listener's. */\n private loopPlayPending = false\n private loopSeekPending = false\n\n constructor(options: { idleMs?: number } = {}) {\n super()\n this.idleMs = options.idleMs ?? 5 * 60_000\n }\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:play', () => {\n this.playing = true\n if (this.loopPlayPending) {\n this.loopPlayPending = false\n this.resume()\n } else {\n this.arm()\n }\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:pause', () => {\n this.playing = false\n this.clear()\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:ended', () => {\n this.playing = false\n this.clear()\n }),\n )\n // Any user interaction during playback restarts the idle countdown.\n const resetIfPlaying = () => {\n if (this.playing) this.arm()\n }\n this.subscriptions.push(\n this.ctx.bus.on('engine:seeked', () => {\n if (this.loopSeekPending) {\n this.loopSeekPending = false\n return\n }\n resetIfPlaying()\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('queue:track_change', (e) => {\n if (e.reason === 'auto' && e.from !== null && e.from.trackUuid === e.to.trackUuid) {\n this.loopPlayPending = true\n this.loopSeekPending = true\n }\n }),\n )\n this.subscriptions.push(this.ctx.bus.on('engine:volumechange', resetIfPlaying))\n this.subscriptions.push(this.ctx.bus.on('engine:ratechange', resetIfPlaying))\n this.subscriptions.push(this.ctx.bus.on('visibility:change', resetIfPlaying))\n }\n\n /** Start the countdown from zero: the listener just did something. */\n private arm(): void {\n this.idleSince = Date.now()\n this.schedule(this.idleMs)\n }\n\n /** Carry on the countdown already running (a loop restart). */\n private resume(): void {\n this.schedule(Math.max(0, this.idleMs - (Date.now() - this.idleSince)))\n }\n\n private schedule(ms: number): void {\n this.clear()\n this.timer = setTimeout(() => {\n this.emit('play:idle', { idle_ms: this.idleMs })\n // Re-arm: still playing, still no interaction → keep sampling.\n this.arm()\n }, ms)\n }\n\n private clear(): void {\n if (this.timer !== null) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n\n protected override onDetach(): void {\n this.clear()\n }\n\n protected override onTrackReset(): void {\n this.playing = false\n this.clear()\n }\n}\n","import { BaseCollector } from '../BaseCollector'\nimport type { WireEvent } from '../../wire'\n\n/**\n * Emits `play:pause` on user pause. Browsers also fire `pause` immediately\n * before `ended` and during some buffer stalls — we ignore pauses fired\n * within 50ms of an `ended` event to avoid double-recording the natural end.\n */\nexport class PlayPauseCollector extends BaseCollector {\n readonly name = 'PlayPauseCollector'\n private endedRecently = false\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:ended', () => {\n this.endedRecently = true\n setTimeout(() => {\n this.endedRecently = false\n }, 50)\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:pause', (e) => {\n if (this.endedRecently) return\n this.emit('play:pause', { current_time: e.currentTime })\n }),\n )\n }\n\n /**\n * The \"left the page\" pause: the listener closed or left the page while\n * the track was still playing. A `play:pause` with `reason: 'left_page'`\n * at the current position, so the heatmap can show where people stopped\n * mid-song, apart from those who paused and carried on. A pause rather\n * than a new event name, so an ingestion that predates it still accepts\n * it. Null when nothing is playing (a paused or finished track already\n * said how it ended).\n */\n buildLeave(): WireEvent | null {\n const audio = this.ctx?.audio\n if (!audio || audio.paused || audio.ended || !this.ctx.getCurrentTrack()) return null\n return this.build('play:pause', { current_time: audio.currentTime, reason: 'left_page' })\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:progress` every `intervalSeconds` while a track is playing.\n *\n * Drives off the engine's 1 Hz `engine:timeupdate` (ticking whether the tab\n * is visible or not — see AudioEngine; each event marks `background`). The collector\n * additionally enforces a coarser interval so the wire isn't flooded.\n *\n * Default 5s: `play:progress` is the dwell/\"linger\" signal behind the\n * per-position engagement heatmap (each event carries `current_time`,\n * persisted as a position row in ClickHouse — see\n * shared/schemas/play_position_event.schema.json). The heatmap buckets\n * positions at 5s by default, so emitting at 5s gives ~one dwell sample\n * per bin a listener passes through. Lower it for a finer heatmap at the\n * cost of more wire volume; raise it to spend less. The discrete jump\n * signals (seek/skip/replay) are unaffected — they emit at full fidelity.\n */\nexport class PlayProgressCollector extends BaseCollector {\n readonly name = 'PlayProgressCollector'\n private lastEmitAt = 0\n private readonly intervalMs: number\n\n constructor(options: { intervalSeconds?: number } = {}) {\n super()\n this.intervalMs = (options.intervalSeconds ?? 5) * 1000\n }\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:timeupdate', (e) => {\n // Muted (or volume 0) is not being heard: no seconds toward\n // completion and no progress row (so no dwell, no listening time, no\n // \"listening now\"). The playhead still moves, so the next audible\n // tick measures from here, not from before the mute.\n const audio = this.ctx.audio\n if (audio.muted || audio.volume === 0) {\n this.ctx.listen.silent(e.currentTime)\n return\n }\n this.ctx.listen.advance(e.currentTime)\n if (e.at - this.lastEmitAt < this.intervalMs) return\n this.lastEmitAt = e.at\n this.emit('play:progress', { current_time: e.currentTime })\n }),\n )\n }\n\n protected override onTrackReset(): void {\n this.lastEmitAt = 0\n }\n}\n","/**\n * Shared seek-classification thresholds for the seek/skip/replay collectors.\n *\n * The \"skip suppresses seek\" coordination — PlaySeekCollector stays silent\n * when a seek matches PlaySkipCollector's skip rule, and likewise for replay —\n * only works if every collector evaluates the *identical* numbers. These used\n * to be duplicated as local constants in each collector, with nothing forcing\n * the copies to agree; defining them once here removes that drift hazard.\n *\n * Fractions are of the track's duration. The grace is in milliseconds.\n */\n\n/**\n * A forward jump larger than this fraction of duration (and at least\n * SKIP_MIN_SECONDS) counts as a skip. 10%, not the old 30%: jumping from\n * 0:15 to a hook at 1:00 in a 3:30 song is a skip of the intro, and at 30%\n * it was recorded as an ordinary seek.\n */\nexport const SKIP_THRESHOLD = 0.1\n/** ...and never a jump shorter than this many seconds (a nudge, not a skip). */\nexport const SKIP_MIN_SECONDS = 10\n/** ...but only when the jump originates below this fraction (not end-scrubbing). */\nexport const SKIP_TAIL_EXCLUDE = 0.85\n/** A seek landing below this fraction of duration is a candidate replay. */\nexport const REPLAY_TARGET = 0.05\n/** ...when it originates above this fraction of duration. */\nexport const REPLAY_SOURCE = 0.3\n/**\n * A backward jump of at least this many seconds that isn't a restart is a\n * candidate section replay (going back to hear the hook again).\n */\nexport const SECTION_REPLAY_MIN_BACK_SECONDS = 3\n/**\n * ...confirmed once the listener has played this many seconds on from where\n * they landed. Scrubbing back and forth never gets there, so it stays a seek.\n */\nexport const SECTION_REPLAY_CONFIRM_SECONDS = 5\n/**\n * Seeks arriving within this many ms of a `play` start are treated as\n * programmatic (the browser snapping currentTime to a valid frame), not user\n * intent, and are ignored.\n */\nexport const PROGRAMMATIC_SEEK_GRACE_MS = 250\n\n/** A forward jump over the intro/verse: big enough, and not wrapping up the end. */\nexport function isSkip(from: number, to: number, duration: number | null): boolean {\n if (duration === null) return false\n const jump = to - from\n return (\n jump > 0 &&\n jump > Math.max(SKIP_THRESHOLD * duration, SKIP_MIN_SECONDS) &&\n from < SKIP_TAIL_EXCLUDE * duration\n )\n}\n\n/** Dragged back to the top from well into the track: a hand restart. */\nexport function isRestart(from: number, to: number, duration: number | null): boolean {\n return duration !== null && to < REPLAY_TARGET * duration && from > REPLAY_SOURCE * duration\n}\n\n/** Went back to an earlier part that isn't the start (the hook, a verse). */\nexport function isSectionReplayCandidate(\n from: number,\n to: number,\n duration: number | null,\n): boolean {\n return from - to >= SECTION_REPLAY_MIN_BACK_SECONDS && !isRestart(from, to, duration)\n}\n","import { BaseCollector } from '../BaseCollector'\nimport { REPLAY_SOURCE, REPLAY_TARGET } from '../thresholds'\n\n/**\n * Emits `play:replay` for the three re-listen gestures reachable today:\n *\n * - Replay after the end (`replay_after_end`): `engine:ended` → `engine:play`\n * on the SAME track with no queue move in between, at ANY gap. With\n * nothing queued after it the player stops at the end (`queue:complete`)\n * and never restarts a track by itself, so this play is always the\n * listener pressing play again — a deliberate whole-track replay. It used\n * to count only within 2 s (as `auto_or_quick_retap`), so a fan who\n * finished the song and played it again a moment later emitted no replay\n * and no `play:start` at all: the listen was invisible. If the queue moves\n * between `ended` and `play` (`resetForNewTrack()` fires), the ended flag is\n * cleared so a *different* track's first play is never a false replay.\n *\n * - Manual restart (`manual_restart`): `engine:seeked` with\n * `to < REPLAY_TARGET×duration` from `from > REPLAY_SOURCE×duration`.\n * PlaySeekCollector suppresses its own emission when this shape is\n * detected (same threshold constants), so the gesture produces exactly one\n * `play:replay`, not a dropped or doubled event.\n *\n * - Loop (`auto_or_quick_retap`, the stored value kept for old rows): a\n * same-track auto-advance — `queue:track_change` with `reason === 'auto'`\n * and `from.trackUuid === to.trackUuid`, emitted by QueueManager's\n * repeat-one mode or a queued ×N repeat when a track ends. Detected off the\n * track-change event directly (NOT via the ended flag), so it's immune to the\n * reset-ordering on track change. The reset also makes the loop emit a\n * fresh `play:start`, which is why reads never count this trigger as a\n * play of its own.\n *\n * Still not covered: native `audio.loop`, which restarts a track without firing\n * `engine:ended` or any queue change. The player loops via QueueManager repeat,\n * not the native flag, so this is a documented non-path rather than a gap.\n */\nexport class PlayReplayCollector extends BaseCollector {\n readonly name = 'PlayReplayCollector'\n\n // Whether the current track has ended and not been played since.\n private ended = false\n // A loop (repeat-one / ×N) restarted the track: the next engine:play is the\n // queue's restart, already recorded as a loop — not the listener's replay.\n // Needed because the player's own engine:ended handler (registered first)\n // runs the auto-advance, and so this track's reset, BEFORE this collector\n // sees `ended`; the reset alone can't clear a flag that is set after it.\n private loopRestartPending = false\n // After a track ends, the next jump from the end back to the top is not a\n // hand restart: it is the player looping (the bar seeks to 0:00 and plays)\n // or the browser itself rewinding an ended track when play is pressed again.\n // Either way the new listen is already recorded — by the loop's own\n // `play:start`, or by `replay_after_end` — so without this flag each one\n // also emitted a `manual_restart`: a second listen start milliseconds\n // later, a hand replay in the artist's numbers, and — when a retry put\n // several loops in one batch — a burst ingestion's rapid-replay check\n // flags. Cleared by the first seeked after the end, or once the pass plays\n // on past REPLAY_SOURCE (a surface that restarts without a seek must not\n // swallow a later real restart).\n private restartSeekPending = false\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:ended', () => {\n this.ended = true\n this.restartSeekPending = true\n }),\n )\n\n this.subscriptions.push(\n this.ctx.bus.on('engine:play', () => {\n const loop = this.loopRestartPending\n this.loopRestartPending = false\n if (this.ended && !loop) {\n // A hand replay begins a new listen (own played_at, own seconds),\n // so its completion and drop-off are its own. Begun BEFORE the\n // emit, so the replay row carries the new listen's start.\n this.ctx.listen.begin()\n this.emit('play:replay', { trigger: 'replay_after_end' })\n }\n this.ended = false\n }),\n )\n\n this.subscriptions.push(\n this.ctx.bus.on('engine:seeked', (e) => {\n const duration = this.ctx.audio.duration\n const dur = Number.isFinite(duration) && duration > 0 ? duration : null\n if (this.restartSeekPending) {\n this.restartSeekPending = false\n // The rewind after the end: this listen is already recorded.\n if (dur === null || e.to < REPLAY_TARGET * dur) return\n }\n if (dur !== null && e.to < REPLAY_TARGET * dur && e.from > REPLAY_SOURCE * dur) {\n this.ctx.listen.begin()\n this.emit('play:replay', { trigger: 'manual_restart' })\n }\n }),\n )\n\n this.subscriptions.push(\n this.ctx.bus.on('engine:timeupdate', (e) => {\n if (!this.restartSeekPending) return\n const duration = this.ctx.audio.duration\n if (Number.isFinite(duration) && duration > 0 && e.currentTime > REPLAY_SOURCE * duration) {\n this.restartSeekPending = false\n }\n }),\n )\n\n this.subscriptions.push(\n this.ctx.bus.on('queue:track_change', (e) => {\n // A same-track auto-advance is QueueManager repeat-one (the repeat\n // button) or a queued ×N repeat looping the track. The\n // `reason === 'auto'` guard keeps a deliberate re-jump to the current\n // track (reason 'jump') out of the replay signal.\n if (e.reason === 'auto' && e.from !== null && e.from.trackUuid === e.to.trackUuid) {\n this.loopRestartPending = true\n this.restartSeekPending = true\n this.emit('play:replay', { trigger: 'auto_or_quick_retap' })\n }\n }),\n )\n }\n\n // Clears only `ended`: the loop flags are set by the same track change that\n // triggers this reset, and must survive it until the restart's play/seek.\n protected override onTrackReset(): void {\n this.ended = false\n }\n}\n","import type { PlayerEvents } from '@delistening/player'\nimport { BaseCollector } from '../BaseCollector'\nimport {\n PROGRAMMATIC_SEEK_GRACE_MS,\n SECTION_REPLAY_CONFIRM_SECONDS,\n isRestart,\n isSectionReplayCandidate,\n isSkip,\n} from '../thresholds'\n\n/**\n * Emits `play:seek` for ordinary user seeks that aren't classified as skip\n * (forward, large) or replay (to-zero from non-start), and `play:replay`\n * with trigger `section_replay` for going back to hear a part again.\n *\n * Plan §5 items 3, 8, 9:\n * - Item 3: ignore `seeked` within 0.25s of the most recent `play` start.\n * - Item 8: skip threshold lives in PlaySkipCollector (this collector\n * suppresses its own emission when the seek matches skip).\n * - Item 9: this collector suppresses when the seek matches replay\n * (\"manual restart\").\n *\n * Each collector evaluates the same rules from `../thresholds`\n * independently and the dominant one (skip > replay > seek) wins, so a\n * gesture is never dropped or doubled.\n *\n * Section replay: a backward jump that isn't a restart (back to the hook,\n * not the top) is held until the listener has played\n * SECTION_REPLAY_CONFIRM_SECONDS on from where they landed — then it is a\n * `play:replay { trigger: 'section_replay', from, to }`, positioned where\n * they went back to, so \"Most replayed\" can name the hook. If they jump\n * again first (scrubbing), the track changes, or it ends, the held jump is\n * sent as the ordinary `play:seek` it was. A section replay is part of the\n * listen in progress: it never starts a new listen or counts as one.\n */\nexport class PlaySeekCollector extends BaseCollector {\n readonly name = 'PlaySeekCollector'\n private lastPlayStartAt = -Number.POSITIVE_INFINITY\n /** A backward jump waiting to be confirmed as a section replay. */\n private pending: { from: number; to: number } | null = null\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:play', (e) => {\n this.lastPlayStartAt = e.at\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:seeked', (e) => {\n // Programmatic-seek grace: ignore seeks that arrive in the first\n // 250ms after `play` — those are typically the browser snapping\n // currentTime to a valid frame.\n if (e.at - this.lastPlayStartAt < PROGRAMMATIC_SEEK_GRACE_MS) {\n return\n }\n this.flushPending()\n const duration = this.ctx.audio.duration\n const dur = Number.isFinite(duration) && duration > 0 ? duration : null\n const { from, to } = e\n\n // Skip → PlaySkipCollector; restart → PlayReplayCollector.\n if (isSkip(from, to, dur) || isRestart(from, to, dur)) return\n\n if (isSectionReplayCandidate(from, to, dur)) {\n this.pending = { from, to }\n return\n }\n\n this.emit('play:seek', { from, to })\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:timeupdate', (e) => {\n const pending = this.pending\n if (pending === null || e.currentTime < pending.to + SECTION_REPLAY_CONFIRM_SECONDS) return\n this.pending = null\n this.emit('play:replay', { trigger: 'section_replay', from: pending.from, to: pending.to })\n }),\n )\n this.subscriptions.push(this.ctx.bus.on('engine:ended', () => this.flushPending()))\n }\n\n // While the finished track is still current, so the held seek is its own.\n override beforeTrackChange(_change: PlayerEvents['queue:track_change']): void {\n this.flushPending()\n }\n\n protected override onTrackReset(): void {\n this.lastPlayStartAt = -Number.POSITIVE_INFINITY\n this.pending = null\n }\n\n private flushPending(): void {\n const pending = this.pending\n if (pending === null) return\n this.pending = null\n this.emit('play:seek', pending)\n }\n}\n","import { BaseCollector } from '../BaseCollector'\nimport { PROGRAMMATIC_SEEK_GRACE_MS, isSkip } from '../thresholds'\n\n/**\n * Emits `play:skip` when a forward seek jumps past\n * `max(SKIP_THRESHOLD × duration, SKIP_MIN_SECONDS)`, but only if the source\n * position is below `SKIP_TAIL_EXCLUDE × duration` — so scrubbing through\n * the end-of-track to wrap up doesn't trigger a skip event.\n *\n * Plan §5 item 8. The rule lives in `../thresholds` (`isSkip`), shared with\n * PlaySeekCollector so the \"skip suppresses seek\" rule stays consistent\n * across both collectors.\n */\nexport class PlaySkipCollector extends BaseCollector {\n readonly name = 'PlaySkipCollector'\n private lastPlayStartAt = -Number.POSITIVE_INFINITY\n private readonly graceMs: number\n\n constructor(options: { graceMs?: number } = {}) {\n super()\n this.graceMs = options.graceMs ?? PROGRAMMATIC_SEEK_GRACE_MS\n }\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:play', (e) => {\n this.lastPlayStartAt = e.at\n }),\n )\n this.subscriptions.push(\n this.ctx.bus.on('engine:seeked', (e) => {\n if (e.at - this.lastPlayStartAt < this.graceMs) return\n const duration = this.ctx.audio.duration\n const dur = Number.isFinite(duration) && duration > 0 ? duration : null\n if (isSkip(e.from, e.to, dur)) {\n this.emit('play:skip', { from: e.from, to: e.to, jump_size: e.to - e.from })\n }\n }),\n )\n }\n\n protected override onTrackReset(): void {\n this.lastPlayStartAt = -Number.POSITIVE_INFINITY\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:start` the first time a track transitions to playing.\n *\n * Why \"first time\": browsers fire `play` repeatedly (after seek, after buffer\n * stall recovery). The MVP collector treats `play` after any of those as a\n * continuation, not a new start. We do the same here.\n */\nexport class PlayStartCollector extends BaseCollector {\n readonly name = 'PlayStartCollector'\n private emitted = false\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:play', () => {\n if (this.emitted) return\n this.emitted = true\n this.emit('play:start')\n }),\n )\n }\n\n protected override onTrackReset(): void {\n this.emitted = false\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:visibility` when the tab visibility changes. Useful fraud-\n * detection signal: bots typically don't switch tabs while \"listening\".\n *\n * The 500ms debounce lives in PlayerController so a quick alt-tab cycle\n * collapses to a single emission. This collector just forwards.\n */\nexport class PlayVisibilityCollector extends BaseCollector {\n readonly name = 'PlayVisibilityCollector'\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('visibility:change', (e) => {\n this.emit('play:visibility', { hidden: e.hidden })\n }),\n )\n }\n}\n","import { BaseCollector } from '../BaseCollector'\n\n/**\n * Emits `play:volume` on every volume / mute change, carrying\n * `{ volume_from, volume_to, muted }`. Useful human-behaviour signal for fraud\n * detection: bots rarely twiddle volume mid-track.\n *\n * `engine:volumechange` only reports the *new* `{ volume, muted }`, so this\n * collector remembers the last level itself to fill `volume_from`. The first\n * change after attach uses the audio element's current volume as the baseline\n * (defaulting to 1.0 — the HTML media default — when the element doesn't\n * expose it, e.g. under test stubs). Volume is a player-global setting, not a\n * per-track one, so the remembered level deliberately survives track changes.\n */\nexport class PlayVolumeCollector extends BaseCollector {\n readonly name = 'PlayVolumeCollector'\n private lastVolume: number | null = null\n\n protected onAttach(): void {\n this.subscriptions.push(\n this.ctx.bus.on('engine:volumechange', (e) => {\n const from = this.lastVolume ?? this.initialVolume()\n this.lastVolume = e.volume\n this.emit('play:volume', { volume_from: from, volume_to: e.volume, muted: e.muted })\n }),\n )\n }\n\n private initialVolume(): number {\n const v = (this.ctx.audio as HTMLAudioElement).volume\n return typeof v === 'number' && Number.isFinite(v) ? v : 1\n }\n}\n","import type { EventBus, PlayerEvents, SessionManager, TrackRef } from '@delistening/player'\nimport { EventDispatcher } from './EventDispatcher'\nimport type { OfflineStore } from './OfflineStore'\nimport type { BaseCollector, CollectorMetadata } from './collectors/BaseCollector'\nimport { ListenState } from './collectors/ListenState'\nimport { SessionCompleteCollector } from './collectors/session/complete'\nimport { SessionTrackChangeCollector } from './collectors/session/track_change'\nimport { PlayBufferCollector } from './collectors/track/play_buffer'\nimport { PlayCompleteCollector } from './collectors/track/play_complete'\nimport { PlayIdleCollector } from './collectors/track/play_idle'\nimport { PlayPauseCollector } from './collectors/track/play_pause'\nimport { PlayProgressCollector } from './collectors/track/play_progress'\nimport { PlayReplayCollector } from './collectors/track/play_replay'\nimport { PlaySeekCollector } from './collectors/track/play_seek'\nimport { PlaySkipCollector } from './collectors/track/play_skip'\nimport { PlayStartCollector } from './collectors/track/play_start'\nimport { PlayVisibilityCollector } from './collectors/track/play_visibility'\nimport { PlayVolumeCollector } from './collectors/track/play_volume'\nimport { cryptoRandomUuid } from './internal/uuid'\nimport type { EventName, WireEvent } from './wire'\nimport { WIRE_SCHEMA_VERSION, detectDeviceType } from './wire'\n\nexport interface CreateTelemetryOptions {\n /** Ingestion endpoint. POSTs JSON `{ events: WireEvent[] }`. */\n endpoint: string\n /** Player's EventBus — every collector subscribes through this. */\n bus: EventBus\n /** Player's SessionManager — provides session_id and listens for reset. */\n session: SessionManager\n /** Optional auth token for Authorization: Bearer . */\n authToken?: string\n /** Fetch a fresh bearer when ingestion answers 401 (see EventDispatcher). */\n refreshAuthToken?: () => Promise\n /** Override the offline store (tests). */\n offline?: OfflineStore\n /** Override fetch (tests). */\n fetchImpl?: typeof fetch\n /** Flush interval (ms). Default: 5000. */\n flushIntervalMs?: number\n /** Batch size per POST. Default: 50. */\n batchSize?: number\n /** Attach metadata sent on every event. Listener UUID, UTM, share token, etc. */\n metadata?: Partial\n}\n\n/**\n * Orchestrator: composes the dispatcher, the offline store, the collector\n * registry, and the current-track state. Consumer code talks to the\n * `Telemetry` returned from `createTelemetry()`, not to this class directly.\n */\nexport class TelemetryManager {\n private readonly collectors: BaseCollector[] = []\n private readonly dispatcher: EventDispatcher\n private readonly bus: EventBus\n private readonly session: SessionManager\n private audio: HTMLAudioElement | null = null\n private currentTrack: TrackRef | null = null\n private metadata: CollectorMetadata\n private attached = false\n private queueUnsubscribe: (() => void) | null = null\n private readonly listen = new ListenState()\n private exitHookRegistered = false\n\n constructor(private readonly options: CreateTelemetryOptions) {\n this.bus = options.bus\n this.session = options.session\n this.metadata = {\n listenerUuid: options.metadata?.listenerUuid ?? null,\n ...(options.metadata?.referrerDomain !== undefined\n ? { referrerDomain: options.metadata.referrerDomain }\n : {}),\n ...(options.metadata?.utmSource !== undefined\n ? { utmSource: options.metadata.utmSource }\n : {}),\n ...(options.metadata?.utmCampaign !== undefined\n ? { utmCampaign: options.metadata.utmCampaign }\n : {}),\n ...(options.metadata?.utmMedium !== undefined\n ? { utmMedium: options.metadata.utmMedium }\n : {}),\n ...(options.metadata?.shareToken !== undefined\n ? { shareToken: options.metadata.shareToken }\n : {}),\n ...(options.metadata?.shareSource !== undefined\n ? { shareSource: options.metadata.shareSource }\n : {}),\n ...(options.metadata?.embedOrigin !== undefined\n ? { embedOrigin: options.metadata.embedOrigin }\n : {}),\n }\n this.dispatcher = new EventDispatcher({\n endpoint: options.endpoint,\n ...(options.offline ? { offline: options.offline } : {}),\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.flushIntervalMs !== undefined\n ? { flushIntervalMs: options.flushIntervalMs }\n : {}),\n ...(options.batchSize !== undefined ? { batchSize: options.batchSize } : {}),\n ...(options.authToken !== undefined ? { authToken: options.authToken } : {}),\n ...(options.refreshAuthToken !== undefined\n ? { refreshAuthToken: options.refreshAuthToken }\n : {}),\n })\n }\n\n /** Register the default collector set. */\n registerDefaults(): void {\n this.register(new PlayStartCollector())\n this.register(new PlayProgressCollector())\n this.register(new PlayPauseCollector())\n this.register(new PlayCompleteCollector())\n this.register(new PlaySeekCollector())\n this.register(new PlaySkipCollector())\n this.register(new PlayVisibilityCollector())\n this.register(new PlayReplayCollector())\n this.register(new PlayVolumeCollector())\n this.register(new PlayIdleCollector())\n this.register(new PlayBufferCollector())\n this.register(new SessionTrackChangeCollector())\n this.register(new SessionCompleteCollector())\n }\n\n /** Register one collector. Order is insignificant. */\n register(collector: BaseCollector): void {\n this.collectors.push(collector)\n }\n\n /**\n * Attach to an audio element + initial track. Subsequent track changes are\n * tracked via `queue:track_change`; callers don't need to re-attach.\n */\n attach(audio: HTMLAudioElement, trackUuid: string, artistUuid?: string): void {\n if (this.attached) {\n throw new Error('TelemetryManager: already attached. Call detach() first.')\n }\n this.attached = true\n this.audio = audio\n this.currentTrack = {\n trackUuid,\n artistUuid: artistUuid ?? '',\n ...(audio.duration > 0 ? { durationSeconds: audio.duration } : {}),\n }\n const ctx = {\n bus: this.bus,\n session: this.session,\n audio,\n getCurrentTrack: () => this.currentTrack,\n emit: (event: WireEvent) => {\n void this.dispatcher.enqueue(event)\n },\n listen: this.listen,\n }\n for (const c of this.collectors) c.attach(ctx, this.metadata)\n // Left the page mid-play: queue the exit pause before the final flush.\n if (!this.exitHookRegistered) {\n this.exitHookRegistered = true\n this.dispatcher.onExit(async () => {\n if (!this.attached) return\n const pause = this.collectors.find((c): c is PlayPauseCollector => c instanceof PlayPauseCollector)\n const leave = pause?.buildLeave() ?? null\n if (leave) await this.dispatcher.enqueue(leave)\n })\n }\n // Keep currentTrack in sync as the queue moves.\n this.queueUnsubscribe = this.bus.on('queue:track_change', (e) => {\n for (const c of this.collectors) c.beforeTrackChange(e)\n this.currentTrack = e.to\n for (const c of this.collectors) c.resetForNewTrack()\n })\n this.dispatcher.start()\n }\n\n /** Detach collectors and stop dispatching. The offline store retains queued events. */\n detach(): void {\n if (!this.attached) return\n this.attached = false\n this.queueUnsubscribe?.()\n this.queueUnsubscribe = null\n for (const c of this.collectors) c.detach()\n this.listen.reset()\n this.dispatcher.stop()\n this.audio = null\n this.currentTrack = null\n }\n\n /**\n * Emit an app-level event that is NOT derived from an engine signal — e.g. a\n * structure-strip section navigation (`nav:section`). The auto-collectors\n * only translate `engine:*` events; a deliberate UI intent like \"jump to the\n * drop\" has no engine origin, so the app emits it here directly.\n *\n * `trackRef` names the track the event is about. For a section jump that\n * STARTS a not-yet-current track, the clicked track's ref is passed\n * explicitly (the manager's `currentTrack` still points at the prior track\n * until `queue:track_change` lands). Falls back to the current track when\n * omitted. No-ops if detached or no track is resolvable.\n *\n * These events aren't tied to a play-start, so `played_at` is stamped now\n * (satisfying played_at <= client_emitted_at) and the playback-accounting\n * fields are the unknown sentinels (duration_played 0, completion_rate -1):\n * the meaningful payload is in `eventData`.\n */\n track(\n name: EventName,\n eventData: Record = {},\n trackRef?: { trackUuid: string; artistUuid?: string },\n ): void {\n if (!this.attached) return\n const track = trackRef ?? this.currentTrack\n if (!track || !track.trackUuid) return\n\n const now = new Date().toISOString()\n const event: WireEvent = {\n schema_version: WIRE_SCHEMA_VERSION,\n event_id: cryptoRandomUuid(),\n event_name: name,\n track_uuid: track.trackUuid,\n ...(track.artistUuid ? { artist_uuid: track.artistUuid } : {}),\n session_id: this.session.getSessionId(),\n listener_uuid: this.metadata.listenerUuid,\n played_at: now,\n client_emitted_at: now,\n duration_played: 0,\n completion_rate: -1,\n device_type: detectDeviceType(),\n ...(this.metadata.referrerDomain ? { referrer_domain: this.metadata.referrerDomain } : {}),\n ...(this.metadata.utmSource ? { utm_source: this.metadata.utmSource } : {}),\n ...(this.metadata.utmCampaign ? { utm_campaign: this.metadata.utmCampaign } : {}),\n ...(this.metadata.utmMedium ? { utm_medium: this.metadata.utmMedium } : {}),\n ...(this.metadata.shareToken ? { share_token: this.metadata.shareToken } : {}),\n ...(this.metadata.shareSource ? { share_source: this.metadata.shareSource } : {}),\n ...(this.metadata.embedOrigin ? { embed_origin: this.metadata.embedOrigin } : {}),\n event_data: eventData,\n }\n void this.dispatcher.enqueue(event)\n }\n\n /** Replace the ingestion bearer mid-session (the token lives an hour). */\n setAuthToken(token: string | undefined): void {\n this.dispatcher.setAuthToken(token)\n }\n\n /** Force a flush. Returns the count of events sent. */\n async flush(): Promise {\n return this.dispatcher.flush()\n }\n\n /** Update metadata mid-session (e.g. when the listener signs in). */\n updateMetadata(patch: Partial): void {\n // Mutate the existing object in place rather than reassigning. Attached\n // collectors hold a reference to this exact object (handed to them at\n // attach() time via the attach contract) and read it fresh on each emit.\n // A reassignment (`this.metadata = {...}`) would leave them pointing at\n // the stale object, so mid-session updates — e.g. setting listenerUuid\n // after login — would silently never reach any running collector.\n Object.assign(this.metadata, patch)\n }\n}\n\n/**\n * Convenience factory matching the ARCHITECTURE.md example shape:\n *\n * const telemetry = createTelemetry({ endpoint, bus, session })\n * telemetry.registerDefaults()\n * telemetry.attach(audioElement, trackId)\n */\nexport function createTelemetry(options: CreateTelemetryOptions): TelemetryManager {\n return new TelemetryManager(options)\n}\n"]}