@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.
Files changed (54) hide show
  1. package/dungeons/technical/ad-spend.js +15 -4
  2. package/dungeons/technical/anonymous-users.js +15 -5
  3. package/dungeons/technical/array-of-object-lookup.js +21 -5
  4. package/dungeons/technical/experiments.js +23 -5
  5. package/dungeons/technical/foobar.js +28 -12
  6. package/dungeons/technical/group-analytics.js +15 -5
  7. package/dungeons/technical/mirror-strategies.js +15 -5
  8. package/dungeons/technical/nested-objects.js +15 -4
  9. package/dungeons/technical/retention-cadence.js +13 -7
  10. package/dungeons/technical/sanity.js +25 -6
  11. package/dungeons/technical/scale-test.js +15 -4
  12. package/dungeons/technical/scd.js +24 -5
  13. package/dungeons/technical/simple.js +20 -5
  14. package/dungeons/technical/simplest.js +23 -6
  15. package/dungeons/technical/text-generation.js +22 -6
  16. package/dungeons/user/.gitkeep +0 -0
  17. package/dungeons/vertical/community-schema.json +1 -1
  18. package/dungeons/vertical/community.js +49 -28
  19. package/dungeons/vertical/devtools-schema.json +1 -1
  20. package/dungeons/vertical/devtools.js +50 -29
  21. package/dungeons/vertical/ecommerce.js +23 -6
  22. package/dungeons/vertical/education-schema.json +1 -1
  23. package/dungeons/vertical/education.js +29 -19
  24. package/dungeons/vertical/fintech-schema.json +1 -1
  25. package/dungeons/vertical/fintech.js +27 -8
  26. package/dungeons/vertical/fitness-schema.json +1 -1
  27. package/dungeons/vertical/fitness.js +48 -22
  28. package/dungeons/vertical/food-delivery-schema.json +1 -1
  29. package/dungeons/vertical/food-delivery.js +51 -12
  30. package/dungeons/vertical/gaming-schema.json +1 -1
  31. package/dungeons/vertical/gaming.js +41 -7
  32. package/dungeons/vertical/healthcare-schema.json +1 -1
  33. package/dungeons/vertical/healthcare.js +69 -40
  34. package/dungeons/vertical/insurance-application-schema.json +1 -1
  35. package/dungeons/vertical/insurance-application.js +26 -8
  36. package/dungeons/vertical/logistics-schema.json +1 -1
  37. package/dungeons/vertical/logistics.js +69 -41
  38. package/dungeons/vertical/marketplace-schema.json +1 -1
  39. package/dungeons/vertical/marketplace.js +44 -19
  40. package/dungeons/vertical/media.js +56 -15
  41. package/dungeons/vertical/rpg-schema.json +1 -1
  42. package/dungeons/vertical/rpg.js +38 -8
  43. package/dungeons/vertical/sass.js +24 -7
  44. package/dungeons/vertical/social.js +23 -7
  45. package/dungeons/vertical/travel-schema.json +1 -1
  46. package/dungeons/vertical/travel.js +50 -19
  47. package/index.js +3 -0
  48. package/lib/core/config-validator.js +13 -2
  49. package/lib/generators/events.js +3 -5
  50. package/lib/orchestrators/mixpanel-sender.js +64 -3
  51. package/lib/orchestrators/user-loop.js +20 -0
  52. package/lib/utils/utils.js +129 -9
  53. package/package.json +2 -2
  54. package/types.d.ts +5 -3
@@ -1,14 +1,21 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "SUPER DUPER DANGEROUS BROOO";
3
+ const num_days = 92;
4
+ const num_users = 8_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 { weighNumRange, range, date, initChance, exhaust, choose, integer } from "../../lib/utils/utils.js";
5
15
  import { createTextGenerator, generateBatch } from "../../lib/generators/text.js";
6
16
 
7
- const SEED = "SUPER DUPER DANGEROUS BROOO";
8
17
  dayjs.extend(utc);
9
18
  const chance = initChance(SEED);
10
- const num_users = 8_000;
11
- const days = 92;
12
19
 
