Skip to content

feat: Session replays - #563

Merged
Blaumaus merged 35 commits into
mainfrom
feature/session-replays
Jun 5, 2026
Merged

feat: Session replays#563
Blaumaus merged 35 commits into
mainfrom
feature/session-replays

Conversation

@Blaumaus

@Blaumaus Blaumaus commented Jun 3, 2026

Copy link
Copy Markdown
Member

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

  • New Features

    • Session replay recording, playback, MP4 export and screenshot capture
    • Dedicated Replays dashboard tab, session list, filters, and per-replay details/player
    • Project Settings: configurable replay retention (30,90,365,1825 days)
    • Client SDK: startSessionReplay()/preloadSessionReplay() and recorder controls
    • UI: pricing tooltip, billing add-on management, checkout & user settings integration
  • Billing

    • Session replay offered as a premium add-on with per-tier quotas and updated Plus plan limits

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 180da281-e43b-47cb-ac49-cc6e9f15e5a1

📥 Commits

Reviewing files that changed from the base of the PR and between e0f5ca4 and e72e836.

📒 Files selected for processing (6)
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • packages/tracker-js/jest.config.js
  • packages/tracker-js/src/Lib.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • packages/tracker-js/tsconfig.test.json
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/tracker-js/tsconfig.test.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/tracker-js/src/Lib.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
  • backend/apps/cloud/src/analytics/analytics.service.ts

📝 Walkthrough

Walkthrough

Adds session replay across stack: tracker recorder and uploads; backend ingest, S3 storage and signed requests, ClickHouse schema, MP4 export worker, web replays list/player/export, project retention, billing add-ons, docs, and packaging/runtime tweaks.

Changes

Session Replay feature

