@ak--47/dungeon-master 1.2.3 → 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/CHANGELOG.md +42 -0
- package/README.md +51 -13
- package/dungeons/technical/ad-spend.js +2 -2
- package/dungeons/technical/anonymous-users.js +2 -2
- package/dungeons/technical/array-of-object-lookup.js +2 -4
- package/dungeons/technical/experiments.js +2 -2
- package/dungeons/technical/foobar.js +2 -2
- package/dungeons/technical/group-analytics.js +2 -2
- package/dungeons/technical/mirror-strategies.js +2 -2
- package/dungeons/technical/nested-objects.js +2 -2
- package/dungeons/technical/retention-cadence.js +2 -3
- package/dungeons/technical/sanity.js +2 -2
- package/dungeons/technical/scale-test.js +2 -2
- package/dungeons/technical/scd.js +2 -2
- package/dungeons/technical/simple.js +5 -6
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/simplest.js +2 -2
- package/dungeons/technical/text-generation.js +3 -3
- package/dungeons/vertical/ai-platform.js +858 -0
- package/dungeons/vertical/community.js +84 -40
- package/dungeons/vertical/crypto.js +830 -0
- package/dungeons/vertical/dating.js +744 -0
- package/dungeons/vertical/devtools.js +175 -69
- package/dungeons/vertical/ecommerce.js +242 -94
- package/dungeons/vertical/education.js +330 -313
- package/dungeons/vertical/fintech.js +442 -313
- package/dungeons/vertical/fitness.js +143 -61
- package/dungeons/vertical/food-delivery.js +327 -353
- package/dungeons/vertical/gaming.js +912 -382
- package/dungeons/vertical/healthcare.js +142 -63
- package/dungeons/vertical/insurance-application.js +170 -76
- package/dungeons/vertical/logistics.js +115 -20
- package/dungeons/vertical/marketplace.js +152 -58
- package/dungeons/vertical/media.js +248 -384
- package/dungeons/vertical/real-estate.js +781 -0
- package/dungeons/vertical/sass.js +255 -266
- package/dungeons/vertical/social.js +264 -206
- package/dungeons/vertical/travel.js +117 -41
- package/index.js +17 -17
- package/lib/core/config-validator.js +159 -31
- 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 +81 -47
- package/lib/templates/macro-presets.js +111 -0
- package/lib/templates/soup-presets.js +19 -36
- package/lib/utils/utils.js +71 -39
- package/package.json +8 -2
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +251 -51
- 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/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +0 -579
- 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 -438
- 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/rpg-schema.json +0 -2491
- package/dungeons/vertical/rpg.js +0 -976
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const SEED = "harness-media";
|
|
3
3
|
const num_days = 100;
|
|
4
4
|
const num_users = 5_000;
|
|
5
|
-
const
|
|
5
|
+
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
@@ -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:
|
|
109
|
+
*
|
|
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
|
|
84
116
|
*
|
|
85
|
-
*
|
|
86
|
-
* - Insights: "playback completed", average of "watch_duration_min", breakdown by "weekend_viewing"
|
|
87
|
-
* - Expected: weekend_viewing=true ~67 min avg vs false ~45 min avg (1.5x)
|
|
88
|
-
* - Also try: Total "playback completed" events by "prime_time" (60-70% of weekday views)
|
|
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:
|
|
127
|
+
*
|
|
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
|
|
96
133
|
*
|
|
97
|
-
*
|
|
98
|
-
* - Retention: any event (filter subscription_plan=free), segment by ad impression count >= 10 vs < 10
|
|
99
|
-
* - Expected: 10+ ad users show ~50% retention drop after day 45
|
|
100
|
-
* - Also try: Insights line chart, any event filtered by "ad_fatigue" = true, weekly trend
|
|
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.
|
|
110
144
|
*
|
|
111
|
-
* HOW TO FIND IT:
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* -
|
|
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
|
|
154
|
+
*
|
|
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
|
|
124
|
-
*
|
|
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
|
|
161
|
+
* PATTERN: 15% of "content selected" / "playback started" events get genre
|
|
162
|
+
* restricted to "animation" or "documentary". Mutates the existing genre prop.
|
|
129
163
|
*
|
|
130
|
-
*
|
|
131
|
-
* 7. RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
|
|
132
|
-
* ───────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
133
165
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
*
|
|
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.
|
|
180
207
|
*
|
|
181
|
-
*
|
|
182
|
-
* total watch hours? (Hooks #2 + #8 combined)
|
|
208
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
183
209
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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%
|
|
186
217
|
*
|
|
187
|
-
*
|
|
188
|
-
* + #3)? Does weekend kids viewing show different genre preferences?
|
|
218
|
+
* REAL-WORLD ANALOGUE: Subtitle adoption correlates with attentive viewers.
|
|
189
219
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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,8 +273,10 @@ const blockbusterId = `blockbuster_${v.uid(8)}`;
|
|
|
227
273
|
const config = {
|
|
228
274
|
token,
|
|
229
275
|
seed: SEED,
|
|
230
|
-
|
|
231
|
-
|
|
276
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
277
|
+
datasetEnd: "2026-04-28T23:59:59Z",
|
|
278
|
+
// numDays: num_days,
|
|
279
|
+
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
232
280
|
numUsers: num_users,
|
|
233
281
|
hasAnonIds: false,
|
|
234
282
|
hasSessionIds: true,
|
|
@@ -243,7 +291,6 @@ const config = {
|
|
|
243
291
|
hasCampaigns: false,
|
|
244
292
|
isAnonymous: false,
|
|
245
293
|
hasAdSpend: false,
|
|
246
|
-
percentUsersBornInDataset: 50,
|
|
247
294
|
hasAvatar: true,
|
|
248
295
|
concurrency: 1,
|
|
249
296
|
writeToDisk: false,
|
|
@@ -324,10 +371,6 @@ const config = {
|
|
|
324
371
|
properties: {
|
|
325
372
|
"signup_source": ["organic", "referral", "trial_offer", "ad"],
|
|
326
373
|
"plan_selected": ["free", "standard", "premium"],
|
|
327
|
-
"weekend_viewing": [false],
|
|
328
|
-
"prime_time": [false],
|
|
329
|
-
"kids_profile": [false],
|
|
330
|
-
"ad_fatigue": [false],
|
|
331
374
|
}
|
|
332
375
|
},
|
|
333
376
|
{
|
|
@@ -336,10 +379,6 @@ const config = {
|
|
|
336
379
|
properties: {
|
|
337
380
|
"browse_section": ["home", "trending", "new_releases", "genre", "continue_watching"],
|
|
338
381
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
339
|
-
"weekend_viewing": [false],
|
|
340
|
-
"prime_time": [false],
|
|
341
|
-
"kids_profile": [false],
|
|
342
|
-
"ad_fatigue": [false],
|
|
343
382
|
}
|
|
344
383
|
},
|
|
345
384
|
{
|
|
@@ -349,11 +388,6 @@ const config = {
|
|
|
349
388
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
350
389
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
351
390
|
"content_id": contentIds,
|
|
352
|
-
"blockbuster_release": [false],
|
|
353
|
-
"kids_profile": [false],
|
|
354
|
-
"weekend_viewing": [false],
|
|
355
|
-
"prime_time": [false],
|
|
356
|
-
"ad_fatigue": [false],
|
|
357
391
|
}
|
|
358
392
|
},
|
|
359
393
|
{
|
|
@@ -365,12 +399,6 @@ const config = {
|
|
|
365
399
|
"playback_quality": ["480p", "720p", "1080p", "4k"],
|
|
366
400
|
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
367
401
|
"playback_speed": ["0.5x", "1x", "1x", "1x", "1.25x", "1.5x", "2x"],
|
|
368
|
-
"blockbuster_release": [false],
|
|
369
|
-
"kids_profile": [false],
|
|
370
|
-
"binge_session": [false],
|
|
371
|
-
"weekend_viewing": [false],
|
|
372
|
-
"prime_time": [false],
|
|
373
|
-
"ad_fatigue": [false],
|
|
374
402
|
}
|
|
375
403
|
},
|
|
376
404
|
{
|
|
@@ -381,12 +409,6 @@ const config = {
|
|
|
381
409
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
382
410
|
"watch_duration_min": u.weighNumRange(5, 180, 0.5, 45),
|
|
383
411
|
"completion_percent": u.weighNumRange(10, 100, 1.5, 85),
|
|
384
|
-
"binge_session": [false],
|
|
385
|
-
"subtitle_user": [false],
|
|
386
|
-
"weekend_viewing": [false],
|
|
387
|
-
"prime_time": [false],
|
|
388
|
-
"kids_profile": [false],
|
|
389
|
-
"ad_fatigue": [false],
|
|
390
412
|
}
|
|
391
413
|
},
|
|
392
414
|
{
|
|
@@ -395,10 +417,6 @@ const config = {
|
|
|
395
417
|
properties: {
|
|
396
418
|
"content_id": contentIds,
|
|
397
419
|
"pause_reason": ["manual", "ad_break", "buffering", "notification"],
|
|
398
|
-
"weekend_viewing": [false],
|
|
399
|
-
"prime_time": [false],
|
|
400
|
-
"kids_profile": [false],
|
|
401
|
-
"ad_fatigue": [false],
|
|
402
420
|
}
|
|
403
421
|
},
|
|
404
422
|
{
|
|
@@ -408,11 +426,6 @@ const config = {
|
|
|
408
426
|
"content_id": contentIds,
|
|
409
427
|
"rating": u.weighNumRange(1, 5, 2, 4),
|
|
410
428
|
"review_text_length": u.weighNumRange(0, 500, 0.2, 0),
|
|
411
|
-
"blockbuster_release": [false],
|
|
412
|
-
"weekend_viewing": [false],
|
|
413
|
-
"prime_time": [false],
|
|
414
|
-
"kids_profile": [false],
|
|
415
|
-
"ad_fatigue": [false],
|
|
416
429
|
}
|
|
417
430
|
},
|
|
418
431
|
{
|
|
@@ -422,10 +435,6 @@ const config = {
|
|
|
422
435
|
"content_id": contentIds,
|
|
423
436
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
424
437
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
425
|
-
"weekend_viewing": [false],
|
|
426
|
-
"prime_time": [false],
|
|
427
|
-
"kids_profile": [false],
|
|
428
|
-
"ad_fatigue": [false],
|
|
429
438
|
}
|
|
430
439
|
},
|
|
431
440
|
{
|
|
@@ -434,10 +443,6 @@ const config = {
|
|
|
434
443
|
properties: {
|
|
435
444
|
"content_id": contentIds,
|
|
436
445
|
"reason": ["watched", "not_interested", "expired"],
|
|
437
|
-
"weekend_viewing": [false],
|
|
438
|
-
"prime_time": [false],
|
|
439
|
-
"kids_profile": [false],
|
|
440
|
-
"ad_fatigue": [false],
|
|
441
446
|
}
|
|
442
447
|
},
|
|
443
448
|
{
|
|
@@ -447,10 +452,6 @@ const config = {
|
|
|
447
452
|
"search_term": () => chance.word(),
|
|
448
453
|
"results_count": u.weighNumRange(0, 50, 0.5, 15),
|
|
449
454
|
"search_type": ["title", "actor", "director", "genre"],
|
|
450
|
-
"weekend_viewing": [false],
|
|
451
|
-
"prime_time": [false],
|
|
452
|
-
"kids_profile": [false],
|
|
453
|
-
"ad_fatigue": [false],
|
|
454
455
|
}
|
|
455
456
|
},
|
|
456
457
|
{
|
|
@@ -459,10 +460,6 @@ const config = {
|
|
|
459
460
|
properties: {
|
|
460
461
|
"algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
|
|
461
462
|
"position": u.weighNumRange(1, 20),
|
|
462
|
-
"weekend_viewing": [false],
|
|
463
|
-
"prime_time": [false],
|
|
464
|
-
"kids_profile": [false],
|
|
465
|
-
"ad_fatigue": [false],
|
|
466
463
|
}
|
|
467
464
|
},
|
|
468
465
|
{
|
|
@@ -470,10 +467,6 @@ const config = {
|
|
|
470
467
|
weight: 4,
|
|
471
468
|
properties: {
|
|
472
469
|
"profile_type": ["main", "kids", "partner", "guest"],
|
|
473
|
-
"weekend_viewing": [false],
|
|
474
|
-
"prime_time": [false],
|
|
475
|
-
"kids_profile": [false],
|
|
476
|
-
"ad_fatigue": [false],
|
|
477
470
|
}
|
|
478
471
|
},
|
|
479
472
|
{
|
|
@@ -483,11 +476,6 @@ const config = {
|
|
|
483
476
|
"ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
|
|
484
477
|
"ad_duration_sec": u.weighNumRange(5, 30),
|
|
485
478
|
"skipped": [false, false, false, true, true],
|
|
486
|
-
"ad_blocked": [false],
|
|
487
|
-
"kids_profile": [false],
|
|
488
|
-
"weekend_viewing": [false],
|
|
489
|
-
"prime_time": [false],
|
|
490
|
-
"ad_fatigue": [false],
|
|
491
479
|
}
|
|
492
480
|
},
|
|
493
481
|
{
|
|
@@ -497,10 +485,6 @@ const config = {
|
|
|
497
485
|
"old_plan": ["free", "standard", "premium"],
|
|
498
486
|
"new_plan": ["free", "standard", "premium"],
|
|
499
487
|
"change_reason": ["upgrade", "downgrade", "cancel", "resubscribe"],
|
|
500
|
-
"weekend_viewing": [false],
|
|
501
|
-
"prime_time": [false],
|
|
502
|
-
"kids_profile": [false],
|
|
503
|
-
"ad_fatigue": [false],
|
|
504
488
|
}
|
|
505
489
|
},
|
|
506
490
|
{
|
|
@@ -510,10 +494,6 @@ const config = {
|
|
|
510
494
|
"content_id": contentIds,
|
|
511
495
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
512
496
|
"download_quality": ["720p", "1080p", "4k"],
|
|
513
|
-
"weekend_viewing": [false],
|
|
514
|
-
"prime_time": [false],
|
|
515
|
-
"kids_profile": [false],
|
|
516
|
-
"ad_fatigue": [false],
|
|
517
497
|
}
|
|
518
498
|
},
|
|
519
499
|
{
|
|
@@ -522,10 +502,6 @@ const config = {
|
|
|
522
502
|
properties: {
|
|
523
503
|
"share_method": ["link", "social", "dm", "email"],
|
|
524
504
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
525
|
-
"weekend_viewing": [false],
|
|
526
|
-
"prime_time": [false],
|
|
527
|
-
"kids_profile": [false],
|
|
528
|
-
"ad_fatigue": [false],
|
|
529
505
|
}
|
|
530
506
|
},
|
|
531
507
|
{
|
|
@@ -534,10 +510,6 @@ const config = {
|
|
|
534
510
|
properties: {
|
|
535
511
|
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
536
512
|
"action": ["enabled", "disabled", "changed"],
|
|
537
|
-
"weekend_viewing": [false],
|
|
538
|
-
"prime_time": [false],
|
|
539
|
-
"kids_profile": [false],
|
|
540
|
-
"ad_fatigue": [false],
|
|
541
513
|
}
|
|
542
514
|
},
|
|
543
515
|
],
|
|
@@ -568,156 +540,68 @@ const config = {
|
|
|
568
540
|
|
|
569
541
|
lookupTables: [],
|
|
570
542
|
|
|
571
|
-
/**
|
|
572
|
-
* ARCHITECTED ANALYTICS HOOKS
|
|
573
|
-
*
|
|
574
|
-
* This hook function creates 8 deliberate patterns in the data:
|
|
575
|
-
*
|
|
576
|
-
* 1. GENRE FUNNEL CONVERSION: Comedy/Animation props in funnel-pre; documentary completion drop in everything
|
|
577
|
-
* 2. BINGE-WATCHING: Users with 3+ consecutive completions get extra episodes (everything)
|
|
578
|
-
* 3. WEEKEND vs WEEKDAY: Weekend sessions 1.5x longer; weekday prime-time tagging (event)
|
|
579
|
-
* 4. AD FATIGUE CHURN: Free-tier users with 10+ ads churn after day 45 (everything)
|
|
580
|
-
* 5. NEW RELEASE SPIKE: Blockbuster release on day 50 drives content selection (event)
|
|
581
|
-
* 6. KIDS PROFILE SAFETY: Kids mode restricts genres and drops ads (event)
|
|
582
|
-
* 7. RECOMMENDATION ENGINE IMPROVEMENT: Props in funnel-pre; pre-day-60 content rated drop in everything
|
|
583
|
-
* 8. SUBTITLE USERS WATCH MORE: Subtitle-enabled users have higher completion rates (everything)
|
|
584
|
-
*/
|
|
585
543
|
hook: function (record, type, meta) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
record.props.genre_penalty = false;
|
|
604
|
-
} else if (genre === "documentary") {
|
|
605
|
-
record.props.genre_boost = false;
|
|
606
|
-
record.props.genre_penalty = true;
|
|
607
|
-
} else if (!genre && chance.bool({ likelihood: 25 })) {
|
|
608
|
-
// Randomly apply genre effects when genre isn't in funnel props
|
|
609
|
-
if (chance.bool({ likelihood: 60 })) {
|
|
610
|
-
record.props.genre_boost = true;
|
|
611
|
-
record.props.genre_penalty = false;
|
|
612
|
-
} else {
|
|
613
|
-
record.props.genre_boost = false;
|
|
614
|
-
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
|
+
}
|
|
615
561
|
}
|
|
616
|
-
} else {
|
|
617
|
-
record.props.genre_boost = false;
|
|
618
|
-
record.props.genre_penalty = false;
|
|
619
562
|
}
|
|
620
|
-
|
|
621
|
-
// ─────────────────────────────────────────────────────────────
|
|
622
|
-
// Hook #7: RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
|
|
623
|
-
// After day 60, engagement funnel gets 1.5x boost
|
|
624
|
-
// ─────────────────────────────────────────────────────────────
|
|
625
|
-
const seq = record.sequence || [];
|
|
626
|
-
const isEngagementFunnel = seq.includes("recommendation clicked");
|
|
627
|
-
const funnelTime = meta && meta.firstEventTime ? dayjs.unix(meta.firstEventTime) : null;
|
|
628
|
-
const isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));
|
|
629
|
-
|
|
630
|
-
if (isEngagementFunnel && isAfterImprovement) {
|
|
631
|
-
record.props.improved_recs = true;
|
|
632
|
-
} else {
|
|
633
|
-
record.props.improved_recs = false;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return record;
|
|
637
563
|
}
|
|
638
564
|
|
|
639
|
-
// ─────────────────────────────────────────────────────────────
|
|
640
|
-
// Hook #3: WEEKEND vs WEEKDAY PATTERNS (event)
|
|
641
|
-
// Weekend: 1.5x watch duration. Weekday 6PM-11PM: prime_time tag
|
|
642
|
-
// ─────────────────────────────────────────────────────────────
|
|
643
565
|
if (type === "event") {
|
|
566
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
567
|
+
const BLOCKBUSTER_START = datasetStart.add(50, 'days');
|
|
568
|
+
const BLOCKBUSTER_END = datasetStart.add(65, 'days');
|
|
644
569
|
const EVENT_TIME = dayjs(record.time);
|
|
645
|
-
const dayOfWeek = EVENT_TIME.day(); // 0 = Sunday, 6 = Saturday
|
|
646
|
-
const hour = EVENT_TIME.hour();
|
|
647
|
-
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
648
|
-
|
|
649
|
-
if (isWeekend) {
|
|
650
|
-
record.weekend_viewing = true;
|
|
651
|
-
record.prime_time = false;
|
|
652
|
-
if (record.event === "playback completed" && record.watch_duration_min) {
|
|
653
|
-
record.watch_duration_min = Math.round(record.watch_duration_min * 1.5);
|
|
654
|
-
}
|
|
655
|
-
} else {
|
|
656
|
-
record.weekend_viewing = false;
|
|
657
|
-
// Weekday prime-time: 6PM to 11PM
|
|
658
|
-
if (hour >= 18 && hour <= 23) {
|
|
659
|
-
record.prime_time = true;
|
|
660
|
-
} else {
|
|
661
|
-
record.prime_time = false;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
570
|
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// After day 50, blockbuster release drives content selection
|
|
668
|
-
// ─────────────────────────────────────────────────────────────
|
|
669
|
-
const BLOCKBUSTER_RELEASE = DATASET_START.add(50, 'days');
|
|
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.
|
|
670
573
|
if (record.event === "content selected" || record.event === "playback started") {
|
|
671
|
-
if (EVENT_TIME.isAfter(
|
|
574
|
+
if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
|
|
672
575
|
record.content_type = "movie";
|
|
673
576
|
record.content_id = blockbusterId;
|
|
674
|
-
record.blockbuster_release = true;
|
|
675
|
-
} else {
|
|
676
|
-
record.blockbuster_release = false;
|
|
677
577
|
}
|
|
678
578
|
}
|
|
679
|
-
|
|
680
579
|
if (record.event === "content rated") {
|
|
681
|
-
if (EVENT_TIME.isAfter(
|
|
580
|
+
if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
|
|
682
581
|
record.rating = chance.integer({ min: 4, max: 5 });
|
|
683
582
|
record.content_id = blockbusterId;
|
|
684
|
-
record.blockbuster_release = true;
|
|
685
|
-
} else {
|
|
686
|
-
record.blockbuster_release = false;
|
|
687
583
|
}
|
|
688
584
|
}
|
|
689
585
|
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
// 15% of the time, apply kids mode: restrict genres, drop ads
|
|
693
|
-
// ─────────────────────────────────────────────────────────────
|
|
586
|
+
// Hook #6: KIDS PROFILE SAFETY — 15% of selections/starts get genre
|
|
587
|
+
// restricted to animation or documentary. Mutates existing genre prop.
|
|
694
588
|
if (chance.bool({ likelihood: 15 })) {
|
|
695
|
-
record.kids_profile = true;
|
|
696
589
|
if (record.event === "content selected" || record.event === "playback started") {
|
|
697
590
|
record.genre = chance.pickone(["animation", "documentary"]);
|
|
698
|
-
} else if (record.event === "ad impression") {
|
|
699
|
-
record.ad_blocked = true;
|
|
700
|
-
}
|
|
701
|
-
} else {
|
|
702
|
-
record.kids_profile = false;
|
|
703
|
-
if (record.event === "ad impression") {
|
|
704
|
-
record.ad_blocked = false;
|
|
705
591
|
}
|
|
706
592
|
}
|
|
707
593
|
|
|
708
594
|
return record;
|
|
709
595
|
}
|
|
710
596
|
|
|
711
|
-
// ─────────────────────────────────────────────────────────────
|
|
712
|
-
// Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns
|
|
713
|
-
// ─────────────────────────────────────────────────────────────
|
|
714
597
|
if (type === "everything") {
|
|
598
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
715
599
|
const userEvents = record;
|
|
716
600
|
if (!userEvents || userEvents.length === 0) return record;
|
|
717
601
|
|
|
718
602
|
const profile = meta.profile;
|
|
719
603
|
|
|
720
|
-
//
|
|
604
|
+
// Stamp superProps from profile (consistent per user)
|
|
721
605
|
const stampPlan = profile && profile.subscription_plan ? profile.subscription_plan : undefined;
|
|
722
606
|
const stampDevice = profile && profile.device_type ? profile.device_type : undefined;
|
|
723
607
|
if (stampPlan || stampDevice) {
|
|
@@ -727,101 +611,75 @@ const config = {
|
|
|
727
611
|
});
|
|
728
612
|
}
|
|
729
613
|
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
|
|
614
|
+
// Hook #3: WEEKEND VS WEEKDAY — 1.5x watch_duration_min on weekends.
|
|
615
|
+
// No flag — analyst breaks down by day of week.
|
|
616
|
+
for (const e of userEvents) {
|
|
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);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Hook #1: GENRE FUNNEL CONVERSION — drop 25% of documentary playback
|
|
624
|
+
// completed events to depress documentary funnel conversion. Raw genre check.
|
|
733
625
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
734
626
|
const evt = userEvents[i];
|
|
735
|
-
if (evt.event === "playback completed" && evt.
|
|
736
|
-
|
|
737
|
-
userEvents.splice(i, 1);
|
|
738
|
-
}
|
|
627
|
+
if (evt.event === "playback completed" && evt.genre === "documentary" && chance.bool({ likelihood: 25 })) {
|
|
628
|
+
userEvents.splice(i, 1);
|
|
739
629
|
}
|
|
740
630
|
}
|
|
741
631
|
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
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');
|
|
746
635
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
747
636
|
const evt = userEvents[i];
|
|
748
|
-
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE)) {
|
|
749
|
-
|
|
750
|
-
userEvents.splice(i, 1);
|
|
751
|
-
}
|
|
637
|
+
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE) && chance.bool({ likelihood: 30 })) {
|
|
638
|
+
userEvents.splice(i, 1);
|
|
752
639
|
}
|
|
753
640
|
}
|
|
754
641
|
|
|
755
642
|
const firstEventTime = dayjs(userEvents[0].time);
|
|
756
643
|
|
|
757
|
-
//
|
|
644
|
+
// Identify behavioral patterns (no flags written)
|
|
758
645
|
let consecutiveCompletions = 0;
|
|
759
646
|
let maxConsecutiveCompletions = 0;
|
|
760
|
-
let isBingeWatcher = false;
|
|
761
647
|
let adImpressionCount = 0;
|
|
762
648
|
let isFreeTier = false;
|
|
763
649
|
let hasSubtitlesEnabled = false;
|
|
650
|
+
let recClickCount = 0;
|
|
764
651
|
|
|
765
652
|
userEvents.forEach((event, idx) => {
|
|
766
|
-
// Track subscription tier from superProps
|
|
767
653
|
if (idx === 0 && event.subscription_plan) {
|
|
768
654
|
isFreeTier = event.subscription_plan === "free";
|
|
769
655
|
}
|
|
770
|
-
|
|
771
|
-
// Hook #2: Track consecutive playback completions
|
|
772
656
|
if (event.event === "playback completed") {
|
|
773
657
|
consecutiveCompletions++;
|
|
774
658
|
if (consecutiveCompletions > maxConsecutiveCompletions) {
|
|
775
659
|
maxConsecutiveCompletions = consecutiveCompletions;
|
|
776
660
|
}
|
|
777
661
|
} else if (event.event !== "playback started") {
|
|
778
|
-
// Reset streak on non-playback events (started doesn't break streak)
|
|
779
662
|
consecutiveCompletions = 0;
|
|
780
663
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (event.event === "
|
|
784
|
-
adImpressionCount++;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Hook #8: Check for subtitle enabled
|
|
788
|
-
if (event.event === "subtitle toggled" && event.action === "enabled") {
|
|
789
|
-
hasSubtitlesEnabled = true;
|
|
790
|
-
}
|
|
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++;
|
|
791
667
|
});
|
|
792
668
|
|
|
793
|
-
isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
794
|
-
|
|
795
|
-
// ─── Second pass: set schema defaults then modify ───
|
|
796
|
-
|
|
797
|
-
// Set defaults for conditional properties on all events
|
|
798
|
-
userEvents.forEach((event) => {
|
|
799
|
-
if (event.event === "playback completed") {
|
|
800
|
-
if (event.binge_session === undefined) event.binge_session = false;
|
|
801
|
-
if (event.subtitle_user === undefined) event.subtitle_user = false;
|
|
802
|
-
}
|
|
803
|
-
if (event.event === "playback started") {
|
|
804
|
-
if (event.binge_session === undefined) event.binge_session = false;
|
|
805
|
-
}
|
|
806
|
-
if (event.ad_fatigue === undefined) event.ad_fatigue = false;
|
|
807
|
-
});
|
|
669
|
+
const isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
808
670
|
|
|
809
|
-
// Hook #2: BINGE-WATCHING
|
|
810
|
-
//
|
|
671
|
+
// Hook #2: BINGE-WATCHING — drop pauses, inject extra start+complete pairs.
|
|
672
|
+
// Cloned events use unique offset timestamps. No flag.
|
|
811
673
|
if (isBingeWatcher) {
|
|
812
674
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
813
675
|
const event = userEvents[i];
|
|
814
676
|
const eventTime = dayjs(event.time);
|
|
815
677
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
userEvents.splice(i, 1);
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
678
|
+
if (event.event === "playback paused" && chance.bool({ likelihood: 60 })) {
|
|
679
|
+
userEvents.splice(i, 1);
|
|
680
|
+
continue;
|
|
822
681
|
}
|
|
823
682
|
|
|
824
|
-
// Add extra viewing events after completions
|
|
825
683
|
if (event.event === "playback completed" && chance.bool({ likelihood: 40 })) {
|
|
826
684
|
const nextContentId = chance.pickone(contentIds);
|
|
827
685
|
const startTemplate = userEvents.find(e => e.event === "playback started");
|
|
@@ -833,7 +691,6 @@ const config = {
|
|
|
833
691
|
content_id: nextContentId,
|
|
834
692
|
content_type: "series",
|
|
835
693
|
playback_quality: event.playback_quality || "1080p",
|
|
836
|
-
binge_session: true,
|
|
837
694
|
};
|
|
838
695
|
const extraComplete = {
|
|
839
696
|
...event,
|
|
@@ -843,51 +700,42 @@ const config = {
|
|
|
843
700
|
content_type: "series",
|
|
844
701
|
watch_duration_min: chance.integer({ min: 20, max: 55 }),
|
|
845
702
|
completion_percent: chance.integer({ min: 90, max: 100 }),
|
|
846
|
-
binge_session: true,
|
|
847
703
|
};
|
|
848
704
|
userEvents.splice(i + 1, 0, extraStart, extraComplete);
|
|
849
705
|
}
|
|
850
706
|
}
|
|
851
707
|
}
|
|
852
708
|
|
|
853
|
-
// Hook #4: AD FATIGUE CHURN
|
|
854
|
-
//
|
|
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.
|
|
855
711
|
if (isFreeTier && adImpressionCount >= 10) {
|
|
856
712
|
const churnCutoff = firstEventTime.add(45, 'days');
|
|
857
713
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
858
714
|
const evt = userEvents[i];
|
|
859
|
-
if (dayjs(evt.time).isAfter(churnCutoff)) {
|
|
860
|
-
|
|
861
|
-
userEvents.splice(i, 1);
|
|
862
|
-
} else {
|
|
863
|
-
evt.ad_fatigue = true;
|
|
864
|
-
}
|
|
715
|
+
if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 50 })) {
|
|
716
|
+
userEvents.splice(i, 1);
|
|
865
717
|
}
|
|
866
718
|
}
|
|
867
719
|
}
|
|
868
720
|
|
|
869
|
-
// Hook #8: SUBTITLE USERS WATCH MORE
|
|
870
|
-
//
|
|
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.
|
|
871
724
|
if (hasSubtitlesEnabled) {
|
|
872
725
|
for (let i = 0; i < userEvents.length; i++) {
|
|
873
726
|
const event = userEvents[i];
|
|
874
|
-
|
|
875
727
|
if (event.event === "playback completed") {
|
|
876
|
-
// Boost completion percent (cap at 100)
|
|
877
728
|
if (event.completion_percent) {
|
|
878
729
|
event.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));
|
|
879
730
|
}
|
|
880
|
-
// Boost watch duration
|
|
881
731
|
if (event.watch_duration_min) {
|
|
882
732
|
event.watch_duration_min = Math.round(event.watch_duration_min * 1.15);
|
|
883
733
|
}
|
|
884
|
-
event.subtitle_user = true;
|
|
885
734
|
}
|
|
886
735
|
}
|
|
887
736
|
|
|
888
|
-
// Splice extra playback completed events (subtitle users watch more overall)
|
|
889
737
|
const completionEvents = userEvents.filter(e => e.event === "playback completed");
|
|
890
|
-
const extraCount = Math.floor(completionEvents.length * 0.2);
|
|
738
|
+
const extraCount = Math.floor(completionEvents.length * 0.2);
|
|
891
739
|
for (let j = 0; j < extraCount; j++) {
|
|
892
740
|
const templateEvent = chance.pickone(completionEvents);
|
|
893
741
|
const templateTime = dayjs(templateEvent.time);
|
|
@@ -899,12 +747,28 @@ const config = {
|
|
|
899
747
|
content_type: chance.pickone(["movie", "series", "documentary"]),
|
|
900
748
|
watch_duration_min: chance.integer({ min: 25, max: 120 }),
|
|
901
749
|
completion_percent: chance.integer({ min: 80, max: 100 }),
|
|
902
|
-
subtitle_user: true,
|
|
903
750
|
};
|
|
904
751
|
userEvents.push(extraCompletion);
|
|
905
752
|
}
|
|
906
753
|
}
|
|
907
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
|
+
|
|
908
772
|
return record;
|
|
909
773
|
}
|
|
910
774
|
|