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