@ak--47/dungeon-master 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +7 -7
  2. package/dungeons/technical/ad-spend-schema.json +128 -0
  3. package/dungeons/technical/ad-spend.js +116 -0
  4. package/dungeons/technical/anonymous-users-schema.json +92 -0
  5. package/dungeons/technical/anonymous-users.js +102 -0
  6. package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
  7. package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
  8. package/dungeons/technical/experiments-schema.json +203 -0
  9. package/dungeons/technical/experiments.js +168 -0
  10. package/dungeons/technical/foobar-schema.json +362 -0
  11. package/dungeons/{foobar.js → technical/foobar.js} +3 -3
  12. package/dungeons/technical/group-analytics-schema.json +241 -0
  13. package/dungeons/technical/group-analytics.js +151 -0
  14. package/dungeons/technical/mirror-strategies-schema.json +84 -0
  15. package/dungeons/technical/mirror-strategies.js +126 -0
  16. package/dungeons/technical/nested-objects-schema.json +145 -0
  17. package/dungeons/technical/nested-objects.js +207 -0
  18. package/dungeons/technical/retention-cadence-schema.json +37 -0
  19. package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
  20. package/dungeons/technical/sanity-schema.json +185 -0
  21. package/dungeons/{sanity.js → technical/sanity.js} +4 -2
  22. package/dungeons/technical/scale-test-schema.json +70 -0
  23. package/dungeons/technical/scale-test.js +90 -0
  24. package/dungeons/technical/scd-schema.json +467 -0
  25. package/dungeons/{scd.js → technical/scd.js} +20 -10
  26. package/dungeons/technical/simple-schema.json +362 -0
  27. package/dungeons/{simple.js → technical/simple.js} +7 -7
  28. package/dungeons/technical/simplest-schema.json +724 -0
  29. package/dungeons/{simplest.js → technical/simplest.js} +7 -7
  30. package/dungeons/technical/text-generation-schema.json +1062 -0
  31. package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
  32. package/dungeons/vertical/community-schema.json +579 -0
  33. package/dungeons/vertical/community.js +794 -0
  34. package/dungeons/vertical/devtools-schema.json +601 -0
  35. package/dungeons/vertical/devtools.js +843 -0
  36. package/dungeons/vertical/ecommerce-schema.json +604 -0
  37. package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
  38. package/dungeons/vertical/education-schema.json +5686 -0
  39. package/dungeons/{education.js → vertical/education.js} +70 -39
  40. package/dungeons/vertical/fintech-schema.json +630 -0
  41. package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
  42. package/dungeons/vertical/fitness-schema.json +530 -0
  43. package/dungeons/vertical/fitness.js +808 -0
  44. package/dungeons/vertical/food-delivery-schema.json +36728 -0
  45. package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
  46. package/dungeons/vertical/gaming-schema.json +438 -0
  47. package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
  48. package/dungeons/vertical/healthcare-schema.json +549 -0
  49. package/dungeons/vertical/healthcare.js +775 -0
  50. package/dungeons/vertical/insurance-application-schema.json +485 -0
  51. package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
  52. package/dungeons/vertical/logistics-schema.json +574 -0
  53. package/dungeons/vertical/logistics.js +799 -0
  54. package/dungeons/vertical/marketplace-schema.json +533 -0
  55. package/dungeons/vertical/marketplace.js +850 -0
  56. package/dungeons/vertical/media-schema.json +4749 -0
  57. package/dungeons/{media.js → vertical/media.js} +103 -19
  58. package/dungeons/vertical/rpg-schema.json +2491 -0
  59. package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
  60. package/dungeons/vertical/sass-schema.json +3128 -0
  61. package/dungeons/{sass.js → vertical/sass.js} +65 -26
  62. package/dungeons/vertical/social-schema.json +620 -0
  63. package/dungeons/{social.js → vertical/social.js} +76 -17
  64. package/dungeons/vertical/travel-schema.json +580 -0
  65. package/dungeons/vertical/travel.js +742 -0
  66. package/index.js +20 -16
  67. package/lib/core/config-validator.js +286 -1
  68. package/lib/core/context.js +16 -5
  69. package/lib/core/storage.js +2 -0
  70. package/lib/generators/adspend.js +3 -3
  71. package/lib/generators/events.js +141 -14
  72. package/lib/generators/funnels.js +67 -7
  73. package/lib/generators/text.js +2 -2
  74. package/lib/orchestrators/user-loop.js +444 -22
  75. package/lib/templates/soup-presets.js +2 -2
  76. package/lib/utils/utils.js +4 -5
  77. package/package.json +4 -3
  78. package/scripts/dungeon-to-json.mjs +163 -0
  79. package/scripts/extract-dungeon-schema.mjs +343 -0
  80. package/scripts/json-to-dungeon.mjs +200 -0
  81. package/scripts/run-dungeon.mjs +76 -0
  82. package/scripts/run-many.mjs +118 -0
  83. package/scripts/verify-runner.mjs +42 -0
  84. package/types.d.ts +316 -1
  85. package/dungeons/array-of-object-lookup-schema.json +0 -327
  86. package/dungeons/ecommerce-schema.json +0 -462
  87. package/dungeons/education-schema.json +0 -2409
  88. package/dungeons/fintech-schema.json +0 -14034
  89. package/dungeons/foobar-schema.json +0 -403
  90. package/dungeons/food-delivery-schema.json +0 -192
  91. package/dungeons/food-delivery.js +0 -602
  92. package/dungeons/food-schema.json +0 -1152
  93. package/dungeons/gaming-schema.json +0 -1270
  94. package/dungeons/insurance-application-schema.json +0 -204
  95. package/dungeons/media-schema.json +0 -906
  96. package/dungeons/retention-cadence-schema.json +0 -78
  97. package/dungeons/rpg-schema.json +0 -4526
  98. package/dungeons/sanity-schema.json +0 -255
  99. package/dungeons/sass-schema.json +0 -1291
  100. package/dungeons/scd-schema.json +0 -919
  101. package/dungeons/simple-schema.json +0 -608
  102. package/dungeons/simplest-schema.json +0 -1418
  103. package/dungeons/social-schema.json +0 -1118
  104. package/dungeons/text-generation-schema.json +0 -3096
