Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .context/retros/2026-03-12-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"date": "2026-03-12",
"window": "7d",
"metrics": {
"commits": 7,
"prs_merged": 2,
"insertions": 3003,
"deletions": 165,
"net_loc": 2838,
"test_loc": 473,
"test_ratio": 0.16,
"active_days": 3,
"sessions": 6,
"deep_sessions": 0,
"avg_session_minutes": 7,
"loc_per_session_hour": 4200,
"feat_pct": 0.29,
"fix_pct": 0.57,
"peak_hour": 10
},
"version_range": ["package-bump", "package-bump"],
"streak_days": 0,
"tweetable": "Week of Mar 5: 7 commits, 3.0k LOC, 16% tests, 2 PRs, peak: 10am | Streak: 0d"
}
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
## Commands
- **Lint**: `pnpm lint`
- **Typecheck**: `pnpm typecheck`

## gstack
- Use the `/browse` skill from `gstack` for all web browsing.
- Never use `mcp__claude-in-chrome__*` tools.
- Available gstack skills: `/plan-ceo-review`, `/plan-eng-review`, `/review`, `/ship`, `/browse`, `/retro`
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
CREATE TYPE "ReplayAiAnalysisStatus" AS ENUM ('PENDING', 'READY', 'ERROR');
CREATE TYPE "ReplayIssueSeverity" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL');

CREATE TABLE "ReplayIssueCluster" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenancyId" UUID NOT NULL,
"fingerprint" TEXT NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT,
"severity" "ReplayIssueSeverity" NOT NULL,
"confidence" DOUBLE PRECISION NOT NULL,
"occurrenceCount" INTEGER NOT NULL,
"affectedUserCount" INTEGER NOT NULL,
"firstSeenAt" TIMESTAMP(3) NOT NULL,
"lastSeenAt" TIMESTAMP(3) NOT NULL,
"topEvidence" JSONB NOT NULL DEFAULT '[]'::jsonb,
"textEmbedding" JSONB,
"visualEmbedding" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ReplayIssueCluster_pkey" PRIMARY KEY ("id")
);

CREATE TABLE "ReplayAiSummary" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenancyId" UUID NOT NULL,
"sessionReplayId" UUID NOT NULL,
"issueClusterId" UUID,
"status" "ReplayAiAnalysisStatus" NOT NULL,
"issueFingerprint" TEXT,
"issueTitle" TEXT,
"summary" TEXT,
"whyLikely" TEXT,
"severity" "ReplayIssueSeverity",
"confidence" DOUBLE PRECISION,
"evidence" JSONB NOT NULL DEFAULT '[]'::jsonb,
"visualArtifacts" JSONB NOT NULL DEFAULT '[]'::jsonb,
"relatedReplayIds" JSONB NOT NULL DEFAULT '[]'::jsonb,
"textEmbedding" JSONB,
"visualEmbedding" JSONB,
"providerMetadata" JSONB NOT NULL DEFAULT '{}'::jsonb,
"errorMessage" TEXT,
"analyzedAt" TIMESTAMP(3),
"lastAnalyzedChunkCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ReplayAiSummary_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "ReplayIssueCluster_tenancyId_fingerprint_key" ON "ReplayIssueCluster"("tenancyId", "fingerprint");
CREATE UNIQUE INDEX "ReplayAiSummary_tenancyId_sessionReplayId_key" ON "ReplayAiSummary"("tenancyId", "sessionReplayId");
CREATE INDEX "ReplayAiSummary_tenancyId_issueClusterId_idx" ON "ReplayAiSummary"("tenancyId", "issueClusterId");

