@ak--47/dungeon-master 1.3.1 → 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/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/vertical/ai-platform.js +45 -52
- package/dungeons/vertical/community.js +11 -8
- package/dungeons/vertical/crypto.js +25 -24
- package/dungeons/vertical/dating.js +56 -48
- package/dungeons/vertical/devtools.js +25 -18
- package/dungeons/vertical/ecommerce.js +42 -38
- package/dungeons/vertical/education.js +24 -9
- package/dungeons/vertical/fintech.js +13 -8
- package/dungeons/vertical/fitness.js +73 -122
- package/dungeons/vertical/food-delivery.js +18 -19
- package/dungeons/vertical/gaming.js +19 -20
- package/dungeons/vertical/healthcare.js +11 -8
- package/dungeons/vertical/insurance-application.js +6 -3
- package/dungeons/vertical/logistics.js +15 -9
- package/dungeons/vertical/marketplace.js +36 -27
- package/dungeons/vertical/media.js +27 -25
- package/dungeons/vertical/real-estate.js +18 -7
- package/dungeons/vertical/sass.js +84 -68
- package/dungeons/vertical/social.js +46 -47
- package/dungeons/vertical/travel.js +8 -5
- package/lib/core/config-validator.js +136 -157
- package/lib/generators/events.js +49 -93
- package/lib/generators/funnels.js +202 -91
- 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 +119 -269
- package/lib/utils/utils.js +29 -16
- 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/types.d.ts +397 -211
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "dm4-community";
|
|
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
|
|
|
@@ -31,7 +31,7 @@ const communityIds = v.range(1, 30).map(() => `COMM_${v.uid(4)}`);
|
|
|
31
31
|
* users create articles, discuss topics, moderate content, and
|
|
32
32
|
* build collaborative knowledge bases across fandoms.
|
|
33
33
|
*
|
|
34
|
-
* -
|
|
34
|
+
* - 10,000 users over 120 days
|
|
35
35
|
* - Multi-role system: power creators (5%), moderators (8%),
|
|
36
36
|
* active contributors (25%), readers (45%), lurkers (17%)
|
|
37
37
|
* - Core loop: sign up -> search -> read articles -> contribute -> discuss
|
|
@@ -262,14 +262,16 @@ const communityIds = v.range(1, 30).map(() => `COMM_${v.uid(4)}`);
|
|
|
262
262
|
|
|
263
263
|
/** @type {Config} */
|
|
264
264
|
const config = {
|
|
265
|
+
version: 2,
|
|
265
266
|
token,
|
|
266
267
|
seed: SEED,
|
|
267
268
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
268
|
-
datasetEnd: "2026-
|
|
269
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
269
270
|
// numDays: num_days,
|
|
270
271
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
271
272
|
numUsers: num_users,
|
|
272
|
-
hasAnonIds:
|
|
273
|
+
hasAnonIds: true,
|
|
274
|
+
avgDevicePerUser: 2,
|
|
273
275
|
hasSessionIds: true,
|
|
274
276
|
format: "json",
|
|
275
277
|
gzip: true,
|
|
@@ -302,6 +304,7 @@ const config = {
|
|
|
302
304
|
event: "account created",
|
|
303
305
|
weight: 1,
|
|
304
306
|
isFirstEvent: true,
|
|
307
|
+
isAuthEvent: true,
|
|
305
308
|
properties: {
|
|
306
309
|
referral_source: ["organic", "google_search", "reddit_referral", "youtube_link", "friend_invite"],
|
|
307
310
|
},
|
|
@@ -618,7 +621,7 @@ const config = {
|
|
|
618
621
|
name: "google_search",
|
|
619
622
|
source: "google",
|
|
620
623
|
medium: "organic",
|
|
621
|
-
activeDays: [0,
|
|
624
|
+
activeDays: [0, 120],
|
|
622
625
|
dailyBudget: [100, 400],
|
|
623
626
|
acquisitionRate: 0.03,
|
|
624
627
|
userPersonaBias: { reader: 0.5, lurker: 0.3 },
|
|
@@ -627,7 +630,7 @@ const config = {
|
|
|
627
630
|
name: "reddit_referral",
|
|
628
631
|
source: "reddit",
|
|
629
632
|
medium: "referral",
|
|
630
|
-
activeDays: [0,
|
|
633
|
+
activeDays: [0, 120],
|
|
631
634
|
dailyBudget: [50, 200],
|
|
632
635
|
acquisitionRate: 0.02,
|
|
633
636
|
userPersonaBias: { active_contributor: 0.5, moderator: 0.3 },
|
|
@@ -636,7 +639,7 @@ const config = {
|
|
|
636
639
|
name: "youtube_link",
|
|
637
640
|
source: "youtube",
|
|
638
641
|
medium: "referral",
|
|
639
|
-
activeDays: [0,
|
|
642
|
+
activeDays: [0, 120],
|
|
640
643
|
dailyBudget: [75, 250],
|
|
641
644
|
acquisitionRate: 0.02,
|
|
642
645
|
userPersonaBias: { lurker: 0.5, reader: 0.3 },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "coinnest";
|
|
3
3
|
const num_days = 120;
|
|
4
|
-
const num_users =
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 0.83;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -200,7 +200,7 @@ const chance = u.initChance(SEED);
|
|
|
200
200
|
* ---------------------------------------------------------------
|
|
201
201
|
* 8. RUG-PULL AFTERMATH (everything)
|
|
202
202
|
*
|
|
203
|
-
* PATTERN: ~
|
|
203
|
+
* PATTERN: ~2% of pre-day-70 swaps get token_pair flipped to a SCAM
|
|
204
204
|
* pair. Users who held SCAM lose 80% of post-day-70 events. No flag.
|
|
205
205
|
*
|
|
206
206
|
* HOW TO FIND IT IN MIXPANEL:
|
|
@@ -307,14 +307,16 @@ const NFT_COLLECTIONS = [
|
|
|
307
307
|
|
|
308
308
|
/** @type {Config} */
|
|
309
309
|
const config = {
|
|
310
|
+
version: 2,
|
|
310
311
|
token,
|
|
311
312
|
seed: SEED,
|
|
312
313
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
313
|
-
datasetEnd: "2026-
|
|
314
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
314
315
|
// numDays: num_days,
|
|
315
316
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
316
317
|
numUsers: num_users,
|
|
317
|
-
hasAnonIds:
|
|
318
|
+
hasAnonIds: true,
|
|
319
|
+
avgDevicePerUser: 2,
|
|
318
320
|
hasSessionIds: true,
|
|
319
321
|
format: "json",
|
|
320
322
|
gzip: true,
|
|
@@ -369,6 +371,7 @@ const config = {
|
|
|
369
371
|
event: "wallet connected",
|
|
370
372
|
weight: 1,
|
|
371
373
|
isFirstEvent: true,
|
|
374
|
+
isAuthEvent: true,
|
|
372
375
|
properties: {
|
|
373
376
|
wallet_type: ["MetaMask", "Phantom", "Coinbase Wallet", "Rainbow", "WalletConnect"],
|
|
374
377
|
chain: ["Ethereum", "Solana", "Base", "Arbitrum", "Polygon"],
|
|
@@ -575,21 +578,7 @@ const config = {
|
|
|
575
578
|
});
|
|
576
579
|
}
|
|
577
580
|
|
|
578
|
-
// HOOK 2
|
|
579
|
-
// 40% of swaps get swap_status=failed. Runs in everything hook so
|
|
580
|
-
// timestamp checks see post-bunchIntoSessions times.
|
|
581
|
-
const day35 = datasetStart.add(35, "days");
|
|
582
|
-
const day38 = datasetStart.add(38, "days");
|
|
583
|
-
userEvents.forEach(e => {
|
|
584
|
-
if (e.event !== "swap" && e.event !== "transfer") return;
|
|
585
|
-
const t = dayjs(e.time);
|
|
586
|
-
if (t.isAfter(day35) && t.isBefore(day38)) {
|
|
587
|
-
e.gas_fee_usd = Math.round((e.gas_fee_usd || 5) * 10);
|
|
588
|
-
if (e.event === "swap" && chance.bool({ likelihood: 40 })) {
|
|
589
|
-
e.swap_status = "failed";
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
});
|
|
581
|
+
// HOOK 2 moved to end (after all cloning) for full coverage
|
|
593
582
|
|
|
594
583
|
// HOOK 3: TOKEN LAUNCH SURGE — after d50, 25% of swaps flip token_pair to MOON.
|
|
595
584
|
const day50 = datasetStart.add(50, "days");
|
|
@@ -767,15 +756,11 @@ const config = {
|
|
|
767
756
|
// -----------------------------------------------------------
|
|
768
757
|
const day70 = datasetStart.add(70, "days");
|
|
769
758
|
|
|
770
|
-
// Check if user traded SCAM before day 70
|
|
771
|
-
// ~10% of pre-day-70 swaps naturally get SCAM token pair
|
|
772
|
-
// (injected here since SCAM isn't in the default TOKEN_PAIRS list)
|
|
773
759
|
let hadScam = false;
|
|
774
760
|
userEvents.forEach(e => {
|
|
775
761
|
if (e.event === "swap") {
|
|
776
762
|
const swapDay = dayjs(e.time).diff(datasetStart, "day");
|
|
777
|
-
|
|
778
|
-
if (swapDay >= 10 && swapDay < 70 && chance.bool({ likelihood: 8 })) {
|
|
763
|
+
if (swapDay >= 10 && swapDay < 70 && chance.bool({ likelihood: 2 })) {
|
|
779
764
|
e.token_pair = chance.pickone(["SCAM/USDC", "SCAM/ETH", "ETH/SCAM"]);
|
|
780
765
|
hadScam = true;
|
|
781
766
|
}
|
|
@@ -820,6 +805,22 @@ const config = {
|
|
|
820
805
|
}
|
|
821
806
|
}
|
|
822
807
|
|
|
808
|
+
// HOOK 2: GAS PRICE SPIKE — days 35-37 swap+transfer gas_fee_usd 10x,
|
|
809
|
+
// 40% of swaps get swap_status=failed. Runs AFTER all cloning to
|
|
810
|
+
// catch events that land in the spike window from clone offsets.
|
|
811
|
+
const day35 = datasetStart.add(35, "days");
|
|
812
|
+
const day38 = datasetStart.add(38, "days");
|
|
813
|
+
userEvents.forEach(e => {
|
|
814
|
+
if (e.event !== "swap" && e.event !== "transfer") return;
|
|
815
|
+
const t = dayjs(e.time);
|
|
816
|
+
if (t.isAfter(day35) && t.isBefore(day38)) {
|
|
817
|
+
e.gas_fee_usd = Math.round((e.gas_fee_usd || 5) * 10);
|
|
818
|
+
if (e.event === "swap" && chance.bool({ likelihood: 40 })) {
|
|
819
|
+
e.swap_status = "failed";
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
823
824
|
userEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
824
825
|
}
|
|
825
826
|
|
|
@@ -81,7 +81,8 @@ const chance = u.initChance(SEED);
|
|
|
81
81
|
* -------------------------------------------------------------------------------------
|
|
82
82
|
* 2. WEEKEND SWIPE SURGE (everything)
|
|
83
83
|
* -------------------------------------------------------------------------------------
|
|
84
|
-
* PATTERN: Sunday
|
|
84
|
+
* PATTERN: Sunday swipes get heavy cloning (evening 4x, daytime 2x)
|
|
85
|
+
* to overcome soup DOW weight deficit. No flag.
|
|
85
86
|
*
|
|
86
87
|
* HOW TO FIND IT IN MIXPANEL:
|
|
87
88
|
* Report 1: Swipe Volume by Day of Week
|
|
@@ -215,14 +216,16 @@ const chance = u.initChance(SEED);
|
|
|
215
216
|
|
|
216
217
|
/** @type {Config} */
|
|
217
218
|
const config = {
|
|
219
|
+
version: 2,
|
|
218
220
|
token,
|
|
219
221
|
seed: SEED,
|
|
220
222
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
221
|
-
datasetEnd: "2026-
|
|
223
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
222
224
|
// numDays: num_days,
|
|
223
225
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
224
226
|
numUsers: num_users,
|
|
225
|
-
hasAnonIds:
|
|
227
|
+
hasAnonIds: true,
|
|
228
|
+
avgDevicePerUser: 2,
|
|
226
229
|
hasSessionIds: true,
|
|
227
230
|
format: "json",
|
|
228
231
|
gzip: true,
|
|
@@ -246,6 +249,7 @@ const config = {
|
|
|
246
249
|
event: "profile created",
|
|
247
250
|
weight: 1,
|
|
248
251
|
isFirstEvent: true,
|
|
252
|
+
isAuthEvent: true,
|
|
249
253
|
properties: {
|
|
250
254
|
age_range: ["18-24", "25-29", "30-34", "35-39", "40+"],
|
|
251
255
|
gender: ["Male", "Male", "Female", "Female", "Non-binary"],
|
|
@@ -544,24 +548,22 @@ const config = {
|
|
|
544
548
|
});
|
|
545
549
|
}
|
|
546
550
|
|
|
547
|
-
// HOOK 2: WEEKEND SWIPE SURGE — Sunday
|
|
548
|
-
//
|
|
551
|
+
// HOOK 2: WEEKEND SWIPE SURGE — Sunday swipes get heavy cloning
|
|
552
|
+
// to overcome the soup DOW weight deficit. Evening swipes (18-23)
|
|
553
|
+
// get 4 clones; daytime Sunday swipes get 2 clones.
|
|
554
|
+
// No flag — discover via day-of-week chart.
|
|
549
555
|
for (let idx = events.length - 1; idx >= 0; idx--) {
|
|
550
556
|
const event = events[idx];
|
|
551
557
|
if (event.event === "swipe right") {
|
|
552
558
|
const dow = new Date(event.time).getUTCDay();
|
|
553
|
-
|
|
554
|
-
|
|
559
|
+
if (dow === 0) {
|
|
560
|
+
const hr = new Date(event.time).getUTCHours();
|
|
561
|
+
const clones = (hr >= 18 && hr <= 23) ? 4 : 2;
|
|
555
562
|
const etime = dayjs(event.time);
|
|
556
|
-
|
|
557
|
-
...event,
|
|
558
|
-
time: etime.add(chance.integer({ min: 1, max: 30 }), "minutes").toISOString(),
|
|
559
|
-
user_id: event.user_id,
|
|
560
|
-
});
|
|
561
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
563
|
+
for (let c = 0; c < clones; c++) {
|
|
562
564
|
events.push({
|
|
563
565
|
...event,
|
|
564
|
-
time: etime.add(chance.integer({ min:
|
|
566
|
+
time: etime.add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
565
567
|
user_id: event.user_id,
|
|
566
568
|
});
|
|
567
569
|
}
|
|
@@ -587,15 +589,50 @@ const config = {
|
|
|
587
589
|
});
|
|
588
590
|
}
|
|
589
591
|
|
|
592
|
+
// HOOK 5: GHOSTING CHURN — users with match but no message within
|
|
593
|
+
// 48hrs lose 80% of post-match events. No flag.
|
|
594
|
+
// (runs BEFORE premium boost so injected premium matches survive)
|
|
595
|
+
if (matchEvents.length > 0) {
|
|
596
|
+
let hasTimely = false;
|
|
597
|
+
for (const m of matchEvents) {
|
|
598
|
+
const matchTime = dayjs(m.time);
|
|
599
|
+
const deadline = matchTime.add(48, "hours");
|
|
600
|
+
for (const msg of messageSentEvents) {
|
|
601
|
+
const msgTime = dayjs(msg.time);
|
|
602
|
+
if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
|
|
603
|
+
hasTimely = true;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (hasTimely) break;
|
|
608
|
+
}
|
|
609
|
+
if (!hasTimely) {
|
|
610
|
+
const earliestMatch = matchEvents.reduce((min, m) =>
|
|
611
|
+
dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
|
|
612
|
+
);
|
|
613
|
+
const churnAfter = dayjs(earliestMatch.time);
|
|
614
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
615
|
+
if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
|
|
616
|
+
events.splice(i, 1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
590
622
|
// HOOK 4: PREMIUM MATCH BOOST — Premium 2x, Elite 4x match events.
|
|
591
623
|
// Elite users also get profile-viewed events injected (see-who-liked-you).
|
|
592
|
-
// Reads subscription from profile.
|
|
624
|
+
// Reads subscription from profile. Runs AFTER ghosting churn so
|
|
625
|
+
// injected matches are not culled.
|
|
593
626
|
const sub = profile.subscription;
|
|
594
627
|
if ((sub === "Premium" || sub === "Elite") && matchEvents.length > 0) {
|
|
595
|
-
|
|
628
|
+
// Count surviving match events post-churn
|
|
629
|
+
const survivingMatches = events.filter(e => e.event === "match received");
|
|
630
|
+
const baseCount = survivingMatches.length || 1;
|
|
631
|
+
const targetMultiplier = sub === "Elite" ? 4 : 2;
|
|
632
|
+
const toAdd = Math.max(0, baseCount * targetMultiplier - baseCount);
|
|
596
633
|
const matchTemplate = matchEvents[0];
|
|
597
|
-
for (let i = 0; i <
|
|
598
|
-
const sourceMatch =
|
|
634
|
+
for (let i = 0; i < toAdd; i++) {
|
|
635
|
+
const sourceMatch = survivingMatches[i % survivingMatches.length] || matchTemplate;
|
|
599
636
|
events.push({
|
|
600
637
|
...matchTemplate,
|
|
601
638
|
time: dayjs(sourceMatch.time).add(chance.integer({ min: 10, max: 240 }), "minutes").toISOString(),
|
|
@@ -605,7 +642,7 @@ const config = {
|
|
|
605
642
|
}
|
|
606
643
|
if (sub === "Elite") {
|
|
607
644
|
const viewTemplate = events.find(e => e.event === "profile viewed") || matchTemplate;
|
|
608
|
-
|
|
645
|
+
survivingMatches.forEach(m => {
|
|
609
646
|
events.push({
|
|
610
647
|
...viewTemplate,
|
|
611
648
|
event: "profile viewed",
|
|
@@ -705,35 +742,6 @@ const config = {
|
|
|
705
742
|
}
|
|
706
743
|
}
|
|
707
744
|
|
|
708
|
-
// HOOK 5: GHOSTING CHURN — users with match but no message within
|
|
709
|
-
// 48hrs lose 80% of post-match events. No flag.
|
|
710
|
-
if (matchEvents.length > 0) {
|
|
711
|
-
let hasTimely = false;
|
|
712
|
-
for (const m of matchEvents) {
|
|
713
|
-
const matchTime = dayjs(m.time);
|
|
714
|
-
const deadline = matchTime.add(48, "hours");
|
|
715
|
-
for (const msg of messageSentEvents) {
|
|
716
|
-
const msgTime = dayjs(msg.time);
|
|
717
|
-
if (msgTime.isAfter(matchTime) && msgTime.isBefore(deadline)) {
|
|
718
|
-
hasTimely = true;
|
|
719
|
-
break;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
if (hasTimely) break;
|
|
723
|
-
}
|
|
724
|
-
if (!hasTimely) {
|
|
725
|
-
const earliestMatch = matchEvents.reduce((min, m) =>
|
|
726
|
-
dayjs(m.time).isBefore(dayjs(min.time)) ? m : min
|
|
727
|
-
);
|
|
728
|
-
const churnAfter = dayjs(earliestMatch.time);
|
|
729
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
730
|
-
if (dayjs(events[i].time).isAfter(churnAfter) && chance.bool({ likelihood: 80 })) {
|
|
731
|
-
events.splice(i, 1);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
745
|
return record;
|
|
738
746
|
}
|
|
739
747
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "dm4-devtools";
|
|
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
|
|
|
@@ -281,14 +281,16 @@ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
|
|
|
281
281
|
|
|
282
282
|
/** @type {Config} */
|
|
283
283
|
const config = {
|
|
284
|
+
version: 2,
|
|
284
285
|
token,
|
|
285
286
|
seed: SEED,
|
|
286
287
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
287
|
-
datasetEnd: "2026-
|
|
288
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
288
289
|
// numDays: num_days,
|
|
289
290
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
290
291
|
numUsers: num_users,
|
|
291
|
-
hasAnonIds:
|
|
292
|
+
hasAnonIds: true,
|
|
293
|
+
avgDevicePerUser: 2,
|
|
292
294
|
hasSessionIds: true,
|
|
293
295
|
format: "json",
|
|
294
296
|
gzip: true,
|
|
@@ -321,6 +323,7 @@ const config = {
|
|
|
321
323
|
event: "account created",
|
|
322
324
|
weight: 1,
|
|
323
325
|
isFirstEvent: true,
|
|
326
|
+
isAuthEvent: true,
|
|
324
327
|
properties: {
|
|
325
328
|
referral_source: ["organic", "github", "conference", "blog_post", "colleague", "search"],
|
|
326
329
|
},
|
|
@@ -841,11 +844,16 @@ const config = {
|
|
|
841
844
|
}
|
|
842
845
|
|
|
843
846
|
// -- HOOK 3: COPILOT PR VELOCITY ----------------------
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
);
|
|
848
|
-
if (
|
|
847
|
+
// ~30% of users are copilot adopters (hash-based cohort).
|
|
848
|
+
// Copilot users get ai_assist="copilot" stamped and 1.5x more PRs.
|
|
849
|
+
const uid = events[0]?.user_id || "";
|
|
850
|
+
const isCopilotUser = (typeof uid === "string" ? uid.charCodeAt(0) : uid) % 10 < 3;
|
|
851
|
+
if (isCopilotUser) {
|
|
852
|
+
events.forEach(e => {
|
|
853
|
+
if (e.event === "pull request created" || e.event === "code review completed") {
|
|
854
|
+
e.ai_assist = "copilot";
|
|
855
|
+
}
|
|
856
|
+
});
|
|
849
857
|
const prEvents = events.filter(e => e.event === "pull request created");
|
|
850
858
|
const extraCount = Math.floor(prEvents.length * 0.5);
|
|
851
859
|
for (let i = 0; i < extraCount; i++) {
|
|
@@ -909,16 +917,15 @@ const config = {
|
|
|
909
917
|
}
|
|
910
918
|
|
|
911
919
|
// -- HOOK 9: BUILD-COUNT MAGIC NUMBER (no flags) ------
|
|
912
|
-
// Sweet 15-30 builds → +
|
|
913
|
-
// Over 31+ → drop
|
|
920
|
+
// Sweet 15-30 builds → +50% deploys (clone with unique offset).
|
|
921
|
+
// Over 31+ → drop 40% of deploys (flaky CI burnout).
|
|
914
922
|
const buildCount = events.filter(e => e.event === "build completed").length;
|
|
915
923
|
if (buildCount >= 15 && buildCount <= 30) {
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
const tpl = deploys[k % deploys.length];
|
|
924
|
+
const deploys = events.filter(e => e.event === "deployment completed");
|
|
925
|
+
const extras = Math.max(Math.floor(deploys.length * 0.5), 1);
|
|
926
|
+
for (let k = 0; k < extras; k++) {
|
|
927
|
+
const tpl = deploys[k % deploys.length];
|
|
928
|
+
if (tpl) {
|
|
922
929
|
events.push({
|
|
923
930
|
...tpl,
|
|
924
931
|
time: dayjs(tpl.time).add(chance.integer({ min: 5, max: 360 }), "minutes").toISOString(),
|
|
@@ -928,7 +935,7 @@ const config = {
|
|
|
928
935
|
}
|
|
929
936
|
} else if (buildCount >= 31) {
|
|
930
937
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
931
|
-
if (events[i].event === "deployment completed" && chance.bool({ likelihood:
|
|
938
|
+
if (events[i].event === "deployment completed" && chance.bool({ likelihood: 40 })) {
|
|
932
939
|
events.splice(i, 1);
|
|
933
940
|
}
|
|
934
941
|
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "simple is best";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 42_000;
|
|
5
5
|
const avg_events_per_user_per_day = 0.37;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
8
8
|
// ── env overrides ──
|
|
9
9
|
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
10
|
|
|
11
|
-
import Chance from 'chance';
|
|
12
|
-
let chance = new Chance();
|
|
13
11
|
import dayjs from "dayjs";
|
|
14
12
|
import utc from "dayjs/plugin/utc.js";
|
|
15
13
|
dayjs.extend(utc);
|
|
16
14
|
import { uid, comma } from 'ak-tools';
|
|
17
|
-
import { weighNumRange, date, integer, weighChoices, decimal } from "../../lib/utils/utils.js";
|
|
15
|
+
import { weighNumRange, date, integer, weighChoices, decimal, initChance } from "../../lib/utils/utils.js";
|
|
16
|
+
const chance = initChance(SEED);
|
|
18
17
|
|
|
19
18
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
20
19
|
const itemCategories = ["Books", "Movies", "Music", "Games", "Electronics", "Computers", "Smart Home", "Home", "Garden", "Pet", "Beauty", "Health", "Toys", "Kids", "Baby", "Handmade", "Sports", "Outdoors", "Automotive", "Industrial", "Entertainment", "Art", "Food", "Appliances", "Office", "Wedding", "Software"];
|
|
@@ -292,17 +291,19 @@ function makeProducts(maxItems = 5) {
|
|
|
292
291
|
|
|
293
292
|
/** @type {import('../types.d.ts').Dungeon} */
|
|
294
293
|
const config = {
|
|
294
|
+
version: 2,
|
|
295
295
|
token,
|
|
296
296
|
seed: SEED,
|
|
297
297
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
298
|
-
datasetEnd: "2026-
|
|
298
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
299
299
|
// numDays: num_days,
|
|
300
300
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
301
301
|
numUsers: num_users,
|
|
302
302
|
format: 'json', //csv or json
|
|
303
303
|
region: "US",
|
|
304
|
-
hasAnonIds:
|
|
305
|
-
|
|
304
|
+
hasAnonIds: true,
|
|
305
|
+
avgDevicePerUser: 2,
|
|
306
|
+
hasSessionIds: true,
|
|
306
307
|
hasAdSpend: false,
|
|
307
308
|
hasLocation: true,
|
|
308
309
|
hasAndroidDevices: true,
|
|
@@ -406,6 +407,7 @@ const config = {
|
|
|
406
407
|
event: "sign up",
|
|
407
408
|
weight: 1,
|
|
408
409
|
isFirstEvent: true,
|
|
410
|
+
isAuthEvent: true,
|
|
409
411
|
properties: {
|
|
410
412
|
signupMethod: ["email", "google", "facebook", "twitter", "linkedin", "github"],
|
|
411
413
|
referral: weighChoices(["none", "none", "none", "friend", "ad", "ad", "ad", "friend", "friend", "friend", "friend"]),
|
|
@@ -512,42 +514,12 @@ const config = {
|
|
|
512
514
|
}
|
|
513
515
|
|
|
514
516
|
if (type === "event") {
|
|
515
|
-
const datasetEnd = dayjs.unix(meta.datasetEnd);
|
|
516
|
-
const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(7, 'day');
|
|
517
|
-
const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(30, 'day');
|
|
518
|
-
const eventTime = dayjs(record.time);
|
|
519
|
-
|
|
520
517
|
// unflattering 'items'
|
|
521
518
|
if (record.item && Array.isArray(record.item)) {
|
|
522
519
|
record = { ...record, ...record.item[0] };
|
|
523
520
|
delete record.item;
|
|
524
521
|
}
|
|
525
522
|
|
|
526
|
-
if (record.event === 'sign up') {
|
|
527
|
-
record.signup_flow = "v1";
|
|
528
|
-
if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
|
|
529
|
-
// tag 50% for removal (filtered in "everything" hook)
|
|
530
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
531
|
-
record._drop = true;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
|
|
535
|
-
record.signup_flow = "v2";
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (record.event === 'watch video') {
|
|
540
|
-
const factor = decimal(0.25, 0.79);
|
|
541
|
-
if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
|
|
542
|
-
record.watchTimeSec = Math.round(record.watchTimeSec * (1 - factor));
|
|
543
|
-
}
|
|
544
|
-
if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
|
|
545
|
-
// increase watch time by 33%
|
|
546
|
-
record.watchTimeSec = Math.round(record.watchTimeSec * (1 + factor));
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
523
|
// toys + shoes frequently purchases together (and are higher cart values)
|
|
552
524
|
if (record.event === 'checkout' && Array.isArray(record.cart)) {
|
|
553
525
|
const hasToys = record.cart.some(item => item.category === 'toys');
|
|
@@ -609,6 +581,38 @@ const config = {
|
|
|
609
581
|
e.theme = profile.theme;
|
|
610
582
|
});
|
|
611
583
|
|
|
584
|
+
const datasetEnd = dayjs.unix(meta.datasetEnd);
|
|
585
|
+
const DAY_SIGNUPS_IMPROVED = datasetEnd.subtract(7, 'day');
|
|
586
|
+
const DAY_WATCH_TIME_WENT_UP = datasetEnd.subtract(30, 'day');
|
|
587
|
+
|
|
588
|
+
// Hook 1: Signup flow improvement — pre-fix signups (v1) get 50% dropped
|
|
589
|
+
record.forEach(e => {
|
|
590
|
+
if (e.event !== 'sign up') return;
|
|
591
|
+
const eventTime = dayjs(e.time);
|
|
592
|
+
e.signup_flow = "v1";
|
|
593
|
+
if (eventTime.isBefore(DAY_SIGNUPS_IMPROVED)) {
|
|
594
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
595
|
+
e._drop = true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (eventTime.isAfter(DAY_SIGNUPS_IMPROVED)) {
|
|
599
|
+
e.signup_flow = "v2";
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Hook 2: Watch time inflection — before 30 days ago: reduce, after: increase
|
|
604
|
+
record.forEach(e => {
|
|
605
|
+
if (e.event !== 'watch video') return;
|
|
606
|
+
const eventTime = dayjs(e.time);
|
|
607
|
+
const factor = decimal(0.25, 0.79);
|
|
608
|
+
if (eventTime.isBefore(DAY_WATCH_TIME_WENT_UP)) {
|
|
609
|
+
e.watchTimeSec = Math.round(e.watchTimeSec * (1 - factor));
|
|
610
|
+
}
|
|
611
|
+
if (eventTime.isAfter(DAY_WATCH_TIME_WENT_UP)) {
|
|
612
|
+
e.watchTimeSec = Math.round(e.watchTimeSec * (1 + factor));
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
612
616
|
// Hook 6: View-Item Magic Number (behavioral, no flags)
|
|
613
617
|
// Users in 3-8 view-item sweet spot show ~25% higher cart amounts.
|
|
614
618
|
// Users >=9 are over-engaged window-shoppers; ~30% of their checkouts drop.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-education";
|
|
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
|
|
|
@@ -28,7 +28,7 @@ const chance = u.initChance(SEED);
|
|
|
28
28
|
* Supports self-paced and cohort-based learning with courses, quizzes,
|
|
29
29
|
* assignments, and social study features.
|
|
30
30
|
*
|
|
31
|
-
* Scale:
|
|
31
|
+
* Scale: 10,000 users / ~1.4M events / 120 days / 17 event types
|
|
32
32
|
*
|
|
33
33
|
* CORE LOOP:
|
|
34
34
|
* Register → browse/enroll in courses → watch lectures → practice problems →
|
|
@@ -314,14 +314,16 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
|
314
314
|
|
|
315
315
|
/** @type {Config} */
|
|
316
316
|
const config = {
|
|
317
|
+
version: 2,
|
|
317
318
|
token,
|
|
318
319
|
seed: SEED,
|
|
319
320
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
320
|
-
datasetEnd: "2026-
|
|
321
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
321
322
|
// numDays: num_days,
|
|
322
323
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
323
324
|
numUsers: num_users,
|
|
324
|
-
hasAnonIds:
|
|
325
|
+
hasAnonIds: true,
|
|
326
|
+
avgDevicePerUser: 2,
|
|
325
327
|
hasSessionIds: true,
|
|
326
328
|
format: "json",
|
|
327
329
|
gzip: true,
|
|
@@ -394,6 +396,7 @@ const config = {
|
|
|
394
396
|
event: "account registered",
|
|
395
397
|
weight: 1,
|
|
396
398
|
isFirstEvent: true,
|
|
399
|
+
isAuthEvent: true,
|
|
397
400
|
properties: {
|
|
398
401
|
"account_type": ["instructor", "instructor", "instructor", "instructor", "instructor", "instructor", "student"],
|
|
399
402
|
"signup_source": ["organic", "referral", "school_partnership", "social_ad"],
|
|
@@ -660,6 +663,17 @@ const config = {
|
|
|
660
663
|
}
|
|
661
664
|
}
|
|
662
665
|
|
|
666
|
+
// HOOK 7 (funnel-pre): FREE VS PAID — free users get 0.5x conversion rate;
|
|
667
|
+
// paid subscribers get 1.5x. Applies to course-completion funnel.
|
|
668
|
+
if (type === "funnel-pre") {
|
|
669
|
+
const subStatus = meta?.profile?.subscription_status;
|
|
670
|
+
if (subStatus === "free") {
|
|
671
|
+
record.conversionRate = Math.round(record.conversionRate * 0.5);
|
|
672
|
+
} else if (subStatus === "monthly" || subStatus === "annual") {
|
|
673
|
+
record.conversionRate = Math.min(100, Math.round(record.conversionRate * 1.5));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
663
677
|
// HOOK 9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (funnel-post)
|
|
664
678
|
// Annual subscribers complete the Course Completion funnel 1.4x
|
|
665
679
|
// faster (factor 0.71); Free users 1.4x slower (factor 1.4).
|
|
@@ -732,11 +746,12 @@ const config = {
|
|
|
732
746
|
}
|
|
733
747
|
}
|
|
734
748
|
} else if (notesTakenCount >= 9) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
749
|
+
// Over-noters: drop 35% of certificates (stuck in study mode)
|
|
750
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
751
|
+
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 35 })) {
|
|
752
|
+
userEvents.splice(i, 1);
|
|
738
753
|
}
|
|
739
|
-
}
|
|
754
|
+
}
|
|
740
755
|
}
|
|
741
756
|
|
|
742
757
|
// HOOK 8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
|