@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "meetcute";
|
|
3
3
|
const num_days = 120;
|
|
4
|
-
const num_users =
|
|
5
|
-
const avg_events_per_user_per_day =
|
|
4
|
+
const num_users = 30_000;
|
|
5
|
+
const avg_events_per_user_per_day = 1.5;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
@@ -16,9 +16,6 @@ import * as v from "ak-tools";
|
|
|
16
16
|
|
|
17
17
|
dayjs.extend(utc);
|
|
18
18
|
const chance = u.initChance(SEED);
|
|
19
|
-
const NOW = dayjs();
|
|
20
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
21
|
-
|
|
22
19
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
23
20
|
|
|
24
21
|
/*
|
|
@@ -51,180 +48,178 @@ const DATASET_START = NOW.subtract(num_days, "days");
|
|
|
51
48
|
|
|
52
49
|
/*
|
|
53
50
|
* =====================================================================================
|
|
54
|
-
* ANALYTICS HOOKS (
|
|
51
|
+
* ANALYTICS HOOKS (9 hooks)
|
|
55
52
|
* =====================================================================================
|
|
56
53
|
*
|
|
54
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
|
|
55
|
+
* behavioral cohorts, raw-prop breakdowns, or funnel analysis.
|
|
56
|
+
*
|
|
57
57
|
* -------------------------------------------------------------------------------------
|
|
58
|
-
* 1. PHOTO
|
|
58
|
+
* 1. PHOTO MAGIC NUMBER (everything)
|
|
59
59
|
* -------------------------------------------------------------------------------------
|
|
60
|
-
* PATTERN:
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* PATTERN: Sweet 2-5 photos uploaded → 2-4 extra cloned match-received events
|
|
61
|
+
* per existing match. Over 6+ photos → match_score drops 35% on match-received
|
|
62
|
+
* events (over-curated profile reads as fake; quality matches don't trust it).
|
|
63
|
+
* No flag.
|
|
63
64
|
*
|
|
64
65
|
* HOW TO FIND IT IN MIXPANEL:
|
|
65
|
-
* Report 1:
|
|
66
|
-
* -
|
|
67
|
-
* -
|
|
66
|
+
* Report 1: Matches per User by Photo-Count Bucket
|
|
67
|
+
* - Cohort A: users with 2-5 "photo uploaded"
|
|
68
|
+
* - Cohort B: users with 0-1
|
|
69
|
+
* - Event: "match received" → Total per user
|
|
70
|
+
* - Expected: A ~ 2-3x B
|
|
68
71
|
*
|
|
69
|
-
* Report 2: Photo
|
|
70
|
-
* -
|
|
71
|
-
* -
|
|
72
|
+
* Report 2: Avg match_score on Heavy Photo Uploaders
|
|
73
|
+
* - Cohort C: users with >= 6 "photo uploaded"
|
|
74
|
+
* - Cohort A: users with 2-5
|
|
75
|
+
* - Event: "match received" → AVG of match_score
|
|
76
|
+
* - Expected: C ~ 0.65x A (35% lower match quality)
|
|
77
|
+
*
|
|
78
|
+
* REAL-WORLD ANALOGUE: A few good photos signal authenticity; too many
|
|
79
|
+
* curated shots read as catfish or staged.
|
|
72
80
|
*
|
|
73
81
|
* -------------------------------------------------------------------------------------
|
|
74
|
-
* 2. WEEKEND SWIPE SURGE (
|
|
82
|
+
* 2. WEEKEND SWIPE SURGE (everything)
|
|
75
83
|
* -------------------------------------------------------------------------------------
|
|
76
|
-
* PATTERN:
|
|
77
|
-
* duplicated 2.5x with weekend_surge=true. The Sunday Scaries drive swipes.
|
|
84
|
+
* PATTERN: Sunday 18-23 UTC swipes get 1.5x duplication. No flag.
|
|
78
85
|
*
|
|
79
86
|
* HOW TO FIND IT IN MIXPANEL:
|
|
80
87
|
* Report 1: Swipe Volume by Day of Week
|
|
81
|
-
* - Insights
|
|
82
|
-
* - Expected: Sunday
|
|
88
|
+
* - Insights > "swipe right" → Total → Breakdown: Day of Week
|
|
89
|
+
* - Expected: Sunday taller than other days
|
|
83
90
|
*
|
|
84
|
-
*
|
|
85
|
-
* - Insights (line) > "swipe right" > Total > Breakdown: "weekend_surge"
|
|
86
|
-
* - Expected: weekend_surge=true events clustered on Sunday evenings
|
|
91
|
+
* REAL-WORLD ANALOGUE: Sunday Scaries drive swipe activity.
|
|
87
92
|
*
|
|
88
93
|
* -------------------------------------------------------------------------------------
|
|
89
|
-
* 3. SUPER-LIKE EFFECT (everything
|
|
94
|
+
* 3. SUPER-LIKE EFFECT (everything)
|
|
90
95
|
* -------------------------------------------------------------------------------------
|
|
91
|
-
* PATTERN:
|
|
92
|
-
*
|
|
93
|
-
* matches. Super-likes dramatically improve match rates.
|
|
96
|
+
* PATTERN: Each is_super_like=true swipe clones 3 extra match-received events
|
|
97
|
+
* within 5-120 minutes. No flag.
|
|
94
98
|
*
|
|
95
99
|
* HOW TO FIND IT IN MIXPANEL:
|
|
96
|
-
* Report 1:
|
|
97
|
-
* -
|
|
98
|
-
* -
|
|
100
|
+
* Report 1: Super-Like to Match Funnel
|
|
101
|
+
* - Funnels > "swipe right" (filter: is_super_like=true) → "match received"
|
|
102
|
+
* - vs same with is_super_like=false
|
|
103
|
+
* - Expected: super-like funnel ~ 3x higher conversion
|
|
99
104
|
*
|
|
100
|
-
*
|
|
101
|
-
* - Funnels > "swipe right" (filter: is_super_like=true) > "match received"
|
|
102
|
-
* - Compare to: "swipe right" (filter: is_super_like=false) > "match received"
|
|
103
|
-
* - Expected: super-like funnel ~3x higher conversion
|
|
105
|
+
* REAL-WORLD ANALOGUE: Super-likes dramatically lift match rates.
|
|
104
106
|
*
|
|
105
107
|
* -------------------------------------------------------------------------------------
|
|
106
|
-
* 4. PREMIUM MATCH BOOST (everything
|
|
108
|
+
* 4. PREMIUM MATCH BOOST (everything)
|
|
107
109
|
* -------------------------------------------------------------------------------------
|
|
108
|
-
* PATTERN: Premium subscribers get 2x
|
|
109
|
-
*
|
|
110
|
-
* premium_boost=true on injected events. Subscription pays off.
|
|
110
|
+
* PATTERN: Premium subscribers get 2x match events; Elite get 4x + cloned
|
|
111
|
+
* profile-viewed events (see-who-liked-you). Reads subscription from profile.
|
|
111
112
|
*
|
|
112
113
|
* HOW TO FIND IT IN MIXPANEL:
|
|
113
114
|
* Report 1: Matches by Subscription Tier
|
|
114
|
-
* - Insights > "match received"
|
|
115
|
-
* - Expected: Free ~
|
|
115
|
+
* - Insights > "match received" → Total per user → Breakdown: subscription
|
|
116
|
+
* - Expected: Free ~ 1x, Premium ~ 2x, Elite ~ 4x
|
|
116
117
|
*
|
|
117
|
-
*
|
|
118
|
-
* - Insights > "profile viewed" > Total > Breakdown: "premium_boost"
|
|
119
|
-
* - Expected: premium_boost=true events only from Elite subscribers
|
|
118
|
+
* REAL-WORLD ANALOGUE: Premium tiers boost visibility.
|
|
120
119
|
*
|
|
121
120
|
* -------------------------------------------------------------------------------------
|
|
122
|
-
* 5. GHOSTING CHURN (everything
|
|
121
|
+
* 5. GHOSTING CHURN (everything)
|
|
123
122
|
* -------------------------------------------------------------------------------------
|
|
124
|
-
* PATTERN: Users
|
|
125
|
-
*
|
|
126
|
-
* remaining "app opened" events. Ghosters churn.
|
|
123
|
+
* PATTERN: Users with match-received but no message-sent within 48 hours lose
|
|
124
|
+
* 80% of post-match events. No flag.
|
|
127
125
|
*
|
|
128
126
|
* HOW TO FIND IT IN MIXPANEL:
|
|
129
127
|
* Report 1: Retention by Messaging Behavior
|
|
130
|
-
* - Retention
|
|
131
|
-
* -
|
|
132
|
-
* -
|
|
128
|
+
* - Retention starting "match received"
|
|
129
|
+
* - Cohort A: users with >= 1 "message sent" within 48 hours of match
|
|
130
|
+
* - Cohort B: rest
|
|
131
|
+
* - Expected: B drops sharply
|
|
133
132
|
*
|
|
134
|
-
*
|
|
135
|
-
* - Insights (line) > "app opened" > Total > Breakdown: "ghosted"
|
|
136
|
-
* - Expected: ghosted=true users trail off sharply after match date
|
|
133
|
+
* REAL-WORLD ANALOGUE: Non-responders churn.
|
|
137
134
|
*
|
|
138
135
|
* -------------------------------------------------------------------------------------
|
|
139
|
-
* 6. BIO + PROMPT POWER USERS (everything
|
|
136
|
+
* 6. BIO + PROMPT POWER USERS (everything)
|
|
140
137
|
* -------------------------------------------------------------------------------------
|
|
141
|
-
* PATTERN: Users with
|
|
142
|
-
*
|
|
143
|
-
* dates. Effort on profile = real dates.
|
|
138
|
+
* PATTERN: Users with bio-updated AND 3+ prompt-answered events get 3 extra
|
|
139
|
+
* cloned date-scheduled events per existing. No flag.
|
|
144
140
|
*
|
|
145
141
|
* HOW TO FIND IT IN MIXPANEL:
|
|
146
|
-
* Report 1: Dates by Profile Effort
|
|
147
|
-
* -
|
|
148
|
-
* -
|
|
142
|
+
* Report 1: Dates per User by Profile Effort
|
|
143
|
+
* - Cohort A: users with bio-updated AND >= 3 "prompt answered"
|
|
144
|
+
* - Cohort B: rest
|
|
145
|
+
* - Event: "date scheduled" → Total per user
|
|
146
|
+
* - Expected: A ~ 4x B
|
|
149
147
|
*
|
|
150
|
-
*
|
|
151
|
-
* - Funnels > "bio updated" > "prompt answered" > "date scheduled"
|
|
152
|
-
* - Expected: users completing all 3 have dramatically higher date rates
|
|
148
|
+
* REAL-WORLD ANALOGUE: Profile effort signals serious intent.
|
|
153
149
|
*
|
|
154
150
|
* -------------------------------------------------------------------------------------
|
|
155
|
-
* 7. VALENTINE'S DAY SPIKE (
|
|
151
|
+
* 7. VALENTINE'S DAY SPIKE (everything)
|
|
156
152
|
* -------------------------------------------------------------------------------------
|
|
157
|
-
* PATTERN:
|
|
158
|
-
*
|
|
159
|
-
* events 5x for users active during V-Day window. Love is expensive.
|
|
153
|
+
* PATTERN: Days 58-63 (V-Day window): profile-created events cloned 3x and
|
|
154
|
+
* premium-upgrade events cloned 5x. No flag — discover via line chart by day.
|
|
160
155
|
*
|
|
161
156
|
* HOW TO FIND IT IN MIXPANEL:
|
|
162
|
-
* Report 1: Signup
|
|
163
|
-
* - Insights
|
|
164
|
-
* - Expected:
|
|
157
|
+
* Report 1: Signup Volume Over Time
|
|
158
|
+
* - Insights > "profile created" → Total → Line by day
|
|
159
|
+
* - Expected: spike ~ 3x days 58-63
|
|
165
160
|
*
|
|
166
|
-
*
|
|
167
|
-
* - Insights (line) > "premium upgrade" > Total > Breakdown: "valentines"
|
|
168
|
-
* - Expected: 5x premium upgrades during V-Day window
|
|
161
|
+
* REAL-WORLD ANALOGUE: Love is expensive.
|
|
169
162
|
*
|
|
170
163
|
* -------------------------------------------------------------------------------------
|
|
171
|
-
* 8. OFF-APP RETENTION (everything
|
|
164
|
+
* 8. OFF-APP RETENTION (everything)
|
|
172
165
|
* -------------------------------------------------------------------------------------
|
|
173
|
-
* PATTERN: Users with
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* once they find someone IRL.
|
|
166
|
+
* PATTERN: Users with phone-exchanged OR date-scheduled in first 14 days
|
|
167
|
+
* get extra cloned app-open + swipe events past day 30. Non-milestone
|
|
168
|
+
* users lose 80% of post-day-30 events. No flag.
|
|
177
169
|
*
|
|
178
170
|
* HOW TO FIND IT IN MIXPANEL:
|
|
179
|
-
* Report 1: Retention by Milestone
|
|
180
|
-
* -
|
|
181
|
-
* -
|
|
182
|
-
* - Expected:
|
|
171
|
+
* Report 1: Retention by Milestone Cohort
|
|
172
|
+
* - Cohort A: users with phone-exchanged OR date-scheduled in first 14 days
|
|
173
|
+
* - Cohort B: rest
|
|
174
|
+
* - Expected: A ~ 60% D30 retention vs B ~ 20%
|
|
183
175
|
*
|
|
184
|
-
*
|
|
185
|
-
* - Insights (line) > "app opened" > Total per user > Weekly > Breakdown: user prop "subscription"
|
|
186
|
-
* - Expected: after week 4, non-milestone users nearly disappear
|
|
176
|
+
* REAL-WORLD ANALOGUE: Once they find someone IRL, app becomes irrelevant.
|
|
187
177
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
178
|
+
* -------------------------------------------------------------------------------------
|
|
179
|
+
* 9. MATCH FLOW TIME-TO-CONVERT (funnel-post)
|
|
180
|
+
* -------------------------------------------------------------------------------------
|
|
181
|
+
* PATTERN: Elite users complete swipe→match→message funnel 1.4x faster
|
|
182
|
+
* (factor 0.71 on inter-event gaps); Free users 1.4x slower (factor 1.4).
|
|
191
183
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
184
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
185
|
+
* Report 1: Match Flow Median Time-to-Convert by Tier
|
|
186
|
+
* - Funnels > "swipe right" → "match received" → "message sent"
|
|
187
|
+
* - Measure: Median time to convert
|
|
188
|
+
* - Breakdown: subscription
|
|
189
|
+
* - Expected: Elite ~ 0.71x; Free ~ 1.4x
|
|
190
|
+
*
|
|
191
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
192
|
+
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
193
|
+
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
194
|
+
* across the user's full event history.
|
|
195
|
+
*
|
|
196
|
+
* REAL-WORLD ANALOGUE: Premium notifications + boost surface matches faster.
|
|
202
197
|
*
|
|
203
198
|
* =====================================================================================
|
|
204
|
-
*
|
|
199
|
+
* EXPECTED METRICS SUMMARY
|
|
205
200
|
* =====================================================================================
|
|
206
201
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* -
|
|
218
|
-
*
|
|
219
|
-
* date funnel by profile completeness.
|
|
220
|
-
* =====================================================================================
|
|
202
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
203
|
+
* ----------------------------|----------------------|----------|----------|------
|
|
204
|
+
* Photo Magic Number | sweet matches/user | 1x | ~ 1.7x | 1.7x
|
|
205
|
+
* Photo Magic Number | over matches/user | 1x | 0.7x | -30%
|
|
206
|
+
* Weekend Swipe Surge | Sunday swipes | 1x | ~ 1.5x | 1.5x
|
|
207
|
+
* Super-Like Effect | matches from super | 1x | 3x | 3x
|
|
208
|
+
* Premium Match Boost | matches (Elite) | 1x | 4x | 4x
|
|
209
|
+
* Ghosting Churn | post-match retention | 100% | 20% | 0.2x
|
|
210
|
+
* Bio + Prompt Power Users | dates/user | 1x | 4x | 4x
|
|
211
|
+
* Valentine's Day Spike | signups days 58-63 | 1x | 3x | 3x
|
|
212
|
+
* Off-App Retention | D30 retention | ~ 20% | ~ 60% | 3x
|
|
213
|
+
* Match Flow T2C | median min by tier | 1x | 0.71x/1.4x| ~ 2x range
|
|
221
214
|
*/
|
|
222
215
|
|
|
223
216
|
/** @type {Config} */
|
|
224
217
|
const config = {
|
|
225
218
|
token,
|
|
226
219
|
seed: SEED,
|
|
227
|
-
|
|
220
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
221
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
222
|
+
// numDays: num_days,
|
|
228
223
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
229
224
|
numUsers: num_users,
|
|
230
225
|
hasAnonIds: false,
|
|
@@ -255,12 +250,11 @@ const config = {
|
|
|
255
250
|
age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
|
|
256
251
|
gender: ["Male", "Male", "Female", "Female", "Non-binary"],
|
|
257
252
|
looking_for: ["Men", "Women", "Everyone"],
|
|
258
|
-
valentines: [false],
|
|
259
253
|
},
|
|
260
254
|
},
|
|
261
255
|
{
|
|
262
256
|
event: "photo uploaded",
|
|
263
|
-
weight:
|
|
257
|
+
weight: 12,
|
|
264
258
|
properties: {
|
|
265
259
|
photo_number: u.weighNumRange(1, 6, 0.5, 2),
|
|
266
260
|
has_face: [true, true, true, true, false],
|
|
@@ -286,7 +280,6 @@ const config = {
|
|
|
286
280
|
weight: 10,
|
|
287
281
|
properties: {
|
|
288
282
|
is_super_like: [false, false, false, false, false, false, false, false, false, true],
|
|
289
|
-
weekend_surge: [false],
|
|
290
283
|
swipe_source: ["feed", "feed", "feed", "discover", "boost", "nearby"],
|
|
291
284
|
},
|
|
292
285
|
},
|
|
@@ -302,9 +295,6 @@ const config = {
|
|
|
302
295
|
weight: 4,
|
|
303
296
|
properties: {
|
|
304
297
|
match_score: u.weighNumRange(50, 100, 0.5, 75),
|
|
305
|
-
complete_profile: [false],
|
|
306
|
-
super_like_match: [false],
|
|
307
|
-
premium_boost: [false],
|
|
308
298
|
},
|
|
309
299
|
},
|
|
310
300
|
{
|
|
@@ -335,7 +325,6 @@ const config = {
|
|
|
335
325
|
weight: 1,
|
|
336
326
|
properties: {
|
|
337
327
|
venue_type: ["coffee", "dinner", "drinks", "activity", "virtual"],
|
|
338
|
-
complete_profile_power: [false],
|
|
339
328
|
},
|
|
340
329
|
},
|
|
341
330
|
{
|
|
@@ -343,7 +332,6 @@ const config = {
|
|
|
343
332
|
weight: 5,
|
|
344
333
|
properties: {
|
|
345
334
|
viewer_source: ["feed", "discover", "liked_you", "nearby"],
|
|
346
|
-
premium_boost: [false],
|
|
347
335
|
},
|
|
348
336
|
},
|
|
349
337
|
{
|
|
@@ -352,7 +340,6 @@ const config = {
|
|
|
352
340
|
properties: {
|
|
353
341
|
plan: ["Premium", "Premium", "Premium", "Elite"],
|
|
354
342
|
price_usd: [14.99, 14.99, 14.99, 29.99],
|
|
355
|
-
valentines: [false],
|
|
356
343
|
},
|
|
357
344
|
},
|
|
358
345
|
{
|
|
@@ -385,7 +372,6 @@ const config = {
|
|
|
385
372
|
weight: 8,
|
|
386
373
|
properties: {
|
|
387
374
|
session_duration_mins: u.weighNumRange(1, 120, 0.3, 8),
|
|
388
|
-
ghosted: [false],
|
|
389
375
|
},
|
|
390
376
|
},
|
|
391
377
|
],
|
|
@@ -430,7 +416,7 @@ const config = {
|
|
|
430
416
|
// ── SuperProps ──────────────────────────────────────────
|
|
431
417
|
superProps: {
|
|
432
418
|
subscription: ["Free", "Free", "Free", "Premium", "Elite"],
|
|
433
|
-
|
|
419
|
+
Platform: ["ios", "ios", "android"],
|
|
434
420
|
},
|
|
435
421
|
|
|
436
422
|
// ── UserProps ──────────────────────────────────────────
|
|
@@ -443,7 +429,7 @@ const config = {
|
|
|
443
429
|
total_matches: u.weighNumRange(0, 200, 0.3, 15),
|
|
444
430
|
total_messages_sent: u.weighNumRange(0, 500, 0.3, 30),
|
|
445
431
|
profile_completeness: ["incomplete", "incomplete", "basic", "basic", "complete"],
|
|
446
|
-
|
|
432
|
+
Platform: ["ios", "ios", "android"],
|
|
447
433
|
},
|
|
448
434
|
|
|
449
435
|
// ── SCD Props ──────────────────────────────────────────
|
|
@@ -461,25 +447,25 @@ const config = {
|
|
|
461
447
|
mirrorProps: {},
|
|
462
448
|
lookupTables: [],
|
|
463
449
|
|
|
464
|
-
// ── Hook Function ──────────────────────────────────────
|
|
465
450
|
hook: function (record, type, meta) {
|
|
466
|
-
const VALENTINES_DAY = DATASET_START.add(60, "days");
|
|
467
|
-
const VDAY_WINDOW_START = DATASET_START.add(58, "days");
|
|
468
|
-
const VDAY_WINDOW_END = DATASET_START.add(63, "days");
|
|
469
|
-
|
|
470
|
-
// ─── EVENT-LEVEL HOOKS ───────────────────────────────────────────
|
|
471
|
-
|
|
472
|
-
if (type === "event") {
|
|
473
|
-
const EVENT_TIME = dayjs(record.time);
|
|
474
451
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (record.
|
|
481
|
-
|
|
482
|
-
|
|
452
|
+
// HOOK 9 (T2C): MATCH FLOW TIME-TO-CONVERT (funnel-post)
|
|
453
|
+
// Elite users complete swipe→match→message funnel 1.4x faster
|
|
454
|
+
// (factor 0.71); Free users 1.4x slower (factor 1.4).
|
|
455
|
+
if (type === "funnel-post") {
|
|
456
|
+
const segment = meta?.profile?.subscription;
|
|
457
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
458
|
+
const factor = (
|
|
459
|
+
segment === "Elite" ? 0.71 :
|
|
460
|
+
segment === "Free" ? 1.4 :
|
|
461
|
+
1.0
|
|
462
|
+
);
|
|
463
|
+
if (factor !== 1.0) {
|
|
464
|
+
for (let i = 1; i < record.length; i++) {
|
|
465
|
+
const prev = dayjs(record[i - 1].time);
|
|
466
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
467
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
468
|
+
}
|
|
483
469
|
}
|
|
484
470
|
}
|
|
485
471
|
}
|
|
@@ -487,25 +473,25 @@ const config = {
|
|
|
487
473
|
// ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
|
|
488
474
|
|
|
489
475
|
if (type === "everything") {
|
|
476
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
477
|
+
const VDAY_WINDOW_START = datasetStart.add(58, "days");
|
|
478
|
+
const VDAY_WINDOW_END = datasetStart.add(63, "days");
|
|
490
479
|
const events = record;
|
|
491
480
|
if (!events || events.length === 0) return record;
|
|
492
481
|
|
|
493
482
|
const profile = meta.profile || {};
|
|
494
483
|
|
|
495
|
-
// Stamp superProps from profile for consistency
|
|
496
484
|
events.forEach(e => {
|
|
497
485
|
if (profile.subscription) e.subscription = profile.subscription;
|
|
498
486
|
if (profile.platform) e.platform = profile.platform;
|
|
499
487
|
});
|
|
500
488
|
|
|
501
|
-
// ── First pass: scan user patterns ──
|
|
502
489
|
let photoUploadCount = 0;
|
|
503
490
|
let promptAnsweredCount = 0;
|
|
504
491
|
let hasBioUpdated = false;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
let superLikeEvents = [];
|
|
492
|
+
const matchEvents = [];
|
|
493
|
+
const messageSentEvents = [];
|
|
494
|
+
const superLikeEvents = [];
|
|
509
495
|
let hasPhoneExchangedEarly = false;
|
|
510
496
|
let hasDateScheduledEarly = false;
|
|
511
497
|
let firstEventTime = null;
|
|
@@ -519,13 +505,9 @@ const config = {
|
|
|
519
505
|
if (event.event === "bio updated") hasBioUpdated = true;
|
|
520
506
|
if (event.event === "match received") matchEvents.push(event);
|
|
521
507
|
if (event.event === "message sent") messageSentEvents.push(event);
|
|
522
|
-
if (event.event === "swipe right" && event.is_super_like === true)
|
|
523
|
-
hasSuperLike = true;
|
|
524
|
-
superLikeEvents.push(event);
|
|
525
|
-
}
|
|
508
|
+
if (event.event === "swipe right" && event.is_super_like === true) superLikeEvents.push(event);
|
|
526
509
|
});
|
|
527
510
|
|
|
528
|
-
// Check early milestones (first 14 days)
|
|
529
511
|
if (firstEventTime) {
|
|
530
512
|
const earlyWindow = dayjs(firstEventTime).add(14, "days");
|
|
531
513
|
events.forEach(event => {
|
|
@@ -537,65 +519,61 @@ const config = {
|
|
|
537
519
|
});
|
|
538
520
|
}
|
|
539
521
|
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
|
|
522
|
+
// HOOK 1 + HOOK 9: PHOTO MAGIC NUMBER (no flags)
|
|
523
|
+
// Sweet 2-5 photos → clone 2-4 extra match events per existing.
|
|
524
|
+
// Over 6+ photos → drop match_score by 35% on match received events
|
|
525
|
+
// (over-curated profile reads as fake/staged).
|
|
526
|
+
if (photoUploadCount >= 2 && photoUploadCount <= 5 && matchEvents.length > 0) {
|
|
543
527
|
const matchTemplate = matchEvents[0];
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
528
|
+
matchEvents.forEach(m => {
|
|
529
|
+
const extras = chance.integer({ min: 2, max: 4 });
|
|
530
|
+
for (let i = 0; i < extras; i++) {
|
|
531
|
+
events.push({
|
|
532
|
+
...matchTemplate,
|
|
533
|
+
time: dayjs(m.time).add(chance.integer({ min: 1, max: 180 }), "minutes").toISOString(),
|
|
534
|
+
user_id: m.user_id,
|
|
535
|
+
match_score: chance.integer({ min: 60, max: 98 }),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
} else if (photoUploadCount >= 6) {
|
|
540
|
+
events.forEach(e => {
|
|
541
|
+
if (e.event === "match received" && typeof e.match_score === "number") {
|
|
542
|
+
e.match_score = Math.max(20, Math.round(e.match_score * 0.65));
|
|
543
|
+
}
|
|
544
|
+
});
|
|
558
545
|
}
|
|
559
546
|
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
events.
|
|
547
|
+
// HOOK 2: WEEKEND SWIPE SURGE — Sunday 18-23 UTC swipes get cloned
|
|
548
|
+
// 1.5x. No flag — discover via day-of-week chart.
|
|
549
|
+
for (let idx = events.length - 1; idx >= 0; idx--) {
|
|
550
|
+
const event = events[idx];
|
|
563
551
|
if (event.event === "swipe right") {
|
|
564
552
|
const dow = new Date(event.time).getUTCDay();
|
|
565
553
|
const hr = new Date(event.time).getUTCHours();
|
|
566
554
|
if (dow === 0 && hr >= 18 && hr <= 23) {
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
// Duplicate weekend_surge=true swipes to create 2.5x volume
|
|
572
|
-
for (let idx = events.length - 1; idx >= 0; idx--) {
|
|
573
|
-
const event = events[idx];
|
|
574
|
-
if (event.event === "swipe right" && event.weekend_surge === true) {
|
|
575
|
-
// Add 1.5 extra copies on average (1 always + 50% chance of second)
|
|
576
|
-
const etime = dayjs(event.time);
|
|
577
|
-
events.push({
|
|
578
|
-
...event,
|
|
579
|
-
time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
|
|
580
|
-
user_id: event.user_id,
|
|
581
|
-
});
|
|
582
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
555
|
+
const etime = dayjs(event.time);
|
|
583
556
|
events.push({
|
|
584
557
|
...event,
|
|
585
|
-
time: etime.add(chance.integer({ min:
|
|
558
|
+
time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
|
|
586
559
|
user_id: event.user_id,
|
|
587
560
|
});
|
|
561
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
562
|
+
events.push({
|
|
563
|
+
...event,
|
|
564
|
+
time: etime.add(chance.integer({ min: 5, max: 60 }), "minutes").toISOString(),
|
|
565
|
+
user_id: event.user_id,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
588
568
|
}
|
|
589
569
|
}
|
|
590
570
|
}
|
|
591
571
|
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
: events.find(e => e.event === "swipe right") || events[0];
|
|
598
|
-
|
|
572
|
+
// HOOK 3: SUPER-LIKE EFFECT — clone 3 extra match events per
|
|
573
|
+
// super-like, near in time. No flag — discover via funnel
|
|
574
|
+
// "swipe right where is_super_like=true" → "match received".
|
|
575
|
+
if (superLikeEvents.length > 0) {
|
|
576
|
+
const matchTemplate = matchEvents[0] || events[0];
|
|
599
577
|
superLikeEvents.forEach(sle => {
|
|
600
578
|
for (let i = 0; i < 3; i++) {
|
|
601
579
|
events.push({
|
|
@@ -604,36 +582,27 @@ const config = {
|
|
|
604
582
|
time: dayjs(sle.time).add(chance.integer({ min: 5, max: 120 }), "minutes").toISOString(),
|
|
605
583
|
user_id: sle.user_id,
|
|
606
584
|
match_score: chance.integer({ min: 70, max: 99 }),
|
|
607
|
-
complete_profile: false,
|
|
608
|
-
super_like_match: true,
|
|
609
|
-
premium_boost: false,
|
|
610
585
|
});
|
|
611
586
|
}
|
|
612
587
|
});
|
|
613
588
|
}
|
|
614
589
|
|
|
615
|
-
//
|
|
616
|
-
//
|
|
590
|
+
// HOOK 4: PREMIUM MATCH BOOST — Premium 2x, Elite 4x match events.
|
|
591
|
+
// Elite users also get profile-viewed events injected (see-who-liked-you).
|
|
592
|
+
// Reads subscription from profile.
|
|
617
593
|
const sub = profile.subscription;
|
|
618
594
|
if ((sub === "Premium" || sub === "Elite") && matchEvents.length > 0) {
|
|
619
|
-
const multiplier = sub === "Elite" ? 3 : 1;
|
|
595
|
+
const multiplier = sub === "Elite" ? 3 : 1;
|
|
620
596
|
const matchTemplate = matchEvents[0];
|
|
621
|
-
|
|
622
597
|
for (let i = 0; i < matchEvents.length * multiplier; i++) {
|
|
623
598
|
const sourceMatch = matchEvents[i % matchEvents.length];
|
|
624
599
|
events.push({
|
|
625
600
|
...matchTemplate,
|
|
626
|
-
event: "match received",
|
|
627
601
|
time: dayjs(sourceMatch.time).add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
|
|
628
602
|
user_id: sourceMatch.user_id,
|
|
629
603
|
match_score: chance.integer({ min: 65, max: 99 }),
|
|
630
|
-
complete_profile: false,
|
|
631
|
-
super_like_match: false,
|
|
632
|
-
premium_boost: true,
|
|
633
604
|
});
|
|
634
605
|
}
|
|
635
|
-
|
|
636
|
-
// Elite exclusive: inject "profile viewed" events (see-who-liked-you)
|
|
637
606
|
if (sub === "Elite") {
|
|
638
607
|
const viewTemplate = events.find(e => e.event === "profile viewed") || matchTemplate;
|
|
639
608
|
matchEvents.forEach(m => {
|
|
@@ -643,49 +612,47 @@ const config = {
|
|
|
643
612
|
time: dayjs(m.time).subtract(chance.integer({ min: 10, max: 120 }), "minutes").toISOString(),
|
|
644
613
|
user_id: m.user_id,
|
|
645
614
|
viewer_source: "liked_you",
|
|
646
|
-
premium_boost: true,
|
|
647
615
|
});
|
|
648
616
|
});
|
|
649
617
|
}
|
|
650
618
|
}
|
|
651
619
|
|
|
652
|
-
//
|
|
653
|
-
//
|
|
620
|
+
// HOOK 6: BIO + PROMPT POWER USERS — bio + 3+ prompts → 3 extra
|
|
621
|
+
// cloned date events per existing. No flag.
|
|
654
622
|
if (hasBioUpdated && promptAnsweredCount >= 3) {
|
|
655
623
|
const dateEvents = events.filter(e => e.event === "date scheduled");
|
|
656
624
|
if (dateEvents.length > 0) {
|
|
657
625
|
const dateTemplate = dateEvents[0];
|
|
658
|
-
const extraDates = dateEvents.length * 3; // +3 = 4x total
|
|
659
626
|
const venueTypes = ["coffee", "dinner", "drinks", "activity", "virtual"];
|
|
660
|
-
for (let i = 0; i <
|
|
627
|
+
for (let i = 0; i < dateEvents.length * 3; i++) {
|
|
661
628
|
const sourceDate = dateEvents[i % dateEvents.length];
|
|
662
629
|
events.push({
|
|
663
630
|
...dateTemplate,
|
|
664
|
-
event: "date scheduled",
|
|
665
631
|
time: dayjs(sourceDate.time).add(chance.integer({ min: 1, max: 72 }), "hours").toISOString(),
|
|
666
632
|
user_id: sourceDate.user_id,
|
|
667
633
|
venue_type: chance.pickone(venueTypes),
|
|
668
|
-
complete_profile_power: true,
|
|
669
634
|
});
|
|
670
635
|
}
|
|
671
636
|
}
|
|
672
637
|
}
|
|
673
638
|
|
|
674
|
-
//
|
|
675
|
-
// 3x
|
|
676
|
-
const vdaySignups = events.filter(e =>
|
|
639
|
+
// HOOK 7: VALENTINE'S DAY SPIKE — clone profile-created events during
|
|
640
|
+
// days 58-63 (3x volume), plus clone premium-upgrade events 5x. No flag.
|
|
641
|
+
const vdaySignups = events.filter(e =>
|
|
642
|
+
e.event === "profile created" &&
|
|
643
|
+
dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
|
|
644
|
+
dayjs(e.time).isBefore(VDAY_WINDOW_END)
|
|
645
|
+
);
|
|
677
646
|
vdaySignups.forEach(signup => {
|
|
678
|
-
for (let i = 0; i < 2; i++) {
|
|
647
|
+
for (let i = 0; i < 2; i++) {
|
|
679
648
|
events.push({
|
|
680
649
|
...signup,
|
|
681
650
|
time: dayjs(signup.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
|
|
682
651
|
user_id: signup.user_id,
|
|
683
|
-
valentines: true,
|
|
684
652
|
});
|
|
685
653
|
}
|
|
686
654
|
});
|
|
687
655
|
|
|
688
|
-
// 5x premium upgrade events for users active during V-Day window
|
|
689
656
|
const vdayUpgrades = events.filter(e =>
|
|
690
657
|
e.event === "premium upgrade" &&
|
|
691
658
|
dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
|
|
@@ -694,74 +661,29 @@ const config = {
|
|
|
694
661
|
if (vdayUpgrades.length > 0) {
|
|
695
662
|
const upgradeTemplate = vdayUpgrades[0];
|
|
696
663
|
vdayUpgrades.forEach(upgrade => {
|
|
697
|
-
for (let i = 0; i < 4; i++) {
|
|
664
|
+
for (let i = 0; i < 4; i++) {
|
|
698
665
|
events.push({
|
|
699
666
|
...upgradeTemplate,
|
|
700
|
-
event: "premium upgrade",
|
|
701
667
|
time: dayjs(upgrade.time).add(chance.integer({ min: 1, max: 24 }), "hours").toISOString(),
|
|
702
668
|
user_id: upgrade.user_id,
|
|
703
669
|
plan: upgrade.plan,
|
|
704
670
|
price_usd: upgrade.price_usd,
|
|
705
|
-
valentines: true,
|
|
706
671
|
});
|
|
707
672
|
}
|
|
708
673
|
});
|
|
709
674
|
}
|
|
710
675
|
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
// Check if any match has a message within 48 hours
|
|
716
|
-
let hasTimely = false;
|
|
717
|
-
for (const m of matchEvents) {
|
|
718
|
-
const matchTime = dayjs(m.time);
|
|
719
|
-
const deadline = matchTime.add(48, "hours");
|
|
720
|
-
for (const msg of messageSentEvents) {
|
|
721
|
-
const msgTime = dayjs(msg.time);
|
|
722
|
-
if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
|
|
723
|
-
hasTimely = true;
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (hasTimely) break;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
if (!hasTimely) {
|
|
731
|
-
isGhoster = true;
|
|
732
|
-
// Find earliest match time as churn reference point
|
|
733
|
-
const earliestMatch = matchEvents.reduce((min, m) =>
|
|
734
|
-
dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
|
|
735
|
-
);
|
|
736
|
-
const churnAfter = dayjs(earliestMatch.time);
|
|
737
|
-
|
|
738
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
739
|
-
const evt = events[i];
|
|
740
|
-
if (dayjs(evt.time).isAfter(churnAfter)) {
|
|
741
|
-
if (chance.bool({ likelihood: 70 })) {
|
|
742
|
-
events.splice(i, 1);
|
|
743
|
-
} else if (evt.event === "app opened") {
|
|
744
|
-
evt.ghosted = true;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// ── HOOK 8: OFF-APP RETENTION ───────────────────
|
|
752
|
-
// Users with phone exchange or date in first 14 days: 60% D30 retention
|
|
753
|
-
// Others: lose events after day 30
|
|
676
|
+
// HOOK 8: OFF-APP RETENTION — users with phone-exchanged or
|
|
677
|
+
// date-scheduled in first 14 days get extra cloned app-open + swipe
|
|
678
|
+
// events past day 30. Non-milestone users lose 80% of post-day-30
|
|
679
|
+
// events. No flag.
|
|
754
680
|
if (firstEventTime) {
|
|
755
681
|
const day30 = dayjs(firstEventTime).add(30, "days");
|
|
756
682
|
const hasEarlyMilestone = hasPhoneExchangedEarly || hasDateScheduledEarly;
|
|
757
|
-
|
|
758
683
|
if (hasEarlyMilestone) {
|
|
759
|
-
// Good retention: inject extra events after day 30 to sustain engagement
|
|
760
684
|
const appOpenedTemplate = events.find(e => e.event === "app opened") || events[0];
|
|
761
685
|
const swipeTemplate = events.find(e => e.event === "swipe right") || events[0];
|
|
762
686
|
const postDay30Events = events.filter(e => dayjs(e.time).isAfter(day30));
|
|
763
|
-
|
|
764
|
-
// If they don't have enough post-day30 events, inject some
|
|
765
687
|
if (postDay30Events.length < events.length * 0.3) {
|
|
766
688
|
const retentionCount = Math.floor(events.length * 0.3);
|
|
767
689
|
for (let i = 0; i < retentionCount; i++) {
|
|
@@ -775,14 +697,38 @@ const config = {
|
|
|
775
697
|
}
|
|
776
698
|
}
|
|
777
699
|
} else {
|
|
778
|
-
// No milestone: churn after day 30
|
|
779
700
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
701
|
+
if (dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: 80 })) {
|
|
702
|
+
events.splice(i, 1);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// HOOK 5: GHOSTING CHURN — users with match but no message within
|
|
709
|
+
// 48hrs lose 80% of post-match events. No flag.
|
|
710
|
+
if (matchEvents.length > 0) {
|
|
711
|
+
let hasTimely = false;
|
|
712
|
+
for (const m of matchEvents) {
|
|
713
|
+
const matchTime = dayjs(m.time);
|
|
714
|
+
const deadline = matchTime.add(48, "hours");
|
|
715
|
+
for (const msg of messageSentEvents) {
|
|
716
|
+
const msgTime = dayjs(msg.time);
|
|
717
|
+
if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
|
|
718
|
+
hasTimely = true;
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (hasTimely) break;
|
|
723
|
+
}
|
|
724
|
+
if (!hasTimely) {
|
|
725
|
+
const earliestMatch = matchEvents.reduce((min, m) =>
|
|
726
|
+
dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
|
|
727
|
+
);
|
|
728
|
+
const churnAfter = dayjs(earliestMatch.time);
|
|
729
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
730
|
+
if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
|
|
731
|
+
events.splice(i, 1);
|
|
786
732
|
}
|
|
787
733
|
}
|
|
788
734
|
}
|