@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.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/vertical/ai-platform.js +45 -52
- package/dungeons/vertical/community.js +11 -8
- package/dungeons/vertical/crypto.js +25 -24
- package/dungeons/vertical/dating.js +56 -48
- package/dungeons/vertical/devtools.js +25 -18
- package/dungeons/vertical/ecommerce.js +42 -38
- package/dungeons/vertical/education.js +24 -9
- package/dungeons/vertical/fintech.js +13 -8
- package/dungeons/vertical/fitness.js +73 -122
- package/dungeons/vertical/food-delivery.js +18 -19
- package/dungeons/vertical/gaming.js +19 -20
- package/dungeons/vertical/healthcare.js +11 -8
- package/dungeons/vertical/insurance-application.js +6 -3
- package/dungeons/vertical/logistics.js +15 -9
- package/dungeons/vertical/marketplace.js +36 -27
- package/dungeons/vertical/media.js +27 -25
- package/dungeons/vertical/real-estate.js +18 -7
- package/dungeons/vertical/sass.js +84 -68
- package/dungeons/vertical/social.js +46 -47
- package/dungeons/vertical/travel.js +8 -5
- package/lib/core/config-validator.js +136 -157
- package/lib/generators/events.js +49 -93
- package/lib/generators/funnels.js +202 -91
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +119 -269
- package/lib/utils/utils.js +29 -16
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/types.d.ts +397 -211
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-fintech";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
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-
|
|
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:
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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 =
|
|
4
|
-
const num_users =
|
|
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
|
|
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 (
|
|
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-
|
|
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:
|
|
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
|
|
673
|
+
const tier = meta?.profile?.subscription_tier;
|
|
741
674
|
if (Array.isArray(record) && record.length > 1) {
|
|
742
675
|
const factor = (
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
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
|
|
811
|
-
if (
|
|
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
|
|
849
|
-
if (
|
|
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 <
|
|
869
|
-
if (meta && meta.profile && meta.profile.segment === "resolver" && events.length <
|
|
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 =
|
|
4
|
-
const num_users =
|
|
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-
|
|
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:
|
|
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 =
|
|
4
|
-
const num_users =
|
|
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-
|
|
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:
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
972
|
-
if (
|
|
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 &&
|
|
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(
|
|
984
|
+
time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
|
|
986
985
|
user_id: event.user_id,
|
|
987
|
-
treasure_type:
|
|
988
|
-
treasure_value:
|
|
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 >=
|
|
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:
|
|
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 =
|
|
4
|
-
const num_users =
|
|
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-
|
|
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:
|
|
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"
|
|
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
|
|
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 === "
|
|
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 =
|
|
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-
|
|
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:
|
|
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"],
|