@@ -0,0 +1,742 @@
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 = "dm4-travel";
8
+ dayjs.extend(utc);
9
+ const chance = u.initChance(SEED);
10
+ const num_users = 5_000;
11
+ const days = 100;
12
+ const NOW = dayjs();
13
+ const DATASET_START = NOW.subtract(days, "days");
14
+
15
+ /** @typedef {import("../../types").Dungeon} Config */
16
+
17
+ const hotelIds = v.range(1, 200).map(() => `HTL_${v.uid(6)}`);
18
+ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona", "Dubai", "Sydney", "Rome", "Bangkok", "Cancun", "Bali", "Amsterdam", "Miami", "Singapore", "Lisbon"];
19
+
20
+ /**
21
+ * ═══════════════════════════════════════════════════════════════
22
+ * DATASET OVERVIEW
23
+ * ═══════════════════════════════════════════════════════════════
24
+ *
25
+ * StayQuest — a hotel booking platform for business and leisure travelers.
26
+ * Users search destinations, compare hotels, book rooms, and leave reviews.
27
+ *
28
+ * - 5,000 users over 100 days, ~600K events
29
+ * - Segments: business travelers (weekday), leisure families, luxury, budget
30
+ * - Core loop: search → view hotel → compare → book → stay → review
31
+ * - Revenue: commission per booking + premium loyalty membership
32
+ *
33
+ * Advanced Features:
34
+ * - Personas: 4 traveler archetypes with different booking behaviors
35
+ * - World Events: summer sale (day 40), hurricane disruption (day 65), loyalty launch (day 50)
36
+ * - Engagement Decay: exponential with 90-day half-life (travel is infrequent)
37
+ * - Attribution: Google Hotels, Instagram, TripAdvisor, organic
38
+ * - Geo: North America, Europe, Asia-Pacific with timezone + currency
39
+ * - Anomalies: whale bookings, coordinated group booking spike
40
+ */
41
+
42
+ /**
43
+ * ═══════════════════════════════════════════════════════════════
44
+ * ANALYTICS HOOKS (8 hooks)
45
+ * ═══════════════════════════════════════════════════════════════
46
+ *
47
+ * ───────────────────────────────────────────────────────────────
48
+ * 1. WEEKEND LEISURE SURGE (event hook)
49
+ * ───────────────────────────────────────────────────────────────
50
+ * PATTERN: Weekend bookings (Fri-Sun) get 1.3x higher nightly_rate
51
+ * due to leisure demand pricing.
52
+ *
53
+ * HOW TO FIND IT IN MIXPANEL:
54
+ * Report 1: Weekend vs Weekday Rates
55
+ * • Report type: Insights
56
+ * • Event: "booking completed"
57
+ * • Measure: Average of "nightly_rate"
58
+ * • Breakdown: Day of week
59
+ * • Expected: Fri-Sun avg ≈ $195 vs Mon-Thu avg ≈ $150
60
+ *
61
+ * REAL-WORLD ANALOGUE: Hotels use dynamic pricing with higher
62
+ * weekend rates driven by leisure traveler demand.
63
+ *
64
+ * ───────────────────────────────────────────────────────────────
65
+ * 2. ADVANCE BOOKING DISCOUNT (event hook)
66
+ * ───────────────────────────────────────────────────────────────
67
+ * PATTERN: Bookings made > 21 days before the dataset end get 0.8x
68
+ * nightly_rate (advance purchase discount). Last-minute bookings
69
+ * (< 3 days) get 1.4x nightly_rate.
70
+ *
71
+ * HOW TO FIND IT IN MIXPANEL:
72
+ * Report 1: Rate by Booking Window
73
+ * • Report type: Insights
74
+ * • Event: "booking completed"
75
+ * • Measure: Average of "nightly_rate"
76
+ * • Breakdown: "booking_window"
77
+ * • Expected: last_minute ≈ $210 vs advance ≈ $120 (1.75x)
78
+ *
79
+ * REAL-WORLD ANALOGUE: Hotels offer early-bird discounts and charge
80
+ * premiums for last-minute availability.
81
+ *
82
+ * ───────────────────────────────────────────────────────────────
83
+ * 3. LOYALTY TIER UPGRADE PATH (everything hook)
84
+ * ───────────────────────────────────────────────────────────────
85
+ * PATTERN: Users with 5+ bookings get boosted loyalty_points on all
86
+ * booking events (3x the baseline).
87
+ *
88
+ * HOW TO FIND IT IN MIXPANEL:
89
+ * Report 1: Loyalty Points by Booking Frequency
90
+ * • Report type: Insights
91
+ * • Event: "booking completed"
92
+ * • Measure: Average of "loyalty_points"
93
+ * • Breakdown: user property "customer_segment"
94
+ * • Expected: power users ≈ 3x points vs casuals
95
+ *
96
+ * REAL-WORLD ANALOGUE: Hotel loyalty programs accelerate rewards
97
+ * for frequent guests, creating a flywheel.
98
+ *
99
+ * ───────────────────────────────────────────────────────────────
100
+ * 4. CANCELLATION BY BOOKING WINDOW (everything hook)
101
+ * ───────────────────────────────────────────────────────────────
102
+ * PATTERN: Users who book last-minute (< 7 days out) have 40% of
103
+ * their "booking cancelled" events dropped — they rarely cancel.
104
+ * Advance bookers keep all cancellation events.
105
+ *
106
+ * HOW TO FIND IT IN MIXPANEL:
107
+ * Report 1: Cancellation Rate by Window
108
+ * • Report type: Funnels
109
+ * • Steps: "booking completed" → "booking cancelled"
110
+ * • Breakdown: "booking_window"
111
+ * • Expected: last_minute ~8% cancel vs advance ~20% cancel
112
+ *
113
+ * REAL-WORLD ANALOGUE: Last-minute bookers are committed;
114
+ * advance bookers have more flexible cancellation policies.
115
+ *
116
+ * ───────────────────────────────────────────────────────────────
117
+ * 5. UPSELL SUCCESS BY SEGMENT (everything hook)
118
+ * ───────────────────────────────────────────────────────────────
119
+ * PATTERN: After a booking, luxury_seeker users get cloned "room
120
+ * upgrade selected" events injected. Budget users rarely see upsells.
121
+ *
122
+ * HOW TO FIND IT IN MIXPANEL:
123
+ * Report 1: Upgrade Rate by Segment
124
+ * • Report type: Insights
125
+ * • Event: "room upgrade selected"
126
+ * • Measure: Total per user
127
+ * • Breakdown: user property "customer_segment"
128
+ * • Expected: luxury_seeker ≈ 3x upgrades vs budget_hunter
129
+ *
130
+ * REAL-WORLD ANALOGUE: Luxury travelers are receptive to premium
131
+ * upsells (suite upgrades, spa packages).
132
+ *
133
+ * ───────────────────────────────────────────────────────────────
134
+ * 6. REVIEW QUALITY BY STAY RATING (everything hook)
135
+ * ───────────────────────────────────────────────────────────────
136
+ * PATTERN: Users whose avg stay_rating is >= 4 have longer review_length
137
+ * (1.5x words). Low-rating users write shorter, negative reviews.
138
+ *
139
+ * HOW TO FIND IT IN MIXPANEL:
140
+ * Report 1: Review Length vs Stay Rating
141
+ * • Report type: Insights
142
+ * • Event: "review submitted"
143
+ * • Measure: Average of "review_length"
144
+ * • Breakdown: "stay_rating"
145
+ * • Expected: 4-5 star reviews ≈ 180 words, 1-2 star ≈ 80 words
146
+ *
147
+ * REAL-WORLD ANALOGUE: Satisfied guests write detailed positive
148
+ * reviews; dissatisfied guests write brief complaints.
149
+ *
150
+ * ───────────────────────────────────────────────────────────────
151
+ * 7. BUSINESS TRAVELER PROFILE (user hook)
152
+ * ───────────────────────────────────────────────────────────────
153
+ * PATTERN: Users in business_traveler segment get company_name set
154
+ * to a realistic company and travel_frequency to "weekly".
155
+ *
156
+ * HOW TO FIND IT IN MIXPANEL:
157
+ * Report 1: Travel Frequency by Segment
158
+ * • Report type: Insights
159
+ * • Measure: Profiles → Breakdown by "travel_frequency"
160
+ * • Filter: customer_segment = "business_traveler"
161
+ * • Expected: 100% weekly for business, mixed for others
162
+ *
163
+ * REAL-WORLD ANALOGUE: Business travelers have corporate accounts
164
+ * with consistent, high-frequency booking patterns.
165
+ *
166
+ * ───────────────────────────────────────────────────────────────
167
+ * 8. REPEAT DESTINATION CLUSTERING (funnel-pre hook)
168
+ * ───────────────────────────────────────────────────────────────
169
+ * PATTERN: Business travelers convert 1.3x better through the
170
+ * search-to-book funnel (repeat bookings are habitual).
171
+ *
172
+ * HOW TO FIND IT IN MIXPANEL:
173
+ * Report 1: Conversion by Segment
174
+ * • Report type: Funnels
175
+ * • Steps: "destination searched" → "hotel viewed" → "booking completed"
176
+ * • Breakdown: user property "customer_segment"
177
+ * • Expected: business_traveler ≈ 52% vs budget_hunter ≈ 30%
178
+ *
179
+ * REAL-WORLD ANALOGUE: Business travelers book the same hotels
180
+ * repeatedly, leading to faster, more confident conversions.
181
+ *
182
+ * ═══════════════════════════════════════════════════════════════
183
+ * EXPECTED METRICS SUMMARY
184
+ * ═══════════════════════════════════════════════════════════════
185
+ *
186
+ * Hook | Metric | Baseline | Effect | Ratio
187
+ * ────────────────────────────|─────────────────────|──────────|─────────|──────
188
+ * Weekend Leisure Surge | nightly_rate | $150 | $195 | 1.3x
189
+ * Advance Booking Discount | nightly_rate | $150 | $120/$210 | 0.8x/1.4x
190
+ * Loyalty Tier Upgrade | loyalty_points | 100 | 300 | 3x
191
+ * Cancellation by Window | cancel rate | 20% | 8% | 0.4x
192
+ * Upsell by Segment | upgrades/user | 0.5 | 1.5 | 3x
193
+ * Review Quality | review_length | 120 | 180/80 | 1.5x/0.67x
194
+ * Business Profile | travel_frequency | mixed | weekly | —
195
+ * Repeat Destination | funnel conversion | 40% | 52% | 1.3x
196
+ */
197
+
198
+ /** @type {Config} */
199
+ const config = {
200
+ token: "",
201
+ seed: SEED,
202
+ numDays: days,
203
+ numEvents: num_users * 120,
204
+ numUsers: num_users,
205
+ hasAnonIds: false,
206
+ hasSessionIds: true,
207
+ format: "json",
208
+ gzip: true,
209
+ alsoInferFunnels: false,
210
+ hasLocation: true,
211
+ hasAndroidDevices: true,
212
+ hasIOSDevices: true,
213
+ hasDesktopDevices: true,
214
+ hasBrowser: false,
215
+ hasCampaigns: false,
216
+ isAnonymous: false,
217
+ hasAdSpend: false,
218
+ percentUsersBornInDataset: 35,
219
+ hasAvatar: true,
220
+ concurrency: 1,
221
+ writeToDisk: false,
222
+ scdProps: {
223
+ membership_tier: {
224
+ values: ["member", "silver", "gold", "platinum"],
225
+ frequency: "month",
226
+ timing: "fuzzy",
227
+ max: 8
228
+ }
229
+ },
230
+ mirrorProps: {},
231
+ lookupTables: [],
232
+
233
+ // ── Events (17) ──────────────────────────────────────────
234
+ events: [
235
+ {
236
+ event: "account created",
237
+ weight: 1,
238
+ isFirstEvent: true,
239
+ properties: {
240
+ signup_source: ["organic", "google", "instagram", "tripadvisor", "referral", "email_campaign"],
241
+ },
242
+ },
243
+ {
244
+ event: "destination searched",
245
+ weight: 8,
246
+ properties: {
247
+ destination: chance.pickone.bind(chance, destinationCities),
248
+ check_in_days_out: u.weighNumRange(1, 90, 0.4, 14),
249
+ nights: u.weighNumRange(1, 14, 0.4, 3),
250
+ guests: u.weighNumRange(1, 6, 0.3, 2),
251
+ search_filters: ["price", "price", "rating", "location", "amenities", "pool", "breakfast"],
252
+ },
253
+ },
254
+ {
255
+ event: "hotel viewed",
256
+ weight: 7,
257
+ properties: {
258
+ hotel_id: chance.pickone.bind(chance, hotelIds),
259
+ destination: chance.pickone.bind(chance, destinationCities),
260
+ star_rating: [3, 3, 4, 4, 4, 5],
261
+ nightly_rate: u.weighNumRange(50, 500, 0.3, 150),
262
+ has_pool: [true, true, false],
263
+ has_breakfast: [true, false],
264
+ photos_viewed: u.weighNumRange(0, 20, 0.5, 5),
265
+ },
266
+ },
267
+ {
268
+ event: "price compared",
269
+ weight: 4,
270
+ properties: {
271
+ hotels_compared: u.weighNumRange(2, 5),
272
+ price_difference_pct: u.weighNumRange(0, 40, 0.5, 10),
273
+ chose_cheapest: [true, true, true, false, false],
274
+ },
275
+ },
276
+ {
277
+ event: "booking completed",
278
+ weight: 3,
279
+ properties: {
280
+ hotel_id: chance.pickone.bind(chance, hotelIds),
281
+ destination: chance.pickone.bind(chance, destinationCities),
282
+ nightly_rate: u.weighNumRange(50, 500, 0.3, 150),
283
+ total_cost: u.weighNumRange(100, 5000, 0.3, 450),
284
+ nights: u.weighNumRange(1, 14, 0.4, 3),
285
+ guests: u.weighNumRange(1, 6, 0.3, 2),
286
+ booking_window: ["advance", "advance", "standard", "standard", "last_minute"],
287
+ payment_method: ["credit_card", "credit_card", "debit", "paypal", "apple_pay"],
288
+ loyalty_points: u.weighNumRange(0, 500, 0.5, 100),
289
+ },
290
+ },
291
+ {
292
+ event: "room upgrade selected",
293
+ weight: 2,
294
+ properties: {
295
+ upgrade_type: ["suite", "ocean_view", "executive_floor", "club_lounge", "premium_king"],
296
+ upgrade_cost: u.weighNumRange(25, 200, 0.4, 75),
297
+ hotel_id: chance.pickone.bind(chance, hotelIds),
298
+ },
299
+ },
300
+ {
301
+ event: "booking cancelled",
302
+ weight: 2,
303
+ properties: {
304
+ cancellation_reason: ["change_of_plans", "found_cheaper", "travel_restriction", "schedule_conflict", "weather"],
305
+ refund_amount: u.weighNumRange(0, 2000, 0.4, 200),
306
+ days_before_checkin: u.weighNumRange(0, 60, 0.5, 14),
307
+ booking_window: ["advance", "advance", "standard", "standard", "last_minute"],
308
+ },
309
+ },
310
+ {
311
+ event: "check in completed",
312
+ weight: 2,
313
+ properties: {
314
+ hotel_id: chance.pickone.bind(chance, hotelIds),
315
+ check_in_method: ["app", "app", "front_desk", "kiosk"],
316
+ early_checkin: [false, false, false, true],
317
+ },
318
+ },
319
+ {
320
+ event: "amenity used",
321
+ weight: 4,
322
+ properties: {
323
+ amenity_type: ["pool", "gym", "spa", "restaurant", "bar", "room_service", "business_center", "concierge"],
324
+ spend_amount: u.weighNumRange(0, 200, 0.4, 30),
325
+ },
326
+ },
327
+ {
328
+ event: "review submitted",
329
+ weight: 2,
330
+ properties: {
331
+ hotel_id: chance.pickone.bind(chance, hotelIds),
332
+ stay_rating: u.weighNumRange(1, 5, 0.7, 4),
333
+ review_length: u.weighNumRange(20, 300, 0.5, 120),
334
+ would_return: [true, true, true, false],
335
+ },
336
+ },
337
+ {
338
+ event: "loyalty points redeemed",
339
+ weight: 1,
340
+ properties: {
341
+ points_used: u.weighNumRange(100, 5000, 0.4, 500),
342
+ redemption_type: ["room_discount", "free_night", "upgrade", "amenity_credit"],
343
+ },
344
+ },
345
+ {
346
+ event: "notification received",
347
+ weight: 5,
348
+ properties: {
349
+ notification_type: ["deal_alert", "deal_alert", "booking_confirmation", "checkin_reminder", "review_prompt", "loyalty_update"],
350
+ channel: ["push", "push", "email", "sms"],
351
+ opened: [true, true, true, false],
352
+ },
353
+ },
354
+ {
355
+ event: "wishlist updated",
356
+ weight: 3,
357
+ properties: {
358
+ action: ["added", "added", "added", "removed"],
359
+ destination: chance.pickone.bind(chance, destinationCities),
360
+ hotel_id: chance.pickone.bind(chance, hotelIds),
361
+ },
362
+ },
363
+ {
364
+ event: "support contacted",
365
+ weight: 1,
366
+ properties: {
367
+ issue_type: ["booking_change", "refund", "complaint", "question", "loyalty_inquiry"],
368
+ channel: ["chat", "chat", "phone", "email"],
369
+ resolution_time_hours: u.weighNumRange(0.5, 48, 0.3, 4),
370
+ },
371
+ },
372
+ {
373
+ event: "app session",
374
+ weight: 7,
375
+ properties: {
376
+ session_duration_sec: u.weighNumRange(15, 1200, 0.4, 180),
377
+ pages_viewed: u.weighNumRange(1, 20, 0.5, 4),
378
+ },
379
+ },
380
+ {
381
+ event: "price alert set",
382
+ weight: 2,
383
+ properties: {
384
+ destination: chance.pickone.bind(chance, destinationCities),
385
+ target_price: u.weighNumRange(50, 300, 0.5, 120),
386
+ alert_status: ["active", "active", "triggered", "expired"],
387
+ },
388
+ },
389
+ {
390
+ event: "account deactivated",
391
+ weight: 1,
392
+ isChurnEvent: true,
393
+ returnLikelihood: 0.2,
394
+ isStrictEvent: true,
395
+ properties: {
396
+ reason: ["found_alternative", "too_expensive", "poor_experience", "infrequent_travel", "privacy"],
397
+ },
398
+ },
399
+ ],
400
+
401
+ // ── Funnels (5) ──────────────────────────────────────────
402
+ funnels: [
403
+ {
404
+ name: "Onboarding to First Booking",
405
+ sequence: ["account created", "destination searched", "hotel viewed", "booking completed"],
406
+ conversionRate: 35,
407
+ order: "sequential",
408
+ isFirstFunnel: true,
409
+ timeToConvert: 72,
410
+ weight: 3,
411
+ },
412
+ {
413
+ name: "Search to Book",
414
+ sequence: ["destination searched", "hotel viewed", "price compared", "booking completed"],
415
+ conversionRate: 25,
416
+ order: "sequential",
417
+ timeToConvert: 48,
418
+ weight: 5,
419
+ },
420
+ {
421
+ name: "Full Stay Journey",
422
+ sequence: ["booking completed", "check in completed", "amenity used", "review submitted"],
423
+ conversionRate: 30,
424
+ order: "sequential",
425
+ timeToConvert: 336,
426
+ weight: 3,
427
+ },
428
+ {
429
+ name: "Loyalty Engagement",
430
+ sequence: ["booking completed", "loyalty points redeemed", "review submitted"],
431
+ conversionRate: 15,
432
+ order: "sequential",
433
+ timeToConvert: 720,
434
+ weight: 2,
435
+ },
436
+ {
437
+ name: "Upsell Path",
438
+ sequence: ["hotel viewed", "booking completed", "room upgrade selected"],
439
+ conversionRate: 20,
440
+ order: "sequential",
441
+ timeToConvert: 24,
442
+ weight: 2,
443
+ },
444
+ ],
445
+
446
+ // ── SuperProps ──────────────────────────────────────────
447
+ superProps: {
448
+ platform: ["ios", "android", "web", "web"],
449
+ membership_tier: ["standard", "standard", "standard", "gold", "platinum"],
450
+ },
451
+
452
+ // ── UserProps ──────────────────────────────────────────
453
+ userProps: {
454
+ customer_segment: ["leisure_family"],
455
+ travel_frequency: ["occasional", "occasional", "monthly", "weekly"],
456
+ company_name: ["none"],
457
+ preferred_destination: chance.pickone.bind(chance, destinationCities),
458
+ avg_budget_per_night: u.weighNumRange(50, 400, 0.4, 150),
459
+ },
460
+
461
+ // ── Personas ──────────────────────────────────
462
+ personas: [
463
+ {
464
+ name: "business_traveler",
465
+ weight: 20,
466
+ eventMultiplier: 2.5,
467
+ conversionModifier: 1.3,
468
+ churnRate: 0.02,
469
+ properties: { customer_segment: "business_traveler", travel_frequency: "weekly" },
470
+ },
471
+ {
472
+ name: "leisure_family",
473
+ weight: 35,
474
+ eventMultiplier: 0.8,
475
+ conversionModifier: 0.9,
476
+ churnRate: 0.08,
477
+ properties: { customer_segment: "leisure_family", travel_frequency: "occasional" },
478
+ },
479
+ {
480
+ name: "luxury_seeker",
481
+ weight: 15,
482
+ eventMultiplier: 1.5,
483
+ conversionModifier: 1.2,
484
+ churnRate: 0.04,
485
+ properties: { customer_segment: "luxury_seeker", travel_frequency: "monthly" },
486
+ },
487
+ {
488
+ name: "budget_hunter",
489
+ weight: 30,
490
+ eventMultiplier: 1.0,
491
+ conversionModifier: 0.7,
492
+ churnRate: 0.10,
493
+ properties: { customer_segment: "budget_hunter", travel_frequency: "occasional" },
494
+ },
495
+ ],
496
+
497
+ // ── World Events ──────────────────────────────
498
+ worldEvents: [
499
+ {
500
+ name: "summer_sale",
501
+ type: "campaign",
502
+ startDay: 40,
503
+ duration: 5,
504
+ volumeMultiplier: 2.0,
505
+ conversionModifier: 1.5,
506
+ injectProps: { promo: "summer_sale" },
507
+ affectsEvents: ["booking completed", "hotel viewed", "destination searched"],
508
+ },
509
+ {
510
+ name: "hurricane_disruption",
511
+ type: "outage",
512
+ startDay: 65,
513
+ duration: 0.5,
514
+ volumeMultiplier: 0.2,
515
+ affectsEvents: ["booking completed", "check in completed"],
516
+ aftermath: { duration: 3, volumeMultiplier: 1.4 },
517
+ },
518
+ {
519
+ name: "loyalty_program_launch",
520
+ type: "product_launch",
521
+ startDay: 50,
522
+ duration: null,
523
+ injectProps: { loyalty_program: "active" },
524
+ affectsEvents: ["booking completed", "loyalty points redeemed"],
525
+ },
526
+ ],
527
+
528
+ // ── Engagement Decay ──────────────────────────
529
+ engagementDecay: {
530
+ model: "exponential",
531
+ halfLife: 90,
532
+ floor: 0.2,
533
+ reactivationChance: 0.02,
534
+ },
535
+
536
+ // ── Attribution ──────────────────────────────
537
+ attribution: {
538
+ model: "last_touch",
539
+ window: 7,
540
+ campaigns: [
541
+ {
542
+ name: "google_hotels",
543
+ source: "google",
544
+ medium: "cpc",
545
+ activeDays: [0, 100],
546
+ dailyBudget: [300, 1000],
547
+ acquisitionRate: 0.03,
548
+ userPersonaBias: { business_traveler: 0.5, budget_hunter: 0.3 },
549
+ },
550
+ {
551
+ name: "instagram_travel",
552
+ source: "instagram",
553
+ medium: "social",
554
+ activeDays: [10, 90],
555
+ dailyBudget: [150, 500],
556
+ acquisitionRate: 0.02,
557
+ userPersonaBias: { leisure_family: 0.5, luxury_seeker: 0.3 },
558
+ },
559
+ {
560
+ name: "tripadvisor_reviews",
561
+ source: "tripadvisor",
562
+ medium: "referral",
563
+ activeDays: [0, 100],
564
+ dailyBudget: [100, 300],
565
+ acquisitionRate: 0.02,
566
+ userPersonaBias: { leisure_family: 0.4 },
567
+ },
568
+ ],
569
+ organicRate: 0.35,
570
+ },
571
+
572
+ // ── Geo ──────────────────────────────────────
573
+ geo: {
574
+ sticky: true,
575
+ regions: [
576
+ {
577
+ name: "north_america",
578
+ countries: ["US", "CA"],
579
+ weight: 40,
580
+ timezoneOffset: -5,
581
+ properties: { currency: "USD", locale: "en-US" },
582
+ },
583
+ {
584
+ name: "europe",
585
+ countries: ["GB", "DE", "FR", "ES"],
586
+ weight: 35,
587
+ timezoneOffset: 1,
588
+ properties: { currency: "EUR", locale: "en-EU" },
589
+ },
590
+ {
591
+ name: "asia_pacific",
592
+ countries: ["JP", "AU", "SG"],
593
+ weight: 25,
594
+ timezoneOffset: 9,
595
+ properties: { currency: "JPY", locale: "en-APAC" },
596
+ },
597
+ ],
598
+ },
599
+
600
+ // ── Anomalies ──────────────────────────────────
601
+ anomalies: [
602
+ {
603
+ type: "extreme_value",
604
+ event: "booking completed",
605
+ property: "total_cost",
606
+ frequency: 0.003,
607
+ multiplier: 20,
608
+ tag: "whale_booking",
609
+ },
610
+ {
611
+ type: "coordinated",
612
+ event: "booking completed",
613
+ day: 75,
614
+ window: 0.04,
615
+ count: 80,
616
+ tag: "group_conference_booking",
617
+ },
618
+ ],
619
+
620
+ // ── Hook Function ──────────────────────────────────────
621
+ hook: function (record, type, meta) {
622
+ // ── HOOK 7: BUSINESS TRAVELER PROFILE (user) ─────────
623
+ if (type === "user") {
624
+ if (record.customer_segment === "business_traveler") {
625
+ record.company_name = chance.pickone(["Acme Corp", "GlobalTech", "Initech", "Prestige Consulting", "Summit Partners", "Atlas Industries"]);
626
+ record.travel_frequency = "weekly";
627
+ } else if (record.customer_segment === "luxury_seeker") {
628
+ record.avg_budget_per_night = chance.integer({ min: 250, max: 500 });
629
+ } else if (record.customer_segment === "budget_hunter") {
630
+ record.avg_budget_per_night = chance.integer({ min: 50, max: 120 });
631
+ }
632
+ }
633
+
634
+ // ── HOOK 8: REPEAT DESTINATION CLUSTERING (funnel-pre) ─
635
+ if (type === "funnel-pre") {
636
+ if (meta && meta.profile) {
637
+ if (meta.profile.customer_segment === "business_traveler") {
638
+ record.conversionRate = Math.min(record.conversionRate * 1.3, 90);
639
+ } else if (meta.profile.customer_segment === "luxury_seeker") {
640
+ record.conversionRate = Math.min(record.conversionRate * 1.15, 85);
641
+ }
642
+ }
643
+ }
644
+
645
+ // ── HOOK 1: WEEKEND LEISURE SURGE (event) ────────────
646
+ if (type === "event") {
647
+ if (record.event === "booking completed") {
648
+ const dayOfWeek = dayjs(record.time).day();
649
+ // Friday=5, Saturday=6, Sunday=0
650
+ if (dayOfWeek === 0 || dayOfWeek === 5 || dayOfWeek === 6) {
651
+ record.nightly_rate = Math.floor((record.nightly_rate || 150) * 1.3);
652
+ record.total_cost = Math.floor((record.total_cost || 450) * 1.3);
653
+ }
654
+ }
655
+
656
+ // ── HOOK 2: ADVANCE BOOKING DISCOUNT (event) ─────
657
+ if (record.event === "booking completed") {
658
+ const eventTime = dayjs(record.time);
659
+ const daysUntilEnd = NOW.diff(eventTime, "days");
660
+ if (daysUntilEnd > 21) {
661
+ record.booking_window = "advance";
662
+ record.nightly_rate = Math.floor((record.nightly_rate || 150) * 0.8);
663
+ } else if (daysUntilEnd < 3) {
664
+ record.booking_window = "last_minute";
665
+ record.nightly_rate = Math.floor((record.nightly_rate || 150) * 1.4);
666
+ }
667
+ }
668
+ }
669
+
670
+ // ── EVERYTHING HOOKS ─────────────────────────────────
671
+ if (type === "everything") {
672
+ const events = record;
673
+ if (!events.length) return record;
674
+
675
+ // ── HOOK 3: LOYALTY TIER UPGRADE PATH ────────────
676
+ let bookingCount = 0;
677
+ events.forEach(e => { if (e.event === "booking completed") bookingCount++; });
678
+ if (bookingCount >= 5) {
679
+ events.forEach(e => {
680
+ if (e.event === "booking completed" && e.loyalty_points) {
681
+ e.loyalty_points = Math.floor(e.loyalty_points * (2.5 + chance.floating({ min: 0, max: 1.0 })));
682
+ }
683
+ });
684
+ }
685
+
686
+ // ── HOOK 4: CANCELLATION BY BOOKING WINDOW ───────
687
+ // Last-minute bookers rarely cancel.
688
+ for (let i = events.length - 1; i >= 0; i--) {
689
+ if (events[i].event === "booking cancelled" && events[i].booking_window === "last_minute") {
690
+ if (chance.bool({ likelihood: 40 })) {
691
+ events.splice(i, 1);
692
+ }
693
+ }
694
+ }
695
+
696
+ // ── HOOK 5: UPSELL SUCCESS BY SEGMENT ────────────
697
+ const profile = meta.profile;
698
+ if (profile && profile.customer_segment === "luxury_seeker") {
699
+ const templateUpgrade = events.find(e => e.event === "room upgrade selected");
700
+ if (templateUpgrade) {
701
+ const bookings = events.filter(e => e.event === "booking completed");
702
+ bookings.forEach(booking => {
703
+ if (chance.bool({ likelihood: 50 })) {
704
+ events.push({
705
+ ...templateUpgrade,
706
+ time: dayjs(booking.time).add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
707
+ user_id: booking.user_id,
708
+ upgrade_cost: chance.integer({ min: 75, max: 200 }),
709
+ });
710
+ }
711
+ });
712
+ }
713
+ }
714
+
715
+ // ── HOOK 6: REVIEW QUALITY BY STAY RATING ────────
716
+ let totalRating = 0;
717
+ let ratingCount = 0;
718
+ events.forEach(e => {
719
+ if (e.event === "review submitted" && e.stay_rating) {
720
+ totalRating += e.stay_rating;
721
+ ratingCount++;
722
+ }
723
+ });
724
+ const avgRating = ratingCount > 0 ? totalRating / ratingCount : 3;
725
+ events.forEach(e => {
726
+ if (e.event === "review submitted") {
727
+ if (avgRating >= 4) {
728
+ e.review_length = Math.floor((e.review_length || 120) * 1.5);
729
+ } else if (avgRating <= 2) {
730
+ e.review_length = Math.floor((e.review_length || 120) * 0.5);
731
+ }
732
+ }
733
+ });
734
+
735
+ return record;
736
+ }
737
+
738
+ return record;
739
+ },
740
+ };
741
+
742
+ export default config;