@ak--47/dungeon-master 1.4.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
@@ -1,70 +1,57 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "harness-sass";
3
- const num_days = 120;
4
- const num_users = 10_000;
5
- const avg_events_per_user_per_day = 1.2;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
1
+ // ── IMPORTS ──
11
2
  import dayjs from "dayjs";
12
3
  import utc from "dayjs/plugin/utc.js";
4
+ dayjs.extend(utc);
13
5
  import "dotenv/config";
14
6
  import * as u from "../../lib/utils/utils.js";
15
7
  import * as v from "ak-tools";
16
8
  import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
17
-
18
- dayjs.extend(utc);
19
- const chance = u.initChance(SEED);
20
-
21
9
  /** @typedef {import("../../types").Dungeon} Config */
22
10
 
11
+ // ── OVERVIEW ──
23
12
  /*
24
- * ═══════════════════════════════════════════════════════════════════════════════
25
- * DATASET OVERVIEW
26
- * ═══════════════════════════════════════════════════════════════════════════════
27
- *
28
- * CLOUDFORGE - B2B Cloud Infrastructure Monitoring & Deployment Platform
29
- *
30
- * CloudForge is a B2B SaaS platform that combines infrastructure monitoring (like Datadog)
31
- * with deployment automation (like Terraform). It serves engineering teams across companies
32
- * of all sizes - from startups deploying their first microservice to enterprises managing
33
- * thousands of services across multi-cloud environments.
34
- *
35
- * - 5,000 users over 100 days
36
- * - 600K events across 18 event types (+ 1 hook-created event type)
37
- * - 8 funnels (onboarding, monitoring, incident response, deployment, infra, team, docs, billing)
38
- * - Group analytics (companies)
39
- * - Desktop/browser only (B2B SaaS - no mobile devices)
40
- *
41
- * CORE PLATFORM:
42
- * Teams create workspaces, deploy services across AWS/GCP/Azure, and monitor everything
43
- * from a unified dashboard. The platform tracks uptime, latency, error rates, CPU/memory
44
- * usage, and costs. When things go wrong, CloudForge triggers alerts that route through
45
- * PagerDuty/Slack integrations, and on-call engineers acknowledge and resolve incidents
46
- * using automated runbooks.
47
- *
48
- * PRICING MODEL:
49
- * Four tiers: Free, Team, Business, Enterprise. Enterprise customers get dedicated
50
- * customer success managers and annual contracts. Pricing based on seat count and
51
- * resource usage.
13
+ * NAME: CloudForge
14
+ * APP: B2B SaaS that fuses infrastructure monitoring (Datadog-style) with
15
+ * deployment automation (Terraform-style). Engineering teams create
16
+ * workspaces, deploy services across AWS/GCP/Azure, monitor uptime
17
+ * and cost, and respond to alerts via Slack/PagerDuty runbooks.
18
+ * Pricing: Free / Team / Business / Enterprise (seats + usage).
19
+ * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 2026-05-01)
20
+ * CORE LOOP: workspace created service deployed dashboard viewed alert/resolve
21
+ *
22
+ * EVENTS (19):
23
+ * dashboard viewed (20) > api call (16) > query executed (15) > alert triggered (12)
24
+ * > service deployed (10) > deployment pipeline run (9) > alert acknowledged (8)
25
+ * > alert resolved (7) > documentation viewed (7) > security scan (6)
26
+ * > infrastructure scaled (5) > cost report generated (4) > integration configured (4)
27
+ * > feature flag toggled (4) > team member invited (3) > runbook executed (3)
28
+ * > billing event (3) > workspace created (1) > incident created (1)
29
+ *
30
+ * FUNNELS (8):
31
+ * - Onboarding: workspace created service deployed dashboard viewed (70%)
32
+ * - Daily Monitoring: dashboard viewed query executed api call (80%)
33
+ * - Incident Response: alert triggered alert acknowledged alert resolved (55%)
34
+ * - Deployment: deployment pipeline run service deployed dashboard viewed (65%, Canary A/B)
35
+ * - Infrastructure Mgmt: cost report generated → infrastructure scaled → security scan (50%)
36
+ * - Team & Config: team member invited → integration configured → feature flag toggled (40%)
37
+ * - Docs & Runbooks: documentation viewed → runbook executed → service deployed (45%)
38
+ * - Billing: billing event dashboard viewed (60%)
39
+ *
40
+ * USER PROPS: company_size, primary_role, team_name, seat_count, annual_contract_value,
41
+ * customer_success_manager, customer_health_score, plan_tier, cloud_provider
42
+ * SUPER PROPS: plan_tier, cloud_provider
43
+ * SCD PROPS: primary_role (viewer/editor/admin/owner, monthly fuzzy, max 6),
44
+ * plan_tier (starter/growth/enterprise/scale, monthly fixed, max 6, company_id-scoped)
45
+ * GROUPS: company_id (300 companies)
52
46
  */
53
47
 
48
+ // ── HOOK STORIES ──
54
49
  /*
55
- * ═══════════════════════════════════════════════════════════════════════════════
56
- * ANALYTICS HOOKS (11 hooks)
57
- * ═══════════════════════════════════════════════════════════════════════════════
58
- *
59
- * 10 deliberately architected patterns hidden in the data. NOTE: All cohort
60
- * effects are HIDDEN — no flag stamping. Discoverable via behavioral cohorts
61
- * or raw-prop breakdowns (company_size, day, doc_section). Adds:
62
- * 9. INCIDENT RESPONSE TIME-TO-CONVERT (Enterprise 0.67x gap vs Startup 1.5x)
63
- * [everything hook: scales response_time_mins and resolution_time_mins by company_size]
64
- * 10. DOCS MAGIC NUMBER (sweet 4-7 docs → +40% deploys; over 8+ → drop 25%)
50
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
51
+ * behavioral cohorts or raw-prop breakdowns (company_size, day, doc_section).
65
52
  *
66
53
  * ─────────────────────────────────────────────────────────────────────────────
67
- * 1. END-OF-QUARTER SPIKE (event)
54
+ * 1. END-OF-QUARTER SPIKE (everything)
68
55
  * ─────────────────────────────────────────────────────────────────────────────
69
56
  *
70
57
  * PATTERN: Days 100-110 billing events shift event_type toward "plan_upgraded"
@@ -217,13 +204,13 @@ const chance = u.initChance(SEED);
217
204
  * downscale; no engineer ignores a 25% month-over-month cost jump.
218
205
  *
219
206
  * ─────────────────────────────────────────────────────────────────────────────
220
- * 7. FAILED DEPLOYMENT RECOVERY (event — closure state)
207
+ * 7. FAILED DEPLOYMENT RECOVERY (everything)
221
208
  * ─────────────────────────────────────────────────────────────────────────────
222
209
  *
223
210
  * PATTERN: After a failed pipeline run, the user's next successful deploy has
224
- * duration_sec * 1.5 (recovery deploys are slower). Uses a module-level Map
225
- * for cross-call state. No flag discover by sequencing failed → next-success
226
- * pipeline events per user and comparing duration.
211
+ * duration_sec * 1.5 (recovery deploys are slower). No flag discover by
212
+ * sequencing failed next-success pipeline events per user and comparing
213
+ * duration.
227
214
  *
228
215
  * HOW TO FIND IT IN MIXPANEL:
229
216
  *
@@ -397,73 +384,292 @@ const chance = u.initChance(SEED);
397
384
  * Docs Magic Number | over (8+) deploys/user | 1x | ~0.75x | -25%
398
385
  * Deploy Experiment | Canary conversion | 65% | ~78% | 1.2x
399
386
  * Deploy Experiment | Canary TTC | 1d | ~0.85d | 0.85x
400
- *
401
- * ─────────────────────────────────────────────────────────────────────────────
402
- * ADVANCED ANALYSIS IDEAS
403
- * ─────────────────────────────────────────────────────────────────────────────
404
- *
405
- * CROSS-HOOK PATTERNS:
406
- * - Churned + Enterprise: Do churned accounts skew toward startups or are
407
- * enterprise accounts also silenced?
408
- * - Integration + Cost: Do teams with full integrations manage costs better?
409
- * - Docs + Deploys + Failures: Do docs readers have fewer failed deployments?
410
- * - Quarter Spike + Churn: Are quarter-end upgrades correlated with later churn?
411
- * - Enterprise Recovery: Do enterprise customers recover from failed deploys
412
- * differently than startups?
413
- *
414
- * COHORT ANALYSIS:
415
- * - By company_size: Compare all metrics across startup/smb/mid_market/enterprise
416
- * - By plan_tier: Free vs. Team vs. Business vs. Enterprise engagement
417
- * - By cloud_provider: AWS vs. GCP vs. Azure deployment and alert patterns
418
- * - By primary_role: Engineer vs. SRE vs. DevOps vs. Manager behaviors
419
- *
420
- * KEY METRICS:
421
- * - MTTR: alert triggered → alert resolved duration
422
- * - Deployment Frequency: service deployed per user per week
423
- * - Deployment Success Rate: pipeline success vs. failure ratio
424
- * - Cost Efficiency: total_cost trend over time per company
425
- * - Feature Adoption: integration configured events by type
426
- * - Documentation Engagement: documentation viewed by section
427
387
  */
428
388
 
429
- // Generate consistent IDs for lookup tables and event properties
389
+ // ── SCALE ──
390
+ const SEED = "harness-sass";
391
+ const NUM_USERS = 10_000;
392
+ const DATASET_START = "2026-01-01T00:00:00Z";
393
+ const DATASET_END = "2026-05-01T23:59:59Z";
394
+ const EVENTS_PER_DAY = 1.2;
395
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
396
+
397
+ const chance = u.initChance(SEED);
398
+
399
+ // ── KNOBS (tweak these to reshape stories) ──
400
+ const EOQ_START_DAY = 100;
401
+ const EOQ_END_DAY = 110;
402
+ const EOQ_UPGRADE_LIKELIHOOD = 40;
403
+ const EOQ_INVITE_CLONE_LIKELIHOOD = 50;
404
+
405
+ const CHURN_USER_HASH_MOD = 5;
406
+ const CHURN_CUTOFF_DAYS = 30;
407
+
408
+ const ALERT_ESCALATION_LIKELIHOOD = 30;
409
+
410
+ const INTEGRATION_RESPONSE_FACTOR = 0.4;
411
+ const INTEGRATION_RESOLUTION_FACTOR = 0.5;
412
+
413
+ const COST_OVERRUN_THRESHOLD = 25;
414
+
415
+ const FAILED_DEPLOY_RECOVERY_MULT = 1.5;
416
+
417
+ const DOCS_SWEET_MIN = 4;
418
+ const DOCS_SWEET_MAX = 7;
419
+ const DOCS_OVER_THRESHOLD = 8;
420
+ const DOCS_EXTRA_DEPLOYS_MIN = 2;
421
+ const DOCS_EXTRA_DEPLOYS_MAX = 3;
422
+ const DOCS_DEPLOY_DROP_LIKELIHOOD = 25;
423
+
424
+ const TTC_ENTERPRISE_FACTOR = 0.67;
425
+ const TTC_STARTUP_FACTOR = 1.5;
426
+ const INCIDENT_SEQ_WINDOW_MINS = 60 * 24 * 30;
427
+
428
+ // ── DATA ARRAYS ──
430
429
  const serviceIds = v.range(1, 201).map(() => `svc_${v.uid(8)}`);
431
430
  const alertIds = v.range(1, 501).map(() => `alert_${v.uid(6)}`);
432
431
  const pipelineIds = v.range(1, 101).map(() => `pipe_${v.uid(6)}`);
433
432
  const runbookIds = v.range(1, 51).map(() => `rb_${v.uid(6)}`);
434
- const companyIds = v.range(1, 301).map(() => `comp_${v.uid(8)}`);
435
433
 
436
- // Module-level Maps for closure-based state tracking across hook calls
434
+ // ── HOOK STATE ──
435
+ // Module-level Map for closure-based state tracking across event-hook calls
437
436
  const costOverrunUsers = new Map();
438
- const failedDeployUsers = new Map();
439
437
 
