Session replay operations

Embed
Preview of the card
<a href="https://forumulate.com/p/watchtower/docs/replay-operations"><img src="https://forumulate.com/api/public/orgs/watchtower/spaces/docs/pages/c6be4f4d-4131-4e84-89cf-68914b37cbb9/social-card.svg?v=3e7ae33eec8b" alt="Session replay operations" width="600" height="315"></a>

Recordings are rrweb events, scrubbed at wt-edge, gzip-compressed and stored in S3 at replay/{project}/{session}/{seq:08}. Metadata lives in ClickHouse (replay_sessions_v1, replay_chunks_v1). Bytes are only served through the API after a project check.

Retention

  • Each session carries the project's replay_retention_days as of its first chunk. Changing the setting applies to new sessions. POST /v1/projects/{p}/replays/retention/apply (ManagePrivacy, audited) rewrites existing sessions to the current value.

  • ClickHouse TTL removes metadata. The worker's daily job (replay_retention) removes the S3 objects of sessions past last_seen + retention_days, deletes objects with no metadata older than one day, and marks metadata whose objects are gone as deleted. Counts appear in wt_replay_orphans_total{kind="object|metadata"}.

  • A legal hold (POST /v1/projects/{p}/replays/{session}/legal-hold, DELETE to release) exempts one session from retention and from bulk deletion until released. A hold raises the session's ClickHouse retention to the maximum (65535 days); release restores the project's current value.

Deletion

  • DELETE /v1/projects/{p}/replays/{session}: removes the S3 prefix, then sets deleted = 1 and drops chunk metadata. DELETE /v1/projects/{p}/replays?before=<RFC 3339> deletes up to 500 sessions per call (call again while more is true). Both need ManagePrivacy and are audited.

  • Deleting a project queues a purge of replay/{project}/ (purge_project_replays, also the entry point for the Phase 6 purge job).

  • Every deletion writes a replay_deletions_v1 row first. If the API or worker dies midway the worker finishes it on its next pass, so a started deletion always completes.

Deletion guarantee

Once a delete call returns 204 the recording is not readable through Watchtower. In a bucket with versioning enabled the objects remain as noncurrent versions until the bucket's lifecycle rule expires them, and backups of the bucket or of ClickHouse keep copies until the backup's own retention ends. Set the noncurrent-version expiry and backup retention at or below the deletion promise you make to your users.

Bucket lifecycle example

Defense in depth for the worker job (set the days to the longest replay_retention_days plus a week):

{
  "Rules": [
    {
      "ID": "wt-replay-expire",
      "Filter": { "Prefix": "replay/" },
      "Status": "Enabled",
      "Expiration": { "Days": 37 },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 7 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
    }
  ]
}

Do not apply a shorter expiry than a held session needs: lifecycle rules know nothing of legal holds. Exclude held sessions by moving them under a separate prefix first, or leave the rule off and rely on the worker job.

Stats and sizing

GET /v1/projects/{p}/replays/stats (ReadReplay) returns sessions, bytes, bytes per day (90 days), the top 10 routes by bytes and projected_steady_state_bytes (avg_daily_bytes * retention_days, averaged over the last seven recorded days).

Sizing rule of thumb: measure bytes / sessions for your traffic. Scrubbed, gzipped sessions are typically a few hundred KiB at the median, with a long tail from long sessions and heavy DOMs. Budget sessions per day * median bytes * retention days, plus 30% for the tail.

Export

GET /v1/projects/{p}/replays/export?from=&to=&format=rrweb-ndjson streams a zip with one {session}.ndjson per session (one rrweb event per line, readable by rrweb tooling). Needs ReadReplay (Engineer and above); audited; at most 500 sessions per call (narrow the range otherwise). The data is the scrubbed copy; unmasked content never reaches storage.

Not yet built: copying new chunks to a project export destination (S3 or Kafka) as they arrive. Until it lands, use the export API on a schedule.

Self-hosted storage notes

  • Any S3-compatible store works (WT_ARTIFACT_STORE=s3://bucket, AWS_* variables). A file:///dir store is for single-node trials only.

  • The API and worker need s3:GetObject, PutObject, DeleteObject and ListBucket on the replay/ prefix. Chunk writes are idempotent by (session, seq).

  • Size the bucket from the stats projection; alert on wt_replay_orphans_total rising, which means writes and metadata are diverging.

Updated