Skip to content

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

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

[Dashboard][Backend][SDK] - Adds sharable session replay ids.#1294
madster456 wants to merge 14 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.

https://www.loom.com/share/1e3298a19b114fc38af4bc43dcd5ec48

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

  • New Features

    • Backend: internal endpoint to fetch a single session replay with user info, millisecond timestamps, and chunk/event counts.
    • Admin SDK/App: added response type and admin method to retrieve a single session replay; admin app maps response into the app model.
    • Dashboard: standalone session-replay page, UI adjustments for standalone mode, and a “copy replay link” button.
  • Tests

    • Added end-to-end tests for retrieval, not-found, and access-control scenarios.

…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 Apr 15, 2026 8:27pm
stack-backend Ready Ready Preview, Comment Apr 15, 2026 8:27pm
stack-dashboard Ready Ready Preview, Comment Apr 15, 2026 8:27pm
stack-demo Ready Ready Preview, Comment Apr 15, 2026 8:27pm
stack-docs Ready Ready Preview, Comment Apr 15, 2026 8:27pm
stack-preview-backend Ready Ready Preview, Comment Apr 15, 2026 8:27pm
stack-preview-dashboard Ready Ready Preview, Comment Apr 15, 2026 8:27pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75c8923a-ebcf-42a2-8625-c9ba7cd6f5b9

📥 Commits

Reviewing files that changed from the base of the PR and between ac97154 and fd9fe69.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx

📝 Walkthrough

Walkthrough

Adds an admin-only internal GET endpoint to fetch a single session replay by ID, backend query/aggregation/mapping helpers, a dashboard standalone replay page/client flow, shared admin API types/methods, and end-to-end tests for access control and responses.

Changes

Cohort / File(s) Summary
Backend: New GET route
apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/route.tsx
New hidden admin GET route using createSmartRouteHandler with yup validation; uses tenancy-scoped Prisma, queries helper functions, and returns typed JSON or ItemNotFound.
Backend: Query & mapping helpers
apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts
New module defining SessionReplayAdminListRow and SessionReplayChunkAgg, querySessionReplayAdminRows, aggregateSessionReplayChunksByReplayIds, and sessionReplayAdminRowToApiItem to centralize raw SQL, aggregation, and mapping.
Backend: route refactor
apps/backend/src/app/api/latest/internal/session-replays/route.tsx
Route simplified to call the new helper functions instead of inline raw SQL/groupBy/mapping.
Frontend: Standalone replay page & client
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
Adds page that passes initialReplayId into PageClient; PageClient accepts optional initialReplayId, fetches standalone replay via getSessionReplay, adjusts selection/loader effects and UI (hide left panel, larger main panel, back link, copy-link button), and surfaces standalone errors.
E2E tests
apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
Adds tests: admin happy path (200 + payload), 404 for unknown UUID, and 401 for non-admin access types; tests create a replay and exercise access types.
Shared types & admin client
packages/stack-shared/src/interface/crud/session-replays.ts, packages/stack-shared/src/interface/admin-interface.ts
Adds AdminGetSessionReplayResponse type and StackAdminInterface.getSessionReplay(sessionReplayId) method to call /internal/session-replays/{id} and return the new response type.
Template: admin app interface & impl
packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts, packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Exposes getSessionReplay(sessionReplayId): Promise<AdminSessionReplay> on StackAdminApp and implements it in admin app impl mapping timestamps → Date and project_userprojectUser.

Sequence Diagram

sequenceDiagram
    actor User
    participant Dashboard as Dashboard (Browser)
    participant PageClient as PageClient (client)
    participant API as Backend API (/internal/session-replays)
    participant DB as Database (Prisma)

    User->>Dashboard: navigate to /replays/{replayId}
    Dashboard->>PageClient: mount with initialReplayId
    PageClient->>API: GET /internal/session-replays/{replayId} (admin auth)
    API->>API: validate auth & params (yup)
    API->>DB: querySessionReplayAdminRows(tenancy, replayId)
    DB-->>API: replay row with project_user fields
    API->>DB: aggregateSessionReplayChunksByReplayIds([replayId])
    DB-->>API: chunk_count & event_count
    API->>API: sessionReplayAdminRowToApiItem -> JSON
    API-->>PageClient: 200 + replay payload
    PageClient->>Dashboard: set standaloneReplay state
    Dashboard->>User: render standalone replay view
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • N2D4

Poem

🐇 I hopped to fetch a replay by name,
Admin-only path, a tiny new claim.
From DB to dashboard the bytes softly flow,
Chunks counted, timestamps set to show.
A rabbit-cheer for one-id replay fame! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding shareable session replay IDs across the dashboard, backend, and SDK components.
Description check ✅ Passed The PR description is well-structured and comprehensive, covering all key changes, test plan, and manual verification steps for the shareable session replay feature.

✏️ 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