@ak--47/dungeon-master 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -2
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +2 -2
  16. package/dungeons/technical/simplest.js +2 -2
  17. package/dungeons/technical/text-generation.js +2 -2
  18. package/dungeons/vertical/ai-platform-schema.json +617 -0
  19. package/dungeons/vertical/ai-platform.js +799 -0
  20. package/dungeons/vertical/community.js +40 -26
  21. package/dungeons/vertical/crypto-schema.json +546 -0
  22. package/dungeons/vertical/crypto.js +721 -0
  23. package/dungeons/vertical/dating-schema.json +401 -0
  24. package/dungeons/vertical/dating.js +798 -0
  25. package/dungeons/vertical/devtools.js +13 -9
  26. package/dungeons/vertical/ecommerce.js +2 -3
  27. package/dungeons/vertical/education.js +4 -5
  28. package/dungeons/vertical/fintech.js +32 -29
  29. package/dungeons/vertical/fitness.js +2 -3
  30. package/dungeons/vertical/food-delivery.js +37 -43
  31. package/dungeons/vertical/gaming-schema.json +2495 -230
  32. package/dungeons/vertical/gaming.js +771 -388
  33. package/dungeons/vertical/healthcare.js +2 -3
  34. package/dungeons/vertical/insurance-application.js +2 -3
  35. package/dungeons/vertical/logistics.js +20 -14
  36. package/dungeons/vertical/marketplace.js +22 -14
  37. package/dungeons/vertical/media.js +39 -30
  38. package/dungeons/vertical/real-estate-schema.json +527 -0
  39. package/dungeons/vertical/real-estate.js +774 -0
  40. package/dungeons/vertical/sass.js +2 -3
  41. package/dungeons/vertical/social.js +15 -13
  42. package/dungeons/vertical/travel.js +59 -26
  43. package/lib/core/config-validator.js +71 -15
  44. package/lib/core/storage.js +14 -4
  45. package/lib/orchestrators/user-loop.js +39 -5
  46. package/lib/templates/macro-presets.js +111 -0
  47. package/lib/templates/soup-presets.js +19 -36
  48. package/package.json +8 -2
  49. package/types.d.ts +219 -42
  50. package/dungeons/user/.gitkeep +0 -0
  51. package/dungeons/vertical/rpg-schema.json +0 -2491
  52. package/dungeons/vertical/rpg.js +0 -976
