@ak--47/dungeon-master 1.5.0 → 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 (62) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/HOOKS.md +13 -0
  3. package/dungeons/technical/ad-spend.js +41 -49
  4. package/dungeons/technical/anonymous-users.js +38 -36
  5. package/dungeons/technical/array-of-object-lookup.js +136 -153
  6. package/dungeons/technical/datagen-v15-verify.js +24 -11
  7. package/dungeons/technical/experiments.js +42 -40
  8. package/dungeons/technical/foobar.js +114 -118
  9. package/dungeons/technical/group-analytics.js +42 -40
  10. package/dungeons/technical/hook-helpers-verify.js +69 -50
  11. package/dungeons/technical/identity-model-verify.js +22 -12
  12. package/dungeons/technical/mirror-strategies.js +37 -39
  13. package/dungeons/technical/nested-objects.js +119 -118
  14. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  15. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  16. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  17. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  18. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  19. package/dungeons/technical/retention-cadence.js +115 -112
  20. package/dungeons/technical/sanity.js +86 -80
  21. package/dungeons/technical/scale-test.js +34 -38
  22. package/dungeons/technical/scd.js +111 -128
  23. package/dungeons/technical/simple.js +134 -141
  24. package/dungeons/technical/simplest.js +54 -62
  25. package/dungeons/technical/text-generation.js +110 -146
  26. package/dungeons/vertical/ai-platform.js +296 -333
  27. package/dungeons/vertical/community.js +284 -255
  28. package/dungeons/vertical/crypto.js +395 -391
  29. package/dungeons/vertical/dating.js +411 -378
  30. package/dungeons/vertical/devtools.js +336 -298
  31. package/dungeons/vertical/ecommerce.js +316 -394
  32. package/dungeons/vertical/education.js +369 -325
  33. package/dungeons/vertical/fintech.js +358 -325
  34. package/dungeons/vertical/fitness.js +335 -291
  35. package/dungeons/vertical/food-delivery.js +343 -307
  36. package/dungeons/vertical/gaming.js +480 -444
  37. package/dungeons/vertical/healthcare.js +306 -262
  38. package/dungeons/vertical/insurance-application.js +427 -409
  39. package/dungeons/vertical/logistics.js +271 -252
  40. package/dungeons/vertical/marketplace.js +333 -323
  41. package/dungeons/vertical/media.js +382 -335
  42. package/dungeons/vertical/real-estate.js +395 -346
  43. package/dungeons/vertical/sass.js +319 -333
  44. package/dungeons/vertical/social.js +368 -316
  45. package/dungeons/vertical/travel.js +297 -295
  46. package/index.js +46 -4
  47. package/lib/core/config-validator.js +126 -28
  48. package/lib/generators/funnels.js +4 -1
  49. package/lib/orchestrators/mixpanel-sender.js +7 -0
  50. package/lib/orchestrators/user-loop.js +132 -31
  51. package/lib/templates/defaults.js +59 -59
  52. package/lib/templates/macro-presets.js +14 -2
  53. package/lib/utils/dataset-context.js +103 -0
  54. package/lib/utils/retention-curve.js +140 -0
  55. package/lib/utils/utils.js +149 -38
  56. package/lib/verify/counting.js +40 -0
  57. package/lib/verify/emulate-breakdown.js +20 -1
  58. package/lib/verify/index.js +1 -0
  59. package/lib/verify/schema-validator.js +3 -1
  60. package/package.json +11 -2
  61. package/scripts/run-dungeon.mjs +12 -1
  62. package/types.d.ts +117 -1
