Skip to content

feat(storage): support any S3-compatible provider for session replays#1021

Open
DanielvdSpoel wants to merge 1 commit into
rybbit-io:masterfrom
DanielvdSpoel:feature/s3-generic-storage
Open

feat(storage): support any S3-compatible provider for session replays#1021
DanielvdSpoel wants to merge 1 commit into
rybbit-io:masterfrom
DanielvdSpoel:feature/s3-generic-storage

Conversation

@DanielvdSpoel

@DanielvdSpoel DanielvdSpoel commented Jun 8, 2026

Copy link
Copy Markdown

feat(storage): support any S3-compatible provider for session replays

Summary

Session replay batches could previously only be offloaded to Cloudflare R2, and only on cloud (IS_CLOUD) deployments — the endpoint, region, and credentials were hardcoded to R2. Since the service already uses the AWS S3 SDK, this PR generalizes it to any S3-compatible provider (AWS S3, Cloudflare R2, MinIO, …) so self-hosted instances can offload replays too.

Storage is now enabled automatically whenever credentials are present. When no credentials are configured, replays continue to be stored in ClickHouse exactly as before.

Changes

  • Generalize the storage service — rename r2StorageServices3StorageService (singleton exported as objectStorage) and update the session-replay ingest/query consumers. Configuration is resolved from the environment into a typed config object, with credentials being the only requirement to enable storage.
  • Generic S3_* configurationS3_ENDPOINT, S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_FORCE_PATH_STYLE. No longer gated behind IS_CLOUD.
  • Backward compatible — legacy R2_* variables (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_ACCOUNT_ID) are still honored as a fallback, and the R2 endpoint is derived from R2_ACCOUNT_ID when S3_ENDPOINT is unset. Existing cloud/R2 deployments need no changes.
  • Checksum-header stripping retained (and documented) — the AWS SDK validates response checksums by default, which breaks reads against several S3-compatible providers (R2, MinIO); responses have checksum headers stripped, which is harmless against AWS S3.
  • Docs & compose — document the new variables in managing-your-installation.mdx and wire them into docker-compose.yml.

Configuration

Variable Required Default Description
S3_ACCESS_KEY_ID Yes Access key for the bucket
S3_SECRET_ACCESS_KEY Yes Secret key for the bucket
S3_BUCKET_NAME No rybbit Bucket name
S3_ENDPOINT No AWS S3 Custom endpoint, e.g. https://<account>.r2.cloudflarestorage.com or http://minio:9000. Omit for AWS S3.
S3_REGION No auto Bucket region
S3_FORCE_PATH_STYLE No true Path-style addressing. Required by most non-AWS providers; set false for virtual-hosted AWS S3.

Compatibility / migration

  • No breaking changes. Existing R2 deployments using R2_* vars keep working with the endpoint derived from R2_ACCOUNT_ID.
  • Replay storage was previously cloud-only; self-hosted instances can now opt in by supplying credentials.

Testing

  • cd server && tsc (type check)

Please let me know if you'd prefer the legacy R2_* fallback dropped or kept — it's included here to avoid disrupting current cloud deployments.

Summary by CodeRabbit

  • New Features

    • Added support for S3-compatible object storage to offload session replay event data from ClickHouse, reducing storage load and costs.
  • Documentation

    • Added comprehensive configuration guide for S3-compatible storage backends with automatic endpoint derivation for existing Cloudflare R2 deployments.
  • Chores

    • Refactored storage service to support multiple S3-compatible providers while maintaining full backward compatibility.

Session replay batches were stored only in Cloudflare R2, with the
endpoint, region and credentials hardcoded to R2. The service already
used the AWS S3 SDK, so generalize it to any S3-compatible provider
(AWS S3, R2, MinIO, ...):

- Rename r2StorageService -> s3StorageService (export objectStorage) and
  update the session-replay ingest/query consumers.
- Configure via generic S3_* env vars (S3_ENDPOINT, S3_REGION,
  S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME,
  S3_FORCE_PATH_STYLE). Enabled whenever credentials are present, so
  self-hosted instances can offload replays without IS_CLOUD.
- Keep legacy R2_* vars as a fallback and derive the R2 endpoint from
  R2_ACCOUNT_ID, so existing cloud/R2 deployments work unchanged.
- Document the new variables and wire them into docker-compose.yml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

@DanielvdSpoel is attempting to deploy a commit to the goldflag's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR migrates session replay storage from Cloudflare R2 to a generalized S3-compatible object storage service. The storage layer is refactored to support multiple S3 providers with credential/endpoint configuration, while maintaining backward compatibility with existing R2 environment variables. Session replay ingest and query services are updated to use the new storage, with error-tolerant parallel batch retrieval and deletion patterns.

