@ak--47/dungeon-master 1.5.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +183 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -1,930 +0,0 @@
|
|
|
1
|
-
// ── IMPORTS ──
|
|
2
|
-
import dayjs from "dayjs";
|
|
3
|
-
import utc from "dayjs/plugin/utc.js";
|
|
4
|
-
dayjs.extend(utc);
|
|
5
|
-
import "dotenv/config";
|
|
6
|
-
import * as u from "../../lib/utils/utils.js";
|
|
7
|
-
import * as v from "ak-tools";
|
|
8
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
9
|
-
|
|
10
|
-
// ── OVERVIEW ──
|
|
11
|
-
/*
|
|
12
|
-
* NAME: StayQuest
|
|
13
|
-
* APP: Hotel booking platform for business and leisure travelers.
|
|
14
|
-
* Users search destinations, compare hotels, book rooms, and leave
|
|
15
|
-
* reviews. Revenue from commission per booking plus premium loyalty
|
|
16
|
-
* membership. Four traveler archetypes: business, leisure family,
|
|
17
|
-
* luxury, budget.
|
|
18
|
-
* SCALE: 10,000 users, ~600K events, 121 days (2026-01-01 → 2026-05-01)
|
|
19
|
-
* CORE LOOP: search → view hotel → compare → book → stay → review
|
|
20
|
-
*
|
|
21
|
-
* EVENTS (17):
|
|
22
|
-
* destination searched (8) > hotel viewed (7) > app session (7) > notification received (5)
|
|
23
|
-
* > price compared (4) > amenity used (4) > booking completed (3) > wishlist updated (3)
|
|
24
|
-
* > room upgrade selected (2) > booking cancelled (2) > check in completed (2)
|
|
25
|
-
* > review submitted (2) > price alert set (2) > account created (1)
|
|
26
|
-
* > loyalty points redeemed (1) > support contacted (1) > account deactivated (1)
|
|
27
|
-
*
|
|
28
|
-
* FUNNELS (5):
|
|
29
|
-
* - Onboarding to First Booking: account created → destination searched → hotel viewed → booking completed (35%)
|
|
30
|
-
* - Search to Book: destination searched → hotel viewed → price compared → booking completed (25%)
|
|
31
|
-
* - Full Stay Journey: booking completed → check in completed → amenity used → review submitted (30%)
|
|
32
|
-
* - Loyalty Engagement: booking completed → loyalty points redeemed → review submitted (15%)
|
|
33
|
-
* - Upsell Path: hotel viewed → booking completed → room upgrade selected (20%)
|
|
34
|
-
*
|
|
35
|
-
* USER PROPS: customer_segment, travel_frequency, company_name, preferred_destination,
|
|
36
|
-
* avg_budget_per_night, Platform, membership_tier
|
|
37
|
-
* SUPER PROPS: Platform, membership_tier
|
|
38
|
-
* SCD PROPS: membership_tier (member/silver/gold/platinum, monthly fuzzy, max 8)
|
|
39
|
-
* GROUPS: none
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
// ── HOOK STORIES ──
|
|
43
|
-
/*
|
|
44
|
-
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
|
|
45
|
-
* raw-prop breakdowns (booking_window, day, segment) or behavioral cohorts.
|
|
46
|
-
*
|
|
47
|
-
* ───────────────────────────────────────────────────────────────
|
|
48
|
-
* 1. WEEKEND LEISURE SURGE (everything)
|
|
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 (everything)
|
|
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)
|
|
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)
|
|
101
|
-
* ───────────────────────────────────────────────────────────────
|
|
102
|
-
* PATTERN: Users who book last-minute (< 7 days out) have 60% 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)
|
|
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)
|
|
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)
|
|
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 (everything — event filtering)
|
|
168
|
-
* ───────────────────────────────────────────────────────────────
|
|
169
|
-
* PATTERN: Non-business/luxury users have ~25% of "booking completed"
|
|
170
|
-
* events dropped, simulating lower funnel conversion for casual segments.
|
|
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
|
-
* 9. BOOKING TIME-TO-CONVERT (funnel-post)
|
|
184
|
-
* ───────────────────────────────────────────────────────────────
|
|
185
|
-
*
|
|
186
|
-
* PATTERN: Business travelers complete the Search-to-Book funnel
|
|
187
|
-
* 1.35x faster (time gaps scaled by 0.74). Budget and leisure-family
|
|
188
|
-
* users complete it 1.25x slower (gaps scaled by 1.25). The hook
|
|
189
|
-
* iterates over the funnel-post event array, compresses or stretches
|
|
190
|
-
* the inter-step time gaps based on the user's customer_segment from
|
|
191
|
-
* meta.profile, then rewrites each event's timestamp.
|
|
192
|
-
*
|
|
193
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
194
|
-
*
|
|
195
|
-
* Report 1: Search-to-Book TTC by Segment
|
|
196
|
-
* - Report type: Funnels
|
|
197
|
-
* - Steps: "destination searched" -> "hotel viewed" -> "price compared" -> "booking completed"
|
|
198
|
-
* - Breakdown: user property "customer_segment"
|
|
199
|
-
* - Metric: Median time to convert
|
|
200
|
-
* - Expected: business_traveler median TTC ~ 0.74x of budget/leisure TTC
|
|
201
|
-
* (e.g., business ~ 24h vs budget ~ 43h)
|
|
202
|
-
*
|
|
203
|
-
* NOTE: This effect is visible ONLY in Mixpanel funnel median TTC.
|
|
204
|
-
* Cross-event MIN->MIN SQL queries on raw events do NOT show this
|
|
205
|
-
* because funnel-post mutates timestamps after event generation but
|
|
206
|
-
* before storage.
|
|
207
|
-
*
|
|
208
|
-
* REAL-WORLD ANALOGUE: Business travelers know their preferred hotel
|
|
209
|
-
* chains and corporate rates, moving from search to booking with
|
|
210
|
-
* minimal comparison. Leisure and budget travelers deliberate longer,
|
|
211
|
-
* comparing options and waiting for deals.
|
|
212
|
-
*
|
|
213
|
-
* ───────────────────────────────────────────────────────────────
|
|
214
|
-
* 10. HOTEL-VIEWED MAGIC NUMBER (everything)
|
|
215
|
-
* ───────────────────────────────────────────────────────────────
|
|
216
|
-
*
|
|
217
|
-
* PATTERN: Users who viewed 5-10 hotels sit in a "decisive comparison
|
|
218
|
-
* shopper" sweet spot -- all their nightly_rate values on "booking
|
|
219
|
-
* completed" events are boosted by +30% (factor 1.3), indicating they
|
|
220
|
-
* chose higher-tier rooms after deliberate comparison. Users who
|
|
221
|
-
* viewed 11+ hotels trigger analysis paralysis: 35% of their
|
|
222
|
-
* "booking completed" events are dropped entirely. No flag is
|
|
223
|
-
* stamped -- discoverable only by binning users on hotel-viewed
|
|
224
|
-
* COUNT and comparing booking revenue or conversion volume.
|
|
225
|
-
*
|
|
226
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
227
|
-
*
|
|
228
|
-
* Report 1: Nightly Rate by Hotel-View Cohort
|
|
229
|
-
* - Report type: Insights (with cohorts)
|
|
230
|
-
* - Cohort A: users who did "hotel viewed" 5-10 times
|
|
231
|
-
* - Cohort B: users who did "hotel viewed" 0-4 times
|
|
232
|
-
* - Event: "booking completed"
|
|
233
|
-
* - Measure: Average of "nightly_rate"
|
|
234
|
-
* - Compare cohort A vs cohort B
|
|
235
|
-
* - Expected: cohort A ~ 1.3x higher avg nightly_rate
|
|
236
|
-
*
|
|
237
|
-
* Report 2: Bookings per User by Hotel-View Volume
|
|
238
|
-
* - Report type: Insights (with cohorts)
|
|
239
|
-
* - Cohort C: users who did "hotel viewed" 11+ times
|
|
240
|
-
* - Cohort A: users who did "hotel viewed" 5-10 times
|
|
241
|
-
* - Event: "booking completed"
|
|
242
|
-
* - Measure: Total events per user
|
|
243
|
-
* - Compare cohort C vs cohort A
|
|
244
|
-
* - Expected: cohort C ~ 35% fewer bookings per user
|
|
245
|
-
*
|
|
246
|
-
* REAL-WORLD ANALOGUE: Travelers who compare a handful of hotels
|
|
247
|
-
* make confident, higher-value bookings; those who endlessly browse
|
|
248
|
-
* suffer decision fatigue and often abandon the search entirely.
|
|
249
|
-
*
|
|
250
|
-
* ═══════════════════════════════════════════════════════════════
|
|
251
|
-
* EXPECTED METRICS SUMMARY
|
|
252
|
-
* ═══════════════════════════════════════════════════════════════
|
|
253
|
-
*
|
|
254
|
-
* Hook | Metric | Baseline | Effect | Ratio
|
|
255
|
-
* ────────────────────────────|─────────────────────|──────────|─────────|──────
|
|
256
|
-
* Weekend Leisure Surge | nightly_rate | $150 | $195 | 1.3x
|
|
257
|
-
* Advance Booking Discount | nightly_rate | $150 | $120/$210 | 0.8x/1.4x
|
|
258
|
-
* Loyalty Tier Upgrade | loyalty_points | 100 | 300 | 3x
|
|
259
|
-
* Cancellation by Window | cancel rate | 20% | 8% | 0.4x
|
|
260
|
-
* Upsell by Segment | upgrades/user | 0.5 | 1.5 | 3x
|
|
261
|
-
* Review Quality | review_length | 120 | 180/80 | 1.5x/0.67x
|
|
262
|
-
* Business Profile | travel_frequency | mixed | weekly | —
|
|
263
|
-
* Repeat Destination | funnel conversion | 40% | 52% | 1.3x
|
|
264
|
-
* Booking TTC | funnel median TTC | 1x | 0.74x | 1.35x faster (business)
|
|
265
|
-
* Hotel-Viewed Magic Num | sweet nightly_rate | 1x | 1.3x | +30%
|
|
266
|
-
* Hotel-Viewed Magic Num | over bookings/user | 1x | 0.65x | -35%
|
|
267
|
-
*/
|
|
268
|
-
|
|
269
|
-
// ── SCALE ──
|
|
270
|
-
const SEED = "dm4-travel";
|
|
271
|
-
const NUM_USERS = 10_000;
|
|
272
|
-
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
273
|
-
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
274
|
-
const EVENTS_PER_DAY = 1.2;
|
|
275
|
-
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
276
|
-
|
|
277
|
-
const chance = u.initChance(SEED);
|
|
278
|
-
|
|
279
|
-
// ── KNOBS (tweak these to reshape stories) ──
|
|
280
|
-
const WEEKEND_RATE_BOOST = 1.3;
|
|
281
|
-
const ADVANCE_DAYS_THRESHOLD = 21;
|
|
282
|
-
const ADVANCE_RATE_FACTOR = 0.8;
|
|
283
|
-
const LAST_MINUTE_DAYS_THRESHOLD = 3;
|
|
284
|
-
const LAST_MINUTE_RATE_FACTOR = 1.4;
|
|
285
|
-
const LOYALTY_BOOKING_THRESHOLD = 5;
|
|
286
|
-
const LOYALTY_POINT_BASE_MULT = 2.5;
|
|
287
|
-
const LOYALTY_POINT_VARIANCE = 1.0;
|
|
288
|
-
const REPEAT_DEST_DROP_LIKELIHOOD = 25;
|
|
289
|
-
const CANCEL_LAST_MINUTE_DROP_LIKELIHOOD = 60;
|
|
290
|
-
const UPSELL_LUXURY_LIKELIHOOD = 50;
|
|
291
|
-
const REVIEW_HIGH_RATING_THRESHOLD = 4;
|
|
292
|
-
const REVIEW_LOW_RATING_THRESHOLD = 2;
|
|
293
|
-
const REVIEW_HIGH_LENGTH_MULT = 1.5;
|
|
294
|
-
const REVIEW_LOW_LENGTH_MULT = 0.5;
|
|
295
|
-
const HOTEL_SWEET_MIN = 5;
|
|
296
|
-
const HOTEL_SWEET_MAX = 10;
|
|
297
|
-
const HOTEL_OVER_THRESHOLD = 11;
|
|
298
|
-
const HOTEL_SWEET_RATE_BOOST = 1.3;
|
|
299
|
-
const HOTEL_OVER_BOOKING_DROP_LIKELIHOOD = 35;
|
|
300
|
-
const TTC_BUSINESS_FACTOR = 0.74;
|
|
301
|
-
const TTC_LEISURE_BUDGET_FACTOR = 1.25;
|
|
302
|
-
|
|
303
|
-
// ── DATA ARRAYS ──
|
|
304
|
-
const hotelIds = v.range(1, 200).map(() => `HTL_${v.uid(6)}`);
|
|
305
|
-
const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona", "Dubai", "Sydney", "Rome", "Bangkok", "Cancun", "Bali", "Amsterdam", "Miami", "Singapore", "Lisbon"];
|
|
306
|
-
|
|
307
|
-
// ── HELPER FUNCTIONS ──
|
|
308
|
-
function handleUserHooks(record) {
|
|
309
|
-
// H7: BUSINESS TRAVELER PROFILE
|
|
310
|
-
if (record.customer_segment === "business_traveler") {
|
|
311
|
-
record.company_name = chance.pickone(["Acme Corp", "GlobalTech", "Initech", "Prestige Consulting", "Summit Partners", "Atlas Industries"]);
|
|
312
|
-
record.travel_frequency = "weekly";
|
|
313
|
-
} else if (record.customer_segment === "luxury_seeker") {
|
|
314
|
-
record.avg_budget_per_night = chance.integer({ min: 250, max: 500 });
|
|
315
|
-
} else if (record.customer_segment === "budget_hunter") {
|
|
316
|
-
record.avg_budget_per_night = chance.integer({ min: 50, max: 120 });
|
|
317
|
-
}
|
|
318
|
-
return record;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function handleFunnelPostHooks(record, meta) {
|
|
322
|
-
// H9: BOOKING TIME-TO-CONVERT — business 1.35x faster (0.74),
|
|
323
|
-
// leisure_family/budget 1.25x slower.
|
|
324
|
-
const segment = meta?.profile?.customer_segment;
|
|
325
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
326
|
-
const factor = (
|
|
327
|
-
segment === "business_traveler" ? TTC_BUSINESS_FACTOR :
|
|
328
|
-
segment === "budget_hunter" || segment === "leisure_family" ? TTC_LEISURE_BUDGET_FACTOR :
|
|
329
|
-
1.0
|
|
330
|
-
);
|
|
331
|
-
if (factor !== 1.0) {
|
|
332
|
-
for (let i = 1; i < record.length; i++) {
|
|
333
|
-
const prev = dayjs(record[i - 1].time);
|
|
334
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
335
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
return record;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function handleEverythingHooks(record, meta) {
|
|
343
|
-
const events = record;
|
|
344
|
-
if (!events.length) return record;
|
|
345
|
-
|
|
346
|
-
const profile = meta.profile;
|
|
347
|
-
|
|
348
|
-
// Stamp superProps from profile (consistent per user)
|
|
349
|
-
const stampPlatform = profile && profile.Platform ? profile.Platform : undefined;
|
|
350
|
-
const stampTier = profile && profile.membership_tier ? profile.membership_tier : undefined;
|
|
351
|
-
if (stampPlatform || stampTier) {
|
|
352
|
-
events.forEach(e => {
|
|
353
|
-
if (stampPlatform) e.Platform = stampPlatform;
|
|
354
|
-
if (stampTier) e.membership_tier = stampTier;
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// H1: WEEKEND LEISURE SURGE — Fri/Sat/Sun bookings get +30% rate
|
|
359
|
-
events.forEach(e => {
|
|
360
|
-
if (e.event === "booking completed") {
|
|
361
|
-
const dayOfWeek = new Date(e.time).getUTCDay();
|
|
362
|
-
// Friday=5, Saturday=6, Sunday=0
|
|
363
|
-
if (dayOfWeek === 0 || dayOfWeek === 5 || dayOfWeek === 6) {
|
|
364
|
-
e.nightly_rate = Math.floor((e.nightly_rate || 150) * WEEKEND_RATE_BOOST);
|
|
365
|
-
e.total_cost = Math.floor((e.total_cost || 450) * WEEKEND_RATE_BOOST);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// H2: ADVANCE BOOKING DISCOUNT
|
|
371
|
-
const datasetEndForBooking = dayjs.unix(meta.datasetEnd);
|
|
372
|
-
events.forEach(e => {
|
|
373
|
-
if (e.event === "booking completed") {
|
|
374
|
-
const eventTime = dayjs(e.time);
|
|
375
|
-
const daysUntilEnd = datasetEndForBooking.diff(eventTime, "days");
|
|
376
|
-
if (daysUntilEnd > ADVANCE_DAYS_THRESHOLD) {
|
|
377
|
-
e.booking_window = "advance";
|
|
378
|
-
e.nightly_rate = Math.floor((e.nightly_rate || 150) * ADVANCE_RATE_FACTOR);
|
|
379
|
-
} else if (daysUntilEnd < LAST_MINUTE_DAYS_THRESHOLD) {
|
|
380
|
-
e.booking_window = "last_minute";
|
|
381
|
-
e.nightly_rate = Math.floor((e.nightly_rate || 150) * LAST_MINUTE_RATE_FACTOR);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// H8: REPEAT DESTINATION CLUSTERING — drop ~25% of "booking completed"
|
|
387
|
-
// for non-business/luxury users.
|
|
388
|
-
const segment = profile && profile.customer_segment;
|
|
389
|
-
if (segment !== "business_traveler" && segment !== "luxury_seeker"
|
|
390
|
-
&& chance.bool({ likelihood: REPEAT_DEST_DROP_LIKELIHOOD })) {
|
|
391
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
392
|
-
if (events[i].event === "booking completed") {
|
|
393
|
-
events.splice(i, 1);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// H3: LOYALTY TIER UPGRADE PATH — 5+ bookings → ~3x loyalty_points
|
|
399
|
-
let bookingCount = 0;
|
|
400
|
-
events.forEach(e => { if (e.event === "booking completed") bookingCount++; });
|
|
401
|
-
if (bookingCount >= LOYALTY_BOOKING_THRESHOLD) {
|
|
402
|
-
events.forEach(e => {
|
|
403
|
-
if (e.event === "booking completed" && e.loyalty_points) {
|
|
404
|
-
e.loyalty_points = Math.floor(e.loyalty_points * (LOYALTY_POINT_BASE_MULT + chance.floating({ min: 0, max: LOYALTY_POINT_VARIANCE })));
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// H4: CANCELLATION BY BOOKING WINDOW — copy nearest preceding booking's
|
|
410
|
-
// booking_window onto each cancellation, then drop 60% of last-minute cancels.
|
|
411
|
-
const bookingsByTime = events
|
|
412
|
-
.filter(e => e.event === "booking completed")
|
|
413
|
-
.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
414
|
-
events.forEach(e => {
|
|
415
|
-
if (e.event === "booking cancelled" && bookingsByTime.length > 0) {
|
|
416
|
-
const cancelTime = new Date(e.time).getTime();
|
|
417
|
-
let matched = bookingsByTime[0];
|
|
418
|
-
for (let b = bookingsByTime.length - 1; b >= 0; b--) {
|
|
419
|
-
if (new Date(bookingsByTime[b].time).getTime() <= cancelTime) {
|
|
420
|
-
matched = bookingsByTime[b];
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
e.booking_window = matched.booking_window;
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
428
|
-
if (events[i].event === "booking cancelled" && events[i].booking_window === "last_minute") {
|
|
429
|
-
if (chance.bool({ likelihood: CANCEL_LAST_MINUTE_DROP_LIKELIHOOD })) {
|
|
430
|
-
events.splice(i, 1);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// H5: UPSELL SUCCESS BY SEGMENT — luxury_seeker users get cloned upgrades
|
|
436
|
-
if (profile && profile.customer_segment === "luxury_seeker") {
|
|
437
|
-
const templateUpgrade = events.find(e => e.event === "room upgrade selected");
|
|
438
|
-
if (templateUpgrade) {
|
|
439
|
-
const bookings = events.filter(e => e.event === "booking completed");
|
|
440
|
-
bookings.forEach(booking => {
|
|
441
|
-
if (chance.bool({ likelihood: UPSELL_LUXURY_LIKELIHOOD })) {
|
|
442
|
-
events.push({
|
|
443
|
-
...templateUpgrade,
|
|
444
|
-
time: dayjs(booking.time).add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
|
|
445
|
-
user_id: booking.user_id,
|
|
446
|
-
upgrade_cost: chance.integer({ min: 75, max: 200 }),
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// H6: REVIEW QUALITY BY STAY RATING — high avg → 1.5x review_length;
|
|
454
|
-
// low avg → 0.5x.
|
|
455
|
-
let totalRating = 0;
|
|
456
|
-
let ratingCount = 0;
|
|
457
|
-
events.forEach(e => {
|
|
458
|
-
if (e.event === "review submitted" && e.stay_rating) {
|
|
459
|
-
totalRating += e.stay_rating;
|
|
460
|
-
ratingCount++;
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
const avgRating = ratingCount > 0 ? totalRating / ratingCount : 3;
|
|
464
|
-
events.forEach(e => {
|
|
465
|
-
if (e.event === "review submitted") {
|
|
466
|
-
if (avgRating >= REVIEW_HIGH_RATING_THRESHOLD) {
|
|
467
|
-
e.review_length = Math.floor((e.review_length || 120) * REVIEW_HIGH_LENGTH_MULT);
|
|
468
|
-
} else if (avgRating <= REVIEW_LOW_RATING_THRESHOLD) {
|
|
469
|
-
e.review_length = Math.floor((e.review_length || 120) * REVIEW_LOW_LENGTH_MULT);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// H10: HOTEL-VIEWED MAGIC NUMBER — sweet 5-10 → +30% nightly_rate;
|
|
475
|
-
// over 11+ → drop 35% of bookings.
|
|
476
|
-
const hotelViews = events.filter(e => e.event === "hotel viewed").length;
|
|
477
|
-
if (hotelViews >= HOTEL_SWEET_MIN && hotelViews <= HOTEL_SWEET_MAX) {
|
|
478
|
-
events.forEach(e => {
|
|
479
|
-
if (e.event === "booking completed" && typeof e.nightly_rate === "number") {
|
|
480
|
-
e.nightly_rate = Math.round(e.nightly_rate * HOTEL_SWEET_RATE_BOOST);
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
} else if (hotelViews >= HOTEL_OVER_THRESHOLD) {
|
|
484
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
485
|
-
if (events[i].event === "booking completed" && chance.bool({ likelihood: HOTEL_OVER_BOOKING_DROP_LIKELIHOOD })) {
|
|
486
|
-
events.splice(i, 1);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return record;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// ── CONFIG ──
|
|
495
|
-
/** @type {Config} */
|
|
496
|
-
const config = {
|
|
497
|
-
version: 2,
|
|
498
|
-
seed: SEED,
|
|
499
|
-
datasetStart: DATASET_START,
|
|
500
|
-
datasetEnd: DATASET_END,
|
|
501
|
-
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
502
|
-
numUsers: NUM_USERS,
|
|
503
|
-
format: "json",
|
|
504
|
-
gzip: true,
|
|
505
|
-
credentials: {
|
|
506
|
-
token,
|
|
507
|
-
},
|
|
508
|
-
switches: {
|
|
509
|
-
hasSessionIds: true,
|
|
510
|
-
alsoInferFunnels: false,
|
|
511
|
-
hasLocation: true,
|
|
512
|
-
hasAndroidDevices: true,
|
|
513
|
-
hasIOSDevices: true,
|
|
514
|
-
hasDesktopDevices: true,
|
|
515
|
-
hasBrowser: false,
|
|
516
|
-
hasCampaigns: false,
|
|
517
|
-
isAnonymous: false,
|
|
518
|
-
hasAdSpend: false,
|
|
519
|
-
hasAvatar: true,
|
|
520
|
-
},
|
|
521
|
-
identity: {
|
|
522
|
-
avgDevicePerUser: 2,
|
|
523
|
-
},
|
|
524
|
-
concurrency: 1,
|
|
525
|
-
writeToDisk: false,
|
|
526
|
-
scdProps: {
|
|
527
|
-
membership_tier: {
|
|
528
|
-
values: ["member", "silver", "gold", "platinum"],
|
|
529
|
-
frequency: "month",
|
|
530
|
-
timing: "fuzzy",
|
|
531
|
-
max: 8
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
mirrorProps: {},
|
|
535
|
-
lookupTables: [],
|
|
536
|
-
|
|
537
|
-
events: [
|
|
538
|
-
{
|
|
539
|
-
event: "account created",
|
|
540
|
-
weight: 1,
|
|
541
|
-
isFirstEvent: true,
|
|
542
|
-
isAuthEvent: true,
|
|
543
|
-
properties: {
|
|
544
|
-
signup_source: ["organic", "google", "instagram", "tripadvisor", "referral", "email_campaign"],
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
event: "destination searched",
|
|
549
|
-
weight: 8,
|
|
550
|
-
properties: {
|
|
551
|
-
destination: chance.pickone.bind(chance, destinationCities),
|
|
552
|
-
check_in_days_out: u.weighNumRange(1, 90, 0.4, 14),
|
|
553
|
-
nights: u.weighNumRange(1, 14, 0.4, 3),
|
|
554
|
-
guests: u.weighNumRange(1, 6, 0.3, 2),
|
|
555
|
-
search_filters: ["price", "price", "rating", "location", "amenities", "pool", "breakfast"],
|
|
556
|
-
},
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
event: "hotel viewed",
|
|
560
|
-
weight: 7,
|
|
561
|
-
isStrictEvent: false,
|
|
562
|
-
properties: {
|
|
563
|
-
hotel_id: chance.pickone.bind(chance, hotelIds),
|
|
564
|
-
destination: chance.pickone.bind(chance, destinationCities),
|
|
565
|
-
star_rating: [3, 3, 4, 4, 4, 5],
|
|
566
|
-
nightly_rate: u.weighNumRange(50, 500, 0.3, 150),
|
|
567
|
-
has_pool: [true, true, false],
|
|
568
|
-
has_breakfast: [true, false],
|
|
569
|
-
photos_viewed: u.weighNumRange(0, 20, 0.5, 5),
|
|
570
|
-
},
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
event: "price compared",
|
|
574
|
-
weight: 4,
|
|
575
|
-
properties: {
|
|
576
|
-
hotels_compared: u.weighNumRange(2, 5),
|
|
577
|
-
price_difference_pct: u.weighNumRange(0, 40, 0.5, 10),
|
|
578
|
-
chose_cheapest: [true, true, true, false, false],
|
|
579
|
-
},
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
event: "booking completed",
|
|
583
|
-
weight: 3,
|
|
584
|
-
isStrictEvent: false,
|
|
585
|
-
properties: {
|
|
586
|
-
hotel_id: chance.pickone.bind(chance, hotelIds),
|
|
587
|
-
destination: chance.pickone.bind(chance, destinationCities),
|
|
588
|
-
nightly_rate: u.weighNumRange(50, 500, 0.3, 150),
|
|
589
|
-
total_cost: u.weighNumRange(100, 5000, 0.3, 450),
|
|
590
|
-
nights: u.weighNumRange(1, 14, 0.4, 3),
|
|
591
|
-
guests: u.weighNumRange(1, 6, 0.3, 2),
|
|
592
|
-
booking_window: ["advance", "advance", "standard", "standard", "last_minute"],
|
|
593
|
-
payment_method: ["credit_card", "credit_card", "debit", "paypal", "apple_pay"],
|
|
594
|
-
loyalty_points: u.weighNumRange(0, 500, 0.5, 100),
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
{
|
|
598
|
-
event: "room upgrade selected",
|
|
599
|
-
weight: 2,
|
|
600
|
-
isStrictEvent: false,
|
|
601
|
-
properties: {
|
|
602
|
-
upgrade_type: ["suite", "ocean_view", "executive_floor", "club_lounge", "premium_king"],
|
|
603
|
-
upgrade_cost: u.weighNumRange(25, 200, 0.4, 75),
|
|
604
|
-
hotel_id: chance.pickone.bind(chance, hotelIds),
|
|
605
|
-
},
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
event: "booking cancelled",
|
|
609
|
-
weight: 2,
|
|
610
|
-
properties: {
|
|
611
|
-
cancellation_reason: ["change_of_plans", "found_cheaper", "travel_restriction", "schedule_conflict", "weather"],
|
|
612
|
-
refund_amount: u.weighNumRange(0, 2000, 0.4, 200),
|
|
613
|
-
days_before_checkin: u.weighNumRange(0, 60, 0.5, 14),
|
|
614
|
-
booking_window: ["advance", "advance", "standard", "standard", "last_minute"],
|
|
615
|
-
},
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
event: "check in completed",
|
|
619
|
-
weight: 2,
|
|
620
|
-
properties: {
|
|
621
|
-
hotel_id: chance.pickone.bind(chance, hotelIds),
|
|
622
|
-
check_in_method: ["app", "app", "front_desk", "kiosk"],
|
|
623
|
-
early_checkin: [false, false, false, true],
|
|
624
|
-
},
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
event: "amenity used",
|
|
628
|
-
weight: 4,
|
|
629
|
-
properties: {
|
|
630
|
-
amenity_type: ["pool", "gym", "spa", "restaurant", "bar", "room_service", "business_center", "concierge"],
|
|
631
|
-
spend_amount: u.weighNumRange(0, 200, 0.4, 30),
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
event: "review submitted",
|
|
636
|
-
weight: 2,
|
|
637
|
-
isStrictEvent: false,
|
|
638
|
-
properties: {
|
|
639
|
-
hotel_id: chance.pickone.bind(chance, hotelIds),
|
|
640
|
-
stay_rating: u.weighNumRange(1, 5, 0.7, 4),
|
|
641
|
-
review_length: u.weighNumRange(20, 300, 0.5, 120),
|
|
642
|
-
would_return: [true, true, true, false],
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
event: "loyalty points redeemed",
|
|
647
|
-
weight: 1,
|
|
648
|
-
properties: {
|
|
649
|
-
points_used: u.weighNumRange(100, 5000, 0.4, 500),
|
|
650
|
-
redemption_type: ["room_discount", "free_night", "upgrade", "amenity_credit"],
|
|
651
|
-
},
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
event: "notification received",
|
|
655
|
-
weight: 5,
|
|
656
|
-
properties: {
|
|
657
|
-
notification_type: ["deal_alert", "deal_alert", "booking_confirmation", "checkin_reminder", "review_prompt", "loyalty_update"],
|
|
658
|
-
channel: ["push", "push", "email", "sms"],
|
|
659
|
-
opened: [true, true, true, false],
|
|
660
|
-
},
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
event: "wishlist updated",
|
|
664
|
-
weight: 3,
|
|
665
|
-
properties: {
|
|
666
|
-
action: ["added", "added", "added", "removed"],
|
|
667
|
-
destination: chance.pickone.bind(chance, destinationCities),
|
|
668
|
-
hotel_id: chance.pickone.bind(chance, hotelIds),
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
event: "support contacted",
|
|
673
|
-
weight: 1,
|
|
674
|
-
properties: {
|
|
675
|
-
issue_type: ["booking_change", "refund", "complaint", "question", "loyalty_inquiry"],
|
|
676
|
-
channel: ["chat", "chat", "phone", "email"],
|
|
677
|
-
resolution_time_hours: u.weighNumRange(0.5, 48, 0.3, 4),
|
|
678
|
-
},
|
|
679
|
-
},
|
|
680
|
-
{
|
|
681
|
-
event: "app session",
|
|
682
|
-
weight: 7,
|
|
683
|
-
properties: {
|
|
684
|
-
session_duration_sec: u.weighNumRange(15, 1200, 0.4, 180),
|
|
685
|
-
pages_viewed: u.weighNumRange(1, 20, 0.5, 4),
|
|
686
|
-
},
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
event: "price alert set",
|
|
690
|
-
weight: 2,
|
|
691
|
-
properties: {
|
|
692
|
-
destination: chance.pickone.bind(chance, destinationCities),
|
|
693
|
-
target_price: u.weighNumRange(50, 300, 0.5, 120),
|
|
694
|
-
alert_status: ["active", "active", "triggered", "expired"],
|
|
695
|
-
},
|
|
696
|
-
},
|
|
697
|
-
{
|
|
698
|
-
event: "account deactivated",
|
|
699
|
-
weight: 1,
|
|
700
|
-
isChurnEvent: true,
|
|
701
|
-
returnLikelihood: 0.2,
|
|
702
|
-
isStrictEvent: true,
|
|
703
|
-
properties: {
|
|
704
|
-
reason: ["found_alternative", "too_expensive", "poor_experience", "infrequent_travel", "privacy"],
|
|
705
|
-
},
|
|
706
|
-
},
|
|
707
|
-
],
|
|
708
|
-
|
|
709
|
-
funnels: [
|
|
710
|
-
{
|
|
711
|
-
name: "Onboarding to First Booking",
|
|
712
|
-
sequence: ["account created", "destination searched", "hotel viewed", "booking completed"],
|
|
713
|
-
conversionRate: 35,
|
|
714
|
-
order: "sequential",
|
|
715
|
-
isFirstFunnel: true,
|
|
716
|
-
timeToConvert: 72,
|
|
717
|
-
weight: 3,
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
name: "Search to Book",
|
|
721
|
-
sequence: ["destination searched", "hotel viewed", "price compared", "booking completed"],
|
|
722
|
-
conversionRate: 25,
|
|
723
|
-
order: "sequential",
|
|
724
|
-
timeToConvert: 48,
|
|
725
|
-
weight: 5,
|
|
726
|
-
reentry: true,
|
|
727
|
-
},
|
|
728
|
-
{
|
|
729
|
-
name: "Full Stay Journey",
|
|
730
|
-
sequence: ["booking completed", "check in completed", "amenity used", "review submitted"],
|
|
731
|
-
conversionRate: 30,
|
|
732
|
-
order: "sequential",
|
|
733
|
-
timeToConvert: 336,
|
|
734
|
-
weight: 3,
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
name: "Loyalty Engagement",
|
|
738
|
-
sequence: ["booking completed", "loyalty points redeemed", "review submitted"],
|
|
739
|
-
conversionRate: 15,
|
|
740
|
-
order: "sequential",
|
|
741
|
-
timeToConvert: 720,
|
|
742
|
-
weight: 2,
|
|
743
|
-
},
|
|
744
|
-
{
|
|
745
|
-
name: "Upsell Path",
|
|
746
|
-
sequence: ["hotel viewed", "booking completed", "room upgrade selected"],
|
|
747
|
-
conversionRate: 20,
|
|
748
|
-
order: "sequential",
|
|
749
|
-
timeToConvert: 24,
|
|
750
|
-
weight: 2,
|
|
751
|
-
},
|
|
752
|
-
],
|
|
753
|
-
|
|
754
|
-
superProps: {
|
|
755
|
-
Platform: ["ios", "android", "web", "web"],
|
|
756
|
-
membership_tier: ["standard", "standard", "standard", "gold", "platinum"],
|
|
757
|
-
},
|
|
758
|
-
|
|
759
|
-
userProps: {
|
|
760
|
-
customer_segment: ["leisure_family"],
|
|
761
|
-
travel_frequency: ["occasional", "occasional", "monthly", "weekly"],
|
|
762
|
-
company_name: ["none"],
|
|
763
|
-
preferred_destination: chance.pickone.bind(chance, destinationCities),
|
|
764
|
-
avg_budget_per_night: u.weighNumRange(50, 400, 0.4, 150),
|
|
765
|
-
Platform: ["ios", "android", "web", "web"],
|
|
766
|
-
membership_tier: ["standard", "standard", "standard", "gold", "platinum"],
|
|
767
|
-
},
|
|
768
|
-
|
|
769
|
-
personas: [
|
|
770
|
-
{
|
|
771
|
-
name: "business_traveler",
|
|
772
|
-
weight: 20,
|
|
773
|
-
eventMultiplier: 2.5,
|
|
774
|
-
conversionModifier: 1.3,
|
|
775
|
-
churnRate: 0.02,
|
|
776
|
-
properties: { customer_segment: "business_traveler", travel_frequency: "weekly" },
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
name: "leisure_family",
|
|
780
|
-
weight: 35,
|
|
781
|
-
eventMultiplier: 0.8,
|
|
782
|
-
conversionModifier: 0.9,
|
|
783
|
-
churnRate: 0.08,
|
|
784
|
-
properties: { customer_segment: "leisure_family", travel_frequency: "occasional" },
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
name: "luxury_seeker",
|
|
788
|
-
weight: 15,
|
|
789
|
-
eventMultiplier: 1.5,
|
|
790
|
-
conversionModifier: 1.2,
|
|
791
|
-
churnRate: 0.04,
|
|
792
|
-
properties: { customer_segment: "luxury_seeker", travel_frequency: "monthly" },
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
name: "budget_hunter",
|
|
796
|
-
weight: 30,
|
|
797
|
-
eventMultiplier: 1.0,
|
|
798
|
-
conversionModifier: 0.7,
|
|
799
|
-
churnRate: 0.10,
|
|
800
|
-
properties: { customer_segment: "budget_hunter", travel_frequency: "occasional" },
|
|
801
|
-
},
|
|
802
|
-
],
|
|
803
|
-
|
|
804
|
-
worldEvents: [
|
|
805
|
-
{
|
|
806
|
-
name: "summer_sale",
|
|
807
|
-
type: "campaign",
|
|
808
|
-
startDay: 40,
|
|
809
|
-
duration: 5,
|
|
810
|
-
volumeMultiplier: 2.0,
|
|
811
|
-
conversionModifier: 1.5,
|
|
812
|
-
injectProps: { promo: "summer_sale" },
|
|
813
|
-
affectsEvents: ["booking completed", "hotel viewed", "destination searched"],
|
|
814
|
-
},
|
|
815
|
-
{
|
|
816
|
-
name: "hurricane_disruption",
|
|
817
|
-
type: "outage",
|
|
818
|
-
startDay: 65,
|
|
819
|
-
duration: 0.5,
|
|
820
|
-
volumeMultiplier: 0.2,
|
|
821
|
-
affectsEvents: ["booking completed", "check in completed"],
|
|
822
|
-
aftermath: { duration: 3, volumeMultiplier: 1.4 },
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
name: "loyalty_program_launch",
|
|
826
|
-
type: "product_launch",
|
|
827
|
-
startDay: 50,
|
|
828
|
-
duration: null,
|
|
829
|
-
injectProps: { loyalty_program: "active" },
|
|
830
|
-
affectsEvents: ["booking completed", "loyalty points redeemed"],
|
|
831
|
-
},
|
|
832
|
-
],
|
|
833
|
-
|
|
834
|
-
engagementDecay: {
|
|
835
|
-
model: "exponential",
|
|
836
|
-
halfLife: 90,
|
|
837
|
-
floor: 0.2,
|
|
838
|
-
reactivationChance: 0.02,
|
|
839
|
-
},
|
|
840
|
-
|
|
841
|
-
attribution: {
|
|
842
|
-
model: "last_touch",
|
|
843
|
-
window: 7,
|
|
844
|
-
campaigns: [
|
|
845
|
-
{
|
|
846
|
-
name: "google_hotels",
|
|
847
|
-
source: "google",
|
|
848
|
-
medium: "cpc",
|
|
849
|
-
activeDays: [0, 100],
|
|
850
|
-
dailyBudget: [300, 1000],
|
|
851
|
-
acquisitionRate: 0.03,
|
|
852
|
-
userPersonaBias: { business_traveler: 0.5, budget_hunter: 0.3 },
|
|
853
|
-
},
|
|
854
|
-
{
|
|
855
|
-
name: "instagram_travel",
|
|
856
|
-
source: "instagram",
|
|
857
|
-
medium: "social",
|
|
858
|
-
activeDays: [10, 90],
|
|
859
|
-
dailyBudget: [150, 500],
|
|
860
|
-
acquisitionRate: 0.02,
|
|
861
|
-
userPersonaBias: { leisure_family: 0.5, luxury_seeker: 0.3 },
|
|
862
|
-
},
|
|
863
|
-
{
|
|
864
|
-
name: "tripadvisor_reviews",
|
|
865
|
-
source: "tripadvisor",
|
|
866
|
-
medium: "referral",
|
|
867
|
-
activeDays: [0, 100],
|
|
868
|
-
dailyBudget: [100, 300],
|
|
869
|
-
acquisitionRate: 0.02,
|
|
870
|
-
userPersonaBias: { leisure_family: 0.4 },
|
|
871
|
-
},
|
|
872
|
-
],
|
|
873
|
-
organicRate: 0.35,
|
|
874
|
-
},
|
|
875
|
-
|
|
876
|
-
geo: {
|
|
877
|
-
sticky: true,
|
|
878
|
-
regions: [
|
|
879
|
-
{
|
|
880
|
-
name: "north_america",
|
|
881
|
-
countries: ["US", "CA"],
|
|
882
|
-
weight: 40,
|
|
883
|
-
timezoneOffset: -5,
|
|
884
|
-
properties: { currency: "USD", locale: "en-US" },
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
name: "europe",
|
|
888
|
-
countries: ["GB", "DE", "FR", "ES"],
|
|
889
|
-
weight: 35,
|
|
890
|
-
timezoneOffset: 1,
|
|
891
|
-
properties: { currency: "EUR", locale: "en-EU" },
|
|
892
|
-
},
|
|
893
|
-
{
|
|
894
|
-
name: "asia_pacific",
|
|
895
|
-
countries: ["JP", "AU", "SG"],
|
|
896
|
-
weight: 25,
|
|
897
|
-
timezoneOffset: 9,
|
|
898
|
-
properties: { currency: "JPY", locale: "en-APAC" },
|
|
899
|
-
},
|
|
900
|
-
],
|
|
901
|
-
},
|
|
902
|
-
|
|
903
|
-
anomalies: [
|
|
904
|
-
{
|
|
905
|
-
type: "extreme_value",
|
|
906
|
-
event: "booking completed",
|
|
907
|
-
property: "total_cost",
|
|
908
|
-
frequency: 0.003,
|
|
909
|
-
multiplier: 20,
|
|
910
|
-
tag: "whale_booking",
|
|
911
|
-
},
|
|
912
|
-
{
|
|
913
|
-
type: "coordinated",
|
|
914
|
-
event: "booking completed",
|
|
915
|
-
day: 75,
|
|
916
|
-
window: 0.04,
|
|
917
|
-
count: 80,
|
|
918
|
-
tag: "group_conference_booking",
|
|
919
|
-
},
|
|
920
|
-
],
|
|
921
|
-
|
|
922
|
-
hook(record, type, meta) {
|
|
923
|
-
if (type === "user") return handleUserHooks(record);
|
|
924
|
-
if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
|
|
925
|
-
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
926
|
-
return record;
|
|
927
|
-
},
|
|
928
|
-
};
|
|
929
|
-
|
|
930
|
-
export default config;
|