Files
meseo-staging/prisma/seed-metrics.ts
T
Andres Resiri 70daeb214a
CI / Quality gate (push) Has been cancelled
Initial project commit for Coolify staging
2026-08-03 10:45:41 -04:00

152 lines
6.0 KiB
TypeScript

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<string, { sessions: number; clicks: number; conversions: number; revenue: number; seoScore: number; crawlHealth: number; aiMentions: number }> = {
"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<string, number> = {
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());