[Dashboard][Backend][SDK] - Adds sharable session replay ids.#1294
[Dashboard][Backend][SDK] - Adds sharable session replay ids.#1294madster456 wants to merge 9 commits intodevfrom
Conversation
…dpoint response shape
…ionReplayId) method to StackAdminInterface class that sends a GET to /internal/session-replays/{id}
…nReplayId): Promise<AdminSessionReplay> to the StackAdminApp type
…essionReplay() and maps the snake_case API response to camelCase ADminSessionReplay.
…. Uses raw SQL to join SessinoReplay with ProjectUser and ContactChannel, aggregates chunk/event counts via Prisma groupBy. Returns 303 ItemNotFound if not found.
… PageClient as initialReplayId
…and isStandaloneReplayPage derived flag. Added standalone replay fetching via adminApp.getSessionReplay() with loading/error state. Conditionally hides sidebar panel on standalone page. Added "Back to all replays" link under page title via PageLayout description prop. Added copy-link button to the header bar next to settings button. Changed viewer gate from selectedRecording to selectedRecordingId so standalone page can render before metadata loads.
…in get session replay returns 404 for nonexistent id, and non-admin access cannot call single session replay endpoint.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughIntroduces a new internal API endpoint to fetch individual session replay metadata by ID, with corresponding frontend page for standalone replay viewing, type definitions across the stack, and comprehensive end-to-end tests for the endpoint. Changes
Sequence DiagramsequenceDiagram
actor User
participant Dashboard as Dashboard<br/>(Replay Page)
participant API as Backend API<br/>(Internal Endpoint)
participant DB as Database<br/>(Prisma)
User->>Dashboard: Navigate to /replays/[replayId]
Dashboard->>Dashboard: Extract replayId from params
Dashboard->>Dashboard: Initialize PageClient with initialReplayId
Dashboard->>API: GET /internal/session-replays/{replayId}
API->>API: Validate admin auth + path param
API->>DB: Query SessionReplay + ProjectUser join
DB-->>API: Replay metadata + project user
API->>DB: GroupBy SessionReplayChunk for stats
DB-->>API: Chunk count + event count
API->>API: Assemble response with timestamps (millis)
API-->>Dashboard: 200 with replay metadata + stats
Dashboard->>Dashboard: Update standaloneReplay state
Dashboard->>User: Render standalone replay view
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Greptile SummaryThis PR adds shareable session replay links by introducing a new Confidence Score: 5/5Safe to merge — the single remaining finding is a P2 style issue (missing runAsynchronouslyWithAlert) that doesn't block the primary feature path. All P0/P1 concerns are absent: the backend uses SmartRouteHandler with admin-only auth, SQL params are properly interpolated (no injection risk), cross-tenant isolation is enforced via tenancyId, the SDK types align exactly with the wire format, and three e2e tests cover the critical paths. The only open finding is a P2 style violation where the async clipboard handler should use runAsynchronouslyWithAlert. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx — copy-link async handler at line 1825. Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser
participant DashboardPage as Dashboard<br/>/replays/:replayId
participant PageClient as PageClient<br/>(standalone mode)
participant AdminApp as _StackAdminAppImpl
participant Interface as StackAdminInterface
participant Backend as GET /api/v1/internal<br/>/session-replays/:id
participant DB as Database
Browser->>DashboardPage: Navigate to /replays/:replayId
DashboardPage->>PageClient: render with initialReplayId
PageClient->>PageClient: isStandaloneReplayPage=true<br/>skip list/filter load
PageClient->>AdminApp: getSessionReplay(replayId)
AdminApp->>Interface: getSessionReplay(replayId)
Interface->>Backend: GET /internal/session-replays/:id (admin key)
Backend->>DB: SELECT SessionReplay + ProjectUser<br/>+ ContactChannel WHERE id=:id AND tenancyId=:tid
DB-->>Backend: row
Backend->>DB: sessionReplayChunk.groupBy(sessionReplayId)
DB-->>Backend: chunkAgg
Backend-->>Interface: 200 {id, project_user, started_at_millis, ...}
Interface-->>AdminApp: AdminGetSessionReplayResponse
AdminApp-->>PageClient: AdminSessionReplay (camelCase)
PageClient->>PageClient: setStandaloneReplay(replay)
PageClient->>AdminApp: getSessionReplayEvents(replayId)
AdminApp-->>PageClient: chunks + events
PageClient-->>Browser: Render replay viewer (full width, no sidebar)
note over Browser,PageClient: Copy-link button builds URL and writes to clipboard
Reviews (1): Last reviewed commit: "update lock" | Re-trigger Greptile |
| onClick={async () => { | ||
| await navigator.clipboard.writeText( | ||
| `${window.location.origin}/projects/${encodeURIComponent(adminApp.projectId)}/analytics/replays/${encodeURIComponent(selectedRecordingId)}`, | ||
| ); | ||
| }} |
There was a problem hiding this comment.
Async click handler missing
runAsynchronouslyWithAlert
The async onClick handler directly awaits navigator.clipboard.writeText(...) without any error handling. If clipboard access is denied (e.g., the user's browser has blocked clipboard permissions, or the page isn't in focus), the rejection is silently dropped and the user receives no feedback about the failure.
Per the project convention, async button click handlers should use runAsynchronouslyWithAlert instead of bare async arrow functions so errors are automatically surfaced to the user:
| onClick={async () => { | |
| await navigator.clipboard.writeText( | |
| `${window.location.origin}/projects/${encodeURIComponent(adminApp.projectId)}/analytics/replays/${encodeURIComponent(selectedRecordingId)}`, | |
| ); | |
| }} | |
| onClick={() => runAsynchronouslyWithAlert(async () => { | |
| await navigator.clipboard.writeText( | |
| `${window.location.origin}/projects/${encodeURIComponent(adminApp.projectId)}/analytics/replays/${encodeURIComponent(selectedRecordingId)}`, | |
| ); | |
| })} |
You'll also need to add runAsynchronouslyWithAlert to the import on line 22:
import { runAsynchronously, runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";Rule Used: Use runAsynchronouslyWithAlert from `@stackframe... (source)
Learnt From
stack-auth/stack-auth#943
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsx (1)
47-70: Consider adding UUID validation forsession_replay_idparameter.The
sessionReplayIdparameter is passed directly to the SQL query without UUID format validation. While Postgres will reject invalid UUIDs, the resulting error would be a database-level error rather than a cleanITEM_NOT_FOUNDresponse. ThetenancyIdis explicitly cast with::UUID, butsessionReplayIdis not.This is a minor consistency issue since invalid UUIDs are an edge case, but adding a
.uuid()validation to the yup schema or explicit casting in the query would improve error handling.💡 Optional: Add UUID validation
request: yupObject({ auth: yupObject({ type: adminAuthTypeSchema.defined(), tenancy: adaptSchema.defined(), }).defined(), params: yupObject({ - session_replay_id: yupString().defined(), + session_replay_id: yupString().uuid().defined(), }).defined(), }),Or alternatively, add explicit UUID cast in the query:
WHERE sr."tenancyId" = ${auth.tenancy.id}::UUID - AND sr."id" = ${sessionReplayId} + AND sr."id" = ${sessionReplayId}::UUID🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/session-replays/`[session_replay_id]/route.tsx around lines 47 - 70, Validate the session_replay_id as a UUID before using it in the query: update the parsing/validation logic that produces sessionReplayId (the yup schema or request param validator used in route.tsx) to include .uuid() so invalid IDs are rejected with a controlled error, or alternately change the prisma.$queryRaw call (the query that uses sessionReplayId) to explicitly cast the parameter to UUID (e.g. ${sessionReplayId}::UUID) so Postgres rejects invalid input in a consistent way; ensure you reference the sessionReplayId variable used in the SELECT and keep the rest of the query (including tenancyId casting) unchanged.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsx (1)
3-9: Prefer a client-side param reader for this wrapper.This page only forwards
replayIdintoPageClient, soawait props.paramsis avoidable here. A tiny client wrapper usinguseParams()would keep this route aligned with the repo’s Next.js guidance and avoid using a dynamic request API just to pass one string.As per coding guidelines, "NEVER use Next.js dynamic functions if you can avoid them. Instead, prefer using a client component."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsx around lines 3 - 9, The Page component currently awaits props.params to forward replayId to PageClient; replace this server-side async wrapper with a client component that uses Next.js' useParams() to read replayId and render PageClient. Create a simple client wrapper (export default) that calls useParams(), extracts replayId, and passes it as initialReplayId to PageClient, remove the async/Page-level awaiting of props.params and any related server-only signatures so the route no longer uses dynamic server params.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@apps/backend/src/app/api/latest/internal/session-replays/`[session_replay_id]/route.tsx:
- Around line 47-70: Validate the session_replay_id as a UUID before using it in
the query: update the parsing/validation logic that produces sessionReplayId
(the yup schema or request param validator used in route.tsx) to include .uuid()
so invalid IDs are rejected with a controlled error, or alternately change the
prisma.$queryRaw call (the query that uses sessionReplayId) to explicitly cast
the parameter to UUID (e.g. ${sessionReplayId}::UUID) so Postgres rejects
invalid input in a consistent way; ensure you reference the sessionReplayId
variable used in the SELECT and keep the rest of the query (including tenancyId
casting) unchanged.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsx:
- Around line 3-9: The Page component currently awaits props.params to forward
replayId to PageClient; replace this server-side async wrapper with a client
component that uses Next.js' useParams() to read replayId and render PageClient.
Create a simple client wrapper (export default) that calls useParams(), extracts
replayId, and passes it as initialReplayId to PageClient, remove the
async/Page-level awaiting of props.params and any related server-only signatures
so the route no longer uses dynamic server params.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8fd1e171-15dc-4571-9009-168dc87f6830
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsxapps/e2e/tests/backend/endpoints/api/v1/session-replays.test.tspackages/stack-shared/src/interface/admin-interface.tspackages/stack-shared/src/interface/crud/session-replays.tspackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/template/src/lib/stack-app/apps/interfaces/admin-app.ts
Shareable Session Replay Links
Adds the ability to share individual session replays via unique, direct URLs.
What changed
New standalone replay page — /projects/:projectId/analytics/replays/:replayId
Copy link button
SDK plumbing
Tests
Test plan
Summary by CodeRabbit
Release Notes
New Features
Tests