Skip to content

[Dashboard][Backend][SDK] - Adds sharable session replay ids.#1294

Open
madster456 wants to merge 9 commits intodevfrom
dashboard/share-replays
Open

[Dashboard][Backend][SDK] - Adds sharable session replay ids.#1294
madster456 wants to merge 9 commits intodevfrom
dashboard/share-replays

Conversation

@madster456
Copy link
Copy Markdown
Collaborator

@madster456 madster456 commented Mar 27, 2026

Shareable Session Replay Links

Adds the ability to share individual session replays via unique, direct URLs.

What changed

  • New admin endpoint — GET /api/v1/internal/session-replays/:id
  • Fetches a single session replay by ID with user metadata (display name, primary email) and chunk/event counts
  • Returns 404 if the replay doesn't exist
  • Admin-only access, consistent with the existing list endpoint

New standalone replay page — /projects/:projectId/analytics/replays/:replayId

  • Thin server page wrapper that passes the replay ID to the existing PageClient
  • PageClient detects standalone mode via initialReplayId prop and fetches replay metadata directly instead of loading the full session list
  • Sidebar is hidden; the replay viewer takes the full width
  • "Back to all replays" link shown under the page title

Copy link button

  • Moved from per-session sidebar items to the replay viewer header (next to the settings gear)
  • Copies a direct URL to the currently selected replay

SDK plumbing

  • AdminGetSessionReplayResponse type in stack-shared
  • getSessionReplay() on StackAdminInterface, StackAdminApp interface, and _StackAdminAppImplIncomplete

Tests

  • Happy path: fetch single replay by ID with inline snapshot
  • 404 for nonexistent replay ID
  • 401 for non-admin access (client and server)

Test plan

  • Open /analytics/replays, select a replay, click the link icon in the header — verify URL is copied to clipboard
  • Paste that URL in a new tab — verify the standalone replay page loads and plays the correct replay
  • Verify "Back to all replays" link navigates back to the list page
  • Verify the original /analytics/replays list page still works as before (selecting, filtering, pagination)
  • Run pnpm test run session-replays

Summary by CodeRabbit

Release Notes

  • New Features

    • Added dedicated page for viewing individual session replays.
    • Added "copy replay link" button to share session recordings.
    • Implemented standalone replay mode for flexible viewing without the replay list panel.
  • Tests

    • Added end-to-end tests for single session replay retrieval and access control.

…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.
…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.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Mar 27, 2026 6:20pm
stack-backend Ready Ready Preview, Comment Mar 27, 2026 6:20pm
stack-dashboard Ready Ready Preview, Comment Mar 27, 2026 6:20pm
stack-demo Ready Ready Preview, Comment Mar 27, 2026 6:20pm
stack-docs Ready Ready Preview, Comment Mar 27, 2026 6:20pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Introduces 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

Cohort / File(s) Summary
Backend API Route
apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsx
New GET endpoint with admin auth validation that queries replay metadata (id, project user details, timestamps) and aggregated chunk statistics, returning 404 if replay not found.
Type Definitions
packages/stack-shared/src/interface/crud/session-replays.ts, packages/stack-shared/src/interface/admin-interface.ts, packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts, packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
New AdminGetSessionReplayResponse type and corresponding interface/implementation methods for fetching single replay via admin API.
Frontend Pages
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
New page component and enhanced page-client supporting standalone replay mode with fetch logic, conditional UI rendering (header title, left panel visibility, copy link button), and error handling for individual replay viewing.
End-to-End Tests
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
Three new test cases covering happy-path replay fetch, 404 handling for nonexistent replay, and 401 access control for non-admin callers.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #1210: Modifies the same page-client.tsx component for timeline/marker state and UI changes—coordination needed to avoid conflicts with standalone replay mode implementation.
  • PR #1206: Introduces or renames session-replay models and API routes affecting the same data structures and endpoints used by this PR's new GET handler.

Suggested reviewers

  • N2D4
  • Developing-Gamer

Poem

