@ak--47/dungeon-master 1.0.0
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/README.md +518 -0
- package/dungeons/array-of-object-lookup-schema.json +327 -0
- package/dungeons/array-of-object-lookup.js +220 -0
- package/dungeons/ecommerce-schema.json +462 -0
- package/dungeons/ecommerce.js +447 -0
- package/dungeons/education-schema.json +2409 -0
- package/dungeons/education.js +768 -0
- package/dungeons/fintech-schema.json +14034 -0
- package/dungeons/fintech.js +696 -0
- package/dungeons/foobar-schema.json +403 -0
- package/dungeons/foobar.js +296 -0
- package/dungeons/food-delivery-schema.json +192 -0
- package/dungeons/food-delivery.js +602 -0
- package/dungeons/food-schema.json +1152 -0
- package/dungeons/food.js +754 -0
- package/dungeons/gaming-schema.json +1270 -0
- package/dungeons/gaming.js +508 -0
- package/dungeons/insurance-application-schema.json +204 -0
- package/dungeons/insurance-application.js +605 -0
- package/dungeons/media-schema.json +906 -0
- package/dungeons/media.js +790 -0
- package/dungeons/retention-cadence-schema.json +78 -0
- package/dungeons/retention-cadence.js +244 -0
- package/dungeons/rpg-schema.json +4526 -0
- package/dungeons/rpg.js +919 -0
- package/dungeons/sanity-schema.json +255 -0
- package/dungeons/sanity.js +152 -0
- package/dungeons/sass-schema.json +1291 -0
- package/dungeons/sass.js +795 -0
- package/dungeons/scd-schema.json +919 -0
- package/dungeons/scd.js +277 -0
- package/dungeons/simple-schema.json +608 -0
- package/dungeons/simple.js +285 -0
- package/dungeons/simplest-schema.json +1418 -0
- package/dungeons/simplest.js +392 -0
- package/dungeons/social-schema.json +1118 -0
- package/dungeons/social.js +686 -0
- package/dungeons/text-generation-schema.json +3096 -0
- package/dungeons/text-generation.js +812 -0
- package/index.js +567 -0
- package/lib/core/config-validator.js +395 -0
- package/lib/core/context.js +204 -0
- package/lib/core/dungeon-loader.js +337 -0
- package/lib/core/storage.js +379 -0
- package/lib/generators/adspend.js +132 -0
- package/lib/generators/events.js +271 -0
- package/lib/generators/funnels.js +407 -0
- package/lib/generators/mirror.js +167 -0
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +124 -0
- package/lib/generators/text.js +1192 -0
- package/lib/orchestrators/mixpanel-sender.js +266 -0
- package/lib/orchestrators/user-loop.js +335 -0
- package/lib/templates/abbreviated.d.ts +169 -0
- package/lib/templates/defaults.js +1405 -0
- package/lib/templates/phrases.js +2526 -0
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/soup-presets.js +188 -0
- package/lib/utils/function-registry.js +302 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +34 -0
- package/lib/utils/utils.js +1490 -0
- package/package.json +89 -0
- package/types.d.ts +865 -0
package/README.md
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# dungeon-master
|
|
2
|
+
|
|
3
|
+
generate realistic fake analytics data at scale. events, users, groups, funnels, SCDs, lookup tables, ad spend, mirror datasets, organic text, and more.
|
|
4
|
+
|
|
5
|
+
this is the best kind of test data: real fake data (really).
|
|
6
|
+
|
|
7
|
+
## what is this
|
|
8
|
+
|
|
9
|
+
dungeon-master creates high-volume, semi-structured event data with deliberate patterns ("hooks") baked in. you define a "dungeon" (a configuration describing your data model), and the generator produces millions of events that look like real user behavior, because the time distributions, property weights, and behavioral patterns are all modeled from real-world analytics data.
|
|
10
|
+
|
|
11
|
+
the key piece is the **hook system**. hooks let you engineer specific, discoverable patterns into the generated data. things like "premium users convert 2x better" or "there was a service outage during days 40-47" or "users who watch low-quality video churn at 50%." the data looks organic, but you have the answer key.
|
|
12
|
+
|
|
13
|
+
this matters because it's really hard to build, test, and train analytics tools without realistic data that has known ground truth. and it's especially hard when you need millions of events with temporal patterns, funnel behaviors, and cross-table correlations.
|
|
14
|
+
|
|
15
|
+
i built this because i needed it. and after using it across hundreds of customer demos, internal testing, and AI training workflows... it just works.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @ak--47/dungeon-master
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## quick start
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
import DUNGEON_MASTER from '@ak--47/dungeon-master';
|
|
25
|
+
|
|
26
|
+
// simple: pass a config object
|
|
27
|
+
const result = await DUNGEON_MASTER({
|
|
28
|
+
numUsers: 1_000,
|
|
29
|
+
numEvents: 100_000,
|
|
30
|
+
numDays: 90,
|
|
31
|
+
format: 'json',
|
|
32
|
+
writeToDisk: true,
|
|
33
|
+
events: [
|
|
34
|
+
{ event: 'page view', weight: 10 },
|
|
35
|
+
{ event: 'sign up', weight: 1, isFirstEvent: true },
|
|
36
|
+
{ event: 'purchase', weight: 3, properties: { amount: [10, 25, 50, 100, 250] } }
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(`${result.eventCount} events, ${result.userCount} users`);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
the main export accepts multiple input formats. use whatever fits your workflow:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
// load a dungeon file from disk
|
|
47
|
+
const result = await DUNGEON_MASTER('./dungeons/simple.js');
|
|
48
|
+
|
|
49
|
+
// load a JSON dungeon (exported from the UI)
|
|
50
|
+
const result = await DUNGEON_MASTER('./dungeons/simple-schema.json');
|
|
51
|
+
|
|
52
|
+
// run multiple dungeons
|
|
53
|
+
const results = await DUNGEON_MASTER([
|
|
54
|
+
'./dungeons/gaming.js',
|
|
55
|
+
'./dungeons/media.js',
|
|
56
|
+
'./dungeons/food-delivery.js'
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// pass raw javascript as a string
|
|
60
|
+
const result = await DUNGEON_MASTER(`
|
|
61
|
+
export default {
|
|
62
|
+
numUsers: 500,
|
|
63
|
+
numEvents: 50_000,
|
|
64
|
+
numDays: 60,
|
|
65
|
+
events: [
|
|
66
|
+
{ event: 'click', weight: 5 },
|
|
67
|
+
{ event: 'submit', weight: 2 }
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
// override any config when loading from files
|
|
73
|
+
const result = await DUNGEON_MASTER('./dungeons/complex.js', {
|
|
74
|
+
numUsers: 100, // shrink for testing
|
|
75
|
+
writeToDisk: true,
|
|
76
|
+
verbose: true
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## send data to mixpanel
|
|
81
|
+
|
|
82
|
+
pass a project token and the generated data imports directly:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
const result = await DUNGEON_MASTER({
|
|
86
|
+
...myDungeon,
|
|
87
|
+
token: process.env.MIXPANEL_TOKEN,
|
|
88
|
+
region: 'US'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(result.importResults);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## dungeons
|
|
95
|
+
|
|
96
|
+
a dungeon is a javascript file that exports a configuration object. it defines your entire data model: events, funnels, user properties, group analytics, SCDs, and a hook function that engineers discoverable patterns into the data.
|
|
97
|
+
|
|
98
|
+
see `dungeons/` for examples ranging from simple (5 events, no hooks) to complex (18 events, 8 funnels, 8 hooks with temporal windowing, closure state, and cross-table correlation).
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// dungeons/my-app.js
|
|
102
|
+
import dayjs from 'dayjs';
|
|
103
|
+
import { pickAWinner, weighNumRange, integer } from '@ak--47/dungeon-master/utils';
|
|
104
|
+
|
|
105
|
+
export default {
|
|
106
|
+
seed: 'my-app-v1',
|
|
107
|
+
numUsers: 10_000,
|
|
108
|
+
numEvents: 1_000_000,
|
|
109
|
+
numDays: 120,
|
|
110
|
+
format: 'json',
|
|
111
|
+
|
|
112
|
+
events: [
|
|
113
|
+
{ event: 'page view', weight: 10, properties: { page: ['/', '/pricing', '/docs', '/blog'] } },
|
|
114
|
+
{ event: 'sign up', weight: 1, isFirstEvent: true },
|
|
115
|
+
{ event: 'feature used', weight: 8, properties: { feature: pickAWinner(['search', 'export', 'share', 'filter']) } },
|
|
116
|
+
{ event: 'upgrade', weight: 2, properties: { plan: ['starter', 'pro', 'enterprise'], amount: weighNumRange(10, 500) } },
|
|
117
|
+
{ event: 'support ticket', weight: 1, properties: { priority: ['low', 'medium', 'high', 'critical'] } }
|
|
118
|
+
],
|
|
119
|
+
|
|
120
|
+
funnels: [
|
|
121
|
+
{ sequence: ['page view', 'sign up'], conversionRate: 40, isFirstFunnel: true, order: 'sequential' },
|
|
122
|
+
{ sequence: ['feature used', 'upgrade'], conversionRate: 15, order: 'sequential', timeToConvert: 72 }
|
|
123
|
+
],
|
|
124
|
+
|
|
125
|
+
userProps: {
|
|
126
|
+
plan: ['free', 'free', 'free', 'starter', 'starter', 'pro', 'enterprise'],
|
|
127
|
+
company_size: weighNumRange(1, 500, 0.5)
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
soup: 'growth',
|
|
131
|
+
|
|
132
|
+
hook: function(record, type, meta) {
|
|
133
|
+
// enterprise users convert 3x better
|
|
134
|
+
if (type === 'funnel-pre' && meta.profile.plan === 'enterprise') {
|
|
135
|
+
record.conversionRate = Math.min(record.conversionRate * 3, 95);
|
|
136
|
+
}
|
|
137
|
+
return record;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### dungeon utilities
|
|
143
|
+
|
|
144
|
+
dungeon scripts can import utilities from the package directly:
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// weighted random selection, number ranges, date generation, and more
|
|
148
|
+
import { pickAWinner, weighNumRange, weighChoices, date, integer } from '@ak--47/dungeon-master/utils';
|
|
149
|
+
|
|
150
|
+
// organic text generation (support tickets, reviews, search queries, etc.)
|
|
151
|
+
import { createTextGenerator, generateBatch } from '@ak--47/dungeon-master/text';
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
these are the same functions used internally. `pickAWinner` creates weighted distributions, `weighNumRange` generates realistic numeric ranges with configurable skew, and the text generators produce organic-looking strings with sentiment analysis and keyword injection.
|
|
155
|
+
|
|
156
|
+
## the hook system
|
|
157
|
+
|
|
158
|
+
hooks are the most important feature. a hook is a single function on your dungeon config that receives every piece of data as it flows through the pipeline. you can mutate events, modify conversion rates, inject synthetic events, simulate churn, engineer temporal patterns, and correlate behaviors across tables.
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
hook: function(record, type, meta) {
|
|
162
|
+
// type tells you what kind of data you're looking at
|
|
163
|
+
// record is the data object (event, profile, or array of events)
|
|
164
|
+
// meta has contextual info (user profile, config, etc.)
|
|
165
|
+
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### hook types
|
|
171
|
+
|
|
172
|
+
hooks fire in this order for each user:
|
|
173
|
+
|
|
174
|
+
| type | what you get | what you can do |
|
|
175
|
+
|------|-------------|----------------|
|
|
176
|
+
| `"user"` | user profile object | set computed properties, segments, tiers |
|
|
177
|
+
| `"scd-pre"` | SCD entries array | modify time-series attribute mutations |
|
|
178
|
+
| `"funnel-pre"` | funnel config + `meta.profile` | change conversion rates based on user properties |
|
|
179
|
+
| `"event"` | single event (flat props) | modify properties, tag events, rename events |
|
|
180
|
+
| `"funnel-post"` | array of funnel events | splice extra events between funnel steps |
|
|
181
|
+
| `"everything"` | ALL events for one user | correlate across event types, filter/inject/duplicate events |
|
|
182
|
+
|
|
183
|
+
storage hooks (`"ad-spend"`, `"group"`, `"mirror"`, `"lookup"`) fire during write, not generation.
|
|
184
|
+
|
|
185
|
+
### hook patterns
|
|
186
|
+
|
|
187
|
+
these are the patterns i use most. they cover probably 90% of what you'd want to engineer into test data:
|
|
188
|
+
|
|
189
|
+
**temporal windowing** (simulate a product launch, outage, or improvement):
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
hook: function(record, type, meta) {
|
|
193
|
+
if (type === 'event') {
|
|
194
|
+
const LAUNCH_DAY = dayjs().subtract(30, 'day');
|
|
195
|
+
if (dayjs(record.time).isAfter(LAUNCH_DAY)) {
|
|
196
|
+
if (record.event === 'purchase') record.amount *= 2;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return record;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**user-property-driven conversion** (premium users convert better):
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
hook: function(record, type, meta) {
|
|
207
|
+
if (type === 'funnel-pre') {
|
|
208
|
+
if (meta.profile.plan === 'enterprise') {
|
|
209
|
+
record.conversionRate = Math.min(record.conversionRate * 2.5, 95);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return record;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**two-pass behavioral tagging** (identify power users, then tag all their events):
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
hook: function(record, type, meta) {
|
|
220
|
+
if (type === 'everything') {
|
|
221
|
+
const purchases = record.filter(e => e.event === 'purchase');
|
|
222
|
+
const isPowerUser = purchases.length > 5;
|
|
223
|
+
for (const event of record) {
|
|
224
|
+
event.is_power_user = isPowerUser;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return record;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**simulating churn** (low-quality users drop off):
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
hook: function(record, type, meta) {
|
|
235
|
+
if (type === 'everything') {
|
|
236
|
+
const lowQuality = record.filter(e => e.quality === '240p').length;
|
|
237
|
+
const highQuality = record.filter(e => e.quality === '1080p').length;
|
|
238
|
+
if (lowQuality > highQuality && chance.bool({ likelihood: 50 })) {
|
|
239
|
+
const midpoint = Math.floor(record.length / 2);
|
|
240
|
+
return record.slice(0, midpoint); // user "churns" halfway through
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return record;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**event injection** (add synthetic milestone events):
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
hook: function(record, type, meta) {
|
|
251
|
+
if (type === 'everything') {
|
|
252
|
+
const purchases = record.filter(e => e.event === 'purchase');
|
|
253
|
+
if (purchases.length >= 10) {
|
|
254
|
+
record.push({
|
|
255
|
+
event: 'loyalty milestone',
|
|
256
|
+
time: purchases[9].time,
|
|
257
|
+
user_id: purchases[9].user_id,
|
|
258
|
+
milestone: '10th purchase',
|
|
259
|
+
total_spend: purchases.reduce((sum, p) => sum + (p.amount || 0), 0)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return record;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### critical hook rules
|
|
268
|
+
|
|
269
|
+
1. event properties are **flat** on the record: `record.amount`, not `record.properties.amount`
|
|
270
|
+
2. injected events need `user_id` (not `distinct_id`) and a valid `time` string
|
|
271
|
+
3. use `dayjs` for time operations inside hooks
|
|
272
|
+
4. to drop events, use the `everything` hook and return a filtered array. don't return `{}` from event hooks (creates broken events)
|
|
273
|
+
5. the `everything` hook is the most powerful. it sees all events for one user, has access to `meta.profile`, and can correlate across event types
|
|
274
|
+
|
|
275
|
+
## timesoup
|
|
276
|
+
|
|
277
|
+
timesoup controls how events are distributed across time. it uses gaussian cluster sampling layered with day-of-week and hour-of-day weighting derived from... i won't tell you. a prize goes to whoever can guess. the result is realistic temporal patterns: weekday peaks, weekend valleys, morning surges, afternoon dips.
|
|
278
|
+
|
|
279
|
+
### presets
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
soup: 'growth' // default. gradual uptrend with weekly cycles
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
| preset | pattern | use case |
|
|
286
|
+
|--------|---------|----------|
|
|
287
|
+
| `"steady"` | flat, minimal variation | mature saas, utility apps |
|
|
288
|
+
| `"growth"` | gradual uptrend + weekly cycle | general purpose (default) |
|
|
289
|
+
| `"spiky"` | dramatic peaks and valleys | gaming, social, viral products |
|
|
290
|
+
| `"seasonal"` | 3-4 major waves | ecommerce, education |
|
|
291
|
+
| `"global"` | flat DOW + flat HOD | global saas, infrastructure |
|
|
292
|
+
| `"churny"` | flat, no growth trend | declining products (pair with churn hooks) |
|
|
293
|
+
| `"chaotic"` | wild variation | anomaly detection, incident response |
|
|
294
|
+
|
|
295
|
+
### custom configuration
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
// preset with overrides
|
|
299
|
+
soup: { preset: 'spiky', deviation: 5 }
|
|
300
|
+
|
|
301
|
+
// fully custom
|
|
302
|
+
soup: {
|
|
303
|
+
peaks: 200,
|
|
304
|
+
deviation: 2,
|
|
305
|
+
mean: 0,
|
|
306
|
+
dayOfWeekWeights: [0.637, 1.0, 0.999, 0.998, 0.966, 0.802, 0.528], // [Sun..Sat]
|
|
307
|
+
hourOfDayWeights: [/* 24 values, index 0 = midnight UTC */]
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
^ be warned, this can blow up your dataset. most of the 'growth' is what you want (right?)
|
|
311
|
+
|
|
312
|
+
## output formats
|
|
313
|
+
|
|
314
|
+
generated data writes to `./data/` by default, but you can pass a path to `writeToDisk` and it will write there instead. supported formats:
|
|
315
|
+
|
|
316
|
+
| format | extension | notes |
|
|
317
|
+
|--------|-----------|-------|
|
|
318
|
+
| `json` | `.json` | newline-delimited JSON (one object per line) |
|
|
319
|
+
| `csv` | `.csv` | standard CSV with headers |
|
|
320
|
+
| `parquet` | `.parquet` | columnar format via hyparquet-writer |
|
|
321
|
+
|
|
322
|
+
all formats support gzip compression (`gzip: true`).
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
{
|
|
326
|
+
format: 'parquet',
|
|
327
|
+
writeToDisk: true,
|
|
328
|
+
gzip: true
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### cloud storage
|
|
333
|
+
|
|
334
|
+
you can write directly to google cloud storage by using a `gs://` path:
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
{
|
|
338
|
+
writeToDisk: 'gs://my-bucket/datasets/gaming/',
|
|
339
|
+
format: 'json'
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## OOM protection and batch mode
|
|
344
|
+
|
|
345
|
+
large datasets (2M+ events) automatically enable batch mode, which flushes data to disk in chunks to prevent out-of-memory crashes. you can configure this manually:
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
{
|
|
349
|
+
numEvents: 50_000_000,
|
|
350
|
+
batchSize: 1_000_000, // flush every 1M records
|
|
351
|
+
writeToDisk: true,
|
|
352
|
+
format: 'csv'
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
batch mode writes numbered files (`dataset-EVENTS-001.csv`, `dataset-EVENTS-002.csv`, etc.) and streams data through instead of holding everything in memory. this means you can generate datasets of arbitrary size on a machine with limited RAM.
|
|
357
|
+
|
|
358
|
+
## data model
|
|
359
|
+
|
|
360
|
+
dungeon-master generates multiple data types that mirror a real analytics implementation:
|
|
361
|
+
|
|
362
|
+
| data type | config key | description |
|
|
363
|
+
|-----------|-----------|-------------|
|
|
364
|
+
| events | `events` | timestamped user actions with arbitrary properties |
|
|
365
|
+
| user profiles | `userProps` | per-user attributes (plan, company, preferences) |
|
|
366
|
+
| super properties | `superProps` | properties attached to every event (theme, platform) |
|
|
367
|
+
| funnels | `funnels` | conversion sequences with configurable rates and ordering |
|
|
368
|
+
| group profiles | `groupKeys` + `groupProps` | B2B group analytics (companies, teams) |
|
|
369
|
+
| SCDs | `scdProps` | slowly changing dimensions (subscription tier over time) |
|
|
370
|
+
| lookup tables | `lookupTables` | dimension tables (product catalog, region mapping) |
|
|
371
|
+
| ad spend | `hasAdSpend` | daily ad spend with impressions, clicks, cost metrics |
|
|
372
|
+
| mirror datasets | `mirrorProps` | transformed copies of event data (A/B versions) |
|
|
373
|
+
| organic text | `createTextGenerator` | reviews, support tickets, search queries, chat messages |
|
|
374
|
+
|
|
375
|
+
## funnels
|
|
376
|
+
|
|
377
|
+
funnels define conversion sequences. users enter a funnel, and at each step some percentage drops off. the ordering strategy controls how events within the funnel are sequenced:
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
funnels: [
|
|
381
|
+
{
|
|
382
|
+
sequence: ['page view', 'sign up', 'onboarding', 'first action'],
|
|
383
|
+
conversionRate: 35,
|
|
384
|
+
order: 'sequential', // strict left-to-right ordering
|
|
385
|
+
timeToConvert: 24, // hours between steps
|
|
386
|
+
isFirstFunnel: true, // this is the entry funnel
|
|
387
|
+
experiment: true // generates A/B/C variants automatically
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
sequence: ['view item', 'add to cart', 'checkout'],
|
|
391
|
+
conversionRate: 20,
|
|
392
|
+
order: 'first-and-last-fixed', // first and last steps are fixed, middle shuffled
|
|
393
|
+
timeToConvert: 48,
|
|
394
|
+
props: { source: 'organic' }, // constant props on all funnel events
|
|
395
|
+
bindPropsIndex: 1 // props bind at step 1 and persist through
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
ordering strategies: `sequential`, `random`, `first-fixed`, `last-fixed`, `first-and-last-fixed`, `middle-fixed`, `interrupted`
|
|
401
|
+
|
|
402
|
+
## user generation
|
|
403
|
+
|
|
404
|
+
users are generated with configurable birth distributions. `percentUsersBornInDataset` controls how many users were "created" during the dataset window vs. pre-existing. `bornRecentBias` skews new user creation toward recent dates (0 = uniform, 1 = heavily recent).
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
{
|
|
408
|
+
numUsers: 10_000,
|
|
409
|
+
percentUsersBornInDataset: 25, // 25% of users sign up during the time window
|
|
410
|
+
bornRecentBias: 0.5 // new users skew toward recent dates
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## seeded generation
|
|
415
|
+
|
|
416
|
+
all randomness is seeded. same seed + same config + concurrency=1 = identical output every time:
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
{
|
|
420
|
+
seed: 'my-reproducible-dataset',
|
|
421
|
+
concurrency: 1
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## what gets generated
|
|
426
|
+
|
|
427
|
+
the result object contains everything:
|
|
428
|
+
|
|
429
|
+
```javascript
|
|
430
|
+
const result = await DUNGEON_MASTER(config);
|
|
431
|
+
|
|
432
|
+
result.eventData // all generated events
|
|
433
|
+
result.userProfilesData // user profiles
|
|
434
|
+
result.scdTableData // SCD mutations
|
|
435
|
+
result.groupProfilesData // group profiles
|
|
436
|
+
result.adSpendData // ad spend data
|
|
437
|
+
result.lookupTableData // lookup table entries
|
|
438
|
+
result.mirrorEventData // mirror dataset
|
|
439
|
+
|
|
440
|
+
result.eventCount // total event count
|
|
441
|
+
result.userCount // total user count
|
|
442
|
+
result.files // written file paths (if writeToDisk)
|
|
443
|
+
result.time // { start, end, delta, human }
|
|
444
|
+
result.importResults // mixpanel import results (if token provided)
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## text generation
|
|
448
|
+
|
|
449
|
+
dungeon-master includes a built-in text generator for creating organic-looking strings (support tickets, product reviews, search queries, chat messages, etc.) with configurable sentiment, style, and keyword injection:
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
import { createTextGenerator, generateBatch } from '@ak--47/dungeon-master/text';
|
|
453
|
+
|
|
454
|
+
const generator = createTextGenerator({
|
|
455
|
+
style: 'review',
|
|
456
|
+
tone: 'pos',
|
|
457
|
+
keywords: { products: ['AcmeWidget', 'ProPlan'], features: ['dashboard', 'API'] },
|
|
458
|
+
keywordDensity: 0.3,
|
|
459
|
+
typos: true,
|
|
460
|
+
typoRate: 0.02
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const reviews = generator.generateBatch({ n: 1000, returnType: 'objects' });
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
styles: `support`, `review`, `search`, `feedback`, `chat`, `email`, `forum`, `comments`, `tweet`
|
|
467
|
+
|
|
468
|
+
## scripts
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
npm run dungeon:run # run a dungeon file locally
|
|
472
|
+
npm run dungeon:to-json # convert JS dungeon to JSON (for UI import)
|
|
473
|
+
npm run dungeon:from-json # convert JSON to JS dungeon
|
|
474
|
+
npm test # vitest test suite
|
|
475
|
+
npm run typecheck # typescript check
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## config reference
|
|
479
|
+
|
|
480
|
+
see [types.d.ts](types.d.ts) for the complete `Dungeon` interface. here are the most commonly used properties:
|
|
481
|
+
|
|
482
|
+
| property | type | default | description |
|
|
483
|
+
|----------|------|---------|-------------|
|
|
484
|
+
| `numUsers` | number | 1000 | number of users to generate |
|
|
485
|
+
| `numEvents` | number | 100000 | target event count |
|
|
486
|
+
| `numDays` | number | 30 | days the dataset spans |
|
|
487
|
+
| `seed` | string | random | RNG seed for reproducibility |
|
|
488
|
+
| `format` | string | `'csv'` | output format (csv, json, parquet) |
|
|
489
|
+
| `token` | string | null | mixpanel project token (triggers import) |
|
|
490
|
+
| `region` | string | `'US'` | mixpanel data residency |
|
|
491
|
+
| `writeToDisk` | boolean/string | false | write files to ./data/ or a gs:// path |
|
|
492
|
+
| `gzip` | boolean | false | compress output files |
|
|
493
|
+
| `verbose` | boolean | false | print progress |
|
|
494
|
+
| `strictEventCount` | boolean | false | stop at exact numEvents |
|
|
495
|
+
| `batchSize` | number | 2500000 | records before auto-flush |
|
|
496
|
+
| `concurrency` | number | 1 | parallel user generation |
|
|
497
|
+
| `soup` | string/object | `'growth'` | time distribution preset |
|
|
498
|
+
| `bornRecentBias` | number | 0.3 | user birth date skew (0-1) |
|
|
499
|
+
| `percentUsersBornInDataset` | number | 15 | % of users born in time window |
|
|
500
|
+
| `hook` | function/string | passthrough | data transformation function |
|
|
501
|
+
| `hasLocation` | boolean | false | include geo properties |
|
|
502
|
+
| `hasCampaigns` | boolean | false | include UTM properties |
|
|
503
|
+
| `hasAdSpend` | boolean | false | generate ad spend data |
|
|
504
|
+
| `hasAnonIds` | boolean | false | generate anonymous IDs |
|
|
505
|
+
| `hasSessionIds` | boolean | false | generate session IDs |
|
|
506
|
+
| `alsoInferFunnels` | boolean | false | auto-generate funnels from events |
|
|
507
|
+
|
|
508
|
+
## why
|
|
509
|
+
|
|
510
|
+
i'm building a system that teaches LLMs how to find insights in high-volume, noisy, semi-structured event data. to do that, you need datasets where the patterns are known, complex, and realistic. dungeon-master is that dataset generator.
|
|
511
|
+
|
|
512
|
+
it's the very best kind of test data: the kind where you have the answer key. you know exactly what patterns are in there because you engineered them. and the noise around those patterns is realistic because the time distributions, property weights, and user behaviors are modeled from real analytics data.
|
|
513
|
+
|
|
514
|
+
pretty much any engineer who works with product analytics, data pipelines, or AI/ML would find this useful. it's also great for demos, load testing, and integration testing against any system that ingests event data.
|
|
515
|
+
|
|
516
|
+
## contributing
|
|
517
|
+
|
|
518
|
+
contributions welcome. for issues or feature requests: [github.com/ak--47/dungeon-master/issues](https://github.com/ak--47/dungeon-master/issues)
|