@ak--47/dungeon-master 1.3.1 → 1.4.0

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 (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/hook-helpers-verify.js +89 -0
  3. package/dungeons/technical/identity-model-verify.js +47 -0
  4. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  5. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  6. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  7. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  8. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  9. package/dungeons/vertical/ai-platform.js +45 -52
  10. package/dungeons/vertical/community.js +11 -8
  11. package/dungeons/vertical/crypto.js +25 -24
  12. package/dungeons/vertical/dating.js +56 -48
  13. package/dungeons/vertical/devtools.js +25 -18
  14. package/dungeons/vertical/ecommerce.js +42 -38
  15. package/dungeons/vertical/education.js +24 -9
  16. package/dungeons/vertical/fintech.js +13 -8
  17. package/dungeons/vertical/fitness.js +73 -122
  18. package/dungeons/vertical/food-delivery.js +18 -19
  19. package/dungeons/vertical/gaming.js +19 -20
  20. package/dungeons/vertical/healthcare.js +11 -8
  21. package/dungeons/vertical/insurance-application.js +6 -3
  22. package/dungeons/vertical/logistics.js +15 -9
  23. package/dungeons/vertical/marketplace.js +36 -27
  24. package/dungeons/vertical/media.js +27 -25
  25. package/dungeons/vertical/real-estate.js +18 -7
  26. package/dungeons/vertical/sass.js +84 -68
  27. package/dungeons/vertical/social.js +46 -47
  28. package/dungeons/vertical/travel.js +8 -5
  29. package/lib/core/config-validator.js +136 -157
  30. package/lib/generators/events.js +49 -93
  31. package/lib/generators/funnels.js +202 -91
  32. package/lib/hook-helpers/_internal.js +23 -0
  33. package/lib/hook-helpers/cohort.js +124 -0
  34. package/lib/hook-helpers/identity.js +56 -0
  35. package/lib/hook-helpers/index.js +44 -0
  36. package/lib/hook-helpers/inject.js +99 -0
  37. package/lib/hook-helpers/mutate.js +151 -0
  38. package/lib/hook-helpers/timing.js +99 -0
  39. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  40. package/lib/hook-patterns/attributed-by-source.js +72 -0
  41. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  42. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  43. package/lib/hook-patterns/index.js +14 -0
  44. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  45. package/lib/orchestrators/user-loop.js +119 -269
  46. package/lib/utils/utils.js +29 -16
  47. package/lib/verify/emulate-breakdown.js +281 -0
  48. package/lib/verify/index.js +12 -0
  49. package/lib/verify/verify-dungeon.js +61 -0
  50. package/package.json +6 -4
  51. package/types.d.ts +397 -211
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-fintech";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -348,14 +348,16 @@ const chance = u.initChance(SEED);
348
348
 
349
349
  /** @type {Config} */
350
350
  const config = {
351
+ version: 2,
351
352
  token,
352
353
  seed: SEED,
353
354
  datasetStart: "2026-01-01T00:00:00Z",
354
- datasetEnd: "2026-04-28T23:59:59Z",
355
+ datasetEnd: "2026-05-01T23:59:59Z",
355
356
  // numDays: num_days,
356
357
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
357
358
  numUsers: num_users,
358
- hasAnonIds: false,
359
+ hasAnonIds: true,
360
+ avgDevicePerUser: 2,
359
361
  hasSessionIds: true,
360
362
  format: "json",
361
363
  gzip: true,
@@ -453,6 +455,7 @@ const config = {
453
455
  event: "account opened",
454
456
  weight: 1,
455
457
  isFirstEvent: true,
458
+ isAuthEvent: true,
456
459
  properties: {
457
460
  "account_type": ["personal", "business", "personal"],
458
461
  "signup_channel": ["app", "web", "referral", "branch"],
@@ -505,6 +508,7 @@ const config = {
505
508
  {
506
509
  event: "bill payment missed",
507
510
  weight: 1,
511
+ isStrictEvent: true, // hook-only: created by Hook 6 from manual bill-paid events
508
512
  properties: {
509
513
  "bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
510
514
  "amount": u.weighNumRange(20, 3000, 0.5, 150),
@@ -880,11 +884,12 @@ const config = {
880
884
  }
881
885
  });
882
886
  } else if (txnCount >= 11) {
883
- userEvents.forEach(e => {
884
- if (e.event === "premium upgraded" && typeof e.monthly_fee === "number") {
885
- e.monthly_fee = Math.round(e.monthly_fee * 0.7 * 100) / 100;
887
+ // Drop 20% of premium-upgraded events for heavy transactors
888
+ for (let i = userEvents.length - 1; i >= 0; i--) {
889
+ if (userEvents[i].event === "premium upgraded" && chance.bool({ likelihood: 20 })) {
890
+ userEvents.splice(i, 1);
886
891
  }
887
- });
892
+ }
888
893
  }
889
894
  }
890
895
 
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-fitness";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -25,7 +25,7 @@ const chance = u.initChance(SEED);
25
25
  * FitQuest — a fitness & wellness app for workout tracking, meal
26
26
  * planning, social fitness challenges, and AI coaching.
27
27
  *
28
- * - 5,000 users over 100 days, ~600K events
28
+ * - 5,000 users over 120 days, ~600K events
29
29
  * - 5 personas: athlete (10%), casual_exerciser (40%),
30
30
  * new_year_resolver (25%), social_motivator (15%), coach (10%)
31
31
  * - Core loop: sign up → plan workout → complete workout → track progress
@@ -71,7 +71,7 @@ const chance = u.initChance(SEED);
71
71
  * REAL-WORLD ANALOGUE: Morning workouts get a metabolic boost.
72
72
  *
73
73
  * ───────────────────────────────────────────────────────────────
74
- * 2. POST-LAUNCH AI COACHING LIFT (event hook)
74
+ * 2. POST-LAUNCH AI COACHING LIFT (everything hook)
75
75
  * ───────────────────────────────────────────────────────────────
76
76
  *
77
77
  * PATTERN: After day 35 (ai_coach feature launch), workouts
@@ -308,14 +308,16 @@ const chance = u.initChance(SEED);
308
308
 
309
309
  /** @type {Config} */
310
310
  const config = {
311
+ version: 2,
311
312
  token,
312
313
  seed: SEED,
313
314
  datasetStart: "2026-01-01T00:00:00Z",
314
- datasetEnd: "2026-04-28T23:59:59Z",
315
+ datasetEnd: "2026-05-01T23:59:59Z",
315
316
  // numDays: num_days,
316
317
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
317
318
  numUsers: num_users,
318
- hasAnonIds: false,
319
+ hasAnonIds: true,
320
+ avgDevicePerUser: 3,
319
321
  hasSessionIds: true,
320
322
  format: "json",
321
323
  gzip: true,
@@ -348,6 +350,7 @@ const config = {
348
350
  event: "account created",
349
351
  weight: 1,
350
352
  isFirstEvent: true,
353
+ isAuthEvent: true,
351
354
  properties: {
352
355
  referral_source: ["organic", "friend_invite", "app_store", "social_media", "search"],
353
356
  },
@@ -555,6 +558,7 @@ const config = {
555
558
  superProps: {
556
559
  Platform: ["ios", "ios", "android"],
557
560
  workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
561
+ subscription_tier: ["free"],
558
562
  },
559
563
 
560
564
  // ── UserProps ──────────────────────────────────────────
@@ -567,6 +571,7 @@ const config = {
567
571
  goal: ["weight_loss", "muscle_gain", "endurance", "flexibility", "general_health"],
568
572
  Platform: ["ios", "ios", "android"],
569
573
  workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
574
+ subscription_tier: ["free"],
570
575
  },
571
576
 
572
577
  // ── Personas ──────────────────────────────────
@@ -629,69 +634,6 @@ const config = {
629
634
  },
630
635
  ],
631
636
 
632
- // ── Subscription ──────────────────────────────
633
- subscription: {
634
- plans: [
635
- { name: "free", price: 0, default: true },
636
- { name: "monthly", price: 12.99, trialDays: 7 },
637
- { name: "annual", price: 99.99 },
638
- { name: "family", price: 149.99 },
639
- ],
640
- lifecycle: {
641
- trialToPayRate: 0.65,
642
- upgradeRate: 0.65,
643
- downgradeRate: 0.03,
644
- churnRate: 0.05,
645
- winBackRate: 0.10,
646
- winBackDelay: 21,
647
- paymentFailureRate: 0.02,
648
- },
649
- },
650
-
651
- // ── Attribution ──────────────────────────────
652
- attribution: {
653
- model: "last_touch",
654
- window: 7,
655
- campaigns: [
656
- {
657
- name: "app_store",
658
- source: "apple",
659
- medium: "app_store",
660
- activeDays: [0, 100],
661
- dailyBudget: [200, 600],
662
- acquisitionRate: 0.03,
663
- userPersonaBias: { athlete: 0.5, coach: 0.3 },
664
- },
665
- {
666
- name: "google_play",
667
- source: "google",
668
- medium: "app_store",
669
- activeDays: [0, 100],
670
- dailyBudget: [150, 500],
671
- acquisitionRate: 0.02,
672
- },
673
- {
674
- name: "instagram_ads",
675
- source: "instagram",
676
- medium: "social",
677
- activeDays: [5, 90],
678
- dailyBudget: [200, 800],
679
- acquisitionRate: 0.03,
680
- userPersonaBias: { new_year_resolver: 0.5, social_motivator: 0.3 },
681
- },
682
- {
683
- name: "referral_program",
684
- source: "referral",
685
- medium: "referral",
686
- activeDays: [0, 100],
687
- dailyBudget: [50, 200],
688
- acquisitionRate: 0.02,
689
- userPersonaBias: { social_motivator: 0.4 },
690
- },
691
- ],
692
- organicRate: 0.30,
693
- },
694
-
695
637
  // ── Engagement Decay ──────────────────────────
696
638
  engagementDecay: {
697
639
  model: "step",
@@ -700,48 +642,39 @@ const config = {
700
642
  reactivationChance: 0.02,
701
643
  },
702
644
 
703
- // ── Features ──────────────────────────────────
704
- features: [
705
- {
706
- name: "ai_coach",
707
- launchDay: 35,
708
- adoptionCurve: "fast",
709
- property: "coaching_mode",
710
- values: ["self_guided", "ai_assisted"],
711
- defaultBefore: "self_guided",
712
- affectsEvents: ["workout completed", "workout planned"],
713
- },
714
- {
715
- name: "group_challenges",
716
- launchDay: 55,
717
- adoptionCurve: { k: 0.1, midpoint: 25 },
718
- property: "challenge_mode",
719
- values: ["solo", "group"],
720
- defaultBefore: "solo",
721
- affectsEvents: ["challenge joined", "workout completed"],
722
- },
723
- ],
724
-
725
645
  // ── Hook Function ──────────────────────────────────────
726
646
  hook: function (record, type, meta) {
727
647
  // HOOK 7: COACH PROFILE ENRICHMENT (user) — coach segment users
728
648
  // get high total_workouts + streak_days.
649
+ // Also assign subscription_tier by segment (replacing deprecated subscription feature).
729
650
  if (type === "user") {
730
651
  if (record.segment === "coach") {
731
652
  record.total_workouts = chance.integer({ min: 200, max: 500 });
732
653
  record.streak_days = chance.integer({ min: 60, max: 365 });
733
654
  }
655
+ // Subscription tier: athletes/coaches → annual/family; social → monthly; casual/resolver → mostly free
656
+ if (record.segment === "athlete") {
657
+ record.subscription_tier = chance.pickone(["annual", "annual", "family", "monthly"]);
658
+ } else if (record.segment === "coach") {
659
+ record.subscription_tier = chance.pickone(["annual", "family", "family"]);
660
+ } else if (record.segment === "social") {
661
+ record.subscription_tier = chance.pickone(["monthly", "monthly", "annual", "free"]);
662
+ } else if (record.segment === "resolver") {
663
+ record.subscription_tier = chance.pickone(["free", "free", "free", "monthly"]);
664
+ } else {
665
+ record.subscription_tier = chance.pickone(["free", "free", "monthly"]);
666
+ }
734
667
  }
735
668
 
736
669
  // HOOK 9 (T2C): WORKOUT LOOP TIME-TO-CONVERT (funnel-post)
737
670
  // Annual subscribers complete the Workout Loop funnel 1.3x faster
738
671
  // (factor 0.77); Free users 1.25x slower (factor 1.25).
739
672
  if (type === "funnel-post") {
740
- const segment = meta?.profile?.subscription_plan;
673
+ const tier = meta?.profile?.subscription_tier;
741
674
  if (Array.isArray(record) && record.length > 1) {
742
675
  const factor = (
743
- segment === "annual" || segment === "family" ? 0.77 :
744
- segment === "free" ? 1.25 :
676
+ tier === "annual" || tier === "family" ? 0.77 :
677
+ tier === "free" ? 1.25 :
745
678
  1.0
746
679
  );
747
680
  if (factor !== 1.0) {
@@ -754,25 +687,8 @@ const config = {
754
687
  }
755
688
  }
756
689
 
757
- // (HOOK 1: MORNING WORKOUT BOOST moved to everything hook — hour checks
690
+ // (HOOK 1 & 2 moved to everything hook — temporal and hour checks
758
691
  // must run after bunchIntoSessions redistributes timestamps)
759
- if (type === "event") {
760
- const datasetStart = dayjs.unix(meta.datasetStart);
761
- // ── HOOK 2: POST-LAUNCH AI COACHING LIFT (event) ────
762
- // After day 35, ai_assisted workouts get 1.2x duration.
763
- if (record.event === "workout completed" || record.event === "workout planned") {
764
- const AI_LAUNCH = datasetStart.add(35, "days");
765
- const eventTime = dayjs(record.time);
766
- if (eventTime.isAfter(AI_LAUNCH) && record.coaching_mode === "ai_assisted") {
767
- if (record.duration_minutes) {
768
- record.duration_minutes = Math.floor(record.duration_minutes * 1.2);
769
- }
770
- if (record.planned_duration_minutes) {
771
- record.planned_duration_minutes = Math.floor(record.planned_duration_minutes * 1.2);
772
- }
773
- }
774
- }
775
- }
776
692
 
777
693
  // ── EVERYTHING HOOKS ─────────────────────────────────
778
694
  if (type === "everything") {
@@ -787,6 +703,7 @@ const config = {
787
703
  events.forEach(e => {
788
704
  if (p.Platform) e.Platform = p.Platform;
789
705
  if (p.workout_type) e.workout_type = p.workout_type;
706
+ if (p.subscription_tier) e.subscription_tier = p.subscription_tier;
790
707
  });
791
708
  }
792
709
 
@@ -801,14 +718,48 @@ const config = {
801
718
  }
802
719
  });
803
720
 
721
+ // ── HOOK 2: POST-LAUNCH AI COACHING LIFT ────────────
722
+ // After day 35, ~40% of workouts switch to ai_assisted coaching,
723
+ // then ai_assisted workouts get 1.2x duration.
724
+ // (Replaces deprecated 'features' config; temporal check in everything per L1)
725
+ const AI_LAUNCH = datasetStart.add(35, "days");
726
+ events.forEach(e => {
727
+ if ((e.event === "workout completed" || e.event === "workout planned") &&
728
+ dayjs(e.time).isAfter(AI_LAUNCH)) {
729
+ // Adopt ai_assisted for ~40% of post-launch workouts
730
+ if (chance.bool({ likelihood: 40 })) {
731
+ e.coaching_mode = "ai_assisted";
732
+ }
733
+ // AI-assisted workouts get 1.2x duration
734
+ if (e.coaching_mode === "ai_assisted") {
735
+ if (e.duration_minutes) {
736
+ e.duration_minutes = Math.floor(e.duration_minutes * 1.2);
737
+ }
738
+ if (e.planned_duration_minutes) {
739
+ e.planned_duration_minutes = Math.floor(e.planned_duration_minutes * 1.2);
740
+ }
741
+ }
742
+ }
743
+ });
744
+
745
+ // ── GROUP CHALLENGES ADOPTION ────────────────────
746
+ // After day 55, ~30% of challenge events switch to group mode.
747
+ // (Replaces deprecated 'features' config)
748
+ const GROUP_LAUNCH = datasetStart.add(55, "days");
749
+ events.forEach(e => {
750
+ if ((e.event === "challenge joined" || e.event === "workout completed") &&
751
+ dayjs(e.time).isAfter(GROUP_LAUNCH) &&
752
+ chance.bool({ likelihood: 30 })) {
753
+ e.challenge_mode = "group";
754
+ }
755
+ });
756
+
804
757
  // ── HOOK 8: ANNUAL SUBSCRIBER CONVERSION FILTER ─
805
758
  // Free/monthly-tier users drop ~30% of "progress checked"
806
759
  // (last step of Workout Loop funnel) to simulate lower conversion.
807
- // NOTE: subscription field is named `subscription_plan` (set by the
808
- // subscription feature in user-loop.js), NOT `subscription_tier`.
809
760
  if (meta && meta.profile) {
810
- const plan = meta.profile.subscription_plan;
811
- if (plan !== "annual" && plan !== "family" && chance.bool({ likelihood: 30 })) {
761
+ const tier = meta.profile.subscription_tier;
762
+ if (tier !== "annual" && tier !== "family" && chance.bool({ likelihood: 30 })) {
812
763
  record = record.filter(e => e.event !== "progress checked");
813
764
  events = record;
814
765
  }
@@ -844,13 +795,13 @@ const config = {
844
795
  }
845
796
 
846
797
  // ── HOOK 4: SOCIAL CHALLENGE COMPLETION ──────────
847
- // Users with friend_added events get 1.5x challenge completions.
848
- const hasFriends = events.some(e => e.event === "friend added");
849
- if (hasFriends) {
798
+ // Users with >=3 friend_added events get 1.5x challenge completions.
799
+ const friendCount = events.filter(e => e.event === "friend added").length;
800
+ if (friendCount >= 3) {
850
801
  const templateChallenge = events.find(e => e.event === "challenge completed");
851
802
  if (templateChallenge) {
852
803
  const challengeCompletions = events.filter(e => e.event === "challenge completed");
853
- const extraCount = Math.floor(challengeCompletions.length * 0.5);
804
+ const extraCount = Math.max(1, Math.floor(challengeCompletions.length * 0.5));
854
805
  for (let i = 0; i < extraCount; i++) {
855
806
  const source = challengeCompletions[i % challengeCompletions.length];
856
807
  events.push({
@@ -865,8 +816,8 @@ const config = {
865
816
  }
866
817
 
867
818
  // ── HOOK 5: RESOLVER CHURN CLIFF ─────────────────
868
- // Resolver segment users with <8 events lose 70% after day 14.
869
- if (meta && meta.profile && meta.profile.segment === "resolver" && events.length < 8) {
819
+ // Resolver segment users with <30 events lose 70% after day 14.
820
+ if (meta && meta.profile && meta.profile.segment === "resolver" && events.length < 30) {
870
821
  const CHURN_CLIFF = datasetStart.add(14, "days");
871
822
  for (let i = events.length - 1; i >= 0; i--) {
872
823
  const eventTime = dayjs(events[i].time);
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "harness-food";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -261,14 +261,16 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
261
261
 
262
262
  /** @type {Config} */
263
263
  const config = {
264
+ version: 2,
264
265
  token,
265
266
  seed: SEED,
266
267
  datasetStart: "2026-01-01T00:00:00Z",
267
- datasetEnd: "2026-04-28T23:59:59Z",
268
+ datasetEnd: "2026-05-01T23:59:59Z",
268
269
  // numDays: num_days,
269
270
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
270
271
  numUsers: num_users,
271
- hasAnonIds: false,
272
+ hasAnonIds: true,
273
+ avgDevicePerUser: 2,
272
274
  hasSessionIds: true,
273
275
  format: "json",
274
276
  gzip: true,
@@ -365,6 +367,7 @@ const config = {
365
367
  event: "account created",
366
368
  weight: 1,
367
369
  isFirstEvent: true,
370
+ isAuthEvent: true,
368
371
  properties: {
369
372
  "signup_method": ["email", "google", "apple", "facebook"],
370
373
  "referral_code": [false, false, true],
@@ -611,21 +614,6 @@ const config = {
611
614
  }
612
615
  }
613
616
 
614
- if (type === "event") {
615
- const datasetStart = dayjs.unix(meta.datasetStart);
616
- const RAINY_WEEK_START = datasetStart.add(20, 'days');
617
- const RAINY_WEEK_END = datasetStart.add(27, 'days');
618
- const EVENT_TIME = dayjs(record.time);
619
-
620
- // HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
621
- // order placed events. Mutates existing prop. No flag.
622
- if (record.event === "order placed") {
623
- if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
624
- record.delivery_fee = (record.delivery_fee || 5) * 2;
625
- }
626
- }
627
- }
628
-
629
617
  if (type === "everything") {
630
618
  const datasetStart = dayjs.unix(meta.datasetStart);
631
619
  const RAINY_WEEK_START = datasetStart.add(20, 'days');
@@ -768,6 +756,17 @@ const config = {
768
756
  }
769
757
  }
770
758
 
759
+ // HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
760
+ // order placed events. Mutates existing prop. No flag.
761
+ userEvents.forEach(e => {
762
+ if (e.event === "order placed") {
763
+ const t = dayjs(e.time);
764
+ if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END)) {
765
+ e.delivery_fee = (e.delivery_fee || 5) * 2;
766
+ }
767
+ }
768
+ });
769
+
771
770
  // HOOK 4 (cont): RAINY WEEK SURGE — duplicate 40% of order placed
772
771
  // events that fall in the rainy window. Cloned with unique offset.
773
772
  const rainyDuplicates = [];
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "questforge";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -235,10 +235,6 @@ const chance = u.initChance(SEED);
235
235
  * audience segmentation lens for narrative design and content tuning.
236
236
  *
237
237
  * ───────────────────────────────────────────────────────────────────────────────
238
- * EXPECTED METRICS SUMMARY
239
- * ───────────────────────────────────────────────────────────────────────────────
240
- *
241
- * ───────────────────────────────────────────────────────────────────────────────
242
238
  * 12. COMBAT FUNNEL TIME-TO-CONVERT (funnel-post)
243
239
  *
244
240
  * PATTERN: Elite tier completes the Combat funnel 1.4x faster (factor
@@ -315,14 +311,16 @@ const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
315
311
 
316
312
  /** @type {Config} */
317
313
  const config = {
314
+ version: 2,
318
315
  token,
319
316
  seed: SEED,
320
317
  datasetStart: "2026-01-01T00:00:00Z",
321
- datasetEnd: "2026-04-28T23:59:59Z",
318
+ datasetEnd: "2026-05-01T23:59:59Z",
322
319
  // numDays: num_days,
323
320
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
324
321
  numUsers: num_users,
325
- hasAnonIds: false,
322
+ hasAnonIds: true,
323
+ avgDevicePerUser: 2,
326
324
  hasSessionIds: true,
327
325
  format: "json",
328
326
  gzip: true,
@@ -402,6 +400,7 @@ const config = {
402
400
  event: "character created",
403
401
  weight: 1,
404
402
  isFirstEvent: true,
403
+ isAuthEvent: true,
405
404
  properties: {
406
405
  "character_class": [
407
406
  "Barbarian", "Bard", "Cleric", "Druid", "Fighter", "Monk",
@@ -889,8 +888,8 @@ const config = {
889
888
  // Hook 5: PURCHASE VALUE — Lucky charm buyers see 2x prices
890
889
  // and 35% chance of bonus high-value cloned purchase.
891
890
  if (boughtLuckyCharm) {
892
- if (event.event === "real money purchase" && event.price_usd && event.price_usd < 49.99) {
893
- event.price_usd = event.price_usd * 2;
891
+ if (event.event === "real money purchase" && event.price_usd) {
892
+ event.price_usd = Math.round(event.price_usd * 2.5 * 100) / 100;
894
893
  }
895
894
  if (event.event === "item purchased" && chance.bool({ likelihood: 35 })) {
896
895
  const purchaseTemplate = userEvents.find(e => e.event === "real money purchase");
@@ -941,7 +940,7 @@ const config = {
941
940
  const isElite = subscriptionTier === "Elite";
942
941
  if (event.event === "combat completed" && event.outcome !== "Victory") {
943
942
  const winBoost = isElite ? 70 : 50;
944
- if (Math.random() * 100 < winBoost) {
943
+ if (chance.bool({ likelihood: winBoost })) {
945
944
  event.outcome = "Victory";
946
945
  event.loot_gained = true;
947
946
  }
@@ -954,7 +953,7 @@ const config = {
954
953
  if (event.event === "exit dungeon") {
955
954
  if (event.completion_status !== "completed") {
956
955
  const completionBoost = isElite ? 65 : 45;
957
- if (Math.random() * 100 < completionBoost) {
956
+ if (chance.bool({ likelihood: completionBoost })) {
958
957
  event.completion_status = "completed";
959
958
  }
960
959
  }
@@ -968,24 +967,24 @@ const config = {
968
967
  event.treasure_value = Math.floor((event.treasure_value || 50) * treasureBoost);
969
968
  }
970
969
  if (event.event === "player death") {
971
- const survivalChance = isElite ? 50 : 30;
972
- if (Math.random() * 100 < survivalChance) {
970
+ const survivalLikelihood = isElite ? 50 : 30;
971
+ if (chance.bool({ likelihood: survivalLikelihood })) {
973
972
  event.event = "combat completed";
974
973
  event.outcome = "Victory";
975
974
  event.loot_gained = true;
976
975
  }
977
976
  }
978
- if (isElite && Math.random() * 100 < 5) {
977
+ if (isElite && chance.bool({ likelihood: 5 })) {
979
978
  if (event.event === "quest turned in" || event.event === "exit dungeon") {
980
979
  const treasureTemplate = userEvents.find(e => e.event === "find treasure");
981
980
  if (treasureTemplate) {
982
981
  const treasureTypes = ["Rare Artifact", "Gold", "Weapon", "Armor"];
983
982
  userEvents.splice(idx + 1, 0, {
984
983
  ...treasureTemplate,
985
- time: eventTime.add(Math.floor(Math.random() * 26) + 5, 'minutes').toISOString(),
984
+ time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
986
985
  user_id: event.user_id,
987
- treasure_type: treasureTypes[Math.floor(Math.random() * treasureTypes.length)],
988
- treasure_value: Math.floor(Math.random() * 601) + 200,
986
+ treasure_type: chance.pickone(treasureTypes),
987
+ treasure_value: chance.integer({ min: 200, max: 800 }),
989
988
  });
990
989
  }
991
990
  }
@@ -1024,11 +1023,11 @@ const config = {
1024
1023
  // Hook 3 RETENTION + Hook 4 CHURN — early guild-joiners get
1025
1024
  // extra cloned combat events; non-joiners with 3+ deaths in
1026
1025
  // first week lose 70% of post-week events. No flag.
1027
- const shouldChurn = (!joinedGuildEarly && earlyDeaths >= 3) || (earlyDeaths >= 5);
1026
+ const shouldChurn = (!joinedGuildEarly && earlyDeaths >= 2) || (earlyDeaths >= 4);
1028
1027
  if (shouldChurn) {
1029
1028
  const firstWeekEnd = firstEventTime ? firstEventTime.add(7, 'days') : null;
1030
1029
  for (let i = userEvents.length - 1; i >= 0; i--) {
1031
- if (firstWeekEnd && dayjs(userEvents[i].time).isAfter(firstWeekEnd) && chance.bool({ likelihood: 70 })) {
1030
+ if (firstWeekEnd && dayjs(userEvents[i].time).isAfter(firstWeekEnd) && chance.bool({ likelihood: 80 })) {
1032
1031
  userEvents.splice(i, 1);
1033
1032
  }
1034
1033
  }
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-healthcare";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -292,14 +292,16 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
292
292
 
293
293
  /** @type {Config} */
294
294
  const config = {
295
+ version: 2,
295
296
  token,
296
297
  seed: SEED,
297
298
  datasetStart: "2026-01-01T00:00:00Z",
298
- datasetEnd: "2026-04-28T23:59:59Z",
299
+ datasetEnd: "2026-05-01T23:59:59Z",
299
300
  // numDays: num_days,
300
301
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
301
302
  numUsers: num_users,
302
- hasAnonIds: false,
303
+ hasAnonIds: true,
304
+ avgDevicePerUser: 2,
303
305
  hasSessionIds: true,
304
306
  format: "json",
305
307
  gzip: true,
@@ -332,6 +334,7 @@ const config = {
332
334
  event: "account created",
333
335
  weight: 1,
334
336
  isFirstEvent: true,
337
+ isAuthEvent: true,
335
338
  properties: {
336
339
  referral_source: ["organic", "doctor_referral", "insurance_partner", "social_media", "search"],
337
340
  },
@@ -360,7 +363,7 @@ const config = {
360
363
  weight: 5,
361
364
  properties: {
362
365
  doctor_id: chance.pickone.bind(chance, doctorIds),
363
- consultation_mode: ["phone"], // default; feature rollout changes to video
366
+ consultation_mode: ["phone", "phone", "video"],
364
367
  duration_minutes: u.weighNumRange(5, 60, 0.6, 15),
365
368
  consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
366
369
  satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
@@ -394,7 +397,7 @@ const config = {
394
397
  doctor_id: chance.pickone.bind(chance, doctorIds),
395
398
  days_until_followup: u.weighNumRange(3, 30, 0.5, 7),
396
399
  condition_type: ["general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
397
- consultation_mode: ["phone"],
400
+ consultation_mode: ["phone", "phone", "video"],
398
401
  },
399
402
  },
400
403
  {
@@ -845,10 +848,10 @@ const config = {
845
848
  }
846
849
 
847
850
  // HOOK 6: OCCASIONAL PATIENT NO-SHOWS — low-activity patients
848
- // (< 15 events) lose 25% of appointments. No flag.
851
+ // (< 15 events) lose 25% of consultations (they booked but didn't show).
849
852
  if (record.length < 15) {
850
853
  for (let i = record.length - 1; i >= 0; i--) {
851
- if (record[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
854
+ if (record[i].event === "consultation completed" && chance.bool({ likelihood: 25 })) {
852
855
  record.splice(i, 1);
853
856
  }
854
857
  }
@@ -1,6 +1,6 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "dm4-insurance";
3
- const num_days = 100;
3
+ const num_days = 120;
4
4
  const num_users = 15_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
@@ -184,14 +184,16 @@ const chance = u.initChance(SEED);
184
184
 
185
185
  /** @type {Config} */
186
186
  const config = {
187
+ version: 2,
187
188
  token,
188
189
  seed: SEED,
189
190
  datasetStart: "2026-01-01T00:00:00Z",
190
- datasetEnd: "2026-04-28T23:59:59Z",
191
+ datasetEnd: "2026-05-01T23:59:59Z",
191
192
  // numDays: num_days,
192
193
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
193
194
  numUsers: num_users,
194
- hasAnonIds: false,
195
+ hasAnonIds: true,
196
+ avgDevicePerUser: 2,
195
197
  hasSessionIds: true,
196
198
  format: "json",
197
199
  gzip: true,
@@ -225,6 +227,7 @@ const config = {
225
227
  event: "account created",
226
228
  weight: 1,
227
229
  isFirstEvent: true,
230
+ isAuthEvent: true,
228
231
  properties: {
229
232
  signup_source: ["web", "mobile", "agent_referral", "partner"],
230
233
  account_type: ["individual", "family", "business"],