@ak--47/dungeon-master 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-fintech";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -15,7 +15,6 @@ import * as u from "../../lib/utils/utils.js";
|
|
|
15
15
|
|
|
16
16
|
dayjs.extend(utc);
|
|
17
17
|
const chance = u.initChance(SEED);
|
|
18
|
-
|
|
19
18
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
20
19
|
|
|
21
20
|
/**
|
|
@@ -48,58 +47,223 @@ const chance = u.initChance(SEED);
|
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* ===================================================================
|
|
51
|
-
* ANALYTICS HOOKS (
|
|
50
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
52
51
|
* ===================================================================
|
|
53
52
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
53
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable
|
|
54
|
+
* via behavioral cohorts, raw-prop breakdowns (date, account_tier),
|
|
55
|
+
* or funnel time-to-convert.
|
|
56
|
+
*
|
|
57
|
+
* 1. PERSONAL VS BUSINESS ACCOUNTS (user)
|
|
58
|
+
*
|
|
59
|
+
* PATTERN: 20% of accounts are business (employee_count, revenue,
|
|
60
|
+
* industry attached) and 80% are personal (age_range, life_stage).
|
|
61
|
+
* Account segment shapes downstream transaction sizes.
|
|
62
|
+
*
|
|
63
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
64
|
+
*
|
|
65
|
+
* Report 1: Account Segment Mix
|
|
66
|
+
* - Report type: Insights
|
|
67
|
+
* - Event: any event
|
|
68
|
+
* - Measure: Unique users
|
|
69
|
+
* - Breakdown: "account_segment"
|
|
70
|
+
* - Expected: ~80% personal, ~20% business
|
|
71
|
+
*
|
|
72
|
+
* Report 2: Transaction Size by Segment
|
|
73
|
+
* - Report type: Insights
|
|
74
|
+
* - Event: "transaction completed"
|
|
75
|
+
* - Measure: Average of "amount"
|
|
76
|
+
* - Breakdown: "account_segment"
|
|
77
|
+
* - Expected: business ~ $200+, personal ~ $50 (~4x larger)
|
|
78
|
+
*
|
|
79
|
+
* REAL-WORLD ANALOGUE: Neobanks serve both consumers and small
|
|
80
|
+
* businesses with the same core product, but business activity is
|
|
81
|
+
* meaningfully higher value per transaction.
|
|
82
|
+
*
|
|
83
|
+
* ---------------------------------------------------------------
|
|
84
|
+
* 2. PAYDAY PATTERNS (everything)
|
|
85
|
+
*
|
|
86
|
+
* PATTERN: Direct deposit transactions are 3x larger on the 1st and
|
|
87
|
+
* 15th of the month. Transfers are ~1.6x larger on the 1st-3rd and
|
|
88
|
+
* 15th-17th (60% likelihood × 2x boost). No flag — discover via day-of-month
|
|
89
|
+
* breakdown on raw amount.
|
|
90
|
+
*
|
|
91
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
92
|
+
*
|
|
93
|
+
* Report 1: Direct Deposit Size by Day of Month
|
|
94
|
+
* - Report type: Insights
|
|
95
|
+
* - Event: "transaction completed"
|
|
96
|
+
* - Measure: Average of "amount"
|
|
97
|
+
* - Filter: "transaction_type" = "direct_deposit"
|
|
98
|
+
* - Breakdown: day of month
|
|
99
|
+
* - Expected: 1st and 15th avg ~ 3x other days
|
|
100
|
+
*
|
|
101
|
+
* Report 2: Post-Payday Transfer Spending by Day of Month
|
|
102
|
+
* - Report type: Insights
|
|
103
|
+
* - Event: "transfer sent"
|
|
104
|
+
* - Measure: Average of "amount"
|
|
105
|
+
* - Breakdown: day of month
|
|
106
|
+
* - Expected: 1-3 and 15-17 avg ~ 1.6x other days
|
|
107
|
+
*
|
|
108
|
+
* REAL-WORLD ANALOGUE: Bi-monthly payroll cycles drive predictable
|
|
109
|
+
* spikes in deposit and outbound spending volume.
|
|
110
|
+
*
|
|
111
|
+
* ---------------------------------------------------------------
|
|
112
|
+
* 3. FRAUD DETECTION (everything)
|
|
113
|
+
*
|
|
114
|
+
* PATTERN: ~1-2% of users experience a fraud burst at the timeline
|
|
115
|
+
* midpoint: 3-5 rapid high-value transactions, then card locked
|
|
116
|
+
* (reason="suspicious_activity"), dispute filed (reason="unauthorized"),
|
|
117
|
+
* and support contacted (issue_type="card"). No flag — derive cohort
|
|
118
|
+
* by joining users who had all three event types within ~1 hour.
|
|
119
|
+
*
|
|
120
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
121
|
+
*
|
|
122
|
+
* Report 1: Fraud Cohort
|
|
123
|
+
* - Report type: Cohort builder
|
|
124
|
+
* - Filter: did "card locked" with reason="suspicious_activity"
|
|
125
|
+
* AND "dispute filed" with reason="unauthorized"
|
|
126
|
+
* within 1 hour of each other
|
|
127
|
+
* - Expected: ~1-2% of total users
|
|
128
|
+
*
|
|
129
|
+
* Report 2: Fraud Resolution Funnel
|
|
130
|
+
* - Report type: Funnels
|
|
131
|
+
* - Steps: "card locked" -> "dispute filed" -> "support contacted"
|
|
132
|
+
* - Filter: card locked.reason = "suspicious_activity"
|
|
133
|
+
* - Expected: high completion across all three resolution steps
|
|
134
|
+
*
|
|
135
|
+
* REAL-WORLD ANALOGUE: A small but consistent slice of accounts
|
|
136
|
+
* triggers fraud pipelines every cycle, generating the bulk of
|
|
137
|
+
* dispute and support load.
|
|
138
|
+
*
|
|
139
|
+
* ---------------------------------------------------------------
|
|
140
|
+
* 4. LOW BALANCE CHURN (everything)
|
|
141
|
+
*
|
|
142
|
+
* PATTERN: Users with 3+ "balance checked" events where account_balance
|
|
143
|
+
* < $15K lose 50% of their events after day 30. No flag — derive cohort
|
|
144
|
+
* by counting low-balance checks per user.
|
|
145
|
+
*
|
|
146
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
147
|
+
*
|
|
148
|
+
* Report 1: Activity by Low Balance Cohort
|
|
149
|
+
* - Cohort A: users with >= 3 "balance checked" where account_balance < 15000
|
|
150
|
+
* - Cohort B: users with < 3
|
|
151
|
+
* - Event: any event
|
|
152
|
+
* - Measure: Total per user, line chart by day
|
|
153
|
+
* - Expected: A growth post-d30 ~ 0.5x B's growth (suppressed)
|
|
154
|
+
*
|
|
155
|
+
* Report 2: Activity Decline Timeline
|
|
156
|
+
* - Report type: Insights (with cohort A above)
|
|
157
|
+
* - Event: any event
|
|
158
|
+
* - Measure: Total
|
|
159
|
+
* - Line chart by day
|
|
160
|
+
* - Expected: pre-d30/post-d30 ratio ~ 1.0x for A vs ~ 2.0x for B
|
|
161
|
+
*
|
|
162
|
+
* REAL-WORLD ANALOGUE: Customers running thin balances lose trust
|
|
163
|
+
* in the platform and migrate their primary banking elsewhere.
|
|
164
|
+
*
|
|
165
|
+
* ---------------------------------------------------------------
|
|
166
|
+
* 5. BUDGET USERS SAVE MORE (everything)
|
|
167
|
+
*
|
|
168
|
+
* PATTERN: Users with any "budget created" event get 2x savings
|
|
169
|
+
* contributions, 1.5x investment amounts, and extra cloned savings
|
|
170
|
+
* goal events. No flag — derive cohort behaviorally.
|
|
171
|
+
*
|
|
172
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
173
|
+
*
|
|
174
|
+
* Report 1: Savings Contribution by Budget Cohort
|
|
175
|
+
* - Cohort A: users with >= 1 "budget created" event
|
|
176
|
+
* - Cohort B: users with 0
|
|
177
|
+
* - Event: "savings goal set"
|
|
178
|
+
* - Measure: Average of "monthly_contribution"
|
|
179
|
+
* - Expected: A ~ 2x B
|
|
180
|
+
*
|
|
181
|
+
* Report 2: Investment Size by Budget Cohort
|
|
182
|
+
* - Cohort A vs B (as above)
|
|
183
|
+
* - Event: "investment made"
|
|
184
|
+
* - Measure: Average of "amount"
|
|
185
|
+
* - Expected: A ~ 1.5x B
|
|
186
|
+
*
|
|
187
|
+
* REAL-WORLD ANALOGUE: Active budget tooling correlates strongly
|
|
188
|
+
* with healthier savings rates and broader product adoption.
|
|
189
|
+
*
|
|
190
|
+
* ---------------------------------------------------------------
|
|
191
|
+
* 6. AUTO-PAY LOYALTY (event)
|
|
192
|
+
*
|
|
193
|
+
* PATTERN: Manual payers (auto_pay=false) miss 30% of their bill
|
|
194
|
+
* payments — those events are renamed to "bill payment missed".
|
|
195
|
+
* Auto-pay users never miss.
|
|
196
|
+
*
|
|
197
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
198
|
+
*
|
|
199
|
+
* Report 1: Bill Outcomes by Manual vs Auto-Pay
|
|
200
|
+
* - Report type: Insights
|
|
201
|
+
* - Events: "bill paid" and "bill payment missed"
|
|
202
|
+
* - Measure: Total
|
|
203
|
+
* - Expected: missed events appear only for manual payers, ~30% rate
|
|
204
|
+
*
|
|
205
|
+
* Report 2: Bill Completion Rate by Auto-Pay
|
|
206
|
+
* - Report type: Insights
|
|
207
|
+
* - Event: "bill paid"
|
|
208
|
+
* - Measure: Total
|
|
209
|
+
* - Breakdown: "auto_pay"
|
|
210
|
+
* - Expected: auto_pay=false ~ 70% completion vs auto_pay=true ~ 100%
|
|
211
|
+
*
|
|
212
|
+
* REAL-WORLD ANALOGUE: Auto-pay locks users into a frictionless
|
|
213
|
+
* payment cadence that virtually eliminates missed bills.
|
|
214
|
+
*
|
|
215
|
+
* ---------------------------------------------------------------
|
|
216
|
+
* 7. PREMIUM TIER VALUE (event)
|
|
217
|
+
*
|
|
218
|
+
* PATTERN: Premium-tier users get 3x reward values and 2x sell
|
|
219
|
+
* returns on investments. Plus tier gets 1.5x rewards. No flag —
|
|
220
|
+
* mutates raw "value"/"amount" properties; discover via account_tier breakdown.
|
|
221
|
+
*
|
|
222
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
223
|
+
*
|
|
224
|
+
* Report 1: Reward Value by Tier
|
|
225
|
+
* - Report type: Insights
|
|
226
|
+
* - Event: "reward redeemed"
|
|
227
|
+
* - Measure: Average of "value"
|
|
228
|
+
* - Breakdown: "account_tier"
|
|
229
|
+
* - Expected: Premium ~ $30, Plus ~ $15, Basic ~ $10
|
|
230
|
+
*
|
|
231
|
+
* Report 2: Investment Sell Amount by Tier
|
|
232
|
+
* - Report type: Insights
|
|
233
|
+
* - Event: "investment made"
|
|
234
|
+
* - Measure: Average of "amount"
|
|
235
|
+
* - Filter: "action" = "sell"
|
|
236
|
+
* - Breakdown: "account_tier"
|
|
237
|
+
* - Expected: Premium ~ 2x baseline; Basic/Plus baseline
|
|
238
|
+
*
|
|
239
|
+
* REAL-WORLD ANALOGUE: Premium subscription tiers justify their
|
|
240
|
+
* price by delivering visibly better cashback and investment perks.
|
|
241
|
+
*
|
|
242
|
+
* ---------------------------------------------------------------
|
|
243
|
+
* 8. MONTH-END ANXIETY (everything)
|
|
244
|
+
*
|
|
245
|
+
* PATTERN: On days >= 28 of the calendar month, app sessions run
|
|
246
|
+
* 40% longer and reported balances are 30% lower. No flag — discover
|
|
247
|
+
* via day-of-month breakdown.
|
|
248
|
+
*
|
|
249
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
250
|
+
*
|
|
251
|
+
* Report 1: Session Duration by Day of Month
|
|
252
|
+
* - Report type: Insights
|
|
253
|
+
* - Event: "app session"
|
|
254
|
+
* - Measure: Average of "session_duration_sec"
|
|
255
|
+
* - Breakdown: day of month
|
|
256
|
+
* - Expected: days >= 28 ~ 1.4x other days
|
|
257
|
+
*
|
|
258
|
+
* Report 2: Balance by Day of Month
|
|
259
|
+
* - Report type: Insights
|
|
260
|
+
* - Event: "balance checked"
|
|
261
|
+
* - Measure: Average of "account_balance"
|
|
262
|
+
* - Breakdown: day of month
|
|
263
|
+
* - Expected: days >= 28 ~ 0.7x other days
|
|
264
|
+
*
|
|
265
|
+
* REAL-WORLD ANALOGUE: Users obsessively check balances at month
|
|
266
|
+
* end as bills hit and runway tightens.
|
|
103
267
|
*
|
|
104
268
|
* ===================================================================
|
|
105
269
|
* ADVANCED ANALYSIS IDEAS
|
|
@@ -118,30 +282,82 @@ const chance = u.initChance(SEED);
|
|
|
118
282
|
* - By income_bracket: feature adoption by income
|
|
119
283
|
* - By credit_score_range: loan approvals, tier adoption
|
|
120
284
|
*
|
|
285
|
+
* ---------------------------------------------------------------
|
|
286
|
+
* 9. ONBOARDING TIME-TO-CONVERT (funnel-post)
|
|
287
|
+
*
|
|
288
|
+
* PATTERN: Premium tier users complete the Onboarding funnel 1.5x
|
|
289
|
+
* faster (factor 0.67); Basic users 1.33x slower (factor 1.33).
|
|
290
|
+
*
|
|
291
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
292
|
+
*
|
|
293
|
+
* Report 1: Onboarding Median Time-to-Convert by Tier
|
|
294
|
+
* - Funnels > "account opened" -> "app session" -> "balance checked"
|
|
295
|
+
* - Measure: Median time to convert
|
|
296
|
+
* - Breakdown: account_tier
|
|
297
|
+
* - Expected: premium ~ 0.67x baseline; basic ~ 1.33x
|
|
298
|
+
*
|
|
299
|
+
* NOTE (funnel-post measurement): visible only via Mixpanel funnel
|
|
300
|
+
* median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
|
|
301
|
+
* show this — funnel-post adjusts gaps within funnel instances, not
|
|
302
|
+
* across the user's full event history.
|
|
303
|
+
*
|
|
304
|
+
* ---------------------------------------------------------------
|
|
305
|
+
* 10. TRANSACTION-COUNT MAGIC NUMBER (everything)
|
|
306
|
+
*
|
|
307
|
+
* PATTERN: Sweet 6-10 transactions/user → +40% on investment-made
|
|
308
|
+
* amount (engaged transactor compounds wealth). Over 11+ → drop 20%
|
|
309
|
+
* of premium-upgraded events. No flag.
|
|
310
|
+
*
|
|
311
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
312
|
+
*
|
|
313
|
+
* Report 1: Avg Investment Amount by Transaction Bucket
|
|
314
|
+
* - Cohort A: users with 6-10 "transaction completed"
|
|
315
|
+
* - Cohort B: users with 0-5
|
|
316
|
+
* - Event: "investment made"
|
|
317
|
+
* - Measure: Average of "amount"
|
|
318
|
+
* - Expected: A ~ 1.4x B
|
|
319
|
+
*
|
|
320
|
+
* Report 2: Premium Upgrades on Heavy Transactors
|
|
321
|
+
* - Cohort C: users with >= 11 "transaction completed"
|
|
322
|
+
* - Cohort A: users with 6-10
|
|
323
|
+
* - Event: "premium upgraded"
|
|
324
|
+
* - Measure: Total per user
|
|
325
|
+
* - Expected: C ~ 20% fewer upgrades per user
|
|
326
|
+
*
|
|
327
|
+
* REAL-WORLD ANALOGUE: Engaged transactors invest more; over-active
|
|
328
|
+
* already extract value without upgrading.
|
|
329
|
+
*
|
|
121
330
|
* ===================================================================
|
|
122
331
|
* EXPECTED METRICS SUMMARY
|
|
123
332
|
* ===================================================================
|
|
124
333
|
*
|
|
125
|
-
* Hook | Metric | Baseline | Effect
|
|
126
|
-
*
|
|
127
|
-
* Personal vs Business | Avg transaction amt | $50 | $200+
|
|
128
|
-
* Payday Patterns | Deposit
|
|
129
|
-
* Fraud Detection | Users affected | 0% | 3%
|
|
130
|
-
* Low Balance Churn | D30+
|
|
131
|
-
* Budget Discipline |
|
|
132
|
-
* Auto-Pay Loyalty | Bill completion rate
|
|
133
|
-
* Premium Tier Value | Reward value
|
|
134
|
-
* Month-End Anxiety | Session duration |
|
|
334
|
+
* Hook | Metric | Baseline | Effect | Ratio
|
|
335
|
+
* ----------------------|-----------------------|----------|-----------|------
|
|
336
|
+
* Personal vs Business | Avg transaction amt | $50 | $200+ | ~ 4x
|
|
337
|
+
* Payday Patterns | Deposit amt 1st/15th | 1x | 3x | 3x
|
|
338
|
+
* Fraud Detection | Users affected | 0% | 3% | --
|
|
339
|
+
* Low Balance Churn | D30+ events | 1x | 0.5x | -50%
|
|
340
|
+
* Budget Discipline | Savings contribution | 1x | 2x | 2x
|
|
341
|
+
* Auto-Pay Loyalty | Bill completion rate | 100% | 70% | -30%
|
|
342
|
+
* Premium Tier Value | Reward value (Premium)| 1x | 3x | 3x
|
|
343
|
+
* Month-End Anxiety | Session duration d28+ | 1x | 1.4x | 1.4x
|
|
344
|
+
* Onboarding T2C | median min by tier | 1x | 0.67/1.33x| 2x range
|
|
345
|
+
* Txn-Count Magic Num | sweet investment amt | 1x | 1.4x | 1.4x
|
|
346
|
+
* Txn-Count Magic Num | over premium upgrades | 1x | 0.8x | -20%
|
|
135
347
|
*/
|
|
136
348
|
|
|
137
349
|
/** @type {Config} */
|
|
138
350
|
const config = {
|
|
351
|
+
version: 2,
|
|
139
352
|
token,
|
|
140
353
|
seed: SEED,
|
|
141
|
-
|
|
354
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
355
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
356
|
+
// numDays: num_days,
|
|
142
357
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
143
358
|
numUsers: num_users,
|
|
144
|
-
hasAnonIds:
|
|
359
|
+
hasAnonIds: true,
|
|
360
|
+
avgDevicePerUser: 2,
|
|
145
361
|
hasSessionIds: true,
|
|
146
362
|
format: "json",
|
|
147
363
|
gzip: true,
|
|
@@ -239,6 +455,7 @@ const config = {
|
|
|
239
455
|
event: "account opened",
|
|
240
456
|
weight: 1,
|
|
241
457
|
isFirstEvent: true,
|
|
458
|
+
isAuthEvent: true,
|
|
242
459
|
properties: {
|
|
243
460
|
"account_type": ["personal", "business", "personal"],
|
|
244
461
|
"signup_channel": ["app", "web", "referral", "branch"],
|
|
@@ -250,7 +467,6 @@ const config = {
|
|
|
250
467
|
properties: {
|
|
251
468
|
"session_duration_sec": u.weighNumRange(10, 600, 0.3, 60),
|
|
252
469
|
"pages_viewed": u.weighNumRange(1, 15, 0.5, 3),
|
|
253
|
-
"month_end_anxiety": [false],
|
|
254
470
|
}
|
|
255
471
|
},
|
|
256
472
|
{
|
|
@@ -259,7 +475,6 @@ const config = {
|
|
|
259
475
|
properties: {
|
|
260
476
|
"account_balance": u.weighNumRange(0, 50000, 0.8, 2500),
|
|
261
477
|
"account_type": ["checking", "savings", "investment"],
|
|
262
|
-
"month_end_check": [false],
|
|
263
478
|
}
|
|
264
479
|
},
|
|
265
480
|
{
|
|
@@ -270,8 +485,6 @@ const config = {
|
|
|
270
485
|
"amount": u.weighNumRange(1, 5000, 0.3, 50),
|
|
271
486
|
"merchant_category": ["grocery", "restaurant", "gas", "retail", "online", "subscription", "utilities"],
|
|
272
487
|
"payment_method": ["debit", "credit", "contactless", "online"],
|
|
273
|
-
"payday": [false],
|
|
274
|
-
"fraud_sequence": [false],
|
|
275
488
|
}
|
|
276
489
|
},
|
|
277
490
|
{
|
|
@@ -281,7 +494,6 @@ const config = {
|
|
|
281
494
|
"transfer_type": ["internal", "external", "p2p", "wire"],
|
|
282
495
|
"amount": u.weighNumRange(10, 10000, 0.3, 200),
|
|
283
496
|
"recipient_type": ["friend", "family", "business", "self"],
|
|
284
|
-
"post_payday_spending": [false],
|
|
285
497
|
}
|
|
286
498
|
},
|
|
287
499
|
{
|
|
@@ -291,8 +503,15 @@ const config = {
|
|
|
291
503
|
"bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
|
|
292
504
|
"amount": u.weighNumRange(20, 3000, 0.5, 150),
|
|
293
505
|
"auto_pay": [false, false, false, true, true],
|
|
294
|
-
|
|
295
|
-
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
event: "bill payment missed",
|
|
510
|
+
weight: 1,
|
|
511
|
+
isStrictEvent: true, // hook-only: created by Hook 6 from manual bill-paid events
|
|
512
|
+
properties: {
|
|
513
|
+
"bill_type": ["rent", "utilities", "phone", "insurance", "subscription", "loan_payment"],
|
|
514
|
+
"amount": u.weighNumRange(20, 3000, 0.5, 150),
|
|
296
515
|
}
|
|
297
516
|
},
|
|
298
517
|
{
|
|
@@ -318,7 +537,6 @@ const config = {
|
|
|
318
537
|
"goal_type": ["emergency", "vacation", "car", "home", "education", "retirement"],
|
|
319
538
|
"target_amount": u.weighNumRange(500, 50000, 0.3, 5000),
|
|
320
539
|
"monthly_contribution": u.weighNumRange(25, 2000, 0.5, 200),
|
|
321
|
-
"budget_discipline": [false],
|
|
322
540
|
}
|
|
323
541
|
},
|
|
324
542
|
{
|
|
@@ -328,25 +546,21 @@ const config = {
|
|
|
328
546
|
"investment_type": ["stocks", "etf", "crypto", "bonds", "mutual_fund"],
|
|
329
547
|
"amount": u.weighNumRange(10, 10000, 0.3, 250),
|
|
330
548
|
"action": ["buy", "sell", "buy"],
|
|
331
|
-
"premium_returns": [false],
|
|
332
|
-
"budget_discipline": [false],
|
|
333
549
|
}
|
|
334
550
|
},
|
|
335
551
|
{
|
|
336
552
|
event: "card locked",
|
|
337
|
-
weight:
|
|
553
|
+
weight: 2,
|
|
338
554
|
properties: {
|
|
339
555
|
"reason": ["lost", "stolen", "suspicious_activity", "travel"],
|
|
340
|
-
"fraud_sequence": [false],
|
|
341
556
|
}
|
|
342
557
|
},
|
|
343
558
|
{
|
|
344
559
|
event: "dispute filed",
|
|
345
|
-
weight:
|
|
560
|
+
weight: 2,
|
|
346
561
|
properties: {
|
|
347
562
|
"dispute_amount": u.weighNumRange(10, 2000, 0.5, 100),
|
|
348
563
|
"reason": ["unauthorized", "duplicate", "not_received", "damaged", "wrong_amount"],
|
|
349
|
-
"fraud_sequence": [false],
|
|
350
564
|
}
|
|
351
565
|
},
|
|
352
566
|
{
|
|
@@ -382,7 +596,6 @@ const config = {
|
|
|
382
596
|
"channel": ["chat", "phone", "email", "in_app"],
|
|
383
597
|
"issue_type": ["transaction", "account", "card", "transfer", "technical"],
|
|
384
598
|
"resolved": [false, true, true, true, true],
|
|
385
|
-
"fraud_sequence": [false],
|
|
386
599
|
}
|
|
387
600
|
},
|
|
388
601
|
{
|
|
@@ -399,7 +612,6 @@ const config = {
|
|
|
399
612
|
properties: {
|
|
400
613
|
"reward_type": ["cashback", "points", "discount", "partner_offer"],
|
|
401
614
|
"value": u.weighNumRange(1, 100, 0.5, 10),
|
|
402
|
-
"premium_reward": [false],
|
|
403
615
|
}
|
|
404
616
|
}
|
|
405
617
|
],
|
|
@@ -407,13 +619,11 @@ const config = {
|
|
|
407
619
|
superProps: {
|
|
408
620
|
account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
|
|
409
621
|
Platform: ["ios", "android", "web"],
|
|
410
|
-
low_balance_churn: [false],
|
|
411
622
|
},
|
|
412
623
|
|
|
413
624
|
userProps: {
|
|
414
625
|
account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
|
|
415
626
|
Platform: ["ios", "android", "web"],
|
|
416
|
-
low_balance_churn: [false],
|
|
417
627
|
"credit_score_range": ["300-579", "580-669", "670-739", "740-799", "800-850"],
|
|
418
628
|
"income_bracket": ["under_30k", "30k_50k", "50k_75k", "75k_100k", "100k_150k", "over_150k"],
|
|
419
629
|
"account_age_months": u.weighNumRange(1, 60, 0.5, 12),
|
|
@@ -448,24 +658,16 @@ const config = {
|
|
|
448
658
|
* This hook function creates 8 deliberate patterns in the data:
|
|
449
659
|
*
|
|
450
660
|
* 1. PERSONAL VS BUSINESS: Business accounts get employee_count, revenue; personal get age_range, life_stage
|
|
451
|
-
* 2. PAYDAY PATTERNS: Transactions spike on 1st/15th with bigger deposits and post-payday spending
|
|
661
|
+
* 2. PAYDAY PATTERNS: Transactions spike on 1st/15th with bigger deposits and post-payday spending (everything hook — runs after sessionization)
|
|
452
662
|
* 3. FRAUD DETECTION: 3% of users experience a fraud burst (rapid high-value txns -> card lock -> dispute -> support)
|
|
453
663
|
* 4. LOW BALANCE CHURN: Users with chronic low balances (<$15K) lose 50% of activity after day 30
|
|
454
664
|
* 5. BUDGET DISCIPLINE: Budget creators save 2x more and invest 1.5x more
|
|
455
665
|
* 6. AUTO-PAY LOYALTY: Auto-pay users never miss bills; manual payers miss 30%
|
|
456
666
|
* 7. PREMIUM TIER VALUE: Premium users get 3x rewards; Plus users get 1.5x; Premium investors get 2x returns
|
|
457
|
-
* 8. MONTH-END ANXIETY: Last 3 days of month see 40% longer sessions and 30% lower balances
|
|
667
|
+
* 8. MONTH-END ANXIETY: Last 3 days of month see 40% longer sessions and 30% lower balances (everything hook — runs after sessionization)
|
|
458
668
|
*/
|
|
459
669
|
hook: function (record, type, meta) {
|
|
460
|
-
|
|
461
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
462
|
-
|
|
463
|
-
// ===============================================================
|
|
464
|
-
// Hook #1: PERSONAL VS BUSINESS ACCOUNTS (user)
|
|
465
|
-
// 20% of users are randomly tagged as business accounts with
|
|
466
|
-
// employee_count, annual_revenue, and industry. The other 80% get
|
|
467
|
-
// personal attributes: age_range and life_stage.
|
|
468
|
-
// ===============================================================
|
|
670
|
+
// HOOK 1: PERSONAL VS BUSINESS ACCOUNTS (user) — role-based attrs.
|
|
469
671
|
if (type === "user") {
|
|
470
672
|
const isBusiness = chance.bool({ likelihood: 20 });
|
|
471
673
|
if (isBusiness) {
|
|
@@ -480,286 +682,215 @@ const config = {
|
|
|
480
682
|
}
|
|
481
683
|
}
|
|
482
684
|
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
// On the 1st and 15th, direct deposit transactions are 2x bigger
|
|
486
|
-
// and tagged with payday: true. On days 1-3 and 15-17, transfers
|
|
487
|
-
// have a 40% chance of 1.5x boost (post-payday spending).
|
|
488
|
-
// ===============================================================
|
|
685
|
+
// HOOK 6: AUTO-PAY LOYALTY (event) — manual bill-paid events have
|
|
686
|
+
// 30% chance of becoming "bill payment missed". Mutates event name.
|
|
489
687
|
if (type === "event") {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// Payday: 1st and 15th
|
|
494
|
-
if (record.event === "transaction completed" && record.transaction_type === "direct_deposit") {
|
|
495
|
-
if (dayOfMonth === 1 || dayOfMonth === 15) {
|
|
496
|
-
record.amount = Math.floor((record.amount || 50) * 3);
|
|
497
|
-
record.payday = true;
|
|
498
|
-
} else {
|
|
499
|
-
record.payday = false;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Post-payday spending: days 1-3 and 15-17
|
|
504
|
-
if (record.event === "transfer sent") {
|
|
505
|
-
const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
|
|
506
|
-
if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
|
|
507
|
-
record.amount = Math.floor((record.amount || 200) * 2.0);
|
|
508
|
-
record.post_payday_spending = true;
|
|
509
|
-
} else {
|
|
510
|
-
record.post_payday_spending = false;
|
|
511
|
-
}
|
|
688
|
+
if (record.event === "bill paid" && record.auto_pay !== true && chance.bool({ likelihood: 30 })) {
|
|
689
|
+
record.event = "bill payment missed";
|
|
512
690
|
}
|
|
691
|
+
}
|
|
513
692
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
record.
|
|
693
|
+
// HOOK 9 (T2C): ONBOARDING TIME-TO-CONVERT (funnel-post)
|
|
694
|
+
// Premium tier completes Onboarding funnel 1.5x faster (factor 0.67);
|
|
695
|
+
// Basic users 1.33x slower (factor 1.33).
|
|
696
|
+
if (type === "funnel-post") {
|
|
697
|
+
const segment = meta?.profile?.account_tier;
|
|
698
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
699
|
+
const factor = (
|
|
700
|
+
segment === "premium" ? 0.67 :
|
|
701
|
+
segment === "basic" ? 1.33 :
|
|
702
|
+
1.0
|
|
703
|
+
);
|
|
704
|
+
if (factor !== 1.0) {
|
|
705
|
+
for (let i = 1; i < record.length; i++) {
|
|
706
|
+
const prev = dayjs(record[i - 1].time);
|
|
707
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
708
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
529
709
|
}
|
|
530
|
-
} else {
|
|
531
|
-
record.missed_payment = false;
|
|
532
|
-
record.manual_payment = false;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// ===============================================================
|
|
537
|
-
// Hook #8: MONTH-END ANXIETY (event)
|
|
538
|
-
// Last 3 days of month (day >= 28) see 40% longer app sessions
|
|
539
|
-
// and 30% lower balances when checked.
|
|
540
|
-
// ===============================================================
|
|
541
|
-
if (record.event === "app session") {
|
|
542
|
-
if (dayOfMonth >= 28) {
|
|
543
|
-
record.session_duration_sec = Math.floor((record.session_duration_sec || 60) * 1.4);
|
|
544
|
-
record.month_end_anxiety = true;
|
|
545
|
-
} else {
|
|
546
|
-
record.month_end_anxiety = false;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (record.event === "balance checked") {
|
|
551
|
-
if (dayOfMonth >= 28) {
|
|
552
|
-
record.account_balance = Math.floor((record.account_balance || 2500) * 0.7);
|
|
553
|
-
record.month_end_check = true;
|
|
554
|
-
} else {
|
|
555
|
-
record.month_end_check = false;
|
|
556
710
|
}
|
|
557
711
|
}
|
|
558
712
|
}
|
|
559
713
|
|
|
560
|
-
// ===============================================================
|
|
561
|
-
// Hook #3, #4, #5: EVERYTHING - Complex behavioral patterns
|
|
562
|
-
// These hooks operate on the full event stream per user for
|
|
563
|
-
// fraud injection, low-balance churn, and budget discipline.
|
|
564
|
-
// ===============================================================
|
|
565
714
|
if (type === "everything") {
|
|
715
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
566
716
|
const userEvents = record;
|
|
567
|
-
|
|
568
|
-
// Stamp superProps from profile for consistency
|
|
569
717
|
const profile = meta.profile;
|
|
718
|
+
|
|
570
719
|
userEvents.forEach(e => {
|
|
571
720
|
e.account_tier = profile.account_tier;
|
|
572
721
|
e.Platform = profile.Platform;
|
|
573
|
-
e.low_balance_churn = profile.low_balance_churn;
|
|
574
722
|
});
|
|
575
723
|
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const tier = profile.account_tier;
|
|
583
|
-
userEvents.forEach(e => {
|
|
584
|
-
if (e.event === "reward redeemed") {
|
|
585
|
-
if (tier === "premium") {
|
|
586
|
-
e.value = Math.floor((e.value || 10) * 3);
|
|
587
|
-
e.premium_reward = true;
|
|
588
|
-
} else if (tier === "plus") {
|
|
589
|
-
e.value = Math.floor((e.value || 10) * 1.5);
|
|
590
|
-
e.premium_reward = false;
|
|
591
|
-
} else {
|
|
592
|
-
e.premium_reward = false;
|
|
724
|
+
// HOOK 1B: PERSONAL VS BUSINESS — business segment txns 4x larger
|
|
725
|
+
// (per Report 2 in JSDoc: business ~ $200, personal ~ $50).
|
|
726
|
+
if (profile.account_segment === "business") {
|
|
727
|
+
userEvents.forEach(e => {
|
|
728
|
+
if (e.event === "transaction completed" && typeof e.amount === "number") {
|
|
729
|
+
e.amount = Math.floor(e.amount * 4);
|
|
593
730
|
}
|
|
594
|
-
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
595
733
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
734
|
+
// HOOK 2: PAYDAY PATTERNS — 1st & 15th: direct_deposit amount 3x.
|
|
735
|
+
// Days 1-3 and 15-17: 60% of transfers get amount 2x. No flag.
|
|
736
|
+
for (const e of userEvents) {
|
|
737
|
+
const dayOfMonth = new Date(e.time).getUTCDate();
|
|
738
|
+
if (e.event === "transaction completed" && e.transaction_type === "direct_deposit") {
|
|
739
|
+
if (dayOfMonth === 1 || dayOfMonth === 15) {
|
|
740
|
+
e.amount = Math.floor((e.amount || 50) * 3);
|
|
602
741
|
}
|
|
603
742
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
// -----------------------------------------------------------
|
|
609
|
-
// Hook #3: FRAUD DETECTION PATTERN
|
|
610
|
-
// 3% of users experience a fraud event sequence: a burst of
|
|
611
|
-
// 3-5 rapid high-value transactions within 1 hour, followed by
|
|
612
|
-
// card locked, dispute filed, and support contacted events.
|
|
613
|
-
// All injected events are tagged with fraud_sequence: true.
|
|
614
|
-
// -----------------------------------------------------------
|
|
615
|
-
if (chance.bool({ likelihood: 3 })) {
|
|
616
|
-
// Find the midpoint of the user's timeline
|
|
617
|
-
if (userEvents.length >= 2) {
|
|
618
|
-
const midIdx = Math.floor(userEvents.length / 2);
|
|
619
|
-
const midEvent = userEvents[midIdx];
|
|
620
|
-
const midTime = dayjs(midEvent.time);
|
|
621
|
-
const distinctId = midEvent.user_id;
|
|
622
|
-
|
|
623
|
-
// Inject 3-5 rapid high-value transactions
|
|
624
|
-
const burstCount = chance.integer({ min: 3, max: 5 });
|
|
625
|
-
const fraudEvents = [];
|
|
626
|
-
|
|
627
|
-
// Find template events for cloning
|
|
628
|
-
const txnTemplate = userEvents.find(e => e.event === "transaction completed");
|
|
629
|
-
const cardTemplate = userEvents.find(e => e.event === "card locked");
|
|
630
|
-
const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
|
|
631
|
-
const supportTemplate = userEvents.find(e => e.event === "support contacted");
|
|
632
|
-
|
|
633
|
-
for (let i = 0; i < burstCount; i++) {
|
|
634
|
-
if (txnTemplate) {
|
|
635
|
-
fraudEvents.push({
|
|
636
|
-
...txnTemplate,
|
|
637
|
-
time: midTime.add(i * 10, "minutes").toISOString(),
|
|
638
|
-
user_id: distinctId,
|
|
639
|
-
transaction_type: "purchase",
|
|
640
|
-
amount: chance.integer({ min: 500, max: 3000 }),
|
|
641
|
-
merchant_category: chance.pickone(["online", "retail"]),
|
|
642
|
-
payment_method: "credit",
|
|
643
|
-
fraud_sequence: true,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
743
|
+
if (e.event === "transfer sent") {
|
|
744
|
+
const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
|
|
745
|
+
if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
|
|
746
|
+
e.amount = Math.floor((e.amount || 200) * 2.0);
|
|
646
747
|
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
647
750
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
fraud_sequence: true,
|
|
656
|
-
});
|
|
751
|
+
// HOOK 8: MONTH-END ANXIETY — days >= 28: app_session duration
|
|
752
|
+
// 1.4x; balance_checked account_balance 0.7x. Mutates raw props.
|
|
753
|
+
for (const e of userEvents) {
|
|
754
|
+
const dayOfMonth = new Date(e.time).getUTCDate();
|
|
755
|
+
if (dayOfMonth >= 28) {
|
|
756
|
+
if (e.event === "app session") {
|
|
757
|
+
e.session_duration_sec = Math.floor((e.session_duration_sec || 60) * 1.4);
|
|
657
758
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (disputeTemplate) {
|
|
661
|
-
fraudEvents.push({
|
|
662
|
-
...disputeTemplate,
|
|
663
|
-
time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
|
|
664
|
-
user_id: distinctId,
|
|
665
|
-
dispute_amount: chance.integer({ min: 500, max: 3000 }),
|
|
666
|
-
reason: "unauthorized",
|
|
667
|
-
fraud_sequence: true,
|
|
668
|
-
});
|
|
759
|
+
if (e.event === "balance checked") {
|
|
760
|
+
e.account_balance = Math.floor((e.account_balance || 2500) * 0.7);
|
|
669
761
|
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
670
764
|
|
|
671
|
-
|
|
672
|
-
|
|
765
|
+
// HOOK 7: PREMIUM TIER VALUE — Premium 3x reward value + 2x
|
|
766
|
+
// investment-sell amount; Plus 1.5x reward value. Reads tier
|
|
767
|
+
// from profile. No flag.
|
|
768
|
+
const tier = profile.account_tier;
|
|
769
|
+
userEvents.forEach(e => {
|
|
770
|
+
if (e.event === "reward redeemed") {
|
|
771
|
+
if (tier === "premium") e.value = Math.floor((e.value || 10) * 3);
|
|
772
|
+
else if (tier === "plus") e.value = Math.floor((e.value || 10) * 1.5);
|
|
773
|
+
}
|
|
774
|
+
if (e.event === "investment made" && e.action === "sell" && tier === "premium") {
|
|
775
|
+
e.amount = Math.floor((e.amount || 250) * 2);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// HOOK 3: FRAUD DETECTION — 3% of users get fraud burst
|
|
780
|
+
// (3-5 rapid high-value transactions + card locked + dispute +
|
|
781
|
+
// support contacted) at timeline midpoint. No flag — discover
|
|
782
|
+
// via cohort builder on users with card-locked + dispute-filed.
|
|
783
|
+
if (chance.bool({ likelihood: 15 }) && userEvents.length >= 2) {
|
|
784
|
+
const midIdx = Math.floor(userEvents.length / 2);
|
|
785
|
+
const midEvent = userEvents[midIdx];
|
|
786
|
+
const midTime = dayjs(midEvent.time);
|
|
787
|
+
const distinctId = midEvent.user_id;
|
|
788
|
+
const burstCount = chance.integer({ min: 3, max: 5 });
|
|
789
|
+
const fraudEvents = [];
|
|
790
|
+
const txnTemplate = userEvents.find(e => e.event === "transaction completed");
|
|
791
|
+
const cardTemplate = userEvents.find(e => e.event === "card locked");
|
|
792
|
+
const disputeTemplate = userEvents.find(e => e.event === "dispute filed");
|
|
793
|
+
const supportTemplate = userEvents.find(e => e.event === "support contacted");
|
|
794
|
+
|
|
795
|
+
for (let i = 0; i < burstCount; i++) {
|
|
796
|
+
if (txnTemplate) {
|
|
673
797
|
fraudEvents.push({
|
|
674
|
-
...
|
|
675
|
-
time: midTime.add(
|
|
798
|
+
...txnTemplate,
|
|
799
|
+
time: midTime.add(i * 10, "minutes").toISOString(),
|
|
676
800
|
user_id: distinctId,
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
801
|
+
transaction_type: "purchase",
|
|
802
|
+
amount: chance.integer({ min: 500, max: 3000 }),
|
|
803
|
+
merchant_category: chance.pickone(["online", "retail"]),
|
|
804
|
+
payment_method: "credit",
|
|
681
805
|
});
|
|
682
806
|
}
|
|
683
|
-
|
|
684
|
-
// Splice all fraud events into the user's timeline
|
|
685
|
-
userEvents.splice(midIdx + 1, 0, ...fraudEvents);
|
|
686
807
|
}
|
|
808
|
+
if (cardTemplate) fraudEvents.push({
|
|
809
|
+
...cardTemplate,
|
|
810
|
+
time: midTime.add(burstCount * 10 + 5, "minutes").toISOString(),
|
|
811
|
+
user_id: distinctId,
|
|
812
|
+
reason: "suspicious_activity",
|
|
813
|
+
});
|
|
814
|
+
if (disputeTemplate) fraudEvents.push({
|
|
815
|
+
...disputeTemplate,
|
|
816
|
+
time: midTime.add(burstCount * 10 + 30, "minutes").toISOString(),
|
|
817
|
+
user_id: distinctId,
|
|
818
|
+
dispute_amount: chance.integer({ min: 500, max: 3000 }),
|
|
819
|
+
reason: "unauthorized",
|
|
820
|
+
});
|
|
821
|
+
if (supportTemplate) fraudEvents.push({
|
|
822
|
+
...supportTemplate,
|
|
823
|
+
time: midTime.add(burstCount * 10 + 45, "minutes").toISOString(),
|
|
824
|
+
user_id: distinctId,
|
|
825
|
+
channel: "phone",
|
|
826
|
+
issue_type: "card",
|
|
827
|
+
resolved: true,
|
|
828
|
+
});
|
|
829
|
+
userEvents.splice(midIdx + 1, 0, ...fraudEvents);
|
|
687
830
|
}
|
|
688
831
|
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// -----------------------------------------------------------
|
|
695
|
-
let lowBalanceChecks = 0;
|
|
696
|
-
userEvents.forEach((event) => {
|
|
697
|
-
if (event.event === "balance checked" && (event.account_balance || 0) < 15000) {
|
|
698
|
-
lowBalanceChecks++;
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
|
|
832
|
+
// HOOK 4: LOW BALANCE CHURN — users with 3+ balance checks
|
|
833
|
+
// under $15K lose 50% of post-day-30 events. No flag.
|
|
834
|
+
const lowBalanceChecks = userEvents.filter(e =>
|
|
835
|
+
e.event === "balance checked" && (e.account_balance || 0) < 15000
|
|
836
|
+
).length;
|
|
702
837
|
if (lowBalanceChecks >= 3) {
|
|
703
|
-
const day30 =
|
|
838
|
+
const day30 = datasetStart.add(30, "days");
|
|
704
839
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
708
|
-
userEvents.splice(i, 1);
|
|
709
|
-
} else {
|
|
710
|
-
evt.low_balance_churn = true;
|
|
711
|
-
}
|
|
840
|
+
if (dayjs(userEvents[i].time).isAfter(day30) && chance.bool({ likelihood: 50 })) {
|
|
841
|
+
userEvents.splice(i, 1);
|
|
712
842
|
}
|
|
713
843
|
}
|
|
714
844
|
}
|
|
715
845
|
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
// spliced into their timeline. Tagged budget_discipline: true.
|
|
721
|
-
// -----------------------------------------------------------
|
|
722
|
-
let hasBudget = false;
|
|
723
|
-
userEvents.forEach((event) => {
|
|
724
|
-
if (event.event === "budget created") {
|
|
725
|
-
hasBudget = true;
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
|
|
846
|
+
// HOOK 5: BUDGET DISCIPLINE — users with any budget-created event
|
|
847
|
+
// get savings 2x, investment amounts 1.5x, and extra cloned
|
|
848
|
+
// savings-goal events. No flag.
|
|
849
|
+
const hasBudget = userEvents.some(e => e.event === "budget created");
|
|
729
850
|
if (hasBudget) {
|
|
730
851
|
userEvents.forEach((event, idx) => {
|
|
731
852
|
const eventTime = dayjs(event.time);
|
|
732
|
-
|
|
733
|
-
// Double savings contributions
|
|
734
853
|
if (event.event === "savings goal set") {
|
|
735
854
|
event.monthly_contribution = Math.floor((event.monthly_contribution || 200) * 2);
|
|
736
|
-
event.budget_discipline = true;
|
|
737
855
|
}
|
|
738
|
-
|
|
739
|
-
// 1.5x investment amounts
|
|
740
856
|
if (event.event === "investment made") {
|
|
741
857
|
event.amount = Math.floor((event.amount || 250) * 1.5);
|
|
742
|
-
event.budget_discipline = true;
|
|
743
858
|
}
|
|
744
|
-
|
|
745
|
-
// Splice extra savings goal events (budget-conscious users set more goals)
|
|
746
859
|
if (event.event === "budget created" && chance.bool({ likelihood: 50 })) {
|
|
747
860
|
const savingsTemplate = userEvents.find(e => e.event === "savings goal set");
|
|
748
861
|
if (savingsTemplate) {
|
|
749
|
-
|
|
862
|
+
userEvents.splice(idx + 1, 0, {
|
|
750
863
|
...savingsTemplate,
|
|
751
864
|
time: eventTime.add(chance.integer({ min: 1, max: 7 }), "days").toISOString(),
|
|
752
865
|
user_id: event.user_id,
|
|
753
866
|
goal_type: chance.pickone(["emergency", "vacation", "car", "home"]),
|
|
754
867
|
target_amount: chance.integer({ min: 1000, max: 20000 }),
|
|
755
868
|
monthly_contribution: chance.integer({ min: 100, max: 800 }),
|
|
756
|
-
|
|
757
|
-
};
|
|
758
|
-
userEvents.splice(idx + 1, 0, extraGoal);
|
|
869
|
+
});
|
|
759
870
|
}
|
|
760
871
|
}
|
|
761
872
|
});
|
|
762
873
|
}
|
|
874
|
+
|
|
875
|
+
// HOOK 10: TRANSACTION-COUNT MAGIC NUMBER (no flags)
|
|
876
|
+
// Sweet 6-10 transactions/user → +40% on investment_made amount
|
|
877
|
+
// (engaged transactor compounds wealth). Over 11+ → drop 20% of
|
|
878
|
+
// premium-upgraded events (already engaged; less upgrade pressure).
|
|
879
|
+
const txnCount = userEvents.filter(e => e.event === "transaction completed").length;
|
|
880
|
+
if (txnCount >= 6 && txnCount <= 10) {
|
|
881
|
+
userEvents.forEach(e => {
|
|
882
|
+
if (e.event === "investment made" && typeof e.amount === "number") {
|
|
883
|
+
e.amount = Math.round(e.amount * 1.4);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
} else if (txnCount >= 11) {
|
|
887
|
+
// Drop 20% of premium-upgraded events for heavy transactors
|
|
888
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
889
|
+
if (userEvents[i].event === "premium upgraded" && chance.bool({ likelihood: 20 })) {
|
|
890
|
+
userEvents.splice(i, 1);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
763
894
|
}
|
|
764
895
|
|
|
765
896
|
return record;
|