This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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());
|
||||
Reference in New Issue
Block a user