make-mp-data 2.1.6 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dungeons/adspend.js +2 -2
- package/dungeons/ai-chat-analytics-ed.js +3 -2
- package/dungeons/anon.js +2 -2
- package/dungeons/array-of-object-loopup.js +181 -0
- package/dungeons/benchmark-heavy.js +241 -0
- package/dungeons/benchmark-light.js +141 -0
- package/dungeons/big.js +9 -8
- package/dungeons/business.js +2 -1
- package/dungeons/clinch-agi.js +632 -0
- package/dungeons/complex.js +3 -2
- package/dungeons/copilot.js +383 -0
- package/dungeons/ecommerce-store.js +0 -0
- package/dungeons/experiments.js +5 -4
- package/dungeons/foobar.js +101 -101
- package/dungeons/funnels.js +2 -2
- package/dungeons/gaming.js +3 -2
- package/dungeons/harness/harness-education.js +988 -0
- package/dungeons/harness/harness-fintech.js +976 -0
- package/dungeons/harness/harness-food.js +985 -0
- package/dungeons/harness/harness-gaming.js +1178 -0
- package/dungeons/harness/harness-media.js +961 -0
- package/dungeons/harness/harness-sass.js +923 -0
- package/dungeons/harness/harness-social.js +928 -0
- package/dungeons/kurby.js +211 -0
- package/dungeons/media.js +5 -4
- package/dungeons/mil.js +4 -3
- package/dungeons/mirror.js +2 -2
- package/dungeons/money2020-ed.js +8 -7
- package/dungeons/sanity.js +3 -2
- package/dungeons/scd.js +3 -2
- package/dungeons/simple.js +29 -14
- package/dungeons/strict-event-test.js +30 -0
- package/dungeons/student-teacher.js +3 -2
- package/dungeons/text-generation.js +84 -85
- package/dungeons/too-big-events.js +166 -0
- package/dungeons/uday-schema.json +220 -0
- package/dungeons/userAgent.js +4 -3
- package/index.js +41 -54
- package/lib/core/config-validator.js +122 -7
- package/lib/core/context.js +7 -14
- package/lib/core/storage.js +60 -30
- package/lib/generators/adspend.js +12 -27
- package/lib/generators/events.js +6 -7
- package/lib/generators/funnels.js +16 -5
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +3 -3
- package/lib/generators/scd.js +13 -3
- package/lib/generators/text.js +17 -4
- package/lib/orchestrators/mixpanel-sender.js +251 -208
- package/lib/orchestrators/user-loop.js +57 -19
- package/lib/templates/funnels-instructions.txt +272 -0
- package/lib/templates/hook-examples.json +187 -0
- package/lib/templates/hooks-instructions.txt +295 -8
- package/lib/templates/phrases.js +473 -16
- package/lib/templates/refine-instructions.txt +485 -0
- package/lib/templates/schema-instructions.txt +239 -109
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/verbose-schema.js +140 -206
- package/lib/utils/ai.js +853 -77
- package/lib/utils/chart.js +210 -0
- package/lib/utils/function-registry.js +285 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +38 -0
- package/lib/utils/mixpanel.js +101 -0
- package/lib/utils/project.js +3 -2
- package/lib/utils/utils.js +41 -4
- package/package.json +13 -19
- package/types.d.ts +15 -5
- package/lib/generators/text-bak-old.js +0 -1121
- package/lib/orchestrators/worker-manager.js +0 -203
- package/lib/templates/phrases-bak.js +0 -925
- package/lib/templates/prompt (old).txt +0 -98
- package/lib/templates/scratch-dungeon-template.js +0 -116
- package/lib/templates/textQuickTest.js +0 -172
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import * as u from "../../lib/utils/utils.js";
|
|
5
|
+
import * as v from "ak-tools";
|
|
6
|
+
|
|
7
|
+
const SEED = "harness-food";
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
const chance = u.initChance(SEED);
|
|
10
|
+
const num_users = 5_000;
|
|
11
|
+
const days = 100;
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../../types.js").Dungeon} Config */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* NEEDLE IN A HAYSTACK - FOOD DELIVERY APP DESIGN
|
|
17
|
+
*
|
|
18
|
+
* QuickBite - A food delivery platform connecting hungry customers with local restaurants.
|
|
19
|
+
* Think DoorDash/Uber Eats: users browse restaurants, build carts, place orders, track
|
|
20
|
+
* deliveries in real-time, and rate their experiences afterward.
|
|
21
|
+
*
|
|
22
|
+
* CORE USER LOOP:
|
|
23
|
+
* Users sign up (email, Google, Apple, Facebook) and immediately enter a discovery flow.
|
|
24
|
+
* They browse restaurants by cuisine, sort by distance/rating/price, search for specific
|
|
25
|
+
* dishes, and eventually land on a restaurant page. From there, they build a cart by adding
|
|
26
|
+
* items (entrees, appetizers, drinks, desserts, sides) with customizations. Checkout
|
|
27
|
+
* captures payment, tip, and delivery details. After ordering, users track their delivery
|
|
28
|
+
* through multiple statuses (confirmed, preparing, picked up, en route, delivered).
|
|
29
|
+
*
|
|
30
|
+
* RESTAURANT ECOSYSTEM:
|
|
31
|
+
* 200 restaurants across cuisine types: American, Italian, Chinese, Japanese, Mexican,
|
|
32
|
+
* Indian, Thai, and Mediterranean. Restaurants span four price tiers ($, $$, $$$, $$$$)
|
|
33
|
+
* with varying delivery times and ratings. This models a realistic marketplace with
|
|
34
|
+
* restaurant-level analytics via group profiles.
|
|
35
|
+
*
|
|
36
|
+
* DISCOVERY & SEARCH:
|
|
37
|
+
* Two paths to finding food: browsing (filtering by cuisine, sorting by rating/distance/price)
|
|
38
|
+
* and searching (by restaurant name, cuisine type, or specific dish). The search-to-order
|
|
39
|
+
* funnel captures intent-driven behavior vs. casual browsing.
|
|
40
|
+
*
|
|
41
|
+
* MONETIZATION MODEL:
|
|
42
|
+
* - Delivery fees ($0-$12, waived for QuickBite+ subscribers)
|
|
43
|
+
* - QuickBite+ subscription ($9.99/month or $79.99/year) for free delivery and perks
|
|
44
|
+
* - Promotions and coupons drive trial and reactivation
|
|
45
|
+
* - Restaurant-promoted listings (not modeled as user events)
|
|
46
|
+
*
|
|
47
|
+
* CART BEHAVIOR:
|
|
48
|
+
* Cart events (add/remove) capture the deliberation process. Removal reasons
|
|
49
|
+
* (changed_mind, too_expensive, substitution) reveal price sensitivity and UX friction.
|
|
50
|
+
* Customization counts show engagement depth with individual items.
|
|
51
|
+
*
|
|
52
|
+
* ORDER LIFECYCLE:
|
|
53
|
+
* order placed -> order tracked (multiple status updates) -> order delivered -> order rated
|
|
54
|
+
* This models the full post-purchase experience. On-time delivery, actual vs. estimated
|
|
55
|
+
* delivery time, and food/delivery ratings capture service quality metrics.
|
|
56
|
+
*
|
|
57
|
+
* SUPPORT & RETENTION:
|
|
58
|
+
* Support tickets (missing items, wrong orders, late delivery, quality issues, refund
|
|
59
|
+
* requests) model service failures. Reorder events capture repeat behavior and loyalty.
|
|
60
|
+
* The ratio of support tickets to orders is a key service quality indicator.
|
|
61
|
+
*
|
|
62
|
+
* WHY THESE EVENTS/PROPERTIES?
|
|
63
|
+
* - Events model the complete food delivery loop: discovery -> consideration -> purchase -> fulfillment -> retention
|
|
64
|
+
* - Properties enable cohort analysis: cuisine preferences, price sensitivity, platform usage, subscription status
|
|
65
|
+
* - Funnels reveal friction: where do users drop off between browsing and ordering?
|
|
66
|
+
* - Time-based patterns (lunch rush, late night) simulate real delivery demand curves
|
|
67
|
+
* - Subscription tier (Free vs QuickBite+) creates a natural A/B comparison for monetization analysis
|
|
68
|
+
* - Support and rating events drive churn prediction and service quality monitoring
|
|
69
|
+
* - The "needle in haystack" hooks simulate real product insights hidden in production data
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
// Generate consistent IDs for lookup tables and event properties
|
|
73
|
+
const restaurantIds = v.range(1, 201).map(n => `rest_${v.uid(6)}`);
|
|
74
|
+
const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
|
|
75
|
+
const orderIds = v.range(1, 5001).map(n => `order_${v.uid(8)}`);
|
|
76
|
+
const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
|
|
77
|
+
|
|
78
|
+
/** @type {Config} */
|
|
79
|
+
const config = {
|
|
80
|
+
token: "9ffdb0649ebbea4ff7a742725166b06f",
|
|
81
|
+
seed: SEED,
|
|
82
|
+
numDays: days,
|
|
83
|
+
numEvents: num_users * 120,
|
|
84
|
+
numUsers: num_users,
|
|
85
|
+
hasAnonIds: false,
|
|
86
|
+
hasSessionIds: true,
|
|
87
|
+
format: "json",
|
|
88
|
+
gzip: true,
|
|
89
|
+
alsoInferFunnels: false,
|
|
90
|
+
hasLocation: true,
|
|
91
|
+
hasAndroidDevices: true,
|
|
92
|
+
hasIOSDevices: true,
|
|
93
|
+
hasDesktopDevices: true,
|
|
94
|
+
hasBrowser: false,
|
|
95
|
+
hasCampaigns: false,
|
|
96
|
+
isAnonymous: false,
|
|
97
|
+
hasAdSpend: false,
|
|
98
|
+
percentUsersBornInDataset: 50,
|
|
99
|
+
hasAvatar: true,
|
|
100
|
+
makeChart: false,
|
|
101
|
+
batchSize: 2_500_000,
|
|
102
|
+
concurrency: 10,
|
|
103
|
+
writeToDisk: false,
|
|
104
|
+
scdProps: {},
|
|
105
|
+
|
|
106
|
+
funnels: [
|
|
107
|
+
{
|
|
108
|
+
sequence: ["account created", "restaurant browsed", "restaurant viewed"],
|
|
109
|
+
isFirstFunnel: true,
|
|
110
|
+
conversionRate: 80,
|
|
111
|
+
timeToConvert: 0.5,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
// Browse and discover: most common action on food delivery apps
|
|
115
|
+
sequence: ["restaurant browsed", "restaurant viewed", "item added to cart"],
|
|
116
|
+
conversionRate: 55,
|
|
117
|
+
timeToConvert: 1,
|
|
118
|
+
weight: 5,
|
|
119
|
+
props: { "restaurant_id": u.pickAWinner(restaurantIds) },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
// Search-driven ordering
|
|
123
|
+
sequence: ["search performed", "restaurant viewed", "item added to cart", "checkout started"],
|
|
124
|
+
conversionRate: 45,
|
|
125
|
+
timeToConvert: 2,
|
|
126
|
+
weight: 3,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
// Full order lifecycle: checkout to delivery
|
|
130
|
+
sequence: ["checkout started", "order placed", "order tracked", "order delivered"],
|
|
131
|
+
conversionRate: 65,
|
|
132
|
+
timeToConvert: 2,
|
|
133
|
+
weight: 4,
|
|
134
|
+
props: { "order_id": u.pickAWinner(orderIds) },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
// Post-order: rate and reorder
|
|
138
|
+
sequence: ["order delivered", "order rated", "reorder initiated"],
|
|
139
|
+
conversionRate: 40,
|
|
140
|
+
timeToConvert: 24,
|
|
141
|
+
weight: 2,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
// Browsing promos and coupons
|
|
145
|
+
sequence: ["promotion viewed", "coupon applied", "checkout started"],
|
|
146
|
+
conversionRate: 50,
|
|
147
|
+
timeToConvert: 1,
|
|
148
|
+
weight: 2,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
// Support flow
|
|
152
|
+
sequence: ["support ticket", "order rated"],
|
|
153
|
+
conversionRate: 45,
|
|
154
|
+
timeToConvert: 6,
|
|
155
|
+
weight: 1,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
// Subscription management
|
|
159
|
+
sequence: ["subscription started", "order placed", "subscription cancelled"],
|
|
160
|
+
conversionRate: 20,
|
|
161
|
+
timeToConvert: 48,
|
|
162
|
+
weight: 1,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
|
|
166
|
+
events: [
|
|
167
|
+
{
|
|
168
|
+
event: "account created",
|
|
169
|
+
weight: 1,
|
|
170
|
+
isFirstEvent: true,
|
|
171
|
+
properties: {
|
|
172
|
+
"signup_method": u.pickAWinner(["email", "google", "apple", "facebook"]),
|
|
173
|
+
"referral_code": u.pickAWinner([true, false], 0.3),
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
event: "restaurant browsed",
|
|
178
|
+
weight: 18,
|
|
179
|
+
properties: {
|
|
180
|
+
"cuisine_type": u.pickAWinner([
|
|
181
|
+
"American",
|
|
182
|
+
"Italian",
|
|
183
|
+
"Chinese",
|
|
184
|
+
"Japanese",
|
|
185
|
+
"Mexican",
|
|
186
|
+
"Indian",
|
|
187
|
+
"Thai",
|
|
188
|
+
"Mediterranean"
|
|
189
|
+
]),
|
|
190
|
+
"sort_by": u.pickAWinner(["recommended", "distance", "rating", "price"]),
|
|
191
|
+
"filter_applied": u.pickAWinner([true, false], 0.4),
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
event: "restaurant viewed",
|
|
196
|
+
weight: 15,
|
|
197
|
+
properties: {
|
|
198
|
+
"restaurant_id": u.pickAWinner(restaurantIds),
|
|
199
|
+
"cuisine_type": u.pickAWinner([
|
|
200
|
+
"American",
|
|
201
|
+
"Italian",
|
|
202
|
+
"Chinese",
|
|
203
|
+
"Japanese",
|
|
204
|
+
"Mexican",
|
|
205
|
+
"Indian",
|
|
206
|
+
"Thai",
|
|
207
|
+
"Mediterranean"
|
|
208
|
+
]),
|
|
209
|
+
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
210
|
+
"delivery_time_est_mins": u.weighNumRange(15, 90, 1.2, 40),
|
|
211
|
+
"price_tier": u.pickAWinner(["$", "$$", "$$$", "$$$$"]),
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
event: "item added to cart",
|
|
216
|
+
weight: 14,
|
|
217
|
+
properties: {
|
|
218
|
+
"item_id": u.pickAWinner(itemIds),
|
|
219
|
+
"item_category": u.pickAWinner(["entree", "appetizer", "drink", "dessert", "side"]),
|
|
220
|
+
"item_price": u.weighNumRange(3, 65, 1.0, 40),
|
|
221
|
+
"customization_count": u.weighNumRange(0, 5, 1.5, 20),
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
event: "item removed from cart",
|
|
226
|
+
weight: 5,
|
|
227
|
+
properties: {
|
|
228
|
+
"item_id": u.pickAWinner(itemIds),
|
|
229
|
+
"removal_reason": u.pickAWinner(["changed_mind", "too_expensive", "substitution"]),
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
event: "coupon applied",
|
|
234
|
+
weight: 4,
|
|
235
|
+
properties: {
|
|
236
|
+
"coupon_code": u.pickAWinner(couponCodes),
|
|
237
|
+
"discount_type": u.pickAWinner(["percent", "flat", "free_delivery"]),
|
|
238
|
+
"discount_value": u.weighNumRange(5, 50, 1.2, 20),
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
event: "checkout started",
|
|
243
|
+
weight: 12,
|
|
244
|
+
properties: {
|
|
245
|
+
"cart_total": u.weighNumRange(8, 150, 0.8, 40),
|
|
246
|
+
"items_count": u.weighNumRange(1, 8, 1.2, 20),
|
|
247
|
+
"delivery_address_saved": u.pickAWinner([true, false], 0.7),
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
event: "order placed",
|
|
252
|
+
weight: 10,
|
|
253
|
+
properties: {
|
|
254
|
+
"order_id": u.pickAWinner(orderIds),
|
|
255
|
+
"payment_method": u.pickAWinner(["credit_card", "apple_pay", "google_pay", "paypal", "cash"]),
|
|
256
|
+
"order_total": u.weighNumRange(10, 200, 0.8, 40),
|
|
257
|
+
"tip_amount": u.weighNumRange(0, 30, 1.5, 20),
|
|
258
|
+
"delivery_fee": u.weighNumRange(0, 12, 1.0, 20),
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
event: "order tracked",
|
|
263
|
+
weight: 13,
|
|
264
|
+
properties: {
|
|
265
|
+
"order_id": u.pickAWinner(orderIds),
|
|
266
|
+
"order_status": u.pickAWinner(["confirmed", "preparing", "picked_up", "en_route", "delivered"]),
|
|
267
|
+
"eta_mins": u.weighNumRange(5, 60, 1.0, 30),
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
event: "order delivered",
|
|
272
|
+
weight: 9,
|
|
273
|
+
properties: {
|
|
274
|
+
"order_id": u.pickAWinner(orderIds),
|
|
275
|
+
"actual_delivery_mins": u.weighNumRange(12, 90, 1.0, 40),
|
|
276
|
+
"on_time": u.pickAWinner([true, false], 0.7),
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
event: "order rated",
|
|
281
|
+
weight: 7,
|
|
282
|
+
properties: {
|
|
283
|
+
"order_id": u.pickAWinner(orderIds),
|
|
284
|
+
"food_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
285
|
+
"delivery_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
286
|
+
"would_reorder": u.pickAWinner([true, false], 0.65),
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
event: "search performed",
|
|
291
|
+
weight: 11,
|
|
292
|
+
properties: {
|
|
293
|
+
"search_query": () => chance.pickone([
|
|
294
|
+
"pizza", "sushi", "burger", "tacos", "pad thai",
|
|
295
|
+
"chicken", "salad", "ramen", "pasta", "sandwich",
|
|
296
|
+
"wings", "curry", "pho", "burritos", "steak"
|
|
297
|
+
]),
|
|
298
|
+
"results_count": u.weighNumRange(0, 50, 0.8, 30),
|
|
299
|
+
"search_type": u.pickAWinner(["restaurant", "cuisine", "dish"]),
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
event: "promotion viewed",
|
|
304
|
+
weight: 8,
|
|
305
|
+
properties: {
|
|
306
|
+
"promo_id": () => `promo_${v.uid(5)}`,
|
|
307
|
+
"promo_type": u.pickAWinner(["banner", "push", "in_feed"]),
|
|
308
|
+
"promo_value": u.pickAWinner(["10%", "15%", "20%", "25%", "30%", "40%", "50%"]),
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
event: "subscription started",
|
|
313
|
+
weight: 2,
|
|
314
|
+
properties: {
|
|
315
|
+
"plan": u.pickAWinner(["quickbite_plus_monthly", "quickbite_plus_monthly", "quickbite_plus_annual"]),
|
|
316
|
+
"price": u.pickAWinner([9.99, 9.99, 79.99]),
|
|
317
|
+
"trial": u.pickAWinner([true, false], 0.5),
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
event: "subscription cancelled",
|
|
322
|
+
weight: 1,
|
|
323
|
+
properties: {
|
|
324
|
+
"reason": u.pickAWinner(["too_expensive", "not_ordering_enough", "found_alternative", "bad_experience"]),
|
|
325
|
+
"months_subscribed": u.weighNumRange(1, 24, 1.5, 15),
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
event: "support ticket",
|
|
330
|
+
weight: 3,
|
|
331
|
+
properties: {
|
|
332
|
+
"issue_type": u.pickAWinner(["missing_item", "wrong_order", "late_delivery", "quality_issue", "refund_request"]),
|
|
333
|
+
"order_id": u.pickAWinner(orderIds),
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
event: "reorder initiated",
|
|
338
|
+
weight: 6,
|
|
339
|
+
properties: {
|
|
340
|
+
"order_id": u.pickAWinner(orderIds),
|
|
341
|
+
"original_order_age_days": u.weighNumRange(1, 60, 1.5, 30),
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
|
|
346
|
+
superProps: {
|
|
347
|
+
platform: u.pickAWinner(["iOS", "Android", "Web"]),
|
|
348
|
+
subscription_tier: u.pickAWinner(["Free", "Free", "Free", "Free", "QuickBite+"]),
|
|
349
|
+
city: u.pickAWinner(["New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "San Francisco"]),
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
userProps: {
|
|
353
|
+
"preferred_cuisine": u.pickAWinner([
|
|
354
|
+
"American",
|
|
355
|
+
"Italian",
|
|
356
|
+
"Chinese",
|
|
357
|
+
"Japanese",
|
|
358
|
+
"Mexican",
|
|
359
|
+
"Indian",
|
|
360
|
+
"Thai",
|
|
361
|
+
"Mediterranean"
|
|
362
|
+
]),
|
|
363
|
+
"avg_order_value": u.weighNumRange(15, 80, 0.8, 40),
|
|
364
|
+
"orders_per_month": u.weighNumRange(1, 20, 1.5, 10),
|
|
365
|
+
"favorite_restaurant_count": u.weighNumRange(1, 10),
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
groupKeys: [
|
|
369
|
+
["restaurant_id", 200, ["restaurant viewed", "order placed", "order rated"]],
|
|
370
|
+
],
|
|
371
|
+
|
|
372
|
+
groupProps: {
|
|
373
|
+
restaurant_id: {
|
|
374
|
+
"name": () => `${chance.pickone(["The", "Big", "Lucky", "Golden", "Fresh", "Urban"])} ${chance.pickone(["Kitchen", "Grill", "Bowl", "Wok", "Bistro", "Plate", "Table", "Fork"])}`,
|
|
375
|
+
"cuisine": u.pickAWinner([
|
|
376
|
+
"American",
|
|
377
|
+
"Italian",
|
|
378
|
+
"Chinese",
|
|
379
|
+
"Japanese",
|
|
380
|
+
"Mexican",
|
|
381
|
+
"Indian",
|
|
382
|
+
"Thai",
|
|
383
|
+
"Mediterranean"
|
|
384
|
+
]),
|
|
385
|
+
"avg_rating": u.weighNumRange(1, 5, 0.8, 30),
|
|
386
|
+
"delivery_radius_mi": u.weighNumRange(1, 15, 1.0, 10),
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
lookupTables: [],
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* ARCHITECTED ANALYTICS HOOKS
|
|
394
|
+
*
|
|
395
|
+
* This hook function creates 8 deliberate patterns in the data:
|
|
396
|
+
*
|
|
397
|
+
* 1. LUNCH RUSH CONVERSION: Orders during lunch/dinner hours convert at higher rates
|
|
398
|
+
* 2. COUPON INJECTION: Free-tier users get coupons spliced into funnels
|
|
399
|
+
* 3. LATE NIGHT MUNCHIES: Late-night orders skew to fast food with inflated prices
|
|
400
|
+
* 4. RAINY WEEK SURGE: Days 20-27 have surge pricing and doubled order volume
|
|
401
|
+
* 5. REFERRAL POWER USERS: Referred users reorder more and rate higher
|
|
402
|
+
* 6. TRIAL CONVERSION: Trial subscribers with 3+ early orders are retained
|
|
403
|
+
* 7. SUPPORT TICKET CHURN: 15% of users flagged as high-risk churners
|
|
404
|
+
* 8. FIRST ORDER BONUS: New users in the Order Completion funnel convert at 90%
|
|
405
|
+
*/
|
|
406
|
+
hook: function (record, type, meta) {
|
|
407
|
+
const NOW = dayjs();
|
|
408
|
+
const DATASET_START = NOW.subtract(days, 'days');
|
|
409
|
+
const RAINY_WEEK_START = DATASET_START.add(20, 'days');
|
|
410
|
+
const RAINY_WEEK_END = DATASET_START.add(27, 'days');
|
|
411
|
+
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
413
|
+
// HOOK 1: LUNCH RUSH CONVERSION (funnel-pre)
|
|
414
|
+
// During lunch (11AM-1PM) and dinner (5PM-8PM), funnel conversion
|
|
415
|
+
// rates are boosted to reflect real-world meal-time ordering surges.
|
|
416
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
417
|
+
if (type === "funnel-pre") {
|
|
418
|
+
if (meta && meta.firstEventTime) {
|
|
419
|
+
const hour = dayjs(meta.firstEventTime).hour();
|
|
420
|
+
|
|
421
|
+
record.props = record.props || {};
|
|
422
|
+
|
|
423
|
+
// Lunch rush: 11AM - 1PM
|
|
424
|
+
if (hour >= 11 && hour <= 13) {
|
|
425
|
+
record.conversionRate = record.conversionRate * 1.4;
|
|
426
|
+
record.props.lunch_rush = true;
|
|
427
|
+
record.props.dinner_rush = false;
|
|
428
|
+
}
|
|
429
|
+
// Dinner rush: 5PM - 8PM
|
|
430
|
+
else if (hour >= 17 && hour <= 20) {
|
|
431
|
+
record.conversionRate = record.conversionRate * 1.2;
|
|
432
|
+
record.props.lunch_rush = false;
|
|
433
|
+
record.props.dinner_rush = true;
|
|
434
|
+
} else {
|
|
435
|
+
record.props.lunch_rush = false;
|
|
436
|
+
record.props.dinner_rush = false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════
|
|
441
|
+
// HOOK 8: FIRST ORDER BONUS (funnel-pre)
|
|
442
|
+
// New users (born in dataset) get a massive conversion boost on
|
|
443
|
+
// the Order Completion funnel, simulating first-order promotions
|
|
444
|
+
// and new-user incentives that drive initial purchases.
|
|
445
|
+
// ═══════════════════════════════════════════════════════════════
|
|
446
|
+
if (record.sequence &&
|
|
447
|
+
record.sequence[0] === "checkout started" &&
|
|
448
|
+
record.sequence[1] === "order placed") {
|
|
449
|
+
record.props = record.props || {};
|
|
450
|
+
if (meta && meta.userIsBornInDataset) {
|
|
451
|
+
record.conversionRate = 0.90;
|
|
452
|
+
record.props.first_order_bonus = true;
|
|
453
|
+
} else {
|
|
454
|
+
record.props.first_order_bonus = false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
460
|
+
// HOOK 2: COUPON INJECTION (funnel-post)
|
|
461
|
+
// Free-tier users get coupon_applied events spliced into their
|
|
462
|
+
// funnel sequences 30% of the time, simulating promotional nudges
|
|
463
|
+
// that push non-subscribers toward conversion.
|
|
464
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
465
|
+
if (type === "funnel-post") {
|
|
466
|
+
if (Array.isArray(record) && record.length >= 2) {
|
|
467
|
+
// Check if user is Free tier from first event's super props
|
|
468
|
+
const firstEvent = record[0];
|
|
469
|
+
const isFreeUser = firstEvent && firstEvent.subscription_tier === "Free";
|
|
470
|
+
|
|
471
|
+
if (isFreeUser && chance.bool({ likelihood: 30 })) {
|
|
472
|
+
// Pick a random insertion point between funnel steps
|
|
473
|
+
const insertIdx = chance.integer({ min: 1, max: record.length - 1 });
|
|
474
|
+
const prevEvent = record[insertIdx - 1];
|
|
475
|
+
const nextEvent = record[insertIdx];
|
|
476
|
+
const midTime = dayjs(prevEvent.time).add(
|
|
477
|
+
dayjs(nextEvent.time).diff(dayjs(prevEvent.time)) / 2,
|
|
478
|
+
'milliseconds'
|
|
479
|
+
).toISOString();
|
|
480
|
+
|
|
481
|
+
const couponEvent = {
|
|
482
|
+
event: "coupon applied",
|
|
483
|
+
time: midTime,
|
|
484
|
+
user_id: firstEvent.user_id,
|
|
485
|
+
coupon_code: chance.pickone(couponCodes),
|
|
486
|
+
discount_type: chance.pickone(["percent", "flat", "free_delivery"]),
|
|
487
|
+
discount_value: chance.integer({ min: 10, max: 30 }),
|
|
488
|
+
coupon_injected: true,
|
|
489
|
+
};
|
|
490
|
+
record.splice(insertIdx, 0, couponEvent);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
496
|
+
// HOOK 3: LATE NIGHT MUNCHIES (event)
|
|
497
|
+
// Between 10PM and 2AM, restaurant views and cart additions skew
|
|
498
|
+
// heavily toward fast food (American cuisine) with inflated prices,
|
|
499
|
+
// modeling the real-world late-night ordering pattern.
|
|
500
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
501
|
+
if (type === "event") {
|
|
502
|
+
const EVENT_TIME = dayjs(record.time);
|
|
503
|
+
const hour = EVENT_TIME.hour();
|
|
504
|
+
const isLateNight = hour >= 22 || hour <= 2;
|
|
505
|
+
|
|
506
|
+
if (record.event === "restaurant viewed" || record.event === "item added to cart") {
|
|
507
|
+
if (isLateNight) {
|
|
508
|
+
// 70% of late-night browsing/ordering is fast food (American)
|
|
509
|
+
if (chance.bool({ likelihood: 70 })) {
|
|
510
|
+
if (record.cuisine_type !== undefined) {
|
|
511
|
+
record.cuisine_type = "American";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Boost item price by 1.3x (late-night surcharge effect)
|
|
516
|
+
if (record.item_price !== undefined) {
|
|
517
|
+
record.item_price = Math.round(record.item_price * 1.3 * 100) / 100;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
record.late_night_order = true;
|
|
521
|
+
} else {
|
|
522
|
+
record.late_night_order = false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ═══════════════════════════════════════════════════════════════
|
|
527
|
+
// HOOK 4: RAINY WEEK SURGE (event)
|
|
528
|
+
// During days 20-27, delivery fees double on order_placed events
|
|
529
|
+
// and there is a 40% chance of duplicating the event, simulating
|
|
530
|
+
// a weather-driven demand surge with surge pricing.
|
|
531
|
+
// ═══════════════════════════════════════════════════════════════
|
|
532
|
+
if (record.event === "order placed") {
|
|
533
|
+
if (EVENT_TIME.isAfter(RAINY_WEEK_START) && EVENT_TIME.isBefore(RAINY_WEEK_END)) {
|
|
534
|
+
record.delivery_fee = (record.delivery_fee || 5) * 2;
|
|
535
|
+
record.surge_pricing = true;
|
|
536
|
+
|
|
537
|
+
// 40% chance to duplicate the event (more orders during rain)
|
|
538
|
+
if (chance.bool({ likelihood: 40 })) {
|
|
539
|
+
const duplicateEvent = {
|
|
540
|
+
event: "order placed",
|
|
541
|
+
time: EVENT_TIME.add(chance.integer({ min: 5, max: 60 }), 'minutes').toISOString(),
|
|
542
|
+
user_id: record.user_id,
|
|
543
|
+
order_id: chance.pickone(orderIds),
|
|
544
|
+
payment_method: chance.pickone(["credit_card", "apple_pay", "google_pay"]),
|
|
545
|
+
order_total: chance.integer({ min: 15, max: 120 }),
|
|
546
|
+
tip_amount: chance.integer({ min: 0, max: 20 }),
|
|
547
|
+
delivery_fee: chance.integer({ min: 6, max: 24 }),
|
|
548
|
+
surge_pricing: true,
|
|
549
|
+
rainy_week: true,
|
|
550
|
+
};
|
|
551
|
+
return [record, duplicateEvent];
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
record.surge_pricing = false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
560
|
+
// HOOK 5: REFERRAL POWER USERS (everything)
|
|
561
|
+
// Users who signed up with a referral code get 2x more reorder
|
|
562
|
+
// events and boosted food ratings. Referred users are more loyal
|
|
563
|
+
// and satisfied, mirroring real referral program outcomes.
|
|
564
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
565
|
+
if (type === "everything") {
|
|
566
|
+
const userEvents = record;
|
|
567
|
+
|
|
568
|
+
// First pass: identify user patterns
|
|
569
|
+
let isReferralUser = false;
|
|
570
|
+
let hasTrialSubscription = false;
|
|
571
|
+
let earlyOrderCount = 0;
|
|
572
|
+
let firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
573
|
+
|
|
574
|
+
userEvents.forEach((event) => {
|
|
575
|
+
const eventTime = dayjs(event.time);
|
|
576
|
+
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
577
|
+
|
|
578
|
+
// Hook #5: Track referral users
|
|
579
|
+
if (event.event === "account created" && event.referral_code === true) {
|
|
580
|
+
isReferralUser = true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Hook #6: Track trial subscribers and early orders
|
|
584
|
+
if (event.event === "subscription started" && event.trial === true) {
|
|
585
|
+
hasTrialSubscription = true;
|
|
586
|
+
}
|
|
587
|
+
if (event.event === "order placed" && daysSinceStart <= 14) {
|
|
588
|
+
earlyOrderCount++;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Second pass: apply referral power user effects
|
|
593
|
+
userEvents.forEach((event, idx) => {
|
|
594
|
+
if (event.event === "order rated") {
|
|
595
|
+
if (isReferralUser) {
|
|
596
|
+
event.food_rating = chance.integer({ min: 4, max: 5 });
|
|
597
|
+
event.referral_user = true;
|
|
598
|
+
} else {
|
|
599
|
+
event.referral_user = false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (isReferralUser && event.event === "reorder initiated" && chance.bool({ likelihood: 50 })) {
|
|
603
|
+
const eventTime = dayjs(event.time);
|
|
604
|
+
const extraReorder = {
|
|
605
|
+
event: "reorder initiated",
|
|
606
|
+
time: eventTime.add(chance.integer({ min: 1, max: 7 }), 'days').toISOString(),
|
|
607
|
+
user_id: event.user_id,
|
|
608
|
+
order_id: chance.pickone(orderIds),
|
|
609
|
+
original_order_age_days: chance.integer({ min: 3, max: 30 }),
|
|
610
|
+
referral_user: true,
|
|
611
|
+
};
|
|
612
|
+
userEvents.splice(idx + 1, 0, extraReorder);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ═══════════════════════════════════════════════════════════════
|
|
617
|
+
// HOOK 6: TRIAL CONVERSION (everything)
|
|
618
|
+
// Trial subscribers who place 3+ orders in their first 14 days
|
|
619
|
+
// are retained. Those with fewer orders churn: 60% of their
|
|
620
|
+
// events after day 14 are removed, simulating subscription
|
|
621
|
+
// abandonment after a failed trial experience.
|
|
622
|
+
// ═══════════════════════════════════════════════════════════════
|
|
623
|
+
if (hasTrialSubscription) {
|
|
624
|
+
const trialCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
625
|
+
|
|
626
|
+
if (earlyOrderCount >= 3) {
|
|
627
|
+
// Retained: mark events as trial_retained
|
|
628
|
+
userEvents.forEach((event) => {
|
|
629
|
+
event.trial_retained = true;
|
|
630
|
+
});
|
|
631
|
+
} else {
|
|
632
|
+
// Churned: remove 60% of events after day 14
|
|
633
|
+
userEvents.forEach((event) => {
|
|
634
|
+
event.trial_retained = false;
|
|
635
|
+
});
|
|
636
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
637
|
+
const evt = userEvents[i];
|
|
638
|
+
if (trialCutoff && dayjs(evt.time).isAfter(trialCutoff)) {
|
|
639
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
640
|
+
userEvents.splice(i, 1);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
649
|
+
// HOOK 7: SUPPORT TICKET CHURN (user)
|
|
650
|
+
// 15% of users are flagged as high-risk with elevated churn scores.
|
|
651
|
+
// The remaining users get low churn scores. This creates a clear
|
|
652
|
+
// segmentation opportunity for proactive retention campaigns.
|
|
653
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
654
|
+
if (type === "user") {
|
|
655
|
+
if (chance.bool({ likelihood: 15 })) {
|
|
656
|
+
record.is_high_risk = true;
|
|
657
|
+
record.churn_risk_score = chance.integer({ min: 70, max: 100 });
|
|
658
|
+
} else {
|
|
659
|
+
record.is_high_risk = false;
|
|
660
|
+
record.churn_risk_score = chance.integer({ min: 0, max: 40 });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return record;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
export default config;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
672
|
+
* NEEDLE IN A HAYSTACK - QUICKBITE FOOD DELIVERY ANALYTICS
|
|
673
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
674
|
+
*
|
|
675
|
+
* A food delivery app dungeon with 8 deliberately architected analytics
|
|
676
|
+
* insights hidden in the data. This dungeon is designed to showcase advanced
|
|
677
|
+
* product analytics patterns and demonstrate how to find "needles" (meaningful
|
|
678
|
+
* insights) in "haystacks" (large datasets).
|
|
679
|
+
*
|
|
680
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
681
|
+
* DATASET OVERVIEW
|
|
682
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
683
|
+
*
|
|
684
|
+
* - 5,000 users over 100 days
|
|
685
|
+
* - 360K events across 17 event types
|
|
686
|
+
* - 3 funnels (onboarding, order completion, discovery to order)
|
|
687
|
+
* - Group analytics (restaurants)
|
|
688
|
+
* - Lookup tables (menu items, coupon codes)
|
|
689
|
+
* - Subscription tiers (Free, QuickBite+)
|
|
690
|
+
*
|
|
691
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
692
|
+
* THE 8 ARCHITECTED HOOKS
|
|
693
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
694
|
+
*
|
|
695
|
+
* Each hook creates a specific, discoverable analytics insight that simulates
|
|
696
|
+
* real-world product behavior patterns.
|
|
697
|
+
*
|
|
698
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
699
|
+
* 1. LUNCH RUSH CONVERSION (funnel-pre)
|
|
700
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
701
|
+
*
|
|
702
|
+
* PATTERN: Funnel conversion rates are boosted during meal-time hours.
|
|
703
|
+
* Lunch (11AM-1PM) gets a 1.4x multiplier; dinner (5PM-8PM) gets 1.2x.
|
|
704
|
+
* Events during these windows carry lunch_rush or dinner_rush properties.
|
|
705
|
+
*
|
|
706
|
+
* HOW TO FIND IT:
|
|
707
|
+
* - Break down funnel conversion by hour of day
|
|
708
|
+
* - Compare conversion rates during 11AM-1PM vs. off-peak hours
|
|
709
|
+
* - Filter events where lunch_rush = true or dinner_rush = true
|
|
710
|
+
*
|
|
711
|
+
* EXPECTED INSIGHT: Lunch hour funnels convert ~40% better than baseline.
|
|
712
|
+
* Dinner hour funnels convert ~20% better. Clear time-of-day pattern in
|
|
713
|
+
* the checkout-to-order-to-delivery pipeline.
|
|
714
|
+
*
|
|
715
|
+
* REAL-WORLD ANALOGUE: Food delivery apps see massive demand spikes during
|
|
716
|
+
* traditional meal times. Users ordering during these windows have higher
|
|
717
|
+
* intent and thus higher conversion rates.
|
|
718
|
+
*
|
|
719
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
720
|
+
* 2. COUPON INJECTION (funnel-post)
|
|
721
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
722
|
+
*
|
|
723
|
+
* PATTERN: Free-tier users get coupon_applied events spliced into their
|
|
724
|
+
* funnel sequences 30% of the time. These injected coupons carry the
|
|
725
|
+
* coupon_injected: true property to distinguish them from organic usage.
|
|
726
|
+
*
|
|
727
|
+
* HOW TO FIND IT:
|
|
728
|
+
* - Segment by subscription_tier = "Free"
|
|
729
|
+
* - Look for coupon_applied events with coupon_injected = true
|
|
730
|
+
* - Compare funnel completion rates for Free users with/without coupons
|
|
731
|
+
*
|
|
732
|
+
* EXPECTED INSIGHT: ~30% of Free users receive promotional coupons mid-funnel.
|
|
733
|
+
* These users should show different downstream conversion behavior, simulating
|
|
734
|
+
* how promotional nudges affect the purchase funnel.
|
|
735
|
+
*
|
|
736
|
+
* REAL-WORLD ANALOGUE: Apps frequently inject promotional offers (push
|
|
737
|
+
* notifications, in-app banners) to non-paying users during key funnel
|
|
738
|
+
* moments to boost conversion.
|
|
739
|
+
*
|
|
740
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
741
|
+
* 3. LATE NIGHT MUNCHIES (event)
|
|
742
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
743
|
+
*
|
|
744
|
+
* PATTERN: Between 10PM and 2AM, restaurant views and cart additions skew
|
|
745
|
+
* 70% toward American (fast food) cuisine. Item prices are boosted by 1.3x.
|
|
746
|
+
* Events carry late_night_order: true.
|
|
747
|
+
*
|
|
748
|
+
* HOW TO FIND IT:
|
|
749
|
+
* - Break down restaurant_viewed by hour of day and cuisine_type
|
|
750
|
+
* - Compare cuisine distribution at 10PM-2AM vs. daytime
|
|
751
|
+
* - Filter late_night_order = true and compare average item_price
|
|
752
|
+
*
|
|
753
|
+
* EXPECTED INSIGHT: Late-night orders are overwhelmingly fast food with
|
|
754
|
+
* higher average prices. The cuisine distribution at night is dramatically
|
|
755
|
+
* different from daytime patterns.
|
|
756
|
+
*
|
|
757
|
+
* REAL-WORLD ANALOGUE: Late-night food delivery is dominated by fast food
|
|
758
|
+
* and comfort food. Many platforms charge late-night surcharges or see
|
|
759
|
+
* naturally inflated basket sizes during these hours.
|
|
760
|
+
*
|
|
761
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
762
|
+
* 4. RAINY WEEK SURGE (event)
|
|
763
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
764
|
+
*
|
|
765
|
+
* PATTERN: During days 20-27 of the dataset, a simulated rainy week causes:
|
|
766
|
+
* - Delivery fees double on order_placed events
|
|
767
|
+
* - surge_pricing: true flag added
|
|
768
|
+
* - 40% chance of duplicate order events (demand surge)
|
|
769
|
+
*
|
|
770
|
+
* HOW TO FIND IT:
|
|
771
|
+
* - Chart order_placed event count by day
|
|
772
|
+
* - Filter days 20-27 and compare delivery_fee averages
|
|
773
|
+
* - Look for surge_pricing = true and rainy_week = true properties
|
|
774
|
+
*
|
|
775
|
+
* EXPECTED INSIGHT: Clear spike in order volume around days 20-27, with
|
|
776
|
+
* doubled delivery fees and surge pricing markers. The demand surge is
|
|
777
|
+
* visible as a ~40% increase in order_placed event volume.
|
|
778
|
+
*
|
|
779
|
+
* REAL-WORLD ANALOGUE: Weather events drive significant demand surges for
|
|
780
|
+
* food delivery. Platforms respond with surge pricing on delivery fees,
|
|
781
|
+
* and order volume increases as people avoid going out.
|
|
782
|
+
*
|
|
783
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
784
|
+
* 5. REFERRAL POWER USERS (everything)
|
|
785
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
786
|
+
*
|
|
787
|
+
* PATTERN: Users whose account_created event has referral_code = true:
|
|
788
|
+
* - Get 2x more reorder_initiated events (higher loyalty)
|
|
789
|
+
* - Have food_rating boosted to 4-5 (higher satisfaction)
|
|
790
|
+
* - Carry referral_user: true on affected events
|
|
791
|
+
*
|
|
792
|
+
* HOW TO FIND IT:
|
|
793
|
+
* - Segment users by: referral_code = true on account_created
|
|
794
|
+
* - Compare: reorder_initiated event count per user
|
|
795
|
+
* - Compare: average food_rating on order_rated events
|
|
796
|
+
*
|
|
797
|
+
* EXPECTED INSIGHT: Referred users reorder roughly 2x more often and rate
|
|
798
|
+
* food 4-5 stars consistently. They represent a higher-LTV, more-satisfied
|
|
799
|
+
* segment of the user base.
|
|
800
|
+
*
|
|
801
|
+
* REAL-WORLD ANALOGUE: Referral programs consistently produce higher-quality
|
|
802
|
+
* users. Referred customers have lower acquisition costs, higher retention,
|
|
803
|
+
* and higher satisfaction because they come with built-in social proof.
|
|
804
|
+
*
|
|
805
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
806
|
+
* 6. TRIAL CONVERSION (everything)
|
|
807
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
808
|
+
*
|
|
809
|
+
* PATTERN: Users with subscription_started where trial = true are evaluated:
|
|
810
|
+
* - If they place 3+ orders in their first 14 days: retained (all events kept)
|
|
811
|
+
* - If fewer than 3 orders: churned (60% of events after day 14 removed)
|
|
812
|
+
* - Retained users carry trial_retained: true
|
|
813
|
+
*
|
|
814
|
+
* HOW TO FIND IT:
|
|
815
|
+
* - Segment users by: subscription_started with trial = true
|
|
816
|
+
* - Count order_placed events within first 14 days per user
|
|
817
|
+
* - Compare: event volume after day 14 for 3+ orders vs. <3 orders
|
|
818
|
+
* - Look for trial_retained = true property
|
|
819
|
+
*
|
|
820
|
+
* EXPECTED INSIGHT: Trial users who place 3+ early orders show sustained
|
|
821
|
+
* engagement. Those who don't show a dramatic drop-off after day 14, with
|
|
822
|
+
* 60% fewer events. The "magic number" for trial conversion is 3 orders.
|
|
823
|
+
*
|
|
824
|
+
* REAL-WORLD ANALOGUE: Subscription services often find a "magic number"
|
|
825
|
+
* of early actions that predict long-term retention. For food delivery,
|
|
826
|
+
* this is typically a threshold of orders during the trial period.
|
|
827
|
+
*
|
|
828
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
829
|
+
* 7. SUPPORT TICKET CHURN (user)
|
|
830
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
831
|
+
*
|
|
832
|
+
* PATTERN: 15% of users are flagged as is_high_risk = true with a
|
|
833
|
+
* churn_risk_score between 70-100. The remaining 85% get scores of 0-40.
|
|
834
|
+
* This creates a binary segmentation opportunity for retention teams.
|
|
835
|
+
*
|
|
836
|
+
* HOW TO FIND IT:
|
|
837
|
+
* - Segment users by: is_high_risk = true
|
|
838
|
+
* - Compare: churn_risk_score distribution
|
|
839
|
+
* - Cross-reference: support_ticket event frequency for high-risk users
|
|
840
|
+
* - Compare: retention and ordering behavior by risk segment
|
|
841
|
+
*
|
|
842
|
+
* EXPECTED INSIGHT: 15% of users have churn scores above 70, creating a
|
|
843
|
+
* clear at-risk segment. These users likely correlate with higher support
|
|
844
|
+
* ticket rates and lower order frequency.
|
|
845
|
+
*
|
|
846
|
+
* REAL-WORLD ANALOGUE: Customer health scoring is used by subscription
|
|
847
|
+
* businesses to identify at-risk users for proactive retention outreach.
|
|
848
|
+
* ML models in production generate similar risk scores from behavioral data.
|
|
849
|
+
*
|
|
850
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
851
|
+
* 8. FIRST ORDER BONUS (funnel-pre)
|
|
852
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
853
|
+
*
|
|
854
|
+
* PATTERN: For the Order Completion funnel (checkout started -> order placed
|
|
855
|
+
* -> order delivered), new users (born in dataset) get their conversion rate
|
|
856
|
+
* boosted to 90%. Events carry first_order_bonus: true.
|
|
857
|
+
*
|
|
858
|
+
* HOW TO FIND IT:
|
|
859
|
+
* - Segment the Order Completion funnel by new vs. existing users
|
|
860
|
+
* - Compare: conversion rates for new users vs. returning users
|
|
861
|
+
* - Filter: first_order_bonus = true
|
|
862
|
+
*
|
|
863
|
+
* EXPECTED INSIGHT: New users convert at ~90% through the order funnel,
|
|
864
|
+
* dramatically higher than the 60% baseline. This simulates first-order
|
|
865
|
+
* promotions (free delivery, $10 off) that most food delivery apps offer.
|
|
866
|
+
*
|
|
867
|
+
* REAL-WORLD ANALOGUE: Every major food delivery app offers aggressive
|
|
868
|
+
* first-order incentives. These dramatically boost initial conversion but
|
|
869
|
+
* the real question is whether those users return (see Hook #6).
|
|
870
|
+
*
|
|
871
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
872
|
+
* ADVANCED ANALYSIS IDEAS
|
|
873
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
874
|
+
*
|
|
875
|
+
* CROSS-HOOK PATTERNS:
|
|
876
|
+
*
|
|
877
|
+
* 1. The Perfect Customer: Users who:
|
|
878
|
+
* - Signed up via referral (Hook #5)
|
|
879
|
+
* - Started a trial and placed 3+ early orders (Hook #6)
|
|
880
|
+
* - Order during lunch rush (Hook #1)
|
|
881
|
+
* - Have low churn risk score (Hook #7)
|
|
882
|
+
* These users should have exceptional LTV and retention metrics.
|
|
883
|
+
*
|
|
884
|
+
* 2. Rainy Night Double Whammy: Do late-night orders during rainy week
|
|
885
|
+
* (Hook #3 + Hook #4) show compounded surge pricing effects?
|
|
886
|
+
*
|
|
887
|
+
* 3. Coupon-Driven Trial Conversion: Do free users who receive injected
|
|
888
|
+
* coupons (Hook #2) eventually start trials (Hook #6)?
|
|
889
|
+
*
|
|
890
|
+
* 4. Referral + First Order: Referred users (Hook #5) who also get the
|
|
891
|
+
* first order bonus (Hook #8) should have the highest conversion rates.
|
|
892
|
+
*
|
|
893
|
+
* 5. Support Tickets and Churn Risk: Do high-risk users (Hook #7) file
|
|
894
|
+
* more support tickets, and does this correlate with rainy week
|
|
895
|
+
* late deliveries (Hook #4)?
|
|
896
|
+
*
|
|
897
|
+
* COHORT ANALYSIS:
|
|
898
|
+
*
|
|
899
|
+
* - Cohort by signup week: Users who started during rainy week (days 20-27)
|
|
900
|
+
* should show different ordering patterns
|
|
901
|
+
* - Cohort by referral source: Referred vs. organic user lifecycle comparison
|
|
902
|
+
* - Cohort by subscription tier: Free vs. QuickBite+ across all metrics
|
|
903
|
+
* - Cohort by city: Do different cities show different cuisine preferences
|
|
904
|
+
* and ordering patterns?
|
|
905
|
+
*
|
|
906
|
+
* FUNNEL ANALYSIS:
|
|
907
|
+
*
|
|
908
|
+
* - Onboarding Funnel: How quickly do users go from account creation to
|
|
909
|
+
* first restaurant view? Break down by signup method.
|
|
910
|
+
* - Order Funnel: Compare checkout-to-delivery completion by platform,
|
|
911
|
+
* subscription tier, and time of day.
|
|
912
|
+
* - Discovery Funnel: How does search type (restaurant vs. cuisine vs. dish)
|
|
913
|
+
* affect downstream conversion to ordering?
|
|
914
|
+
*
|
|
915
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
916
|
+
* EXPECTED METRICS SUMMARY
|
|
917
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
918
|
+
*
|
|
919
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
920
|
+
* ──────────────────────|──────────────────────|──────────|─────────────|──────
|
|
921
|
+
* Lunch Rush | Funnel conversion | 60% | 84% | 1.4x
|
|
922
|
+
* Coupon Injection | Free user coupons | 0% | 30% | N/A
|
|
923
|
+
* Late Night Munchies | American cuisine % | ~15% | 70% | ~4.7x
|
|
924
|
+
* Rainy Week Surge | Order volume | 100% | 140% | 1.4x
|
|
925
|
+
* Referral Power Users | Reorder frequency | 1x | 2x | 2.0x
|
|
926
|
+
* Trial Conversion | Post-trial retention | 100% | 40% | 0.4x
|
|
927
|
+
* Support Ticket Churn | High-risk users | 0% | 15% | N/A
|
|
928
|
+
* First Order Bonus | New user conversion | 60% | 90% | 1.5x
|
|
929
|
+
*
|
|
930
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
931
|
+
* HOW TO RUN THIS DUNGEON
|
|
932
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
933
|
+
*
|
|
934
|
+
* From the dm4 root directory:
|
|
935
|
+
*
|
|
936
|
+
* npm start
|
|
937
|
+
*
|
|
938
|
+
* Or programmatically:
|
|
939
|
+
*
|
|
940
|
+
* import generate from './index.js';
|
|
941
|
+
* import config from './dungeons/harness-food.js';
|
|
942
|
+
* const results = await generate(config);
|
|
943
|
+
*
|
|
944
|
+
* OUTPUT FILES (with writeToDisk: true, format: "json", gzip: true):
|
|
945
|
+
*
|
|
946
|
+
* - needle-haystack-food__events.json.gz - All event data
|
|
947
|
+
* - needle-haystack-food__user_profiles.json.gz - User profiles
|
|
948
|
+
* - needle-haystack-food__group_profiles.json.gz - Restaurant profiles
|
|
949
|
+
* - needle-haystack-food__item_id_lookup.json.gz - Menu item catalog
|
|
950
|
+
* - needle-haystack-food__coupon_code_lookup.json.gz - Coupon catalog
|
|
951
|
+
*
|
|
952
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
953
|
+
* TESTING YOUR ANALYTICS PLATFORM
|
|
954
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
955
|
+
*
|
|
956
|
+
* This dungeon is perfect for testing:
|
|
957
|
+
*
|
|
958
|
+
* 1. Time-of-Day Analysis: Can you detect the lunch and dinner rush patterns?
|
|
959
|
+
* 2. Promotional Impact: Can you measure the effect of injected coupons?
|
|
960
|
+
* 3. Behavioral Shifts: Can you discover the late-night cuisine preferences?
|
|
961
|
+
* 4. Anomaly Detection: Can you spot the rainy week demand surge?
|
|
962
|
+
* 5. Referral Analysis: Can you quantify the referral user advantage?
|
|
963
|
+
* 6. Trial Optimization: Can you find the "magic number" of early orders?
|
|
964
|
+
* 7. Churn Prediction: Can you identify high-risk users before they leave?
|
|
965
|
+
* 8. New User Funnels: Can you measure the first-order bonus impact?
|
|
966
|
+
*
|
|
967
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
968
|
+
* WHY "NEEDLE IN A HAYSTACK"?
|
|
969
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
970
|
+
*
|
|
971
|
+
* Each hook is a "needle" - a meaningful, actionable insight hidden in a
|
|
972
|
+
* "haystack" of 360K events. The challenge is:
|
|
973
|
+
*
|
|
974
|
+
* 1. FINDING the needles (discovery)
|
|
975
|
+
* 2. VALIDATING they're real patterns (statistical significance)
|
|
976
|
+
* 3. UNDERSTANDING why they matter (business impact)
|
|
977
|
+
* 4. ACTING on them (product decisions)
|
|
978
|
+
*
|
|
979
|
+
* This mirrors real-world product analytics: your data contains valuable insights,
|
|
980
|
+
* but you need the right tools and skills to find them.
|
|
981
|
+
*
|
|
982
|
+
* Happy Hunting!
|
|
983
|
+
*
|
|
984
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
985
|
+
*/
|