# Changelog

All notable changes to `@groundcover/browser` are documented here. This project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) and the
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.

## [1.0.9]

Stable release of `1.0.9-rc.0`. The shipped `dist/` is byte-for-byte the RC's — nothing landed after
it, in the browser package or in the shared core it bundles; the version field and this entry are the
only differences in the package.

A wire-level ordering fix — sessions could report a start time later than events they carried,
producing negative durations in the RUM session drawer — plus two `enrichEvent` corrections found
while closing it. No API change and no configuration change: the `/json/rum` envelope keeps the same
shape, and only the value of `session_start_time` on a rotated session changes.

### Fixed

- **A rotated session could report a `session_start_time` later than events it shipped with.** The
  new session's start was stamped from a second `Date.now()` read taken *after* the rotation's
  logging, buffer drain, host hooks and storage writes — milliseconds after the enricher had already
  timestamped the very event that triggered the rotation. So the event that opened a session could
  predate the session by a few milliseconds, and downstream that yields `endTime < startTime` and a
  negative `durationMilli`. The start is now derived from the earliest trusted time that belongs to
  the new session: the triggering event, any events a no-cluster flush left buffered, or the rotation
  instant when neither qualifies. Candidates are only trusted within one idle window and never in the
  future, so an `enrichEvent` that returns milliseconds where nanoseconds belong cannot backdate a
  session — which matters because the start time also drives the max-duration and inactivity clocks,
  and a 1970 value would otherwise rotate the session on every subsequent event. Rotations not driven
  by an event, the id/start write order, and the activity-clock reset that keeps a rotated session
  unconfirmed are all unchanged.

  Two cases are deliberately out of scope and still behave as before: a start stamped at SDK
  initialization (no events exist yet to reconcile against), and a client whose clock is uniformly
  wrong (the start, the events and the current reading all share one offset, so the SDK cannot detect
  it — the envelope stays self-consistent, but absolute correctness needs a trusted server clock).

- **`enrichEvent` no longer runs after session reconciliation.** The hook that drives session
  rotation fired *before* `enrichEvent`, but it is `enrichEvent`'s return value that enters the
  batch — so a hook that moved an event's timestamp earlier could ship an event predating the very
  session it opened, defeating the ordering guarantee above for a supported transformation.
  Reconciliation now runs on the final, enriched event, still before it is enqueued. `beforeSend`
  ordering is unchanged, `enrichEvent` still receives the pre-enrichment event, and a throwing
  `enrichEvent` still drops the event as before.

- **An unusable `enrichEvent` return value now drops the event.** The hook's result was cast to an
  event and buffered with no runtime check, so a callback that forgot to return, or returned a scalar
  or an object without a `type`, put `null` (or a bare value) into the `events` array on the wire. Such
  a result is now rejected and **the event is discarded**, matching what an `enrichEvent` that *throws*
  has always done. The un-enriched event is deliberately not sent as a fallback: `enrichEvent` is the
  documented way to scrub `sendCustomEvent` payloads (which are not auto-redacted), so shipping the
  original would leak exactly the data a failed scrubber was meant to remove. The SDK logs once rather
  than on every event. A well-formed result, including one that replaces the event object wholesale, is
  honoured exactly as before.

## [1.0.8]

Stable release of the `1.0.8-rc.0` … `1.0.8-rc.7` line, plus one fix that landed after rc.7 (passing
`console.assert()` calls are no longer captured — first item under **Fixed**).

The public API is unchanged: `EventTypes`, `SDKOptions`, `PrivacyOptions` and `Redactor` are
structurally identical to 1.0.7, the published bundle contains the same 10 files, and the
`/json/rum` wire format is unchanged (pinned by a golden characterization suite). The generated
declarations are no longer byte-for-byte identical to 1.0.7 — core's internal event types are now
generic over the event vocabulary so the React Native package can extend it — but the exported
surface is the same and `attributes.location` is still on the browser event map. The two behaviour
changes below are the reason this shipped as an RC first rather than straight as a patch. The RC
line was verified end-to-end against a live deployment (batches accepted, envelope matching the
golden wire format, `attributes.location` intact, zero console errors).

### Changed