Changes

R2 to S3-Compatible Storage Migration

Layer / File(s) Summary
S3-compatible Storage Service Foundation
server/src/services/storage/s3StorageService.ts
New S3StorageConfig and resolveStorageConfig() enable credential and endpoint resolution from S3_* variables with R2_* fallback. S3 client initialization gated by credential presence with custom HTTP handler stripping checksum headers for provider compatibility. Error handling updated: storeBatch() and getBatch() throw on failure; deleteBatch() logs non-critically. Exported singleton renamed from r2Storage to objectStorage.
Session Replay Event Ingestion
server/src/services/replay/sessionReplayIngestService.ts
Import updated to use objectStorage. Event payloads are extracted and stored via objectStorage.storeBatch() with generated batchKey. ClickHouse rows store metadata only (event_data empty, event_data_key set to batchKey) on success. Storage failures are caught, logged, and fallback to storing full serialized event data in ClickHouse with batchKey nullified.
Session Replay Event Retrieval and Deletion
server/src/services/replay/sessionReplayQueryService.ts
Import updated to use objectStorage. getSessionReplayEvents separates events by event_data_key: object-storage-backed batches fetched concurrently in 50-batch chunks with per-batch error handling (logs, returns null, continues). Events reconstructed via batch_index lookup, combined with ClickHouse data, sorted by timestamp. deleteSessionReplay fetches distinct event_data_key values and deletes object-storage batches in parallel; deletion errors logged but do not block ClickHouse cleanup.
Deployment Configuration and Documentation
docker-compose.yml, docs/content/docs/managing-your-installation.mdx
Docker Compose adds optional S3_* environment variables (credentials, bucket, endpoint, region, path-style setting). Documentation section explains offload behavior, configuration table, and backward compatibility including automatic R2 endpoint derivation from R2_ACCOUNT_ID when S3_ENDPOINT unset.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant IngestService as SessionReplayIngestService
    participant ObjectStore as objectStorage
    participant ClickHouse
    
    Client->>IngestService: replay events
    IngestService->>IngestService: prepare event batch
    alt Storage Enabled
        IngestService->>ObjectStore: storeBatch(batchKey, events)
        alt Success
            ObjectStore-->>IngestService: ✓
            IngestService->>ClickHouse: insert metadata rows<br/>(event_data_key=batchKey)
        else Failure
            ObjectStore-->>IngestService: error
            IngestService->>IngestService: fallback batchKey=null
            IngestService->>ClickHouse: insert full event data<br/>(event_data=payload)
        end
    else Storage Disabled
        IngestService->>ClickHouse: insert full event data
    end
Loading
sequenceDiagram
    participant Client
    participant QueryService as SessionReplayQueryService
    participant ClickHouse
    participant ObjectStore as objectStorage
    
    Note over QueryService: getSessionReplayEvents
    Client->>QueryService: fetch session replays
    QueryService->>ClickHouse: query event metadata
    alt Object Storage Enabled
        ClickHouse-->>QueryService: split into<br/>clickhouseBatches +<br/>objectStorageBatches
        par Parallel Batch Fetch (chunk 50)
            QueryService->>ObjectStore: getBatch(key1)
            QueryService->>ObjectStore: getBatch(key2)
            QueryService->>ObjectStore: getBatch(keyN)
        and
            ObjectStore-->>QueryService: batch data or null on error
        end
        QueryService->>QueryService: reconstruct events<br/>by batch_index
    else Direct ClickHouse
        ClickHouse-->>QueryService: full event data
    end
    QueryService->>QueryService: sort by timestamp
    QueryService-->>Client: combined events
    
    Note over QueryService: deleteSessionReplay
    Client->>QueryService: delete session
    QueryService->>ClickHouse: fetch distinct event_data_keys
    alt Object Storage Enabled
        par Parallel Deletion
            QueryService->>ObjectStore: deleteBatch(key1)
            QueryService->>ObjectStore: deleteBatch(key2)
        and
            ObjectStore-->>QueryService: ✓ or logged error
        end
    end
    QueryService->>ClickHouse: delete session_replay_events
    QueryService->>ClickHouse: delete session_replay_metadata
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A storage tale, from R2 to S3,
The rabbit hops through clouds with glee,
Each replay batch now split so neat—
Some in storage, some in SQL's beat,
Parallel fetches, error-tolerant and sweet! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(storage): support any S3-compatible provider for session replays' accurately and concisely summarizes the main objective of the PR—extending storage support from R2-only to generic S3-compatible providers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
server/src/services/storage/s3StorageService.ts (1)

