Ctrl K
ComunidadGuías…

simulate-events.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const ENV = require("dotenv").config({ path: "./.env" });

/**
 * CLI Argument Parsing
 */
const args = process.argv.slice(2);

const DEFAULT_ARGS = {
  NUMBER_OF_EVENTS: 4000,
  NUMBER_OF_USERS: 2000,
  SIMULATED_DAYS: 7,
  BATCH_SIZE: 500,
  CUSTOM_EVENT_FREQUENCY: 0.2,
  OUTBOUND_LINK_FREQUENCY: 0.05,
  CAMPAIGN_FREQUENCY: 0.3,
  NUM_CAMPAIGNS: 6,
  REFERRER_FREQUENCY: 0.6,
}


if (!args[0] || args[0].startsWith("--")) {
  const WIDTH = 8;
  const formatNumber = (num) => {
    return typeof num === "number" && num % 1 !== 0
      ? num.toFixed(1).padStart(WIDTH, " ")
      : num.toString().padStart(WIDTH, " ");
  };
  console.error("[Error] ❌ Missing SITE_ID.\nUsage:");
  console.error(`
    ------------------------------------------------------------------------------------------------
    | Flag             | Description                                                | Default  |
    | ---------------- | ---------------------------------------------------------- | -------- |
    | '--events'       | Total number of events to simulate                         | ${formatNumber(DEFAULT_ARGS.NUMBER_OF_EVENTS)} |
    | '--users'        | Number of unique simulated users                           | ${formatNumber(DEFAULT_ARGS.NUMBER_OF_USERS)} |
    | '--days'         | Number of days to spread events across (0 = today only)    | ${formatNumber(DEFAULT_ARGS.SIMULATED_DAYS)} |
    | '--batch-size'   | Number of events sent per batch (concurrent POSTs)         | ${formatNumber(DEFAULT_ARGS.BATCH_SIZE)} |
    | '--event-freq'   | Fraction (0–1) of events that are custom (non-pageview)    | ${formatNumber(DEFAULT_ARGS.CUSTOM_EVENT_FREQUENCY)} |
    | '--outbound-freq'| Fraction (0–1) of events that are outbound link clicks     | ${formatNumber(DEFAULT_ARGS.OUTBOUND_LINK_FREQUENCY)} |
    | '--campaign-freq'| Fraction (0–1) of events that have campaign UTM tags       | ${formatNumber(DEFAULT_ARGS.CAMPAIGN_FREQUENCY)} |
    | '--campaigns'    | Number of unique campaigns to generate                     | ${formatNumber(DEFAULT_ARGS.NUM_CAMPAIGNS)} |
    | '--referrer-freq'| Fraction (0–1) of users arriving via an external referrer  | ${formatNumber(DEFAULT_ARGS.REFERRER_FREQUENCY)} |
    ------------------------------------------------------------------------------------------------

    Example:
    ./simulate-events "your-site-id" \\
      --events=${DEFAULT_ARGS.NUMBER_OF_EVENTS} \\
      --users=${DEFAULT_ARGS.NUMBER_OF_USERS} \\
      --days=${DEFAULT_ARGS.SIMULATED_DAYS} \\
      --batch-size=${DEFAULT_ARGS.BATCH_SIZE} \\
      --event-freq=${DEFAULT_ARGS.CUSTOM_EVENT_FREQUENCY} \\
      --outbound-freq=${DEFAULT_ARGS.OUTBOUND_LINK_FREQUENCY} \\
      --campaign-freq=${DEFAULT_ARGS.CAMPAIGN_FREQUENCY} \\
      --campaigns=${DEFAULT_ARGS.NUM_CAMPAIGNS} \\
      --referrer-freq=${DEFAULT_ARGS.REFERRER_FREQUENCY}
  `);
  process.exit(1);
}

const getFlag = (name, fallback) => {
  const arg = args.find((arg) => arg.startsWith(`--${name}=`));
  return arg ? parseFloat(arg.split("=")[1]) : fallback;
};

