Files
stats/scripts/seed-events.mjs

1338 lines
35 KiB
JavaScript

#!/usr/bin/env node
/**
* Seed script for generating realistic analytics events.
*
* Usage:
* node scripts/seed-events.mjs [--timeline=30] [--sessions=500] [--url=http://localhost:3333]
*
* Options:
* --timeline=N Duration in minutes to spread events over (default: 30)
* --sessions=N Number of sessions to generate (default: 500)
* --url=URL API base URL (default: http://localhost:3333)
* --clientId=ID Client ID to use (required or set CLIENT_ID env var)
*/
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const args = Object.fromEntries(
process.argv.slice(2).map((a) => {
const [k, v] = a.replace(/^--/, '').split('=');
return [k, v ?? true];
})
);
const TIMELINE_MINUTES = Number(args.timeline ?? 30);
const SESSION_COUNT = Number(args.sessions ?? 500);
const BASE_URL = args.url ?? 'http://localhost:3333';
const CLIENT_ID = args.clientId ?? process.env.CLIENT_ID ?? '';
const ORIGIN = args.origin ?? process.env.ORIGIN ?? 'https://shop.example.com';
const CONCURRENCY = 20; // max parallel requests
if (!CLIENT_ID) {
console.error('ERROR: provide --clientId=<id> or set CLIENT_ID env var');
process.exit(1);
}
const TRACK_URL = `${BASE_URL}/track`;
// ---------------------------------------------------------------------------
// Deterministic seeded random (mulberry32) — keeps identities stable across runs
// ---------------------------------------------------------------------------
function mulberry32(seed) {
return () => {
seed |= 0;
seed = (seed + 0x6d_2b_79_f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;
};
}
// Non-deterministic random for events (differs on each run)
const eventRng = Math.random.bind(Math);
function pick(arr, rng = eventRng) {
return arr[Math.floor(rng() * arr.length)];
}
function randInt(min, max, rng = eventRng) {
return Math.floor(rng() * (max - min + 1)) + min;
}
function randFloat(min, max, rng = eventRng) {
return rng() * (max - min) + min;
}
// ---------------------------------------------------------------------------
// Fake data pools
// ---------------------------------------------------------------------------
const FIRST_NAMES = [
'Alice',
'Bob',
'Charlie',
'Diana',
'Eve',
'Frank',
'Grace',
'Hank',
'Iris',
'Jack',
'Karen',
'Leo',
'Mia',
'Noah',
'Olivia',
'Pete',
'Quinn',
'Rachel',
'Sam',
'Tina',
'Uma',
'Victor',
'Wendy',
'Xavier',
'Yara',
'Zoe',
'Aaron',
'Bella',
'Carlos',
'Dani',
'Ethan',
'Fiona',
];
const LAST_NAMES = [
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Garcia',
'Miller',
'Davis',
'Wilson',
'Taylor',
'Anderson',
'Thomas',
'Jackson',
'White',
'Harris',
'Martin',
'Thompson',
'Moore',
'Young',
'Allen',
];
const EMAIL_DOMAINS = [
'gmail.com',
'yahoo.com',
'outlook.com',
'icloud.com',
'proton.me',
];
const USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:122.0) Gecko/20100101 Firefox/122.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36',
'Mozilla/5.0 (Linux; Android 13; Samsung Galaxy S23) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 OPR/107.0.0.0',
];
// Ensure each session has a unique UA by appending a suffix
function makeUniqueUA(base, index) {
return `${base} Session/${index}`;
}
// Generate a plausible IP address (avoiding private ranges)
function makeIP(index) {
// Use a spread across several /8 public ranges
const ranges = [
[34, 0, 0, 0],
[52, 0, 0, 0],
[104, 0, 0, 0],
[185, 0, 0, 0],
[213, 0, 0, 0],
];
const base = ranges[index % ranges.length];
const a = base[0];
const b = Math.floor(index / 65_025) % 256;
const c = Math.floor(index / 255) % 256;
const d = (index % 255) + 1;
return `${a}.${b}.${c}.${d}`;
}
// ---------------------------------------------------------------------------
// Products & categories
// ---------------------------------------------------------------------------
const PRODUCTS = [
{
id: 'prod_001',
name: 'Wireless Headphones',
category: 'Electronics',
price: 8999,
},
{ id: 'prod_002', name: 'Running Shoes', category: 'Sports', price: 12_999 },
{ id: 'prod_003', name: 'Coffee Maker', category: 'Kitchen', price: 5499 },
{ id: 'prod_004', name: 'Yoga Mat', category: 'Sports', price: 2999 },
{
id: 'prod_005',
name: 'Smart Watch',
category: 'Electronics',
price: 29_999,
},
{ id: 'prod_006', name: 'Blender', category: 'Kitchen', price: 7999 },
{ id: 'prod_007', name: 'Backpack', category: 'Travel', price: 4999 },
{ id: 'prod_008', name: 'Sunglasses', category: 'Accessories', price: 3499 },
{
id: 'prod_009',
name: 'Novel: The Last Algorithm',
category: 'Books',
price: 1499,
},
{
id: 'prod_010',
name: 'Standing Desk',
category: 'Furniture',
price: 45_999,
},
];
const CATEGORIES = [
'Electronics',
'Sports',
'Kitchen',
'Travel',
'Accessories',
'Books',
'Furniture',
];
// ---------------------------------------------------------------------------
// Groups (3 pre-defined companies)
// ---------------------------------------------------------------------------
const GROUPS = [
{
id: 'org_acme',
type: 'company',
name: 'Acme Corp',
properties: { plan: 'enterprise', industry: 'Technology', employees: 500 },
},
{
id: 'org_globex',
type: 'company',
name: 'Globex Inc',
properties: { plan: 'pro', industry: 'Finance', employees: 120 },
},
{
id: 'org_initech',
type: 'company',
name: 'Initech LLC',
properties: { plan: 'starter', industry: 'Consulting', employees: 45 },
},
];
// ---------------------------------------------------------------------------
// Scenarios — 20 distinct user journeys
// ---------------------------------------------------------------------------
/**
* Each scenario returns a list of event descriptors.
* screen_view events use a `path` property (origin + pathname).
*/
const SCENARIOS = [
// 1. Full e-commerce checkout success
(product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/`,
title: 'Home',
referrer: 'https://google.com',
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{ name: 'screen_view', props: { path: `${ORIGIN}/cart`, title: 'Cart' } },
{
name: 'checkout_started',
props: { cart_total: product.price, item_count: 1 },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/shipping`, title: 'Shipping Info' },
},
{
name: 'shipping_info_submitted',
props: { shipping_method: 'standard', estimated_days: 5 },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/payment`, title: 'Payment' },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/review`, title: 'Order Review' },
},
{
name: 'purchase',
props: {
order_id: `ord_${Date.now()}`,
revenue: product.price,
product_id: product.id,
product_name: product.name,
quantity: 1,
},
revenue: true,
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/success`, title: 'Order Confirmed' },
},
{
name: 'checkout_success',
props: { order_id: `ord_${Date.now()}`, revenue: product.price },
},
],
// 2. Checkout failed (payment declined)
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{
name: 'checkout_started',
props: { cart_total: product.price, item_count: 1 },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/shipping`, title: 'Shipping Info' },
},
{
name: 'shipping_info_submitted',
props: { shipping_method: 'express', estimated_days: 2 },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/payment`, title: 'Payment' },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'checkout_failed',
props: { reason: 'payment_declined', error_code: 'insufficient_funds' },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/payment`, title: 'Payment' },
},
],
// 3. Browse only — no purchase
(product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/`,
title: 'Home',
referrer: 'https://facebook.com',
},
},
{
name: 'screen_view',
props: {
path: `${ORIGIN}/categories/${product.category.toLowerCase()}`,
title: product.category,
},
},
{ name: 'category_viewed', props: { category: product.category } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'screen_view',
props: {
path: `${ORIGIN}/products/${PRODUCTS[1].id}`,
title: PRODUCTS[1].name,
},
},
{
name: 'product_viewed',
props: {
product_id: PRODUCTS[1].id,
product_name: PRODUCTS[1].name,
price: PRODUCTS[1].price,
category: PRODUCTS[1].category,
},
},
],
// 4. Add to cart then abandon
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{ name: 'screen_view', props: { path: `${ORIGIN}/cart`, title: 'Cart' } },
{
name: 'cart_abandoned',
props: { cart_total: product.price, item_count: 1 },
},
],
// 5. Search → product → purchase
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'search',
props: {
query: product.name.split(' ')[0],
result_count: randInt(3, 20),
},
},
{
name: 'screen_view',
props: {
path: `${ORIGIN}/search?q=${encodeURIComponent(product.name)}`,
title: 'Search Results',
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{
name: 'checkout_started',
props: { cart_total: product.price, item_count: 1 },
},
{
name: 'shipping_info_submitted',
props: { shipping_method: 'standard', estimated_days: 5 },
},
{ name: 'payment_info_submitted', props: { payment_method: 'paypal' } },
{
name: 'purchase',
props: {
order_id: `ord_${Date.now()}`,
revenue: product.price,
product_id: product.id,
product_name: product.name,
quantity: 1,
},
revenue: true,
},
{
name: 'checkout_success',
props: { order_id: `ord_${Date.now()}`, revenue: product.price },
},
],
// 6. Sign up flow
(_product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/`,
title: 'Home',
referrer: 'https://twitter.com',
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/signup`, title: 'Sign Up' },
},
{ name: 'signup_started', props: {} },
{ name: 'signup_step_completed', props: { step: 'email', step_number: 1 } },
{
name: 'signup_step_completed',
props: { step: 'password', step_number: 2 },
},
{
name: 'signup_step_completed',
props: { step: 'profile', step_number: 3 },
},
{ name: 'signup_completed', props: { method: 'email' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/dashboard`, title: 'Dashboard' },
},
],
// 7. Login → browse → wishlist
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/login`, title: 'Login' } },
{ name: 'login', props: { method: 'email' } },
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_wishlist',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/wishlist`, title: 'Wishlist' },
},
],
// 8. Promo code → purchase
(product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/`,
title: 'Home',
referrer: 'https://newsletter.example.com',
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{
name: 'checkout_started',
props: { cart_total: product.price, item_count: 1 },
},
{
name: 'promo_code_applied',
props: {
code: 'SAVE20',
discount_percent: 20,
discount_amount: Math.round(product.price * 0.2),
},
},
{
name: 'shipping_info_submitted',
props: { shipping_method: 'standard', estimated_days: 5 },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'purchase',
props: {
order_id: `ord_${Date.now()}`,
revenue: Math.round(product.price * 0.8),
product_id: product.id,
product_name: product.name,
quantity: 1,
promo_code: 'SAVE20',
},
revenue: true,
},
{
name: 'checkout_success',
props: {
order_id: `ord_${Date.now()}`,
revenue: Math.round(product.price * 0.8),
},
},
],
// 9. Multi-item purchase
(_product) => {
const p1 = PRODUCTS[0];
const p2 = PRODUCTS[3];
const total = p1.price + p2.price;
return [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${p1.id}`, title: p1.name },
},
{
name: 'product_viewed',
props: {
product_id: p1.id,
product_name: p1.name,
price: p1.price,
category: p1.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: p1.id,
product_name: p1.name,
price: p1.price,
quantity: 1,
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${p2.id}`, title: p2.name },
},
{
name: 'product_viewed',
props: {
product_id: p2.id,
product_name: p2.name,
price: p2.price,
category: p2.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: p2.id,
product_name: p2.name,
price: p2.price,
quantity: 1,
},
},
{ name: 'checkout_started', props: { cart_total: total, item_count: 2 } },
{
name: 'shipping_info_submitted',
props: { shipping_method: 'express', estimated_days: 2 },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'purchase',
props: { order_id: `ord_${Date.now()}`, revenue: total, item_count: 2 },
revenue: true,
},
{
name: 'checkout_success',
props: { order_id: `ord_${Date.now()}`, revenue: total },
},
];
},
// 10. Help center visit
(_product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/help`, title: 'Help Center' },
},
{ name: 'help_search', props: { query: 'return policy', result_count: 4 } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/help/returns`, title: 'Return Policy' },
},
{
name: 'help_article_read',
props: { article: 'return_policy', time_on_page: randInt(60, 180) },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/help/shipping`, title: 'Shipping Info' },
},
{
name: 'help_article_read',
props: { article: 'shipping_times', time_on_page: randInt(30, 120) },
},
],
// 11. Product review submitted
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{ name: 'review_started', props: { product_id: product.id } },
{
name: 'review_submitted',
props: { product_id: product.id, rating: randInt(3, 5), has_text: true },
},
],
// 12. Newsletter signup only
(_product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/`,
title: 'Home',
referrer: 'https://instagram.com',
},
},
{ name: 'screen_view', props: { path: `${ORIGIN}/blog`, title: 'Blog' } },
{
name: 'screen_view',
props: {
path: `${ORIGIN}/blog/top-10-gadgets-2024`,
title: 'Top 10 Gadgets 2024',
},
},
{
name: 'newsletter_signup',
props: { source: 'blog_article', campaign: 'gadgets_2024' },
},
],
// 13. Account settings update
(_product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/login`, title: 'Login' } },
{ name: 'login', props: { method: 'google' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/account`, title: 'Account' },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/account/settings`, title: 'Settings' },
},
{
name: 'settings_updated',
props: { field: 'notification_preferences', value: 'email_only' },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/account/address`, title: 'Addresses' },
},
{ name: 'address_added', props: { is_default: true } },
],
// 14. Referral program engagement
(product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/`,
title: 'Home',
referrer: 'https://referral.example.com/?ref=abc123',
},
},
{
name: 'referral_link_clicked',
props: { referrer_id: 'usr_ref123', campaign: 'summer_referral' },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{
name: 'checkout_started',
props: { cart_total: product.price, item_count: 1 },
},
{
name: 'shipping_info_submitted',
props: { shipping_method: 'standard', estimated_days: 5 },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'purchase',
props: {
order_id: `ord_${Date.now()}`,
revenue: product.price,
referral_code: 'abc123',
},
revenue: true,
},
{
name: 'checkout_success',
props: { order_id: `ord_${Date.now()}`, revenue: product.price },
},
],
// 15. Mobile quick browse — short session
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: {
path: `${ORIGIN}/categories/${product.category.toLowerCase()}`,
title: product.category,
},
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
],
// 16. Compare products
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{ name: 'compare_added', props: { product_id: product.id } },
{
name: 'screen_view',
props: {
path: `${ORIGIN}/products/${PRODUCTS[4].id}`,
title: PRODUCTS[4].name,
},
},
{
name: 'product_viewed',
props: {
product_id: PRODUCTS[4].id,
product_name: PRODUCTS[4].name,
price: PRODUCTS[4].price,
category: PRODUCTS[4].category,
},
},
{ name: 'compare_added', props: { product_id: PRODUCTS[4].id } },
{
name: 'screen_view',
props: {
path: `${ORIGIN}/compare?ids=${product.id},${PRODUCTS[4].id}`,
title: 'Compare Products',
},
},
{
name: 'compare_viewed',
props: { product_ids: [product.id, PRODUCTS[4].id] },
},
],
// 17. Shipping failure retry → success
(product) => [
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
{
name: 'screen_view',
props: { path: `${ORIGIN}/products/${product.id}`, title: product.name },
},
{
name: 'product_viewed',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
},
},
{
name: 'add_to_cart',
props: {
product_id: product.id,
product_name: product.name,
price: product.price,
quantity: 1,
},
},
{
name: 'checkout_started',
props: { cart_total: product.price, item_count: 1 },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/shipping`, title: 'Shipping Info' },
},
{
name: 'shipping_info_error',
props: { error: 'invalid_address', attempt: 1 },
},
{
name: 'shipping_info_submitted',
props: { shipping_method: 'standard', estimated_days: 5, attempt: 2 },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'purchase',
props: {
order_id: `ord_${Date.now()}`,
revenue: product.price,
product_id: product.id,
},
revenue: true,
},
{
name: 'checkout_success',
props: { order_id: `ord_${Date.now()}`, revenue: product.price },
},
],
// 18. Subscription / SaaS upgrade
(_product) => [
{
name: 'screen_view',
props: { path: `${ORIGIN}/pricing`, title: 'Pricing' },
},
{ name: 'pricing_viewed', props: {} },
{
name: 'plan_selected',
props: { plan: 'pro', billing: 'annual', price: 9900 },
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/checkout/subscription`, title: 'Subscribe' },
},
{
name: 'payment_info_submitted',
props: { payment_method: 'credit_card' },
},
{
name: 'subscription_started',
props: { plan: 'pro', billing: 'annual', revenue: 9900 },
revenue: true,
},
{
name: 'screen_view',
props: { path: `${ORIGIN}/dashboard`, title: 'Dashboard' },
},
],
// 19. Deep content engagement (blog)
(_product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/blog`,
title: 'Blog',
referrer: 'https://google.com',
},
},
{
name: 'screen_view',
props: {
path: `${ORIGIN}/blog/buying-guide-headphones`,
title: 'Headphones Buying Guide',
},
},
{
name: 'content_read',
props: {
article: 'headphones_buying_guide',
reading_time: randInt(120, 480),
scroll_depth: randFloat(0.6, 1.0),
},
},
{
name: 'screen_view',
props: {
path: `${ORIGIN}/blog/best-running-shoes-2024`,
title: 'Best Running Shoes 2024',
},
},
{
name: 'content_read',
props: {
article: 'best_running_shoes_2024',
reading_time: randInt(90, 300),
scroll_depth: randFloat(0.5, 1.0),
},
},
],
// 20. Error / 404 bounce
(_product) => [
{
name: 'screen_view',
props: {
path: `${ORIGIN}/products/old-discontinued-product`,
title: 'Product Not Found',
},
},
{
name: 'page_error',
props: { error_code: 404, path: '/products/old-discontinued-product' },
},
{ name: 'screen_view', props: { path: `${ORIGIN}/`, title: 'Home' } },
],
];
// ---------------------------------------------------------------------------
// Identity generation (deterministic by session index)
// ---------------------------------------------------------------------------
function generateIdentity(sessionIndex, sessionRng) {
const firstName = pick(FIRST_NAMES, sessionRng);
const lastName = pick(LAST_NAMES, sessionRng);
const emailDomain = pick(EMAIL_DOMAINS, sessionRng);
const profileId = `user_${String(sessionIndex + 1).padStart(4, '0')}`;
return {
profileId,
firstName,
lastName,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${sessionIndex}@${emailDomain}`,
};
}
// Which sessions belong to which group (roughly 1/6 each)
function getGroupForSession(sessionIndex) {
if (sessionIndex % 6 === 0) {
return GROUPS[0];
}
if (sessionIndex % 6 === 1) {
return GROUPS[1];
}
if (sessionIndex % 6 === 2) {
return GROUPS[2];
}
return null;
}
// ---------------------------------------------------------------------------
// HTTP helper
// ---------------------------------------------------------------------------
async function sendEvent(payload, ua, ip) {
const headers = {
'Content-Type': 'application/json',
'user-agent': ua,
'openpanel-client-id': CLIENT_ID,
'x-forwarded-for': ip,
origin: ORIGIN,
};
const res = await fetch(TRACK_URL, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
console.warn(
` [WARN] ${res.status} ${payload.type}/${payload.payload?.name ?? ''}: ${text.slice(0, 120)}`
);
}
return res;
}
// ---------------------------------------------------------------------------
// Build session event list
// ---------------------------------------------------------------------------
function buildSession(sessionIndex) {
const sessionRng = mulberry32(sessionIndex * 9973 + 1337); // deterministic per session
const identity = generateIdentity(sessionIndex, sessionRng);
const group = getGroupForSession(sessionIndex);
const ua = makeUniqueUA(pick(USER_AGENTS, sessionRng), sessionIndex);
const ip = makeIP(sessionIndex);
const product = pick(PRODUCTS, sessionRng);
const scenarioFn = SCENARIOS[sessionIndex % SCENARIOS.length];
const events = scenarioFn(product);
return { identity, group, ua, ip, events };
}
// ---------------------------------------------------------------------------
// Schedule events across timeline
// ---------------------------------------------------------------------------
function scheduleSession(session, sessionIndex, totalSessions) {
const timelineMs = TIMELINE_MINUTES * 60 * 1000;
const now = Date.now();
// Sessions are spread across the timeline
const sessionStartOffset = (sessionIndex / totalSessions) * timelineMs;
const sessionStart = now - timelineMs + sessionStartOffset;
// Events within session: spread over 2-10 minutes
const sessionDurationMs = randInt(2, 10) * 60 * 1000;
const eventCount = session.events.length;
return session.events.map((event, i) => {
const eventOffset =
eventCount > 1 ? (i / (eventCount - 1)) * sessionDurationMs : 0;
return {
...event,
timestamp: Math.round(sessionStart + eventOffset),
};
});
}
// ---------------------------------------------------------------------------
// Concurrency limiter
// ---------------------------------------------------------------------------
async function withConcurrency(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(task);
results.push(p);
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
console.log(
`\nSeeding ${SESSION_COUNT} sessions over ${TIMELINE_MINUTES} minutes`
);
console.log(`API: ${TRACK_URL}`);
console.log(`Client ID: ${CLIENT_ID}\n`);
let totalEvents = 0;
let errors = 0;
const sessionTasks = Array.from(
{ length: SESSION_COUNT },
(_, i) => async () => {
const session = buildSession(i);
const scheduledEvents = scheduleSession(session, i, SESSION_COUNT);
const { identity, group, ua, ip } = session;
// 1. Identify
try {
await sendEvent({ type: 'identify', payload: identity }, ua, ip);
} catch (e) {
errors++;
console.error(` [ERROR] identify session ${i}:`, e.message);
}
// 2. Group (if applicable)
if (group) {
try {
await sendEvent(
{
type: 'group',
payload: { ...group, profileId: identity.profileId },
},
ua,
ip
);
} catch (e) {
errors++;
console.error(` [ERROR] group session ${i}:`, e.message);
}
}
// 3. Track events in order
for (const ev of scheduledEvents) {
const trackPayload = {
name: ev.name,
profileId: identity.profileId,
properties: {
...ev.props,
__timestamp: new Date(ev.timestamp).toISOString(),
...(group ? { __group: group.id } : {}),
},
groups: group ? [group.id] : [],
};
if (ev.revenue) {
trackPayload.properties.__revenue = ev.props.revenue;
}
try {
await sendEvent({ type: 'track', payload: trackPayload }, ua, ip);
totalEvents++;
} catch (e) {
errors++;
console.error(` [ERROR] track ${ev.name} session ${i}:`, e.message);
}
}
if ((i + 1) % 50 === 0 || i + 1 === SESSION_COUNT) {
console.log(` Progress: ${i + 1}/${SESSION_COUNT} sessions`);
}
}
);
await withConcurrency(sessionTasks, CONCURRENCY);
console.log('\nDone!');
console.log(` Sessions: ${SESSION_COUNT}`);
console.log(` Events sent: ${totalEvents}`);
console.log(` Errors: ${errors}`);
}
main().catch((err) => {
console.error('Fatal:', err);
process.exit(1);
});