@ak--47/dungeon-master 1.3.1 → 1.4.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/CHANGELOG.md +58 -0
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/vertical/ai-platform.js +45 -52
- package/dungeons/vertical/community.js +11 -8
- package/dungeons/vertical/crypto.js +25 -24
- package/dungeons/vertical/dating.js +56 -48
- package/dungeons/vertical/devtools.js +25 -18
- package/dungeons/vertical/ecommerce.js +42 -38
- package/dungeons/vertical/education.js +24 -9
- package/dungeons/vertical/fintech.js +13 -8
- package/dungeons/vertical/fitness.js +73 -122
- package/dungeons/vertical/food-delivery.js +18 -19
- package/dungeons/vertical/gaming.js +19 -20
- package/dungeons/vertical/healthcare.js +11 -8
- package/dungeons/vertical/insurance-application.js +6 -3
- package/dungeons/vertical/logistics.js +15 -9
- package/dungeons/vertical/marketplace.js +36 -27
- package/dungeons/vertical/media.js +27 -25
- package/dungeons/vertical/real-estate.js +18 -7
- package/dungeons/vertical/sass.js +84 -68
- package/dungeons/vertical/social.js +46 -47
- package/dungeons/vertical/travel.js +8 -5
- package/lib/core/config-validator.js +136 -157
- package/lib/generators/events.js +49 -93
- package/lib/generators/funnels.js +202 -91
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +119 -269
- package/lib/utils/utils.js +29 -16
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/types.d.ts +397 -211
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Frequency × Frequency.
|
|
3
|
+
*
|
|
4
|
+
* Engineers the joint distribution of `count(metricEvent)` × `count(cohortEvent)`
|
|
5
|
+
* per user, so Mixpanel's "Frequency Distribution of A by per-user count of B"
|
|
6
|
+
* Insights view shows a deliberate shape (e.g., users with 5–20 cohort events
|
|
7
|
+
* have 2x the metric event count of users with <5).
|
|
8
|
+
*
|
|
9
|
+
* Mechanism: classify the user into a bin based on their `cohortEvent` count,
|
|
10
|
+
* then `scaleEventCount(events, targetEvent, multipliers[bin])` to scale that
|
|
11
|
+
* user's count of the target event up or down.
|
|
12
|
+
*
|
|
13
|
+
* Identity & schema constraints:
|
|
14
|
+
* - Operates on the user's full event stream — call from the `everything` hook.
|
|
15
|
+
* - Does NOT add new properties; uses existing event names defined in the dungeon
|
|
16
|
+
* schema.
|
|
17
|
+
* - Cloned events have their `insert_id` stripped (mutate.scaleEventCount handles
|
|
18
|
+
* that), so the engine's batch writer can re-stamp them downstream.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { binUsersByEventCount } from '../hook-helpers/cohort.js';
|
|
22
|
+
import { scaleEventCount } from '../hook-helpers/mutate.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Array<Object>} events - User's event stream (mutated in place).
|
|
26
|
+
* @param {Object} _profile - User profile (unused, kept for API symmetry).
|
|
27
|
+
* @param {Object} opts
|
|
28
|
+
* @param {string} opts.cohortEvent - Event whose per-user count classifies the user.
|
|
29
|
+
* @param {Record<string, [number, number]>} opts.bins - Bin name → [lo, hi).
|
|
30
|
+
* @param {string} opts.targetEvent - Event whose count is scaled per bin.
|
|
31
|
+
* @param {Record<string, number>} opts.multipliers - Bin name → multiplier (1 = no-op,
|
|
32
|
+
* 2 = double, 0.5 = halve). Bins absent from this map use multiplier 1.
|
|
33
|
+
* @returns {{ bin: string|null, delta: number }} Bin assigned + signed delta from
|
|
34
|
+
* scaleEventCount (positive = clones added; negative = events dropped).
|
|
35
|
+
*/
|
|
36
|
+
export function applyFrequencyByFrequency(events, _profile, { cohortEvent, bins, targetEvent, multipliers }) {
|
|
37
|
+
if (!events || !cohortEvent || !bins || !targetEvent || !multipliers) {
|
|
38
|
+
return { bin: null, delta: 0 };
|
|
39
|
+
}
|
|
40
|
+
const bin = binUsersByEventCount(events, cohortEvent, bins);
|
|
41
|
+
if (!bin) return { bin: null, delta: 0 };
|
|
42
|
+
const factor = multipliers[bin];
|
|
43
|
+
if (typeof factor !== 'number' || factor === 1) return { bin, delta: 0 };
|
|
44
|
+
const delta = scaleEventCount(events, targetEvent, factor);
|
|
45
|
+
return { bin, delta };
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Funnel Frequency Breakdown.
|
|
3
|
+
*
|
|
4
|
+
* Inside a `funnel-post` hook, vary the user's completion of the funnel by their
|
|
5
|
+
* count of `cohortEvent` (anywhere in the dataset, not just the funnel). Used
|
|
6
|
+
* when you want Mixpanel's funnel report — broken down by per-user count of an
|
|
7
|
+
* activity event — to show e.g. "users who did 5+ X are 1.4x as likely to
|
|
8
|
+
* complete this funnel."
|
|
9
|
+
*
|
|
10
|
+
* Mechanism: for users in a "drop-prone" bin, drop the funnel's final step
|
|
11
|
+
* event(s) per the `dropMultipliers` config (1 = drop none, 0 = drop all).
|
|
12
|
+
*
|
|
13
|
+
* Schema-first: does not add new event properties or invent events. Operates on
|
|
14
|
+
* the funnelEvents array passed by the funnel-post hook.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { binUsersByEventCount } from '../hook-helpers/cohort.js';
|
|
18
|
+
import { dropEventsWhere } from '../hook-helpers/mutate.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Array<Object>} allUserEvents - Full per-user event history (read-only;
|
|
22
|
+
* used to count `cohortEvent`). When called inside `funnel-post`, derive this
|
|
23
|
+
* from `meta.profile` or pass the user's accumulated events from a closure.
|
|
24
|
+
* When `null`, falls back to counting cohortEvent inside `funnelEvents`.
|
|
25
|
+
* @param {Object} _profile
|
|
26
|
+
* @param {Array<Object>} funnelEvents - Funnel events produced by `makeFunnel`
|
|
27
|
+
* (mutated in place).
|
|
28
|
+
* @param {Object} opts
|
|
29
|
+
* @param {string} opts.cohortEvent
|
|
30
|
+
* @param {Record<string, [number, number]>} opts.bins
|
|
31
|
+
* @param {Record<string, number>} opts.dropMultipliers - Bin name → keep-rate (0..1)
|
|
32
|
+
* for the FINAL step event. 1 = always keep, 0 = always drop.
|
|
33
|
+
* @param {string} [opts.finalStep] - Event name of the final step. Defaults to the
|
|
34
|
+
* last event in `funnelEvents` (in time order).
|
|
35
|
+
* @returns {{ bin: string|null, droppedFinal: boolean }}
|
|
36
|
+
*/
|
|
37
|
+
export function applyFunnelFrequencyBreakdown(allUserEvents, _profile, funnelEvents, opts) {
|
|
38
|
+
const { cohortEvent, bins, dropMultipliers, finalStep } = opts || {};
|
|
39
|
+
if (!funnelEvents || !cohortEvent || !bins || !dropMultipliers) {
|
|
40
|
+
return { bin: null, droppedFinal: false };
|
|
41
|
+
}
|
|
42
|
+
const sourceForBin = allUserEvents || funnelEvents;
|
|
43
|
+
const bin = binUsersByEventCount(sourceForBin, cohortEvent, bins);
|
|
44
|
+
if (!bin) return { bin: null, droppedFinal: false };
|
|
45
|
+
const keepRate = dropMultipliers[bin];
|
|
46
|
+
if (typeof keepRate !== 'number' || keepRate >= 1) return { bin, droppedFinal: false };
|
|
47
|
+
|
|
48
|
+
// Identify the final step. If finalStep is named, use it; otherwise pick the
|
|
49
|
+
// latest-in-time event in the funnel as the final step.
|
|
50
|
+
let stepName = finalStep;
|
|
51
|
+
if (!stepName) {
|
|
52
|
+
const sorted = funnelEvents.slice().sort((a, b) => Date.parse(a.time) - Date.parse(b.time));
|
|
53
|
+
stepName = sorted.length ? sorted[sorted.length - 1].event : null;
|
|
54
|
+
}
|
|
55
|
+
if (!stepName) return { bin, droppedFinal: false };
|
|
56
|
+
|
|
57
|
+
// Coin-flip drop using a deterministic-ish heuristic — this is called from a
|
|
58
|
+
// non-RNG context (funnel-post), so use Math.random would break determinism.
|
|
59
|
+
// Instead, use a hash on the funnel's first event's insert_id (deterministic
|
|
60
|
+
// per-call) modulo 1000 / 1000 vs. (1 - keepRate). For simplicity we use
|
|
61
|
+
// chance from utils when available.
|
|
62
|
+
const dropProb = 1 - keepRate;
|
|
63
|
+
const seed = funnelEvents[0] && (funnelEvents[0].insert_id || funnelEvents[0].time) || '';
|
|
64
|
+
const det = simpleHashFloat(String(seed));
|
|
65
|
+
if (det < dropProb) {
|
|
66
|
+
const before = funnelEvents.length;
|
|
67
|
+
dropEventsWhere(funnelEvents, e => e && e.event === stepName);
|
|
68
|
+
return { bin, droppedFinal: funnelEvents.length < before };
|
|
69
|
+
}
|
|
70
|
+
return { bin, droppedFinal: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
import { simpleHashFloat } from '../hook-helpers/_internal.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ak--47/dungeon-master/hook-patterns — Phase 4 pattern barrel.
|
|
3
|
+
*
|
|
4
|
+
* Patterns are higher-level recipes built on Phase 3 atoms. Each one engineers
|
|
5
|
+
* the kind of distribution / table shape Mixpanel surfaces in a specific report.
|
|
6
|
+
* Pair with `verifyDungeon` + `emulateBreakdown` from `../verify` to assert the
|
|
7
|
+
* pattern is producing what you expect.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { applyFrequencyByFrequency } from './frequency-by-frequency.js';
|
|
11
|
+
export { applyFunnelFrequencyBreakdown } from './funnel-frequency-breakdown.js';
|
|
12
|
+
export { applyAggregateByBin } from './aggregate-per-user-by-bin.js';
|
|
13
|
+
export { applyTTCBySegment } from './time-to-convert-by-segment.js';
|
|
14
|
+
export { applyAttributedBySource } from './attributed-by-source.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Time to Convert, broken down by user segment.
|
|
3
|
+
*
|
|
4
|
+
* Inside a `funnel-post` hook, scale the funnel's time-to-convert by a factor
|
|
5
|
+
* keyed off of a user-profile property value (e.g., trial users convert 3x
|
|
6
|
+
* slower than enterprise). Lets Mixpanel's TTC funnel report — broken down by
|
|
7
|
+
* a profile property — show a deliberate spread.
|
|
8
|
+
*
|
|
9
|
+
* Mechanism: read `profile[segmentKey]`, look up the multiplier in `factors`,
|
|
10
|
+
* and `scaleFunnelTTC(funnelEvents, factor)`. The first event's time is the
|
|
11
|
+
* anchor (unchanged); subsequent steps' offsets from it are scaled.
|
|
12
|
+
*
|
|
13
|
+
* Caveat: Mixpanel's "Time to Convert" funnel report uses the time between the
|
|
14
|
+
* FIRST event of step A and the FIRST event of step B per user, not the actual
|
|
15
|
+
* gap inside any one funnel run. The scaled funnel run will reflect in TTC only
|
|
16
|
+
* when this funnel is the user's first occurrence of those steps — which it is
|
|
17
|
+
* for an `isFirstFunnel`. For usage funnels, document this caveat to authors.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { scaleFunnelTTC } from '../hook-helpers/timing.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Array<Object>} funnelEvents - Mutated in place.
|
|
24
|
+
* @param {Object} profile - The user's profile (must contain `segmentKey`).
|
|
25
|
+
* @param {Object} opts
|
|
26
|
+
* @param {string} opts.segmentKey - Profile property name to look up.
|
|
27
|
+
* @param {Record<string, number>} opts.factors - Profile-value → TTC factor.
|
|
28
|
+
* @returns {{ segmentValue: any, factor: number, shifted: number }}
|
|
29
|
+
*/
|
|
30
|
+
export function applyTTCBySegment(funnelEvents, profile, { segmentKey, factors }) {
|
|
31
|
+
if (!funnelEvents || !funnelEvents.length || !profile || !segmentKey || !factors) {
|
|
32
|
+
return { segmentValue: null, factor: 1, shifted: 0 };
|
|
33
|
+
}
|
|
34
|
+
const segmentValue = profile[segmentKey];
|
|
35
|
+
const factor = factors[segmentValue];
|
|
36
|
+
if (typeof factor !== 'number' || factor === 1) {
|
|
37
|
+
return { segmentValue, factor: 1, shifted: 0 };
|
|
38
|
+
}
|
|
39
|
+
const shifted = scaleFunnelTTC(funnelEvents, factor);
|
|
40
|
+
return { segmentValue, factor, shifted };
|
|
41
|
+
}
|