@ak--47/dungeon-master 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +192 -133
- package/dungeons/vertical/community.js +64 -34
- package/dungeons/vertical/crypto.js +333 -224
- package/dungeons/vertical/dating.js +228 -282
- package/dungeons/vertical/devtools.js +164 -62
- package/dungeons/vertical/ecommerce.js +240 -91
- package/dungeons/vertical/education.js +328 -310
- package/dungeons/vertical/fintech.js +437 -311
- package/dungeons/vertical/fitness.js +141 -58
- package/dungeons/vertical/food-delivery.js +298 -318
- package/dungeons/vertical/gaming.js +378 -231
- package/dungeons/vertical/healthcare.js +140 -60
- package/dungeons/vertical/insurance-application.js +168 -73
- package/dungeons/vertical/logistics.js +95 -6
- package/dungeons/vertical/marketplace.js +137 -51
- package/dungeons/vertical/media.js +241 -386
- package/dungeons/vertical/real-estate.js +322 -315
- package/dungeons/vertical/sass.js +253 -263
- package/dungeons/vertical/social.js +262 -206
- package/dungeons/vertical/travel.js +59 -16
- package/index.js +17 -17
- package/lib/core/config-validator.js +93 -21
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/orchestrators/user-loop.js +47 -47
- package/lib/utils/utils.js +71 -39
- package/package.json +1 -1
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +38 -15
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -16,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.
|
|
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.
|
|
78
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
|
|
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
|
|
85
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:
|
|
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
|
|
93
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
|
|
@@ -165,7 +263,9 @@ 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
|
-
|
|
266
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
267
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
268
|
+
// numDays: num_days,
|
|
169
269
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
170
270
|
numUsers: num_users,
|
|
171
271
|
hasAnonIds: false,
|
|
@@ -268,7 +368,6 @@ const config = {
|
|
|
268
368
|
properties: {
|
|
269
369
|
"signup_method": ["email", "google", "apple", "facebook"],
|
|
270
370
|
"referral_code": [false, false, true],
|
|
271
|
-
"trial_retained": [false],
|
|
272
371
|
}
|
|
273
372
|
},
|
|
274
373
|
{
|
|
@@ -287,7 +386,6 @@ const config = {
|
|
|
287
386
|
],
|
|
288
387
|
"sort_by": ["recommended", "distance", "rating", "price"],
|
|
289
388
|
"filter_applied": [false, false, false, true, true],
|
|
290
|
-
"trial_retained": [false],
|
|
291
389
|
}
|
|
292
390
|
},
|
|
293
391
|
{
|
|
@@ -308,8 +406,6 @@ const config = {
|
|
|
308
406
|
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
309
407
|
"delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
|
|
310
408
|
"price_tier": ["$", "$$", "$$$", "$$$$"],
|
|
311
|
-
"late_night_order": [false],
|
|
312
|
-
"trial_retained": [false],
|
|
313
409
|
}
|
|
314
410
|
},
|
|
315
411
|
{
|
|
@@ -320,8 +416,6 @@ const config = {
|
|
|
320
416
|
"item_category": ["entree", "appetizer", "drink", "dessert", "side"],
|
|
321
417
|
"item_price": u.weighNumRange(3, 65, 1.0, 40),
|
|
322
418
|
"customization_count": u.weighNumRange(0, 5, 1.5, 20),
|
|
323
|
-
"late_night_order": [false],
|
|
324
|
-
"trial_retained": [false],
|
|
325
419
|
}
|
|
326
420
|
},
|
|
327
421
|
{
|
|
@@ -330,7 +424,6 @@ const config = {
|
|
|
330
424
|
properties: {
|
|
331
425
|
"item_id": itemIds,
|
|
332
426
|
"removal_reason": ["changed_mind", "too_expensive", "substitution"],
|
|
333
|
-
"trial_retained": [false],
|
|
334
427
|
}
|
|
335
428
|
},
|
|
336
429
|
{
|
|
@@ -340,8 +433,6 @@ const config = {
|
|
|
340
433
|
"coupon_code": couponCodes,
|
|
341
434
|
"discount_type": ["percent", "flat", "free_delivery"],
|
|
342
435
|
"discount_value": u.weighNumRange(5, 50, 1.2, 20),
|
|
343
|
-
"coupon_injected": [false],
|
|
344
|
-
"trial_retained": [false],
|
|
345
436
|
}
|
|
346
437
|
},
|
|
347
438
|
{
|
|
@@ -351,7 +442,6 @@ const config = {
|
|
|
351
442
|
"cart_total": u.weighNumRange(8, 150, 0.8, 40),
|
|
352
443
|
"items_count": u.weighNumRange(1, 8, 1.2, 20),
|
|
353
444
|
"delivery_address_saved": [false, false, false, true, true, true, true, true, true, true],
|
|
354
|
-
"trial_retained": [false],
|
|
355
445
|
}
|
|
356
446
|
},
|
|
357
447
|
{
|
|
@@ -363,9 +453,6 @@ const config = {
|
|
|
363
453
|
"order_total": u.weighNumRange(10, 200, 0.8, 40),
|
|
364
454
|
"tip_amount": u.weighNumRange(0, 30, 1.5, 20),
|
|
365
455
|
"delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
|
|
366
|
-
"surge_pricing": [false],
|
|
367
|
-
"rainy_week": [false],
|
|
368
|
-
"trial_retained": [false],
|
|
369
456
|
}
|
|
370
457
|
},
|
|
371
458
|
{
|
|
@@ -375,7 +462,6 @@ const config = {
|
|
|
375
462
|
"order_id": orderIds,
|
|
376
463
|
"order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
|
|
377
464
|
"eta_mins": u.weighNumRange(5, 60, 1.0, 30),
|
|
378
|
-
"trial_retained": [false],
|
|
379
465
|
}
|
|
380
466
|
},
|
|
381
467
|
{
|
|
@@ -385,10 +471,6 @@ const config = {
|
|
|
385
471
|
"order_id": orderIds,
|
|
386
472
|
"actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
|
|
387
473
|
"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
474
|
}
|
|
393
475
|
},
|
|
394
476
|
{
|
|
@@ -399,8 +481,6 @@ const config = {
|
|
|
399
481
|
"food_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
400
482
|
"delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
401
483
|
"would_reorder": [false, true, true],
|
|
402
|
-
"referral_user": [false],
|
|
403
|
-
"trial_retained": [false],
|
|
404
484
|
}
|
|
405
485
|
},
|
|
406
486
|
{
|
|
@@ -414,7 +494,6 @@ const config = {
|
|
|
414
494
|
]),
|
|
415
495
|
"results_count": u.weighNumRange(0, 50, 0.8, 30),
|
|
416
496
|
"search_type": ["restaurant", "cuisine", "dish"],
|
|
417
|
-
"trial_retained": [false],
|
|
418
497
|
}
|
|
419
498
|
},
|
|
420
499
|
{
|
|
@@ -424,7 +503,6 @@ const config = {
|
|
|
424
503
|
"promo_id": () => `promo_${v.uid(5)}`,
|
|
425
504
|
"promo_type": ["banner", "push", "in_feed"],
|
|
426
505
|
"promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
|
|
427
|
-
"trial_retained": [false],
|
|
428
506
|
}
|
|
429
507
|
},
|
|
430
508
|
{
|
|
@@ -434,7 +512,6 @@ const config = {
|
|
|
434
512
|
"plan": ["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"],
|
|
435
513
|
"price": [9.99, 9.99, 79.99],
|
|
436
514
|
"trial": [true, false],
|
|
437
|
-
"trial_retained": [false],
|
|
438
515
|
}
|
|
439
516
|
},
|
|
440
517
|
{
|
|
@@ -443,7 +520,6 @@ const config = {
|
|
|
443
520
|
properties: {
|
|
444
521
|
"reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
|
|
445
522
|
"months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
|
|
446
|
-
"trial_retained": [false],
|
|
447
523
|
}
|
|
448
524
|
},
|
|
449
525
|
{
|
|
@@ -452,7 +528,6 @@ const config = {
|
|
|
452
528
|
properties: {
|
|
453
529
|
"issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
|
|
454
530
|
"order_id": orderIds,
|
|
455
|
-
"trial_retained": [false],
|
|
456
531
|
}
|
|
457
532
|
},
|
|
458
533
|
{
|
|
@@ -461,8 +536,6 @@ const config = {
|
|
|
461
536
|
properties: {
|
|
462
537
|
"order_id": orderIds,
|
|
463
538
|
"original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
|
|
464
|
-
"referral_user": [false],
|
|
465
|
-
"trial_retained": [false],
|
|
466
539
|
}
|
|
467
540
|
},
|
|
468
541
|
],
|
|
@@ -487,8 +560,6 @@ const config = {
|
|
|
487
560
|
"avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
|
|
488
561
|
"orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
|
|
489
562
|
"favorite_restaurant_count": u.weighNumRange(1, 10),
|
|
490
|
-
"is_high_risk": [false],
|
|
491
|
-
"churn_risk_score": [0],
|
|
492
563
|
"Platform": ["iOS", "Android", "Web"],
|
|
493
564
|
"subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
|
|
494
565
|
"city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
|
|
@@ -518,135 +589,51 @@ const config = {
|
|
|
518
589
|
|
|
519
590
|
lookupTables: [],
|
|
520
591
|
|
|
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
592
|
hook: function (record, type, meta) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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;
|
|
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).
|
|
596
|
+
if (type === "funnel-post") {
|
|
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
|
+
}
|
|
584
610
|
}
|
|
585
611
|
}
|
|
586
612
|
}
|
|
587
613
|
|
|
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
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');
|
|
598
618
|
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
|
-
}
|
|
609
|
-
}
|
|
610
619
|
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
// ═══════════════════════════════════════════════════════════════
|
|
620
|
+
// HOOK 4: RAINY WEEK SURGE — days 20-27, double delivery_fee on
|
|
621
|
+
// order placed events. Mutates existing prop. No flag.
|
|
628
622
|
if (record.event === "order placed") {
|
|
629
623
|
if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
|
|
630
624
|
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
625
|
}
|
|
636
626
|
}
|
|
637
627
|
}
|
|
638
628
|
|
|
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
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');
|
|
646
633
|
const userEvents = record;
|
|
647
634
|
const profile = meta.profile;
|
|
648
635
|
|
|
649
|
-
//
|
|
636
|
+
// Stamp superProps from profile so they stay consistent per user
|
|
650
637
|
if (profile) {
|
|
651
638
|
userEvents.forEach((event) => {
|
|
652
639
|
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
@@ -655,10 +642,28 @@ const config = {
|
|
|
655
642
|
});
|
|
656
643
|
}
|
|
657
644
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
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.
|
|
662
667
|
if (profile && profile.subscription_tier === "Free") {
|
|
663
668
|
for (let i = userEvents.length - 1; i >= 1; i--) {
|
|
664
669
|
const evt = userEvents[i];
|
|
@@ -681,143 +686,118 @@ const config = {
|
|
|
681
686
|
coupon_code: chance.pickone(couponCodes),
|
|
682
687
|
discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
|
|
683
688
|
discount_value: chance.integer({ min: 10, max: 30 }),
|
|
684
|
-
coupon_injected: true,
|
|
685
689
|
};
|
|
686
690
|
userEvents.splice(i, 0, couponEvent);
|
|
687
691
|
}
|
|
688
692
|
}
|
|
689
693
|
}
|
|
690
694
|
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
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.
|
|
694
698
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
695
699
|
const event = userEvents[i];
|
|
696
700
|
if (event.event === "order delivered") {
|
|
697
|
-
|
|
698
|
-
|
|
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 })) {
|
|
699
704
|
userEvents.splice(i, 1);
|
|
700
|
-
continue;
|
|
701
705
|
}
|
|
702
|
-
|
|
703
|
-
|
|
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 })) {
|
|
704
717
|
userEvents.splice(i, 1);
|
|
705
|
-
continue;
|
|
706
718
|
}
|
|
707
719
|
}
|
|
708
720
|
}
|
|
709
721
|
|
|
710
|
-
// First pass: identify
|
|
722
|
+
// First pass: identify behavioral patterns (no flags written)
|
|
711
723
|
let isReferralUser = false;
|
|
712
724
|
let hasTrialSubscription = false;
|
|
713
725
|
let earlyOrderCount = 0;
|
|
714
|
-
let
|
|
726
|
+
let orderPlacedCount = 0;
|
|
727
|
+
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
715
728
|
|
|
716
729
|
userEvents.forEach((event) => {
|
|
717
730
|
const eventTime = dayjs(event.time);
|
|
718
731
|
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++;
|
|
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++;
|
|
731
737
|
}
|
|
732
738
|
});
|
|
733
739
|
|
|
734
|
-
//
|
|
740
|
+
// HOOK 5: REFERRAL POWER USERS — boost food rating to 4-5,
|
|
741
|
+
// clone reorder events. No flag.
|
|
735
742
|
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
|
-
}
|
|
743
|
+
if (isReferralUser && event.event === "order rated") {
|
|
744
|
+
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
743
745
|
}
|
|
744
746
|
if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
|
|
745
747
|
const eventTime = dayjs(event.time);
|
|
746
|
-
|
|
748
|
+
userEvents.splice(idx + 1, 0, {
|
|
747
749
|
...event,
|
|
748
750
|
time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
|
|
749
751
|
user_id: event.user_id,
|
|
750
752
|
order_id: chance.pickone(orderIds),
|
|
751
753
|
original_order_age_days: chance.integer({ min: 3, max: 30 }),
|
|
752
|
-
|
|
753
|
-
};
|
|
754
|
-
userEvents.splice(idx + 1, 0, extraReorder);
|
|
754
|
+
});
|
|
755
755
|
}
|
|
756
756
|
});
|
|
757
757
|
|
|
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) {
|
|
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 })) {
|
|
766
761
|
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
|
-
});
|
|
762
|
+
if (trialCutoff) {
|
|
778
763
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
782
|
-
userEvents.splice(i, 1);
|
|
783
|
-
}
|
|
764
|
+
if (dayjs(userEvents[i].time).isAfter(trialCutoff)) {
|
|
765
|
+
userEvents.splice(i, 1);
|
|
784
766
|
}
|
|
785
767
|
}
|
|
786
768
|
}
|
|
787
769
|
}
|
|
788
770
|
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
// Events tagged surge_pricing=true in the event hook get a 40%
|
|
792
|
-
// chance of duplication here, creating visible demand spikes.
|
|
793
|
-
// ═══════════════════════════════════════════════════════════════
|
|
771
|
+
// HOOK 4 (cont): RAINY WEEK SURGE — duplicate 40% of order placed
|
|
772
|
+
// events that fall in the rainy window. Cloned with unique offset.
|
|
794
773
|
const rainyDuplicates = [];
|
|
795
774
|
userEvents.forEach((event) => {
|
|
796
|
-
if (event.
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
+
}
|
|
801
782
|
}
|
|
802
783
|
});
|
|
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 });
|
|
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
|
+
});
|
|
821
801
|
}
|
|
822
802
|
}
|
|
823
803
|
|