mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
feat(dev): add sample data generator script
Adds a CLI tool to generate realistic analytics data for local development and testing. Creates two demo websites with varying traffic patterns and realistic user behavior distributions.
This commit is contained in:
parent
a19b92a5cb
commit
b7807ed466
13 changed files with 1645 additions and 2 deletions
191
scripts/seed/generators/events.ts
Normal file
191
scripts/seed/generators/events.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { uuid, addSeconds, randomInt } from '../utils.js';
|
||||
import { getRandomReferrer } from '../distributions/referrers.js';
|
||||
import type { SessionData } from './sessions.js';
|
||||
|
||||
export const EVENT_TYPE = {
|
||||
pageView: 1,
|
||||
customEvent: 2,
|
||||
} as const;
|
||||
|
||||
export interface PageConfig {
|
||||
path: string;
|
||||
title: string;
|
||||
weight: number;
|
||||
avgTimeOnPage: number;
|
||||
}
|
||||
|
||||
export interface CustomEventConfig {
|
||||
name: string;
|
||||
weight: number;
|
||||
pages?: string[];
|
||||
data?: Record<string, string[] | number[]>;
|
||||
}
|
||||
|
||||
export interface JourneyConfig {
|
||||
pages: string[];
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
visitId: string;
|
||||
eventType: number;
|
||||
urlPath: string;
|
||||
urlQuery: string | null;
|
||||
pageTitle: string | null;
|
||||
hostname: string;
|
||||
referrerDomain: string | null;
|
||||
referrerPath: string | null;
|
||||
utmSource: string | null;
|
||||
utmMedium: string | null;
|
||||
utmCampaign: string | null;
|
||||
utmContent: string | null;
|
||||
utmTerm: string | null;
|
||||
gclid: string | null;
|
||||
fbclid: string | null;
|
||||
eventName: string | null;
|
||||
tag: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface EventDataEntry {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
websiteEventId: string;
|
||||
dataKey: string;
|
||||
stringValue: string | null;
|
||||
numberValue: number | null;
|
||||
dateValue: Date | null;
|
||||
dataType: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface SiteConfig {
|
||||
hostname: string;
|
||||
pages: PageConfig[];
|
||||
journeys: JourneyConfig[];
|
||||
customEvents: CustomEventConfig[];
|
||||
}
|
||||
|
||||
function getPageTitle(pages: PageConfig[], path: string): string | null {
|
||||
const page = pages.find(p => p.path === path);
|
||||
return page?.title ?? null;
|
||||
}
|
||||
|
||||
function getPageTimeOnPage(pages: PageConfig[], path: string): number {
|
||||
const page = pages.find(p => p.path === path);
|
||||
return page?.avgTimeOnPage ?? 30;
|
||||
}
|
||||
|
||||
export function generateEventsForSession(
|
||||
session: SessionData,
|
||||
siteConfig: SiteConfig,
|
||||
journey: string[],
|
||||
): { events: EventData[]; eventDataEntries: EventDataEntry[] } {
|
||||
const events: EventData[] = [];
|
||||
const eventDataEntries: EventDataEntry[] = [];
|
||||
const visitId = uuid();
|
||||
|
||||
let currentTime = session.createdAt;
|
||||
const referrer = getRandomReferrer();
|
||||
|
||||
for (let i = 0; i < journey.length; i++) {
|
||||
const pagePath = journey[i];
|
||||
const isFirstPage = i === 0;
|
||||
|
||||
const eventId = uuid();
|
||||
const pageTitle = getPageTitle(siteConfig.pages, pagePath);
|
||||
|
||||
events.push({
|
||||
id: eventId,
|
||||
websiteId: session.websiteId,
|
||||
sessionId: session.id,
|
||||
visitId,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
urlPath: pagePath,
|
||||
urlQuery: null,
|
||||
pageTitle,
|
||||
hostname: siteConfig.hostname,
|
||||
referrerDomain: isFirstPage ? referrer.domain : null,
|
||||
referrerPath: isFirstPage ? referrer.path : null,
|
||||
utmSource: isFirstPage ? referrer.utmSource : null,
|
||||
utmMedium: isFirstPage ? referrer.utmMedium : null,
|
||||
utmCampaign: isFirstPage ? referrer.utmCampaign : null,
|
||||
utmContent: isFirstPage ? referrer.utmContent : null,
|
||||
utmTerm: isFirstPage ? referrer.utmTerm : null,
|
||||
gclid: isFirstPage ? referrer.gclid : null,
|
||||
fbclid: isFirstPage ? referrer.fbclid : null,
|
||||
eventName: null,
|
||||
tag: null,
|
||||
createdAt: currentTime,
|
||||
});
|
||||
|
||||
// Check for custom events on this page
|
||||
for (const customEvent of siteConfig.customEvents) {
|
||||
// Check if this event can occur on this page
|
||||
if (customEvent.pages && !customEvent.pages.includes(pagePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Random chance based on weight
|
||||
if (Math.random() < customEvent.weight) {
|
||||
currentTime = addSeconds(currentTime, randomInt(2, 15));
|
||||
|
||||
const customEventId = uuid();
|
||||
events.push({
|
||||
id: customEventId,
|
||||
websiteId: session.websiteId,
|
||||
sessionId: session.id,
|
||||
visitId,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
urlPath: pagePath,
|
||||
urlQuery: null,
|
||||
pageTitle,
|
||||
hostname: siteConfig.hostname,
|
||||
referrerDomain: null,
|
||||
referrerPath: null,
|
||||
utmSource: null,
|
||||
utmMedium: null,
|
||||
utmCampaign: null,
|
||||
utmContent: null,
|
||||
utmTerm: null,
|
||||
gclid: null,
|
||||
fbclid: null,
|
||||
eventName: customEvent.name,
|
||||
tag: null,
|
||||
createdAt: currentTime,
|
||||
});
|
||||
|
||||
// Generate event data if configured
|
||||
if (customEvent.data) {
|
||||
for (const [key, values] of Object.entries(customEvent.data)) {
|
||||
const value = values[Math.floor(Math.random() * values.length)];
|
||||
const isNumber = typeof value === 'number';
|
||||
|
||||
eventDataEntries.push({
|
||||
id: uuid(),
|
||||
websiteId: session.websiteId,
|
||||
websiteEventId: customEventId,
|
||||
dataKey: key,
|
||||
stringValue: isNumber ? null : String(value),
|
||||
numberValue: isNumber ? value : null,
|
||||
dateValue: null,
|
||||
dataType: isNumber ? 2 : 1, // 1 = string, 2 = number
|
||||
createdAt: currentTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Time spent on page before navigating
|
||||
const timeOnPage = getPageTimeOnPage(siteConfig.pages, pagePath);
|
||||
const variance = Math.floor(timeOnPage * 0.5);
|
||||
const actualTime = timeOnPage + randomInt(-variance, variance);
|
||||
currentTime = addSeconds(currentTime, Math.max(5, actualTime));
|
||||
}
|
||||
|
||||
return { events, eventDataEntries };
|
||||
}
|
||||
65
scripts/seed/generators/revenue.ts
Normal file
65
scripts/seed/generators/revenue.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { uuid, randomFloat } from '../utils.js';
|
||||
import type { EventData } from './events.js';
|
||||
|
||||
export interface RevenueConfig {
|
||||
eventName: string;
|
||||
minAmount: number;
|
||||
maxAmount: number;
|
||||
currency: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface RevenueData {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
eventId: string;
|
||||
eventName: string;
|
||||
currency: string;
|
||||
revenue: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export function generateRevenue(event: EventData, config: RevenueConfig): RevenueData | null {
|
||||
if (event.eventName !== config.eventName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Math.random() > config.weight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const revenue = randomFloat(config.minAmount, config.maxAmount);
|
||||
|
||||
return {
|
||||
id: uuid(),
|
||||
websiteId: event.websiteId,
|
||||
sessionId: event.sessionId,
|
||||
eventId: event.id,
|
||||
eventName: event.eventName!,
|
||||
currency: config.currency,
|
||||
revenue: Math.round(revenue * 100) / 100, // Round to 2 decimal places
|
||||
createdAt: event.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateRevenueForEvents(
|
||||
events: EventData[],
|
||||
configs: RevenueConfig[],
|
||||
): RevenueData[] {
|
||||
const revenueEntries: RevenueData[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
if (!event.eventName) continue;
|
||||
|
||||
for (const config of configs) {
|
||||
const revenue = generateRevenue(event, config);
|
||||
if (revenue) {
|
||||
revenueEntries.push(revenue);
|
||||
break; // Only one revenue per event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return revenueEntries;
|
||||
}
|
||||
52
scripts/seed/generators/sessions.ts
Normal file
52
scripts/seed/generators/sessions.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { uuid } from '../utils.js';
|
||||
import { getRandomDevice } from '../distributions/devices.js';
|
||||
import { getRandomGeo, getRandomLanguage } from '../distributions/geographic.js';
|
||||
import { generateTimestampForDay } from '../distributions/temporal.js';
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
device: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
country: string;
|
||||
region: string;
|
||||
city: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export function createSession(websiteId: string, day: Date): SessionData {
|
||||
const deviceInfo = getRandomDevice();
|
||||
const geo = getRandomGeo();
|
||||
const language = getRandomLanguage();
|
||||
const createdAt = generateTimestampForDay(day);
|
||||
|
||||
return {
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
browser: deviceInfo.browser,
|
||||
os: deviceInfo.os,
|
||||
device: deviceInfo.device,
|
||||
screen: deviceInfo.screen,
|
||||
language,
|
||||
country: geo.country,
|
||||
region: geo.region,
|
||||
city: geo.city,
|
||||
createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function createSessions(websiteId: string, day: Date, count: number): SessionData[] {
|
||||
const sessions: SessionData[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
sessions.push(createSession(websiteId, day));
|
||||
}
|
||||
|
||||
// Sort by createdAt to maintain chronological order
|
||||
sessions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
|
||||
return sessions;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue