@ak--47/dungeon-master 1.0.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/README.md +518 -0
- package/dungeons/array-of-object-lookup-schema.json +327 -0
- package/dungeons/array-of-object-lookup.js +220 -0
- package/dungeons/ecommerce-schema.json +462 -0
- package/dungeons/ecommerce.js +447 -0
- package/dungeons/education-schema.json +2409 -0
- package/dungeons/education.js +768 -0
- package/dungeons/fintech-schema.json +14034 -0
- package/dungeons/fintech.js +696 -0
- package/dungeons/foobar-schema.json +403 -0
- package/dungeons/foobar.js +296 -0
- package/dungeons/food-delivery-schema.json +192 -0
- package/dungeons/food-delivery.js +602 -0
- package/dungeons/food-schema.json +1152 -0
- package/dungeons/food.js +754 -0
- package/dungeons/gaming-schema.json +1270 -0
- package/dungeons/gaming.js +508 -0
- package/dungeons/insurance-application-schema.json +204 -0
- package/dungeons/insurance-application.js +605 -0
- package/dungeons/media-schema.json +906 -0
- package/dungeons/media.js +790 -0
- package/dungeons/retention-cadence-schema.json +78 -0
- package/dungeons/retention-cadence.js +244 -0
- package/dungeons/rpg-schema.json +4526 -0
- package/dungeons/rpg.js +919 -0
- package/dungeons/sanity-schema.json +255 -0
- package/dungeons/sanity.js +152 -0
- package/dungeons/sass-schema.json +1291 -0
- package/dungeons/sass.js +795 -0
- package/dungeons/scd-schema.json +919 -0
- package/dungeons/scd.js +277 -0
- package/dungeons/simple-schema.json +608 -0
- package/dungeons/simple.js +285 -0
- package/dungeons/simplest-schema.json +1418 -0
- package/dungeons/simplest.js +392 -0
- package/dungeons/social-schema.json +1118 -0
- package/dungeons/social.js +686 -0
- package/dungeons/text-generation-schema.json +3096 -0
- package/dungeons/text-generation.js +812 -0
- package/index.js +567 -0
- package/lib/core/config-validator.js +395 -0
- package/lib/core/context.js +204 -0
- package/lib/core/dungeon-loader.js +337 -0
- package/lib/core/storage.js +379 -0
- package/lib/generators/adspend.js +132 -0
- package/lib/generators/events.js +271 -0
- package/lib/generators/funnels.js +407 -0
- package/lib/generators/mirror.js +167 -0
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +124 -0
- package/lib/generators/text.js +1192 -0
- package/lib/orchestrators/mixpanel-sender.js +266 -0
- package/lib/orchestrators/user-loop.js +335 -0
- package/lib/templates/abbreviated.d.ts +169 -0
- package/lib/templates/defaults.js +1405 -0
- package/lib/templates/phrases.js +2526 -0
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/soup-presets.js +188 -0
- package/lib/utils/function-registry.js +302 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +34 -0
- package/lib/utils/utils.js +1490 -0
- package/package.json +89 -0
- package/types.d.ts +865 -0
package/dungeons/food.js
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
|
+
import * as v from "ak-tools";
|
|
6
|
+
|
|
7
|
+
const SEED = "harness-food";
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
const chance = u.initChance(SEED);
|
|
10
|
+
const num_users = 5_000;
|
|
11
|
+
const days = 100;
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../types.d.ts").Dungeon} Config */
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
* DATASET OVERVIEW
|
|
18
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
*
|
|
20
|
+
* QuickBite — a food delivery platform (DoorDash/Uber Eats style).
|
|
21
|
+
* Users browse restaurants, build carts, place orders, track deliveries,
|
|
22
|
+
* and rate their experiences.
|
|
23
|
+
*
|
|
24
|
+
* Scale: 5,000 users · 600K events · 100 days · 17 event types
|
|
25
|
+
*
|
|
26
|
+
* Core loop:
|
|
27
|
+
* sign up → browse/search restaurants → add items to cart →
|
|
28
|
+
* checkout → order placed → track delivery → rate → reorder
|
|
29
|
+
*
|
|
30
|
+
* Restaurant ecosystem: 200 restaurants across 8 cuisine types,
|
|
31
|
+
* four price tiers ($–$$$$), modeled as group profiles.
|
|
32
|
+
*
|
|
33
|
+
* Monetization: delivery fees, QuickBite+ subscription ($9.99/mo or
|
|
34
|
+
* $79.99/yr for free delivery), and promotional coupons.
|
|
35
|
+
*
|
|
36
|
+
* Support & retention: support tickets (missing items, wrong orders,
|
|
37
|
+
* late delivery, quality, refunds) and reorder events model service
|
|
38
|
+
* quality and repeat behavior.
|
|
39
|
+
*
|
|
40
|
+
* Subscription tiers: Free vs QuickBite+ create a natural A/B
|
|
41
|
+
* comparison for monetization and retention analysis.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
46
|
+
* ANALYTICS HOOKS (8 architected patterns)
|
|
47
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
*
|
|
49
|
+
* 1. LUNCH RUSH CONVERSION (funnel-pre)
|
|
50
|
+
* Funnel conversion boosted during meal hours: lunch 1.4x, dinner 1.2x.
|
|
51
|
+
* Mixpanel:
|
|
52
|
+
* • Funnels → "checkout started" → "order placed" → "order delivered"
|
|
53
|
+
* Breakdown: "lunch_rush" → expect ~84% vs ~60% baseline
|
|
54
|
+
* • Same funnel, breakdown: "dinner_rush" → expect ~72% vs ~60%
|
|
55
|
+
*
|
|
56
|
+
* 2. COUPON INJECTION (funnel-post)
|
|
57
|
+
* Free-tier users get coupon_applied spliced into funnels 30% of the time.
|
|
58
|
+
* Mixpanel:
|
|
59
|
+
* • Insights → "coupon applied" → breakdown "coupon_injected"
|
|
60
|
+
* → ~30% of Free-tier coupons are injected
|
|
61
|
+
* • Insights → "coupon applied" → breakdown "subscription_tier"
|
|
62
|
+
* → Free users have more coupon events
|
|
63
|
+
*
|
|
64
|
+
* 3. LATE NIGHT MUNCHIES (event)
|
|
65
|
+
* 10PM–2AM: 70% American cuisine, 1.3x item prices, late_night_order=true.
|
|
66
|
+
* Mixpanel:
|
|
67
|
+
* • Insights → "restaurant viewed" → breakdown "cuisine_type"
|
|
68
|
+
* filter: late_night_order=true → ~70% American vs ~12% daytime
|
|
69
|
+
* • Insights → "item added to cart" → avg "item_price"
|
|
70
|
+
* breakdown: "late_night_order" → 1.3x higher at night
|
|
71
|
+
*
|
|
72
|
+
* 4. RAINY WEEK SURGE (event + everything)
|
|
73
|
+
* Days 20–27: delivery fees 2x, surge_pricing=true, 40% order duplication.
|
|
74
|
+
* Mixpanel:
|
|
75
|
+
* • Insights (line) → "order placed" → daily → visible spike days 20–27
|
|
76
|
+
* • Insights → "order placed" → avg "delivery_fee"
|
|
77
|
+
* breakdown: "surge_pricing" → 2x higher
|
|
78
|
+
*
|
|
79
|
+
* 5. REFERRAL POWER USERS (everything)
|
|
80
|
+
* Referred users: 2x reorders, food_rating 4–5 stars, referral_user=true.
|
|
81
|
+
* Mixpanel:
|
|
82
|
+
* • Insights → "reorder initiated" → per user
|
|
83
|
+
* breakdown: "referral_user" → ~2x more reorders
|
|
84
|
+
* • Insights → "order rated" → avg "food_rating"
|
|
85
|
+
* breakdown: "referral_user" → 4–5 vs ~3.5 baseline
|
|
86
|
+
*
|
|
87
|
+
* 6. TRIAL CONVERSION (everything)
|
|
88
|
+
* Trial subs with 3+ orders in 14 days retained; others lose 60% of events.
|
|
89
|
+
* Mixpanel:
|
|
90
|
+
* • Insights → any event → per user
|
|
91
|
+
* breakdown: "trial_retained" → sustained vs ~60% drop
|
|
92
|
+
* • Retention → "subscription started" (trial=true) → "order placed"
|
|
93
|
+
* → sharp drop after day 14 for non-retained
|
|
94
|
+
*
|
|
95
|
+
* 7. SUPPORT TICKET CHURN (user)
|
|
96
|
+
* 15% of users flagged is_high_risk=true with churn_risk_score 70–100.
|
|
97
|
+
* Mixpanel:
|
|
98
|
+
* • Insights → any event → unique users
|
|
99
|
+
* breakdown: user "is_high_risk" → ~15% high-risk
|
|
100
|
+
* • Insights → breakdown: user "churn_risk_score"
|
|
101
|
+
* → bimodal: 85% at 0–40, 15% at 70–100
|
|
102
|
+
*
|
|
103
|
+
* 8. FIRST ORDER BONUS (funnel-pre)
|
|
104
|
+
* New users get 1.4x conversion boost on checkout→order funnel.
|
|
105
|
+
* Mixpanel:
|
|
106
|
+
* • Funnels → "checkout started" → "order placed" → "order delivered"
|
|
107
|
+
* breakdown: "first_order_bonus" → 1.4x higher conversion
|
|
108
|
+
* • Insights → "order placed" → breakdown "first_order_bonus"
|
|
109
|
+
* → ~50% tagged (hash-based user split)
|
|
110
|
+
*
|
|
111
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
* ADVANCED ANALYSIS IDEAS
|
|
113
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
*
|
|
115
|
+
* Cross-hook patterns:
|
|
116
|
+
* • The Perfect Customer: referral (5) + trial retained (6) + lunch rush (1)
|
|
117
|
+
* + low churn risk (7) → exceptional LTV and retention
|
|
118
|
+
* • Rainy Night Double Whammy: late-night (3) + rainy week (4)
|
|
119
|
+
* → compounded surge pricing?
|
|
120
|
+
* • Coupon-Driven Trial: injected coupons (2) → trial starts (6)?
|
|
121
|
+
* • Referral + First Order: hooks 5 + 8 → highest conversion rates
|
|
122
|
+
* • Support and Churn: high-risk (7) + support tickets during rainy week (4)
|
|
123
|
+
*
|
|
124
|
+
* Cohort analysis:
|
|
125
|
+
* • Signup week (rainy week cohort behaves differently)
|
|
126
|
+
* • Referral vs organic lifecycle
|
|
127
|
+
* • Free vs QuickBite+ across all metrics
|
|
128
|
+
* • City-level cuisine and ordering patterns
|
|
129
|
+
*
|
|
130
|
+
* Funnel analysis:
|
|
131
|
+
* • Onboarding: account created → first restaurant view, by signup method
|
|
132
|
+
* • Order: checkout → delivery, by platform / tier / time of day
|
|
133
|
+
* • Discovery: search type → conversion to ordering
|
|
134
|
+
*
|
|
135
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
* EXPECTED METRICS SUMMARY
|
|
137
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
*
|
|
139
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
140
|
+
* ──────────────────────|──────────────────────|──────────|─────────────|──────
|
|
141
|
+
* Lunch Rush | Funnel conversion | 60% | 84% | 1.4x
|
|
142
|
+
* Coupon Injection | Free user coupons | 0% | 30% | N/A
|
|
143
|
+
* Late Night Munchies | American cuisine % | ~15% | 70% | ~4.7x
|
|
144
|
+
* Rainy Week Surge | Order volume | 100% | 140% | 1.4x
|
|
145
|
+
* Referral Power Users | Reorder frequency | 1x | 2x | 2.0x
|
|
146
|
+
* Trial Conversion | Post-trial retention | 100% | 40% | 0.4x
|
|
147
|
+
* Support Ticket Churn | High-risk users | 0% | 15% | N/A
|
|
148
|
+
* First Order Bonus | New user conversion | 1x | 1.4x | 1.4x
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
// Generate consistent IDs for lookup tables and event properties
|
|
152
|
+
const restaurantIds = v.range(1, 201).map(n => `rest_${v.uid(6)}`);
|
|
153
|
+
const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
|
|
154
|
+
const orderIds = v.range(1, 5001).map(n => `order_${v.uid(8)}`);
|
|
155
|
+
const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
|
|
156
|
+
|
|
157
|
+
/** @type {Config} */
|
|
158
|
+
const config = {
|
|
159
|
+
token: "",
|
|
160
|
+
seed: SEED,
|
|
161
|
+
numDays: days,
|
|
162
|
+
numEvents: num_users * 120,
|
|
163
|
+
numUsers: num_users,
|
|
164
|
+
hasAnonIds: false,
|
|
165
|
+
hasSessionIds: true,
|
|
166
|
+
format: "json",
|
|
167
|
+
gzip: true,
|
|
168
|
+
alsoInferFunnels: false,
|
|
169
|
+
hasLocation: true,
|
|
170
|
+
hasAndroidDevices: true,
|
|
171
|
+
hasIOSDevices: true,
|
|
172
|
+
hasDesktopDevices: true,
|
|
173
|
+
hasBrowser: false,
|
|
174
|
+
hasCampaigns: false,
|
|
175
|
+
isAnonymous: false,
|
|
176
|
+
hasAdSpend: false,
|
|
177
|
+
percentUsersBornInDataset: 50,
|
|
178
|
+
hasAvatar: true,
|
|
179
|
+
batchSize: 2_500_000,
|
|
180
|
+
concurrency: 1,
|
|
181
|
+
writeToDisk: false,
|
|
182
|
+
scdProps: {},
|
|
183
|
+
|
|
184
|
+
funnels: [
|
|
185
|
+
{
|
|
186
|
+
sequence: ["account created", "restaurant browsed", "restaurant viewed"],
|
|
187
|
+
isFirstFunnel: true,
|
|
188
|
+
conversionRate: 80,
|
|
189
|
+
timeToConvert: 0.5,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
// Browse and discover: most common action on food delivery apps
|
|
193
|
+
sequence: ["restaurant browsed", "restaurant viewed", "item added to cart"],
|
|
194
|
+
conversionRate: 55,
|
|
195
|
+
timeToConvert: 1,
|
|
196
|
+
weight: 5,
|
|
197
|
+
props: { "restaurant_id": u.pickAWinner(restaurantIds) },
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
// Search-driven ordering
|
|
201
|
+
sequence: ["search performed", "restaurant viewed", "item added to cart", "checkout started"],
|
|
202
|
+
conversionRate: 45,
|
|
203
|
+
timeToConvert: 2,
|
|
204
|
+
weight: 3,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
// Full order lifecycle: checkout to delivery
|
|
208
|
+
sequence: ["checkout started", "order placed", "order tracked", "order delivered"],
|
|
209
|
+
conversionRate: 65,
|
|
210
|
+
timeToConvert: 2,
|
|
211
|
+
weight: 4,
|
|
212
|
+
props: { "order_id": u.pickAWinner(orderIds) },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
// Post-order: rate and reorder
|
|
216
|
+
sequence: ["order delivered", "order rated", "reorder initiated"],
|
|
217
|
+
conversionRate: 40,
|
|
218
|
+
timeToConvert: 24,
|
|
219
|
+
weight: 2,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
// Browsing promos and coupons
|
|
223
|
+
sequence: ["promotion viewed", "coupon applied", "checkout started"],
|
|
224
|
+
conversionRate: 50,
|
|
225
|
+
timeToConvert: 1,
|
|
226
|
+
weight: 2,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
// Support flow
|
|
230
|
+
sequence: ["support ticket", "order rated"],
|
|
231
|
+
conversionRate: 45,
|
|
232
|
+
timeToConvert: 6,
|
|
233
|
+
weight: 1,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
// Subscription management
|
|
237
|
+
sequence: ["subscription started", "order placed", "subscription cancelled"],
|
|
238
|
+
conversionRate: 20,
|
|
239
|
+
timeToConvert: 48,
|
|
240
|
+
weight: 1,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
|
|
244
|
+
events: [
|
|
245
|
+
{
|
|
246
|
+
event: "account created",
|
|
247
|
+
weight: 1,
|
|
248
|
+
isFirstEvent: true,
|
|
249
|
+
properties: {
|
|
250
|
+
"signup_method": ["email", "google", "apple", "facebook"],
|
|
251
|
+
"referral_code": u.pickAWinner([true, false], 0.3),
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
event: "restaurant browsed",
|
|
256
|
+
weight: 18,
|
|
257
|
+
properties: {
|
|
258
|
+
"cuisine_type": [
|
|
259
|
+
"American",
|
|
260
|
+
"Italian",
|
|
261
|
+
"Chinese",
|
|
262
|
+
"Japanese",
|
|
263
|
+
"Mexican",
|
|
264
|
+
"Indian",
|
|
265
|
+
"Thai",
|
|
266
|
+
"Mediterranean"
|
|
267
|
+
],
|
|
268
|
+
"sort_by": ["recommended", "distance", "rating", "price"],
|
|
269
|
+
"filter_applied": u.pickAWinner([true, false], 0.4),
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
event: "restaurant viewed",
|
|
274
|
+
weight: 15,
|
|
275
|
+
properties: {
|
|
276
|
+
"restaurant_id": u.pickAWinner(restaurantIds),
|
|
277
|
+
"cuisine_type": [
|
|
278
|
+
"American",
|
|
279
|
+
"Italian",
|
|
280
|
+
"Chinese",
|
|
281
|
+
"Japanese",
|
|
282
|
+
"Mexican",
|
|
283
|
+
"Indian",
|
|
284
|
+
"Thai",
|
|
285
|
+
"Mediterranean"
|
|
286
|
+
],
|
|
287
|
+
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
288
|
+
"delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
|
|
289
|
+
"price_tier": ["$", "$$", "$$$", "$$$$"],
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
event: "item added to cart",
|
|
294
|
+
weight: 14,
|
|
295
|
+
properties: {
|
|
296
|
+
"item_id": u.pickAWinner(itemIds),
|
|
297
|
+
"item_category": ["entree", "appetizer", "drink", "dessert", "side"],
|
|
298
|
+
"item_price": u.weighNumRange(3, 65, 1.0, 40),
|
|
299
|
+
"customization_count": u.weighNumRange(0, 5, 1.5, 20),
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
event: "item removed from cart",
|
|
304
|
+
weight: 5,
|
|
305
|
+
properties: {
|
|
306
|
+
"item_id": u.pickAWinner(itemIds),
|
|
307
|
+
"removal_reason": ["changed_mind", "too_expensive", "substitution"],
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
event: "coupon applied",
|
|
312
|
+
weight: 4,
|
|
313
|
+
properties: {
|
|
314
|
+
"coupon_code": u.pickAWinner(couponCodes),
|
|
315
|
+
"discount_type": ["percent", "flat", "free_delivery"],
|
|
316
|
+
"discount_value": u.weighNumRange(5, 50, 1.2, 20),
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
event: "checkout started",
|
|
321
|
+
weight: 12,
|
|
322
|
+
properties: {
|
|
323
|
+
"cart_total": u.weighNumRange(8, 150, 0.8, 40),
|
|
324
|
+
"items_count": u.weighNumRange(1, 8, 1.2, 20),
|
|
325
|
+
"delivery_address_saved": u.pickAWinner([true, false], 0.7),
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
event: "order placed",
|
|
330
|
+
weight: 10,
|
|
331
|
+
properties: {
|
|
332
|
+
"order_id": u.pickAWinner(orderIds),
|
|
333
|
+
"payment_method": ["credit_card", "apple_pay", "google_pay", "paypal", "cash"],
|
|
334
|
+
"order_total": u.weighNumRange(10, 200, 0.8, 40),
|
|
335
|
+
"tip_amount": u.weighNumRange(0, 30, 1.5, 20),
|
|
336
|
+
"delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
event: "order tracked",
|
|
341
|
+
weight: 13,
|
|
342
|
+
properties: {
|
|
343
|
+
"order_id": u.pickAWinner(orderIds),
|
|
344
|
+
"order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
|
|
345
|
+
"eta_mins": u.weighNumRange(5, 60, 1.0, 30),
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
event: "order delivered",
|
|
350
|
+
weight: 9,
|
|
351
|
+
properties: {
|
|
352
|
+
"order_id": u.pickAWinner(orderIds),
|
|
353
|
+
"actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
|
|
354
|
+
"on_time": u.pickAWinner([true, false], 0.7),
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
event: "order rated",
|
|
359
|
+
weight: 7,
|
|
360
|
+
properties: {
|
|
361
|
+
"order_id": u.pickAWinner(orderIds),
|
|
362
|
+
"food_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
363
|
+
"delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
364
|
+
"would_reorder": u.pickAWinner([true, false], 0.65),
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
event: "search performed",
|
|
369
|
+
weight: 11,
|
|
370
|
+
properties: {
|
|
371
|
+
"search_query": () => chance.pickone([
|
|
372
|
+
"pizza", "sushi", "burger", "tacos", "pad thai",
|
|
373
|
+
"chicken", "salad", "ramen", "pasta", "sandwich",
|
|
374
|
+
"wings", "curry", "pho", "burritos", "steak"
|
|
375
|
+
]),
|
|
376
|
+
"results_count": u.weighNumRange(0, 50, 0.8, 30),
|
|
377
|
+
"search_type": ["restaurant", "cuisine", "dish"],
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
event: "promotion viewed",
|
|
382
|
+
weight: 8,
|
|
383
|
+
properties: {
|
|
384
|
+
"promo_id": () => `promo_${v.uid(5)}`,
|
|
385
|
+
"promo_type": ["banner", "push", "in_feed"],
|
|
386
|
+
"promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
event: "subscription started",
|
|
391
|
+
weight: 2,
|
|
392
|
+
properties: {
|
|
393
|
+
"plan": u.pickAWinner(["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"]),
|
|
394
|
+
"price": u.pickAWinner([9.99, 9.99, 79.99]),
|
|
395
|
+
"trial": u.pickAWinner([true, false], 0.5),
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
event: "subscription cancelled",
|
|
400
|
+
weight: 1,
|
|
401
|
+
properties: {
|
|
402
|
+
"reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
|
|
403
|
+
"months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
event: "support ticket",
|
|
408
|
+
weight: 3,
|
|
409
|
+
properties: {
|
|
410
|
+
"issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
|
|
411
|
+
"order_id": u.pickAWinner(orderIds),
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
event: "reorder initiated",
|
|
416
|
+
weight: 6,
|
|
417
|
+
properties: {
|
|
418
|
+
"order_id": u.pickAWinner(orderIds),
|
|
419
|
+
"original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
|
|
424
|
+
superProps: {
|
|
425
|
+
platform: ["iOS", "Android", "Web"],
|
|
426
|
+
subscription_tier: u.pickAWinner(["Free", "Free", "Free", "Free", "QuickBite+"]),
|
|
427
|
+
city: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
userProps: {
|
|
431
|
+
"preferred_cuisine": [
|
|
432
|
+
"American",
|
|
433
|
+
"Italian",
|
|
434
|
+
"Chinese",
|
|
435
|
+
"Japanese",
|
|
436
|
+
"Mexican",
|
|
437
|
+
"Indian",
|
|
438
|
+
"Thai",
|
|
439
|
+
"Mediterranean"
|
|
440
|
+
],
|
|
441
|
+
"avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
|
|
442
|
+
"orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
|
|
443
|
+
"favorite_restaurant_count": u.weighNumRange(1, 10),
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
groupKeys: [
|
|
447
|
+
["restaurant_id", 200, ["restaurant viewed", "order placed", "order rated"]],
|
|
448
|
+
],
|
|
449
|
+
|
|
450
|
+
groupProps: {
|
|
451
|
+
restaurant_id: {
|
|
452
|
+
"name": () => `${chance.pickone(["The", "Big", "Lucky", "Golden", "Fresh", "Urban"])} ${chance.pickone(["Kitchen", "Grill", "Bowl", "Wok", "Bistro", "Plate", "Table", "Fork"])}`,
|
|
453
|
+
"cuisine": [
|
|
454
|
+
"American",
|
|
455
|
+
"Italian",
|
|
456
|
+
"Chinese",
|
|
457
|
+
"Japanese",
|
|
458
|
+
"Mexican",
|
|
459
|
+
"Indian",
|
|
460
|
+
"Thai",
|
|
461
|
+
"Mediterranean"
|
|
462
|
+
],
|
|
463
|
+
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
464
|
+
"delivery_radius_mi": u.weighNumRange(1, 15, 1.0, 10),
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
lookupTables: [],
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* ARCHITECTED ANALYTICS HOOKS
|
|
472
|
+
*
|
|
473
|
+
* This hook function creates 8 deliberate patterns in the data:
|
|
474
|
+
*
|
|
475
|
+
* 1. LUNCH RUSH CONVERSION: Orders during lunch/dinner hours convert at higher rates
|
|
476
|
+
* 2. COUPON INJECTION: Free-tier users get coupons spliced into funnels
|
|
477
|
+
* 3. LATE NIGHT MUNCHIES: Late-night orders skew to fast food with inflated prices
|
|
478
|
+
* 4. RAINY WEEK SURGE: Days 20-27 have surge pricing and doubled order volume
|
|
479
|
+
* 5. REFERRAL POWER USERS: Referred users reorder more and rate higher
|
|
480
|
+
* 6. TRIAL CONVERSION: Trial subscribers with 3+ early orders are retained
|
|
481
|
+
* 7. SUPPORT TICKET CHURN: 15% of users flagged as high-risk churners
|
|
482
|
+
* 8. FIRST ORDER BONUS: New users in the Order Completion funnel convert at 90%
|
|
483
|
+
*/
|
|
484
|
+
hook: function (record, type, meta) {
|
|
485
|
+
const NOW = dayjs();
|
|
486
|
+
const DATASET_START = NOW.subtract(days, 'days');
|
|
487
|
+
const RAINY_WEEK_START = DATASET_START.add(20, 'days');
|
|
488
|
+
const RAINY_WEEK_END = DATASET_START.add(27, 'days');
|
|
489
|
+
|
|
490
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
491
|
+
// HOOK 1: LUNCH RUSH CONVERSION (funnel-pre)
|
|
492
|
+
// During lunch (11AM-1PM) and dinner (5PM-8PM), funnel conversion
|
|
493
|
+
// rates are boosted to reflect real-world meal-time ordering surges.
|
|
494
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
495
|
+
if (type === "funnel-pre") {
|
|
496
|
+
if (meta && meta.firstEventTime) {
|
|
497
|
+
const hour = dayjs.unix(meta.firstEventTime).hour();
|
|
498
|
+
|
|
499
|
+
record.props = record.props || {};
|
|
500
|
+
|
|
501
|
+
// Lunch rush: 11AM - 1PM
|
|
502
|
+
if (hour >= 11 && hour <= 13) {
|
|
503
|
+
record.conversionRate = record.conversionRate * 1.4;
|
|
504
|
+
record.props.lunch_rush = true;
|
|
505
|
+
record.props.dinner_rush = false;
|
|
506
|
+
}
|
|
507
|
+
// Dinner rush: 5PM - 8PM
|
|
508
|
+
else if (hour >= 17 && hour <= 20) {
|
|
509
|
+
record.conversionRate = record.conversionRate * 1.2;
|
|
510
|
+
record.props.lunch_rush = false;
|
|
511
|
+
record.props.dinner_rush = true;
|
|
512
|
+
} else {
|
|
513
|
+
record.props.lunch_rush = false;
|
|
514
|
+
record.props.dinner_rush = false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ═══════════════════════════════════════════════════════════════
|
|
519
|
+
// HOOK 8: FIRST ORDER BONUS (funnel-pre)
|
|
520
|
+
// New users (born in dataset) get a massive conversion boost on
|
|
521
|
+
// the Order Completion funnel, simulating first-order promotions
|
|
522
|
+
// and new-user incentives that drive initial purchases.
|
|
523
|
+
// ═══════════════════════════════════════════════════════════════
|
|
524
|
+
if (record.sequence &&
|
|
525
|
+
record.sequence[0] === "checkout started" &&
|
|
526
|
+
record.sequence[1] === "order placed") {
|
|
527
|
+
record.props = record.props || {};
|
|
528
|
+
// Use hash to deterministically assign ~50% of users as "new"
|
|
529
|
+
const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
|
|
530
|
+
const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
|
|
531
|
+
if (isNewUser) {
|
|
532
|
+
record.conversionRate = Math.min(98, record.conversionRate * 1.4);
|
|
533
|
+
record.props.first_order_bonus = true;
|
|
534
|
+
} else {
|
|
535
|
+
record.props.first_order_bonus = false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
541
|
+
// HOOK 2: COUPON INJECTION (funnel-post)
|
|
542
|
+
// Free-tier users get coupon_applied events spliced into their
|
|
543
|
+
// funnel sequences 30% of the time, simulating promotional nudges
|
|
544
|
+
// that push non-subscribers toward conversion.
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
546
|
+
if (type === "funnel-post") {
|
|
547
|
+
if (Array.isArray(record) && record.length >= 2) {
|
|
548
|
+
// Check if user is Free tier from first event's super props
|
|
549
|
+
const firstEvent = record[0];
|
|
550
|
+
const isFreeUser = firstEvent && firstEvent.subscription_tier === "Free";
|
|
551
|
+
|
|
552
|
+
if (isFreeUser && chance.bool({ likelihood: 30 })) {
|
|
553
|
+
// Pick a random insertion point between funnel steps
|
|
554
|
+
const insertIdx = chance.integer({ min: 1, max: record.length - 1 });
|
|
555
|
+
const prevEvent = record[insertIdx - 1];
|
|
556
|
+
const nextEvent = record[insertIdx];
|
|
557
|
+
const midTime = dayjs(prevEvent.time).add(
|
|
558
|
+
dayjs(nextEvent.time).diff(dayjs(prevEvent.time)) / 2,
|
|
559
|
+
'milliseconds'
|
|
560
|
+
).toISOString();
|
|
561
|
+
|
|
562
|
+
const couponEvent = {
|
|
563
|
+
event: "coupon applied",
|
|
564
|
+
time: midTime,
|
|
565
|
+
user_id: firstEvent.user_id,
|
|
566
|
+
subscription_tier: firstEvent.subscription_tier,
|
|
567
|
+
platform: firstEvent.platform,
|
|
568
|
+
city: firstEvent.city,
|
|
569
|
+
coupon_code: chance.pickone(couponCodes),
|
|
570
|
+
discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
|
|
571
|
+
discount_value: chance.integer({ min: 10, max: 30 }),
|
|
572
|
+
coupon_injected: true,
|
|
573
|
+
};
|
|
574
|
+
record.splice(insertIdx, 0, couponEvent);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
580
|
+
// HOOK 3: LATE NIGHT MUNCHIES (event)
|
|
581
|
+
// Between 10PM and 2AM, restaurant views and cart additions skew
|
|
582
|
+
// heavily toward fast food (American cuisine) with inflated prices,
|
|
583
|
+
// modeling the real-world late-night ordering pattern.
|
|
584
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
585
|
+
if (type === "event") {
|
|
586
|
+
const EVENT_TIME = dayjs(record.time);
|
|
587
|
+
const hour = EVENT_TIME.hour();
|
|
588
|
+
const isLateNight = hour >= 22 || hour <= 2;
|
|
589
|
+
|
|
590
|
+
if (record.event === "restaurant viewed" || record.event === "item added to cart") {
|
|
591
|
+
if (isLateNight) {
|
|
592
|
+
// 70% of late-night browsing/ordering is fast food (American)
|
|
593
|
+
if (chance.bool({ likelihood: 70 })) {
|
|
594
|
+
if (record.cuisine_type !== undefined) {
|
|
595
|
+
record.cuisine_type = "American";
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Boost item price by 1.3x (late-night surcharge effect)
|
|
600
|
+
if (record.item_price !== undefined) {
|
|
601
|
+
record.item_price = Math.round(record.item_price * 1.3 * 100) / 100;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
record.late_night_order = true;
|
|
605
|
+
} else {
|
|
606
|
+
record.late_night_order = false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ═══════════════════════════════════════════════════════════════
|
|
611
|
+
// HOOK 4: RAINY WEEK SURGE (event)
|
|
612
|
+
// During days 20-27, delivery fees double on order_placed events
|
|
613
|
+
// and there is a 40% chance of duplicating the event, simulating
|
|
614
|
+
// a weather-driven demand surge with surge pricing.
|
|
615
|
+
// ═══════════════════════════════════════════════════════════════
|
|
616
|
+
if (record.event === "order placed") {
|
|
617
|
+
if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
|
|
618
|
+
record.delivery_fee = (record.delivery_fee || 5) * 2;
|
|
619
|
+
record.surge_pricing = true;
|
|
620
|
+
record.rainy_week = true;
|
|
621
|
+
} else {
|
|
622
|
+
record.surge_pricing = false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
628
|
+
// HOOK 5: REFERRAL POWER USERS (everything)
|
|
629
|
+
// Users who signed up with a referral code get 2x more reorder
|
|
630
|
+
// events and boosted food ratings. Referred users are more loyal
|
|
631
|
+
// and satisfied, mirroring real referral program outcomes.
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
633
|
+
if (type === "everything") {
|
|
634
|
+
const userEvents = record;
|
|
635
|
+
|
|
636
|
+
// First pass: identify user patterns
|
|
637
|
+
let isReferralUser = false;
|
|
638
|
+
let hasTrialSubscription = false;
|
|
639
|
+
let earlyOrderCount = 0;
|
|
640
|
+
let firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
641
|
+
|
|
642
|
+
userEvents.forEach((event) => {
|
|
643
|
+
const eventTime = dayjs(event.time);
|
|
644
|
+
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
645
|
+
|
|
646
|
+
// Hook #5: Track referral users
|
|
647
|
+
if (event.event === "account created" && event.referral_code === true) {
|
|
648
|
+
isReferralUser = true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Hook #6: Track trial subscribers and early orders
|
|
652
|
+
if (event.event === "subscription started" && event.trial === true) {
|
|
653
|
+
hasTrialSubscription = true;
|
|
654
|
+
}
|
|
655
|
+
if (event.event === "order placed" && daysSinceStart <= 14) {
|
|
656
|
+
earlyOrderCount++;
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Second pass: apply referral power user effects
|
|
661
|
+
userEvents.forEach((event, idx) => {
|
|
662
|
+
if (event.event === "order rated") {
|
|
663
|
+
if (isReferralUser) {
|
|
664
|
+
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
665
|
+
event.referral_user = true;
|
|
666
|
+
} else {
|
|
667
|
+
event.referral_user = false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
|
|
671
|
+
const eventTime = dayjs(event.time);
|
|
672
|
+
const extraReorder = {
|
|
673
|
+
event: "reorder initiated",
|
|
674
|
+
time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
|
|
675
|
+
user_id: event.user_id,
|
|
676
|
+
order_id: chance.pickone(orderIds),
|
|
677
|
+
original_order_age_days: chance.integer({ min: 3, max: 30 }),
|
|
678
|
+
referral_user: true,
|
|
679
|
+
};
|
|
680
|
+
userEvents.splice(idx + 1, 0, extraReorder);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// ═══════════════════════════════════════════════════════════════
|
|
685
|
+
// HOOK 6: TRIAL CONVERSION (everything)
|
|
686
|
+
// Trial subscribers who place 3+ orders in their first 14 days
|
|
687
|
+
// are retained. Those with fewer orders churn: 60% of their
|
|
688
|
+
// events after day 14 are removed, simulating subscription
|
|
689
|
+
// abandonment after a failed trial experience.
|
|
690
|
+
// ═══════════════════════════════════════════════════════════════
|
|
691
|
+
if (hasTrialSubscription) {
|
|
692
|
+
const trialCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
693
|
+
|
|
694
|
+
if (earlyOrderCount >= 3) {
|
|
695
|
+
// Retained: mark events as trial_retained
|
|
696
|
+
userEvents.forEach((event) => {
|
|
697
|
+
event.trial_retained = true;
|
|
698
|
+
});
|
|
699
|
+
} else {
|
|
700
|
+
// Churned: remove 60% of events after day 14
|
|
701
|
+
userEvents.forEach((event) => {
|
|
702
|
+
event.trial_retained = false;
|
|
703
|
+
});
|
|
704
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
705
|
+
const evt = userEvents[i];
|
|
706
|
+
if (trialCutoff && dayjs(evt.time).isAfter(trialCutoff)) {
|
|
707
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
708
|
+
userEvents.splice(i, 1);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ═══════════════════════════════════════════════════════════════
|
|
716
|
+
// HOOK 4: RAINY WEEK SURGE - duplicate order events (everything)
|
|
717
|
+
// Events tagged surge_pricing=true in the event hook get a 40%
|
|
718
|
+
// chance of duplication here, creating visible demand spikes.
|
|
719
|
+
// ═══════════════════════════════════════════════════════════════
|
|
720
|
+
const rainyDuplicates = [];
|
|
721
|
+
userEvents.forEach((event) => {
|
|
722
|
+
if (event.surge_pricing === true && event.event === "order placed" && chance.bool({ likelihood: 40 })) {
|
|
723
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
724
|
+
dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
|
|
725
|
+
dup.rainy_week = true;
|
|
726
|
+
rainyDuplicates.push(dup);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
if (rainyDuplicates.length > 0) {
|
|
730
|
+
userEvents.push(...rainyDuplicates);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
735
|
+
// HOOK 7: SUPPORT TICKET CHURN (user)
|
|
736
|
+
// 15% of users are flagged as high-risk with elevated churn scores.
|
|
737
|
+
// The remaining users get low churn scores. This creates a clear
|
|
738
|
+
// segmentation opportunity for proactive retention campaigns.
|
|
739
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
740
|
+
if (type === "user") {
|
|
741
|
+
if (chance.bool({ likelihood: 15 })) {
|
|
742
|
+
record.is_high_risk = true;
|
|
743
|
+
record.churn_risk_score = chance.integer({ min: 70, max: 100 });
|
|
744
|
+
} else {
|
|
745
|
+
record.is_high_risk = false;
|
|
746
|
+
record.churn_risk_score = chance.integer({ min: 0, max: 40 });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return record;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
export default config;
|