@ak--47/dungeon-master 1.2.3 → 1.3.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.
- package/CHANGELOG.md +42 -0
- package/README.md +51 -13
- package/dungeons/technical/ad-spend.js +2 -2
- package/dungeons/technical/anonymous-users.js +2 -2
- package/dungeons/technical/array-of-object-lookup.js +2 -4
- package/dungeons/technical/experiments.js +2 -2
- package/dungeons/technical/foobar.js +2 -2
- package/dungeons/technical/group-analytics.js +2 -2
- package/dungeons/technical/mirror-strategies.js +2 -2
- package/dungeons/technical/nested-objects.js +2 -2
- package/dungeons/technical/retention-cadence.js +2 -3
- package/dungeons/technical/sanity.js +2 -2
- package/dungeons/technical/scale-test.js +2 -2
- package/dungeons/technical/scd.js +2 -2
- package/dungeons/technical/simple.js +5 -6
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/simplest.js +2 -2
- package/dungeons/technical/text-generation.js +3 -3
- package/dungeons/vertical/ai-platform.js +858 -0
- package/dungeons/vertical/community.js +84 -40
- package/dungeons/vertical/crypto.js +830 -0
- package/dungeons/vertical/dating.js +744 -0
- package/dungeons/vertical/devtools.js +175 -69
- package/dungeons/vertical/ecommerce.js +242 -94
- package/dungeons/vertical/education.js +330 -313
- package/dungeons/vertical/fintech.js +442 -313
- package/dungeons/vertical/fitness.js +143 -61
- package/dungeons/vertical/food-delivery.js +327 -353
- package/dungeons/vertical/gaming.js +912 -382
- package/dungeons/vertical/healthcare.js +142 -63
- package/dungeons/vertical/insurance-application.js +170 -76
- package/dungeons/vertical/logistics.js +115 -20
- package/dungeons/vertical/marketplace.js +152 -58
- package/dungeons/vertical/media.js +248 -384
- package/dungeons/vertical/real-estate.js +781 -0
- package/dungeons/vertical/sass.js +255 -266
- package/dungeons/vertical/social.js +264 -206
- package/dungeons/vertical/travel.js +117 -41
- package/index.js +17 -17
- package/lib/core/config-validator.js +159 -31
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/orchestrators/user-loop.js +81 -47
- package/lib/templates/macro-presets.js +111 -0
- package/lib/templates/soup-presets.js +19 -36
- package/lib/utils/utils.js +71 -39
- package/package.json +8 -2
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +251 -51
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -438
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/rpg-schema.json +0 -2491
- package/dungeons/vertical/rpg.js +0 -976
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const SEED = "dm4-healthcare";
|
|
3
3
|
const num_days = 100;
|
|
4
4
|
const num_users = 5_000;
|
|
5
|
-
const
|
|
5
|
+
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
|
|
|
16
16
|
|
|
17
17
|
dayjs.extend(utc);
|
|
18
18
|
const chance = u.initChance(SEED);
|
|
19
|
-
const NOW = dayjs();
|
|
20
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
21
|
-
|
|
22
19
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
23
20
|
|
|
24
21
|
// Generate consistent doctor/clinic IDs at module level
|
|
@@ -54,7 +51,10 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
53
|
* ═══════════════════════════════════════════════════════════════
|
|
57
|
-
* ANALYTICS HOOKS (
|
|
54
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
55
|
+
*
|
|
56
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
57
|
+
* via raw-prop breakdowns (HOD, day, tier) or behavioral cohorts.
|
|
58
58
|
* ═══════════════════════════════════════════════════════════════
|
|
59
59
|
*
|
|
60
60
|
* ───────────────────────────────────────────────────────────────
|
|
@@ -70,9 +70,9 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
70
70
|
* • Report type: Insights
|
|
71
71
|
* • Event: "consultation completed"
|
|
72
72
|
* • Measure: Average of "consultation_fee"
|
|
73
|
-
* • Breakdown:
|
|
74
|
-
* • Expected:
|
|
75
|
-
* (
|
|
73
|
+
* • Breakdown: hour of day
|
|
74
|
+
* • Expected: hours 19-06 UTC ~ 1.5x avg fee vs hours 07-18
|
|
75
|
+
* (after-hours ≈ $112, business ≈ $75)
|
|
76
76
|
*
|
|
77
77
|
* REAL-WORLD ANALOGUE: Telehealth platforms charge premiums for
|
|
78
78
|
* after-hours urgent consultations, a key revenue driver.
|
|
@@ -226,28 +226,78 @@ const clinicIds = v.range(1, 25).map(() => `CLINIC_${v.uid(4)}`);
|
|
|
226
226
|
* REAL-WORLD ANALOGUE: Free-tier patients face longer wait times
|
|
227
227
|
* and limited scheduling, reducing completed consultations.
|
|
228
228
|
*
|
|
229
|
+
* ───────────────────────────────────────────────────────────────
|
|
230
|
+
* 9. BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
231
|
+
*
|
|
232
|
+
* PATTERN: Premium tier completes Booking funnel 1.35x faster
|
|
233
|
+
* (factor 0.74); Free tier 1.33x slower (factor 1.33).
|
|
234
|
+
*
|
|
235
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
236
|
+
*
|
|
237
|
+
* Report 1: Booking Funnel Median Time-to-Convert by Tier
|
|
238
|
+
* - Funnels > "symptom search" -> "appointment booked" -> "consultation completed"
|
|
239
|
+
* - Measure: Median time to convert
|
|
240
|
+
* - Breakdown: subscription_tier
|
|
241
|
+
* - Expected: premium ~ 0.74x; free ~ 1.33x
|
|
242
|
+
*
|
|
243
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
244
|
+
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
245
|
+
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
246
|
+
* across the user's full event history.
|
|
247
|
+
*
|
|
248
|
+
* ───────────────────────────────────────────────────────────────
|
|
249
|
+
* 10. CONSULTATION-COUNT MAGIC NUMBER (everything)
|
|
250
|
+
*
|
|
251
|
+
* PATTERN: Sweet 3-6 consultations → +25% on consultation_fee.
|
|
252
|
+
* Over 7+ → days_until_followup multiplied by 1.5 (over-consulted
|
|
253
|
+
* patients wait 50% longer for next visit). No flag.
|
|
254
|
+
*
|
|
255
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
256
|
+
*
|
|
257
|
+
* Report 1: Avg Consultation Fee by Consult-Count Bucket
|
|
258
|
+
* - Cohort A: users with 3-6 "consultation completed"
|
|
259
|
+
* - Cohort B: users with 0-2
|
|
260
|
+
* - Event: "consultation completed"
|
|
261
|
+
* - Measure: Average of "consultation_fee"
|
|
262
|
+
* - Expected: A ~ 1.25x B
|
|
263
|
+
*
|
|
264
|
+
* Report 2: Follow-Up Wait Time on Heavy Consulters
|
|
265
|
+
* - Cohort C: users with >= 7 consultations
|
|
266
|
+
* - Cohort A: users with 3-6
|
|
267
|
+
* - Event: "follow up scheduled"
|
|
268
|
+
* - Measure: Average of "days_until_followup"
|
|
269
|
+
* - Expected: C ~ 1.5x A (longer gap before next visit)
|
|
270
|
+
*
|
|
271
|
+
* REAL-WORLD ANALOGUE: Engaged patients pay more; over-engaged
|
|
272
|
+
* patients hit care-fatigue and stretch the gap to next visit.
|
|
273
|
+
*
|
|
229
274
|
* ═══════════════════════════════════════════════════════════════
|
|
230
275
|
* EXPECTED METRICS SUMMARY
|
|
231
276
|
* ═══════════════════════════════════════════════════════════════
|
|
232
277
|
*
|
|
233
|
-
* Hook | Metric | Baseline | Effect
|
|
234
|
-
*
|
|
235
|
-
* After-Hours Pricing | consultation_fee |
|
|
236
|
-
* Flu Season Spike | respiratory
|
|
237
|
-
* Experienced Doctor Sat. | satisfaction_score
|
|
238
|
-
* Video Follow-Up Lift | follow-ups/user |
|
|
239
|
-
* Chronic Refill Chain | refills (chronic) | 1 | 3-4
|
|
240
|
-
* Occasional No-Shows | booking→consult | 95% | 75%
|
|
241
|
-
* Doctor Specialization | years_experience | 5 | 22
|
|
242
|
-
* Free-Tier Conversion Drop | funnel conversion | 40% | 28%
|
|
278
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
279
|
+
* ----------------------------|---------------------|----------|-----------|------
|
|
280
|
+
* After-Hours Pricing | consultation_fee | 1x | 1.5x | 1.5x
|
|
281
|
+
* Flu Season Spike | respiratory share | ~ 15% | ~ 60% | 4x
|
|
282
|
+
* Experienced Doctor Sat. | satisfaction_score | 3.5 | 4.2 | 1.2x
|
|
283
|
+
* Video Follow-Up Lift | follow-ups/user | 1x | 2x | 2x
|
|
284
|
+
* Chronic Refill Chain | refills (chronic) | 1 | 3-4 | 3-4x
|
|
285
|
+
* Occasional No-Shows | booking→consult | 95% | 75% | -20%
|
|
286
|
+
* Doctor Specialization | years_experience | 5 | 22 | 4.4x
|
|
287
|
+
* Free-Tier Conversion Drop | funnel conversion | 40% | 28% | -30%
|
|
288
|
+
* Booking T2C | median min by tier | 1x | 0.74/1.33x| ~ 1.8x range
|
|
289
|
+
* Consult-Count Magic Number | sweet consult fee | 1x | 1.25x | 1.25x
|
|
290
|
+
* Consult-Count Magic Number | over days_until_fu | 1x | 1.5x | +50%
|
|
243
291
|
*/
|
|
244
292
|
|
|
245
293
|
/** @type {Config} */
|
|
246
294
|
const config = {
|
|
247
295
|
token,
|
|
248
296
|
seed: SEED,
|
|
249
|
-
|
|
250
|
-
|
|
297
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
298
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
299
|
+
// numDays: num_days,
|
|
300
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
251
301
|
numUsers: num_users,
|
|
252
302
|
hasAnonIds: false,
|
|
253
303
|
hasSessionIds: true,
|
|
@@ -262,7 +312,6 @@ const config = {
|
|
|
262
312
|
hasCampaigns: false,
|
|
263
313
|
isAnonymous: false,
|
|
264
314
|
hasAdSpend: false,
|
|
265
|
-
percentUsersBornInDataset: 35,
|
|
266
315
|
hasAvatar: true,
|
|
267
316
|
concurrency: 1,
|
|
268
317
|
writeToDisk: false,
|
|
@@ -304,7 +353,6 @@ const config = {
|
|
|
304
353
|
condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
305
354
|
wait_time_hours: u.weighNumRange(1, 72, 0.4),
|
|
306
355
|
appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
|
|
307
|
-
after_hours: [false],
|
|
308
356
|
},
|
|
309
357
|
},
|
|
310
358
|
{
|
|
@@ -317,7 +365,6 @@ const config = {
|
|
|
317
365
|
consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
|
|
318
366
|
satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
|
|
319
367
|
condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
320
|
-
after_hours: [false],
|
|
321
368
|
},
|
|
322
369
|
},
|
|
323
370
|
{
|
|
@@ -578,8 +625,8 @@ const config = {
|
|
|
578
625
|
{ name: "premium", price: 29.99 },
|
|
579
626
|
],
|
|
580
627
|
lifecycle: {
|
|
581
|
-
trialToPayRate: 0.
|
|
582
|
-
upgradeRate: 0.
|
|
628
|
+
trialToPayRate: 0.55,
|
|
629
|
+
upgradeRate: 0.20,
|
|
583
630
|
downgradeRate: 0.03,
|
|
584
631
|
churnRate: 0.05,
|
|
585
632
|
winBackRate: 0.10,
|
|
@@ -656,44 +703,38 @@ const config = {
|
|
|
656
703
|
}
|
|
657
704
|
}
|
|
658
705
|
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (
|
|
671
|
-
record.
|
|
672
|
-
|
|
673
|
-
|
|
706
|
+
// HOOK 9 (T2C): BOOKING FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
707
|
+
// Premium tier completes Booking funnel 1.35x faster (factor 0.74);
|
|
708
|
+
// Free tier 1.33x slower (factor 1.33).
|
|
709
|
+
if (type === "funnel-post") {
|
|
710
|
+
const segment = meta?.profile?.subscription_tier;
|
|
711
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
712
|
+
const factor = (
|
|
713
|
+
segment === "premium" ? 0.74 :
|
|
714
|
+
segment === "free" ? 1.33 :
|
|
715
|
+
1.0
|
|
716
|
+
);
|
|
717
|
+
if (factor !== 1.0) {
|
|
718
|
+
for (let i = 1; i < record.length; i++) {
|
|
719
|
+
const prev = dayjs(record[i - 1].time);
|
|
720
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
721
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
674
722
|
}
|
|
675
723
|
}
|
|
676
724
|
}
|
|
677
|
-
|
|
678
|
-
// ── HOOK 2: FLU SEASON SPIKE (event) ─────────────
|
|
679
|
-
// Days 50-70: respiratory conditions dominate, wait times double.
|
|
680
|
-
const FLU_START = DATASET_START.add(50, "days");
|
|
681
|
-
const FLU_END = DATASET_START.add(70, "days");
|
|
682
|
-
const eventTime = dayjs(record.time);
|
|
683
|
-
if (record.event === "appointment booked" && eventTime.isAfter(FLU_START) && eventTime.isBefore(FLU_END)) {
|
|
684
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
685
|
-
record.condition_type = "respiratory";
|
|
686
|
-
}
|
|
687
|
-
if (record.condition_type === "respiratory") {
|
|
688
|
-
record.wait_time_hours = Math.floor((record.wait_time_hours || 12) * 2);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
725
|
}
|
|
692
726
|
|
|
727
|
+
// (HOOK 1: AFTER-HOURS SURGE PRICING moved to everything hook — hour checks
|
|
728
|
+
// must run after bunchIntoSessions redistributes timestamps)
|
|
729
|
+
// (HOOK 2: FLU SEASON SPIKE moved to everything hook — same reason)
|
|
730
|
+
|
|
693
731
|
// ── EVERYTHING HOOKS ─────────────────────────────────
|
|
694
732
|
if (type === "everything") {
|
|
695
733
|
if (!record.length) return record;
|
|
696
734
|
const profile = meta.profile;
|
|
735
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
736
|
+
const FLU_START = datasetStart.add(50, "days");
|
|
737
|
+
const FLU_END = datasetStart.add(70, "days");
|
|
697
738
|
|
|
698
739
|
// ── SUPER-PROP STAMPING ──────────────────────────
|
|
699
740
|
// Stamp superProps from profile so they are consistent per-user.
|
|
@@ -706,17 +747,36 @@ const config = {
|
|
|
706
747
|
});
|
|
707
748
|
}
|
|
708
749
|
|
|
750
|
+
// HOOK 1: AFTER-HOURS SURGE PRICING — consultations 7PM-7AM
|
|
751
|
+
// UTC get consultation_fee 1.5x. No flag — discover via HOD chart.
|
|
752
|
+
record.forEach(e => {
|
|
753
|
+
if (e.event === "consultation completed" || e.event === "appointment booked") {
|
|
754
|
+
const hour = new Date(e.time).getUTCHours();
|
|
755
|
+
if ((hour >= 19 || hour < 7) && e.consultation_fee) {
|
|
756
|
+
e.consultation_fee = Math.floor(e.consultation_fee * 1.5);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// HOOK 2: FLU SEASON SPIKE — d50-70 respiratory dominates, wait_time doubles.
|
|
762
|
+
// Runs in everything hook so timestamp checks see post-bunchIntoSessions times.
|
|
763
|
+
record.forEach(e => {
|
|
764
|
+
if (e.event !== "appointment booked") return;
|
|
765
|
+
const t = dayjs(e.time);
|
|
766
|
+
if (t.isAfter(FLU_START) && t.isBefore(FLU_END)) {
|
|
767
|
+
if (chance.bool({ likelihood: 60 })) e.condition_type = "respiratory";
|
|
768
|
+
if (e.condition_type === "respiratory") {
|
|
769
|
+
e.wait_time_hours = Math.floor((e.wait_time_hours || 12) * 2);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
709
774
|
// ── HOOK 8: FREE-TIER CONVERSION DROP ────────────
|
|
710
775
|
// Free-tier users lose ~30% of "consultation completed" events
|
|
711
776
|
// (last step of Booking to Consultation funnel), simulating
|
|
712
777
|
// lower conversion for non-paying patients.
|
|
713
|
-
if (profile && profile.subscription_tier === "free") {
|
|
714
|
-
record = record.filter(e =>
|
|
715
|
-
if (e.event === "consultation completed" && chance.bool({ likelihood: 30 })) {
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
return true;
|
|
719
|
-
});
|
|
778
|
+
if (profile && profile.subscription_tier === "free" && chance.bool({ likelihood: 30 })) {
|
|
779
|
+
record = record.filter(e => e.event !== "consultation completed");
|
|
720
780
|
}
|
|
721
781
|
|
|
722
782
|
// ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
|
|
@@ -784,8 +844,8 @@ const config = {
|
|
|
784
844
|
}
|
|
785
845
|
}
|
|
786
846
|
|
|
787
|
-
//
|
|
788
|
-
//
|
|
847
|
+
// HOOK 6: OCCASIONAL PATIENT NO-SHOWS — low-activity patients
|
|
848
|
+
// (< 15 events) lose 25% of appointments. No flag.
|
|
789
849
|
if (record.length < 15) {
|
|
790
850
|
for (let i = record.length - 1; i >= 0; i--) {
|
|
791
851
|
if (record[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
|
|
@@ -794,6 +854,25 @@ const config = {
|
|
|
794
854
|
}
|
|
795
855
|
}
|
|
796
856
|
|
|
857
|
+
// HOOK 10: CONSULTATION-COUNT MAGIC NUMBER (no flags)
|
|
858
|
+
// Sweet 3-6 consultations → +25% on consultation_fee. Over 7+ →
|
|
859
|
+
// drop 30% of "follow up scheduled" events (over-consulted →
|
|
860
|
+
// follow-up fatigue).
|
|
861
|
+
const consultCt = record.filter(e => e.event === "consultation completed").length;
|
|
862
|
+
if (consultCt >= 3 && consultCt <= 6) {
|
|
863
|
+
record.forEach(e => {
|
|
864
|
+
if (e.event === "consultation completed" && typeof e.consultation_fee === "number") {
|
|
865
|
+
e.consultation_fee = Math.round(e.consultation_fee * 1.25);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
} else if (consultCt >= 7) {
|
|
869
|
+
record.forEach(e => {
|
|
870
|
+
if (e.event === "follow up scheduled" && typeof e.days_until_followup === "number") {
|
|
871
|
+
e.days_until_followup = Math.round(e.days_until_followup * 1.5);
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
797
876
|
return record;
|
|
798
877
|
}
|
|
799
878
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "dm4-insurance";
|
|
3
3
|
const num_days = 100;
|
|
4
|
-
const num_users =
|
|
5
|
-
const
|
|
4
|
+
const num_users = 15_000;
|
|
5
|
+
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
@@ -52,98 +52,144 @@ const chance = u.initChance(SEED);
|
|
|
52
52
|
|
|
53
53
|
/*
|
|
54
54
|
* ===================================================================
|
|
55
|
-
* ANALYTICS HOOKS
|
|
55
|
+
* ANALYTICS HOOKS (5 hooks)
|
|
56
|
+
*
|
|
57
|
+
* Adds 5. APPLICATION COMPLETION TIME-TO-CONVERT: business 0.74x faster,
|
|
58
|
+
* family 1.3x slower (funnel-post). Discover via funnel median TTC by account_type.
|
|
59
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
|
|
60
|
+
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
56
61
|
* ===================================================================
|
|
57
62
|
*
|
|
63
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
64
|
+
* via raw-prop breakdowns (date, app_version, issue_category) or
|
|
65
|
+
* behavioral cohorts.
|
|
66
|
+
*
|
|
58
67
|
* -------------------------------------------------------------------
|
|
59
|
-
* 1. VERSION STAMPING (everything
|
|
68
|
+
* 1. VERSION STAMPING (everything)
|
|
60
69
|
* -------------------------------------------------------------------
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* All users shift simultaneously on release dates:
|
|
70
|
+
*
|
|
71
|
+
* PATTERN: Every event gets a deterministic app_version based on its
|
|
72
|
+
* final timestamp. All users shift simultaneously on release dates:
|
|
65
73
|
* - Days 0-30: v2.10
|
|
66
74
|
* - Days 30-60: v2.11
|
|
67
75
|
* - Days 60-90: v2.12
|
|
68
76
|
* - Last 10 days: v2.13
|
|
77
|
+
* app_version is a real product property already in superProps.
|
|
78
|
+
*
|
|
79
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
80
|
+
*
|
|
81
|
+
* Report 1: Event Volume by App Version
|
|
82
|
+
* - Report type: Insights
|
|
83
|
+
* - Event: "page viewed"
|
|
84
|
+
* - Measure: Total
|
|
85
|
+
* - Breakdown: "app_version"
|
|
86
|
+
* - Line chart by day
|
|
87
|
+
* - Expected: clean cutovers between versions, no overlap
|
|
69
88
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* 2. Chart event volume over time, colored by app_version
|
|
73
|
-
* 3. Confirm: no overlap between versions (deterministic cutover)
|
|
89
|
+
* REAL-WORLD ANALOGUE: Forced auto-update SaaS apps cut all users over
|
|
90
|
+
* on release day, producing crisp version bands.
|
|
74
91
|
*
|
|
75
92
|
* -------------------------------------------------------------------
|
|
76
|
-
* 2. SUPPORT TICKET VOLUME DROP (everything
|
|
93
|
+
* 2. SUPPORT TICKET VOLUME DROP (everything)
|
|
77
94
|
* -------------------------------------------------------------------
|
|
78
|
-
* Before v2.13, support ticket volume is high — each user gets 2-3
|
|
79
|
-
* extra tickets injected with bug-related categories (form_crash,
|
|
80
|
-
* login_error, page_timeout, payment_failure). After v2.13, tickets
|
|
81
|
-
* are progressively removed (30% on day 1 → 85% on day 10).
|
|
82
95
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
96
|
+
* PATTERN: Pre-v2.13: each user gets 2-3 extra cloned support-ticket
|
|
97
|
+
* created events with bug-related issue_category values (form_crash,
|
|
98
|
+
* login_error, page_timeout, payment_failure — added to event config).
|
|
99
|
+
* Post-v2.13: tickets progressively removed (30% day 1 → 85% day 10).
|
|
100
|
+
* No flag — discover via issue_category breakdown over time.
|
|
101
|
+
*
|
|
102
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
103
|
+
*
|
|
104
|
+
* Report 1: Ticket Volume Over Time
|
|
105
|
+
* - Report type: Insights
|
|
106
|
+
* - Event: "support ticket created"
|
|
107
|
+
* - Measure: Total
|
|
108
|
+
* - Breakdown: "app_version"
|
|
109
|
+
* - Line chart by day
|
|
110
|
+
* - Expected: high volume on v2.12, sharp drop on v2.13
|
|
111
|
+
*
|
|
112
|
+
* Report 2: Bug Category Share Pre vs Post v2.13
|
|
113
|
+
* - Report type: Insights
|
|
114
|
+
* - Event: "support ticket created"
|
|
115
|
+
* - Measure: Total
|
|
116
|
+
* - Breakdown: "issue_category"
|
|
117
|
+
* - Compare pre-v2.13 vs v2.13 date ranges
|
|
118
|
+
* - Expected: form_crash / login_error / page_timeout / payment_failure
|
|
119
|
+
* dominate pre-v2.13, vanish post-v2.13
|
|
120
|
+
*
|
|
121
|
+
* REAL-WORLD ANALOGUE: A UX-fix release reduces ticket volume overnight.
|
|
89
122
|
*
|
|
90
123
|
* -------------------------------------------------------------------
|
|
91
|
-
* 3. APPLICATION CONVERSION BOOST (everything
|
|
124
|
+
* 3. APPLICATION CONVERSION BOOST (everything)
|
|
92
125
|
* -------------------------------------------------------------------
|
|
93
|
-
* Before v2.13, 40% of "application approved" and "policy activated"
|
|
94
|
-
* events are removed, simulating a broken application flow. After
|
|
95
|
-
* v2.13, all events are preserved — creating a visible conversion jump.
|
|
96
126
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* 3. Compare conversion rates: pre-v2.13 ~60% of post-v2.13
|
|
101
|
-
* 4. Insights > "application approved" count over time — step change
|
|
127
|
+
* PATTERN: Pre-v2.13: ~95% of users have ALL their application approved +
|
|
128
|
+
* policy activated events dropped (per-user gating). Post-v2.13: kept.
|
|
129
|
+
* No flag — discover via funnel by app_version or volume line chart over time.
|
|
102
130
|
*
|
|
103
|
-
*
|
|
104
|
-
* EXPECTED METRICS SUMMARY
|
|
105
|
-
* ===================================================================
|
|
131
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
106
132
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
133
|
+
* Report 1: Application Funnel by Version
|
|
134
|
+
* - Report type: Funnels
|
|
135
|
+
* - Steps: "application submitted" -> "application approved" -> "policy activated"
|
|
136
|
+
* - Breakdown: "app_version"
|
|
137
|
+
* - Expected: post-v2.13 ~ 1.3x pre-v2.13 conversion rate
|
|
138
|
+
* (~58% v2.13 vs ~44% v2.12; some dilution because users span versions)
|
|
112
139
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
140
|
+
* REAL-WORLD ANALOGUE: A buggy multi-step form fixed by release.
|
|
141
|
+
*
|
|
142
|
+
* -------------------------------------------------------------------
|
|
143
|
+
* 4. APPLICATION-STEP MAGIC NUMBER (everything)
|
|
144
|
+
* -------------------------------------------------------------------
|
|
145
|
+
*
|
|
146
|
+
* PATTERN: Users with 8-14 application step completed events sit in the
|
|
147
|
+
* sweet spot — approved_premium boosted +35%. Users with 15+ steps
|
|
148
|
+
* are over-engaged (likely fraud or signal review); 40% of their
|
|
149
|
+
* application approved events drop. No flag.
|
|
116
150
|
*
|
|
117
|
-
*
|
|
118
|
-
* application conversion by app_version to show v2.13's dual impact.
|
|
151
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
119
152
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
153
|
+
* Report 1: Avg Approved Premium by Step Bucket
|
|
154
|
+
* - Report type: Insights (with cohort)
|
|
155
|
+
* - Cohort A: users with 8-14 "application step completed"
|
|
156
|
+
* - Cohort B: users with 0-7
|
|
157
|
+
* - Event: "application approved"
|
|
158
|
+
* - Measure: Average of "approved_premium"
|
|
159
|
+
* - Expected: A ~ 1.35x B
|
|
123
160
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
161
|
+
* Report 2: Approvals per User on Heavy Step-Completers
|
|
162
|
+
* - Report type: Insights (with cohort)
|
|
163
|
+
* - Cohort C: users with >= 15 "application step completed"
|
|
164
|
+
* - Cohort A: users with 8-14
|
|
165
|
+
* - Event: "application approved"
|
|
166
|
+
* - Measure: Total per user
|
|
167
|
+
* - Expected: C ~ 40% fewer approvals per user vs A
|
|
126
168
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
169
|
+
* REAL-WORLD ANALOGUE: Engaged applicants get higher premiums approved;
|
|
170
|
+
* over-engaged ones look like fraud and get flagged.
|
|
171
|
+
*
|
|
172
|
+
* ===================================================================
|
|
173
|
+
* EXPECTED METRICS SUMMARY
|
|
174
|
+
* ===================================================================
|
|
129
175
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
176
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
177
|
+
* ------------------------|-------------------------|----------|---------|------
|
|
178
|
+
* Version Stamping | Events per version | n/a | clean | bands
|
|
179
|
+
* Support Ticket Volume | tickets v2.12 -> v2.13 | 1x | ~ 0.3x | -70%
|
|
180
|
+
* Application Conversion | approval rate pre/post | 1x | ~ 1.3x | step-up
|
|
181
|
+
* Step-Count Magic Number | sweet approved_premium | 1x | 1.35x | 1.35x
|
|
182
|
+
* Step-Count Magic Number | over approvals/user | 1x | 0.6x | -40%
|
|
132
183
|
*/
|
|
133
184
|
|
|
134
|
-
// ── Time constants for hook calculations ──
|
|
135
|
-
const NOW = dayjs();
|
|
136
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
137
|
-
const V211_DATE = DATASET_START.add(30, "days");
|
|
138
|
-
const V212_DATE = DATASET_START.add(60, "days");
|
|
139
|
-
const V213_DATE = NOW.subtract(10, "days");
|
|
140
|
-
|
|
141
185
|
/** @type {Config} */
|
|
142
186
|
const config = {
|
|
143
187
|
token,
|
|
144
188
|
seed: SEED,
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
190
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
191
|
+
// numDays: num_days,
|
|
192
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
147
193
|
numUsers: num_users,
|
|
148
194
|
hasAnonIds: false,
|
|
149
195
|
hasSessionIds: true,
|
|
@@ -158,7 +204,6 @@ const config = {
|
|
|
158
204
|
hasCampaigns: false,
|
|
159
205
|
isAnonymous: false,
|
|
160
206
|
hasAdSpend: false,
|
|
161
|
-
percentUsersBornInDataset: 35,
|
|
162
207
|
hasAvatar: true,
|
|
163
208
|
concurrency: 1,
|
|
164
209
|
writeToDisk: false,
|
|
@@ -329,10 +374,13 @@ const config = {
|
|
|
329
374
|
"coverage",
|
|
330
375
|
"technical",
|
|
331
376
|
"policy_change",
|
|
377
|
+
"form_crash",
|
|
378
|
+
"login_error",
|
|
379
|
+
"page_timeout",
|
|
380
|
+
"payment_failure",
|
|
332
381
|
],
|
|
333
382
|
priority: ["low", "medium", "medium", "high"],
|
|
334
383
|
channel: ["chat", "phone", "email", "web_form"],
|
|
335
|
-
pre_release_bug: [false],
|
|
336
384
|
},
|
|
337
385
|
},
|
|
338
386
|
{
|
|
@@ -478,12 +526,39 @@ const config = {
|
|
|
478
526
|
* are left intact, making the conversion visibly jump up.
|
|
479
527
|
*/
|
|
480
528
|
hook: function (record, type, meta) {
|
|
529
|
+
// HOOK 5 (T2C): APPLICATION COMPLETION TIME-TO-CONVERT (funnel-post)
|
|
530
|
+
// Business accounts complete the Application Completion funnel 1.35x
|
|
531
|
+
// faster (factor 0.74); family accounts 1.3x slower (factor 1.3).
|
|
532
|
+
if (type === "funnel-post") {
|
|
533
|
+
const profile = meta?.profile;
|
|
534
|
+
const accountType = profile && profile.account_type;
|
|
535
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
536
|
+
const factor = (
|
|
537
|
+
accountType === "business" ? 0.74 :
|
|
538
|
+
accountType === "family" ? 1.3 :
|
|
539
|
+
1.0
|
|
540
|
+
);
|
|
541
|
+
if (factor !== 1.0) {
|
|
542
|
+
for (let i = 1; i < record.length; i++) {
|
|
543
|
+
const prev = dayjs(record[i - 1].time);
|
|
544
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
545
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
481
551
|
// =============================================================
|
|
482
552
|
// Hooks #1-#3 all run in the "everything" hook so that version
|
|
483
553
|
// stamping sees final timestamps (funnel events adjust time
|
|
484
554
|
// AFTER the event hook runs).
|
|
485
555
|
// =============================================================
|
|
486
556
|
if (type === "everything") {
|
|
557
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
558
|
+
const datasetEnd = dayjs.unix(meta.datasetEnd);
|
|
559
|
+
const V211_DATE = datasetStart.add(30, "days");
|
|
560
|
+
const V212_DATE = datasetStart.add(60, "days");
|
|
561
|
+
const V213_DATE = datasetEnd.subtract(10, "days");
|
|
487
562
|
const userEvents = record;
|
|
488
563
|
if (userEvents.length === 0) return record;
|
|
489
564
|
|
|
@@ -571,7 +646,6 @@ const config = {
|
|
|
571
646
|
"email",
|
|
572
647
|
"web_form",
|
|
573
648
|
]),
|
|
574
|
-
pre_release_bug: true,
|
|
575
649
|
});
|
|
576
650
|
}
|
|
577
651
|
}
|
|
@@ -603,16 +677,36 @@ const config = {
|
|
|
603
677
|
}
|
|
604
678
|
|
|
605
679
|
// ─── Hook #3: APPLICATION CONVERSION BOOST ───
|
|
606
|
-
// PRE-V2.13: Remove
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
680
|
+
// PRE-V2.13: Remove ALL application approved + policy activated events
|
|
681
|
+
// for ~80% of users (per-user gating, not per-event). This produces
|
|
682
|
+
// visible funnel completion gap pre-v2.13 vs post-v2.13.
|
|
683
|
+
if (chance.bool({ likelihood: 95 })) {
|
|
684
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
685
|
+
const evt = userEvents[i];
|
|
686
|
+
if (
|
|
687
|
+
(evt.event === "application approved" ||
|
|
688
|
+
evt.event === "policy activated") &&
|
|
689
|
+
dayjs(evt.time).isBefore(V213_DATE)
|
|
690
|
+
) {
|
|
691
|
+
userEvents.splice(i, 1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Hook 4: APPLICATION-STEP MAGIC NUMBER (no flags)
|
|
697
|
+
// Sweet 8-14 application step completed → +35% on approved_premium
|
|
698
|
+
// for application approved events. Over 15+ → drop 40% of
|
|
699
|
+
// application approved events (fraud filter triggers).
|
|
700
|
+
const stepCount = userEvents.filter(e => e.event === "application step completed").length;
|
|
701
|
+
if (stepCount >= 8 && stepCount <= 14) {
|
|
702
|
+
userEvents.forEach(e => {
|
|
703
|
+
if (e.event === "application approved" && typeof e.approved_premium === "number") {
|
|
704
|
+
e.approved_premium = Math.round(e.approved_premium * 1.35);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
} else if (stepCount >= 15) {
|
|
708
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
709
|
+
if (userEvents[i].event === "application approved" && chance.bool({ likelihood: 40 })) {
|
|
616
710
|
userEvents.splice(i, 1);
|
|
617
711
|
}
|
|
618
712
|
}
|