@ak--47/dungeon-master 1.4.2 → 1.4.4
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/CHANGELOG.md +71 -0
- package/HOOKS.md +1138 -0
- package/dungeons/vertical/community.js +75 -6
- package/dungeons/vertical/crypto.js +63 -8
- package/dungeons/vertical/dating.js +36 -4
- package/dungeons/vertical/devtools.js +25 -0
- package/dungeons/vertical/ecommerce.js +260 -31
- package/dungeons/vertical/education.js +107 -39
- package/dungeons/vertical/fintech.js +28 -28
- package/dungeons/vertical/fitness.js +48 -34
- package/dungeons/vertical/food-delivery.js +112 -30
- package/dungeons/vertical/gaming.js +48 -58
- package/dungeons/vertical/healthcare.js +47 -37
- package/dungeons/vertical/insurance-application.js +342 -61
- package/dungeons/vertical/logistics.js +37 -5
- package/dungeons/vertical/marketplace.js +42 -18
- package/dungeons/vertical/media.js +125 -57
- package/dungeons/vertical/real-estate.js +25 -0
- package/dungeons/vertical/sass.js +161 -20
- package/dungeons/vertical/social.js +25 -0
- package/dungeons/vertical/travel.js +74 -3
- package/index.js +2 -1
- package/lib/core/config-validator.js +2 -2
- package/lib/orchestrators/mixpanel-sender.js +3 -2
- package/lib/utils/utils.js +118 -24
- package/lib/verify/emulate-breakdown.js +1 -1
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +258 -0
- package/lib/verify/verify-dungeon.js +6 -5
- package/package.json +3 -2
- package/scripts/verify-runner.mjs +1 -1
- package/types.d.ts +29 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Mixpanel breakdown emulator.
|
|
3
3
|
*
|
|
4
4
|
* Best-effort approximation of the table shapes Mixpanel produces for the five
|
|
5
|
-
* analyses the Phase 4 hook patterns target. Used by `verify-
|
|
5
|
+
* analyses the Phase 4 hook patterns target. Used by `verify-dungeon` to assert
|
|
6
6
|
* that engineered patterns actually produce the expected ratios in the data, AND
|
|
7
7
|
* by consumers who want to validate dungeons against expected business shapes
|
|
8
8
|
* outside of Mixpanel.
|
package/lib/verify/index.js
CHANGED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema validation for dungeon output.
|
|
3
|
+
*
|
|
4
|
+
* Derives the expected set of property keys per event type from a dungeon
|
|
5
|
+
* config, then compares against actual output to catch hooks that introduce
|
|
6
|
+
* undeclared columns ("flag stamping").
|
|
7
|
+
*
|
|
8
|
+
* Rule: a hook-introduced column is acceptable (PASS) only if it appears on
|
|
9
|
+
* 100% of events of that type. Partial coverage is a FAIL.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @typedef {import('../../types.js').Dungeon} Dungeon */
|
|
13
|
+
|
|
14
|
+
const CORE_KEYS = new Set(['event', 'time', 'insert_id', 'user_id']);
|
|
15
|
+
const LOCATION_KEYS = ['city', 'region', 'country', 'country_code'];
|
|
16
|
+
const DEVICE_KEYS = ['model', 'screen_height', 'screen_width', 'os', 'Platform', 'carrier', 'radio'];
|
|
17
|
+
const CAMPAIGN_KEYS = ['utm_source', 'utm_campaign', 'utm_medium', 'utm_content', 'utm_term'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive the expected property keys per event type from config alone.
|
|
21
|
+
* @param {Dungeon} config
|
|
22
|
+
* @returns {Map<string, Set<string>>} eventName → set of expected property keys
|
|
23
|
+
*/
|
|
24
|
+
export function deriveExpectedSchema(config) {
|
|
25
|
+
const globalKeys = new Set(CORE_KEYS);
|
|
26
|
+
/** @type {Map<string, Set<string>>} */
|
|
27
|
+
const perType = new Map();
|
|
28
|
+
|
|
29
|
+
if (config.avgDevicePerUser > 0 || config.hasAnonIds) {
|
|
30
|
+
globalKeys.add('device_id');
|
|
31
|
+
}
|
|
32
|
+
if (config.hasSessionIds) {
|
|
33
|
+
globalKeys.add('session_id');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.superProps) {
|
|
37
|
+
for (const key of Object.keys(config.superProps)) {
|
|
38
|
+
globalKeys.add(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (config.hasLocation) {
|
|
43
|
+
for (const k of LOCATION_KEYS) globalKeys.add(k);
|
|
44
|
+
}
|
|
45
|
+
if (config.hasBrowser) {
|
|
46
|
+
globalKeys.add('browser');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hasDevices = config.hasAndroidDevices || config.hasIOSDevices ||
|
|
50
|
+
config.hasDesktopDevices || (config.avgDevicePerUser && config.avgDevicePerUser > 0);
|
|
51
|
+
if (hasDevices) {
|
|
52
|
+
for (const k of DEVICE_KEYS) globalKeys.add(k);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (config.hasCampaigns) {
|
|
56
|
+
for (const k of CAMPAIGN_KEYS) globalKeys.add(k);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.personas) {
|
|
60
|
+
globalKeys.add('_persona');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (config.dataQuality) {
|
|
64
|
+
globalKeys.add('_drop');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const events = config.events || [];
|
|
68
|
+
for (const ev of events) {
|
|
69
|
+
const keys = new Set();
|
|
70
|
+
if (ev.properties) {
|
|
71
|
+
for (const k of Object.keys(ev.properties)) {
|
|
72
|
+
keys.add(k);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
perType.set(ev.event, keys);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Group keys — per event type or global
|
|
79
|
+
if (config.groupKeys && Array.isArray(config.groupKeys)) {
|
|
80
|
+
for (const groupPair of config.groupKeys) {
|
|
81
|
+
const groupKey = groupPair[0];
|
|
82
|
+
const groupEvents = groupPair[2] || [];
|
|
83
|
+
if (!groupEvents.length) {
|
|
84
|
+
globalKeys.add(groupKey);
|
|
85
|
+
} else {
|
|
86
|
+
for (const eventName of groupEvents) {
|
|
87
|
+
ensurePerType(perType, eventName).add(groupKey);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Funnel props — applied to events in funnel sequences
|
|
94
|
+
if (config.funnels && Array.isArray(config.funnels)) {
|
|
95
|
+
for (const funnel of config.funnels) {
|
|
96
|
+
if (funnel.props && Object.keys(funnel.props).length) {
|
|
97
|
+
const funnelPropKeys = Object.keys(funnel.props);
|
|
98
|
+
for (const eventName of (funnel.sequence || [])) {
|
|
99
|
+
if (eventName === '$experiment_started') continue;
|
|
100
|
+
for (const k of funnelPropKeys) {
|
|
101
|
+
ensurePerType(perType, eventName).add(k);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// $experiment_started event from experiment funnels
|
|
106
|
+
if (funnel.experiment) {
|
|
107
|
+
const expKeys = ensurePerType(perType, '$experiment_started');
|
|
108
|
+
expKeys.add('Experiment name');
|
|
109
|
+
expKeys.add('Variant name');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// World event injected props
|
|
115
|
+
if (config.worldEvents && Array.isArray(config.worldEvents)) {
|
|
116
|
+
for (const we of config.worldEvents) {
|
|
117
|
+
if (we.injectProps) {
|
|
118
|
+
const injectedKeys = Object.keys(we.injectProps);
|
|
119
|
+
const affects = we.affectsEvents;
|
|
120
|
+
if (affects === '*') {
|
|
121
|
+
for (const k of injectedKeys) globalKeys.add(k);
|
|
122
|
+
} else if (Array.isArray(affects)) {
|
|
123
|
+
for (const eventName of affects) {
|
|
124
|
+
for (const k of injectedKeys) {
|
|
125
|
+
ensurePerType(perType, eventName).add(k);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build final schema: for each event type, merge global + per-type
|
|
134
|
+
const schema = new Map();
|
|
135
|
+
for (const [eventName, typeKeys] of perType) {
|
|
136
|
+
const merged = new Set(globalKeys);
|
|
137
|
+
for (const k of typeKeys) merged.add(k);
|
|
138
|
+
schema.set(eventName, merged);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Event types that only appear in funnels but not in events[] config
|
|
142
|
+
// (e.g. $experiment_started) should still be in the schema
|
|
143
|
+
for (const [eventName, typeKeys] of perType) {
|
|
144
|
+
if (!schema.has(eventName)) {
|
|
145
|
+
const merged = new Set(globalKeys);
|
|
146
|
+
for (const k of typeKeys) merged.add(k);
|
|
147
|
+
schema.set(eventName, merged);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return schema;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {Object} EventTypeReport
|
|
156
|
+
* @property {string[]} expected
|
|
157
|
+
* @property {string[]} actual
|
|
158
|
+
* @property {string[]} added
|
|
159
|
+
* @property {string[]} missing
|
|
160
|
+
* @property {Object<string, {count: number, total: number, pct: number}>} coverage
|
|
161
|
+
* @property {'PASS'|'FAIL'} verdict
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @typedef {Object} SchemaReport
|
|
166
|
+
* @property {boolean} pass
|
|
167
|
+
* @property {Object<string, EventTypeReport>} eventTypes
|
|
168
|
+
* @property {{pass: number, fail: number}} summary
|
|
169
|
+
* @property {Array<{eventType: string, column: string, coverage: number}>} flagStamping
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate generated events against the config-derived schema.
|
|
174
|
+
* @param {Object[]} events — flat event objects from dungeon output
|
|
175
|
+
* @param {Dungeon} config — the dungeon config (pre- or post-validation)
|
|
176
|
+
* @returns {SchemaReport}
|
|
177
|
+
*/
|
|
178
|
+
export function validateSchema(events, config) {
|
|
179
|
+
const expectedSchema = deriveExpectedSchema(config);
|
|
180
|
+
|
|
181
|
+
// Group events by type
|
|
182
|
+
/** @type {Map<string, Object[]>} */
|
|
183
|
+
const byType = new Map();
|
|
184
|
+
for (const ev of events) {
|
|
185
|
+
const name = ev.event;
|
|
186
|
+
if (!name) continue;
|
|
187
|
+
if (!byType.has(name)) byType.set(name, []);
|
|
188
|
+
byType.get(name).push(ev);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @type {Object<string, EventTypeReport>} */
|
|
192
|
+
const eventTypes = {};
|
|
193
|
+
/** @type {Array<{eventType: string, column: string, coverage: number}>} */
|
|
194
|
+
const flagStamping = [];
|
|
195
|
+
let passCount = 0;
|
|
196
|
+
let failCount = 0;
|
|
197
|
+
|
|
198
|
+
for (const [eventName, eventsOfType] of byType) {
|
|
199
|
+
const expected = expectedSchema.get(eventName) || new Set(CORE_KEYS);
|
|
200
|
+
|
|
201
|
+
// Collect all actual keys across events of this type
|
|
202
|
+
const actualKeys = new Set();
|
|
203
|
+
for (const ev of eventsOfType) {
|
|
204
|
+
for (const k of Object.keys(ev)) {
|
|
205
|
+
actualKeys.add(k);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const expectedArr = Array.from(expected).sort();
|
|
210
|
+
const actualArr = Array.from(actualKeys).sort();
|
|
211
|
+
const added = actualArr.filter(k => !expected.has(k));
|
|
212
|
+
const missing = expectedArr.filter(k => !actualKeys.has(k));
|
|
213
|
+
|
|
214
|
+
// Check coverage for added columns
|
|
215
|
+
/** @type {Object<string, {count: number, total: number, pct: number}>} */
|
|
216
|
+
const coverage = {};
|
|
217
|
+
let hasFailure = false;
|
|
218
|
+
const total = eventsOfType.length;
|
|
219
|
+
|
|
220
|
+
for (const col of added) {
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const ev of eventsOfType) {
|
|
223
|
+
if (ev[col] !== undefined) count++;
|
|
224
|
+
}
|
|
225
|
+
const pct = Math.round((count / total) * 10000) / 100;
|
|
226
|
+
coverage[col] = { count, total, pct };
|
|
227
|
+
if (pct < 100) {
|
|
228
|
+
hasFailure = true;
|
|
229
|
+
flagStamping.push({ eventType: eventName, column: col, coverage: pct });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const verdict = hasFailure ? 'FAIL' : 'PASS';
|
|
234
|
+
if (verdict === 'PASS') passCount++;
|
|
235
|
+
else failCount++;
|
|
236
|
+
|
|
237
|
+
eventTypes[eventName] = {
|
|
238
|
+
expected: expectedArr,
|
|
239
|
+
actual: actualArr,
|
|
240
|
+
added,
|
|
241
|
+
missing,
|
|
242
|
+
coverage,
|
|
243
|
+
verdict,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
pass: failCount === 0,
|
|
249
|
+
eventTypes,
|
|
250
|
+
summary: { pass: passCount, fail: failCount },
|
|
251
|
+
flagStamping,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function ensurePerType(perType, eventName) {
|
|
256
|
+
if (!perType.has(eventName)) perType.set(eventName, new Set());
|
|
257
|
+
return perType.get(eventName);
|
|
258
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import DUNGEON_MASTER from '../../index.js';
|
|
23
23
|
import { emulateBreakdown } from './emulate-breakdown.js';
|
|
24
|
+
import { validateSchema } from './schema-validator.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* @typedef {Object} VerifyCheck
|
|
@@ -32,7 +33,7 @@ import { emulateBreakdown } from './emulate-breakdown.js';
|
|
|
32
33
|
/**
|
|
33
34
|
* @param {Object} config - Dungeon config (or path; passed straight to DUNGEON_MASTER).
|
|
34
35
|
* @param {VerifyCheck[]} checks
|
|
35
|
-
* @returns {Promise<{ pass: boolean, results: Array<{ name: string, pass: boolean, detail?: string, rows?: Array<Object> }
|
|
36
|
+
* @returns {Promise<{ pass: boolean, results: Array<{ name: string, pass: boolean, detail?: string, rows?: Array<Object> }>, schemaReport: Object }>}
|
|
36
37
|
*/
|
|
37
38
|
export async function verifyDungeon(config, checks) {
|
|
38
39
|
if (!checks || !checks.length) throw new Error('verifyDungeon: at least one check required');
|
|
@@ -40,12 +41,12 @@ export async function verifyDungeon(config, checks) {
|
|
|
40
41
|
if (Array.isArray(result)) result = result[0];
|
|
41
42
|
const events = Array.isArray(result.eventData) ? result.eventData : Array.from(result.eventData);
|
|
42
43
|
const profiles = Array.isArray(result.userProfilesData) ? result.userProfilesData : Array.from(result.userProfilesData);
|
|
43
|
-
const
|
|
44
|
+
const schemaReport = validateSchema(events, config);
|
|
45
|
+
const ctx = { events, profiles, schemaReport };
|
|
44
46
|
const results = [];
|
|
45
47
|
for (const check of checks) {
|
|
46
48
|
try {
|
|
47
49
|
const breakdownArgs = { ...check.breakdown };
|
|
48
|
-
// timeToConvert + attributedBy may want profiles; auto-inject if not provided.
|
|
49
50
|
if (breakdownArgs.type === 'timeToConvert' && !breakdownArgs.profiles) {
|
|
50
51
|
breakdownArgs.profiles = profiles;
|
|
51
52
|
}
|
|
@@ -56,6 +57,6 @@ export async function verifyDungeon(config, checks) {
|
|
|
56
57
|
results.push({ name: check.name, pass: false, detail: `error: ${err.message}` });
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
const pass = results.every(r => r.pass);
|
|
60
|
-
return { pass, results };
|
|
60
|
+
const pass = results.every(r => r.pass) && schemaReport.pass;
|
|
61
|
+
return { pass, results, schemaReport };
|
|
61
62
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"scripts/",
|
|
27
27
|
"package.json",
|
|
28
28
|
"README.md",
|
|
29
|
-
"CHANGELOG.md"
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"HOOKS.md"
|
|
30
31
|
],
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=18.0.0"
|
|
@@ -29,7 +29,7 @@ if (!dungeonPath) {
|
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const runName = positional[1] || 'verify-
|
|
32
|
+
const runName = positional[1] || 'verify-dungeon';
|
|
33
33
|
const isSmall = flags.has('--small');
|
|
34
34
|
const absolutePath = path.isAbsolute(dungeonPath)
|
|
35
35
|
? dungeonPath
|
package/types.d.ts
CHANGED
|
@@ -1570,5 +1570,33 @@ export interface EmulateOptions {
|
|
|
1570
1570
|
|
|
1571
1571
|
declare module '@ak--47/dungeon-master/verify' {
|
|
1572
1572
|
export function emulateBreakdown(events: EventSchema[], config: EmulateOptions): Array<Record<string, unknown>>;
|
|
1573
|
-
export function verifyDungeon(config: Dungeon, checks: Array<{ name: string; breakdown: EmulateOptions; assert: (rows: Array<Record<string, unknown>>, ctx: { events: EventSchema[]; profiles: UserProfile[] }) => { pass: boolean; detail?: string } }>): Promise<{ pass: boolean; results: Array<{ name: string; pass: boolean; detail?: string; rows?: Array<Record<string, unknown>> }
|
|
1573
|
+
export function verifyDungeon(config: Dungeon, checks: Array<{ name: string; breakdown: EmulateOptions; assert: (rows: Array<Record<string, unknown>>, ctx: { events: EventSchema[]; profiles: UserProfile[] }) => { pass: boolean; detail?: string } }>): Promise<{ pass: boolean; results: Array<{ name: string; pass: boolean; detail?: string; rows?: Array<Record<string, unknown>> }>; schemaReport: SchemaReport }>;
|
|
1574
|
+
export function deriveExpectedSchema(config: Dungeon): Map<string, Set<string>>;
|
|
1575
|
+
export function validateSchema(events: EventSchema[], config: Dungeon): SchemaReport;
|
|
1576
|
+
|
|
1577
|
+
interface SchemaReport {
|
|
1578
|
+
pass: boolean;
|
|
1579
|
+
eventTypes: Record<string, {
|
|
1580
|
+
expected: string[];
|
|
1581
|
+
actual: string[];
|
|
1582
|
+
added: string[];
|
|
1583
|
+
missing: string[];
|
|
1584
|
+
coverage: Record<string, { count: number; total: number; pct: number }>;
|
|
1585
|
+
verdict: 'PASS' | 'FAIL';
|
|
1586
|
+
}>;
|
|
1587
|
+
summary: { pass: number; fail: number };
|
|
1588
|
+
flagStamping: Array<{ eventType: string; column: string; coverage: number }>;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
declare module '@ak--47/dungeon-master/utils' {
|
|
1593
|
+
export function dateRange(start?: string | number, end?: string | number, format?: string | null): () => string;
|
|
1594
|
+
export function listOf<T>(pool: T[], options?: { min?: number; max?: number }): () => T[];
|
|
1595
|
+
export function objectList(template: Record<string, ValueValid>, options?: { min?: number; max?: number }): () => Array<Record<string, unknown>>;
|
|
1596
|
+
export function weighNumRange(min: number, max: number, skew?: number, size?: number): number[];
|
|
1597
|
+
export function pickAWinner(items: string[], mostChosenIndex?: number): () => string[];
|
|
1598
|
+
export function initChance(seed?: string): unknown;
|
|
1599
|
+
export function TimeSoup(earliestTime: number, latestTime: number, peaks?: number, deviation?: number, mean?: number, dayOfWeekWeights?: number[] | null, hourOfDayWeights?: number[] | null): number;
|
|
1600
|
+
export function weighArray<T>(items: T[]): T[];
|
|
1601
|
+
export function generateUser(user_id: string, opts: Record<string, unknown>): Record<string, unknown>;
|
|
1574
1602
|
}
|