@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,20 +1,19 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "simple is best";
3
- const num_days = 108;
4
- const num_users = 50_000;
3
+ const num_days = 120;
4
+ const num_users = 42_000;
5
5
  const avg_events_per_user_per_day = 0.37;
6
6
  let token = "your-mixpanel-token";
7
7
 
8
8
  // ── env overrides ──
9
9
  if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
10
 
11
- import Chance from 'chance';
12
- let chance = new Chance();
13
11
  import dayjs from "dayjs";
14
12
  import utc from "dayjs/plugin/utc.js";
15
13
  dayjs.extend(utc);
16
14
  import { uid, comma } from 'ak-tools';
17
- import { weighNumRange, date, integer, weighChoices, decimal } from "../../lib/utils/utils.js";
15
+ import { weighNumRange, date, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
16
+ const chance = initChance(SEED);
18
17
 
19
18
  /** @typedef {import("../../types").Dungeon} Config */
20
19
  const itemCategories = ["Books", "Movies", "Music", "Games", "Electronics", "Computers", "Smart Home", "Home", "Garden", "Pet", "Beauty", "Health", "Toys", "Kids", "Baby", "Handmade", "Sports", "Outdoors", "Automotive", "Industrial", "Entertainment", "Art", "Food", "Appliances", "Office", "Wedding", "Software"];
@@ -50,47 +49,199 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
50
49
 
51
50
  /*
52
51
  * ============================================================================
53
- * ANALYTICS HOOKS
52
+ * ANALYTICS HOOKS (7 hooks)
53
+ *
54
+ * Adds 7. SIGNUP FLOW TIME-TO-CONVERT: gold/platinum loyalty 0.71x faster,
55
+ * bronze 1.3x slower (funnel-post). Discover via funnel median time-to-convert
56
+ * by loyalty_tier breakdown.
57
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
58
+ * Cross-event MIN→MIN SQL queries on raw events do NOT show this.
59
+ * ============================================================================
60
+ *
61
+ * ----------------------------------------------------------------------------
62
+ * Hook 1: Signup Flow Improvement (event + everything)
63
+ * ----------------------------------------------------------------------------
64
+ *
65
+ * PATTERN: 7 days ago, signup_flow switches from "v1" to "v2". Before
66
+ * that date, 50% of sign ups are tagged _drop and removed in the
67
+ * everything hook, simulating a broken flow that got fixed.
68
+ *
69
+ * HOW TO FIND IT IN MIXPANEL:
70
+ *
71
+ * Report 1: Sign Ups by Flow Version
72
+ * - Report type: Insights
73
+ * - Event: "sign up"
74
+ * - Measure: Total
75
+ * - Breakdown: "signup_flow"
76
+ * - Line chart by day
77
+ * - Expected: v1 disappears at day -7; v2 takes over and roughly 2x daily volume
78
+ *
79
+ * Report 2: Pre vs Post Volume
80
+ * - Report type: Insights
81
+ * - Event: "sign up"
82
+ * - Measure: Total
83
+ * - Compare date ranges (last 7 days vs prior 7 days)
84
+ * - Expected: ~ 2x signups in the post-fix window
85
+ *
86
+ * REAL-WORLD ANALOGUE: A broken signup flow silently halved conversions
87
+ * until a release shipped a fix; daily signups roughly doubled overnight.
88
+ *
89
+ * ----------------------------------------------------------------------------
90
+ * Hook 2: Watch Time Inflection (event)
91
+ * ----------------------------------------------------------------------------
92
+ *
93
+ * PATTERN: 30 days ago, watch time shifts. Before that date, watchTimeSec
94
+ * is reduced by 25-79%. After, it is increased by 25-79%. Creates a
95
+ * clear before/after inflection.
96
+ *
97
+ * HOW TO FIND IT IN MIXPANEL:
98
+ *
99
+ * Report 1: Avg Watch Time Over Time
100
+ * - Report type: Insights
101
+ * - Event: "watch video"
102
+ * - Measure: Average of "watchTimeSec"
103
+ * - Line chart by day
104
+ * - Expected: clear upward inflection ~ 30 days ago; watch time ~ doubles
105
+ *
106
+ * Report 2: Watch Time Distribution Pre vs Post
107
+ * - Report type: Insights
108
+ * - Event: "watch video"
109
+ * - Measure: Average of "watchTimeSec"
110
+ * - Compare date ranges (last 30 days vs prior 30 days)
111
+ * - Expected: post-inflection ~ 2x avg watch time
112
+ *
113
+ * REAL-WORLD ANALOGUE: An algorithm or UX change (autoplay, recs)
114
+ * inflects average watch duration sharply on a release date.
115
+ *
116
+ * ----------------------------------------------------------------------------
117
+ * Hook 3: Toys + Shoes Cart Correlation (event)
118
+ * ----------------------------------------------------------------------------
119
+ *
120
+ * PATTERN: Checkout carts with toys get a shoes item injected (and vice
121
+ * versa). Carts with neither toys nor shoes have all item prices
122
+ * discounted to 75-90% of normal.
123
+ *
124
+ * HOW TO FIND IT IN MIXPANEL:
125
+ *
126
+ * Report 1: Cart Category Co-occurrence
127
+ * - Report type: Insights
128
+ * - Event: "checkout"
129
+ * - Measure: Total
130
+ * - Breakdown: "category" (flattened item category)
131
+ * - Expected: toys and shoes appear at high co-occurrence rate
132
+ *
133
+ * Report 2: Cart Value by Category Mix
134
+ * - Report type: Insights
135
+ * - Event: "checkout"
136
+ * - Measure: Average of "amount"
137
+ * - Breakdown: "category"
138
+ * - Expected: carts lacking both toys and shoes have lower avg amount
139
+ *
140
+ * REAL-WORLD ANALOGUE: Family shoppers buying for kids tend to bundle
141
+ * toys with shoes; a market-basket pattern that retailers exploit.
142
+ *
143
+ * ----------------------------------------------------------------------------
144
+ * Hook 4: Quality to Watch Time Correlation (event)
145
+ * ----------------------------------------------------------------------------
146
+ *
147
+ * PATTERN: Video quality multiplies watchTimeSec: 2160p=1.5x, 1440p=1.4x,
148
+ * 1080p=1.3x, 720p=1.15x, 480p=1.0x, 360p=0.85x, 240p=0.7x.
149
+ *
150
+ * HOW TO FIND IT IN MIXPANEL:
151
+ *
152
+ * Report 1: Avg Watch Time by Quality
153
+ * - Report type: Insights
154
+ * - Event: "watch video"
155
+ * - Measure: Average of "watchTimeSec"
156
+ * - Breakdown: "quality"
157
+ * - Expected: monotonic increase from 240p to 2160p
158
+ *
159
+ * Report 2: Quality Distribution
160
+ * - Report type: Insights
161
+ * - Event: "watch video"
162
+ * - Measure: Total
163
+ * - Breakdown: "quality"
164
+ * - Expected: viewers split across all quality tiers; HD tiers earn more time
165
+ *
166
+ * REAL-WORLD ANALOGUE: Higher-resolution streams correlate with longer
167
+ * sessions because they signal a better connection and more invested viewer.
168
+ *
169
+ * ----------------------------------------------------------------------------
170
+ * Hook 5: Item Flattening (event)
171
+ * ----------------------------------------------------------------------------
172
+ *
173
+ * PATTERN: Events with an item array property get the first item's
174
+ * fields (category, amount, slug, etc.) flattened onto the event record
175
+ * as top-level properties — making them available for direct breakdown
176
+ * in reports.
177
+ *
178
+ * HOW TO FIND IT IN MIXPANEL:
179
+ *
180
+ * Report 1: View Items by Category
181
+ * - Report type: Insights
182
+ * - Event: "view item"
183
+ * - Measure: Total
184
+ * - Breakdown: "category"
185
+ * - Expected: clean breakdown across category values without nested-property issues
186
+ *
187
+ * Report 2: Add to Cart Avg Amount by Category
188
+ * - Report type: Insights
189
+ * - Event: "add to cart"
190
+ * - Measure: Average of "amount"
191
+ * - Breakdown: "category"
192
+ * - Expected: per-category averages render correctly off flat properties
193
+ *
194
+ * REAL-WORLD ANALOGUE: Analytics teams routinely flatten nested cart
195
+ * objects onto events so downstream tools can group/filter without joins.
196
+ *
197
+ * ----------------------------------------------------------------------------
198
+ * Hook 6: View-Item Magic Number (everything)
199
+ * ----------------------------------------------------------------------------
200
+ *
201
+ * PATTERN: Users who view 3-8 items in the dataset window are in the
202
+ * "considered buyer" sweet spot — every cart item amount and total_value
203
+ * gets boosted ~25%. Users who view 9 or more items are over-engaged
204
+ * window-shoppers; ~30% of their checkout events are dropped (decision
205
+ * paralysis / browse without buy). No flag is stamped — discoverable
206
+ * only by binning users on view-item COUNT and comparing avg cart total.
207
+ *
208
+ * HOW TO FIND IT IN MIXPANEL:
209
+ *
210
+ * Report 1: Avg Cart Total by View-Item Bucket
211
+ * - Report type: Insights (with cohort)
212
+ * - Cohort A: users with 3-8 "view item" events
213
+ * - Cohort B: users with 0-2 "view item" events
214
+ * - Event: "checkout"
215
+ * - Measure: Average of "amount" (sum across cart items if exposed)
216
+ * - Compare cohort A vs cohort B
217
+ * - Expected: cohort A ~ 1.25x higher cart total than B
218
+ *
219
+ * Report 2: Checkouts per User by Browse Intensity
220
+ * - Report type: Insights (with cohort)
221
+ * - Cohort C: users with >= 9 "view item" events
222
+ * - Cohort A: users with 3-8 "view item" events
223
+ * - Event: "checkout"
224
+ * - Measure: Total events per user
225
+ * - Compare cohort C vs cohort A
226
+ * - Expected: cohort C has ~ 30% fewer checkouts per user
227
+ *
228
+ * REAL-WORLD ANALOGUE: A focused buyer who reviews a handful of items
229
+ * tends to convert at higher cart value; an excessive browser is a
230
+ * tire-kicker who abandons more often.
231
+ *
232
+ * ============================================================================
233
+ * EXPECTED METRICS SUMMARY
54
234
  * ============================================================================
55
235
  *
56
- * Hook 1: Signup Flow Improvement
57
- * Type: event + everything (tag-and-filter)
58
- * What: 7 days ago, signup flow switches from "v1" to "v2". Before that date,
59
- * 50% of sign ups are tagged _drop and removed in the everything hook,
60
- * simulating a broken flow that got fixed.
61
- * Mixpanel report:
62
- * - Insights > line chart > "sign up" totaled by day, breakdown by signup_flow
63
- * - Expect: ~2x volume increase in last 7 days; v1 disappears, v2 takes over
64
- *
65
- * Hook 2: Watch Time Inflection
66
- * Type: event
67
- * What: 30 days ago, watch time shifts. Before: watchTimeSec reduced by 25-79%.
68
- * After: increased by 25-79%. Creates a clear before/after inflection.
69
- * Mixpanel report:
70
- * - Insights > line chart > "watch video" with AVG(watchTimeSec) by day
71
- * - Expect: clear upward inflection ~30 days ago; watch time roughly doubles
72
- *
73
- * Hook 3: Toys + Shoes Cart Correlation
74
- * Type: event
75
- * What: Checkout carts with toys get shoes injected (and vice versa). Carts
76
- * with neither toys nor shoes have all item prices discounted to 75-90%.
77
- * Mixpanel report:
78
- * - Insights > bar chart > "checkout" broken down by cart[].category
79
- * - Expect: toys and shoes co-occur at high rate; carts without both have lower values
80
- *
81
- * Hook 4: Quality → Watch Time Correlation
82
- * Type: event
83
- * What: Video quality multiplies watchTimeSec: 2160p=1.5x, 1440p=1.4x,
84
- * 1080p=1.3x, 720p=1.15x, 480p=1.0x, 360p=0.85x, 240p=0.7x.
85
- * Mixpanel report:
86
- * - Insights > bar chart > "watch video" with AVG(watchTimeSec), breakdown by quality
87
- * - Expect: monotonic increase from 240p to 2160p
88
- *
89
- * Hook 5: Item Flattening
90
- * Type: event
91
- * What: Events with an item array property get the first item's fields
92
- * flattened onto the event record, making category/amount/slug available
93
- * as top-level properties for easier breakdown in reports.
236
+ * Hook | Metric | Baseline | Hook Effect | Ratio
237
+ * ---------------------------|-------------------------|------------|-------------|------
238
+ * Signup Flow Improvement | Daily sign ups | 1x | ~ 2x | 2x
239
+ * Watch Time Inflection | Avg watchTimeSec | 1x | ~ 2x | 2x
240
+ * Toys + Shoes Correlation | toys/shoes co-occurrence| ~ 7% | ~ 50%+ | ~ 7x
241
+ * Quality -> Watch Time | Avg watchTimeSec (240p->2160p) | 0.7x | 1.5x | ~ 2.1x
242
+ * Item Flattening | category breakdown | nested | flat | n/a
243
+ * View-Item Magic Number | sweet (3-8) cart total | 1x | ~ 1.25x | 1.25x
244
+ * View-Item Magic Number | over (9+) checkouts/user| 1x | ~ 0.7x | -30%
94
245
  * ============================================================================
95
246
  */
96
247
 
@@ -140,15 +291,19 @@ function makeProducts(maxItems = 5) {
140
291
 
141
292
  /** @type {import('../types.d.ts').Dungeon} */
142
293
  const config = {
294
+ version: 2,
143
295
  token,
144
296
  seed: SEED,
145
- numDays: num_days,
297
+ datasetStart: "2026-01-01T00:00:00Z",
298
+ datasetEnd: "2026-05-01T23:59:59Z",
299
+ // numDays: num_days,
146
300
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
147
301
  numUsers: num_users,
148
302
  format: 'json', //csv or json
149
303
  region: "US",
150
- hasAnonIds: false, //if true, anonymousIds are created for each user
151
- hasSessionIds: false, //if true, hasSessionIds are created for each user
304
+ hasAnonIds: true,
305
+ avgDevicePerUser: 2,
306
+ hasSessionIds: true,
152
307
  hasAdSpend: false,
153
308
  hasLocation: true,
154
309
  hasAndroidDevices: true,
@@ -166,7 +321,7 @@ const config = {
166
321
  properties: {
167
322
  currency: ["USD", "CAD", "EUR", "BTC", "ETH", "JPY"],
168
323
  coupon: weighChoices(["none", "none", "none", "none", "10%OFF", "20%OFF", "10%OFF", "20%OFF", "30%OFF", "40%OFF", "50%OFF"]),
169
- cart: makeProducts()
324
+ cart: makeProducts(),
170
325
  }
171
326
  },
172
327
  {
@@ -252,6 +407,7 @@ const config = {
252
407
  event: "sign up",
253
408
  weight: 1,
254
409
  isFirstEvent: true,
410
+ isAuthEvent: true,
255
411
  properties: {
256
412
  signupMethod: ["email", "google", "facebook", "twitter", "linkedin", "github"],
257
413
  referral: weighChoices(["none", "none", "none", "friend", "ad", "ad", "ad", "friend", "friend", "friend", "friend"]),
@@ -336,43 +492,34 @@ const config = {
336
492
  lookupTables: [],
337
493
  hook: function (record, type, meta) {
338
494
 
339
- if (type === "event") {
340
- const NOW = dayjs();
341
- const DAY_SIGNUPS_IMPROVED = NOW.subtract(7, 'day');
342
- const DAY_WATCH_TIME_WENT_UP = NOW.subtract(30, 'day');
343
- const eventTime = dayjs(record.time);
495
+ // Hook 7 (T2C): SIGNUP FLOW TIME-TO-CONVERT (funnel-post)
496
+ // Gold/platinum loyalty tier users complete the Signup Flow funnel
497
+ // 1.4x faster (factor 0.71); bronze users 1.3x slower (factor 1.3).
498
+ if (type === "funnel-post") {
499
+ const segment = meta?.profile?.loyalty_tier;
500
+ if (Array.isArray(record) && record.length > 1) {
501
+ const factor = (
502
+ segment === "gold" || segment === "platinum" ? 0.71 :
503
+ segment === "bronze" ? 1.3 :
504
+ 1.0
505
+ );
506
+ if (factor !== 1.0) {
507
+ for (let i = 1; i < record.length; i++) {
508
+ const prev = dayjs(record[i - 1].time);
509
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
510
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
511
+ }
512
+ }
513
+ }
514
+ }
344
515
 
516
+ if (type === "event") {
345
517
  // unflattering 'items'
346
518
  if (record.item && Array.isArray(record.item)) {
347
519
  record = { ...record, ...record.item[0] };
348
520
  delete record.item;
349
521
  }
350
522
 
351
- if (record.event === 'sign up') {
352
- record.signup_flow = "v1";
353
- if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
354
- // tag 50% for removal (filtered in "everything" hook)
355
- if (chance.bool({ likelihood: 50 })) {
356
- record._drop = true;
357
- }
358
- }
359
- if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
360
- record.signup_flow = "v2";
361
- }
362
- }
363
-
364
- if (record.event === 'watch video') {
365
- const factor = decimal(0.25, 0.79);
366
- if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
367
- record.watchTimeSec = Math.round(record.watchTimeSec * (1 - factor));
368
- }
369
- if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
370
- // increase watch time by 33%
371
- record.watchTimeSec = Math.round(record.watchTimeSec * (1 + factor));
372
- }
373
-
374
- }
375
-
376
523
  // toys + shoes frequently purchases together (and are higher cart values)
377
524
  if (record.event === 'checkout' && Array.isArray(record.cart)) {
378
525
  const hasToys = record.cart.some(item => item.category === 'toys');
@@ -434,53 +581,59 @@ const config = {
434
581
  e.theme = profile.theme;
435
582
  });
436
583
 
437
- // big themes!
438
-
439
- // users who view items in the home and garden category churn more frequently
440
-
441
-
442
- // dark mode leads to faster purchase conversion
443
-
444
-
445
- // people who pay in bitcoin tend to buy more electronics and gadgets
446
-
447
-
448
- //custom themes purchase more:
449
- // const numCustomMode = record.filter(a => a.theme === 'custom').length;
450
- // const numLightMode = record.filter(a => a.theme === 'light').length;
451
- // const numDarkMode = record.filter(a => a.theme === 'dark').length;
452
- // if (numCustomMode > numLightMode || numCustomMode > numDarkMode) {
453
- // //triple their checkout events
454
- // const checkoutEvents = record.filter(a => a.event === 'checkout');
455
- // const newCheckouts = checkoutEvents.map(a => {
456
- // const randomInt = integer(-48, 48);
457
- // const newCheckout = {
458
- // ...a,
459
- // time: dayjs(a.time).add(randomInt, 'hour').toISOString(),
460
- // event: "checkout",
461
- // amount: a.amount * 2,
462
- // coupon: "50%OFF"
463
- // };
464
- // return newCheckout;
465
- // });
466
- // record.push(...newCheckouts);
467
- // }
468
-
469
- // //users who watch low quality videos churn more:
470
- // const loQuality = ["480p", "360p", "240p"];
471
- // const lowQualityWatches = record.filter(a => a.event === 'watch video' && loQuality.includes(a.quality));
472
- // const highQualityWatches = record.filter(a => a.event === 'watch video' && !loQuality.includes(a.quality));
473
- // if (lowQualityWatches.length > highQualityWatches.length) {
474
- // if (flip()) {
475
- // // find midpoint of records
476
- // const midpoint = Math.floor(record.length / 2);
477
- // record = record.slice(0, midpoint);
478
-
479
- // }
480
- // }
584
+ const datasetEnd = dayjs.unix(meta.datasetEnd);
585
+ const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(7, 'day');
586
+ const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(30, 'day');
481
587
 
588
+ // Hook 1: Signup flow improvement — pre-fix signups (v1) get 50% dropped
589
+ record.forEach(e => {
590
+ if (e.event !== 'sign up') return;
591
+ const eventTime = dayjs(e.time);
592
+ e.signup_flow = "v1";
593
+ if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
594
+ if (chance.bool({ likelihood: 50 })) {
595
+ e._drop = true;
596
+ }
597
+ }
598
+ if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
599
+ e.signup_flow = "v2";
600
+ }
601
+ });
482
602
 
603
+ // Hook 2: Watch time inflection — before 30 days ago: reduce, after: increase
604
+ record.forEach(e => {
605
+ if (e.event !== 'watch video') return;
606
+ const eventTime = dayjs(e.time);
607
+ const factor = decimal(0.25, 0.79);
608
+ if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
609
+ e.watchTimeSec = Math.round(e.watchTimeSec * (1 - factor));
610
+ }
611
+ if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
612
+ e.watchTimeSec = Math.round(e.watchTimeSec * (1 + factor));
613
+ }
614
+ });
483
615
 
616
+ // Hook 6: View-Item Magic Number (behavioral, no flags)
617
+ // Users in 3-8 view-item sweet spot show ~25% higher cart amounts.
618
+ // Users >=9 are over-engaged window-shoppers; ~30% of their checkouts drop.
619
+ const viewItemCount = record.filter(e => e.event === 'view item').length;
620
+ if (viewItemCount >= 3 && viewItemCount <= 8) {
621
+ record.forEach(e => {
622
+ if (e.event === 'checkout' && Array.isArray(e.cart)) {
623
+ e.cart = e.cart.map(it => ({
624
+ ...it,
625
+ amount: typeof it.amount === 'number' ? Math.round(it.amount * 1.25) : it.amount,
626
+ total_value: typeof it.total_value === 'number' ? Math.round(it.total_value * 1.25) : it.total_value
627
+ }));
628
+ }
629
+ });
630
+ } else if (viewItemCount >= 9) {
631
+ for (let i = record.length - 1; i >= 0; i--) {
632
+ if (record[i].event === 'checkout' && chance.bool({ likelihood: 30 })) {
633
+ record.splice(i, 1);
634
+ }
635
+ }
636
+ }
484
637
 
485
638
  }
486
639