@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.
Files changed (66) hide show
  1. package/README.md +518 -0
  2. package/dungeons/array-of-object-lookup-schema.json +327 -0
  3. package/dungeons/array-of-object-lookup.js +220 -0
  4. package/dungeons/ecommerce-schema.json +462 -0
  5. package/dungeons/ecommerce.js +447 -0
  6. package/dungeons/education-schema.json +2409 -0
  7. package/dungeons/education.js +768 -0
  8. package/dungeons/fintech-schema.json +14034 -0
  9. package/dungeons/fintech.js +696 -0
  10. package/dungeons/foobar-schema.json +403 -0
  11. package/dungeons/foobar.js +296 -0
  12. package/dungeons/food-delivery-schema.json +192 -0
  13. package/dungeons/food-delivery.js +602 -0
  14. package/dungeons/food-schema.json +1152 -0
  15. package/dungeons/food.js +754 -0
  16. package/dungeons/gaming-schema.json +1270 -0
  17. package/dungeons/gaming.js +508 -0
  18. package/dungeons/insurance-application-schema.json +204 -0
  19. package/dungeons/insurance-application.js +605 -0
  20. package/dungeons/media-schema.json +906 -0
  21. package/dungeons/media.js +790 -0
  22. package/dungeons/retention-cadence-schema.json +78 -0
  23. package/dungeons/retention-cadence.js +244 -0
  24. package/dungeons/rpg-schema.json +4526 -0
  25. package/dungeons/rpg.js +919 -0
  26. package/dungeons/sanity-schema.json +255 -0
  27. package/dungeons/sanity.js +152 -0
  28. package/dungeons/sass-schema.json +1291 -0
  29. package/dungeons/sass.js +795 -0
  30. package/dungeons/scd-schema.json +919 -0
  31. package/dungeons/scd.js +277 -0
  32. package/dungeons/simple-schema.json +608 -0
  33. package/dungeons/simple.js +285 -0
  34. package/dungeons/simplest-schema.json +1418 -0
  35. package/dungeons/simplest.js +392 -0
  36. package/dungeons/social-schema.json +1118 -0
  37. package/dungeons/social.js +686 -0
  38. package/dungeons/text-generation-schema.json +3096 -0
  39. package/dungeons/text-generation.js +812 -0
  40. package/index.js +567 -0
  41. package/lib/core/config-validator.js +395 -0
  42. package/lib/core/context.js +204 -0
  43. package/lib/core/dungeon-loader.js +337 -0
  44. package/lib/core/storage.js +379 -0
  45. package/lib/generators/adspend.js +132 -0
  46. package/lib/generators/events.js +271 -0
  47. package/lib/generators/funnels.js +407 -0
  48. package/lib/generators/mirror.js +167 -0
  49. package/lib/generators/product-lookup.js +262 -0
  50. package/lib/generators/product-names.js +195 -0
  51. package/lib/generators/profiles.js +93 -0
  52. package/lib/generators/scd.js +124 -0
  53. package/lib/generators/text.js +1192 -0
  54. package/lib/orchestrators/mixpanel-sender.js +266 -0
  55. package/lib/orchestrators/user-loop.js +335 -0
  56. package/lib/templates/abbreviated.d.ts +169 -0
  57. package/lib/templates/defaults.js +1405 -0
  58. package/lib/templates/phrases.js +2526 -0
  59. package/lib/templates/schema.d.ts +173 -0
  60. package/lib/templates/soup-presets.js +188 -0
  61. package/lib/utils/function-registry.js +302 -0
  62. package/lib/utils/json-evaluator.js +172 -0
  63. package/lib/utils/logger.js +34 -0
  64. package/lib/utils/utils.js +1490 -0
  65. package/package.json +89 -0
  66. package/types.d.ts +865 -0
@@ -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;