@ak--47/dungeon-master 1.3.0 → 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/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +192 -133
- package/dungeons/vertical/community.js +64 -34
- package/dungeons/vertical/crypto.js +333 -224
- package/dungeons/vertical/dating.js +228 -282
- package/dungeons/vertical/devtools.js +164 -62
- package/dungeons/vertical/ecommerce.js +240 -91
- package/dungeons/vertical/education.js +328 -310
- package/dungeons/vertical/fintech.js +437 -311
- package/dungeons/vertical/fitness.js +141 -58
- package/dungeons/vertical/food-delivery.js +298 -318
- package/dungeons/vertical/gaming.js +378 -231
- package/dungeons/vertical/healthcare.js +140 -60
- package/dungeons/vertical/insurance-application.js +168 -73
- package/dungeons/vertical/logistics.js +95 -6
- package/dungeons/vertical/marketplace.js +137 -51
- package/dungeons/vertical/media.js +241 -386
- package/dungeons/vertical/real-estate.js +322 -315
- package/dungeons/vertical/sass.js +253 -263
- package/dungeons/vertical/social.js +262 -206
- package/dungeons/vertical/travel.js +59 -16
- package/index.js +17 -17
- package/lib/core/config-validator.js +93 -21
- 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 +47 -47
- package/lib/utils/utils.js +71 -39
- package/package.json +1 -1
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +38 -15
- 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/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- 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 -2703
- 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/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -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,27 +226,77 @@ 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
|
-
|
|
297
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
298
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
299
|
+
// numDays: num_days,
|
|
250
300
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
251
301
|
numUsers: num_users,
|
|
252
302
|
hasAnonIds: false,
|
|
@@ -303,7 +353,6 @@ const config = {
|
|
|
303
353
|
condition_type: ["general", "general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
304
354
|
wait_time_hours: u.weighNumRange(1, 72, 0.4),
|
|
305
355
|
appointment_type: ["new_patient", "follow_up", "follow_up", "urgent", "routine", "routine"],
|
|
306
|
-
after_hours: [false],
|
|
307
356
|
},
|
|
308
357
|
},
|
|
309
358
|
{
|
|
@@ -316,7 +365,6 @@ const config = {
|
|
|
316
365
|
consultation_fee: u.weighNumRange(25, 200, 0.4, 75),
|
|
317
366
|
satisfaction_score: u.weighNumRange(1, 5, 0.8, 3),
|
|
318
367
|
condition_type: ["general", "general", "respiratory", "dermatology", "mental_health", "chronic", "pediatric"],
|
|
319
|
-
after_hours: [false],
|
|
320
368
|
},
|
|
321
369
|
},
|
|
322
370
|
{
|
|
@@ -577,8 +625,8 @@ const config = {
|
|
|
577
625
|
{ name: "premium", price: 29.99 },
|
|
578
626
|
],
|
|
579
627
|
lifecycle: {
|
|
580
|
-
trialToPayRate: 0.
|
|
581
|
-
upgradeRate: 0.
|
|
628
|
+
trialToPayRate: 0.55,
|
|
629
|
+
upgradeRate: 0.20,
|
|
582
630
|
downgradeRate: 0.03,
|
|
583
631
|
churnRate: 0.05,
|
|
584
632
|
winBackRate: 0.10,
|
|
@@ -655,44 +703,38 @@ const config = {
|
|
|
655
703
|
}
|
|
656
704
|
}
|
|
657
705
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (
|
|
670
|
-
record.
|
|
671
|
-
|
|
672
|
-
|
|
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();
|
|
673
722
|
}
|
|
674
723
|
}
|
|
675
724
|
}
|
|
676
|
-
|
|
677
|
-
// ── HOOK 2: FLU SEASON SPIKE (event) ─────────────
|
|
678
|
-
// Days 50-70: respiratory conditions dominate, wait times double.
|
|
679
|
-
const FLU_START = DATASET_START.add(50, "days");
|
|
680
|
-
const FLU_END = DATASET_START.add(70, "days");
|
|
681
|
-
const eventTime = dayjs(record.time);
|
|
682
|
-
if (record.event === "appointment booked" && eventTime.isAfter(FLU_START) && eventTime.isBefore(FLU_END)) {
|
|
683
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
684
|
-
record.condition_type = "respiratory";
|
|
685
|
-
}
|
|
686
|
-
if (record.condition_type === "respiratory") {
|
|
687
|
-
record.wait_time_hours = Math.floor((record.wait_time_hours || 12) * 2);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
725
|
}
|
|
691
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
|
+
|
|
692
731
|
// ── EVERYTHING HOOKS ─────────────────────────────────
|
|
693
732
|
if (type === "everything") {
|
|
694
733
|
if (!record.length) return record;
|
|
695
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");
|
|
696
738
|
|
|
697
739
|
// ── SUPER-PROP STAMPING ──────────────────────────
|
|
698
740
|
// Stamp superProps from profile so they are consistent per-user.
|
|
@@ -705,17 +747,36 @@ const config = {
|
|
|
705
747
|
});
|
|
706
748
|
}
|
|
707
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
|
+
|
|
708
774
|
// ── HOOK 8: FREE-TIER CONVERSION DROP ────────────
|
|
709
775
|
// Free-tier users lose ~30% of "consultation completed" events
|
|
710
776
|
// (last step of Booking to Consultation funnel), simulating
|
|
711
777
|
// lower conversion for non-paying patients.
|
|
712
|
-
if (profile && profile.subscription_tier === "free") {
|
|
713
|
-
record = record.filter(e =>
|
|
714
|
-
if (e.event === "consultation completed" && chance.bool({ likelihood: 30 })) {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
return true;
|
|
718
|
-
});
|
|
778
|
+
if (profile && profile.subscription_tier === "free" && chance.bool({ likelihood: 30 })) {
|
|
779
|
+
record = record.filter(e => e.event !== "consultation completed");
|
|
719
780
|
}
|
|
720
781
|
|
|
721
782
|
// ── HOOK 3: EXPERIENCED DOCTOR SATISFACTION ──────
|
|
@@ -783,8 +844,8 @@ const config = {
|
|
|
783
844
|
}
|
|
784
845
|
}
|
|
785
846
|
|
|
786
|
-
//
|
|
787
|
-
//
|
|
847
|
+
// HOOK 6: OCCASIONAL PATIENT NO-SHOWS — low-activity patients
|
|
848
|
+
// (< 15 events) lose 25% of appointments. No flag.
|
|
788
849
|
if (record.length < 15) {
|
|
789
850
|
for (let i = record.length - 1; i >= 0; i--) {
|
|
790
851
|
if (record[i].event === "appointment booked" && chance.bool({ likelihood: 25 })) {
|
|
@@ -793,6 +854,25 @@ const config = {
|
|
|
793
854
|
}
|
|
794
855
|
}
|
|
795
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
|
+
|
|
796
876
|
return record;
|
|
797
877
|
}
|
|
798
878
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "dm4-insurance";
|
|
3
3
|
const num_days = 100;
|
|
4
|
-
const num_users =
|
|
4
|
+
const num_users = 15_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -52,97 +52,143 @@ 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
|
-
|
|
189
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
190
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
191
|
+
// numDays: num_days,
|
|
146
192
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
147
193
|
numUsers: num_users,
|
|
148
194
|
hasAnonIds: false,
|
|
@@ -328,10 +374,13 @@ const config = {
|
|
|
328
374
|
"coverage",
|
|
329
375
|
"technical",
|
|
330
376
|
"policy_change",
|
|
377
|
+
"form_crash",
|
|
378
|
+
"login_error",
|
|
379
|
+
"page_timeout",
|
|
380
|
+
"payment_failure",
|
|
331
381
|
],
|
|
332
382
|
priority: ["low", "medium", "medium", "high"],
|
|
333
383
|
channel: ["chat", "phone", "email", "web_form"],
|
|
334
|
-
pre_release_bug: [false],
|
|
335
384
|
},
|
|
336
385
|
},
|
|
337
386
|
{
|
|
@@ -477,12 +526,39 @@ const config = {
|
|
|
477
526
|
* are left intact, making the conversion visibly jump up.
|
|
478
527
|
*/
|
|
479
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
|
+
|
|
480
551
|
// =============================================================
|
|
481
552
|
// Hooks #1-#3 all run in the "everything" hook so that version
|
|
482
553
|
// stamping sees final timestamps (funnel events adjust time
|
|
483
554
|
// AFTER the event hook runs).
|
|
484
555
|
// =============================================================
|
|
485
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");
|
|
486
562
|
const userEvents = record;
|
|
487
563
|
if (userEvents.length === 0) return record;
|
|
488
564
|
|
|
@@ -570,7 +646,6 @@ const config = {
|
|
|
570
646
|
"email",
|
|
571
647
|
"web_form",
|
|
572
648
|
]),
|
|
573
|
-
pre_release_bug: true,
|
|
574
649
|
});
|
|
575
650
|
}
|
|
576
651
|
}
|
|
@@ -602,16 +677,36 @@ const config = {
|
|
|
602
677
|
}
|
|
603
678
|
|
|
604
679
|
// ─── Hook #3: APPLICATION CONVERSION BOOST ───
|
|
605
|
-
// PRE-V2.13: Remove
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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 })) {
|
|
615
710
|
userEvents.splice(i, 1);
|
|
616
711
|
}
|
|
617
712
|
}
|