Files
sp/apps/web/prisma/schema.prisma
Semih 95c7e8e321 feat(phase6b): LLM analysis layer — DeepSeek + insights + cost ledger + budget guard
Schema:
- insights (fingerprint dedup, occurrence_count, related_session_ids[], priority_score)
- cost_ledger (per-call, cache_hit/miss/output token split + USD)
- prompt_templates (versioned, per-tag, with JSON schema + tier + temperature)
- budget_settings (global key/value: monthly_hard_cap_usd, daily_soft/hard, per_call_max, analysis_paused)

Worker:
- lib/deepseek.ts: Anthropic-compat endpoint client (deepseek-v4-flash | deepseek-v4-pro),
  usage→USD with V4 promo pricing, cache_read_input_tokens awareness, extractJson() helper
- lib/budget.ts: checkBudget() returns active|soft_throttled|hard_paused|monthly_paused,
  forces flash tier on soft cap, halts on hard/monthly cap or analysis_paused
- lib/prompts.ts: 5 seed templates (bug_triage P, ux_friction F, payment_issue P,
  onboarding_stuck F, provider_quality F) with embedded JSON schemas + Sase.tr context;
  pickPromptTag() maps session tags → prompt
- lib/json-validate.ts: lightweight schema validator (no ajv dep)
- lib/seed-runtime.ts: idempotent upsert of prompts + default budget settings on worker boot
- lib/minio.ts: +getText() for compressed timeline fetch
- jobs/analyze.ts: budget guard → 6h fingerprint cache (attach session to existing insight) →
  template lookup → severity-based tier override → DeepSeek call → JSON parse + validate →
  insert insight (or aggregate occurrence) → write cost_ledger
- scheduler: analyze@*/4min on insight-pipeline queue

UI:
- /insights (was pipeline view) → now Insight Inbox: priority-sorted list w/ KPI strip
  (new/in_backlog/shipped/today $/month $), severity badges, link to detail
- /insights/i/[id]: insight detail with structured body render, related sessions,
  per-session cost breakdown, raw JSON collapsible
- /insights/costs: KPI cards (today, month, avg, cache hit), daily 30d bar table,
  by-model + by-prompt breakdowns, top 10 expensive, recent errors
- /insights/pipeline: moved old session-pipeline view here
- /insights/sessions/[id]: unchanged session timeline viewer

Defaults:
- monthly cap $30, daily soft $1.50 / hard $3, per-call $0.20, analysis_paused=false
- Severity→Tier: P0/P1=pro, P2/P3/INFO=flash; budget soft-cap forces flash

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:01:55 +00:00

325 lines
8.9 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?
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")
}