@ak--47/dungeon-master 1.5.0 → 1.5.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/.claude/skills/create-dungeon/SKILL.md +139 -46
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
- package/.claude/skills/write-hooks/SKILL.md +31 -3
- package/CHANGELOG.md +85 -0
- package/HOOKS.md +13 -0
- package/dungeons/technical/ad-spend.js +41 -49
- package/dungeons/technical/anonymous-users.js +38 -36
- package/dungeons/technical/array-of-object-lookup.js +136 -153
- package/dungeons/technical/datagen-v15-verify.js +24 -11
- package/dungeons/technical/experiments.js +42 -40
- package/dungeons/technical/foobar.js +114 -118
- package/dungeons/technical/group-analytics.js +42 -40
- package/dungeons/technical/hook-helpers-verify.js +69 -50
- package/dungeons/technical/identity-model-verify.js +22 -12
- package/dungeons/technical/mirror-strategies.js +37 -39
- package/dungeons/technical/nested-objects.js +119 -118
- package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
- package/dungeons/technical/pattern-attributed-by-source.js +23 -9
- package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
- package/dungeons/technical/pattern-funnel-frequency.js +30 -15
- package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
- package/dungeons/technical/retention-cadence.js +115 -112
- package/dungeons/technical/sanity.js +86 -80
- package/dungeons/technical/scale-test.js +34 -38
- package/dungeons/technical/scd.js +111 -128
- package/dungeons/technical/simple.js +134 -141
- package/dungeons/technical/simplest.js +54 -62
- package/dungeons/technical/text-generation.js +110 -146
- package/dungeons/vertical/ai-platform.js +296 -333
- package/dungeons/vertical/community.js +284 -255
- package/dungeons/vertical/crypto.js +395 -391
- package/dungeons/vertical/dating.js +411 -378
- package/dungeons/vertical/devtools.js +336 -298
- package/dungeons/vertical/ecommerce.js +316 -394
- package/dungeons/vertical/education.js +369 -325
- package/dungeons/vertical/fintech.js +358 -325
- package/dungeons/vertical/fitness.js +335 -291
- package/dungeons/vertical/food-delivery.js +343 -307
- package/dungeons/vertical/gaming.js +480 -444
- package/dungeons/vertical/healthcare.js +306 -262
- package/dungeons/vertical/insurance-application.js +427 -409
- package/dungeons/vertical/logistics.js +271 -252
- package/dungeons/vertical/marketplace.js +333 -323
- package/dungeons/vertical/media.js +382 -335
- package/dungeons/vertical/real-estate.js +395 -346
- package/dungeons/vertical/sass.js +319 -333
- package/dungeons/vertical/social.js +368 -316
- package/dungeons/vertical/travel.js +297 -295
- package/index.js +46 -4
- package/lib/core/config-validator.js +126 -28
- package/lib/generators/funnels.js +4 -1
- package/lib/orchestrators/mixpanel-sender.js +7 -0
- package/lib/orchestrators/user-loop.js +132 -31
- package/lib/templates/defaults.js +59 -59
- package/lib/templates/macro-presets.js +14 -2
- package/lib/utils/dataset-context.js +103 -0
- package/lib/utils/retention-curve.js +140 -0
- package/lib/utils/utils.js +149 -38
- package/lib/verify/counting.js +40 -0
- package/lib/verify/emulate-breakdown.js +20 -1
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +3 -1
- package/package.json +11 -2
- package/scripts/run-dungeon.mjs +12 -1
- package/types.d.ts +117 -1
|
@@ -1,38 +1,25 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
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:
|
|
113
|
-
avgEventsPerUserPerDay:
|
|
114
|
-
numUsers:
|
|
115
|
-
hasAnonIds: false,
|
|
116
|
-
hasSessionIds: false,
|
|
174
|
+
numDays: NUM_DAYS,
|
|
175
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
176
|
+
numUsers: NUM_USERS,
|
|
117
177
|
format: "json",
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
46
|
-
avgEventsPerUserPerDay:
|
|
47
|
-
numUsers:
|
|
81
|
+
numDays: NUM_DAYS,
|
|
82
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
83
|
+
numUsers: NUM_USERS,
|
|
48
84
|
format: 'json', //csv or json
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
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:
|
|
127
|
-
color:
|
|
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
|
-
|
|
133
|
-
if (type === "
|
|
134
|
-
|
|
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;
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
const SEED = "scale test";
|
|
3
|
-
const num_days = 365;
|
|
4
|
-
const num_users = 10_000;
|
|
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
|
-
|
|
1
|
+
// ── IMPORTS ──
|
|
11
2
|
import Chance from 'chance';
|
|
12
3
|
let chance = new Chance();
|
|
13
4
|
import dayjs from "dayjs";
|
|
@@ -15,26 +6,27 @@ import utc from "dayjs/plugin/utc.js";
|
|
|
15
6
|
dayjs.extend(utc);
|
|
16
7
|
import { uid, comma } from 'ak-tools';
|
|
17
8
|
import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
|
|
18
|
-
|
|
19
9
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - Minimal event schema (3 events, few properties)
|
|
29
|
-
* - No hooks, no funnels, no extras
|
|
30
|
-
*
|
|
31
|
-
* Explicitly for performance benchmarking and verifying
|
|
32
|
-
* large file output + batch splitting behavior.
|
|
10
|
+
|
|
11
|
+
// ── OVERVIEW ──
|
|
12
|
+
/*
|
|
13
|
+
* NAME: scale-test
|
|
14
|
+
* PURPOSE: High-volume scale fixture — exercises batch sizing, gzip, large file output, no hooks
|
|
15
|
+
* SCALE: 10,000 users, ~500K events, 365 days
|
|
16
|
+
* EVENTS (3): page view, click, api call
|
|
17
|
+
* FUNNELS (0): none
|
|
33
18
|
*/
|
|
34
19
|
|
|
35
|
-
|
|
20
|
+
// ── SCALE ──
|
|
21
|
+
const SEED = "scale test";
|
|
22
|
+
const num_days = 365;
|
|
23
|
+
const num_users = 10_000;
|
|
24
|
+
const avg_events_per_user_per_day = 0.14;
|
|
25
|
+
const token = process.env.MP_TOKEN || "";
|
|
26
|
+
|
|
27
|
+
// ── CONFIG ──
|
|
28
|
+
/** @type {Config} */
|
|
36
29
|
const config = {
|
|
37
|
-
token,
|
|
38
30
|
seed: SEED,
|
|
39
31
|
name: "scale-test",
|
|
40
32
|
numDays: num_days,
|
|
@@ -43,18 +35,22 @@ const config = {
|
|
|
43
35
|
format: 'json',
|
|
44
36
|
gzip: true,
|
|
45
37
|
concurrency: 1,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
38
|
+
credentials: {
|
|
39
|
+
token,
|
|
40
|
+
region: "US",
|
|
41
|
+
},
|
|
42
|
+
switches: {
|
|
43
|
+
hasSessionIds: false,
|
|
44
|
+
hasAdSpend: false,
|
|
45
|
+
hasLocation: false,
|
|
46
|
+
hasAndroidDevices: false,
|
|
47
|
+
hasIOSDevices: false,
|
|
48
|
+
hasDesktopDevices: false,
|
|
49
|
+
hasBrowser: false,
|
|
50
|
+
hasCampaigns: false,
|
|
51
|
+
isAnonymous: false,
|
|
52
|
+
alsoInferFunnels: false,
|
|
53
|
+
},
|
|
58
54
|
writeToDisk: false,
|
|
59
55
|
|
|
60
56
|
events: [
|