ALTER TABLE "ReplayIssueCluster"
ADD CONSTRAINT "ReplayIssueCluster_tenancyId_fkey"
FOREIGN KEY ("tenancyId") REFERENCES "Tenancy"("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "ReplayAiSummary"
ADD CONSTRAINT "ReplayAiSummary_tenancyId_fkey"
FOREIGN KEY ("tenancyId") REFERENCES "Tenancy"("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "ReplayAiSummary"
ADD CONSTRAINT "ReplayAiSummary_tenancyId_sessionReplayId_fkey"
FOREIGN KEY ("tenancyId", "sessionReplayId") REFERENCES "SessionReplay"("tenancyId", "id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "ReplayAiSummary"
ADD CONSTRAINT "ReplayAiSummary_issueClusterId_fkey"
FOREIGN KEY ("issueClusterId") REFERENCES "ReplayIssueCluster"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { randomUUID } from "crypto";
import type { Sql } from "postgres";
import { expect } from "vitest";

export const preMigration = async (sql: Sql) => {
const projectId = `test-${randomUUID()}`;
const tenancyId = randomUUID();
const projectUserId = randomUUID();
const sessionReplayId = randomUUID();

await sql`INSERT INTO "Project" ("id", "createdAt", "updatedAt", "displayName", "description", "isProductionMode") VALUES (${projectId}, NOW(), NOW(), 'Test', '', false)`;
await sql`INSERT INTO "Tenancy" ("id", "createdAt", "updatedAt", "projectId", "branchId", "hasNoOrganization") VALUES (${tenancyId}::uuid, NOW(), NOW(), ${projectId}, 'main', 'TRUE'::"BooleanTrue")`;
await sql`INSERT INTO "ProjectUser" ("projectUserId", "tenancyId", "mirroredProjectId", "mirroredBranchId", "createdAt", "updatedAt", "lastActiveAt") VALUES (${projectUserId}::uuid, ${tenancyId}::uuid, ${projectId}, 'main', NOW(), NOW(), NOW())`;
await sql`

Check failure on line 14 in apps/backend/prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

src/auto-migrations/migration-tests.test.ts > database migration tests > 20260312120000_replay_ai_analytics

PostgresError: column "sessionRefreshTokenId" of relation "SessionReplay" does not exist ❯ ErrorResponse ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:794:26 ❯ handle ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:480:6 ❯ Socket.data ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:315:9 ❯ Socket.emit ../../node:events:519:28 ❯ cachedError ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:170:23 ❯ new Query ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:36:24 ❯ sql ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/index.js:112:11 ❯ Module.preMigration prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts:14:9 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { severity_local: 'ERROR', severity: 'ERROR', code: '42703', position: '71', file: 'parse_target.c', line: '1075', routine: 'checkInsertTargets', query: '\n INSERT INTO "SessionReplay" ("id", "tenancyId", "projectUserId", "sessionRefreshTokenId", "sessionReplaySegmentId", "startedAt", "lastEventAt", "createdAt", "updatedAt")\n VALUES ($1::uuid, $2::uuid, $3::uuid, \'placeholder-refresh-token\', \'segment-1\', NOW(), NOW(), NOW(), NOW())\n ', parameters: [ 'e536513e-e6d4-4ed8-8252-66062db27fe3', 'dcf9df63-ba26-41c7-9c3b-e3ac2c1035b9', 'b7f36d43-e9a0-4c4c-aa83-3bd9b46e7b0d' ], args: [ 'e536513e-e6d4-4ed8-8252-66062db27fe3', 'dcf9df63-ba26-41c7-9c3b-e3ac2c1035b9', 'b7f36d43-e9a0-4c4c-aa83-3bd9b46e7b0d' ], types: [ +0, +0, +0 ] }

Check failure on line 14 in apps/backend/prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (Local Emulator, Node 22.x)

src/auto-migrations/migration-tests.test.ts > database migration tests > 20260312120000_replay_ai_analytics

PostgresError: column "sessionRefreshTokenId" of relation "SessionReplay" does not exist ❯ ErrorResponse ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:794:26 ❯ handle ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:480:6 ❯ Socket.data ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:315:9 ❯ Socket.emit ../../node:events:519:28 ❯ cachedError ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:170:23 ❯ new Query ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:36:24 ❯ sql ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/index.js:112:11 ❯ Module.preMigration prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts:14:9 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { severity_local: 'ERROR', severity: 'ERROR', code: '42703', position: '71', file: 'parse_target.c', line: '1075', routine: 'checkInsertTargets', query: '\n INSERT INTO "SessionReplay" ("id", "tenancyId", "projectUserId", "sessionRefreshTokenId", "sessionReplaySegmentId", "startedAt", "lastEventAt", "createdAt", "updatedAt")\n VALUES ($1::uuid, $2::uuid, $3::uuid, \'placeholder-refresh-token\', \'segment-1\', NOW(), NOW(), NOW(), NOW())\n ', parameters: [ '379f6aad-d549-40e7-a473-4a29c8dce776', 'b3a45f0c-ad00-4f0c-b676-ee452cbb4071', '2eef49c6-fa47-431a-ac3f-0e195bfd4fde' ], args: [ '379f6aad-d549-40e7-a473-4a29c8dce776', 'b3a45f0c-ad00-4f0c-b676-ee452cbb4071', '2eef49c6-fa47-431a-ac3f-0e195bfd4fde' ], types: [ +0, +0, +0 ] }

Check failure on line 14 in apps/backend/prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (Node 22.x, Freestyle mock)

src/auto-migrations/migration-tests.test.ts > database migration tests > 20260312120000_replay_ai_analytics

PostgresError: column "sessionRefreshTokenId" of relation "SessionReplay" does not exist ❯ ErrorResponse ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:794:26 ❯ handle ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:480:6 ❯ Socket.data ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:315:9 ❯ Socket.emit ../../node:events:519:28 ❯ cachedError ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:170:23 ❯ new Query ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:36:24 ❯ sql ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/index.js:112:11 ❯ Module.preMigration prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts:14:9 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { severity_local: 'ERROR', severity: 'ERROR', code: '42703', position: '71', file: 'parse_target.c', line: '1075', routine: 'checkInsertTargets', query: '\n INSERT INTO "SessionReplay" ("id", "tenancyId", "projectUserId", "sessionRefreshTokenId", "sessionReplaySegmentId", "startedAt", "lastEventAt", "createdAt", "updatedAt")\n VALUES ($1::uuid, $2::uuid, $3::uuid, \'placeholder-refresh-token\', \'segment-1\', NOW(), NOW(), NOW(), NOW())\n ', parameters: [ '5f7d2469-6444-472e-8e43-17dd4db9b489', 'ecb048ea-d6c5-4d56-954f-fc19be2db18c', 'e3b9d5a9-3bf6-4739-bc50-b73ffc58ff34' ], args: [ '5f7d2469-6444-472e-8e43-17dd4db9b489', 'ecb048ea-d6c5-4d56-954f-fc19be2db18c', 'e3b9d5a9-3bf6-4739-bc50-b73ffc58ff34' ], types: [ +0, +0, +0 ] }

Check failure on line 14 in apps/backend/prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts

View workflow job for this annotation

GitHub Actions / Back-compat — Current branch migrations with dev branch code

src/auto-migrations/migration-tests.test.ts > database migration tests > 20260312120000_replay_ai_analytics

PostgresError: column "sessionRefreshTokenId" of relation "SessionReplay" does not exist ❯ ErrorResponse ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:794:26 ❯ handle ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:480:6 ❯ Socket.data ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:315:9 ❯ Socket.emit ../../node:events:519:28 ❯ cachedError ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:170:23 ❯ new Query ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:36:24 ❯ sql ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/index.js:112:11 ❯ Module.preMigration prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts:14:9 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { severity_local: 'ERROR', severity: 'ERROR', code: '42703', position: '71', file: 'parse_target.c', line: '1075', routine: 'checkInsertTargets', query: '\n INSERT INTO "SessionReplay" ("id", "tenancyId", "projectUserId", "sessionRefreshTokenId", "sessionReplaySegmentId", "startedAt", "lastEventAt", "createdAt", "updatedAt")\n VALUES ($1::uuid, $2::uuid, $3::uuid, \'placeholder-refresh-token\', \'segment-1\', NOW(), NOW(), NOW(), NOW())\n ', parameters: [ 'ca833d31-9b78-4867-8e0b-dfc7557d8c88', '0894fbee-7ac4-4220-ae39-0d7212fd46e8', '4f1a8f65-0389-4efc-a714-47b988490670' ], args: [ 'ca833d31-9b78-4867-8e0b-dfc7557d8c88', '0894fbee-7ac4-4220-ae39-0d7212fd46e8', '4f1a8f65-0389-4efc-a714-47b988490670' ], types: [ +0, +0, +0 ] }

Check failure on line 14 in apps/backend/prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts

View workflow job for this annotation

GitHub Actions / Back-compat — Current branch migrations with dev branch code

src/auto-migrations/migration-tests.test.ts > database migration tests > 20260312120000_replay_ai_analytics

PostgresError: column "sessionRefreshTokenId" of relation "SessionReplay" does not exist ❯ ErrorResponse ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:794:26 ❯ handle ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:480:6 ❯ Socket.data ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/connection.js:315:9 ❯ Socket.emit ../../node:events:519:28 ❯ cachedError ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:170:23 ❯ new Query ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/query.js:36:24 ❯ sql ../../node_modules/.pnpm/postgres@3.4.7/node_modules/postgres/src/index.js:112:11 ❯ Module.preMigration prisma/migrations/20260312120000_replay_ai_analytics/tests/creates-replay-ai-artifacts.ts:14:9 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { severity_local: 'ERROR', severity: 'ERROR', code: '42703', position: '71', file: 'parse_target.c', line: '1075', routine: 'checkInsertTargets', query: '\n INSERT INTO "SessionReplay" ("id", "tenancyId", "projectUserId", "sessionRefreshTokenId", "sessionReplaySegmentId", "startedAt", "lastEventAt", "createdAt", "updatedAt")\n VALUES ($1::uuid, $2::uuid, $3::uuid, \'placeholder-refresh-token\', \'segment-1\', NOW(), NOW(), NOW(), NOW())\n ', parameters: [ 'efd84ca3-5c9d-44a3-be82-cbbb04131903', '2399400a-06a7-4895-90a6-f0bf726b1cdf', '669226cd-324c-424d-839c-70a59e2bc96e' ], args: [ 'efd84ca3-5c9d-44a3-be82-cbbb04131903', '2399400a-06a7-4895-90a6-f0bf726b1cdf', '669226cd-324c-424d-839c-70a59e2bc96e' ], types: [ +0, +0, +0 ] }
INSERT INTO "SessionReplay" ("id", "tenancyId", "projectUserId", "sessionRefreshTokenId", "sessionReplaySegmentId", "startedAt", "lastEventAt", "createdAt", "updatedAt")
VALUES (${sessionReplayId}::uuid, ${tenancyId}::uuid, ${projectUserId}::uuid, 'placeholder-refresh-token', 'segment-1', NOW(), NOW(), NOW(), NOW())
`;

return { tenancyId, sessionReplayId };
};

export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof preMigration>>) => {
await sql`
INSERT INTO "ReplayIssueCluster" (
"id",
"tenancyId",
"fingerprint",
"title",
"summary",
"severity",
"confidence",
"occurrenceCount",
"affectedUserCount",
"firstSeenAt",
"lastSeenAt",
"topEvidence",
"textEmbedding",
"visualEmbedding",
"createdAt",
"updatedAt"
)
VALUES (
${randomUUID()}::uuid,
${ctx.tenancyId}::uuid,
'frontend-error:/sign-in',
'Frontend error surfaced during replay',
'The replay captured a frontend error.',
'HIGH'::"ReplayIssueSeverity",
0.92,
1,
1,
NOW(),
NOW(),
'[]'::jsonb,
'{"provider":"local-hash","model":"gemini-embedding-001","dimensions":2,"generated_at_millis":1,"values":[0.1,0.9]}'::jsonb,
NULL,
NOW(),
NOW()
)
`;

await sql`
INSERT INTO "ReplayAiSummary" (
"id",
"tenancyId",
"sessionReplayId",
"status",
"issueFingerprint",
"issueTitle",
"summary",
"whyLikely",
"severity",
"confidence",
"evidence",
"visualArtifacts",
"relatedReplayIds",
"providerMetadata",
"lastAnalyzedChunkCount",
"createdAt",
"updatedAt"
)
VALUES (
${randomUUID()}::uuid,
${ctx.tenancyId}::uuid,
${ctx.sessionReplayId}::uuid,
'READY'::"ReplayAiAnalysisStatus",
'frontend-error:/sign-in',
'Frontend error surfaced during replay',
'The replay captured a frontend error.',
'A direct browser error event was captured.',
'HIGH'::"ReplayIssueSeverity",
0.92,
'[]'::jsonb,
'[]'::jsonb,
'[]'::jsonb,
'{}'::jsonb,
1,
NOW(),
NOW()
)
`;

const [summary] = await sql`
SELECT "status"::text AS "status", "severity"::text AS "severity"
FROM "ReplayAiSummary"
WHERE "tenancyId" = ${ctx.tenancyId}::uuid
AND "sessionReplayId" = ${ctx.sessionReplayId}::uuid
`;

expect(summary).toMatchObject({
status: "READY",
severity: "HIGH",
});
};
83 changes: 83 additions & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ model Tenancy {
emailOutboxes EmailOutbox[]
sessionReplays SessionReplay[]
sessionReplayChunks SessionReplayChunk[]
replayAiSummaries ReplayAiSummary[]
replayIssueClusters ReplayIssueCluster[]
managedEmailDomains ManagedEmailDomain[]

// Email capacity boost - when set and in the future, email capacity is multiplied by 4
Expand Down Expand Up @@ -349,6 +351,7 @@ model SessionReplay {
tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade)

chunks SessionReplayChunk[]
replayAiSummary ReplayAiSummary?

@@id([tenancyId, id])
@@map("SessionReplay")
Expand Down Expand Up @@ -390,6 +393,86 @@ model SessionReplayChunk {
@@index([tenancyId, sessionReplayId, createdAt])
}

enum ReplayAiAnalysisStatus {
PENDING
READY
ERROR
}

enum ReplayIssueSeverity {
LOW
MEDIUM
HIGH
CRITICAL
}

model ReplayIssueCluster {
id String @id @default(uuid()) @db.Uuid

tenancyId String @db.Uuid
tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade)

fingerprint String
title String
summary String?
severity ReplayIssueSeverity
confidence Float

occurrenceCount Int
affectedUserCount Int

firstSeenAt DateTime
lastSeenAt DateTime

topEvidence Json
textEmbedding Json?
visualEmbedding Json?

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

replayAiSummaries ReplayAiSummary[]

@@unique([tenancyId, fingerprint])
}

model ReplayAiSummary {
id String @id @default(uuid()) @db.Uuid

tenancyId String @db.Uuid
sessionReplayId String @db.Uuid
issueClusterId String? @db.Uuid

tenancy Tenancy @relation(fields: [tenancyId], references: [id], onDelete: Cascade)
sessionReplay SessionReplay @relation(fields: [tenancyId, sessionReplayId], references: [tenancyId, id], onDelete: Cascade)
issueCluster ReplayIssueCluster? @relation(fields: [issueClusterId], references: [id], onDelete: SetNull)

status ReplayAiAnalysisStatus
issueFingerprint String?
issueTitle String?
summary String?
whyLikely String?
severity ReplayIssueSeverity?
confidence Float?

evidence Json
visualArtifacts Json
relatedReplayIds Json
textEmbedding Json?
visualEmbedding Json?
providerMetadata Json

errorMessage String?
analyzedAt DateTime?
lastAnalyzedChunkCount Int @default(0)

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([tenancyId, sessionReplayId])
@@index([tenancyId, issueClusterId])
}

enum ContactChannelType {
EMAIL
// PHONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

const MAX_EVENTS = 500;
const ALLOWED_EVENT_TYPES = ["$page-view", "$click", "$input", "$submit", "$error", "$network-error"] as const;

export const POST = createSmartRouteHandler({
metadata: {
summary: "Upload analytics event batch",
description: "Uploads a batch of auto-captured analytics events ($page-view, $click).",
description: "Uploads a batch of auto-captured analytics events for replay analysis and behavior insights.",
tags: ["Analytics Events"],
hidden: true,
},
Expand All @@ -30,7 +31,7 @@ export const POST = createSmartRouteHandler({
sent_at_ms: yupNumber().defined().integer().min(0),
events: yupArray(
yupObject({
event_type: yupString().defined().oneOf(["$page-view", "$click"]),
event_type: yupString().defined().oneOf(ALLOWED_EVENT_TYPES),
event_at_ms: yupNumber().defined().integer().min(0),
data: yupMixed().defined(),
}).defined(),
Expand Down Expand Up @@ -86,6 +87,7 @@ export const POST = createSmartRouteHandler({
clickhouse_settings: {
date_time_input_format: "best_effort",
async_insert: 1,
wait_for_async_insert: 1,
},
});

Expand Down
Loading
Loading