@ak--47/dungeon-master 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-media";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -29,7 +29,7 @@ const chance = u.initChance(SEED);
|
|
|
29
29
|
* content with configurable playback options, rate and share content, and manage family
|
|
30
30
|
* profiles under a single account.
|
|
31
31
|
*
|
|
32
|
-
* -
|
|
32
|
+
* - 10,000 users over 120 days
|
|
33
33
|
* - ~600,000 events across 17 event types
|
|
34
34
|
* - 9 funnels (onboarding, content discovery, engagement loop, search, watchlist, etc.)
|
|
35
35
|
* - Subscription tiers: Free (ad-supported), Standard ($9.99/mo), Premium ($14.99/mo)
|
|
@@ -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
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
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
|
-
* disproportionately help certain genres (Hook #1)?
|
|
218
|
+
* REAL-WORLD ANALOGUE: Subtitle adoption correlates with attentive viewers.
|
|
192
219
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
220
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
* 9. RECOMMENDATION-CLICKED MAGIC NUMBER (everything)
|
|
222
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
195
223
|
*
|
|
196
|
-
*
|
|
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.
|
|
197
228
|
*
|
|
198
|
-
*
|
|
199
|
-
* - Cohort by device_type: Do smart TV users watch longer than mobile?
|
|
200
|
-
* - Cohort by subscription_plan: Do premium users binge more, or does
|
|
201
|
-
* ad-free viewing change consumption patterns?
|
|
202
|
-
* - Cohort by preferred_genre: Does genre preference predict churn?
|
|
229
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
203
230
|
*
|
|
204
|
-
*
|
|
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
|
|
205
238
|
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
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
|
|
212
246
|
*
|
|
213
|
-
*
|
|
247
|
+
* REAL-WORLD ANALOGUE: A few good recs surface a watchworthy title; too many
|
|
248
|
+
* clicks signals indecision and drives abandonment.
|
|
214
249
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
250
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
251
|
+
* EXPECTED METRICS SUMMARY
|
|
252
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
253
|
+
*
|
|
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
|
|
@@ -225,12 +271,16 @@ const blockbusterId = `blockbuster_${v.uid(8)}`;
|
|
|
225
271
|
|
|
226
272
|
/** @type {Config} */
|
|
227
273
|
const config = {
|
|
274
|
+
version: 2,
|
|
228
275
|
token,
|
|
229
276
|
seed: SEED,
|
|
230
|
-
|
|
277
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
278
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
279
|
+
// numDays: num_days,
|
|
231
280
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
232
281
|
numUsers: num_users,
|
|
233
|
-
hasAnonIds:
|
|
282
|
+
hasAnonIds: true,
|
|
283
|
+
avgDevicePerUser: 2,
|
|
234
284
|
hasSessionIds: true,
|
|
235
285
|
format: "json",
|
|
236
286
|
gzip: true,
|
|
@@ -320,13 +370,10 @@ const config = {
|
|
|
320
370
|
event: "account created",
|
|
321
371
|
weight: 1,
|
|
322
372
|
isFirstEvent: true,
|
|
373
|
+
isAuthEvent: true,
|
|
323
374
|
properties: {
|
|
324
375
|
"signup_source": ["organic", "referral", "trial_offer", "ad"],
|
|
325
376
|
"plan_selected": ["free", "standard", "premium"],
|
|
326
|
-
"weekend_viewing": [false],
|
|
327
|
-
"prime_time": [false],
|
|
328
|
-
"kids_profile": [false],
|
|
329
|
-
"ad_fatigue": [false],
|
|
330
377
|
}
|
|
331
378
|
},
|
|
332
379
|
{
|
|
@@ -335,10 +382,6 @@ const config = {
|
|
|
335
382
|
properties: {
|
|
336
383
|
"browse_section": ["home", "trending", "new_releases", "genre", "continue_watching"],
|
|
337
384
|
"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
385
|
}
|
|
343
386
|
},
|
|
344
387
|
{
|
|
@@ -348,11 +391,6 @@ const config = {
|
|
|
348
391
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
349
392
|
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
350
393
|
"content_id": contentIds,
|
|
351
|
-
"blockbuster_release": [false],
|
|
352
|
-
"kids_profile": [false],
|
|
353
|
-
"weekend_viewing": [false],
|
|
354
|
-
"prime_time": [false],
|
|
355
|
-
"ad_fatigue": [false],
|
|
356
394
|
}
|
|
357
395
|
},
|
|
358
396
|
{
|
|
@@ -364,12 +402,6 @@ const config = {
|
|
|
364
402
|
"playback_quality": ["480p", "720p", "1080p", "4k"],
|
|
365
403
|
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
366
404
|
"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
405
|
}
|
|
374
406
|
},
|
|
375
407
|
{
|
|
@@ -380,12 +412,6 @@ const config = {
|
|
|
380
412
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
381
413
|
"watch_duration_min": u.weighNumRange(5, 180, 0.5, 45),
|
|
382
414
|
"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
415
|
}
|
|
390
416
|
},
|
|
391
417
|
{
|
|
@@ -394,10 +420,6 @@ const config = {
|
|
|
394
420
|
properties: {
|
|
395
421
|
"content_id": contentIds,
|
|
396
422
|
"pause_reason": ["manual", "ad_break", "buffering", "notification"],
|
|
397
|
-
"weekend_viewing": [false],
|
|
398
|
-
"prime_time": [false],
|
|
399
|
-
"kids_profile": [false],
|
|
400
|
-
"ad_fatigue": [false],
|
|
401
423
|
}
|
|
402
424
|
},
|
|
403
425
|
{
|
|
@@ -407,11 +429,6 @@ const config = {
|
|
|
407
429
|
"content_id": contentIds,
|
|
408
430
|
"rating": u.weighNumRange(1, 5, 2, 4),
|
|
409
431
|
"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
432
|
}
|
|
416
433
|
},
|
|
417
434
|
{
|
|
@@ -421,10 +438,6 @@ const config = {
|
|
|
421
438
|
"content_id": contentIds,
|
|
422
439
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
423
440
|
"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
441
|
}
|
|
429
442
|
},
|
|
430
443
|
{
|
|
@@ -433,10 +446,6 @@ const config = {
|
|
|
433
446
|
properties: {
|
|
434
447
|
"content_id": contentIds,
|
|
435
448
|
"reason": ["watched", "not_interested", "expired"],
|
|
436
|
-
"weekend_viewing": [false],
|
|
437
|
-
"prime_time": [false],
|
|
438
|
-
"kids_profile": [false],
|
|
439
|
-
"ad_fatigue": [false],
|
|
440
449
|
}
|
|
441
450
|
},
|
|
442
451
|
{
|
|
@@ -446,10 +455,6 @@ const config = {
|
|
|
446
455
|
"search_term": () => chance.word(),
|
|
447
456
|
"results_count": u.weighNumRange(0, 50, 0.5, 15),
|
|
448
457
|
"search_type": ["title", "actor", "director", "genre"],
|
|
449
|
-
"weekend_viewing": [false],
|
|
450
|
-
"prime_time": [false],
|
|
451
|
-
"kids_profile": [false],
|
|
452
|
-
"ad_fatigue": [false],
|
|
453
458
|
}
|
|
454
459
|
},
|
|
455
460
|
{
|
|
@@ -458,10 +463,6 @@ const config = {
|
|
|
458
463
|
properties: {
|
|
459
464
|
"algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
|
|
460
465
|
"position": u.weighNumRange(1, 20),
|
|
461
|
-
"weekend_viewing": [false],
|
|
462
|
-
"prime_time": [false],
|
|
463
|
-
"kids_profile": [false],
|
|
464
|
-
"ad_fatigue": [false],
|
|
465
466
|
}
|
|
466
467
|
},
|
|
467
468
|
{
|
|
@@ -469,10 +470,6 @@ const config = {
|
|
|
469
470
|
weight: 4,
|
|
470
471
|
properties: {
|
|
471
472
|
"profile_type": ["main", "kids", "partner", "guest"],
|
|
472
|
-
"weekend_viewing": [false],
|
|
473
|
-
"prime_time": [false],
|
|
474
|
-
"kids_profile": [false],
|
|
475
|
-
"ad_fatigue": [false],
|
|
476
473
|
}
|
|
477
474
|
},
|
|
478
475
|
{
|
|
@@ -482,11 +479,6 @@ const config = {
|
|
|
482
479
|
"ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
|
|
483
480
|
"ad_duration_sec": u.weighNumRange(5, 30),
|
|
484
481
|
"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
482
|
}
|
|
491
483
|
},
|
|
492
484
|
{
|
|
@@ -496,10 +488,6 @@ const config = {
|
|
|
496
488
|
"old_plan": ["free", "standard", "premium"],
|
|
497
489
|
"new_plan": ["free", "standard", "premium"],
|
|
498
490
|
"change_reason": ["upgrade", "downgrade", "cancel", "resubscribe"],
|
|
499
|
-
"weekend_viewing": [false],
|
|
500
|
-
"prime_time": [false],
|
|
501
|
-
"kids_profile": [false],
|
|
502
|
-
"ad_fatigue": [false],
|
|
503
491
|
}
|
|
504
492
|
},
|
|
505
493
|
{
|
|
@@ -509,10 +497,6 @@ const config = {
|
|
|
509
497
|
"content_id": contentIds,
|
|
510
498
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
511
499
|
"download_quality": ["720p", "1080p", "4k"],
|
|
512
|
-
"weekend_viewing": [false],
|
|
513
|
-
"prime_time": [false],
|
|
514
|
-
"kids_profile": [false],
|
|
515
|
-
"ad_fatigue": [false],
|
|
516
500
|
}
|
|
517
501
|
},
|
|
518
502
|
{
|
|
@@ -521,10 +505,6 @@ const config = {
|
|
|
521
505
|
properties: {
|
|
522
506
|
"share_method": ["link", "social", "dm", "email"],
|
|
523
507
|
"content_type": ["movie", "series", "documentary", "special"],
|
|
524
|
-
"weekend_viewing": [false],
|
|
525
|
-
"prime_time": [false],
|
|
526
|
-
"kids_profile": [false],
|
|
527
|
-
"ad_fatigue": [false],
|
|
528
508
|
}
|
|
529
509
|
},
|
|
530
510
|
{
|
|
@@ -533,10 +513,6 @@ const config = {
|
|
|
533
513
|
properties: {
|
|
534
514
|
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
535
515
|
"action": ["enabled", "disabled", "changed"],
|
|
536
|
-
"weekend_viewing": [false],
|
|
537
|
-
"prime_time": [false],
|
|
538
|
-
"kids_profile": [false],
|
|
539
|
-
"ad_fatigue": [false],
|
|
540
516
|
}
|
|
541
517
|
},
|
|
542
518
|
],
|
|
@@ -567,141 +543,48 @@ const config = {
|
|
|
567
543
|
|
|
568
544
|
lookupTables: [],
|
|
569
545
|
|
|
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
546
|
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;
|
|
547
|
+
// Hook #10 (T2C): CORE VIEWING LOOP TIME-TO-CONVERT (funnel-post)
|
|
548
|
+
// Premium subscribers complete browse→play funnel 1.4x faster
|
|
549
|
+
// (factor 0.71); free users 1.25x slower (factor 1.25).
|
|
550
|
+
if (type === "funnel-post") {
|
|
551
|
+
const segment = meta?.profile?.subscription_plan;
|
|
552
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
553
|
+
const factor = (
|
|
554
|
+
segment === "premium" ? 0.71 :
|
|
555
|
+
segment === "free" ? 1.25 :
|
|
556
|
+
1.0
|
|
557
|
+
);
|
|
558
|
+
if (factor !== 1.0) {
|
|
559
|
+
for (let i = 1; i < record.length; i++) {
|
|
560
|
+
const prev = dayjs(record[i - 1].time);
|
|
561
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
562
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
563
|
+
}
|
|
614
564
|
}
|
|
615
|
-
} else {
|
|
616
|
-
record.props.genre_boost = false;
|
|
617
|
-
record.props.genre_penalty = false;
|
|
618
565
|
}
|
|
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
566
|
}
|
|
637
567
|
|
|
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
568
|
if (type === "event") {
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
// Days 50-65: blockbuster release drives content selection.
|
|
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');
|
|
653
|
-
const EVENT_TIME = dayjs(record.time);
|
|
654
|
-
if (record.event === "content selected" || record.event === "playback started") {
|
|
655
|
-
if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
|
|
656
|
-
record.content_type = "movie";
|
|
657
|
-
record.content_id = blockbusterId;
|
|
658
|
-
record.blockbuster_release = true;
|
|
659
|
-
} else {
|
|
660
|
-
record.blockbuster_release = false;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (record.event === "content rated") {
|
|
665
|
-
if (EVENT_TIME.isAfter(BLOCKBUSTER_START) && EVENT_TIME.isBefore(BLOCKBUSTER_END) && chance.bool({ likelihood: 20 })) {
|
|
666
|
-
record.rating = chance.integer({ min: 4, max: 5 });
|
|
667
|
-
record.content_id = blockbusterId;
|
|
668
|
-
record.blockbuster_release = true;
|
|
669
|
-
} else {
|
|
670
|
-
record.blockbuster_release = false;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// ─────────────────────────────────────────────────────────────
|
|
675
|
-
// Hook #6: KIDS PROFILE SAFETY (event)
|
|
676
|
-
// 15% of the time, apply kids mode: restrict genres, drop ads
|
|
677
|
-
// ─────────────────────────────────────────────────────────────
|
|
569
|
+
// Hook #6: KIDS PROFILE SAFETY — 15% of selections/starts get genre
|
|
570
|
+
// restricted to animation or documentary. Mutates existing genre prop.
|
|
678
571
|
if (chance.bool({ likelihood: 15 })) {
|
|
679
|
-
record.kids_profile = true;
|
|
680
572
|
if (record.event === "content selected" || record.event === "playback started") {
|
|
681
573
|
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
574
|
}
|
|
690
575
|
}
|
|
691
576
|
|
|
692
577
|
return record;
|
|
693
578
|
}
|
|
694
579
|
|
|
695
|
-
// ─────────────────────────────────────────────────────────────
|
|
696
|
-
// Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns
|
|
697
|
-
// ─────────────────────────────────────────────────────────────
|
|
698
580
|
if (type === "everything") {
|
|
581
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
699
582
|
const userEvents = record;
|
|
700
583
|
if (!userEvents || userEvents.length === 0) return record;
|
|
701
584
|
|
|
702
585
|
const profile = meta.profile;
|
|
703
586
|
|
|
704
|
-
//
|
|
587
|
+
// Stamp superProps from profile (consistent per user)
|
|
705
588
|
const stampPlan = profile && profile.subscription_plan ? profile.subscription_plan : undefined;
|
|
706
589
|
const stampDevice = profile && profile.device_type ? profile.device_type : undefined;
|
|
707
590
|
if (stampPlan || stampDevice) {
|
|
@@ -711,126 +594,94 @@ const config = {
|
|
|
711
594
|
});
|
|
712
595
|
}
|
|
713
596
|
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
597
|
+
// Hook #5: NEW RELEASE SPIKE — days 50-65, 20% of selections/starts
|
|
598
|
+
// switch to the blockbuster id. Mutates existing content_id/content_type props.
|
|
599
|
+
// (Moved from event hook to everything hook per L1: temporal checks belong here.)
|
|
600
|
+
const BLOCKBUSTER_START = datasetStart.add(50, 'days');
|
|
601
|
+
const BLOCKBUSTER_END = datasetStart.add(65, 'days');
|
|
717
602
|
for (const e of userEvents) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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);
|
|
603
|
+
const eventTime = dayjs(e.time);
|
|
604
|
+
if (eventTime.isAfter(BLOCKBUSTER_START) && eventTime.isBefore(BLOCKBUSTER_END)) {
|
|
605
|
+
if ((e.event === "content selected" || e.event === "playback started") && chance.bool({ likelihood: 20 })) {
|
|
606
|
+
e.content_type = "movie";
|
|
607
|
+
e.content_id = blockbusterId;
|
|
728
608
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
e.prime_time = true;
|
|
733
|
-
} else {
|
|
734
|
-
e.prime_time = false;
|
|
609
|
+
if (e.event === "content rated" && chance.bool({ likelihood: 20 })) {
|
|
610
|
+
e.rating = chance.integer({ min: 4, max: 5 });
|
|
611
|
+
e.content_id = blockbusterId;
|
|
735
612
|
}
|
|
736
613
|
}
|
|
737
614
|
}
|
|
738
615
|
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
|
|
616
|
+
// Hook #3: WEEKEND VS WEEKDAY — 1.5x watch_duration_min on weekends.
|
|
617
|
+
// No flag — analyst breaks down by day of week.
|
|
618
|
+
for (const e of userEvents) {
|
|
619
|
+
const dow = new Date(e.time).getUTCDay();
|
|
620
|
+
if ((dow === 0 || dow === 6) && e.event === "playback completed" && typeof e.watch_duration_min === "number") {
|
|
621
|
+
e.watch_duration_min = Math.round(e.watch_duration_min * 1.5);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Hook #1: GENRE FUNNEL CONVERSION — drop 25% of documentary playback
|
|
626
|
+
// completed events to depress documentary funnel conversion. Raw genre check.
|
|
742
627
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
743
628
|
const evt = userEvents[i];
|
|
744
|
-
if (evt.event === "playback completed" && evt.
|
|
745
|
-
|
|
746
|
-
userEvents.splice(i, 1);
|
|
747
|
-
}
|
|
629
|
+
if (evt.event === "playback completed" && evt.genre === "documentary" && chance.bool({ likelihood: 25 })) {
|
|
630
|
+
userEvents.splice(i, 1);
|
|
748
631
|
}
|
|
749
632
|
}
|
|
750
633
|
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
const IMPROVEMENT_DATE = DATASET_START.add(60, 'days');
|
|
634
|
+
// Hook #7: RECOMMENDATION ENGINE IMPROVEMENT — drop 30% of pre-day-60
|
|
635
|
+
// content rated events. Raw time check.
|
|
636
|
+
const IMPROVEMENT_DATE = datasetStart.add(60, 'days');
|
|
755
637
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
756
638
|
const evt = userEvents[i];
|
|
757
|
-
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE)) {
|
|
758
|
-
|
|
759
|
-
userEvents.splice(i, 1);
|
|
760
|
-
}
|
|
639
|
+
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE) && chance.bool({ likelihood: 30 })) {
|
|
640
|
+
userEvents.splice(i, 1);
|
|
761
641
|
}
|
|
762
642
|
}
|
|
763
643
|
|
|
764
644
|
const firstEventTime = dayjs(userEvents[0].time);
|
|
765
645
|
|
|
766
|
-
//
|
|
646
|
+
// Identify behavioral patterns (no flags written)
|
|
767
647
|
let consecutiveCompletions = 0;
|
|
768
648
|
let maxConsecutiveCompletions = 0;
|
|
769
|
-
let isBingeWatcher = false;
|
|
770
649
|
let adImpressionCount = 0;
|
|
771
650
|
let isFreeTier = false;
|
|
772
651
|
let hasSubtitlesEnabled = false;
|
|
652
|
+
let recClickCount = 0;
|
|
773
653
|
|
|
774
654
|
userEvents.forEach((event, idx) => {
|
|
775
|
-
// Track subscription tier from superProps
|
|
776
655
|
if (idx === 0 && event.subscription_plan) {
|
|
777
656
|
isFreeTier = event.subscription_plan === "free";
|
|
778
657
|
}
|
|
779
|
-
|
|
780
|
-
// Hook #2: Track consecutive playback completions
|
|
781
658
|
if (event.event === "playback completed") {
|
|
782
659
|
consecutiveCompletions++;
|
|
783
660
|
if (consecutiveCompletions > maxConsecutiveCompletions) {
|
|
784
661
|
maxConsecutiveCompletions = consecutiveCompletions;
|
|
785
662
|
}
|
|
786
663
|
} else if (event.event !== "playback started") {
|
|
787
|
-
// Reset streak on non-playback events (started doesn't break streak)
|
|
788
664
|
consecutiveCompletions = 0;
|
|
789
665
|
}
|
|
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
|
-
}
|
|
666
|
+
if (event.event === "ad impression") adImpressionCount++;
|
|
667
|
+
if (event.event === "subtitle toggled" && event.action === "enabled") hasSubtitlesEnabled = true;
|
|
668
|
+
if (event.event === "recommendation clicked") recClickCount++;
|
|
800
669
|
});
|
|
801
670
|
|
|
802
|
-
isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
803
|
-
|
|
804
|
-
// ─── Second pass: set schema defaults then modify ───
|
|
671
|
+
const isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
805
672
|
|
|
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
|
|
673
|
+
// Hook #2: BINGE-WATCHING — drop pauses, inject extra start+complete pairs.
|
|
674
|
+
// Cloned events use unique offset timestamps. No flag.
|
|
820
675
|
if (isBingeWatcher) {
|
|
821
676
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
822
677
|
const event = userEvents[i];
|
|
823
678
|
const eventTime = dayjs(event.time);
|
|
824
679
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
userEvents.splice(i, 1);
|
|
829
|
-
continue;
|
|
830
|
-
}
|
|
680
|
+
if (event.event === "playback paused" && chance.bool({ likelihood: 60 })) {
|
|
681
|
+
userEvents.splice(i, 1);
|
|
682
|
+
continue;
|
|
831
683
|
}
|
|
832
684
|
|
|
833
|
-
// Add extra viewing events after completions
|
|
834
685
|
if (event.event === "playback completed" && chance.bool({ likelihood: 40 })) {
|
|
835
686
|
const nextContentId = chance.pickone(contentIds);
|
|
836
687
|
const startTemplate = userEvents.find(e => e.event === "playback started");
|
|
@@ -842,7 +693,6 @@ const config = {
|
|
|
842
693
|
content_id: nextContentId,
|
|
843
694
|
content_type: "series",
|
|
844
695
|
playback_quality: event.playback_quality || "1080p",
|
|
845
|
-
binge_session: true,
|
|
846
696
|
};
|
|
847
697
|
const extraComplete = {
|
|
848
698
|
...event,
|
|
@@ -852,51 +702,42 @@ const config = {
|
|
|
852
702
|
content_type: "series",
|
|
853
703
|
watch_duration_min: chance.integer({ min: 20, max: 55 }),
|
|
854
704
|
completion_percent: chance.integer({ min: 90, max: 100 }),
|
|
855
|
-
binge_session: true,
|
|
856
705
|
};
|
|
857
706
|
userEvents.splice(i + 1, 0, extraStart, extraComplete);
|
|
858
707
|
}
|
|
859
708
|
}
|
|
860
709
|
}
|
|
861
710
|
|
|
862
|
-
// Hook #4: AD FATIGUE CHURN
|
|
863
|
-
//
|
|
711
|
+
// Hook #4: AD FATIGUE CHURN — free-tier users w/ 10+ ads lose 50% of
|
|
712
|
+
// events after day 45. No flag — discover via cohort retention.
|
|
864
713
|
if (isFreeTier && adImpressionCount >= 10) {
|
|
865
714
|
const churnCutoff = firstEventTime.add(45, 'days');
|
|
866
715
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
867
716
|
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
|
-
}
|
|
717
|
+
if (dayjs(evt.time).isAfter(churnCutoff) && chance.bool({ likelihood: 50 })) {
|
|
718
|
+
userEvents.splice(i, 1);
|
|
874
719
|
}
|
|
875
720
|
}
|
|
876
721
|
}
|
|
877
722
|
|
|
878
|
-
// Hook #8: SUBTITLE USERS WATCH MORE
|
|
879
|
-
//
|
|
723
|
+
// Hook #8: SUBTITLE USERS WATCH MORE — 1.25x completion_percent (cap 100),
|
|
724
|
+
// 1.15x watch_duration_min, plus 20% extra cloned playback completions.
|
|
725
|
+
// No flag — discover via cohort builder on subtitle-toggled-enabled.
|
|
880
726
|
if (hasSubtitlesEnabled) {
|
|
881
727
|
for (let i = 0; i < userEvents.length; i++) {
|
|
882
728
|
const event = userEvents[i];
|
|
883
|
-
|
|
884
729
|
if (event.event === "playback completed") {
|
|
885
|
-
// Boost completion percent (cap at 100)
|
|
886
730
|
if (event.completion_percent) {
|
|
887
731
|
event.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));
|
|
888
732
|
}
|
|
889
|
-
// Boost watch duration
|
|
890
733
|
if (event.watch_duration_min) {
|
|
891
734
|
event.watch_duration_min = Math.round(event.watch_duration_min * 1.15);
|
|
892
735
|
}
|
|
893
|
-
event.subtitle_user = true;
|
|
894
736
|
}
|
|
895
737
|
}
|
|
896
738
|
|
|
897
|
-
// Splice extra playback completed events (subtitle users watch more overall)
|
|
898
739
|
const completionEvents = userEvents.filter(e => e.event === "playback completed");
|
|
899
|
-
const extraCount = Math.floor(completionEvents.length * 0.2);
|
|
740
|
+
const extraCount = Math.floor(completionEvents.length * 0.2);
|
|
900
741
|
for (let j = 0; j < extraCount; j++) {
|
|
901
742
|
const templateEvent = chance.pickone(completionEvents);
|
|
902
743
|
const templateTime = dayjs(templateEvent.time);
|
|
@@ -908,12 +749,28 @@ const config = {
|
|
|
908
749
|
content_type: chance.pickone(["movie", "series", "documentary"]),
|
|
909
750
|
watch_duration_min: chance.integer({ min: 25, max: 120 }),
|
|
910
751
|
completion_percent: chance.integer({ min: 80, max: 100 }),
|
|
911
|
-
subtitle_user: true,
|
|
912
752
|
};
|
|
913
753
|
userEvents.push(extraCompletion);
|
|
914
754
|
}
|
|
915
755
|
}
|
|
916
756
|
|
|
757
|
+
// Hook #9: RECOMMENDATION-CLICKED MAGIC NUMBER (no flags)
|
|
758
|
+
// Sweet 4-6 rec clicks → +25% watch_duration_min on playback completed.
|
|
759
|
+
// Over 7+ → drop 30% of playback completed events (rec fatigue).
|
|
760
|
+
if (recClickCount >= 4 && recClickCount <= 6) {
|
|
761
|
+
userEvents.forEach(e => {
|
|
762
|
+
if (e.event === 'playback completed' && typeof e.watch_duration_min === 'number') {
|
|
763
|
+
e.watch_duration_min = Math.round(e.watch_duration_min * 1.25);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
} else if (recClickCount >= 7) {
|
|
767
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
768
|
+
if (userEvents[i].event === 'playback completed' && chance.bool({ likelihood: 30 })) {
|
|
769
|
+
userEvents.splice(i, 1);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
917
774
|
return record;
|
|
918
775
|
}
|
|
919
776
|
|