@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 = "harness-food";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -16,7 +16,6 @@ import * as v from "ak-tools";
|
|
|
16
16
|
|
|
17
17
|
dayjs.extend(utc);
|
|
18
18
|
const chance = u.initChance(SEED);
|
|
19
|
-
|
|
20
19
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
21
20
|
|
|
22
21
|
/*
|
|
@@ -50,109 +49,208 @@ const chance = u.initChance(SEED);
|
|
|
50
49
|
|
|
51
50
|
/*
|
|
52
51
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
53
|
-
* ANALYTICS HOOKS (
|
|
52
|
+
* ANALYTICS HOOKS (9 hooks)
|
|
53
|
+
*
|
|
54
|
+
* Adds 9. ORDER LIFECYCLE TIME-TO-CONVERT: QuickBite+ 0.74x faster, Free 1.3x
|
|
55
|
+
* slower (funnel-post). Discover via order funnel median TTC by subscription_tier.
|
|
56
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
|
|
57
|
+
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
54
58
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
55
59
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
60
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable only via
|
|
61
|
+
* behavioral cohorts or raw-prop breakdowns (HOD, day, segment).
|
|
62
|
+
*
|
|
63
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
* 1. LUNCH/DINNER RUSH (everything)
|
|
65
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
*
|
|
67
|
+
* PATTERN: 30% of "order delivered" events that fall outside meal-hour
|
|
68
|
+
* windows (11-13 UTC and 17-20 UTC) are dropped, depressing non-meal-time
|
|
69
|
+
* conversion. Mutation: event drop. Discover via order delivered HOD chart.
|
|
70
|
+
*
|
|
71
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
72
|
+
*
|
|
73
|
+
* Report 1: Order Delivered Volume by Hour of Day
|
|
74
|
+
* - Report type: Insights
|
|
75
|
+
* - Event: "order delivered"
|
|
76
|
+
* - Measure: Total
|
|
77
|
+
* - Breakdown: Hour of day
|
|
78
|
+
* - Expected: 11-13 and 17-20 dominate; off-hours visibly suppressed
|
|
79
|
+
*
|
|
80
|
+
* REAL-WORLD ANALOGUE: Meal-hour orders convert at higher rates.
|
|
81
|
+
*
|
|
82
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
* 2. COUPON INJECTION (everything)
|
|
84
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
*
|
|
86
|
+
* PATTERN: Free-tier users get extra "coupon applied" events cloned into the
|
|
87
|
+
* stream near checkout (30% chance per checkout). Cloned with unique offset
|
|
88
|
+
* timestamps. No flag.
|
|
78
89
|
*
|
|
90
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
91
|
+
*
|
|
92
|
+
* Report 1: Coupons per User by Tier
|
|
93
|
+
* - Report type: Insights
|
|
94
|
+
* - Event: "coupon applied"
|
|
95
|
+
* - Measure: Total per user
|
|
96
|
+
* - Breakdown: "subscription_tier"
|
|
97
|
+
* - Expected: Free ~ 1.3x QuickBite+
|
|
98
|
+
*
|
|
99
|
+
* REAL-WORLD ANALOGUE: Free-tier users are the target of coupon promos.
|
|
100
|
+
*
|
|
101
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
* 3. LATE NIGHT MUNCHIES (everything)
|
|
103
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
*
|
|
105
|
+
* PATTERN: 10PM-2AM UTC: 70% of "restaurant viewed" / "item added to cart"
|
|
106
|
+
* events get cuisine_type flipped to American, item_price bumped 1.3x.
|
|
107
|
+
* Mutates existing props. No flag — discover via HOD breakdown.
|
|
108
|
+
*
|
|
109
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
110
|
+
*
|
|
111
|
+
* Report 1: Cuisine Distribution by Hour of Day
|
|
112
|
+
* - Report type: Insights
|
|
113
|
+
* - Event: "restaurant viewed"
|
|
114
|
+
* - Measure: Total
|
|
115
|
+
* - Breakdown: "cuisine_type"
|
|
116
|
+
* - Filter: hour 22-02
|
|
117
|
+
* - Expected: American share spikes
|
|
118
|
+
*
|
|
119
|
+
* Report 2: Avg item_price by Hour of Day
|
|
120
|
+
* - Report type: Insights
|
|
121
|
+
* - Event: "item added to cart"
|
|
122
|
+
* - Measure: Average of "item_price"
|
|
123
|
+
* - Breakdown: Hour of day
|
|
124
|
+
* - Expected: 22-02 hours show ~ 1.3x baseline price
|
|
125
|
+
*
|
|
126
|
+
* REAL-WORLD ANALOGUE: Late-night ordering skews to fast food and impulse buys.
|
|
127
|
+
*
|
|
128
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
79
129
|
* 4. RAINY WEEK SURGE (event + everything)
|
|
80
|
-
*
|
|
81
|
-
* Mixpanel:
|
|
82
|
-
* • Insights (line) → "order placed" → daily → visible spike days 20–27
|
|
83
|
-
* • Insights → "order placed" → avg "delivery_fee"
|
|
84
|
-
* breakdown: "surge_pricing" → 2x higher
|
|
130
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
85
131
|
*
|
|
132
|
+
* PATTERN: Days 20-27, "order placed" delivery_fee doubled and 40% of those
|
|
133
|
+
* events get a duplicate cloned event with unique offset. No flag — discover
|
|
134
|
+
* via line chart by day on order placed volume + delivery_fee average.
|
|
135
|
+
*
|
|
136
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
137
|
+
*
|
|
138
|
+
* Report 1: Order Volume Over Time
|
|
139
|
+
* - Report type: Insights
|
|
140
|
+
* - Event: "order placed"
|
|
141
|
+
* - Measure: Total
|
|
142
|
+
* - Line chart by day
|
|
143
|
+
* - Expected: visible spike days 20-27
|
|
144
|
+
*
|
|
145
|
+
* Report 2: Avg delivery_fee Over Time
|
|
146
|
+
* - Report type: Insights
|
|
147
|
+
* - Event: "order placed"
|
|
148
|
+
* - Measure: Average of "delivery_fee"
|
|
149
|
+
* - Line chart by day
|
|
150
|
+
* - Expected: ~ 2x days 20-27
|
|
151
|
+
*
|
|
152
|
+
* REAL-WORLD ANALOGUE: Weather-driven demand surge with surge pricing.
|
|
153
|
+
*
|
|
154
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
86
155
|
* 5. REFERRAL POWER USERS (everything)
|
|
87
|
-
*
|
|
88
|
-
* Mixpanel:
|
|
89
|
-
* • Insights → "reorder initiated" → per user
|
|
90
|
-
* breakdown: "referral_user" → ~2x more reorders
|
|
91
|
-
* • Insights → "order rated" → avg "food_rating"
|
|
92
|
-
* breakdown: "referral_user" → 4–5 vs ~3.5 baseline
|
|
156
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
93
157
|
*
|
|
158
|
+
* PATTERN: Users with referral_code=true on account-created event get
|
|
159
|
+
* food_rating boosted to 4-5 and 50% of their reorder events cloned (unique
|
|
160
|
+
* offset). Mutates existing prop, no flag.
|
|
161
|
+
*
|
|
162
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
163
|
+
*
|
|
164
|
+
* Report 1: Reorders per User by Referral Cohort
|
|
165
|
+
* - Report type: Insights (with cohort)
|
|
166
|
+
* - Cohort A: users with account-created.referral_code=true
|
|
167
|
+
* - Cohort B: rest
|
|
168
|
+
* - Event: "reorder initiated"
|
|
169
|
+
* - Measure: Total per user
|
|
170
|
+
* - Expected: A ~ 1.5-2x B
|
|
171
|
+
*
|
|
172
|
+
* REAL-WORLD ANALOGUE: Referred users tend to be more loyal.
|
|
173
|
+
*
|
|
174
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
94
175
|
* 6. TRIAL CONVERSION (everything)
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* 8. FIRST ORDER BONUS (funnel-pre)
|
|
111
|
-
* New users get 1.4x conversion boost on checkout→order funnel.
|
|
112
|
-
* Mixpanel:
|
|
113
|
-
* • Funnels → "checkout started" → "order placed" → "order delivered"
|
|
114
|
-
* breakdown: "first_order_bonus" → 1.4x higher conversion
|
|
115
|
-
* • Insights → "order placed" → breakdown "first_order_bonus"
|
|
116
|
-
* → ~50% tagged (hash-based user split)
|
|
176
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
*
|
|
178
|
+
* PATTERN: Users with subscription-started.trial=true who place <3 orders in
|
|
179
|
+
* first 14 days have 60% of post-day-14 events dropped. No flag.
|
|
180
|
+
*
|
|
181
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
182
|
+
*
|
|
183
|
+
* Report 1: Retention by Trial Order Count
|
|
184
|
+
* - Report type: Retention
|
|
185
|
+
* - Cohort A: trial users with >= 3 orders in first 14 days
|
|
186
|
+
* - Cohort B: trial users with < 3
|
|
187
|
+
* - Expected: B sharp activity drop after day 14
|
|
188
|
+
*
|
|
189
|
+
* REAL-WORLD ANALOGUE: Trial users who fail to activate churn fast.
|
|
117
190
|
*
|
|
118
191
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
119
|
-
*
|
|
192
|
+
* 7. FIRST ORDER BONUS (everything)
|
|
120
193
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
121
194
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
195
|
+
* PATTERN: ~50% of users (deterministic by user_id hash) have 30% of their
|
|
196
|
+
* "order delivered" events dropped — simulating that returning users convert
|
|
197
|
+
* worse than new users. No flag — analyst sees segment-level conversion gap
|
|
198
|
+
* via cohort builder by hash bucket.
|
|
199
|
+
*
|
|
200
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
201
|
+
*
|
|
202
|
+
* Report 1: Order Delivered Conversion by First-Letter-Hash
|
|
203
|
+
* - Report type: Funnels
|
|
204
|
+
* - Steps: "checkout started" -> "order placed" -> "order delivered"
|
|
205
|
+
* - Breakdown: derived hash bucket on distinct_id
|
|
206
|
+
* - Expected: half of users show ~ 30% lower conversion on final step
|
|
207
|
+
*
|
|
208
|
+
* REAL-WORLD ANALOGUE: First-order promos lift new-user conversion.
|
|
209
|
+
*
|
|
210
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
* 8. ORDER-COUNT MAGIC NUMBER (everything)
|
|
212
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
*
|
|
214
|
+
* PATTERN: Users in the 4-8 order-placed sweet spot get +40% on order_total.
|
|
215
|
+
* Users with 9+ orders are over-engaged; 35% of their order-placed events
|
|
216
|
+
* drop. No flag — discover by binning users on order count.
|
|
217
|
+
*
|
|
218
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
219
|
+
*
|
|
220
|
+
* Report 1: Avg Order Total by Order-Count Bucket
|
|
221
|
+
* - Report type: Insights (with cohort)
|
|
222
|
+
* - Cohort A: users with 4-8 "order placed"
|
|
223
|
+
* - Cohort B: users with 0-3
|
|
224
|
+
* - Event: "order placed"
|
|
225
|
+
* - Measure: Average of "order_total"
|
|
226
|
+
* - Expected: A ~ 1.4x B
|
|
227
|
+
*
|
|
228
|
+
* Report 2: Orders per User on Heavy Orderers
|
|
229
|
+
* - Report type: Insights (with cohort)
|
|
230
|
+
* - Cohort C: users with >= 9 "order placed"
|
|
231
|
+
* - Cohort A: users with 4-8
|
|
232
|
+
* - Event: "order placed"
|
|
233
|
+
* - Measure: Total per user
|
|
234
|
+
* - Expected: C ~ 35% fewer orders per user vs A
|
|
235
|
+
*
|
|
236
|
+
* REAL-WORLD ANALOGUE: Engaged orderers lift basket size; over-orderers
|
|
237
|
+
* hit fatigue and slow down.
|
|
141
238
|
*
|
|
142
239
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
143
240
|
* EXPECTED METRICS SUMMARY
|
|
144
241
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
145
242
|
*
|
|
146
243
|
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
147
|
-
*
|
|
148
|
-
* Lunch Rush
|
|
149
|
-
* Coupon Injection | Free user coupons
|
|
150
|
-
* Late Night Munchies | American
|
|
151
|
-
* Rainy Week Surge |
|
|
152
|
-
* Referral Power Users |
|
|
153
|
-
* Trial Conversion |
|
|
154
|
-
*
|
|
155
|
-
*
|
|
244
|
+
* ──────────────────────|──────────────────────|----------|-------------|------
|
|
245
|
+
* Lunch/Dinner Rush | off-hour delivered | 1x | 0.7x | -30%
|
|
246
|
+
* Coupon Injection | Free user coupons/u | 1x | ~ 1.3x | 1.3x
|
|
247
|
+
* Late Night Munchies | American share 22-02 | ~ 15% | ~ 60% | 4x
|
|
248
|
+
* Rainy Week Surge | order vol days 20-27 | 1x | ~ 1.4x | 1.4x
|
|
249
|
+
* Referral Power Users | Reorders/user | 1x | ~ 1.5-2x | ~ 1.7x
|
|
250
|
+
* Trial Conversion | post-day-14 activity | 1x | ~ 0.4x | -60%
|
|
251
|
+
* First Order Bonus | returning conversion | 1x | 0.7x | -30%
|
|
252
|
+
* Order-Count Magic Num | sweet order_total | 1x | 1.4x | 1.4x
|
|
253
|
+
* Order-Count Magic Num | over orders/user | 1x | 0.65x | -35%
|
|
156
254
|
*/
|
|
157
255
|
|
|
158
256
|
// Generate consistent IDs for lookup tables and event properties
|
|
@@ -163,12 +261,16 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
|
|
|
163
261
|
|
|
164
262
|
/** @type {Config} */
|
|
165
263
|
const config = {
|
|
264
|
+
version: 2,
|
|
166
265
|
token,
|
|
167
266
|
seed: SEED,
|
|
168
|
-
|
|
267
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
268
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
269
|
+
// numDays: num_days,
|
|
169
270
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
170
271
|
numUsers: num_users,
|
|
171
|
-
hasAnonIds:
|
|
272
|
+
hasAnonIds: true,
|
|
273
|
+
avgDevicePerUser: 2,
|
|
172
274
|
hasSessionIds: true,
|
|
173
275
|
format: "json",
|
|
174
276
|
gzip: true,
|
|
@@ -265,10 +367,10 @@ const config = {
|
|
|
265
367
|
event: "account created",
|
|
266
368
|
weight: 1,
|
|
267
369
|
isFirstEvent: true,
|
|
370
|
+
isAuthEvent: true,
|
|
268
371
|
properties: {
|
|
269
372
|
"signup_method": ["email", "google", "apple", "facebook"],
|
|
270
373
|
"referral_code": [false, false, true],
|
|
271
|
-
"trial_retained": [false],
|
|
272
374
|
}
|
|
273
375
|
},
|
|
274
376
|
{
|
|
@@ -287,7 +389,6 @@ const config = {
|
|
|
287
389
|
],
|
|
288
390
|
"sort_by": ["recommended", "distance", "rating", "price"],
|
|
289
391
|
"filter_applied": [false, false, false, true, true],
|
|
290
|
-
"trial_retained": [false],
|
|
291
392
|
}
|
|
292
393
|
},
|
|
293
394
|
{
|
|
@@ -308,8 +409,6 @@ const config = {
|
|
|
308
409
|
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
309
410
|
"delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
|
|
310
411
|
"price_tier": ["$", "$$", "$$$", "$$$$"],
|
|
311
|
-
"late_night_order": [false],
|
|
312
|
-
"trial_retained": [false],
|
|
313
412
|
}
|
|
314
413
|
},
|
|
315
414
|
{
|
|
@@ -320,8 +419,6 @@ const config = {
|
|
|
320
419
|
"item_category": ["entree", "appetizer", "drink", "dessert", "side"],
|
|
321
420
|
"item_price": u.weighNumRange(3, 65, 1.0, 40),
|
|
322
421
|
"customization_count": u.weighNumRange(0, 5, 1.5, 20),
|
|
323
|
-
"late_night_order": [false],
|
|
324
|
-
"trial_retained": [false],
|
|
325
422
|
}
|
|
326
423
|
},
|
|
327
424
|
{
|
|
@@ -330,7 +427,6 @@ const config = {
|
|
|
330
427
|
properties: {
|
|
331
428
|
"item_id": itemIds,
|
|
332
429
|
"removal_reason": ["changed_mind", "too_expensive", "substitution"],
|
|
333
|
-
"trial_retained": [false],
|
|
334
430
|
}
|
|
335
431
|
},
|
|
336
432
|
{
|
|
@@ -340,8 +436,6 @@ const config = {
|
|
|
340
436
|
"coupon_code": couponCodes,
|
|
341
437
|
"discount_type": ["percent", "flat", "free_delivery"],
|
|
342
438
|
"discount_value": u.weighNumRange(5, 50, 1.2, 20),
|
|
343
|
-
"coupon_injected": [false],
|
|
344
|
-
"trial_retained": [false],
|
|
345
439
|
}
|
|
346
440
|
},
|
|
347
441
|
{
|
|
@@ -351,7 +445,6 @@ const config = {
|
|
|
351
445
|
"cart_total": u.weighNumRange(8, 150, 0.8, 40),
|
|
352
446
|
"items_count": u.weighNumRange(1, 8, 1.2, 20),
|
|
353
447
|
"delivery_address_saved": [false, false, false, true, true, true, true, true, true, true],
|
|
354
|
-
"trial_retained": [false],
|
|
355
448
|
}
|
|
356
449
|
},
|
|
357
450
|
{
|
|
@@ -363,9 +456,6 @@ const config = {
|
|
|
363
456
|
"order_total": u.weighNumRange(10, 200, 0.8, 40),
|
|
364
457
|
"tip_amount": u.weighNumRange(0, 30, 1.5, 20),
|
|
365
458
|
"delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
|
|
366
|
-
"surge_pricing": [false],
|
|
367
|
-
"rainy_week": [false],
|
|
368
|
-
"trial_retained": [false],
|
|
369
459
|
}
|
|
370
460
|
},
|
|
371
461
|
{
|
|
@@ -375,7 +465,6 @@ const config = {
|
|
|
375
465
|
"order_id": orderIds,
|
|
376
466
|
"order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
|
|
377
467
|
"eta_mins": u.weighNumRange(5, 60, 1.0, 30),
|
|
378
|
-
"trial_retained": [false],
|
|
379
468
|
}
|
|
380
469
|
},
|
|
381
470
|
{
|
|
@@ -385,10 +474,6 @@ const config = {
|
|
|
385
474
|
"order_id": orderIds,
|
|
386
475
|
"actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
|
|
387
476
|
"on_time": [false, false, false, true, true, true, true, true, true, true],
|
|
388
|
-
"lunch_rush": [false],
|
|
389
|
-
"dinner_rush": [false],
|
|
390
|
-
"first_order_bonus": [false],
|
|
391
|
-
"trial_retained": [false],
|
|
392
477
|
}
|
|
393
478
|
},
|
|
394
479
|
{
|
|
@@ -399,8 +484,6 @@ const config = {
|
|
|
399
484
|
"food_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
400
485
|
"delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
401
486
|
"would_reorder": [false, true, true],
|
|
402
|
-
"referral_user": [false],
|
|
403
|
-
"trial_retained": [false],
|
|
404
487
|
}
|
|
405
488
|
},
|
|
406
489
|
{
|
|
@@ -414,7 +497,6 @@ const config = {
|
|
|
414
497
|
]),
|
|
415
498
|
"results_count": u.weighNumRange(0, 50, 0.8, 30),
|
|
416
499
|
"search_type": ["restaurant", "cuisine", "dish"],
|
|
417
|
-
"trial_retained": [false],
|
|
418
500
|
}
|
|
419
501
|
},
|
|
420
502
|
{
|
|
@@ -424,7 +506,6 @@ const config = {
|
|
|
424
506
|
"promo_id": () => `promo_${v.uid(5)}`,
|
|
425
507
|
"promo_type": ["banner", "push", "in_feed"],
|
|
426
508
|
"promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
|
|
427
|
-
"trial_retained": [false],
|
|
428
509
|
}
|
|
429
510
|
},
|
|
430
511
|
{
|
|
@@ -434,7 +515,6 @@ const config = {
|
|
|
434
515
|
"plan": ["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"],
|
|
435
516
|
"price": [9.99, 9.99, 79.99],
|
|
436
517
|
"trial": [true, false],
|
|
437
|
-
"trial_retained": [false],
|
|
438
518
|
}
|
|
439
519
|
},
|
|
440
520
|
{
|
|
@@ -443,7 +523,6 @@ const config = {
|
|
|
443
523
|
properties: {
|
|
444
524
|
"reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
|
|
445
525
|
"months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
|
|
446
|
-
"trial_retained": [false],
|
|
447
526
|
}
|
|
448
527
|
},
|
|
449
528
|
{
|
|
@@ -452,7 +531,6 @@ const config = {
|
|
|
452
531
|
properties: {
|
|
453
532
|
"issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
|
|
454
533
|
"order_id": orderIds,
|
|
455
|
-
"trial_retained": [false],
|
|
456
534
|
}
|
|
457
535
|
},
|
|
458
536
|
{
|
|
@@ -461,8 +539,6 @@ const config = {
|
|
|
461
539
|
properties: {
|
|
462
540
|
"order_id": orderIds,
|
|
463
541
|
"original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
|
|
464
|
-
"referral_user": [false],
|
|
465
|
-
"trial_retained": [false],
|
|
466
542
|
}
|
|
467
543
|
},
|
|
468
544
|
],
|
|
@@ -487,8 +563,6 @@ const config = {
|
|
|
487
563
|
"avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
|
|
488
564
|
"orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
|
|
489
565
|
"favorite_restaurant_count": u.weighNumRange(1, 10),
|
|
490
|
-
"is_high_risk": [false],
|
|
491
|
-
"churn_risk_score": [0],
|
|
492
566
|
"Platform": ["iOS", "Android", "Web"],
|
|
493
567
|
"subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
|
|
494
568
|
"city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
|
|
@@ -518,135 +592,36 @@ const config = {
|
|
|
518
592
|
|
|
519
593
|
lookupTables: [],
|
|
520
594
|
|
|
521
|
-
/**
|
|
522
|
-
* ARCHITECTED ANALYTICS HOOKS
|
|
523
|
-
*
|
|
524
|
-
* This hook function creates 8 deliberate patterns in the data:
|
|
525
|
-
*
|
|
526
|
-
* 1. LUNCH RUSH CONVERSION: Orders during lunch/dinner hours convert at higher rates
|
|
527
|
-
* 2. COUPON INJECTION: Free-tier users get coupons spliced into funnels
|
|
528
|
-
* 3. LATE NIGHT MUNCHIES: Late-night orders skew to fast food with inflated prices
|
|
529
|
-
* 4. RAINY WEEK SURGE: Days 20-27 have surge pricing and doubled order volume
|
|
530
|
-
* 5. REFERRAL POWER USERS: Referred users reorder more and rate higher
|
|
531
|
-
* 6. TRIAL CONVERSION: Trial subscribers with 3+ early orders are retained
|
|
532
|
-
* 7. SUPPORT TICKET CHURN: 15% of users flagged as high-risk churners
|
|
533
|
-
* 8. FIRST ORDER BONUS: New users in the Order Completion funnel convert at 90%
|
|
534
|
-
*/
|
|
535
595
|
hook: function (record, type, meta) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Lunch rush: 11AM - 1PM
|
|
553
|
-
if (hour >= 11 && hour <= 13) {
|
|
554
|
-
record.props.lunch_rush = true;
|
|
555
|
-
record.props.dinner_rush = false;
|
|
556
|
-
}
|
|
557
|
-
// Dinner rush: 5PM - 8PM
|
|
558
|
-
else if (hour >= 17 && hour <= 20) {
|
|
559
|
-
record.props.lunch_rush = false;
|
|
560
|
-
record.props.dinner_rush = true;
|
|
561
|
-
} else {
|
|
562
|
-
record.props.lunch_rush = false;
|
|
563
|
-
record.props.dinner_rush = false;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ═══════════════════════════════════════════════════════════════
|
|
568
|
-
// HOOK 8: FIRST ORDER BONUS (funnel-pre)
|
|
569
|
-
// New users (born in dataset) get a massive conversion boost on
|
|
570
|
-
// the Order Completion funnel, simulating first-order promotions
|
|
571
|
-
// and new-user incentives that drive initial purchases.
|
|
572
|
-
// ═══════════════════════════════════════════════════════════════
|
|
573
|
-
if (record.sequence &&
|
|
574
|
-
record.sequence[0] === "checkout started" &&
|
|
575
|
-
record.sequence[1] === "order placed") {
|
|
576
|
-
record.props = record.props || {};
|
|
577
|
-
// Use hash to deterministically assign ~50% of users as "new"
|
|
578
|
-
const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
|
|
579
|
-
const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
|
|
580
|
-
if (isNewUser) {
|
|
581
|
-
record.props.first_order_bonus = true;
|
|
582
|
-
} else {
|
|
583
|
-
record.props.first_order_bonus = false;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// (Hook #2 coupon injection moved to everything hook where
|
|
589
|
-
// meta.profile.subscription_tier is authoritative)
|
|
590
|
-
|
|
591
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
592
|
-
// HOOK 3: LATE NIGHT MUNCHIES (event)
|
|
593
|
-
// Between 10PM and 2AM, restaurant views and cart additions skew
|
|
594
|
-
// heavily toward fast food (American cuisine) with inflated prices,
|
|
595
|
-
// modeling the real-world late-night ordering pattern.
|
|
596
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
597
|
-
if (type === "event") {
|
|
598
|
-
const EVENT_TIME = dayjs(record.time);
|
|
599
|
-
const hour = EVENT_TIME.hour();
|
|
600
|
-
const isLateNight = hour >= 22 || hour <= 2;
|
|
601
|
-
|
|
602
|
-
if (record.event === "restaurant viewed" || record.event === "item added to cart") {
|
|
603
|
-
if (isLateNight) {
|
|
604
|
-
// 70% of late-night browsing/ordering is fast food (American)
|
|
605
|
-
if (chance.bool({ likelihood: 70 })) {
|
|
606
|
-
if (record.cuisine_type !== undefined) {
|
|
607
|
-
record.cuisine_type = "American";
|
|
608
|
-
}
|
|
596
|
+
// HOOK 9 (T2C): ORDER LIFECYCLE TIME-TO-CONVERT (funnel-post)
|
|
597
|
+
// QuickBite+ subscribers complete order funnels 1.35x faster
|
|
598
|
+
// (factor 0.74); Free users 1.3x slower (factor 1.3).
|
|
599
|
+
if (type === "funnel-post") {
|
|
600
|
+
const segment = meta?.profile?.subscription_tier;
|
|
601
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
602
|
+
const factor = (
|
|
603
|
+
segment === "QuickBite+" ? 0.74 :
|
|
604
|
+
segment === "Free" ? 1.3 :
|
|
605
|
+
1.0
|
|
606
|
+
);
|
|
607
|
+
if (factor !== 1.0) {
|
|
608
|
+
for (let i = 1; i < record.length; i++) {
|
|
609
|
+
const prev = dayjs(record[i - 1].time);
|
|
610
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
611
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
609
612
|
}
|
|
610
|
-
|
|
611
|
-
// Boost item price by 1.3x (late-night surcharge effect)
|
|
612
|
-
if (record.item_price !== undefined) {
|
|
613
|
-
record.item_price = Math.round(record.item_price * 1.3 * 100) / 100;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
record.late_night_order = true;
|
|
617
|
-
} else {
|
|
618
|
-
record.late_night_order = false;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// ═══════════════════════════════════════════════════════════════
|
|
623
|
-
// HOOK 4: RAINY WEEK SURGE (event)
|
|
624
|
-
// During days 20-27, delivery fees double on order_placed events
|
|
625
|
-
// and there is a 40% chance of duplicating the event, simulating
|
|
626
|
-
// a weather-driven demand surge with surge pricing.
|
|
627
|
-
// ═══════════════════════════════════════════════════════════════
|
|
628
|
-
if (record.event === "order placed") {
|
|
629
|
-
if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
|
|
630
|
-
record.delivery_fee = (record.delivery_fee || 5) * 2;
|
|
631
|
-
record.surge_pricing = true;
|
|
632
|
-
record.rainy_week = true;
|
|
633
|
-
} else {
|
|
634
|
-
record.surge_pricing = false;
|
|
635
613
|
}
|
|
636
614
|
}
|
|
637
615
|
}
|
|
638
616
|
|
|
639
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
640
|
-
// HOOK 5: REFERRAL POWER USERS (everything)
|
|
641
|
-
// Users who signed up with a referral code get 2x more reorder
|
|
642
|
-
// events and boosted food ratings. Referred users are more loyal
|
|
643
|
-
// and satisfied, mirroring real referral program outcomes.
|
|
644
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
645
617
|
if (type === "everything") {
|
|
618
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
619
|
+
const RAINY_WEEK_START = datasetStart.add(20, 'days');
|
|
620
|
+
const RAINY_WEEK_END = datasetStart.add(27, 'days');
|
|
646
621
|
const userEvents = record;
|
|
647
622
|
const profile = meta.profile;
|
|
648
623
|
|
|
649
|
-
//
|
|
624
|
+
// Stamp superProps from profile so they stay consistent per user
|
|
650
625
|
if (profile) {
|
|
651
626
|
userEvents.forEach((event) => {
|
|
652
627
|
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
@@ -655,10 +630,28 @@ const config = {
|
|
|
655
630
|
});
|
|
656
631
|
}
|
|
657
632
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
633
|
+
// HOOK 3: LATE NIGHT MUNCHIES — 10PM-2AM UTC, 70% of restaurant
|
|
634
|
+
// views/cart additions get cuisine flipped to American, 1.3x
|
|
635
|
+
// item_price. Mutates existing props. No flag — discover via
|
|
636
|
+
// HOD breakdown on cuisine_type / item_price.
|
|
637
|
+
userEvents.forEach(e => {
|
|
638
|
+
if (e.event === "restaurant viewed" || e.event === "item added to cart") {
|
|
639
|
+
const hour = new Date(e.time).getUTCHours();
|
|
640
|
+
const isLateNight = hour >= 22 || hour <= 2;
|
|
641
|
+
if (isLateNight) {
|
|
642
|
+
if (e.cuisine_type !== undefined && chance.bool({ likelihood: 70 })) {
|
|
643
|
+
e.cuisine_type = "American";
|
|
644
|
+
}
|
|
645
|
+
if (e.item_price !== undefined) {
|
|
646
|
+
e.item_price = Math.round(e.item_price * 1.3 * 100) / 100;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// HOOK 2: COUPON INJECTION — Free-tier users get coupon applied
|
|
653
|
+
// events spliced into stream near checkout. Cloned from existing
|
|
654
|
+
// coupon-applied event with unique offset time. No flag.
|
|
662
655
|
if (profile && profile.subscription_tier === "Free") {
|
|
663
656
|
for (let i = userEvents.length - 1; i >= 1; i--) {
|
|
664
657
|
const evt = userEvents[i];
|
|
@@ -681,143 +674,129 @@ const config = {
|
|
|
681
674
|
coupon_code: chance.pickone(couponCodes),
|
|
682
675
|
discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
|
|
683
676
|
discount_value: chance.integer({ min: 10, max: 30 }),
|
|
684
|
-
coupon_injected: true,
|
|
685
677
|
};
|
|
686
678
|
userEvents.splice(i, 0, couponEvent);
|
|
687
679
|
}
|
|
688
680
|
}
|
|
689
681
|
}
|
|
690
682
|
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
683
|
+
// HOOK 1: LUNCH/DINNER RUSH — drop 30% of order delivered events
|
|
684
|
+
// that fall outside the meal hours (11-13 UTC and 17-20 UTC).
|
|
685
|
+
// Discoverable via order delivered breakdown by HOD.
|
|
694
686
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
695
687
|
const event = userEvents[i];
|
|
696
688
|
if (event.event === "order delivered") {
|
|
697
|
-
|
|
698
|
-
|
|
689
|
+
const hour = new Date(event.time).getUTCHours();
|
|
690
|
+
const inRush = (hour >= 11 && hour <= 13) || (hour >= 17 && hour <= 20);
|
|
691
|
+
if (!inRush && chance.bool({ likelihood: 30 })) {
|
|
699
692
|
userEvents.splice(i, 1);
|
|
700
|
-
continue;
|
|
701
693
|
}
|
|
702
|
-
|
|
703
|
-
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// HOOK 8: FIRST ORDER BONUS — hash-based ~50% of users (returning)
|
|
698
|
+
// drop 30% of order delivered events. New users keep all.
|
|
699
|
+
// Discover via cohort builder on hash bucket vs conversion.
|
|
700
|
+
const hashUser = userEvents[0] && userEvents[0].user_id;
|
|
701
|
+
const isNewUser = typeof hashUser === "string" && hashUser.charCodeAt(0) % 2 === 0;
|
|
702
|
+
if (!isNewUser) {
|
|
703
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
704
|
+
if (userEvents[i].event === "order delivered" && chance.bool({ likelihood: 30 })) {
|
|
704
705
|
userEvents.splice(i, 1);
|
|
705
|
-
continue;
|
|
706
706
|
}
|
|
707
707
|
}
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
-
// First pass: identify
|
|
710
|
+
// First pass: identify behavioral patterns (no flags written)
|
|
711
711
|
let isReferralUser = false;
|
|
712
712
|
let hasTrialSubscription = false;
|
|
713
713
|
let earlyOrderCount = 0;
|
|
714
|
-
let
|
|
714
|
+
let orderPlacedCount = 0;
|
|
715
|
+
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
715
716
|
|
|
716
717
|
userEvents.forEach((event) => {
|
|
717
718
|
const eventTime = dayjs(event.time);
|
|
718
719
|
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (event.event === "
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
// Hook #6: Track trial subscribers and early orders
|
|
726
|
-
if (event.event === "subscription started" && event.trial === true) {
|
|
727
|
-
hasTrialSubscription = true;
|
|
728
|
-
}
|
|
729
|
-
if (event.event === "order placed" && daysSinceStart <= 14) {
|
|
730
|
-
earlyOrderCount++;
|
|
720
|
+
if (event.event === "account created" && event.referral_code === true) isReferralUser = true;
|
|
721
|
+
if (event.event === "subscription started" && event.trial === true) hasTrialSubscription = true;
|
|
722
|
+
if (event.event === "order placed") {
|
|
723
|
+
orderPlacedCount++;
|
|
724
|
+
if (daysSinceStart <= 14) earlyOrderCount++;
|
|
731
725
|
}
|
|
732
726
|
});
|
|
733
727
|
|
|
734
|
-
//
|
|
728
|
+
// HOOK 5: REFERRAL POWER USERS — boost food rating to 4-5,
|
|
729
|
+
// clone reorder events. No flag.
|
|
735
730
|
userEvents.forEach((event, idx) => {
|
|
736
|
-
if (event.event === "order rated") {
|
|
737
|
-
|
|
738
|
-
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
739
|
-
event.referral_user = true;
|
|
740
|
-
} else {
|
|
741
|
-
event.referral_user = false;
|
|
742
|
-
}
|
|
731
|
+
if (isReferralUser && event.event === "order rated") {
|
|
732
|
+
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
743
733
|
}
|
|
744
734
|
if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
|
|
745
735
|
const eventTime = dayjs(event.time);
|
|
746
|
-
|
|
736
|
+
userEvents.splice(idx + 1, 0, {
|
|
747
737
|
...event,
|
|
748
738
|
time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
|
|
749
739
|
user_id: event.user_id,
|
|
750
740
|
order_id: chance.pickone(orderIds),
|
|
751
741
|
original_order_age_days: chance.integer({ min: 3, max: 30 }),
|
|
752
|
-
|
|
753
|
-
};
|
|
754
|
-
userEvents.splice(idx + 1, 0, extraReorder);
|
|
742
|
+
});
|
|
755
743
|
}
|
|
756
744
|
});
|
|
757
745
|
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
// are retained. Those with fewer orders churn: 60% of their
|
|
762
|
-
// events after day 14 are removed, simulating subscription
|
|
763
|
-
// abandonment after a failed trial experience.
|
|
764
|
-
// ═══════════════════════════════════════════════════════════════
|
|
765
|
-
if (hasTrialSubscription) {
|
|
746
|
+
// HOOK 6: TRIAL CONVERSION — trial subs with <3 early orders
|
|
747
|
+
// drop 60% of post-day-14 events. No flag.
|
|
748
|
+
if (hasTrialSubscription && earlyOrderCount < 3 && chance.bool({ likelihood: 60 })) {
|
|
766
749
|
const trialCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
767
|
-
|
|
768
|
-
if (earlyOrderCount >= 3) {
|
|
769
|
-
// Retained: mark events as trial_retained
|
|
770
|
-
userEvents.forEach((event) => {
|
|
771
|
-
event.trial_retained = true;
|
|
772
|
-
});
|
|
773
|
-
} else {
|
|
774
|
-
// Churned: remove 60% of events after day 14
|
|
775
|
-
userEvents.forEach((event) => {
|
|
776
|
-
event.trial_retained = false;
|
|
777
|
-
});
|
|
750
|
+
if (trialCutoff) {
|
|
778
751
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
782
|
-
userEvents.splice(i, 1);
|
|
783
|
-
}
|
|
752
|
+
if (dayjs(userEvents[i].time).isAfter(trialCutoff)) {
|
|
753
|
+
userEvents.splice(i, 1);
|
|
784
754
|
}
|
|
785
755
|
}
|
|
786
756
|
}
|
|
787
757
|
}
|
|
788
758
|
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
759
|
+
// HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
|
|
760
|
+
// order placed events. Mutates existing prop. No flag.
|
|
761
|
+
userEvents.forEach(e => {
|
|
762
|
+
if (e.event === "order placed") {
|
|
763
|
+
const t = dayjs(e.time);
|
|
764
|
+
if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END)) {
|
|
765
|
+
e.delivery_fee = (e.delivery_fee || 5) * 2;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// HOOK 4 (cont): RAINY WEEK SURGE — duplicate 40% of order placed
|
|
771
|
+
// events that fall in the rainy window. Cloned with unique offset.
|
|
794
772
|
const rainyDuplicates = [];
|
|
795
773
|
userEvents.forEach((event) => {
|
|
796
|
-
if (event.
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
774
|
+
if (event.event === "order placed") {
|
|
775
|
+
const t = dayjs(event.time);
|
|
776
|
+
if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END) && chance.bool({ likelihood: 40 })) {
|
|
777
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
778
|
+
dup.time = t.add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
|
|
779
|
+
rainyDuplicates.push(dup);
|
|
780
|
+
}
|
|
801
781
|
}
|
|
802
782
|
});
|
|
803
|
-
if (rainyDuplicates.length > 0)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
record.churn_risk_score = chance.integer({ min: 0, max: 40 });
|
|
783
|
+
if (rainyDuplicates.length > 0) userEvents.push(...rainyDuplicates);
|
|
784
|
+
|
|
785
|
+
// HOOK 9: ORDER-COUNT MAGIC NUMBER (no flags)
|
|
786
|
+
// Sweet 4-8 orders → +40% on order_total. Over 9+ → drop 35% of
|
|
787
|
+
// order placed events (oversaturated; analyst sees inverted-U).
|
|
788
|
+
if (orderPlacedCount >= 4 && orderPlacedCount <= 8) {
|
|
789
|
+
userEvents.forEach(e => {
|
|
790
|
+
if (e.event === "order placed" && typeof e.order_total === "number") {
|
|
791
|
+
e.order_total = Math.round(e.order_total * 1.4);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
} else if (orderPlacedCount >= 9) {
|
|
795
|
+
userEvents.forEach(e => {
|
|
796
|
+
if (e.event === "order placed" && typeof e.order_total === "number") {
|
|
797
|
+
e.order_total = Math.round(e.order_total * 0.65);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
821
800
|
}
|
|
822
801
|
}
|
|
823
802
|
|