[Dashboard][Backend][SDK] - Adds sharable session replay ids.#1294
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.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an admin-only internal GET endpoint to fetch a single session replay by ID, new backend query/aggregation/mapping helpers, dashboard standalone replay page/client support, shared admin API types/methods, template admin-app wiring, and E2E tests for access control and responses. Changes
Sequence DiagramsequenceDiagram
actor User
participant Browser as Dashboard (Browser)
participant PageClient as PageClient (client)
participant API as Backend API (/internal/session-replays)
participant DB as Database (Prisma)
User->>Browser: navigate to /replays/{replayId}
Browser->>PageClient: mount with initialReplayId
PageClient->>API: GET /internal/session-replays/{replayId} (admin credentials)
API->>API: validate auth & params (yup)
API->>DB: querySessionReplayAdminRows(tenancy, replayId)
DB-->>API: session replay row (incl. project_user)
API->>DB: aggregateSessionReplayChunksByReplayIds([replayId])
DB-->>API: chunk_count & event_count
API->>API: sessionReplayAdminRowToApiItem -> JSON response
API-->>PageClient: 200 + replay payload
PageClient->>Browser: set standaloneReplay state and render standalone view
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
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
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx (1)
1830-1850: Drop the redundantrunAsynchronouslyWithAlertwrapper.
Buttonfrom@stackframe/stack-uialready wraps asynconClickhandlers withrunAsynchronouslyWithAlertinternally and manages the loading state, so the explicit wrapping here duplicates that behavior. Passing the async function directly is simpler and gets the built-in loading indicator.♻️ Proposed simplification
- onClick={() => runAsynchronouslyWithAlert(async () => { + onClick={async () => { await navigator.clipboard.writeText( `${window.location.origin}/projects/${encodeURIComponent(adminApp.projectId)}/analytics/replays/${encodeURIComponent(selectedRecordingId)}`, ); setReplayShareLinkCopied(true); - })} + }}Based on learnings: "Button components from stackframe/stack-ui wrap onClick handlers with
runAsynchronouslyWithAlertinternally, so async onClick functions that throw will automatically surface an alert to the user. There is no need to manually wrap async onClick handlers withrunAsynchronouslyorrunAsynchronouslyWithAlertwhen passing them to a Button's onClick prop."🤖 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/page-client.tsx around lines 1830 - 1850, Remove the redundant runAsynchronouslyWithAlert wrapper around the Button onClick; instead pass the async handler directly to the Button's onClick prop (keep the same logic that calls navigator.clipboard.writeText with the constructed URL using adminApp.projectId and selectedRecordingId, then setReplayShareLinkCopied(true)), and leave the conditional rendering based on selectedRecordingId and the replayShareLinkCopied UI states intact so the Button's built-in loading/alert behavior is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 489-495: selectedRecordingId is only initialized from
initialReplayId and never updated when initialReplayId changes during soft
navigations; add a useEffect that watches initialReplayId (and standaloneReplay
if present) and calls setSelectedRecordingId(initialReplayId ?? null) when
initialReplayId changes in standalone mode so the component,
loadChunksAndDownload calls, and copy-link use the current replay id; reference
selectedRecordingId, setSelectedRecordingId, initialReplayId, and
standaloneReplay when adding the effect.
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 1830-1850: Remove the redundant runAsynchronouslyWithAlert wrapper
around the Button onClick; instead pass the async handler directly to the
Button's onClick prop (keep the same logic that calls
navigator.clipboard.writeText with the constructed URL using adminApp.projectId
and selectedRecordingId, then setReplayShareLinkCopied(true)), and leave the
conditional rendering based on selectedRecordingId and the replayShareLinkCopied
UI states intact so the Button's built-in loading/alert behavior is used.
🪄 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: 9f4f6efb-9f7e-4adb-88c9-0f33d1df24b0
📒 Files selected for processing (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
…nitialReplayId, while accounting for standaloneReplay, so soft navigations update chunk loading and copy-link behavior.
Shareable Session Replay Links
Adds the ability to share individual session replays via unique, direct URLs.
https://www.loom.com/share/1e3298a19b114fc38af4bc43dcd5ec48
What changed
New standalone replay page — /projects/:projectId/analytics/replays/:replayId
Copy link button
SDK plumbing
Tests
Test plan
Summary by CodeRabbit
New Features
Tests