@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
|
@@ -44,179 +44,225 @@ const chance = u.initChance(SEED);
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
47
|
-
* ANALYTICS HOOKS (
|
|
47
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
48
|
+
*
|
|
49
|
+
* Adds 10. CORE VIEWING LOOP TIME-TO-CONVERT: premium 0.71x faster, free 1.25x
|
|
50
|
+
* slower (funnel-post). Discover via funnel median TTC by subscription_plan.
|
|
51
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
|
|
52
|
+
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
48
53
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
49
54
|
*
|
|
55
|
+
* NOTE: All cohort effects are HIDDEN — discoverable only via behavioral cohorts
|
|
56
|
+
* (count an event per user, then measure downstream). No cohort flag is stamped
|
|
57
|
+
* on events. Time-window patterns mutate config-defined props or drop events.
|
|
58
|
+
*
|
|
50
59
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
51
|
-
* 1. GENRE FUNNEL CONVERSION (
|
|
60
|
+
* 1. GENRE FUNNEL CONVERSION (everything)
|
|
52
61
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
53
62
|
*
|
|
54
|
-
* PATTERN:
|
|
55
|
-
*
|
|
63
|
+
* PATTERN: 25% of "playback completed" events on documentary genre are dropped,
|
|
64
|
+
* depressing the documentary funnel completion rate. No flag — discover via
|
|
65
|
+
* funnel breakdown by genre.
|
|
66
|
+
*
|
|
67
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
56
68
|
*
|
|
57
|
-
*
|
|
58
|
-
* -
|
|
69
|
+
* Report 1: Discovery Funnel by Genre
|
|
70
|
+
* - Report type: Funnels
|
|
71
|
+
* - Steps: "content browsed" -> "content selected" -> "playback started" -> "playback completed"
|
|
59
72
|
* - Breakdown: "genre"
|
|
60
|
-
* - Expected:
|
|
61
|
-
*
|
|
73
|
+
* - Expected: documentary ~ 0.7x conversion vs other genres
|
|
74
|
+
*
|
|
75
|
+
* REAL-WORLD ANALOGUE: Heavier content gets started but abandoned more often
|
|
76
|
+
* than light comedy or animation.
|
|
62
77
|
*
|
|
63
78
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
64
79
|
* 2. BINGE-WATCHING PATTERN (everything)
|
|
65
80
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
66
81
|
*
|
|
67
|
-
* PATTERN: Users
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
82
|
+
* PATTERN: Users with 3+ consecutive playback-completed events get extra
|
|
83
|
+
* cloned playback-started + playback-completed pairs (with unique offset
|
|
84
|
+
* timestamps). 60% of their pause events are dropped. No flag — discover by
|
|
85
|
+
* binning users on completion-streak length and comparing per-user completions.
|
|
86
|
+
*
|
|
87
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
72
88
|
*
|
|
73
|
-
*
|
|
74
|
-
* -
|
|
75
|
-
* -
|
|
76
|
-
* -
|
|
89
|
+
* Report 1: Completions per User by Streak Cohort
|
|
90
|
+
* - Report type: Insights (with cohort)
|
|
91
|
+
* - Cohort A: users with >= 3 consecutive "playback completed"
|
|
92
|
+
* - Cohort B: rest
|
|
93
|
+
* - Event: "playback completed"
|
|
94
|
+
* - Measure: Total per user
|
|
95
|
+
* - Expected: A ~ 1.5x more completions per user
|
|
96
|
+
*
|
|
97
|
+
* REAL-WORLD ANALOGUE: Autoplay and cliffhangers push hooked viewers through
|
|
98
|
+
* entire seasons in a sitting.
|
|
77
99
|
*
|
|
78
100
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
79
|
-
* 3. WEEKEND
|
|
101
|
+
* 3. WEEKEND VS WEEKDAY PATTERNS (everything)
|
|
80
102
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
81
103
|
*
|
|
82
|
-
* PATTERN:
|
|
83
|
-
*
|
|
104
|
+
* PATTERN: Saturday/Sunday "playback completed" events get watch_duration_min
|
|
105
|
+
* boosted 1.5x. Mutates the existing watch_duration_min prop. No flag —
|
|
106
|
+
* discover via day-of-week breakdown.
|
|
107
|
+
*
|
|
108
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
84
109
|
*
|
|
85
|
-
*
|
|
86
|
-
* -
|
|
87
|
-
* -
|
|
88
|
-
* -
|
|
110
|
+
* Report 1: Avg Watch Duration by Day of Week
|
|
111
|
+
* - Report type: Insights
|
|
112
|
+
* - Event: "playback completed"
|
|
113
|
+
* - Measure: Average of "watch_duration_min"
|
|
114
|
+
* - Breakdown: Day of week
|
|
115
|
+
* - Expected: Sat/Sun ~ 1.5x weekday avg
|
|
116
|
+
*
|
|
117
|
+
* REAL-WORLD ANALOGUE: Weekend viewing stretches into multi-hour marathons.
|
|
89
118
|
*
|
|
90
119
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
91
120
|
* 4. AD FATIGUE CHURN (everything)
|
|
92
121
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
93
122
|
*
|
|
94
|
-
* PATTERN: Free-tier users
|
|
95
|
-
*
|
|
123
|
+
* PATTERN: Free-tier users with 10+ ad impressions lose 50% of events after
|
|
124
|
+
* day 45 of their lifecycle. No flag — discover via cohort retention.
|
|
125
|
+
*
|
|
126
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
96
127
|
*
|
|
97
|
-
*
|
|
98
|
-
* -
|
|
99
|
-
* -
|
|
100
|
-
* -
|
|
128
|
+
* Report 1: Retention by Ad Exposure Cohort
|
|
129
|
+
* - Report type: Retention
|
|
130
|
+
* - Cohort A: free-tier users with >= 10 "ad impression"
|
|
131
|
+
* - Cohort B: free-tier users with < 10 ads
|
|
132
|
+
* - Expected: A ~ 50% retention drop after day 45
|
|
133
|
+
*
|
|
134
|
+
* REAL-WORLD ANALOGUE: Ad-supported tiers carry a tolerance ceiling.
|
|
101
135
|
*
|
|
102
136
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
103
137
|
* 5. NEW RELEASE SPIKE (event)
|
|
104
138
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
105
139
|
*
|
|
106
|
-
* PATTERN:
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
140
|
+
* PATTERN: Days 50-65, 20% of "content selected" / "playback started" events
|
|
141
|
+
* have content_id swapped to the blockbuster id and content_type to "movie".
|
|
142
|
+
* 20% of "content rated" in the window get a 4-5 star rating on the blockbuster.
|
|
143
|
+
* Mutates existing props — no flag.
|
|
144
|
+
*
|
|
145
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
146
|
+
*
|
|
147
|
+
* Report 1: Selections by content_id Over Time
|
|
148
|
+
* - Report type: Insights
|
|
149
|
+
* - Event: "content selected"
|
|
150
|
+
* - Measure: Total
|
|
151
|
+
* - Filter: content_id = "<blockbuster id>"
|
|
152
|
+
* - Line chart by day
|
|
153
|
+
* - Expected: zero before day 50, ~ 20% of selections days 50-65
|
|
110
154
|
*
|
|
111
|
-
*
|
|
112
|
-
* - Insights line chart: "content selected", filter "blockbuster_release" = true, daily trend
|
|
113
|
-
* - Expected: Zero before day 50, then ~20% of content selections
|
|
114
|
-
* - Also try: "content rated" average of "rating" by "blockbuster_release" (4-5 vs ~3.5)
|
|
155
|
+
* REAL-WORLD ANALOGUE: Tentpole releases dominate traffic.
|
|
115
156
|
*
|
|
116
157
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
117
158
|
* 6. KIDS PROFILE SAFETY (event)
|
|
118
159
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
119
160
|
*
|
|
120
|
-
* PATTERN: 15% of
|
|
121
|
-
*
|
|
122
|
-
* - Ad impressions are blocked (ad_blocked = true)
|
|
123
|
-
* - Events tagged with kids_profile = true
|
|
161
|
+
* PATTERN: 15% of "content selected" / "playback started" events get genre
|
|
162
|
+
* restricted to "animation" or "documentary". Mutates the existing genre prop.
|
|
124
163
|
*
|
|
125
|
-
* HOW TO FIND IT:
|
|
126
|
-
* - Insights: "content selected", breakdown by "genre", filter "kids_profile" = true
|
|
127
|
-
* - Expected: 100% animation and documentary genres only
|
|
128
|
-
* - Also try: "ad impression" breakdown by "kids_profile" — kids should have ad_blocked=true
|
|
164
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
129
165
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* simulating a recommendation engine improvement.
|
|
166
|
+
* Report 1: Genre Distribution Over Time
|
|
167
|
+
* - Report type: Insights
|
|
168
|
+
* - Event: "content selected"
|
|
169
|
+
* - Measure: Total
|
|
170
|
+
* - Breakdown: "genre"
|
|
171
|
+
* - Expected: animation + documentary share is elevated above pure random
|
|
137
172
|
*
|
|
138
|
-
*
|
|
139
|
-
* - Funnels: "recommendation clicked" -> "playback started" -> "playback completed" -> "content rated"
|
|
140
|
-
* - Breakdown: "improved_recs"
|
|
141
|
-
* - Expected: improved_recs=true shows ~1.5x higher conversion rate
|
|
142
|
-
* - Also try: Insights line chart of "recommendation clicked" filtered by "improved_recs" = true
|
|
173
|
+
* REAL-WORLD ANALOGUE: Kids profiles enforce age-appropriate content.
|
|
143
174
|
*
|
|
144
175
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
145
|
-
*
|
|
176
|
+
* 7. RECOMMENDATION ENGINE IMPROVEMENT (everything)
|
|
146
177
|
* ───────────────────────────────────────────────────────────────────────────────
|
|
147
178
|
*
|
|
148
|
-
* PATTERN:
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* - 20% more playback completed events (extra content consumption)
|
|
152
|
-
* - Events tagged with subtitle_user = true
|
|
179
|
+
* PATTERN: Pre-day-60 "content rated" events get 30% dropped, depressing the
|
|
180
|
+
* recommendation funnel conversion in the first 60 days. No flag — discover
|
|
181
|
+
* via funnel/insights line chart by day.
|
|
153
182
|
*
|
|
154
|
-
* HOW TO FIND IT:
|
|
155
|
-
* - Insights: "playback completed", average of "completion_percent", breakdown by "subtitle_user"
|
|
156
|
-
* - Expected: subtitle_user=true ~85% completion vs false ~68% (1.25x)
|
|
157
|
-
* - Also try: Average of "watch_duration_min" by "subtitle_user" (~15% longer)
|
|
158
|
-
* - Also try: Total events per user by "subtitle_user" (~20% more completions)
|
|
183
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
159
184
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
185
|
+
* Report 1: Content Rated Volume Over Time
|
|
186
|
+
* - Report type: Insights
|
|
187
|
+
* - Event: "content rated"
|
|
188
|
+
* - Measure: Total
|
|
189
|
+
* - Line chart by day
|
|
190
|
+
* - Expected: visible step-up at day 60
|
|
163
191
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* Ad Fatigue Churn | Post-day-45 activity | 100% | 50% | 0.5x
|
|
170
|
-
* New Release Spike | Content selections/day | baseline | +20% spike | 1.2x
|
|
171
|
-
* Kids Profile Safety | Ad impressions | normal | 0 (dropped) | 0x
|
|
172
|
-
* Rec Engine Improvement | Engagement funnel conv | 30% | 45% | 1.5x
|
|
173
|
-
* Subtitle Users | Completion percent | 68% | 85% | 1.25x
|
|
192
|
+
* Report 2: Recommendation Funnel Conversion Over Time
|
|
193
|
+
* - Report type: Funnels
|
|
194
|
+
* - Steps: "recommendation clicked" -> "playback started" -> "content rated"
|
|
195
|
+
* - Compare date ranges (days 0-59 vs 60-100)
|
|
196
|
+
* - Expected: post-improvement window ~ 1.5x conversion
|
|
174
197
|
*
|
|
175
|
-
*
|
|
176
|
-
* ADVANCED ANALYSIS IDEAS
|
|
177
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
198
|
+
* REAL-WORLD ANALOGUE: Rec model upgrades produce a step-change in CTR.
|
|
178
199
|
*
|
|
179
|
-
*
|
|
200
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
* 8. SUBTITLE USERS WATCH MORE (everything)
|
|
202
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
180
203
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
204
|
+
* PATTERN: Users who toggle subtitles enabled get 1.25x completion_percent
|
|
205
|
+
* (cap 100), 1.15x watch_duration_min, plus 20% extra cloned playback-completed
|
|
206
|
+
* events. No flag — discover via cohort builder on subtitle-toggled-enabled.
|
|
183
207
|
*
|
|
184
|
-
*
|
|
185
|
-
* free-tier users from ad fatigue churn (Hook #4)?
|
|
208
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
186
209
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
210
|
+
* Report 1: Completion by Subtitle Cohort
|
|
211
|
+
* - Report type: Insights (with cohort)
|
|
212
|
+
* - Cohort A: users with >= 1 "subtitle toggled" with action="enabled"
|
|
213
|
+
* - Cohort B: rest
|
|
214
|
+
* - Event: "playback completed"
|
|
215
|
+
* - Measure: Average of "completion_percent"
|
|
216
|
+
* - Expected: A ~ 85% vs B ~ 68%
|
|
189
217
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
218
|
+
* REAL-WORLD ANALOGUE: Subtitle adoption correlates with attentive viewers.
|
|
219
|
+
*
|
|
220
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
* 9. RECOMMENDATION-CLICKED MAGIC NUMBER (everything)
|
|
222
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
192
223
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
224
|
+
* PATTERN: Users in the 4-6 recommendation-clicked sweet spot get 1.25x
|
|
225
|
+
* watch_duration_min on playback-completed events. Users with 7+ rec clicks
|
|
226
|
+
* are over-engaged (decision fatigue); 30% of their playback-completed events
|
|
227
|
+
* drop. No flag — discover by binning users on rec-click count.
|
|
195
228
|
*
|
|
196
|
-
*
|
|
229
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
197
230
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
231
|
+
* Report 1: Avg Watch Duration by Rec-Click Bucket
|
|
232
|
+
* - Report type: Insights (with cohort)
|
|
233
|
+
* - Cohort A: users with 4-6 "recommendation clicked"
|
|
234
|
+
* - Cohort B: users with 0-3 "recommendation clicked"
|
|
235
|
+
* - Event: "playback completed"
|
|
236
|
+
* - Measure: Average of "watch_duration_min"
|
|
237
|
+
* - Expected: A ~ 1.25x B
|
|
203
238
|
*
|
|
204
|
-
*
|
|
239
|
+
* Report 2: Completions per User on Heavy Rec Clickers
|
|
240
|
+
* - Report type: Insights (with cohort)
|
|
241
|
+
* - Cohort C: users with >= 7 "recommendation clicked"
|
|
242
|
+
* - Cohort A: users with 4-6
|
|
243
|
+
* - Event: "playback completed"
|
|
244
|
+
* - Measure: Total per user
|
|
245
|
+
* - Expected: C ~ 30% fewer completions per user vs A
|
|
205
246
|
*
|
|
206
|
-
* -
|
|
207
|
-
*
|
|
208
|
-
* - Content Discovery Funnel: Does the browse_section (home vs trending vs
|
|
209
|
-
* genre) affect downstream completion rates?
|
|
210
|
-
* - Engagement Loop: How does recommendation algorithm type (collaborative
|
|
211
|
-
* filtering vs editorial) affect the full loop conversion?
|
|
247
|
+
* REAL-WORLD ANALOGUE: A few good recs surface a watchworthy title; too many
|
|
248
|
+
* clicks signals indecision and drives abandonment.
|
|
212
249
|
*
|
|
213
|
-
*
|
|
250
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
251
|
+
* EXPECTED METRICS SUMMARY
|
|
252
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
214
253
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
254
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
255
|
+
* ─────────────────────────|─────────────────────────|----------|-------------|------
|
|
256
|
+
* Genre Funnel Conversion | documentary funnel conv | 1x | 0.7x | -30%
|
|
257
|
+
* Binge-Watching | completions per streak | 1x | ~ 1.5x | 1.5x
|
|
258
|
+
* Weekend vs Weekday | weekend watch_duration | 1x | ~ 1.5x | 1.5x
|
|
259
|
+
* Ad Fatigue Churn | retention free+10+ads | 1x | ~ 0.5x | -50%
|
|
260
|
+
* New Release Spike | blockbuster id share | 0% | ~ 20% | n/a
|
|
261
|
+
* Kids Profile Safety | animation/doc share | baseline | + 15% | n/a
|
|
262
|
+
* Rec Engine Improvement | content-rated post day60| 1x | ~ 1.5x | 1.5x
|
|
263
|
+
* Subtitle Users | completion % | 68% | 85% | 1.25x
|
|
264
|
+
* Rec-Click Magic Number | sweet watch duration | 1x | 1.25x | 1.25x
|
|
265
|
+
* Rec-Click Magic Number | over completions/user | 1x | 0.7x | -30%
|
|
220
266
|
*/
|
|
221
267
|
|
|
222
268
|
// Generate consistent content IDs for lookup tables and events
|
|
@@ -227,7 +273,9 @@ const blockbusterId = `blockbuster_${v.uid(8)}`;
|
|
|
227
273
|
const config = {
|
|
228
274
|
token,
|
|
229
275
|
seed: SEED,
|
|
230
|
-
|
|
276
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
277
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
278
|
+
// numDays: num_days,
|
|
231
279
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
232
280
|
numUsers: num_users,
|
|
233
281
|
hasAnonIds: false,
|
|
@@ -323,10 +371,6 @@ const config = {
|
|
|
323
371
|
properties: {
|
|
324
372
|
"signup_source": ["organic", "referral", "trial_offer", "ad"],
|
|
325
373
|
"plan_selected": ["free", "standard", "premium"],
|
|
326
|
-
"weekend_viewing": [false],
|
|
327
|
-
"prime_time": [false],
|
|
328
|
-
"kids_profile": [false],
|
|
329
|
-
"ad_fatigue": [false],
|
|
330
374
|
}
|
|
331
375
|
},
|
|
332
376
|
{
|
|
@@ -335,10 +379,6 @@ const config = {
|
|
|
335
379
|
properties: {
|
|
336
380
|
"browse_section": ["home", "trending", "new_releases", "genre", "continue_watching"],
|
|
337
381
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
338
|
-
"weekend_viewing": [false],
|
|
339
|
-
"prime_time": [false],
|
|
340
|
-
"kids_profile": [false],
|
|
341
|
-
"ad_fatigue": [false],
|
|
342
382
|
}
|
|
343
383
|
},
|
|
344
384
|
{
|
|
@@ -348,11 +388,6 @@ const config = {
|
|
|
348
388
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
349
389
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
350
390
|
"content_id": contentIds,
|
|
351
|
-
"blockbuster_release": [false],
|
|
352
|
-
"kids_profile": [false],
|
|
353
|
-
"weekend_viewing": [false],
|
|
354
|
-
"prime_time": [false],
|
|
355
|
-
"ad_fatigue": [false],
|
|
356
391
|
}
|
|
357
392
|
},
|
|
358
393
|
{
|
|
@@ -364,12 +399,6 @@ const config = {
|
|
|
364
399
|
"playback_quality": ["480p", "720p", "1080p", "4k"],
|
|
365
400
|
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
366
401
|
"playback_speed": ["0.5x", "1x", "1x", "1x", "1.25x", "1.5x", "2x"],
|
|
367
|
-
"blockbuster_release": [false],
|
|
368
|
-
"kids_profile": [false],
|
|
369
|
-
"binge_session": [false],
|
|
370
|
-
"weekend_viewing": [false],
|
|
371
|
-
"prime_time": [false],
|
|
372
|
-
"ad_fatigue": [false],
|
|
373
402
|
}
|
|
374
403
|
},
|
|
375
404
|
{
|
|
@@ -380,12 +409,6 @@ const config = {
|
|
|
380
409
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
381
410
|
"watch_duration_min": u.weighNumRange(5, 180, 0.5, 45),
|
|
382
411
|
"completion_percent": u.weighNumRange(10, 100, 1.5, 85),
|
|
383
|
-
"binge_session": [false],
|
|
384
|
-
"subtitle_user": [false],
|
|
385
|
-
"weekend_viewing": [false],
|
|
386
|
-
"prime_time": [false],
|
|
387
|
-
"kids_profile": [false],
|
|
388
|
-
"ad_fatigue": [false],
|
|
389
412
|
}
|
|
390
413
|
},
|
|
391
414
|
{
|
|
@@ -394,10 +417,6 @@ const config = {
|
|
|
394
417
|
properties: {
|
|
395
418
|
"content_id": contentIds,
|
|
396
419
|
"pause_reason": ["manual", "ad_break", "buffering", "notification"],
|
|
397
|
-
"weekend_viewing": [false],
|
|
398
|
-
"prime_time": [false],
|
|
399
|
-
"kids_profile": [false],
|
|
400
|
-
"ad_fatigue": [false],
|
|
401
420
|
}
|
|
402
421
|
},
|
|
403
422
|
{
|
|
@@ -407,11 +426,6 @@ const config = {
|
|
|
407
426
|
"content_id": contentIds,
|
|
408
427
|
"rating": u.weighNumRange(1, 5, 2, 4),
|
|
409
428
|
"review_text_length": u.weighNumRange(0, 500, 0.2, 0),
|
|
410
|
-
"blockbuster_release": [false],
|
|
411
|
-
"weekend_viewing": [false],
|
|
412
|
-
"prime_time": [false],
|
|
413
|
-
"kids_profile": [false],
|
|
414
|
-
"ad_fatigue": [false],
|
|
415
429
|
}
|
|
416
430
|
},
|
|
417
431
|
{
|
|
@@ -421,10 +435,6 @@ const config = {
|
|
|
421
435
|
"content_id": contentIds,
|
|
422
436
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
423
437
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
424
|
-
"weekend_viewing": [false],
|
|
425
|
-
"prime_time": [false],
|
|
426
|
-
"kids_profile": [false],
|
|
427
|
-
"ad_fatigue": [false],
|
|
428
438
|
}
|
|
429
439
|
},
|
|
430
440
|
{
|
|
@@ -433,10 +443,6 @@ const config = {
|
|
|
433
443
|
properties: {
|
|
434
444
|
"content_id": contentIds,
|
|
435
445
|
"reason": ["watched", "not_interested", "expired"],
|
|
436
|
-
"weekend_viewing": [false],
|
|
437
|
-
"prime_time": [false],
|
|
438
|
-
"kids_profile": [false],
|
|
439
|
-
"ad_fatigue": [false],
|
|
440
446
|
}
|
|
441
447
|
},
|
|
442
448
|
{
|
|
@@ -446,10 +452,6 @@ const config = {
|
|
|
446
452
|
"search_term": () => chance.word(),
|
|
447
453
|
"results_count": u.weighNumRange(0, 50, 0.5, 15),
|
|
448
454
|
"search_type": ["title", "actor", "director", "genre"],
|
|
449
|
-
"weekend_viewing": [false],
|
|
450
|
-
"prime_time": [false],
|
|
451
|
-
"kids_profile": [false],
|
|
452
|
-
"ad_fatigue": [false],
|
|
453
455
|
}
|
|
454
456
|
},
|
|
455
457
|
{
|
|
@@ -458,10 +460,6 @@ const config = {
|
|
|
458
460
|
properties: {
|
|
459
461
|
"algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
|
|
460
462
|
"position": u.weighNumRange(1, 20),
|
|
461
|
-
"weekend_viewing": [false],
|
|
462
|
-
"prime_time": [false],
|
|
463
|
-
"kids_profile": [false],
|
|
464
|
-
"ad_fatigue": [false],
|
|
465
463
|
}
|
|
466
464
|
},
|
|
467
465
|
{
|
|
@@ -469,10 +467,6 @@ const config = {
|
|
|
469
467
|
weight: 4,
|
|
470
468
|
properties: {
|
|
471
469
|
"profile_type": ["main", "kids", "partner", "guest"],
|
|
472
|
-
"weekend_viewing": [false],
|
|
473
|
-
"prime_time": [false],
|
|
474
|
-
"kids_profile": [false],
|
|
475
|
-
"ad_fatigue": [false],
|
|
476
470
|
}
|
|
477
471
|
},
|
|
478
472
|
{
|
|
@@ -482,11 +476,6 @@ const config = {
|
|
|
482
476
|
"ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
|
|
483
477
|
"ad_duration_sec": u.weighNumRange(5, 30),
|
|
484
478
|
"skipped": [false, false, false, true, true],
|
|
485
|
-
"ad_blocked": [false],
|
|
486
|
-
"kids_profile": [false],
|
|
487
|
-
"weekend_viewing": [false],
|
|
488
|
-
"prime_time": [false],
|
|
489
|
-
"ad_fatigue": [false],
|
|
490
479
|
}
|
|
491
480
|
},
|
|
492
481
|
{
|
|
@@ -496,10 +485,6 @@ const config = {
|
|
|
496
485
|
"old_plan": ["free", "standard", "premium"],
|
|
497
486
|
"new_plan": ["free", "standard", "premium"],
|
|
498
487
|
"change_reason": ["upgrade", "downgrade", "cancel", "resubscribe"],
|
|
499
|
-
"weekend_viewing": [false],
|
|
500
|
-
"prime_time": [false],
|
|
501
|
-
"kids_profile": [false],
|
|
502
|
-
"ad_fatigue": [false],
|
|
503
488
|
}
|
|
504
489
|
},
|
|
505
490
|
{
|
|
@@ -509,10 +494,6 @@ const config = {
|
|
|
509
494
|
"content_id": contentIds,
|
|
510
495
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
511
496
|
"download_quality": ["720p", "1080p", "4k"],
|
|
512
|
-
"weekend_viewing": [false],
|
|
513
|
-
"prime_time": [false],
|
|
514
|
-
"kids_profile": [false],
|
|
515
|
-
"ad_fatigue": [false],
|
|
516
497
|
}
|
|
517
498
|
},
|
|
518
499
|
{
|
|
@@ -521,10 +502,6 @@ const config = {
|
|
|
521
502
|
properties: {
|
|
522
503
|
"share_method": ["link", "social", "dm", "email"],
|
|
523
504
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
524
|
-
"weekend_viewing": [false],
|
|
525
|
-
"prime_time": [false],
|
|
526
|
-
"kids_profile": [false],
|
|
527
|
-
"ad_fatigue": [false],
|
|
528
505
|
}
|
|
529
506
|
},
|
|
530
507
|
{
|
|
@@ -533,10 +510,6 @@ const config = {
|
|
|
533
510
|
properties: {
|
|
534
511
|
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
535
512
|
"action": ["enabled", "disabled", "changed"],
|
|
536
|
-
"weekend_viewing": [false],
|
|
537
|
-
"prime_time": [false],
|
|
538
|
-
"kids_profile": [false],
|
|
539
|
-
"ad_fatigue": [false],
|
|
540
513
|
}
|
|
541
514
|
},
|
|
542
515
|
],
|
|
@@ -567,141 +540,68 @@ const config = {
|
|
|
567
540
|
|
|
568
541
|
lookupTables: [],
|
|
569
542
|
|
|
570
|
-
/**
|
|
571
|
-
* ARCHITECTED ANALYTICS HOOKS
|
|
572
|
-
*
|
|
573
|
-
* This hook function creates 8 deliberate patterns in the data:
|
|
574
|
-
*
|
|
575
|
-
* 1. GENRE FUNNEL CONVERSION: Comedy/Animation props in funnel-pre; documentary completion drop in everything
|
|
576
|
-
* 2. BINGE-WATCHING: Users with 3+ consecutive completions get extra episodes (everything)
|
|
577
|
-
* 3. WEEKEND vs WEEKDAY: Weekend sessions 1.5x longer; weekday prime-time tagging (event)
|
|
578
|
-
* 4. AD FATIGUE CHURN: Free-tier users with 10+ ads churn after day 45 (everything)
|
|
579
|
-
* 5. NEW RELEASE SPIKE: Blockbuster release on day 50 drives content selection (event)
|
|
580
|
-
* 6. KIDS PROFILE SAFETY: Kids mode restricts genres and drops ads (event)
|
|
581
|
-
* 7. RECOMMENDATION ENGINE IMPROVEMENT: Props in funnel-pre; pre-day-60 content rated drop in everything
|
|
582
|
-
* 8. SUBTITLE USERS WATCH MORE: Subtitle-enabled users have higher completion rates (everything)
|
|
583
|
-
*/
|
|
584
543
|
hook: function (record, type, meta) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
record.props.genre_penalty = false;
|
|
603
|
-
} else if (genre === "documentary") {
|
|
604
|
-
record.props.genre_boost = false;
|
|
605
|
-
record.props.genre_penalty = true;
|
|
606
|
-
} else if (!genre && chance.bool({ likelihood: 25 })) {
|
|
607
|
-
// Randomly apply genre effects when genre isn't in funnel props
|
|
608
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
609
|
-
record.props.genre_boost = true;
|
|
610
|
-
record.props.genre_penalty = false;
|
|
611
|
-
} else {
|
|
612
|
-
record.props.genre_boost = false;
|
|
613
|
-
record.props.genre_penalty = true;
|
|
544
|
+
// Hook #10 (T2C): CORE VIEWING LOOP TIME-TO-CONVERT (funnel-post)
|
|
545
|
+
// Premium subscribers complete browse→play funnel 1.4x faster
|
|
546
|
+
// (factor 0.71); free users 1.25x slower (factor 1.25).
|
|
547
|
+
if (type === "funnel-post") {
|
|
548
|
+
const segment = meta?.profile?.subscription_plan;
|
|
549
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
550
|
+
const factor = (
|
|
551
|
+
segment === "premium" ? 0.71 :
|
|
552
|
+
segment === "free" ? 1.25 :
|
|
553
|
+
1.0
|
|
554
|
+
);
|
|
555
|
+
if (factor !== 1.0) {
|
|
556
|
+
for (let i = 1; i < record.length; i++) {
|
|
557
|
+
const prev = dayjs(record[i - 1].time);
|
|
558
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
559
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
560
|
+
}
|
|
614
561
|
}
|
|
615
|
-
} else {
|
|
616
|
-
record.props.genre_boost = false;
|
|
617
|
-
record.props.genre_penalty = false;
|
|
618
562
|
}
|
|
619
|
-
|
|
620
|
-
// ─────────────────────────────────────────────────────────────
|
|
621
|
-
// Hook #7: RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
|
|
622
|
-
// After day 60, engagement funnel gets 1.5x boost
|
|
623
|
-
// ─────────────────────────────────────────────────────────────
|
|
624
|
-
const seq = record.sequence || [];
|
|
625
|
-
const isEngagementFunnel = seq.includes("recommendation clicked");
|
|
626
|
-
const funnelTime = meta && meta.firstEventTime ? dayjs.unix(meta.firstEventTime) : null;
|
|
627
|
-
const isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));
|
|
628
|
-
|
|
629
|
-
if (isEngagementFunnel && isAfterImprovement) {
|
|
630
|
-
record.props.improved_recs = true;
|
|
631
|
-
} else {
|
|
632
|
-
record.props.improved_recs = false;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return record;
|
|
636
563
|
}
|
|
637
564
|
|
|
638
|
-
// ─────────────────────────────────────────────────────────────
|
|
639
|
-
// Hook #3: WEEKEND vs WEEKDAY PATTERNS
|
|
640
|
-
// Moved to everything hook (after sessionization) so DOW/HOD
|
|
641
|
-
// tags match final timestamps. See everything hook below.
|
|
642
|
-
// ─────────────────────────────────────────────────────────────
|
|
643
|
-
|
|
644
565
|
if (type === "event") {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// Bounded window concentrates the spike instead of spreading
|
|
649
|
-
// it across the entire second half of the dataset.
|
|
650
|
-
// ─────────────────────────────────────────────────────────────
|
|
651
|
-
const BLOCKBUSTER_START = DATASET_START.add(50, 'days');
|
|
652
|
-
const BLOCKBUSTER_END = DATASET_START.add(65, 'days');
|
|
566
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
567
|
+
const BLOCKBUSTER_START = datasetStart.add(50, 'days');
|
|
568
|
+
const BLOCKBUSTER_END = datasetStart.add(65, 'days');
|
|
653
569
|
const EVENT_TIME = dayjs(record.time);
|
|
570
|
+
|
|
571
|
+
// Hook #5: NEW RELEASE SPIKE — days 50-65, 20% of selections/starts
|
|
572
|
+
// switch to the blockbuster id. Mutates existing content_id/content_type props.
|
|
654
573
|
if (record.event === "content selected" || record.event === "playback started") {
|
|
655
574
|
if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
|
|
656
575
|
record.content_type = "movie";
|
|
657
576
|
record.content_id = blockbusterId;
|
|
658
|
-
record.blockbuster_release = true;
|
|
659
|
-
} else {
|
|
660
|
-
record.blockbuster_release = false;
|
|
661
577
|
}
|
|
662
578
|
}
|
|
663
|
-
|
|
664
579
|
if (record.event === "content rated") {
|
|
665
580
|
if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
|
|
666
581
|
record.rating = chance.integer({ min: 4, max: 5 });
|
|
667
582
|
record.content_id = blockbusterId;
|
|
668
|
-
record.blockbuster_release = true;
|
|
669
|
-
} else {
|
|
670
|
-
record.blockbuster_release = false;
|
|
671
583
|
}
|
|
672
584
|
}
|
|
673
585
|
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
// 15% of the time, apply kids mode: restrict genres, drop ads
|
|
677
|
-
// ─────────────────────────────────────────────────────────────
|
|
586
|
+
// Hook #6: KIDS PROFILE SAFETY — 15% of selections/starts get genre
|
|
587
|
+
// restricted to animation or documentary. Mutates existing genre prop.
|
|
678
588
|
if (chance.bool({ likelihood: 15 })) {
|
|
679
|
-
record.kids_profile = true;
|
|
680
589
|
if (record.event === "content selected" || record.event === "playback started") {
|
|
681
590
|
record.genre = chance.pickone(["animation", "documentary"]);
|
|
682
|
-
} else if (record.event === "ad impression") {
|
|
683
|
-
record.ad_blocked = true;
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
record.kids_profile = false;
|
|
687
|
-
if (record.event === "ad impression") {
|
|
688
|
-
record.ad_blocked = false;
|
|
689
591
|
}
|
|
690
592
|
}
|
|
691
593
|
|
|
692
594
|
return record;
|
|
693
595
|
}
|
|
694
596
|
|
|
695
|
-
// ─────────────────────────────────────────────────────────────
|
|
696
|
-
// Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns
|
|
697
|
-
// ─────────────────────────────────────────────────────────────
|
|
698
597
|
if (type === "everything") {
|
|
598
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
699
599
|
const userEvents = record;
|
|
700
600
|
if (!userEvents || userEvents.length === 0) return record;
|
|
701
601
|
|
|
702
602
|
const profile = meta.profile;
|
|
703
603
|
|
|
704
|
-
//
|
|
604
|
+
// Stamp superProps from profile (consistent per user)
|
|
705
605
|
const stampPlan = profile && profile.subscription_plan ? profile.subscription_plan : undefined;
|
|
706
606
|
const stampDevice = profile && profile.device_type ? profile.device_type : undefined;
|
|
707
607
|
if (stampPlan || stampDevice) {
|
|
@@ -711,126 +611,75 @@ const config = {
|
|
|
711
611
|
});
|
|
712
612
|
}
|
|
713
613
|
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
// Runs after sessionization so DOW/HOD tags match final timestamps.
|
|
614
|
+
// Hook #3: WEEKEND VS WEEKDAY — 1.5x watch_duration_min on weekends.
|
|
615
|
+
// No flag — analyst breaks down by day of week.
|
|
717
616
|
for (const e of userEvents) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
722
|
-
|
|
723
|
-
if (isWeekend) {
|
|
724
|
-
e.weekend_viewing = true;
|
|
725
|
-
e.prime_time = false;
|
|
726
|
-
if (e.event === "playback completed" && e.watch_duration_min) {
|
|
727
|
-
e.watch_duration_min = Math.round(e.watch_duration_min * 1.5);
|
|
728
|
-
}
|
|
729
|
-
} else {
|
|
730
|
-
e.weekend_viewing = false;
|
|
731
|
-
if (hour >= 18 && hour <= 23) {
|
|
732
|
-
e.prime_time = true;
|
|
733
|
-
} else {
|
|
734
|
-
e.prime_time = false;
|
|
735
|
-
}
|
|
617
|
+
const dow = new Date(e.time).getUTCDay();
|
|
618
|
+
if ((dow === 0 || dow === 6) && e.event === "playback completed" && typeof e.watch_duration_min === "number") {
|
|
619
|
+
e.watch_duration_min = Math.round(e.watch_duration_min * 1.5);
|
|
736
620
|
}
|
|
737
621
|
}
|
|
738
622
|
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
// to simulate lower documentary funnel completion (was conversionRate * 0.7)
|
|
623
|
+
// Hook #1: GENRE FUNNEL CONVERSION — drop 25% of documentary playback
|
|
624
|
+
// completed events to depress documentary funnel conversion. Raw genre check.
|
|
742
625
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
743
626
|
const evt = userEvents[i];
|
|
744
|
-
if (evt.event === "playback completed" && evt.
|
|
745
|
-
|
|
746
|
-
userEvents.splice(i, 1);
|
|
747
|
-
}
|
|
627
|
+
if (evt.event === "playback completed" && evt.genre === "documentary" && chance.bool({ likelihood: 25 })) {
|
|
628
|
+
userEvents.splice(i, 1);
|
|
748
629
|
}
|
|
749
630
|
}
|
|
750
631
|
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
const IMPROVEMENT_DATE = DATASET_START.add(60, 'days');
|
|
632
|
+
// Hook #7: RECOMMENDATION ENGINE IMPROVEMENT — drop 30% of pre-day-60
|
|
633
|
+
// content rated events. Raw time check.
|
|
634
|
+
const IMPROVEMENT_DATE = datasetStart.add(60, 'days');
|
|
755
635
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
756
636
|
const evt = userEvents[i];
|
|
757
|
-
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE)) {
|
|
758
|
-
|
|
759
|
-
userEvents.splice(i, 1);
|
|
760
|
-
}
|
|
637
|
+
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE) && chance.bool({ likelihood: 30 })) {
|
|
638
|
+
userEvents.splice(i, 1);
|
|
761
639
|
}
|
|
762
640
|
}
|
|
763
641
|
|
|
764
642
|
const firstEventTime = dayjs(userEvents[0].time);
|
|
765
643
|
|
|
766
|
-
//
|
|
644
|
+
// Identify behavioral patterns (no flags written)
|
|
767
645
|
let consecutiveCompletions = 0;
|
|
768
646
|
let maxConsecutiveCompletions = 0;
|
|
769
|
-
let isBingeWatcher = false;
|
|
770
647
|
let adImpressionCount = 0;
|
|
771
648
|
let isFreeTier = false;
|
|
772
649
|
let hasSubtitlesEnabled = false;
|
|
650
|
+
let recClickCount = 0;
|
|
773
651
|
|
|
774
652
|
userEvents.forEach((event, idx) => {
|
|
775
|
-
// Track subscription tier from superProps
|
|
776
653
|
if (idx === 0 && event.subscription_plan) {
|
|
777
654
|
isFreeTier = event.subscription_plan === "free";
|
|
778
655
|
}
|
|
779
|
-
|
|
780
|
-
// Hook #2: Track consecutive playback completions
|
|
781
656
|
if (event.event === "playback completed") {
|
|
782
657
|
consecutiveCompletions++;
|
|
783
658
|
if (consecutiveCompletions > maxConsecutiveCompletions) {
|
|
784
659
|
maxConsecutiveCompletions = consecutiveCompletions;
|
|
785
660
|
}
|
|
786
661
|
} else if (event.event !== "playback started") {
|
|
787
|
-
// Reset streak on non-playback events (started doesn't break streak)
|
|
788
662
|
consecutiveCompletions = 0;
|
|
789
663
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (event.event === "
|
|
793
|
-
adImpressionCount++;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Hook #8: Check for subtitle enabled
|
|
797
|
-
if (event.event === "subtitle toggled" && event.action === "enabled") {
|
|
798
|
-
hasSubtitlesEnabled = true;
|
|
799
|
-
}
|
|
664
|
+
if (event.event === "ad impression") adImpressionCount++;
|
|
665
|
+
if (event.event === "subtitle toggled" && event.action === "enabled") hasSubtitlesEnabled = true;
|
|
666
|
+
if (event.event === "recommendation clicked") recClickCount++;
|
|
800
667
|
});
|
|
801
668
|
|
|
802
|
-
isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
803
|
-
|
|
804
|
-
// ─── Second pass: set schema defaults then modify ───
|
|
669
|
+
const isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
805
670
|
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
if (event.event === "playback completed") {
|
|
809
|
-
if (event.binge_session === undefined) event.binge_session = false;
|
|
810
|
-
if (event.subtitle_user === undefined) event.subtitle_user = false;
|
|
811
|
-
}
|
|
812
|
-
if (event.event === "playback started") {
|
|
813
|
-
if (event.binge_session === undefined) event.binge_session = false;
|
|
814
|
-
}
|
|
815
|
-
if (event.ad_fatigue === undefined) event.ad_fatigue = false;
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
// Hook #2: BINGE-WATCHING PATTERN
|
|
819
|
-
// Binge-watchers get extra episodes, high completion, fewer pauses
|
|
671
|
+
// Hook #2: BINGE-WATCHING — drop pauses, inject extra start+complete pairs.
|
|
672
|
+
// Cloned events use unique offset timestamps. No flag.
|
|
820
673
|
if (isBingeWatcher) {
|
|
821
674
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
822
675
|
const event = userEvents[i];
|
|
823
676
|
const eventTime = dayjs(event.time);
|
|
824
677
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
userEvents.splice(i, 1);
|
|
829
|
-
continue;
|
|
830
|
-
}
|
|
678
|
+
if (event.event === "playback paused" && chance.bool({ likelihood: 60 })) {
|
|
679
|
+
userEvents.splice(i, 1);
|
|
680
|
+
continue;
|
|
831
681
|
}
|
|
832
682
|
|
|
833
|
-
// Add extra viewing events after completions
|
|
834
683
|
if (event.event === "playback completed" && chance.bool({ likelihood: 40 })) {
|
|
835
684
|
const nextContentId = chance.pickone(contentIds);
|
|
836
685
|
const startTemplate = userEvents.find(e => e.event === "playback started");
|
|
@@ -842,7 +691,6 @@ const config = {
|
|
|
842
691
|
content_id: nextContentId,
|
|
843
692
|
content_type: "series",
|
|
844
693
|
playback_quality: event.playback_quality || "1080p",
|
|
845
|
-
binge_session: true,
|
|
846
694
|
};
|
|
847
695
|
const extraComplete = {
|
|
848
696
|
...event,
|
|
@@ -852,51 +700,42 @@ const config = {
|
|
|
852
700
|
content_type: "series",
|
|
853
701
|
watch_duration_min: chance.integer({ min: 20, max: 55 }),
|
|
854
702
|
completion_percent: chance.integer({ min: 90, max: 100 }),
|
|
855
|
-
binge_session: true,
|
|
856
703
|
};
|
|
857
704
|
userEvents.splice(i + 1, 0, extraStart, extraComplete);
|
|
858
705
|
}
|
|
859
706
|
}
|
|
860
707
|
}
|
|
861
708
|
|
|
862
|
-
// Hook #4: AD FATIGUE CHURN
|
|
863
|
-
//
|
|
709
|
+
// Hook #4: AD FATIGUE CHURN — free-tier users w/ 10+ ads lose 50% of
|
|
710
|
+
// events after day 45. No flag — discover via cohort retention.
|
|
864
711
|
if (isFreeTier && adImpressionCount >= 10) {
|
|
865
712
|
const churnCutoff = firstEventTime.add(45, 'days');
|
|
866
713
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
867
714
|
const evt = userEvents[i];
|
|
868
|
-
if (dayjs(evt.time).isAfter(churnCutoff)) {
|
|
869
|
-
|
|
870
|
-
userEvents.splice(i, 1);
|
|
871
|
-
} else {
|
|
872
|
-
evt.ad_fatigue = true;
|
|
873
|
-
}
|
|
715
|
+
if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 50 })) {
|
|
716
|
+
userEvents.splice(i, 1);
|
|
874
717
|
}
|
|
875
718
|
}
|
|
876
719
|
}
|
|
877
720
|
|
|
878
|
-
// Hook #8: SUBTITLE USERS WATCH MORE
|
|
879
|
-
//
|
|
721
|
+
// Hook #8: SUBTITLE USERS WATCH MORE — 1.25x completion_percent (cap 100),
|
|
722
|
+
// 1.15x watch_duration_min, plus 20% extra cloned playback completions.
|
|
723
|
+
// No flag — discover via cohort builder on subtitle-toggled-enabled.
|
|
880
724
|
if (hasSubtitlesEnabled) {
|
|
881
725
|
for (let i = 0; i < userEvents.length; i++) {
|
|
882
726
|
const event = userEvents[i];
|
|
883
|
-
|
|
884
727
|
if (event.event === "playback completed") {
|
|
885
|
-
// Boost completion percent (cap at 100)
|
|
886
728
|
if (event.completion_percent) {
|
|
887
729
|
event.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));
|
|
888
730
|
}
|
|
889
|
-
// Boost watch duration
|
|
890
731
|
if (event.watch_duration_min) {
|
|
891
732
|
event.watch_duration_min = Math.round(event.watch_duration_min * 1.15);
|
|
892
733
|
}
|
|
893
|
-
event.subtitle_user = true;
|
|
894
734
|
}
|
|
895
735
|
}
|
|
896
736
|
|
|
897
|
-
// Splice extra playback completed events (subtitle users watch more overall)
|
|
898
737
|
const completionEvents = userEvents.filter(e => e.event === "playback completed");
|
|
899
|
-
const extraCount = Math.floor(completionEvents.length * 0.2);
|
|
738
|
+
const extraCount = Math.floor(completionEvents.length * 0.2);
|
|
900
739
|
for (let j = 0; j < extraCount; j++) {
|
|
901
740
|
const templateEvent = chance.pickone(completionEvents);
|
|
902
741
|
const templateTime = dayjs(templateEvent.time);
|
|
@@ -908,12 +747,28 @@ const config = {
|
|
|
908
747
|
content_type: chance.pickone(["movie", "series", "documentary"]),
|
|
909
748
|
watch_duration_min: chance.integer({ min: 25, max: 120 }),
|
|
910
749
|
completion_percent: chance.integer({ min: 80, max: 100 }),
|
|
911
|
-
subtitle_user: true,
|
|
912
750
|
};
|
|
913
751
|
userEvents.push(extraCompletion);
|
|
914
752
|
}
|
|
915
753
|
}
|
|
916
754
|
|
|
755
|
+
// Hook #9: RECOMMENDATION-CLICKED MAGIC NUMBER (no flags)
|
|
756
|
+
// Sweet 4-6 rec clicks → +25% watch_duration_min on playback completed.
|
|
757
|
+
// Over 7+ → drop 30% of playback completed events (rec fatigue).
|
|
758
|
+
if (recClickCount >= 4 && recClickCount <= 6) {
|
|
759
|
+
userEvents.forEach(e => {
|
|
760
|
+
if (e.event === 'playback completed' && typeof e.watch_duration_min === 'number') {
|
|
761
|
+
e.watch_duration_min = Math.round(e.watch_duration_min * 1.25);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
} else if (recClickCount >= 7) {
|
|
765
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
766
|
+
if (userEvents[i].event === 'playback completed' && chance.bool({ likelihood: 30 })) {
|
|
767
|
+
userEvents.splice(i, 1);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
917
772
|
return record;
|
|
918
773
|
}
|
|
919
774
|
|