@@ -0,0 +1,798 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "meetcute";
3
+ const num_days = 120;
4
+ const num_users = 8_000;
5
+ const avg_events_per_user_per_day = 0.75;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
11
+ import dayjs from "dayjs";
12
+ import utc from "dayjs/plugin/utc.js";
13
+ import "dotenv/config";
14
+ import * as u from "../../lib/utils/utils.js";
15
+ import * as v from "ak-tools";
16
+
17
+ dayjs.extend(utc);
18
+ const chance = u.initChance(SEED);
19
+ const NOW = dayjs();
20
+ const DATASET_START = NOW.subtract(num_days, "days");
21
+
22
+ /** @typedef {import("../../types").Dungeon} Config */
23
+
24
+ /*
25
+ * =====================================================================================
26
+ * DATASET OVERVIEW
27
+ * =====================================================================================
28
+ *
29
+ * MeetCute — a swipe-based dating app (Hinge/Tinder-style) with profile
30
+ * prompts, photo verification, matchmaking, messaging, and premium tiers.
31
+ *
32
+ * CORE LOOP:
33
+ * Users create a profile with photos and prompts, swipe on potential
34
+ * matches, receive matches, message matches, exchange phone numbers,
35
+ * and schedule dates. Premium subscribers get boosts, super-likes,
36
+ * and see-who-liked-you.
37
+ *
38
+ * - 8,000 users over 120 days
39
+ * - ~720,000 base events across 17 event types
40
+ * - 4 funnels (onboarding, match flow, date funnel, monetization)
41
+ * - 3 subscription tiers: Free, Premium, Elite
42
+ *
43
+ * Key entities:
44
+ * - swipe_source: feed / discover / boost / nearby
45
+ * - subscription: Free / Premium / Elite
46
+ * - venue_type: coffee / dinner / drinks / activity / virtual
47
+ * - prompt_type: icebreaker / opinion / hypothetical / personal / creative
48
+ *
49
+ * =====================================================================================
50
+ */
51
+
52
+ /*
53
+ * =====================================================================================
54
+ * ANALYTICS HOOKS (8 hooks)
55
+ * =====================================================================================
56
+ *
57
+ * -------------------------------------------------------------------------------------
58
+ * 1. PHOTO UPLOAD CONVERSION (everything hook)
59
+ * -------------------------------------------------------------------------------------
60
+ * PATTERN: Users with 4+ "photo uploaded" events get 5x more "match received"
61
+ * events injected (cloned from existing matches). complete_profile=true on
62
+ * those match events. Complete profiles are magnets.
63
+ *
64
+ * HOW TO FIND IT IN MIXPANEL:
65
+ * Report 1: Match Volume by Profile Completeness
66
+ * - Insights > "match received" > Total per user > Breakdown: "complete_profile"
67
+ * - Expected: complete_profile=true users have ~5x more matches
68
+ *
69
+ * Report 2: Photo Count Correlation
70
+ * - Insights > "photo uploaded" > Uniques > Compare to "match received" > Uniques
71
+ * - Expected: strong positive correlation between photo uploads and matches
72
+ *
73
+ * -------------------------------------------------------------------------------------
74
+ * 2. WEEKEND SWIPE SURGE (event hook)
75
+ * -------------------------------------------------------------------------------------
76
+ * PATTERN: On Sunday evenings (day=0, hour 18-23), "swipe right" events are
77
+ * duplicated 2.5x with weekend_surge=true. The Sunday Scaries drive swipes.
78
+ *
79
+ * HOW TO FIND IT IN MIXPANEL:
80
+ * Report 1: Swipe Volume by Day of Week
81
+ * - Insights (bar) > "swipe right" > Total > Breakdown: Day of Week
82
+ * - Expected: Sunday bar significantly taller than other days
83
+ *
84
+ * Report 2: Weekend Surge Flag
85
+ * - Insights (line) > "swipe right" > Total > Breakdown: "weekend_surge"
86
+ * - Expected: weekend_surge=true events clustered on Sunday evenings
87
+ *
88
+ * -------------------------------------------------------------------------------------
89
+ * 3. SUPER-LIKE EFFECT (everything hook)
90
+ * -------------------------------------------------------------------------------------
91
+ * PATTERN: "swipe right" events with is_super_like=true generate 3x more
92
+ * "match received" events nearby in time. super_like_match=true on those
93
+ * matches. Super-likes dramatically improve match rates.
94
+ *
95
+ * HOW TO FIND IT IN MIXPANEL:
96
+ * Report 1: Match Rate by Super-Like
97
+ * - Insights > "match received" > Total per user > Breakdown: "super_like_match"
98
+ * - Expected: super_like_match=true concentrated among super-likers
99
+ *
100
+ * Report 2: Super-Like Conversion
101
+ * - Funnels > "swipe right" (filter: is_super_like=true) > "match received"
102
+ * - Compare to: "swipe right" (filter: is_super_like=false) > "match received"
103
+ * - Expected: super-like funnel ~3x higher conversion
104
+ *
105
+ * -------------------------------------------------------------------------------------
106
+ * 4. PREMIUM MATCH BOOST (everything hook)
107
+ * -------------------------------------------------------------------------------------
108
+ * PATTERN: Premium subscribers get 2x "match received" events. Elite
109
+ * subscribers get 4x + injected "profile viewed" events (see-who-liked-you).
110
+ * premium_boost=true on injected events. Subscription pays off.
111
+ *
112
+ * HOW TO FIND IT IN MIXPANEL:
113
+ * Report 1: Matches by Subscription Tier
114
+ * - Insights > "match received" > Total per user > Breakdown: user prop "subscription"
115
+ * - Expected: Free ~baseline, Premium ~2x, Elite ~4x
116
+ *
117
+ * Report 2: Profile Views (Elite Exclusive)
118
+ * - Insights > "profile viewed" > Total > Breakdown: "premium_boost"
119
+ * - Expected: premium_boost=true events only from Elite subscribers
120
+ *
121
+ * -------------------------------------------------------------------------------------
122
+ * 5. GHOSTING CHURN (everything hook)
123
+ * -------------------------------------------------------------------------------------
124
+ * PATTERN: Users who receive a match but send zero messages within 48 hours
125
+ * lose 70% of their events after the match date. ghosted=true stamped on
126
+ * remaining "app opened" events. Ghosters churn.
127
+ *
128
+ * HOW TO FIND IT IN MIXPANEL:
129
+ * Report 1: Retention by Messaging Behavior
130
+ * - Retention > Starting: "match received" > Return: any event
131
+ * - Segment: users who did "message sent" vs users who didn't
132
+ * - Expected: non-messengers have ~70% lower retention
133
+ *
134
+ * Report 2: Ghost Flag
135
+ * - Insights (line) > "app opened" > Total > Breakdown: "ghosted"
136
+ * - Expected: ghosted=true users trail off sharply after match date
137
+ *
138
+ * -------------------------------------------------------------------------------------
139
+ * 6. BIO + PROMPT POWER USERS (everything hook)
140
+ * -------------------------------------------------------------------------------------
141
+ * PATTERN: Users with both "bio updated" AND 3+ "prompt answered" events
142
+ * get 4x "date scheduled" events. complete_profile_power=true on injected
143
+ * dates. Effort on profile = real dates.
144
+ *
145
+ * HOW TO FIND IT IN MIXPANEL:
146
+ * Report 1: Dates by Profile Effort
147
+ * - Insights > "date scheduled" > Total per user > Breakdown: "complete_profile_power"
148
+ * - Expected: complete_profile_power=true users schedule ~4x more dates
149
+ *
150
+ * Report 2: Profile Effort Funnel
151
+ * - Funnels > "bio updated" > "prompt answered" > "date scheduled"
152
+ * - Expected: users completing all 3 have dramatically higher date rates
153
+ *
154
+ * -------------------------------------------------------------------------------------
155
+ * 7. VALENTINE'S DAY SPIKE (event + everything hook)
156
+ * -------------------------------------------------------------------------------------
157
+ * PATTERN: Day 60 = Feb 14. Days 58-63: "profile created" events 3x via
158
+ * cloned events (valentines=true). In everything hook, "premium upgrade"
159
+ * events 5x for users active during V-Day window. Love is expensive.
160
+ *
161
+ * HOW TO FIND IT IN MIXPANEL:
162
+ * Report 1: Signup Spike
163
+ * - Insights (line) > "profile created" > Total > Daily
164
+ * - Expected: sharp 3x spike around day 58-63
165
+ *
166
+ * Report 2: Premium Upgrades Around V-Day
167
+ * - Insights (line) > "premium upgrade" > Total > Breakdown: "valentines"
168
+ * - Expected: 5x premium upgrades during V-Day window
169
+ *
170
+ * -------------------------------------------------------------------------------------
171
+ * 8. OFF-APP RETENTION (everything hook)
172
+ * -------------------------------------------------------------------------------------
173
+ * PATTERN: Users with "phone number exchanged" OR "date scheduled" in first
174
+ * 14 days maintain 60% D30 retention (extra events injected). Users without
175
+ * these milestones lose events after day 30 — the app becomes irrelevant
176
+ * once they find someone IRL.
177
+ *
178
+ * HOW TO FIND IT IN MIXPANEL:
179
+ * Report 1: Retention by Milestone
180
+ * - Retention > Starting: "profile created" > Return: any event
181
+ * - Segment: users who did "date scheduled" in first 14 days vs not
182
+ * - Expected: milestone users ~60% D30 retention vs ~20% for others
183
+ *
184
+ * Report 2: Long-Term Engagement
185
+ * - Insights (line) > "app opened" > Total per user > Weekly > Breakdown: user prop "subscription"
186
+ * - Expected: after week 4, non-milestone users nearly disappear
187
+ *
188
+ * =====================================================================================
189
+ * EXPECTED METRICS SUMMARY
190
+ * =====================================================================================
191
+ *
192
+ * Hook | Metric | Baseline | Effect | Ratio
193
+ * ───────────────────────────-|──────────────────────-|──────────|──────────|──────
194
+ * Photo Upload Conversion | matches/user | ~3 | ~15 | 5x
195
+ * Weekend Swipe Surge | Sunday swipes | 1x | 2.5x | 2.5x
196
+ * Super-Like Effect | matches from super | 1x | 3x | 3x
197
+ * Premium Match Boost | matches (Elite) | 1x | 4x | 4x
198
+ * Ghosting Churn | post-match retention | 100% | 30% | 0.3x
199
+ * Bio + Prompt Power Users | dates/user | 1x | 4x | 4x
200
+ * Valentine's Day Spike | signups (days 58-63) | 1x | 3x | 3x
201
+ * Off-App Retention | D30 retention | ~20% | ~60% | 3x
202
+ *
203
+ * =====================================================================================
204
+ * ADVANCED ANALYSIS IDEAS
205
+ * =====================================================================================
206
+ *
207
+ * - Photo Conversion + Premium Boost: Do complete-profile Elite users compound
208
+ * to 20x matches (5x photos * 4x Elite)?
209
+ * - Weekend Surge + Super-Like: Sunday evening super-likes should be the
210
+ * highest-converting swipe segment.
211
+ * - Ghosting + Off-App Retention: Users who ghost AND never schedule a date
212
+ * churn fastest — double negative retention effect.
213
+ * - Valentine's Spike + Monetization: V-Day signups who upgrade to Premium
214
+ * within 7 days have the highest LTV window.
215
+ * - Bio/Prompt Power + Date Scheduling: Complete profile users schedule dates
216
+ * at 4x the rate, but do they also have higher message response rates?
217
+ * - Cohort by age_range, gender, subscription tier, and signup week.
218
+ * - Funnel analysis: onboarding by gender, match flow by subscription,
219
+ * date funnel by profile completeness.
220
+ * =====================================================================================
221
+ */
222
+
223
+ /** @type {Config} */
224
+ const config = {
225
+ token,
226
+ seed: SEED,
227
+ numDays: num_days,
228
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
229
+ numUsers: num_users,
230
+ hasAnonIds: false,
231
+ hasSessionIds: true,
232
+ format: "json",
233
+ gzip: true,
234
+ alsoInferFunnels: false,
235
+ hasLocation: true,
236
+ hasAndroidDevices: true,
237
+ hasIOSDevices: true,
238
+ hasDesktopDevices: false,
239
+ hasBrowser: false,
240
+ hasCampaigns: false,
241
+ isAnonymous: false,
242
+ hasAdSpend: false,
243
+ hasAvatar: true,
244
+ concurrency: 1,
245
+ writeToDisk: false,
246
+ soup: "growth",
247
+
248
+ // ── Events (17) ──────────────────────────────────────────
249
+ events: [
250
+ {
251
+ event: "profile created",
252
+ weight: 1,
253
+ isFirstEvent: true,
254
+ properties: {
255
+ age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
256
+ gender: ["Male", "Male", "Female", "Female", "Non-binary"],
257
+ looking_for: ["Men", "Women", "Everyone"],
258
+ valentines: [false],
259
+ },
260
+ },
261
+ {
262
+ event: "photo uploaded",
263
+ weight: 3,
264
+ properties: {
265
+ photo_number: u.weighNumRange(1, 6, 0.5, 2),
266
+ has_face: [true, true, true, true, false],
267
+ },
268
+ },
269
+ {
270
+ event: "bio updated",
271
+ weight: 2,
272
+ properties: {
273
+ bio_length: u.weighNumRange(10, 500, 0.4, 120),
274
+ },
275
+ },
276
+ {
277
+ event: "prompt answered",
278
+ weight: 3,
279
+ properties: {
280
+ prompt_type: ["icebreaker", "opinion", "hypothetical", "personal", "creative"],
281
+ answer_length: u.weighNumRange(10, 300, 0.4, 80),
282
+ },
283
+ },
284
+ {
285
+ event: "swipe right",
286
+ weight: 10,
287
+ properties: {
288
+ is_super_like: [false, false, false, false, false, false, false, false, false, true],
289
+ weekend_surge: [false],
290
+ swipe_source: ["feed", "feed", "feed", "discover", "boost", "nearby"],
291
+ },
292
+ },
293
+ {
294
+ event: "swipe left",
295
+ weight: 8,
296
+ properties: {
297
+ swipe_source: ["feed", "feed", "feed", "discover", "boost", "nearby"],
298
+ },
299
+ },
300
+ {
301
+ event: "match received",
302
+ weight: 4,
303
+ properties: {
304
+ match_score: u.weighNumRange(50, 100, 0.5, 75),
305
+ complete_profile: [false],
306
+ super_like_match: [false],
307
+ premium_boost: [false],
308
+ },
309
+ },
310
+ {
311
+ event: "message sent",
312
+ weight: 6,
313
+ properties: {
314
+ message_length: u.weighNumRange(1, 500, 0.3, 40),
315
+ has_emoji: [false, false, true, true, true],
316
+ response_time_mins: u.weighNumRange(1, 1440, 0.3, 30),
317
+ },
318
+ },
319
+ {
320
+ event: "message received",
321
+ weight: 5,
322
+ properties: {
323
+ message_length: u.weighNumRange(1, 500, 0.3, 50),
324
+ },
325
+ },
326
+ {
327
+ event: "phone number exchanged",
328
+ weight: 1,
329
+ properties: {
330
+ exchange_method: ["in_chat", "in_chat", "voice_call", "video_call"],
331
+ },
332
+ },
333
+ {
334
+ event: "date scheduled",
335
+ weight: 1,
336
+ properties: {
337
+ venue_type: ["coffee", "dinner", "drinks", "activity", "virtual"],
338
+ complete_profile_power: [false],
339
+ },
340
+ },
341
+ {
342
+ event: "profile viewed",
343
+ weight: 5,
344
+ properties: {
345
+ viewer_source: ["feed", "discover", "liked_you", "nearby"],
346
+ premium_boost: [false],
347
+ },
348
+ },
349
+ {
350
+ event: "premium upgrade",
351
+ weight: 1,
352
+ properties: {
353
+ plan: ["Premium", "Premium", "Premium", "Elite"],
354
+ price_usd: [14.99, 14.99, 14.99, 29.99],
355
+ valentines: [false],
356
+ },
357
+ },
358
+ {
359
+ event: "premium cancelled",
360
+ weight: 1,
361
+ isStrictEvent: true,
362
+ properties: {
363
+ cancel_reason: ["found_someone", "too_expensive", "not_enough_matches", "bad_experience", "taking_a_break"],
364
+ subscription_duration_days: u.weighNumRange(7, 365, 0.3, 30),
365
+ },
366
+ },
367
+ {
368
+ event: "boost activated",
369
+ weight: 2,
370
+ properties: {
371
+ boost_type: ["standard", "super"],
372
+ boost_duration_mins: [15, 30, 30, 60],
373
+ },
374
+ },
375
+ {
376
+ event: "report user",
377
+ weight: 1,
378
+ isStrictEvent: true,
379
+ properties: {
380
+ report_reason: ["fake_profile", "inappropriate_photos", "harassment", "spam", "underage"],
381
+ },
382
+ },
383
+ {
384
+ event: "app opened",
385
+ weight: 8,
386
+ properties: {
387
+ session_duration_mins: u.weighNumRange(1, 120, 0.3, 8),
388
+ ghosted: [false],
389
+ },
390
+ },
391
+ ],
392
+
393
+ // ── Funnels (4) ──────────────────────────────────────────
394
+ funnels: [
395
+ {
396
+ name: "Onboarding",
397
+ sequence: ["profile created", "photo uploaded", "swipe right"],
398
+ conversionRate: 75,
399
+ order: "sequential",
400
+ isFirstFunnel: true,
401
+ timeToConvert: 1,
402
+ weight: 3,
403
+ },
404
+ {
405
+ name: "Match Flow",
406
+ sequence: ["swipe right", "match received", "message sent"],
407
+ conversionRate: 50,
408
+ order: "sequential",
409
+ timeToConvert: 24,
410
+ weight: 6,
411
+ },
412
+ {
413
+ name: "Date Funnel",
414
+ sequence: ["message sent", "phone number exchanged", "date scheduled"],
415
+ conversionRate: 25,
416
+ order: "sequential",
417
+ timeToConvert: 72,
418
+ weight: 3,
419
+ },
420
+ {
421
+ name: "Monetization",
422
+ sequence: ["app opened", "boost activated", "premium upgrade"],
423
+ conversionRate: 20,
424
+ order: "sequential",
425
+ timeToConvert: 48,
426
+ weight: 2,
427
+ },
428
+ ],
429
+
430
+ // ── SuperProps ──────────────────────────────────────────
431
+ superProps: {
432
+ subscription: ["Free", "Free", "Free", "Premium", "Elite"],
433
+ platform: ["ios", "ios", "android"],
434
+ },
435
+
436
+ // ── UserProps ──────────────────────────────────────────
437
+ userProps: {
438
+ subscription: ["Free", "Free", "Free", "Premium", "Elite"],
439
+ age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
440
+ gender: ["Male", "Male", "Female", "Female", "Non-binary"],
441
+ looking_for: ["Men", "Women", "Everyone"],
442
+ photo_count: u.weighNumRange(0, 8, 0.4, 3),
443
+ total_matches: u.weighNumRange(0, 200, 0.3, 15),
444
+ total_messages_sent: u.weighNumRange(0, 500, 0.3, 30),
445
+ profile_completeness: ["incomplete", "incomplete", "basic", "basic", "complete"],
446
+ platform: ["ios", "ios", "android"],
447
+ },
448
+
449
+ // ── SCD Props ──────────────────────────────────────────
450
+ scdProps: {
451
+ subscription_tier: {
452
+ values: ["Free", "Premium", "Elite"],
453
+ frequency: "month",
454
+ timing: "fuzzy",
455
+ max: 6,
456
+ },
457
+ },
458
+
459
+ groupKeys: [],
460
+ groupProps: {},
461
+ mirrorProps: {},
462
+ lookupTables: [],
463
+
464
+ // ── Hook Function ──────────────────────────────────────
465
+ hook: function (record, type, meta) {
466
+ const VALENTINES_DAY = DATASET_START.add(60, "days");
467
+ const VDAY_WINDOW_START = DATASET_START.add(58, "days");
468
+ const VDAY_WINDOW_END = DATASET_START.add(63, "days");
469
+
470
+ // ─── EVENT-LEVEL HOOKS ───────────────────────────────────────────
471
+
472
+ if (type === "event") {
473
+ const EVENT_TIME = dayjs(record.time);
474
+
475
+ // ── HOOK 2: WEEKEND SWIPE SURGE ─────────────────
476
+ // Tagging moved to everything hook (sessionization reassigns times after event hook)
477
+
478
+ // ── HOOK 7: VALENTINE'S DAY SPIKE (event part) ──
479
+ // Days 58-63: tag profile created events for 3x duplication
480
+ if (record.event === "profile created") {
481
+ if (EVENT_TIME.isAfter(VDAY_WINDOW_START) && EVENT_TIME.isBefore(VDAY_WINDOW_END)) {
482
+ record.valentines = true;
483
+ }
484
+ }
485
+ }
486
+
487
+ // ─── EVERYTHING-LEVEL HOOKS ──────────────────────────────────────
488
+
489
+ if (type === "everything") {
490
+ const events = record;
491
+ if (!events || events.length === 0) return record;
492
+
493
+ const profile = meta.profile || {};
494
+
495
+ // Stamp superProps from profile for consistency
496
+ events.forEach(e => {
497
+ if (profile.subscription) e.subscription = profile.subscription;
498
+ if (profile.platform) e.platform = profile.platform;
499
+ });
500
+
501
+ // ── First pass: scan user patterns ──
502
+ let photoUploadCount = 0;
503
+ let promptAnsweredCount = 0;
504
+ let hasBioUpdated = false;
505
+ let matchEvents = [];
506
+ let messageSentEvents = [];
507
+ let hasSuperLike = false;
508
+ let superLikeEvents = [];
509
+ let hasPhoneExchangedEarly = false;
510
+ let hasDateScheduledEarly = false;
511
+ let firstEventTime = null;
512
+
513
+ events.forEach(event => {
514
+ if (!firstEventTime || dayjs(event.time).isBefore(dayjs(firstEventTime))) {
515
+ firstEventTime = event.time;
516
+ }
517
+ if (event.event === "photo uploaded") photoUploadCount++;
518
+ if (event.event === "prompt answered") promptAnsweredCount++;
519
+ if (event.event === "bio updated") hasBioUpdated = true;
520
+ if (event.event === "match received") matchEvents.push(event);
521
+ if (event.event === "message sent") messageSentEvents.push(event);
522
+ if (event.event === "swipe right" && event.is_super_like === true) {
523
+ hasSuperLike = true;
524
+ superLikeEvents.push(event);
525
+ }
526
+ });
527
+
528
+ // Check early milestones (first 14 days)
529
+ if (firstEventTime) {
530
+ const earlyWindow = dayjs(firstEventTime).add(14, "days");
531
+ events.forEach(event => {
532
+ const t = dayjs(event.time);
533
+ if (t.isBefore(earlyWindow)) {
534
+ if (event.event === "phone number exchanged") hasPhoneExchangedEarly = true;
535
+ if (event.event === "date scheduled") hasDateScheduledEarly = true;
536
+ }
537
+ });
538
+ }
539
+
540
+ // ── HOOK 1: PHOTO UPLOAD CONVERSION ─────────────
541
+ // Users with 4+ photo uploads get 5x match events
542
+ if (photoUploadCount >= 4 && matchEvents.length > 0) {
543
+ const matchTemplate = matchEvents[0];
544
+ const extraMatches = matchEvents.length * 4; // 4 additional = 5x total
545
+ for (let i = 0; i < extraMatches; i++) {
546
+ const sourceMatch = matchEvents[i % matchEvents.length];
547
+ events.push({
548
+ ...matchTemplate,
549
+ event: "match received",
550
+ time: dayjs(sourceMatch.time).add(chance.integer({ min: 1, max: 180 }), "minutes").toISOString(),
551
+ user_id: sourceMatch.user_id,
552
+ match_score: chance.integer({ min: 60, max: 98 }),
553
+ complete_profile: true,
554
+ super_like_match: false,
555
+ premium_boost: false,
556
+ });
557
+ }
558
+ }
559
+
560
+ // ── HOOK 2: WEEKEND SWIPE SURGE (everything part) ─
561
+ // Tag Sunday evening swipes AFTER sessionization has finalized times
562
+ events.forEach(event => {
563
+ if (event.event === "swipe right") {
564
+ const dow = new Date(event.time).getUTCDay();
565
+ const hr = new Date(event.time).getUTCHours();
566
+ if (dow === 0 && hr >= 18 && hr <= 23) {
567
+ event.weekend_surge = true;
568
+ }
569
+ }
570
+ });
571
+ // Duplicate weekend_surge=true swipes to create 2.5x volume
572
+ for (let idx = events.length - 1; idx >= 0; idx--) {
573
+ const event = events[idx];
574
+ if (event.event === "swipe right" && event.weekend_surge === true) {
575
+ // Add 1.5 extra copies on average (1 always + 50% chance of second)
576
+ const etime = dayjs(event.time);
577
+ events.push({
578
+ ...event,
579
+ time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
580
+ user_id: event.user_id,
581
+ });
582
+ if (chance.bool({ likelihood: 50 })) {
583
+ events.push({
584
+ ...event,
585
+ time: etime.add(chance.integer({ min: 5, max: 60 }), "minutes").toISOString(),
586
+ user_id: event.user_id,
587
+ });
588
+ }
589
+ }
590
+ }
591
+
592
+ // ── HOOK 3: SUPER-LIKE EFFECT ───────────────────
593
+ // Super-likes generate 3x more matches nearby in time
594
+ if (hasSuperLike && superLikeEvents.length > 0) {
595
+ const matchTemplate = matchEvents.length > 0
596
+ ? matchEvents[0]
597
+ : events.find(e => e.event === "swipe right") || events[0];
598
+
599
+ superLikeEvents.forEach(sle => {
600
+ for (let i = 0; i < 3; i++) {
601
+ events.push({
602
+ ...matchTemplate,
603
+ event: "match received",
604
+ time: dayjs(sle.time).add(chance.integer({ min: 5, max: 120 }), "minutes").toISOString(),
605
+ user_id: sle.user_id,
606
+ match_score: chance.integer({ min: 70, max: 99 }),
607
+ complete_profile: false,
608
+ super_like_match: true,
609
+ premium_boost: false,
610
+ });
611
+ }
612
+ });
613
+ }
614
+
615
+ // ── HOOK 4: PREMIUM MATCH BOOST ─────────────────
616
+ // Premium: 2x matches. Elite: 4x matches + profile viewed (see-who-liked-you).
617
+ const sub = profile.subscription;
618
+ if ((sub === "Premium" || sub === "Elite") && matchEvents.length > 0) {
619
+ const multiplier = sub === "Elite" ? 3 : 1; // +3 = 4x total for Elite, +1 = 2x for Premium
620
+ const matchTemplate = matchEvents[0];
621
+
622
+ for (let i = 0; i < matchEvents.length * multiplier; i++) {
623
+ const sourceMatch = matchEvents[i % matchEvents.length];
624
+ events.push({
625
+ ...matchTemplate,
626
+ event: "match received",
627
+ time: dayjs(sourceMatch.time).add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
628
+ user_id: sourceMatch.user_id,
629
+ match_score: chance.integer({ min: 65, max: 99 }),
630
+ complete_profile: false,
631
+ super_like_match: false,
632
+ premium_boost: true,
633
+ });
634
+ }
635
+
636
+ // Elite exclusive: inject "profile viewed" events (see-who-liked-you)
637
+ if (sub === "Elite") {
638
+ const viewTemplate = events.find(e => e.event === "profile viewed") || matchTemplate;
639
+ matchEvents.forEach(m => {
640
+ events.push({
641
+ ...viewTemplate,
642
+ event: "profile viewed",
643
+ time: dayjs(m.time).subtract(chance.integer({ min: 10, max: 120 }), "minutes").toISOString(),
644
+ user_id: m.user_id,
645
+ viewer_source: "liked_you",
646
+ premium_boost: true,
647
+ });
648
+ });
649
+ }
650
+ }
651
+
652
+ // ── HOOK 6: BIO + PROMPT POWER USERS ────────────
653
+ // Users with bio updated AND 3+ prompts get 4x date scheduled
654
+ if (hasBioUpdated && promptAnsweredCount >= 3) {
655
+ const dateEvents = events.filter(e => e.event === "date scheduled");
656
+ if (dateEvents.length > 0) {
657
+ const dateTemplate = dateEvents[0];
658
+ const extraDates = dateEvents.length * 3; // +3 = 4x total
659
+ const venueTypes = ["coffee", "dinner", "drinks", "activity", "virtual"];
660
+ for (let i = 0; i < extraDates; i++) {
661
+ const sourceDate = dateEvents[i % dateEvents.length];
662
+ events.push({
663
+ ...dateTemplate,
664
+ event: "date scheduled",
665
+ time: dayjs(sourceDate.time).add(chance.integer({ min: 1, max: 72 }), "hours").toISOString(),
666
+ user_id: sourceDate.user_id,
667
+ venue_type: chance.pickone(venueTypes),
668
+ complete_profile_power: true,
669
+ });
670
+ }
671
+ }
672
+ }
673
+
674
+ // ── HOOK 7: VALENTINE'S DAY SPIKE (everything part) ─
675
+ // 3x profile created events during V-Day window (via cloning tagged events)
676
+ const vdaySignups = events.filter(e => e.event === "profile created" && e.valentines === true);
677
+ vdaySignups.forEach(signup => {
678
+ for (let i = 0; i < 2; i++) { // +2 clones = 3x total
679
+ events.push({
680
+ ...signup,
681
+ time: dayjs(signup.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
682
+ user_id: signup.user_id,
683
+ valentines: true,
684
+ });
685
+ }
686
+ });
687
+
688
+ // 5x premium upgrade events for users active during V-Day window
689
+ const vdayUpgrades = events.filter(e =>
690
+ e.event === "premium upgrade" &&
691
+ dayjs(e.time).isAfter(VDAY_WINDOW_START) &&
692
+ dayjs(e.time).isBefore(VDAY_WINDOW_END)
693
+ );
694
+ if (vdayUpgrades.length > 0) {
695
+ const upgradeTemplate = vdayUpgrades[0];
696
+ vdayUpgrades.forEach(upgrade => {
697
+ for (let i = 0; i < 4; i++) { // +4 clones = 5x total
698
+ events.push({
699
+ ...upgradeTemplate,
700
+ event: "premium upgrade",
701
+ time: dayjs(upgrade.time).add(chance.integer({ min: 1, max: 24 }), "hours").toISOString(),
702
+ user_id: upgrade.user_id,
703
+ plan: upgrade.plan,
704
+ price_usd: upgrade.price_usd,
705
+ valentines: true,
706
+ });
707
+ }
708
+ });
709
+ }
710
+
711
+ // ── HOOK 5: GHOSTING CHURN ──────────────────────
712
+ // Users with match but no message within 48 hours → lose 70% of later events
713
+ let isGhoster = false;
714
+ if (matchEvents.length > 0) {
715
+ // Check if any match has a message within 48 hours
716
+ let hasTimely = false;
717
+ for (const m of matchEvents) {
718
+ const matchTime = dayjs(m.time);
719
+ const deadline = matchTime.add(48, "hours");
720
+ for (const msg of messageSentEvents) {
721
+ const msgTime = dayjs(msg.time);
722
+ if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
723
+ hasTimely = true;
724
+ break;
725
+ }
726
+ }
727
+ if (hasTimely) break;
728
+ }
729
+
730
+ if (!hasTimely) {
731
+ isGhoster = true;
732
+ // Find earliest match time as churn reference point
733
+ const earliestMatch = matchEvents.reduce((min, m) =>
734
+ dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
735
+ );
736
+ const churnAfter = dayjs(earliestMatch.time);
737
+
738
+ for (let i = events.length - 1; i >= 0; i--) {
739
+ const evt = events[i];
740
+ if (dayjs(evt.time).isAfter(churnAfter)) {
741
+ if (chance.bool({ likelihood: 70 })) {
742
+ events.splice(i, 1);
743
+ } else if (evt.event === "app opened") {
744
+ evt.ghosted = true;
745
+ }
746
+ }
747
+ }
748
+ }
749
+ }
750
+
751
+ // ── HOOK 8: OFF-APP RETENTION ───────────────────
752
+ // Users with phone exchange or date in first 14 days: 60% D30 retention
753
+ // Others: lose events after day 30
754
+ if (firstEventTime) {
755
+ const day30 = dayjs(firstEventTime).add(30, "days");
756
+ const hasEarlyMilestone = hasPhoneExchangedEarly || hasDateScheduledEarly;
757
+
758
+ if (hasEarlyMilestone) {
759
+ // Good retention: inject extra events after day 30 to sustain engagement
760
+ const appOpenedTemplate = events.find(e => e.event === "app opened") || events[0];
761
+ const swipeTemplate = events.find(e => e.event === "swipe right") || events[0];
762
+ const postDay30Events = events.filter(e => dayjs(e.time).isAfter(day30));
763
+
764
+ // If they don't have enough post-day30 events, inject some
765
+ if (postDay30Events.length < events.length * 0.3) {
766
+ const retentionCount = Math.floor(events.length * 0.3);
767
+ for (let i = 0; i < retentionCount; i++) {
768
+ const daysAfter = chance.integer({ min: 1, max: 60 });
769
+ const template = chance.bool({ likelihood: 50 }) ? appOpenedTemplate : swipeTemplate;
770
+ events.push({
771
+ ...template,
772
+ time: day30.add(daysAfter, "days").add(chance.integer({ min: 0, max: 23 }), "hours").toISOString(),
773
+ user_id: template.user_id,
774
+ });
775
+ }
776
+ }
777
+ } else {
778
+ // No milestone: churn after day 30
779
+ for (let i = events.length - 1; i >= 0; i--) {
780
+ const evt = events[i];
781
+ if (dayjs(evt.time).isAfter(day30)) {
782
+ // Keep ~20% of events (remove ~80%) to simulate natural drop-off
783
+ if (chance.bool({ likelihood: 80 })) {
784
+ events.splice(i, 1);
785
+ }
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ return record;
792
+ }
793
+
794
+ return record;
795
+ },
796
+ };
797
+
798
+ export default config;