/**
 * Parameters
 */
const SITE_ID = args[0];
const TARGET_URL = `http://127.0.0.1:${process.env.SERVER_PORT || 3001}/event`;
const NUMBER_OF_EVENTS = getFlag("events", DEFAULT_ARGS.NUMBER_OF_EVENTS);
const NUMBER_OF_USERS = getFlag("users", DEFAULT_ARGS.NUMBER_OF_USERS);
const SIMULATED_DAYS = getFlag("days", DEFAULT_ARGS.SIMULATED_DAYS);
const BATCH_SIZE = getFlag("batch-size", DEFAULT_ARGS.BATCH_SIZE);
const CUSTOM_EVENT_FREQUENCY = getFlag("event-freq", DEFAULT_ARGS.CUSTOM_EVENT_FREQUENCY);
const OUTBOUND_LINK_FREQUENCY = getFlag("outbound-freq", DEFAULT_ARGS.OUTBOUND_LINK_FREQUENCY);
const CAMPAIGN_FREQUENCY = getFlag("campaign-freq", DEFAULT_ARGS.CAMPAIGN_FREQUENCY);
const NUM_CAMPAIGNS = getFlag("campaigns", DEFAULT_ARGS.NUM_CAMPAIGNS);
const REFERRER_FREQUENCY = getFlag("referrer-freq", DEFAULT_ARGS.REFERRER_FREQUENCY);

const CUSTOM_EVENTS = [
  {
    event_name: "cart-checkout",
    properties: JSON.stringify({ test_value: 6 }),
  },
  {
    event_name: "product-clicked",
    properties: JSON.stringify({ product_id: "abc123" }),
  },
  {
    event_name: "malformed-props-repro",
    properties: JSON.stringify({ "ran\tdom": "b" }),
  },
];

const OUTBOUND_LINK_URLS = [
  "https://github.com",
  "https://twitter.com",
  "https://linkedin.com",
  "https://youtube.com",
  "https://partner.com",
];

const REFERRER_URLS = [
  "https://duckduckgo.com/",
  "https://www.bing.com/search",
  "https://www.google.com/",
  "https://www.google.com/search",
  "https://news.google.com/",
  "https://www.reddit.com/r/selfhosted/",
  "https://old.reddit.com/r/webdev/",
  "https://news.ycombinator.com/",
  "https://news.ycombinator.com/item?id=39538522",
  "https://github.com/betterlytics/betterlytics",
  "https://t.co/9fKzXqLm",
  "https://www.linkedin.com/feed/",
  "https://www.facebook.com/",
];
/**
 * Weighted device profiles, assigned one per user. The backend derives browser,
 * OS, and their major versions from the user agent, and device type from the
 * screen width, so each profile pairs a real UA string with resolutions from
 * the matching device class. All of these feed the visitor fingerprint, which
 * is why a user keeps the same profile across all of their events.
 */
