aboutsummaryrefslogtreecommitdiff
path: root/scripts/seed/distributions
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/seed/distributions')
-rw-r--r--scripts/seed/distributions/devices.ts80
-rw-r--r--scripts/seed/distributions/geographic.ts144
-rw-r--r--scripts/seed/distributions/referrers.ts163
-rw-r--r--scripts/seed/distributions/temporal.ts69
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);
+}