make-mp-data 3.0.4 → 3.0.5

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.
Files changed (70) hide show
  1. package/README.md +46 -0
  2. package/dungeons/array-of-object-lookup-schema.json +327 -0
  3. package/dungeons/array-of-object-lookup.js +28 -8
  4. package/dungeons/capstone/capstone-ic3.js +291 -0
  5. package/dungeons/capstone/capstone-ic4.js +598 -0
  6. package/dungeons/capstone/capstone-ic5.js +668 -0
  7. package/dungeons/capstone/generate-product-lookup.js +309 -0
  8. package/dungeons/ecommerce-schema.json +462 -0
  9. package/dungeons/{copilot.js → ecommerce.js} +77 -15
  10. package/dungeons/education-schema.json +2409 -0
  11. package/dungeons/education.js +206 -442
  12. package/dungeons/fintech-schema.json +14034 -0
  13. package/dungeons/fintech.js +110 -389
  14. package/dungeons/foobar-schema.json +403 -0
  15. package/dungeons/foobar.js +27 -4
  16. package/dungeons/food-delivery-schema.json +192 -0
  17. package/dungeons/food-delivery.js +602 -0
  18. package/dungeons/food-schema.json +1152 -0
  19. package/dungeons/food.js +150 -383
  20. package/dungeons/gaming-schema.json +1270 -0
  21. package/dungeons/gaming.js +143 -3
  22. package/dungeons/insurance-application-schema.json +204 -0
  23. package/dungeons/insurance-application.js +605 -0
  24. package/dungeons/media-schema.json +906 -0
  25. package/dungeons/media.js +221 -391
  26. package/dungeons/retention-cadence-schema.json +78 -0
  27. package/dungeons/retention-cadence.js +35 -1
  28. package/dungeons/rpg-schema.json +4526 -0
  29. package/dungeons/rpg.js +130 -388
  30. package/dungeons/sanity-schema.json +255 -0
  31. package/dungeons/sanity.js +21 -10
  32. package/dungeons/sass-schema.json +1291 -0
  33. package/dungeons/sass.js +210 -337
  34. package/dungeons/scd-schema.json +919 -0
  35. package/dungeons/scd.js +38 -10
  36. package/dungeons/simple-schema.json +608 -0
  37. package/dungeons/simple.js +48 -11
  38. package/dungeons/simplest-schema.json +1418 -0
  39. package/dungeons/simplest.js +392 -0
  40. package/dungeons/social-schema.json +1118 -0
  41. package/dungeons/social.js +124 -365
  42. package/dungeons/text-generation-schema.json +3096 -0
  43. package/dungeons/text-generation.js +71 -0
  44. package/index.js +6 -3
  45. package/lib/core/config-validator.js +18 -0
  46. package/lib/core/storage.js +5 -5
  47. package/lib/generators/events.js +4 -4
  48. package/lib/orchestrators/mixpanel-sender.js +12 -7
  49. package/lib/orchestrators/user-loop.js +14 -6
  50. package/lib/templates/soup-presets.js +188 -0
  51. package/lib/utils/utils.js +52 -6
  52. package/package.json +1 -1
  53. package/types.d.ts +20 -3
  54. package/dungeons/adspend.js +0 -117
  55. package/dungeons/anon.js +0 -128
  56. package/dungeons/benchmark-heavy.js +0 -240
  57. package/dungeons/benchmark-light.js +0 -126
  58. package/dungeons/big.js +0 -226
  59. package/dungeons/business.js +0 -391
  60. package/dungeons/complex.js +0 -428
  61. package/dungeons/experiments.js +0 -137
  62. package/dungeons/funnels.js +0 -309
  63. package/dungeons/mil.js +0 -323
  64. package/dungeons/mirror.js +0 -160
  65. package/dungeons/soup-test.js +0 -52
  66. package/dungeons/streaming.js +0 -372
  67. package/dungeons/strict-event-test.js +0 -30
  68. package/dungeons/student-teacher.js +0 -438
  69. package/dungeons/too-big-events.js +0 -203
  70. package/dungeons/user-agent.js +0 -209