Layer / File(s) Summary
Tracker recorder and packaging
packages/tracker-js/*, packages/tracker-js/tests/*, packages/tracker-js/rollup.config.mjs
Adds rrweb-based recorder API startSessionReplay, chunking/flush/idle-duration logic, rrweb preloading, bundling of rrweb UMD asset, TypeScript shims, and comprehensive tests.
Backend ingest & storage
backend/apps/cloud/src/analytics/*, backend/migrations/clickhouse/*
New public ingest endpoints, DTOs, ClickHouse session_replay_chunks table, gzip chunk upload flow, quota reservation Lua scripts, and analytics-service replay methods (store, summary, fetch, delete, cleanup).
Export worker & S3 service
backend/apps/cloud/src/analytics/session-replay-export.service.ts, session-replay-export.processor.ts, session-replay-s3.service.ts, backend/scripts/patch-rrvideo.js, backend/Dockerfile
Adds export service/processor to render MP4 via rrvideo+ffmpeg, Redis export state, deterministic object keys, signed S3 client with AWSv4 signing, and Docker/runtime adjustments (Playwright base, ffmpeg, tzdata) plus rrvideo patching.
Project retention and usage
backend/apps/cloud/src/project/*, backend/migrations/mysql/*, backend/apps/cloud/src/task-manager/*
Adds per-project sessionReplayRetentionDays with validation/DB migration, ProjectService monthly usage query, and daily cleanup cron for expired chunks.
Billing entitlements & add-ons
admin/src/billing/pricing.ts, backend/apps/cloud/src/user/*, backend/migrations/mysql/*
Changes Plus plan entitlement to 'byEventTier', adds per-tier quotas, implements session-replay addon DTOs/service/controller flows (preview, update, renewals, cancellations), and schema migration for user_addon.
Web API contracts & proxies
web/app/api/api.server.ts, web/app/routes/api.session-replay-export.ts, web/app/hooks/useAnalyticsProxy.ts
Adds server-side types/endpoints for replays, proxy route for export/download, and hooks for list/detail/delete/export.
Web UI playback and settings
web/app/pages/Project/tabs/Replays/*, web/app/pages/Project/tabs/Sessions/*, web/app/pages/Project/Settings/*
Implements Replays tab, list rows, SessionReplayModal player with timeline/screenshot/MP4 export, Sessions "Watch replay" button, no-replays empty state, and project retention selector UI.
Docs, marketing, locales, pricing UI
docs/content/docs/*, web/public/locales/en.json, web/app/components/pricing/*, web/app/components/marketing/*
Adds session replay docs and script reference, marketing features, pricing tooltip, numerous locale strings, and checkout pricing tooltip integration.
Build/config/runtime
web/vite.config.ts, packages/tracker-js/tsconfig*, backend/.env.example, backend/package.json
Vite alias for dom-playerrrweb, tsconfig shims, new S3 env examples for session replay, added rrweb/rrvideo deps and postinstall patch script.

Estimated code review effort
🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

  • Swetrix/swetrix#561 — Overlaps admin/billing pricing.ts changes for Plus/session-replay entitlements and effective replay quota logic.
  • Swetrix/swetrix#459 — Related server API and proxy patterns used by the new session replay endpoints and hooks.

Poem

Bunny hops the timeline track, a tiny replay song,
Chunks to S3 and ClickHouse hum along.
rrweb whispers frames, rrvideo paints the sky,
Retention burrows safe where old replays lie.
I twitch my whiskers — watch the user fly. 🐇🎥

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/session-replays

@Blaumaus Blaumaus self-assigned this Jun 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
admin/src/billing/pricing.ts (1)

42-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync this entitlement table with the backend copy.

admin/src/billing/pricing.ts and backend/apps/cloud/src/user/entities/user.entity.ts already disagree on apiRateLimitPerHour for standard and plus (600/5000 here vs 300/6000 there). Anything that renders limits from this file can now advertise different numbers than the cloud backend serializes and enforces.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@admin/src/billing/pricing.ts` around lines 42 - 64, The planEntitlements
constants in planEntitlements (referencing PlanType.standard and PlanType.plus)
are out of sync with the backend: update apiRateLimitPerHour for
PlanType.standard to 300 and for PlanType.plus to 6000 so this frontend/admin
source matches backend/apps/cloud/src/user/entities/user.entity.ts; change the
numeric values in the planEntitlements object and run or update any
snapshot/tests that assert these advertised limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/apps/cloud/src/analytics/analytics.service.ts`:
- Around line 1824-1841: The quota enforcement is race-prone because you SET NX
a per-replay reservation key and then separately call
getMonthlyReplayUsage/getReplayAccountProjectIds to decide if reservation should
stand; concurrent requests can all pass the read and over-admit. Replace the
two-step logic with an atomic Redis operation (a Lua EVAL script or a single
Redis command) that reads the current monthly usage for the account/project(s),
compares it to getSessionReplayQuota(project.admin), and only sets the
reservation key (and/or increments a usage counter) if usage < quota; reference
the current symbols: redis.set(key, ...), getReplayUsageTtlSeconds(),
getReplayAccountProjectIds(), getMonthlyReplayUsage(), getSessionReplayQuota(),
and ensure the script rolls back/does not create the reservation when quota
would be exceeded (or increments a counter atomically so removals/del logic
stays consistent).
- Around line 1903-1928: The current clickhouse.insert call in
analytics.service.ts writes chunks append-only which allows duplicate rows for
the same (pid, psid, replayId, chunkIndex); make ingestion idempotent by
enforcing uniqueness on that chunk key. Fix by changing the ClickHouse table
engine to a deduplicating engine (e.g., ReplacingMergeTree or
CollapsingMergeTree) with PRIMARY KEY (pid, psid, replayId, chunkIndex) and a
version/created column so duplicates are collapsed, and/or add a lightweight
pre-insert existence check in the code path that calls clickhouse.insert to skip
inserting if a row already exists for the same keys; update the
clickhouse.insert usage in analytics.service.ts to include the version/created
field used for dedupe. Ensure the unique key names (pid, psid, replayId,
chunkIndex) and the clickhouse.insert call are the targets of this change.

In `@backend/apps/cloud/src/analytics/dto/session-replay.dto.ts`:
- Around line 100-109: Update the GetSessionReplaysDto so pagination fields are
optional: add `@IsOptional`() to the take and skip properties (which are currently
decorated with `@Type`(() => Number), `@IsInt`(), `@Min`(0) and `@Max`(150) for take) so
omitted query params pass validation and the controller’s getSafeNumber defaults
can apply; also tighten replayId by adding `@IsNotEmpty`() (in addition to its
existing `@IsOptional`(), `@IsString`(), `@MaxLength`(80)) to reject empty strings as
invalid identities.
- Around line 33-35: SessionReplayStartDto.replayId currently allows empty
strings which can cause storage key collisions when SessionReplayChunkDto (which
extends it) forwards replayId into startSessionReplay/storeSessionReplayChunk;
add a non-empty check to the DTO by applying a class-validator constraint such
as `@IsNotEmpty`() (or `@MinLength`(1)) alongside the existing `@IsString`() and
`@MaxLength`(80), and ensure the decorator is imported (from class-validator) so
replayId cannot be ''.

In `@backend/apps/cloud/src/analytics/session-replay-export.service.ts`:
- Around line 385-387: The code currently buffers the entire MP4 via
fs.readFile(mp4Path) before calling sessionReplayStorage.putObject, which can
OOM for large exports; change it to stream the file instead: replace
fs.readFile(mp4Path) with a readable stream created from mp4Path (e.g.,
fs.createReadStream(mp4Path)) and pass that stream to
sessionReplayStorage.putObject (or use the storage client's streaming/multipart
upload helper) while still providing the same objectKey from getObjectKey and
content type 'video/mp4' so the file is uploaded without materializing the whole
Buffer in memory.
- Around line 389-414: The export is already marked ready and objectKey set by
updateState, but a failure in exportQueue.add causes the outer catch to call
markFailed and clear the objectKey, making a valid export unavailable; instead,
wrap the exportQueue.add call in its own try/catch so that failures to schedule
cleanup do not flip the export to failed or clear objectKey—call
updateState/status and persist objectKey only once (updateState(... status:
'ready', objectKey ...)), and if exportQueue.add throws, log the error and
optionally enqueue a retry or alert, but do not call markFailed or modify
objectKey; reference the existing updateState, exportQueue.add, markFailed,
objectKey, and status:'ready' symbols when making the change.
- Around line 429-441: The current inline .catch on
this.sessionReplayStorage.deleteObject silences R2 delete failures so the job is
marked successful and expireState clears state.objectKey; change this so
failures propagate and only expire the state when deletion succeeds: remove the
.catch (or rethrow after logging) from deleteObject call on sessionReplayStorage
(referencing deleteObject, sessionReplayStorage, and data.exportId) and ensure
expireState(state) is invoked only after the deleteObject promise resolves
successfully (so expireState is not called when delete fails).

In `@backend/apps/cloud/src/analytics/session-replay-r2.service.ts`:
- Around line 181-185: The fetch call that performs R2 storage operations (the
invocation using url, method, headers, requestBody in
session-replay-r2.service.ts) needs an explicit request timeout: create an
AbortController, pass controller.signal in the fetch options, schedule a
setTimeout to call controller.abort() after a sensible timeout (e.g., 30s), and
clear the timeout once fetch resolves/rejects so you don’t leak timers; ensure
any AbortError is handled/propagated consistently by the existing caller logic.

In `@backend/apps/cloud/src/user/entities/user.entity.ts`:
- Around line 218-234: The helper getSessionReplayRetentionEntitlement relies on
getEffectivePlanType(user) which can return null when planCode is missing, so
passing only { planType: PlanType.enterprise } currently yields the wrong
default; fix by first honoring an explicit user.planType (e.g., if
user?.planType === PlanType.enterprise return 1825), then fall back to
getEffectivePlanType(user) for determination, and optionally update the user
input type or call sites to include planCode if you want getEffectivePlanType to
be authoritative; reference getSessionReplayRetentionEntitlement,
getEffectivePlanType, and PlanType when making the change.
- Around line 95-108: PLUS_SESSION_REPLAY_QUOTA is missing an entry for the
legacy PlanCode.freelancer, so getSessionReplayQuota() falls back to 0 for those
users; add a mapping for PlanCode.freelancer with the same value as the 100k
tier in PLUS_SESSION_REPLAY_QUOTA (i.e., mirror the value used for
PlanCode['100k']) so legacy freelancer plus plans receive the correct replay
quota.

In `@packages/tracker-js/README.md`:
- Around line 197-223: The README and the script reference disagree on the
default privacy for startSessionReplay (README says 'total', script reference
says 'normal'); inspect the actual implementation of startSessionReplay to
determine the true default, then update the documentation so both the README
entry for startSessionReplay (example and table) and the script-reference doc
entry use that verified default value and wording; ensure the `privacy` default
is consistent in the function docs, table row, and any example code.

In `@packages/tracker-js/src/Lib.ts`:
- Around line 803-805: Validate runtime privacy input before using it: replace
the direct use of options.privacy (and the similar block around lines 1149-1179)
with a whitelist check against the allowed privacy constants/enum and only
accept exact matches; if options.privacy is missing or not one of the allowed
values, explicitly set privacy = DEFAULT_SESSION_REPLAY_PRIVACY (the safe
default) so typos like "totl" cannot fall through to a less-private behavior;
apply this validation where replayId is created and before calling
this.sendSessionReplayStart and any other call sites that consume privacy.
- Around line 1113-1130: The current rrweb loader Promise stored in
window.__SWETRIX_RRWEB_LOADING__ and this.rrwebLoader is left rejected on a load
failure, preventing retries; modify the loader creation in Lib.ts so that on
script.onerror (or promise rejection) you clear the global and instance
references (delete window.__SWETRIX_RRWEB_LOADING__ and set this.rrwebLoader =
undefined/null) before rejecting so subsequent startSessionReplay() calls can
attempt to load again; locate the loader assignment around
this.getSessionReplayUrl(), window.__SWETRIX_RRWEB_LOADING__, and
this.rrwebLoader to implement the change.

In `@web/app/pages/Project/Settings/tabs/SessionReplays.tsx`:
- Around line 80-87: The radio inputs in SessionReplays.tsx lack a shared name,
so they don't behave as a single radio group; update the <input> inside the map
(the element using props/variables days, label, className and the onChange
handler onRetentionChange) to include a common name attribute (e.g.,
name="retention" or a constant like RETENTION_RADIO_NAME) so all retention
options share the same radio group and restore native keyboard behavior.

In `@web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx`:
- Around line 1388-1394: The modal close handler currently clears the timeout
via clearExportPoll but doesn't prevent an in-flight poll request from resolving
and scheduling another timeout; update the polling flow (the pollExportStatus
function and any place that schedules setTimeout) to check a modal-open guard
before scheduling the next poll or showing toasts, and cancel/ignore in-flight
fetches using an AbortController or an "isOpen" ref (e.g., isModalOpenRef) that
is set false on close; also ensure clearExportPoll aborts the controller or
flips the ref so resolved promises do nothing. Apply the same guard/abort logic
to the other polling blocks referenced (around the functions used at 1678-1704
and 1761-1771).

In `@web/app/routes/api.session-replay-export.ts`:
- Around line 29-40: The handler currently calls request.json() directly into
the body variable which will throw on malformed JSON and produce a 500; wrap the
JSON parsing in a try/catch around the request.json() call (the body variable)
and return a 400 ProxyResponse when parsing fails with a clear error message
before performing the existing checks for projectId/psid. Keep the existing
validation logic for body.projectId and body.psid (the subsequent conditional)
but ensure it only runs when parsing succeeded.

---

Outside diff comments:
In `@admin/src/billing/pricing.ts`:
- Around line 42-64: The planEntitlements constants in planEntitlements
(referencing PlanType.standard and PlanType.plus) are out of sync with the
backend: update apiRateLimitPerHour for PlanType.standard to 300 and for
PlanType.plus to 6000 so this frontend/admin source matches
backend/apps/cloud/src/user/entities/user.entity.ts; change the numeric values
in the planEntitlements object and run or update any snapshot/tests that assert
these advertised limits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39e13fe8-bd53-43c2-9e5a-0c055560768d

📥 Commits

Reviewing files that changed from the base of the PR and between 16da1fc and aa5f3d0.

⛔ Files ignored due to path filters (3)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • packages/tracker-js/package-lock.json is excluded by !**/package-lock.json
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (75)
  • admin/src/billing/pricing.ts
  • backend/.env.example
  • backend/Dockerfile
  • backend/apps/cloud/src/analytics/analytics.controller.ts
  • backend/apps/cloud/src/analytics/analytics.module.ts
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • backend/apps/cloud/src/analytics/bot-detection.service.ts
  • backend/apps/cloud/src/analytics/dto/session-replay.dto.ts
  • backend/apps/cloud/src/analytics/session-replay-export.processor.ts
  • backend/apps/cloud/src/analytics/session-replay-export.service.ts
  • backend/apps/cloud/src/analytics/session-replay-r2.service.ts
  • backend/apps/cloud/src/logger/logger.service.ts
  • backend/apps/cloud/src/main.ts
  • backend/apps/cloud/src/project/dto/update-project.dto.ts
  • backend/apps/cloud/src/project/entity/project.entity.ts
  • backend/apps/cloud/src/project/project.controller.ts
  • backend/apps/cloud/src/project/project.service.ts
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • backend/apps/cloud/src/user/entities/user.entity.ts
  • backend/apps/cloud/src/user/user.service.ts
  • backend/knip.jsonc
  • backend/migrations/clickhouse/2026_06_03_session_replays.js
  • backend/migrations/clickhouse/initialise_database.js
  • backend/migrations/mysql/2026_06_03_session_replays.sql
  • backend/package.json
  • backend/scripts/patch-rrvideo.js
  • docs/content/docs/add-script.mdx
  • docs/content/docs/analytics-dashboard/error-tracking.mdx
  • docs/content/docs/analytics-dashboard/meta.json
  • docs/content/docs/analytics-dashboard/profiles-and-sessions.mdx
  • docs/content/docs/analytics-dashboard/session-replays.mdx
  • docs/content/docs/script-reference.mdx
  • docs/content/docs/sitesettings/project-configuration.mdx
  • packages/tracker-js/README.md
  • packages/tracker-js/package.json
  • packages/tracker-js/rollup.config.mjs
  • packages/tracker-js/src/Lib.ts
  • packages/tracker-js/src/index.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • packages/tracker-js/tsconfig.esnext.json
  • packages/tracker-js/tsconfig.json
  • web/app/api/api.server.ts
  • web/app/components/marketing/FeaturesGrid.tsx
  • web/app/components/pricing/MarketingPricing.tsx
  • web/app/components/pricing/SessionReplayPricingTooltip.tsx
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/lib/constants/index.ts
  • web/app/lib/models/Project.ts
  • web/app/pages/Project/Settings/ProjectSettings.tsx
  • web/app/pages/Project/Settings/SettingsSidebar.tsx
  • web/app/pages/Project/Settings/tabs/SessionReplays.tsx
  • web/app/pages/Project/View/ViewProject.tsx
  • web/app/pages/Project/View/components/ProjectSidebar.tsx
  • web/app/pages/Project/View/utils/projectViewSegments.ts
  • web/app/pages/Project/tabs/Replays/NoReplays.tsx
  • web/app/pages/Project/tabs/Replays/Replays.tsx
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
  • web/app/pages/Project/tabs/Sessions/Pageflow.tsx
  • web/app/pages/Project/tabs/Sessions/SessionDetailView.tsx
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • web/app/pages/Project/tabs/Sessions/Sessions.tsx
  • web/app/pages/Project/tabs/Sessions/SessionsView.tsx
  • web/app/pages/Project/tabs/Sessions/replayPlayer.css
  • web/app/pages/Subscribe/Subscribe.tsx
  • web/app/pages/UserSettings/UserSettings.tsx
  • web/app/routes/_index.tsx
  • web/app/routes/api.analytics.ts
  • web/app/routes/api.session-replay-export.ts
  • web/app/routes/projects.$id.tsx
  • web/app/routes/projects.settings.$id.tsx
  • web/app/types/dom-player.d.ts
  • web/knip.jsonc
  • web/package.json
  • web/public/locales/en.json
  • web/vite.config.ts

