Skip to content

Commit 68ab2d3

Browse files
committed
rename tab id
1 parent d8b3c51 commit 68ab2d3

16 files changed

Lines changed: 96 additions & 62 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "SessionRecordingChunk" RENAME COLUMN "tabId" TO "sessionReplaySegmentId";

apps/backend/prisma/schema.prisma

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ model SessionRecordingChunk {
314314
// Unique per uploaded batch for a given session id.
315315
batchId String @db.Uuid
316316
317-
// Ephemeral in-memory id generated by the client. Stored for future tab separation if needed.
318-
tabId String
317+
// Ephemeral in-memory id generated by the client. Used to group recording chunks into per-tab replay segments.
318+
sessionReplaySegmentId String
319319
320320
// Client-generated session id from localStorage, stored as metadata.
321321
browserSessionId String

apps/backend/scripts/clickhouse-migrations.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export async function runClickhouseMigrations() {
1616
await client.exec({ query: EVENTS_VIEW_SQL });
1717
await client.exec({ query: USERS_TABLE_BASE_SQL });
1818
await client.exec({ query: USERS_VIEW_SQL });
19+
await client.exec({ query: EVENTS_ADD_REPLAY_COLUMNS_SQL });
1920
await client.exec({ query: TOKEN_REFRESH_EVENT_ROW_FORMAT_MUTATION_SQL });
21+
await client.exec({ query: BACKFILL_REFRESH_TOKEN_ID_COLUMN_SQL });
2022
await client.exec({ query: SIGN_UP_RULE_TRIGGER_EVENT_ROW_FORMAT_MUTATION_SQL });
2123
const queries = [
2224
"REVOKE ALL PRIVILEGES ON *.* FROM limited_user;",
@@ -177,6 +179,22 @@ ENGINE ReplacingMergeTree(updated_at)
177179
ORDER BY (tenancy_id, mapping_name);
178180
`;
179181

182+
const EVENTS_ADD_REPLAY_COLUMNS_SQL = `
183+
ALTER TABLE analytics_internal.events
184+
ADD COLUMN IF NOT EXISTS refresh_token_id Nullable(String) AFTER team_id,
185+
ADD COLUMN IF NOT EXISTS session_replay_id Nullable(String) AFTER refresh_token_id,
186+
ADD COLUMN IF NOT EXISTS session_replay_segment_id Nullable(String) AFTER session_replay_id;
187+
`;
188+
189+
// Backfill refresh_token_id from data.refresh_token_id for existing $token-refresh rows
190+
const BACKFILL_REFRESH_TOKEN_ID_COLUMN_SQL = `
191+
ALTER TABLE analytics_internal.events
192+
UPDATE refresh_token_id = data.refresh_token_id::Nullable(String)
193+
WHERE event_type = '$token-refresh'
194+
AND refresh_token_id IS NULL
195+
AND data.refresh_token_id::Nullable(String) IS NOT NULL;
196+
`;
197+
180198
const EXTERNAL_ANALYTICS_DB_SQL = `
181199
CREATE DATABASE IF NOT EXISTS analytics_internal;
182200
`;

apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/chunks/route.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const GET = createSmartRouteHandler({
2929
items: yupArray(yupObject({
3030
id: yupString().defined(),
3131
batch_id: yupString().defined(),
32-
tab_id: yupString().nullable().defined(),
32+
session_replay_segment_id: yupString().nullable().defined(),
3333
browser_session_id: yupString().nullable().defined(),
3434
event_count: yupNumber().defined(),
3535
byte_length: yupNumber().defined(),
@@ -92,7 +92,7 @@ export const GET = createSmartRouteHandler({
9292
select: {
9393
id: true,
9494
batchId: true,
95-
tabId: true,
95+
sessionReplaySegmentId: true,
9696
browserSessionId: true,
9797
eventCount: true,
9898
byteLength: true,
@@ -113,7 +113,7 @@ export const GET = createSmartRouteHandler({
113113
items: page.map((c) => ({
114114
id: c.id,
115115
batch_id: c.batchId,
116-
tab_id: c.tabId,
116+
session_replay_segment_id: c.sessionReplaySegmentId,
117117
browser_session_id: c.browserSessionId,
118118
event_count: c.eventCount,
119119
byte_length: c.byteLength,

apps/backend/src/app/api/latest/internal/session-recordings/[session_recording_id]/events/route.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const GET = createSmartRouteHandler({
3333
chunks: yupArray(yupObject({
3434
id: yupString().defined(),
3535
batch_id: yupString().defined(),
36-
tab_id: yupString().nullable().defined(),
36+
session_replay_segment_id: yupString().nullable().defined(),
3737
event_count: yupNumber().defined(),
3838
byte_length: yupNumber().defined(),
3939
first_event_at_millis: yupNumber().defined(),
@@ -67,7 +67,7 @@ export const GET = createSmartRouteHandler({
6767
select: {
6868
id: true,
6969
batchId: true,
70-
tabId: true,
70+
sessionReplaySegmentId: true,
7171
eventCount: true,
7272
byteLength: true,
7373
firstEventAt: true,
@@ -143,7 +143,7 @@ export const GET = createSmartRouteHandler({
143143
chunks: chunks.map((c) => ({
144144
id: c.id,
145145
batch_id: c.batchId,
146-
tab_id: c.tabId,
146+
session_replay_segment_id: c.sessionReplaySegmentId,
147147
event_count: c.eventCount,
148148
byte_length: c.byteLength,
149149
first_event_at_millis: c.firstEventAt.getTime(),

apps/backend/src/app/api/latest/session-recordings/batch/route.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const POST = createSmartRouteHandler({
5353
}).defined(),
5454
body: yupObject({
5555
browser_session_id: yupString().defined().matches(UUID_RE, "Invalid browser_session_id"),
56-
tab_id: yupString().defined().matches(UUID_RE, "Invalid tab_id"),
56+
session_replay_segment_id: yupString().defined().matches(UUID_RE, "Invalid session_replay_segment_id"),
5757
batch_id: yupString().defined().matches(UUID_RE, "Invalid batch_id"),
5858
started_at_ms: yupNumber().defined().integer().min(0),
5959
sent_at_ms: yupNumber().defined().integer().min(0),
@@ -105,7 +105,7 @@ export const POST = createSmartRouteHandler({
105105

106106
const browserSessionId = body.browser_session_id;
107107
const batchId = body.batch_id;
108-
const tabId = body.tab_id;
108+
const sessionReplaySegmentId = body.session_replay_segment_id;
109109
const tenancyId = auth.tenancy.id;
110110

111111
const projectId = auth.tenancy.project.id;
@@ -174,7 +174,7 @@ export const POST = createSmartRouteHandler({
174174
v: 1,
175175
session_recording_id: recordingId,
176176
browser_session_id: browserSessionId,
177-
tab_id: tabId,
177+
session_replay_segment_id: sessionReplaySegmentId,
178178
batch_id: batchId,
179179
started_at_ms: body.started_at_ms,
180180
sent_at_ms: body.sent_at_ms,
@@ -197,7 +197,7 @@ export const POST = createSmartRouteHandler({
197197
tenancyId,
198198
sessionRecordingId: recordingId,
199199
batchId,
200-
tabId,
200+
sessionReplaySegmentId,
201201
browserSessionId,
202202
s3Key,
203203
eventCount: body.events.length,

apps/backend/src/lib/events.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ export async function logEvent<T extends EventType[]>(
196196
data: DataOfMany<T>,
197197
options: {
198198
time?: Date | { start: Date, end: Date },
199+
refreshTokenId?: string,
200+
sessionReplayId?: string,
201+
sessionReplaySegmentId?: string,
199202
} = {}
200203
) {
201204
let timeOrTimeRange = options.time ?? new Date();
@@ -320,6 +323,12 @@ export async function logEvent<T extends EventType[]>(
320323
);
321324
}
322325
const clickhouseClient = getClickhouseAdminClient();
326+
// Resolve refresh_token_id: prefer explicit option, fall back to data for $token-refresh events
327+
const resolvedRefreshTokenId = options.refreshTokenId
328+
?? (matchingEventType.id === "$token-refresh" && typeof (clickhouseEventData as any).refresh_token_id === "string"
329+
? (clickhouseEventData as any).refresh_token_id as string
330+
: null);
331+
323332
await clickhouseClient.insert({
324333
table: "analytics_internal.events",
325334
values: [{
@@ -330,6 +339,9 @@ export async function logEvent<T extends EventType[]>(
330339
branch_id: branchId,
331340
user_id: userId || null,
332341
team_id: null,
342+
refresh_token_id: resolvedRefreshTokenId ?? null,
343+
session_replay_id: options.sessionReplayId ?? null,
344+
session_replay_segment_id: options.sessionReplaySegmentId ?? null,
333345
}],
334346
format: "JSONEachRow",
335347
clickhouse_settings: {

apps/backend/src/lib/tokens.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ export async function generateAccessTokenFromRefreshTokenIfValid(options: Refres
293293
isAnonymous: user.is_anonymous,
294294
teamId: undefined,
295295
ipInfo,
296+
},
297+
{
298+
refreshTokenId: options.refreshTokenObj.id,
296299
}
297300
);
298301

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type RecordingRow = {
6060
type ChunkRow = {
6161
id: string,
6262
batchId: string,
63-
tabId: string | null,
63+
sessionReplaySegmentId: string | null,
6464
eventCount: number,
6565
byteLength: number,
6666
firstEventAt: Date,
@@ -860,7 +860,7 @@ export default function PageClient() {
860860
const allChunkRows: ChunkRow[] = initialResponse.chunks.map((c) => ({
861861
id: c.id,
862862
batchId: c.batchId,
863-
tabId: c.tabId,
863+
sessionReplaySegmentId: c.sessionReplaySegmentId,
864864
eventCount: c.eventCount,
865865
byteLength: c.byteLength,
866866
firstEventAt: c.firstEventAt,

apps/dashboard/src/lib/session-replay-streams.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ function d(ms: number) {
1414
}
1515

1616
describe("session-replay-streams", () => {
17-
it("treats null tabId as its own stream", () => {
17+
it("treats null sessionReplaySegmentId as its own stream", () => {
1818
const streams = groupChunksIntoTabStreams([
19-
{ tabId: null, firstEventAt: d(10), lastEventAt: d(20), eventCount: 2 },
20-
{ tabId: null, firstEventAt: d(21), lastEventAt: d(30), eventCount: 1 },
21-
{ tabId: "a", firstEventAt: d(5), lastEventAt: d(25), eventCount: 10 },
19+
{ sessionReplaySegmentId: null, firstEventAt: d(10), lastEventAt: d(20), eventCount: 2 },
20+
{ sessionReplaySegmentId: null, firstEventAt: d(21), lastEventAt: d(30), eventCount: 1 },
21+
{ sessionReplaySegmentId: "a", firstEventAt: d(5), lastEventAt: d(25), eventCount: 10 },
2222
]);
2323

2424
expect(streams.map(s => s.tabKey).sort()).toEqual([NULL_TAB_KEY, "a"].sort());
@@ -27,19 +27,19 @@ describe("session-replay-streams", () => {
2727

2828
it("sorts streams by lastEventAt desc then eventCount desc", () => {
2929
const streams = groupChunksIntoTabStreams([
30-
{ tabId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 5 },
31-
{ tabId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 },
32-
{ tabId: "c", firstEventAt: d(0), lastEventAt: d(20), eventCount: 9 },
30+
{ sessionReplaySegmentId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 5 },
31+
{ sessionReplaySegmentId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 },
32+
{ sessionReplaySegmentId: "c", firstEventAt: d(0), lastEventAt: d(20), eventCount: 9 },
3333
]);
3434

35-
expect(streams.map(s => s.tabId)).toEqual(["c", "b", "a"]);
35+
expect(streams.map(s => s.sessionReplaySegmentId)).toEqual(["c", "b", "a"]);
3636
});
3737

3838
it("limits streams and reports hiddenCount", () => {
3939
const streams = groupChunksIntoTabStreams([
40-
{ tabId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 1 },
41-
{ tabId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 },
42-
{ tabId: "c", firstEventAt: d(0), lastEventAt: d(30), eventCount: 1 },
40+
{ sessionReplaySegmentId: "a", firstEventAt: d(0), lastEventAt: d(10), eventCount: 1 },
41+
{ sessionReplaySegmentId: "b", firstEventAt: d(0), lastEventAt: d(20), eventCount: 1 },
42+
{ sessionReplaySegmentId: "c", firstEventAt: d(0), lastEventAt: d(30), eventCount: 1 },
4343
]);
4444

4545
const limited = limitTabStreams(streams, 2);
@@ -49,8 +49,8 @@ describe("session-replay-streams", () => {
4949

5050
it("maps global offsets to local offsets and back", () => {
5151
const streams = groupChunksIntoTabStreams([
52-
{ tabId: "a", firstEventAt: d(1000), lastEventAt: d(5000), eventCount: 1 },
53-
{ tabId: "b", firstEventAt: d(2000), lastEventAt: d(4000), eventCount: 1 },
52+
{ sessionReplaySegmentId: "a", firstEventAt: d(1000), lastEventAt: d(5000), eventCount: 1 },
53+
{ sessionReplaySegmentId: "b", firstEventAt: d(2000), lastEventAt: d(4000), eventCount: 1 },
5454
]);
5555
const { globalStartTs } = computeGlobalTimeline(streams);
5656

0 commit comments

Comments
 (0)