🐰 A single replay, now fetchable by ID,
Standalone pages where sessions can be seen,
Type-safe from backend to dashboard so clean,
With admin auth guards and tests that are keen,
One hop, one click—the replay story's green! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding shareable session replay IDs with support across dashboard, backend, and SDK.
Description check ✅ Passed The PR description is well-structured with clear sections covering changes, implementation details, SDK plumbing, tests, and a comprehensive test plan checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ 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 dashboard/share-replays

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR adds shareable session replay links by introducing a new GET /api/v1/internal/session-replays/:id admin endpoint, a standalone /projects/:projectId/analytics/replays/:replayId dashboard page, and a copy-link button in the replay viewer header. The SDK is plumbed end-to-end (AdminGetSessionReplayResponseStackAdminInterface.getSessionReplay()_StackAdminAppImplIncomplete.getSessionReplay()), and three e2e tests cover the happy path, 404, and 401 cases.\n\nKey changes:\n- New backend route (route.tsx) fetches a single replay by ID with user metadata and chunk/event counts, using createSmartRouteHandler and parameterized SQL — consistent with the existing list endpoint.\n- PageClient is extended with an initialReplayId prop; in standalone mode the sidebar is hidden, metadata is fetched independently, and chunk downloading starts on the ID alone.\n- Copy-link button added to the replay viewer header; builds a direct URL to the replay.\n- One style issue: the copy-link button's async onClick does not use runAsynchronouslyWithAlert, so a clipboard permission error would be silently dropped with no user feedback.

Confidence Score: 5/5

Safe 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

Filename Overview
apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsx New GET endpoint for fetching a single session replay by ID; uses SmartRouteHandler, admin-only auth, parameterized SQL (no injection risk), and consistent with the existing list endpoint pattern.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsx Thin Next.js server page wrapper that extracts replayId from URL params and passes it to the existing PageClient as initialReplayId. Clean and minimal.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx Adds standalone replay mode via initialReplayId prop; sidebar hidden, metadata fetched separately, chunk download starts on ID alone. Copy-link async onClick is missing runAsynchronouslyWithAlert, so clipboard errors are silently dropped.
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts Adds three new tests: happy path with inline snapshot, 404 for nonexistent ID, and 401 for client/server access. Tests are well-structured and consistent with existing patterns.
packages/stack-shared/src/interface/admin-interface.ts Adds getSessionReplay() method that calls the new endpoint with URL-encoded ID; follows the same pattern as the surrounding listSessionReplays method.
packages/stack-shared/src/interface/crud/session-replays.ts Adds AdminGetSessionReplayResponse type that mirrors the backend response shape exactly.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts Adds getSessionReplay() on _StackAdminAppImplIncomplete; correctly maps snake_case API response to camelCase AdminSessionReplay SDK type.
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts Adds getSessionReplay to StackAdminApp interface and re-exports AdminSessionReplay; consistent with adjacent method declarations.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "update lock" | Re-trigger Greptile

Comment on lines +1825 to +1829
onClick={async () => {
await navigator.clipboard.writeText(
`${window.location.origin}/projects/${encodeURIComponent(adminApp.projectId)}/analytics/replays/${encodeURIComponent(selectedRecordingId)}`,
);
}}
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.

P2 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:

Suggested change
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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsx (1)

47-70: Consider adding UUID validation for session_replay_id parameter.

The sessionReplayId parameter 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 clean ITEM_NOT_FOUND response. The tenancyId is explicitly cast with ::UUID, but sessionReplayId is 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 replayId into PageClient, so await props.params is avoidable here. A tiny client wrapper using useParams() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9cf0d43 and fa22972.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/[replayId]/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
  • packages/stack-shared/src/interface/admin-interface.ts
  • packages/stack-shared/src/interface/crud/session-replays.ts
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts

@madster456 madster456 requested a review from N2D4 March 27, 2026 18:41
@madster456 madster456 requested a review from BilalG1 March 27, 2026 18:41
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.

3 participants