@ak--47/dungeon-master 1.5.3 → 1.6.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 (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +96 -0
  4. package/.claude/skills/create-project/provision.mjs +336 -0
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +211 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +3 -2
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. package/dungeons/vertical/travel.js +0 -930
@@ -1,934 +0,0 @@
1
- // ── IMPORTS ──
2
- import dayjs from "dayjs";
3
- import utc from "dayjs/plugin/utc.js";
4
- dayjs.extend(utc);
5
- import "dotenv/config";
6
- import * as u from "../../lib/utils/utils.js";
7
- import * as v from "ak-tools";
8
- import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
9
- /** @typedef {import("../../types").Dungeon} Config */
10
-
11
- // ── OVERVIEW ──
12
- /*
13
- * NAME: QuickBite
14
- * APP: Food delivery platform (DoorDash/Uber Eats style). Users browse
15
- * restaurants, build carts, place orders, track deliveries, and
16
- * rate their experiences. Monetization via delivery fees,
17
- * QuickBite+ subscription, and promotional coupons.
18
- * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
19
- * CORE LOOP: sign up → browse/search → add to cart → checkout → order placed → track → rate → reorder
20
- *
21
- * EVENTS (17):
22
- * restaurant browsed (18) > restaurant viewed (15) > item added to cart (14)
23
- * > order tracked (13) > checkout started (12) > search performed (11)
24
- * > order placed (10) > order delivered (9) > promotion viewed (8)
25
- * > order rated (7) > reorder initiated (6) > item removed from cart (5)
26
- * > coupon applied (4) > support ticket (3) > subscription started (2)
27
- * > subscription cancelled (1) > account created (1)
28
- *
29
- * FUNNELS (8):
30
- * - Onboarding: account created → restaurant browsed → restaurant viewed (80%)
31
- * - Browse Discovery: restaurant browsed → restaurant viewed → item added to cart (55%)
32
- * - Search Ordering: search performed → restaurant viewed → item added to cart → checkout started (45%)
33
- * - Order Lifecycle: checkout started → order placed → order tracked → order delivered (65%)
34
- * - Reorder Loop: order delivered → order rated → reorder initiated (40%)
35
- * - Promo Flow: promotion viewed → coupon applied → checkout started (50%)
36
- * - Support Flow: support ticket → order rated (45%)
37
- * - Subscription Mgmt: subscription started → order placed → subscription cancelled (20%)
38
- *
39
- * USER PROPS: preferred_cuisine, avg_order_value, orders_per_month, favorite_restaurant_count, Platform, subscription_tier, city
40
- * SUPER PROPS: Platform, subscription_tier, city
41
- * SCD PROPS: subscription_tier (free/trial/monthly/annual, monthly fuzzy, max 6),
42
- * restaurant_tier (new/verified/featured/premium, monthly fixed, max 6, type=restaurant_id)
43
- * GROUPS: restaurant_id (200 restaurants; restaurant viewed / order placed / order rated)
44
- */
45
-
46
- // ── HOOK STORIES ──
47
- /*
48
- * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable only via
49
- * behavioral cohorts or raw-prop breakdowns (HOD, day, segment).
50
- *
51
- * ───────────────────────────────────────────────────────────────────────────────
52
- * 1. LUNCH/DINNER RUSH (everything)
53
- * ───────────────────────────────────────────────────────────────────────────────
54
- *
55
- * PATTERN: 30% of "order delivered" events that fall outside meal-hour
56
- * windows (11-13 UTC and 17-20 UTC) are dropped, depressing non-meal-time
57
- * conversion. Mutation: event drop. Discover via order delivered HOD chart.
58
- *
59
- * HOW TO FIND IT IN MIXPANEL:
60
- *
61
- * Report 1: Order Delivered Volume by Hour of Day
62
- * - Report type: Insights
63
- * - Event: "order delivered"
64
- * - Measure: Total
65
- * - Breakdown: Hour of day
66
- * - Expected: 11-13 and 17-20 dominate; off-hours visibly suppressed
67
- *
68
- * REAL-WORLD ANALOGUE: Meal-hour orders convert at higher rates.
69
- *
70
- * ───────────────────────────────────────────────────────────────────────────────
71
- * 2. COUPON INJECTION (everything)
72
- * ───────────────────────────────────────────────────────────────────────────────
73
- *
74
- * PATTERN: Free-tier users get extra "coupon applied" events cloned into the
75
- * stream near checkout (30% chance per checkout). Cloned with unique offset
76
- * timestamps. No flag.
77
- *
78
- * HOW TO FIND IT IN MIXPANEL:
79
- *
80
- * Report 1: Coupons per User by Tier
81
- * - Report type: Insights
82
- * - Event: "coupon applied"
83
- * - Measure: Total per user
84
- * - Breakdown: "subscription_tier"
85
- * - Expected: Free ~ 1.3x QuickBite+
86
- *
87
- * REAL-WORLD ANALOGUE: Free-tier users are the target of coupon promos.
88
- *
89
- * ───────────────────────────────────────────────────────────────────────────────
90
- * 3. LATE NIGHT MUNCHIES (everything)
91
- * ───────────────────────────────────────────────────────────────────────────────
92
- *
93
- * PATTERN: 10PM-2AM UTC: 70% of "restaurant viewed" / "item added to cart"
94
- * events get cuisine_type flipped to American, item_price bumped 1.3x.
95
- * Mutates existing props. No flag — discover via HOD breakdown.
96
- *
97
- * HOW TO FIND IT IN MIXPANEL:
98
- *
99
- * Report 1: Cuisine Distribution by Hour of Day
100
- * - Report type: Insights
101
- * - Event: "restaurant viewed"
102
- * - Measure: Total
103
- * - Breakdown: "cuisine_type"
104
- * - Filter: hour 22-02
105
- * - Expected: American share spikes
106
- *
107
- * Report 2: Avg item_price by Hour of Day
108
- * - Report type: Insights
109
- * - Event: "item added to cart"
110
- * - Measure: Average of "item_price"
111
- * - Breakdown: Hour of day
112
- * - Expected: 22-02 hours show ~ 1.3x baseline price
113
- *
114
- * REAL-WORLD ANALOGUE: Late-night ordering skews to fast food and impulse buys.
115
- *
116
- * ───────────────────────────────────────────────────────────────────────────────
117
- * 4. RAINY WEEK SURGE (everything)
118
- * ───────────────────────────────────────────────────────────────────────────────
119
- *
120
- * PATTERN: Days 20-27, "order placed" delivery_fee doubled and 40% of those
121
- * events get a duplicate cloned event with unique offset. No flag — discover
122
- * via line chart by day on order placed volume + delivery_fee average.
123
- *
124
- * HOW TO FIND IT IN MIXPANEL:
125
- *
126
- * Report 1: Order Volume Over Time
127
- * - Report type: Insights
128
- * - Event: "order placed"
129
- * - Measure: Total
130
- * - Line chart by day
131
- * - Expected: visible spike days 20-27
132
- *
133
- * Report 2: Avg delivery_fee Over Time
134
- * - Report type: Insights
135
- * - Event: "order placed"
136
- * - Measure: Average of "delivery_fee"
137
- * - Line chart by day
138
- * - Expected: ~ 2x days 20-27
139
- *
140
- * REAL-WORLD ANALOGUE: Weather-driven demand surge with surge pricing.
141
- *
142
- * ───────────────────────────────────────────────────────────────────────────────
143
- * 5. REFERRAL POWER USERS (everything)
144
- * ───────────────────────────────────────────────────────────────────────────────
145
- *
146
- * PATTERN: Users with referral_code=true on account-created event get
147
- * food_rating boosted to 4-5 and 50% of their reorder events cloned (unique
148
- * offset). Mutates existing prop, no flag.
149
- *
150
- * HOW TO FIND IT IN MIXPANEL:
151
- *
152
- * Report 1: Reorders per User by Referral Cohort
153
- * - Report type: Insights (with cohort)
154
- * - Cohort A: users with account-created.referral_code=true
155
- * - Cohort B: rest
156
- * - Event: "reorder initiated"
157
- * - Measure: Total per user
158
- * - Expected: A ~ 1.5-2x B
159
- *
160
- * REAL-WORLD ANALOGUE: Referred users tend to be more loyal.
161
- *
162
- * ───────────────────────────────────────────────────────────────────────────────
163
- * 6. TRIAL CONVERSION (everything)
164
- * ───────────────────────────────────────────────────────────────────────────────
165
- *
166
- * PATTERN: Users with subscription-started.trial=true who place <3 orders in
167
- * first 14 days have 60% of post-day-14 events dropped. No flag.
168
- *
169
- * HOW TO FIND IT IN MIXPANEL:
170
- *
171
- * Report 1: Retention by Trial Order Count
172
- * - Report type: Retention
173
- * - Cohort A: trial users with >= 3 orders in first 14 days
174
- * - Cohort B: trial users with < 3
175
- * - Expected: B sharp activity drop after day 14
176
- *
177
- * REAL-WORLD ANALOGUE: Trial users who fail to activate churn fast.
178
- *
179
- * ───────────────────────────────────────────────────────────────────────────────
180
- * 7. FIRST ORDER BONUS (everything)
181
- * ───────────────────────────────────────────────────────────────────────────────
182
- *
183
- * PATTERN: ~50% of users (deterministic by user_id hash) have 30% of their
184
- * "order delivered" events dropped — simulating that returning users convert
185
- * worse than new users. No flag — analyst sees segment-level conversion gap
186
- * via cohort builder by hash bucket.
187
- *
188
- * HOW TO FIND IT IN MIXPANEL:
189
- *
190
- * Report 1: Order Delivered Conversion by First-Letter-Hash
191
- * - Report type: Funnels
192
- * - Steps: "checkout started" -> "order placed" -> "order delivered"
193
- * - Breakdown: derived hash bucket on distinct_id
194
- * - Expected: half of users show ~ 30% lower conversion on final step
195
- *
196
- * REAL-WORLD ANALOGUE: First-order promos lift new-user conversion.
197
- *
198
- * ───────────────────────────────────────────────────────────────────────────────
199
- * 8. ORDER-COUNT MAGIC NUMBER (everything)
200
- * ───────────────────────────────────────────────────────────────────────────────
201
- *
202
- * PATTERN: Users in the 4-8 order-placed sweet spot get +40% on order_total.
203
- * Users with 9+ orders are over-engaged; their order_total is reduced to
204
- * 0.65x (basket fatigue). No flag — discover by binning users on order count.
205
- *
206
- * HOW TO FIND IT IN MIXPANEL:
207
- *
208
- * Report 1: Avg Order Total by Order-Count Bucket
209
- * - Report type: Insights (with cohort)
210
- * - Cohort A: users with 4-8 "order placed"
211
- * - Cohort B: users with 0-3
212
- * - Event: "order placed"
213
- * - Measure: Average of "order_total"
214
- * - Expected: A ~ 1.4x B
215
- *
216
- * Report 2: Avg Order Total on Heavy Orderers
217
- * - Report type: Insights (with cohort)
218
- * - Cohort C: users with >= 9 "order placed"
219
- * - Cohort A: users with 4-8
220
- * - Event: "order placed"
221
- * - Measure: Average of "order_total"
222
- * - Expected: C ~ 0.65x order_total vs A (basket fatigue)
223
- *
224
- * REAL-WORLD ANALOGUE: Engaged orderers lift basket size; over-orderers
225
- * hit fatigue and slow down.
226
- *
227
- * ───────────────────────────────────────────────────────────────────────────────
228
- * 9. ORDER LIFECYCLE TTC (everything)
229
- * ───────────────────────────────────────────────────────────────────────────────
230
- *
231
- * PATTERN: QuickBite+ users get delivery timing properties scaled 0.67x
232
- * (faster), Free users get 1.4x (slower). Affects actual_delivery_mins,
233
- * eta_mins, delivery_time_est_mins. No flag — discover via property avg
234
- * breakdown by subscription_tier.
235
- *
236
- * HOW TO FIND IT IN MIXPANEL:
237
- *
238
- * Report 1: Avg Delivery Time by Subscription Tier
239
- * - Report type: Insights
240
- * - Event: "order delivered"
241
- * - Measure: Average of "actual_delivery_mins"
242
- * - Breakdown: "subscription_tier"
243
- * - Expected: QuickBite+ ~ 0.67x Free
244
- *
245
- * Report 2: Avg ETA by Subscription Tier
246
- * - Report type: Insights
247
- * - Event: "order tracked"
248
- * - Measure: Average of "eta_mins"
249
- * - Breakdown: "subscription_tier"
250
- * - Expected: QuickBite+ ~ 0.67x Free
251
- *
252
- * REAL-WORLD ANALOGUE: Premium subscribers get priority dispatch and faster
253
- * delivery routing.
254
- *
255
- * ───────────────────────────────────────────────────────────────────────────────
256
- * 10. CITY DENSITY REORDER BOOST (funnel-pre)
257
- * ───────────────────────────────────────────────────────────────────────────────
258
- *
259
- * PATTERN: On the reorder funnel (order delivered → order rated → reorder
260
- * initiated), dense cities (SF, NYC) convert at 1.4x; sprawl cities
261
- * (Houston, Phoenix) at 0.7x. Scoped to the funnel containing
262
- * "reorder initiated".
263
- *
264
- * HOW TO FIND IT IN MIXPANEL:
265
- *
266
- * Report 1: Reorder Funnel Conversion by City
267
- * - Report type: Funnels
268
- * - Steps: "order delivered" → "order rated" → "reorder initiated"
269
- * - Breakdown: "city"
270
- * - Expected: SF / NYC ~ 1.4x baseline; Houston / Phoenix ~ 0.7x
271
- *
272
- * REAL-WORLD ANALOGUE: Dense cities have more restaurant choice and
273
- * faster delivery, driving higher repeat ordering behavior.
274
- *
275
- * ───────────────────────────────────────────────────────────────────────────────
276
- * EXPECTED METRICS SUMMARY
277
- * ───────────────────────────────────────────────────────────────────────────────
278
- *
279
- * Hook | Metric | Baseline | Hook Effect | Ratio
280
- * ──────────────────────|──────────────────────|----------|-------------|------
281
- * Lunch/Dinner Rush | off-hour delivered | 1x | 0.7x | -30%
282
- * Coupon Injection | Free user coupons/u | 1x | ~ 1.3x | 1.3x
283
- * Late Night Munchies | American share 22-02 | ~ 15% | ~ 60% | 4x
284
- * Rainy Week Surge | order vol days 20-27 | 1x | ~ 1.4x | 1.4x
285
- * Referral Power Users | Reorders/user | 1x | ~ 1.5-2x | ~ 1.7x
286
- * Trial Conversion | post-day-14 activity | 1x | ~ 0.4x | -60%
287
- * First Order Bonus | returning conversion | 1x | 0.7x | -30%
288
- * Order-Count Magic Num | sweet order_total | 1x | 1.4x | 1.4x
289
- * Order-Count Magic Num | over order_total | 1x | 0.65x | -35%
290
- * Order Lifecycle TTC | QB+ delivery_mins | 1x | 0.67x | -33%
291
- * Order Lifecycle TTC | Free delivery_mins | 1x | 1.4x | +40%
292
- * City Density Reorder | SF/NYC reorder conv | 1x | 1.4x | 1.4x
293
- * City Density Reorder | HOU/PHX reorder conv | 1x | 0.7x | -30%
294
- */
295
-
296
- // ── SCALE ──
297
- const SEED = "harness-food";
298
- const NUM_USERS = 10_000;
299
- const DATASET_START = "2026-01-01T00:00:00Z";
300
- const DATASET_END = "2026-05-01T23:59:59Z";
301
- const EVENTS_PER_DAY = 1.2;
302
- const token = process.env.MP_TOKEN || "your-mixpanel-token";
303
-
304
- const chance = u.initChance(SEED);
305
-
306
- // ── KNOBS (tweak these to reshape stories) ──
307
- const RUSH_DROP_LIKELIHOOD = 30;
308
- const RUSH_LUNCH_START = 11;
309
- const RUSH_LUNCH_END = 13;
310
- const RUSH_DINNER_START = 17;
311
- const RUSH_DINNER_END = 20;
312
-
313
- const COUPON_INJECT_LIKELIHOOD = 30;
314
-
315
- const LATE_NIGHT_START = 22;
316
- const LATE_NIGHT_END = 2;
317
- const LATE_NIGHT_FLIP_LIKELIHOOD = 70;
318
- const LATE_NIGHT_PRICE_MULT = 1.3;
319
-
320
- const RAINY_START_DAY = 20;
321
- const RAINY_END_DAY = 27;
322
- const RAINY_FEE_MULT = 2;
323
- const RAINY_DUP_LIKELIHOOD = 40;
324
-
325
- const REFERRAL_CLONE_LIKELIHOOD = 50;
326
- const REFERRAL_RATING_MIN = 4;
327
- const REFERRAL_RATING_MAX = 5;
328
-
329
- const TRIAL_EARLY_DAYS = 14;
330
- const TRIAL_MIN_ORDERS = 3;
331
- const TRIAL_DROP_LIKELIHOOD = 60;
332
-
333
- const FIRST_ORDER_DROP_LIKELIHOOD = 30;
334
-
335
- const ORDER_SWEET_MIN = 4;
336
- const ORDER_SWEET_MAX = 8;
337
- const ORDER_OVER_THRESHOLD = 9;
338
- const ORDER_SWEET_BOOST = 1.4;
339
- const ORDER_OVER_FACTOR = 0.65;
340
-
341
- const TTC_QB_PLUS_FACTOR = 0.67;
342
- const TTC_FREE_FACTOR = 1.4;
343
-
344
- const CITY_DENSE_MULT = 1.4;
345
- const CITY_SPRAWL_MULT = 0.7;
346
-
347
- // ── DATA ARRAYS ──
348
- const restaurantIds = v.range(1, 201).map(n => `rest_${v.uid(6)}`);
349
- const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
350
- const orderIds = v.range(1, 5001).map(n => `order_${v.uid(8)}`);
351
- const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
352
-
353
- // ── HELPER FUNCTIONS ──
354
- function handleFunnelPreHooks(record, meta) {
355
- // H10: CITY DENSITY REORDER BOOST — dense cities 1.4x; sprawl 0.7x
356
- // on the reorder funnel.
357
- const isReorderFunnel = meta.funnel?.sequence?.includes("reorder initiated");
358
- if (isReorderFunnel) {
359
- const city = meta.profile?.city;
360
- if (city === "San Francisco" || city === "New York") {
361
- record.conversionRate = Math.min(95, Math.round(record.conversionRate * CITY_DENSE_MULT));
362
- } else if (city === "Houston" || city === "Phoenix") {
363
- record.conversionRate = Math.round(record.conversionRate * CITY_SPRAWL_MULT);
364
- }
365
- }
366
- return record;
367
- }
368
-
369
- function handleEverythingHooks(record, meta) {
370
- const datasetStart = dayjs.unix(meta.datasetStart);
371
- const RAINY_WEEK_START = datasetStart.add(RAINY_START_DAY, 'days');
372
- const RAINY_WEEK_END = datasetStart.add(RAINY_END_DAY, 'days');
373
- const userEvents = record;
374
- const profile = meta.profile;
375
-
376
- // Stamp superProps from profile so they stay consistent per user
377
- if (profile) {
378
- userEvents.forEach((event) => {
379
- if (profile.Platform !== undefined) event.Platform = profile.Platform;
380
- if (profile.subscription_tier !== undefined) event.subscription_tier = profile.subscription_tier;
381
- if (profile.city !== undefined) event.city = profile.city;
382
- });
383
- }
384
-
385
- // H9: ORDER LIFECYCLE TTC — QuickBite+ 0.67x, Free 1.4x on delivery
386
- // timing properties + funnel timestamp shift.
387
- if (profile) {
388
- const tier = profile.subscription_tier;
389
- const ttcFactor = (
390
- tier === "QuickBite+" ? TTC_QB_PLUS_FACTOR :
391
- tier === "Free" ? TTC_FREE_FACTOR :
392
- 1.0
393
- );
394
- if (ttcFactor !== 1.0) {
395
- // Timestamp shift: affects Mixpanel funnel TTC
396
- const orderSeq = findFirstSequence(
397
- userEvents,
398
- ["checkout started", "order placed", "order tracked", "order delivered"],
399
- 60 * 24 * 7
400
- );
401
- if (orderSeq) scaleFunnelTTC(orderSeq, ttcFactor);
402
- // Property scale: affects Insights AVG reports
403
- userEvents.forEach(e => {
404
- if (typeof e.actual_delivery_mins === "number") {
405
- e.actual_delivery_mins = Math.round(e.actual_delivery_mins * ttcFactor);
406
- }
407
- if (typeof e.eta_mins === "number") {
408
- e.eta_mins = Math.round(e.eta_mins * ttcFactor);
409
- }
410
- if (typeof e.delivery_time_est_mins === "number") {
411
- e.delivery_time_est_mins = Math.round(e.delivery_time_est_mins * ttcFactor);
412
- }
413
- });
414
- }
415
- }
416
-
417
- // H3: LATE NIGHT MUNCHIES — 22-02 UTC: 70% flip to American, 1.3x price
418
- userEvents.forEach(e => {
419
- if (e.event === "restaurant viewed" || e.event === "item added to cart") {
420
- const hour = new Date(e.time).getUTCHours();
421
- const isLateNight = hour >= LATE_NIGHT_START || hour <= LATE_NIGHT_END;
422
- if (isLateNight) {
423
- if (e.cuisine_type !== undefined && chance.bool({ likelihood: LATE_NIGHT_FLIP_LIKELIHOOD })) {
424
- e.cuisine_type = "American";
425
- }
426
- if (e.item_price !== undefined) {
427
- e.item_price = Math.round(e.item_price * LATE_NIGHT_PRICE_MULT * 100) / 100;
428
- }
429
- }
430
- }
431
- });
432
-
433
- // H2: COUPON INJECTION — Free-tier users get coupon-applied events
434
- // spliced near checkout. Cloned from existing template with unique offset.
435
- if (profile && profile.subscription_tier === "Free") {
436
- for (let i = userEvents.length - 1; i >= 1; i--) {
437
- const evt = userEvents[i];
438
- if (evt.event === "checkout started" && chance.bool({ likelihood: COUPON_INJECT_LIKELIHOOD })) {
439
- const prevEvent = userEvents[i - 1];
440
- const midTime = dayjs(prevEvent.time).add(
441
- dayjs(evt.time).diff(dayjs(prevEvent.time)) / 2,
442
- 'milliseconds'
443
- ).toISOString();
444
-
445
- const couponTemplate = userEvents.find(e => e.event === "coupon applied");
446
- const couponEvent = {
447
- ...(couponTemplate || evt),
448
- event: "coupon applied",
449
- time: midTime,
450
- user_id: evt.user_id,
451
- subscription_tier: profile.subscription_tier,
452
- Platform: profile.Platform,
453
- city: profile.city,
454
- coupon_code: chance.pickone(couponCodes),
455
- discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
456
- discount_value: chance.integer({ min: 10, max: 30 }),
457
- };
458
- userEvents.splice(i, 0, couponEvent);
459
- }
460
- }
461
- }
462
-
463
- // H1: LUNCH/DINNER RUSH — drop 30% of order-delivered events outside
464
- // meal windows (11-13 UTC and 17-20 UTC).
465
- for (let i = userEvents.length - 1; i >= 0; i--) {
466
- const event = userEvents[i];
467
- if (event.event === "order delivered") {
468
- const hour = new Date(event.time).getUTCHours();
469
- const inRush = (hour >= RUSH_LUNCH_START && hour <= RUSH_LUNCH_END) || (hour >= RUSH_DINNER_START && hour <= RUSH_DINNER_END);
470
- if (!inRush && chance.bool({ likelihood: RUSH_DROP_LIKELIHOOD })) {
471
- userEvents.splice(i, 1);
472
- }
473
- }
474
- }
475
-
476
- // H7: FIRST ORDER BONUS — hash-based ~50% of users (returning) drop
477
- // 30% of order delivered events. New users keep all.
478
- const hashUser = userEvents[0] && userEvents[0].user_id;
479
- const isNewUser = typeof hashUser === "string" && hashUser.charCodeAt(0) % 2 === 0;
480
- if (!isNewUser) {
481
- for (let i = userEvents.length - 1; i >= 0; i--) {
482
- if (userEvents[i].event === "order delivered" && chance.bool({ likelihood: FIRST_ORDER_DROP_LIKELIHOOD })) {
483
- userEvents.splice(i, 1);
484
- }
485
- }
486
- }
487
-
488
- // First pass: identify behavioral patterns (no flags written)
489
- let isReferralUser = false;
490
- let hasTrialSubscription = false;
491
- let earlyOrderCount = 0;
492
- let orderPlacedCount = 0;
493
- const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
494
-
495
- userEvents.forEach((event) => {
496
- const eventTime = dayjs(event.time);
497
- const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
498
- if (event.event === "account created" && event.referral_code === true) isReferralUser = true;
499
- if (event.event === "subscription started" && event.trial === true) hasTrialSubscription = true;
500
- if (event.event === "order placed") {
501
- orderPlacedCount++;
502
- if (daysSinceStart <= TRIAL_EARLY_DAYS) earlyOrderCount++;
503
- }
504
- });
505
-
506
- // H5: REFERRAL POWER USERS — boost food rating to 4-5, clone reorders.
507
- userEvents.forEach((event, idx) => {
508
- if (isReferralUser && event.event === "order rated") {
509
- event.food_rating = chance.integer({ min: REFERRAL_RATING_MIN, max: REFERRAL_RATING_MAX });
510
- }
511
- if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: REFERRAL_CLONE_LIKELIHOOD })) {
512
- const eventTime = dayjs(event.time);
513
- userEvents.splice(idx + 1, 0, {
514
- ...event,
515
- time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
516
- user_id: event.user_id,
517
- order_id: chance.pickone(orderIds),
518
- original_order_age_days: chance.integer({ min: 3, max: 30 }),
519
- });
520
- }
521
- });
522
-
523
- // H6: TRIAL CONVERSION — trial subs with <3 early orders drop 60% of
524
- // post-day-14 events.
525
- if (hasTrialSubscription && earlyOrderCount < TRIAL_MIN_ORDERS && chance.bool({ likelihood: TRIAL_DROP_LIKELIHOOD })) {
526
- const trialCutoff = firstEventTime ? firstEventTime.add(TRIAL_EARLY_DAYS, 'days') : null;
527
- if (trialCutoff) {
528
- for (let i = userEvents.length - 1; i >= 0; i--) {
529
- if (dayjs(userEvents[i].time).isAfter(trialCutoff)) {
530
- userEvents.splice(i, 1);
531
- }
532
- }
533
- }
534
- }
535
-
536
- // H4: RAINY WEEK SURGE — days 20-27, double delivery_fee on order-placed.
537
- userEvents.forEach(e => {
538
- if (e.event === "order placed") {
539
- const t = dayjs(e.time);
540
- if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END)) {
541
- e.delivery_fee = (e.delivery_fee || 5) * RAINY_FEE_MULT;
542
- }
543
- }
544
- });
545
-
546
- // H4 (cont): RAINY WEEK SURGE — duplicate 40% of order-placed events
547
- // in the rainy window. Cloned with unique offset.
548
- const rainyDuplicates = [];
549
- userEvents.forEach((event) => {
550
- if (event.event === "order placed") {
551
- const t = dayjs(event.time);
552
- if (t.isAfter(RAINY_WEEK_START) && t.isBefore(RAINY_WEEK_END) && chance.bool({ likelihood: RAINY_DUP_LIKELIHOOD })) {
553
- const dup = JSON.parse(JSON.stringify(event));
554
- dup.time = t.add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString();
555
- rainyDuplicates.push(dup);
556
- }
557
- }
558
- });
559
- if (rainyDuplicates.length > 0) userEvents.push(...rainyDuplicates);
560
-
561
- // H8: ORDER-COUNT MAGIC NUMBER — sweet 4-8 → +40% on order_total;
562
- // over 9+ → 0.65x order_total (basket fatigue). No flag.
563
- if (orderPlacedCount >= ORDER_SWEET_MIN && orderPlacedCount <= ORDER_SWEET_MAX) {
564
- userEvents.forEach(e => {
565
- if (e.event === "order placed" && typeof e.order_total === "number") {
566
- e.order_total = Math.round(e.order_total * ORDER_SWEET_BOOST);
567
- }
568
- });
569
- } else if (orderPlacedCount >= ORDER_OVER_THRESHOLD) {
570
- userEvents.forEach(e => {
571
- if (e.event === "order placed" && typeof e.order_total === "number") {
572
- e.order_total = Math.round(e.order_total * ORDER_OVER_FACTOR);
573
- }
574
- });
575
- }
576
-
577
- return userEvents;
578
- }
579
-
580
- // ── CONFIG ──
581
- /** @type {Config} */
582
- const config = {
583
- version: 2,
584
- seed: SEED,
585
- datasetStart: DATASET_START,
586
- datasetEnd: DATASET_END,
587
- avgEventsPerUserPerDay: EVENTS_PER_DAY,
588
- numUsers: NUM_USERS,
589
- format: "json",
590
- gzip: true,
591
- credentials: {
592
- token,
593
- },
594
- switches: {
595
- hasSessionIds: true,
596
- alsoInferFunnels: false,
597
- hasLocation: true,
598
- hasAndroidDevices: true,
599
- hasIOSDevices: true,
600
- hasDesktopDevices: true,
601
- hasBrowser: false,
602
- hasCampaigns: false,
603
- isAnonymous: false,
604
- hasAdSpend: false,
605
- hasAvatar: true,
606
- },
607
- identity: {
608
- avgDevicePerUser: 2,
609
- },
610
- concurrency: 1,
611
- writeToDisk: false,
612
- scdProps: {
613
- subscription_tier: {
614
- values: ["free", "trial", "monthly", "annual"],
615
- frequency: "month",
616
- timing: "fuzzy",
617
- max: 6
618
- },
619
- restaurant_tier: {
620
- values: ["new", "verified", "featured", "premium"],
621
- frequency: "month",
622
- timing: "fixed",
623
- max: 6,
624
- type: "restaurant_id"
625
- }
626
- },
627
-
628
- funnels: [
629
- {
630
- sequence: ["account created", "restaurant browsed", "restaurant viewed"],
631
- isFirstFunnel: true,
632
- conversionRate: 80,
633
- timeToConvert: 0.5,
634
- },
635
- {
636
- // Browse and discover: most common action on food delivery apps
637
- sequence: ["restaurant browsed", "restaurant viewed", "item added to cart"],
638
- conversionRate: 55,
639
- timeToConvert: 1,
640
- weight: 5,
641
- props: { "restaurant_id": restaurantIds },
642
- },
643
- {
644
- // Search-driven ordering
645
- sequence: ["search performed", "restaurant viewed", "item added to cart", "checkout started"],
646
- conversionRate: 45,
647
- timeToConvert: 2,
648
- weight: 3,
649
- },
650
- {
651
- // Full order lifecycle: checkout to delivery
652
- sequence: ["checkout started", "order placed", "order tracked", "order delivered"],
653
- conversionRate: 65,
654
- timeToConvert: 2,
655
- weight: 4,
656
- props: { "order_id": orderIds },
657
- },
658
- {
659
- // Post-order: rate and reorder
660
- sequence: ["order delivered", "order rated", "reorder initiated"],
661
- conversionRate: 40,
662
- timeToConvert: 24,
663
- weight: 2,
664
- },
665
- {
666
- // Browsing promos and coupons
667
- sequence: ["promotion viewed", "coupon applied", "checkout started"],
668
- conversionRate: 50,
669
- timeToConvert: 1,
670
- weight: 2,
671
- },
672
- {
673
- // Support flow
674
- sequence: ["support ticket", "order rated"],
675
- conversionRate: 45,
676
- timeToConvert: 6,
677
- weight: 1,
678
- },
679
- {
680
- // Subscription management
681
- sequence: ["subscription started", "order placed", "subscription cancelled"],
682
- conversionRate: 20,
683
- timeToConvert: 48,
684
- weight: 1,
685
- },
686
- ],
687
-
688
- events: [
689
- {
690
- event: "account created",
691
- weight: 1,
692
- isFirstEvent: true,
693
- isAuthEvent: true,
694
- properties: {
695
- "signup_method": ["email", "google", "apple", "facebook"],
696
- "referral_code": [false, false, true],
697
- }
698
- },
699
- {
700
- event: "restaurant browsed",
701
- weight: 18,
702
- properties: {
703
- "cuisine_type": [
704
- "American",
705
- "Italian",
706
- "Chinese",
707
- "Japanese",
708
- "Mexican",
709
- "Indian",
710
- "Thai",
711
- "Mediterranean"
712
- ],
713
- "sort_by": ["recommended", "distance", "rating", "price"],
714
- "filter_applied": [false, false, false, true, true],
715
- }
716
- },
717
- {
718
- event: "restaurant viewed",
719
- weight: 15,
720
- isStrictEvent: false,
721
- properties: {
722
- "restaurant_id": restaurantIds,
723
- "cuisine_type": [
724
- "American",
725
- "Italian",
726
- "Chinese",
727
- "Japanese",
728
- "Mexican",
729
- "Indian",
730
- "Thai",
731
- "Mediterranean"
732
- ],
733
- "avg_rating": u.weighNumRange(1, 5, 0.8, 30),
734
- "delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
735
- "price_tier": ["$", "$$", "$$$", "$$$$"],
736
- }
737
- },
738
- {
739
- event: "item added to cart",
740
- weight: 14,
741
- isStrictEvent: false,
742
- properties: {
743
- "item_id": itemIds,
744
- "item_category": ["entree", "appetizer", "drink", "dessert", "side"],
745
- "item_price": u.weighNumRange(3, 65, 1.0, 40),
746
- "customization_count": u.weighNumRange(0, 5, 1.5, 20),
747
- }
748
- },
749
- {
750
- event: "item removed from cart",
751
- weight: 5,
752
- properties: {
753
- "item_id": itemIds,
754
- "removal_reason": ["changed_mind", "too_expensive", "substitution"],
755
- }
756
- },
757
- {
758
- event: "coupon applied",
759
- weight: 4,
760
- isStrictEvent: false,
761
- properties: {
762
- "coupon_code": couponCodes,
763
- "discount_type": ["percent", "flat", "free_delivery"],
764
- "discount_value": u.weighNumRange(5, 50, 1.2, 20),
765
- }
766
- },
767
- {
768
- event: "checkout started",
769
- weight: 12,
770
- isStrictEvent: false,
771
- properties: {
772
- "cart_total": u.weighNumRange(8, 150, 0.8, 40),
773
- "items_count": u.weighNumRange(1, 8, 1.2, 20),
774
- "delivery_address_saved": [false, false, false, true, true, true, true, true, true, true],
775
- }
776
- },
777
- {
778
- event: "order placed",
779
- weight: 10,
780
- isStrictEvent: false,
781
- properties: {
782
- "order_id": orderIds,
783
- "payment_method": ["credit_card", "apple_pay", "google_pay", "paypal", "cash"],
784
- "order_total": u.weighNumRange(10, 200, 0.8, 40),
785
- "tip_amount": u.weighNumRange(0, 30, 1.5, 20),
786
- "delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
787
- }
788
- },
789
- {
790
- event: "order tracked",
791
- weight: 13,
792
- properties: {
793
- "order_id": orderIds,
794
- "order_status": ["confirmed", "preparing", "picked_up", "en_route", "delivered"],
795
- "eta_mins": u.weighNumRange(5, 60, 1.0, 30),
796
- }
797
- },
798
- {
799
- event: "order delivered",
800
- weight: 9,
801
- isStrictEvent: false,
802
- properties: {
803
- "order_id": orderIds,
804
- "actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
805
- "on_time": [false, false, false, true, true, true, true, true, true, true],
806
- }
807
- },
808
- {
809
- event: "order rated",
810
- weight: 7,
811
- isStrictEvent: false,
812
- properties: {
813
- "order_id": orderIds,
814
- "food_rating": u.weighNumRange(1, 5, 0.8, 30),
815
- "delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
816
- "would_reorder": [false, true, true],
817
- }
818
- },
819
- {
820
- event: "search performed",
821
- weight: 11,
822
- properties: {
823
- "search_query": () => chance.pickone([
824
- "pizza", "sushi", "burger", "tacos", "pad thai",
825
- "chicken", "salad", "ramen", "pasta", "sandwich",
826
- "wings", "curry", "pho", "burritos", "steak"
827
- ]),
828
- "results_count": u.weighNumRange(0, 50, 0.8, 30),
829
- "search_type": ["restaurant", "cuisine", "dish"],
830
- }
831
- },
832
- {
833
- event: "promotion viewed",
834
- weight: 8,
835
- properties: {
836
- "promo_id": () => `promo_${v.uid(5)}`,
837
- "promo_type": ["banner", "push", "in_feed"],
838
- "promo_value": ["10%", "15%", "20%", "25%", "30%", "40%", "50%"],
839
- }
840
- },
841
- {
842
- event: "subscription started",
843
- weight: 2,
844
- isStrictEvent: false,
845
- properties: {
846
- "plan": ["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"],
847
- "price": [9.99, 9.99, 79.99],
848
- "trial": [true, false],
849
- }
850
- },
851
- {
852
- event: "subscription cancelled",
853
- weight: 1,
854
- properties: {
855
- "reason": ["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"],
856
- "months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
857
- }
858
- },
859
- {
860
- event: "support ticket",
861
- weight: 3,
862
- properties: {
863
- "issue_type": ["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"],
864
- "order_id": orderIds,
865
- }
866
- },
867
- {
868
- event: "reorder initiated",
869
- weight: 6,
870
- isStrictEvent: false,
871
- properties: {
872
- "order_id": orderIds,
873
- "original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
874
- }
875
- },
876
- ],
877
-
878
- superProps: {
879
- Platform: ["iOS", "Android", "Web"],
880
- subscription_tier: ["Free", "Free", "Free", "Free", "QuickBite+"],
881
- city: ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
882
- },
883
-
884
- userProps: {
885
- "preferred_cuisine": [
886
- "American",
887
- "Italian",
888
- "Chinese",
889
- "Japanese",
890
- "Mexican",
891
- "Indian",
892
- "Thai",
893
- "Mediterranean"
894
- ],
895
- "avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
896
- "orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
897
- "favorite_restaurant_count": u.weighNumRange(1, 10),
898
- "Platform": ["iOS", "Android", "Web"],
899
- "subscription_tier": ["Free", "Free", "Free", "Free", "QuickBite+"],
900
- "city": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"],
901
- },
902
-
903
- groupKeys: [
904
- ["restaurant_id", 200, ["restaurant viewed", "order placed", "order rated"]],
905
- ],
906
-
907
- groupProps: {
908
- restaurant_id: {
909
- "name": () => `${chance.pickone(["The", "Big", "Lucky", "Golden", "Fresh", "Urban"])} ${chance.pickone(["Kitchen", "Grill", "Bowl", "Wok", "Bistro", "Plate", "Table", "Fork"])}`,
910
- "cuisine": [
911
- "American",
912
- "Italian",
913
- "Chinese",
914
- "Japanese",
915
- "Mexican",
916
- "Indian",
917
- "Thai",
918
- "Mediterranean"
919
- ],
920
- "avg_rating": u.weighNumRange(1, 5, 0.8, 30),
921
- "delivery_radius_mi": u.weighNumRange(1, 15, 1.0, 10),
922
- }
923
- },
924
-
925
- lookupTables: [],
926
-
927
- hook(record, type, meta) {
928
- if (type === "funnel-pre") return handleFunnelPreHooks(record, meta);
929
- if (type === "everything") return handleEverythingHooks(record, meta);
930
- return record;
931
- }
932
- };
933
-
934
- export default config;