Comment thread backend/apps/cloud/src/analytics/analytics.service.ts Outdated
Comment thread backend/apps/cloud/src/analytics/analytics.service.ts
Comment thread backend/apps/cloud/src/analytics/dto/session-replay.dto.ts
Comment thread backend/apps/cloud/src/analytics/dto/session-replay.dto.ts Outdated
Comment thread backend/apps/cloud/src/analytics/session-replay-export.service.ts Outdated
Comment thread packages/tracker-js/src/Lib.ts Outdated
Comment thread packages/tracker-js/src/Lib.ts
Comment thread web/app/pages/Project/Settings/tabs/SessionReplays.tsx Outdated
Comment thread web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
Comment thread web/app/routes/api.session-replay-export.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
admin/src/billing/pricing.ts (1)

66-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add legacy freelancer tier to admin replay quotas.

sessionReplayQuotas omits PlanCode.freelancer, so plus users on that legacy 100k SKU resolve to 0 in getEffectiveLimits. Backend quota logic maps freelancer to 5000 (backend/apps/cloud/src/user/entities/user.entity.ts, Line 95-98), so admin and backend currently diverge.

Suggested fix
 const sessionReplayQuotas: Partial<Record<PlanCode, number>> = {
+  [PlanCode.freelancer]: 5000,
   [PlanCode['100k']]: 5000,
   [PlanCode['200k']]: 10000,
   [PlanCode['500k']]: 25000,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@admin/src/billing/pricing.ts` around lines 66 - 79, sessionReplayQuotas is
missing the legacy PlanCode.freelancer entry so getEffectiveLimits returns 0 for
those users; add a freelancer key to sessionReplayQuotas with the same quota as
the 100k SKU (5000) to match backend behavior. Locate the sessionReplayQuotas
object in admin/src/billing/pricing.ts and add [PlanCode.freelancer]: 5000
(matching backend/apps/cloud/src/user/entities/user.entity.ts mapping) so admin
and backend quotas align.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@admin/src/billing/pricing.ts`:
- Around line 66-79: sessionReplayQuotas is missing the legacy
PlanCode.freelancer entry so getEffectiveLimits returns 0 for those users; add a
freelancer key to sessionReplayQuotas with the same quota as the 100k SKU (5000)
to match backend behavior. Locate the sessionReplayQuotas object in
admin/src/billing/pricing.ts and add [PlanCode.freelancer]: 5000 (matching
backend/apps/cloud/src/user/entities/user.entity.ts mapping) so admin and
backend quotas align.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ebd196b4-7763-4a1b-88b4-e1cd681f7d05

📥 Commits

Reviewing files that changed from the base of the PR and between aa5f3d0 and 68aeb0e.

📒 Files selected for processing (16)
  • admin/src/billing/pricing.ts
  • backend/.env.example
  • backend/apps/cloud/src/analytics/analytics.module.ts
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • backend/apps/cloud/src/analytics/dto/session-replay.dto.ts
  • backend/apps/cloud/src/analytics/session-replay-export.service.ts
  • backend/apps/cloud/src/analytics/session-replay-s3.service.ts
  • backend/apps/cloud/src/user/entities/user.entity.ts
  • docs/content/docs/script-reference.mdx
  • packages/tracker-js/src/Lib.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/pages/Project/Settings/tabs/SessionReplays.tsx
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • web/app/pages/UserSettings/UserSettings.tsx
  • web/app/routes/api.session-replay-export.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • web/app/pages/Project/Settings/tabs/SessionReplays.tsx
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/routes/api.session-replay-export.ts
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • docs/content/docs/script-reference.mdx
  • backend/apps/cloud/src/analytics/dto/session-replay.dto.ts
  • web/app/pages/UserSettings/UserSettings.tsx
  • packages/tracker-js/tests/sessionReplay.test.ts
  • backend/apps/cloud/src/analytics/session-replay-export.service.ts
  • packages/tracker-js/src/Lib.ts
  • backend/apps/cloud/src/analytics/analytics.service.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
backend/apps/cloud/src/project/project.service.ts (1)

1032-1074: ⚡ Quick win

Cache monthly replay usage to avoid repeated uniqExact scans.

GET /user/usageinfo now calls this path directly, and this method always hits ClickHouse. Adding Redis caching (same TTL strategy as other usage methods) would reduce expensive repeated scans.

♻️ Suggested change
   async getMonthlySessionReplayUsage(uid: string): Promise<number> {
+    const key = `${getRedisUserUsageInfoKey(uid)}_sessionReplays`
+    const cached = await redis.get(key)
+    if (!_isEmpty(cached)) {
+      const parsed = Number(cached)
+      if (Number.isFinite(parsed)) {
+        return parsed
+      }
+    }
+
     const projects = await this.find({
       where: {
         admin: { id: uid },
       },
       select: ['id'],
@@
     for (let i = 0; i < pids.length; i += CHUNK_SIZE) {
@@
       usage += Number(data[0]?.usage) || 0
     }

+    await redis.set(key, `${usage}`, 'EX', redisUserUsageinfoCacheTimeout)
     return usage
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/project/project.service.ts` around lines 1032 - 1074,
The getMonthlySessionReplayUsage method currently always queries ClickHouse; add
Redis caching around it using the same TTL strategy as other usage methods:
compose a stable cache key (e.g., `monthly_replay_usage:{uid}:{YYYY-MM}`) at the
start of getMonthlySessionReplayUsage, try to read and return the cached numeric
value if present, and only if missing run the existing CHUNK_SIZE
loop/clickhouse.query logic to compute usage, then store the computed value back
into Redis with the same TTL used elsewhere before returning; ensure you use the
same Redis client/service the project already uses so you don’t introduce a new
dependency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/apps/cloud/src/task-manager/task-manager.service.ts`:
- Around line 1455-1458: The parallel invocation of
processDueWebsiteAddonRenewals and processDueSessionReplayAddonRenewals via
Promise.all can concurrently mutate the same user's addon override state and
cause races; change the TaskManagerService call site to run them sequentially
instead of in Promise.all (await
this.userService.processDueWebsiteAddonRenewals() then await
this.userService.processDueSessionReplayAddonRenewals()), preserving the
existing catch/error handling, or alternatively implement row-level
merge/locking inside the userService methods if you prefer a concurrency-safe
merge strategy; update the call where both functions are referenced to ensure
one completes before the next starts.

In `@backend/migrations/mysql/2026_06_04_session_replay_addons.sql`:
- Around line 1-2: The migration alters user_addon.code enum via ALTER TABLE
`user_addon` MODIFY COLUMN `code` enum('websites','session_replays') NOT NULL
which can fail if existing rows contain NULL or any value outside the new enum
set; update the PR description to state this is a schema change (ENUM update +
column definition) and, before deploying, run a data validation query to find
non-conforming rows (WHERE code IS NULL OR code NOT IN
('websites','session_replays')), then fix them by either mapping/fixing values,
setting a valid default, or removing offending rows prior to running the ALTER
TABLE so the enum change cannot fail at runtime.

---

Nitpick comments:
In `@backend/apps/cloud/src/project/project.service.ts`:
- Around line 1032-1074: The getMonthlySessionReplayUsage method currently
always queries ClickHouse; add Redis caching around it using the same TTL
strategy as other usage methods: compose a stable cache key (e.g.,
`monthly_replay_usage:{uid}:{YYYY-MM}`) at the start of
getMonthlySessionReplayUsage, try to read and return the cached numeric value if
present, and only if missing run the existing CHUNK_SIZE loop/clickhouse.query
logic to compute usage, then store the computed value back into Redis with the
same TTL used elsewhere before returning; ensure you use the same Redis
client/service the project already uses so you don’t introduce a new dependency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a015d119-4548-4737-a120-e37cf0a10e17

📥 Commits

Reviewing files that changed from the base of the PR and between 68aeb0e and e8df315.

📒 Files selected for processing (17)
  • backend/apps/cloud/src/project/project.service.ts
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • backend/apps/cloud/src/user/dto/update-website-addon.dto.ts
  • backend/apps/cloud/src/user/entities/user-addon.entity.ts
  • backend/apps/cloud/src/user/entities/user.entity.ts
  • backend/apps/cloud/src/user/interfaces/usage-info.ts
  • backend/apps/cloud/src/user/user.controller.ts
  • backend/apps/cloud/src/user/user.service.ts
  • backend/apps/cloud/src/webhook/webhook.controller.ts
  • backend/migrations/mysql/2026_06_04_session_replay_addons.sql
  • web/app/lib/models/Usageinfo.ts
  • web/app/lib/models/User.ts
  • web/app/pages/Project/tabs/Replays/Replays.tsx
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
  • web/app/pages/UserSettings/UserSettings.tsx
  • web/app/routes/user-settings.tsx
  • web/public/locales/en.json
💤 Files with no reviewable changes (1)
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
✅ Files skipped from review due to trivial changes (1)
  • backend/apps/cloud/src/user/entities/user-addon.entity.ts

Comment thread backend/apps/cloud/src/task-manager/task-manager.service.ts Outdated
Comment thread backend/migrations/mysql/2026_06_04_session_replay_addons.sql

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/apps/cloud/src/analytics/analytics.service.ts (1)

2208-2225: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

argMax(..., created) is still ambiguous for same-second duplicate rows.

The new read-side dedupe picks the “latest” chunk metadata by created, but inserts still round created to YYYY-MM-DD HH:mm:ss. Two writes of the same chunk within one second will share the same sort key, so getSessionReplaySummary(), getSessionReplay(), and getSessionReplaysList() can still select stale metadata nondeterministically. This needs a strictly monotonic version column (for example DateTime64 or epoch millis) and the argMax calls should switch to that key.

Also applies to: 2266-2282, 2346-2355, 6743-6759

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/analytics/analytics.service.ts` around lines 2208 -
2225, The insert uses a seconds-granularity "created" timestamp
(dayjs.utc().format('YYYY-MM-DD HH:mm:ss')) which allows duplicate sort keys for
writes within the same second; change the insert into session_replay_chunks (and
the other similar inserts) to include a strictly monotonic version column (e.g.,
createdMillis as epoch milliseconds or a DateTime64 value) alongside the
existing created field, and update all read-side argMax(...) calls (used by
getSessionReplaySummary, getSessionReplay, getSessionReplaysList) to use this
new createdMillis/version column as the sort key so argMax is deterministic for
same-second duplicate rows.
♻️ Duplicate comments (1)
backend/apps/cloud/src/analytics/analytics.service.ts (1)

2089-2100: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat duplicate chunk retries as idempotent success.

When the same chunkIndex is retried after the original upload already succeeded, this path now returns 400 instead of a no-op success. That still breaks safe client retries on lost responses/timeouts, even though the duplicate write is suppressed.

🛠️ Possible direction
-      case -4:
-        throw new BadRequestException('Session replay chunk already exists')
+      case -4:
+        return null
-    const chunkReservation = await this.reserveReplayChunkStorage(
+    const chunkReservation = await this.reserveReplayChunkStorage(
       pid,
       psid,
       replayId,
       chunkIndex,
       events.length,
       uncompressedBytes,
       retention,
       timestamps,
     )
+    if (!chunkReservation) {
+      return {
+        replayId,
+        psid,
+        chunkIndex,
+        eventCount: events.length,
+        ...retention,
+        countedUsage: false,
+      }
+    }

Also applies to: 2241-2248

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/analytics/analytics.service.ts` around lines 2089 -
2100, The switch on Number(result) currently throws BadRequestException for
duplicate chunk result (-5); change the -5 branch to treat duplicate chunk
retries as idempotent success by returning the same success payload as case 1
(use the existing variables keys, chunkIndex, eventCount, uncompressedBytes)
instead of throwing; apply the same change to the other equivalent switch block
(the similar handling at the 2241-2248 region) so client retries receive a no-op
success rather than 400.
🧹 Nitpick comments (1)
packages/tracker-js/tests/sessionReplay.test.ts (1)

297-303: ⚡ Quick win

Avoid asserting that retries leave failed loader scripts behind.

expect(scripts).toHaveLength(2) locks the test to the current DOM leak/implementation detail. If the loader starts removing failed <script> nodes, retry behavior is still correct but this suite will fail. Assert the cleared retry state and successful reload instead of the exact stale node count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tracker-js/tests/sessionReplay.test.ts` around lines 297 - 303,
Remove the brittle assertion that checks the DOM node count
(expect(scripts).toHaveLength(2)) and instead verify the retry machinery and
success path: after calling startSessionReplay() and simulating the rrweb load
(using startSessionReplay, RRWEB_URL, scripts and scripts[1].dispatchEvent(new
Event('load'))), assert that the retry state has been cleared (e.g. any retry
counter or flag set by startSessionReplay is reset) and that the reload
succeeded (e.g. rrweb.record is present or the success callback/state the loader
sets is true) rather than asserting the exact number of <script> nodes. Ensure
you reference the same symbols (startSessionReplay, RRWEB_URL, scripts, rrweb)
when updating the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@backend/apps/cloud/src/analytics/analytics.service.ts`:
- Around line 2208-2225: The insert uses a seconds-granularity "created"
timestamp (dayjs.utc().format('YYYY-MM-DD HH:mm:ss')) which allows duplicate
sort keys for writes within the same second; change the insert into
session_replay_chunks (and the other similar inserts) to include a strictly
monotonic version column (e.g., createdMillis as epoch milliseconds or a
DateTime64 value) alongside the existing created field, and update all read-side
argMax(...) calls (used by getSessionReplaySummary, getSessionReplay,
getSessionReplaysList) to use this new createdMillis/version column as the sort
key so argMax is deterministic for same-second duplicate rows.

---

Duplicate comments:
In `@backend/apps/cloud/src/analytics/analytics.service.ts`:
- Around line 2089-2100: The switch on Number(result) currently throws
BadRequestException for duplicate chunk result (-5); change the -5 branch to
treat duplicate chunk retries as idempotent success by returning the same
success payload as case 1 (use the existing variables keys, chunkIndex,
eventCount, uncompressedBytes) instead of throwing; apply the same change to the
other equivalent switch block (the similar handling at the 2241-2248 region) so
client retries receive a no-op success rather than 400.

---

Nitpick comments:
In `@packages/tracker-js/tests/sessionReplay.test.ts`:
- Around line 297-303: Remove the brittle assertion that checks the DOM node
count (expect(scripts).toHaveLength(2)) and instead verify the retry machinery
and success path: after calling startSessionReplay() and simulating the rrweb
load (using startSessionReplay, RRWEB_URL, scripts and
scripts[1].dispatchEvent(new Event('load'))), assert that the retry state has
been cleared (e.g. any retry counter or flag set by startSessionReplay is reset)
and that the reload succeeded (e.g. rrweb.record is present or the success
callback/state the loader sets is true) rather than asserting the exact number
of <script> nodes. Ensure you reference the same symbols (startSessionReplay,
RRWEB_URL, scripts, rrweb) when updating the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5c9b2cc-e428-4b90-b349-20982e7186c1

📥 Commits

Reviewing files that changed from the base of the PR and between e8df315 and ebf078e.

📒 Files selected for processing (13)
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • backend/apps/cloud/src/analytics/session-replay-export.service.ts
  • backend/apps/cloud/src/analytics/session-replay-s3.service.ts
  • backend/apps/cloud/src/project/project.service.ts
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • backend/scripts/patch-rrvideo.js
  • docs/content/docs/analytics-dashboard/session-replays.mdx
  • packages/tracker-js/rollup.config.mjs
  • packages/tracker-js/src/Lib.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • web/app/lib/pricing/catalog.ts
  • web/app/pages/UserSettings/UserSettings.tsx
  • web/public/locales/en.json
💤 Files with no reviewable changes (1)
  • web/app/lib/pricing/catalog.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/content/docs/analytics-dashboard/session-replays.mdx
  • web/public/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/apps/cloud/src/project/project.service.ts
  • backend/scripts/patch-rrvideo.js
  • packages/tracker-js/src/Lib.ts
  • backend/apps/cloud/src/analytics/session-replay-s3.service.ts
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • web/app/pages/UserSettings/UserSettings.tsx
  • backend/apps/cloud/src/analytics/session-replay-export.service.ts

@Blaumaus
Blaumaus changed the base branch from improvement/billing to main June 5, 2026 11:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/pages/Project/tabs/Replays/ReplaysView.tsx`:
- Around line 304-315: removeReplayFromList currently always decrements
replaysSkip even when nothing was removed; change it so you detect if a replay
was actually removed (e.g., inside the setReplays callback compare prev.length
and next.length or check presence of removedReplayKey) and only call
setReplaysSkip(prev => Math.max(0, prev - 1)) when a removal occurred; update
hasShownContentRef.current based on the computed next list as you already do.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bbd7f181-aa69-42d2-9b63-604e1f48e41b

📥 Commits

Reviewing files that changed from the base of the PR and between ebf078e and 9e4fcd0.

📒 Files selected for processing (14)
  • backend/apps/cloud/src/analytics/analytics.controller.ts
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • web/app/api/api.server.ts
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/pages/Project/Settings/tabs/SessionReplays.tsx
  • web/app/pages/Project/tabs/Replays/Replays.tsx
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
  • web/app/pages/Project/tabs/Sessions/SessionDetailView.tsx
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • web/app/pages/Project/tabs/Sessions/Sessions.tsx
  • web/app/routes/api.analytics.ts
  • web/app/ui/Select.tsx
  • web/public/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (11)
  • web/app/pages/Project/tabs/Sessions/Sessions.tsx
  • web/app/routes/api.analytics.ts
  • web/app/pages/Project/tabs/Replays/Replays.tsx
  • web/app/pages/Project/tabs/Sessions/SessionDetailView.tsx
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/api/api.server.ts
  • backend/apps/cloud/src/analytics/analytics.controller.ts
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • web/public/locales/en.json
  • packages/tracker-js/tests/sessionReplay.test.ts

Comment thread web/app/pages/Project/tabs/Replays/ReplaysView.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/tracker-js/src/Lib.ts (1)

789-815: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize concurrent startSessionReplay() calls.

Line 789 only deduplicates after this.sessionReplayActions exists. Until Line 937, a second caller can run the same async path, mint another replayId, and start a second rrweb.record() instance. That duplicates stored chunks and can overcount replay usage/billing.

Suggested fix
+  private sessionReplayStartPromise: Promise<SessionReplayActions> | null = null
+
   async startSessionReplay(
     options: SessionReplayOptions = {},
   ): Promise<SessionReplayActions> {
     if (this.sessionReplayActions) {
       return this.sessionReplayActions
     }
+    if (this.sessionReplayStartPromise) {
+      return this.sessionReplayStartPromise
+    }
 
-    if (!this.canTrack()) {
-      return defaultSessionReplayActions
-    }
+    const startPromise = (async (): Promise<SessionReplayActions> => {
+      if (!this.canTrack()) {
+        return defaultSessionReplayActions
+      }
 
-    if (!this.shouldSampleSessionReplay(options.sampleRate)) {
-      return defaultSessionReplayActions
-    }
+      if (!this.shouldSampleSessionReplay(options.sampleRate)) {
+        return defaultSessionReplayActions
+      }
 
-    try {
-      await this.preloadSessionReplay()
-    } catch {
-      return defaultSessionReplayActions
-    }
+      try {
+        await this.preloadSessionReplay()
+      } catch {
+        return defaultSessionReplayActions
+      }
 
-    // ... existing start logic ...
+      // ... existing start logic ...
 
-    this.sessionReplayActions = {
-      stop: stopSessionReplay,
-      flush: async () => {
-        await flush()
-      },
-    }
+      this.sessionReplayActions = {
+        stop: stopSessionReplay,
+        flush: async () => {
+          await flush()
+        },
+      }
 
-    return this.sessionReplayActions
+      return this.sessionReplayActions
+    })()
+
+    this.sessionReplayStartPromise = startPromise
+
+    try {
+      return await startPromise
+    } finally {
+      if (this.sessionReplayStartPromise === startPromise) {
+        this.sessionReplayStartPromise = null
+      }
+    }
   }

Also applies to: 937-944

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tracker-js/src/Lib.ts` around lines 789 - 815, Concurrent callers to
startSessionReplay() can race before this.sessionReplayActions is set, causing
multiple replayIds and duplicate rrweb.record() instances; fix by serializing
initialization: create and assign a single in-flight promise (e.g.
this.sessionReplayInitPromise or set this.sessionReplayActions to a placeholder
promise) at the very start of startSessionReplay() so subsequent calls await it,
perform the async steps (preloadSessionReplay, sendSessionReplayStart,
rrweb.record) only once, and on failure clear the promise so retries are
possible; ensure the same guard is applied around the rrweb.record creation path
(the code that calls window.rrweb.record and the block referenced around lines
937-944) to prevent a second recorder from starting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/apps/cloud/src/analytics/analytics.service.ts`:
- Around line 2098-2099: The -4 switch branch in the reservation logic currently
returns the same reservation shape as a fresh reservation, allowing
storeSessionReplayChunk() to continue and later call
releaseReplayChunkStorageReservation() on an already-committed chunk; change the
-4 branch to explicitly mark the result as a duplicate (e.g., include a flag
like duplicate: true or status: 'duplicate') and short-circuit the flow so
upload/rollback steps are skipped; then update storeSessionReplayChunk() to
check that flag and return early (without calling
releaseReplayChunkStorageReservation()) when a duplicate is detected to avoid
removing the original chunk and decrementing counters.
- Around line 2417-2429: deleteSessionReplay currently deletes S3 objects via
sessionReplayStorage.deleteObject and then issues an asynchronous ClickHouse
DELETE through clickhouse.command on session_replay_chunks, causing races;
change the flow so the ClickHouse DELETE runs synchronously before removing S3
objects (or set mutations_sync=2 on the ALTER TABLE DELETE) to ensure rows are
gone before object deletion—update the call around clickhouse.command in
deleteSessionReplay to include mutations_sync=2 in the query params or move the
ALTER TABLE block to execute and wait (synchronously) prior to calling mapLimit
on sessionReplayStorage.deleteObject.

---

Outside diff comments:
In `@packages/tracker-js/src/Lib.ts`:
- Around line 789-815: Concurrent callers to startSessionReplay() can race
before this.sessionReplayActions is set, causing multiple replayIds and
duplicate rrweb.record() instances; fix by serializing initialization: create
and assign a single in-flight promise (e.g. this.sessionReplayInitPromise or set
this.sessionReplayActions to a placeholder promise) at the very start of
startSessionReplay() so subsequent calls await it, perform the async steps
(preloadSessionReplay, sendSessionReplayStart, rrweb.record) only once, and on
failure clear the promise so retries are possible; ensure the same guard is
applied around the rrweb.record creation path (the code that calls
window.rrweb.record and the block referenced around lines 937-944) to prevent a
second recorder from starting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d6d1e2f-9489-487e-a121-a8f9a855b479

📥 Commits

Reviewing files that changed from the base of the PR and between ebf078e and e0f5ca4.

📒 Files selected for processing (23)
  • backend/apps/cloud/src/analytics/analytics.controller.ts
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • docs/content/docs/add-script.mdx
  • docs/content/docs/analytics-dashboard/session-replays.mdx
  • docs/content/docs/script-reference.mdx
  • packages/tracker-js/README.md
  • packages/tracker-js/rollup.config.mjs
  • packages/tracker-js/src/Lib.ts
  • packages/tracker-js/src/types/rrweb-shim.d.ts
  • packages/tracker-js/tests/sessionReplay.test.ts
  • packages/tracker-js/tsconfig.esnext.json
  • packages/tracker-js/tsconfig.json
  • web/app/api/api.server.ts
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/pages/Project/Settings/tabs/SessionReplays.tsx
  • web/app/pages/Project/tabs/Replays/Replays.tsx
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
  • web/app/pages/Project/tabs/Sessions/SessionDetailView.tsx
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • web/app/pages/Project/tabs/Sessions/Sessions.tsx
  • web/app/routes/api.analytics.ts
  • web/app/ui/Select.tsx
  • web/public/locales/en.json
✅ Files skipped from review due to trivial changes (3)
  • packages/tracker-js/src/types/rrweb-shim.d.ts
  • docs/content/docs/add-script.mdx
  • docs/content/docs/analytics-dashboard/session-replays.mdx
🚧 Files skipped from review as they are similar to previous changes (16)
  • docs/content/docs/script-reference.mdx
  • packages/tracker-js/tsconfig.json
  • packages/tracker-js/README.md
  • web/app/pages/Project/Settings/tabs/SessionReplays.tsx
  • packages/tracker-js/rollup.config.mjs
  • web/app/ui/Select.tsx
  • web/app/hooks/useAnalyticsProxy.ts
  • web/app/pages/Project/tabs/Sessions/SessionDetailView.tsx
  • web/app/routes/api.analytics.ts
  • web/app/pages/Project/tabs/Replays/ReplaysView.tsx
  • web/app/pages/Project/tabs/Replays/Replays.tsx
  • web/app/pages/Project/tabs/Sessions/SessionReplayModal.tsx
  • web/app/pages/Project/tabs/Sessions/Sessions.tsx
  • web/app/api/api.server.ts
  • backend/apps/cloud/src/analytics/analytics.controller.ts
  • web/public/locales/en.json

Comment thread backend/apps/cloud/src/analytics/analytics.service.ts Outdated
Comment thread backend/apps/cloud/src/analytics/analytics.service.ts Outdated
@Blaumaus
Blaumaus merged commit a0a5d5e into main Jun 5, 2026
12 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 5, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant