@ak--47/dungeon-master 1.4.5 → 1.5.1

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 (78) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
@@ -1,40 +1,24 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "dm4-array-of-object-lookup";
3
- const num_days = 60;
4
- const num_users = 1_000;
5
- const avg_events_per_user_per_day = 1.67;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
11
- import Chance from 'chance';
12
- let chance = new Chance();
1
+ // ── IMPORTS ──
2
+ import Chance from "chance";
13
3
  import dayjs from "dayjs";
14
4
  import utc from "dayjs/plugin/utc.js";
15
5
  dayjs.extend(utc);
16
- import { uid, comma } from 'ak-tools';
17
- import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
18
-
6
+ import { weighNumRange, integer, weighChoices } from "../../lib/utils/utils.js";
19
7
  /** @typedef {import("../../types").Dungeon} Config */
20
- const videoCategories = ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"];
21
- const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
22
8
 
23
- /**
24
- * ═══════════════════════════════════════════════════════════════
25
- * DATASET OVERVIEW
26
- * ═══════════════════════════════════════════════════════════════
27
- *
28
- * Array-of-Object Lookup Test tests nested product arrays in events.
29
- * - 1,000 users over 60 days, ~100K events
30
- * - Events: checkout (cart array), add to cart, view/save item (single item)
31
- * - Lookup table: 1,000 products with price, category, descriptor
32
- * - Tests Mixpanel's array-of-objects property handling
33
- *
34
- * ═══════════════════════════════════════════════════════════════
35
- * ANALYTICS HOOKS (3 patterns)
36
- * ═══════════════════════════════════════════════════════════════
37
- *
9
+ // ── OVERVIEW ──
10
+ /*
11
+ * NAME: array-of-object-lookup
12
+ * PURPOSE: Exercises nested product arrays in events plus a lookup table
13
+ * keyed by product_id with rich attributes.
14
+ * SCALE: 1,000 users, ~100K events, 60 days
15
+ * EVENTS (4): checkout (cart array), add to cart, view item, save item (single item)
16
+ * FUNNELS (0): none
17
+ * LOOKUPS (1): product_id (1,000 entries; amount, quantity, featured, category, descriptor)
18
+ */
19
+
20
+ // ── HOOK STORIES ──
21
+ /*
38
22
  * 1. COUPON DISCOUNT TAGGING (event hook)
39
23
  * Checkout events with coupons get discount_applied: true and
40
24
  * discount_percent extracted from the coupon string.
@@ -48,27 +32,124 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
48
32
  * events tagged user_segment: "window_shopper".
49
33
  */
50
34
 
51
- /** @type {import('../types.js').Dungeon} */
35
+ // ── SCALE ──
36
+ const SEED = "dm4-array-of-object-lookup";
37
+ const NUM_DAYS = 60;
38
+ const NUM_USERS = 1_000;
39
+ const EVENTS_PER_DAY = 1.67;
40
+ const token = process.env.MP_TOKEN || "";
41
+
42
+ const chance = new Chance();
43
+
44
+ // ── DATA ARRAYS ──
45
+ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
46
+
47
+ // ── HELPER FUNCTIONS ──
48
+ function makeProducts(maxItems = 5) {
49
+ return function () {
50
+ const categories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
51
+ const descriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
52
+ const suffix = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
53
+ const assetPreview = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
54
+ const data = [];
55
+ const numOfItems = integer(1, maxItems);
56
+
57
+ for (var i = 0; i < numOfItems; i++) {
58
+ const category = chance.pickone(categories);
59
+ const descriptor = chance.pickone(descriptors);
60
+ const suffixWord = chance.pickone(suffix);
61
+ const slug = `${descriptor.replace(/\s+/g, '-').toLowerCase()}-${suffixWord.replace(/\s+/g, '-').toLowerCase()}`;
62
+ const asset = chance.pickone(assetPreview);
63
+
64
+ const price = integer(1, 100);
65
+ const quantity = integer(1, 5);
66
+ const product_id = integer(1, 1_000);
67
+
68
+ const item = {
69
+ product_id: product_id,
70
+ product_url: `https://example.com/assets/${product_id}`,
71
+ };
72
+
73
+ data.push(item);
74
+ }
75
+
76
+ return () => [data];
77
+ };
78
+ }
79
+
80
+ function flip(likelihood = 50) {
81
+ return chance.bool({ likelihood });
82
+ }
83
+
84
+ function handleEventHooks(record) {
85
+ // Pattern 1: Checkouts with coupons get a discount_applied flag and adjusted total
86
+ if (record.event === "checkout" && record.coupon && record.coupon !== "none") {
87
+ record.discount_applied = true;
88
+ const pctMatch = record.coupon.match(/(\d+)%/);
89
+ if (pctMatch) {
90
+ record.discount_percent = parseInt(pctMatch[1]);
91
+ }
92
+ }
93
+
94
+ // Pattern 2: "save item" events on weekends are tagged as wishlist behavior
95
+ if (record.event === "save item") {
96
+ const dow = dayjs(record.time).day();
97
+ if (dow === 0 || dow === 6) {
98
+ record.save_context = "weekend_browse";
99
+ } else {
100
+ record.save_context = "weekday_intent";
101
+ }
102
+ }
103
+
104
+ return record;
105
+ }
106
+
107
+ function handleEverythingHooks(record, meta) {
108
+ const profile = meta.profile;
109
+ record.forEach(e => {
110
+ e.theme = profile.theme;
111
+ });
112
+
113
+ // Pattern 3: Users who view 5+ items but never checkout are tagged as window shoppers
114
+ const views = record.filter(e => e.event === "view item").length;
115
+ const checkouts = record.filter(e => e.event === "checkout").length;
116
+ if (views >= 5 && checkouts === 0) {
117
+ for (const e of record) {
118
+ e.user_segment = "window_shopper";
119
+ }
120
+ }
121
+
122
+ return record;
123
+ }
124
+
125
+ // ── CONFIG ──
126
+ /** @type {Config} */
52
127
  const config = {
53
- token,
54
128
  seed: SEED,
55
129
  name: "array-of-object-lookup",
56
- numDays: num_days,
57
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
58
- numUsers: num_users,
59
- format: 'json', //csv or json
60
- region: "US",
61
- hasAnonIds: true, //if true, anonymousIds are created for each user
62
- hasSessionIds: true, //if true, hasSessionIds are created for each user
63
- hasAdSpend: false,
64
- hasLocation: true,
65
- hasAndroidDevices: false,
66
- hasIOSDevices: false,
67
- hasDesktopDevices: true,
68
- hasBrowser: true,
69
- hasCampaigns: false,
70
- isAnonymous: false,
71
- alsoInferFunnels: true,
130
+ numDays: NUM_DAYS,
131
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
132
+ numUsers: NUM_USERS,
133
+ format: 'json',
134
+ credentials: {
135
+ token,
136
+ region: "US",
137
+ },
138
+ switches: {
139
+ hasSessionIds: true,
140
+ hasAdSpend: false,
141
+ hasLocation: true,
142
+ hasAndroidDevices: false,
143
+ hasIOSDevices: false,
144
+ hasDesktopDevices: true,
145
+ hasBrowser: true,
146
+ hasCampaigns: false,
147
+ isAnonymous: false,
148
+ alsoInferFunnels: true,
149
+ },
150
+ identity: {
151
+ avgDevicePerUser: 1,
152
+ },
72
153
  concurrency: 1,
73
154
  writeToDisk: false,
74
155
  events: [
@@ -108,28 +189,18 @@ const config = {
108
189
  save_context: ["weekday_intent"],
109
190
  user_segment: ["regular"],
110
191
  }
111
- }
192
+ }
112
193
  ],
113
- funnels: [ ],
194
+ funnels: [],
114
195
  superProps: {
115
196
  theme: ["light", "dark", "custom", "light", "dark"],
116
197
  },
117
- /*
118
- user properties work the same as event properties
119
- each key should be an array or function reference
120
- */
121
198
  userProps: {
122
199
  theme: ["light", "dark", "custom", "light", "dark"],
123
- // title: chance.profession.bind(chance),
124
- // luckyNumber: weighNumRange(1, 500, .3),
125
200
  spiritAnimal: spiritAnimals
126
201
  },
127
202
  scdProps: {},
128
203
  mirrorProps: {},
129
- /*
130
- for group analytics keys, we need an array of arrays [[],[],[]]
131
- each pair represents a group_key and the number of profiles for that key
132
- */
133
204
  groupKeys: [],
134
205
  groupProps: {},
135
206
  lookupTables: [{
@@ -141,101 +212,13 @@ const config = {
141
212
  featured: flip,
142
213
  category: ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"],
143
214
  descriptor: ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"]
144
-
145
215
  }
146
-
147
216
  }],
148
217
  hook: function (record, type, meta) {
149
-
150
- if (type === "event") {
151
- // Pattern 1: Checkouts with coupons get a discount_applied flag and adjusted total
152
- if (record.event === "checkout" && record.coupon && record.coupon !== "none") {
153
- record.discount_applied = true;
154
- const pctMatch = record.coupon.match(/(\d+)%/);
155
- if (pctMatch) {
156
- record.discount_percent = parseInt(pctMatch[1]);
157
- }
158
- }
159
-
160
- // Pattern 2: "save item" events on weekends are tagged as wishlist behavior
161
- if (record.event === "save item") {
162
- const dow = dayjs(record.time).day();
163
- if (dow === 0 || dow === 6) {
164
- record.save_context = "weekend_browse";
165
- } else {
166
- record.save_context = "weekday_intent";
167
- }
168
- }
169
- }
170
-
171
- if (type === "everything") {
172
- const profile = meta.profile;
173
- record.forEach(e => {
174
- e.theme = profile.theme;
175
- });
176
-
177
- // Pattern 3: Users who view 5+ items but never checkout are tagged as window shoppers
178
- const views = record.filter(e => e.event === "view item").length;
179
- const checkouts = record.filter(e => e.event === "checkout").length;
180
- if (views >= 5 && checkouts === 0) {
181
- for (const e of record) {
182
- e.user_segment = "window_shopper";
183
- }
184
- }
185
- }
186
-
218
+ if (type === "event") return handleEventHooks(record);
219
+ if (type === "everything") return handleEverythingHooks(record, meta);
187
220
  return record;
188
221
  }
189
222
  };
190
223
 
191
- function makeProducts(maxItems = 5) {
192
- return function () {
193
- const categories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
194
- const descriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
195
- const suffix = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
196
- const assetPreview = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
197
- const data = [];
198
- const numOfItems = integer(1, maxItems);
199
-
200
- for (var i = 0; i < numOfItems; i++) {
201
- const category = chance.pickone(categories);
202
- const descriptor = chance.pickone(descriptors);
203
- const suffixWord = chance.pickone(suffix);
204
- const slug = `${descriptor.replace(/\s+/g, '-').toLowerCase()}-${suffixWord.replace(/\s+/g, '-').toLowerCase()}`;
205
- const asset = chance.pickone(assetPreview);
206
-
207
- // const product_id = chance.guid();
208
- const price = integer(1, 100);
209
- const quantity = integer(1, 5);
210
- const product_id = integer(1, 1_000);
211
-
212
- const item = {
213
- product_id: product_id,
214
- product_url: `https://example.com/assets/${product_id}`,
215
- // sku: integer(11111, 99999),
216
- // amount: price,
217
- // quantity: quantity,
218
- // total_value: price * quantity,
219
- // featured: chance.pickone([true, false, false]),
220
- // category: category,
221
- // descriptor: descriptor,
222
- // slug: slug,
223
-
224
- // assetType: asset
225
-
226
- };
227
-
228
- data.push(item);
229
- }
230
-
231
- return () => [data];
232
- };
233
- };
234
-
235
-
236
- function flip(likelihood = 50) {
237
- return chance.bool({ likelihood });
238
- }
239
-
240
-
241
- export default config;
224
+ export default config;
@@ -0,0 +1,87 @@
1
+ // ── IMPORTS ──
2
+ import * as u from "../../lib/utils/utils.js";
3
+ /** @typedef {import("../../types").Dungeon} Config */
4
+
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: datagen-v1.5-verify
8
+ * PURPOSE: Canonical v1.5 fixture — active-days, conversion window, touchpoint cap, auto-sort, determinism
9
+ * SCALE: 500 users, ~60K events, 30 days
10
+ * EVENTS (4): page view, click, sign up, purchase
11
+ * FUNNELS (1): page view → sign up → purchase (50%, 14d window)
12
+ *
13
+ * Used by:
14
+ * - tests/active-days.test.js → asserts distinct-day distribution shape
15
+ * - tests/conversion-window.test.js → exercises Funnel.conversionWindowDays
16
+ * - tests/touchpoint-cap.test.js → exercises maxTouchpointsPerUser
17
+ * - tests/datagen-determinism.test.js → byte-equal verification across runs
18
+ * - tests/auto-sort.test.js → relies on sorted output
19
+ *
20
+ * Realistic small-scale config with `hasCampaigns: true` + an explicit
21
+ * `isAttributionEvent` flagged event so the touchpoint cap has eligible
22
+ * candidates beyond the default ~25% legacy fallback.
23
+ *
24
+ * **DO NOT enable `engagementDecay`** — it interacts with `avgActiveDaysPerUser`
25
+ * by dropping events on late picked days, eroding the effective active-day count
26
+ * below the configured target. See HOOKS.md §2.5.
27
+ */
28
+
29
+ // ── SCALE ──
30
+ const SEED = "datagen-v1.5-verify";
31
+ u.initChance(SEED);
32
+
33
+ // ── CONFIG ──
34
+ /** @type {Config} */
35
+ const config = {
36
+ seed: SEED,
37
+ // Pin the dataset window for full determinism (independent of run date).
38
+ datasetStart: "2025-09-01T00:00:00Z",
39
+ datasetEnd: "2025-10-01T00:00:00Z",
40
+ numUsers: 500,
41
+ avgEventsPerUserPerDay: 4,
42
+ // v1.5 distinct-day primitive — concentrate events onto ~6 days/user.
43
+ avgActiveDaysPerUser: 6,
44
+ // v1.5 attribution cap — explicit (default is also 10).
45
+ maxTouchpointsPerUser: 10,
46
+ switches: {
47
+ hasCampaigns: true,
48
+ hasSessionIds: true,
49
+ },
50
+ identity: {
51
+ avgDevicePerUser: 1,
52
+ },
53
+ autoSortAfterEverything: true,
54
+ events: [
55
+ { event: "page view", weight: 5, isAttributionEvent: true },
56
+ { event: "click", weight: 3 },
57
+ {
58
+ event: "sign up",
59
+ isFirstEvent: true,
60
+ isAuthEvent: true,
61
+ isAttributionEvent: true,
62
+ properties: { method: ["email", "google", "apple"] },
63
+ },
64
+ {
65
+ event: "purchase",
66
+ weight: 1,
67
+ properties: { amount: u.weighNumRange(10, 200) },
68
+ },
69
+ ],
70
+ funnels: [
71
+ {
72
+ sequence: ["page view", "sign up", "purchase"],
73
+ isFirstFunnel: true,
74
+ conversionRate: 50,
75
+ timeToConvert: 4, // hours
76
+ // v1.5 explicit conversion window — well under the 30d default.
77
+ conversionWindowDays: 14,
78
+ order: "sequential",
79
+ },
80
+ ],
81
+ superProps: { Plan: ["Free", "Pro"] },
82
+ userProps: { Plan: ["Free", "Pro"] },
83
+ writeToDisk: false,
84
+ verbose: false,
85
+ };
86
+
87
+ export default config;
@@ -1,50 +1,52 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "experiments";
3
- const num_days = 60;
4
- const num_users = 2_000;
5
- const avg_events_per_user_per_day = 0.83;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
1
+ // ── IMPORTS ──
2
+ import Chance from "chance";
3
+ import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
4
+ /** @typedef {import("../../types").Dungeon} Config */
10
5
 