148-151: 💤 Low value

Use the structured logger for error paths.

The service initializes this.logger but error handling uses console.error. This loses structured context (bucket, endpoint) that would aid debugging in production.

Suggested fix
     } catch (error) {
-      console.error("[S3Storage] Failed to store batch:", error);
+      this.logger.error({ error, key }, "Failed to store batch");
       throw error;
     }
     } catch (error) {
-      console.error("[S3Storage] Failed to retrieve batch:", error);
+      this.logger.error({ error, key }, "Failed to retrieve batch");
       throw error;
     }
     } catch (error) {
-      console.error("[S3Storage] Failed to delete batch:", error);
+      this.logger.error({ error, key }, "Failed to delete batch");
       // Non-critical error, log but don't throw
     }

Also applies to: 224-227, 245-248

🤖 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 `@server/src/services/storage/s3StorageService.ts` around lines 148 - 151,
Replace console.error usages in S3StorageService catch blocks with
this.logger.error and pass structured context (e.g., bucket and endpoint) and
the caught error; specifically update the catch in the method handling batch
storage (where console.error("[S3Storage] Failed to store batch:", error)
currently appears) and the other two catch blocks flagged (around the same
pattern) to call this.logger.error with a descriptive message and an object like
{ bucket: this.bucket, endpoint: this.endpoint, error } so the logger emits
structured fields rather than plain console output.
server/src/services/replay/sessionReplayQueryService.ts (1)

229-242: 💤 Low value

Consider explicit bounds check instead of truthy check.

Line 233 uses data[event.batch_index] as a truthy guard, which would skip events with falsy data (e.g., 0, "", false). While rrweb event data is always an object so this is safe in practice, an explicit bounds check is more defensive.

Suggested fix
       if (data) {
         for (const event of batchEvents) {
-          if (event.batch_index !== null && data[event.batch_index]) {
+          if (event.batch_index !== null && event.batch_index < data.length) {
             events.push({
               timestamp: event.timestamp,
               type: event.type,
               data: data[event.batch_index],
             });
           }
         }
       }
🤖 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 `@server/src/services/replay/sessionReplayQueryService.ts` around lines 229 -
242, Replace the truthy guard on data[event.batch_index] with an explicit
bounds/exists check: when iterating objectStorageResults and inner batchEvents
in sessionReplayQueryService (variables: objectStorageResults, batchEvents,
data, event.batch_index, events), ensure you verify that event.batch_index is
not null/undefined and that the index/key actually exists on data (e.g., check
typeof event.batch_index === 'number' and data[event.batch_index] !== undefined
or use Object.prototype.hasOwnProperty.call(data, event.batch_index)) before
pushing to events so falsy values like 0, "", or false are preserved.
🤖 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.

Nitpick comments:
In `@server/src/services/replay/sessionReplayQueryService.ts`:
- Around line 229-242: Replace the truthy guard on data[event.batch_index] with
an explicit bounds/exists check: when iterating objectStorageResults and inner
batchEvents in sessionReplayQueryService (variables: objectStorageResults,
batchEvents, data, event.batch_index, events), ensure you verify that
event.batch_index is not null/undefined and that the index/key actually exists
on data (e.g., check typeof event.batch_index === 'number' and
data[event.batch_index] !== undefined or use
Object.prototype.hasOwnProperty.call(data, event.batch_index)) before pushing to
events so falsy values like 0, "", or false are preserved.

In `@server/src/services/storage/s3StorageService.ts`:
- Around line 148-151: Replace console.error usages in S3StorageService catch
blocks with this.logger.error and pass structured context (e.g., bucket and
endpoint) and the caught error; specifically update the catch in the method
handling batch storage (where console.error("[S3Storage] Failed to store
batch:", error) currently appears) and the other two catch blocks flagged
(around the same pattern) to call this.logger.error with a descriptive message
and an object like { bucket: this.bucket, endpoint: this.endpoint, error } so
the logger emits structured fields rather than plain console output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83a3db78-c6a4-4b3f-9ce9-4a66020f7c5b

📥 Commits

Reviewing files that changed from the base of the PR and between 2e31255 and 25adee3.

📒 Files selected for processing (5)
  • docker-compose.yml
  • docs/content/docs/managing-your-installation.mdx
  • server/src/services/replay/sessionReplayIngestService.ts
  • server/src/services/replay/sessionReplayQueryService.ts
  • server/src/services/storage/s3StorageService.ts

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