@@ -0,0 +1,906 @@
1
+ {
2
+ "schema": {
3
+ "token": "",
4
+ "seed": "harness-media",
5
+ "numDays": 100,
6
+ "numEvents": 600000,
7
+ "numUsers": 5000,
8
+ "hasAnonIds": false,
9
+ "hasSessionIds": true,
10
+ "format": "json",
11
+ "gzip": true,
12
+ "alsoInferFunnels": false,
13
+ "hasLocation": true,
14
+ "hasAndroidDevices": true,
15
+ "hasIOSDevices": true,
16
+ "hasDesktopDevices": true,
17
+ "hasBrowser": false,
18
+ "hasCampaigns": false,
19
+ "isAnonymous": false,
20
+ "hasAdSpend": false,
21
+ "percentUsersBornInDataset": 50,
22
+ "hasAvatar": true,
23
+ "batchSize": 2500000,
24
+ "concurrency": 1,
25
+ "writeToDisk": false,
26
+ "funnels": [
27
+ {
28
+ "sequence": [
29
+ "account created",
30
+ "content browsed",
31
+ "playback started"
32
+ ],
33
+ "isFirstFunnel": true,
34
+ "conversionRate": 80,
35
+ "timeToConvert": 0.25
36
+ },
37
+ {
38
+ "sequence": [
39
+ "content browsed",
40
+ "content selected",
41
+ "playback started",
42
+ "playback completed"
43
+ ],
44
+ "conversionRate": 55,
45
+ "timeToConvert": 2,
46
+ "weight": 5,
47
+ "props": {
48
+ "genre": [
49
+ "action",
50
+ "comedy",
51
+ "drama",
52
+ "documentary",
53
+ "horror",
54
+ "sci_fi",
55
+ "animation",
56
+ "thriller",
57
+ "romance"
58
+ ]
59
+ }
60
+ },
61
+ {
62
+ "sequence": [
63
+ "recommendation clicked",
64
+ "playback started",
65
+ "playback completed",
66
+ "content rated"
67
+ ],
68
+ "conversionRate": 35,
69
+ "timeToConvert": 1,
70
+ "weight": 3
71
+ },
72
+ {
73
+ "sequence": [
74
+ "search performed",
75
+ "content selected",
76
+ "playback started"
77
+ ],
78
+ "conversionRate": 50,
79
+ "timeToConvert": 0.5,
80
+ "weight": 3
81
+ },
82
+ {
83
+ "sequence": [
84
+ "content browsed",
85
+ "watchlist added",
86
+ "content selected",
87
+ "playback started"
88
+ ],
89
+ "conversionRate": 40,
90
+ "timeToConvert": 12,
91
+ "weight": 2
92
+ },
93
+ {
94
+ "sequence": [
95
+ "profile switched",
96
+ "subtitle toggled",
97
+ "playback started",
98
+ "playback completed"
99
+ ],
100
+ "conversionRate": 45,
101
+ "timeToConvert": 1,
102
+ "weight": 2
103
+ },
104
+ {
105
+ "sequence": [
106
+ "ad impression",
107
+ "playback started",
108
+ "playback paused"
109
+ ],
110
+ "conversionRate": 60,
111
+ "timeToConvert": 0.5,
112
+ "weight": 2
113
+ },
114
+ {
115
+ "sequence": [
116
+ "playback completed",
117
+ "share content",
118
+ "download started"
119
+ ],
120
+ "conversionRate": 25,
121
+ "timeToConvert": 1,
122
+ "weight": 1
123
+ },
124
+ {
125
+ "sequence": [
126
+ "content browsed",
127
+ "subscription changed"
128
+ ],
129
+ "conversionRate": 15,
130
+ "timeToConvert": 24,
131
+ "weight": 1
132
+ }
133
+ ],
134
+ "events": [
135
+ {
136
+ "event": "account created",
137
+ "weight": 1,
138
+ "isFirstEvent": true,
139
+ "properties": {
140
+ "signup_source": [
141
+ "organic",
142
+ "referral",
143
+ "trial_offer",
144
+ "ad"
145
+ ],
146
+ "plan_selected": [
147
+ "free",
148
+ "standard",
149
+ "premium"
150
+ ]
151
+ }
152
+ },
153
+ {
154
+ "event": "content browsed",
155
+ "weight": 20,
156
+ "properties": {
157
+ "browse_section": [
158
+ "home",
159
+ "trending",
160
+ "new_releases",
161
+ "genre",
162
+ "continue_watching"
163
+ ],
164
+ "genre": [
165
+ "action",
166
+ "comedy",
167
+ "drama",
168
+ "documentary",
169
+ "horror",
170
+ "sci_fi",
171
+ "animation",
172
+ "thriller",
173
+ "romance"
174
+ ]
175
+ }
176
+ },
177
+ {
178
+ "event": "content selected",
179
+ "weight": 15,
180
+ "properties": {
181
+ "content_type": [
182
+ "movie",
183
+ "series",
184
+ "documentary",
185
+ "special"
186
+ ],
187
+ "genre": [
188
+ "action",
189
+ "comedy",
190
+ "drama",
191
+ "documentary",
192
+ "horror",
193
+ "sci_fi",
194
+ "animation",
195
+ "thriller",
196
+ "romance"
197
+ ],
198
+ "content_id": {
199
+ "functionName": "arrow",
200
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
201
+ }
202
+ }
203
+ },
204
+ {
205
+ "event": "playback started",
206
+ "weight": 18,
207
+ "properties": {
208
+ "content_id": {
209
+ "functionName": "arrow",
210
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
211
+ },
212
+ "content_type": [
213
+ "movie",
214
+ "series",
215
+ "documentary",
216
+ "special"
217
+ ],
218
+ "playback_quality": [
219
+ "480p",
220
+ "720p",
221
+ "1080p",
222
+ "4k"
223
+ ],
224
+ "subtitle_language": [
225
+ "none",
226
+ "english",
227
+ "spanish",
228
+ "french",
229
+ "japanese",
230
+ "korean"
231
+ ],
232
+ "playback_speed": {
233
+ "functionName": "arrow",
234
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
235
+ }
236
+ }
237
+ },
238
+ {
239
+ "event": "playback completed",
240
+ "weight": 12,
241
+ "properties": {
242
+ "content_id": {
243
+ "functionName": "arrow",
244
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
245
+ },
246
+ "content_type": [
247
+ "movie",
248
+ "series",
249
+ "documentary",
250
+ "special"
251
+ ],
252
+ "watch_duration_min": [
253
+ 64,
254
+ 87,
255
+ 153,
256
+ 42,
257
+ 50,
258
+ 68,
259
+ 77,
260
+ 149,
261
+ 127,
262
+ 36,
263
+ 98,
264
+ 119,
265
+ 54,
266
+ 132,
267
+ 51,
268
+ 58,
269
+ 48,
270
+ 63,
271
+ 143,
272
+ 114,
273
+ 127,
274
+ 35,
275
+ 61,
276
+ 112,
277
+ 67,
278
+ 81,
279
+ 68,
280
+ 42,
281
+ 115,
282
+ 132,
283
+ 48,
284
+ 46,
285
+ 138,
286
+ 71,
287
+ 49,
288
+ 146,
289
+ 47,
290
+ 65,
291
+ 46,
292
+ 117,
293
+ 84,
294
+ 68,
295
+ 50,
296
+ 93,
297
+ 62
298
+ ],
299
+ "completion_percent": [
300
+ 50,
301
+ 51,
302
+ 47,
303
+ 57,
304
+ 35,
305
+ 55,
306
+ 52,
307
+ 52,
308
+ 40,
309
+ 31,
310
+ 50,
311
+ 49,
312
+ 60,
313
+ 25,
314
+ 49,
315
+ 40,
316
+ 95,
317
+ 25,
318
+ 80,
319
+ 80,
320
+ 67,
321
+ 35,
322
+ 75,
323
+ 42,
324
+ 58,
325
+ 98,
326
+ 66,
327
+ 53,
328
+ 75,
329
+ 46,
330
+ 47,
331
+ 53,
332
+ 52,
333
+ 33,
334
+ 55,
335
+ 49,
336
+ 42,
337
+ 41,
338
+ 31,
339
+ 58,
340
+ 54,
341
+ 51,
342
+ 55,
343
+ 50,
344
+ 59,
345
+ 66,
346
+ 43,
347
+ 38,
348
+ 32,
349
+ 11,
350
+ 54,
351
+ 89,
352
+ 25,
353
+ 35,
354
+ 41,
355
+ 46,
356
+ 51,
357
+ 72,
358
+ 46,
359
+ 52,
360
+ 91,
361
+ 51,
362
+ 32,
363
+ 43,
364
+ 40,
365
+ 14,
366
+ 42,
367
+ 87,
368
+ 54,
369
+ 55,
370
+ 60,
371
+ 39,
372
+ 28,
373
+ 39,
374
+ 66,
375
+ 33,
376
+ 48,
377
+ 34,
378
+ 47,
379
+ 51,
380
+ 76,
381
+ 86,
382
+ 40,
383
+ 17,
384
+ 56
385
+ ]
386
+ }
387
+ },
388
+ {
389
+ "event": "playback paused",
390
+ "weight": 10,
391
+ "properties": {
392
+ "content_id": {
393
+ "functionName": "arrow",
394
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
395
+ },
396
+ "pause_reason": [
397
+ "manual",
398
+ "ad_break",
399
+ "buffering",
400
+ "notification"
401
+ ]
402
+ }
403
+ },
404
+ {
405
+ "event": "content rated",
406
+ "weight": 6,
407
+ "properties": {
408
+ "content_id": {
409
+ "functionName": "arrow",
410
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
411
+ },
412
+ "rating": [
413
+ 3,
414
+ 2,
415
+ 2,
416
+ 2
417
+ ],
418
+ "review_text_length": []
419
+ }
420
+ },
421
+ {
422
+ "event": "watchlist added",
423
+ "weight": 8,
424
+ "properties": {
425
+ "content_id": {
426
+ "functionName": "arrow",
427
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
428
+ },
429
+ "content_type": [
430
+ "movie",
431
+ "series",
432
+ "documentary",
433
+ "special"
434
+ ],
435
+ "genre": [
436
+ "action",
437
+ "comedy",
438
+ "drama",
439
+ "documentary",
440
+ "horror",
441
+ "sci_fi",
442
+ "animation",
443
+ "thriller",
444
+ "romance"
445
+ ]
446
+ }
447
+ },
448
+ {
449
+ "event": "watchlist removed",
450
+ "weight": 3,
451
+ "properties": {
452
+ "content_id": {
453
+ "functionName": "arrow",
454
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
455
+ },
456
+ "reason": [
457
+ "watched",
458
+ "not_interested",
459
+ "expired"
460
+ ]
461
+ }
462
+ },
463
+ {
464
+ "event": "search performed",
465
+ "weight": 7,
466
+ "properties": {
467
+ "search_term": {
468
+ "functionName": "arrow",
469
+ "body": "() => chance.word()"
470
+ },
471
+ "results_count": [
472
+ 13,
473
+ 25,
474
+ 37,
475
+ 19,
476
+ 11,
477
+ 41,
478
+ 20,
479
+ 11,
480
+ 32,
481
+ 35,
482
+ 24,
483
+ 34,
484
+ 14,
485
+ 14,
486
+ 12
487
+ ],
488
+ "search_type": [
489
+ "title",
490
+ "actor",
491
+ "director",
492
+ "genre"
493
+ ]
494
+ }
495
+ },
496
+ {
497
+ "event": "recommendation clicked",
498
+ "weight": 9,
499
+ "properties": {
500
+ "algorithm": [
501
+ "collaborative_filtering",
502
+ "content_based",
503
+ "trending",
504
+ "editorial"
505
+ ],
506
+ "position": [
507
+ 9,
508
+ 10,
509
+ 11,
510
+ 4,
511
+ 3,
512
+ 5,
513
+ 19,
514
+ 5,
515
+ 6,
516
+ 6,
517
+ 6,
518
+ 5,
519
+ 4,
520
+ 11,
521
+ 7,
522
+ 17,
523
+ 12,
524
+ 8,
525
+ 8,
526
+ 9,
527
+ 9,
528
+ 6,
529
+ 13,
530
+ 13,
531
+ 14,
532
+ 4,
533
+ 5,
534
+ 9,
535
+ 18,
536
+ 18,
537
+ 7,
538
+ 4,
539
+ 5,
540
+ 6,
541
+ 13,
542
+ 7,
543
+ 6,
544
+ 13,
545
+ 13,
546
+ 11,
547
+ 7,
548
+ 12,
549
+ 9,
550
+ 6,
551
+ 18,
552
+ 16,
553
+ 6,
554
+ 16,
555
+ 12,
556
+ 10
557
+ ]
558
+ }
559
+ },
560
+ {
561
+ "event": "profile switched",
562
+ "weight": 4,
563
+ "properties": {
564
+ "profile_type": [
565
+ "main",
566
+ "kids",
567
+ "partner",
568
+ "guest"
569
+ ]
570
+ }
571
+ },
572
+ {
573
+ "event": "ad impression",
574
+ "weight": 8,
575
+ "properties": {
576
+ "ad_type": [
577
+ "pre_roll",
578
+ "mid_roll",
579
+ "banner",
580
+ "interstitial"
581
+ ],
582
+ "ad_duration_sec": [
583
+ 16,
584
+ 8,
585
+ 22,
586
+ 11,
587
+ 17,
588
+ 21,
589
+ 16,
590
+ 11,
591
+ 13,
592
+ 14,
593
+ 11,
594
+ 16,
595
+ 18,
596
+ 12,
597
+ 15,
598
+ 16,
599
+ 12,
600
+ 11,
601
+ 9,
602
+ 12,
603
+ 15,
604
+ 18,
605
+ 25,
606
+ 15,
607
+ 9,
608
+ 19,
609
+ 21,
610
+ 23,
611
+ 11,
612
+ 20,
613
+ 7,
614
+ 15,
615
+ 24,
616
+ 14,
617
+ 18,
618
+ 15,
619
+ 12,
620
+ 18,
621
+ 19,
622
+ 25,
623
+ 11,
624
+ 10,
625
+ 14,
626
+ 11,
627
+ 11,
628
+ 19,
629
+ 11,
630
+ 21,
631
+ 26,
632
+ 8
633
+ ],
634
+ "skipped": {
635
+ "functionName": "arrow",
636
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
637
+ }
638
+ }
639
+ },
640
+ {
641
+ "event": "subscription changed",
642
+ "weight": 2,
643
+ "properties": {
644
+ "old_plan": [
645
+ "free",
646
+ "standard",
647
+ "premium"
648
+ ],
649
+ "new_plan": [
650
+ "free",
651
+ "standard",
652
+ "premium"
653
+ ],
654
+ "change_reason": [
655
+ "upgrade",
656
+ "downgrade",
657
+ "cancel",
658
+ "resubscribe"
659
+ ]
660
+ }
661
+ },
662
+ {
663
+ "event": "download started",
664
+ "weight": 5,
665
+ "properties": {
666
+ "content_id": {
667
+ "functionName": "arrow",
668
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
669
+ },
670
+ "content_type": [
671
+ "movie",
672
+ "series",
673
+ "documentary",
674
+ "special"
675
+ ],
676
+ "download_quality": [
677
+ "720p",
678
+ "1080p",
679
+ "4k"
680
+ ]
681
+ }
682
+ },
683
+ {
684
+ "event": "share content",
685
+ "weight": 3,
686
+ "properties": {
687
+ "share_method": [
688
+ "link",
689
+ "social",
690
+ "dm",
691
+ "email"
692
+ ],
693
+ "content_type": [
694
+ "movie",
695
+ "series",
696
+ "documentary",
697
+ "special"
698
+ ]
699
+ }
700
+ },
701
+ {
702
+ "event": "subtitle toggled",
703
+ "weight": 4,
704
+ "properties": {
705
+ "subtitle_language": [
706
+ "none",
707
+ "english",
708
+ "spanish",
709
+ "french",
710
+ "japanese",
711
+ "korean"
712
+ ],
713
+ "action": [
714
+ "enabled",
715
+ "disabled",
716
+ "changed"
717
+ ]
718
+ }
719
+ }
720
+ ],
721
+ "superProps": {
722
+ "subscription_plan": {
723
+ "functionName": "arrow",
724
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
725
+ },
726
+ "device_type": [
727
+ "smart_tv",
728
+ "mobile",
729
+ "tablet",
730
+ "laptop",
731
+ "desktop"
732
+ ]
733
+ },
734
+ "scdProps": {},
735
+ "userProps": {
736
+ "preferred_genre": [
737
+ "action",
738
+ "comedy",
739
+ "drama",
740
+ "documentary",
741
+ "horror",
742
+ "sci_fi",
743
+ "animation"
744
+ ],
745
+ "avg_session_duration_min": [
746
+ 63,
747
+ 54,
748
+ 136,
749
+ 60,
750
+ 45,
751
+ 59,
752
+ 58,
753
+ 53,
754
+ 54,
755
+ 61,
756
+ 87,
757
+ 36,
758
+ 123,
759
+ 65,
760
+ 126,
761
+ 72,
762
+ 95,
763
+ 51,
764
+ 66,
765
+ 146,
766
+ 47,
767
+ 138,
768
+ 131,
769
+ 76,
770
+ 54,
771
+ 53,
772
+ 53,
773
+ 129,
774
+ 53,
775
+ 52,
776
+ 65,
777
+ 107,
778
+ 114,
779
+ 48,
780
+ 141,
781
+ 39,
782
+ 58,
783
+ 138,
784
+ 119,
785
+ 47,
786
+ 68,
787
+ 51,
788
+ 95,
789
+ 54,
790
+ 62
791
+ ],
792
+ "total_watch_hours": [
793
+ 135,
794
+ 201,
795
+ 119,
796
+ 346,
797
+ 331,
798
+ 211,
799
+ 340,
800
+ 156,
801
+ 334,
802
+ 206,
803
+ 157,
804
+ 175,
805
+ 231,
806
+ 97,
807
+ 217,
808
+ 250,
809
+ 231,
810
+ 384,
811
+ 298,
812
+ 155,
813
+ 253,
814
+ 136,
815
+ 173,
816
+ 202,
817
+ 158,
818
+ 339,
819
+ 315,
820
+ 90,
821
+ 67,
822
+ 150,
823
+ 410,
824
+ 338,
825
+ 161,
826
+ 178,
827
+ 330,
828
+ 187,
829
+ 149,
830
+ 324,
831
+ 173,
832
+ 331,
833
+ 125,
834
+ 153,
835
+ 250,
836
+ 195,
837
+ 151,
838
+ 393,
839
+ 394,
840
+ 125,
841
+ 197,
842
+ 150
843
+ ],
844
+ "profiles_count": [
845
+ 2,
846
+ 2,
847
+ 1,
848
+ 4,
849
+ 2,
850
+ 2,
851
+ 2,
852
+ 2,
853
+ 2,
854
+ 3,
855
+ 4,
856
+ 2,
857
+ 3,
858
+ 2,
859
+ 2,
860
+ 2,
861
+ 4,
862
+ 3,
863
+ 2,
864
+ 3,
865
+ 2,
866
+ 1,
867
+ 3,
868
+ 4,
869
+ 3,
870
+ 2,
871
+ 2,
872
+ 2,
873
+ 2,
874
+ 2,
875
+ 3,
876
+ 2,
877
+ 2,
878
+ 2,
879
+ 3,
880
+ 2,
881
+ 2,
882
+ 3,
883
+ 1,
884
+ 3,
885
+ 3,
886
+ 3,
887
+ 1,
888
+ 2,
889
+ 4,
890
+ 4,
891
+ 2,
892
+ 1,
893
+ 2,
894
+ 1
895
+ ],
896
+ "downloads_enabled": {
897
+ "functionName": "arrow",
898
+ "body": "function () {\n\t\tconst weighted = [];\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\tconst rand = chance.d10(); // Random number between 1 and 10\n\n\t\t\t// 35% chance to favor the most chosen index\n\t\t\tif (chance.bool({ likelihood: 35 })) {\n\t\t\t\t// 50% chance to slightly alter the index\n\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\tweighted.push(items[mostChosenIndex]);\n\t\t\t\t} else {\n\t\t\t\t\tconst addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;\n\t\t\t\t\tlet newIndex = mostChosenIndex + addOrSubtract;\n\n\t\t\t\t\t// Ensure newIndex is within bounds\n\t\t\t\t\tif (newIndex < 0) newIndex = 0;\n\t\t\t\t\tif (newIndex >= items.length) newIndex = items.length - 1;\n\t\t\t\t\tweighted.push(items[newIndex]);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 25% chance to favor the second most chosen index\n\t\t\telse if (chance.bool({ likelihood: 25 })) {\n\t\t\t\tweighted.push(items[secondMostChosenIndex]);\n\t\t\t}\n\t\t\t// 15% chance to favor the third most chosen index\n\t\t\telse if (chance.bool({ likelihood: 15 })) {\n\t\t\t\tweighted.push(items[thirdMostChosenIndex]);\n\t\t\t}\n\t\t\t// Otherwise, pick a random item from the list\n\t\t\telse {\n\t\t\t\tweighted.push(chance.pickone(items));\n\t\t\t}\n\t\t}\n\t\treturn weighted;\n\t}"
899
+ }
900
+ },
901
+ "lookupTables": []
902
+ },
903
+ "hooks": "function (record, type, meta) {\n\t\tconst NOW = dayjs();\n\t\tconst DATASET_START = NOW.subtract(days, 'days');\n\t\t// FIXED_START is the pre-shift time range start; needed because\n\t\t// meta.firstEventTime in funnel-pre is in the FIXED (pre-shift) timeframe\n\t\tconst FIXED_START = dayjs.unix(global.FIXED_NOW).subtract(days, 'days');\n\n\t\t// ─────────────────────────────────────────────────────────────\n\t\t// Hook #1: GENRE FUNNEL CONVERSION (funnel-pre)\n\t\t// Comedy/Animation funnels convert 1.3x better; Documentary 0.7x\n\t\t// ─────────────────────────────────────────────────────────────\n\t\tif (type === \"funnel-pre\") {\n\t\t\trecord.props = record.props || {};\n\t\t\tconst genre = record.props.genre;\n\n\t\t\tif (genre === \"comedy\" || genre === \"animation\") {\n\t\t\t\trecord.conversionRate = record.conversionRate * 1.3;\n\t\t\t\trecord.props.genre_boost = true;\n\t\t\t\trecord.props.genre_penalty = false;\n\t\t\t} else if (genre === \"documentary\") {\n\t\t\t\trecord.conversionRate = record.conversionRate * 0.7;\n\t\t\t\trecord.props.genre_boost = false;\n\t\t\t\trecord.props.genre_penalty = true;\n\t\t\t} else if (!genre && chance.bool({ likelihood: 25 })) {\n\t\t\t\t// Randomly apply genre effects when genre isn't in funnel props\n\t\t\t\tif (chance.bool({ likelihood: 60 })) {\n\t\t\t\t\trecord.conversionRate = record.conversionRate * 1.3;\n\t\t\t\t\trecord.props.genre_boost = true;\n\t\t\t\t\trecord.props.genre_penalty = false;\n\t\t\t\t} else {\n\t\t\t\t\trecord.conversionRate = record.conversionRate * 0.7;\n\t\t\t\t\trecord.props.genre_boost = false;\n\t\t\t\t\trecord.props.genre_penalty = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trecord.props.genre_boost = false;\n\t\t\t\trecord.props.genre_penalty = false;\n\t\t\t}\n\n\t\t\t// ─────────────────────────────────────────────────────────────\n\t\t\t// Hook #7: RECOMMENDATION ENGINE IMPROVEMENT (funnel-pre)\n\t\t\t// After day 60, engagement funnel gets 1.5x boost\n\t\t\t// ─────────────────────────────────────────────────────────────\n\t\t\tconst seq = record.sequence || [];\n\t\t\tconst isEngagementFunnel = seq.includes(\"recommendation clicked\");\n\t\t\tconst funnelTime = meta && meta.firstEventTime ? dayjs.unix(meta.firstEventTime) : null;\n\t\t\tconst isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));\n\n\t\t\tif (isEngagementFunnel && isAfterImprovement) {\n\t\t\t\trecord.conversionRate = record.conversionRate * 1.5;\n\t\t\t\trecord.props.improved_recs = true;\n\t\t\t} else {\n\t\t\t\trecord.props.improved_recs = false;\n\t\t\t}\n\n\t\t\treturn record;\n\t\t}\n\n\t\t// ─────────────────────────────────────────────────────────────\n\t\t// Hook #3: WEEKEND vs WEEKDAY PATTERNS (event)\n\t\t// Weekend: 1.5x watch duration. Weekday 6PM-11PM: prime_time tag\n\t\t// ─────────────────────────────────────────────────────────────\n\t\tif (type === \"event\") {\n\t\t\tconst EVENT_TIME = dayjs(record.time);\n\t\t\tconst dayOfWeek = EVENT_TIME.day(); // 0 = Sunday, 6 = Saturday\n\t\t\tconst hour = EVENT_TIME.hour();\n\t\t\tconst isWeekend = dayOfWeek === 0 || dayOfWeek === 6;\n\n\t\t\tif (isWeekend) {\n\t\t\t\trecord.weekend_viewing = true;\n\t\t\t\trecord.prime_time = false;\n\t\t\t\tif (record.event === \"playback completed\" && record.watch_duration_min) {\n\t\t\t\t\trecord.watch_duration_min = Math.round(record.watch_duration_min * 1.5);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trecord.weekend_viewing = false;\n\t\t\t\t// Weekday prime-time: 6PM to 11PM\n\t\t\t\tif (hour >= 18 && hour <= 23) {\n\t\t\t\t\trecord.prime_time = true;\n\t\t\t\t} else {\n\t\t\t\t\trecord.prime_time = false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// ─────────────────────────────────────────────────────────────\n\t\t\t// Hook #5: NEW RELEASE SPIKE (event)\n\t\t\t// After day 50, blockbuster release drives content selection\n\t\t\t// ─────────────────────────────────────────────────────────────\n\t\t\tconst BLOCKBUSTER_RELEASE = DATASET_START.add(50, 'days');\n\t\t\tif (record.event === \"content selected\" || record.event === \"playback started\") {\n\t\t\t\tif (EVENT_TIME.isAfter(BLOCKBUSTER_RELEASE) && chance.bool({ likelihood: 20 })) {\n\t\t\t\t\trecord.content_type = \"movie\";\n\t\t\t\t\trecord.content_id = blockbusterId;\n\t\t\t\t\trecord.blockbuster_release = true;\n\t\t\t\t} else {\n\t\t\t\t\trecord.blockbuster_release = false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (record.event === \"content rated\") {\n\t\t\t\tif (EVENT_TIME.isAfter(BLOCKBUSTER_RELEASE) && chance.bool({ likelihood: 20 })) {\n\t\t\t\t\trecord.rating = chance.integer({ min: 4, max: 5 });\n\t\t\t\t\trecord.content_id = blockbusterId;\n\t\t\t\t\trecord.blockbuster_release = true;\n\t\t\t\t} else {\n\t\t\t\t\trecord.blockbuster_release = false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// ─────────────────────────────────────────────────────────────\n\t\t\t// Hook #6: KIDS PROFILE SAFETY (event)\n\t\t\t// 15% of the time, apply kids mode: restrict genres, drop ads\n\t\t\t// ─────────────────────────────────────────────────────────────\n\t\t\tif (chance.bool({ likelihood: 15 })) {\n\t\t\t\trecord.kids_profile = true;\n\t\t\t\tif (record.event === \"content selected\" || record.event === \"playback started\") {\n\t\t\t\t\trecord.genre = chance.pickone([\"animation\", \"documentary\"]);\n\t\t\t\t} else if (record.event === \"ad impression\") {\n\t\t\t\t\trecord.ad_blocked = true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trecord.kids_profile = false;\n\t\t\t\tif (record.event === \"ad impression\") {\n\t\t\t\t\trecord.ad_blocked = false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn record;\n\t\t}\n\n\t\t// ─────────────────────────────────────────────────────────────\n\t\t// Hook #2, #4, #8: EVERYTHING PASS - Complex behavioral patterns\n\t\t// ─────────────────────────────────────────────────────────────\n\t\tif (type === \"everything\") {\n\t\t\tconst userEvents = record;\n\t\t\tif (!userEvents || userEvents.length === 0) return record;\n\n\t\t\tconst firstEventTime = dayjs(userEvents[0].time);\n\n\t\t\t// ─── First pass: identify user patterns ───\n\t\t\tlet consecutiveCompletions = 0;\n\t\t\tlet maxConsecutiveCompletions = 0;\n\t\t\tlet isBingeWatcher = false;\n\t\t\tlet adImpressionCount = 0;\n\t\t\tlet isFreeTier = false;\n\t\t\tlet hasSubtitlesEnabled = false;\n\n\t\t\tuserEvents.forEach((event, idx) => {\n\t\t\t\t// Track subscription tier from superProps\n\t\t\t\tif (idx === 0 && event.subscription_plan) {\n\t\t\t\t\tisFreeTier = event.subscription_plan === \"free\";\n\t\t\t\t}\n\n\t\t\t\t// Hook #2: Track consecutive playback completions\n\t\t\t\tif (event.event === \"playback completed\") {\n\t\t\t\t\tconsecutiveCompletions++;\n\t\t\t\t\tif (consecutiveCompletions > maxConsecutiveCompletions) {\n\t\t\t\t\t\tmaxConsecutiveCompletions = consecutiveCompletions;\n\t\t\t\t\t}\n\t\t\t\t} else if (event.event !== \"playback started\") {\n\t\t\t\t\t// Reset streak on non-playback events (started doesn't break streak)\n\t\t\t\t\tconsecutiveCompletions = 0;\n\t\t\t\t}\n\n\t\t\t\t// Hook #4: Count ad impressions for free-tier users\n\t\t\t\tif (event.event === \"ad impression\") {\n\t\t\t\t\tadImpressionCount++;\n\t\t\t\t}\n\n\t\t\t\t// Hook #8: Check for subtitle enabled\n\t\t\t\tif (event.event === \"subtitle toggled\" && event.action === \"enabled\") {\n\t\t\t\t\thasSubtitlesEnabled = true;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tisBingeWatcher = maxConsecutiveCompletions >= 3;\n\n\t\t\t// ─── Second pass: set schema defaults then modify ───\n\n\t\t\t// Set defaults for conditional properties on all events\n\t\t\tuserEvents.forEach((event) => {\n\t\t\t\tif (event.event === \"playback completed\") {\n\t\t\t\t\tif (event.binge_session === undefined) event.binge_session = false;\n\t\t\t\t\tif (event.subtitle_user === undefined) event.subtitle_user = false;\n\t\t\t\t}\n\t\t\t\tif (event.event === \"playback started\") {\n\t\t\t\t\tif (event.binge_session === undefined) event.binge_session = false;\n\t\t\t\t}\n\t\t\t\tif (event.ad_fatigue === undefined) event.ad_fatigue = false;\n\t\t\t});\n\n\t\t\t// Hook #2: BINGE-WATCHING PATTERN\n\t\t\t// Binge-watchers get extra episodes, high completion, fewer pauses\n\t\t\tif (isBingeWatcher) {\n\t\t\t\tfor (let i = userEvents.length - 1; i >= 0; i--) {\n\t\t\t\t\tconst event = userEvents[i];\n\t\t\t\t\tconst eventTime = dayjs(event.time);\n\n\t\t\t\t\t// Remove some pause events (binge-watchers don't stop)\n\t\t\t\t\tif (event.event === \"playback paused\") {\n\t\t\t\t\t\tif (chance.bool({ likelihood: 60 })) {\n\t\t\t\t\t\t\tuserEvents.splice(i, 1);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add extra viewing events after completions\n\t\t\t\t\tif (event.event === \"playback completed\" && chance.bool({ likelihood: 40 })) {\n\t\t\t\t\t\tconst nextContentId = chance.pickone(contentIds);\n\t\t\t\t\t\tconst extraStart = {\n\t\t\t\t\t\t\tevent: \"playback started\",\n\t\t\t\t\t\t\ttime: eventTime.add(chance.integer({ min: 1, max: 5 }), 'minutes').toISOString(),\n\t\t\t\t\t\t\tuser_id: event.user_id,\n\t\t\t\t\t\t\tcontent_id: nextContentId,\n\t\t\t\t\t\t\tcontent_type: \"series\",\n\t\t\t\t\t\t\tplayback_quality: event.playback_quality || \"1080p\",\n\t\t\t\t\t\t\tbinge_session: true,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst extraComplete = {\n\t\t\t\t\t\t\tevent: \"playback completed\",\n\t\t\t\t\t\t\ttime: eventTime.add(chance.integer({ min: 25, max: 60 }), 'minutes').toISOString(),\n\t\t\t\t\t\t\tuser_id: event.user_id,\n\t\t\t\t\t\t\tcontent_id: nextContentId,\n\t\t\t\t\t\t\tcontent_type: \"series\",\n\t\t\t\t\t\t\twatch_duration_min: chance.integer({ min: 20, max: 55 }),\n\t\t\t\t\t\t\tcompletion_percent: chance.integer({ min: 90, max: 100 }),\n\t\t\t\t\t\t\tbinge_session: true,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tuserEvents.splice(i + 1, 0, extraStart, extraComplete);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Hook #4: AD FATIGUE CHURN\n\t\t\t// Free-tier users with 10+ ads lose 50% of events after day 45\n\t\t\tif (isFreeTier && adImpressionCount >= 10) {\n\t\t\t\tconst churnCutoff = firstEventTime.add(45, 'days');\n\t\t\t\tfor (let i = userEvents.length - 1; i >= 0; i--) {\n\t\t\t\t\tconst evt = userEvents[i];\n\t\t\t\t\tif (dayjs(evt.time).isAfter(churnCutoff)) {\n\t\t\t\t\t\tif (chance.bool({ likelihood: 50 })) {\n\t\t\t\t\t\t\tuserEvents.splice(i, 1);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tevt.ad_fatigue = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Hook #8: SUBTITLE USERS WATCH MORE\n\t\t\t// Users who enabled subtitles have 25% higher completion and 15% longer watch time\n\t\t\tif (hasSubtitlesEnabled) {\n\t\t\t\tfor (let i = 0; i < userEvents.length; i++) {\n\t\t\t\t\tconst event = userEvents[i];\n\n\t\t\t\t\tif (event.event === \"playback completed\") {\n\t\t\t\t\t\t// Boost completion percent (cap at 100)\n\t\t\t\t\t\tif (event.completion_percent) {\n\t\t\t\t\t\t\tevent.completion_percent = Math.min(100, Math.round(event.completion_percent * 1.25));\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Boost watch duration\n\t\t\t\t\t\tif (event.watch_duration_min) {\n\t\t\t\t\t\t\tevent.watch_duration_min = Math.round(event.watch_duration_min * 1.15);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tevent.subtitle_user = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Splice extra playback completed events (subtitle users watch more overall)\n\t\t\t\tconst completionEvents = userEvents.filter(e => e.event === \"playback completed\");\n\t\t\t\tconst extraCount = Math.floor(completionEvents.length * 0.2); // 20% more completions\n\t\t\t\tfor (let j = 0; j < extraCount; j++) {\n\t\t\t\t\tconst templateEvent = chance.pickone(completionEvents);\n\t\t\t\t\tconst templateTime = dayjs(templateEvent.time);\n\t\t\t\t\tconst extraCompletion = {\n\t\t\t\t\t\tevent: \"playback completed\",\n\t\t\t\t\t\ttime: templateTime.add(chance.integer({ min: 30, max: 180 }), 'minutes').toISOString(),\n\t\t\t\t\t\tuser_id: templateEvent.user_id,\n\t\t\t\t\t\tcontent_id: chance.pickone(contentIds),\n\t\t\t\t\t\tcontent_type: chance.pickone([\"movie\", \"series\", \"documentary\"]),\n\t\t\t\t\t\twatch_duration_min: chance.integer({ min: 25, max: 120 }),\n\t\t\t\t\t\tcompletion_percent: chance.integer({ min: 80, max: 100 }),\n\t\t\t\t\t\tsubtitle_user: true,\n\t\t\t\t\t};\n\t\t\t\t\tuserEvents.push(extraCompletion);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn record;\n\t\t}\n\n\t\treturn record;\n\t}",
904
+ "timestamp": "2026-04-10T01:39:07.207Z",
905
+ "version": "4.0"
906
+ }