@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.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +3 -2
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- 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;
|