- **`sessionSampleRate: 0` now samples nothing.** It was previously treated as "unset" by a falsy
  check, so configuring `0` sampled *every* session — the exact opposite of the intent. If you set
  `sessionSampleRate: 0` today and rely on the current (inverted) behaviour, sessions will stop
  being collected when you upgrade.
- **The session sampling verdict is now re-rolled per session.** It is documented as "decided once
  per session", but nothing ever removed the stored decision, so in practice it was decided once per
  browser-storage lifetime and every session after the first rotation inherited the original
  verdict. The decision is now cleared when a session rotates (inactivity gap, max-duration cap, or
  a new session id supplied by the host). Re-supplying the *same* session id is still a continuation
  and keeps its verdict.

### Fixed

- **A passing `console.assert()` is no longer captured as a log.** `console.assert(cond, …)` prints
  nothing when `cond` is truthy, but the SDK recorded *every* call as a `log` event — so assertions
  that passed silently filled RUM with lines the console itself never showed. Only failing
  assertions are captured now, matching the console's own semantics. Landed after `1.0.8-rc.7`,
  alongside the React Native SDK, which hit this hard: RN's event-target shim asserts on every
  DevTools-WebSocket event and stormed the listener.
- **`groundcover.startNavigation()` / `endNavigation()` can no longer throw into your code.** The
  manual navigation API read `location.href` with no error boundary, so on a page with an
  instrumented or restricted `location` getter the exception propagated out of the SDK and into the
  caller's own stack. The automatic navigation path was already contained; these two now are as
  well. Pre-existing since before the core extraction, and unrelated to it.
- **The current view's title is no longer discarded when view derivation fails.** The error boundary
  around the browser's view provider returned a hardcoded empty view, so a throw anywhere after the
  document title had already been read (a hostile `location` getter, a user-supplied redactor)
  silently dropped a title that was perfectly good — the no-location path has always preserved it.
  The title is now read first under its own guard, and both failure paths report through the SDK's
  error handler instead of swallowing: a view provider failing on every event is a real signal, not
  something to hide.
- **Session rotation could stop permanently if a host hook threw.** The in-progress flag was set
  before the host's rotation-context hook ran but only cleared in a `finally` that the hook sat
  outside, so a throw left it stuck — silently disabling all further session rotation for the life
  of the page. Not reachable through the browser SDK's own hook, but the surrounding error handling
  swallowed the throw without resetting the flag, so it would have failed invisibly.
- **Configuration option groups were shared between SDK instances.** `privacy`, `tracing`,
  `transport` and `replay` groups the caller didn't supply were held by reference from the shared
  defaults, so multiple SDK instances in one page aliased the same objects (and the module-level
  defaults themselves).
- **A failed batch send could raise an unhandled promise rejection** instead of logging, when
  `options` was absent on the resolved config.

### Internal

- The platform-agnostic pipeline (config, session policy, event enrichment, batching, redaction,
  transport) now lives in a private `@groundcover/rum-core` package, inlined into this bundle at
  build time. It compiles with no DOM lib so platform coupling fails the build, and the network
  send, storage, device metadata and view derivation are injected by this package. Groundwork for
  the React Native SDK; no effect on the published browser artifact.

## [1.0.5]

### Added

- **Network request & response body capture.** `fetch` and XHR request/response bodies are recorded —
  including `fetch(new Request(url, { body }))` (openapi-fetch), where the body lives on the `Request`
  and was previously dropped. Bodies are read from a clone taken before the request is sent, so the
  app's copy is never consumed.

### Changed