13
20
  /** @typedef {import("../../types").Dungeon} Dungeon */
14
21
 
@@ -398,9 +405,9 @@ const webinarChatGen = createTextGenerator({
398
405
  /** @type {Dungeon} */
399
406
  const dungeon = {
400
407
  seed: SEED,
401
- token: "",
402
- numDays: days,
403
- numEvents: num_users * 120, // Increased for more variety
408
+ token,
409
+ numDays: num_days,
410
+ numEvents: num_users * avg_events_per_user,
404
411
  numUsers: num_users,
405
412
  hasAnonIds: false,
406
413
  hasSessionIds: true, // Enable for chat flow tracking
@@ -770,6 +777,8 @@ const dungeon = {
770
777
  preferred_communication: ["email", "chat", "phone", "self_service"],
771
778
  is_power_user: [false],
772
779
  risk_level: ["healthy"],
780
+ product_tier: ["free", "basic", "pro", "enterprise"],
781
+ data_center: ["us-east", "us-west", "eu-central", "asia-pacific"],
773
782
  },
774
783
 
775
784
  // ============= Slowly Changing Dimensions =============
@@ -804,6 +813,13 @@ const dungeon = {
804
813
 
805
814
  // --- everything hook: enterprise users get a satisfaction survey event ---
806
815
  if (type === "everything" && meta && meta.profile) {
816
+ // stamp superProps from profile for consistency (skip feature_flags — intentionally per-event)
817
+ const profile = meta.profile;
818
+ record.forEach(e => {
819
+ e.product_tier = profile.product_tier;
820
+ e.data_center = profile.data_center;
821
+ });
822
+
807
823
  if (meta.profile.user_tier === "enterprise" && record.length > 5) {
808
824
  const lastEvent = record[record.length - 1];
809
825
  record.push({
File without changes
@@ -520,7 +520,7 @@
520
520
  "supporter",
521
521
  "pro"
522
522
  ],
523
- "platform": [
523
+ "Platform": [
524
524
  "ios",
525
525
  "android",
526
526
  "web"
@@ -1,16 +1,23 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "dm4-community";
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-community";
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(days, "days");
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 communityIds = v.range(1, 30).map(() => `COMM_${v.uid(4)}`);
213
220
  * articles and high community reputation scores.
214
221
  *
215
222
  * -------------------------------------------------------------------
216
- * 8. PRO SUBSCRIBER CONTENT CREATION LIFT (funnel-pre hook)
223
+ * 8. PRO SUBSCRIBER CONTENT CREATION LIFT (everything hook)
217
224
  * -------------------------------------------------------------------
218
225
  *
219
- * PATTERN: Users with subscription_tier "pro" get 1.5x conversion
220
- * rate on the Content Creation funnel. Pro users have analytics and
221
- * tools that help them publish more effectively.
226
+ * PATTERN: Free-tier users drop 35% of final funnel step events
227
+ * ("comment posted"), creating a visible conversion gap between
228
+ * paid and free users. Pro/supporter users keep all their events.
222
229
  *
223
230
  * HOW TO FIND IT IN MIXPANEL:
224
231
  *
@@ -244,15 +251,15 @@ const communityIds = v.range(1, 30).map(() => `COMM_${v.uid(4)}`);
244
251
  * Edit War Detection | edit_quality | 3.0 | 1.5 | 0.5x
245
252
  * Lurker Churn | events after day 10 | 100% | 40% | 0.4x
246
253
  * Creator Profiles | reputation_score | 25 | 90 | 3.6x
247
- * Pro Content Creation Lift | funnel conversion | 35% | 52% | 1.5x
254
+ * Pro Content Creation Lift | funnel conversion | 35% | 52% | ~1.5x
248
255
  */
249
256
 
250
257
  /** @type {Config} */
251
258
  const config = {
252
- token: "",
259
+ token,
253
260
  seed: SEED,
254
- numDays: days,
255
- numEvents: num_users * 120,
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,
@@ -504,7 +511,7 @@ const config = {
504
511
  // -- SuperProps ----------------------------------------------------
505
512
  superProps: {
506
513
  subscription_tier: ["free", "free", "free", "free", "supporter", "pro"],
507
- platform: ["ios", "android", "web"],
514
+ Platform: ["ios", "android", "web"],
508
515
  content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
509
516
  },
510
517
 
@@ -515,6 +522,9 @@ const config = {
515
522
  articles_created: [0],
516
523
  reputation_score: u.weighNumRange(0, 100, 0.3, 25),
517
524
  preferred_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
525
+ subscription_tier: ["free", "free", "free", "free", "supporter", "pro"],
526
+ Platform: ["ios", "android", "web"],
527
+ content_hub: ["gaming", "anime", "movies", "tv", "comics", "music"],
518
528
  },
519
529
 
520
530
  // -- Phase 2: Personas --------------------------------------------
@@ -685,18 +695,9 @@ const config = {
685
695
  }
686
696
  }
687
697
 
688
- // -- HOOK 8: PRO SUBSCRIBER CONTENT CREATION LIFT (funnel-pre)
689
- // Pro subscribers convert 1.5x better through content creation.
690
- if (type === "funnel-pre") {
691
- if (meta && meta.profile) {
692
- const tier = meta.profile.subscription_tier;
693
- if (tier === "pro") {
694
- record.conversionRate = Math.min(record.conversionRate * 1.5, 95);
695
- } else if (tier === "supporter") {
696
- record.conversionRate = Math.min(record.conversionRate * 1.2, 90);
697
- }
698
- }
699
- }
698
+ // -- HOOK 8: PRO SUBSCRIBER CONTENT CREATION LIFT
699
+ // Conversion differences handled in everything hook via event filtering.
700
+ // (funnel-pre conversionRate modifications are diluted by organic events)
700
701
 
701
702
  // -- HOOK 1: WEEKEND CONTENT SURGE (event) --------------------
702
703
  // Articles published on weekends get 1.5x word_count.
@@ -725,8 +726,28 @@ const config = {
725
726
 
726
727
  // -- EVERYTHING HOOKS -----------------------------------------
727
728
  if (type === "everything") {
728
- const events = record;
729
+ let events = record;
729
730
  if (!events.length) return record;
731
+ const profile = meta && meta.profile ? meta.profile : {};
732
+
733
+ // -- SUPERPROP STAMPING -----------------------------------
734
+ // Stamp superProp values from profile onto every event so
735
+ // they stay consistent per-user instead of randomizing per-event.
736
+ events.forEach(e => {
737
+ if (profile.subscription_tier) e.subscription_tier = profile.subscription_tier;
738
+ if (profile.Platform) e.Platform = profile.Platform;
739
+ if (profile.content_hub) e.content_hub = profile.content_hub;
740
+ });
741
+
742
+ // -- HOOK 8: PRO SUBSCRIBER CONTENT CREATION LIFT ---------
743
+ // Free-tier users drop 35% of final funnel step events to
744
+ // create visible conversion gap vs paid subscribers.
745
+ if (profile.subscription_tier !== "pro" && profile.subscription_tier !== "supporter") {
746
+ events = events.filter(e => {
747
+ if (e.event === "comment posted" && chance.bool({ likelihood: 35 })) return false;
748
+ return true;
749
+ });
750
+ }
730
751
 
731
752
  // -- HOOK 3: POWER CREATOR ENGAGEMENT LIFT ----------------
732
753
  // Users with >20 article_published events get 3x upvote_count.
@@ -745,7 +766,7 @@ const config = {
745
766
 
746
767
  // -- HOOK 4: DISCUSSION DEPTH BY CONTRIBUTOR TYPE ---------
747
768
  // Active contributors get cloned comment_posted events.
748
- if (meta && meta.profile && meta.profile.segment === "active_contributor") {
769
+ if (profile.segment === "active_contributor") {
749
770
  const templateComment = events.find(e => e.event === "comment posted");
750
771
  if (templateComment) {
751
772
  const existingComments = events.filter(e => e.event === "comment posted");
@@ -784,7 +805,7 @@ const config = {
784
805
  }
785
806
  }
786
807
 
787
- return record;
808
+ return events;
788
809
  }
789
810
 
790
811
  return record;
@@ -545,7 +545,7 @@
545
545
  "business",
546
546
  "enterprise"
547
547
  ],
548
- "platform": [
548
+ "Platform": [
549
549
  "web",
550
550
  "desktop_app",
551
551
  "cli"
@@ -1,16 +1,23 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "dm4-devtools";
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-devtools";
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(days, "days");
20
+ const DATASET_START = NOW.subtract(num_days, "days");
14
21
 
15
22
  /** @typedef {import("../../types").Dungeon} Config */
16
23
 
@@ -208,12 +215,13 @@ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
208
215
  * manage organization-wide CI/CD infrastructure.
209
216
  *
210
217
  * ---------------------------------------------------------------
211
- * 8. ENTERPRISE BUILD-DEPLOY FUNNEL LIFT (funnel-pre hook)
218
+ * 8. ENTERPRISE BUILD-DEPLOY FUNNEL LIFT (everything hook)
212
219
  * ---------------------------------------------------------------
213
220
  *
214
- * PATTERN: Users with subscription_tier "enterprise" get 1.5x
215
- * conversion rate on the build-deploy pipeline funnel. Enterprise
216
- * customers have dedicated support and optimized pipelines.
221
+ * PATTERN: Free-tier users drop 35% of final funnel step events
222
+ * ("monitoring dashboard viewed"), creating a visible conversion
223
+ * gap between paid and free users. Enterprise/business users keep
224
+ * all their events.
217
225
  *
218
226
  * HOW TO FIND IT IN MIXPANEL:
219
227
  *
@@ -240,15 +248,15 @@ const repoIds = v.range(1, 150).map(() => `REPO_${v.uid(6)}`);
240
248
  * OSS -> Paid Conversion | team tier events | 0% | 30% | -
241
249
  * Post-Outage Recovery | deploys/day | 50 | 100+ | 2x+
242
250
  * DevOps Lead Profiles | team_size | 10 | 30 | 3x
243
- * Enterprise Funnel Lift | funnel conversion | 40% | 60% | 1.5x
251
+ * Enterprise Funnel Lift | funnel conversion | 40% | 60% | ~1.5x
244
252
  */
245
253
 
246
254
  /** @type {Config} */
247
255
  const config = {
248
- token: "",
256
+ token,
249
257
  seed: SEED,
250
- numDays: days,
251
- numEvents: num_users * 120,
258
+ numDays: num_days,
259
+ numEvents: num_users * avg_events_per_user,
252
260
  numUsers: num_users,
253
261
  hasAnonIds: false,
254
262
  hasSessionIds: true,
@@ -507,7 +515,7 @@ const config = {
507
515
  // -- SuperProps --------------------------------------------
508
516
  superProps: {
509
517
  subscription_tier: ["free", "free", "free", "team", "team", "business", "enterprise"],
510
- platform: ["web", "web", "desktop_app", "cli"],
518
+ Platform: ["web", "web", "desktop_app", "cli"],
511
519
  language: ["javascript", "python", "go", "rust", "java", "typescript"],
512
520
  },
513
521
 
@@ -519,6 +527,9 @@ const config = {
519
527
  repos_connected: [0],
520
528
  org_name: ["personal"],
521
529
  experience_level: ["junior", "junior", "mid", "mid", "mid", "senior", "senior"],
530
+ subscription_tier: ["free", "free", "free", "team", "team", "business", "enterprise"],
531
+ Platform: ["web", "web", "desktop_app", "cli"],
532
+ language: ["javascript", "python", "go", "rust", "java", "typescript"],
522
533
  },
523
534
 
524
535
  // -- Phase 2: Personas ------------------------------------
@@ -726,19 +737,9 @@ const config = {
726
737
  }
727
738
  }
728
739
 
729
- // -- HOOK 8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT (funnel-pre)
730
- // Enterprise subscribers convert 1.5x through build-deploy pipeline.
731
- // Business subscribers get a smaller lift.
732
- if (type === "funnel-pre") {
733
- if (meta && meta.profile) {
734
- const tier = meta.profile.subscription_tier;
735
- if (tier === "enterprise") {
736
- record.conversionRate = Math.min(record.conversionRate * 1.5, 95);
737
- } else if (tier === "business") {
738
- record.conversionRate = Math.min(record.conversionRate * 1.2, 90);
739
- }
740
- }
741
- }
740
+ // -- HOOK 8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT
741
+ // Conversion differences handled in everything hook via event filtering.
742
+ // (funnel-pre conversionRate modifications are diluted by organic events)
742
743
 
743
744
  // -- HOOK 1: BUILD FAILURE CASCADE (event) ----------------
744
745
  // Failed builds get 2x duration (retries take longer).
@@ -762,8 +763,28 @@ const config = {
762
763
 
763
764
  // -- EVERYTHING HOOKS -------------------------------------
764
765
  if (type === "everything") {
765
- const events = record;
766
+ let events = record;
766
767
  if (!events.length) return record;
768
+ const profile = meta && meta.profile ? meta.profile : {};
769
+
770
+ // -- SUPERPROP STAMPING -------------------------------
771
+ // Stamp superProp values from profile onto every event so
772
+ // they stay consistent per-user instead of randomizing per-event.
773
+ events.forEach(e => {
774
+ if (profile.subscription_tier) e.subscription_tier = profile.subscription_tier;
775
+ if (profile.Platform) e.Platform = profile.Platform;
776
+ if (profile.language) e.language = profile.language;
777
+ });
778
+
779
+ // -- HOOK 8: ENTERPRISE BUILD-DEPLOY FUNNEL LIFT ------
780
+ // Free-tier users drop 35% of final funnel step events to
781
+ // create visible conversion gap vs paid subscribers.
782
+ if (profile.subscription_tier !== "enterprise" && profile.subscription_tier !== "business") {
783
+ events = events.filter(e => {
784
+ if (e.event === "monitoring dashboard viewed" && chance.bool({ likelihood: 35 })) return false;
785
+ return true;
786
+ });
787
+ }
767
788
 
768
789
  // -- HOOK 3: COPILOT PR VELOCITY ----------------------
769
790
  // Users with ai_assist="copilot" on any PR get 1.5x more PRs.
@@ -805,7 +826,7 @@ const config = {
805
826
 
806
827
  // -- HOOK 5: OPEN SOURCE -> PAID CONVERSION -----------
807
828
  // OSS users with >15 events get 30% of events converted to "team" tier.
808
- if (meta && meta.profile && meta.profile.segment === "oss_user" && events.length > 15) {
829
+ if (profile.segment === "oss_user" && events.length > 15) {
809
830
  const conversionPoint = Math.floor(events.length * 0.7);
810
831
  for (let i = conversionPoint; i < events.length; i++) {
811
832
  events[i].subscription_tier = "team";
@@ -833,7 +854,7 @@ const config = {
833
854
  }
834
855
  });
835
856
 
836
- return record;
857
+ return events;
837
858
  }
838
859
 
839
860
  return record;
@@ -1,3 +1,13 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "simple is best";
3
+ const num_days = 108;
4
+ const num_users = 50_000;
5
+ const avg_events_per_user = 40;
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 Chance from 'chance';
2
12
  let chance = new Chance();
3
13
  import dayjs from "dayjs";
@@ -130,11 +140,11 @@ function makeProducts(maxItems = 5) {
130
140
 
131
141
  /** @type {import('../types.d.ts').Dungeon} */
132
142
  const config = {
133
- token: "",
134
- seed: "simple is best",
135
- numDays: 108, //how many days worth1 of data
136
- numEvents: 2_000_000, //how many events
137
- numUsers: 50_000, //how many users
143
+ token,
144
+ seed: SEED,
145
+ numDays: num_days,
146
+ numEvents: num_users * avg_events_per_user,
147
+ numUsers: num_users,
138
148
  format: 'json', //csv or json
139
149
  region: "US",
140
150
  hasAnonIds: false, //if true, anonymousIds are created for each user
@@ -305,7 +315,8 @@ const config = {
305
315
  userProps: {
306
316
  title: chance.profession.bind(chance),
307
317
  luckyNumber: weighNumRange(42, 420, .3),
308
- spiritAnimal: spiritAnimals
318
+ spiritAnimal: spiritAnimals,
319
+ theme: ["light", "dark", "custom", "light", "dark"],
309
320
  },
310
321
  scdProps: {
311
322
  loyalty_tier: {
@@ -418,6 +429,12 @@ const config = {
418
429
 
419
430
 
420
431
  if (type === "everything") {
432
+ // Stamp superProps from profile for consistency
433
+ const profile = meta.profile;
434
+ record.forEach(e => {
435
+ e.theme = profile.theme;
436
+ });
437
+
421
438
  // big themes!
422
439
 
423
440
  // users who view items in the home and garden category churn more frequently
@@ -5543,7 +5543,7 @@
5543
5543
  }
5544
5544
  ],
5545
5545
  "superProps": {
5546
- "platform": [
5546
+ "Platform": [
5547
5547
  "Web",
5548
5548
  "iOS",
5549
5549
  "Android",
@@ -1,14 +1,21 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "harness-education";
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-education";
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
 
@@ -183,10 +190,10 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
183
190
 
184
191
  /** @type {Config} */
185
192
  const config = {
186
- token: "",
193
+ token,
187
194
  seed: SEED,
188
- numDays: days,
189
- numEvents: num_users * 120,
195
+ numDays: num_days,
196
+ numEvents: num_users * avg_events_per_user,
190
197
  numUsers: num_users,
191
198
  hasAnonIds: false,
192
199
  hasSessionIds: true,
@@ -436,7 +443,7 @@ const config = {
436
443
  ],
437
444
 
438
445
  superProps: {
439
- platform: ["Web", "iOS", "Android", "iPad"],
446
+ Platform: ["Web", "iOS", "Android", "iPad"],
440
447
  },
441
448
 
442
449
  scdProps: {
@@ -466,6 +473,7 @@ const config = {
466
473
  "instructor_rating": [0],
467
474
  "learning_goal": ["none"],
468
475
  "study_hours_per_week": [0],
476
+ "Platform": ["Web", "iOS", "Android", "iPad"],
469
477
  },
470
478
 
471
479
  groupKeys: [
@@ -505,7 +513,7 @@ const config = {
505
513
  */
506
514
  hook: function (record, type, meta) {
507
515
  const NOW = dayjs();
508
- const DATASET_START = NOW.subtract(days, 'days');
516
+ const DATASET_START = NOW.subtract(num_days, 'days');
509
517
 
510
518
  // ═══════════════════════════════════════════════════════════════════
511
519
  // Hook #1: STUDENT VS INSTRUCTOR PROFILES
@@ -621,8 +629,16 @@ const config = {
621
629
  // ═══════════════════════════════════════════════════════════════════
622
630
  if (type === "everything") {
623
631
  const userEvents = record;
632
+ const profile = meta.profile;
624
633
  const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
625
634
 
635
+ // ── Stamp superProps from profile so they stay consistent per user ──
636
+ if (profile) {
637
+ userEvents.forEach((event) => {
638
+ if (profile.Platform !== undefined) event.Platform = profile.Platform;
639
+ });
640
+ }
641
+
626
642
  // ---------------------------------------------------------------
627
643
  // First pass: identify user patterns
628
644
  // ---------------------------------------------------------------
@@ -720,7 +736,7 @@ const config = {
720
736
  }
721
737
 
722
738
  // Hook #7: FREE VS PAID - reinforce the subscription effect on certificates
723
- const subStatus = meta && meta.profile ? meta.profile.subscription_status : "free";
739
+ const subStatus = profile ? profile.subscription_status : "free";
724
740
  if (subStatus === "free") {
725
741
  // Free users lose 55% of their certificates (simulating lower completion)
726
742
  for (let i = userEvents.length - 1; i >= 0; i--) {
@@ -778,17 +794,11 @@ const config = {
778
794
  // Hook #7: FREE VS PAID COURSES (funnel-pre)
779
795
  // ═══════════════════════════════════════════════════════════════════
780
796
  if (type === "funnel-pre") {
781
- // Target funnels containing course completion events
797
+ // Hook #7: FREE VS PAID — prop tagging only
798
+ // conversionRate manipulation removed; the everything hook handles
799
+ // the completion gap by dropping 55% of free-user certificates
782
800
  if (meta && meta.profile && meta.funnel) {
783
- const subscriptionStatus = meta.profile.subscription_status;
784
-
785
- if (subscriptionStatus === "free") {
786
- // Free users convert at 0.5x rate
787
- record.conversionRate = (record.conversionRate || 0.25) * 0.5;
788
- } else if (subscriptionStatus === "monthly" || subscriptionStatus === "annual") {
789
- // Paid subscribers convert at 1.5x rate
790
- record.conversionRate = (record.conversionRate || 0.25) * 1.5;
791
- }
801
+ // no-op: kept for future prop-setting if needed
792
802
  }
793
803
  }
794
804
 
@@ -504,7 +504,7 @@
504
504
  "plus",
505
505
  "premium"
506
506
  ],
507
- "platform": [
507
+ "Platform": [
508
508
  "ios",
509
509
  "android",
510
510
  "web"
@@ -1,13 +1,20 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "harness-fintech";
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 = "harness-fintech";
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
 
@@ -129,10 +136,10 @@ const days = 100;
129
136
 
130
137
  /** @type {Config} */
131
138
  const config = {
132
- token: "",
139
+ token,
133
140
  seed: SEED,
134
- numDays: days,
135
- numEvents: num_users * 120,
141
+ numDays: num_days,
142
+ numEvents: num_users * avg_events_per_user,
136
143
  numUsers: num_users,
137
144
  hasAnonIds: false,
138
145
  hasSessionIds: true,
@@ -400,11 +407,14 @@ const config = {
400
407
 
401
408
  superProps: {
402
409
  account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
403
- platform: ["ios", "android", "web"],
410
+ Platform: ["ios", "android", "web"],
404
411
  low_balance_churn: [false],
405
412
  },
406
413
 
407
414
  userProps: {
415
+ account_tier: ["basic", "basic", "basic", "plus", "plus", "premium"],
416
+ Platform: ["ios", "android", "web"],
417
+ low_balance_churn: [false],
408
418
  "credit_score_range": ["300-579", "580-669", "670-739", "740-799", "800-850"],
409
419
  "income_bracket": ["under_30k", "30k_50k", "50k_75k", "75k_100k", "100k_150k", "over_150k"],
410
420
  "account_age_months": u.weighNumRange(1, 60, 0.5, 12),
@@ -449,7 +459,7 @@ const config = {
449
459
  */
450
460
  hook: function (record, type, meta) {
451
461
  const NOW = dayjs();
452
- const DATASET_START = NOW.subtract(days, "days");
462
+ const DATASET_START = NOW.subtract(num_days, "days");
453
463
 
454
464
  // ===============================================================
455
465
  // Hook #1: PERSONAL VS BUSINESS ACCOUNTS (user)
@@ -581,6 +591,15 @@ const config = {
581
591
  // ===============================================================
582
592
  if (type === "everything") {
583
593
  const userEvents = record;
594
+
595
+ // Stamp superProps from profile for consistency
596
+ const profile = meta.profile;
597
+ userEvents.forEach(e => {
598
+ e.account_tier = profile.account_tier;
599
+ e.Platform = profile.Platform;
600
+ e.low_balance_churn = profile.low_balance_churn;
601
+ });
602
+
584
603
  const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
585
604
 
586
605
  // -----------------------------------------------------------
@@ -468,7 +468,7 @@
468
468
  }
469
469
  ],
470
470
  "superProps": {
471
- "platform": [
471
+ "Platform": [
472
472
  "ios",
473
473
  "android"
474
474
  ],