@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.
Files changed (146) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +21 -12
  2. package/.claude/skills/create-dungeon/SKILL.md +65 -5
  3. package/.claude/skills/create-project/SKILL.md +1 -1
  4. package/.claude/skills/create-project/provision.mjs +57 -10
  5. package/.claude/skills/verify-dungeon/SKILL.md +56 -25
  6. package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
  7. package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
  8. package/.claude/skills/write-hooks/SKILL.md +156 -18
  9. package/CHANGELOG.md +183 -0
  10. package/HOOKS.md +566 -112
  11. package/README.md +28 -9
  12. package/docs/guides/1.3.0-upgrade-guide.md +262 -0
  13. package/docs/guides/1.3.1-upgrade-guide.md +328 -0
  14. package/docs/guides/1.4.0-upgrade-guide.md +130 -0
  15. package/docs/guides/1.4.1-upgrade-guide.md +125 -0
  16. package/docs/guides/1.4.3-upgrade-guide.md +200 -0
  17. package/docs/guides/1.4.4-upgrade-guide.md +95 -0
  18. package/docs/guides/1.5.0-upgrade-guide.md +426 -0
  19. package/docs/guides/1.5.1-upgrade-guide.md +178 -0
  20. package/docs/guides/1.5.3-upgrade-guide.md +117 -0
  21. package/docs/guides/1.6.0-upgrade-guide.md +204 -0
  22. package/dungeons/technical/stories-verify.js +152 -0
  23. package/dungeons/vertical/README.md +154 -0
  24. package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
  25. package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
  26. package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
  27. package/dungeons/vertical/community/community.js +1589 -0
  28. package/dungeons/vertical/community/community.sql +216 -0
  29. package/dungeons/vertical/community/community.verify.mjs +72 -0
  30. package/dungeons/vertical/crypto/crypto.js +1483 -0
  31. package/dungeons/vertical/crypto/crypto.sql +194 -0
  32. package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
  33. package/dungeons/vertical/dating/dating.js +1540 -0
  34. package/dungeons/vertical/dating/dating.sql +288 -0
  35. package/dungeons/vertical/dating/dating.verify.mjs +72 -0
  36. package/dungeons/vertical/devtools/devtools.js +1524 -0
  37. package/dungeons/vertical/devtools/devtools.sql +161 -0
  38. package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
  39. package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
  40. package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
  41. package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
  42. package/dungeons/vertical/education/education.js +1763 -0
  43. package/dungeons/vertical/education/education.sql +243 -0
  44. package/dungeons/vertical/education/education.verify.mjs +75 -0
  45. package/dungeons/vertical/fintech/fintech.js +1410 -0
  46. package/dungeons/vertical/fintech/fintech.sql +175 -0
  47. package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
  48. package/dungeons/vertical/fitness/fitness.js +1495 -0
  49. package/dungeons/vertical/fitness/fitness.sql +206 -0
  50. package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
  51. package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
  52. package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
  53. package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
  54. package/dungeons/vertical/gaming/gaming.js +1741 -0
  55. package/dungeons/vertical/gaming/gaming.sql +249 -0
  56. package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
  57. package/dungeons/vertical/healthcare/healthcare.js +1488 -0
  58. package/dungeons/vertical/healthcare/healthcare.sql +193 -0
  59. package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
  60. package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
  61. package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
  62. package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
  63. package/dungeons/vertical/logistics/logistics.js +1541 -0
  64. package/dungeons/vertical/logistics/logistics.sql +205 -0
  65. package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
  66. package/dungeons/vertical/marketplace/marketplace.js +1438 -0
  67. package/dungeons/vertical/marketplace/marketplace.sql +148 -0
  68. package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
  69. package/dungeons/vertical/{media.js → media/media.js} +557 -58
  70. package/dungeons/vertical/media/media.sql +173 -0
  71. package/dungeons/vertical/media/media.verify.mjs +74 -0
  72. package/dungeons/vertical/real-estate/real-estate.js +1629 -0
  73. package/dungeons/vertical/real-estate/real-estate.sql +167 -0
  74. package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
  75. package/dungeons/vertical/sass/sass.js +1733 -0
  76. package/dungeons/vertical/sass/sass.sql +173 -0
  77. package/dungeons/vertical/sass/sass.verify.mjs +74 -0
  78. package/dungeons/vertical/social/social.js +1456 -0
  79. package/dungeons/vertical/social/social.sql +160 -0
  80. package/dungeons/vertical/social/social.verify.mjs +76 -0
  81. package/dungeons/vertical/streaming/streaming.js +677 -0
  82. package/dungeons/vertical/streaming/streaming.sql +101 -0
  83. package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
  84. package/dungeons/vertical/support-desk/support-desk.js +741 -0
  85. package/dungeons/vertical/support-desk/support-desk.sql +137 -0
  86. package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
  87. package/dungeons/vertical/travel/travel.js +1603 -0
  88. package/dungeons/vertical/travel/travel.sql +236 -0
  89. package/dungeons/vertical/travel/travel.verify.mjs +72 -0
  90. package/index.js +2 -2
  91. package/lib/core/config-validator.js +37 -2
  92. package/lib/core/context.js +4 -2
  93. package/lib/core/dungeon-loader.js +20 -15
  94. package/lib/core/dungeon-to-json.js +22 -4
  95. package/lib/core/storage.js +3 -3
  96. package/lib/generators/funnels.js +36 -9
  97. package/lib/hook-helpers/cohort.js +53 -0
  98. package/lib/hook-helpers/index.js +8 -0
  99. package/lib/hook-helpers/mutate.js +1 -1
  100. package/lib/hook-helpers/shape.js +319 -0
  101. package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
  102. package/lib/hook-patterns/attributed-by-source.js +60 -58
  103. package/lib/hook-patterns/frequency-by-frequency.js +14 -4
  104. package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
  105. package/lib/hook-patterns/index.js +1 -1
  106. package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
  107. package/lib/orchestrators/user-loop.js +41 -0
  108. package/lib/templates/story-spec.schema.json +175 -0
  109. package/lib/utils/json-evaluator.js +1 -1
  110. package/lib/utils/utils.js +5 -4
  111. package/lib/verify/coerce.js +186 -0
  112. package/lib/verify/counting.js +270 -32
  113. package/lib/verify/emulate-breakdown.js +1285 -203
  114. package/lib/verify/first-time.js +89 -0
  115. package/lib/verify/flows.js +679 -0
  116. package/lib/verify/formula.js +259 -0
  117. package/lib/verify/funnel-engine.js +697 -113
  118. package/lib/verify/index.js +32 -4
  119. package/lib/verify/sessionize.js +275 -0
  120. package/lib/verify/story-runner.js +483 -0
  121. package/lib/verify/verify-dungeon.js +59 -37
  122. package/package.json +2 -1
  123. package/scripts/extract-dungeon-schema.mjs +2 -2
  124. package/scripts/run-many.mjs +2 -2
  125. package/scripts/verify-runner.mjs +2 -2
  126. package/scripts/verify-stories.mjs +244 -0
  127. package/types.d.ts +296 -30
  128. package/dungeons/vertical/ai-platform.js +0 -818
  129. package/dungeons/vertical/community.js +0 -966
  130. package/dungeons/vertical/crypto.js +0 -895
  131. package/dungeons/vertical/dating.js +0 -830
  132. package/dungeons/vertical/devtools.js +0 -1050
  133. package/dungeons/vertical/ecommerce.js +0 -811
  134. package/dungeons/vertical/education.js +0 -964
  135. package/dungeons/vertical/fintech.js +0 -946
  136. package/dungeons/vertical/fitness.js +0 -935
  137. package/dungeons/vertical/food-delivery.js +0 -934
  138. package/dungeons/vertical/gaming.js +0 -1120
  139. package/dungeons/vertical/healthcare.js +0 -945
  140. package/dungeons/vertical/insurance-application.js +0 -1039
  141. package/dungeons/vertical/logistics.js +0 -986
  142. package/dungeons/vertical/marketplace.js +0 -1019
  143. package/dungeons/vertical/real-estate.js +0 -872
  144. package/dungeons/vertical/sass.js +0 -995
  145. package/dungeons/vertical/social.js +0 -904
  146. 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;