@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,872 +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
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
8
|
-
|
|
9
|
-
// ── OVERVIEW ──
|
|
10
|
-
/*
|
|
11
|
-
* NAME: HomeNest
|
|
12
|
-
* APP: Property listings and agent CRM platform (Zillow meets
|
|
13
|
-
* Compass). Buyers search listings, save properties, schedule
|
|
14
|
-
* tours, get pre-approved for mortgages, and submit offers.
|
|
15
|
-
* Agents list properties, manage clients, and close deals.
|
|
16
|
-
* SCALE: 10,000 users, 121 days (2026-01-01 → 2026-05-01)
|
|
17
|
-
* CORE LOOP: account created → property search → property viewed → property saved → tour scheduled → offer submitted (buyer) | property listed → open house attended → property sold (agent)
|
|
18
|
-
*
|
|
19
|
-
* EVENTS (17):
|
|
20
|
-
* property viewed (12) > property search (10) > property saved (5) > virtual tour (4)
|
|
21
|
-
* > tour scheduled (4) > agent contacted (4) > saved search created (3) > in-person tour (3)
|
|
22
|
-
* > property listed (3) > mortgage pre-approval (2) > offer submitted (2) > open house attended (2)
|
|
23
|
-
* > review submitted (2) > account created (1) > offer accepted (1) > offer rejected (1)
|
|
24
|
-
* > property sold (1)
|
|
25
|
-
*
|
|
26
|
-
* FUNNELS (3):
|
|
27
|
-
* - Buyer Journey: account created → property search → property viewed (80%)
|
|
28
|
-
* - Tour Funnel: property viewed → tour scheduled → offer submitted (40%, reentry)
|
|
29
|
-
* - Seller Funnel: property listed → open house attended → property sold (35%)
|
|
30
|
-
*
|
|
31
|
-
* USER PROPS: user_type, preferred_location, property_preference, budget_max, agent_tier, total_properties_viewed, pre_approval_status
|
|
32
|
-
* SUPER PROPS: user_type, preferred_location, property_preference
|
|
33
|
-
* SCD PROPS: agent_tier (Standard/Premier, monthly fuzzy, max 4)
|
|
34
|
-
* GROUPS: brokerage_id (200 brokerages — property listed, property sold, agent contacted, open house attended)
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
// ── HOOK STORIES ──
|
|
38
|
-
/*
|
|
39
|
-
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
40
|
-
* only via behavioral cohorts (count event per user, then measure
|
|
41
|
-
* downstream) or raw-prop breakdowns (date, agent_tier).
|
|
42
|
-
*
|
|
43
|
-
* -------------------------------------------------------------------
|
|
44
|
-
* 1. SPRING BUYING SEASON (event)
|
|
45
|
-
* -------------------------------------------------------------------
|
|
46
|
-
*
|
|
47
|
-
* PATTERN: Days 30-60, tour-scheduled duration_mins boosted 3x and
|
|
48
|
-
* offer-submitted offer_price boosted 2.5x. Mutates raw props. No flag.
|
|
49
|
-
*
|
|
50
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
51
|
-
*
|
|
52
|
-
* Report 1: Avg Offer Price by Day
|
|
53
|
-
* - Report type: Insights
|
|
54
|
-
* - Event: "offer submitted"
|
|
55
|
-
* - Measure: Average of "offer_price"
|
|
56
|
-
* - Line chart by day
|
|
57
|
-
* - Expected: visible bulge days 30-60 (~ 2.5x baseline)
|
|
58
|
-
*
|
|
59
|
-
* REAL-WORLD ANALOGUE: Spring buying season elevates prices and tour intent.
|
|
60
|
-
*
|
|
61
|
-
* -------------------------------------------------------------------
|
|
62
|
-
* 2. MORTGAGE RATE SHOCK (event + everything)
|
|
63
|
-
* -------------------------------------------------------------------
|
|
64
|
-
*
|
|
65
|
-
* PATTERN: Days 75-89, mortgage_rate forced to 7.5 on pre-approval
|
|
66
|
-
* events. In everything hook, 45% of post-day-75 offer-submitted events
|
|
67
|
-
* dropped. No flag.
|
|
68
|
-
*
|
|
69
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
70
|
-
*
|
|
71
|
-
* Report 1: Mortgage Rate Step Change
|
|
72
|
-
* - Report type: Insights
|
|
73
|
-
* - Event: "mortgage pre-approval"
|
|
74
|
-
* - Measure: Average of "mortgage_rate"
|
|
75
|
-
* - Line chart by day
|
|
76
|
-
* - Expected: clear step change from ~ 6.5 to 7.5 at day 75
|
|
77
|
-
*
|
|
78
|
-
* Report 2: Offer Volume Drop After Rate Hike
|
|
79
|
-
* - Report type: Insights
|
|
80
|
-
* - Event: "offer submitted"
|
|
81
|
-
* - Measure: Total
|
|
82
|
-
* - Line chart by day
|
|
83
|
-
* - Expected: ~ 45% volume drop after day 75
|
|
84
|
-
*
|
|
85
|
-
* REAL-WORLD ANALOGUE: Buyer demand is sensitive to mortgage rates.
|
|
86
|
-
*
|
|
87
|
-
* -------------------------------------------------------------------
|
|
88
|
-
* 3. SAVED-SEARCH RETENTION (everything)
|
|
89
|
-
* -------------------------------------------------------------------
|
|
90
|
-
*
|
|
91
|
-
* PATTERN: Users who create saved-search-created in first 7 days get
|
|
92
|
-
* extra cloned property-viewed + tour-scheduled events. Non-savers lose
|
|
93
|
-
* 80% of post-day-30 events. No flag — discover via cohort builder.
|
|
94
|
-
*
|
|
95
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
96
|
-
*
|
|
97
|
-
* Report 1: Retention by Saved-Search Cohort
|
|
98
|
-
* - Report type: Retention
|
|
99
|
-
* - Cohort A: users with >= 1 "saved search created" in first 7 days
|
|
100
|
-
* - Cohort B: rest
|
|
101
|
-
* - Expected: A sustains higher retention through dataset
|
|
102
|
-
*
|
|
103
|
-
* REAL-WORLD ANALOGUE: Saved searches indicate buyer intent.
|
|
104
|
-
*
|
|
105
|
-
* -------------------------------------------------------------------
|
|
106
|
-
* 4. PRE-APPROVED BUYER CONVERSION (everything)
|
|
107
|
-
* -------------------------------------------------------------------
|
|
108
|
-
*
|
|
109
|
-
* PATTERN: Users with a mortgage-pre-approval event get 4-6 extra
|
|
110
|
-
* cloned offer-submitted events. No flag — discover via cohort.
|
|
111
|
-
*
|
|
112
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
113
|
-
*
|
|
114
|
-
* Report 1: Offers per User by Pre-Approval
|
|
115
|
-
* - Report type: Insights (with cohort)
|
|
116
|
-
* - Cohort A: users with >= 1 "mortgage pre-approval"
|
|
117
|
-
* - Cohort B: rest
|
|
118
|
-
* - Event: "offer submitted"
|
|
119
|
-
* - Measure: Total per user
|
|
120
|
-
* - Expected: A ~ 5x B
|
|
121
|
-
*
|
|
122
|
-
* REAL-WORLD ANALOGUE: Pre-approval is the strongest buyer indicator.
|
|
123
|
-
*
|
|
124
|
-
* -------------------------------------------------------------------
|
|
125
|
-
* 5. TOP-TIER AGENT ADVANTAGE (everything)
|
|
126
|
-
* -------------------------------------------------------------------
|
|
127
|
-
*
|
|
128
|
-
* PATTERN: Premier-tier agents get 2 extra cloned listings per existing
|
|
129
|
-
* (3x rate) and 1 extra cloned sale per existing (2x rate). Mutates
|
|
130
|
-
* cloned events with raw values; reads agent_tier from profile (existing
|
|
131
|
-
* SCD prop). Discover via agent_tier breakdown.
|
|
132
|
-
*
|
|
133
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
134
|
-
*
|
|
135
|
-
* Report 1: Listings per Agent by Tier
|
|
136
|
-
* - Report type: Insights
|
|
137
|
-
* - Event: "property listed"
|
|
138
|
-
* - Measure: Total per user
|
|
139
|
-
* - Breakdown: user "agent_tier"
|
|
140
|
-
* - Expected: Premier ~ 3x Standard
|
|
141
|
-
*
|
|
142
|
-
* REAL-WORLD ANALOGUE: Top producers list and close more.
|
|
143
|
-
*
|
|
144
|
-
* -------------------------------------------------------------------
|
|
145
|
-
* 6. TOUR-TO-OFFER POWER USERS (everything)
|
|
146
|
-
* -------------------------------------------------------------------
|
|
147
|
-
*
|
|
148
|
-
* PATTERN: Users with both virtual-tour AND in-person-tour events get
|
|
149
|
-
* 5-7 extra cloned offer-submitted events. No flag — discover via
|
|
150
|
-
* compound cohort builder.
|
|
151
|
-
*
|
|
152
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
153
|
-
*
|
|
154
|
-
* Report 1: Offers per User by Dual-Tour Cohort
|
|
155
|
-
* - Report type: Insights (with cohort)
|
|
156
|
-
* - Cohort A: users with both >= 1 "virtual tour" AND >= 1 "in-person tour"
|
|
157
|
-
* - Cohort B: rest
|
|
158
|
-
* - Event: "offer submitted"
|
|
159
|
-
* - Measure: Total per user
|
|
160
|
-
* - Expected: A ~ 6x B
|
|
161
|
-
*
|
|
162
|
-
* REAL-WORLD ANALOGUE: Dual-tour buyers self-qualify into serious bidders.
|
|
163
|
-
*
|
|
164
|
-
* -------------------------------------------------------------------
|
|
165
|
-
* 7. LUXURY LISTING RELEASE (event + everything)
|
|
166
|
-
* -------------------------------------------------------------------
|
|
167
|
-
*
|
|
168
|
-
* PATTERN: After day 50, 3% of property-listed events get listing_price
|
|
169
|
-
* set to $5M-$15M (raw mutation). ~3% of users (by id hash) get extra
|
|
170
|
-
* luxury cloned property-viewed events. No flag — discover via line
|
|
171
|
-
* chart by day on listing_price.
|
|
172
|
-
*
|
|
173
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
174
|
-
*
|
|
175
|
-
* Report 1: Listing Price Distribution Over Time
|
|
176
|
-
* - Report type: Insights
|
|
177
|
-
* - Event: "property listed"
|
|
178
|
-
* - Measure: Distribution of "listing_price"
|
|
179
|
-
* - Line chart by week
|
|
180
|
-
* - Expected: $5M+ outliers appear only after day 50
|
|
181
|
-
*
|
|
182
|
-
* REAL-WORLD ANALOGUE: Luxury inventory drop attracts HNW shoppers.
|
|
183
|
-
*
|
|
184
|
-
* -------------------------------------------------------------------
|
|
185
|
-
* 8. COLD-LEAD CHURN (everything)
|
|
186
|
-
* -------------------------------------------------------------------
|
|
187
|
-
*
|
|
188
|
-
* PATTERN: Users who view (property viewed) but never save (property
|
|
189
|
-
* saved) in the first 14 days lose 90% of events after day 14. No flag.
|
|
190
|
-
*
|
|
191
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
192
|
-
*
|
|
193
|
-
* Report 1: Cold-Lead Retention
|
|
194
|
-
* - Report type: Retention
|
|
195
|
-
* - Cohort A: users with >= 1 "property viewed" but 0 "property saved" in first 14 days
|
|
196
|
-
* - Cohort B: rest
|
|
197
|
-
* - Expected: A retention drops to near-zero after day 14
|
|
198
|
-
*
|
|
199
|
-
* REAL-WORLD ANALOGUE: Browsers who never save are window-shoppers.
|
|
200
|
-
*
|
|
201
|
-
* -------------------------------------------------------------------
|
|
202
|
-
* 9. PROPERTY-VIEWED MAGIC NUMBER (everything)
|
|
203
|
-
* -------------------------------------------------------------------
|
|
204
|
-
*
|
|
205
|
-
* PATTERN: Users in the 6-12 property-viewed sweet spot get +30% on
|
|
206
|
-
* offer_price for offer-submitted events. Users with 13+ views are
|
|
207
|
-
* tire-kickers; 35% of their offer-submitted events drop. No flag.
|
|
208
|
-
*
|
|
209
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
210
|
-
*
|
|
211
|
-
* Report 1: Avg Offer Price by View Bucket
|
|
212
|
-
* - Report type: Insights (with cohort)
|
|
213
|
-
* - Cohort A: users with 6-12 "property viewed"
|
|
214
|
-
* - Cohort B: users with 0-5
|
|
215
|
-
* - Event: "offer submitted"
|
|
216
|
-
* - Measure: Average of "offer_price"
|
|
217
|
-
* - Expected: A ~ 1.3x B
|
|
218
|
-
*
|
|
219
|
-
* Report 2: Offers per User on Heavy Viewers
|
|
220
|
-
* - Report type: Insights (with cohort)
|
|
221
|
-
* - Cohort C: users with >= 13 "property viewed"
|
|
222
|
-
* - Cohort A: users with 6-12
|
|
223
|
-
* - Event: "offer submitted"
|
|
224
|
-
* - Measure: Total per user
|
|
225
|
-
* - Expected: C ~ 35% fewer offers per user vs A
|
|
226
|
-
*
|
|
227
|
-
* REAL-WORLD ANALOGUE: Focused buyers commit; obsessive browsers
|
|
228
|
-
* tire-kick and never make the leap.
|
|
229
|
-
*
|
|
230
|
-
* -------------------------------------------------------------------
|
|
231
|
-
* 10. TOUR FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
232
|
-
* -------------------------------------------------------------------
|
|
233
|
-
*
|
|
234
|
-
* PATTERN: Premier-tier agents move users through the Tour funnel
|
|
235
|
-
* (property viewed -> tour scheduled -> offer submitted) 1.4x faster
|
|
236
|
-
* (factor 0.71). Standard-tier agents complete it 1.3x slower
|
|
237
|
-
* (factor 1.3). The hook intercepts funnel-post arrays, computes the
|
|
238
|
-
* time gap between consecutive steps, and scales each gap by the
|
|
239
|
-
* tier-specific factor before rewriting the step timestamps.
|
|
240
|
-
*
|
|
241
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
242
|
-
*
|
|
243
|
-
* Report 1: Tour Funnel Median TTC by Agent Tier
|
|
244
|
-
* - Report type: Funnels
|
|
245
|
-
* - Steps: "property viewed" -> "tour scheduled" -> "offer submitted"
|
|
246
|
-
* - Measure: Median time to convert
|
|
247
|
-
* - Breakdown: "agent_tier" (user property / SCD)
|
|
248
|
-
* - Expected: Premier ~ 0.71x baseline; Standard ~ 1.3x baseline
|
|
249
|
-
*
|
|
250
|
-
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
251
|
-
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
252
|
-
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
253
|
-
* across the user's full event history.
|
|
254
|
-
*
|
|
255
|
-
* REAL-WORLD ANALOGUE: Premier agents have larger networks, faster
|
|
256
|
-
* scheduling workflows, and prioritized showing slots, translating
|
|
257
|
-
* to shorter tour-to-offer cycles.
|
|
258
|
-
*
|
|
259
|
-
* ===================================================================
|
|
260
|
-
* EXPECTED METRICS SUMMARY
|
|
261
|
-
* ===================================================================
|
|
262
|
-
*
|
|
263
|
-
* Hook | Metric | Baseline | Effect | Ratio
|
|
264
|
-
* -------------------------|------------------------|----------|---------|------
|
|
265
|
-
* Spring Buying Season | Avg offer_price d30-60 | 1x | 2.5x | 2.5x
|
|
266
|
-
* Mortgage Rate Shock | Offer vol post-day-75 | 1x | 0.55x | -45%
|
|
267
|
-
* Saved-Search Retention | non-saver post-day-30 | 1x | 0.2x | -80%
|
|
268
|
-
* Pre-Approved Conversion | offers/user | 1x | ~ 5x | 5x
|
|
269
|
-
* Premier Agent Advantage | listings/user | 1x | ~ 3x | 3x
|
|
270
|
-
* Dual-Tour Power Users | offers/user | 1x | ~ 6x | 6x
|
|
271
|
-
* Luxury Listing Release | $5M+ listings | 0% | ~ 3% | d50+
|
|
272
|
-
* Cold-Lead Churn | non-save post-day-14 | 1x | 0.1x | -90%
|
|
273
|
-
* View-Count Magic Number | sweet offer_price | 1x | 1.3x | 1.3x
|
|
274
|
-
* View-Count Magic Number | over offers/user | 1x | 0.65x | -35%
|
|
275
|
-
* Tour Funnel TTC | median TTC by tier | 1x | 0.71/1.3x| ~ 1.8x range
|
|
276
|
-
*/
|
|
277
|
-
|
|
278
|
-
// ── SCALE ──
|
|
279
|
-
const SEED = "homenest";
|
|
280
|
-
const NUM_USERS = 10_000;
|
|
281
|
-
const NUM_DAYS = 120;
|
|
282
|
-
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
283
|
-
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
284
|
-
const EVENTS_PER_DAY = 0.53;
|
|
285
|
-
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
286
|
-
|
|
287
|
-
const chance = u.initChance(SEED);
|
|
288
|
-
|
|
289
|
-
// ── KNOBS (tweak these to reshape stories) ──
|
|
290
|
-
const SPRING_START_DAY = 30;
|
|
291
|
-
const SPRING_END_DAY = 60;
|
|
292
|
-
const SPRING_TOUR_DURATION_MULT = 3;
|
|
293
|
-
const SPRING_OFFER_PRICE_MULT = 2.5;
|
|
294
|
-
|
|
295
|
-
const SHOCK_START_DAY = 75;
|
|
296
|
-
const SHOCK_END_DAY = 89;
|
|
297
|
-
const SHOCK_MORTGAGE_RATE = 7.5;
|
|
298
|
-
const SHOCK_OFFER_DROP_LIKELIHOOD = 45;
|
|
299
|
-
|
|
300
|
-
const SAVED_SEARCH_WINDOW_DAYS = 7;
|
|
301
|
-
const SAVED_SEARCH_VIEW_CLONES_MIN = 3;
|
|
302
|
-
const SAVED_SEARCH_VIEW_CLONES_MAX = 8;
|
|
303
|
-
const SAVED_SEARCH_TOUR_CLONES_MIN = 1;
|
|
304
|
-
const SAVED_SEARCH_TOUR_CLONES_MAX = 3;
|
|
305
|
-
const NON_SAVER_CUTOFF_DAY = 30;
|
|
306
|
-
const NON_SAVER_DROP_LIKELIHOOD = 80;
|
|
307
|
-
|
|
308
|
-
const PRE_APPROVAL_OFFER_CLONES_MIN = 4;
|
|
309
|
-
const PRE_APPROVAL_OFFER_CLONES_MAX = 6;
|
|
310
|
-
|
|
311
|
-
const PREMIER_LISTING_CLONE_MULT = 2;
|
|
312
|
-
const PREMIER_SALE_CLONE_MULT = 1;
|
|
313
|
-
|
|
314
|
-
const DUAL_TOUR_OFFER_CLONES_MIN = 5;
|
|
315
|
-
const DUAL_TOUR_OFFER_CLONES_MAX = 7;
|
|
316
|
-
|
|
317
|
-
const LUXURY_RELEASE_DAY = 50;
|
|
318
|
-
const LUXURY_LISTING_LIKELIHOOD = 3;
|
|
319
|
-
const LUXURY_PRICE_MIN = 5_000_000;
|
|
320
|
-
const LUXURY_PRICE_MAX = 15_000_000;
|
|
321
|
-
const LUXURY_USER_HASH_MOD = 33;
|
|
322
|
-
const LUXURY_VIEW_CLONES_MIN = 3;
|
|
323
|
-
const LUXURY_VIEW_CLONES_MAX = 6;
|
|
324
|
-
|
|
325
|
-
const COLD_LEAD_WINDOW_DAYS = 14;
|
|
326
|
-
const COLD_LEAD_DROP_LIKELIHOOD = 90;
|
|
327
|
-
|
|
328
|
-
const VIEW_SWEET_MIN = 6;
|
|
329
|
-
const VIEW_SWEET_MAX = 12;
|
|
330
|
-
const VIEW_OVER_THRESHOLD = 13;
|
|
331
|
-
const VIEW_OFFER_PRICE_BOOST = 1.3;
|
|
332
|
-
const VIEW_OVER_OFFER_DROP_LIKELIHOOD = 35;
|
|
333
|
-
|
|
334
|
-
const TTC_PREMIER_FACTOR = 0.71;
|
|
335
|
-
const TTC_STANDARD_FACTOR = 1.3;
|
|
336
|
-
|
|
337
|
-
// ── HELPER FUNCTIONS ──
|
|
338
|
-
function handleFunnelPostHooks(record, meta) {
|
|
339
|
-
// H10: Tour Funnel TTC — Premier 0.71x, Standard 1.3x.
|
|
340
|
-
const segment = meta?.profile?.agent_tier;
|
|
341
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
342
|
-
const factor = (
|
|
343
|
-
segment === "Premier" ? TTC_PREMIER_FACTOR :
|
|
344
|
-
segment === "Standard" ? TTC_STANDARD_FACTOR :
|
|
345
|
-
1.0
|
|
346
|
-
);
|
|
347
|
-
if (factor !== 1.0) {
|
|
348
|
-
for (let i = 1; i < record.length; i++) {
|
|
349
|
-
const prev = dayjs(record[i - 1].time);
|
|
350
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
351
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return record;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function handleEverythingHooks(record, meta) {
|
|
359
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
360
|
-
const userEvents = record;
|
|
361
|
-
const profile = meta.profile;
|
|
362
|
-
|
|
363
|
-
// Stamp superProps from profile for consistency
|
|
364
|
-
userEvents.forEach(e => {
|
|
365
|
-
e.user_type = profile.user_type;
|
|
366
|
-
e.preferred_location = profile.preferred_location;
|
|
367
|
-
e.property_preference = profile.property_preference;
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// HOOK 1: SPRING BUYING SEASON — d30-60 tour duration 3x, offer_price 2.5x
|
|
371
|
-
// HOOK 2: MORTGAGE RATE SHOCK — d75-89 mortgage_rate pinned 7.5
|
|
372
|
-
// HOOK 7: LUXURY LISTING RELEASE — post-d50, 3% of listings priced $5M-$15M
|
|
373
|
-
const springStart = datasetStart.add(SPRING_START_DAY, "days");
|
|
374
|
-
const springEnd = datasetStart.add(SPRING_END_DAY, "days");
|
|
375
|
-
const shockStart = datasetStart.add(SHOCK_START_DAY, "days");
|
|
376
|
-
const shockEnd = datasetStart.add(SHOCK_END_DAY, "days");
|
|
377
|
-
const luxuryStart = datasetStart.add(LUXURY_RELEASE_DAY, "days");
|
|
378
|
-
userEvents.forEach(e => {
|
|
379
|
-
const t = dayjs.utc(e.time);
|
|
380
|
-
const inSpring = (t.isAfter(springStart) || t.isSame(springStart)) && t.isBefore(springEnd);
|
|
381
|
-
if (inSpring && e.event === "tour scheduled" && typeof e.duration_mins === "number") {
|
|
382
|
-
e.duration_mins = Math.round(e.duration_mins * SPRING_TOUR_DURATION_MULT);
|
|
383
|
-
}
|
|
384
|
-
// Spring offer_price boost moved to end (after all cloning)
|
|
385
|
-
if (e.event === "mortgage pre-approval") {
|
|
386
|
-
if ((t.isAfter(shockStart) || t.isSame(shockStart)) && t.isBefore(shockEnd)) {
|
|
387
|
-
e.mortgage_rate = SHOCK_MORTGAGE_RATE;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (e.event === "property listed" && t.isAfter(luxuryStart) && chance.bool({ likelihood: LUXURY_LISTING_LIKELIHOOD })) {
|
|
391
|
-
e.listing_price = chance.integer({ min: LUXURY_PRICE_MIN, max: LUXURY_PRICE_MAX });
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
396
|
-
|
|
397
|
-
// HOOK 2 (cont): MORTGAGE RATE SHOCK — drop 45% of post-day-75
|
|
398
|
-
// offer-submitted events. No flag.
|
|
399
|
-
const day75 = datasetStart.add(SHOCK_START_DAY, "days");
|
|
400
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
401
|
-
const evt = userEvents[i];
|
|
402
|
-
if (evt.event === "offer submitted" && dayjs(evt.time).isAfter(day75) && chance.bool({ likelihood: SHOCK_OFFER_DROP_LIKELIHOOD })) {
|
|
403
|
-
userEvents.splice(i, 1);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// HOOK 3: SAVED-SEARCH RETENTION — early savers (saved-search-created
|
|
408
|
-
// in first 7 days) get extra cloned view + tour events. Non-savers
|
|
409
|
-
// lose 80% of events after day 30. No flag.
|
|
410
|
-
const day7 = firstEventTime ? firstEventTime.add(SAVED_SEARCH_WINDOW_DAYS, "days") : null;
|
|
411
|
-
const day30 = datasetStart.add(NON_SAVER_CUTOFF_DAY, "days");
|
|
412
|
-
const hasSavedSearch = day7 ? userEvents.some(e =>
|
|
413
|
-
e.event === "saved search created" && dayjs(e.time).isBefore(day7)
|
|
414
|
-
) : false;
|
|
415
|
-
|
|
416
|
-
if (hasSavedSearch) {
|
|
417
|
-
const viewTemplate = userEvents.find(e => e.event === "property viewed");
|
|
418
|
-
const tourTemplate = userEvents.find(e => e.event === "tour scheduled");
|
|
419
|
-
if (viewTemplate) {
|
|
420
|
-
const extras = chance.integer({ min: SAVED_SEARCH_VIEW_CLONES_MIN, max: SAVED_SEARCH_VIEW_CLONES_MAX });
|
|
421
|
-
for (let i = 0; i < extras; i++) {
|
|
422
|
-
const offset = chance.integer({ min: 10, max: NUM_DAYS - 10 });
|
|
423
|
-
userEvents.push({
|
|
424
|
-
...viewTemplate,
|
|
425
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 0, max: 23 }), "hours").toISOString(),
|
|
426
|
-
user_id: viewTemplate.user_id,
|
|
427
|
-
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
428
|
-
listing_price: chance.integer({ min: 200000, max: 1200000 }),
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (tourTemplate) {
|
|
433
|
-
const extras = chance.integer({ min: SAVED_SEARCH_TOUR_CLONES_MIN, max: SAVED_SEARCH_TOUR_CLONES_MAX });
|
|
434
|
-
for (let i = 0; i < extras; i++) {
|
|
435
|
-
const offset = chance.integer({ min: 15, max: NUM_DAYS - 10 });
|
|
436
|
-
userEvents.push({
|
|
437
|
-
...tourTemplate,
|
|
438
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
|
|
439
|
-
user_id: tourTemplate.user_id,
|
|
440
|
-
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
446
|
-
const evt = userEvents[i];
|
|
447
|
-
if (dayjs(evt.time).isAfter(day30) && chance.bool({ likelihood: NON_SAVER_DROP_LIKELIHOOD })) {
|
|
448
|
-
userEvents.splice(i, 1);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// HOOK 4: PRE-APPROVED BUYER CONVERSION — clone 4-6 extra
|
|
454
|
-
// offer-submitted events for users with a mortgage pre-approval
|
|
455
|
-
// event. No flag.
|
|
456
|
-
const hasPreApproval = userEvents.some(e => e.event === "mortgage pre-approval");
|
|
457
|
-
if (hasPreApproval) {
|
|
458
|
-
profile.pre_approval_status = "approved";
|
|
459
|
-
const offerTemplate = userEvents.find(e => e.event === "offer submitted");
|
|
460
|
-
if (offerTemplate) {
|
|
461
|
-
const extras = chance.integer({ min: PRE_APPROVAL_OFFER_CLONES_MIN, max: PRE_APPROVAL_OFFER_CLONES_MAX });
|
|
462
|
-
for (let i = 0; i < extras; i++) {
|
|
463
|
-
const offset = chance.integer({ min: 20, max: NUM_DAYS - 5 });
|
|
464
|
-
userEvents.push({
|
|
465
|
-
...offerTemplate,
|
|
466
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
|
|
467
|
-
user_id: offerTemplate.user_id,
|
|
468
|
-
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
469
|
-
offer_price: chance.integer({ min: 250000, max: 1200000 }),
|
|
470
|
-
listing_price: chance.integer({ min: 250000, max: 1200000 }),
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// HOOK 5: TOP-TIER AGENT ADVANTAGE — Premier agents get 2 extra
|
|
477
|
-
// cloned listings per existing (3x rate) and 1 extra clone per
|
|
478
|
-
// sale (2x rate). Reads agent_tier from profile. No flag.
|
|
479
|
-
if (profile.agent_tier === "Premier") {
|
|
480
|
-
const listTemplate = userEvents.find(e => e.event === "property listed");
|
|
481
|
-
const soldTemplate = userEvents.find(e => e.event === "property sold");
|
|
482
|
-
|
|
483
|
-
if (listTemplate) {
|
|
484
|
-
const existingListings = userEvents.filter(e => e.event === "property listed").length;
|
|
485
|
-
const extras = existingListings * PREMIER_LISTING_CLONE_MULT;
|
|
486
|
-
for (let i = 0; i < extras; i++) {
|
|
487
|
-
const offset = chance.integer({ min: 5, max: NUM_DAYS - 5 });
|
|
488
|
-
userEvents.push({
|
|
489
|
-
...listTemplate,
|
|
490
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
|
|
491
|
-
user_id: listTemplate.user_id,
|
|
492
|
-
listing_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
493
|
-
listing_status: chance.pickone(["active", "pending", "coming soon"]),
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
if (soldTemplate) {
|
|
498
|
-
const existingSales = userEvents.filter(e => e.event === "property sold").length;
|
|
499
|
-
const extras = existingSales * PREMIER_SALE_CLONE_MULT;
|
|
500
|
-
for (let i = 0; i < extras; i++) {
|
|
501
|
-
const offset = chance.integer({ min: 10, max: NUM_DAYS - 5 });
|
|
502
|
-
userEvents.push({
|
|
503
|
-
...soldTemplate,
|
|
504
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 9, max: 17 }), "hours").toISOString(),
|
|
505
|
-
user_id: soldTemplate.user_id,
|
|
506
|
-
sale_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
507
|
-
days_on_market: chance.integer({ min: 5, max: 90 }),
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// HOOK 6: TOUR-TO-OFFER POWER USERS — users with both virtual
|
|
514
|
-
// tour AND in-person tour get 5-7 extra cloned offers. No flag.
|
|
515
|
-
const hasVirtualTour = userEvents.some(e => e.event === "virtual tour");
|
|
516
|
-
const hasInPersonTour = userEvents.some(e => e.event === "in-person tour");
|
|
517
|
-
if (hasVirtualTour && hasInPersonTour) {
|
|
518
|
-
const offerTemplate = userEvents.find(e => e.event === "offer submitted");
|
|
519
|
-
if (offerTemplate) {
|
|
520
|
-
const extras = chance.integer({ min: DUAL_TOUR_OFFER_CLONES_MIN, max: DUAL_TOUR_OFFER_CLONES_MAX });
|
|
521
|
-
for (let i = 0; i < extras; i++) {
|
|
522
|
-
const offset = chance.integer({ min: 15, max: NUM_DAYS - 5 });
|
|
523
|
-
userEvents.push({
|
|
524
|
-
...offerTemplate,
|
|
525
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
|
|
526
|
-
user_id: offerTemplate.user_id,
|
|
527
|
-
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
528
|
-
offer_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
529
|
-
listing_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// HOOK 7 (cont): LUXURY LISTING RELEASE — ~3% of users (by hash)
|
|
536
|
-
// get 3-6 extra luxury property-viewed events after day 50. No flag.
|
|
537
|
-
if (userEvents.length > 0) {
|
|
538
|
-
const userId = userEvents[0].user_id || "";
|
|
539
|
-
const isLuxuryBrowser = typeof userId === "string" && userId.length > 0 && userId.charCodeAt(0) % LUXURY_USER_HASH_MOD === 0;
|
|
540
|
-
if (isLuxuryBrowser) {
|
|
541
|
-
const viewTemplate = userEvents.find(e => e.event === "property viewed");
|
|
542
|
-
if (viewTemplate) {
|
|
543
|
-
const extras = chance.integer({ min: LUXURY_VIEW_CLONES_MIN, max: LUXURY_VIEW_CLONES_MAX });
|
|
544
|
-
for (let i = 0; i < extras; i++) {
|
|
545
|
-
const offset = chance.integer({ min: LUXURY_RELEASE_DAY, max: NUM_DAYS - 5 });
|
|
546
|
-
userEvents.push({
|
|
547
|
-
...viewTemplate,
|
|
548
|
-
time: datasetStart.add(offset, "days").add(chance.integer({ min: 9, max: 21 }), "hours").toISOString(),
|
|
549
|
-
user_id: viewTemplate.user_id,
|
|
550
|
-
listing_id: `LST-${chance.integer({ min: 90000, max: 99999 })}`,
|
|
551
|
-
listing_price: chance.integer({ min: LUXURY_PRICE_MIN, max: LUXURY_PRICE_MAX }),
|
|
552
|
-
property_type: chance.pickone(["Single Family", "Condo"]),
|
|
553
|
-
bedrooms: chance.integer({ min: 4, max: 8 }),
|
|
554
|
-
square_feet: chance.integer({ min: 4000, max: 15000 }),
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// HOOK 8: COLD-LEAD CHURN — users who view but never save in
|
|
562
|
-
// first 14 days lose 90% of post-day-14 events. No flag.
|
|
563
|
-
const day14 = firstEventTime ? firstEventTime.add(COLD_LEAD_WINDOW_DAYS, "days") : null;
|
|
564
|
-
if (day14) {
|
|
565
|
-
const earlyEvents = userEvents.filter(e => dayjs(e.time).isBefore(day14));
|
|
566
|
-
const hasView = earlyEvents.some(e => e.event === "property viewed");
|
|
567
|
-
const hasSave = earlyEvents.some(e => e.event === "property saved");
|
|
568
|
-
if (hasView && !hasSave) {
|
|
569
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
570
|
-
const evt = userEvents[i];
|
|
571
|
-
if (dayjs(evt.time).isAfter(day14) && chance.bool({ likelihood: COLD_LEAD_DROP_LIKELIHOOD })) {
|
|
572
|
-
userEvents.splice(i, 1);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// HOOK 9: PROPERTY-VIEWED MAGIC NUMBER (no flags)
|
|
579
|
-
// Sweet 6-12 views → +30% offer_price on offer submitted events.
|
|
580
|
-
// Over 13+ → drop 35% of offer submitted events (tire-kickers).
|
|
581
|
-
const viewCount = userEvents.filter(e => e.event === "property viewed").length;
|
|
582
|
-
if (viewCount >= VIEW_SWEET_MIN && viewCount <= VIEW_SWEET_MAX) {
|
|
583
|
-
userEvents.forEach(e => {
|
|
584
|
-
if (e.event === "offer submitted" && typeof e.offer_price === "number") {
|
|
585
|
-
e.offer_price = Math.round(e.offer_price * VIEW_OFFER_PRICE_BOOST);
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
} else if (viewCount >= VIEW_OVER_THRESHOLD) {
|
|
589
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
590
|
-
if (userEvents[i].event === "offer submitted" && chance.bool({ likelihood: VIEW_OVER_OFFER_DROP_LIKELIHOOD })) {
|
|
591
|
-
userEvents.splice(i, 1);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// HOOK 1 (cont): Spring offer_price boost — runs AFTER all cloning
|
|
597
|
-
// so cloned offers in the spring window also get the 2.5x boost
|
|
598
|
-
userEvents.forEach(e => {
|
|
599
|
-
if (e.event !== "offer submitted") return;
|
|
600
|
-
const t = dayjs.utc(e.time);
|
|
601
|
-
if ((t.isAfter(springStart) || t.isSame(springStart)) && t.isBefore(springEnd)) {
|
|
602
|
-
e.offer_price = Math.floor((e.offer_price || 400000) * SPRING_OFFER_PRICE_MULT);
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
return record;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// ── CONFIG ──
|
|
610
|
-
/** @type {Config} */
|
|
611
|
-
const config = {
|
|
612
|
-
version: 2,
|
|
613
|
-
seed: SEED,
|
|
614
|
-
datasetStart: DATASET_START,
|
|
615
|
-
datasetEnd: DATASET_END,
|
|
616
|
-
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
617
|
-
numUsers: NUM_USERS,
|
|
618
|
-
format: "json",
|
|
619
|
-
gzip: true,
|
|
620
|
-
credentials: {
|
|
621
|
-
token,
|
|
622
|
-
},
|
|
623
|
-
switches: {
|
|
624
|
-
hasSessionIds: true,
|
|
625
|
-
alsoInferFunnels: false,
|
|
626
|
-
hasLocation: true,
|
|
627
|
-
hasAndroidDevices: true,
|
|
628
|
-
hasIOSDevices: true,
|
|
629
|
-
hasDesktopDevices: true,
|
|
630
|
-
hasBrowser: true,
|
|
631
|
-
hasCampaigns: false,
|
|
632
|
-
isAnonymous: false,
|
|
633
|
-
hasAdSpend: false,
|
|
634
|
-
hasAvatar: true,
|
|
635
|
-
},
|
|
636
|
-
identity: {
|
|
637
|
-
avgDevicePerUser: 2,
|
|
638
|
-
},
|
|
639
|
-
|
|
640
|
-
concurrency: 1,
|
|
641
|
-
writeToDisk: false,
|
|
642
|
-
|
|
643
|
-
soup: "growth",
|
|
644
|
-
|
|
645
|
-
scdProps: {
|
|
646
|
-
agent_tier: {
|
|
647
|
-
values: ["Standard", "Standard", "Standard", "Premier"],
|
|
648
|
-
frequency: "month",
|
|
649
|
-
timing: "fuzzy",
|
|
650
|
-
max: 4
|
|
651
|
-
}
|
|
652
|
-
},
|
|
653
|
-
|
|
654
|
-
funnels: [
|
|
655
|
-
{
|
|
656
|
-
sequence: ["account created", "property search", "property viewed"],
|
|
657
|
-
isFirstFunnel: true,
|
|
658
|
-
conversionRate: 80,
|
|
659
|
-
timeToConvert: 0.5,
|
|
660
|
-
name: "Buyer Journey"
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
sequence: ["property viewed", "tour scheduled", "offer submitted"],
|
|
664
|
-
conversionRate: 40,
|
|
665
|
-
timeToConvert: 24,
|
|
666
|
-
weight: 5,
|
|
667
|
-
name: "Tour Funnel",
|
|
668
|
-
reentry: true,
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
sequence: ["property listed", "open house attended", "property sold"],
|
|
672
|
-
conversionRate: 35,
|
|
673
|
-
timeToConvert: 48,
|
|
674
|
-
weight: 3,
|
|
675
|
-
name: "Seller Funnel"
|
|
676
|
-
}
|
|
677
|
-
],
|
|
678
|
-
|
|
679
|
-
events: [
|
|
680
|
-
{
|
|
681
|
-
event: "account created",
|
|
682
|
-
weight: 1,
|
|
683
|
-
isFirstEvent: true,
|
|
684
|
-
isAuthEvent: true,
|
|
685
|
-
properties: {
|
|
686
|
-
signup_method: ["email", "google", "apple", "facebook"],
|
|
687
|
-
}
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
event: "property search",
|
|
691
|
-
weight: 10,
|
|
692
|
-
properties: {
|
|
693
|
-
location: ["Manhattan", "Brooklyn", "Austin", "Miami", "San Francisco", "Denver", "Seattle", "Chicago", "Nashville", "Portland"],
|
|
694
|
-
price_range_min: u.weighNumRange(100000, 500000, 0.5, 40),
|
|
695
|
-
price_range_max: u.weighNumRange(300000, 2000000, 0.5, 40),
|
|
696
|
-
bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
|
|
697
|
-
property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
698
|
-
}
|
|
699
|
-
},
|
|
700
|
-
{
|
|
701
|
-
event: "property viewed",
|
|
702
|
-
weight: 12,
|
|
703
|
-
isStrictEvent: false,
|
|
704
|
-
properties: {
|
|
705
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
706
|
-
listing_price: u.weighNumRange(150000, 1500000, 0.4, 60),
|
|
707
|
-
property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
708
|
-
bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
|
|
709
|
-
square_feet: u.weighNumRange(600, 5000, 0.5, 50),
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
{
|
|
713
|
-
event: "property saved",
|
|
714
|
-
weight: 5,
|
|
715
|
-
properties: {
|
|
716
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
717
|
-
listing_price: u.weighNumRange(150000, 1500000, 0.4, 40),
|
|
718
|
-
}
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
event: "saved search created",
|
|
722
|
-
weight: 3,
|
|
723
|
-
properties: {
|
|
724
|
-
location: ["Manhattan", "Brooklyn", "Austin", "Miami", "San Francisco", "Denver", "Seattle", "Chicago", "Nashville", "Portland"],
|
|
725
|
-
criteria_count: u.weighNumRange(2, 8, 0.8, 10),
|
|
726
|
-
}
|
|
727
|
-
},
|
|
728
|
-
{
|
|
729
|
-
event: "virtual tour",
|
|
730
|
-
weight: 4,
|
|
731
|
-
properties: {
|
|
732
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
733
|
-
duration_mins: u.weighNumRange(5, 45, 0.5, 30),
|
|
734
|
-
}
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
event: "in-person tour",
|
|
738
|
-
weight: 3,
|
|
739
|
-
properties: {
|
|
740
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
741
|
-
agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
|
|
742
|
-
}
|
|
743
|
-
},
|
|
744
|
-
{
|
|
745
|
-
event: "tour scheduled",
|
|
746
|
-
weight: 4,
|
|
747
|
-
isStrictEvent: false,
|
|
748
|
-
properties: {
|
|
749
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
750
|
-
agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
|
|
751
|
-
duration_mins: u.weighNumRange(5, 90, 0.5, 30),
|
|
752
|
-
}
|
|
753
|
-
},
|
|
754
|
-
{
|
|
755
|
-
event: "mortgage pre-approval",
|
|
756
|
-
weight: 2,
|
|
757
|
-
properties: {
|
|
758
|
-
lender: ["Chase", "Wells Fargo", "Bank of America", "Rocket Mortgage", "United Wholesale", "loanDepot"],
|
|
759
|
-
approved_amount: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
760
|
-
mortgage_rate: u.weighNumRange(5.5, 7.5, 0.8, 20),
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
event: "offer submitted",
|
|
765
|
-
weight: 2,
|
|
766
|
-
isStrictEvent: false,
|
|
767
|
-
properties: {
|
|
768
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
769
|
-
offer_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
770
|
-
listing_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
771
|
-
}
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
event: "offer accepted",
|
|
775
|
-
weight: 1,
|
|
776
|
-
properties: {
|
|
777
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
778
|
-
final_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
779
|
-
}
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
event: "offer rejected",
|
|
783
|
-
weight: 1,
|
|
784
|
-
properties: {
|
|
785
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
786
|
-
reason: ["price too low", "competing offer", "seller changed mind", "inspection issues", "financing fell through"],
|
|
787
|
-
}
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
event: "property listed",
|
|
791
|
-
weight: 3,
|
|
792
|
-
isStrictEvent: false,
|
|
793
|
-
properties: {
|
|
794
|
-
listing_price: u.weighNumRange(200000, 1500000, 0.4, 50),
|
|
795
|
-
property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
796
|
-
bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
|
|
797
|
-
listing_status: ["active", "pending", "coming soon"],
|
|
798
|
-
}
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
event: "property sold",
|
|
802
|
-
weight: 1,
|
|
803
|
-
isStrictEvent: false,
|
|
804
|
-
properties: {
|
|
805
|
-
sale_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
806
|
-
days_on_market: u.weighNumRange(5, 180, 0.4, 30),
|
|
807
|
-
}
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
event: "agent contacted",
|
|
811
|
-
weight: 4,
|
|
812
|
-
properties: {
|
|
813
|
-
contact_method: ["phone", "email", "in-app message", "text"],
|
|
814
|
-
}
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
event: "open house attended",
|
|
818
|
-
weight: 2,
|
|
819
|
-
properties: {
|
|
820
|
-
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
821
|
-
attendees: u.weighNumRange(2, 30, 0.5, 15),
|
|
822
|
-
}
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
event: "review submitted",
|
|
826
|
-
weight: 2,
|
|
827
|
-
properties: {
|
|
828
|
-
rating: [1, 2, 3, 3, 4, 4, 4, 5, 5, 5],
|
|
829
|
-
review_type: ["agent", "property", "platform"],
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
],
|
|
833
|
-
|
|
834
|
-
superProps: {
|
|
835
|
-
user_type: ["Buyer", "Buyer", "Buyer", "Agent", "Both"],
|
|
836
|
-
preferred_location: ["Urban", "Suburban", "Rural"],
|
|
837
|
-
property_preference: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
838
|
-
},
|
|
839
|
-
|
|
840
|
-
userProps: {
|
|
841
|
-
user_type: ["Buyer", "Buyer", "Buyer", "Agent", "Both"],
|
|
842
|
-
preferred_location: ["Urban", "Suburban", "Rural"],
|
|
843
|
-
property_preference: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
844
|
-
budget_max: u.weighNumRange(200000, 2000000, 0.4, 50),
|
|
845
|
-
agent_tier: ["Standard", "Standard", "Standard", "Premier"],
|
|
846
|
-
total_properties_viewed: u.weighNumRange(0, 100, 0.5, 30),
|
|
847
|
-
pre_approval_status: ["none"],
|
|
848
|
-
},
|
|
849
|
-
|
|
850
|
-
groupKeys: [
|
|
851
|
-
["brokerage_id", 200, ["property listed", "property sold", "agent contacted", "open house attended"]],
|
|
852
|
-
],
|
|
853
|
-
|
|
854
|
-
groupProps: {
|
|
855
|
-
brokerage_id: {
|
|
856
|
-
brokerage_name: ["Compass", "RE/MAX", "Keller Williams", "Coldwell Banker", "Century 21", "Sotheby's", "eXp Realty", "Redfin"],
|
|
857
|
-
agent_count: u.weighNumRange(5, 200, 0.4, 30),
|
|
858
|
-
region: ["Northeast", "Southeast", "Midwest", "Southwest", "West Coast", "Pacific Northwest"],
|
|
859
|
-
avg_listing_price: u.weighNumRange(200000, 1500000, 0.4, 30),
|
|
860
|
-
}
|
|
861
|
-
},
|
|
862
|
-
|
|
863
|
-
lookupTables: [],
|
|
864
|
-
|
|
865
|
-
hook(record, type, meta) {
|
|
866
|
-
if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
|
|
867
|
-
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
868
|
-
return record;
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
export default config;
|