@@ -1,20 +1,39 @@
1
- /**
2
- * Phase 4 reference dungeon — funnel frequency breakdown.
3
- *
4
- * Engaged-cohort users (high count of `Browse`) are 1.5× more likely to complete
5
- * the activation funnel's last step than low-cohort users. Verified via the
6
- * funnelFrequency emulator.
1
+ // ── IMPORTS ──
2
+ import dayjs from 'dayjs';
3
+ import { applyFunnelFrequencyBreakdown } from '../../lib/hook-patterns/index.js';
4
+
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-funnel-frequency
8
+ * PURPOSE: Phase 4 reference fixture — funnel frequency breakdown.
9
+ * Engaged-cohort users (high count of `Browse`) are 1.5× more likely
10
+ * to complete the activation funnel's last step than low-cohort
11
+ * users. Verified via the funnelFrequency emulator.
12
+ * SCALE: 1,000 users, ~120K events, 30 days
13
+ * EVENTS (4): Browse (5) > Land (1) > Sign Up (1) > Activate (1)
14
+ * FUNNELS (1): Activation: Land → Sign Up → Activate (70%)
15
+ */
16
+
17
+ // ── HOOK STORIES ──
18
+ /*
19
+ * PATTERN: Bin users by Browse count (low 0-3, high 3+); within funnel-post,
20
+ * drop final-step `Activate` events at rates {low:0.5, high:0.95}, lifting
21
+ * the high cohort's conversion-per-step.
7
22
  *
8
- * Note: this pattern is funnel-post; it edits the funnel's own events. Combined
9
- * with the per-user funnel attempts, the breakdown should show a stronger
23
+ * Note: this pattern is funnel-post; it edits the funnel's own events.
24
+ * Combined with per-user funnel attempts, the breakdown shows a stronger
10
25
  * conversion-per-step lift in the high cohort.
26
+ *
27
+ * In funnel-post we don't have the user's full event stream — the pattern
28
+ * accepts null for `allUserEvents` and falls back to counting cohortEvent
29
+ * inside the funnel-only events. Real dungeons usually wire this via
30
+ * `everything` instead.
11
31
  */
12
32
 
13
- import dayjs from 'dayjs';
14
- import { applyFunnelFrequencyBreakdown } from '../../lib/hook-patterns/index.js';
15
-
33
+ // ── SCALE ──
16
34
  const FIXED_NOW = dayjs('2024-02-02').unix();
17
35
 
36
+ // ── CONFIG ──
18
37
  export default {
19
38
  name: 'pattern-funnel-freq',
20
39
  seed: 'phase4-funnel-freq',
@@ -23,7 +42,6 @@ export default {
23
42
  numUsers: 1_000,
24
43
  avgEventsPerUserPerDay: 4,
25
44
  percentUsersBornInDataset: 100,
26
- hasAnonIds: false,
27
45
  format: 'json',
28
46
  concurrency: 1,
29
47
  writeToDisk: true,
@@ -41,9 +59,6 @@ export default {
41
59
  hook: function (record, type, meta) {
42
60
  if (type !== 'funnel-post' || !Array.isArray(record)) return;
43
61
  if (!meta.isFirstFunnel) return;
44
- // In funnel-post we don't have the user's full event stream — pattern accepts
45
- // null for `allUserEvents` and falls back to counting cohortEvent inside the
46
- // funnel-only events. Real dungeons usually wire this via `everything` instead.
47
62
  applyFunnelFrequencyBreakdown(null, meta.profile || {}, record, {
48
63
  cohortEvent: 'Browse',
49
64
  bins: { low: [0, 3], high: [3, Infinity] },
@@ -1,15 +1,29 @@
1
- /**
2
- * Phase 4 reference dungeon — time to convert by segment.
3
- *
4
- * Trial users take 4× longer to complete the activation funnel than enterprise
5
- * users (0.5× faster). Verified via the timeToConvert emulator.
6
- */
7
-
1
+ // ── IMPORTS ──
8
2
  import dayjs from 'dayjs';
9
3
  import { applyTTCBySegment } from '../../lib/hook-patterns/index.js';
10
4
 
5
+ // ── OVERVIEW ──
6
+ /*
7
+ * NAME: pattern-ttc-by-segment
8
+ * PURPOSE: Phase 4 reference fixture — time-to-convert by segment. Trial
9
+ * users take 4× longer to complete the activation funnel; enterprise
10
+ * 0.5× faster. Verified via the timeToConvert emulator.
11
+ * SCALE: 1,000 users, ~120K events, 30 days
12
+ * EVENTS (4): Browse (5) > Land (1) > Sign Up (1) > Activate (1)
13
+ * FUNNELS (1): Activation: Land → Sign Up → Activate (100%)
14
+ */
15
+
16
+ // ── HOOK STORIES ──
17
+ /*
18
+ * PATTERN: Scale the activation funnel's time-to-convert by user tier:
19
+ * trial users 4× slower, enterprise 0.5× faster. Verified via the
20
+ * timeToConvert emulator.
21
+ */
22
+
23
+ // ── SCALE ──
11
24
  const FIXED_NOW = dayjs('2024-02-02').unix();
12
25
 
26
+ // ── CONFIG ──
13
27
  export default {
14
28
  name: 'pattern-ttc-by-segment',
15
29
  seed: 'phase4-ttc-seg',
@@ -18,7 +32,6 @@ export default {
18
32
  numUsers: 1_000,
19
33
  avgEventsPerUserPerDay: 4,
20
34
  percentUsersBornInDataset: 100,
21
- hasAnonIds: false,
22
35
  format: 'json',
23
36
  concurrency: 1,
24
37
  writeToDisk: true,
@@ -1,38 +1,25 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "kurby-retention";
3
- const num_days = 360;
4
- const num_users = 100;
5
- const avg_events_per_user_per_day = 0.14;
6
- let token = "";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
11
- import dayjs from 'dayjs';
12
- import utc from 'dayjs/plugin/utc.js';
1
+ // ── IMPORTS ──
2
+ import dayjs from "dayjs";
3
+ import utc from "dayjs/plugin/utc.js";
13
4
  dayjs.extend(utc);
14
5
  import * as u from "../../lib/utils/utils.js";
15
- import * as v from 'ak-tools';
16
- const chance = u.initChance(SEED);
17
-
6
+ import * as v from "ak-tools";
18
7
  /** @typedef {import("../../types").Dungeon} Config */
19
8
 
20
- /**
21
- * ═══════════════════════════════════════════════════════════════
22
- * DATASET OVERVIEW
23
- * ═══════════════════════════════════════════════════════════════
24
- *
25
- * Retention Cadence — tests behavior frequency and color-based churn.
26
- * - 100 users over 360 days, ~5K base events (replaced by cadenced events)
27
- * - 4 behavior cadences: hourly, daily, weekly, monthly
28
- * - Churn rates vary by user's "favorite color" (red, blue, green, yellow, purple)
29
- * - The everything hook REPLACES all generated events with precisely
30
- * cadenced behavior events, making this a pure retention analysis dataset
31
- *
32
- * ═══════════════════════════════════════════════════════════════
33
- * ANALYTICS HOOKS (1 pattern — everything hook)
34
- * ═══════════════════════════════════════════════════════════════
35
- *
9
+ // ── OVERVIEW ──
10
+ /*
11
+ * NAME: retention-cadence
12
+ * PURPOSE: Exercises retention shaping — the everything hook REPLACES all
13
+ * generated events with precisely cadenced behavior streams
14
+ * (hourly/daily/weekly/monthly) with color-driven churn.
15
+ * SCALE: 100 users, ~5K base events (replaced by cadenced events), 360 days
16
+ * EVENTS (6): first behavior, hourly behavior, daily behavior, weekly behavior,
17
+ * monthly behavior, placeholder
18
+ * FUNNELS (0): none
19
+ */
20
+
21
+ // ── HOOK STORIES ──
22
+ /*
36
23
  * 1. COLOR-BASED CADENCED CHURN (everything hook)
37
24
  * All raw events are replaced with 4 cadenced behavior streams
38
25
  * (hourly/daily/weekly/monthly). Each user's "favorite color"
@@ -52,6 +39,16 @@ const chance = u.initChance(SEED);
52
39
  * Expected: each color dominates its respective cadence's churn events
53
40
  */
54
41
 
42
+ // ── SCALE ──
43
+ const SEED = "kurby-retention";
44
+ const NUM_DAYS = 360;
45
+ const NUM_USERS = 100;
46
+ const EVENTS_PER_DAY = 0.14;
47
+ const token = process.env.MP_TOKEN || "";
48
+
49
+ const chance = u.initChance(SEED);
50
+
51
+ // ── KNOBS (tweak these to reshape stories) ──
55
52
  // Churn rates by favorite color per behavior type (percentage likelihood)
56
53
  const CHURN_RATES = {
57
54
  red: { hourly: 15, daily: 40, weekly: 15, monthly: 10 },
@@ -61,7 +58,7 @@ const CHURN_RATES = {
61
58
  purple: { hourly: 40, daily: 15, weekly: 15, monthly: 10 },
62
59
  };
63
60
 
64
- // Behavior cadence definitions: [intervalAmount, intervalUnit, jitterMax, jitterUnit]
61
+ // Behavior cadence definitions: interval + unit + jitter window
65
62
  const BEHAVIORS = {
66
63
  hourly: { interval: 1, unit: 'hour', jitterMax: 10, jitterUnit: 'minute' },
67
64
  daily: { interval: 1, unit: 'day', jitterMax: 2, jitterUnit: 'hour' },
@@ -69,6 +66,7 @@ const BEHAVIORS = {
69
66
  monthly: { interval: 30, unit: 'day', jitterMax: 2, jitterUnit: 'day' },
70
67
  };
71
68
 
69
+ // ── HELPER FUNCTIONS ──
72
70
  function generateCadencedEvents(behaviorName, cadence, startTime, endTime, churnTime, userId) {
73
71
  const events = [];
74
72
  let cursor = dayjs(startTime);
@@ -105,26 +103,94 @@ function generateCadencedEvents(behaviorName, cadence, startTime, endTime, churn
105
103
  return events;
106
104
  }
107
105
 
106
+ function handleEverythingHooks(record, meta) {
107
+ if (!record.length) return record;
108
+
109
+ const userId = record[0].user_id || record[0].device_id;
110
+ if (!userId) return record;
111
+
112
+ const favoriteColor = meta.profile?.["favorite color"] || "green";
113
+ const colorChurnRates = CHURN_RATES[favoriteColor] || CHURN_RATES.green;
114
+
115
+ // Find the time range from the raw events
116
+ const times = record.map(e => new Date(e.time).getTime()).filter(t => !isNaN(t));
117
+ if (!times.length) return record;
118
+ const startTime = dayjs(Math.min(...times));
119
+ const endTime = dayjs(Math.max(...times));
120
+ const totalSpanHours = endTime.diff(startTime, 'hour');
121
+
122
+ if (totalSpanHours < 1) return record;
123
+
124
+ // Keep the "first behavior" event from the original stream
125
+ const firstBehavior = record.find(e => e.event === "first behavior");
126
+ const newEvents = [];
127
+
128
+ if (firstBehavior) {
129
+ newEvents.push(firstBehavior);
130
+ } else {
131
+ // Create a first behavior event at the start
132
+ newEvents.push({
133
+ event: "first behavior",
134
+ time: startTime.toISOString(),
135
+ user_id: userId,
136
+ insert_id: v.uid(12),
137
+ });
138
+ }
139
+
140
+ // For each behavior type, determine churn and generate cadenced events
141
+ for (const [behaviorKey, cadence] of Object.entries(BEHAVIORS)) {
142
+ const churnRate = colorChurnRates[behaviorKey];
143
+ let churnTime = null;
144
+
145
+ if (chance.bool({ likelihood: churnRate })) {
146
+ // Pick churn time between 20% and 80% into the active period
147
+ const churnOffsetHours = chance.integer({
148
+ min: Math.floor(totalSpanHours * 0.2),
149
+ max: Math.floor(totalSpanHours * 0.8),
150
+ });
151
+ churnTime = startTime.add(churnOffsetHours, 'hour');
152
+ }
153
+
154
+ const behaviorEvents = generateCadencedEvents(
155
+ behaviorKey,
156
+ cadence,
157
+ startTime,
158
+ endTime,
159
+ churnTime,
160
+ userId
161
+ );
162
+ newEvents.push(...behaviorEvents);
163
+ }
164
+
165
+ // Sort all events by time
166
+ newEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
167
+ return newEvents;
168
+ }
169
+
170
+ // ── CONFIG ──
108
171
  /** @type {Config} */
109
172
  const config = {
110
- token,
111
173
  seed: SEED,
112
- numDays: num_days,
113
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
114
- numUsers: num_users,
115
- hasAnonIds: false,
116
- hasSessionIds: false,
174
+ numDays: NUM_DAYS,
175
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
176
+ numUsers: NUM_USERS,
117
177
  format: "json",
118
- alsoInferFunnels: false,
119
- hasLocation: false,
120
- hasAndroidDevices: false,
121
- hasIOSDevices: false,
122
- hasDesktopDevices: false,
123
- hasBrowser: false,
124
- hasCampaigns: false,
125
- isAnonymous: false,
126
- hasAdSpend: false,
127
- hasAvatar: false,
178
+ credentials: {
179
+ token,
180
+ },
181
+ switches: {
182
+ hasSessionIds: false,
183
+ alsoInferFunnels: false,
184
+ hasLocation: false,
185
+ hasAndroidDevices: false,
186
+ hasIOSDevices: false,
187
+ hasDesktopDevices: false,
188
+ hasBrowser: false,
189
+ hasCampaigns: false,
190
+ isAnonymous: false,
191
+ hasAdSpend: false,
192
+ hasAvatar: false,
193
+ },
128
194
  concurrency: 1,
129
195
  writeToDisk: false,
130
196
 
@@ -177,70 +243,7 @@ const config = {
177
243
  lookupTables: [],
178
244
 
179
245
  hook: function (record, type, meta) {
180
- if (type === "everything") {
181
- if (!record.length) return record;
182
-
183
- const userId = record[0].user_id || record[0].device_id;
184
- if (!userId) return record;
185
-
186
- const favoriteColor = meta.profile?.["favorite color"] || "green";
187
- const colorChurnRates = CHURN_RATES[favoriteColor] || CHURN_RATES.green;
188
-
189
- // Find the time range from the raw events
190
- const times = record.map(e => new Date(e.time).getTime()).filter(t => !isNaN(t));
191
- if (!times.length) return record;
192
- const startTime = dayjs(Math.min(...times));
193
- const endTime = dayjs(Math.max(...times));
194
- const totalSpanHours = endTime.diff(startTime, 'hour');
195
-
196
- if (totalSpanHours < 1) return record;
197
-
198
- // Keep the "first behavior" event from the original stream
199
- const firstBehavior = record.find(e => e.event === "first behavior");
200
- const newEvents = [];
201
-
202
- if (firstBehavior) {
203
- newEvents.push(firstBehavior);
204
- } else {
205
- // Create a first behavior event at the start
206
- newEvents.push({
207
- event: "first behavior",
208
- time: startTime.toISOString(),
209
- user_id: userId,
210
- insert_id: v.uid(12),
211
- });
212
- }
213
-
214
- // For each behavior type, determine churn and generate cadenced events
215
- for (const [behaviorKey, cadence] of Object.entries(BEHAVIORS)) {
216
- const churnRate = colorChurnRates[behaviorKey];
217
- let churnTime = null;
218
-
219
- if (chance.bool({ likelihood: churnRate })) {
220
- // Pick churn time between 20% and 80% into the active period
221
- const churnOffsetHours = chance.integer({
222
- min: Math.floor(totalSpanHours * 0.2),
223
- max: Math.floor(totalSpanHours * 0.8),
224
- });
225
- churnTime = startTime.add(churnOffsetHours, 'hour');
226
- }
227
-
228
- const behaviorEvents = generateCadencedEvents(
229
- behaviorKey,
230
- cadence,
231
- startTime,
232
- endTime,
233
- churnTime,
234
- userId
235
- );
236
- newEvents.push(...behaviorEvents);
237
- }
238
-
239
- // Sort all events by time
240
- newEvents.sort((a, b) => new Date(a.time) - new Date(b.time));
241
- return newEvents;
242
- }
243
-
246
+ if (type === "everything") return handleEverythingHooks(record, meta);
244
247
  return record;
245
248
  },
246
249
  };
@@ -1,60 +1,100 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "foo bar";
3
- const num_days = 90;
4
- const num_users = 500;
5
- const avg_events_per_user_per_day = 1.11;
6
- let token = "";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
1
+ // ── IMPORTS ──
11
2
  import Chance from 'chance';
12
- const chance = new Chance();
13
- import dayjs from 'dayjs';
14
- import utc from 'dayjs/plugin/utc.js';
15
- dayjs.extend(utc);
16
3
  import { weighNumRange, integer } from "../../lib/utils/utils.js";
17
-
18
4
  /** @typedef {import("../../types").Dungeon} Config */
19
- /**
20
- * ═══════════════════════════════════════════════════════════════
21
- * DATASET OVERVIEW
22
- * ═══════════════════════════════════════════════════════════════
23
- *
24
- * Sanity Test lightweight dungeon for module integration testing.
25
- * - 500 users over 30 days, ~50K events
26
- * - 10 abstract events (foo-yak) with no properties
27
- * - Super props: color, number — for quick breakdown testing
28
- * - Groups, SCDs, inferred funnels all disabled
29
- *
30
- * ═══════════════════════════════════════════════════════════════
31
- * ANALYTICS HOOKS (2 patterns)
32
- * ═══════════════════════════════════════════════════════════════
33
- *
5
+
6
+ // ── OVERVIEW ──
7
+ /*
8
+ * NAME: sanity
9
+ * PURPOSE: lightweight dungeon for module integration testing — abstract foo-yak events, inferred funnels
10
+ * SCALE: 500 users, ~50K events, 90 days
11
+ * EVENTS (10): foo, bar, baz, qux, garply, durtle, linny, fonk, crumn, yak
12
+ * FUNNELS (2): qux→garply→durtle→linny→fonk→crumn→yak; foo→bar→baz (first)
13
+ */
14
+
15
+ // ── HOOK STORIES ──
16
+ /*
34
17
  * 1. TEMPERATURE TAGGING (event hook)
35
18
  * foo/bar/baz = "hot", crumn/yak = "cold", everything else = "warm".
19
+ * (NOTE: current implementation stamps an integer engagement_score
20
+ * bucketed by event name — high/mid/low engagement bands.)
36
21
  *
37
22
  * 2. HASH-BASED POWER USERS (everything hook)
38
23
  * ~10% of users (by distinct_id hash) get 3 extra duplicate events.
24
+ * (NOTE: current implementation trims the tail of users with 4-7
25
+ * events to simulate light-user churn.)
39
26
  */
40
27
 
28
+ // ── SCALE ──
29
+ const SEED = "foo bar";
30
+ const NUM_DAYS = 90;
31
+ const NUM_USERS = 500;
32
+ const EVENTS_PER_DAY = 1.11;
33
+ const token = process.env.MP_TOKEN || "";
34
+
35
+ const chance = new Chance();
36
+
37
+ // ── DATA ARRAYS ──
38
+ 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"];
39
+ const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
40
+
41
+ // ── HELPER FUNCTIONS ──
42
+ function handleUserHook(record) {
43
+ // tag power users based on luckyNumber
44
+ record.userTier = record.luckyNumber > 300 ? "power" : "regular";
45
+ return record;
46
+ }
47
+
48
+ function handleEventHook(record) {
49
+ // add an engagement score based on event type
50
+ const highEngagement = ["fonk", "crumn", "yak"];
51
+ const midEngagement = ["garply", "durtle", "linny"];
52
+ if (highEngagement.includes(record.event)) {
53
+ record.engagement_score = chance.integer({ min: 70, max: 100 });
54
+ } else if (midEngagement.includes(record.event)) {
55
+ record.engagement_score = chance.integer({ min: 30, max: 69 });
56
+ } else {
57
+ record.engagement_score = chance.integer({ min: 1, max: 29 });
58
+ }
59
+ return record;
60
+ }
61
+
62
+ function handleEverythingHook(record, meta) {
63
+ // low-activity users lose their last few events (churn simulation)
64
+ const profile = meta.profile;
65
+ record.forEach(e => {
66
+ e.color = profile.color;
67
+ e.number = profile.number;
68
+ // engagement_score intentionally varies per event (set by event hook)
69
+ });
70
+ if (record.length > 3 && record.length < 8) {
71
+ // users with few events lose the tail end — simulates churn
72
+ return record.slice(0, Math.ceil(record.length * 0.6));
73
+ }
74
+ return record;
75
+ }
76
+
77
+ // ── CONFIG ──
41
78
  /** @type {import('../types.js').Dungeon} */
42
79
  const config = {
43
- token,
44
80
  seed: SEED,
45
- numDays: num_days,
46
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
47
- numUsers: num_users,
81
+ numDays: NUM_DAYS,
82
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
83
+ numUsers: NUM_USERS,
48
84
  format: 'json', //csv or json
49
- region: "US",
50
- hasAnonIds: false, //if true, anonymousIds are created for each user
51
- hasSessionIds: false, //if true, hasSessionIds are created for each user
52
- alsoInferFunnels: true, //if true, infer funnels from events
85
+ credentials: {
86
+ token,
87
+ region: "US",
88
+ },
89
+ switches: {
90
+ hasSessionIds: false, //if true, hasSessionIds are created for each user
91
+ alsoInferFunnels: true, //if true, infer funnels from events
92
+ },
53
93
  writeToDisk: false,
54
94
  concurrency: 1,
55
95
  funnels: [
56
96
  {
57
- sequence: ["qux", "garply", "durtle", "linny", "fonk", "crumn", "yak"],
97
+ sequence: ["qux", "garply", "durtle", "linny", "fonk", "crumn", "yak"],
58
98
  },
59
99
  {
60
100
  sequence: ["foo", "bar", "baz"],
@@ -115,7 +155,7 @@ const config = {
115
155
  }
116
156
  ],
117
157
  superProps: {
118
- color: ["red", "orange", "yellow", "green", "blue", "indigo", "violet"],
158
+ color: colors,
119
159
  number: integer,
120
160
  engagement_score: [0],
121
161
  },
@@ -123,51 +163,17 @@ const config = {
123
163
  title: chance.profession.bind(chance),
124
164
  luckyNumber: weighNumRange(42, 420),
125
165
  userTier: ["regular"],
126
- spiritAnimal: ["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"],
127
- color: ["red", "orange", "yellow", "green", "blue", "indigo", "violet"],
166
+ spiritAnimal: spiritAnimals,
167
+ color: colors,
128
168
  number: integer,
129
169
  engagement_score: [0],
130
170
  },
131
171
  hook: function (record, type, meta) {
132
- // --- user hook: tag power users based on luckyNumber ---
133
- if (type === "user") {
134
- record.userTier = record.luckyNumber > 300 ? "power" : "regular";
135
- return record;
136
- }
137
-
138
- // --- event hook: add an engagement score based on event type ---
139
- if (type === "event") {
140
- const highEngagement = ["fonk", "crumn", "yak"];
141
- const midEngagement = ["garply", "durtle", "linny"];
142
- if (highEngagement.includes(record.event)) {
143
- record.engagement_score = chance.integer({ min: 70, max: 100 });
144
- } else if (midEngagement.includes(record.event)) {
145
- record.engagement_score = chance.integer({ min: 30, max: 69 });
146
- } else {
147
- record.engagement_score = chance.integer({ min: 1, max: 29 });
148
- }
149
- return record;
150
- }
151
-
152
- // --- everything hook: low-activity users lose their last few events (churn simulation) ---
153
- if (type === "everything") {
154
- const profile = meta.profile;
155
- record.forEach(e => {
156
- e.color = profile.color;
157
- e.number = profile.number;
158
- // engagement_score intentionally varies per event (set by event hook)
159
- });
160
- if (record.length > 3 && record.length < 8) {
161
- // users with few events lose the tail end — simulates churn
162
- return record.slice(0, Math.ceil(record.length * 0.6));
163
- }
164
- return record;
165
- }
166
-
172
+ if (type === "user") return handleUserHook(record);
173
+ if (type === "event") return handleEventHook(record);
174
+ if (type === "everything") return handleEverythingHook(record, meta);
167
175
  return record;
168
176
  }
169
177
  };
170
178
 
171
-
172
-
173
- export default config;
179
+ export default config;