@ak--47/dungeon-master 1.3.0 → 1.3.1
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/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +192 -133
- package/dungeons/vertical/community.js +64 -34
- package/dungeons/vertical/crypto.js +333 -224
- package/dungeons/vertical/dating.js +228 -282
- package/dungeons/vertical/devtools.js +164 -62
- package/dungeons/vertical/ecommerce.js +240 -91
- package/dungeons/vertical/education.js +328 -310
- package/dungeons/vertical/fintech.js +437 -311
- package/dungeons/vertical/fitness.js +141 -58
- package/dungeons/vertical/food-delivery.js +298 -318
- package/dungeons/vertical/gaming.js +378 -231
- package/dungeons/vertical/healthcare.js +140 -60
- package/dungeons/vertical/insurance-application.js +168 -73
- package/dungeons/vertical/logistics.js +95 -6
- package/dungeons/vertical/marketplace.js +137 -51
- package/dungeons/vertical/media.js +241 -386
- package/dungeons/vertical/real-estate.js +322 -315
- package/dungeons/vertical/sass.js +253 -263
- package/dungeons/vertical/social.js +262 -206
- package/dungeons/vertical/travel.js +59 -16
- package/index.js +17 -17
- package/lib/core/config-validator.js +93 -21
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +11 -14
- package/lib/generators/funnels.js +12 -4
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/orchestrators/user-loop.js +47 -47
- package/lib/utils/utils.js +71 -39
- package/package.json +1 -1
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +38 -15
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -48,91 +48,204 @@ const chance = u.initChance(SEED);
|
|
|
48
48
|
* - Seller funnel: property listed → open house attended → property sold (35%)
|
|
49
49
|
*
|
|
50
50
|
* ===================================================================
|
|
51
|
-
* ANALYTICS HOOKS (
|
|
51
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
52
|
+
*
|
|
53
|
+
* Adds 10. TOUR FUNNEL TIME-TO-CONVERT: Premier agents 0.71x faster, Standard
|
|
54
|
+
* 1.3x slower (funnel-post). Discover via Tour Funnel median TTC by agent_tier.
|
|
55
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
|
|
56
|
+
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
52
57
|
* ===================================================================
|
|
53
58
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* → Insights: "tour scheduled" total over time, breakdown spring_season
|
|
58
|
-
* (expect: 3x volume spike during days 30-60)
|
|
59
|
-
* → Insights: "offer submitted" avg offer_price, breakdown spring_season
|
|
60
|
-
* (expect: 2.5x average during spring)
|
|
61
|
-
*
|
|
62
|
-
* 2. MORTGAGE RATE SHOCK (event + everything hook — TIME-BASED)
|
|
63
|
-
* Day 75: mortgage_rate jumps from 6.5 to 7.5. After day 75 and
|
|
64
|
-
* before day 89, mortgage_rate forced to 7.5 on pre-approval events.
|
|
65
|
-
* In everything hook, 45% of offer_submitted events after day 75
|
|
66
|
-
* are removed (buyer pullback).
|
|
67
|
-
* → Insights: "mortgage pre-approval" avg mortgage_rate over time
|
|
68
|
-
* (expect: step change from ~6.5 to 7.5 at day 75)
|
|
69
|
-
* → Insights: "offer submitted" total over time
|
|
70
|
-
* (expect: ~45% volume drop after day 75)
|
|
71
|
-
*
|
|
72
|
-
* 3. SAVED-SEARCH RETENTION (everything hook — RETENTION)
|
|
73
|
-
* Users who create a "saved search created" within first 7 days
|
|
74
|
-
* get extra property_viewed and tour_scheduled events injected.
|
|
75
|
-
* Non-savers lose 80% of events after day 30.
|
|
76
|
-
* → Retention: any → any, segment has_saved_search = true vs false
|
|
77
|
-
* (expect: dramatically higher retention for saved-search users)
|
|
78
|
-
* → Insights: "property viewed" total, breakdown has_saved_search
|
|
79
|
-
*
|
|
80
|
-
* 4. PRE-APPROVED BUYER CONVERSION (everything hook — CONVERSION)
|
|
81
|
-
* Users with a "mortgage pre-approval" event complete offer_submitted
|
|
82
|
-
* 5x more often. Extra offer events injected, pre_approved=true.
|
|
83
|
-
* → Insights: "offer submitted" total, breakdown pre_approved
|
|
84
|
-
* (expect: 5x volume for pre_approved=true)
|
|
85
|
-
* → Funnels: property viewed → tour scheduled → offer submitted,
|
|
86
|
-
* segment pre_approved = true vs false
|
|
87
|
-
*
|
|
88
|
-
* 5. TOP-TIER AGENT ADVANTAGE (everything hook — SUBSCRIPTION TIER)
|
|
89
|
-
* Users with agent_tier="Premier" from profile get 3x property_listed
|
|
90
|
-
* events and 2x property_sold events. premier_agent=true.
|
|
91
|
-
* → Insights: "property listed" total, breakdown premier_agent
|
|
92
|
-
* (expect: Premier agents ~3x listings)
|
|
93
|
-
* → Insights: "property sold" total, breakdown premier_agent
|
|
94
|
-
* (expect: Premier agents ~2x sales)
|
|
95
|
-
*
|
|
96
|
-
* 6. TOUR-TO-OFFER POWER USERS (everything hook — BEHAVIORS TOGETHER)
|
|
97
|
-
* Users who did BOTH "virtual tour" AND "in-person tour" get 6x
|
|
98
|
-
* offer_submitted events injected. dual_tour=true on offers.
|
|
99
|
-
* → Insights: "offer submitted" total, breakdown dual_tour
|
|
100
|
-
* (expect: dual_tour=true ~6x volume)
|
|
101
|
-
* → Funnels: virtual tour → in-person tour → offer submitted,
|
|
102
|
-
* filter dual_tour = true
|
|
103
|
-
*
|
|
104
|
-
* 7. LUXURY LISTING RELEASE (event + everything hook — TIMED RELEASE)
|
|
105
|
-
* Day 50: luxury listings appear. After day 50, 3% of property_listed
|
|
106
|
-
* events get listing_price set to $5M+ and luxury=true. ~3% of users
|
|
107
|
-
* (by hash) get extra luxury property_viewed events.
|
|
108
|
-
* → Insights: "property listed" avg listing_price, filter luxury=true
|
|
109
|
-
* (expect: $5M+ only after day 50)
|
|
110
|
-
* → Insights: "property viewed" total, breakdown luxury
|
|
111
|
-
* (expect: luxury=true cluster starting day 50)
|
|
112
|
-
*
|
|
113
|
-
* 8. COLD-LEAD CHURN (everything hook — CHURN)
|
|
114
|
-
* Users who browse (property viewed) but never save (property saved)
|
|
115
|
-
* in the first 14 days lose 90% of events after day 14.
|
|
116
|
-
* → Retention: any → any, segment cold_lead = true vs false
|
|
117
|
-
* (expect: cold_lead=true drops to near zero after day 14)
|
|
118
|
-
* → Insights: any event total, filter cold_lead = true
|
|
59
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
60
|
+
* only via behavioral cohorts (count event per user, then measure
|
|
61
|
+
* downstream) or raw-prop breakdowns (date, agent_tier).
|
|
119
62
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
63
|
+
* -------------------------------------------------------------------
|
|
64
|
+
* 1. SPRING BUYING SEASON (event)
|
|
65
|
+
* -------------------------------------------------------------------
|
|
66
|
+
*
|
|
67
|
+
* PATTERN: Days 30-60, tour-scheduled duration_mins boosted 3x and
|
|
68
|
+
* offer-submitted offer_price boosted 2.5x. Mutates raw props. No flag.
|
|
69
|
+
*
|
|
70
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
71
|
+
*
|
|
72
|
+
* Report 1: Avg Offer Price by Day
|
|
73
|
+
* - Report type: Insights
|
|
74
|
+
* - Event: "offer submitted"
|
|
75
|
+
* - Measure: Average of "offer_price"
|
|
76
|
+
* - Line chart by day
|
|
77
|
+
* - Expected: visible bulge days 30-60 (~ 2.5x baseline)
|
|
78
|
+
*
|
|
79
|
+
* REAL-WORLD ANALOGUE: Spring buying season elevates prices and tour intent.
|
|
80
|
+
*
|
|
81
|
+
* -------------------------------------------------------------------
|
|
82
|
+
* 2. MORTGAGE RATE SHOCK (event + everything)
|
|
83
|
+
* -------------------------------------------------------------------
|
|
84
|
+
*
|
|
85
|
+
* PATTERN: Days 75-89, mortgage_rate forced to 7.5 on pre-approval
|
|
86
|
+
* events. In everything hook, 45% of post-day-75 offer-submitted events
|
|
87
|
+
* dropped. No flag.
|
|
88
|
+
*
|
|
89
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
90
|
+
*
|
|
91
|
+
* Report 1: Mortgage Rate Step Change
|
|
92
|
+
* - Report type: Insights
|
|
93
|
+
* - Event: "mortgage pre-approval"
|
|
94
|
+
* - Measure: Average of "mortgage_rate"
|
|
95
|
+
* - Line chart by day
|
|
96
|
+
* - Expected: clear step change from ~ 6.5 to 7.5 at day 75
|
|
97
|
+
*
|
|
98
|
+
* Report 2: Offer Volume Drop After Rate Hike
|
|
99
|
+
* - Report type: Insights
|
|
100
|
+
* - Event: "offer submitted"
|
|
101
|
+
* - Measure: Total
|
|
102
|
+
* - Line chart by day
|
|
103
|
+
* - Expected: ~ 45% volume drop after day 75
|
|
104
|
+
*
|
|
105
|
+
* REAL-WORLD ANALOGUE: Buyer demand is sensitive to mortgage rates.
|
|
106
|
+
*
|
|
107
|
+
* -------------------------------------------------------------------
|
|
108
|
+
* 3. SAVED-SEARCH RETENTION (everything)
|
|
109
|
+
* -------------------------------------------------------------------
|
|
110
|
+
*
|
|
111
|
+
* PATTERN: Users who create saved-search-created in first 7 days get
|
|
112
|
+
* extra cloned property-viewed + tour-scheduled events. Non-savers lose
|
|
113
|
+
* 80% of post-day-30 events. No flag — discover via cohort builder.
|
|
114
|
+
*
|
|
115
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
116
|
+
*
|
|
117
|
+
* Report 1: Retention by Saved-Search Cohort
|
|
118
|
+
* - Report type: Retention
|
|
119
|
+
* - Cohort A: users with >= 1 "saved search created" in first 7 days
|
|
120
|
+
* - Cohort B: rest
|
|
121
|
+
* - Expected: A sustains higher retention through dataset
|
|
122
|
+
*
|
|
123
|
+
* REAL-WORLD ANALOGUE: Saved searches indicate buyer intent.
|
|
124
|
+
*
|
|
125
|
+
* -------------------------------------------------------------------
|
|
126
|
+
* 4. PRE-APPROVED BUYER CONVERSION (everything)
|
|
127
|
+
* -------------------------------------------------------------------
|
|
128
|
+
*
|
|
129
|
+
* PATTERN: Users with a mortgage-pre-approval event get 4-6 extra
|
|
130
|
+
* cloned offer-submitted events. No flag — discover via cohort.
|
|
131
|
+
*
|
|
132
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
133
|
+
*
|
|
134
|
+
* Report 1: Offers per User by Pre-Approval
|
|
135
|
+
* - Report type: Insights (with cohort)
|
|
136
|
+
* - Cohort A: users with >= 1 "mortgage pre-approval"
|
|
137
|
+
* - Cohort B: rest
|
|
138
|
+
* - Event: "offer submitted"
|
|
139
|
+
* - Measure: Total per user
|
|
140
|
+
* - Expected: A ~ 5x B
|
|
141
|
+
*
|
|
142
|
+
* REAL-WORLD ANALOGUE: Pre-approval is the strongest buyer indicator.
|
|
143
|
+
*
|
|
144
|
+
* -------------------------------------------------------------------
|
|
145
|
+
* 5. TOP-TIER AGENT ADVANTAGE (everything)
|
|
146
|
+
* -------------------------------------------------------------------
|
|
147
|
+
*
|
|
148
|
+
* PATTERN: Premier-tier agents get 2 extra cloned listings per existing
|
|
149
|
+
* (3x rate) and 1 extra cloned sale per existing (2x rate). Mutates
|
|
150
|
+
* cloned events with raw values; reads agent_tier from profile (existing
|
|
151
|
+
* SCD prop). Discover via agent_tier breakdown.
|
|
152
|
+
*
|
|
153
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
154
|
+
*
|
|
155
|
+
* Report 1: Listings per Agent by Tier
|
|
156
|
+
* - Report type: Insights
|
|
157
|
+
* - Event: "property listed"
|
|
158
|
+
* - Measure: Total per user
|
|
159
|
+
* - Breakdown: user "agent_tier"
|
|
160
|
+
* - Expected: Premier ~ 3x Standard
|
|
123
161
|
*
|
|
124
|
-
*
|
|
125
|
-
* - Spring + Rate Shock: Does the spring surge survive the rate hike?
|
|
126
|
-
* - Saved Search + Cold Lead: Are saved-search users immune to churn?
|
|
127
|
-
* - Pre-Approved + Dual Tour: Do pre-approved dual-tourers dominate offers?
|
|
128
|
-
* - Premier Agent + Luxury: Do Premier agents list more luxury properties?
|
|
129
|
-
* - Rate Shock + Pre-Approved: Do pre-approved buyers still submit after rate jump?
|
|
162
|
+
* REAL-WORLD ANALOGUE: Top producers list and close more.
|
|
130
163
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
164
|
+
* -------------------------------------------------------------------
|
|
165
|
+
* 6. TOUR-TO-OFFER POWER USERS (everything)
|
|
166
|
+
* -------------------------------------------------------------------
|
|
167
|
+
*
|
|
168
|
+
* PATTERN: Users with both virtual-tour AND in-person-tour events get
|
|
169
|
+
* 5-7 extra cloned offer-submitted events. No flag — discover via
|
|
170
|
+
* compound cohort builder.
|
|
171
|
+
*
|
|
172
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
173
|
+
*
|
|
174
|
+
* Report 1: Offers per User by Dual-Tour Cohort
|
|
175
|
+
* - Report type: Insights (with cohort)
|
|
176
|
+
* - Cohort A: users with both >= 1 "virtual tour" AND >= 1 "in-person tour"
|
|
177
|
+
* - Cohort B: rest
|
|
178
|
+
* - Event: "offer submitted"
|
|
179
|
+
* - Measure: Total per user
|
|
180
|
+
* - Expected: A ~ 6x B
|
|
181
|
+
*
|
|
182
|
+
* REAL-WORLD ANALOGUE: Dual-tour buyers self-qualify into serious bidders.
|
|
183
|
+
*
|
|
184
|
+
* -------------------------------------------------------------------
|
|
185
|
+
* 7. LUXURY LISTING RELEASE (event + everything)
|
|
186
|
+
* -------------------------------------------------------------------
|
|
187
|
+
*
|
|
188
|
+
* PATTERN: After day 50, 3% of property-listed events get listing_price
|
|
189
|
+
* set to $5M-$15M (raw mutation). ~3% of users (by id hash) get extra
|
|
190
|
+
* luxury cloned property-viewed events. No flag — discover via line
|
|
191
|
+
* chart by day on listing_price.
|
|
192
|
+
*
|
|
193
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
194
|
+
*
|
|
195
|
+
* Report 1: Listing Price Distribution Over Time
|
|
196
|
+
* - Report type: Insights
|
|
197
|
+
* - Event: "property listed"
|
|
198
|
+
* - Measure: Distribution of "listing_price"
|
|
199
|
+
* - Line chart by week
|
|
200
|
+
* - Expected: $5M+ outliers appear only after day 50
|
|
201
|
+
*
|
|
202
|
+
* REAL-WORLD ANALOGUE: Luxury inventory drop attracts HNW shoppers.
|
|
203
|
+
*
|
|
204
|
+
* -------------------------------------------------------------------
|
|
205
|
+
* 8. COLD-LEAD CHURN (everything)
|
|
206
|
+
* -------------------------------------------------------------------
|
|
207
|
+
*
|
|
208
|
+
* PATTERN: Users who view (property viewed) but never save (property
|
|
209
|
+
* saved) in the first 14 days lose 90% of events after day 14. No flag.
|
|
210
|
+
*
|
|
211
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
212
|
+
*
|
|
213
|
+
* Report 1: Cold-Lead Retention
|
|
214
|
+
* - Report type: Retention
|
|
215
|
+
* - Cohort A: users with >= 1 "property viewed" but 0 "property saved" in first 14 days
|
|
216
|
+
* - Cohort B: rest
|
|
217
|
+
* - Expected: A retention drops to near-zero after day 14
|
|
218
|
+
*
|
|
219
|
+
* REAL-WORLD ANALOGUE: Browsers who never save are window-shoppers.
|
|
220
|
+
*
|
|
221
|
+
* -------------------------------------------------------------------
|
|
222
|
+
* 9. PROPERTY-VIEWED MAGIC NUMBER (everything)
|
|
223
|
+
* -------------------------------------------------------------------
|
|
224
|
+
*
|
|
225
|
+
* PATTERN: Users in the 6-12 property-viewed sweet spot get +30% on
|
|
226
|
+
* offer_price for offer-submitted events. Users with 13+ views are
|
|
227
|
+
* tire-kickers; 35% of their offer-submitted events drop. No flag.
|
|
228
|
+
*
|
|
229
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
230
|
+
*
|
|
231
|
+
* Report 1: Avg Offer Price by View Bucket
|
|
232
|
+
* - Report type: Insights (with cohort)
|
|
233
|
+
* - Cohort A: users with 6-12 "property viewed"
|
|
234
|
+
* - Cohort B: users with 0-5
|
|
235
|
+
* - Event: "offer submitted"
|
|
236
|
+
* - Measure: Average of "offer_price"
|
|
237
|
+
* - Expected: A ~ 1.3x B
|
|
238
|
+
*
|
|
239
|
+
* Report 2: Offers per User on Heavy Viewers
|
|
240
|
+
* - Report type: Insights (with cohort)
|
|
241
|
+
* - Cohort C: users with >= 13 "property viewed"
|
|
242
|
+
* - Cohort A: users with 6-12
|
|
243
|
+
* - Event: "offer submitted"
|
|
244
|
+
* - Measure: Total per user
|
|
245
|
+
* - Expected: C ~ 35% fewer offers per user vs A
|
|
246
|
+
*
|
|
247
|
+
* REAL-WORLD ANALOGUE: Focused buyers commit; obsessive browsers
|
|
248
|
+
* tire-kick and never make the leap.
|
|
136
249
|
*
|
|
137
250
|
* ===================================================================
|
|
138
251
|
* EXPECTED METRICS SUMMARY
|
|
@@ -140,21 +253,25 @@ const chance = u.initChance(SEED);
|
|
|
140
253
|
*
|
|
141
254
|
* Hook | Metric | Baseline | Effect | Ratio
|
|
142
255
|
* -------------------------|------------------------|----------|---------|------
|
|
143
|
-
* Spring Buying Season |
|
|
144
|
-
* Mortgage Rate Shock | Offer
|
|
145
|
-
* Saved-Search Retention |
|
|
146
|
-
* Pre-Approved Conversion |
|
|
147
|
-
* Premier Agent Advantage |
|
|
148
|
-
* Dual
|
|
149
|
-
* Luxury Listing Release |
|
|
150
|
-
* Cold-Lead Churn |
|
|
256
|
+
* Spring Buying Season | Avg offer_price d30-60 | 1x | 2.5x | 2.5x
|
|
257
|
+
* Mortgage Rate Shock | Offer vol post-day-75 | 1x | 0.55x | -45%
|
|
258
|
+
* Saved-Search Retention | non-saver post-day-30 | 1x | 0.2x | -80%
|
|
259
|
+
* Pre-Approved Conversion | offers/user | 1x | ~ 5x | 5x
|
|
260
|
+
* Premier Agent Advantage | listings/user | 1x | ~ 3x | 3x
|
|
261
|
+
* Dual-Tour Power Users | offers/user | 1x | ~ 6x | 6x
|
|
262
|
+
* Luxury Listing Release | $5M+ listings | 0% | ~ 3% | d50+
|
|
263
|
+
* Cold-Lead Churn | non-save post-day-14 | 1x | 0.1x | -90%
|
|
264
|
+
* View-Count Magic Number | sweet offer_price | 1x | 1.3x | 1.3x
|
|
265
|
+
* View-Count Magic Number | over offers/user | 1x | 0.65x | -35%
|
|
151
266
|
*/
|
|
152
267
|
|
|
153
268
|
/** @type {Config} */
|
|
154
269
|
const config = {
|
|
155
270
|
token,
|
|
156
271
|
seed: SEED,
|
|
157
|
-
|
|
272
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
273
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
274
|
+
// numDays: num_days,
|
|
158
275
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
159
276
|
numUsers: num_users,
|
|
160
277
|
hasAnonIds: false,
|
|
@@ -240,8 +357,6 @@ const config = {
|
|
|
240
357
|
property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
241
358
|
bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
|
|
242
359
|
square_feet: u.weighNumRange(600, 5000, 0.5, 50),
|
|
243
|
-
spring_season: [false],
|
|
244
|
-
luxury: [false],
|
|
245
360
|
}
|
|
246
361
|
},
|
|
247
362
|
{
|
|
@@ -282,7 +397,7 @@ const config = {
|
|
|
282
397
|
properties: {
|
|
283
398
|
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
284
399
|
agent_id: () => `AGT-${chance.integer({ min: 1000, max: 9999 })}`,
|
|
285
|
-
|
|
400
|
+
duration_mins: u.weighNumRange(5, 90, 0.5, 30),
|
|
286
401
|
}
|
|
287
402
|
},
|
|
288
403
|
{
|
|
@@ -301,9 +416,6 @@ const config = {
|
|
|
301
416
|
listing_id: () => `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
302
417
|
offer_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
303
418
|
listing_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
304
|
-
pre_approved: [false],
|
|
305
|
-
spring_season: [false],
|
|
306
|
-
dual_tour: [false],
|
|
307
419
|
}
|
|
308
420
|
},
|
|
309
421
|
{
|
|
@@ -330,8 +442,6 @@ const config = {
|
|
|
330
442
|
property_type: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
331
443
|
bedrooms: [1, 2, 2, 3, 3, 3, 4, 4, 5],
|
|
332
444
|
listing_status: ["active", "pending", "coming soon"],
|
|
333
|
-
luxury: [false],
|
|
334
|
-
premier_agent: [false],
|
|
335
445
|
}
|
|
336
446
|
},
|
|
337
447
|
{
|
|
@@ -340,7 +450,6 @@ const config = {
|
|
|
340
450
|
properties: {
|
|
341
451
|
sale_price: u.weighNumRange(200000, 1500000, 0.4, 40),
|
|
342
452
|
days_on_market: u.weighNumRange(5, 180, 0.4, 30),
|
|
343
|
-
premier_agent: [false],
|
|
344
453
|
}
|
|
345
454
|
},
|
|
346
455
|
{
|
|
@@ -372,8 +481,6 @@ const config = {
|
|
|
372
481
|
user_type: ["Buyer", "Buyer", "Buyer", "Agent", "Both"],
|
|
373
482
|
preferred_location: ["Urban", "Suburban", "Rural"],
|
|
374
483
|
property_preference: ["Single Family", "Condo", "Townhouse", "Multi-Family"],
|
|
375
|
-
has_saved_search: [false],
|
|
376
|
-
cold_lead: [false],
|
|
377
484
|
},
|
|
378
485
|
|
|
379
486
|
userProps: {
|
|
@@ -384,8 +491,6 @@ const config = {
|
|
|
384
491
|
agent_tier: ["Standard", "Standard", "Standard", "Premier"],
|
|
385
492
|
total_properties_viewed: u.weighNumRange(0, 100, 0.5, 30),
|
|
386
493
|
pre_approval_status: ["none"],
|
|
387
|
-
has_saved_search: [false],
|
|
388
|
-
cold_lead: [false],
|
|
389
494
|
},
|
|
390
495
|
|
|
391
496
|
groupKeys: [
|
|
@@ -403,88 +508,34 @@ const config = {
|
|
|
403
508
|
|
|
404
509
|
lookupTables: [],
|
|
405
510
|
|
|
406
|
-
/**
|
|
407
|
-
* ARCHITECTED ANALYTICS HOOKS
|
|
408
|
-
*
|
|
409
|
-
* This hook function creates 8 deliberate patterns in the data:
|
|
410
|
-
*
|
|
411
|
-
* 1. SPRING BUYING SEASON: Days 30-60, tours get 3x duration_mins, offers get 2.5x offer_price, spring_season=true
|
|
412
|
-
* 2. MORTGAGE RATE SHOCK: Day 75, rate jumps to 7.5; 45% of post-day-75 offers removed
|
|
413
|
-
* 3. SAVED-SEARCH RETENTION: First-7-day savers get extra engagement; non-savers lose 80% after day 30
|
|
414
|
-
* 4. PRE-APPROVED BUYER CONVERSION: Users with pre-approval get 5x offer events, pre_approved=true
|
|
415
|
-
* 5. TOP-TIER AGENT ADVANTAGE: Premier agents get 3x listings, 2x sales, premier_agent=true
|
|
416
|
-
* 6. TOUR-TO-OFFER POWER USERS: Users with both virtual + in-person tours get 6x offers, dual_tour=true
|
|
417
|
-
* 7. LUXURY LISTING RELEASE: Day 50+, 3% of listings become $5M+ luxury; ~3% of users browse luxury
|
|
418
|
-
* 8. COLD-LEAD CHURN: Browsers who never save in first 14 days lose 90% of events after day 14
|
|
419
|
-
*/
|
|
420
511
|
hook: function (record, type, meta) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
record.spring_season = true;
|
|
439
|
-
} else {
|
|
440
|
-
record.spring_season = false;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (record.event === "offer submitted") {
|
|
445
|
-
if (isSpring) {
|
|
446
|
-
record.offer_price = Math.floor((record.offer_price || 400000) * 2.5);
|
|
447
|
-
record.spring_season = true;
|
|
448
|
-
} else {
|
|
449
|
-
record.spring_season = false;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (record.event === "property viewed" && isSpring) {
|
|
454
|
-
record.spring_season = true;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ===============================================================
|
|
458
|
-
// Hook #2 (event part): MORTGAGE RATE SHOCK
|
|
459
|
-
// Day 75-89: mortgage rates forced to 7.5 on pre-approval events
|
|
460
|
-
// (up from baseline ~6.5).
|
|
461
|
-
// ===============================================================
|
|
462
|
-
if (record.event === "mortgage pre-approval") {
|
|
463
|
-
if (dayInDataset >= 75 && dayInDataset < 89) {
|
|
464
|
-
record.mortgage_rate = 7.5;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ===============================================================
|
|
469
|
-
// Hook #7 (event part): LUXURY LISTING RELEASE
|
|
470
|
-
// After day 50, 3% of property_listed events become luxury
|
|
471
|
-
// ($5M-$15M) with luxury=true. Before day 50, no luxury.
|
|
472
|
-
// ===============================================================
|
|
473
|
-
if (record.event === "property listed") {
|
|
474
|
-
if (dayInDataset >= 50 && chance.bool({ likelihood: 3 })) {
|
|
475
|
-
record.listing_price = chance.integer({ min: 5000000, max: 15000000 });
|
|
476
|
-
record.luxury = true;
|
|
477
|
-
record.premier_agent = false;
|
|
478
|
-
} else {
|
|
479
|
-
record.luxury = false;
|
|
512
|
+
// HOOK 10 (T2C): TOUR FUNNEL TIME-TO-CONVERT (funnel-post)
|
|
513
|
+
// Premier agents move users through Tour funnel 1.4x faster
|
|
514
|
+
// (factor 0.71); Standard agents 1.3x slower (factor 1.3).
|
|
515
|
+
if (type === "funnel-post") {
|
|
516
|
+
const segment = meta?.profile?.agent_tier;
|
|
517
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
518
|
+
const factor = (
|
|
519
|
+
segment === "Premier" ? 0.71 :
|
|
520
|
+
segment === "Standard" ? 1.3 :
|
|
521
|
+
1.0
|
|
522
|
+
);
|
|
523
|
+
if (factor !== 1.0) {
|
|
524
|
+
for (let i = 1; i < record.length; i++) {
|
|
525
|
+
const prev = dayjs(record[i - 1].time);
|
|
526
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
527
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
528
|
+
}
|
|
480
529
|
}
|
|
481
530
|
}
|
|
482
531
|
}
|
|
483
532
|
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
533
|
+
// HOOKS 1, 2, 7 (time-window event-level effects) moved to everything hook —
|
|
534
|
+
// event hook fires before bunchIntoSessions reshuffles timestamps, so day-window
|
|
535
|
+
// tags leak across boundaries.
|
|
536
|
+
|
|
487
537
|
if (type === "everything") {
|
|
538
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
488
539
|
const userEvents = record;
|
|
489
540
|
const profile = meta.profile;
|
|
490
541
|
|
|
@@ -493,276 +544,232 @@ const config = {
|
|
|
493
544
|
e.user_type = profile.user_type;
|
|
494
545
|
e.preferred_location = profile.preferred_location;
|
|
495
546
|
e.property_preference = profile.property_preference;
|
|
496
|
-
|
|
497
|
-
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// HOOK 1: SPRING BUYING SEASON — d30-60 tour duration 3x, offer_price 2.5x
|
|
550
|
+
// HOOK 2: MORTGAGE RATE SHOCK — d75-89 mortgage_rate pinned 7.5
|
|
551
|
+
// HOOK 7: LUXURY LISTING RELEASE — post-d50, 3% of listings priced $5M-$15M
|
|
552
|
+
const springStart = datasetStart.add(30, "days");
|
|
553
|
+
const springEnd = datasetStart.add(60, "days");
|
|
554
|
+
const shockStart = datasetStart.add(75, "days");
|
|
555
|
+
const shockEnd = datasetStart.add(89, "days");
|
|
556
|
+
const luxuryStart = datasetStart.add(50, "days");
|
|
557
|
+
userEvents.forEach(e => {
|
|
558
|
+
const t = dayjs.utc(e.time);
|
|
559
|
+
const inSpring = (t.isAfter(springStart) || t.isSame(springStart)) && t.isBefore(springEnd);
|
|
560
|
+
if (inSpring && e.event === "tour scheduled" && typeof e.duration_mins === "number") {
|
|
561
|
+
e.duration_mins = Math.round(e.duration_mins * 3);
|
|
562
|
+
}
|
|
563
|
+
if (inSpring && e.event === "offer submitted") {
|
|
564
|
+
e.offer_price = Math.floor((e.offer_price || 400000) * 2.5);
|
|
565
|
+
}
|
|
566
|
+
if (e.event === "mortgage pre-approval") {
|
|
567
|
+
if ((t.isAfter(shockStart) || t.isSame(shockStart)) && t.isBefore(shockEnd)) {
|
|
568
|
+
e.mortgage_rate = 7.5;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (e.event === "property listed" && t.isAfter(luxuryStart) && chance.bool({ likelihood: 3 })) {
|
|
572
|
+
e.listing_price = chance.integer({ min: 5000000, max: 15000000 });
|
|
573
|
+
}
|
|
498
574
|
});
|
|
499
575
|
|
|
500
576
|
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
501
577
|
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
// (buyer pullback due to higher rates).
|
|
506
|
-
// -----------------------------------------------------------
|
|
507
|
-
const day75 = DATASET_START.add(75, "days");
|
|
578
|
+
// HOOK 2 (cont): MORTGAGE RATE SHOCK — drop 45% of post-day-75
|
|
579
|
+
// offer-submitted events. No flag.
|
|
580
|
+
const day75 = datasetStart.add(75, "days");
|
|
508
581
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
509
582
|
const evt = userEvents[i];
|
|
510
|
-
if (evt.event === "offer submitted" && dayjs(evt.time).isAfter(day75)) {
|
|
511
|
-
|
|
512
|
-
userEvents.splice(i, 1);
|
|
513
|
-
}
|
|
583
|
+
if (evt.event === "offer submitted" && dayjs(evt.time).isAfter(day75) && chance.bool({ likelihood: 45 })) {
|
|
584
|
+
userEvents.splice(i, 1);
|
|
514
585
|
}
|
|
515
586
|
}
|
|
516
587
|
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
// days get extra property_viewed and tour_scheduled events.
|
|
521
|
-
// Non-savers lose 80% of events after day 30.
|
|
522
|
-
// -----------------------------------------------------------
|
|
588
|
+
// HOOK 3: SAVED-SEARCH RETENTION — early savers (saved-search-created
|
|
589
|
+
// in first 7 days) get extra cloned view + tour events. Non-savers
|
|
590
|
+
// lose 80% of events after day 30. No flag.
|
|
523
591
|
const day7 = firstEventTime ? firstEventTime.add(7, "days") : null;
|
|
524
|
-
const day30 =
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
hasSavedSearch = userEvents.some(e =>
|
|
529
|
-
e.event === "saved search created" &&
|
|
530
|
-
dayjs(e.time).isBefore(day7)
|
|
531
|
-
);
|
|
532
|
-
}
|
|
592
|
+
const day30 = datasetStart.add(30, "days");
|
|
593
|
+
const hasSavedSearch = day7 ? userEvents.some(e =>
|
|
594
|
+
e.event === "saved search created" && dayjs(e.time).isBefore(day7)
|
|
595
|
+
) : false;
|
|
533
596
|
|
|
534
597
|
if (hasSavedSearch) {
|
|
535
|
-
// Mark the user and inject extra engagement events
|
|
536
|
-
profile.has_saved_search = true;
|
|
537
|
-
userEvents.forEach(e => { e.has_saved_search = true; });
|
|
538
|
-
|
|
539
|
-
// Inject extra property_viewed and tour_scheduled events
|
|
540
598
|
const viewTemplate = userEvents.find(e => e.event === "property viewed");
|
|
541
599
|
const tourTemplate = userEvents.find(e => e.event === "tour scheduled");
|
|
542
|
-
|
|
543
600
|
if (viewTemplate) {
|
|
544
|
-
const
|
|
545
|
-
for (let i = 0; i <
|
|
601
|
+
const extras = chance.integer({ min: 3, max: 8 });
|
|
602
|
+
for (let i = 0; i < extras; i++) {
|
|
546
603
|
const offset = chance.integer({ min: 10, max: num_days - 10 });
|
|
547
604
|
userEvents.push({
|
|
548
605
|
...viewTemplate,
|
|
549
|
-
time:
|
|
606
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 0, max: 23 }), "hours").toISOString(),
|
|
550
607
|
user_id: viewTemplate.user_id,
|
|
551
608
|
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
552
609
|
listing_price: chance.integer({ min: 200000, max: 1200000 }),
|
|
553
|
-
has_saved_search: true,
|
|
554
610
|
});
|
|
555
611
|
}
|
|
556
612
|
}
|
|
557
613
|
if (tourTemplate) {
|
|
558
|
-
const
|
|
559
|
-
for (let i = 0; i <
|
|
614
|
+
const extras = chance.integer({ min: 1, max: 3 });
|
|
615
|
+
for (let i = 0; i < extras; i++) {
|
|
560
616
|
const offset = chance.integer({ min: 15, max: num_days - 10 });
|
|
561
617
|
userEvents.push({
|
|
562
618
|
...tourTemplate,
|
|
563
|
-
time:
|
|
619
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
|
|
564
620
|
user_id: tourTemplate.user_id,
|
|
565
621
|
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
566
|
-
has_saved_search: true,
|
|
567
622
|
});
|
|
568
623
|
}
|
|
569
624
|
}
|
|
570
625
|
} else {
|
|
571
|
-
// Non-savers: remove 80% of events after day 30
|
|
572
626
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
573
627
|
const evt = userEvents[i];
|
|
574
|
-
if (dayjs(evt.time).isAfter(day30)) {
|
|
575
|
-
|
|
576
|
-
userEvents.splice(i, 1);
|
|
577
|
-
}
|
|
628
|
+
if (dayjs(evt.time).isAfter(day30) && chance.bool({ likelihood: 80 })) {
|
|
629
|
+
userEvents.splice(i, 1);
|
|
578
630
|
}
|
|
579
631
|
}
|
|
580
632
|
}
|
|
581
633
|
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
// offer_submitted events injected. pre_approved=true.
|
|
586
|
-
// -----------------------------------------------------------
|
|
634
|
+
// HOOK 4: PRE-APPROVED BUYER CONVERSION — clone 4-6 extra
|
|
635
|
+
// offer-submitted events for users with a mortgage pre-approval
|
|
636
|
+
// event. No flag.
|
|
587
637
|
const hasPreApproval = userEvents.some(e => e.event === "mortgage pre-approval");
|
|
588
|
-
|
|
589
638
|
if (hasPreApproval) {
|
|
590
639
|
profile.pre_approval_status = "approved";
|
|
591
640
|
const offerTemplate = userEvents.find(e => e.event === "offer submitted");
|
|
592
641
|
if (offerTemplate) {
|
|
593
|
-
const
|
|
594
|
-
for (let i = 0; i <
|
|
642
|
+
const extras = chance.integer({ min: 4, max: 6 });
|
|
643
|
+
for (let i = 0; i < extras; i++) {
|
|
595
644
|
const offset = chance.integer({ min: 20, max: num_days - 5 });
|
|
596
645
|
userEvents.push({
|
|
597
646
|
...offerTemplate,
|
|
598
|
-
time:
|
|
647
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
|
|
599
648
|
user_id: offerTemplate.user_id,
|
|
600
649
|
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
601
650
|
offer_price: chance.integer({ min: 250000, max: 1200000 }),
|
|
602
651
|
listing_price: chance.integer({ min: 250000, max: 1200000 }),
|
|
603
|
-
pre_approved: true,
|
|
604
|
-
spring_season: false,
|
|
605
|
-
dual_tour: false,
|
|
606
652
|
});
|
|
607
653
|
}
|
|
608
654
|
}
|
|
609
|
-
|
|
610
|
-
// Also tag existing offers
|
|
611
|
-
userEvents.forEach(e => {
|
|
612
|
-
if (e.event === "offer submitted") {
|
|
613
|
-
e.pre_approved = true;
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
655
|
}
|
|
617
656
|
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
// events. premier_agent=true on all their listing/sale events.
|
|
622
|
-
// -----------------------------------------------------------
|
|
657
|
+
// HOOK 5: TOP-TIER AGENT ADVANTAGE — Premier agents get 2 extra
|
|
658
|
+
// cloned listings per existing (3x rate) and 1 extra clone per
|
|
659
|
+
// sale (2x rate). Reads agent_tier from profile. No flag.
|
|
623
660
|
if (profile.agent_tier === "Premier") {
|
|
624
661
|
const listTemplate = userEvents.find(e => e.event === "property listed");
|
|
625
662
|
const soldTemplate = userEvents.find(e => e.event === "property sold");
|
|
626
663
|
|
|
627
|
-
// Tag existing events
|
|
628
|
-
userEvents.forEach(e => {
|
|
629
|
-
if (e.event === "property listed" || e.event === "property sold") {
|
|
630
|
-
e.premier_agent = true;
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
// Inject extra listings (3x = 2 more copies per existing)
|
|
635
664
|
if (listTemplate) {
|
|
636
665
|
const existingListings = userEvents.filter(e => e.event === "property listed").length;
|
|
637
|
-
const
|
|
638
|
-
for (let i = 0; i <
|
|
666
|
+
const extras = existingListings * 2;
|
|
667
|
+
for (let i = 0; i < extras; i++) {
|
|
639
668
|
const offset = chance.integer({ min: 5, max: num_days - 5 });
|
|
640
669
|
userEvents.push({
|
|
641
670
|
...listTemplate,
|
|
642
|
-
time:
|
|
671
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 18 }), "hours").toISOString(),
|
|
643
672
|
user_id: listTemplate.user_id,
|
|
644
673
|
listing_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
645
674
|
listing_status: chance.pickone(["active", "pending", "coming soon"]),
|
|
646
|
-
luxury: false,
|
|
647
|
-
premier_agent: true,
|
|
648
675
|
});
|
|
649
676
|
}
|
|
650
677
|
}
|
|
651
|
-
|
|
652
|
-
// Inject extra sales (2x = 1 more copy per existing)
|
|
653
678
|
if (soldTemplate) {
|
|
654
679
|
const existingSales = userEvents.filter(e => e.event === "property sold").length;
|
|
655
680
|
for (let i = 0; i < existingSales; i++) {
|
|
656
681
|
const offset = chance.integer({ min: 10, max: num_days - 5 });
|
|
657
682
|
userEvents.push({
|
|
658
683
|
...soldTemplate,
|
|
659
|
-
time:
|
|
684
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 9, max: 17 }), "hours").toISOString(),
|
|
660
685
|
user_id: soldTemplate.user_id,
|
|
661
686
|
sale_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
662
687
|
days_on_market: chance.integer({ min: 5, max: 90 }),
|
|
663
|
-
premier_agent: true,
|
|
664
688
|
});
|
|
665
689
|
}
|
|
666
690
|
}
|
|
667
691
|
}
|
|
668
692
|
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// Users who did BOTH "virtual tour" AND "in-person tour"
|
|
672
|
-
// get 6x offer_submitted events. dual_tour=true on offers.
|
|
673
|
-
// -----------------------------------------------------------
|
|
693
|
+
// HOOK 6: TOUR-TO-OFFER POWER USERS — users with both virtual
|
|
694
|
+
// tour AND in-person tour get 5-7 extra cloned offers. No flag.
|
|
674
695
|
const hasVirtualTour = userEvents.some(e => e.event === "virtual tour");
|
|
675
696
|
const hasInPersonTour = userEvents.some(e => e.event === "in-person tour");
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (isDualTourer) {
|
|
697
|
+
if (hasVirtualTour && hasInPersonTour) {
|
|
679
698
|
const offerTemplate = userEvents.find(e => e.event === "offer submitted");
|
|
680
699
|
if (offerTemplate) {
|
|
681
|
-
const
|
|
682
|
-
for (let i = 0; i <
|
|
700
|
+
const extras = chance.integer({ min: 5, max: 7 });
|
|
701
|
+
for (let i = 0; i < extras; i++) {
|
|
683
702
|
const offset = chance.integer({ min: 15, max: num_days - 5 });
|
|
684
703
|
userEvents.push({
|
|
685
704
|
...offerTemplate,
|
|
686
|
-
time:
|
|
705
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 8, max: 20 }), "hours").toISOString(),
|
|
687
706
|
user_id: offerTemplate.user_id,
|
|
688
707
|
listing_id: `LST-${chance.integer({ min: 10000, max: 99999 })}`,
|
|
689
708
|
offer_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
690
709
|
listing_price: chance.integer({ min: 300000, max: 1500000 }),
|
|
691
|
-
dual_tour: true,
|
|
692
|
-
pre_approved: false,
|
|
693
|
-
spring_season: false,
|
|
694
710
|
});
|
|
695
711
|
}
|
|
696
712
|
}
|
|
697
|
-
|
|
698
|
-
// Tag existing offers
|
|
699
|
-
userEvents.forEach(e => {
|
|
700
|
-
if (e.event === "offer submitted") {
|
|
701
|
-
e.dual_tour = true;
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
713
|
}
|
|
705
714
|
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
// ~3% of users (by hash) get extra luxury property_viewed
|
|
709
|
-
// events after day 50. Deterministic via character code.
|
|
710
|
-
// -----------------------------------------------------------
|
|
711
|
-
const day50 = DATASET_START.add(50, "days");
|
|
715
|
+
// HOOK 7 (cont): LUXURY LISTING RELEASE — ~3% of users (by hash)
|
|
716
|
+
// get 3-6 extra luxury property-viewed events after day 50. No flag.
|
|
712
717
|
if (userEvents.length > 0) {
|
|
713
718
|
const userId = userEvents[0].user_id || "";
|
|
714
719
|
const isLuxuryBrowser = typeof userId === "string" && userId.length > 0 && userId.charCodeAt(0) % 33 === 0;
|
|
715
|
-
|
|
716
720
|
if (isLuxuryBrowser) {
|
|
717
721
|
const viewTemplate = userEvents.find(e => e.event === "property viewed");
|
|
718
722
|
if (viewTemplate) {
|
|
719
|
-
const
|
|
720
|
-
for (let i = 0; i <
|
|
723
|
+
const extras = chance.integer({ min: 3, max: 6 });
|
|
724
|
+
for (let i = 0; i < extras; i++) {
|
|
721
725
|
const offset = chance.integer({ min: 50, max: num_days - 5 });
|
|
722
726
|
userEvents.push({
|
|
723
727
|
...viewTemplate,
|
|
724
|
-
time:
|
|
728
|
+
time: datasetStart.add(offset, "days").add(chance.integer({ min: 9, max: 21 }), "hours").toISOString(),
|
|
725
729
|
user_id: viewTemplate.user_id,
|
|
726
730
|
listing_id: `LST-${chance.integer({ min: 90000, max: 99999 })}`,
|
|
727
731
|
listing_price: chance.integer({ min: 5000000, max: 15000000 }),
|
|
728
732
|
property_type: chance.pickone(["Single Family", "Condo"]),
|
|
729
733
|
bedrooms: chance.integer({ min: 4, max: 8 }),
|
|
730
734
|
square_feet: chance.integer({ min: 4000, max: 15000 }),
|
|
731
|
-
luxury: true,
|
|
732
|
-
spring_season: false,
|
|
733
735
|
});
|
|
734
736
|
}
|
|
735
737
|
}
|
|
736
738
|
}
|
|
737
739
|
}
|
|
738
740
|
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
// Users who browse (property viewed) but never save (property
|
|
742
|
-
// saved) in the first 14 days lose 90% of events after day 14.
|
|
743
|
-
// -----------------------------------------------------------
|
|
741
|
+
// HOOK 8: COLD-LEAD CHURN — users who view but never save in
|
|
742
|
+
// first 14 days lose 90% of post-day-14 events. No flag.
|
|
744
743
|
const day14 = firstEventTime ? firstEventTime.add(14, "days") : null;
|
|
745
|
-
|
|
746
744
|
if (day14) {
|
|
747
745
|
const earlyEvents = userEvents.filter(e => dayjs(e.time).isBefore(day14));
|
|
748
746
|
const hasView = earlyEvents.some(e => e.event === "property viewed");
|
|
749
747
|
const hasSave = earlyEvents.some(e => e.event === "property saved");
|
|
750
|
-
|
|
751
748
|
if (hasView && !hasSave) {
|
|
752
|
-
// Cold lead: remove 90% of events after day 14
|
|
753
|
-
profile.cold_lead = true;
|
|
754
749
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
755
750
|
const evt = userEvents[i];
|
|
756
|
-
if (dayjs(evt.time).isAfter(day14)) {
|
|
757
|
-
|
|
758
|
-
userEvents.splice(i, 1);
|
|
759
|
-
} else {
|
|
760
|
-
evt.cold_lead = true;
|
|
761
|
-
}
|
|
751
|
+
if (dayjs(evt.time).isAfter(day14) && chance.bool({ likelihood: 90 })) {
|
|
752
|
+
userEvents.splice(i, 1);
|
|
762
753
|
}
|
|
763
754
|
}
|
|
764
|
-
|
|
765
|
-
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// HOOK 9: PROPERTY-VIEWED MAGIC NUMBER (no flags)
|
|
759
|
+
// Sweet 6-12 views → +30% offer_price on offer submitted events.
|
|
760
|
+
// Over 13+ → drop 35% of offer submitted events (tire-kickers).
|
|
761
|
+
const viewCount = userEvents.filter(e => e.event === "property viewed").length;
|
|
762
|
+
if (viewCount >= 6 && viewCount <= 12) {
|
|
763
|
+
userEvents.forEach(e => {
|
|
764
|
+
if (e.event === "offer submitted" && typeof e.offer_price === "number") {
|
|
765
|
+
e.offer_price = Math.round(e.offer_price * 1.3);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
} else if (viewCount >= 13) {
|
|
769
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
770
|
+
if (userEvents[i].event === "offer submitted" && chance.bool({ likelihood: 35 })) {
|
|
771
|
+
userEvents.splice(i, 1);
|
|
772
|
+
}
|
|
766
773
|
}
|
|
767
774
|
}
|
|
768
775
|
}
|