@ak--47/dungeon-master 1.0.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/README.md +518 -0
- package/dungeons/array-of-object-lookup-schema.json +327 -0
- package/dungeons/array-of-object-lookup.js +220 -0
- package/dungeons/ecommerce-schema.json +462 -0
- package/dungeons/ecommerce.js +447 -0
- package/dungeons/education-schema.json +2409 -0
- package/dungeons/education.js +768 -0
- package/dungeons/fintech-schema.json +14034 -0
- package/dungeons/fintech.js +696 -0
- package/dungeons/foobar-schema.json +403 -0
- package/dungeons/foobar.js +296 -0
- package/dungeons/food-delivery-schema.json +192 -0
- package/dungeons/food-delivery.js +602 -0
- package/dungeons/food-schema.json +1152 -0
- package/dungeons/food.js +754 -0
- package/dungeons/gaming-schema.json +1270 -0
- package/dungeons/gaming.js +508 -0
- package/dungeons/insurance-application-schema.json +204 -0
- package/dungeons/insurance-application.js +605 -0
- package/dungeons/media-schema.json +906 -0
- package/dungeons/media.js +790 -0
- package/dungeons/retention-cadence-schema.json +78 -0
- package/dungeons/retention-cadence.js +244 -0
- package/dungeons/rpg-schema.json +4526 -0
- package/dungeons/rpg.js +919 -0
- package/dungeons/sanity-schema.json +255 -0
- package/dungeons/sanity.js +152 -0
- package/dungeons/sass-schema.json +1291 -0
- package/dungeons/sass.js +795 -0
- package/dungeons/scd-schema.json +919 -0
- package/dungeons/scd.js +277 -0
- package/dungeons/simple-schema.json +608 -0
- package/dungeons/simple.js +285 -0
- package/dungeons/simplest-schema.json +1418 -0
- package/dungeons/simplest.js +392 -0
- package/dungeons/social-schema.json +1118 -0
- package/dungeons/social.js +686 -0
- package/dungeons/text-generation-schema.json +3096 -0
- package/dungeons/text-generation.js +812 -0
- package/index.js +567 -0
- package/lib/core/config-validator.js +395 -0
- package/lib/core/context.js +204 -0
- package/lib/core/dungeon-loader.js +337 -0
- package/lib/core/storage.js +379 -0
- package/lib/generators/adspend.js +132 -0
- package/lib/generators/events.js +271 -0
- package/lib/generators/funnels.js +407 -0
- package/lib/generators/mirror.js +167 -0
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +124 -0
- package/lib/generators/text.js +1192 -0
- package/lib/orchestrators/mixpanel-sender.js +266 -0
- package/lib/orchestrators/user-loop.js +335 -0
- package/lib/templates/abbreviated.d.ts +169 -0
- package/lib/templates/defaults.js +1405 -0
- package/lib/templates/phrases.js +2526 -0
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/soup-presets.js +188 -0
- package/lib/utils/function-registry.js +302 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +34 -0
- package/lib/utils/utils.js +1490 -0
- package/package.json +89 -0
- package/types.d.ts +865 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
|
+
import * as v from "ak-tools";
|
|
6
|
+
|
|
7
|
+
const SEED = "harness-media";
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
const chance = u.initChance(SEED);
|
|
10
|
+
const num_users = 5_000;
|
|
11
|
+
const days = 100;
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../types.js").Dungeon} Config */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
* DATASET OVERVIEW — STREAMVAULT VIDEO STREAMING PLATFORM
|
|
18
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
*
|
|
20
|
+
* StreamVault is a Netflix/Hulu-style video streaming platform where users browse a rich
|
|
21
|
+
* catalog of movies, series, documentaries, and specials. Users manage watchlists, watch
|
|
22
|
+
* content with configurable playback options, rate and share content, and manage family
|
|
23
|
+
* profiles under a single account.
|
|
24
|
+
*
|
|
25
|
+
* - 5,000 users over 100 days
|
|
26
|
+
* - ~600,000 events across 17 event types
|
|
27
|
+
* - 9 funnels (onboarding, content discovery, engagement loop, search, watchlist, etc.)
|
|
28
|
+
* - Subscription tiers: Free (ad-supported), Standard ($9.99/mo), Premium ($14.99/mo)
|
|
29
|
+
* - Device types: Smart TV, Mobile, Tablet, Laptop, Desktop
|
|
30
|
+
* - Content catalog: 500 titles with genres, types, and a blockbuster release event
|
|
31
|
+
*
|
|
32
|
+
* Core loop: onboarding -> discovery -> consumption -> engagement -> monetization.
|
|
33
|
+
* Users land on a personalized home screen, discover content via browse/search/recommendations,
|
|
34
|
+
* watch with quality and subtitle options, rate and share, and manage subscriptions.
|
|
35
|
+
* Profile switching (main, kids, partner, guest) reveals household dynamics.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
40
|
+
* ANALYTICS HOOKS (8 architected patterns)
|
|
41
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
42
|
+
*
|
|
43
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
* 1. GENRE FUNNEL CONVERSION (funnel-pre)
|
|
45
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* PATTERN: Comedy and Animation content has 1.3x higher funnel conversion rates,
|
|
48
|
+
* while Documentary content has 0.7x conversion (users browse but abandon more).
|
|
49
|
+
*
|
|
50
|
+
* HOW TO FIND IT:
|
|
51
|
+
* - Funnels: "content browsed" -> "content selected" -> "playback started" -> "playback completed"
|
|
52
|
+
* - Breakdown: "genre"
|
|
53
|
+
* - Expected: Comedy/Animation convert at ~65% vs Documentary at ~35%
|
|
54
|
+
* - Also try: Insights on "content selected", breakdown by "genre_boost"
|
|
55
|
+
*
|
|
56
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
* 2. BINGE-WATCHING PATTERN (everything)
|
|
58
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
*
|
|
60
|
+
* PATTERN: Users who complete 3+ episodes consecutively become "binge-watchers":
|
|
61
|
+
* - Extra playback started + playback completed events are spliced in
|
|
62
|
+
* - Completion percentages are 90-100% (they finish every episode)
|
|
63
|
+
* - Pause events are reduced by 60% (they don't stop watching)
|
|
64
|
+
* - Events tagged with binge_session = true
|
|
65
|
+
*
|
|
66
|
+
* HOW TO FIND IT:
|
|
67
|
+
* - Insights: "playback completed", total events per user, breakdown by "binge_session"
|
|
68
|
+
* - Expected: binge_session=true users have 40-60% more completions per user
|
|
69
|
+
* - Also try: Average of "completion_percent" by "binge_session" (90-100% vs ~70%)
|
|
70
|
+
*
|
|
71
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
* 3. WEEKEND vs WEEKDAY PATTERNS (event)
|
|
73
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
*
|
|
75
|
+
* PATTERN: Weekend viewing sessions are 1.5x longer than weekday sessions.
|
|
76
|
+
* Weekday viewing concentrates in evening prime-time (6PM-11PM).
|
|
77
|
+
*
|
|
78
|
+
* HOW TO FIND IT:
|
|
79
|
+
* - Insights: "playback completed", average of "watch_duration_min", breakdown by "weekend_viewing"
|
|
80
|
+
* - Expected: weekend_viewing=true ~67 min avg vs false ~45 min avg (1.5x)
|
|
81
|
+
* - Also try: Total "playback completed" events by "prime_time" (60-70% of weekday views)
|
|
82
|
+
*
|
|
83
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
* 4. AD FATIGUE CHURN (everything)
|
|
85
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
*
|
|
87
|
+
* PATTERN: Free-tier users who see 10+ ad impressions experience 50% churn
|
|
88
|
+
* after day 45 of their lifecycle.
|
|
89
|
+
*
|
|
90
|
+
* HOW TO FIND IT:
|
|
91
|
+
* - Retention: any event (filter subscription_plan=free), segment by ad impression count >= 10 vs < 10
|
|
92
|
+
* - Expected: 10+ ad users show ~50% retention drop after day 45
|
|
93
|
+
* - Also try: Insights line chart, any event filtered by "ad_fatigue" = true, weekly trend
|
|
94
|
+
*
|
|
95
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
* 5. NEW RELEASE SPIKE (event)
|
|
97
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
*
|
|
99
|
+
* PATTERN: On day 50, a blockbuster movie releases, creating a content spike:
|
|
100
|
+
* - 20% of content selected and playback started events redirect to the blockbuster
|
|
101
|
+
* - Content rated events for the blockbuster skew to 4-5 star ratings
|
|
102
|
+
* - All affected events tagged with blockbuster_release = true
|
|
103
|
+
*
|
|
104
|
+
* HOW TO FIND IT:
|
|
105
|
+
* - Insights line chart: "content selected", filter "blockbuster_release" = true, daily trend
|
|
106
|
+
* - Expected: Zero before day 50, then ~20% of content selections
|
|
107
|
+
* - Also try: "content rated" average of "rating" by "blockbuster_release" (4-5 vs ~3.5)
|
|
108
|
+
*
|
|
109
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
* 6. KIDS PROFILE SAFETY (event)
|
|
111
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
*
|
|
113
|
+
* PATTERN: 15% of the time, events are tagged as kids profile activity:
|
|
114
|
+
* - Content selection restricted to animation and documentary genres
|
|
115
|
+
* - Ad impressions are blocked (ad_blocked = true)
|
|
116
|
+
* - Events tagged with kids_profile = true
|
|
117
|
+
*
|
|
118
|
+
* HOW TO FIND IT:
|
|
119
|
+
* - Insights: "content selected", breakdown by "genre", filter "kids_profile" = true
|
|
120
|
+
* - Expected: 100% animation and documentary genres only
|
|
121
|
+
* - Also try: "ad impression" breakdown by "kids_profile" — kids should have ad_blocked=true
|
|
122
|
+
*
|
|
123
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
124
|
+
* 7. RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
|
|
125
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
*
|
|
127
|
+
* PATTERN: After day 60, the engagement loop funnel (recommendation clicked ->
|
|
128
|
+
* playback started -> content rated) gets a 1.5x conversion rate boost,
|
|
129
|
+
* simulating a recommendation engine improvement.
|
|
130
|
+
*
|
|
131
|
+
* HOW TO FIND IT:
|
|
132
|
+
* - Funnels: "recommendation clicked" -> "playback started" -> "playback completed" -> "content rated"
|
|
133
|
+
* - Breakdown: "improved_recs"
|
|
134
|
+
* - Expected: improved_recs=true shows ~1.5x higher conversion rate
|
|
135
|
+
* - Also try: Insights line chart of "recommendation clicked" filtered by "improved_recs" = true
|
|
136
|
+
*
|
|
137
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
* 8. SUBTITLE USERS WATCH MORE (everything)
|
|
139
|
+
* ───────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
*
|
|
141
|
+
* PATTERN: Users who enable subtitles have measurably higher engagement:
|
|
142
|
+
* - 25% higher completion_percent on playback completed events (capped at 100)
|
|
143
|
+
* - 15% longer watch_duration_min
|
|
144
|
+
* - 20% more playback completed events (extra content consumption)
|
|
145
|
+
* - Events tagged with subtitle_user = true
|
|
146
|
+
*
|
|
147
|
+
* HOW TO FIND IT:
|
|
148
|
+
* - Insights: "playback completed", average of "completion_percent", breakdown by "subtitle_user"
|
|
149
|
+
* - Expected: subtitle_user=true ~85% completion vs false ~68% (1.25x)
|
|
150
|
+
* - Also try: Average of "watch_duration_min" by "subtitle_user" (~15% longer)
|
|
151
|
+
* - Also try: Total events per user by "subtitle_user" (~20% more completions)
|
|
152
|
+
*
|
|
153
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
154
|
+
* EXPECTED METRICS SUMMARY
|
|
155
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
156
|
+
*
|
|
157
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
158
|
+
* ─────────────────────────|─────────────────────────|───────────|─────────────|──────
|
|
159
|
+
* Genre Funnel Conversion | Funnel conversion rate | 50% | 65% / 35% | 1.3x / 0.7x
|
|
160
|
+
* Binge-Watching | Content consumed/user | 12 | 18-20 | ~1.5x
|
|
161
|
+
* Weekend vs Weekday | Watch duration (min) | 45 | 67 (weekend)| 1.5x
|
|
162
|
+
* Ad Fatigue Churn | Post-day-45 activity | 100% | 50% | 0.5x
|
|
163
|
+
* New Release Spike | Content selections/day | baseline | +20% spike | 1.2x
|
|
164
|
+
* Kids Profile Safety | Ad impressions | normal | 0 (dropped) | 0x
|
|
165
|
+
* Rec Engine Improvement | Engagement funnel conv | 30% | 45% | 1.5x
|
|
166
|
+
* Subtitle Users | Completion percent | 68% | 85% | 1.25x
|
|
167
|
+
*
|
|
168
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
169
|
+
* ADVANCED ANALYSIS IDEAS
|
|
170
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
171
|
+
*
|
|
172
|
+
* CROSS-HOOK PATTERNS:
|
|
173
|
+
*
|
|
174
|
+
* 1. Binge + Subtitle: Do subtitle-enabled binge-watchers have the highest
|
|
175
|
+
* total watch hours? (Hooks #2 + #8 combined)
|
|
176
|
+
*
|
|
177
|
+
* 2. Ad Fatigue + Blockbuster: Does the blockbuster release (Hook #5) rescue
|
|
178
|
+
* free-tier users from ad fatigue churn (Hook #4)?
|
|
179
|
+
*
|
|
180
|
+
* 3. Kids + Weekend: Is kids profile viewing concentrated on weekends (Hook #6
|
|
181
|
+
* + #3)? Does weekend kids viewing show different genre preferences?
|
|
182
|
+
*
|
|
183
|
+
* 4. Rec Engine + Genre: Does the recommendation engine improvement (Hook #7)
|
|
184
|
+
* disproportionately help certain genres (Hook #1)?
|
|
185
|
+
*
|
|
186
|
+
* 5. Subtitle + Binge + Weekend: The "super viewer" - subtitle-enabled,
|
|
187
|
+
* binge-watching on weekends. What is their lifetime watch hours?
|
|
188
|
+
*
|
|
189
|
+
* COHORT ANALYSIS:
|
|
190
|
+
*
|
|
191
|
+
* - Cohort by signup_source: Do referral users binge more than organic?
|
|
192
|
+
* - Cohort by device_type: Do smart TV users watch longer than mobile?
|
|
193
|
+
* - Cohort by subscription_plan: Do premium users binge more, or does
|
|
194
|
+
* ad-free viewing change consumption patterns?
|
|
195
|
+
* - Cohort by preferred_genre: Does genre preference predict churn?
|
|
196
|
+
*
|
|
197
|
+
* FUNNEL ANALYSIS:
|
|
198
|
+
*
|
|
199
|
+
* - Onboarding Funnel: account created -> content browsed -> playback started.
|
|
200
|
+
* How does signup_source affect first-session conversion?
|
|
201
|
+
* - Content Discovery Funnel: Does the browse_section (home vs trending vs
|
|
202
|
+
* genre) affect downstream completion rates?
|
|
203
|
+
* - Engagement Loop: How does recommendation algorithm type (collaborative
|
|
204
|
+
* filtering vs editorial) affect the full loop conversion?
|
|
205
|
+
*
|
|
206
|
+
* MONETIZATION ANALYSIS:
|
|
207
|
+
*
|
|
208
|
+
* - Free-to-Standard conversion: Which events predict upgrade?
|
|
209
|
+
* - Ad tolerance threshold: At what ad count do free users start churning?
|
|
210
|
+
* - Premium value: Do premium users actually consume more content, or just
|
|
211
|
+
* consume at higher quality (4K)?
|
|
212
|
+
* - Download behavior: Does offline download usage correlate with retention?
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
// Generate consistent content IDs for lookup tables and events
|
|
216
|
+
const contentIds = v.range(1, 501).map(n => `content_${v.uid(8)}`);
|
|
217
|
+
const blockbusterId = `blockbuster_${v.uid(8)}`;
|
|
218
|
+
|
|
219
|
+
/** @type {Config} */
|
|
220
|
+
const config = {
|
|
221
|
+
token: "",
|
|
222
|
+
seed: SEED,
|
|
223
|
+
numDays: days,
|
|
224
|
+
numEvents: num_users * 120,
|
|
225
|
+
numUsers: num_users,
|
|
226
|
+
hasAnonIds: false,
|
|
227
|
+
hasSessionIds: true,
|
|
228
|
+
format: "json",
|
|
229
|
+
gzip: true,
|
|
230
|
+
alsoInferFunnels: false,
|
|
231
|
+
hasLocation: true,
|
|
232
|
+
hasAndroidDevices: true,
|
|
233
|
+
hasIOSDevices: true,
|
|
234
|
+
hasDesktopDevices: true,
|
|
235
|
+
hasBrowser: false,
|
|
236
|
+
hasCampaigns: false,
|
|
237
|
+
isAnonymous: false,
|
|
238
|
+
hasAdSpend: false,
|
|
239
|
+
percentUsersBornInDataset: 50,
|
|
240
|
+
hasAvatar: true,
|
|
241
|
+
batchSize: 2_500_000,
|
|
242
|
+
concurrency: 1,
|
|
243
|
+
writeToDisk: false,
|
|
244
|
+
|
|
245
|
+
funnels: [
|
|
246
|
+
{
|
|
247
|
+
sequence: ["account created", "content browsed", "playback started"],
|
|
248
|
+
isFirstFunnel: true,
|
|
249
|
+
conversionRate: 80,
|
|
250
|
+
timeToConvert: 0.25,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
// Core viewing loop: browse → select → watch → finish (most common)
|
|
254
|
+
sequence: ["content browsed", "content selected", "playback started", "playback completed"],
|
|
255
|
+
conversionRate: 55,
|
|
256
|
+
timeToConvert: 2,
|
|
257
|
+
weight: 5,
|
|
258
|
+
props: {
|
|
259
|
+
genre: ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
// Recommendation-driven viewing
|
|
264
|
+
sequence: ["recommendation clicked", "playback started", "playback completed", "content rated"],
|
|
265
|
+
conversionRate: 35,
|
|
266
|
+
timeToConvert: 1,
|
|
267
|
+
weight: 3,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
// Search-driven discovery
|
|
271
|
+
sequence: ["search performed", "content selected", "playback started"],
|
|
272
|
+
conversionRate: 50,
|
|
273
|
+
timeToConvert: 0.5,
|
|
274
|
+
weight: 3,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
// Watchlist management
|
|
278
|
+
sequence: ["content browsed", "watchlist added", "content selected", "playback started"],
|
|
279
|
+
conversionRate: 40,
|
|
280
|
+
timeToConvert: 12,
|
|
281
|
+
weight: 2,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
// Profile and subtitle management
|
|
285
|
+
sequence: ["profile switched", "subtitle toggled", "playback started", "playback completed"],
|
|
286
|
+
conversionRate: 45,
|
|
287
|
+
timeToConvert: 1,
|
|
288
|
+
weight: 2,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
// Ad experience (free tier)
|
|
292
|
+
sequence: ["ad impression", "playback started", "playback paused"],
|
|
293
|
+
conversionRate: 60,
|
|
294
|
+
timeToConvert: 0.5,
|
|
295
|
+
weight: 2,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
// Content sharing and downloads
|
|
299
|
+
sequence: ["playback completed", "share content", "download started"],
|
|
300
|
+
conversionRate: 25,
|
|
301
|
+
timeToConvert: 1,
|
|
302
|
+
weight: 1,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
// Subscription changes
|
|
306
|
+
sequence: ["content browsed", "subscription changed"],
|
|
307
|
+
conversionRate: 15,
|
|
308
|
+
timeToConvert: 24,
|
|
309
|
+
weight: 1,
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
|
|
313
|
+
events: [
|
|
314
|
+
{
|
|
315
|
+
event: "account created",
|
|
316
|
+
weight: 1,
|
|
317
|
+
isFirstEvent: true,
|
|
318
|
+
properties: {
|
|
319
|
+
"signup_source": ["organic", "referral", "trial_offer", "ad"],
|
|
320
|
+
"plan_selected": ["free", "standard", "premium"],
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
event: "content browsed",
|
|
325
|
+
weight: 20,
|
|
326
|
+
properties: {
|
|
327
|
+
"browse_section": ["home", "trending", "new_releases", "genre", "continue_watching"],
|
|
328
|
+
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
event: "content selected",
|
|
333
|
+
weight: 15,
|
|
334
|
+
properties: {
|
|
335
|
+
"content_type": ["movie", "series", "documentary", "special"],
|
|
336
|
+
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
337
|
+
"content_id": u.pickAWinner(contentIds),
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
event: "playback started",
|
|
342
|
+
weight: 18,
|
|
343
|
+
properties: {
|
|
344
|
+
"content_id": u.pickAWinner(contentIds),
|
|
345
|
+
"content_type": ["movie", "series", "documentary", "special"],
|
|
346
|
+
"playback_quality": ["480p", "720p", "1080p", "4k"],
|
|
347
|
+
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
348
|
+
"playback_speed": u.pickAWinner(["0.5x", "1x", "1x", "1x", "1.25x", "1.5x", "2x"]),
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
event: "playback completed",
|
|
353
|
+
weight: 12,
|
|
354
|
+
properties: {
|
|
355
|
+
"content_id": u.pickAWinner(contentIds),
|
|
356
|
+
"content_type": ["movie", "series", "documentary", "special"],
|
|
357
|
+
"watch_duration_min": u.weighNumRange(5, 180, 0.5, 45),
|
|
358
|
+
"completion_percent": u.weighNumRange(10, 100, 1.5, 85),
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
event: "playback paused",
|
|
363
|
+
weight: 10,
|
|
364
|
+
properties: {
|
|
365
|
+
"content_id": u.pickAWinner(contentIds),
|
|
366
|
+
"pause_reason": ["manual", "ad_break", "buffering", "notification"],
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
event: "content rated",
|
|
371
|
+
weight: 6,
|
|
372
|
+
properties: {
|
|
373
|
+
"content_id": u.pickAWinner(contentIds),
|
|
374
|
+
"rating": u.weighNumRange(1, 5, 2, 4),
|
|
375
|
+
"review_text_length": u.weighNumRange(0, 500, 0.2, 0),
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
event: "watchlist added",
|
|
380
|
+
weight: 8,
|
|
381
|
+
properties: {
|
|
382
|
+
"content_id": u.pickAWinner(contentIds),
|
|
383
|
+
"content_type": ["movie", "series", "documentary", "special"],
|
|
384
|
+
"genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation", "thriller", "romance"],
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
event: "watchlist removed",
|
|
389
|
+
weight: 3,
|
|
390
|
+
properties: {
|
|
391
|
+
"content_id": u.pickAWinner(contentIds),
|
|
392
|
+
"reason": ["watched", "not_interested", "expired"],
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
event: "search performed",
|
|
397
|
+
weight: 7,
|
|
398
|
+
properties: {
|
|
399
|
+
"search_term": () => chance.word(),
|
|
400
|
+
"results_count": u.weighNumRange(0, 50, 0.5, 15),
|
|
401
|
+
"search_type": ["title", "actor", "director", "genre"],
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
event: "recommendation clicked",
|
|
406
|
+
weight: 9,
|
|
407
|
+
properties: {
|
|
408
|
+
"algorithm": ["collaborative_filtering", "content_based", "trending", "editorial"],
|
|
409
|
+
"position": u.weighNumRange(1, 20),
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
event: "profile switched",
|
|
414
|
+
weight: 4,
|
|
415
|
+
properties: {
|
|
416
|
+
"profile_type": ["main", "kids", "partner", "guest"],
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
event: "ad impression",
|
|
421
|
+
weight: 8,
|
|
422
|
+
properties: {
|
|
423
|
+
"ad_type": ["pre_roll", "mid_roll", "banner", "interstitial"],
|
|
424
|
+
"ad_duration_sec": u.weighNumRange(5, 30),
|
|
425
|
+
"skipped": u.pickAWinner([true, false], 0.4),
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
event: "subscription changed",
|
|
430
|
+
weight: 2,
|
|
431
|
+
properties: {
|
|
432
|
+
"old_plan": ["free", "standard", "premium"],
|
|
433
|
+
"new_plan": ["free", "standard", "premium"],
|
|
434
|
+
"change_reason": ["upgrade", "downgrade", "cancel", "resubscribe"],
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
event: "download started",
|
|
439
|
+
weight: 5,
|
|
440
|
+
properties: {
|
|
441
|
+
"content_id": u.pickAWinner(contentIds),
|
|
442
|
+
"content_type": ["movie", "series", "documentary", "special"],
|
|
443
|
+
"download_quality": ["720p", "1080p", "4k"],
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
event: "share content",
|
|
448
|
+
weight: 3,
|
|
449
|
+
properties: {
|
|
450
|
+
"share_method": ["link", "social", "dm", "email"],
|
|
451
|
+
"content_type": ["movie", "series", "documentary", "special"],
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
event: "subtitle toggled",
|
|
456
|
+
weight: 4,
|
|
457
|
+
properties: {
|
|
458
|
+
"subtitle_language": ["none", "english", "spanish", "french", "japanese", "korean"],
|
|
459
|
+
"action": ["enabled", "disabled", "changed"],
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
|
|
464
|
+
superProps: {
|
|
465
|
+
subscription_plan: u.pickAWinner(["free", "free", "standard", "standard", "standard", "premium"]),
|
|
466
|
+
device_type: ["smart_tv", "mobile", "tablet", "laptop", "desktop"],
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
scdProps: {},
|
|
470
|
+
|
|
471
|
+
userProps: {
|
|
472
|
+
"preferred_genre": ["action", "comedy", "drama", "documentary", "horror", "sci_fi", "animation"],
|
|
473
|
+
"avg_session_duration_min": u.weighNumRange(10, 180, 0.5, 45),
|
|
474
|
+
"total_watch_hours": u.weighNumRange(0, 500, 0.8, 50),
|
|
475
|
+
"profiles_count": u.weighNumRange(1, 5),
|
|
476
|
+
"downloads_enabled": u.pickAWinner([true, false], 0.4),
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
lookupTables: [],
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* ARCHITECTED ANALYTICS HOOKS
|
|
483
|
+
*
|
|
484
|
+
* This hook function creates 8 deliberate patterns in the data:
|
|
485
|
+
*
|
|
486
|
+
* 1. GENRE FUNNEL CONVERSION: Comedy/Animation complete more; Documentary abandons more (funnel-pre)
|
|
487
|
+
* 2. BINGE-WATCHING: Users with 3+ consecutive completions get extra episodes (everything)
|
|
488
|
+
* 3. WEEKEND vs WEEKDAY: Weekend sessions 1.5x longer; weekday prime-time tagging (event)
|
|
489
|
+
* 4. AD FATIGUE CHURN: Free-tier users with 10+ ads churn after day 45 (everything)
|
|
490
|
+
* 5. NEW RELEASE SPIKE: Blockbuster release on day 50 drives content selection (event)
|
|
491
|
+
* 6. KIDS PROFILE SAFETY: Kids mode restricts genres and drops ads (event)
|
|
492
|
+
* 7. RECOMMENDATION ENGINE IMPROVEMENT: Post-day-60 boost to engagement funnel (funnel-pre)
|
|
493
|
+
* 8. SUBTITLE USERS WATCH MORE: Subtitle-enabled users have higher completion rates (everything)
|
|
494
|
+
*/
|
|
495
|
+
hook: function (record, type, meta) {
|
|
496
|
+
const NOW = dayjs();
|
|
497
|
+
const DATASET_START = NOW.subtract(days, 'days');
|
|
498
|
+
// FIXED_START is the pre-shift time range start; needed because
|
|
499
|
+
// meta.firstEventTime in funnel-pre is in the FIXED (pre-shift) timeframe
|
|
500
|
+
const FIXED_START = dayjs.unix(global.FIXED_NOW).subtract(days, 'days');
|
|
501
|
+
|
|
502
|
+
// ─────────────────────────────────────────────────────────────
|
|
503
|
+
// Hook #1: GENRE FUNNEL CONVERSION (funnel-pre)
|
|
504
|
+
// Comedy/Animation funnels convert 1.3x better; Documentary 0.7x
|
|
505
|
+
// ─────────────────────────────────────────────────────────────
|
|
506
|
+
if (type === "funnel-pre") {
|
|
507
|
+
record.props = record.props || {};
|
|
508
|
+
const genre = record.props.genre;
|
|
509
|
+
|
|
510
|
+
if (genre === "comedy" || genre === "animation") {
|
|
511
|
+
record.conversionRate = record.conversionRate * 1.3;
|
|
512
|
+
record.props.genre_boost = true;
|
|
513
|
+
record.props.genre_penalty = false;
|
|
514
|
+
} else if (genre === "documentary") {
|
|
515
|
+
record.conversionRate = record.conversionRate * 0.7;
|
|
516
|
+
record.props.genre_boost = false;
|
|
517
|
+
record.props.genre_penalty = true;
|
|
518
|
+
} else if (!genre && chance.bool({ likelihood: 25 })) {
|
|
519
|
+
// Randomly apply genre effects when genre isn't in funnel props
|
|
520
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
521
|
+
record.conversionRate = record.conversionRate * 1.3;
|
|
522
|
+
record.props.genre_boost = true;
|
|
523
|
+
record.props.genre_penalty = false;
|
|
524
|
+
} else {
|
|
525
|
+
record.conversionRate = record.conversionRate * 0.7;
|
|
526
|
+
record.props.genre_boost = false;
|
|
527
|
+
record.props.genre_penalty = true;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
record.props.genre_boost = false;
|
|
531
|
+
record.props.genre_penalty = false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ─────────────────────────────────────────────────────────────
|
|
535
|
+
// Hook #7: RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)
|
|
536
|
+
// After day 60, engagement funnel gets 1.5x boost
|
|
537
|
+
// ─────────────────────────────────────────────────────────────
|
|
538
|
+
const seq = record.sequence || [];
|
|
539
|
+
const isEngagementFunnel = seq.includes("recommendation clicked");
|
|
540
|
+
const funnelTime = meta && meta.firstEventTime ? dayjs.unix(meta.firstEventTime) : null;
|
|
541
|
+
const isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));
|
|
542
|
+
|
|
543
|
+
if (isEngagementFunnel && isAfterImprovement) {
|
|
544
|
+
record.conversionRate = record.conversionRate * 1.5;
|
|
545
|
+
record.props.improved_recs = true;
|
|
546
|
+
} else {
|
|
547
|
+
record.props.improved_recs = false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return record;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ─────────────────────────────────────────────────────────────
|
|
554
|
+
// Hook #3: WEEKEND vs WEEKDAY PATTERNS (event)
|
|
555
|
+
// Weekend: 1.5x watch duration. Weekday 6PM-11PM: prime_time tag
|
|
556
|
+
// ─────────────────────────────────────────────────────────────
|
|
557
|
+
if (type === "event") {
|
|
558
|
+
const EVENT_TIME = dayjs(record.time);
|
|
559
|
+
const dayOfWeek = EVENT_TIME.day(); // 0 = Sunday, 6 = Saturday
|
|
560
|
+
const hour = EVENT_TIME.hour();
|
|
561
|
+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
|
562
|
+
|
|
563
|
+
if (isWeekend) {
|
|
564
|
+
record.weekend_viewing = true;
|
|
565
|
+
record.prime_time = false;
|
|
566
|
+
if (record.event === "playback completed" && record.watch_duration_min) {
|
|
567
|
+
record.watch_duration_min = Math.round(record.watch_duration_min * 1.5);
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
record.weekend_viewing = false;
|
|
571
|
+
// Weekday prime-time: 6PM to 11PM
|
|
572
|
+
if (hour >= 18 && hour <= 23) {
|
|
573
|
+
record.prime_time = true;
|
|
574
|
+
} else {
|
|
575
|
+
record.prime_time = false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ─────────────────────────────────────────────────────────────
|
|
580
|
+
// Hook #5: NEW RELEASE SPIKE (event)
|
|
581
|
+
// After day 50, blockbuster release drives content selection
|
|
582
|
+
// ─────────────────────────────────────────────────────────────
|
|
583
|
+
const BLOCKBUSTER_RELEASE = DATASET_START.add(50, 'days');
|
|
584
|
+
if (record.event === "content selected" || record.event === "playback started") {
|
|
585
|
+
if (EVENT_TIME.isAfter(BLOCKBUSTER_RELEASE) && chance.bool({ likelihood: 20 })) {
|
|
586
|
+
record.content_type = "movie";
|
|
587
|
+
record.content_id = blockbusterId;
|
|
588
|
+
record.blockbuster_release = true;
|
|
589
|
+
} else {
|
|
590
|
+
record.blockbuster_release = false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (record.event === "content rated") {
|
|
595
|
+
if (EVENT_TIME.isAfter(BLOCKBUSTER_RELEASE) && chance.bool({ likelihood: 20 })) {
|
|
596
|
+
record.rating = chance.integer({ min: 4, max: 5 });
|
|
597
|
+
record.content_id = blockbusterId;
|
|
598
|
+
record.blockbuster_release = true;
|
|
599
|
+
} else {
|
|
600
|
+
record.blockbuster_release = false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ─────────────────────────────────────────────────────────────
|
|
605
|
+
// Hook #6: KIDS PROFILE SAFETY (event)
|
|
606
|
+
// 15% of the time, apply kids mode: restrict genres, drop ads
|
|
607
|
+
// ─────────────────────────────────────────────────────────────
|
|
608
|
+
if (chance.bool({ likelihood: 15 })) {
|
|
609
|
+
record.kids_profile = true;
|
|
610
|
+
if (record.event === "content selected" || record.event === "playback started") {
|
|
611
|
+
record.genre = chance.pickone(["animation", "documentary"]);
|
|
612
|
+
} else if (record.event === "ad impression") {
|
|
613
|
+
record.ad_blocked = true;
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
record.kids_profile = false;
|
|
617
|
+
if (record.event === "ad impression") {
|
|
618
|
+
record.ad_blocked = false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return record;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ─────────────────────────────────────────────────────────────
|
|
626
|
+
// Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns
|
|
627
|
+
// ─────────────────────────────────────────────────────────────
|
|
628
|
+
if (type === "everything") {
|
|
629
|
+
const userEvents = record;
|
|
630
|
+
if (!userEvents || userEvents.length === 0) return record;
|
|
631
|
+
|
|
632
|
+
const firstEventTime = dayjs(userEvents[0].time);
|
|
633
|
+
|
|
634
|
+
// ─── First pass: identify user patterns ───
|
|
635
|
+
let consecutiveCompletions = 0;
|
|
636
|
+
let maxConsecutiveCompletions = 0;
|
|
637
|
+
let isBingeWatcher = false;
|
|
638
|
+
let adImpressionCount = 0;
|
|
639
|
+
let isFreeTier = false;
|
|
640
|
+
let hasSubtitlesEnabled = false;
|
|
641
|
+
|
|
642
|
+
userEvents.forEach((event, idx) => {
|
|
643
|
+
// Track subscription tier from superProps
|
|
644
|
+
if (idx === 0 && event.subscription_plan) {
|
|
645
|
+
isFreeTier = event.subscription_plan === "free";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Hook #2: Track consecutive playback completions
|
|
649
|
+
if (event.event === "playback completed") {
|
|
650
|
+
consecutiveCompletions++;
|
|
651
|
+
if (consecutiveCompletions > maxConsecutiveCompletions) {
|
|
652
|
+
maxConsecutiveCompletions = consecutiveCompletions;
|
|
653
|
+
}
|
|
654
|
+
} else if (event.event !== "playback started") {
|
|
655
|
+
// Reset streak on non-playback events (started doesn't break streak)
|
|
656
|
+
consecutiveCompletions = 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Hook #4: Count ad impressions for free-tier users
|
|
660
|
+
if (event.event === "ad impression") {
|
|
661
|
+
adImpressionCount++;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Hook #8: Check for subtitle enabled
|
|
665
|
+
if (event.event === "subtitle toggled" && event.action === "enabled") {
|
|
666
|
+
hasSubtitlesEnabled = true;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
isBingeWatcher = maxConsecutiveCompletions >= 3;
|
|
671
|
+
|
|
672
|
+
// ─── Second pass: set schema defaults then modify ───
|
|
673
|
+
|
|
674
|
+
// Set defaults for conditional properties on all events
|
|
675
|
+
userEvents.forEach((event) => {
|
|
676
|
+
if (event.event === "playback completed") {
|
|
677
|
+
if (event.binge_session === undefined) event.binge_session = false;
|
|
678
|
+
if (event.subtitle_user === undefined) event.subtitle_user = false;
|
|
679
|
+
}
|
|
680
|
+
if (event.event === "playback started") {
|
|
681
|
+
if (event.binge_session === undefined) event.binge_session = false;
|
|
682
|
+
}
|
|
683
|
+
if (event.ad_fatigue === undefined) event.ad_fatigue = false;
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Hook #2: BINGE-WATCHING PATTERN
|
|
687
|
+
// Binge-watchers get extra episodes, high completion, fewer pauses
|
|
688
|
+
if (isBingeWatcher) {
|
|
689
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
690
|
+
const event = userEvents[i];
|
|
691
|
+
const eventTime = dayjs(event.time);
|
|
692
|
+
|
|
693
|
+
// Remove some pause events (binge-watchers don't stop)
|
|
694
|
+
if (event.event === "playback paused") {
|
|
695
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
696
|
+
userEvents.splice(i, 1);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Add extra viewing events after completions
|
|
702
|
+
if (event.event === "playback completed" && chance.bool({ likelihood: 40 })) {
|
|
703
|
+
const nextContentId = chance.pickone(contentIds);
|
|
704
|
+
const extraStart = {
|
|
705
|
+
event: "playback started",
|
|
706
|
+
time: eventTime.add(chance.integer({ min: 1, max: 5 }), 'minutes').toISOString(),
|
|
707
|
+
user_id: event.user_id,
|
|
708
|
+
content_id: nextContentId,
|
|
709
|
+
content_type: "series",
|
|
710
|
+
playback_quality: event.playback_quality || "1080p",
|
|
711
|
+
binge_session: true,
|
|
712
|
+
};
|
|
713
|
+
const extraComplete = {
|
|
714
|
+
event: "playback completed",
|
|
715
|
+
time: eventTime.add(chance.integer({ min: 25, max: 60 }), 'minutes').toISOString(),
|
|
716
|
+
user_id: event.user_id,
|
|
717
|
+
content_id: nextContentId,
|
|
718
|
+
content_type: "series",
|
|
719
|
+
watch_duration_min: chance.integer({ min: 20, max: 55 }),
|
|
720
|
+
completion_percent: chance.integer({ min: 90, max: 100 }),
|
|
721
|
+
binge_session: true,
|
|
722
|
+
};
|
|
723
|
+
userEvents.splice(i + 1, 0, extraStart, extraComplete);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Hook #4: AD FATIGUE CHURN
|
|
729
|
+
// Free-tier users with 10+ ads lose 50% of events after day 45
|
|
730
|
+
if (isFreeTier && adImpressionCount >= 10) {
|
|
731
|
+
const churnCutoff = firstEventTime.add(45, 'days');
|
|
732
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
733
|
+
const evt = userEvents[i];
|
|
734
|
+
if (dayjs(evt.time).isAfter(churnCutoff)) {
|
|
735
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
736
|
+
userEvents.splice(i, 1);
|
|
737
|
+
} else {
|
|
738
|
+
evt.ad_fatigue = true;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Hook #8: SUBTITLE USERS WATCH MORE
|
|
745
|
+
// Users who enabled subtitles have 25% higher completion and 15% longer watch time
|
|
746
|
+
if (hasSubtitlesEnabled) {
|
|
747
|
+
for (let i = 0; i < userEvents.length; i++) {
|
|
748
|
+
const event = userEvents[i];
|
|
749
|
+
|
|
750
|
+
if (event.event === "playback completed") {
|
|
751
|
+
// Boost completion percent (cap at 100)
|
|
752
|
+
if (event.completion_percent) {
|
|
753
|
+
event.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));
|
|
754
|
+
}
|
|
755
|
+
// Boost watch duration
|
|
756
|
+
if (event.watch_duration_min) {
|
|
757
|
+
event.watch_duration_min = Math.round(event.watch_duration_min * 1.15);
|
|
758
|
+
}
|
|
759
|
+
event.subtitle_user = true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Splice extra playback completed events (subtitle users watch more overall)
|
|
764
|
+
const completionEvents = userEvents.filter(e => e.event === "playback completed");
|
|
765
|
+
const extraCount = Math.floor(completionEvents.length * 0.2); // 20% more completions
|
|
766
|
+
for (let j = 0; j < extraCount; j++) {
|
|
767
|
+
const templateEvent = chance.pickone(completionEvents);
|
|
768
|
+
const templateTime = dayjs(templateEvent.time);
|
|
769
|
+
const extraCompletion = {
|
|
770
|
+
event: "playback completed",
|
|
771
|
+
time: templateTime.add(chance.integer({ min: 30, max: 180 }), 'minutes').toISOString(),
|
|
772
|
+
user_id: templateEvent.user_id,
|
|
773
|
+
content_id: chance.pickone(contentIds),
|
|
774
|
+
content_type: chance.pickone(["movie", "series", "documentary"]),
|
|
775
|
+
watch_duration_min: chance.integer({ min: 25, max: 120 }),
|
|
776
|
+
completion_percent: chance.integer({ min: 80, max: 100 }),
|
|
777
|
+
subtitle_user: true,
|
|
778
|
+
};
|
|
779
|
+
userEvents.push(extraCompletion);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return record;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return record;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
export default config;
|