feat: session recording DB schema, S3 utils, batch endpoint#1184
feat: session recording DB schema, S3 utils, batch endpoint#1184
Conversation
…ad endpoint - Add SessionRecording and SessionRecordingChunk Prisma models - Add migration for session_recordings_mvp - Add seed data for session recordings - Add S3 uploadBytes, downloadBytes, and getS3PublicUrl utilities - Add POST /session-recordings/batch endpoint for client uploads
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds an MVP for session recordings: database migrations and Prisma models, a batch upload API that validates/deduplicates and stores gzipped payloads to S3, S3 upload/download helpers, seed data generation, and local/dev docker/env updates for a private S3 bucket. Changes
Sequence DiagramsequenceDiagram
participant Client
participant API as POST /session-recordings/batch
participant Prisma as Database
participant S3 as AWS S3
Client->>API: POST {session_id, batch_id, tab_id, events, started_at_ms, sent_at_ms}
API->>API: Validate auth, refreshTokenId, size limits, and events
API->>API: Compute firstMs/lastMs from events (fallback to sent_at_ms)
API->>Prisma: Read existing SessionRecording (by tenancyId + session_id)
alt existing session
Prisma-->>API: session record
API->>Prisma: Upsert updated startedAt/lastEventAt
else new session
API->>Prisma: Create SessionRecording
end
API->>Prisma: Check SessionRecordingChunk unique (tenancyId, sessionRecordingId, batchId)
alt chunk exists
Prisma-->>API: existing chunk with s3Key
API-->>Client: {session_id, batch_id, s3_key, deduped: true}
else new chunk
API->>API: Create gzipped payload
API->>S3: uploadBytes(gzipped payload) -> s3Key
S3-->>API: s3Key
API->>Prisma: Insert SessionRecordingChunk (metadata)
alt unique constraint violation
Prisma-->>API: duplicate error
API-->>Client: {session_id, batch_id, s3_key, deduped: true}
else success
Prisma-->>API: chunk created
API-->>Client: {session_id, batch_id, s3_key, deduped: false}
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/session-recordings/batch/route.tsx`:
- Around line 104-127: The current flow reads with
prisma.sessionRecording.findUnique then computes newStartedAtMs/newLastEventAtMs
and calls prisma.sessionRecording.upsert, which opens a TOCTOU race that can
overwrite concurrent updates; replace this two-step logic with a single atomic
upsert using raw SQL (e.g., an INSERT ... ON CONFLICT ... DO UPDATE) that sets
startedAt = LEAST(existing.startedAt, EXCLUDED.startedAt) and lastEventAt =
GREATEST(existing.lastEventAt, EXCLUDED.lastEventAt) so startedAt/lastEventAt
bounds are merged atomically (remove the findUnique and compute-ms logic around
newStartedAtMs/newLastEventAtMs and use the SQL LEAST/GREATEST approach in place
of prisma upsert).
- Around line 79-81: The MAX_BODY_BYTES check currently runs after buffering
(fullReq.bodyBuffer) which allows req.arrayBuffer() in createSmartRouteHandler
to consume large bodies; update the flow to validate size before any buffering
by checking the Content-Length header (or an upstream middleware limit) and
returning StatusError.PayloadTooLarge if it exceeds MAX_BODY_BYTES, or move the
MAX_BODY_BYTES check into createSmartRouteHandler so it verifies Content-Length
and rejects early before calling req.arrayBuffer(); reference MAX_BODY_BYTES,
fullReq.bodyBuffer, createSmartRouteHandler and req.arrayBuffer() when making
the change.
🧹 Nitpick comments (5)
apps/backend/prisma/migrations/20260210120000_session_recordings_mvp/migration.sql (1)
52-53: Unique index name is misleading — it omitstenancyIdfrom the name despite including it in the columns.The index
SessionRecordingChunk_sessionRecordingId_batchId_keyactually covers(tenancyId, sessionRecordingId, batchId). Consider renaming it toSessionRecordingChunk_tenancyId_sessionRecordingId_batchId_keyto match the Prisma-generated naming convention and avoid confusion during debugging.Suggested rename
-CREATE UNIQUE INDEX "SessionRecordingChunk_sessionRecordingId_batchId_key" +CREATE UNIQUE INDEX "SessionRecordingChunk_tenancyId_sessionRecordingId_batchId_key" ON "SessionRecordingChunk"("tenancyId", "sessionRecordingId", "batchId");apps/backend/prisma/schema.prisma (1)
283-305:refreshTokenIdhas no FK constraint toProjectUserRefreshToken.
refreshTokenIdis stored as a plain UUID without a foreign key relation. If a refresh token is revoked or deleted, this becomes a dangling reference. If this is intentional (to preserve recording metadata after token cleanup), a brief comment explaining the design choice would be helpful for future readers.apps/backend/src/s3.tsx (1)
63-88: Add comments explaining theas anycasts per coding guidelines.The duck-typing checks here require
as anybecause the AWS SDK v3Bodytype isStreamingBlobPayloadOutputTypes | undefined, a complex union that doesn't exposetransformToByteArrayorSymbol.asyncIteratorin its static type. A short comment on each cast would satisfy the project's guideline. As per coding guidelines: "Try to avoid theanytype. When usingany, leave a comment explaining why it's being used."Suggested comments
// Web ReadableStream (some runtimes) + // `any` cast: AWS SDK v3 Body is a complex union; `transformToByteArray` is a SdkStreamMixin method not visible in all type overlaps. if (typeof body === "object" && body !== null && "transformToByteArray" in body && typeof (body as any).transformToByteArray === "function") { return (body as any).transformToByteArray(); } // Node.js Readable or any AsyncIterable<Uint8Array> + // `any` cast: Symbol.asyncIterator is not in the static Body type union from AWS SDK. if (typeof body === "object" && body !== null && Symbol.asyncIterator in (body as any)) { const chunks: Buffer[] = []; for await (const chunk of body as any) {apps/backend/prisma/seed.ts (1)
1819-1819: Prefer?? throwErr(...)over the non-null assertion.Even though
userIdsis verified non-empty on line 1804, the coding guidelines prefer defensive coding over!.Suggested fix
- const projectUserId = userIds[Math.floor(Math.random() * userIds.length)]!; + const projectUserId = userIds[Math.floor(Math.random() * userIds.length)] ?? throwErr('userIds index out of bounds');As per coding guidelines: "Code defensively. Prefer
?? throwErr(...)over non-null assertions with good error messages explicitly stating the assumption that must've been violated."apps/backend/src/app/api/latest/session-recordings/batch/route.tsx (1)
22-29: Add a comment explaining theas anycast.
eis typed asunknownfrom theunknown[]parameter, so the cast is unavoidable, but the coding guidelines ask for a comment. As per coding guidelines: "Try to avoid theanytype. When usingany, leave a comment explaining why it's being used."Suggested fix
if (!("timestamp" in e)) continue; + // `any` cast: events are untyped rrweb payloads; we duck-type check for `timestamp` above. const ts = (e as any).timestamp;
Greptile OverviewGreptile SummaryThis PR introduces initial “session recording” support by adding The endpoint’s flow is: validate auth/body → upsert Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as SDK Client
participant API as POST /api/latest/session-recordings/batch
participant Prisma as Prisma (tenant DB)
participant S3 as S3 Bucket
Client->>API: batch upload {session_id,batch_id,events...}
API->>API: validate auth + size + events
API->>Prisma: upsert SessionRecording (tenancyId, sessionId)
API->>Prisma: findUnique SessionRecordingChunk by (tenancyId, sessionId, batchId)
alt chunk exists
Prisma-->>API: existing s3Key
API-->>Client: 200 {deduped:true, s3_key}
else new chunk
API->>API: gzip(JSON payload)
API->>S3: PutObject(s3Key, gzipped bytes)
API->>Prisma: create SessionRecordingChunk
Prisma-->>API: created
API-->>Client: 200 {deduped:false, s3_key}
end
|
Summary
SessionRecordingandSessionRecordingChunkPrisma models with migrationuploadBytes,downloadBytes, andgetS3PublicUrlutilitiesPOST /session-recordings/batchendpoint for client recording uploadsStack: PR 1/4 —
dev←analytics-replays-1←analytics-replays-2←analytics-replays-3←analytics-replays-4Test plan
Summary by CodeRabbit
New Features
Chores