From 68ab2d333c3ef2e2067c4965cf76093a370ef880 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Tue, 17 Feb 2026 09:27:16 -0800 Subject: [PATCH 1/2] rename tab id --- .../migration.sql | 1 + apps/backend/prisma/schema.prisma | 4 +-- apps/backend/scripts/clickhouse-migrations.ts | 18 +++++++++++ .../[session_recording_id]/chunks/route.tsx | 6 ++-- .../[session_recording_id]/events/route.tsx | 6 ++-- .../latest/session-recordings/batch/route.tsx | 8 ++--- apps/backend/src/lib/events.tsx | 12 +++++++ apps/backend/src/lib/tokens.tsx | 3 ++ .../analytics/replays/page-client.tsx | 4 +-- .../src/lib/session-replay-streams.test.ts | 26 +++++++-------- .../src/lib/session-replay-streams.ts | 20 ++++++------ .../api/v1/session-recordings.test.ts | 32 +++++++++---------- .../src/interface/crud/session-recordings.ts | 4 +-- .../apps/implementations/admin-app-impl.ts | 4 +-- .../apps/implementations/session-recording.ts | 6 ++-- .../lib/stack-app/session-recordings/index.ts | 4 +-- 16 files changed, 96 insertions(+), 62 deletions(-) create mode 100644 apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql diff --git a/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql b/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql new file mode 100644 index 0000000000..11a201f1dc --- /dev/null +++ b/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "SessionRecordingChunk" RENAME COLUMN "tabId" TO "sessionReplaySegmentId"; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 5f22387ff3..4a1226d2a7 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -314,8 +314,8 @@ model SessionRecordingChunk { // Unique per uploaded batch for a given session id. batchId String @db.Uuid - // Ephemeral in-memory id generated by the client. Stored for future tab separation if needed. - tabId String + // Ephemeral in-memory id generated by the client. Used to group recording chunks into per-tab replay segments. + sessionReplaySegmentId String // Client-generated session id from localStorage, stored as metadata. browserSessionId String diff --git a/apps/backend/scripts/clickhouse-migrations.ts b/apps/backend/scripts/clickhouse-migrations.ts index 29f5918498..8f3f15c5a0 100644 --- a/apps/backend/scripts/clickhouse-migrations.ts +++ b/apps/backend/scripts/clickhouse-migrations.ts @@ -16,7 +16,9 @@ export async function runClickhouseMigrations() { await client.exec({ query: EVENTS_VIEW_SQL }); await client.exec({ query: USERS_TABLE_BASE_SQL }); await client.exec({ query: USERS_VIEW_SQL }); + await client.exec({ query: EVENTS_ADD_REPLAY_COLUMNS_SQL }); await client.exec({ query: TOKEN_REFRESH_EVENT_ROW_FORMAT_MUTATION_SQL }); + await client.exec({ query: BACKFILL_REFRESH_TOKEN_ID_COLUMN_SQL }); await client.exec({ query: SIGN_UP_RULE_TRIGGER_EVENT_ROW_FORMAT_MUTATION_SQL }); const queries = [ "REVOKE ALL PRIVILEGES ON *.* FROM limited_user;", @@ -177,6 +179,22 @@ ENGINE ReplacingMergeTree(updated_at) ORDER BY (tenancy_id, mapping_name); `; +const EVENTS_ADD_REPLAY_COLUMNS_SQL = ` +ALTER TABLE analytics_internal.events + ADD COLUMN IF NOT EXISTS refresh_token_id Nullable(String) AFTER team_id, + ADD COLUMN IF NOT EXISTS session_replay_id Nullable(String) AFTER refresh_token_id, + ADD COLUMN IF NOT EXISTS session_replay_segment_id Nullable(String) AFTER session_replay_id; +`; + +// Backfill refresh_token_id from data.refresh_token_id for existing $token-refresh rows +const BACKFILL_REFRESH_TOKEN_ID_COLUMN_SQL = ` +ALTER TABLE analytics_internal.events +UPDATE refresh_token_id = data.refresh_token_id::Nullable(String) +WHERE event_type = '$token-refresh' + AND refresh_token_id IS NULL + AND data.refresh_token_id::Nullable(String) IS NOT NULL; +`; + const EXTERNAL_ANALYTICS_DB_SQL = ` CREATE DATABASE IF NOT EXISTS analytics_internal; `; diff --git a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx b/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx index fb5e9b71c3..2e4c72b78f 100644 --- a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx @@ -29,7 +29,7 @@ export const GET = createSmartRouteHandler({ items: yupArray(yupObject({ id: yupString().defined(), batch_id: yupString().defined(), - tab_id: yupString().nullable().defined(), + session_replay_segment_id: yupString().nullable().defined(), browser_session_id: yupString().nullable().defined(), event_count: yupNumber().defined(), byte_length: yupNumber().defined(), @@ -92,7 +92,7 @@ export const GET = createSmartRouteHandler({ select: { id: true, batchId: true, - tabId: true, + sessionReplaySegmentId: true, browserSessionId: true, eventCount: true, byteLength: true, @@ -113,7 +113,7 @@ export const GET = createSmartRouteHandler({ items: page.map((c) => ({ id: c.id, batch_id: c.batchId, - tab_id: c.tabId, + session_replay_segment_id: c.sessionReplaySegmentId, browser_session_id: c.browserSessionId, event_count: c.eventCount, byte_length: c.byteLength, diff --git a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx b/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx index 29bb40f6b8..16ac617047 100644 --- a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx @@ -33,7 +33,7 @@ export const GET = createSmartRouteHandler({ chunks: yupArray(yupObject({ id: yupString().defined(), batch_id: yupString().defined(), - tab_id: yupString().nullable().defined(), + session_replay_segment_id: yupString().nullable().defined(), event_count: yupNumber().defined(), byte_length: yupNumber().defined(), first_event_at_millis: yupNumber().defined(), @@ -67,7 +67,7 @@ export const GET = createSmartRouteHandler({ select: { id: true, batchId: true, - tabId: true, + sessionReplaySegmentId: true, eventCount: true, byteLength: true, firstEventAt: true, @@ -143,7 +143,7 @@ export const GET = createSmartRouteHandler({ chunks: chunks.map((c) => ({ id: c.id, batch_id: c.batchId, - tab_id: c.tabId, + session_replay_segment_id: c.sessionReplaySegmentId, event_count: c.eventCount, byte_length: c.byteLength, first_event_at_millis: c.firstEventAt.getTime(), diff --git a/apps/backend/src/app/api/latest/session-recordings/batch/route.tsx b/apps/backend/src/app/api/latest/session-recordings/batch/route.tsx index f7649fcfc4..21a509bfc4 100644 --- a/apps/backend/src/app/api/latest/session-recordings/batch/route.tsx +++ b/apps/backend/src/app/api/latest/session-recordings/batch/route.tsx @@ -53,7 +53,7 @@ export const POST = createSmartRouteHandler({ }).defined(), body: yupObject({ browser_session_id: yupString().defined().matches(UUID_RE, "Invalid browser_session_id"), - tab_id: yupString().defined().matches(UUID_RE, "Invalid tab_id"), + session_replay_segment_id: yupString().defined().matches(UUID_RE, "Invalid session_replay_segment_id"), batch_id: yupString().defined().matches(UUID_RE, "Invalid batch_id"), started_at_ms: yupNumber().defined().integer().min(0), sent_at_ms: yupNumber().defined().integer().min(0), @@ -105,7 +105,7 @@ export const POST = createSmartRouteHandler({ const browserSessionId = body.browser_session_id; const batchId = body.batch_id; - const tabId = body.tab_id; + const sessionReplaySegmentId = body.session_replay_segment_id; const tenancyId = auth.tenancy.id; const projectId = auth.tenancy.project.id; @@ -174,7 +174,7 @@ export const POST = createSmartRouteHandler({ v: 1, session_recording_id: recordingId, browser_session_id: browserSessionId, - tab_id: tabId, + session_replay_segment_id: sessionReplaySegmentId, batch_id: batchId, started_at_ms: body.started_at_ms, sent_at_ms: body.sent_at_ms, @@ -197,7 +197,7 @@ export const POST = createSmartRouteHandler({ tenancyId, sessionRecordingId: recordingId, batchId, - tabId, + sessionReplaySegmentId, browserSessionId, s3Key, eventCount: body.events.length, diff --git a/apps/backend/src/lib/events.tsx b/apps/backend/src/lib/events.tsx index 281db375ae..8e2fc272d0 100644 --- a/apps/backend/src/lib/events.tsx +++ b/apps/backend/src/lib/events.tsx @@ -196,6 +196,9 @@ export async function logEvent( data: DataOfMany, options: { time?: Date | { start: Date, end: Date }, + refreshTokenId?: string, + sessionReplayId?: string, + sessionReplaySegmentId?: string, } = {} ) { let timeOrTimeRange = options.time ?? new Date(); @@ -320,6 +323,12 @@ export async function logEvent( ); } const clickhouseClient = getClickhouseAdminClient(); + // Resolve refresh_token_id: prefer explicit option, fall back to data for $token-refresh events + const resolvedRefreshTokenId = options.refreshTokenId + ?? (matchingEventType.id === "$token-refresh" && typeof (clickhouseEventData as any).refresh_token_id === "string" + ? (clickhouseEventData as any).refresh_token_id as string + : null); + await clickhouseClient.insert({ table: "analytics_internal.events", values: [{ @@ -330,6 +339,9 @@ export async function logEvent( branch_id: branchId, user_id: userId || null, team_id: null, + refresh_token_id: resolvedRefreshTokenId ?? null, + session_replay_id: options.sessionReplayId ?? null, + session_replay_segment_id: options.sessionReplaySegmentId ?? null, }], format: "JSONEachRow", clickhouse_settings: { diff --git a/apps/backend/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx index 71b201f673..e6d6cf1609 100644 --- a/apps/backend/src/lib/tokens.tsx +++ b/apps/backend/src/lib/tokens.tsx @@ -293,6 +293,9 @@ export async function generateAccessTokenFromRefreshTokenIfValid(options: Refres isAnonymous: user.is_anonymous, teamId: undefined, ipInfo, + }, + { + refreshTokenId: options.refreshTokenObj.id, } ); diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx index 945e84e231..f5a30c473e 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx @@ -60,7 +60,7 @@ type RecordingRow = { type ChunkRow = { id: string, batchId: string, - tabId: string | null, + sessionReplaySegmentId: string | null, eventCount: number, byteLength: number, firstEventAt: Date, @@ -860,7 +860,7 @@ export default function PageClient() { const allChunkRows: ChunkRow[] = initialResponse.chunks.map((c) => ({ id: c.id, batchId: c.batchId, - tabId: c.tabId, + sessionReplaySegmentId: c.sessionReplaySegmentId, eventCount: c.eventCount, byteLength: c.byteLength, firstEventAt: c.firstEventAt, diff --git a/apps/dashboard/src/lib/session-replay-streams.test.ts b/apps/dashboard/src/lib/session-replay-streams.test.ts index 7ea951704b..febd529f08 100644 --- a/apps/dashboard/src/lib/session-replay-streams.test.ts +++ b/apps/dashboard/src/lib/session-replay-streams.test.ts @@ -14,11 +14,11 @@ function d(ms: number) { } describe("session-replay-streams", () => { - it("treats null tabId as its own stream", () => { + it("treats null sessionReplaySegmentId as its own stream", () => { const streams = groupChunksIntoTabStreams([ - { tabId: null, firstEventAt: d(10), lastEventAt: d(20), eventCount: 2 }, - { tabId: null, firstEventAt: d(21), lastEventAt: d(30), eventCount: 1 }, - { tabId: "a", firstEventAt: d(5), lastEventAt: d(25), eventCount: 10 }, + { sessionReplaySegmentId: null, firstEventAt: d(10), lastEventAt: d(20), eventCount: 2 }, + { sessionReplaySegmentId: null, firstEventAt: d(21), lastEventAt: d(30), eventCount: 1 }, + { sessionReplaySegmentId: "a", firstEventAt: d(5), lastEventAt: d(25), eventCount: 10 }, ]); expect(streams.map(s => s.tabKey).sort()).toEqual([NULL_TAB_KEY, "a"].sort()); @@ -27,19 +27,19 @@ describe("session-replay-streams", () => { it("sorts streams by lastEventAt desc then eventCount desc", () => { const streams = groupChunksIntoTabStreams([ - { tabId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 5 }, - { tabId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 }, - { tabId: "c", firstEventAt: d(0), lastEventAt: d(20), eventCount: 9 }, + { sessionReplaySegmentId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 5 }, + { sessionReplaySegmentId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 }, + { sessionReplaySegmentId: "c", firstEventAt: d(0), lastEventAt: d(20), eventCount: 9 }, ]); - expect(streams.map(s => s.tabId)).toEqual(["c", "b", "a"]); + expect(streams.map(s => s.sessionReplaySegmentId)).toEqual(["c", "b", "a"]); }); it("limits streams and reports hiddenCount", () => { const streams = groupChunksIntoTabStreams([ - { tabId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 1 }, - { tabId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 }, - { tabId: "c", firstEventAt: d(0), lastEventAt: d(30), eventCount: 1 }, + { sessionReplaySegmentId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 1 }, + { sessionReplaySegmentId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 }, + { sessionReplaySegmentId: "c", firstEventAt: d(0), lastEventAt: d(30), eventCount: 1 }, ]); const limited = limitTabStreams(streams, 2); @@ -49,8 +49,8 @@ describe("session-replay-streams", () => { it("maps global offsets to local offsets and back", () => { const streams = groupChunksIntoTabStreams([ - { tabId: "a", firstEventAt: d(1000), lastEventAt: d(5000), eventCount: 1 }, - { tabId: "b", firstEventAt: d(2000), lastEventAt: d(4000), eventCount: 1 }, + { sessionReplaySegmentId: "a", firstEventAt: d(1000), lastEventAt: d(5000), eventCount: 1 }, + { sessionReplaySegmentId: "b", firstEventAt: d(2000), lastEventAt: d(4000), eventCount: 1 }, ]); const { globalStartTs } = computeGlobalTimeline(streams); diff --git a/apps/dashboard/src/lib/session-replay-streams.ts b/apps/dashboard/src/lib/session-replay-streams.ts index a6adb948ea..080233f9c5 100644 --- a/apps/dashboard/src/lib/session-replay-streams.ts +++ b/apps/dashboard/src/lib/session-replay-streams.ts @@ -2,12 +2,12 @@ export const NULL_TAB_KEY = "__null_tab__"; export type TabKey = string; -export function toTabKey(tabId: string | null): TabKey { - return tabId ?? NULL_TAB_KEY; +export function toTabKey(sessionReplaySegmentId: string | null): TabKey { + return sessionReplaySegmentId ?? NULL_TAB_KEY; } export type TabStream = { - tabId: string | null, + sessionReplaySegmentId: string | null, tabKey: TabKey, chunks: TChunk[], firstEventAt: Date, @@ -17,7 +17,7 @@ export type TabStream = { }; type ChunkLike = { - tabId: string | null, + sessionReplaySegmentId: string | null, firstEventAt: Date, lastEventAt: Date, eventCount: number, @@ -41,20 +41,20 @@ function compareChunks(a: ChunkLike, b: ChunkLike) { } export function groupChunksIntoTabStreams(chunks: TChunk[]): TabStream[] { - const byTab = new Map(); + const byTab = new Map(); for (const c of chunks) { - const tabKey = toTabKey(c.tabId); + const tabKey = toTabKey(c.sessionReplaySegmentId); const existing = byTab.get(tabKey); if (existing) { existing.chunks.push(c); } else { - byTab.set(tabKey, { tabId: c.tabId, chunks: [c] }); + byTab.set(tabKey, { sessionReplaySegmentId: c.sessionReplaySegmentId, chunks: [c] }); } } const streams: TabStream[] = []; - for (const { tabId, chunks: tabChunks } of byTab.values()) { + for (const { sessionReplaySegmentId, chunks: tabChunks } of byTab.values()) { tabChunks.sort(compareChunks); let firstEventAtMs = Infinity; @@ -71,8 +71,8 @@ export function groupChunksIntoTabStreams(chunks: TChu const lastEventAt = new Date(Number.isFinite(lastEventAtMs) ? lastEventAtMs : 0); streams.push({ - tabId, - tabKey: toTabKey(tabId), + sessionReplaySegmentId, + tabKey: toTabKey(sessionReplaySegmentId), chunks: tabChunks, firstEventAt, lastEventAt, diff --git a/apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts index 5b19fdf32f..ca7c7e1f6a 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts @@ -8,14 +8,14 @@ async function uploadBatch(options: { startedAtMs: number, sentAtMs: number, events: unknown[], - tabId?: string, + sessionReplaySegmentId?: string, }) { return await niceBackendFetch("/api/v1/session-recordings/batch", { method: "POST", accessType: "client", body: { browser_session_id: options.browserSessionId, - tab_id: options.tabId ?? randomUUID(), + session_replay_segment_id: options.sessionReplaySegmentId ?? randomUUID(), batch_id: options.batchId, started_at_ms: options.startedAtMs, sent_at_ms: options.sentAtMs, @@ -34,7 +34,7 @@ it("requires a user token", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: Date.now(), sent_at_ms: Date.now(), @@ -56,7 +56,7 @@ it("returns 200 no-op when analytics is not enabled", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: Date.now(), sent_at_ms: Date.now(), @@ -77,14 +77,14 @@ it("stores session recording batch metadata and dedupes by (session_recording_id const now = Date.now(); const browserSessionId = randomUUID(); const batchId = randomUUID(); - const tabId = randomUUID(); + const sessionReplaySegmentId = randomUUID(); const first = await niceBackendFetch("/api/v1/session-recordings/batch", { method: "POST", accessType: "client", body: { browser_session_id: browserSessionId, - tab_id: tabId, + session_replay_segment_id: sessionReplaySegmentId, batch_id: batchId, started_at_ms: now, sent_at_ms: now + 500, @@ -110,7 +110,7 @@ it("stores session recording batch metadata and dedupes by (session_recording_id accessType: "client", body: { browser_session_id: browserSessionId, - tab_id: tabId, + session_replay_segment_id: sessionReplaySegmentId, batch_id: batchId, started_at_ms: now, sent_at_ms: now + 500, @@ -136,7 +136,7 @@ it("rejects empty events", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: Date.now(), sent_at_ms: Date.now(), @@ -160,7 +160,7 @@ it("rejects too many events", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: 1_700_000_000_000, sent_at_ms: 1_700_000_000_100, @@ -182,7 +182,7 @@ it("rejects invalid browser_session_id", async ({ expect }) => { accessType: "client", body: { browser_session_id: "not-a-uuid", - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: Date.now(), sent_at_ms: Date.now(), @@ -204,7 +204,7 @@ it("rejects invalid batch_id", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: "not-a-uuid", started_at_ms: Date.now(), sent_at_ms: Date.now(), @@ -216,7 +216,7 @@ it("rejects invalid batch_id", async ({ expect }) => { expect(res.status).toBeLessThan(500); }); -it("rejects invalid tab_id", async ({ expect }) => { +it("rejects invalid session_replay_segment_id", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); @@ -226,7 +226,7 @@ it("rejects invalid tab_id", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: "not-a-uuid", + session_replay_segment_id: "not-a-uuid", batch_id: randomUUID(), started_at_ms: Date.now(), sent_at_ms: Date.now(), @@ -251,7 +251,7 @@ it("accepts events without timestamps (falls back to sent_at_ms)", async ({ expe accessType: "client", body: { browser_session_id: browserSessionId, - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: batchId, started_at_ms: 1_700_000_000_000, sent_at_ms: 1_700_000_000_500, @@ -277,7 +277,7 @@ it("rejects non-integer started_at_ms", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: 123.4, sent_at_ms: Date.now(), @@ -302,7 +302,7 @@ it("rejects oversized payloads", async ({ expect }) => { accessType: "client", body: { browser_session_id: randomUUID(), - tab_id: randomUUID(), + session_replay_segment_id: randomUUID(), batch_id: randomUUID(), started_at_ms: Date.now(), sent_at_ms: Date.now(), diff --git a/packages/stack-shared/src/interface/crud/session-recordings.ts b/packages/stack-shared/src/interface/crud/session-recordings.ts index ce6c190170..138e0ad94d 100644 --- a/packages/stack-shared/src/interface/crud/session-recordings.ts +++ b/packages/stack-shared/src/interface/crud/session-recordings.ts @@ -30,7 +30,7 @@ export type AdminListSessionRecordingChunksResponse = { items: Array<{ id: string, batch_id: string, - tab_id: string | null, + session_replay_segment_id: string | null, browser_session_id: string | null, event_count: number, byte_length: number, @@ -51,7 +51,7 @@ export type AdminGetSessionRecordingAllEventsResponse = { chunks: Array<{ id: string, batch_id: string, - tab_id: string | null, + session_replay_segment_id: string | null, event_count: number, byte_length: number, first_event_at_millis: number, diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index 15fc4e4445..9fd48d44b3 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -1076,7 +1076,7 @@ export class _StackAdminAppImplIncomplete ({ id: c.id, batchId: c.batch_id, - tabId: c.tab_id, + sessionReplaySegmentId: c.session_replay_segment_id, browserSessionId: c.browser_session_id, eventCount: c.event_count, byteLength: c.byte_length, @@ -1101,7 +1101,7 @@ export class _StackAdminAppImplIncomplete ({ id: c.id, batchId: c.batch_id, - tabId: c.tab_id, + sessionReplaySegmentId: c.session_replay_segment_id, eventCount: c.event_count, byteLength: c.byte_length, firstEventAt: new Date(c.first_event_at_millis), diff --git a/packages/template/src/lib/stack-app/apps/implementations/session-recording.ts b/packages/template/src/lib/stack-app/apps/implementations/session-recording.ts index 315ca05b9b..226df280bf 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/session-recording.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/session-recording.ts @@ -150,7 +150,7 @@ export class SessionRecorder { private _rrwebModule: typeof import("rrweb") | null = null; private _lastKnownAccessToken: string | null = null; private _wasAuthenticated = false; - private readonly _tabId: string; + private readonly _sessionReplaySegmentId: string; private readonly _storageKey: string; private readonly _deps: SessionRecorderDeps; private readonly _replayOptions: AnalyticsReplayOptions; @@ -158,7 +158,7 @@ export class SessionRecorder { constructor(deps: SessionRecorderDeps, replayOptions: AnalyticsReplayOptions) { this._deps = deps; this._replayOptions = replayOptions; - this._tabId = generateUuid(); + this._sessionReplaySegmentId = generateUuid(); this._storageKey = makeStorageKey(deps.projectId); } @@ -206,7 +206,7 @@ export class SessionRecorder { const batchId = generateUuid(); const payload = { browser_session_id: stored.session_id, - tab_id: this._tabId, + session_replay_segment_id: this._sessionReplaySegmentId, batch_id: batchId, started_at_ms: stored.created_at_ms, sent_at_ms: nowMs, diff --git a/packages/template/src/lib/stack-app/session-recordings/index.ts b/packages/template/src/lib/stack-app/session-recordings/index.ts index 53b23fa805..71f7be6f0e 100644 --- a/packages/template/src/lib/stack-app/session-recordings/index.ts +++ b/packages/template/src/lib/stack-app/session-recordings/index.ts @@ -14,7 +14,7 @@ export type AdminSessionRecording = { export type AdminSessionRecordingChunk = { id: string, batchId: string, - tabId: string | null, + sessionReplaySegmentId: string | null, browserSessionId: string | null, eventCount: number, byteLength: number, @@ -47,7 +47,7 @@ export type SessionRecordingAllEventsResult = { chunks: Array<{ id: string, batchId: string, - tabId: string | null, + sessionReplaySegmentId: string | null, eventCount: number, byteLength: number, firstEventAt: Date, From 98c9acbd639cef45c616be5ac58039e6cc625f9d Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Tue, 17 Feb 2026 10:59:51 -0800 Subject: [PATCH 2/2] rename session recording to session replay (#1207) --- .../migration.sql | 21 ++++ apps/backend/prisma/schema.prisma | 26 ++--- apps/backend/prisma/seed.ts | 28 +++--- .../chunks/[chunk_id]/events/route.tsx | 22 ++--- .../[session_replay_id]}/chunks/route.tsx | 20 ++-- .../[session_replay_id]}/events/route.tsx | 28 +++--- .../route.tsx | 16 +-- .../batch/route.tsx | 46 ++++----- .../analytics/replays/page-client.tsx | 14 +-- ...rdings.test.ts => session-replays.test.ts} | 98 +++++++++---------- .../src/interface/admin-interface.ts | 30 +++--- .../src/interface/client-interface.ts | 4 +- ...ssion-recordings.ts => session-replays.ts} | 13 ++- packages/template/src/index.ts | 2 +- .../apps/implementations/admin-app-impl.ts | 24 ++--- .../apps/implementations/client-app-impl.ts | 8 +- ...session-recording.ts => session-replay.ts} | 2 +- .../stack-app/apps/interfaces/admin-app.ts | 14 +-- .../stack-app/apps/interfaces/client-app.ts | 4 +- .../index.ts | 18 ++-- 20 files changed, 230 insertions(+), 208 deletions(-) rename apps/backend/src/app/api/latest/internal/{session-recordings/[session_recording_id] => session-replays/[session_replay_id]}/chunks/[chunk_id]/events/route.tsx (77%) rename apps/backend/src/app/api/latest/internal/{session-recordings/[session_recording_id] => session-replays/[session_replay_id]}/chunks/route.tsx (87%) rename apps/backend/src/app/api/latest/internal/{session-recordings/[session_recording_id] => session-replays/[session_replay_id]}/events/route.tsx (85%) rename apps/backend/src/app/api/latest/internal/{session-recordings => session-replays}/route.tsx (89%) rename apps/backend/src/app/api/latest/{session-recordings => session-replays}/batch/route.tsx (84%) rename apps/e2e/tests/backend/endpoints/api/v1/{session-recordings.test.ts => session-replays.test.ts} (84%) rename packages/stack-shared/src/interface/crud/{session-recordings.ts => session-replays.ts} (77%) rename packages/template/src/lib/stack-app/apps/implementations/{session-recording.ts => session-replay.ts} (99%) rename packages/template/src/lib/stack-app/{session-recordings => session-replays}/index.ts (68%) diff --git a/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql b/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql index 11a201f1dc..4319d95770 100644 --- a/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql +++ b/apps/backend/prisma/migrations/20260216000000_rename_tab_id_to_session_replay_segment_id/migration.sql @@ -1 +1,22 @@ ALTER TABLE "SessionRecordingChunk" RENAME COLUMN "tabId" TO "sessionReplaySegmentId"; + +ALTER TABLE "SessionRecording" RENAME TO "SessionReplay"; +ALTER TABLE "SessionRecordingChunk" RENAME TO "SessionReplayChunk"; +ALTER TABLE "SessionReplayChunk" RENAME COLUMN "sessionRecordingId" TO "sessionReplayId"; + +-- Rename primary key constraints +ALTER TABLE "SessionReplay" RENAME CONSTRAINT "SessionRecording_pkey" TO "SessionReplay_pkey"; +ALTER TABLE "SessionReplayChunk" RENAME CONSTRAINT "SessionRecordingChunk_pkey" TO "SessionReplayChunk_pkey"; + +-- Rename foreign key constraints +ALTER TABLE "SessionReplay" RENAME CONSTRAINT "SessionRecording_tenancyId_fkey" TO "SessionReplay_tenancyId_fkey"; +ALTER TABLE "SessionReplay" RENAME CONSTRAINT "SessionRecording_tenancyId_projectUserId_fkey" TO "SessionReplay_tenancyId_projectUserId_fkey"; +ALTER TABLE "SessionReplayChunk" RENAME CONSTRAINT "SessionRecordingChunk_tenancyId_fkey" TO "SessionReplayChunk_tenancyId_fkey"; +ALTER TABLE "SessionReplayChunk" RENAME CONSTRAINT "SessionRecordingChunk_tenancyId_sessionRecordingId_fkey" TO "SessionReplayChunk_tenancyId_sessionReplayId_fkey"; + +-- Rename indexes +ALTER INDEX "SessionRecording_tenancyId_lastEventAt_idx" RENAME TO "SessionReplay_tenancyId_lastEventAt_idx"; +ALTER INDEX "SessionRecording_tenancyId_projectUserId_startedAt_idx" RENAME TO "SessionReplay_tenancyId_projectUserId_startedAt_idx"; +ALTER INDEX "SessionRecording_tenancyId_refreshTokenId_updatedAt_idx" RENAME TO "SessionReplay_tenancyId_refreshTokenId_updatedAt_idx"; +ALTER INDEX "SessionRecordingChunk_tenancyId_sessionRecordingId_batchId_key" RENAME TO "SessionReplayChunk_tenancyId_sessionReplayId_batchId_key"; +ALTER INDEX "SessionRecordingChunk_tenancyId_sessionRecordingId_createdA_idx" RENAME TO "SessionReplayChunk_tenancyId_sessionReplayId_createdAt_idx"; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 4a1226d2a7..93d73842c9 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -60,8 +60,8 @@ model Tenancy { organizationId String? @db.Uuid hasNoOrganization BooleanTrue? emailOutboxes EmailOutbox[] - sessionRecordings SessionRecording[] - sessionRecordingChunks SessionRecordingChunk[] + sessionReplays SessionReplay[] + sessionReplayChunks SessionReplayChunk[] @@unique([projectId, branchId, organizationId]) @@unique([projectId, branchId, hasNoOrganization]) @@ -236,7 +236,7 @@ model ProjectUser { Project Project? @relation(fields: [projectId], references: [id]) projectId String? userNotificationPreference UserNotificationPreference[] - sessionRecordings SessionRecording[] + sessionReplays SessionReplay[] @@id([tenancyId, projectUserId]) @@unique([mirroredProjectId, mirroredBranchId, projectUserId]) @@ -280,7 +280,7 @@ model ProjectUserOAuthAccount { @@index([tenancyId, projectUserId]) } -model SessionRecording { +model SessionReplay { id String @db.Uuid tenancyId String @db.Uuid @@ -296,20 +296,21 @@ model SessionRecording { projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade) tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade) - chunks SessionRecordingChunk[] + chunks SessionReplayChunk[] @@id([tenancyId, id]) + @@map("SessionReplay") @@index([tenancyId, projectUserId, startedAt]) @@index([tenancyId, lastEventAt]) // index by updatedAt instead of lastEventAt because event timing can be spoofed @@index([tenancyId, refreshTokenId, updatedAt]) } -model SessionRecordingChunk { +model SessionReplayChunk { id String @id @default(uuid()) @db.Uuid - tenancyId String @db.Uuid - sessionRecordingId String @db.Uuid + tenancyId String @db.Uuid + sessionReplayId String @db.Uuid @map("sessionReplayId") // Unique per uploaded batch for a given session id. batchId String @db.Uuid @@ -329,11 +330,12 @@ model SessionRecordingChunk { createdAt DateTime @default(now()) - sessionRecording SessionRecording @relation(fields: [tenancyId, sessionRecordingId], references: [tenancyId, id], onDelete: Cascade) - tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade) + sessionReplay SessionReplay @relation(fields: [tenancyId, sessionReplayId], references: [tenancyId, id], onDelete: Cascade) + tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade) - @@unique([tenancyId, sessionRecordingId, batchId]) - @@index([tenancyId, sessionRecordingId, createdAt]) + @@unique([tenancyId, sessionReplayId, batchId]) + @@map("SessionReplayChunk") + @@index([tenancyId, sessionReplayId, createdAt]) } enum ContactChannelType { diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index 301bf21944..93df1dd7b1 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -1118,11 +1118,11 @@ async function seedDummyProject(options: DummyProjectSeedOptions) { userEmailToId, }); - await seedDummySessionRecordings({ + await seedDummySessionReplays({ prisma: dummyPrisma, tenancyId: dummyTenancy.id, userEmailToId, - targetSessionRecordingCount: 75 + targetSessionReplayCount: 75 }); console.log('Seeded dummy project data'); @@ -1773,43 +1773,43 @@ async function seedDummySessionActivityEvents(options: SessionActivityEventSeedO console.log('Finished seeding session activity events'); } -type SessionRecordingSeedOptions = { +type SessionReplaySeedOptions = { prisma: PrismaClientTransaction, tenancyId: string, userEmailToId: Map, - targetSessionRecordingCount?: number, + targetSessionReplayCount?: number, }; -async function seedDummySessionRecordings(options: SessionRecordingSeedOptions) { +async function seedDummySessionReplays(options: SessionReplaySeedOptions) { const { prisma, tenancyId, userEmailToId, - targetSessionRecordingCount = 250, + targetSessionReplayCount = 250, } = options; - const existingCount = await prisma.sessionRecording.count({ + const existingCount = await prisma.sessionReplay.count({ where: { tenancyId, }, }); - if (existingCount >= targetSessionRecordingCount) { - console.log(`Dummy project already has ${existingCount} session recordings, skipping seeding`); + if (existingCount >= targetSessionReplayCount) { + console.log(`Dummy project already has ${existingCount} session replays, skipping seeding`); return; } - const toCreate = targetSessionRecordingCount - existingCount; + const toCreate = targetSessionReplayCount - existingCount; const userIds = Array.from(userEmailToId.values()); if (userIds.length === 0) { - throw new Error('Cannot seed session recordings: no dummy project users exist'); + throw new Error('Cannot seed session replays: no dummy project users exist'); } const now = new Date(); const twoWeeksAgo = new Date(now); twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); - const seeds: Prisma.SessionRecordingCreateManyInput[] = []; + const seeds: Prisma.SessionReplayCreateManyInput[] = []; for (let i = 0; i < toCreate; i++) { const startedAt = new Date( twoWeeksAgo.getTime() + Math.random() * (now.getTime() - twoWeeksAgo.getTime()), @@ -1828,9 +1828,9 @@ async function seedDummySessionRecordings(options: SessionRecordingSeedOptions) }); } - await prisma.sessionRecording.createMany({ + await prisma.sessionReplay.createMany({ data: seeds, }); - console.log(`Seeded ${toCreate} session recordings`); + console.log(`Seeded ${toCreate} session replays`); } diff --git a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/[chunk_id]/events/route.tsx b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx similarity index 77% rename from apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/[chunk_id]/events/route.tsx rename to apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx index d8d2aaf479..3b47cd4cb2 100644 --- a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/[chunk_id]/events/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/[chunk_id]/events/route.tsx @@ -17,7 +17,7 @@ export const GET = createSmartRouteHandler({ tenancy: adaptSchema.defined(), }).defined(), params: yupObject({ - session_recording_id: yupString().defined(), + session_replay_id: yupString().defined(), chunk_id: yupString().defined(), }).defined(), }), @@ -31,13 +31,13 @@ export const GET = createSmartRouteHandler({ async handler({ auth, params }) { const prisma = await getPrismaClientForTenancy(auth.tenancy); - const sessionRecordingId = params.session_recording_id; + const sessionReplayId = params.session_replay_id; const chunkId = params.chunk_id; - const chunk = await prisma.sessionRecordingChunk.findFirst({ + const chunk = await prisma.sessionReplayChunk.findFirst({ where: { tenancyId: auth.tenancy.id, - sessionRecordingId, + sessionReplayId, id: chunkId, }, select: { @@ -64,20 +64,20 @@ export const GET = createSmartRouteHandler({ try { parsed = JSON.parse(new TextDecoder().decode(unzipped)); } catch (e) { - throw new StackAssertionError("Failed to decode session recording chunk JSON", { cause: e }); + throw new StackAssertionError("Failed to decode session replay chunk JSON", { cause: e }); } if (typeof parsed !== "object" || parsed === null) { - throw new StackAssertionError("Decoded session recording chunk is not an object"); + throw new StackAssertionError("Decoded session replay chunk is not an object"); } - if (parsed.session_recording_id !== sessionRecordingId) { - throw new StackAssertionError("Decoded session recording chunk session_recording_id mismatch", { - expected: sessionRecordingId, - actual: parsed.session_recording_id, + if (parsed.session_replay_id !== sessionReplayId) { + throw new StackAssertionError("Decoded session replay chunk session_replay_id mismatch", { + expected: sessionReplayId, + actual: parsed.session_replay_id, }); } if (!Array.isArray(parsed.events)) { - throw new StackAssertionError("Decoded session recording chunk events is not an array"); + throw new StackAssertionError("Decoded session replay chunk events is not an array"); } return { diff --git a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/route.tsx similarity index 87% rename from apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx rename to apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/route.tsx index 2e4c72b78f..39e85cf8ab 100644 --- a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/chunks/route.tsx @@ -15,7 +15,7 @@ export const GET = createSmartRouteHandler({ tenancy: adaptSchema.defined(), }).defined(), params: yupObject({ - session_recording_id: yupString().defined(), + session_replay_id: yupString().defined(), }).defined(), query: yupObject({ cursor: yupString().optional(), @@ -45,13 +45,13 @@ export const GET = createSmartRouteHandler({ async handler({ auth, params, query }) { const prisma = await getPrismaClientForTenancy(auth.tenancy); - const sessionRecordingId = params.session_recording_id; - const exists = await prisma.sessionRecording.findUnique({ - where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: sessionRecordingId } }, + const sessionReplayId = params.session_replay_id; + const exists = await prisma.sessionReplay.findUnique({ + where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: sessionReplayId } }, select: { id: true }, }); if (!exists) { - throw new KnownErrors.ItemNotFound(sessionRecordingId); + throw new KnownErrors.ItemNotFound(sessionReplayId); } const rawLimit = query.limit ?? String(DEFAULT_LIMIT); @@ -61,10 +61,10 @@ export const GET = createSmartRouteHandler({ const cursorId = query.cursor; let cursorPivot: { firstEventAt: Date } | null = null; if (cursorId) { - cursorPivot = await prisma.sessionRecordingChunk.findFirst({ + cursorPivot = await prisma.sessionReplayChunk.findFirst({ where: { tenancyId: auth.tenancy.id, - sessionRecordingId, + sessionReplayId, id: cursorId, }, select: { firstEventAt: true }, @@ -74,17 +74,17 @@ export const GET = createSmartRouteHandler({ } } - const cursorWhere: Prisma.SessionRecordingChunkWhereInput = cursorId && cursorPivot ? { + const cursorWhere: Prisma.SessionReplayChunkWhereInput = cursorId && cursorPivot ? { OR: [ { firstEventAt: { gt: cursorPivot.firstEventAt } }, { AND: [{ firstEventAt: { equals: cursorPivot.firstEventAt } }, { id: { gt: cursorId } }] }, ], } : {}; - const chunks = await prisma.sessionRecordingChunk.findMany({ + const chunks = await prisma.sessionReplayChunk.findMany({ where: { tenancyId: auth.tenancy.id, - sessionRecordingId, + sessionReplayId, ...cursorWhere, }, orderBy: [{ firstEventAt: "asc" }, { id: "asc" }], diff --git a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx similarity index 85% rename from apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx rename to apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx index 16ac617047..3d38cb6462 100644 --- a/apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-replays/[session_replay_id]/events/route.tsx @@ -19,7 +19,7 @@ export const GET = createSmartRouteHandler({ tenancy: adaptSchema.defined(), }).defined(), params: yupObject({ - session_recording_id: yupString().defined(), + session_replay_id: yupString().defined(), }).defined(), query: yupObject({ offset: yupString().optional(), @@ -49,19 +49,19 @@ export const GET = createSmartRouteHandler({ async handler({ auth, params, query }) { const prisma = await getPrismaClientForTenancy(auth.tenancy); - const sessionRecordingId = params.session_recording_id; - const exists = await prisma.sessionRecording.findUnique({ - where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: sessionRecordingId } }, + const sessionReplayId = params.session_replay_id; + const exists = await prisma.sessionReplay.findUnique({ + where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: sessionReplayId } }, select: { id: true }, }); if (!exists) { - throw new KnownErrors.ItemNotFound(sessionRecordingId); + throw new KnownErrors.ItemNotFound(sessionReplayId); } - const chunks = await prisma.sessionRecordingChunk.findMany({ + const chunks = await prisma.sessionReplayChunk.findMany({ where: { tenancyId: auth.tenancy.id, - sessionRecordingId, + sessionReplayId, }, orderBy: [{ firstEventAt: "asc" }, { id: "asc" }], select: { @@ -110,20 +110,20 @@ export const GET = createSmartRouteHandler({ try { parsed = JSON.parse(new TextDecoder().decode(unzipped)); } catch (e) { - throw new StackAssertionError("Failed to decode session recording chunk JSON", { cause: e }); + throw new StackAssertionError("Failed to decode session replay chunk JSON", { cause: e }); } if (typeof parsed !== "object" || parsed === null) { - throw new StackAssertionError("Decoded session recording chunk is not an object"); + throw new StackAssertionError("Decoded session replay chunk is not an object"); } - if (parsed.session_recording_id !== sessionRecordingId) { - throw new StackAssertionError("Decoded session recording chunk session_recording_id mismatch", { - expected: sessionRecordingId, - actual: parsed.session_recording_id, + if (parsed.session_replay_id !== sessionReplayId) { + throw new StackAssertionError("Decoded session replay chunk session_replay_id mismatch", { + expected: sessionReplayId, + actual: parsed.session_replay_id, }); } if (!Array.isArray(parsed.events)) { - throw new StackAssertionError("Decoded session recording chunk events is not an array"); + throw new StackAssertionError("Decoded session replay chunk events is not an array"); } chunkEvents[idx] = { chunk_id: chunk.id, events: parsed.events as any[] }; diff --git a/apps/backend/src/app/api/latest/internal/session-recordings/route.tsx b/apps/backend/src/app/api/latest/internal/session-replays/route.tsx similarity index 89% rename from apps/backend/src/app/api/latest/internal/session-recordings/route.tsx rename to apps/backend/src/app/api/latest/internal/session-replays/route.tsx index 55d41c696b..45f45290b6 100644 --- a/apps/backend/src/app/api/latest/internal/session-recordings/route.tsx +++ b/apps/backend/src/app/api/latest/internal/session-replays/route.tsx @@ -50,7 +50,7 @@ export const GET = createSmartRouteHandler({ const cursorId = query.cursor; let cursorPivot: { lastEventAt: Date } | null = null; if (cursorId) { - cursorPivot = await prisma.sessionRecording.findUnique({ + cursorPivot = await prisma.sessionReplay.findUnique({ where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: cursorId } }, select: { lastEventAt: true }, }); @@ -59,14 +59,14 @@ export const GET = createSmartRouteHandler({ } } - const where: Prisma.SessionRecordingWhereInput = cursorId && cursorPivot ? { + const where: Prisma.SessionReplayWhereInput = cursorId && cursorPivot ? { OR: [ { lastEventAt: { lt: cursorPivot.lastEventAt } }, { AND: [{ lastEventAt: { equals: cursorPivot.lastEventAt } }, { id: { lt: cursorId } }] }, ], } : {}; - const sessions = await prisma.sessionRecording.findMany({ + const sessions = await prisma.sessionReplay.findMany({ where: { tenancyId: auth.tenancy.id, ...where }, orderBy: [{ lastEventAt: "desc" }, { id: "desc" }], take: limit + 1, @@ -86,12 +86,12 @@ export const GET = createSmartRouteHandler({ const userIds = [...new Set(page.map(s => s.projectUserId))]; const [chunkAggs, users] = await Promise.all([ - sessionIds.length ? prisma.sessionRecordingChunk.groupBy({ - by: ["sessionRecordingId"], - where: { tenancyId: auth.tenancy.id, sessionRecordingId: { in: sessionIds } }, + sessionIds.length ? prisma.sessionReplayChunk.groupBy({ + by: ["sessionReplayId"], + where: { tenancyId: auth.tenancy.id, sessionReplayId: { in: sessionIds } }, _count: { _all: true }, _sum: { eventCount: true }, - }) : Promise.resolve([] as Array<{ sessionRecordingId: string, _count: { _all: number }, _sum: { eventCount: number | null } }>), + }) : Promise.resolve([] as Array<{ sessionReplayId: string, _count: { _all: number }, _sum: { eventCount: number | null } }>), userIds.length ? prisma.projectUser.findMany({ where: { tenancyId: auth.tenancy.id, projectUserId: { in: userIds } }, select: { @@ -108,7 +108,7 @@ export const GET = createSmartRouteHandler({ const aggBySessionId = new Map(); for (const a of chunkAggs) { - aggBySessionId.set(a.sessionRecordingId, { + aggBySessionId.set(a.sessionReplayId, { chunkCount: a._count._all, eventCount: a._sum.eventCount ?? 0, }); diff --git a/apps/backend/src/app/api/latest/session-recordings/batch/route.tsx b/apps/backend/src/app/api/latest/session-replays/batch/route.tsx similarity index 84% rename from apps/backend/src/app/api/latest/session-recordings/batch/route.tsx rename to apps/backend/src/app/api/latest/session-replays/batch/route.tsx index 21a509bfc4..3ec4c6d0cf 100644 --- a/apps/backend/src/app/api/latest/session-recordings/batch/route.tsx +++ b/apps/backend/src/app/api/latest/session-replays/batch/route.tsx @@ -39,9 +39,9 @@ function extractEventTimesMs(events: unknown[], fallbackMs: number) { export const POST = createSmartRouteHandler({ metadata: { - summary: "Upload rrweb session recording batch", - description: "Uploads a batch of rrweb events for a cross-tab session recording.", - tags: ["Session Recordings"], + summary: "Upload rrweb session replay batch", + description: "Uploads a batch of rrweb events for a cross-tab session replay.", + tags: ["Session Replays"], hidden: true }, request: yupObject({ @@ -64,7 +64,7 @@ export const POST = createSmartRouteHandler({ statusCode: yupNumber().oneOf([200]).defined(), bodyType: yupString().oneOf(["json"]).defined(), body: yupObject({ - session_recording_id: yupString().defined(), + session_replay_id: yupString().defined(), batch_id: yupString().defined(), s3_key: yupString().defined(), deduped: yupMixed().defined(), @@ -76,7 +76,7 @@ export const POST = createSmartRouteHandler({ statusCode: 200, bodyType: "json", body: { - session_recording_id: "", + session_replay_id: "", batch_id: body.batch_id, s3_key: "", deduped: false, @@ -87,7 +87,7 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.UserAuthenticationRequired(); } if (!auth.refreshTokenId) { - throw new StatusError(StatusError.BadRequest, "A refresh token is required for session recordings"); + throw new StatusError(StatusError.BadRequest, "A refresh token is required for session replays"); } const projectUserId = auth.user.id; const refreshTokenId = auth.refreshTokenId; @@ -115,12 +115,12 @@ export const POST = createSmartRouteHandler({ const prisma = await getPrismaClientForTenancy(auth.tenancy); - // Find a recent session recording for this refresh token (temporal grouping). - // If the last batch arrived within SESSION_IDLE_TIMEOUT_MS, reuse that recording. - // Also enforce a max session duration so recordings don't grow indefinitely. + // Find a recent session replay for this refresh token (temporal grouping). + // If the last batch arrived within SESSION_IDLE_TIMEOUT_MS, reuse that replay. + // Also enforce a max session duration so replays don't grow indefinitely. const cutoff = new Date(Date.now() - SESSION_IDLE_TIMEOUT_MS); const maxDurationCutoff = new Date(Date.now() - MAX_SESSION_DURATION_MS); - const recentSession = await prisma.sessionRecording.findFirst({ + const recentSession = await prisma.sessionReplay.findFirst({ where: { tenancyId, refreshTokenId, @@ -131,15 +131,15 @@ export const POST = createSmartRouteHandler({ select: { id: true, startedAt: true, lastEventAt: true }, }); - const recordingId = recentSession?.id ?? randomUUID(); - const s3Key = `session-recordings/${projectId}/${branchId}/${recordingId}/${batchId}.json.gz`; + const replayId = recentSession?.id ?? randomUUID(); + const s3Key = `session-replays/${projectId}/${branchId}/${replayId}/${batchId}.json.gz`; const newStartedAtMs = Math.min(recentSession?.startedAt.getTime() ?? Number.POSITIVE_INFINITY, firstMs); const newLastEventAtMs = Math.max(recentSession?.lastEventAt.getTime() ?? 0, lastMs); - await prisma.sessionRecording.upsert({ - where: { tenancyId_id: { tenancyId, id: recordingId } }, + await prisma.sessionReplay.upsert({ + where: { tenancyId_id: { tenancyId, id: replayId } }, create: { - id: recordingId, + id: replayId, tenancyId, projectUserId, refreshTokenId, @@ -153,8 +153,8 @@ export const POST = createSmartRouteHandler({ }); // If we already have this batch for this session, return deduped without touching S3. - const existingChunk = await prisma.sessionRecordingChunk.findUnique({ - where: { tenancyId_sessionRecordingId_batchId: { tenancyId, sessionRecordingId: recordingId, batchId } }, + const existingChunk = await prisma.sessionReplayChunk.findUnique({ + where: { tenancyId_sessionReplayId_batchId: { tenancyId, sessionReplayId: replayId, batchId } }, select: { s3Key: true }, }); if (existingChunk) { @@ -162,7 +162,7 @@ export const POST = createSmartRouteHandler({ statusCode: 200, bodyType: "json", body: { - session_recording_id: recordingId, + session_replay_id: replayId, batch_id: batchId, s3_key: existingChunk.s3Key, deduped: true, @@ -172,7 +172,7 @@ export const POST = createSmartRouteHandler({ const payload = { v: 1, - session_recording_id: recordingId, + session_replay_id: replayId, browser_session_id: browserSessionId, session_replay_segment_id: sessionReplaySegmentId, batch_id: batchId, @@ -192,10 +192,10 @@ export const POST = createSmartRouteHandler({ }); try { - await prisma.sessionRecordingChunk.create({ + await prisma.sessionReplayChunk.create({ data: { tenancyId, - sessionRecordingId: recordingId, + sessionReplayId: replayId, batchId, sessionReplaySegmentId, browserSessionId, @@ -212,7 +212,7 @@ export const POST = createSmartRouteHandler({ statusCode: 200, bodyType: "json", body: { - session_recording_id: recordingId, + session_replay_id: replayId, batch_id: batchId, s3_key: s3Key, deduped: true, @@ -226,7 +226,7 @@ export const POST = createSmartRouteHandler({ statusCode: 200, bodyType: "json", body: { - session_recording_id: recordingId, + session_replay_id: replayId, batch_id: batchId, s3_key: s3Key, deduped: false, diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx index f5a30c473e..e0a4a3b771 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx @@ -68,12 +68,12 @@ type ChunkRow = { createdAt: Date, }; -type AdminAppWithSessionRecordings = ReturnType & { - listSessionRecordings: (options?: { limit?: number, cursor?: string }) => Promise<{ +type AdminAppWithSessionReplays = ReturnType & { + listSessionReplays: (options?: { limit?: number, cursor?: string }) => Promise<{ items: RecordingRow[], nextCursor: string | null, }>, - getSessionRecordingEvents: (sessionRecordingId: string, options?: { offset?: number, limit?: number }) => Promise<{ + getSessionReplayEvents: (sessionReplayId: string, options?: { offset?: number, limit?: number }) => Promise<{ chunks: ChunkRow[], chunkEvents: Array<{ chunkId: string, events: unknown[] }>, }>, @@ -337,7 +337,7 @@ function useReplayMachine(initialSettings: ReplaySettings) { // --------------------------------------------------------------------------- export default function PageClient() { - const adminApp = useAdminApp() as AdminAppWithSessionRecordings; + const adminApp = useAdminApp() as AdminAppWithSessionReplays; // ---- Recording list state (unchanged from original) ---- @@ -371,7 +371,7 @@ export default function PageClient() { setListError(null); try { - const res = await adminApp.listSessionRecordings({ limit: PAGE_SIZE, cursor: cursor ?? undefined }); + const res = await adminApp.listSessionReplays({ limit: PAGE_SIZE, cursor: cursor ?? undefined }); const items = cursor ? [...recordings, ...res.items] : res.items; setRecordings(items); setNextCursor(res.nextCursor); @@ -854,7 +854,7 @@ export default function PageClient() { try { // Phase 1: Fetch initial batch (fast start). - const initialResponse = await adminApp.getSessionRecordingEvents(recordingId, { offset: 0, limit: INITIAL_CHUNK_BATCH }); + const initialResponse = await adminApp.getSessionReplayEvents(recordingId, { offset: 0, limit: INITIAL_CHUNK_BATCH }); if (msRef.current.generation !== gen) return; const allChunkRows: ChunkRow[] = initialResponse.chunks.map((c) => ({ @@ -942,7 +942,7 @@ export default function PageClient() { while (offset < totalChunks) { if (msRef.current.generation !== gen) return; - const batchResponse = await adminApp.getSessionRecordingEvents(recordingId, { offset, limit: BACKGROUND_CHUNK_BATCH }); + const batchResponse = await adminApp.getSessionReplayEvents(recordingId, { offset, limit: BACKGROUND_CHUNK_BATCH }); if (msRef.current.generation !== gen) return; processChunkEvents(batchResponse.chunkEvents, allStreams, chunkIdToTabKey); diff --git a/apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts similarity index 84% rename from apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts rename to apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts index ca7c7e1f6a..a01ce8312b 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/session-recordings.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts @@ -10,7 +10,7 @@ async function uploadBatch(options: { events: unknown[], sessionReplaySegmentId?: string, }) { - return await niceBackendFetch("/api/v1/session-recordings/batch", { + return await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -29,7 +29,7 @@ it("requires a user token", async ({ expect }) => { await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); backendContext.set({ userAuth: null }); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -51,7 +51,7 @@ it("returns 200 no-op when analytics is not enabled", async ({ expect }) => { // Analytics is disabled by default - do NOT call Project.updateConfig await Auth.Otp.signIn(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -65,11 +65,11 @@ it("returns 200 no-op when analytics is not enabled", async ({ expect }) => { }); expect(res.status).toBe(200); - expect(res.body?.session_recording_id).toBe(""); + expect(res.body?.session_replay_id).toBe(""); expect(res.body?.s3_key).toBe(""); }); -it("stores session recording batch metadata and dedupes by (session_recording_id, batch_id)", async ({ expect }) => { +it("stores session replay batch metadata and dedupes by (session_replay_id, batch_id)", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); @@ -79,7 +79,7 @@ it("stores session recording batch metadata and dedupes by (session_recording_id const batchId = randomUUID(); const sessionReplaySegmentId = randomUUID(); - const first = await niceBackendFetch("/api/v1/session-recordings/batch", { + const first = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -96,16 +96,16 @@ it("stores session recording batch metadata and dedupes by (session_recording_id }); expect(first.status).toBe(200); - expect(typeof first.body?.session_recording_id).toBe("string"); + expect(typeof first.body?.session_replay_id).toBe("string"); expect(first.body).toMatchObject({ batch_id: batchId, deduped: false, }); expect(typeof first.body?.s3_key).toBe("string"); - const recordingId = first.body?.session_recording_id; + const recordingId = first.body?.session_replay_id; - const second = await niceBackendFetch("/api/v1/session-recordings/batch", { + const second = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -120,7 +120,7 @@ it("stores session recording batch metadata and dedupes by (session_recording_id expect(second.status).toBe(200); expect(second.body).toMatchObject({ - session_recording_id: recordingId, + session_replay_id: recordingId, batch_id: batchId, deduped: true, }); @@ -131,7 +131,7 @@ it("rejects empty events", async ({ expect }) => { await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -155,7 +155,7 @@ it("rejects too many events", async ({ expect }) => { const tooManyEvents = Array.from({ length: 5001 }, (_, i) => ({ timestamp: 1_700_000_000_000 + i })); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -177,7 +177,7 @@ it("rejects invalid browser_session_id", async ({ expect }) => { await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -199,7 +199,7 @@ it("rejects invalid batch_id", async ({ expect }) => { await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -221,7 +221,7 @@ it("rejects invalid session_replay_segment_id", async ({ expect }) => { await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -246,7 +246,7 @@ it("accepts events without timestamps (falls back to sent_at_ms)", async ({ expe const browserSessionId = randomUUID(); const batchId = randomUUID(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -260,7 +260,7 @@ it("accepts events without timestamps (falls back to sent_at_ms)", async ({ expe }); expect(res.status).toBe(200); - expect(typeof res.body?.session_recording_id).toBe("string"); + expect(typeof res.body?.session_replay_id).toBe("string"); expect(res.body).toMatchObject({ batch_id: batchId, deduped: false, @@ -272,7 +272,7 @@ it("rejects non-integer started_at_ms", async ({ expect }) => { await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -297,7 +297,7 @@ it("rejects oversized payloads", async ({ expect }) => { // Backend limit is 5_000_000 bytes; a single large string is sufficient to exceed it. const hugeString = "a".repeat(5_100_000); - const res = await niceBackendFetch("/api/v1/session-recordings/batch", { + const res = await niceBackendFetch("/api/v1/session-replays/batch", { method: "POST", accessType: "client", body: { @@ -313,7 +313,7 @@ it("rejects oversized payloads", async ({ expect }) => { expect(res.status).toBe(413); }); -it("admin can list session recordings, list chunks, and fetch events", async ({ expect }) => { +it("admin can list session replays, list chunks, and fetch events", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); @@ -333,17 +333,17 @@ it("admin can list session recordings, list chunks, and fetch events", async ({ events, }); expect(uploadRes.status).toBe(200); - const recordingId = uploadRes.body?.session_recording_id; + const recordingId = uploadRes.body?.session_replay_id; expect(typeof recordingId).toBe("string"); - const listRes = await niceBackendFetch("/api/v1/internal/session-recordings", { + const listRes = await niceBackendFetch("/api/v1/internal/session-replays", { method: "GET", accessType: "admin", }); expect(listRes.status).toBe(200); expect(listRes.body?.items?.length).toBeGreaterThanOrEqual(1); - const chunksRes = await niceBackendFetch(`/api/v1/internal/session-recordings/${recordingId}/chunks`, { + const chunksRes = await niceBackendFetch(`/api/v1/internal/session-replays/${recordingId}/chunks`, { method: "GET", accessType: "admin", }); @@ -351,10 +351,10 @@ it("admin can list session recordings, list chunks, and fetch events", async ({ const chunkId = chunksRes.body?.items?.[0]?.id; expect(typeof chunkId).toBe("string"); if (typeof chunkId !== "string") { - throw new Error("Expected session recording chunks response to include an item id."); + throw new Error("Expected session replay chunks response to include an item id."); } - const eventsRes = await niceBackendFetch(`/api/v1/internal/session-recordings/${recordingId}/chunks/${chunkId}/events`, { + const eventsRes = await niceBackendFetch(`/api/v1/internal/session-replays/${recordingId}/chunks/${chunkId}/events`, { method: "GET", accessType: "admin", }); @@ -362,11 +362,11 @@ it("admin can list session recordings, list chunks, and fetch events", async ({ expect(eventsRes.body?.events?.length).toBe(events.length); }); -it("admin list session recordings paginates without skipping items", async ({ expect }) => { +it("admin list session replays paginates without skipping items", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); - // Use separate sign-ins to get different refresh tokens → different session recordings. + // Use separate sign-ins to get different refresh tokens → different session replays. await Auth.Otp.signIn(); const uploadA = await uploadBatch({ browserSessionId: randomUUID(), @@ -376,7 +376,7 @@ it("admin list session recordings paginates without skipping items", async ({ ex events: [{ type: 1, timestamp: 1_700_000_000_100 }], }); expect(uploadA.status).toBe(200); - const recordingA = uploadA.body?.session_recording_id; + const recordingA = uploadA.body?.session_replay_id; await Auth.Otp.signIn(); const uploadB = await uploadBatch({ @@ -387,9 +387,9 @@ it("admin list session recordings paginates without skipping items", async ({ ex events: [{ type: 1, timestamp: 1_700_000_000_200 }], }); expect(uploadB.status).toBe(200); - const recordingB = uploadB.body?.session_recording_id; + const recordingB = uploadB.body?.session_replay_id; - const first = await niceBackendFetch("/api/v1/internal/session-recordings?limit=1", { + const first = await niceBackendFetch("/api/v1/internal/session-replays?limit=1", { method: "GET", accessType: "admin", }); @@ -404,7 +404,7 @@ it("admin list session recordings paginates without skipping items", async ({ ex throw new Error("Expected next_cursor to be a string."); } - const second = await niceBackendFetch(`/api/v1/internal/session-recordings?limit=1&cursor=${encodeURIComponent(nextCursor)}`, { + const second = await niceBackendFetch(`/api/v1/internal/session-replays?limit=1&cursor=${encodeURIComponent(nextCursor)}`, { method: "GET", accessType: "admin", }); @@ -415,12 +415,12 @@ it("admin list session recordings paginates without skipping items", async ({ ex expect(secondId).not.toBe(firstId); }); -it("admin list session recordings rejects unknown cursor", async ({ expect }) => { +it("admin list session replays rejects unknown cursor", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Auth.Otp.signIn(); const cursor = randomUUID(); - const res = await niceBackendFetch(`/api/v1/internal/session-recordings?cursor=${encodeURIComponent(cursor)}`, { + const res = await niceBackendFetch(`/api/v1/internal/session-replays?cursor=${encodeURIComponent(cursor)}`, { method: "GET", accessType: "admin", }); @@ -445,7 +445,7 @@ it("admin list chunks paginates and rejects a cursor from another session", asyn events: [{ type: 1, timestamp: now + 10 }], }); expect(upload1a.status).toBe(200); - const recording1 = upload1a.body?.session_recording_id; + const recording1 = upload1a.body?.session_replay_id; await uploadBatch({ browserSessionId: randomUUID(), @@ -465,9 +465,9 @@ it("admin list chunks paginates and rejects a cursor from another session", asyn events: [{ type: 1, timestamp: now + 30 }], }); expect(upload2.status).toBe(200); - const recording2 = upload2.body?.session_recording_id; + const recording2 = upload2.body?.session_replay_id; - const first = await niceBackendFetch(`/api/v1/internal/session-recordings/${recording1}/chunks?limit=1`, { + const first = await niceBackendFetch(`/api/v1/internal/session-replays/${recording1}/chunks?limit=1`, { method: "GET", accessType: "admin", }); @@ -480,7 +480,7 @@ it("admin list chunks paginates and rejects a cursor from another session", asyn throw new Error("Expected next_cursor to be a string."); } - const second = await niceBackendFetch(`/api/v1/internal/session-recordings/${recording1}/chunks?limit=1&cursor=${encodeURIComponent(nextCursor)}`, { + const second = await niceBackendFetch(`/api/v1/internal/session-replays/${recording1}/chunks?limit=1&cursor=${encodeURIComponent(nextCursor)}`, { method: "GET", accessType: "admin", }); @@ -489,7 +489,7 @@ it("admin list chunks paginates and rejects a cursor from another session", asyn expect(second.body?.items?.[0]?.id).not.toBe(first.body?.items?.[0]?.id); // Cursor from another session should be rejected. - const otherChunks = await niceBackendFetch(`/api/v1/internal/session-recordings/${recording2}/chunks?limit=1`, { + const otherChunks = await niceBackendFetch(`/api/v1/internal/session-replays/${recording2}/chunks?limit=1`, { method: "GET", accessType: "admin", }); @@ -500,7 +500,7 @@ it("admin list chunks paginates and rejects a cursor from another session", asyn throw new Error("Expected otherCursor to be a string."); } - const bad = await niceBackendFetch(`/api/v1/internal/session-recordings/${recording1}/chunks?cursor=${encodeURIComponent(otherCursor)}`, { + const bad = await niceBackendFetch(`/api/v1/internal/session-replays/${recording1}/chunks?cursor=${encodeURIComponent(otherCursor)}`, { method: "GET", accessType: "admin", }); @@ -523,7 +523,7 @@ it("admin events endpoint does not allow fetching a chunk via the wrong session events: [{ type: 1, timestamp: 1_700_000_000_010 }], }); expect(upload1.status).toBe(200); - const recording1 = upload1.body?.session_recording_id; + const recording1 = upload1.body?.session_replay_id; // session2: upload under a different refresh token await Auth.Otp.signIn(); @@ -535,9 +535,9 @@ it("admin events endpoint does not allow fetching a chunk via the wrong session events: [{ type: 1, timestamp: 1_700_000_000_020 }], }); expect(upload2.status).toBe(200); - const recording2 = upload2.body?.session_recording_id; + const recording2 = upload2.body?.session_replay_id; - const chunks = await niceBackendFetch(`/api/v1/internal/session-recordings/${recording1}/chunks`, { + const chunks = await niceBackendFetch(`/api/v1/internal/session-replays/${recording1}/chunks`, { method: "GET", accessType: "admin", }); @@ -548,7 +548,7 @@ it("admin events endpoint does not allow fetching a chunk via the wrong session throw new Error("Expected chunk id."); } - const wrong = await niceBackendFetch(`/api/v1/internal/session-recordings/${recording2}/chunks/${chunkId}/events`, { + const wrong = await niceBackendFetch(`/api/v1/internal/session-replays/${recording2}/chunks/${chunkId}/events`, { method: "GET", accessType: "admin", }); @@ -556,18 +556,18 @@ it("admin events endpoint does not allow fetching a chunk via the wrong session expect(wrong.body?.code).toBe("ITEM_NOT_FOUND"); }); -it("non-admin access cannot call internal session recordings endpoints", async ({ expect }) => { +it("non-admin access cannot call internal session replays endpoints", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Auth.Otp.signIn(); - const clientRes = await niceBackendFetch("/api/v1/internal/session-recordings", { + const clientRes = await niceBackendFetch("/api/v1/internal/session-replays", { method: "GET", accessType: "client", }); expect(clientRes.status).toBeGreaterThanOrEqual(400); expect(clientRes.status).toBeLessThan(500); - const serverRes = await niceBackendFetch("/api/v1/internal/session-recordings", { + const serverRes = await niceBackendFetch("/api/v1/internal/session-replays", { method: "GET", accessType: "server", }); @@ -575,7 +575,7 @@ it("non-admin access cannot call internal session recordings endpoints", async ( expect(serverRes.status).toBeLessThan(500); }); -it("groups batches from same refresh token into one session recording", async ({ expect }) => { +it("groups batches from same refresh token into one session replay", async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } }); await Auth.Otp.signIn(); @@ -601,6 +601,6 @@ it("groups batches from same refresh token into one session recording", async ({ }); expect(upload2.status).toBe(200); - // Same refresh token within idle timeout → same session recording - expect(upload1.body?.session_recording_id).toBe(upload2.body?.session_recording_id); + // Same refresh token within idle timeout → same session replay + expect(upload1.body?.session_replay_id).toBe(upload2.body?.session_replay_id); }); diff --git a/packages/stack-shared/src/interface/admin-interface.ts b/packages/stack-shared/src/interface/admin-interface.ts index f08895923a..0c10351d31 100644 --- a/packages/stack-shared/src/interface/admin-interface.ts +++ b/packages/stack-shared/src/interface/admin-interface.ts @@ -12,13 +12,13 @@ import { InternalApiKeysCrud } from "./crud/internal-api-keys"; import { ProjectPermissionDefinitionsCrud } from "./crud/project-permissions"; import { ProjectsCrud } from "./crud/projects"; import type { - AdminGetSessionRecordingAllEventsResponse, - AdminGetSessionRecordingChunkEventsResponse, - AdminListSessionRecordingChunksOptions, - AdminListSessionRecordingChunksResponse, - AdminListSessionRecordingsOptions, - AdminListSessionRecordingsResponse -} from "./crud/session-recordings"; + AdminGetSessionReplayAllEventsResponse, + AdminGetSessionReplayChunkEventsResponse, + AdminListSessionReplayChunksOptions, + AdminListSessionReplayChunksResponse, + AdminListSessionReplaysOptions, + AdminListSessionReplaysResponse +} from "./crud/session-replays"; import { SvixTokenCrud } from "./crud/svix-token"; import { TeamPermissionDefinitionsCrud } from "./crud/team-permissions"; import type { Transaction, TransactionType } from "./crud/transactions"; @@ -783,45 +783,45 @@ export class StackAdminInterface extends StackServerInterface { return { transactions: json.transactions, nextCursor: json.next_cursor }; } - async listSessionRecordings(params?: AdminListSessionRecordingsOptions): Promise { + async listSessionReplays(params?: AdminListSessionReplaysOptions): Promise { const qs = new URLSearchParams(); if (params?.cursor) qs.set("cursor", params.cursor); if (typeof params?.limit === "number") qs.set("limit", String(params.limit)); const response = await this.sendAdminRequest( - `/internal/session-recordings${qs.size ? `?${qs.toString()}` : ""}`, + `/internal/session-replays${qs.size ? `?${qs.toString()}` : ""}`, { method: "GET" }, null, ); return await response.json(); } - async listSessionRecordingChunks(sessionRecordingId: string, params?: AdminListSessionRecordingChunksOptions): Promise { + async listSessionReplayChunks(sessionReplayId: string, params?: AdminListSessionReplayChunksOptions): Promise { const qs = new URLSearchParams(); if (params?.cursor) qs.set("cursor", params.cursor); if (typeof params?.limit === "number") qs.set("limit", String(params.limit)); const response = await this.sendAdminRequest( - `/internal/session-recordings/${encodeURIComponent(sessionRecordingId)}/chunks${qs.size ? `?${qs.toString()}` : ""}`, + `/internal/session-replays/${encodeURIComponent(sessionReplayId)}/chunks${qs.size ? `?${qs.toString()}` : ""}`, { method: "GET" }, null, ); return await response.json(); } - async getSessionRecordingChunkEvents(sessionRecordingId: string, chunkId: string): Promise { + async getSessionReplayChunkEvents(sessionReplayId: string, chunkId: string): Promise { const response = await this.sendAdminRequest( - `/internal/session-recordings/${encodeURIComponent(sessionRecordingId)}/chunks/${encodeURIComponent(chunkId)}/events`, + `/internal/session-replays/${encodeURIComponent(sessionReplayId)}/chunks/${encodeURIComponent(chunkId)}/events`, { method: "GET" }, null, ); return await response.json(); } - async getSessionRecordingEvents(sessionRecordingId: string, options?: { offset?: number, limit?: number }): Promise { + async getSessionReplayEvents(sessionReplayId: string, options?: { offset?: number, limit?: number }): Promise { const qs = new URLSearchParams(); if (typeof options?.offset === "number") qs.set("offset", String(options.offset)); if (typeof options?.limit === "number") qs.set("limit", String(options.limit)); const response = await this.sendAdminRequest( - `/internal/session-recordings/${encodeURIComponent(sessionRecordingId)}/events${qs.size ? `?${qs.toString()}` : ""}`, + `/internal/session-replays/${encodeURIComponent(sessionReplayId)}/events${qs.size ? `?${qs.toString()}` : ""}`, { method: "GET" }, null, ); diff --git a/packages/stack-shared/src/interface/client-interface.ts b/packages/stack-shared/src/interface/client-interface.ts index 319824ad98..23419b4fcf 100644 --- a/packages/stack-shared/src/interface/client-interface.ts +++ b/packages/stack-shared/src/interface/client-interface.ts @@ -244,14 +244,14 @@ export class StackClientInterface { return session; } - async sendSessionRecordingBatch( + async sendSessionReplayBatch( body: string, session: InternalSession | null, options: { keepalive: boolean }, ): Promise> { try { const response = await this.sendClientRequest( - "/session-recordings/batch", + "/session-replays/batch", { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/packages/stack-shared/src/interface/crud/session-recordings.ts b/packages/stack-shared/src/interface/crud/session-replays.ts similarity index 77% rename from packages/stack-shared/src/interface/crud/session-recordings.ts rename to packages/stack-shared/src/interface/crud/session-replays.ts index 138e0ad94d..4c7faee297 100644 --- a/packages/stack-shared/src/interface/crud/session-recordings.ts +++ b/packages/stack-shared/src/interface/crud/session-replays.ts @@ -1,9 +1,9 @@ -export type AdminListSessionRecordingsOptions = { +export type AdminListSessionReplaysOptions = { limit?: number, cursor?: string, }; -export type AdminListSessionRecordingsResponse = { +export type AdminListSessionReplaysResponse = { items: Array<{ id: string, project_user: { @@ -21,12 +21,12 @@ export type AdminListSessionRecordingsResponse = { }, }; -export type AdminListSessionRecordingChunksOptions = { +export type AdminListSessionReplayChunksOptions = { limit?: number, cursor?: string, }; -export type AdminListSessionRecordingChunksResponse = { +export type AdminListSessionReplayChunksResponse = { items: Array<{ id: string, batch_id: string, @@ -43,11 +43,11 @@ export type AdminListSessionRecordingChunksResponse = { }, }; -export type AdminGetSessionRecordingChunkEventsResponse = { +export type AdminGetSessionReplayChunkEventsResponse = { events: unknown[], }; -export type AdminGetSessionRecordingAllEventsResponse = { +export type AdminGetSessionReplayAllEventsResponse = { chunks: Array<{ id: string, batch_id: string, @@ -63,4 +63,3 @@ export type AdminGetSessionRecordingAllEventsResponse = { events: unknown[], }>, }; - diff --git a/packages/template/src/index.ts b/packages/template/src/index.ts index a64b0cfc96..f97202d24a 100644 --- a/packages/template/src/index.ts +++ b/packages/template/src/index.ts @@ -2,7 +2,7 @@ export * from './lib/stack-app'; export { getConvexProvidersConfig } from "./integrations/convex"; // IF_PLATFORM react-like -export type { AnalyticsOptions, AnalyticsReplayOptions } from "./lib/stack-app/apps/implementations/session-recording"; +export type { AnalyticsOptions, AnalyticsReplayOptions } from "./lib/stack-app/apps/implementations/session-replay"; export { default as StackHandler } from "./components-page/stack-handler"; export { useStackApp, useUser } from "./lib/hooks"; export { default as StackProvider } from "./providers/stack-provider"; diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index 9fd48d44b3..389f53db74 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -2,7 +2,7 @@ import { StackAdminInterface } from "@stackframe/stack-shared"; import { getProductionModeErrors } from "@stackframe/stack-shared/dist/helpers/production-mode"; import { InternalApiKeyCreateCrudResponse } from "@stackframe/stack-shared/dist/interface/admin-interface"; import { AnalyticsQueryOptions, AnalyticsQueryResponse } from "@stackframe/stack-shared/dist/interface/crud/analytics"; -import type { AdminGetSessionRecordingAllEventsResponse, AdminGetSessionRecordingChunkEventsResponse } from "@stackframe/stack-shared/dist/interface/crud/session-recordings"; +import type { AdminGetSessionReplayAllEventsResponse, AdminGetSessionReplayChunkEventsResponse } from "@stackframe/stack-shared/dist/interface/crud/session-replays"; import { EmailTemplateCrud } from "@stackframe/stack-shared/dist/interface/crud/email-templates"; import { InternalApiKeysCrud } from "@stackframe/stack-shared/dist/interface/crud/internal-api-keys"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; @@ -19,7 +19,7 @@ import { AdminEmailTemplate } from "../../email-templates"; import { InternalApiKey, InternalApiKeyBase, InternalApiKeyBaseCrudRead, InternalApiKeyCreateOptions, InternalApiKeyFirstView, internalApiKeyCreateOptionsToCrud } from "../../internal-api-keys"; import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, adminProjectPermissionDefinitionCreateOptionsToCrud, adminProjectPermissionDefinitionUpdateOptionsToCrud, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud } from "../../permissions"; import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, PushConfigOptions, adminProjectUpdateOptionsToCrud } from "../../projects"; -import type { AdminSessionRecording, AdminSessionRecordingChunk, ListSessionRecordingChunksOptions, ListSessionRecordingChunksResult, ListSessionRecordingsOptions, ListSessionRecordingsResult, SessionRecordingAllEventsResult } from "../../session-recordings"; +import type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays"; import { StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app"; import { clientVersion, createCache, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, resolveConstructorOptions } from "./common"; import { _StackServerAppImplIncomplete } from "./server-app-impl"; @@ -1042,13 +1042,13 @@ export class _StackAdminAppImplIncomplete { - const response = await this._interface.listSessionRecordings({ + async listSessionReplays(options?: ListSessionReplaysOptions): Promise { + const response = await this._interface.listSessionReplays({ cursor: options?.cursor, limit: options?.limit, }); - const items: AdminSessionRecording[] = response.items.map((r) => ({ + const items: AdminSessionReplay[] = response.items.map((r) => ({ id: r.id, projectUser: { id: r.project_user.id, @@ -1067,13 +1067,13 @@ export class _StackAdminAppImplIncomplete { - const response = await this._interface.listSessionRecordingChunks(sessionRecordingId, { + async listSessionReplayChunks(sessionReplayId: string, options?: ListSessionReplayChunksOptions): Promise { + const response = await this._interface.listSessionReplayChunks(sessionReplayId, { cursor: options?.cursor, limit: options?.limit, }); - const items: AdminSessionRecordingChunk[] = response.items.map((c) => ({ + const items: AdminSessionReplayChunk[] = response.items.map((c) => ({ id: c.id, batchId: c.batch_id, sessionReplaySegmentId: c.session_replay_segment_id, @@ -1091,12 +1091,12 @@ export class _StackAdminAppImplIncomplete { - return await this._interface.getSessionRecordingChunkEvents(sessionRecordingId, chunkId); + async getSessionReplayChunkEvents(sessionReplayId: string, chunkId: string): Promise { + return await this._interface.getSessionReplayChunkEvents(sessionReplayId, chunkId); } - async getSessionRecordingEvents(sessionRecordingId: string, options?: { offset?: number, limit?: number }): Promise { - const response = await this._interface.getSessionRecordingEvents(sessionRecordingId, options); + async getSessionReplayEvents(sessionReplayId: string, options?: { offset?: number, limit?: number }): Promise { + const response = await this._interface.getSessionReplayEvents(sessionReplayId, options); return { chunks: response.chunks.map((c) => ({ id: c.id, diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts index 0612286105..2c9d8e3b44 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts @@ -53,7 +53,7 @@ import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, OAuthPro import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app"; import { _StackAdminAppImplIncomplete } from "./admin-app-impl"; import { TokenObject, clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common"; -import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-recording"; +import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay"; // IF_PLATFORM react-like import { useAsyncCache } from "./common"; @@ -446,7 +446,7 @@ export class _StackClientAppImplIncomplete { - return await this._interface.sendSessionRecordingBatch(body, await this._getSession(), opts); + return await this._interface.sendSessionReplayBatch(body, await this._getSession(), opts); }, }, this._analyticsOptions.replays); this._sessionRecorder.start(); @@ -2853,8 +2853,8 @@ export class _StackClientAppImplIncomplete this._options, - sendSessionRecordingBatch: async (body: string, options: { keepalive: boolean }) => { - return await this._interface.sendSessionRecordingBatch(body, await this._getSession(), options); + sendSessionReplayBatch: async (body: string, options: { keepalive: boolean }) => { + return await this._interface.sendSessionReplayBatch(body, await this._getSession(), options); }, sendRequest: async ( path: string, diff --git a/packages/template/src/lib/stack-app/apps/implementations/session-recording.ts b/packages/template/src/lib/stack-app/apps/implementations/session-replay.ts similarity index 99% rename from packages/template/src/lib/stack-app/apps/implementations/session-recording.ts rename to packages/template/src/lib/stack-app/apps/implementations/session-replay.ts index 226df280bf..70bd146c16 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/session-recording.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/session-replay.ts @@ -79,7 +79,7 @@ export function analyticsOptionsFromJson(json: AnalyticsOptions | undefined): An // ---------- Recording internals ---------- -const LOCAL_STORAGE_PREFIX = "stack:session-recording:v1"; +const LOCAL_STORAGE_PREFIX = "stack:session-replay:v1"; const IDLE_TTL_MS = 3 * 60 * 1000; const FLUSH_INTERVAL_MS = 5_000; diff --git a/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts b/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts index ea3e0dc088..5e3c6db5f3 100644 --- a/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts +++ b/packages/template/src/lib/stack-app/apps/interfaces/admin-app.ts @@ -1,6 +1,6 @@ import { ChatContent } from "@stackframe/stack-shared/dist/interface/admin-interface"; import { AnalyticsQueryOptions, AnalyticsQueryResponse } from "@stackframe/stack-shared/dist/interface/crud/analytics"; -import type { AdminGetSessionRecordingChunkEventsResponse, AdminGetSessionRecordingAllEventsResponse } from "@stackframe/stack-shared/dist/interface/crud/session-recordings"; +import type { AdminGetSessionReplayChunkEventsResponse, AdminGetSessionReplayAllEventsResponse } from "@stackframe/stack-shared/dist/interface/crud/session-replays"; import type { Transaction, TransactionType } from "@stackframe/stack-shared/dist/interface/crud/transactions"; import { InternalSession } from "@stackframe/stack-shared/dist/sessions"; import type { MoneyAmount } from "@stackframe/stack-shared/dist/utils/currency-constants"; @@ -32,8 +32,8 @@ export type EmailOutboxUpdateOptions = { cancel?: boolean, }; -import type { ListSessionRecordingChunksOptions, ListSessionRecordingChunksResult, ListSessionRecordingsOptions, ListSessionRecordingsResult, SessionRecordingAllEventsResult } from "../../session-recordings"; -export type { AdminSessionRecording, AdminSessionRecordingChunk, ListSessionRecordingsOptions, ListSessionRecordingsResult, ListSessionRecordingChunksOptions, ListSessionRecordingChunksResult, SessionRecordingAllEventsResult } from "../../session-recordings"; +import type { ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays"; +export type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplaysOptions, ListSessionReplaysResult, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, SessionReplayAllEventsResult } from "../../session-replays"; export type StackAdminAppConstructorOptions = ( @@ -136,10 +136,10 @@ export type StackAdminApp, queryAnalytics(options: AnalyticsQueryOptions): Promise, - listSessionRecordings(options?: ListSessionRecordingsOptions): Promise, - listSessionRecordingChunks(sessionRecordingId: string, options?: ListSessionRecordingChunksOptions): Promise, - getSessionRecordingChunkEvents(sessionRecordingId: string, chunkId: string): Promise, - getSessionRecordingEvents(sessionRecordingId: string, options?: { offset?: number, limit?: number }): Promise, + listSessionReplays(options?: ListSessionReplaysOptions): Promise, + listSessionReplayChunks(sessionReplayId: string, options?: ListSessionReplayChunksOptions): Promise, + getSessionReplayChunkEvents(sessionReplayId: string, chunkId: string): Promise, + getSessionReplayEvents(sessionReplayId: string, options?: { offset?: number, limit?: number }): Promise, // Email Outbox methods listOutboxEmails(options?: EmailOutboxListOptions): Promise, diff --git a/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts b/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts index 4c6261483a..79006b412e 100644 --- a/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts +++ b/packages/template/src/lib/stack-app/apps/interfaces/client-app.ts @@ -6,7 +6,7 @@ import { CustomerInvoicesList, CustomerInvoicesRequestOptions, CustomerProductsL import { Project } from "../../projects"; import { ProjectCurrentUser, SyncedPartialUser, TokenPartialUser } from "../../users"; import { _StackClientAppImpl } from "../implementations"; -import { AnalyticsOptions } from "../implementations/session-recording"; +import { AnalyticsOptions } from "../implementations/session-replay"; export type StackClientAppConstructorOptions = { baseUrl?: string | { browser: string, server: string }, @@ -100,7 +100,7 @@ export type StackClientApp, setCurrentUser(userJsonPromise: Promise): void, getConstructorOptions(): StackClientAppConstructorOptions & { inheritsFrom?: undefined }, - sendSessionRecordingBatch(body: string, options: { keepalive: boolean }): Promise>, + sendSessionReplayBatch(body: string, options: { keepalive: boolean }): Promise>, }, } & AsyncStoreProperty<"project", [], Project, false> diff --git a/packages/template/src/lib/stack-app/session-recordings/index.ts b/packages/template/src/lib/stack-app/session-replays/index.ts similarity index 68% rename from packages/template/src/lib/stack-app/session-recordings/index.ts rename to packages/template/src/lib/stack-app/session-replays/index.ts index 71f7be6f0e..781cb86012 100644 --- a/packages/template/src/lib/stack-app/session-recordings/index.ts +++ b/packages/template/src/lib/stack-app/session-replays/index.ts @@ -1,4 +1,4 @@ -export type AdminSessionRecording = { +export type AdminSessionReplay = { id: string, projectUser: { id: string, @@ -11,7 +11,7 @@ export type AdminSessionRecording = { eventCount: number, }; -export type AdminSessionRecordingChunk = { +export type AdminSessionReplayChunk = { id: string, batchId: string, sessionReplaySegmentId: string | null, @@ -23,27 +23,27 @@ export type AdminSessionRecordingChunk = { createdAt: Date, }; -export type ListSessionRecordingsOptions = { +export type ListSessionReplaysOptions = { limit?: number, cursor?: string, }; -export type ListSessionRecordingsResult = { - items: AdminSessionRecording[], +export type ListSessionReplaysResult = { + items: AdminSessionReplay[], nextCursor: string | null, }; -export type ListSessionRecordingChunksOptions = { +export type ListSessionReplayChunksOptions = { limit?: number, cursor?: string, }; -export type ListSessionRecordingChunksResult = { - items: AdminSessionRecordingChunk[], +export type ListSessionReplayChunksResult = { + items: AdminSessionReplayChunk[], nextCursor: string | null, }; -export type SessionRecordingAllEventsResult = { +export type SessionReplayAllEventsResult = { chunks: Array<{ id: string, batchId: string,