438
+ // ── HELPER FUNCTIONS ──
439
+ function handleEventHooks(record) {
440
+ // H3: ALERT ESCALATION REPLACEMENT — critical/emergency alerts sometimes
441
+ // become incident-created events.
442
+ if (record.event === "alert triggered") {
443
+ const severity = record.severity;
444
+ if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: ALERT_ESCALATION_LIKELIHOOD })) {
445
+ return {
446
+ ...record,
447
+ event: "incident created",
448
+ escalation_level: chance.pickone(["P1", "P2"]),
449
+ teams_paged: chance.integer({ min: 1, max: 5 }),
450
+ incident_id: `inc_${v.uid(8)}`,
451
+ original_severity: severity,
452
+ original_alert_type: record.alert_type,
453
+ auto_escalated: true,
454
+ };
455
+ }
456
+ }
457
+
458
+ // H6: COST OVERRUN PATTERN — cost reports with cost_change > 25% record
459
+ // the user, then the next infrastructure-scaled event from that user is
460
+ // forced to scale_direction = "down".
461
+ if (record.event === "cost report generated" && record.cost_change_percent > COST_OVERRUN_THRESHOLD) {
462
+ costOverrunUsers.set(record.user_id, true);
463
+ }
464
+ if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
465
+ record.scale_direction = "down";
466
+ costOverrunUsers.delete(record.user_id);
467
+ }
468
+
469
+ return record;
470
+ }
471
+
472
+ function handleUserHooks(record) {
473
+ // H8: ENTERPRISE VS STARTUP — company size determines seat count, ACV,
474
+ // and CSM. Real profile attrs.
475
+ const companySize = record.company_size;
476
+ if (companySize === "enterprise") {
477
+ record.seat_count = chance.integer({ min: 50, max: 500 });
478
+ record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
479
+ record.customer_success_manager = true;
480
+ } else if (companySize === "mid_market") {
481
+ record.seat_count = chance.integer({ min: 10, max: 50 });
482
+ record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
483
+ record.customer_success_manager = false;
484
+ } else if (companySize === "smb") {
485
+ record.seat_count = chance.integer({ min: 3, max: 10 });
486
+ record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
487
+ record.customer_success_manager = false;
488
+ } else if (companySize === "startup") {
489
+ record.seat_count = chance.integer({ min: 1, max: 5 });
490
+ record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
491
+ record.customer_success_manager = false;
492
+ }
493
+ record.customer_health_score = chance.integer({ min: 1, max: 100 });
494
+ return record;
495
+ }
496
+
497
+ function handleEverythingHooks(record, meta) {
498
+ const datasetStart = dayjs.unix(meta.datasetStart);
499
+ const userEvents = record;
500
+ const profile = meta.profile;
501
+
502
+ userEvents.forEach(e => {
503
+ e.plan_tier = profile.plan_tier;
504
+ e.cloud_provider = profile.cloud_provider;
505
+ });
506
+
507
+ // H1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
508
+ // event_type to plan_upgraded 40% of the time.
509
+ userEvents.forEach(e => {
510
+ if (e.event !== "billing event") return;
511
+ const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
512
+ if (dayInDataset >= EOQ_START_DAY && dayInDataset <= EOQ_END_DAY && chance.bool({ likelihood: EOQ_UPGRADE_LIKELIHOOD })) {
513
+ e.event_type = "plan_upgraded";
514
+ }
515
+ });
516
+
517
+ // H1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone 50% of
518
+ // team-member-invited events (push, not return).
519
+ for (let i = userEvents.length - 1; i >= 0; i--) {
520
+ const e = userEvents[i];
521
+ if (e.event !== "team member invited") continue;
522
+ const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
523
+ if (dayInDataset >= EOQ_START_DAY && dayInDataset <= EOQ_END_DAY && chance.bool({ likelihood: EOQ_INVITE_CLONE_LIKELIHOOD })) {
524
+ userEvents.push({
525
+ ...e,
526
+ time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
527
+ user_id: e.user_id,
528
+ role: chance.pickone(["editor", "viewer"]),
529
+ invitation_method: chance.pickone(["email", "sso", "slack"]),
530
+ });
531
+ }
532
+ }
533
+
534
+ // H2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5) have post-day-30
535
+ // events removed.
536
+ if (userEvents && userEvents.length > 0) {
537
+ const firstEvent = userEvents[0];
538
+ const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
539
+ if ((idHash % CHURN_USER_HASH_MOD) === 0) {
540
+ for (let i = userEvents.length - 1; i >= 0; i--) {
541
+ const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
542
+ if (dayInDataset > CHURN_CUTOFF_DAYS) {
543
+ userEvents.splice(i, 1);
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ // H4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get alert
550
+ // response_time_mins 0.4x and resolution_time_mins 0.5x.
551
+ let hasSlack = false;
552
+ let hasPagerduty = false;
553
+ userEvents.forEach((event) => {
554
+ if (event.event === "integration configured") {
555
+ if (event.integration_type === "slack") hasSlack = true;
556
+ if (event.integration_type === "pagerduty") hasPagerduty = true;
557
+ }
558
+ });
559
+ if (hasSlack && hasPagerduty) {
560
+ userEvents.forEach((event) => {
561
+ if (event.event === "alert acknowledged" && event.response_time_mins) {
562
+ event.response_time_mins = Math.floor(event.response_time_mins * INTEGRATION_RESPONSE_FACTOR);
563
+ }
564
+ if (event.event === "alert resolved" && event.resolution_time_mins) {
565
+ event.resolution_time_mins = Math.floor(event.resolution_time_mins * INTEGRATION_RESOLUTION_FACTOR);
566
+ }
567
+ });
568
+ }
569
+
570
+ // H5 + H10: DOCS MAGIC NUMBER — sweet 4-7 docs → +40% extra cloned
571
+ // service-deployed events; over 8+ → drop 25% of service-deployed events.
572
+ const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
573
+ const deployTemplate = userEvents.find(e => e.event === "service deployed");
574
+ if (docsCount >= DOCS_SWEET_MIN && docsCount <= DOCS_SWEET_MAX && deployTemplate) {
575
+ const lastEvent = userEvents[userEvents.length - 1];
576
+ const extraDeploys = chance.integer({ min: DOCS_EXTRA_DEPLOYS_MIN, max: DOCS_EXTRA_DEPLOYS_MAX });
577
+ for (let i = 0; i < extraDeploys; i++) {
578
+ userEvents.push({
579
+ ...deployTemplate,
580
+ time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
581
+ user_id: lastEvent.user_id,
582
+ service_id: chance.pickone(serviceIds),
583
+ service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
584
+ environment: "production",
585
+ cloud_provider: profile.cloud_provider,
586
+ });
587
+ }
588
+ } else if (docsCount >= DOCS_OVER_THRESHOLD) {
589
+ for (let i = userEvents.length - 1; i >= 0; i--) {
590
+ if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: DOCS_DEPLOY_DROP_LIKELIHOOD })) {
591
+ userEvents.splice(i, 1);
592
+ }
593
+ }
594
+ }
595
+
596
+ // H7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs in this
597
+ // user's pipeline events, multiply duration_sec by 1.5 on the recovery
598
+ // deploy.
599
+ const pipelineEvents = userEvents
600
+ .filter(e => e.event === "deployment pipeline run")
601
+ .sort((a, b) => a.time.localeCompare(b.time));
602
+ for (let i = 1; i < pipelineEvents.length; i++) {
603
+ if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
604
+ pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * FAILED_DEPLOY_RECOVERY_MULT);
605
+ }
606
+ }
607
+
608
+ // H9: INCIDENT RESPONSE TTC — enterprise resolves faster, startup
609
+ // resolves slower. Scale response_time_mins on acknowledged events and
610
+ // resolution_time_mins on resolved events by company_size, and shift the
611
+ // first incident-response funnel sequence timestamps for funnel TTC
612
+ // reports. Compounds with H4 (integration users).
613
+ const companySegment = profile?.company_size;
614
+ const ttcFactor = (
615
+ companySegment === "enterprise" ? TTC_ENTERPRISE_FACTOR :
616
+ companySegment === "startup" ? TTC_STARTUP_FACTOR :
617
+ 1.0
618
+ );
619
+ if (ttcFactor !== 1.0) {
620
+ // Timestamp shift: affects Mixpanel funnel TTC
621
+ const incidentSeq = findFirstSequence(
622
+ userEvents,
623
+ ["alert triggered", "alert acknowledged", "alert resolved"],
624
+ INCIDENT_SEQ_WINDOW_MINS
625
+ );
626
+ if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
627
+ // Property scale: affects Insights AVG reports
628
+ userEvents.forEach(e => {
629
+ if (e.event === "alert acknowledged" && e.response_time_mins) {
630
+ e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
631
+ }
632
+ if (e.event === "alert resolved" && e.resolution_time_mins) {
633
+ e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
634
+ }
635
+ });
636
+ }
637
+
638
+ return record;
639
+ }
640
+
641
+ // ── CONFIG ──
440
642
  /** @type {Config} */
