* wip * wip * wip * wip * wip * add buffer * wip * wip * fixes * fix * wip * group validation * fix group issues * docs: add groups
600 lines
30 KiB
JavaScript
600 lines
30 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 function () {
|
|
seed |= 0;
|
|
seed = (seed + 0x6d2b79f5) | 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) / 4294967296;
|
|
};
|
|
}
|
|
|
|
// 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 / 65025) % 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: 12999 },
|
|
{ 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: 29999 },
|
|
{ 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: 45999 },
|
|
];
|
|
|
|
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);
|
|
});
|