11
- /**
12
- * Experiments — tests funnel experiments with A/B/C variants.
13
- *
14
- * Exercises: experiment: true for variant generation, bindPropsIndex
15
- * for property binding, and multiple funnel ordering modes
16
- * (sequential, random, first-and-last-fixed).
17
- *
18
- * - 2000 users, 100K events, 60 days
19
- * - 7 events, 4 funnels with different experiment/order configs
20
- * - No hooks
6
+ // ── OVERVIEW ──
7
+ /*
8
+ * NAME: experiments
9
+ * PURPOSE: Exercises funnel experiments (A/B/C variants), bindPropsIndex,
10
+ * and multiple funnel ordering modes (sequential, first-and-last-fixed, random).
11
+ * SCALE: 2,000 users, ~100K events, 60 days
12
+ * EVENTS (7): page view, signup, feature viewed, action taken, checkout, button click, help viewed
13
+ * FUNNELS (4): Onboarding Flow (sequential, experiment), Purchase Funnel (first-and-last-fixed, experiment),
14
+ * Feature Adoption (random, experiment, bindPropsIndex), Quick Signup (sequential, control)
21
15
  */
22
16
 
23
- import Chance from 'chance';
24
- let chance = new Chance();
25
- import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
17
+ // ── SCALE ──
18
+ const SEED = "experiments";
19
+ const NUM_DAYS = 60;
20
+ const NUM_USERS = 2_000;
21
+ const EVENTS_PER_DAY = 0.83;
22
+ const token = process.env.MP_TOKEN || "";
26
23
 
27
- /** @typedef {import("../../types").Dungeon} Config */
28
- /** @type {import('../../types').Dungeon} */
24
+ const chance = new Chance();
25
+
26
+ // ── CONFIG ──
27
+ /** @type {Config} */
29
28
  const config = {
30
- token,
31
29
  seed: SEED,
32
- numDays: num_days,
33
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
34
- numUsers: num_users,
30
+ numDays: NUM_DAYS,
31
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
32
+ numUsers: NUM_USERS,
35
33
  format: "json",
36
- region: "US",
37
- hasAnonIds: false,
38
- hasSessionIds: true,
39
- hasAdSpend: false,
40
- hasLocation: false,
41
- hasAndroidDevices: true,
42
- hasIOSDevices: true,
43
- hasDesktopDevices: true,
44
- hasBrowser: true,
45
- hasCampaigns: false,
46
- isAnonymous: false,
47
- alsoInferFunnels: false,
34
+ credentials: {
35
+ token,
36
+ region: "US",
37
+ },
38
+ switches: {
39
+ hasSessionIds: true,
40
+ hasAdSpend: false,
41
+ hasLocation: false,
42
+ hasAndroidDevices: true,
43
+ hasIOSDevices: true,
44
+ hasDesktopDevices: true,
45
+ hasBrowser: true,
46
+ hasCampaigns: false,
47
+ isAnonymous: false,
48
+ alsoInferFunnels: false,
49
+ },
48
50
  concurrency: 1,
49
51
  writeToDisk: false,
50
52