441
643
  const config = {
442
- token,
443
644
  seed: SEED,
444
- datasetStart: "2026-01-01T00:00:00Z",
445
- datasetEnd: "2026-05-01T23:59:59Z",
446
- // numDays: num_days,
447
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
448
- numUsers: num_users,
645
+ datasetStart: DATASET_START,
646
+ datasetEnd: DATASET_END,
647
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
648
+ numUsers: NUM_USERS,
649
+ format: "json",
650
+ gzip: true,
651
+ credentials: {
652
+ token,
653
+ },
654
+ switches: {
655
+ hasSessionIds: true,
656
+ alsoInferFunnels: false,
657
+ hasLocation: true,
658
+ hasAndroidDevices: false,
659
+ hasIOSDevices: false,
660
+ hasDesktopDevices: true,
661
+ hasBrowser: true,
662
+ hasCampaigns: false,
663
+ isAnonymous: false,
664
+ hasAdSpend: false,
665
+ hasAvatar: true,
666
+ },
449
667
  // Phase 2 identity model — B2B SaaS reference. Engineers commonly use 1-2
450
668
  // devices (desktop + work laptop). avgDevicePerUser:2 puts a meaningful
451
669
  // per-session sticky-device pattern in Mixpanel device dashboards.
452
- hasAnonIds: true,
453
- avgDevicePerUser: 2,
454
- hasSessionIds: true,
455
- format: "json",
456
- gzip: true,
457
- alsoInferFunnels: false,
458
- hasLocation: true,
459
- hasAndroidDevices: false,
460
- hasIOSDevices: false,
461
- hasDesktopDevices: true,
462
- hasBrowser: true,
463
- hasCampaigns: false,
464
- isAnonymous: false,
465
- hasAdSpend: false,
466
- hasAvatar: true,
670
+ identity: {
671
+ avgDevicePerUser: 2,
672
+ },
467
673
  concurrency: 1,
468
674
  writeToDisk: false,
469
675
  scdProps: {
@@ -568,6 +774,7 @@ const config = {
568
774
  {
569
775
  event: "service deployed",
570
776
  weight: 10,
777
+ isStrictEvent: false,
571
778
  properties: {
572
779
  service_id: serviceIds,
573
780
  service_type: ["web_app", "api", "database", "cache", "queue", "ml_model"],
@@ -578,6 +785,7 @@ const config = {
578
785
  {
579
786
  event: "dashboard viewed",
580
787
  weight: 20,
788
+ isStrictEvent: false,
581
789
  properties: {
582
790
  dashboard_type: ["overview", "cost", "performance", "security", "custom"],
583
791
  time_range: ["1h", "6h", "24h", "7d", "30d"],
@@ -586,6 +794,7 @@ const config = {
586
794
  {
587
795
  event: "alert triggered",
588
796
  weight: 12,
797
+ isStrictEvent: false,
589
798
  properties: {
590
799
  alert_id: alertIds,
591
800
  severity: ["info", "warning", "critical", "emergency"],
@@ -609,6 +818,7 @@ const config = {
609
818
  {
610
819
  event: "alert acknowledged",
611
820
  weight: 8,
821
+ isStrictEvent: false,
612
822
  properties: {
613
823
  alert_id: alertIds,
614
824
  response_time_mins: u.weighNumRange(1, 120),
@@ -618,6 +828,7 @@ const config = {
618
828
  {
619
829
  event: "alert resolved",
620
830
  weight: 7,
831
+ isStrictEvent: false,
621
832
  properties: {
622
833
  alert_id: alertIds,
623
834
  resolution_time_mins: u.weighNumRange(5, 1440),
@@ -627,6 +838,7 @@ const config = {
627
838
  {
628
839
  event: "deployment pipeline run",
629
840
  weight: 9,
841
+ isStrictEvent: false,
630
842
  properties: {
631
843
  pipeline_id: pipelineIds,
632
844
  status: ["success", "failed", "cancelled"],
@@ -637,6 +849,7 @@ const config = {
637
849
  {
638
850
  event: "infrastructure scaled",
639
851
  weight: 5,
852
+ isStrictEvent: false,
640
853
  properties: {
641
854
  service_id: serviceIds,
642
855
  scale_direction: ["up", "up", "up", "down"],
@@ -648,6 +861,7 @@ const config = {
648
861
  {
649
862
  event: "cost report generated",
650
863
  weight: 4,
864
+ isStrictEvent: false,
651
865
  properties: {
652
866
  report_period: ["daily", "weekly", "monthly"],
653
867
  total_cost: u.weighNumRange(100, 50000),
@@ -657,6 +871,7 @@ const config = {
657
871
  {
658
872
  event: "team member invited",
659
873
  weight: 3,
874
+ isStrictEvent: false,
660
875
  properties: {
661
876
  role: ["admin", "editor", "viewer", "billing"],
662
877
  invitation_method: ["email", "sso", "slack"],
@@ -665,6 +880,7 @@ const config = {
665
880
  {
666
881
  event: "integration configured",
667
882
  weight: 4,
883
+ isStrictEvent: false,
668
884
  properties: {
669
885
  integration_type: ["slack", "pagerduty", "jira", "github", "datadog", "terraform"],
670
886
  status: ["active", "paused", "error"],
@@ -691,6 +907,7 @@ const config = {
691
907
  {
692
908
  event: "billing event",
693
909
  weight: 3,
910
+ isStrictEvent: false,
694
911
  properties: {
695
912
  event_type: ["invoice_generated", "invoice_generated", "payment_received", "payment_received", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
696
913
  amount: u.weighNumRange(99, 25000),
@@ -718,6 +935,7 @@ const config = {
718
935
  {
719
936
  event: "documentation viewed",
720
937
  weight: 7,
938
+ isStrictEvent: false,
721
939
  properties: {
722
940
  doc_section: ["getting_started", "api_reference", "best_practices", "troubleshooting", "changelog"],
723
941
  time_on_page_sec: u.weighNumRange(5, 600),
@@ -766,230 +984,10 @@ const config = {
766
984
 
767
985
  lookupTables: [],
768
986
 
769
- /**
770
- * ARCHITECTED ANALYTICS HOOKS
771
- *
772
- * This hook function creates 10 deliberate patterns in the data.
773
- * Hook 11 (Deploy Pipeline Experiment) is engine-managed via funnel
774
- * experiment config — no hook code needed.
775
- *
776
- * 1. END-OF-QUARTER SPIKE: Days 100-110 drive plan upgrades and team expansion
777
- * 2. CHURNED ACCOUNT SILENCING: ~10% of users go completely silent after month 1
778
- * 3. ALERT ESCALATION REPLACEMENT: Critical alerts become "incident created" events
779
- * 4. INTEGRATION USERS SUCCEED: Slack+PagerDuty users resolve incidents 50-60% faster
780
- * 5. DOCS READERS DEPLOY MORE: Best practices readers get extra production deploys
781
- * 6. COST OVERRUN PATTERN: Budget-exceeded users react by scaling down infrastructure
782
- * 7. FAILED DEPLOYMENT RECOVERY: Recovery deploys take 1.5x longer, tracked across calls
783
- * 8. ENTERPRISE VS STARTUP: Company size determines seat count, ACV, and health score
784
- * 9. INCIDENT RESPONSE TTC: Enterprise 0.67x faster, startup 1.5x slower incident resolution
785
- * 10. DOCS MAGIC NUMBER: Sweet 4-7 docs → extra deploys; over 8+ → drop 25% of deploys
786
- * 11. DEPLOY PIPELINE EXPERIMENT: Canary Deploys A/B test on deployment funnel (engine-managed)
787
- */
788
- hook: function (record, type, meta) {
789
- // (Hook 1a moved to everything hook for reliable datasetStart access)
790
-
791
- // HOOK 3: ALERT ESCALATION REPLACEMENT (event) — critical/emergency
792
- // alerts sometimes become incident-created events. Real product flow.
793
- if (type === "event") {
794
- if (record.event === "alert triggered") {
795
- const severity = record.severity;
796
- if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: 30 })) {
797
- return {
798
- ...record,
799
- event: "incident created",
800
- escalation_level: chance.pickone(["P1", "P2"]),
801
- teams_paged: chance.integer({ min: 1, max: 5 }),
802
- incident_id: `inc_${v.uid(8)}`,
803
- original_severity: severity,
804
- original_alert_type: record.alert_type,
805
- auto_escalated: true,
806
- };
807
- }
808
- }
809
- }
810
-
811
- // HOOK 6: COST OVERRUN PATTERN (event) — cost reports with cost_change
812
- // > 25% record user, then next infrastructure-scaled event from that
813
- // user gets scale_direction = "down". No flag.
814
- if (type === "event") {
815
- if (record.event === "cost report generated" && record.cost_change_percent > 25) {
816
- costOverrunUsers.set(record.user_id, true);
817
- }
818
- if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
819
- record.scale_direction = "down";
820
- costOverrunUsers.delete(record.user_id);
821
- }
822
- }
823
-
824
- if (type === "everything") {
825
- const datasetStart = dayjs.unix(meta.datasetStart);
826
- const userEvents = record;
827
- const profile = meta.profile;
828
-
829
- userEvents.forEach(e => {
830
- e.plan_tier = profile.plan_tier;
831
- e.cloud_provider = profile.cloud_provider;
832
- });
833
-
834
- // HOOK 1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
835
- // event_type to plan_upgraded 40% of the time. No flag.
836
- userEvents.forEach(e => {
837
- if (e.event !== "billing event") return;
838
- const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
839
- if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 40 })) {
840
- e.event_type = "plan_upgraded";
841
- }
842
- });
843
-
844
- // HOOK 1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone
845
- // 50% of team-member-invited events (push, not return). No flag.
846
- for (let i = userEvents.length - 1; i >= 0; i--) {
847
- const e = userEvents[i];
848
- if (e.event !== "team member invited") continue;
849
- const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
850
- if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 50 })) {
851
- userEvents.push({
852
- ...e,
853
- time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
854
- user_id: e.user_id,
855
- role: chance.pickone(["editor", "viewer"]),
856
- invitation_method: chance.pickone(["email", "sso", "slack"]),
857
- });
858
- }
859
- }
860
-
861
- // HOOK 2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5)
862
- // have post-day-30 events removed. No flag.
863
- if (userEvents && userEvents.length > 0) {
864
- const firstEvent = userEvents[0];
865
- const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
866
- if ((idHash % 5) === 0) {
867
- for (let i = userEvents.length - 1; i >= 0; i--) {
868
- const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
869
- if (dayInDataset > 30) {
870
- userEvents.splice(i, 1);
871
- }
872
- }
873
- }
874
- }
875
-
876
- // HOOK 4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get
877
- // alert response_time_mins 0.4x and resolution_time_mins 0.5x.
878
- // Mutates raw props. No flag.
879
- let hasSlack = false;
880
- let hasPagerduty = false;
881
- userEvents.forEach((event) => {
882
- if (event.event === "integration configured") {
883
- if (event.integration_type === "slack") hasSlack = true;
884
- if (event.integration_type === "pagerduty") hasPagerduty = true;
885
- }
886
- });
887
- if (hasSlack && hasPagerduty) {
888
- userEvents.forEach((event) => {
889
- if (event.event === "alert acknowledged" && event.response_time_mins) {
890
- event.response_time_mins = Math.floor(event.response_time_mins * 0.4);
891
- }
892
- if (event.event === "alert resolved" && event.resolution_time_mins) {
893
- event.resolution_time_mins = Math.floor(event.resolution_time_mins * 0.5);
894
- }
895
- });
896
- }
897
-
898
- // HOOK 5 + HOOK 10: DOCS MAGIC NUMBER (no flags)
899
- // Sweet 4-7 documentation-viewed events → +40% extra cloned
900
- // service-deployed events. Over 8+ → drop 25% of service-deployed
901
- // events. No flag.
902
- const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
903
- const deployTemplate = userEvents.find(e => e.event === "service deployed");
904
- if (docsCount >= 4 && docsCount <= 7 && deployTemplate) {
905
- const lastEvent = userEvents[userEvents.length - 1];
906
- const extraDeploys = chance.integer({ min: 2, max: 3 });
907
- for (let i = 0; i < extraDeploys; i++) {
908
- userEvents.push({
909
- ...deployTemplate,
910
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
911
- user_id: lastEvent.user_id,
912
- service_id: chance.pickone(serviceIds),
913
- service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
914
- environment: "production",
915
- cloud_provider: profile.cloud_provider,
916
- });
917
- }
918
- } else if (docsCount >= 8) {
919
- for (let i = userEvents.length - 1; i >= 0; i--) {
920
- if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: 25 })) {
921
- userEvents.splice(i, 1);
922
- }
923
- }
924
- }
925
-
926
- // HOOK 7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs
927
- // in this user's pipeline events, multiply duration_sec by 1.5 on
928
- // the recovery deploy. Full control via everything hook.
929
- const pipelineEvents = userEvents
930
- .filter(e => e.event === "deployment pipeline run")
931
- .sort((a, b) => a.time.localeCompare(b.time));
932
- for (let i = 1; i < pipelineEvents.length; i++) {
933
- if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
934
- pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * 1.5);
935
- }
936
- }
937
-
938
- // HOOK 9: INCIDENT RESPONSE TTC — enterprise resolves faster,
939
- // startup resolves slower. Scale response_time_mins on
940
- // acknowledged events and resolution_time_mins on resolved
941
- // events by company_size. This compounds with H4 (integration
942
- // users) — realistic: enterprise + good tooling = fastest.
943
- const companySegment = profile?.company_size;
944
- const ttcFactor = (
945
- companySegment === "enterprise" ? 0.67 :
946
- companySegment === "startup" ? 1.5 :
947
- 1.0
948
- );
949
- if (ttcFactor !== 1.0) {
950
- // Timestamp shift: affects Mixpanel funnel TTC
951
- const incidentSeq = findFirstSequence(
952
- userEvents,
953
- ["alert triggered", "alert acknowledged", "alert resolved"],
954
- 60 * 24 * 30
955
- );
956
- if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
957
- // Property scale: affects Insights AVG reports
958
- userEvents.forEach(e => {
959
- if (e.event === "alert acknowledged" && e.response_time_mins) {
960
- e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
961
- }
962
- if (e.event === "alert resolved" && e.resolution_time_mins) {
963
- e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
964
- }
965
- });
966
- }
967
- }
968
-
969
- // HOOK 8: ENTERPRISE VS STARTUP (user) — company size determines
970
- // seat count, ACV, and CSM. Real profile attrs.
971
- if (type === "user") {
972
- const companySize = record.company_size;
973
- if (companySize === "enterprise") {
974
- record.seat_count = chance.integer({ min: 50, max: 500 });
975
- record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
976
- record.customer_success_manager = true;
977
- } else if (companySize === "mid_market") {
978
- record.seat_count = chance.integer({ min: 10, max: 50 });
979
- record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
980
- record.customer_success_manager = false;
981
- } else if (companySize === "smb") {
982
- record.seat_count = chance.integer({ min: 3, max: 10 });
983
- record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
984
- record.customer_success_manager = false;
985
- } else if (companySize === "startup") {
986
- record.seat_count = chance.integer({ min: 1, max: 5 });
987
- record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
988
- record.customer_success_manager = false;
989
- }
990
- record.customer_health_score = chance.integer({ min: 1, max: 100 });
991
- }
992
-
987
+ hook(record, type, meta) {
988
+ if (type === "event") return handleEventHooks(record);
989
+ if (type === "user") return handleUserHooks(record);
990
+ if (type === "everything") return handleEverythingHooks(record, meta);
993
991
  return record;
994
992
  }
995
993
  };