- **Unified, fail-closed body capture across every path.** `init.body`, `Request`-object, XHR
  request/response, and fetch response capture now share one contract: each emits either a complete
  body within the size cap or a placeholder — never a truncated, partial, or binary body (which
  breaks `redactBody`'s structured pass and can leak nested secrets). Over-cap →
  `[request body too large]` / `[response body too large]`; slow or never-closing read →
  `[… body unavailable]`; binary/multipart classified by content-type → `[binary data]`, a typed
  label (`[image data]`, `[pdf data]`, …), or `[form data]`. Redaction runs *before* truncation so
  sensitive keys in a within-cap body are always masked.

### Fixed

- **Fetch response bodies are now hardened like request bodies (FE-12851).** Responses are classified
  by content-type (binary → placeholder, no mojibake) and their read is bounded by a timeout, so a
  slow or never-ending response stream can no longer stall the network event. An over-cap response is
  placeholdered instead of emitting an unredactable truncated slice.

## [1.0.2]

### Changed

- **Custom events no longer count as user-presence for session inactivity.** `sendCustomEvent` is a
  programmatic API commonly called from timers, background jobs and polling loops, so a custom event
  is no longer treated as proof a human is present: it does not refresh the activity clock and cannot
  trigger an inactivity-gap session rotation. Custom events still flush and still count toward the
  max-duration cap. Real interaction remains covered by `dom.event` / `navigation` / `pageload` and by
  the raw-input presence monitor (scroll / pointer / key / touch).

## [1.0.1]

### Fixed

- **README images now render on npm.** The logo used a relative path and the CI badge pointed at the
  internal GitHub repo, so both 404'd on the npm package page. The logo is now served from the
  published package via jsDelivr and the CI-status badge was removed.

## [1.0.0]

First major release. This version restructures the configuration API by concern and finalizes the
privacy model. It contains **breaking changes** — see [Migrating to 1.0.0](./README.md#migrating-to-100).

### Changed

- **Breaking — `options` is restructured by concern.** Distributed-tracing, transport and replay
  knobs now live in nested groups, `environment` is de-duplicated (top-level only), and
  `userIdentifier` is renamed to `user`:

  | `0.x` | `1.0.0` |
  |---|---|
  | `userIdentifier` | `user` |
  | `options.tracePropagationUrls` | `options.tracing.propagationUrls` |
  | `options.tracePropagationHeaders` | `options.tracing.propagationHeaders` |
  | `options.tracePropagationTraceIdHeaderName` | `options.tracing.traceIdHeaderName` |
  | `options.tracePropagationSpanIdHeaderName` | `options.tracing.spanIdHeaderName` |
  | `options.traceOrigin` | `options.tracing.origin` |
  | `options.batchSize` / `options.batchTimeout` | `options.transport.batchSize` / `options.transport.batchTimeout` |
  | `options.enableCompression` | `options.transport.compression` |
  | `options.sessionReplay.blockedSelectors` | `options.replay.blockedSelectors` |

  Flat options (`debug`, `sessionSampleRate`, `eventSampleRate`, `enabledEvents`, `excludedUrls`,
  `sessionMaxDuration`, `beforeSend`, `enrichEvent`) and the `privacy` group are unchanged. Nested
  groups merge one level deep on `updateConfig` — and `tracing.origin` / `privacy.replay` one level
  deeper — so a partial update preserves sibling keys (e.g. updating `tracing.origin.name` keeps
  `origin.value`).

### Removed

- **Breaking — removed the deprecated masking flags `options.enableMasking` and `options.maskFields`.**
  Use `options.privacy` instead: `enableMasking: true` → `privacy.level: 'mask-all'`, explicit
  `enableMasking: false` → `privacy.level: 'allow'`, and `maskFields` → `privacy.maskSelectors` /
  `privacy.sensitiveKeys`. The removed flags are **ignored, not reinterpreted** — passing either one
  now emits a `console.warn` pointing to this migration so the change isn't silent.

### Included since the previous release

These shipped ahead of 1.0.0 and are part of this major:

- **Privacy on by default.** Unified `options.privacy` config drives masking for both session replay
  and non-replay events (DOM / network / logs / errors), defaulting to `mask-sensitive`. Sensitive
  request/response headers, bodies, and URL query params are redacted; built-in auth headers are
  always stripped. Opt out with `privacy.level: 'allow'`.
- **Web Worker offloading.** Session-replay packing and outgoing-batch gzip run in a dedicated inline
  worker, with a transparent main-thread fallback under strict CSP / sandboxed iframes / SSR.
- **Configurable, lazily-enforced `sessionMaxDuration`** (default 4h, range 1 min–8h) plus 30-minute
  inactivity-gap session rotation.

[1.0.1]: https://github.com/groundcover-com/rum/releases/tag/1.0.1
[1.0.0]: https://github.com/groundcover-com/rum/releases/tag/1.0.0
