@ak--47/dungeon-master 1.3.0 → 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/array-of-object-lookup.js +0 -2
- 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/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 +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- 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/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 +143 -293
- package/lib/utils/utils.js +97 -52
- 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/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- 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
|
@@ -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
|
|
|
@@ -15,9 +15,6 @@ import * as u from "../../lib/utils/utils.js";
|
|
|
15
15
|
|
|
16
16
|
dayjs.extend(utc);
|
|
17
17
|
const chance = u.initChance(SEED);
|
|
18
|
-
const NOW = dayjs();
|
|
19
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
20
|
-
|
|
21
18
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
22
19
|
|
|
23
20
|
/**
|
|
@@ -28,7 +25,7 @@ const DATASET_START = NOW.subtract(num_days, "days");
|
|
|
28
25
|
* FitQuest — a fitness & wellness app for workout tracking, meal
|
|
29
26
|
* planning, social fitness challenges, and AI coaching.
|
|
30
27
|
*
|
|
31
|
-
* - 5,000 users over
|
|
28
|
+
* - 5,000 users over 120 days, ~600K events
|
|
32
29
|
* - 5 personas: athlete (10%), casual_exerciser (40%),
|
|
33
30
|
* new_year_resolver (25%), social_motivator (15%), coach (10%)
|
|
34
31
|
* - Core loop: sign up → plan workout → complete workout → track progress
|
|
@@ -50,32 +47,31 @@ const DATASET_START = NOW.subtract(num_days, "days");
|
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
49
|
* ═══════════════════════════════════════════════════════════════
|
|
53
|
-
* ANALYTICS HOOKS (
|
|
50
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
54
51
|
* ═══════════════════════════════════════════════════════════════
|
|
55
52
|
*
|
|
53
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
54
|
+
* via raw-prop breakdowns (HOD, day, segment) or behavioral cohorts.
|
|
55
|
+
*
|
|
56
56
|
* ───────────────────────────────────────────────────────────────
|
|
57
|
-
* 1. MORNING WORKOUT BOOST (
|
|
57
|
+
* 1. MORNING WORKOUT BOOST (everything)
|
|
58
58
|
* ───────────────────────────────────────────────────────────────
|
|
59
59
|
*
|
|
60
|
-
* PATTERN: Workouts
|
|
61
|
-
*
|
|
62
|
-
* morning exercise.
|
|
60
|
+
* PATTERN: Workouts in 5-9 UTC get calories_burned 1.3x. Mutates
|
|
61
|
+
* raw prop. No flag.
|
|
63
62
|
*
|
|
64
63
|
* HOW TO FIND IT IN MIXPANEL:
|
|
65
64
|
*
|
|
66
|
-
* Report 1:
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* • Expected: is_morning_workout=true should show ~1.3x avg
|
|
72
|
-
* calories vs false (true ≈ 390, false ≈ 300)
|
|
65
|
+
* Report 1: Avg Calories Burned by Hour of Day
|
|
66
|
+
* - Event: "workout completed"
|
|
67
|
+
* - Measure: Average of "calories_burned"
|
|
68
|
+
* - Breakdown: hour of day
|
|
69
|
+
* - Expected: 5-9 hours show ~ 1.3x baseline
|
|
73
70
|
*
|
|
74
|
-
* REAL-WORLD ANALOGUE:
|
|
75
|
-
* as more effective for fat loss, driving engagement.
|
|
71
|
+
* REAL-WORLD ANALOGUE: Morning workouts get a metabolic boost.
|
|
76
72
|
*
|
|
77
73
|
* ───────────────────────────────────────────────────────────────
|
|
78
|
-
* 2. POST-LAUNCH AI COACHING LIFT (
|
|
74
|
+
* 2. POST-LAUNCH AI COACHING LIFT (everything hook)
|
|
79
75
|
* ───────────────────────────────────────────────────────────────
|
|
80
76
|
*
|
|
81
77
|
* PATTERN: After day 35 (ai_coach feature launch), workouts
|
|
@@ -244,30 +240,84 @@ const DATASET_START = NOW.subtract(num_days, "days");
|
|
|
244
240
|
* REAL-WORLD ANALOGUE: Annual gym memberships have higher
|
|
245
241
|
* utilization — sunk cost + commitment drives consistency.
|
|
246
242
|
*
|
|
243
|
+
* ───────────────────────────────────────────────────────────────
|
|
244
|
+
* 9. WORKOUT LOOP TIME-TO-CONVERT (funnel-post)
|
|
245
|
+
* ───────────────────────────────────────────────────────────────
|
|
246
|
+
*
|
|
247
|
+
* PATTERN: Annual + family subscribers complete the Workout Loop
|
|
248
|
+
* funnel 1.3x faster (factor 0.77); Free users 1.25x slower (factor 1.25).
|
|
249
|
+
*
|
|
250
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
251
|
+
*
|
|
252
|
+
* Report 1: Workout Loop Median Time-to-Convert by Subscription
|
|
253
|
+
* - Funnels > "workout planned" -> "workout completed" -> "progress checked"
|
|
254
|
+
* - Measure: Median time to convert
|
|
255
|
+
* - Breakdown: subscription_plan
|
|
256
|
+
* - Expected: annual ~ 0.77x; free ~ 1.25x
|
|
257
|
+
*
|
|
258
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
259
|
+
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
260
|
+
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
261
|
+
* across the user's full event history.
|
|
262
|
+
*
|
|
263
|
+
* ───────────────────────────────────────────────────────────────
|
|
264
|
+
* 10. WORKOUT-COUNT MAGIC NUMBER (everything)
|
|
265
|
+
* ───────────────────────────────────────────────────────────────
|
|
266
|
+
*
|
|
267
|
+
* PATTERN: Sweet 12-20 workouts/user → +35% on workout
|
|
268
|
+
* duration_minutes (peak progression). Over 21+ → drop 30% of
|
|
269
|
+
* post-day-30 events (overtraining churn). No flag.
|
|
270
|
+
*
|
|
271
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
272
|
+
*
|
|
273
|
+
* Report 1: Avg Workout Duration by Workout-Count Bucket
|
|
274
|
+
* - Cohort A: users with 12-20 "workout completed"
|
|
275
|
+
* - Cohort B: users with 0-11
|
|
276
|
+
* - Event: "workout completed"
|
|
277
|
+
* - Measure: Average of "duration_minutes"
|
|
278
|
+
* - Expected: A ~ 1.35x B
|
|
279
|
+
*
|
|
280
|
+
* Report 2: D30+ Activity on Heavy Workout Cohort
|
|
281
|
+
* - Cohort C: users with >= 21 "workout completed"
|
|
282
|
+
* - Cohort A: users with 12-20
|
|
283
|
+
* - Event: any event
|
|
284
|
+
* - Measure: Total per user, post-day-30
|
|
285
|
+
* - Expected: C ~ 30% fewer post-day-30 events per user
|
|
286
|
+
*
|
|
287
|
+
* REAL-WORLD ANALOGUE: Sweet-spot training drives progression;
|
|
288
|
+
* over-training causes injury and burnout.
|
|
289
|
+
*
|
|
247
290
|
* ═══════════════════════════════════════════════════════════════
|
|
248
291
|
* EXPECTED METRICS SUMMARY
|
|
249
292
|
* ═══════════════════════════════════════════════════════════════
|
|
250
293
|
*
|
|
251
|
-
* Hook | Metric | Baseline | Effect
|
|
252
|
-
*
|
|
253
|
-
* Morning Workout Boost | calories_burned
|
|
254
|
-
* AI Coaching Lift | duration_minutes |
|
|
255
|
-
* Streak Retention | achievements/user |
|
|
256
|
-
* Social Challenge Completion | challenges/user |
|
|
257
|
-
* Resolver Churn Cliff | events after day 14 |
|
|
258
|
-
* Coach Session Quality | satisfaction_score
|
|
259
|
-
* Coach Profile Enrichment | total_workouts | 0 | 350
|
|
260
|
-
* Annual Funnel Lift | funnel conversion | 45% | 63%
|
|
294
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
295
|
+
* ----------------------------|---------------------|----------|-----------|------
|
|
296
|
+
* Morning Workout Boost | calories_burned 5-9 | 1x | 1.3x | 1.3x
|
|
297
|
+
* AI Coaching Lift | duration_minutes | 1x | 1.2x | 1.2x
|
|
298
|
+
* Streak Retention | achievements/user | 1x | 2-3x | 2-3x
|
|
299
|
+
* Social Challenge Completion | challenges/user | 1x | 1.5x | 1.5x
|
|
300
|
+
* Resolver Churn Cliff | events after day 14 | 1x | 0.3x | -70%
|
|
301
|
+
* Coach Session Quality | satisfaction_score | 3.0 | 4.3 | 1.4x
|
|
302
|
+
* Coach Profile Enrichment | total_workouts | 0 | 350 | n/a
|
|
303
|
+
* Annual Funnel Lift | funnel conversion | 45% | 63% | 1.4x
|
|
304
|
+
* Workout Loop T2C | median min by tier | 1x | 0.77/1.25x| ~ 1.6x range
|
|
305
|
+
* Workout Magic Number | sweet duration_min | 1x | 1.35x | 1.35x
|
|
306
|
+
* Workout Magic Number | over D30+ activity | 1x | 0.7x | -30%
|
|
261
307
|
*/
|
|
262
308
|
|
|
263
309
|
/** @type {Config} */
|
|
264
310
|
const config = {
|
|
311
|
+
version: 2,
|
|
265
312
|
token,
|
|
266
313
|
seed: SEED,
|
|
267
|
-
|
|
314
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
315
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
316
|
+
// numDays: num_days,
|
|
268
317
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
269
318
|
numUsers: num_users,
|
|
270
|
-
hasAnonIds:
|
|
319
|
+
hasAnonIds: true,
|
|
320
|
+
avgDevicePerUser: 3,
|
|
271
321
|
hasSessionIds: true,
|
|
272
322
|
format: "json",
|
|
273
323
|
gzip: true,
|
|
@@ -300,6 +350,7 @@ const config = {
|
|
|
300
350
|
event: "account created",
|
|
301
351
|
weight: 1,
|
|
302
352
|
isFirstEvent: true,
|
|
353
|
+
isAuthEvent: true,
|
|
303
354
|
properties: {
|
|
304
355
|
referral_source: ["organic", "friend_invite", "app_store", "social_media", "search"],
|
|
305
356
|
},
|
|
@@ -311,7 +362,6 @@ const config = {
|
|
|
311
362
|
duration_minutes: u.weighNumRange(10, 90, 0.5, 40),
|
|
312
363
|
calories_burned: u.weighNumRange(50, 800, 0.4, 300),
|
|
313
364
|
heart_rate_avg: u.weighNumRange(80, 185, 0.5, 130),
|
|
314
|
-
is_morning_workout: [false],
|
|
315
365
|
satisfaction_score: u.weighNumRange(1, 5, 0.7, 3),
|
|
316
366
|
coaching_mode: ["self_guided"],
|
|
317
367
|
challenge_mode: ["solo"],
|
|
@@ -508,6 +558,7 @@ const config = {
|
|
|
508
558
|
superProps: {
|
|
509
559
|
Platform: ["ios", "ios", "android"],
|
|
510
560
|
workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
|
|
561
|
+
subscription_tier: ["free"],
|
|
511
562
|
},
|
|
512
563
|
|
|
513
564
|
// ── UserProps ──────────────────────────────────────────
|
|
@@ -520,6 +571,7 @@ const config = {
|
|
|
520
571
|
goal: ["weight_loss", "muscle_gain", "endurance", "flexibility", "general_health"],
|
|
521
572
|
Platform: ["ios", "ios", "android"],
|
|
522
573
|
workout_type: ["strength", "cardio", "yoga", "hiit", "running", "cycling"],
|
|
574
|
+
subscription_tier: ["free"],
|
|
523
575
|
},
|
|
524
576
|
|
|
525
577
|
// ── Personas ──────────────────────────────────
|
|
@@ -582,69 +634,6 @@ const config = {
|
|
|
582
634
|
},
|
|
583
635
|
],
|
|
584
636
|
|
|
585
|
-
// ── Subscription ──────────────────────────────
|
|
586
|
-
subscription: {
|
|
587
|
-
plans: [
|
|
588
|
-
{ name: "free", price: 0, default: true },
|
|
589
|
-
{ name: "monthly", price: 12.99, trialDays: 7 },
|
|
590
|
-
{ name: "annual", price: 99.99 },
|
|
591
|
-
{ name: "family", price: 149.99 },
|
|
592
|
-
],
|
|
593
|
-
lifecycle: {
|
|
594
|
-
trialToPayRate: 0.30,
|
|
595
|
-
upgradeRate: 0.08,
|
|
596
|
-
downgradeRate: 0.03,
|
|
597
|
-
churnRate: 0.05,
|
|
598
|
-
winBackRate: 0.10,
|
|
599
|
-
winBackDelay: 21,
|
|
600
|
-
paymentFailureRate: 0.02,
|
|
601
|
-
},
|
|
602
|
-
},
|
|
603
|
-
|
|
604
|
-
// ── Attribution ──────────────────────────────
|
|
605
|
-
attribution: {
|
|
606
|
-
model: "last_touch",
|
|
607
|
-
window: 7,
|
|
608
|
-
campaigns: [
|
|
609
|
-
{
|
|
610
|
-
name: "app_store",
|
|
611
|
-
source: "apple",
|
|
612
|
-
medium: "app_store",
|
|
613
|
-
activeDays: [0, 100],
|
|
614
|
-
dailyBudget: [200, 600],
|
|
615
|
-
acquisitionRate: 0.03,
|
|
616
|
-
userPersonaBias: { athlete: 0.5, coach: 0.3 },
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
name: "google_play",
|
|
620
|
-
source: "google",
|
|
621
|
-
medium: "app_store",
|
|
622
|
-
activeDays: [0, 100],
|
|
623
|
-
dailyBudget: [150, 500],
|
|
624
|
-
acquisitionRate: 0.02,
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
name: "instagram_ads",
|
|
628
|
-
source: "instagram",
|
|
629
|
-
medium: "social",
|
|
630
|
-
activeDays: [5, 90],
|
|
631
|
-
dailyBudget: [200, 800],
|
|
632
|
-
acquisitionRate: 0.03,
|
|
633
|
-
userPersonaBias: { new_year_resolver: 0.5, social_motivator: 0.3 },
|
|
634
|
-
},
|
|
635
|
-
{
|
|
636
|
-
name: "referral_program",
|
|
637
|
-
source: "referral",
|
|
638
|
-
medium: "referral",
|
|
639
|
-
activeDays: [0, 100],
|
|
640
|
-
dailyBudget: [50, 200],
|
|
641
|
-
acquisitionRate: 0.02,
|
|
642
|
-
userPersonaBias: { social_motivator: 0.4 },
|
|
643
|
-
},
|
|
644
|
-
],
|
|
645
|
-
organicRate: 0.30,
|
|
646
|
-
},
|
|
647
|
-
|
|
648
637
|
// ── Engagement Decay ──────────────────────────
|
|
649
638
|
engagementDecay: {
|
|
650
639
|
model: "step",
|
|
@@ -653,76 +642,57 @@ const config = {
|
|
|
653
642
|
reactivationChance: 0.02,
|
|
654
643
|
},
|
|
655
644
|
|
|
656
|
-
// ── Features ──────────────────────────────────
|
|
657
|
-
features: [
|
|
658
|
-
{
|
|
659
|
-
name: "ai_coach",
|
|
660
|
-
launchDay: 35,
|
|
661
|
-
adoptionCurve: "fast",
|
|
662
|
-
property: "coaching_mode",
|
|
663
|
-
values: ["self_guided", "ai_assisted"],
|
|
664
|
-
defaultBefore: "self_guided",
|
|
665
|
-
affectsEvents: ["workout completed", "workout planned"],
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
name: "group_challenges",
|
|
669
|
-
launchDay: 55,
|
|
670
|
-
adoptionCurve: { k: 0.1, midpoint: 25 },
|
|
671
|
-
property: "challenge_mode",
|
|
672
|
-
values: ["solo", "group"],
|
|
673
|
-
defaultBefore: "solo",
|
|
674
|
-
affectsEvents: ["challenge joined", "workout completed"],
|
|
675
|
-
},
|
|
676
|
-
],
|
|
677
|
-
|
|
678
645
|
// ── Hook Function ──────────────────────────────────────
|
|
679
646
|
hook: function (record, type, meta) {
|
|
680
|
-
//
|
|
681
|
-
//
|
|
647
|
+
// HOOK 7: COACH PROFILE ENRICHMENT (user) — coach segment users
|
|
648
|
+
// get high total_workouts + streak_days.
|
|
649
|
+
// Also assign subscription_tier by segment (replacing deprecated subscription feature).
|
|
682
650
|
if (type === "user") {
|
|
683
651
|
if (record.segment === "coach") {
|
|
684
652
|
record.total_workouts = chance.integer({ min: 200, max: 500 });
|
|
685
653
|
record.streak_days = chance.integer({ min: 60, max: 365 });
|
|
686
654
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (record.event === "workout completed") {
|
|
699
|
-
const hour = dayjs(record.time).hour();
|
|
700
|
-
if (hour >= 5 && hour < 9) {
|
|
701
|
-
record.is_morning_workout = true;
|
|
702
|
-
if (record.calories_burned) {
|
|
703
|
-
record.calories_burned = Math.floor(record.calories_burned * 1.3);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
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"]);
|
|
706
666
|
}
|
|
667
|
+
}
|
|
707
668
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
669
|
+
// HOOK 9 (T2C): WORKOUT LOOP TIME-TO-CONVERT (funnel-post)
|
|
670
|
+
// Annual subscribers complete the Workout Loop funnel 1.3x faster
|
|
671
|
+
// (factor 0.77); Free users 1.25x slower (factor 1.25).
|
|
672
|
+
if (type === "funnel-post") {
|
|
673
|
+
const tier = meta?.profile?.subscription_tier;
|
|
674
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
675
|
+
const factor = (
|
|
676
|
+
tier === "annual" || tier === "family" ? 0.77 :
|
|
677
|
+
tier === "free" ? 1.25 :
|
|
678
|
+
1.0
|
|
679
|
+
);
|
|
680
|
+
if (factor !== 1.0) {
|
|
681
|
+
for (let i = 1; i < record.length; i++) {
|
|
682
|
+
const prev = dayjs(record[i - 1].time);
|
|
683
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
684
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
719
685
|
}
|
|
720
686
|
}
|
|
721
687
|
}
|
|
722
688
|
}
|
|
723
689
|
|
|
690
|
+
// (HOOK 1 & 2 moved to everything hook — temporal and hour checks
|
|
691
|
+
// must run after bunchIntoSessions redistributes timestamps)
|
|
692
|
+
|
|
724
693
|
// ── EVERYTHING HOOKS ─────────────────────────────────
|
|
725
694
|
if (type === "everything") {
|
|
695
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
726
696
|
let events = record;
|
|
727
697
|
if (!events.length) return record;
|
|
728
698
|
|
|
@@ -733,19 +703,64 @@ const config = {
|
|
|
733
703
|
events.forEach(e => {
|
|
734
704
|
if (p.Platform) e.Platform = p.Platform;
|
|
735
705
|
if (p.workout_type) e.workout_type = p.workout_type;
|
|
706
|
+
if (p.subscription_tier) e.subscription_tier = p.subscription_tier;
|
|
736
707
|
});
|
|
737
708
|
}
|
|
738
709
|
|
|
710
|
+
// HOOK 1: MORNING WORKOUT BOOST — 5AM-9AM UTC workouts get
|
|
711
|
+
// calories_burned 1.3x. No flag — analyst breaks down by HOD.
|
|
712
|
+
events.forEach(e => {
|
|
713
|
+
if (e.event === "workout completed") {
|
|
714
|
+
const hour = new Date(e.time).getUTCHours();
|
|
715
|
+
if (hour >= 5 && hour < 9 && e.calories_burned) {
|
|
716
|
+
e.calories_burned = Math.floor(e.calories_burned * 1.3);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
});
|
|
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
|
+
|
|
739
757
|
// ── HOOK 8: ANNUAL SUBSCRIBER CONVERSION FILTER ─
|
|
740
758
|
// Free/monthly-tier users drop ~30% of "progress checked"
|
|
741
759
|
// (last step of Workout Loop funnel) to simulate lower conversion.
|
|
742
760
|
if (meta && meta.profile) {
|
|
743
761
|
const tier = meta.profile.subscription_tier;
|
|
744
|
-
if (tier !== "annual" && tier !== "family") {
|
|
745
|
-
record = record.filter(e =>
|
|
746
|
-
if (e.event === "progress checked" && chance.bool({ likelihood: 30 })) return false;
|
|
747
|
-
return true;
|
|
748
|
-
});
|
|
762
|
+
if (tier !== "annual" && tier !== "family" && chance.bool({ likelihood: 30 })) {
|
|
763
|
+
record = record.filter(e => e.event !== "progress checked");
|
|
749
764
|
events = record;
|
|
750
765
|
}
|
|
751
766
|
}
|
|
@@ -780,13 +795,13 @@ const config = {
|
|
|
780
795
|
}
|
|
781
796
|
|
|
782
797
|
// ── HOOK 4: SOCIAL CHALLENGE COMPLETION ──────────
|
|
783
|
-
// Users with friend_added events get 1.5x challenge completions.
|
|
784
|
-
const
|
|
785
|
-
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) {
|
|
786
801
|
const templateChallenge = events.find(e => e.event === "challenge completed");
|
|
787
802
|
if (templateChallenge) {
|
|
788
803
|
const challengeCompletions = events.filter(e => e.event === "challenge completed");
|
|
789
|
-
const extraCount = Math.floor(challengeCompletions.length * 0.5);
|
|
804
|
+
const extraCount = Math.max(1, Math.floor(challengeCompletions.length * 0.5));
|
|
790
805
|
for (let i = 0; i < extraCount; i++) {
|
|
791
806
|
const source = challengeCompletions[i % challengeCompletions.length];
|
|
792
807
|
events.push({
|
|
@@ -801,9 +816,9 @@ const config = {
|
|
|
801
816
|
}
|
|
802
817
|
|
|
803
818
|
// ── HOOK 5: RESOLVER CHURN CLIFF ─────────────────
|
|
804
|
-
// Resolver segment users with <
|
|
805
|
-
if (meta && meta.profile && meta.profile.segment === "resolver" && events.length <
|
|
806
|
-
const CHURN_CLIFF =
|
|
819
|
+
// Resolver segment users with <30 events lose 70% after day 14.
|
|
820
|
+
if (meta && meta.profile && meta.profile.segment === "resolver" && events.length < 30) {
|
|
821
|
+
const CHURN_CLIFF = datasetStart.add(14, "days");
|
|
807
822
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
808
823
|
const eventTime = dayjs(events[i].time);
|
|
809
824
|
if (eventTime.isAfter(CHURN_CLIFF) && chance.bool({ likelihood: 70 })) {
|
|
@@ -812,8 +827,7 @@ const config = {
|
|
|
812
827
|
}
|
|
813
828
|
}
|
|
814
829
|
|
|
815
|
-
//
|
|
816
|
-
// Users with coach_session events get higher satisfaction.
|
|
830
|
+
// HOOK 6: COACH SESSION QUALITY — coach-session satisfaction 4-5.
|
|
817
831
|
const hasCoachSessions = events.some(e => e.event === "coach session");
|
|
818
832
|
if (hasCoachSessions) {
|
|
819
833
|
events.forEach(e => {
|
|
@@ -823,6 +837,26 @@ const config = {
|
|
|
823
837
|
});
|
|
824
838
|
}
|
|
825
839
|
|
|
840
|
+
// HOOK 10: WORKOUT-COUNT MAGIC NUMBER (no flags)
|
|
841
|
+
// Sweet 12-20 workouts → +35% on workout duration_minutes (peak
|
|
842
|
+
// progression). Over 21+ → drop 30% of post-day-30 events
|
|
843
|
+
// (overtraining → churn).
|
|
844
|
+
const workoutCount = events.filter(e => e.event === "workout completed").length;
|
|
845
|
+
if (workoutCount >= 12 && workoutCount <= 20) {
|
|
846
|
+
events.forEach(e => {
|
|
847
|
+
if (e.event === "workout completed" && typeof e.duration_minutes === "number") {
|
|
848
|
+
e.duration_minutes = Math.round(e.duration_minutes * 1.35);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
} else if (workoutCount >= 21) {
|
|
852
|
+
const day30 = datasetStart.add(30, "days");
|
|
853
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
854
|
+
if (dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: 30 })) {
|
|
855
|
+
events.splice(i, 1);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
826
860
|
return record;
|
|
827
861
|
}
|
|
828
862
|
|