@ak--47/dungeon-master 1.5.3 → 1.6.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/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +3 -2
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -1,811 +0,0 @@
|
|
|
1
|
-
// ── IMPORTS ──
|
|
2
|
-
import dayjs from "dayjs";
|
|
3
|
-
import utc from "dayjs/plugin/utc.js";
|
|
4
|
-
dayjs.extend(utc);
|
|
5
|
-
import { uid } from "ak-tools";
|
|
6
|
-
import { weighNumRange, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
|
|
7
|
-
import { scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
|
|
8
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
9
|
-
|
|
10
|
-
// ── OVERVIEW ──
|
|
11
|
-
/*
|
|
12
|
-
* NAME: ShopStream
|
|
13
|
-
* APP: eCommerce marketplace with integrated video content. Users browse
|
|
14
|
-
* products, watch videos, build carts, and check out. A signup
|
|
15
|
-
* funnel converts anonymous browsers into registered users; video
|
|
16
|
-
* engagement (like/dislike) runs alongside the shopping flow.
|
|
17
|
-
* SCALE: 42,000 users, ~2M events, 121 days (2026-01-01 → 2026-05-01)
|
|
18
|
-
* CORE LOOP: page view → view item → save/add to cart → checkout (+ video like/dislike side loop)
|
|
19
|
-
*
|
|
20
|
-
* EVENTS (9):
|
|
21
|
-
* page view (10) > view item (8) > watch video (8) > like video (6) > save item (5)
|
|
22
|
-
* > add to cart (4) > dislike video (4) > checkout (2) > sign up (1)
|
|
23
|
-
*
|
|
24
|
-
* FUNNELS (4):
|
|
25
|
-
* - Signup Flow: page view → view item → save item → page view → sign up (50%)
|
|
26
|
-
* - Video Likes: watch video → like video → watch video → like video (60%)
|
|
27
|
-
* - Video Dislikes: watch video → dislike video → watch video → dislike video (20%)
|
|
28
|
-
* - eCommerce Purchase: view item → view item → add to cart → view item → add to cart → checkout (15%, A/B/C experiment)
|
|
29
|
-
*
|
|
30
|
-
* USER PROPS: title, luckyNumber, spiritAnimal, theme
|
|
31
|
-
* SUPER PROPS: theme
|
|
32
|
-
* SCD PROPS: loyalty_tier (bronze/silver/gold/platinum, monthly fuzzy, max 8)
|
|
33
|
-
* GROUPS: none
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
// ── HOOK STORIES ──
|
|
37
|
-
/*
|
|
38
|
-
* ----------------------------------------------------------------------------
|
|
39
|
-
* Hook 1: Signup Flow Improvement (event + everything)
|
|
40
|
-
* ----------------------------------------------------------------------------
|
|
41
|
-
*
|
|
42
|
-
* PATTERN: 7 days ago, signup_flow switches from "v1" to "v2". Before
|
|
43
|
-
* that date, 50% of sign ups are tagged _drop and removed in the
|
|
44
|
-
* everything hook, simulating a broken flow that got fixed.
|
|
45
|
-
*
|
|
46
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
47
|
-
*
|
|
48
|
-
* Report 1: Sign Ups by Flow Version
|
|
49
|
-
* - Report type: Insights
|
|
50
|
-
* - Event: "sign up"
|
|
51
|
-
* - Measure: Total
|
|
52
|
-
* - Breakdown: "signup_flow"
|
|
53
|
-
* - Line chart by day
|
|
54
|
-
* - Expected: v1 disappears at day -7; v2 takes over and roughly 2x daily volume
|
|
55
|
-
*
|
|
56
|
-
* Report 2: Pre vs Post Volume
|
|
57
|
-
* - Report type: Insights
|
|
58
|
-
* - Event: "sign up"
|
|
59
|
-
* - Measure: Total
|
|
60
|
-
* - Compare date ranges (last 7 days vs prior 7 days)
|
|
61
|
-
* - Expected: ~ 2x signups in the post-fix window
|
|
62
|
-
*
|
|
63
|
-
* REAL-WORLD ANALOGUE: A broken signup flow silently halved conversions
|
|
64
|
-
* until a release shipped a fix; daily signups roughly doubled overnight.
|
|
65
|
-
*
|
|
66
|
-
* ----------------------------------------------------------------------------
|
|
67
|
-
* Hook 2: Watch Time Inflection (event)
|
|
68
|
-
* ----------------------------------------------------------------------------
|
|
69
|
-
*
|
|
70
|
-
* PATTERN: 30 days ago, watch time shifts. Before that date, watchTimeSec
|
|
71
|
-
* is reduced by 25-79%. After, it is increased by 25-79%. Creates a
|
|
72
|
-
* clear before/after inflection.
|
|
73
|
-
*
|
|
74
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
75
|
-
*
|
|
76
|
-
* Report 1: Avg Watch Time Over Time
|
|
77
|
-
* - Report type: Insights
|
|
78
|
-
* - Event: "watch video"
|
|
79
|
-
* - Measure: Average of "watchTimeSec"
|
|
80
|
-
* - Line chart by day
|
|
81
|
-
* - Expected: clear upward inflection ~ 30 days ago; watch time ~ doubles
|
|
82
|
-
*
|
|
83
|
-
* Report 2: Watch Time Distribution Pre vs Post
|
|
84
|
-
* - Report type: Insights
|
|
85
|
-
* - Event: "watch video"
|
|
86
|
-
* - Measure: Average of "watchTimeSec"
|
|
87
|
-
* - Compare date ranges (last 30 days vs prior 30 days)
|
|
88
|
-
* - Expected: post-inflection ~ 2x avg watch time
|
|
89
|
-
*
|
|
90
|
-
* REAL-WORLD ANALOGUE: An algorithm or UX change (autoplay, recs)
|
|
91
|
-
* inflects average watch duration sharply on a release date.
|
|
92
|
-
*
|
|
93
|
-
* ----------------------------------------------------------------------------
|
|
94
|
-
* Hook 3: Toys + Shoes Cart Correlation (event)
|
|
95
|
-
* ----------------------------------------------------------------------------
|
|
96
|
-
*
|
|
97
|
-
* PATTERN: Checkout carts with toys get a shoes item injected (and vice
|
|
98
|
-
* versa). Carts with neither toys nor shoes have all item prices
|
|
99
|
-
* discounted to 75-90% of normal.
|
|
100
|
-
*
|
|
101
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
102
|
-
*
|
|
103
|
-
* Report 1: Cart Category Co-occurrence
|
|
104
|
-
* - Report type: Insights
|
|
105
|
-
* - Event: "checkout"
|
|
106
|
-
* - Measure: Total
|
|
107
|
-
* - Breakdown: "category" (flattened item category)
|
|
108
|
-
* - Expected: toys and shoes appear at high co-occurrence rate
|
|
109
|
-
*
|
|
110
|
-
* Report 2: Cart Value by Category Mix
|
|
111
|
-
* - Report type: Insights
|
|
112
|
-
* - Event: "checkout"
|
|
113
|
-
* - Measure: Average of "amount"
|
|
114
|
-
* - Breakdown: "category"
|
|
115
|
-
* - Expected: carts lacking both toys and shoes have lower avg amount
|
|
116
|
-
*
|
|
117
|
-
* REAL-WORLD ANALOGUE: Family shoppers buying for kids tend to bundle
|
|
118
|
-
* toys with shoes; a market-basket pattern that retailers exploit.
|
|
119
|
-
*
|
|
120
|
-
* ----------------------------------------------------------------------------
|
|
121
|
-
* Hook 4: Quality to Watch Time Correlation (event)
|
|
122
|
-
* ----------------------------------------------------------------------------
|
|
123
|
-
*
|
|
124
|
-
* PATTERN: Video quality multiplies watchTimeSec: 2160p=1.5x, 1440p=1.4x,
|
|
125
|
-
* 1080p=1.3x, 720p=1.15x, 480p=1.0x, 360p=0.85x, 240p=0.7x.
|
|
126
|
-
*
|
|
127
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
128
|
-
*
|
|
129
|
-
* Report 1: Avg Watch Time by Quality
|
|
130
|
-
* - Report type: Insights
|
|
131
|
-
* - Event: "watch video"
|
|
132
|
-
* - Measure: Average of "watchTimeSec"
|
|
133
|
-
* - Breakdown: "quality"
|
|
134
|
-
* - Expected: monotonic increase from 240p to 2160p
|
|
135
|
-
*
|
|
136
|
-
* Report 2: Quality Distribution
|
|
137
|
-
* - Report type: Insights
|
|
138
|
-
* - Event: "watch video"
|
|
139
|
-
* - Measure: Total
|
|
140
|
-
* - Breakdown: "quality"
|
|
141
|
-
* - Expected: viewers split across all quality tiers; HD tiers earn more time
|
|
142
|
-
*
|
|
143
|
-
* REAL-WORLD ANALOGUE: Higher-resolution streams correlate with longer
|
|
144
|
-
* sessions because they signal a better connection and more invested viewer.
|
|
145
|
-
*
|
|
146
|
-
* ----------------------------------------------------------------------------
|
|
147
|
-
* Hook 5: Item Flattening (event)
|
|
148
|
-
* ----------------------------------------------------------------------------
|
|
149
|
-
*
|
|
150
|
-
* PATTERN: Events with an item array property get the first item's
|
|
151
|
-
* fields (category, amount, slug, etc.) flattened onto the event record
|
|
152
|
-
* as top-level properties — making them available for direct breakdown
|
|
153
|
-
* in reports.
|
|
154
|
-
*
|
|
155
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
156
|
-
*
|
|
157
|
-
* Report 1: View Items by Category
|
|
158
|
-
* - Report type: Insights
|
|
159
|
-
* - Event: "view item"
|
|
160
|
-
* - Measure: Total
|
|
161
|
-
* - Breakdown: "category"
|
|
162
|
-
* - Expected: clean breakdown across category values without nested-property issues
|
|
163
|
-
*
|
|
164
|
-
* Report 2: Add to Cart Avg Amount by Category
|
|
165
|
-
* - Report type: Insights
|
|
166
|
-
* - Event: "add to cart"
|
|
167
|
-
* - Measure: Average of "amount"
|
|
168
|
-
* - Breakdown: "category"
|
|
169
|
-
* - Expected: per-category averages render correctly off flat properties
|
|
170
|
-
*
|
|
171
|
-
* REAL-WORLD ANALOGUE: Analytics teams routinely flatten nested cart
|
|
172
|
-
* objects onto events so downstream tools can group/filter without joins.
|
|
173
|
-
*
|
|
174
|
-
* ----------------------------------------------------------------------------
|
|
175
|
-
* Hook 6: View-Item Magic Number (everything)
|
|
176
|
-
* ----------------------------------------------------------------------------
|
|
177
|
-
*
|
|
178
|
-
* PATTERN: Users who view 3-8 items in the dataset window are in the
|
|
179
|
-
* "considered buyer" sweet spot — every cart item amount and total_value
|
|
180
|
-
* gets boosted ~25%, AND ~45% of their add-to-cart events are cloned
|
|
181
|
-
* (with time offsets) to elevate their cart add rate. Users who view
|
|
182
|
-
* 9 or more items are over-engaged window-shoppers; ~30% of their
|
|
183
|
-
* checkout events are dropped (decision paralysis / browse without buy).
|
|
184
|
-
* No flag is stamped — discoverable only by binning users on view-item
|
|
185
|
-
* COUNT and comparing avg cart total or add-to-cart rate.
|
|
186
|
-
*
|
|
187
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
188
|
-
*
|
|
189
|
-
* Report 1: Avg Cart Total by View-Item Bucket
|
|
190
|
-
* - Report type: Insights (with cohort)
|
|
191
|
-
* - Cohort A: users with 3-8 "view item" events
|
|
192
|
-
* - Cohort B: users with 0-2 "view item" events
|
|
193
|
-
* - Event: "checkout"
|
|
194
|
-
* - Measure: Average of "amount" (sum across cart items if exposed)
|
|
195
|
-
* - Compare cohort A vs cohort B
|
|
196
|
-
* - Expected: cohort A ~ 1.25x higher cart total than B
|
|
197
|
-
*
|
|
198
|
-
* Report 2: Checkouts per User by Browse Intensity
|
|
199
|
-
* - Report type: Insights (with cohort)
|
|
200
|
-
* - Cohort C: users with >= 9 "view item" events
|
|
201
|
-
* - Cohort A: users with 3-8 "view item" events
|
|
202
|
-
* - Event: "checkout"
|
|
203
|
-
* - Measure: Total events per user
|
|
204
|
-
* - Compare cohort C vs cohort A
|
|
205
|
-
* - Expected: cohort C has ~ 30% fewer checkouts per user
|
|
206
|
-
*
|
|
207
|
-
* REAL-WORLD ANALOGUE: A focused buyer who reviews a handful of items
|
|
208
|
-
* tends to convert at higher cart value; an excessive browser is a
|
|
209
|
-
* tire-kicker who abandons more often.
|
|
210
|
-
*
|
|
211
|
-
* ----------------------------------------------------------------------------
|
|
212
|
-
* Hook 7: Signup Flow Time-to-Convert (everything)
|
|
213
|
-
* ----------------------------------------------------------------------------
|
|
214
|
-
*
|
|
215
|
-
* PATTERN: The Signup Flow funnel (page view → view item → save item →
|
|
216
|
-
* page view → sign up) has its time-to-convert scaled by the user's
|
|
217
|
-
* loyalty tier from SCD data (meta.scd.loyalty_tier). Gold and platinum
|
|
218
|
-
* users complete the funnel 0.67x faster; bronze users take 1.33x longer;
|
|
219
|
-
* silver is unaffected. The hook reads the latest SCD entry for
|
|
220
|
-
* loyalty_tier, falling back to a deterministic hash of user_id for
|
|
221
|
-
* consistent tier assignment. It anchors on the first "sign up" event,
|
|
222
|
-
* looks back up to 2 days for all funnel-step events in that window,
|
|
223
|
-
* and scales the whole cluster via scaleFunnelTTC.
|
|
224
|
-
*
|
|
225
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
226
|
-
*
|
|
227
|
-
* Report 1: Signup Funnel TTC by Loyalty Tier
|
|
228
|
-
* - Report type: Funnels
|
|
229
|
-
* - Steps: "page view" → "view item" → "save item" → "page view" → "sign up"
|
|
230
|
-
* - Measure: Median time to convert
|
|
231
|
-
* - Breakdown: "loyalty_tier" (SCD property)
|
|
232
|
-
* - Expected: gold/platinum ~ 0.67x median TTC vs silver; bronze ~ 1.33x
|
|
233
|
-
*
|
|
234
|
-
* Report 2: Signup Funnel Conversion by Loyalty Tier
|
|
235
|
-
* - Report type: Funnels
|
|
236
|
-
* - Steps: same as above
|
|
237
|
-
* - Breakdown: "loyalty_tier"
|
|
238
|
-
* - Expected: conversion rate similar across tiers (TTC changes, not conversion)
|
|
239
|
-
*
|
|
240
|
-
* REAL-WORLD ANALOGUE: Loyal, high-tier customers already trust the
|
|
241
|
-
* platform and breeze through signup flows, while new or low-engagement
|
|
242
|
-
* users deliberate longer before committing.
|
|
243
|
-
*
|
|
244
|
-
* ----------------------------------------------------------------------------
|
|
245
|
-
* Hook 8: Checkout Flow Experiment (funnel experiment config)
|
|
246
|
-
* ----------------------------------------------------------------------------
|
|
247
|
-
*
|
|
248
|
-
* PATTERN: A/B/C experiment on the eCommerce Purchase funnel. Control
|
|
249
|
-
* group uses base conversion, "Express Checkout" variant gets 1.25x
|
|
250
|
-
* conversion multiplier, "Social Proof" gets 1.15x conversion and 0.9x
|
|
251
|
-
* time-to-convert. Starts 30 days before dataset end.
|
|
252
|
-
*
|
|
253
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
254
|
-
*
|
|
255
|
-
* Report 1: Experiment Events by Variant
|
|
256
|
-
* - Report type: Insights
|
|
257
|
-
* - Event: "$experiment_started"
|
|
258
|
-
* - Measure: Total
|
|
259
|
-
* - Breakdown: "$experiment_variant"
|
|
260
|
-
* - Expected: roughly equal distribution across Control, Express Checkout, Social Proof
|
|
261
|
-
*
|
|
262
|
-
* Report 2: Purchase Funnel Conversion by Variant
|
|
263
|
-
* - Report type: Funnels
|
|
264
|
-
* - Funnel: view item → add to cart → checkout
|
|
265
|
-
* - Breakdown: "$experiment_variant"
|
|
266
|
-
* - Expected: Express Checkout > Social Proof > Control conversion rate
|
|
267
|
-
*
|
|
268
|
-
* REAL-WORLD ANALOGUE: Product team tests a streamlined checkout and a
|
|
269
|
-
* social proof variant against the existing flow to lift conversion.
|
|
270
|
-
*
|
|
271
|
-
* ----------------------------------------------------------------------------
|
|
272
|
-
* Hook 9: Dark Theme Power Users (funnel-pre)
|
|
273
|
-
* ----------------------------------------------------------------------------
|
|
274
|
-
*
|
|
275
|
-
* PATTERN: Dark-theme users convert 1.3x better on the purchase funnel;
|
|
276
|
-
* light-theme users convert at 0.85x. Custom theme is unaffected.
|
|
277
|
-
*
|
|
278
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
279
|
-
*
|
|
280
|
-
* Report 1: Purchase Funnel by Theme
|
|
281
|
-
* - Report type: Funnels
|
|
282
|
-
* - Funnel: view item → add to cart → checkout
|
|
283
|
-
* - Breakdown: "theme"
|
|
284
|
-
* - Expected: dark > custom > light conversion rate
|
|
285
|
-
*
|
|
286
|
-
* Report 2: Checkout Count by Theme
|
|
287
|
-
* - Report type: Insights
|
|
288
|
-
* - Event: "checkout"
|
|
289
|
-
* - Measure: Total
|
|
290
|
-
* - Breakdown: "theme"
|
|
291
|
-
* - Expected: dark-theme users over-index on checkout counts
|
|
292
|
-
*
|
|
293
|
-
* REAL-WORLD ANALOGUE: Power users who customize their UI (dark mode)
|
|
294
|
-
* tend to be more committed and convert at higher rates.
|
|
295
|
-
*
|
|
296
|
-
* ----------------------------------------------------------------------------
|
|
297
|
-
* Hook 10: Save-Item Retention (everything — retention magic number)
|
|
298
|
-
* ----------------------------------------------------------------------------
|
|
299
|
-
*
|
|
300
|
-
* PATTERN: Born-in-dataset users who perform 2+ "save item" events in
|
|
301
|
-
* their first 10 days retain long-term. Users below that threshold lose
|
|
302
|
-
* ~70% of events after day 25, simulating churn. No flag is stamped —
|
|
303
|
-
* discoverable only by segmenting users on early save-item count and
|
|
304
|
-
* comparing retention or late-period activity.
|
|
305
|
-
*
|
|
306
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
307
|
-
*
|
|
308
|
-
* Report 1: Retention by Early Save Count
|
|
309
|
-
* - Report type: Retention
|
|
310
|
-
* - First event: any event
|
|
311
|
-
* - Return event: any event
|
|
312
|
-
* - Cohort A: users with >= 2 "save item" in first 10 days
|
|
313
|
-
* - Cohort B: users with < 2 "save item" in first 10 days
|
|
314
|
-
* - Expected: Cohort A retains at 4+ weeks; Cohort B drops sharply after week 3
|
|
315
|
-
*
|
|
316
|
-
* Report 2: Post-Day-25 Activity
|
|
317
|
-
* - Report type: Insights
|
|
318
|
-
* - Event: any event
|
|
319
|
-
* - Filter: time > first_event + 25 days
|
|
320
|
-
* - Cohort A: >= 2 early saves; Cohort B: < 2 early saves
|
|
321
|
-
* - Expected: Cohort B has ~70% fewer events in the late window
|
|
322
|
-
*
|
|
323
|
-
* REAL-WORLD ANALOGUE: Saving items signals purchase intent and product
|
|
324
|
-
* engagement; users who curate a wishlist early are more likely to return.
|
|
325
|
-
*
|
|
326
|
-
* ============================================================================
|
|
327
|
-
* EXPECTED METRICS SUMMARY
|
|
328
|
-
* ============================================================================
|
|
329
|
-
*
|
|
330
|
-
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
331
|
-
* ---------------------------|-------------------------|------------|-------------|------
|
|
332
|
-
* Signup Flow Improvement | Daily sign ups | 1x | ~ 2x | 2x
|
|
333
|
-
* Watch Time Inflection | Avg watchTimeSec | 1x | ~ 2x | 2x
|
|
334
|
-
* Toys + Shoes Correlation | toys/shoes co-occurrence| ~ 7% | ~ 50%+ | ~ 7x
|
|
335
|
-
* Quality -> Watch Time | Avg watchTimeSec (240p->2160p) | 0.7x | 1.5x | ~ 2.1x
|
|
336
|
-
* Item Flattening | category breakdown | nested | flat | n/a
|
|
337
|
-
* View-Item Magic Number | sweet (3-8) cart rate | 1x | ~ 1.45x | 1.45x
|
|
338
|
-
* View-Item Magic Number | over (9+) checkouts/user| 1x | ~ 0.7x | -30%
|
|
339
|
-
* Signup TTC by Loyalty | gold/plat median TTC | 1x | ~ 0.67x | -33%
|
|
340
|
-
* Signup TTC by Loyalty | bronze median TTC | 1x | ~ 1.33x | +33%
|
|
341
|
-
* Checkout Experiment | Express Checkout conv | 1x | ~ 1.25x | +25%
|
|
342
|
-
* Checkout Experiment | Social Proof conv | 1x | ~ 1.15x | +15%
|
|
343
|
-
* Checkout Experiment | Social Proof TTC | 1x | ~ 0.9x | -10%
|
|
344
|
-
* Dark Theme Power Users | dark-theme conv rate | 1x | ~ 1.3x | +30%
|
|
345
|
-
* Dark Theme Power Users | light-theme conv rate | 1x | ~ 0.85x | -15%
|
|
346
|
-
* Save-Item Retention | post-d25 events (saver) | 1x | ~ 1x | baseline
|
|
347
|
-
* Save-Item Retention | post-d25 events (non-s) | 1x | ~ 0.3x | -70%
|
|
348
|
-
* ============================================================================
|
|
349
|
-
*/
|
|
350
|
-
|
|
351
|
-
// ── SCALE ──
|
|
352
|
-
const SEED = "simple is best";
|
|
353
|
-
const NUM_USERS = 42_000;
|
|
354
|
-
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
355
|
-
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
356
|
-
const EVENTS_PER_DAY = 0.37;
|
|
357
|
-
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
358
|
-
|
|
359
|
-
const chance = initChance(SEED);
|
|
360
|
-
|
|
361
|
-
// ── KNOBS (tweak these to reshape stories) ──
|
|
362
|
-
const SIGNUP_FIX_DAYS_AGO = 7;
|
|
363
|
-
const WATCH_INFLECTION_DAYS_AGO = 30;
|
|
364
|
-
const WATCH_FACTOR_MIN = 0.25;
|
|
365
|
-
const WATCH_FACTOR_MAX = 0.79;
|
|
366
|
-
const VIEW_SWEET_MIN = 3;
|
|
367
|
-
const VIEW_SWEET_MAX = 8;
|
|
368
|
-
const VIEW_OVER_THRESHOLD = 9;
|
|
369
|
-
const SWEET_CART_BOOST = 1.25;
|
|
370
|
-
const SWEET_CLONE_LIKELIHOOD = 45;
|
|
371
|
-
const OVER_CHECKOUT_DROP_LIKELIHOOD = 30;
|
|
372
|
-
const LOYALTY_TTC_FAST = 0.67;
|
|
373
|
-
const LOYALTY_TTC_SLOW = 1.33;
|
|
374
|
-
const LOYALTY_LOOKBACK_DAYS = 2;
|
|
375
|
-
const SAVE_RETENTION_MIN = 2;
|
|
376
|
-
const SAVE_RETENTION_WINDOW_DAYS = 10;
|
|
377
|
-
const SAVE_RETENTION_CUTOFF_DAYS = 25;
|
|
378
|
-
const SAVE_RETENTION_DROP_LIKELIHOOD = 70;
|
|
379
|
-
const QUALITY_FACTORS = {
|
|
380
|
-
"2160p": 1.5,
|
|
381
|
-
"1440p": 1.4,
|
|
382
|
-
"1080p": 1.3,
|
|
383
|
-
"720p": 1.15,
|
|
384
|
-
"480p": 1.0,
|
|
385
|
-
"360p": 0.85,
|
|
386
|
-
"240p": 0.7,
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
// ── DATA ARRAYS ──
|
|
390
|
-
const videoCategories = ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"];
|
|
391
|
-
const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
|
|
392
|
-
const productCategories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
|
|
393
|
-
const productDescriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
|
|
394
|
-
const productSuffixes = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
|
|
395
|
-
const assetPreview = [".png", ".jpg", ".jpeg", ".heic", ".mp4", ".mov", ".avi"];
|
|
396
|
-
|
|
397
|
-
// ── HELPER FUNCTIONS ──
|
|
398
|
-
function makeProducts(maxItems = 5) {
|
|
399
|
-
return function () {
|
|
400
|
-
const data = [];
|
|
401
|
-
const numOfItems = integer(1, maxItems);
|
|
402
|
-
|
|
403
|
-
for (let i = 0; i < numOfItems; i++) {
|
|
404
|
-
const category = chance.pickone(productCategories);
|
|
405
|
-
const descriptor = chance.pickone(productDescriptors);
|
|
406
|
-
const suffixWord = chance.pickone(productSuffixes);
|
|
407
|
-
const slug = `${descriptor.replace(/\s+/g, "-").toLowerCase()}-${suffixWord.replace(/\s+/g, "-").toLowerCase()}`;
|
|
408
|
-
const asset = chance.pickone(assetPreview);
|
|
409
|
-
|
|
410
|
-
const price = integer(1, 100);
|
|
411
|
-
const quantity = integer(1, 5);
|
|
412
|
-
|
|
413
|
-
data.push({
|
|
414
|
-
amount: price,
|
|
415
|
-
quantity: quantity,
|
|
416
|
-
total_value: price * quantity,
|
|
417
|
-
featured: chance.pickone([true, false, false]),
|
|
418
|
-
category: category,
|
|
419
|
-
descriptor: descriptor,
|
|
420
|
-
slug: slug,
|
|
421
|
-
assetPreview: `https://example.com/assets/${slug}${asset}`,
|
|
422
|
-
assetType: asset,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return () => [data];
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function handleFunnelPreHooks(record, meta) {
|
|
431
|
-
// H9: Dark Theme Power Users — dark +30%, light -15%, custom unchanged
|
|
432
|
-
const isPurchaseFunnel = meta.funnel?.sequence?.includes("checkout");
|
|
433
|
-
if (isPurchaseFunnel) {
|
|
434
|
-
const theme = meta.profile?.theme;
|
|
435
|
-
if (theme === "dark") record.conversionRate = Math.min(95, Math.round(record.conversionRate * 1.3));
|
|
436
|
-
else if (theme === "light") record.conversionRate = Math.round(record.conversionRate * 0.85);
|
|
437
|
-
}
|
|
438
|
-
return record;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function handleEventHooks(record) {
|
|
442
|
-
// H5: Item flattening — promote first item's fields to top level
|
|
443
|
-
if (record.item && Array.isArray(record.item)) {
|
|
444
|
-
record = { ...record, ...record.item[0] };
|
|
445
|
-
delete record.item;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// H3: Toys + Shoes cart correlation
|
|
449
|
-
if (record.event === "checkout" && Array.isArray(record.cart)) {
|
|
450
|
-
const hasToys = record.cart.some(item => item.category === "toys");
|
|
451
|
-
const hasShoes = record.cart.some(item => item.category === "shoes");
|
|
452
|
-
if (hasToys && !hasShoes) {
|
|
453
|
-
const bigCart = makeProducts(20)()()[0];
|
|
454
|
-
const shoeItems = bigCart.filter(item => item.category === "shoes");
|
|
455
|
-
if (shoeItems.length > 0) record.cart.push(shoeItems[0]);
|
|
456
|
-
}
|
|
457
|
-
if (hasShoes && !hasToys) {
|
|
458
|
-
const bigCart = makeProducts(20)()()[0];
|
|
459
|
-
const toyItems = bigCart.filter(item => item.category === "toys");
|
|
460
|
-
if (toyItems.length > 0) record.cart.push(toyItems[0]);
|
|
461
|
-
}
|
|
462
|
-
if (!hasToys && !hasShoes) {
|
|
463
|
-
const cheapFactor = decimal(0.75, 0.9);
|
|
464
|
-
record.cart = record.cart.map(item => ({
|
|
465
|
-
...item,
|
|
466
|
-
amount: Math.round(item.amount * cheapFactor),
|
|
467
|
-
total_value: Math.round(item.total_value * cheapFactor),
|
|
468
|
-
}));
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// H4: Quality → watch time correlation
|
|
473
|
-
if (record.event === "watch video") {
|
|
474
|
-
const quality = record.quality || "480p";
|
|
475
|
-
const factor = QUALITY_FACTORS[quality] ?? 1.0;
|
|
476
|
-
record.watchTimeSec = Math.round(record.watchTimeSec * factor);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return record;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function handleEverythingHooks(record, meta) {
|
|
483
|
-
const profile = meta.profile;
|
|
484
|
-
record.forEach(e => {
|
|
485
|
-
e.theme = profile.theme;
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const datasetEnd = dayjs.unix(meta.datasetEnd);
|
|
489
|
-
const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(SIGNUP_FIX_DAYS_AGO, "day");
|
|
490
|
-
const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(WATCH_INFLECTION_DAYS_AGO, "day");
|
|
491
|
-
|
|
492
|
-
// H1: Signup flow v1 → v2; pre-fix v1 signups lose 50% to _drop
|
|
493
|
-
record.forEach(e => {
|
|
494
|
-
if (e.event !== "sign up") return;
|
|
495
|
-
const eventTime = dayjs(e.time);
|
|
496
|
-
e.signup_flow = "v1";
|
|
497
|
-
if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
|
|
498
|
-
if (chance.bool({ likelihood: 50 })) e._drop = true;
|
|
499
|
-
}
|
|
500
|
-
if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
|
|
501
|
-
e.signup_flow = "v2";
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// H2: Watch time inflection — before: shrink, after: grow
|
|
506
|
-
record.forEach(e => {
|
|
507
|
-
if (e.event !== "watch video") return;
|
|
508
|
-
const eventTime = dayjs(e.time);
|
|
509
|
-
const factor = decimal(WATCH_FACTOR_MIN, WATCH_FACTOR_MAX);
|
|
510
|
-
if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
|
|
511
|
-
e.watchTimeSec = Math.round(e.watchTimeSec * (1 - factor));
|
|
512
|
-
}
|
|
513
|
-
if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
|
|
514
|
-
e.watchTimeSec = Math.round(e.watchTimeSec * (1 + factor));
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
// H6: View-item magic number — sweet spot (3-8) boost; over (9+) checkout drop
|
|
519
|
-
const viewItemCount = record.filter(e => e.event === "view item").length;
|
|
520
|
-
if (viewItemCount >= VIEW_SWEET_MIN && viewItemCount <= VIEW_SWEET_MAX) {
|
|
521
|
-
record.forEach(e => {
|
|
522
|
-
if (e.event === "checkout" && Array.isArray(e.cart)) {
|
|
523
|
-
e.cart = e.cart.map(it => ({
|
|
524
|
-
...it,
|
|
525
|
-
amount: typeof it.amount === "number" ? Math.round(it.amount * SWEET_CART_BOOST) : it.amount,
|
|
526
|
-
total_value: typeof it.total_value === "number" ? Math.round(it.total_value * SWEET_CART_BOOST) : it.total_value,
|
|
527
|
-
}));
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
const addToCartEvents = record.filter(e => e.event === "add to cart");
|
|
531
|
-
const clones = [];
|
|
532
|
-
addToCartEvents.forEach(e => {
|
|
533
|
-
if (chance.bool({ likelihood: SWEET_CLONE_LIKELIHOOD })) {
|
|
534
|
-
const offsetMs = integer(60_000, 600_000);
|
|
535
|
-
clones.push({
|
|
536
|
-
...e,
|
|
537
|
-
insert_id: uid(),
|
|
538
|
-
time: dayjs(e.time).add(offsetMs, "milliseconds").toISOString(),
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
if (clones.length > 0) record.push(...clones);
|
|
543
|
-
} else if (viewItemCount >= VIEW_OVER_THRESHOLD) {
|
|
544
|
-
for (let i = record.length - 1; i >= 0; i--) {
|
|
545
|
-
if (record[i].event === "checkout" && chance.bool({ likelihood: OVER_CHECKOUT_DROP_LIKELIHOOD })) {
|
|
546
|
-
record.splice(i, 1);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// H7: Signup-flow TTC scaled by loyalty tier (gold/plat fast, bronze slow)
|
|
552
|
-
{
|
|
553
|
-
let loyaltyTier = "silver";
|
|
554
|
-
const scdEntries = meta?.scd?.loyalty_tier;
|
|
555
|
-
if (Array.isArray(scdEntries) && scdEntries.length > 0) {
|
|
556
|
-
const latest = scdEntries.reduce((a, b) => (a.time > b.time ? a : b));
|
|
557
|
-
loyaltyTier = latest.loyalty_tier || "silver";
|
|
558
|
-
} else {
|
|
559
|
-
const uidStr = record[0]?.user_id || "";
|
|
560
|
-
const hash = uidStr.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
|
561
|
-
const bucket = hash % 100;
|
|
562
|
-
loyaltyTier = bucket < 20 ? "gold" : bucket < 50 ? "silver" : "bronze";
|
|
563
|
-
}
|
|
564
|
-
const ttcFactor = (
|
|
565
|
-
loyaltyTier === "gold" || loyaltyTier === "platinum" ? LOYALTY_TTC_FAST :
|
|
566
|
-
loyaltyTier === "bronze" ? LOYALTY_TTC_SLOW :
|
|
567
|
-
1.0
|
|
568
|
-
);
|
|
569
|
-
if (ttcFactor !== 1.0) {
|
|
570
|
-
const funnelSteps = new Set(["page view", "view item", "save item", "sign up"]);
|
|
571
|
-
const sorted = record.slice().sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
572
|
-
const signupEvent = sorted.find(e => e.event === "sign up");
|
|
573
|
-
if (signupEvent) {
|
|
574
|
-
const signupMs = new Date(signupEvent.time).getTime();
|
|
575
|
-
const windowMs = LOYALTY_LOOKBACK_DAYS * 24 * 60 * 60 * 1000;
|
|
576
|
-
const funnelEvents = sorted.filter(e =>
|
|
577
|
-
funnelSteps.has(e.event) &&
|
|
578
|
-
new Date(e.time).getTime() >= signupMs - windowMs &&
|
|
579
|
-
new Date(e.time).getTime() <= signupMs
|
|
580
|
-
);
|
|
581
|
-
if (funnelEvents.length >= 2) scaleFunnelTTC(funnelEvents, ttcFactor);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// H10: Save-item retention — born-in users below threshold churn after day 25
|
|
587
|
-
if (meta.userIsBornInDataset) {
|
|
588
|
-
const firstT = record[0]?.time;
|
|
589
|
-
if (firstT) {
|
|
590
|
-
const window10 = dayjs(firstT).add(SAVE_RETENTION_WINDOW_DAYS, "days").toISOString();
|
|
591
|
-
const saves = record.filter(e => e.event === "save item" && e.time <= window10).length;
|
|
592
|
-
if (saves < SAVE_RETENTION_MIN) {
|
|
593
|
-
const cutoff = dayjs(firstT).add(SAVE_RETENTION_CUTOFF_DAYS, "days");
|
|
594
|
-
for (let i = record.length - 1; i >= 0; i--) {
|
|
595
|
-
if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: SAVE_RETENTION_DROP_LIKELIHOOD })) {
|
|
596
|
-
record.splice(i, 1);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return record.filter(e => !e._drop);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// ── CONFIG ──
|
|
607
|
-
/** @type {Config} */
|
|
608
|
-
const config = {
|
|
609
|
-
version: 2,
|
|
610
|
-
seed: SEED,
|
|
611
|
-
datasetStart: DATASET_START,
|
|
612
|
-
datasetEnd: DATASET_END,
|
|
613
|
-
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
614
|
-
numUsers: NUM_USERS,
|
|
615
|
-
format: "json",
|
|
616
|
-
credentials: {
|
|
617
|
-
token,
|
|
618
|
-
region: "US",
|
|
619
|
-
},
|
|
620
|
-
switches: {
|
|
621
|
-
hasSessionIds: true,
|
|
622
|
-
hasAdSpend: false,
|
|
623
|
-
hasLocation: true,
|
|
624
|
-
hasAndroidDevices: true,
|
|
625
|
-
hasIOSDevices: true,
|
|
626
|
-
hasDesktopDevices: true,
|
|
627
|
-
hasBrowser: true,
|
|
628
|
-
hasCampaigns: false,
|
|
629
|
-
isAnonymous: false,
|
|
630
|
-
alsoInferFunnels: false,
|
|
631
|
-
},
|
|
632
|
-
identity: {
|
|
633
|
-
avgDevicePerUser: 2,
|
|
634
|
-
},
|
|
635
|
-
concurrency: 1,
|
|
636
|
-
events: [
|
|
637
|
-
{
|
|
638
|
-
event: "checkout",
|
|
639
|
-
weight: 2,
|
|
640
|
-
isStrictEvent: false,
|
|
641
|
-
properties: {
|
|
642
|
-
currency: ["USD", "CAD", "EUR", "BTC", "ETH", "JPY"],
|
|
643
|
-
coupon: weighChoices(["none", "none", "none", "none", "10%OFF", "20%OFF", "10%OFF", "20%OFF", "30%OFF", "40%OFF", "50%OFF"]),
|
|
644
|
-
cart: makeProducts(),
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
event: "add to cart",
|
|
649
|
-
weight: 4,
|
|
650
|
-
isStrictEvent: false,
|
|
651
|
-
properties: {
|
|
652
|
-
item: makeProducts(1),
|
|
653
|
-
amount: weighNumRange(1, 100, 0.3),
|
|
654
|
-
quantity: weighNumRange(1, 5, 0.3),
|
|
655
|
-
total_value: weighNumRange(1, 500, 0.3),
|
|
656
|
-
featured: [true, false, false],
|
|
657
|
-
category: productCategories,
|
|
658
|
-
descriptor: productDescriptors,
|
|
659
|
-
slug: ["item"],
|
|
660
|
-
assetPreview: ["https://example.com/assets/item.png"],
|
|
661
|
-
assetType: assetPreview,
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
event: "view item",
|
|
666
|
-
weight: 8,
|
|
667
|
-
isStrictEvent: false,
|
|
668
|
-
properties: {
|
|
669
|
-
item: makeProducts(1),
|
|
670
|
-
amount: weighNumRange(1, 100, 0.3),
|
|
671
|
-
quantity: weighNumRange(1, 5, 0.3),
|
|
672
|
-
total_value: weighNumRange(1, 500, 0.3),
|
|
673
|
-
featured: [true, false, false],
|
|
674
|
-
category: productCategories,
|
|
675
|
-
descriptor: productDescriptors,
|
|
676
|
-
slug: ["item"],
|
|
677
|
-
assetPreview: ["https://example.com/assets/item.png"],
|
|
678
|
-
assetType: assetPreview,
|
|
679
|
-
},
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
event: "save item",
|
|
683
|
-
weight: 5,
|
|
684
|
-
isStrictEvent: false,
|
|
685
|
-
properties: {
|
|
686
|
-
item: makeProducts(1),
|
|
687
|
-
amount: weighNumRange(1, 100, 0.3),
|
|
688
|
-
quantity: weighNumRange(1, 5, 0.3),
|
|
689
|
-
total_value: weighNumRange(1, 500, 0.3),
|
|
690
|
-
featured: [true, false, false],
|
|
691
|
-
category: productCategories,
|
|
692
|
-
descriptor: productDescriptors,
|
|
693
|
-
slug: ["item"],
|
|
694
|
-
assetPreview: ["https://example.com/assets/item.png"],
|
|
695
|
-
assetType: assetPreview,
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
{
|
|
699
|
-
event: "page view",
|
|
700
|
-
weight: 10,
|
|
701
|
-
isStrictEvent: false,
|
|
702
|
-
properties: {
|
|
703
|
-
page: ["/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
event: "watch video",
|
|
708
|
-
weight: 8,
|
|
709
|
-
isStrictEvent: false,
|
|
710
|
-
properties: {
|
|
711
|
-
watchTimeSec: weighNumRange(10, 600, 0.25),
|
|
712
|
-
quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
|
|
713
|
-
format: ["mp4", "avi", "mov", "mpg"],
|
|
714
|
-
uploader_id: chance.guid.bind(chance),
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
{ event: "like video", weight: 6, properties: {} },
|
|
718
|
-
{ event: "dislike video", weight: 4, properties: {} },
|
|
719
|
-
{
|
|
720
|
-
event: "sign up",
|
|
721
|
-
weight: 1,
|
|
722
|
-
isFirstEvent: true,
|
|
723
|
-
isAuthEvent: true,
|
|
724
|
-
properties: {
|
|
725
|
-
signupMethod: ["email", "google", "facebook", "twitter", "linkedin", "github"],
|
|
726
|
-
referral: weighChoices(["none", "none", "none", "friend", "ad", "ad", "ad", "friend", "friend", "friend", "friend"]),
|
|
727
|
-
signup_flow: ["v1"],
|
|
728
|
-
},
|
|
729
|
-
},
|
|
730
|
-
],
|
|
731
|
-
funnels: [
|
|
732
|
-
{
|
|
733
|
-
sequence: ["page view", "view item", "save item", "page view", "sign up"],
|
|
734
|
-
conversionRate: 50,
|
|
735
|
-
order: "first-and-last-fixed",
|
|
736
|
-
weight: 1,
|
|
737
|
-
isFirstFunnel: true,
|
|
738
|
-
timeToConvert: 2,
|
|
739
|
-
experiment: false,
|
|
740
|
-
name: "Signup Flow",
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
sequence: ["watch video", "like video", "watch video", "like video"],
|
|
744
|
-
name: "Video Likes",
|
|
745
|
-
conversionRate: 60,
|
|
746
|
-
props: {
|
|
747
|
-
videoCategory: videoCategories,
|
|
748
|
-
quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
|
|
749
|
-
format: ["mp4", "avi", "mov", "mpg"],
|
|
750
|
-
uploader_id: chance.guid.bind(chance),
|
|
751
|
-
},
|
|
752
|
-
},
|
|
753
|
-
{
|
|
754
|
-
name: "Video Dislikes",
|
|
755
|
-
sequence: ["watch video", "dislike video", "watch video", "dislike video"],
|
|
756
|
-
conversionRate: 20,
|
|
757
|
-
props: {
|
|
758
|
-
videoCategory: videoCategories,
|
|
759
|
-
quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
|
|
760
|
-
format: ["mp4", "avi", "mov", "mpg"],
|
|
761
|
-
uploader_id: chance.guid.bind(chance),
|
|
762
|
-
},
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
name: "eCommerce Purchase",
|
|
766
|
-
sequence: ["view item", "view item", "add to cart", "view item", "add to cart", "checkout"],
|
|
767
|
-
conversionRate: 15,
|
|
768
|
-
requireRepeats: true,
|
|
769
|
-
weight: 10,
|
|
770
|
-
order: "last-fixed",
|
|
771
|
-
experiment: {
|
|
772
|
-
name: "Express Checkout",
|
|
773
|
-
variants: [
|
|
774
|
-
{ name: "Control" },
|
|
775
|
-
{ name: "Express Checkout", conversionMultiplier: 1.25 },
|
|
776
|
-
{ name: "Social Proof", conversionMultiplier: 1.15, ttcMultiplier: 0.9 },
|
|
777
|
-
],
|
|
778
|
-
startDaysBeforeEnd: 30,
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
],
|
|
782
|
-
superProps: {
|
|
783
|
-
theme: ["light", "dark", "custom", "light", "dark"],
|
|
784
|
-
},
|
|
785
|
-
userProps: {
|
|
786
|
-
title: chance.profession.bind(chance),
|
|
787
|
-
luckyNumber: weighNumRange(42, 420, 0.3),
|
|
788
|
-
spiritAnimal: spiritAnimals,
|
|
789
|
-
theme: ["light", "dark", "custom", "light", "dark"],
|
|
790
|
-
},
|
|
791
|
-
scdProps: {
|
|
792
|
-
loyalty_tier: {
|
|
793
|
-
values: ["bronze", "silver", "gold", "platinum"],
|
|
794
|
-
frequency: "month",
|
|
795
|
-
timing: "fuzzy",
|
|
796
|
-
max: 8,
|
|
797
|
-
},
|
|
798
|
-
},
|
|
799
|
-
mirrorProps: {},
|
|
800
|
-
groupKeys: [],
|
|
801
|
-
groupProps: {},
|
|
802
|
-
lookupTables: [],
|
|
803
|
-
hook(record, type, meta) {
|
|
804
|
-
if (type === "funnel-pre") return handleFunnelPreHooks(record, meta);
|
|
805
|
-
if (type === "event") return handleEventHooks(record);
|
|
806
|
-
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
807
|
-
return record;
|
|
808
|
-
},
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
export default config;
|