@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.
- package/README.md +7 -7
- package/dungeons/technical/ad-spend-schema.json +128 -0
- package/dungeons/technical/ad-spend.js +116 -0
- package/dungeons/technical/anonymous-users-schema.json +92 -0
- package/dungeons/technical/anonymous-users.js +102 -0
- package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
- package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
- package/dungeons/technical/experiments-schema.json +203 -0
- package/dungeons/technical/experiments.js +168 -0
- package/dungeons/technical/foobar-schema.json +362 -0
- package/dungeons/{foobar.js → technical/foobar.js} +3 -3
- package/dungeons/technical/group-analytics-schema.json +241 -0
- package/dungeons/technical/group-analytics.js +151 -0
- package/dungeons/technical/mirror-strategies-schema.json +84 -0
- package/dungeons/technical/mirror-strategies.js +126 -0
- package/dungeons/technical/nested-objects-schema.json +145 -0
- package/dungeons/technical/nested-objects.js +207 -0
- package/dungeons/technical/retention-cadence-schema.json +37 -0
- package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
- package/dungeons/technical/sanity-schema.json +185 -0
- package/dungeons/{sanity.js → technical/sanity.js} +4 -2
- package/dungeons/technical/scale-test-schema.json +70 -0
- package/dungeons/technical/scale-test.js +90 -0
- package/dungeons/technical/scd-schema.json +467 -0
- package/dungeons/{scd.js → technical/scd.js} +20 -10
- package/dungeons/technical/simple-schema.json +362 -0
- package/dungeons/{simple.js → technical/simple.js} +7 -7
- package/dungeons/technical/simplest-schema.json +724 -0
- package/dungeons/{simplest.js → technical/simplest.js} +7 -7
- package/dungeons/technical/text-generation-schema.json +1062 -0
- package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
- package/dungeons/vertical/community-schema.json +579 -0
- package/dungeons/vertical/community.js +794 -0
- package/dungeons/vertical/devtools-schema.json +601 -0
- package/dungeons/vertical/devtools.js +843 -0
- package/dungeons/vertical/ecommerce-schema.json +604 -0
- package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
- package/dungeons/vertical/education-schema.json +5686 -0
- package/dungeons/{education.js → vertical/education.js} +70 -39
- package/dungeons/vertical/fintech-schema.json +630 -0
- package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
- package/dungeons/vertical/fitness-schema.json +530 -0
- package/dungeons/vertical/fitness.js +808 -0
- package/dungeons/vertical/food-delivery-schema.json +36728 -0
- package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
- package/dungeons/vertical/gaming-schema.json +438 -0
- package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
- package/dungeons/vertical/healthcare-schema.json +549 -0
- package/dungeons/vertical/healthcare.js +775 -0
- package/dungeons/vertical/insurance-application-schema.json +485 -0
- package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
- package/dungeons/vertical/logistics-schema.json +574 -0
- package/dungeons/vertical/logistics.js +799 -0
- package/dungeons/vertical/marketplace-schema.json +533 -0
- package/dungeons/vertical/marketplace.js +850 -0
- package/dungeons/vertical/media-schema.json +4749 -0
- package/dungeons/{media.js → vertical/media.js} +103 -19
- package/dungeons/vertical/rpg-schema.json +2491 -0
- package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
- package/dungeons/vertical/sass-schema.json +3128 -0
- package/dungeons/{sass.js → vertical/sass.js} +65 -26
- package/dungeons/vertical/social-schema.json +620 -0
- package/dungeons/{social.js → vertical/social.js} +76 -17
- package/dungeons/vertical/travel-schema.json +580 -0
- package/dungeons/vertical/travel.js +742 -0
- package/index.js +20 -16
- package/lib/core/config-validator.js +286 -1
- package/lib/core/context.js +16 -5
- package/lib/core/storage.js +2 -0
- package/lib/generators/adspend.js +3 -3
- package/lib/generators/events.js +141 -14
- package/lib/generators/funnels.js +67 -7
- package/lib/generators/text.js +2 -2
- package/lib/orchestrators/user-loop.js +444 -22
- package/lib/templates/soup-presets.js +2 -2
- package/lib/utils/utils.js +4 -5
- package/package.json +4 -3
- package/scripts/dungeon-to-json.mjs +163 -0
- package/scripts/extract-dungeon-schema.mjs +343 -0
- package/scripts/json-to-dungeon.mjs +200 -0
- package/scripts/run-dungeon.mjs +76 -0
- package/scripts/run-many.mjs +118 -0
- package/scripts/verify-runner.mjs +42 -0
- package/types.d.ts +316 -1
- package/dungeons/array-of-object-lookup-schema.json +0 -327
- package/dungeons/ecommerce-schema.json +0 -462
- package/dungeons/education-schema.json +0 -2409
- package/dungeons/fintech-schema.json +0 -14034
- package/dungeons/foobar-schema.json +0 -403
- package/dungeons/food-delivery-schema.json +0 -192
- package/dungeons/food-delivery.js +0 -602
- package/dungeons/food-schema.json +0 -1152
- package/dungeons/gaming-schema.json +0 -1270
- package/dungeons/insurance-application-schema.json +0 -204
- package/dungeons/media-schema.json +0 -906
- package/dungeons/retention-cadence-schema.json +0 -78
- package/dungeons/rpg-schema.json +0 -4526
- package/dungeons/sanity-schema.json +0 -255
- package/dungeons/sass-schema.json +0 -1291
- package/dungeons/scd-schema.json +0 -919
- package/dungeons/simple-schema.json +0 -608
- package/dungeons/simplest-schema.json +0 -1418
- package/dungeons/social-schema.json +0 -1118
- 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;
|