const DEVICE_PROFILES = [
  { weight: 8, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", screen_resolutions: ["1920x1080", "2560x1440"] },
  { weight: 6, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", screen_resolutions: ["1920x1080", "1366x768"] },
  { weight: 4, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", screen_resolutions: ["1536x864", "1366x768"] },
  { weight: 2, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", screen_resolutions: ["1280x800"] },
  { weight: 5, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", screen_resolutions: ["1440x900", "2560x1440"] },
  { weight: 3, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", screen_resolutions: ["1440x900"] },
  { weight: 4, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0", screen_resolutions: ["1920x1080"] },
  { weight: 2, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", screen_resolutions: ["1366x768"] },
  { weight: 3, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", screen_resolutions: ["1920x1080"] },
  { weight: 2, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0", screen_resolutions: ["1440x900"] },
  { weight: 2, user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0", screen_resolutions: ["1920x1080"] },
  { weight: 1, user_agent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0", screen_resolutions: ["1366x768"] },
  { weight: 4, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", screen_resolutions: ["1440x900", "2560x1440"] },
  { weight: 2, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15", screen_resolutions: ["1280x800"] },
  { weight: 1, user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 OPR/105.0.0.0", screen_resolutions: ["1920x1080"] },
  { weight: 6, user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", screen_resolutions: ["390x844", "430x932"] },
  { weight: 3, user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", screen_resolutions: ["390x844"] },
  { weight: 2, user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", screen_resolutions: ["375x812"] },
  { weight: 5, user_agent: "Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36", screen_resolutions: ["412x915"] },
  { weight: 3, user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", screen_resolutions: ["412x915"] },
  { weight: 2, user_agent: "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36", screen_resolutions: ["360x800"] },
  { weight: 2, user_agent: "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36", screen_resolutions: ["360x780"] },
  { weight: 2, user_agent: "Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1", screen_resolutions: ["820x1180"] },
  { weight: 1, user_agent: "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", screen_resolutions: ["800x1280"] },
];

/**
 * Campaign UTM data for simulating marketing campaigns
 * Note: utm_campaign array is generated dynamically after uuidv4() is defined
 */
const CAMPAIGN_DATA = {
  utm_source: ["google", "facebook", "twitter", "linkedin", "newsletter", "bing", "instagram"],
  utm_medium: ["cpc", "social", "email", "organic", "referral", "display", "affiliate"],
  utm_campaign: [], // populated dynamically below
  utm_term: ["analytics", "dashboard", "tracking", "marketing", "conversion", ""],
  utm_content: ["banner_a", "banner_b", "sidebar", "footer", "hero", ""],
};

const APP_VERSIONS = [
  "v1.0.0", "v1.0.1", "v1.1.0", "v1.2.0", "v1.2.1", "v1.3.0",
  "v2.0.0", "v2.0.1", "v2.1.0", "v2.1.1", "v2.2.0", "v2.3.0", "v2.3.1",
  "v3.0.0-alpha", "v3.0.0-beta", "v3.0.0-rc1", "v3.0.0", "v3.0.1", "v3.1.0",
];

const REGIONS = [
  "us-east", "us-west", "eu-west", "eu-central", "ap-south", "ap-northeast",
  "sa-east", "af-south", "me-south", "ca-central", "ap-southeast",
];

const GLOBAL_PROPERTIES_POOL = [
  { plan: "free", region: "us-east", theme: "light", locale: "en", role: "viewer", app_version: APP_VERSIONS[Math.floor(Math.random() * APP_VERSIONS.length)] },
  { plan: "premium", region: "eu-west", theme: "dark", locale: "de", role: "editor", org_id: "org-acme", app_version: APP_VERSIONS[Math.floor(Math.random() * APP_VERSIONS.length)] },
  { plan: "enterprise", region: "ap-south", theme: "system", locale: "ja", role: "admin", org_id: "org-globex", app_version: APP_VERSIONS[Math.floor(Math.random() * APP_VERSIONS.length)] },
  { plan: "premium", environment: "production", browser_lang: "en-US", signup_source: "google", referral_code: "REF123", team_size: "10", region: REGIONS[Math.floor(Math.random() * REGIONS.length)] },
  { plan: "free", environment: "staging", app_version: "v3.0.0-beta", feature_flags: "beta_ui", onboarding_step: "3", user_tier: "trial", region: REGIONS[Math.floor(Math.random() * REGIONS.length)] },
  { plan: "enterprise", department: "engineering", cost_center: "CC-100", project: "atlas", sprint: "24", priority: "high", region: REGIONS[Math.floor(Math.random() * REGIONS.length)] },
  { plan: "premium", country: "US", currency: "USD", timezone: "America/New_York", device_class: "desktop", connection_type: "wifi", price: 19.99, app_version: APP_VERSIONS[Math.floor(Math.random() * APP_VERSIONS.length)] },
  { plan: "free", app_version: APP_VERSIONS[Math.floor(Math.random() * APP_VERSIONS.length)], score: 4.5, active: true, seats: 1 },
  { plan: "enterprise", app_version: APP_VERSIONS[Math.floor(Math.random() * APP_VERSIONS.length)], score: 87.125, active: false, seats: 250 },
  {},
  {},
];

function getRandomElement(arr) {
  return arr[Math.floor(Math.random() * arr.length)];
}

/**
 * Select element proportionally to its `weight` field
 */
function getWeightedElement(arr) {
  const totalWeight = arr.reduce((sum, item) => sum + item.weight, 0);
  let roll = Math.random() * totalWeight;
  for (const item of arr) {
    roll -= item.weight;
    if (roll <= 0) return item;
  }
  return arr[arr.length - 1];
}

/**
 * Select element using normal distribution (middle elements more likely)
 */
function getNormalDistributedElement(arr) {
  const index = Math.floor(gaussianRand() * arr.length);
  return arr[Math.min(index, arr.length - 1)];
}

function generateCampaignUrl(baseUrl) {
  const params = new URLSearchParams();
  params.set("utm_source", getRandomElement(CAMPAIGN_DATA.utm_source));
  params.set("utm_medium", getRandomElement(CAMPAIGN_DATA.utm_medium));
  params.set("utm_campaign", getNormalDistributedElement(CAMPAIGN_DATA.utm_campaign));

  const term = getRandomElement(CAMPAIGN_DATA.utm_term);
  if (term) params.set("utm_term", term);

  const content = getRandomElement(CAMPAIGN_DATA.utm_content);
  if (content) params.set("utm_content", content);

  return `${baseUrl}?${params.toString()}`;
}

const PUBLIC_BASE_URL = ENV.PUBLIC_BASE_URL || "http://localhost:3000";

const BASE_PAYLOAD = {
  referrer: null,
  screen_resolution: "1920x1080",
  site_id: SITE_ID,
  event_name: "pageview",
  is_custom_event: false,
  properties: JSON.stringify({}),
  timestamp: 0,
  url: `${PUBLIC_BASE_URL}/dashboard`,
  user_agent:
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
  visitor_id: "placeholder",
};

function uuidv4() {
  return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
    (
      +c ^
      (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
    ).toString(16)
  );
}

function gaussianRand() {
  var rand = 0;

  for (var i = 0; i < 6; i += 1) {
    rand += Math.random();
  }

  return rand / 6;
}

/**
 * Generates a random public IPv4 address by repeatedly sampling until
 * an address outside of private, reserved, or special-use ranges is found.
 *
 * Excluded CIDR blocks include private, loopback, link-local, multicast,
 * and documentation/test addresses.
 *
 * The excluded CIDRs are from IANA registries
 * - https://www.iana.org/assignments/iana-ipv4-special-registry
 */
function getRandomPublicIp() {
  while (true) {
    const ip = Array.from({ length: 4 }, () =>
      Math.floor(Math.random() * 256)
    ).join(".");
    if (isPublicIp(ip)) return ip;
  }
}

function isPublicIp(ip) {
  const excludedCidrs = [
    ["10.0.0.0", 8],
    ["172.16.0.0", 12],
    ["192.168.0.0", 16],
    ["127.0.0.0", 8],
    ["0.0.0.0", 8],
    ["169.254.0.0", 16],
    ["224.0.0.0", 4],
    ["192.0.2.0", 24],
    ["198.51.100.0", 24],
    ["203.0.113.0", 24],
    ["100.64.0.0", 10],
    ["198.18.0.0", 15],
  ];

  return !excludedCidrs.some(([cidrBase, cidrBits]) =>
    ipInCidr(ip, cidrBase, cidrBits)
  );
}

function ipInCidr(ip, cidrBase, cidrBits) {
  const ipInt = ipToInt(ip);
  const baseInt = ipToInt(cidrBase);
  const mask = ~(2 ** (32 - cidrBits) - 1);
  return (ipInt & mask) === (baseInt & mask);
}

function ipToInt(ip) {
  return ip.split(".").reduce((int, octet) => (int << 8) + Number(octet), 0);
}

/**
 * Pre-process
 */
console.log("[+] Setting up...");

const users = new Array(NUMBER_OF_USERS).fill(0).map(() => {
  const device = getWeightedElement(DEVICE_PROFILES);
  return {
    visitor_id: uuidv4(),
    ip: getRandomPublicIp(),
    user_agent: device.user_agent,
    screen_resolution: getRandomElement(device.screen_resolutions),
    globalProperties: getRandomElement(GLOBAL_PROPERTIES_POOL),
    referrer:
      Math.random() < REFERRER_FREQUENCY ? getRandomElement(REFERRER_URLS) : null,
  };
});

// Generate unique campaign IDs (short UUIDs)
CAMPAIGN_DATA.utm_campaign = new Array(NUM_CAMPAIGNS)
  .fill(0)
  .map(() => uuidv4().slice(0, 8));

function getExtraPayload(payload) {
  const hasCampaign = Math.random() < CAMPAIGN_FREQUENCY;
  const baseUrl = `${PUBLIC_BASE_URL}/dashboard`;

  // Mutually exclusive event-class roll: outbound_link, custom event, or pageview
  const r = Math.random();
  const isOutboundLink = r < OUTBOUND_LINK_FREQUENCY;
  const isCustomEvent = !isOutboundLink && r < OUTBOUND_LINK_FREQUENCY + CUSTOM_EVENT_FREQUENCY;

  return {
    ...payload,
    url: hasCampaign ? generateCampaignUrl(baseUrl) : baseUrl,
    ...(isOutboundLink
      ? {
          event_name: "outbound_link",
          outbound_link_url: getRandomElement(OUTBOUND_LINK_URLS),
        }
      : isCustomEvent
      ? {
          ...CUSTOM_EVENTS[Math.floor(Math.random() * CUSTOM_EVENTS.length)],
          is_custom_event: true,
        }
      : {}),
  };
}

const usersByVisitorId = new Map(users.map((u) => [u.visitor_id, u]));

const events = new Array(NUMBER_OF_EVENTS)
  .fill(0)
  .map(() => {
    const daysAgo =
      SIMULATED_DAYS === 0
        ? 0
        : Math.floor(Math.random() * SIMULATED_DAYS) + gaussianRand(); // same logic

    const secondsAgo = Math.floor(daysAgo * 86400);
    const timestamp = Math.floor(Date.now() / 1000 - secondsAgo);

    const user = users[Math.floor(Math.random() * users.length)];
    return {
      timestamp,
      visitor_id: user.visitor_id,
      user_ip: user.ip,
    };
  })
  .sort((a, b) => a.timestamp - b.timestamp)
  .map((payload) => getExtraPayload(payload))
  .map((payload) => {
    const user = usersByVisitorId.get(payload.visitor_id);
    const gp = user?.globalProperties ?? {};
    return {
      ...BASE_PAYLOAD,
      ...payload,
      user_agent: user?.user_agent ?? BASE_PAYLOAD.user_agent,
      screen_resolution: user?.screen_resolution ?? BASE_PAYLOAD.screen_resolution,
      referrer: user?.referrer ?? null,
      ...(Object.keys(gp).length > 0 ? { global_properties: gp } : {}),
    };
  });

console.log("[+] Running...");
console.time("events");

function* toBatches(array, batchSize) {
  for (let i = 0; i < array.length; i += batchSize) {
    yield array.slice(i, i + batchSize);
  }
}

async function executeBatches(batches) {
  for (const batch of batches) {
    await Promise.all(
      batch.map((event) =>
        fetch(TARGET_URL, {
          method: "POST",
          body: JSON.stringify(event),
          headers: {
            "Content-Type": "application/json",
            "X-Forwarded-For": event.user_ip,
            "User-Agent": event.user_agent,
          },
        })
      )
    );
  }
}

executeBatches([...toBatches(events, BATCH_SIZE)]).then(() => {
  console.timeEnd("events");
  console.log("[+] Completed!");
});