@ak--47/dungeon-master 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +51 -13
- package/dungeons/technical/ad-spend.js +2 -2
- package/dungeons/technical/anonymous-users.js +2 -2
- package/dungeons/technical/array-of-object-lookup.js +2 -4
- package/dungeons/technical/experiments.js +2 -2
- package/dungeons/technical/foobar.js +2 -2
- package/dungeons/technical/group-analytics.js +2 -2
- package/dungeons/technical/mirror-strategies.js +2 -2
- package/dungeons/technical/nested-objects.js +2 -2
- package/dungeons/technical/retention-cadence.js +2 -3
- package/dungeons/technical/sanity.js +2 -2
- package/dungeons/technical/scale-test.js +2 -2
- package/dungeons/technical/scd.js +2 -2
- package/dungeons/technical/simple.js +5 -6
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/simplest.js +2 -2
- package/dungeons/technical/text-generation.js +3 -3
- package/dungeons/vertical/ai-platform.js +858 -0
- package/dungeons/vertical/community.js +84 -40
- package/dungeons/vertical/crypto.js +830 -0
- package/dungeons/vertical/dating.js +744 -0
- package/dungeons/vertical/devtools.js +175 -69
- package/dungeons/vertical/ecommerce.js +242 -94
- package/dungeons/vertical/education.js +330 -313
- package/dungeons/vertical/fintech.js +442 -313
- package/dungeons/vertical/fitness.js +143 -61
- package/dungeons/vertical/food-delivery.js +327 -353
- package/dungeons/vertical/gaming.js +912 -382
- package/dungeons/vertical/healthcare.js +142 -63
- package/dungeons/vertical/insurance-application.js +170 -76
- package/dungeons/vertical/logistics.js +115 -20
- package/dungeons/vertical/marketplace.js +152 -58
- package/dungeons/vertical/media.js +248 -384
- package/dungeons/vertical/real-estate.js +781 -0
- package/dungeons/vertical/sass.js +255 -266
- package/dungeons/vertical/social.js +264 -206
- package/dungeons/vertical/travel.js +117 -41
- package/index.js +17 -17
- package/lib/core/config-validator.js +159 -31
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/orchestrators/user-loop.js +81 -47
- package/lib/templates/macro-presets.js +111 -0
- package/lib/templates/soup-presets.js +19 -36
- package/lib/utils/utils.js +71 -39
- package/package.json +8 -2
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +251 -51
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -438
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/rpg-schema.json +0 -2491
- package/dungeons/vertical/rpg.js +0 -976
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const SEED = "harness-food";
|
|
3
3
|
const num_days = 100;
|
|
4
4
|
const num_users = 5_000;
|
|
5
|
-
const
|
|
5
|
+
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
@@ -16,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
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
130
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
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
|
|
85
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
156
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
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:
|
|
93
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
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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.
|
|
190
|
+
*
|
|
191
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
* 7. FIRST ORDER BONUS (everything)
|
|
193
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
*
|
|
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.
|
|
117
209
|
*
|
|
118
210
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
119
|
-
*
|
|
211
|
+
* 8. ORDER-COUNT MAGIC NUMBER (everything)
|
|
120
212
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
121
213
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* Cohort
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|
|
@@ -165,8 +263,10 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
|
|
|
165
263
|
const config = {
|
|
166
264
|
token,
|
|
167
265
|
seed: SEED,
|
|
168
|
-
|
|
169
|
-
|
|
266
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
267
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
268
|
+
// numDays: num_days,
|
|
269
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
170
270
|
numUsers: num_users,
|
|
171
271
|
hasAnonIds: false,
|
|
172
272
|
hasSessionIds: true,
|
|
@@ -181,7 +281,6 @@ const config = {
|
|
|
181
281
|
hasCampaigns: false,
|
|
182
282
|
isAnonymous: false,
|
|
183
283
|
hasAdSpend: false,
|
|
184
|
-
percentUsersBornInDataset: 50,
|
|
185
284
|
hasAvatar: true,
|
|
186
285
|
concurrency: 1,
|
|
187
286
|
writeToDisk: false,
|
|
@@ -269,7 +368,6 @@ const config = {
|
|
|
269
368
|
properties: {
|
|
270
369
|
"signup_method": ["email", "google", "apple", "facebook"],
|
|
271
370
|
"referral_code": [false, false, true],
|
|
272
|
-
"trial_retained": [false],
|
|
273
371
|
}
|
|
274
372
|
},
|
|
275
373
|
{
|
|
@@ -288,7 +386,6 @@ const config = {
|
|
|
288
386
|
],
|
|
289
387
|
"sort_by": ["recommended", "distance", "rating", "price"],
|
|
290
388
|
"filter_applied": [false, false, false, true, true],
|
|
291
|
-
"trial_retained": [false],
|
|
292
389
|
}
|
|
293
390
|
},
|
|
294
391
|
{
|
|
@@ -309,8 +406,6 @@ const config = {
|
|
|
309
406
|
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
310
407
|
"delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
|
|
311
408
|
"price_tier": ["$", "$$", "$$$", "$$$$"],
|
|
312
|
-
"late_night_order": [false],
|
|
313
|
-
"trial_retained": [false],
|
|
314
409
|
}
|
|
315
410
|
},
|
|
316
411
|
{
|
|
@@ -321,8 +416,6 @@ const config = {
|
|
|
321
416
|
"item_category": ["entree", "appetizer", "drink", "dessert", "side"],
|
|
322
417
|
"item_price": u.weighNumRange(3, 65, 1.0, 40),
|
|
323
418
|
"customization_count": u.weighNumRange(0, 5, 1.5, 20),
|
|
324
|
-
"late_night_order": [false],
|
|
325
|
-
"trial_retained": [false],
|
|
326
419
|
}
|
|
327
420
|
},
|
|
328
421
|
{
|
|
@@ -331,7 +424,6 @@ const config = {
|
|
|
331
424
|
properties: {
|
|
332
425
|
"item_id": itemIds,
|
|
333
426
|
"removal_reason": ["changed_mind", "too_expensive", "substitution"],
|
|
334
|
-
"trial_retained": [false],
|
|
335
427
|
}
|
|
336
428
|
},
|
|
337
429
|
{
|
|
@@ -341,8 +433,6 @@ const config = {
|
|
|
341
433
|
"coupon_code": couponCodes,
|
|
342
434
|
"discount_type": ["percent", "flat", "free_delivery"],
|
|
343
435
|
"discount_value": u.weighNumRange(5, 50, 1.2, 20),
|
|
344
|
-
"coupon_injected": [false],
|
|
345
|
-
"trial_retained": [false],
|
|
346
436
|
}
|
|
347
437
|
},
|
|
348
438
|
{
|
|
@@ -352,7 +442,6 @@ const config = {
|
|
|
352
442
|
"cart_total": u.weighNumRange(8, 150, 0.8, 40),
|
|
353
443
|
"items_count": u.weighNumRange(1, 8, 1.2, 20),
|
|
354
444
|
"delivery_address_saved": [false, false, false, true, true, true, true, true, true, true],
|
|
355
|
-
"trial_retained": [false],
|
|
356
445
|
}
|
|
357
446
|
},
|
|
358
447
|
{
|
|
@@ -364,9 +453,6 @@ const config = {
|
|
|
364
453
|
"order_total": u.weighNumRange(10, 200, 0.8, 40),
|
|
365
454
|
"tip_amount": u.weighNumRange(0, 30, 1.5, 20),
|
|
366
455
|
"delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
|
|
367
|
-
"surge_pricing": [false],
|
|
368
|
-
"rainy_week": [false],
|
|
369
|
-
"trial_retained": [false],
|
|
370
456
|
}
|
|
371
457
|
},
|
|
372
458
|
{
|
|
@@ -376,7 +462,6 @@ const config = {
|
|
|
376
462
|
"order_id": orderIds,
|
|
377
463
|
"order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
|
|
378
464
|
"eta_mins": u.weighNumRange(5, 60, 1.0, 30),
|
|
379
|
-
"trial_retained": [false],
|
|
380
465
|
}
|
|
381
466
|
},
|
|
382
467
|
{
|
|
@@ -386,10 +471,6 @@ const config = {
|
|
|
386
471
|
"order_id": orderIds,
|
|
387
472
|
"actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
|
|
388
473
|
"on_time": [false, false, false, true, true, true, true, true, true, true],
|
|
389
|
-
"lunch_rush": [false],
|
|
390
|
-
"dinner_rush": [false],
|
|
391
|
-
"first_order_bonus": [false],
|
|
392
|
-
"trial_retained": [false],
|
|
393
474
|
}
|
|
394
475
|
},
|
|
395
476
|
{
|
|
@@ -400,8 +481,6 @@ const config = {
|
|
|
400
481
|
"food_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
401
482
|
"delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
402
483
|
"would_reorder": [false, true, true],
|
|
403
|
-
"referral_user": [false],
|
|
404
|
-
"trial_retained": [false],
|
|
405
484
|
}
|
|
406
485
|
},
|
|
407
486
|
{
|
|
@@ -415,7 +494,6 @@ const config = {
|
|
|
415
494
|
]),
|
|
416
495
|
"results_count": u.weighNumRange(0, 50, 0.8, 30),
|
|
417
496
|
"search_type": ["restaurant", "cuisine", "dish"],
|
|
418
|
-
"trial_retained": [false],
|
|
419
497
|
}
|
|
420
498
|
},
|
|
421
499
|
{
|
|
@@ -425,7 +503,6 @@ const config = {
|
|
|
425
503
|
"promo_id": () => `promo_${v.uid(5)}`,
|
|
426
504
|
"promo_type": ["banner", "push", "in_feed"],
|
|
427
505
|
"promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
|
|
428
|
-
"trial_retained": [false],
|
|
429
506
|
}
|
|
430
507
|
},
|
|
431
508
|
{
|
|
@@ -435,7 +512,6 @@ const config = {
|
|
|
435
512
|
"plan": ["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"],
|
|
436
513
|
"price": [9.99, 9.99, 79.99],
|
|
437
514
|
"trial": [true, false],
|
|
438
|
-
"trial_retained": [false],
|
|
439
515
|
}
|
|
440
516
|
},
|
|
441
517
|
{
|
|
@@ -444,7 +520,6 @@ const config = {
|
|
|
444
520
|
properties: {
|
|
445
521
|
"reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
|
|
446
522
|
"months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
|
|
447
|
-
"trial_retained": [false],
|
|
448
523
|
}
|
|
449
524
|
},
|
|
450
525
|
{
|
|
@@ -453,7 +528,6 @@ const config = {
|
|
|
453
528
|
properties: {
|
|
454
529
|
"issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
|
|
455
530
|
"order_id": orderIds,
|
|
456
|
-
"trial_retained": [false],
|
|
457
531
|
}
|
|
458
532
|
},
|
|
459
533
|
{
|
|
@@ -462,8 +536,6 @@ const config = {
|
|
|
462
536
|
properties: {
|
|
463
537
|
"order_id": orderIds,
|
|
464
538
|
"original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
|
|
465
|
-
"referral_user": [false],
|
|
466
|
-
"trial_retained": [false],
|
|
467
539
|
}
|
|
468
540
|
},
|
|
469
541
|
],
|
|
@@ -488,8 +560,6 @@ const config = {
|
|
|
488
560
|
"avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
|
|
489
561
|
"orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
|
|
490
562
|
"favorite_restaurant_count": u.weighNumRange(1, 10),
|
|
491
|
-
"is_high_risk": [false],
|
|
492
|
-
"churn_risk_score": [0],
|
|
493
563
|
"Platform": ["iOS", "Android", "Web"],
|
|
494
564
|
"subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
|
|
495
565
|
"city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
|
|
@@ -519,173 +589,51 @@ const config = {
|
|
|
519
589
|
|
|
520
590
|
lookupTables: [],
|
|
521
591
|
|
|
522
|
-
/**
|
|
523
|
-
* ARCHITECTED ANALYTICS HOOKS
|
|
524
|
-
*
|
|
525
|
-
* This hook function creates 8 deliberate patterns in the data:
|
|
526
|
-
*
|
|
527
|
-
* 1. LUNCH RUSH CONVERSION: Orders during lunch/dinner hours convert at higher rates
|
|
528
|
-
* 2. COUPON INJECTION: Free-tier users get coupons spliced into funnels
|
|
529
|
-
* 3. LATE NIGHT MUNCHIES: Late-night orders skew to fast food with inflated prices
|
|
530
|
-
* 4. RAINY WEEK SURGE: Days 20-27 have surge pricing and doubled order volume
|
|
531
|
-
* 5. REFERRAL POWER USERS: Referred users reorder more and rate higher
|
|
532
|
-
* 6. TRIAL CONVERSION: Trial subscribers with 3+ early orders are retained
|
|
533
|
-
* 7. SUPPORT TICKET CHURN: 15% of users flagged as high-risk churners
|
|
534
|
-
* 8. FIRST ORDER BONUS: New users in the Order Completion funnel convert at 90%
|
|
535
|
-
*/
|
|
536
592
|
hook: function (record, type, meta) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const RAINY_WEEK_END = DATASET_START.add(27, 'days');
|
|
541
|
-
|
|
542
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
543
|
-
// HOOK 1: LUNCH RUSH CONVERSION (funnel-pre)
|
|
544
|
-
// During lunch (11AM-1PM) and dinner (5PM-8PM), funnel conversion
|
|
545
|
-
// rates are boosted to reflect real-world meal-time ordering surges.
|
|
546
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
547
|
-
if (type === "funnel-pre") {
|
|
548
|
-
if (meta && meta.firstEventTime) {
|
|
549
|
-
const hour = dayjs.unix(meta.firstEventTime).hour();
|
|
550
|
-
|
|
551
|
-
record.props = record.props || {};
|
|
552
|
-
|
|
553
|
-
// Lunch rush: 11AM - 1PM
|
|
554
|
-
if (hour >= 11 && hour <= 13) {
|
|
555
|
-
record.props.lunch_rush = true;
|
|
556
|
-
record.props.dinner_rush = false;
|
|
557
|
-
}
|
|
558
|
-
// Dinner rush: 5PM - 8PM
|
|
559
|
-
else if (hour >= 17 && hour <= 20) {
|
|
560
|
-
record.props.lunch_rush = false;
|
|
561
|
-
record.props.dinner_rush = true;
|
|
562
|
-
} else {
|
|
563
|
-
record.props.lunch_rush = false;
|
|
564
|
-
record.props.dinner_rush = false;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// ═══════════════════════════════════════════════════════════════
|
|
569
|
-
// HOOK 8: FIRST ORDER BONUS (funnel-pre)
|
|
570
|
-
// New users (born in dataset) get a massive conversion boost on
|
|
571
|
-
// the Order Completion funnel, simulating first-order promotions
|
|
572
|
-
// and new-user incentives that drive initial purchases.
|
|
573
|
-
// ═══════════════════════════════════════════════════════════════
|
|
574
|
-
if (record.sequence &&
|
|
575
|
-
record.sequence[0] === "checkout started" &&
|
|
576
|
-
record.sequence[1] === "order placed") {
|
|
577
|
-
record.props = record.props || {};
|
|
578
|
-
// Use hash to deterministically assign ~50% of users as "new"
|
|
579
|
-
const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
|
|
580
|
-
const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
|
|
581
|
-
if (isNewUser) {
|
|
582
|
-
record.props.first_order_bonus = true;
|
|
583
|
-
} else {
|
|
584
|
-
record.props.first_order_bonus = false;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
590
|
-
// HOOK 2: COUPON INJECTION (funnel-post)
|
|
591
|
-
// Free-tier users get coupon_applied events spliced into their
|
|
592
|
-
// funnel sequences 30% of the time, simulating promotional nudges
|
|
593
|
-
// that push non-subscribers toward conversion.
|
|
594
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
593
|
+
// HOOK 9 (T2C): ORDER LIFECYCLE TIME-TO-CONVERT (funnel-post)
|
|
594
|
+
// QuickBite+ subscribers complete order funnels 1.35x faster
|
|
595
|
+
// (factor 0.74); Free users 1.3x slower (factor 1.3).
|
|
595
596
|
if (type === "funnel-post") {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
).toISOString();
|
|
610
|
-
|
|
611
|
-
const couponTemplate = record.find(e => e.event === "coupon applied");
|
|
612
|
-
const couponEvent = {
|
|
613
|
-
...(couponTemplate || firstEvent),
|
|
614
|
-
event: "coupon applied",
|
|
615
|
-
time: midTime,
|
|
616
|
-
user_id: firstEvent.user_id,
|
|
617
|
-
subscription_tier: firstEvent.subscription_tier,
|
|
618
|
-
Platform: firstEvent.Platform,
|
|
619
|
-
city: firstEvent.city,
|
|
620
|
-
coupon_code: chance.pickone(couponCodes),
|
|
621
|
-
discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
|
|
622
|
-
discount_value: chance.integer({ min: 10, max: 30 }),
|
|
623
|
-
coupon_injected: true,
|
|
624
|
-
};
|
|
625
|
-
record.splice(insertIdx, 0, couponEvent);
|
|
597
|
+
const segment = meta?.profile?.subscription_tier;
|
|
598
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
599
|
+
const factor = (
|
|
600
|
+
segment === "QuickBite+" ? 0.74 :
|
|
601
|
+
segment === "Free" ? 1.3 :
|
|
602
|
+
1.0
|
|
603
|
+
);
|
|
604
|
+
if (factor !== 1.0) {
|
|
605
|
+
for (let i = 1; i < record.length; i++) {
|
|
606
|
+
const prev = dayjs(record[i - 1].time);
|
|
607
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
608
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
609
|
+
}
|
|
626
610
|
}
|
|
627
611
|
}
|
|
628
612
|
}
|
|
629
613
|
|
|
630
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
631
|
-
// HOOK 3: LATE NIGHT MUNCHIES (event)
|
|
632
|
-
// Between 10PM and 2AM, restaurant views and cart additions skew
|
|
633
|
-
// heavily toward fast food (American cuisine) with inflated prices,
|
|
634
|
-
// modeling the real-world late-night ordering pattern.
|
|
635
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
636
614
|
if (type === "event") {
|
|
615
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
616
|
+
const RAINY_WEEK_START = datasetStart.add(20, 'days');
|
|
617
|
+
const RAINY_WEEK_END = datasetStart.add(27, 'days');
|
|
637
618
|
const EVENT_TIME = dayjs(record.time);
|
|
638
|
-
const hour = EVENT_TIME.hour();
|
|
639
|
-
const isLateNight = hour >= 22 || hour <= 2;
|
|
640
|
-
|
|
641
|
-
if (record.event === "restaurant viewed" || record.event === "item added to cart") {
|
|
642
|
-
if (isLateNight) {
|
|
643
|
-
// 70% of late-night browsing/ordering is fast food (American)
|
|
644
|
-
if (chance.bool({ likelihood: 70 })) {
|
|
645
|
-
if (record.cuisine_type !== undefined) {
|
|
646
|
-
record.cuisine_type = "American";
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Boost item price by 1.3x (late-night surcharge effect)
|
|
651
|
-
if (record.item_price !== undefined) {
|
|
652
|
-
record.item_price = Math.round(record.item_price * 1.3 * 100) / 100;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
record.late_night_order = true;
|
|
656
|
-
} else {
|
|
657
|
-
record.late_night_order = false;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
619
|
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
// During days 20-27, delivery fees double on order_placed events
|
|
664
|
-
// and there is a 40% chance of duplicating the event, simulating
|
|
665
|
-
// a weather-driven demand surge with surge pricing.
|
|
666
|
-
// ═══════════════════════════════════════════════════════════════
|
|
620
|
+
// HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
|
|
621
|
+
// order placed events. Mutates existing prop. No flag.
|
|
667
622
|
if (record.event === "order placed") {
|
|
668
623
|
if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
|
|
669
624
|
record.delivery_fee = (record.delivery_fee || 5) * 2;
|
|
670
|
-
record.surge_pricing = true;
|
|
671
|
-
record.rainy_week = true;
|
|
672
|
-
} else {
|
|
673
|
-
record.surge_pricing = false;
|
|
674
625
|
}
|
|
675
626
|
}
|
|
676
627
|
}
|
|
677
628
|
|
|
678
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
679
|
-
// HOOK 5: REFERRAL POWER USERS (everything)
|
|
680
|
-
// Users who signed up with a referral code get 2x more reorder
|
|
681
|
-
// events and boosted food ratings. Referred users are more loyal
|
|
682
|
-
// and satisfied, mirroring real referral program outcomes.
|
|
683
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
684
629
|
if (type === "everything") {
|
|
630
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
631
|
+
const RAINY_WEEK_START = datasetStart.add(20, 'days');
|
|
632
|
+
const RAINY_WEEK_END = datasetStart.add(27, 'days');
|
|
685
633
|
const userEvents = record;
|
|
686
634
|
const profile = meta.profile;
|
|
687
635
|
|
|
688
|
-
//
|
|
636
|
+
// Stamp superProps from profile so they stay consistent per user
|
|
689
637
|
if (profile) {
|
|
690
638
|
userEvents.forEach((event) => {
|
|
691
639
|
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
@@ -694,136 +642,162 @@ const config = {
|
|
|
694
642
|
});
|
|
695
643
|
}
|
|
696
644
|
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
645
|
+
// HOOK 3: LATE NIGHT MUNCHIES — 10PM-2AM UTC, 70% of restaurant
|
|
646
|
+
// views/cart additions get cuisine flipped to American, 1.3x
|
|
647
|
+
// item_price. Mutates existing props. No flag — discover via
|
|
648
|
+
// HOD breakdown on cuisine_type / item_price.
|
|
649
|
+
userEvents.forEach(e => {
|
|
650
|
+
if (e.event === "restaurant viewed" || e.event === "item added to cart") {
|
|
651
|
+
const hour = new Date(e.time).getUTCHours();
|
|
652
|
+
const isLateNight = hour >= 22 || hour <= 2;
|
|
653
|
+
if (isLateNight) {
|
|
654
|
+
if (e.cuisine_type !== undefined && chance.bool({ likelihood: 70 })) {
|
|
655
|
+
e.cuisine_type = "American";
|
|
656
|
+
}
|
|
657
|
+
if (e.item_price !== undefined) {
|
|
658
|
+
e.item_price = Math.round(e.item_price * 1.3 * 100) / 100;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// HOOK 2: COUPON INJECTION — Free-tier users get coupon applied
|
|
665
|
+
// events spliced into stream near checkout. Cloned from existing
|
|
666
|
+
// coupon-applied event with unique offset time. No flag.
|
|
667
|
+
if (profile && profile.subscription_tier === "Free") {
|
|
668
|
+
for (let i = userEvents.length - 1; i >= 1; i--) {
|
|
669
|
+
const evt = userEvents[i];
|
|
670
|
+
if (evt.event === "checkout started" && chance.bool({ likelihood: 30 })) {
|
|
671
|
+
const prevEvent = userEvents[i - 1];
|
|
672
|
+
const midTime = dayjs(prevEvent.time).add(
|
|
673
|
+
dayjs(evt.time).diff(dayjs(prevEvent.time)) / 2,
|
|
674
|
+
'milliseconds'
|
|
675
|
+
).toISOString();
|
|
676
|
+
|
|
677
|
+
const couponTemplate = userEvents.find(e => e.event === "coupon applied");
|
|
678
|
+
const couponEvent = {
|
|
679
|
+
...(couponTemplate || evt),
|
|
680
|
+
event: "coupon applied",
|
|
681
|
+
time: midTime,
|
|
682
|
+
user_id: evt.user_id,
|
|
683
|
+
subscription_tier: profile.subscription_tier,
|
|
684
|
+
Platform: profile.Platform,
|
|
685
|
+
city: profile.city,
|
|
686
|
+
coupon_code: chance.pickone(couponCodes),
|
|
687
|
+
discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
|
|
688
|
+
discount_value: chance.integer({ min: 10, max: 30 }),
|
|
689
|
+
};
|
|
690
|
+
userEvents.splice(i, 0, couponEvent);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// HOOK 1: LUNCH/DINNER RUSH — drop 30% of order delivered events
|
|
696
|
+
// that fall outside the meal hours (11-13 UTC and 17-20 UTC).
|
|
697
|
+
// Discoverable via order delivered breakdown by HOD.
|
|
700
698
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
701
699
|
const event = userEvents[i];
|
|
702
700
|
if (event.event === "order delivered") {
|
|
703
|
-
|
|
704
|
-
|
|
701
|
+
const hour = new Date(event.time).getUTCHours();
|
|
702
|
+
const inRush = (hour >= 11 && hour <= 13) || (hour >= 17 && hour <= 20);
|
|
703
|
+
if (!inRush && chance.bool({ likelihood: 30 })) {
|
|
705
704
|
userEvents.splice(i, 1);
|
|
706
|
-
continue;
|
|
707
705
|
}
|
|
708
|
-
|
|
709
|
-
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// HOOK 8: FIRST ORDER BONUS — hash-based ~50% of users (returning)
|
|
710
|
+
// drop 30% of order delivered events. New users keep all.
|
|
711
|
+
// Discover via cohort builder on hash bucket vs conversion.
|
|
712
|
+
const hashUser = userEvents[0] && userEvents[0].user_id;
|
|
713
|
+
const isNewUser = typeof hashUser === "string" && hashUser.charCodeAt(0) % 2 === 0;
|
|
714
|
+
if (!isNewUser) {
|
|
715
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
716
|
+
if (userEvents[i].event === "order delivered" && chance.bool({ likelihood: 30 })) {
|
|
710
717
|
userEvents.splice(i, 1);
|
|
711
|
-
continue;
|
|
712
718
|
}
|
|
713
719
|
}
|
|
714
720
|
}
|
|
715
721
|
|
|
716
|
-
// First pass: identify
|
|
722
|
+
// First pass: identify behavioral patterns (no flags written)
|
|
717
723
|
let isReferralUser = false;
|
|
718
724
|
let hasTrialSubscription = false;
|
|
719
725
|
let earlyOrderCount = 0;
|
|
720
|
-
let
|
|
726
|
+
let orderPlacedCount = 0;
|
|
727
|
+
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
721
728
|
|
|
722
729
|
userEvents.forEach((event) => {
|
|
723
730
|
const eventTime = dayjs(event.time);
|
|
724
731
|
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (event.event === "
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
// Hook #6: Track trial subscribers and early orders
|
|
732
|
-
if (event.event === "subscription started" && event.trial === true) {
|
|
733
|
-
hasTrialSubscription = true;
|
|
734
|
-
}
|
|
735
|
-
if (event.event === "order placed" && daysSinceStart <= 14) {
|
|
736
|
-
earlyOrderCount++;
|
|
732
|
+
if (event.event === "account created" && event.referral_code === true) isReferralUser = true;
|
|
733
|
+
if (event.event === "subscription started" && event.trial === true) hasTrialSubscription = true;
|
|
734
|
+
if (event.event === "order placed") {
|
|
735
|
+
orderPlacedCount++;
|
|
736
|
+
if (daysSinceStart <= 14) earlyOrderCount++;
|
|
737
737
|
}
|
|
738
738
|
});
|
|
739
739
|
|
|
740
|
-
//
|
|
740
|
+
// HOOK 5: REFERRAL POWER USERS — boost food rating to 4-5,
|
|
741
|
+
// clone reorder events. No flag.
|
|
741
742
|
userEvents.forEach((event, idx) => {
|
|
742
|
-
if (event.event === "order rated") {
|
|
743
|
-
|
|
744
|
-
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
745
|
-
event.referral_user = true;
|
|
746
|
-
} else {
|
|
747
|
-
event.referral_user = false;
|
|
748
|
-
}
|
|
743
|
+
if (isReferralUser && event.event === "order rated") {
|
|
744
|
+
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
749
745
|
}
|
|
750
746
|
if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
|
|
751
747
|
const eventTime = dayjs(event.time);
|
|
752
|
-
|
|
748
|
+
userEvents.splice(idx + 1, 0, {
|
|
753
749
|
...event,
|
|
754
750
|
time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
|
|
755
751
|
user_id: event.user_id,
|
|
756
752
|
order_id: chance.pickone(orderIds),
|
|
757
753
|
original_order_age_days: chance.integer({ min: 3, max: 30 }),
|
|
758
|
-
|
|
759
|
-
};
|
|
760
|
-
userEvents.splice(idx + 1, 0, extraReorder);
|
|
754
|
+
});
|
|
761
755
|
}
|
|
762
756
|
});
|
|
763
757
|
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
|
|
767
|
-
// are retained. Those with fewer orders churn: 60% of their
|
|
768
|
-
// events after day 14 are removed, simulating subscription
|
|
769
|
-
// abandonment after a failed trial experience.
|
|
770
|
-
// ═══════════════════════════════════════════════════════════════
|
|
771
|
-
if (hasTrialSubscription) {
|
|
758
|
+
// HOOK 6: TRIAL CONVERSION — trial subs with <3 early orders
|
|
759
|
+
// drop 60% of post-day-14 events. No flag.
|
|
760
|
+
if (hasTrialSubscription && earlyOrderCount < 3 && chance.bool({ likelihood: 60 })) {
|
|
772
761
|
const trialCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
773
|
-
|
|
774
|
-
if (earlyOrderCount >= 3) {
|
|
775
|
-
// Retained: mark events as trial_retained
|
|
776
|
-
userEvents.forEach((event) => {
|
|
777
|
-
event.trial_retained = true;
|
|
778
|
-
});
|
|
779
|
-
} else {
|
|
780
|
-
// Churned: remove 60% of events after day 14
|
|
781
|
-
userEvents.forEach((event) => {
|
|
782
|
-
event.trial_retained = false;
|
|
783
|
-
});
|
|
762
|
+
if (trialCutoff) {
|
|
784
763
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
788
|
-
userEvents.splice(i, 1);
|
|
789
|
-
}
|
|
764
|
+
if (dayjs(userEvents[i].time).isAfter(trialCutoff)) {
|
|
765
|
+
userEvents.splice(i, 1);
|
|
790
766
|
}
|
|
791
767
|
}
|
|
792
768
|
}
|
|
793
769
|
}
|
|
794
770
|
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
// Events tagged surge_pricing=true in the event hook get a 40%
|
|
798
|
-
// chance of duplication here, creating visible demand spikes.
|
|
799
|
-
// ═══════════════════════════════════════════════════════════════
|
|
771
|
+
// HOOK 4 (cont): RAINY WEEK SURGE — duplicate 40% of order placed
|
|
772
|
+
// events that fall in the rainy window. Cloned with unique offset.
|
|
800
773
|
const rainyDuplicates = [];
|
|
801
774
|
userEvents.forEach((event) => {
|
|
802
|
-
if (event.
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
775
|
+
if (event.event === "order placed") {
|
|
776
|
+
const t = dayjs(event.time);
|
|
777
|
+
if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END) && chance.bool({ likelihood: 40 })) {
|
|
778
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
779
|
+
dup.time = t.add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
|
|
780
|
+
rainyDuplicates.push(dup);
|
|
781
|
+
}
|
|
807
782
|
}
|
|
808
783
|
});
|
|
809
|
-
if (rainyDuplicates.length > 0)
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
record.churn_risk_score = chance.integer({ min: 0, max: 40 });
|
|
784
|
+
if (rainyDuplicates.length > 0) userEvents.push(...rainyDuplicates);
|
|
785
|
+
|
|
786
|
+
// HOOK 9: ORDER-COUNT MAGIC NUMBER (no flags)
|
|
787
|
+
// Sweet 4-8 orders → +40% on order_total. Over 9+ → drop 35% of
|
|
788
|
+
// order placed events (oversaturated; analyst sees inverted-U).
|
|
789
|
+
if (orderPlacedCount >= 4 && orderPlacedCount <= 8) {
|
|
790
|
+
userEvents.forEach(e => {
|
|
791
|
+
if (e.event === "order placed" && typeof e.order_total === "number") {
|
|
792
|
+
e.order_total = Math.round(e.order_total * 1.4);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
} else if (orderPlacedCount >= 9) {
|
|
796
|
+
userEvents.forEach(e => {
|
|
797
|
+
if (e.event === "order placed" && typeof e.order_total === "number") {
|
|
798
|
+
e.order_total = Math.round(e.order_total * 0.65);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
827
801
|
}
|
|
828
802
|
}
|
|
829
803
|
|