@ak--47/dungeon-master 1.3.0 → 1.4.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/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- 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 +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- 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/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- 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,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-social";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -43,124 +43,231 @@ const chance = u.initChance(SEED);
|
|
|
43
43
|
|
|
44
44
|
/*
|
|
45
45
|
* =====================================================================================
|
|
46
|
-
* ANALYTICS HOOKS
|
|
46
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
47
|
+
*
|
|
48
|
+
* Adds 10. ONBOARDING TIME-TO-CONVERT: creator/business 0.71x faster, personal
|
|
49
|
+
* 1.25x slower (funnel-post). Discover via Onboarding funnel median TTC by account_type.
|
|
50
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
|
|
51
|
+
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
47
52
|
* =====================================================================================
|
|
48
53
|
*
|
|
49
|
-
*
|
|
54
|
+
* NOTE: All cohort effects are HIDDEN — discoverable only via behavioral cohorts
|
|
55
|
+
* (count an event per user, then measure downstream). No cohort flag is stamped
|
|
56
|
+
* on events. Algorithm-change source flips and engagement-bait short durations
|
|
57
|
+
* are raw mutations of config-defined props.
|
|
50
58
|
*
|
|
51
59
|
* -------------------------------------------------------------------------------------
|
|
52
|
-
* 1. VIRAL CONTENT CASCADE (everything
|
|
60
|
+
* 1. VIRAL CONTENT CASCADE (everything)
|
|
53
61
|
* -------------------------------------------------------------------------------------
|
|
54
|
-
* 5% of users with 10+ posts become "viral creators." Each of their posts generates
|
|
55
|
-
* 10-20 extra post viewed, post liked, and post shared events (viral_cascade: true).
|
|
56
62
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
63
|
+
* PATTERN: 5% of users with 10+ "post created" events become viral creators.
|
|
64
|
+
* Each of their posts triggers 10-20 cloned post-viewed, post-liked, and
|
|
65
|
+
* post-shared events with unique offset timestamps.
|
|
66
|
+
*
|
|
67
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
68
|
+
*
|
|
69
|
+
* Report 1: Engagement per Post Created (cohort)
|
|
70
|
+
* - Report type: Insights (with cohort)
|
|
71
|
+
* - Cohort A: users with >= 10 "post created"
|
|
72
|
+
* - Cohort B: users with < 10 "post created"
|
|
73
|
+
* - Event: "post viewed"
|
|
74
|
+
* - Measure: Total per user
|
|
75
|
+
* - Compare A vs B
|
|
76
|
+
* - Expected: cohort A ~ 10-20x more views per user than B
|
|
77
|
+
*
|
|
78
|
+
* Report 2: Likes per User by Post-Created Bucket
|
|
79
|
+
* - Report type: Insights (with cohort)
|
|
80
|
+
* - Same cohorts
|
|
81
|
+
* - Event: "post liked"
|
|
82
|
+
* - Measure: Total per user
|
|
83
|
+
* - Expected: A ~ 10-20x more likes per user than B
|
|
84
|
+
*
|
|
85
|
+
* REAL-WORLD ANALOGUE: A small share of creators drive a disproportionate
|
|
86
|
+
* fraction of all platform engagement.
|
|
60
87
|
*
|
|
61
88
|
* -------------------------------------------------------------------------------------
|
|
62
|
-
* 2. FOLLOW-BACK SNOWBALL (everything
|
|
89
|
+
* 2. FOLLOW-BACK SNOWBALL (everything)
|
|
63
90
|
* -------------------------------------------------------------------------------------
|
|
64
|
-
* Users with 5+ "user followed" events become prolific creators. 50% of their posts
|
|
65
|
-
* get duplicated (follow_back_effect: true), plus extra comments are injected.
|
|
66
91
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
92
|
+
* PATTERN: Users with 5+ "user followed" events have 50% of their posts
|
|
93
|
+
* duplicated with a 30-240 minute offset, plus an extra comment cloned.
|
|
94
|
+
* No flag — discover by binning users on user-followed count.
|
|
95
|
+
*
|
|
96
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
97
|
+
*
|
|
98
|
+
* Report 1: Posts per User by Follow Activity
|
|
99
|
+
* - Report type: Insights (with cohort)
|
|
100
|
+
* - Cohort A: users with >= 5 "user followed"
|
|
101
|
+
* - Cohort B: users with < 5 "user followed"
|
|
102
|
+
* - Event: "post created"
|
|
103
|
+
* - Measure: Total per user
|
|
104
|
+
* - Expected: A ~ 1.5x posts per user vs B
|
|
105
|
+
*
|
|
106
|
+
* REAL-WORLD ANALOGUE: Users who actively follow many people tend to
|
|
107
|
+
* receive follow-backs and post more frequently.
|
|
70
108
|
*
|
|
71
109
|
* -------------------------------------------------------------------------------------
|
|
72
|
-
* 3. ALGORITHM CHANGE (
|
|
110
|
+
* 3. ALGORITHM CHANGE (everything)
|
|
73
111
|
* -------------------------------------------------------------------------------------
|
|
74
|
-
* Day 45: content discovery flips from feed to explore. Before day 45, 70% of
|
|
75
|
-
* post viewed source = "feed." After, 70% shift to source = "explore."
|
|
76
112
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
113
|
+
* PATTERN: On day 45, the dominant `source` for "post viewed" flips from
|
|
114
|
+
* "feed" (70% pre) to "explore" (70% post). Mutates an existing config-
|
|
115
|
+
* defined prop — no flag.
|
|
116
|
+
*
|
|
117
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
118
|
+
*
|
|
119
|
+
* Report 1: Source Distribution Over Time
|
|
120
|
+
* - Report type: Insights
|
|
121
|
+
* - Event: "post viewed"
|
|
122
|
+
* - Measure: Total
|
|
123
|
+
* - Breakdown: "source"
|
|
124
|
+
* - Line chart by day
|
|
125
|
+
* - Expected: feed dominates pre-day-45; explore dominates post-day-45
|
|
126
|
+
*
|
|
127
|
+
* REAL-WORLD ANALOGUE: Algorithmic feed redesigns shift content discovery
|
|
128
|
+
* from chronological to interest-based.
|
|
80
129
|
*
|
|
81
130
|
* -------------------------------------------------------------------------------------
|
|
82
|
-
* 4. ENGAGEMENT BAIT (event
|
|
131
|
+
* 4. ENGAGEMENT BAIT (event)
|
|
83
132
|
* -------------------------------------------------------------------------------------
|
|
84
|
-
* 20% of post viewed events are engagement_bait: true with view durations of 1-5 sec.
|
|
85
|
-
* High impressions but terrible engagement quality.
|
|
86
133
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
134
|
+
* PATTERN: 20% of "post viewed" events get view_duration_sec collapsed
|
|
135
|
+
* to 1-5 seconds. No flag — analyst sees a bimodal duration distribution.
|
|
136
|
+
*
|
|
137
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
138
|
+
*
|
|
139
|
+
* Report 1: View Duration Distribution
|
|
140
|
+
* - Report type: Insights
|
|
141
|
+
* - Event: "post viewed"
|
|
142
|
+
* - Measure: Distribution of "view_duration_sec"
|
|
143
|
+
* - Expected: ~ 20% of values cluster at 1-5 sec; rest at 5-120 sec
|
|
144
|
+
*
|
|
145
|
+
* REAL-WORLD ANALOGUE: Clickbait posts collect impressions but bounce
|
|
146
|
+
* quality is awful, dragging down avg watch time.
|
|
147
|
+
*
|
|
148
|
+
* -------------------------------------------------------------------------------------
|
|
149
|
+
* 5. NOTIFICATION RE-ENGAGEMENT (everything)
|
|
150
|
+
* -------------------------------------------------------------------------------------
|
|
151
|
+
*
|
|
152
|
+
* PATTERN: After day 30, 30% of "post viewed" events have source flipped
|
|
153
|
+
* to "notification". Mutates the existing config-defined `source` prop.
|
|
154
|
+
*
|
|
155
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
156
|
+
*
|
|
157
|
+
* Report 1: Notification-Sourced Views Over Time
|
|
158
|
+
* - Report type: Insights
|
|
159
|
+
* - Event: "post viewed"
|
|
160
|
+
* - Measure: Total
|
|
161
|
+
* - Filter: source = "notification"
|
|
162
|
+
* - Line chart by day
|
|
163
|
+
* - Expected: near-zero before day 30, then ~ 30% of views
|
|
164
|
+
*
|
|
165
|
+
* REAL-WORLD ANALOGUE: Push notifications about trending content are a
|
|
166
|
+
* primary lever for waking up dormant users.
|
|
90
167
|
*
|
|
91
168
|
* -------------------------------------------------------------------------------------
|
|
92
|
-
*
|
|
169
|
+
* 6. CREATOR MONETIZATION (everything)
|
|
93
170
|
* -------------------------------------------------------------------------------------
|
|
94
|
-
* After day 30, 30% of post viewed events get source overridden to "notification"
|
|
95
|
-
* with trending_reengagement: true.
|
|
96
171
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
172
|
+
* PATTERN: Users with any "creator subscription started" event get 2 extra
|
|
173
|
+
* cloned posts and stories per original (3x rate), plus 25% extra cloned
|
|
174
|
+
* post-viewed events from `source="profile"`. No flag — discover via
|
|
175
|
+
* cohort builder.
|
|
176
|
+
*
|
|
177
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
178
|
+
*
|
|
179
|
+
* Report 1: Posts per User — Subscribers vs Not
|
|
180
|
+
* - Report type: Insights (with cohort)
|
|
181
|
+
* - Cohort A: users with >= 1 "creator subscription started"
|
|
182
|
+
* - Cohort B: users with 0
|
|
183
|
+
* - Event: "post created"
|
|
184
|
+
* - Measure: Total per user
|
|
185
|
+
* - Expected: A ~ 3x posts per user
|
|
186
|
+
*
|
|
187
|
+
* REAL-WORLD ANALOGUE: Creators with paying subscribers publish more often.
|
|
100
188
|
*
|
|
101
189
|
* -------------------------------------------------------------------------------------
|
|
102
|
-
*
|
|
190
|
+
* 7. TOXICITY CHURN (everything)
|
|
103
191
|
* -------------------------------------------------------------------------------------
|
|
104
|
-
* Users with any "creator subscription started" event post 3x more. Two extra posts
|
|
105
|
-
* injected per original (monetized_creator: true). Also extra profile-source views.
|
|
106
192
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
193
|
+
* PATTERN: Users with 2+ "report submitted" events lose 60% of activity
|
|
194
|
+
* after day 30. No flag — discover via retention or per-user activity drop.
|
|
195
|
+
*
|
|
196
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
197
|
+
*
|
|
198
|
+
* Report 1: Retention by Toxicity
|
|
199
|
+
* - Report type: Retention
|
|
200
|
+
* - Cohort A: users with >= 2 "report submitted"
|
|
201
|
+
* - Cohort B: rest
|
|
202
|
+
* - Expected: A ~ 40% retention vs B ~ 80%
|
|
203
|
+
*
|
|
204
|
+
* REAL-WORLD ANALOGUE: Repeated reporters are signaling dissatisfaction
|
|
205
|
+
* and often quietly churn.
|
|
110
206
|
*
|
|
111
207
|
* -------------------------------------------------------------------------------------
|
|
112
|
-
*
|
|
208
|
+
* 8. WEEKEND CONTENT SURGE (everything)
|
|
113
209
|
* -------------------------------------------------------------------------------------
|
|
114
|
-
* Users with 3+ reports lose 60% of events after day 30. Remaining events tagged
|
|
115
|
-
* toxic_user: true. Simulates churn from toxic content exposure.
|
|
116
210
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
211
|
+
* PATTERN: 30% of Sat/Sun "post created" and "story created" events get a
|
|
212
|
+
* duplicate cloned 1-3 hours later. No flag — discover via day-of-week chart.
|
|
213
|
+
*
|
|
214
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
215
|
+
*
|
|
216
|
+
* Report 1: Posts by Day of Week
|
|
217
|
+
* - Report type: Insights
|
|
218
|
+
* - Event: "post created"
|
|
219
|
+
* - Measure: Total
|
|
220
|
+
* - Breakdown: Day of week
|
|
221
|
+
* - Expected: Sat/Sun ~ 1.3x weekday bars
|
|
222
|
+
*
|
|
223
|
+
* REAL-WORLD ANALOGUE: Weekend leisure time produces a natural creation surge.
|
|
120
224
|
*
|
|
121
225
|
* -------------------------------------------------------------------------------------
|
|
122
|
-
*
|
|
226
|
+
* 9. POST-CREATED MAGIC NUMBER (everything)
|
|
123
227
|
* -------------------------------------------------------------------------------------
|
|
124
|
-
* Saturday/Sunday post/story events tagged weekend_surge: true. 30% get a duplicate
|
|
125
|
-
* event 1-3 hours later (weekend_duplicate: true).
|
|
126
228
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
229
|
+
* PATTERN: Users in the 3-7 post-created sweet spot get +40% comment_length
|
|
230
|
+
* on their comment-posted events (richer engagement). Users with 8+ posts
|
|
231
|
+
* are over-engaged (burnout); ~30% of their post-liked and comment-posted
|
|
232
|
+
* events are dropped. No flag — discover by binning users on post count.
|
|
130
233
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
234
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
235
|
+
*
|
|
236
|
+
* Report 1: Comment Length by Post Bucket
|
|
237
|
+
* - Report type: Insights (with cohort)
|
|
238
|
+
* - Cohort A: users with 3-7 "post created"
|
|
239
|
+
* - Cohort B: users with 0-2 "post created"
|
|
240
|
+
* - Event: "comment posted"
|
|
241
|
+
* - Measure: Average of "comment_length"
|
|
242
|
+
* - Expected: A ~ 1.4x B
|
|
134
243
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
244
|
+
* Report 2: Engagement Drop on Heavy Posters
|
|
245
|
+
* - Report type: Insights (with cohort)
|
|
246
|
+
* - Cohort C: users with >= 8 "post created"
|
|
247
|
+
* - Cohort A: users with 3-7 "post created"
|
|
248
|
+
* - Event: "post liked" + "comment posted" (combined per user)
|
|
249
|
+
* - Measure: Total per user
|
|
250
|
+
* - Expected: C ~ 30% fewer engagement events per user vs A
|
|
251
|
+
*
|
|
252
|
+
* REAL-WORLD ANALOGUE: Moderate posters write thoughtful comments; the
|
|
253
|
+
* over-prolific tend to spam and burn through audience patience.
|
|
145
254
|
*
|
|
146
255
|
* =====================================================================================
|
|
147
|
-
*
|
|
256
|
+
* EXPECTED METRICS SUMMARY
|
|
148
257
|
* =====================================================================================
|
|
149
258
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* -
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* -
|
|
161
|
-
*
|
|
162
|
-
* - Funnel analysis: onboarding by signup method, engagement by source, creator
|
|
163
|
-
* journey before/after algorithm change.
|
|
259
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
260
|
+
* --------------------------|-------------------------|----------|-------------|------
|
|
261
|
+
* Viral Content Cascade | Engagement per user | 1x | ~ 15x | ~ 15x
|
|
262
|
+
* Follow-Back Snowball | Posts per user | 1x | ~ 1.5x | 1.5x
|
|
263
|
+
* Algorithm Change | feed vs explore (post) | 70/15 | 15/70 | flip
|
|
264
|
+
* Engagement Bait | View duration distrib | unimodal | bimodal | n/a
|
|
265
|
+
* Notification Re-engage | source=notification % | ~ 10% | ~ 30% | 3x
|
|
266
|
+
* Creator Monetization | Content rate (sub vs not)| 1x | ~ 3x | 3x
|
|
267
|
+
* Toxicity Churn | Post-day-30 activity | 1x | ~ 0.4x | -60%
|
|
268
|
+
* Weekend Surge | Weekend vs weekday | 1x | ~ 1.3x | 1.3x
|
|
269
|
+
* Post-Created Magic Number | sweet comment_length | 1x | ~ 1.4x | 1.4x
|
|
270
|
+
* Post-Created Magic Number | over engagement/user | 1x | ~ 0.7x | -30%
|
|
164
271
|
* =====================================================================================
|
|
165
272
|
*/
|
|
166
273
|
|
|
@@ -169,12 +276,17 @@ const postIds = v.range(1, 1001).map(n => `post_${v.uid(8)}`);
|
|
|
169
276
|
|
|
170
277
|
/** @type {Config} */
|
|
171
278
|
const config = {
|
|
279
|
+
version: 2,
|
|
172
280
|
token,
|
|
173
281
|
seed: SEED,
|
|
174
|
-
|
|
282
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
283
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
284
|
+
soup: { dayOfWeekWeights: [1.0, 1.0, 1.0, 1.0, 1.0, 1.2, 1.2] },
|
|
285
|
+
// numDays: num_days,
|
|
175
286
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
176
287
|
numUsers: num_users,
|
|
177
|
-
hasAnonIds:
|
|
288
|
+
hasAnonIds: true,
|
|
289
|
+
avgDevicePerUser: 2,
|
|
178
290
|
hasSessionIds: true,
|
|
179
291
|
format: "json",
|
|
180
292
|
gzip: true,
|
|
@@ -269,10 +381,10 @@ const config = {
|
|
|
269
381
|
event: "account created",
|
|
270
382
|
weight: 1,
|
|
271
383
|
isFirstEvent: true,
|
|
384
|
+
isAuthEvent: true,
|
|
272
385
|
properties: {
|
|
273
386
|
"signup_method": ["email", "google", "apple", "sso"],
|
|
274
387
|
"referred_by": ["organic", "friend", "ad", "influencer"],
|
|
275
|
-
"toxic_user": [false],
|
|
276
388
|
}
|
|
277
389
|
},
|
|
278
390
|
{
|
|
@@ -283,11 +395,6 @@ const config = {
|
|
|
283
395
|
"character_count": u.weighNumRange(1, 280),
|
|
284
396
|
"has_media": [false, false, false, true, true],
|
|
285
397
|
"hashtag_count": u.weighNumRange(0, 10, 0.5),
|
|
286
|
-
"weekend_surge": [false],
|
|
287
|
-
"weekend_duplicate": [false],
|
|
288
|
-
"monetized_creator": [false],
|
|
289
|
-
"follow_back_effect": [false],
|
|
290
|
-
"toxic_user": [false],
|
|
291
398
|
}
|
|
292
399
|
},
|
|
293
400
|
{
|
|
@@ -297,11 +404,6 @@ const config = {
|
|
|
297
404
|
"post_type": ["text", "image", "video", "poll", "link"],
|
|
298
405
|
"view_duration_sec": u.weighNumRange(1, 120, 0.3, 5),
|
|
299
406
|
"source": ["feed", "explore", "search", "profile", "notification"],
|
|
300
|
-
"engagement_bait": [false],
|
|
301
|
-
"trending_reengagement": [false],
|
|
302
|
-
"viral_cascade": [false],
|
|
303
|
-
"monetized_creator": [false],
|
|
304
|
-
"toxic_user": [false],
|
|
305
407
|
}
|
|
306
408
|
},
|
|
307
409
|
{
|
|
@@ -309,8 +411,6 @@ const config = {
|
|
|
309
411
|
weight: 18,
|
|
310
412
|
properties: {
|
|
311
413
|
"post_type": ["text", "image", "video", "poll", "link"],
|
|
312
|
-
"viral_cascade": [false],
|
|
313
|
-
"toxic_user": [false],
|
|
314
414
|
}
|
|
315
415
|
},
|
|
316
416
|
{
|
|
@@ -318,8 +418,6 @@ const config = {
|
|
|
318
418
|
weight: 6,
|
|
319
419
|
properties: {
|
|
320
420
|
"share_destination": ["repost", "dm", "external", "copy_link"],
|
|
321
|
-
"viral_cascade": [false],
|
|
322
|
-
"toxic_user": [false],
|
|
323
421
|
}
|
|
324
422
|
},
|
|
325
423
|
{
|
|
@@ -328,8 +426,6 @@ const config = {
|
|
|
328
426
|
properties: {
|
|
329
427
|
"comment_length": u.weighNumRange(1, 500, 0.3, 20),
|
|
330
428
|
"has_mention": [true, false, false],
|
|
331
|
-
"follow_back_effect": [false],
|
|
332
|
-
"toxic_user": [false],
|
|
333
429
|
}
|
|
334
430
|
},
|
|
335
431
|
{
|
|
@@ -337,7 +433,6 @@ const config = {
|
|
|
337
433
|
weight: 8,
|
|
338
434
|
properties: {
|
|
339
435
|
"discovery_source": ["suggested", "search", "post", "profile", "mutual"],
|
|
340
|
-
"toxic_user": [false],
|
|
341
436
|
}
|
|
342
437
|
},
|
|
343
438
|
{
|
|
@@ -345,7 +440,6 @@ const config = {
|
|
|
345
440
|
weight: 2,
|
|
346
441
|
properties: {
|
|
347
442
|
"reason": ["content_quality", "too_frequent", "lost_interest", "offensive"],
|
|
348
|
-
"toxic_user": [false],
|
|
349
443
|
}
|
|
350
444
|
},
|
|
351
445
|
{
|
|
@@ -355,7 +449,6 @@ const config = {
|
|
|
355
449
|
"story_type": ["photo", "video", "text"],
|
|
356
450
|
"view_duration_sec": u.weighNumRange(1, 30, 0.5, 5),
|
|
357
451
|
"completed": [false, false, true, true, true],
|
|
358
|
-
"toxic_user": [false],
|
|
359
452
|
}
|
|
360
453
|
},
|
|
361
454
|
{
|
|
@@ -365,10 +458,6 @@ const config = {
|
|
|
365
458
|
"story_type": ["photo", "video", "text"],
|
|
366
459
|
"has_filter": [true, false],
|
|
367
460
|
"has_sticker": [false, false, true],
|
|
368
|
-
"weekend_surge": [false],
|
|
369
|
-
"weekend_duplicate": [false],
|
|
370
|
-
"monetized_creator": [false],
|
|
371
|
-
"toxic_user": [false],
|
|
372
461
|
}
|
|
373
462
|
},
|
|
374
463
|
{
|
|
@@ -377,7 +466,6 @@ const config = {
|
|
|
377
466
|
properties: {
|
|
378
467
|
"search_type": ["users", "hashtags", "posts"],
|
|
379
468
|
"results_count": u.weighNumRange(0, 50, 0.5, 10),
|
|
380
|
-
"toxic_user": [false],
|
|
381
469
|
}
|
|
382
470
|
},
|
|
383
471
|
{
|
|
@@ -386,7 +474,6 @@ const config = {
|
|
|
386
474
|
properties: {
|
|
387
475
|
"notification_type": ["like", "follow", "comment", "mention", "trending"],
|
|
388
476
|
"clicked": [false, false, false, true, true],
|
|
389
|
-
"toxic_user": [false],
|
|
390
477
|
}
|
|
391
478
|
},
|
|
392
479
|
{
|
|
@@ -395,7 +482,6 @@ const config = {
|
|
|
395
482
|
properties: {
|
|
396
483
|
"message_type": ["text", "image", "voice", "link"],
|
|
397
484
|
"conversation_length": u.weighNumRange(1, 100),
|
|
398
|
-
"toxic_user": [false],
|
|
399
485
|
}
|
|
400
486
|
},
|
|
401
487
|
{
|
|
@@ -405,7 +491,6 @@ const config = {
|
|
|
405
491
|
"ad_format": ["feed_native", "story", "banner", "video"],
|
|
406
492
|
"ad_category": ["retail", "tech", "food", "finance", "entertainment"],
|
|
407
493
|
"view_duration_sec": u.weighNumRange(1, 30, 0.3),
|
|
408
|
-
"toxic_user": [false],
|
|
409
494
|
}
|
|
410
495
|
},
|
|
411
496
|
{
|
|
@@ -414,7 +499,6 @@ const config = {
|
|
|
414
499
|
properties: {
|
|
415
500
|
"ad_format": ["feed_native", "story", "banner", "video"],
|
|
416
501
|
"ad_category": ["retail", "tech", "food", "finance", "entertainment"],
|
|
417
|
-
"toxic_user": [false],
|
|
418
502
|
}
|
|
419
503
|
},
|
|
420
504
|
{
|
|
@@ -423,7 +507,6 @@ const config = {
|
|
|
423
507
|
properties: {
|
|
424
508
|
"report_type": ["spam", "harassment", "misinformation", "hate_speech", "other"],
|
|
425
509
|
"content_type": ["post", "comment", "user", "dm"],
|
|
426
|
-
"toxic_user": [false],
|
|
427
510
|
}
|
|
428
511
|
},
|
|
429
512
|
{
|
|
@@ -431,7 +514,6 @@ const config = {
|
|
|
431
514
|
weight: 3,
|
|
432
515
|
properties: {
|
|
433
516
|
"field_updated": ["bio", "avatar", "display_name", "privacy_settings", "interests"],
|
|
434
|
-
"toxic_user": [false],
|
|
435
517
|
}
|
|
436
518
|
},
|
|
437
519
|
{
|
|
@@ -440,7 +522,6 @@ const config = {
|
|
|
440
522
|
properties: {
|
|
441
523
|
"tier": ["basic", "premium", "vip"],
|
|
442
524
|
"price_usd": [4.99, 9.99, 19.99],
|
|
443
|
-
"toxic_user": [false],
|
|
444
525
|
}
|
|
445
526
|
},
|
|
446
527
|
],
|
|
@@ -476,56 +557,32 @@ const config = {
|
|
|
476
557
|
lookupTables: [],
|
|
477
558
|
|
|
478
559
|
hook: function (record, type, meta) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
record.
|
|
560
|
+
// Hook #10 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
|
|
561
|
+
// Creator/business account_type users complete onboarding 1.4x
|
|
562
|
+
// faster (factor 0.71); personal accounts 1.25x slower (factor 1.25).
|
|
563
|
+
if (type === "funnel-post") {
|
|
564
|
+
const segment = meta?.profile?.account_type;
|
|
565
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
566
|
+
const factor = (
|
|
567
|
+
segment === "creator" || segment === "business" ? 0.71 :
|
|
568
|
+
segment === "personal" ? 1.25 :
|
|
569
|
+
1.0
|
|
570
|
+
);
|
|
571
|
+
if (factor !== 1.0) {
|
|
572
|
+
for (let i = 1; i < record.length; i++) {
|
|
573
|
+
const prev = dayjs(record[i - 1].time);
|
|
574
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
575
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
495
576
|
}
|
|
496
|
-
} else {
|
|
497
|
-
// Before day 45: 70% feed, 15% explore (reinforce default)
|
|
498
|
-
if (chance.bool({ likelihood: 70 })) {
|
|
499
|
-
record.source = "feed";
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Hook #4: ENGAGEMENT BAIT - High hashtag posts get short view durations
|
|
505
|
-
if (record.event === "post viewed") {
|
|
506
|
-
// 20% of post views are engagement-bait content
|
|
507
|
-
if (chance.bool({ likelihood: 20 })) {
|
|
508
|
-
record.view_duration_sec = chance.integer({ min: 1, max: 5 });
|
|
509
|
-
record.engagement_bait = true;
|
|
510
|
-
} else {
|
|
511
|
-
record.engagement_bait = false;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Hook #5: NOTIFICATION RE-ENGAGEMENT - Trending drives views after day 30
|
|
515
|
-
if (EVENT_TIME.isAfter(REENGAGEMENT_START) && chance.bool({ likelihood: 30 })) {
|
|
516
|
-
record.source = "notification";
|
|
517
|
-
record.trending_reengagement = true;
|
|
518
|
-
} else {
|
|
519
|
-
record.trending_reengagement = false;
|
|
520
577
|
}
|
|
521
578
|
}
|
|
522
|
-
|
|
523
|
-
// Hook #8: WEEKEND CONTENT SURGE - tagging moved to everything hook (sessionization reassigns times after event hook)
|
|
524
579
|
}
|
|
525
580
|
|
|
581
|
+
|
|
526
582
|
// ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
|
|
527
583
|
|
|
528
584
|
if (type === "everything") {
|
|
585
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
529
586
|
const userEvents = record;
|
|
530
587
|
if (!userEvents || userEvents.length === 0) return record;
|
|
531
588
|
|
|
@@ -536,56 +593,35 @@ const config = {
|
|
|
536
593
|
e.account_type = profile.account_type;
|
|
537
594
|
});
|
|
538
595
|
|
|
539
|
-
//
|
|
596
|
+
// First pass: identify behavioral patterns (no flags written)
|
|
540
597
|
let postCreatedCount = 0;
|
|
541
598
|
let followReceivedCount = 0;
|
|
542
599
|
let reportSubmittedCount = 0;
|
|
543
600
|
let hasCreatorSubscription = false;
|
|
544
601
|
let isViralCreator = false;
|
|
545
602
|
|
|
546
|
-
// First pass: identify user patterns
|
|
547
603
|
userEvents.forEach((event) => {
|
|
548
|
-
if (event.event === "post created")
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (event.event === "
|
|
552
|
-
followReceivedCount++;
|
|
553
|
-
}
|
|
554
|
-
if (event.event === "report submitted") {
|
|
555
|
-
reportSubmittedCount++;
|
|
556
|
-
}
|
|
557
|
-
if (event.event === "creator subscription started") {
|
|
558
|
-
hasCreatorSubscription = true;
|
|
559
|
-
}
|
|
604
|
+
if (event.event === "post created") postCreatedCount++;
|
|
605
|
+
if (event.event === "user followed") followReceivedCount++;
|
|
606
|
+
if (event.event === "report submitted") reportSubmittedCount++;
|
|
607
|
+
if (event.event === "creator subscription started") hasCreatorSubscription = true;
|
|
560
608
|
});
|
|
561
609
|
|
|
562
|
-
// Hook #1: VIRAL CONTENT CASCADE
|
|
563
|
-
// Users with 10+ posts and 5% random chance are viral creators
|
|
564
610
|
if (postCreatedCount >= 10 && chance.bool({ likelihood: 5 })) {
|
|
565
611
|
isViralCreator = true;
|
|
566
612
|
}
|
|
567
613
|
|
|
568
|
-
// Second pass:
|
|
614
|
+
// Second pass: inject cloned events (no behavioral cohort flags)
|
|
569
615
|
for (let idx = userEvents.length - 1; idx >= 0; idx--) {
|
|
570
616
|
const event = userEvents[idx];
|
|
571
617
|
const eventTime = dayjs(event.time);
|
|
572
618
|
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
if (event.monetized_creator === undefined) event.monetized_creator = false;
|
|
576
|
-
if (event.follow_back_effect === undefined) event.follow_back_effect = false;
|
|
577
|
-
}
|
|
578
|
-
if (event.event === "post viewed") {
|
|
579
|
-
if (event.viral_cascade === undefined) event.viral_cascade = false;
|
|
580
|
-
}
|
|
581
|
-
if (event.toxic_user === undefined) event.toxic_user = false;
|
|
582
|
-
|
|
583
|
-
// Hook #1: VIRAL CONTENT CASCADE
|
|
584
|
-
// Viral creators get 10-20x engagement on their posts
|
|
619
|
+
// Hook #1: VIRAL CONTENT CASCADE — clone 10-20 view/like/share per post.
|
|
620
|
+
// Discovery: bin users by post-created count, observe per-user view/like/share volume.
|
|
585
621
|
if (isViralCreator && event.event === "post created") {
|
|
586
|
-
const viralViews = chance.integer({ min:
|
|
587
|
-
const viralLikes = chance.integer({ min:
|
|
588
|
-
const viralShares = chance.integer({ min:
|
|
622
|
+
const viralViews = chance.integer({ min: 60, max: 120 });
|
|
623
|
+
const viralLikes = chance.integer({ min: 60, max: 120 });
|
|
624
|
+
const viralShares = chance.integer({ min: 60, max: 120 });
|
|
589
625
|
const injected = [];
|
|
590
626
|
|
|
591
627
|
const viewTemplate = userEvents.find(e => e.event === "post viewed");
|
|
@@ -601,9 +637,6 @@ const config = {
|
|
|
601
637
|
post_type: event.post_type || "text",
|
|
602
638
|
source: chance.pickone(["feed", "explore", "search"]),
|
|
603
639
|
view_duration_sec: chance.integer({ min: 5, max: 90 }),
|
|
604
|
-
viral_cascade: true,
|
|
605
|
-
engagement_bait: false,
|
|
606
|
-
trending_reengagement: false,
|
|
607
640
|
});
|
|
608
641
|
}
|
|
609
642
|
for (let i = 0; i < viralLikes; i++) {
|
|
@@ -613,7 +646,6 @@ const config = {
|
|
|
613
646
|
time: eventTime.add(chance.integer({ min: 2, max: 240 }), 'minutes').toISOString(),
|
|
614
647
|
user_id: event.user_id,
|
|
615
648
|
post_type: event.post_type || "text",
|
|
616
|
-
viral_cascade: true,
|
|
617
649
|
});
|
|
618
650
|
}
|
|
619
651
|
for (let i = 0; i < viralShares; i++) {
|
|
@@ -623,16 +655,14 @@ const config = {
|
|
|
623
655
|
time: eventTime.add(chance.integer({ min: 5, max: 300 }), 'minutes').toISOString(),
|
|
624
656
|
user_id: event.user_id,
|
|
625
657
|
share_destination: chance.pickone(["repost", "dm", "external", "copy_link"]),
|
|
626
|
-
viral_cascade: true,
|
|
627
658
|
});
|
|
628
659
|
}
|
|
629
660
|
|
|
630
|
-
// Splice all injected events after the post created event
|
|
631
661
|
userEvents.splice(idx + 1, 0, ...injected);
|
|
632
662
|
}
|
|
633
663
|
|
|
634
|
-
// Hook #2: FOLLOW-BACK SNOWBALL
|
|
635
|
-
//
|
|
664
|
+
// Hook #2: FOLLOW-BACK SNOWBALL — extra post + comment per user-followed cluster.
|
|
665
|
+
// Discovery: cohort users with >=5 user-followed events, compare posts/user.
|
|
636
666
|
if (followReceivedCount >= 5 && event.event === "post created") {
|
|
637
667
|
if (chance.bool({ likelihood: 50 })) {
|
|
638
668
|
const commentTemplate = userEvents.find(e => e.event === "comment posted");
|
|
@@ -644,7 +674,6 @@ const config = {
|
|
|
644
674
|
character_count: chance.integer({ min: 10, max: 280 }),
|
|
645
675
|
has_media: chance.bool({ likelihood: 60 }),
|
|
646
676
|
hashtag_count: chance.integer({ min: 0, max: 5 }),
|
|
647
|
-
follow_back_effect: true,
|
|
648
677
|
};
|
|
649
678
|
const extraComment = {
|
|
650
679
|
...(commentTemplate || event),
|
|
@@ -653,16 +682,14 @@ const config = {
|
|
|
653
682
|
user_id: event.user_id,
|
|
654
683
|
comment_length: chance.integer({ min: 5, max: 200 }),
|
|
655
684
|
has_mention: chance.bool({ likelihood: 40 }),
|
|
656
|
-
follow_back_effect: true,
|
|
657
685
|
};
|
|
658
686
|
userEvents.splice(idx + 1, 0, duplicatePost, extraComment);
|
|
659
687
|
}
|
|
660
688
|
}
|
|
661
689
|
|
|
662
|
-
// Hook #6: CREATOR MONETIZATION
|
|
663
|
-
//
|
|
690
|
+
// Hook #6: CREATOR MONETIZATION — 3x post/story rate for subscribers.
|
|
691
|
+
// Discovery: cohort users with creator-subscription-started event, compare posts/user.
|
|
664
692
|
if (hasCreatorSubscription && event.event === "post created") {
|
|
665
|
-
// Triple frequency: add 2 extra posts for each existing one
|
|
666
693
|
for (let i = 0; i < 2; i++) {
|
|
667
694
|
const extraPost = {
|
|
668
695
|
...event,
|
|
@@ -672,13 +699,11 @@ const config = {
|
|
|
672
699
|
character_count: chance.integer({ min: 20, max: 280 }),
|
|
673
700
|
has_media: chance.bool({ likelihood: 70 }),
|
|
674
701
|
hashtag_count: chance.integer({ min: 1, max: 8 }),
|
|
675
|
-
monetized_creator: true,
|
|
676
702
|
};
|
|
677
703
|
userEvents.splice(idx + 1, 0, extraPost);
|
|
678
704
|
}
|
|
679
705
|
}
|
|
680
706
|
if (hasCreatorSubscription && event.event === "story created") {
|
|
681
|
-
// Also triple story creation
|
|
682
707
|
for (let i = 0; i < 2; i++) {
|
|
683
708
|
const extraStory = {
|
|
684
709
|
...event,
|
|
@@ -687,12 +712,10 @@ const config = {
|
|
|
687
712
|
story_type: chance.pickone(["photo", "video", "text"]),
|
|
688
713
|
has_filter: chance.bool({ likelihood: 60 }),
|
|
689
714
|
has_sticker: chance.bool({ likelihood: 40 }),
|
|
690
|
-
monetized_creator: true,
|
|
691
715
|
};
|
|
692
716
|
userEvents.splice(idx + 1, 0, extraStory);
|
|
693
717
|
}
|
|
694
718
|
}
|
|
695
|
-
// Monetized creators also check their analytics more (extra post views)
|
|
696
719
|
if (hasCreatorSubscription && event.event === "post viewed") {
|
|
697
720
|
if (chance.bool({ likelihood: 25 })) {
|
|
698
721
|
const analyticsView = {
|
|
@@ -702,58 +725,90 @@ const config = {
|
|
|
702
725
|
post_type: event.post_type || "text",
|
|
703
726
|
source: "profile",
|
|
704
727
|
view_duration_sec: chance.integer({ min: 10, max: 60 }),
|
|
705
|
-
monetized_creator: true,
|
|
706
|
-
engagement_bait: false,
|
|
707
|
-
trending_reengagement: false,
|
|
708
|
-
viral_cascade: false,
|
|
709
728
|
};
|
|
710
729
|
userEvents.splice(idx + 1, 0, analyticsView);
|
|
711
730
|
}
|
|
712
731
|
}
|
|
713
732
|
}
|
|
714
733
|
|
|
715
|
-
// Hook #8: WEEKEND CONTENT SURGE
|
|
716
|
-
|
|
717
|
-
if (event.event === "post created" || event.event === "story created") {
|
|
718
|
-
const dow = new Date(event.time).getUTCDay(); // 0 = Sunday, 6 = Saturday
|
|
719
|
-
if (dow === 0 || dow === 6) {
|
|
720
|
-
event.weekend_surge = true;
|
|
721
|
-
} else {
|
|
722
|
-
event.weekend_surge = false;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
// Inject duplicate events for weekend content
|
|
734
|
+
// Hook #8: WEEKEND CONTENT SURGE — duplicate weekend posts/stories with offset.
|
|
735
|
+
// Discovery: line chart by day-of-week shows Sat/Sun bump.
|
|
727
736
|
for (let idx = userEvents.length - 1; idx >= 0; idx--) {
|
|
728
737
|
const event = userEvents[idx];
|
|
729
|
-
if (event.
|
|
730
|
-
|
|
738
|
+
if (event.event === "post created" || event.event === "story created") {
|
|
739
|
+
const dow = new Date(event.time).getUTCDay();
|
|
740
|
+
if ((dow === 0 || dow === 6) && chance.bool({ likelihood: 30 })) {
|
|
731
741
|
const etime = dayjs(event.time);
|
|
732
742
|
const dup = {
|
|
733
743
|
...event,
|
|
734
744
|
time: etime.add(chance.integer({ min: 1, max: 3 }), 'hours').toISOString(),
|
|
735
|
-
weekend_duplicate: true,
|
|
736
745
|
};
|
|
737
746
|
userEvents.splice(idx + 1, 0, dup);
|
|
738
747
|
}
|
|
739
748
|
}
|
|
740
749
|
}
|
|
741
750
|
|
|
742
|
-
// Hook #
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
751
|
+
// Hook #3: ALGORITHM CHANGE — day 45 flips feed → explore on post viewed.
|
|
752
|
+
// Hook #4: ENGAGEMENT BAIT — 20% of post-viewed events get crushed duration.
|
|
753
|
+
// Hook #5: NOTIFICATION RE-ENGAGEMENT — after day 30, 30% of views → notification.
|
|
754
|
+
// All three run AFTER injection passes so they apply to cloned events too.
|
|
755
|
+
const ALGORITHM_CHANGE_DAY = datasetStart.add(45, 'days');
|
|
756
|
+
const REENGAGEMENT_START = datasetStart.add(30, 'days');
|
|
757
|
+
userEvents.forEach(e => {
|
|
758
|
+
if (e.event === "post viewed") {
|
|
759
|
+
const eventTime = dayjs(e.time);
|
|
760
|
+
|
|
761
|
+
// Hook #3: Algorithm Change
|
|
762
|
+
if (eventTime.isAfter(ALGORITHM_CHANGE_DAY)) {
|
|
763
|
+
if (chance.bool({ likelihood: 70 })) {
|
|
764
|
+
e.source = "explore";
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
if (chance.bool({ likelihood: 70 })) {
|
|
768
|
+
e.source = "feed";
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Hook #4: Engagement Bait — 20% crushed view duration
|
|
773
|
+
if (chance.bool({ likelihood: 20 })) {
|
|
774
|
+
e.view_duration_sec = chance.integer({ min: 1, max: 5 });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Hook #5: Notification Re-engagement (runs after #3 so can override)
|
|
778
|
+
if (eventTime.isAfter(REENGAGEMENT_START) && chance.bool({ likelihood: 30 })) {
|
|
779
|
+
e.source = "notification";
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Hook #7: TOXICITY CHURN — drop 60% of activity after day 30 for high reporters.
|
|
785
|
+
// Discovery: cohort users with >=2 report-submitted events, observe retention drop.
|
|
786
|
+
if (reportSubmittedCount >= 2) {
|
|
787
|
+
const churnCutoff = datasetStart.add(30, 'days');
|
|
746
788
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
747
789
|
const evt = userEvents[i];
|
|
748
|
-
if (dayjs(evt.time).isAfter(churnCutoff)) {
|
|
749
|
-
|
|
750
|
-
userEvents.splice(i, 1);
|
|
751
|
-
} else {
|
|
752
|
-
evt.toxic_user = true;
|
|
753
|
-
}
|
|
790
|
+
if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 60 })) {
|
|
791
|
+
userEvents.splice(i, 1);
|
|
754
792
|
}
|
|
755
793
|
}
|
|
756
794
|
}
|
|
795
|
+
|
|
796
|
+
// Hook #9: POST-CREATED MAGIC NUMBER (no flags)
|
|
797
|
+
// Sweet 3-7 posts → +40% on comment_length on the user's comment events.
|
|
798
|
+
// Over 8+ → drop 30% of like/comment events (engagement burnout).
|
|
799
|
+
if (postCreatedCount >= 3 && postCreatedCount <= 7) {
|
|
800
|
+
userEvents.forEach(e => {
|
|
801
|
+
if (e.event === 'comment posted' && typeof e.comment_length === 'number') {
|
|
802
|
+
e.comment_length = Math.round(e.comment_length * 1.4);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
} else if (postCreatedCount >= 8) {
|
|
806
|
+
userEvents.forEach(e => {
|
|
807
|
+
if (e.event === 'comment posted' && typeof e.comment_length === 'number') {
|
|
808
|
+
e.comment_length = Math.round(e.comment_length * 0.7);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
757
812
|
}
|
|
758
813
|
|
|
759
814
|
return record;
|