diff options
Diffstat (limited to 'scripts/seed/distributions')
| -rw-r--r-- | scripts/seed/distributions/devices.ts | 80 | ||||
| -rw-r--r-- | scripts/seed/distributions/geographic.ts | 144 | ||||
| -rw-r--r-- | scripts/seed/distributions/referrers.ts | 163 | ||||
| -rw-r--r-- | scripts/seed/distributions/temporal.ts | 69 |
4 files changed, 456 insertions, 0 deletions
diff --git a/scripts/seed/distributions/devices.ts b/scripts/seed/distributions/devices.ts new file mode 100644 index 0000000..9d8b8c0 --- /dev/null +++ b/scripts/seed/distributions/devices.ts @@ -0,0 +1,80 @@ +import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js'; + +export type DeviceType = 'desktop' | 'mobile' | 'tablet'; + +const deviceWeights: WeightedOption<DeviceType>[] = [ + { value: 'desktop', weight: 0.55 }, + { value: 'mobile', weight: 0.4 }, + { value: 'tablet', weight: 0.05 }, +]; + +const browsersByDevice: Record<DeviceType, WeightedOption<string>[]> = { + desktop: [ + { value: 'Chrome', weight: 0.65 }, + { value: 'Safari', weight: 0.12 }, + { value: 'Firefox', weight: 0.1 }, + { value: 'Edge', weight: 0.1 }, + { value: 'Opera', weight: 0.03 }, + ], + mobile: [ + { value: 'Chrome', weight: 0.55 }, + { value: 'Safari', weight: 0.35 }, + { value: 'Samsung', weight: 0.05 }, + { value: 'Firefox', weight: 0.03 }, + { value: 'Opera', weight: 0.02 }, + ], + tablet: [ + { value: 'Safari', weight: 0.6 }, + { value: 'Chrome', weight: 0.35 }, + { value: 'Firefox', weight: 0.05 }, + ], +}; + +const osByDevice: Record<DeviceType, WeightedOption<string>[]> = { + desktop: [ + { value: 'Windows 10', weight: 0.5 }, + { value: 'Mac OS', weight: 0.3 }, + { value: 'Linux', weight: 0.12 }, + { value: 'Chrome OS', weight: 0.05 }, + { value: 'Windows 11', weight: 0.03 }, + ], + mobile: [ + { value: 'iOS', weight: 0.45 }, + { value: 'Android', weight: 0.55 }, + ], + tablet: [ + { value: 'iOS', weight: 0.75 }, + { value: 'Android', weight: 0.25 }, + ], +}; + +const screensByDevice: Record<DeviceType, string[]> = { + desktop: [ + '1920x1080', + '2560x1440', + '1366x768', + '1440x900', + '3840x2160', + '1536x864', + '1680x1050', + '2560x1080', + ], + mobile: ['390x844', '414x896', '375x812', '360x800', '428x926', '393x873', '412x915', '360x780'], + tablet: ['1024x768', '768x1024', '834x1194', '820x1180', '810x1080', '800x1280'], +}; + +export interface DeviceInfo { + device: DeviceType; + browser: string; + os: string; + screen: string; +} + +export function getRandomDevice(): DeviceInfo { + const device = weightedRandom(deviceWeights); + const browser = weightedRandom(browsersByDevice[device]); + const os = weightedRandom(osByDevice[device]); + const screen = pickRandom(screensByDevice[device]); + + return { device, browser, os, screen }; +} diff --git a/scripts/seed/distributions/geographic.ts b/scripts/seed/distributions/geographic.ts new file mode 100644 index 0000000..ba6ebae --- /dev/null +++ b/scripts/seed/distributions/geographic.ts @@ -0,0 +1,144 @@ +import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js'; + +interface GeoLocation { + country: string; + region: string; + city: string; +} + +const countryWeights: WeightedOption<string>[] = [ + { value: 'US', weight: 0.35 }, + { value: 'GB', weight: 0.08 }, + { value: 'DE', weight: 0.06 }, + { value: 'FR', weight: 0.05 }, + { value: 'CA', weight: 0.04 }, + { value: 'AU', weight: 0.03 }, + { value: 'IN', weight: 0.08 }, + { value: 'BR', weight: 0.04 }, + { value: 'JP', weight: 0.03 }, + { value: 'NL', weight: 0.02 }, + { value: 'ES', weight: 0.02 }, + { value: 'IT', weight: 0.02 }, + { value: 'PL', weight: 0.02 }, + { value: 'SE', weight: 0.01 }, + { value: 'MX', weight: 0.02 }, + { value: 'KR', weight: 0.02 }, + { value: 'SG', weight: 0.01 }, + { value: 'ID', weight: 0.02 }, + { value: 'PH', weight: 0.01 }, + { value: 'TH', weight: 0.01 }, + { value: 'VN', weight: 0.01 }, + { value: 'RU', weight: 0.02 }, + { value: 'UA', weight: 0.01 }, + { value: 'ZA', weight: 0.01 }, + { value: 'NG', weight: 0.01 }, +]; + +const regionsByCountry: Record<string, { region: string; city: string }[]> = { + US: [ + { region: 'CA', city: 'San Francisco' }, + { region: 'CA', city: 'Los Angeles' }, + { region: 'NY', city: 'New York' }, + { region: 'TX', city: 'Austin' }, + { region: 'TX', city: 'Houston' }, + { region: 'WA', city: 'Seattle' }, + { region: 'IL', city: 'Chicago' }, + { region: 'MA', city: 'Boston' }, + { region: 'CO', city: 'Denver' }, + { region: 'GA', city: 'Atlanta' }, + { region: 'FL', city: 'Miami' }, + { region: 'PA', city: 'Philadelphia' }, + ], + GB: [ + { region: 'ENG', city: 'London' }, + { region: 'ENG', city: 'Manchester' }, + { region: 'ENG', city: 'Birmingham' }, + { region: 'SCT', city: 'Edinburgh' }, + { region: 'ENG', city: 'Bristol' }, + ], + DE: [ + { region: 'BE', city: 'Berlin' }, + { region: 'BY', city: 'Munich' }, + { region: 'HH', city: 'Hamburg' }, + { region: 'HE', city: 'Frankfurt' }, + { region: 'NW', city: 'Cologne' }, + ], + FR: [ + { region: 'IDF', city: 'Paris' }, + { region: 'ARA', city: 'Lyon' }, + { region: 'PAC', city: 'Marseille' }, + { region: 'OCC', city: 'Toulouse' }, + ], + CA: [ + { region: 'ON', city: 'Toronto' }, + { region: 'BC', city: 'Vancouver' }, + { region: 'QC', city: 'Montreal' }, + { region: 'AB', city: 'Calgary' }, + ], + AU: [ + { region: 'NSW', city: 'Sydney' }, + { region: 'VIC', city: 'Melbourne' }, + { region: 'QLD', city: 'Brisbane' }, + { region: 'WA', city: 'Perth' }, + ], + IN: [ + { region: 'MH', city: 'Mumbai' }, + { region: 'KA', city: 'Bangalore' }, + { region: 'DL', city: 'New Delhi' }, + { region: 'TN', city: 'Chennai' }, + { region: 'TG', city: 'Hyderabad' }, + ], + BR: [ + { region: 'SP', city: 'Sao Paulo' }, + { region: 'RJ', city: 'Rio de Janeiro' }, + { region: 'MG', city: 'Belo Horizonte' }, + ], + JP: [ + { region: '13', city: 'Tokyo' }, + { region: '27', city: 'Osaka' }, + { region: '23', city: 'Nagoya' }, + ], + NL: [ + { region: 'NH', city: 'Amsterdam' }, + { region: 'ZH', city: 'Rotterdam' }, + { region: 'ZH', city: 'The Hague' }, + ], +}; + +const defaultRegions = [{ region: '', city: '' }]; + +export function getRandomGeo(): GeoLocation { + const country = weightedRandom(countryWeights); + const regions = regionsByCountry[country] || defaultRegions; + const { region, city } = pickRandom(regions); + + return { country, region, city }; +} + +const languages: WeightedOption<string>[] = [ + { value: 'en-US', weight: 0.4 }, + { value: 'en-GB', weight: 0.08 }, + { value: 'de-DE', weight: 0.06 }, + { value: 'fr-FR', weight: 0.05 }, + { value: 'es-ES', weight: 0.05 }, + { value: 'pt-BR', weight: 0.04 }, + { value: 'ja-JP', weight: 0.03 }, + { value: 'zh-CN', weight: 0.05 }, + { value: 'ko-KR', weight: 0.02 }, + { value: 'ru-RU', weight: 0.02 }, + { value: 'it-IT', weight: 0.02 }, + { value: 'nl-NL', weight: 0.02 }, + { value: 'pl-PL', weight: 0.02 }, + { value: 'hi-IN', weight: 0.04 }, + { value: 'ar-SA', weight: 0.02 }, + { value: 'tr-TR', weight: 0.02 }, + { value: 'vi-VN', weight: 0.01 }, + { value: 'th-TH', weight: 0.01 }, + { value: 'id-ID', weight: 0.02 }, + { value: 'sv-SE', weight: 0.01 }, + { value: 'da-DK', weight: 0.01 }, +]; + +export function getRandomLanguage(): string { + return weightedRandom(languages); +} diff --git a/scripts/seed/distributions/referrers.ts b/scripts/seed/distributions/referrers.ts new file mode 100644 index 0000000..5b3f2c4 --- /dev/null +++ b/scripts/seed/distributions/referrers.ts @@ -0,0 +1,163 @@ +import { weightedRandom, pickRandom, randomInt, type WeightedOption } from '../utils.js'; + +export type ReferrerType = 'direct' | 'organic' | 'social' | 'paid' | 'referral'; + +export interface ReferrerInfo { + type: ReferrerType; + domain: string | null; + path: string | null; + utmSource: string | null; + utmMedium: string | null; + utmCampaign: string | null; + utmContent: string | null; + utmTerm: string | null; + gclid: string | null; + fbclid: string | null; +} + +const referrerTypeWeights: WeightedOption<ReferrerType>[] = [ + { value: 'direct', weight: 0.4 }, + { value: 'organic', weight: 0.25 }, + { value: 'social', weight: 0.15 }, + { value: 'paid', weight: 0.1 }, + { value: 'referral', weight: 0.1 }, +]; + +const searchEngines = [ + { domain: 'google.com', path: '/search' }, + { domain: 'bing.com', path: '/search' }, + { domain: 'duckduckgo.com', path: '/' }, + { domain: 'yahoo.com', path: '/search' }, + { domain: 'baidu.com', path: '/s' }, +]; + +const socialPlatforms = [ + { domain: 'twitter.com', path: null }, + { domain: 'x.com', path: null }, + { domain: 'linkedin.com', path: '/feed' }, + { domain: 'facebook.com', path: null }, + { domain: 'reddit.com', path: '/r/programming' }, + { domain: 'news.ycombinator.com', path: '/item' }, + { domain: 'threads.net', path: null }, + { domain: 'bsky.app', path: null }, +]; + +const referralSites = [ + { domain: 'medium.com', path: '/@author/article' }, + { domain: 'dev.to', path: '/post' }, + { domain: 'hashnode.com', path: '/blog' }, + { domain: 'techcrunch.com', path: '/article' }, + { domain: 'producthunt.com', path: '/posts' }, + { domain: 'indiehackers.com', path: '/post' }, +]; + +interface PaidCampaign { + source: string; + medium: string; + campaign: string; + useGclid?: boolean; + useFbclid?: boolean; +} + +const paidCampaigns: PaidCampaign[] = [ + { source: 'google', medium: 'cpc', campaign: 'brand_search', useGclid: true }, + { source: 'google', medium: 'cpc', campaign: 'product_awareness', useGclid: true }, + { source: 'facebook', medium: 'paid_social', campaign: 'retargeting', useFbclid: true }, + { source: 'facebook', medium: 'paid_social', campaign: 'lookalike', useFbclid: true }, + { source: 'linkedin', medium: 'cpc', campaign: 'b2b_targeting' }, + { source: 'twitter', medium: 'paid_social', campaign: 'launch_promo' }, +]; + +const organicCampaigns = [ + { source: 'newsletter', medium: 'email', campaign: 'weekly_digest' }, + { source: 'newsletter', medium: 'email', campaign: 'product_update' }, + { source: 'partner', medium: 'referral', campaign: 'integration_launch' }, +]; + +function generateClickId(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < 32; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +export function getRandomReferrer(): ReferrerInfo { + const type = weightedRandom(referrerTypeWeights); + + const result: ReferrerInfo = { + type, + domain: null, + path: null, + utmSource: null, + utmMedium: null, + utmCampaign: null, + utmContent: null, + utmTerm: null, + gclid: null, + fbclid: null, + }; + + switch (type) { + case 'direct': + // No referrer data + break; + + case 'organic': { + const engine = pickRandom(searchEngines); + result.domain = engine.domain; + result.path = engine.path; + break; + } + + case 'social': { + const platform = pickRandom(socialPlatforms); + result.domain = platform.domain; + result.path = platform.path; + + // Some social traffic has UTM params + if (Math.random() < 0.3) { + result.utmSource = platform.domain.replace('.com', '').replace('.net', ''); + result.utmMedium = 'social'; + } + break; + } + + case 'paid': { + const campaign = pickRandom(paidCampaigns); + result.utmSource = campaign.source; + result.utmMedium = campaign.medium; + result.utmCampaign = campaign.campaign; + result.utmContent = `ad_${randomInt(1, 5)}`; + + if (campaign.useGclid) { + result.gclid = generateClickId(); + result.domain = 'google.com'; + result.path = '/search'; + } else if (campaign.useFbclid) { + result.fbclid = generateClickId(); + result.domain = 'facebook.com'; + result.path = null; + } + break; + } + + case 'referral': { + // Mix of pure referrals and organic campaigns + if (Math.random() < 0.6) { + const site = pickRandom(referralSites); + result.domain = site.domain; + result.path = site.path; + } else { + const campaign = pickRandom(organicCampaigns); + result.utmSource = campaign.source; + result.utmMedium = campaign.medium; + result.utmCampaign = campaign.campaign; + } + break; + } + } + + return result; +} diff --git a/scripts/seed/distributions/temporal.ts b/scripts/seed/distributions/temporal.ts new file mode 100644 index 0000000..da0409a --- /dev/null +++ b/scripts/seed/distributions/temporal.ts @@ -0,0 +1,69 @@ +import { weightedRandom, randomInt, type WeightedOption } from '../utils.js'; + +const hourlyWeights: WeightedOption<number>[] = [ + { value: 0, weight: 0.02 }, + { value: 1, weight: 0.01 }, + { value: 2, weight: 0.01 }, + { value: 3, weight: 0.01 }, + { value: 4, weight: 0.01 }, + { value: 5, weight: 0.02 }, + { value: 6, weight: 0.03 }, + { value: 7, weight: 0.05 }, + { value: 8, weight: 0.07 }, + { value: 9, weight: 0.08 }, + { value: 10, weight: 0.09 }, + { value: 11, weight: 0.08 }, + { value: 12, weight: 0.07 }, + { value: 13, weight: 0.08 }, + { value: 14, weight: 0.09 }, + { value: 15, weight: 0.08 }, + { value: 16, weight: 0.07 }, + { value: 17, weight: 0.06 }, + { value: 18, weight: 0.05 }, + { value: 19, weight: 0.04 }, + { value: 20, weight: 0.03 }, + { value: 21, weight: 0.03 }, + { value: 22, weight: 0.02 }, + { value: 23, weight: 0.02 }, +]; + +const dayOfWeekWeights: WeightedOption<number>[] = [ + { value: 0, weight: 0.08 }, // Sunday + { value: 1, weight: 0.16 }, // Monday + { value: 2, weight: 0.17 }, // Tuesday + { value: 3, weight: 0.17 }, // Wednesday + { value: 4, weight: 0.16 }, // Thursday + { value: 5, weight: 0.15 }, // Friday + { value: 6, weight: 0.11 }, // Saturday +]; + +export function getWeightedHour(): number { + return weightedRandom(hourlyWeights); +} + +export function getDayOfWeekMultiplier(dayOfWeek: number): number { + const weight = dayOfWeekWeights.find(d => d.value === dayOfWeek)?.weight ?? 0.14; + return weight / 0.14; // Normalize around 1.0 +} + +export function generateTimestampForDay(day: Date): Date { + const hour = getWeightedHour(); + const minute = randomInt(0, 59); + const second = randomInt(0, 59); + const millisecond = randomInt(0, 999); + + const timestamp = new Date(day); + timestamp.setHours(hour, minute, second, millisecond); + + return timestamp; +} + +export function getSessionCountForDay(baseCount: number, day: Date): number { + const dayOfWeek = day.getDay(); + const multiplier = getDayOfWeekMultiplier(dayOfWeek); + + // Add some random variance (±20%) + const variance = 0.8 + Math.random() * 0.4; + + return Math.round(baseCount * multiplier * variance); +} |