@ak--47/dungeon-master 1.2.0 → 1.2.2
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/dungeons/technical/ad-spend.js +15 -4
- package/dungeons/technical/anonymous-users.js +15 -5
- package/dungeons/technical/array-of-object-lookup.js +21 -5
- package/dungeons/technical/experiments.js +23 -5
- package/dungeons/technical/foobar.js +28 -12
- package/dungeons/technical/group-analytics.js +15 -5
- package/dungeons/technical/mirror-strategies.js +15 -5
- package/dungeons/technical/nested-objects.js +15 -4
- package/dungeons/technical/retention-cadence.js +13 -7
- package/dungeons/technical/sanity.js +25 -6
- package/dungeons/technical/scale-test.js +15 -4
- package/dungeons/technical/scd.js +24 -5
- package/dungeons/technical/simple.js +20 -5
- package/dungeons/technical/simplest.js +23 -6
- package/dungeons/technical/text-generation.js +22 -6
- package/dungeons/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +1 -1
- package/dungeons/vertical/community.js +49 -28
- package/dungeons/vertical/devtools-schema.json +1 -1
- package/dungeons/vertical/devtools.js +50 -29
- package/dungeons/vertical/ecommerce.js +23 -6
- package/dungeons/vertical/education-schema.json +1 -1
- package/dungeons/vertical/education.js +29 -19
- package/dungeons/vertical/fintech-schema.json +1 -1
- package/dungeons/vertical/fintech.js +27 -8
- package/dungeons/vertical/fitness-schema.json +1 -1
- package/dungeons/vertical/fitness.js +48 -22
- package/dungeons/vertical/food-delivery-schema.json +1 -1
- package/dungeons/vertical/food-delivery.js +51 -12
- package/dungeons/vertical/gaming-schema.json +1 -1
- package/dungeons/vertical/gaming.js +41 -7
- package/dungeons/vertical/healthcare-schema.json +1 -1
- package/dungeons/vertical/healthcare.js +69 -40
- package/dungeons/vertical/insurance-application-schema.json +1 -1
- package/dungeons/vertical/insurance-application.js +26 -8
- package/dungeons/vertical/logistics-schema.json +1 -1
- package/dungeons/vertical/logistics.js +69 -41
- package/dungeons/vertical/marketplace-schema.json +1 -1
- package/dungeons/vertical/marketplace.js +44 -19
- package/dungeons/vertical/media.js +56 -15
- package/dungeons/vertical/rpg-schema.json +1 -1
- package/dungeons/vertical/rpg.js +38 -8
- package/dungeons/vertical/sass.js +24 -7
- package/dungeons/vertical/social.js +23 -7
- package/dungeons/vertical/travel-schema.json +1 -1
- package/dungeons/vertical/travel.js +50 -19
- package/index.js +3 -0
- package/lib/core/config-validator.js +13 -2
- package/lib/generators/events.js +3 -5
- package/lib/orchestrators/mixpanel-sender.js +64 -3
- package/lib/orchestrators/user-loop.js +20 -0
- package/lib/utils/utils.js +129 -9
- package/package.json +2 -2
- package/types.d.ts +5 -3
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "dm4-insurance";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
|
|
6
|
-
const SEED = "dm4-insurance";
|
|
7
16
|
dayjs.extend(utc);
|
|
8
17
|
const chance = u.initChance(SEED);
|
|
9
|
-
const num_users = 5_000;
|
|
10
|
-
const days = 100;
|
|
11
18
|
|
|
12
19
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
13
20
|
|
|
@@ -126,17 +133,17 @@ const days = 100;
|
|
|
126
133
|
|
|
127
134
|
// ── Time constants for hook calculations ──
|
|
128
135
|
const NOW = dayjs();
|
|
129
|
-
const DATASET_START = NOW.subtract(
|
|
136
|
+
const DATASET_START = NOW.subtract(num_days, "days");
|
|
130
137
|
const V211_DATE = DATASET_START.add(30, "days");
|
|
131
138
|
const V212_DATE = DATASET_START.add(60, "days");
|
|
132
139
|
const V213_DATE = NOW.subtract(10, "days");
|
|
133
140
|
|
|
134
141
|
/** @type {Config} */
|
|
135
142
|
const config = {
|
|
136
|
-
token
|
|
143
|
+
token,
|
|
137
144
|
seed: SEED,
|
|
138
|
-
numDays:
|
|
139
|
-
numEvents: num_users *
|
|
145
|
+
numDays: num_days,
|
|
146
|
+
numEvents: num_users * avg_events_per_user,
|
|
140
147
|
numUsers: num_users,
|
|
141
148
|
hasAnonIds: false,
|
|
142
149
|
hasSessionIds: true,
|
|
@@ -431,13 +438,16 @@ const config = {
|
|
|
431
438
|
|
|
432
439
|
// ── Super Props (on every event) ──
|
|
433
440
|
superProps: {
|
|
434
|
-
|
|
441
|
+
Platform: ["web", "ios", "android"],
|
|
435
442
|
insurance_type: ["auto", "home", "life", "health", "renters"],
|
|
436
443
|
app_version: ["2.10"],
|
|
437
444
|
},
|
|
438
445
|
|
|
439
446
|
// ── User Props (set once per user) ──
|
|
440
447
|
userProps: {
|
|
448
|
+
Platform: ["web", "ios", "android"],
|
|
449
|
+
insurance_type: ["auto", "home", "life", "health", "renters"],
|
|
450
|
+
app_version: ["2.10"],
|
|
441
451
|
age_range: ["18-25", "26-35", "36-45", "46-55", "56-65", "65+"],
|
|
442
452
|
risk_profile: ["low", "medium", "high"],
|
|
443
453
|
policy_count: u.weighNumRange(0, 5, 0.5, 1),
|
|
@@ -477,6 +487,14 @@ const config = {
|
|
|
477
487
|
const userEvents = record;
|
|
478
488
|
if (userEvents.length === 0) return record;
|
|
479
489
|
|
|
490
|
+
// Stamp superProps from profile for consistency
|
|
491
|
+
const profile = meta.profile;
|
|
492
|
+
userEvents.forEach(e => {
|
|
493
|
+
e.Platform = profile.Platform;
|
|
494
|
+
e.insurance_type = profile.insurance_type;
|
|
495
|
+
e.app_version = profile.app_version;
|
|
496
|
+
});
|
|
497
|
+
|
|
480
498
|
// ─── Hook #1: VERSION STAMPING ───
|
|
481
499
|
// Deterministic app_version on every event based on its final timestamp.
|
|
482
500
|
// v2.10 (days 0-30) → v2.11 (30-60) → v2.12 (60-90) → v2.13 (last 10 days)
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "dm4-logistics";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "dm4-logistics";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
const NOW = dayjs();
|
|
13
|
-
const DATASET_START = NOW.subtract(
|
|
20
|
+
const DATASET_START = NOW.subtract(num_days, "days");
|
|
14
21
|
|
|
15
22
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
16
23
|
|
|
@@ -216,12 +223,14 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
216
223
|
* networks with large logistics teams, driving higher ACV.
|
|
217
224
|
*
|
|
218
225
|
* -------------------------------------------------------------------
|
|
219
|
-
* 8.
|
|
226
|
+
* 8. SMALL-BUSINESS CONVERSION DROP (everything hook)
|
|
220
227
|
* -------------------------------------------------------------------
|
|
221
228
|
*
|
|
222
|
-
* PATTERN:
|
|
223
|
-
*
|
|
224
|
-
*
|
|
229
|
+
* PATTERN: Small-business users lose ~35% of "alert configured"
|
|
230
|
+
* events (last step of the Integration Setup funnel). This is
|
|
231
|
+
* implemented via event filtering in the everything hook rather
|
|
232
|
+
* than conversionRate modification in funnel-pre, so the effect
|
|
233
|
+
* is not diluted by organic (non-funnel) events.
|
|
225
234
|
*
|
|
226
235
|
* HOW TO FIND IT IN MIXPANEL:
|
|
227
236
|
*
|
|
@@ -229,10 +238,10 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
229
238
|
* - Report type: Funnels
|
|
230
239
|
* - Steps: "integration connected" -> "report generated" -> "alert configured"
|
|
231
240
|
* - Breakdown: user property "company_tier"
|
|
232
|
-
* - Expected:
|
|
241
|
+
* - Expected: small_business ~20% vs enterprise/mid_market ~30% conversion
|
|
233
242
|
*
|
|
234
|
-
* REAL-WORLD ANALOGUE:
|
|
235
|
-
*
|
|
243
|
+
* REAL-WORLD ANALOGUE: Small businesses lack dedicated IT teams,
|
|
244
|
+
* leading to incomplete integration setup and lower alert adoption.
|
|
236
245
|
*
|
|
237
246
|
* ===================================================================
|
|
238
247
|
* EXPECTED METRICS SUMMARY
|
|
@@ -247,15 +256,15 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
247
256
|
* Alert Fatigue | response_time_hours | 4h | 8-12h | 2-3x
|
|
248
257
|
* Trial Churn | events after wk 1 | 5 | 2.5 | 0.5x
|
|
249
258
|
* Enterprise Profiles | warehouse_count | 3 | 10 | 3.3x
|
|
250
|
-
*
|
|
259
|
+
* Small-Biz Conversion Drop | funnel conversion | 30% | 20% | 0.65x
|
|
251
260
|
*/
|
|
252
261
|
|
|
253
262
|
/** @type {Config} */
|
|
254
263
|
const config = {
|
|
255
|
-
token
|
|
264
|
+
token,
|
|
256
265
|
seed: SEED,
|
|
257
|
-
numDays:
|
|
258
|
-
numEvents: num_users *
|
|
266
|
+
numDays: num_days,
|
|
267
|
+
numEvents: num_users * avg_events_per_user,
|
|
259
268
|
numUsers: num_users,
|
|
260
269
|
hasAnonIds: false,
|
|
261
270
|
hasSessionIds: true,
|
|
@@ -505,7 +514,7 @@ const config = {
|
|
|
505
514
|
|
|
506
515
|
// -- SuperProps ----------------------------------------------------
|
|
507
516
|
superProps: {
|
|
508
|
-
|
|
517
|
+
Platform: ["web", "web", "desktop_app"],
|
|
509
518
|
subscription_plan: ["free_trial", "free_trial", "starter", "starter", "professional", "enterprise"],
|
|
510
519
|
},
|
|
511
520
|
|
|
@@ -516,6 +525,8 @@ const config = {
|
|
|
516
525
|
employee_count: u.weighNumRange(1, 500, 0.3, 25),
|
|
517
526
|
industry: ["retail", "manufacturing", "food_beverage", "pharma", "electronics"],
|
|
518
527
|
integration_count: [0],
|
|
528
|
+
Platform: ["web", "web", "desktop_app"],
|
|
529
|
+
subscription_plan: ["free_trial", "free_trial", "starter", "starter", "professional", "enterprise"],
|
|
519
530
|
},
|
|
520
531
|
|
|
521
532
|
// -- Phase 2: Personas --------------------------------------------
|
|
@@ -697,16 +708,9 @@ const config = {
|
|
|
697
708
|
}
|
|
698
709
|
|
|
699
710
|
// -- HOOK 8: ENTERPRISE INTEGRATION FUNNEL LIFT (funnel-pre) --
|
|
700
|
-
//
|
|
711
|
+
// (conversionRate boost removed — filtering applied in everything hook instead)
|
|
701
712
|
if (type === "funnel-pre") {
|
|
702
|
-
|
|
703
|
-
const tier = meta.profile.company_tier;
|
|
704
|
-
if (tier === "enterprise") {
|
|
705
|
-
record.conversionRate = Math.min(record.conversionRate * 1.5, 95);
|
|
706
|
-
} else if (tier === "mid_market") {
|
|
707
|
-
record.conversionRate = Math.min(record.conversionRate * 1.2, 85);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
713
|
+
// no-op: conversion differentiation handled via event filtering below
|
|
710
714
|
}
|
|
711
715
|
|
|
712
716
|
// -- HOOK 1: MONTH-END REPORTING SURGE (event) ----------------
|
|
@@ -728,29 +732,53 @@ const config = {
|
|
|
728
732
|
|
|
729
733
|
// -- EVERYTHING HOOKS -----------------------------------------
|
|
730
734
|
if (type === "everything") {
|
|
731
|
-
|
|
732
|
-
|
|
735
|
+
if (!record.length) return record;
|
|
736
|
+
const profile = meta.profile;
|
|
737
|
+
|
|
738
|
+
// -- SUPER-PROP STAMPING ----------------------------------
|
|
739
|
+
// Stamp superProps from profile so they are consistent per-user.
|
|
740
|
+
if (profile) {
|
|
741
|
+
const plat = profile.Platform;
|
|
742
|
+
const plan = profile.subscription_plan;
|
|
743
|
+
record.forEach(e => {
|
|
744
|
+
if (plat) e.Platform = plat;
|
|
745
|
+
if (plan) e.subscription_plan = plan;
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// -- HOOK 8: SMALL-BUSINESS CONVERSION DROP ---------------
|
|
750
|
+
// Small-business users lose ~35% of "alert configured" events
|
|
751
|
+
// (last step of Integration Setup funnel), simulating lower
|
|
752
|
+
// conversion for smaller teams without dedicated IT resources.
|
|
753
|
+
if (profile && profile.company_tier === "small_business") {
|
|
754
|
+
record = record.filter(e => {
|
|
755
|
+
if (e.event === "alert configured" && chance.bool({ likelihood: 35 })) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
return true;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
733
761
|
|
|
734
762
|
// -- HOOK 3: REORDER ACCURACY BY TIER ---------------------
|
|
735
763
|
// Enterprise users get 10% of stockout alerts removed.
|
|
736
|
-
if (
|
|
737
|
-
for (let i =
|
|
738
|
-
if (
|
|
739
|
-
|
|
764
|
+
if (profile && profile.company_tier === "enterprise") {
|
|
765
|
+
for (let i = record.length - 1; i >= 0; i--) {
|
|
766
|
+
if (record[i].event === "stockout alert" && chance.bool({ likelihood: 10 })) {
|
|
767
|
+
record.splice(i, 1);
|
|
740
768
|
}
|
|
741
769
|
}
|
|
742
770
|
}
|
|
743
771
|
|
|
744
772
|
// -- HOOK 4: INTEGRATION COMPLETION DRIVES RETENTION ------
|
|
745
773
|
// Users with 3+ integration events get cloned report events.
|
|
746
|
-
const integrationCount =
|
|
774
|
+
const integrationCount = record.filter(e => e.event === "integration connected").length;
|
|
747
775
|
if (integrationCount >= 3) {
|
|
748
|
-
const templateReport =
|
|
776
|
+
const templateReport = record.find(e => e.event === "report generated");
|
|
749
777
|
if (templateReport) {
|
|
750
|
-
const integrationEvents =
|
|
778
|
+
const integrationEvents = record.filter(e => e.event === "integration connected");
|
|
751
779
|
integrationEvents.forEach(ie => {
|
|
752
780
|
if (chance.bool({ likelihood: 65 })) {
|
|
753
|
-
|
|
781
|
+
record.push({
|
|
754
782
|
...templateReport,
|
|
755
783
|
time: dayjs(ie.time).add(chance.integer({ min: 1, max: 5 }), "days").toISOString(),
|
|
756
784
|
user_id: ie.user_id,
|
|
@@ -764,7 +792,7 @@ const config = {
|
|
|
764
792
|
|
|
765
793
|
// -- HOOK 5: ALERT FATIGUE --------------------------------
|
|
766
794
|
// Users with >30 stockout alerts get increasing response times.
|
|
767
|
-
const alertEvents =
|
|
795
|
+
const alertEvents = record.filter(e => e.event === "stockout alert");
|
|
768
796
|
if (alertEvents.length > 30) {
|
|
769
797
|
alertEvents.forEach((alert, idx) => {
|
|
770
798
|
if (idx >= 20) {
|
|
@@ -776,14 +804,14 @@ const config = {
|
|
|
776
804
|
|
|
777
805
|
// -- HOOK 6: TRIAL CHURN ----------------------------------
|
|
778
806
|
// Users with <10 events lose 50% after day 7.
|
|
779
|
-
if (
|
|
780
|
-
const userFirstEvent =
|
|
807
|
+
if (record.length < 10) {
|
|
808
|
+
const userFirstEvent = record[0];
|
|
781
809
|
if (userFirstEvent) {
|
|
782
810
|
const firstTime = dayjs(userFirstEvent.time);
|
|
783
811
|
const cutoff = firstTime.add(7, "days");
|
|
784
|
-
for (let i =
|
|
785
|
-
if (dayjs(
|
|
786
|
-
|
|
812
|
+
for (let i = record.length - 1; i >= 0; i--) {
|
|
813
|
+
if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: 50 })) {
|
|
814
|
+
record.splice(i, 1);
|
|
787
815
|
}
|
|
788
816
|
}
|
|
789
817
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "dm4-marketplace";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "dm4-marketplace";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
const NOW = dayjs();
|
|
13
|
-
const DATASET_START = NOW.subtract(
|
|
20
|
+
const DATASET_START = NOW.subtract(num_days, "days");
|
|
14
21
|
|
|
15
22
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
16
23
|
|
|
@@ -213,12 +220,12 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
|
|
|
213
220
|
* high transaction volumes, and near-perfect ratings.
|
|
214
221
|
*
|
|
215
222
|
* ───────────────────────────────────────────────────────────────
|
|
216
|
-
* 8. FREQUENT BUYER FUNNEL LIFT (
|
|
223
|
+
* 8. FREQUENT BUYER FUNNEL LIFT (everything hook)
|
|
217
224
|
* ───────────────────────────────────────────────────────────────
|
|
218
225
|
*
|
|
219
|
-
* PATTERN:
|
|
220
|
-
*
|
|
221
|
-
*
|
|
226
|
+
* PATTERN: Non-frequent-buyer users lose ~25% of "purchase completed"
|
|
227
|
+
* events (last step of Browse to Purchase funnel), simulating lower
|
|
228
|
+
* conversion. Frequent buyers retain all purchase events.
|
|
222
229
|
*
|
|
223
230
|
* HOW TO FIND IT IN MIXPANEL:
|
|
224
231
|
*
|
|
@@ -249,10 +256,10 @@ const listingIds = v.range(1, 500).map(() => `LST_${v.uid(8)}`);
|
|
|
249
256
|
|
|
250
257
|
/** @type {Config} */
|
|
251
258
|
const config = {
|
|
252
|
-
token
|
|
259
|
+
token,
|
|
253
260
|
seed: SEED,
|
|
254
|
-
numDays:
|
|
255
|
-
numEvents: num_users *
|
|
261
|
+
numDays: num_days,
|
|
262
|
+
numEvents: num_users * avg_events_per_user,
|
|
256
263
|
numUsers: num_users,
|
|
257
264
|
hasAnonIds: false,
|
|
258
265
|
hasSessionIds: true,
|
|
@@ -502,7 +509,7 @@ const config = {
|
|
|
502
509
|
|
|
503
510
|
// ── SuperProps ──────────────────────────────────────────
|
|
504
511
|
superProps: {
|
|
505
|
-
|
|
512
|
+
Platform: ["ios", "android", "web"],
|
|
506
513
|
category: ["electronics", "clothing", "home_garden", "collectibles", "sports", "toys", "automotive"],
|
|
507
514
|
},
|
|
508
515
|
|
|
@@ -514,6 +521,8 @@ const config = {
|
|
|
514
521
|
total_transactions: [0],
|
|
515
522
|
response_time_hours: u.weighNumRange(0, 48, 0.3),
|
|
516
523
|
store_name: ["none"],
|
|
524
|
+
Platform: ["ios", "android", "web"],
|
|
525
|
+
category: ["electronics", "clothing", "home_garden", "collectibles", "sports", "toys", "automotive"],
|
|
517
526
|
},
|
|
518
527
|
|
|
519
528
|
// ── Personas ──────────────────────────────────
|
|
@@ -721,13 +730,9 @@ const config = {
|
|
|
721
730
|
}
|
|
722
731
|
|
|
723
732
|
// ── HOOK 8: FREQUENT BUYER FUNNEL LIFT (funnel-pre) ──
|
|
724
|
-
//
|
|
733
|
+
// (conversionRate filtering moved to everything hook)
|
|
725
734
|
if (type === "funnel-pre") {
|
|
726
|
-
|
|
727
|
-
if (meta.profile.segment === "frequent_buyer") {
|
|
728
|
-
record.conversionRate = Math.min(record.conversionRate * 1.3, 90);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
735
|
+
// no-op: conversion filtering handled via event dropping in everything hook
|
|
731
736
|
}
|
|
732
737
|
|
|
733
738
|
// ── HOOK 1: FEE CHANGE IMPACT (event) ────────────────
|
|
@@ -755,11 +760,31 @@ const config = {
|
|
|
755
760
|
|
|
756
761
|
// ── EVERYTHING HOOKS ─────────────────────────────────
|
|
757
762
|
if (type === "everything") {
|
|
758
|
-
|
|
763
|
+
let events = record;
|
|
759
764
|
if (!events.length) return record;
|
|
760
765
|
|
|
761
766
|
const profile = meta.profile;
|
|
762
767
|
|
|
768
|
+
// ── SUPERPROP STAMPING ──────────────────────────
|
|
769
|
+
// Stamp superProps from profile so they are consistent per user.
|
|
770
|
+
if (profile) {
|
|
771
|
+
events.forEach(e => {
|
|
772
|
+
if (profile.Platform) e.Platform = profile.Platform;
|
|
773
|
+
if (profile.category) e.category = profile.category;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ── HOOK 8: FREQUENT BUYER CONVERSION FILTER ────
|
|
778
|
+
// Non-frequent-buyer users drop ~25% of "purchase completed"
|
|
779
|
+
// (last step of Browse to Purchase funnel) to simulate lower conversion.
|
|
780
|
+
if (profile && profile.segment !== "frequent_buyer") {
|
|
781
|
+
record = record.filter(e => {
|
|
782
|
+
if (e.event === "purchase completed" && chance.bool({ likelihood: 25 })) return false;
|
|
783
|
+
return true;
|
|
784
|
+
});
|
|
785
|
+
events = record;
|
|
786
|
+
}
|
|
787
|
+
|
|
763
788
|
// ── HOOK 3: SELLER SUCCESS → BUYER TRUST ─────────
|
|
764
789
|
// Power sellers get 2x purchase events (cloned from existing).
|
|
765
790
|
if (profile && profile.segment === "power_seller") {
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "harness-media";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "harness-media";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
|
|
13
20
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
14
21
|
|
|
@@ -218,10 +225,10 @@ const blockbusterId = `blockbuster_${v.uid(8)}`;
|
|
|
218
225
|
|
|
219
226
|
/** @type {Config} */
|
|
220
227
|
const config = {
|
|
221
|
-
token
|
|
228
|
+
token,
|
|
222
229
|
seed: SEED,
|
|
223
|
-
numDays:
|
|
224
|
-
numEvents: num_users *
|
|
230
|
+
numDays: num_days,
|
|
231
|
+
numEvents: num_users * avg_events_per_user,
|
|
225
232
|
numUsers: num_users,
|
|
226
233
|
hasAnonIds: false,
|
|
227
234
|
hasSessionIds: true,
|
|
@@ -555,6 +562,8 @@ const config = {
|
|
|
555
562
|
"total_watch_hours": u.weighNumRange(0, 500, 0.8, 50),
|
|
556
563
|
"profiles_count": u.weighNumRange(1, 5),
|
|
557
564
|
"downloads_enabled": [false, false, false, true, true],
|
|
565
|
+
"subscription_plan": ["free", "free", "standard", "standard", "standard", "premium"],
|
|
566
|
+
"device_type": ["smart_tv", "mobile", "tablet", "laptop", "desktop"],
|
|
558
567
|
},
|
|
559
568
|
|
|
560
569
|
lookupTables: [],
|
|
@@ -564,22 +573,22 @@ const config = {
|
|
|
564
573
|
*
|
|
565
574
|
* This hook function creates 8 deliberate patterns in the data:
|
|
566
575
|
*
|
|
567
|
-
* 1. GENRE FUNNEL CONVERSION: Comedy/Animation
|
|
576
|
+
* 1. GENRE FUNNEL CONVERSION: Comedy/Animation props in funnel-pre; documentary completion drop in everything
|
|
568
577
|
* 2. BINGE-WATCHING: Users with 3+ consecutive completions get extra episodes (everything)
|
|
569
578
|
* 3. WEEKEND vs WEEKDAY: Weekend sessions 1.5x longer; weekday prime-time tagging (event)
|
|
570
579
|
* 4. AD FATIGUE CHURN: Free-tier users with 10+ ads churn after day 45 (everything)
|
|
571
580
|
* 5. NEW RELEASE SPIKE: Blockbuster release on day 50 drives content selection (event)
|
|
572
581
|
* 6. KIDS PROFILE SAFETY: Kids mode restricts genres and drops ads (event)
|
|
573
|
-
* 7. RECOMMENDATION ENGINE IMPROVEMENT:
|
|
582
|
+
* 7. RECOMMENDATION ENGINE IMPROVEMENT: Props in funnel-pre; pre-day-60 content rated drop in everything
|
|
574
583
|
* 8. SUBTITLE USERS WATCH MORE: Subtitle-enabled users have higher completion rates (everything)
|
|
575
584
|
*/
|
|
576
585
|
hook: function (record, type, meta) {
|
|
577
586
|
const NOW = dayjs();
|
|
578
|
-
const DATASET_START = NOW.subtract(
|
|
587
|
+
const DATASET_START = NOW.subtract(num_days, 'days');
|
|
579
588
|
// FIXED_START is the pre-shift time range start; needed because
|
|
580
589
|
// meta.firstEventTime in funnel-pre is in the FIXED (pre-shift) timeframe
|
|
581
590
|
const FIXED_NOW_UNIX = dayjs('2024-02-02').unix();
|
|
582
|
-
const FIXED_START = dayjs.unix(FIXED_NOW_UNIX).subtract(
|
|
591
|
+
const FIXED_START = dayjs.unix(FIXED_NOW_UNIX).subtract(num_days, 'days');
|
|
583
592
|
|
|
584
593
|
// ─────────────────────────────────────────────────────────────
|
|
585
594
|
// Hook #1: GENRE FUNNEL CONVERSION (funnel-pre)
|
|
@@ -590,21 +599,17 @@ const config = {
|
|
|
590
599
|
const genre = record.props.genre;
|
|
591
600
|
|
|
592
601
|
if (genre === "comedy" || genre === "animation") {
|
|
593
|
-
record.conversionRate = record.conversionRate * 1.3;
|
|
594
602
|
record.props.genre_boost = true;
|
|
595
603
|
record.props.genre_penalty = false;
|
|
596
604
|
} else if (genre === "documentary") {
|
|
597
|
-
record.conversionRate = record.conversionRate * 0.7;
|
|
598
605
|
record.props.genre_boost = false;
|
|
599
606
|
record.props.genre_penalty = true;
|
|
600
607
|
} else if (!genre && chance.bool({ likelihood: 25 })) {
|
|
601
608
|
// Randomly apply genre effects when genre isn't in funnel props
|
|
602
609
|
if (chance.bool({ likelihood: 60 })) {
|
|
603
|
-
record.conversionRate = record.conversionRate * 1.3;
|
|
604
610
|
record.props.genre_boost = true;
|
|
605
611
|
record.props.genre_penalty = false;
|
|
606
612
|
} else {
|
|
607
|
-
record.conversionRate = record.conversionRate * 0.7;
|
|
608
613
|
record.props.genre_boost = false;
|
|
609
614
|
record.props.genre_penalty = true;
|
|
610
615
|
}
|
|
@@ -623,7 +628,6 @@ const config = {
|
|
|
623
628
|
const isAfterImprovement = funnelTime && funnelTime.isAfter(FIXED_START.add(60, 'days'));
|
|
624
629
|
|
|
625
630
|
if (isEngagementFunnel && isAfterImprovement) {
|
|
626
|
-
record.conversionRate = record.conversionRate * 1.5;
|
|
627
631
|
record.props.improved_recs = true;
|
|
628
632
|
} else {
|
|
629
633
|
record.props.improved_recs = false;
|
|
@@ -711,6 +715,43 @@ const config = {
|
|
|
711
715
|
const userEvents = record;
|
|
712
716
|
if (!userEvents || userEvents.length === 0) return record;
|
|
713
717
|
|
|
718
|
+
const profile = meta.profile;
|
|
719
|
+
|
|
720
|
+
// ─── Stamp superProps from profile (consistent per user) ───
|
|
721
|
+
const stampPlan = profile && profile.subscription_plan ? profile.subscription_plan : undefined;
|
|
722
|
+
const stampDevice = profile && profile.device_type ? profile.device_type : undefined;
|
|
723
|
+
if (stampPlan || stampDevice) {
|
|
724
|
+
userEvents.forEach(e => {
|
|
725
|
+
if (stampPlan) e.subscription_plan = stampPlan;
|
|
726
|
+
if (stampDevice) e.device_type = stampDevice;
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ─── Bug 2 fix: Genre funnel conversion filtering ───
|
|
731
|
+
// Drop ~25% of "playback completed" events for documentary genre
|
|
732
|
+
// to simulate lower documentary funnel completion (was conversionRate * 0.7)
|
|
733
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
734
|
+
const evt = userEvents[i];
|
|
735
|
+
if (evt.event === "playback completed" && evt.genre_penalty === true) {
|
|
736
|
+
if (chance.bool({ likelihood: 25 })) {
|
|
737
|
+
userEvents.splice(i, 1);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ─── Bug 2 fix: Recommendation engine improvement filtering ───
|
|
743
|
+
// Drop ~30% of "content rated" events that occur BEFORE day 60
|
|
744
|
+
// to simulate lower pre-improvement rec engine conversion (was conversionRate * 1.5)
|
|
745
|
+
const IMPROVEMENT_DATE = DATASET_START.add(60, 'days');
|
|
746
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
747
|
+
const evt = userEvents[i];
|
|
748
|
+
if (evt.event === "content rated" && dayjs(evt.time).isBefore(IMPROVEMENT_DATE)) {
|
|
749
|
+
if (chance.bool({ likelihood: 30 })) {
|
|
750
|
+
userEvents.splice(i, 1);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
714
755
|
const firstEventTime = dayjs(userEvents[0].time);
|
|
715
756
|
|
|
716
757
|
// ─── First pass: identify user patterns ───
|