Schema:
- eval_sets (promptTag, cases JSON: [{id, timeline, expected: {...}}])
- eval_runs (per-execution scoring: passedSchema/Severity/Rubric, cost, tokens, results)
Prompts:
- 2 new seed templates: upgrade_hesitation (flash), pattern_bundle (pro)
- pickPromptTag() routes 'upgrade_hesitation' tag to upgrade_hesitation prompt
- Editor UI at /insights/settings/prompts/[id]: edit system/user/schema/tier/temp,
publishPromptVersion() creates new version + deactivates old (CRUD with auto bump)
- setPromptActive() to toggle versions
Daily Brief (/insights/brief):
- Last 24h: sessions processed, insights produced (with severity breakdown),
cost, cache hit rate; today's top 5 priorities; week stats (shipped/validated/regressed)
Retention cron (04:15 UTC daily):
- Delete sessions_meta + session_custom_events older than 90d (unless referenced by
active insight)
- Delete compressed_sessions rows + MinIO timeline blobs older than 180d
- raw_metadata 30d (currently no-op; we don't persist raw metadata to MinIO)
Eval framework:
- apps/worker/src/jobs/eval-run.ts: runs cases against current/specified prompt version,
scores schema_pass + severity_match + rubric_substring; stores EvalRun
- apps/worker BullMQ queue handler for 'eval-run' job name
- apps/web installed bullmq; /lib/queue.ts thin Queue accessor
- Web actions: createEvalSet, triggerEvalRun (enqueues job to insight-pipeline queue)
- UI: /insights/settings/eval-sets list, /new create form (paste JSON cases),
/[id] detail with Run button + recent runs + per-case JSON
Bundle mode (analyze job):
- Pull 3x batch, group by fingerprint
- Groups ≥ INSIGHT_BUNDLE_THRESHOLD (default 3) → use 'pattern_bundle' prompt
- Timeline = primary rep + PATTERN BUNDLE summary block (occurrences, unique users, deltas)
- Insight stores ALL group session IDs as relatedSessionIds; all marked analyzed in one tx
- Cost amortized: 1 LLM call per group
Nav/Cmd+K:
- Inbox header links: Brief, Patterns, Eval sets
- Palette: Daily brief, Eval sets entries
Deferred to backlog: embedding-similarity cross-fingerprint clustering, Telegram brief delivery,
Sase.tr-side data-private audit (separate repo).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
366 lines
10 KiB
Plaintext
366 lines
10 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL_PANEL")
|
|
}
|
|
|
|
model User {
|
|
id String @id
|
|
name String
|
|
email String @unique
|
|
emailVerified Boolean @default(false)
|
|
image String?
|
|
twoFactorEnabled Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
sessions Session[]
|
|
accounts Account[]
|
|
twoFactors TwoFactor[]
|
|
}
|
|
|
|
model Session {
|
|
id String @id
|
|
userId String
|
|
token String @unique
|
|
expiresAt DateTime
|
|
ipAddress String?
|
|
userAgent String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model Account {
|
|
id String @id
|
|
userId String
|
|
accountId String
|
|
providerId String
|
|
accessToken String?
|
|
refreshToken String?
|
|
accessTokenExpiresAt DateTime?
|
|
refreshTokenExpiresAt DateTime?
|
|
scope String?
|
|
idToken String?
|
|
password String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model Verification {
|
|
id String @id
|
|
identifier String
|
|
value String
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model TwoFactor {
|
|
id String @id
|
|
userId String
|
|
secret String
|
|
backupCodes String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model Project {
|
|
id String @id @default(cuid())
|
|
key String @unique
|
|
name String
|
|
description String?
|
|
/// planned | wired | active
|
|
status String @default("planned")
|
|
active Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Event {
|
|
id String @id @default(cuid())
|
|
streamId String @unique
|
|
projectKey String
|
|
eventType String
|
|
version Int @default(1)
|
|
payload Json
|
|
occurredAt DateTime
|
|
receivedAt DateTime @default(now())
|
|
|
|
@@index([projectKey, occurredAt])
|
|
@@index([eventType])
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
actorUserId String?
|
|
projectKey String?
|
|
endpoint String
|
|
method String
|
|
requestHash String?
|
|
responseStatus Int?
|
|
durationMs Int?
|
|
sourceIp String?
|
|
userAgent String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([createdAt])
|
|
@@index([actorUserId])
|
|
@@index([projectKey])
|
|
}
|
|
|
|
// ---------- Phase 6a: Behavioral Insight Pipeline ----------
|
|
|
|
model SessionMeta {
|
|
id String @id
|
|
projectKey String
|
|
userIdHash String?
|
|
posthogDistinctId String?
|
|
isAuthenticated Boolean @default(false)
|
|
subscriptionTier String?
|
|
groupKey String?
|
|
startedAt DateTime
|
|
durationMs Int
|
|
pageviewCount Int @default(0)
|
|
clickCount Int @default(0)
|
|
errorCount Int @default(0)
|
|
rageClickCount Int @default(0)
|
|
deadClickCount Int @default(0)
|
|
network5xxCount Int @default(0)
|
|
network4xxCount Int @default(0)
|
|
startUrl String?
|
|
promotionReasons String[]
|
|
tags String[]
|
|
severity String?
|
|
score Int?
|
|
customEventCount Int @default(0)
|
|
rawMetadataUrl String?
|
|
status String @default("pending_signal")
|
|
fingerprint String?
|
|
processedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
compressed CompressedSession?
|
|
|
|
@@index([status, createdAt])
|
|
@@index([fingerprint])
|
|
@@index([projectKey, status])
|
|
@@index([userIdHash, startedAt])
|
|
@@map("sessions_meta")
|
|
}
|
|
|
|
model CompressedSession {
|
|
sessionId String @id
|
|
fingerprint String
|
|
semanticTimelineMinioKey String
|
|
tokenCountInput Int
|
|
tokenCountEstOutput Int
|
|
compressionRatio Float?
|
|
sanitizationMatchCount Int @default(0)
|
|
sanitizationBreakdown Json?
|
|
isBundle Boolean @default(false)
|
|
bundleSessionIds String[]
|
|
readyForAnalysisAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
session SessionMeta @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([fingerprint])
|
|
@@map("compressed_sessions")
|
|
}
|
|
|
|
model IngestionWatermark {
|
|
projectKey String @id
|
|
lastPolledAt DateTime
|
|
posthogCursor String?
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("ingestion_watermarks")
|
|
}
|
|
|
|
// PostHog identify() person properties cache (24h TTL).
|
|
model PosthogPersonCache {
|
|
distinctId String @id
|
|
properties Json
|
|
groups Json?
|
|
refreshedAt DateTime
|
|
ttlAt DateTime
|
|
|
|
@@index([ttlAt])
|
|
@@map("posthog_person_cache")
|
|
}
|
|
|
|
// PostHog group analytics (e.g. company) cache (24h TTL).
|
|
model PosthogGroupCache {
|
|
groupType String
|
|
groupKey String
|
|
properties Json
|
|
refreshedAt DateTime
|
|
ttlAt DateTime
|
|
|
|
@@id([groupType, groupKey])
|
|
@@index([ttlAt])
|
|
@@map("posthog_group_cache")
|
|
}
|
|
|
|
// Per-session custom events fetched from PostHog (joined by $session_id).
|
|
// Stored so compression can interleave them with rrweb timeline.
|
|
model SessionCustomEvent {
|
|
id BigInt @id @default(autoincrement())
|
|
sessionId String
|
|
eventName String
|
|
timestamp DateTime
|
|
properties Json
|
|
|
|
@@index([sessionId, timestamp])
|
|
@@map("session_custom_events")
|
|
}
|
|
|
|
// ---------- Phase 6b: LLM Analysis ----------
|
|
|
|
model PromptTemplate {
|
|
id String @id @default(cuid())
|
|
tag String
|
|
version Int
|
|
name String
|
|
systemPrompt String @db.Text
|
|
userPromptTemplate String @db.Text
|
|
outputSchemaJson Json
|
|
modelTier String // "flash" | "pro"
|
|
maxOutputTokens Int @default(1500)
|
|
temperature Float @default(0.3)
|
|
active Boolean @default(true)
|
|
performanceStats Json @default("{}")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([tag, version])
|
|
@@map("prompt_templates")
|
|
}
|
|
|
|
model Insight {
|
|
id String @id @default(cuid())
|
|
projectKey String
|
|
type String
|
|
severity String
|
|
status String @default("new")
|
|
fingerprint String
|
|
title String
|
|
body Json
|
|
relatedSessionIds String[]
|
|
occurrenceCount Int @default(1)
|
|
uniqueUserCount Int @default(1)
|
|
firstSeenAt DateTime
|
|
lastSeenAt DateTime
|
|
confidence Float?
|
|
priorityScore Int @default(0)
|
|
sourcePromptTag String
|
|
sourcePromptVersion Int
|
|
sourceModel String
|
|
sourceCostUsd Float
|
|
githubIssueUrl String?
|
|
githubIssueId BigInt?
|
|
githubIssueNumber Int?
|
|
githubIssueState String?
|
|
shippedAt DateTime?
|
|
validationStartedAt DateTime?
|
|
validationPeriodDays Int @default(14)
|
|
regressionDetected Boolean @default(false)
|
|
validatedAt DateTime?
|
|
founderNotes String? @db.Text
|
|
founderSeverityOverride String?
|
|
founderPriority Int?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([projectKey, fingerprint])
|
|
@@index([status, projectKey])
|
|
@@index([severity, status])
|
|
@@index([lastSeenAt(sort: Desc)])
|
|
@@index([priorityScore(sort: Desc)])
|
|
@@map("insights")
|
|
}
|
|
|
|
model CostLedger {
|
|
id String @id @default(cuid())
|
|
insightId String?
|
|
sessionId String?
|
|
projectKey String
|
|
promptTag String?
|
|
promptVersion Int?
|
|
provider String // "deepseek" | "anthropic" | "openrouter"
|
|
model String
|
|
tier String // "flash" | "pro"
|
|
tokensInputCacheMiss Int
|
|
tokensInputCacheHit Int
|
|
tokensOutput Int
|
|
costInputCacheMissUsd Float
|
|
costInputCacheHitUsd Float
|
|
costOutputUsd Float
|
|
costTotalUsd Float
|
|
cacheHitRatio Float
|
|
callDurationMs Int?
|
|
wasFallback Boolean @default(false)
|
|
wasRetry Boolean @default(false)
|
|
errorCode String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([createdAt])
|
|
@@index([projectKey, createdAt])
|
|
@@index([insightId])
|
|
@@map("cost_ledger")
|
|
}
|
|
|
|
model BudgetSetting {
|
|
id String @id @default(cuid())
|
|
projectKey String? // null = global
|
|
settingKey String
|
|
settingValue Json
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([projectKey, settingKey])
|
|
@@map("budget_settings")
|
|
}
|
|
|
|
// ---------- Phase 6e: Eval framework ----------
|
|
|
|
model EvalSet {
|
|
id String @id @default(cuid())
|
|
promptTag String
|
|
name String
|
|
description String?
|
|
cases Json // [{ id, timeline, expected: {...}, rubric: {severity, hypothesis, ...} }]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
runs EvalRun[]
|
|
|
|
@@index([promptTag])
|
|
@@map("eval_sets")
|
|
}
|
|
|
|
model EvalRun {
|
|
id String @id @default(cuid())
|
|
evalSetId String
|
|
promptTag String
|
|
promptVersion Int
|
|
model String
|
|
totalCases Int
|
|
passedSchema Int // JSON parse + schema validate succeeded
|
|
passedSeverity Int // severity matched expected
|
|
passedRubric Int // overall rubric pass (subjective fields)
|
|
totalCostUsd Float
|
|
totalDurationMs Int
|
|
avgInputTokens Int
|
|
avgOutputTokens Int
|
|
results Json // per-case: { caseId, ok, errors[], output }
|
|
createdAt DateTime @default(now())
|
|
|
|
evalSet EvalSet @relation(fields: [evalSetId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([evalSetId, createdAt])
|
|
@@index([promptTag, promptVersion])
|
|
@@map("eval_runs")
|
|
}
|