# Tracker

> Release history for @arraypress/waveform-tracker.

<!-- GENERATED by scripts/sync-changelogs.mjs — do not edit by hand. -->

:::note
Generated from [`@arraypress/waveform-tracker`'s CHANGELOG](https://github.com/arraypress/waveform-tracker/blob/main/CHANGELOG.md). Run `npm run sync:changelogs` after a release to refresh.
:::

## [Unreleased]

## [1.1.0] — 2026-09-24

### Fixed

- **Listening in a background tab is counted.** Browsers throttle hidden tabs,
  so the player's timeupdates can stall and then report minutes of playback as
  one jump — which the tracker discarded as a seek. A forward jump over 5s is
  now credited when it fits the real time that passed (at the current playback
  rate, with 50% headroom plus `SEEK_SLACK`, 1s), and only treated as a seek
  when it clearly exceeds it. The stretch between the last timeupdate and
  `ended` is credited the same way. Small jumps behave as before.
- **Live streams send `duration: 0` instead of `null`.** A stream's duration
  is `Infinity`, which JSON serialises as `null`; payloads now always carry a
  number, with `0` meaning unknown. `complete` is explicitly skipped when the
  duration isn't finite (it could never fire, but only by accident).
- **`reset()` fully detaches the tracker.** It cleared the config but left the
  document `ready`/`destroy` listeners attached, so a player created afterwards
  was still tracked and its first timeupdate threw (`this.config` was `null`).
  `reset()` now removes those listeners, and `trackPlayer()` without a config
  (before `init()` or after `reset()`) warns and does nothing.
- **Thresholds crossed in the last second before `ended`, `pause` or destroy
  are no longer lost.** The checks run at most once a second, and `ended`
  only looked at `complete` before resetting, so a `play` or `listen`
  threshold crossed inside that window was dropped. `ended`, `pause` and
  `untrackPlayer()` (which `waveformplayer:destroy` calls) now run every check
  unthrottled first.

### Changed

- **`complete` needs real listening, not just the playhead position.** It
  fired as soon as the position reached the threshold, so scrubbing to 95% (or
  to the end, via `ended`) counted as a completion with a second heard. It now
  also requires the listener to have heard half the audio up to the threshold
  (`COMPLETE_ENGAGEMENT`, 0.5), in media time: with `complete: 90` on a
  4-minute track, 108s. Both the timeupdate and `ended` paths apply it.
  **Expect fewer `complete` events**: skip-to-the-end sessions no longer
  count.
- **A threshold of `0` fires instead of disabling the event.** `events.play: 0`
  (or `listen: 0`, `complete: 0`) was falsy and silently turned the event off;
  it now fires on the first check after playback starts. Omitting the key,
  `null` or `false` still disables an event.
- **`init()` can be called more than once.** Each call added another pair of
  document listeners, so every player was handled twice per extra call. A
  second `init()` now reconfigures the tracker in place: the new config applies
  to players already tracked, and the session id is kept, since it's the same
  page session. After `reset()`, `init()` starts afresh with a new id.
- **Cross-origin endpoints receive `listen` and `complete` via `fetch`
  `keepalive` instead of `sendBeacon`.** A beacon's `application/json` body
  isn't CORS-safelisted and beacons always send credentials, so an endpoint on
  another origin answering `Access-Control-Allow-Origin: *` never got those
  events — and `sendBeacon` still reported success, so nothing fell back.
  Terminal events to another origin now use `fetch` with `keepalive: true`
  (still `Content-Type: application/json`); the endpoint must answer the CORS
  preflight for `Content-Type`. Same-origin endpoints keep using `sendBeacon`
  (unless custom `headers` are set), and it remains the fallback when `fetch`
  is unavailable.
- **The package declares `"type": "module"` and an `exports` map.** `import`
  resolves to the ESM build and `require` to the new CJS build;
  `./dist/*`, `./src/*` and `./package.json` stay reachable. Paths outside
  those are no longer importable.

### Added

- **A CommonJS build, `dist/waveform-tracker.cjs`, now the `main` entry.**
  `main` pointed at the IIFE bundle, so `require('@arraypress/waveform-tracker')`
  returned `{}`. It now returns `{ default: tracker }`, matching
  `@arraypress/waveform-player`'s CJS build.
- **`prepublishOnly` runs the tests and a fresh build**, so `npm publish` can't
  ship a stale `dist/`.

## [1.0.2] — 2026-09-24

### Fixed

- **Per-track state resets when the player's URL changes.** A player outlives
  many tracks, and `load()`/`loadTrack()` swap the URL mid-play without an
  `ended` in between, so the new track inherited the previous one's elapsed
  time (firing `listen` early) and its sent events (never firing `play` or
  `listen` at all). Direct `load(url)` swaps also need
  `@arraypress/waveform-player` 1.27.1, where `load()` records the new URL.
- **Destroyed players are released.** The tracker listens for
  `waveformplayer:destroy` and untracks the player, so SPAs that create and
  destroy players no longer leak them.
- **`complete` on `ended` works in external audio mode**, reading the final
  time from the event detail instead of `player.audio`.

### Changed

- **Engagement is counted in media time**, from `currentTime` deltas, so
  1.5x/2x playback is credited for the content actually heard. Forward jumps
  over 5s are treated as seeks and not credited.
- **`listen` and `complete` are delivered with `sendBeacon`** (falling back to
  `fetch` with `keepalive`) so they survive page navigation.
- **Configuration problems and delivery failures always log**, via
  `console.warn`/`console.error` with a `[WaveformTracker]` prefix; tracing
  stays behind `debug`.
- Peer dependency raised to `@arraypress/waveform-player` `^1.8.0`.

## [1.0.1] — 2026-06-27

### Changed

- Peer dependency raised to `@arraypress/waveform-player` `^1.7.2`. No source
  changes.

## [1.0.0] — 2025-09-16

- Initial release: `play`, `listen` and `complete` events for every
  WaveformPlayer on the page, delivered to an endpoint or a custom handler.
