import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); /** * Seeds 90 days of mock metric snapshots, conversion events, * and channel metrics for each brand. * * Run: npx tsx prisma/seed-metrics.ts */ async function main() { console.log("Seeding metrics data..."); const brands = await prisma.brand.findMany(); if (brands.length === 0) { console.log("No brands found. Run the main seed first: npx tsx prisma/seed.ts"); return; } const DAYS = 90; const channels = ["organic", "ai_search", "social", "gbp", "direct"] as const; const convTypes = ["phone", "form", "order", "booking"] as const; const pages = ["/", "/pricing", "/features", "/blog/seo-guide", "/contact", "/about", "/products"]; const keywords = ["seo tool", "project management", "ai visibility", "website audit", "local seo", "content strategy"]; for (const brand of brands) { console.log(`Seeding metrics for ${brand.name}...`); // Clear existing metric data for this brand await prisma.metricSnapshot.deleteMany({ where: { brandId: brand.id } }); await prisma.conversionEvent.deleteMany({ where: { brandId: brand.id } }); await prisma.channelMetric.deleteMany({ where: { brandId: brand.id } }); // Base values per brand (vary by brand) const base = getBrandBase(brand.id); for (let d = DAYS; d >= 0; d--) { const date = new Date(); date.setDate(date.getDate() - d); date.setHours(12, 0, 0, 0); // Growth factor — slight upward trend over time const growth = 1 + ((DAYS - d) / DAYS) * 0.15; const noise = () => 0.85 + Math.random() * 0.3; const weekday = date.getDay(); const weekendDip = weekday === 0 || weekday === 6 ? 0.65 : 1; // Daily snapshot const sessions = Math.round(base.sessions * growth * noise() * weekendDip); const users = Math.round(sessions * 0.75); const organicClicks = Math.round(base.clicks * growth * noise() * weekendDip); const dayConversions = Math.round(base.conversions * growth * noise() * weekendDip); const dayRevenue = Math.round(base.revenue * growth * noise() * weekendDip); await prisma.metricSnapshot.create({ data: { brandId: brand.id, date, sessions, users, pageviews: Math.round(sessions * 2.4), bounceRate: Math.round((0.35 + Math.random() * 0.15) * 100) / 100, avgSessionSec: Math.round(120 + Math.random() * 120), organicClicks, impressions: Math.round(organicClicks * (15 + Math.random() * 5)), avgPosition: Math.round((10 + Math.random() * 8) * 10) / 10, ctr: Math.round((0.04 + Math.random() * 0.03) * 1000) / 1000, seoScore: Math.min(100, base.seoScore + Math.round((DAYS - d) / DAYS * 8)), crawlHealth: Math.min(100, base.crawlHealth + Math.round((DAYS - d) / DAYS * 5)), aiMentions: Math.round(base.aiMentions * noise()), revenue: dayRevenue, conversions: dayConversions, }, }); // Channel metrics for (const channel of channels) { const channelShare = getChannelShare(channel); await prisma.channelMetric.create({ data: { brandId: brand.id, date, channel, sessions: Math.round(sessions * channelShare), users: Math.round(users * channelShare), conversions: Math.round(dayConversions * channelShare * (channel === "organic" ? 1.3 : 0.8)), revenue: Math.round(dayRevenue * channelShare * (channel === "organic" ? 1.3 : 0.8)), bounceRate: Math.round((0.3 + Math.random() * 0.2) * 100) / 100, }, }); } // Conversion events (random number per day) const eventCount = Math.max(0, dayConversions + Math.round((Math.random() - 0.5) * 4)); for (let e = 0; e < eventCount; e++) { const type = convTypes[Math.floor(Math.random() * convTypes.length)]; const channel = weightedChannel(); const value = type === "order" ? 50 + Math.round(Math.random() * 200) : type === "booking" ? 100 + Math.round(Math.random() * 300) : type === "form" ? 0 : 0; await prisma.conversionEvent.create({ data: { brandId: brand.id, date, type, channel, value, landingPage: pages[Math.floor(Math.random() * pages.length)], keyword: Math.random() > 0.3 ? keywords[Math.floor(Math.random() * keywords.length)] : null, source: channel === "organic" ? "google" : channel === "social" ? "linkedin" : channel === "gbp" ? "google_maps" : null, }, }); } } console.log(` → ${DAYS + 1} days of snapshots, channel metrics, and conversion events`); } console.log("Metrics seeding complete!"); } function getBrandBase(brandId: string) { const bases: Record = { "acme-corp": { sessions: 280, clicks: 180, conversions: 8, revenue: 820, seoScore: 68, crawlHealth: 88, aiMentions: 12 }, "beacon-health": { sessions: 410, clicks: 260, conversions: 12, revenue: 1200, seoScore: 76, crawlHealth: 86, aiMentions: 18 }, "nova-digital": { sessions: 110, clicks: 60, conversions: 3, revenue: 280, seoScore: 55, crawlHealth: 72, aiMentions: 5 }, }; return bases[brandId] ?? bases["acme-corp"]; } function getChannelShare(channel: string): number { const shares: Record = { organic: 0.55, ai_search: 0.08, social: 0.12, gbp: 0.10, direct: 0.15, }; return shares[channel] ?? 0.1; } function weightedChannel(): string { const r = Math.random(); if (r < 0.55) return "organic"; if (r < 0.63) return "ai_search"; if (r < 0.75) return "social"; if (r < 0.85) return "gbp"; return "direct"; } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());