@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@ak--47/dungeon-master`.
|
|
4
4
|
|
|
5
|
+
## 1.4.0 — 2026-05-03
|
|
6
|
+
|
|
7
|
+
The "identity model + hook verification" release. Users get multi-device identity, declarative experiments, and a complete hook authoring pipeline with verification. All 20 vertical dungeons upgraded and verified.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Identity model.** Three additive knobs for realistic user/device identity:
|
|
12
|
+
- `avgDevicePerUser: N` — per-user device pool sized by normal distribution; sessions are sticky to a device drawn from the pool.
|
|
13
|
+
- `EventConfig.isAuthEvent: true` — marks the sign-up/login event as the identity stitch point. Pre-auth funnel steps get `device_id` only; the stitch event gets both; post-auth gets `user_id` only.
|
|
14
|
+
- `Funnel.attempts: { min, max, conversionRate? }` — failed-prior-attempt retries before final conversion. Failed attempts truncate before `isAuthEvent`.
|
|
15
|
+
- `EventConfig.isAttributionEvent: true` — opt-in UTM stamping on specific events (replaces blanket 25% of all events).
|
|
16
|
+
- **Declarative experiments.** `Funnel.experiment: { name, variants, startDaysBeforeEnd }` on any funnel. Engine handles variant assignment (deterministic hash), `$experiment_started` events, and conversion/TTC multipliers. Hooks read `meta.experiment` for variant-specific downstream effects.
|
|
17
|
+
- **Hook helpers** (`@ak--47/dungeon-master/hook-helpers`). 14 composable atoms across 5 modules: cohort binning, event mutation, timing manipulation, event injection, and identity partitioning. Full JSDoc on each atom.
|
|
18
|
+
- **Hook patterns** (`@ak--47/dungeon-master/hook-patterns`). 5 high-level recipes mapping 1:1 to Mixpanel analysis types: frequency-by-frequency, funnel-frequency breakdown, aggregate-by-bin, TTC-by-segment, attributed-by-source.
|
|
19
|
+
- **Verification pipeline** (`@ak--47/dungeon-master/verify`). `emulateBreakdown` re-derives Mixpanel breakdown tables from raw events. `verifyDungeon` runs CI-style assertions with NAILED/STRONG/WEAK/NONE/INVERSE scoring.
|
|
20
|
+
- **`HOOKS.md` encyclopedia.** 23 production-proven hook recipes with code, Mixpanel report instructions, and adaptation notes. 14 core principles. Phase 3/4 atom and pattern reference tables.
|
|
21
|
+
- **`EmulateOptions` type.** Previously referenced but undefined in `types.d.ts`. Now fully typed with per-analysis-type field documentation.
|
|
22
|
+
- **Skills pipeline.** `/create-dungeon` (schema only) → `/write-hooks` (engineer patterns) → `/verify-hooks` (DuckDB verification with 5-tier scoring). Each skill is self-contained with reference examples.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Legacy 42% per-event `user_id` dice removed.** Every event now gets `user_id` by default (unless in a pre-auth funnel step). More correct for Mixpanel identity.
|
|
27
|
+
- **`isStrictEvent` events excluded from standalone generation.** They appear only in explicitly-defined funnels — cleaner data for events like "application approved" that shouldn't exist outside funnel context.
|
|
28
|
+
- **`insert_id` uses `crypto.randomUUID()`.** Eliminates hash collisions. Non-deterministic but irrelevant for analytics.
|
|
29
|
+
- **Funnel-pre hooks have final authority.** Persona and world-event modifiers apply before the hook — hooks can override everything.
|
|
30
|
+
- **Experiment variant assignment is deterministic per user** (hash-based, not random per funnel run).
|
|
31
|
+
- **All 20 vertical dungeons upgraded to version 2.** Identity model adopted, dates standardized (120-day window), temporal hooks migrated to `everything`, deprecated features replaced in hooks, all hooks verified STRONG or NAILED.
|
|
32
|
+
- **`types.d.ts` expanded.** JSDoc warnings on `HookMetaEvent` (temporal unreliability), hook ordering note on `HookMetaEverything`, `numDays` 3-mode resolution docs, `isStrictEvent` clarification.
|
|
33
|
+
|
|
34
|
+
### Removed (silently ignored)
|
|
35
|
+
|
|
36
|
+
- `subscription`, `attribution`, `geo`, `features`, `anomalies` config keys. Engine strips them with one deprecation warning per dungeon and continues. Recreate these patterns via hooks — see `HOOKS.md` recipes 3.22 (deprecated feature replacement) and the `write-hooks` skill.
|
|
37
|
+
|
|
38
|
+
### Key Learnings from 20-Dungeon Eval
|
|
39
|
+
|
|
40
|
+
These patterns are documented in `HOOKS.md` principles 7-14 and the skill REV 10 sections:
|
|
41
|
+
|
|
42
|
+
1. **Temporal hooks belong in `everything`, not `event`.** The event hook's `meta.datasetStart`/`meta.datasetEnd` are in a different time frame than `record.time`. Move any day-in-dataset check to `everything`.
|
|
43
|
+
2. **Temporal mutations run AFTER all cloning.** If Hook A clones events into a time window and Hook B mutates events in that window, B must run after A or clones miss the mutation.
|
|
44
|
+
3. **Cohort detection must survive downstream filtering.** If a churn hook removes the marker events used to detect a cohort, require 3+ markers instead of 1+ so survivors still identify the group.
|
|
45
|
+
4. **Deprecated feature replacement.** Add equivalent property assignments in `user`/`everything` hooks and `superProps`/`userProps`. See recipe 3.22.
|
|
46
|
+
5. **Unseeded Chance breaks determinism.** Replace `new Chance()` with `initChance(SEED)`.
|
|
47
|
+
|
|
48
|
+
### Documentation
|
|
49
|
+
|
|
50
|
+
- **`research/FINALIZE-REPORT.md`** — full eval report: 19 dungeons, 191 hooks, all STRONG/NAILED.
|
|
51
|
+
- **`research/1.4.0-upgrade-guide.md`** — migration checklist and hook-writing guide.
|
|
52
|
+
- **`HOOKS.md`** — 23 recipes, 14 principles, atom/pattern reference.
|
|
53
|
+
- **`.claude/skills/verify-hooks/SKILL.md`** — REV 10: clone dilution, cohort filtering, deprecated features, dynamic date window derivation.
|
|
54
|
+
- **`.claude/skills/write-hooks/SKILL.md`** — REV 10: hook ordering within `everything`, deprecated feature replacement, cohort sizing guidelines.
|
|
55
|
+
|
|
56
|
+
### Backward Compatibility
|
|
57
|
+
|
|
58
|
+
- **No breaking changes to the public API.** `DUNGEON_MASTER(config)` signature unchanged. All named exports unchanged.
|
|
59
|
+
- Existing dungeons run without modification. New features are opt-in.
|
|
60
|
+
- Dungeons that relied on the legacy 42% `user_id` dice will see more consistent identity (every event gets `user_id`). This is more correct for Mixpanel.
|
|
61
|
+
- `numDays`-only configs continue to work (window anchors to today). Pin `datasetStart`+`datasetEnd` for deterministic runs.
|
|
62
|
+
|
|
5
63
|
## 1.3.0 — 2026-04-28
|
|
6
64
|
|
|
7
65
|
The "no more end-of-dataset blowup" release. Rewrote how per-user event budgets and big-picture trends are decided so the default chart looks flat with a weekly cycle, instead of meteoric ramp + cliff in the final ~14 days.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 hook-helpers verification fixture.
|
|
3
|
+
*
|
|
4
|
+
* Exercises three atoms inside one `everything` hook:
|
|
5
|
+
* 1. `binUsersByEventCount` — classify users into "casual" / "engaged" / "power"
|
|
6
|
+
* based on their count of `Browse` events.
|
|
7
|
+
* 2. `scalePropertyValue` — engaged users get 2x `amount` on Purchase events;
|
|
8
|
+
* power users get 3x.
|
|
9
|
+
* 3. `injectBurst` — power users also get a burst of 5 cloned `Browse` events
|
|
10
|
+
* around the midpoint of their stream.
|
|
11
|
+
*
|
|
12
|
+
* Verified by DuckDB queries against the events output (see
|
|
13
|
+
* scripts/verify-runner.mjs + the Phase 3 verification gate notes in the plan).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import dayjs from 'dayjs';
|
|
17
|
+
import {
|
|
18
|
+
binUsersByEventCount,
|
|
19
|
+
scalePropertyValue,
|
|
20
|
+
injectBurst,
|
|
21
|
+
cloneEvent,
|
|
22
|
+
} from '../../lib/hook-helpers/index.js';
|
|
23
|
+
|
|
24
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
25
|
+
|
|
26
|
+
const BINS = {
|
|
27
|
+
casual: [0, 5],
|
|
28
|
+
engaged: [5, 15],
|
|
29
|
+
power: [15, Infinity],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SCALE_BY_BIN = { casual: 1, engaged: 2, power: 3 };
|
|
33
|
+
const BURST_COUNT_BY_BIN = { casual: 0, engaged: 0, power: 5 };
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
name: 'hook-helpers-verify',
|
|
37
|
+
seed: 'phase3-helpers-verify',
|
|
38
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
39
|
+
datasetEnd: FIXED_NOW,
|
|
40
|
+
numUsers: 1_000,
|
|
41
|
+
avgEventsPerUserPerDay: 4,
|
|
42
|
+
percentUsersBornInDataset: 30,
|
|
43
|
+
hasAnonIds: false,
|
|
44
|
+
format: 'json',
|
|
45
|
+
concurrency: 1,
|
|
46
|
+
writeToDisk: true,
|
|
47
|
+
verbose: false,
|
|
48
|
+
events: [
|
|
49
|
+
{ event: 'Browse', weight: 8, properties: { surface: ['home', 'category', 'search'] } },
|
|
50
|
+
{ event: 'Purchase', weight: 2, properties: { amount: [10, 20, 30, 40, 50] } },
|
|
51
|
+
],
|
|
52
|
+
hook: function (record, type, meta) {
|
|
53
|
+
if (type !== 'everything' || !Array.isArray(record)) return record;
|
|
54
|
+
|
|
55
|
+
// 1. Classify the user via cohort atom
|
|
56
|
+
const bin = binUsersByEventCount(record, 'Browse', BINS);
|
|
57
|
+
if (!bin) return record;
|
|
58
|
+
|
|
59
|
+
// Stamp the bin onto each Purchase so verification queries can group by it
|
|
60
|
+
// without re-deriving. Non-flag-stamping: this property is config-defined when
|
|
61
|
+
// dungeons add `cohort` to Purchase.properties; here we accept the small bend
|
|
62
|
+
// for the verification fixture so DuckDB queries are clean.
|
|
63
|
+
for (const ev of record) {
|
|
64
|
+
if (ev.event === 'Purchase') ev.cohort = bin;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Scale Purchase amounts via mutate atom (engaged 2x, power 3x)
|
|
68
|
+
const factor = SCALE_BY_BIN[bin] || 1;
|
|
69
|
+
if (factor !== 1) {
|
|
70
|
+
scalePropertyValue(record, e => e.event === 'Purchase', 'amount', factor);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Power users get a Browse burst clustered at the dataset midpoint
|
|
74
|
+
const burstCount = BURST_COUNT_BY_BIN[bin] || 0;
|
|
75
|
+
if (burstCount > 0 && record.length) {
|
|
76
|
+
const midUnixMs = (meta.datasetStart + (meta.datasetEnd - meta.datasetStart) / 2) * 1000;
|
|
77
|
+
// Use the user's first Browse as a clone template so we honor the schema.
|
|
78
|
+
const template = record.find(e => e.event === 'Browse');
|
|
79
|
+
if (template) {
|
|
80
|
+
const tpl = cloneEvent(template, { cohort: bin });
|
|
81
|
+
injectBurst(record, tpl, burstCount, midUnixMs, 60 * 60 * 1000); // ±1h spread
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Re-sort by time after burst injection so downstream pipeline sees ordered events.
|
|
86
|
+
record.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0));
|
|
87
|
+
return record;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2 identity model — verification fixture (intentionally simple).
|
|
3
|
+
*
|
|
4
|
+
* 1K born-in-dataset users with isAuthEvent on `Sign Up` (the stitch step) and a
|
|
5
|
+
* 3-step first funnel. Dataset window pinned for determinism. avgDevicePerUser:2
|
|
6
|
+
* exercises the multi-device sticky-per-session model. Used by phase 2 verification
|
|
7
|
+
* gate (DuckDB queries against the events output).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import dayjs from 'dayjs';
|
|
11
|
+
|
|
12
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
name: 'identity-model-verify',
|
|
16
|
+
seed: 'phase2-identity-verify',
|
|
17
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
18
|
+
datasetEnd: FIXED_NOW,
|
|
19
|
+
numUsers: 1_000,
|
|
20
|
+
avgEventsPerUserPerDay: 3,
|
|
21
|
+
percentUsersBornInDataset: 100,
|
|
22
|
+
hasAnonIds: true,
|
|
23
|
+
avgDevicePerUser: 2,
|
|
24
|
+
hasSessionIds: true,
|
|
25
|
+
format: 'json',
|
|
26
|
+
concurrency: 1,
|
|
27
|
+
writeToDisk: true,
|
|
28
|
+
verbose: false,
|
|
29
|
+
events: [
|
|
30
|
+
{ event: 'Land', isFirstEvent: true, isStrictEvent: true },
|
|
31
|
+
{ event: 'View Pricing', isStrictEvent: true },
|
|
32
|
+
{ event: 'Sign Up', isAuthEvent: true, isStrictEvent: true },
|
|
33
|
+
{ event: 'Onboarding', isStrictEvent: true },
|
|
34
|
+
{ event: 'Browse', weight: 5 },
|
|
35
|
+
{ event: 'Save Item', weight: 2 },
|
|
36
|
+
],
|
|
37
|
+
funnels: [
|
|
38
|
+
{
|
|
39
|
+
name: 'Acquisition',
|
|
40
|
+
sequence: ['Land', 'View Pricing', 'Sign Up', 'Onboarding'],
|
|
41
|
+
conversionRate: 80,
|
|
42
|
+
isFirstFunnel: true,
|
|
43
|
+
timeToConvert: 2,
|
|
44
|
+
attempts: { min: 0, max: 2 },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 reference dungeon — aggregate per user, by bin.
|
|
3
|
+
*
|
|
4
|
+
* Users in the high-Browse cohort have 4× higher Purchase amounts on average;
|
|
5
|
+
* mid cohort 2×; low cohort 1×. Verified via the aggregatePerUser emulator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import dayjs from 'dayjs';
|
|
9
|
+
import { applyAggregateByBin } from '../../lib/hook-patterns/index.js';
|
|
10
|
+
|
|
11
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'pattern-agg-by-bin',
|
|
15
|
+
seed: 'phase4-aggbybin',
|
|
16
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
17
|
+
datasetEnd: FIXED_NOW,
|
|
18
|
+
numUsers: 1_000,
|
|
19
|
+
avgEventsPerUserPerDay: 6,
|
|
20
|
+
percentUsersBornInDataset: 30,
|
|
21
|
+
hasAnonIds: false,
|
|
22
|
+
format: 'json',
|
|
23
|
+
concurrency: 1,
|
|
24
|
+
writeToDisk: true,
|
|
25
|
+
verbose: false,
|
|
26
|
+
events: [
|
|
27
|
+
{ event: 'Browse', weight: 6 },
|
|
28
|
+
{ event: 'Purchase', weight: 2, properties: { amount: [10, 20, 30] } },
|
|
29
|
+
],
|
|
30
|
+
hook: function (record, type) {
|
|
31
|
+
if (type !== 'everything' || !Array.isArray(record)) return record;
|
|
32
|
+
applyAggregateByBin(record, null, {
|
|
33
|
+
cohortEvent: 'Browse',
|
|
34
|
+
bins: { low: [0, 5], mid: [5, 15], high: [15, Infinity] },
|
|
35
|
+
event: 'Purchase',
|
|
36
|
+
propertyName: 'amount',
|
|
37
|
+
deltas: { low: 1, mid: 2, high: 4 },
|
|
38
|
+
});
|
|
39
|
+
return record;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 reference dungeon — conversions attributed by source.
|
|
3
|
+
*
|
|
4
|
+
* Touch events carry one of three random sources; conversions are stamped with
|
|
5
|
+
* the user's first-touch source weighted google:facebook:twitter = 10:5:1.
|
|
6
|
+
* Verified via the attributedBy emulator.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import dayjs from 'dayjs';
|
|
10
|
+
import { applyAttributedBySource } from '../../lib/hook-patterns/index.js';
|
|
11
|
+
|
|
12
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
name: 'pattern-attributed-by-source',
|
|
16
|
+
seed: 'phase4-attrib',
|
|
17
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
18
|
+
datasetEnd: FIXED_NOW,
|
|
19
|
+
numUsers: 1_000,
|
|
20
|
+
avgEventsPerUserPerDay: 4,
|
|
21
|
+
percentUsersBornInDataset: 50,
|
|
22
|
+
hasAnonIds: false,
|
|
23
|
+
format: 'json',
|
|
24
|
+
concurrency: 1,
|
|
25
|
+
writeToDisk: true,
|
|
26
|
+
verbose: false,
|
|
27
|
+
events: [
|
|
28
|
+
{ event: 'Touch', weight: 5, properties: { source: ['google', 'facebook', 'twitter'] } },
|
|
29
|
+
{ event: 'Convert', weight: 2, properties: { source: ['unknown'] } },
|
|
30
|
+
],
|
|
31
|
+
hook: function (record, type) {
|
|
32
|
+
if (type !== 'everything' || !Array.isArray(record)) return record;
|
|
33
|
+
applyAttributedBySource(record, null, {
|
|
34
|
+
sourceEvent: 'Touch',
|
|
35
|
+
sourceProperty: 'source',
|
|
36
|
+
downstreamEvent: 'Convert',
|
|
37
|
+
weights: { google: 10, facebook: 5, twitter: 1 },
|
|
38
|
+
model: 'firstTouch',
|
|
39
|
+
});
|
|
40
|
+
return record;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 reference dungeon — frequency × frequency.
|
|
3
|
+
*
|
|
4
|
+
* Engages users into low / mid / high "Browse" cohorts; high-cohort users get 3×
|
|
5
|
+
* Purchase events. Verified by the Phase 4 patterns test + verify-runner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import dayjs from 'dayjs';
|
|
9
|
+
import { applyFrequencyByFrequency } from '../../lib/hook-patterns/index.js';
|
|
10
|
+
|
|
11
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'pattern-freq-by-freq',
|
|
15
|
+
seed: 'phase4-freqxfreq',
|
|
16
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
17
|
+
datasetEnd: FIXED_NOW,
|
|
18
|
+
numUsers: 1_000,
|
|
19
|
+
avgEventsPerUserPerDay: 6,
|
|
20
|
+
percentUsersBornInDataset: 30,
|
|
21
|
+
hasAnonIds: false,
|
|
22
|
+
format: 'json',
|
|
23
|
+
concurrency: 1,
|
|
24
|
+
writeToDisk: true,
|
|
25
|
+
verbose: false,
|
|
26
|
+
events: [
|
|
27
|
+
{ event: 'Browse', weight: 6 },
|
|
28
|
+
{ event: 'Purchase', weight: 2, properties: { amount: [10, 20, 30, 40, 50] } },
|
|
29
|
+
],
|
|
30
|
+
hook: function (record, type) {
|
|
31
|
+
if (type !== 'everything' || !Array.isArray(record)) return record;
|
|
32
|
+
applyFrequencyByFrequency(record, null, {
|
|
33
|
+
cohortEvent: 'Browse',
|
|
34
|
+
bins: { low: [0, 5], mid: [5, 15], high: [15, Infinity] },
|
|
35
|
+
targetEvent: 'Purchase',
|
|
36
|
+
multipliers: { low: 1, mid: 2, high: 3 },
|
|
37
|
+
});
|
|
38
|
+
return record;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
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.
|
|
7
|
+
*
|
|
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
|
|
10
|
+
* conversion-per-step lift in the high cohort.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import dayjs from 'dayjs';
|
|
14
|
+
import { applyFunnelFrequencyBreakdown } from '../../lib/hook-patterns/index.js';
|
|
15
|
+
|
|
16
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
name: 'pattern-funnel-freq',
|
|
20
|
+
seed: 'phase4-funnel-freq',
|
|
21
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
22
|
+
datasetEnd: FIXED_NOW,
|
|
23
|
+
numUsers: 1_000,
|
|
24
|
+
avgEventsPerUserPerDay: 4,
|
|
25
|
+
percentUsersBornInDataset: 100,
|
|
26
|
+
hasAnonIds: false,
|
|
27
|
+
format: 'json',
|
|
28
|
+
concurrency: 1,
|
|
29
|
+
writeToDisk: true,
|
|
30
|
+
verbose: false,
|
|
31
|
+
events: [
|
|
32
|
+
{ event: 'Land', isFirstEvent: true, isStrictEvent: true },
|
|
33
|
+
{ event: 'Sign Up', isAuthEvent: true, isStrictEvent: true },
|
|
34
|
+
{ event: 'Activate', isStrictEvent: true },
|
|
35
|
+
{ event: 'Browse', weight: 5 },
|
|
36
|
+
],
|
|
37
|
+
funnels: [{
|
|
38
|
+
sequence: ['Land', 'Sign Up', 'Activate'],
|
|
39
|
+
conversionRate: 70, isFirstFunnel: true, timeToConvert: 4,
|
|
40
|
+
}],
|
|
41
|
+
hook: function (record, type, meta) {
|
|
42
|
+
if (type !== 'funnel-post' || !Array.isArray(record)) return;
|
|
43
|
+
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
|
+
applyFunnelFrequencyBreakdown(null, meta.profile || {}, record, {
|
|
48
|
+
cohortEvent: 'Browse',
|
|
49
|
+
bins: { low: [0, 3], high: [3, Infinity] },
|
|
50
|
+
dropMultipliers: { low: 0.5, high: 0.95 },
|
|
51
|
+
finalStep: 'Activate',
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
8
|
+
import dayjs from 'dayjs';
|
|
9
|
+
import { applyTTCBySegment } from '../../lib/hook-patterns/index.js';
|
|
10
|
+
|
|
11
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'pattern-ttc-by-segment',
|
|
15
|
+
seed: 'phase4-ttc-seg',
|
|
16
|
+
datasetStart: FIXED_NOW - 30 * 86400,
|
|
17
|
+
datasetEnd: FIXED_NOW,
|
|
18
|
+
numUsers: 1_000,
|
|
19
|
+
avgEventsPerUserPerDay: 4,
|
|
20
|
+
percentUsersBornInDataset: 100,
|
|
21
|
+
hasAnonIds: false,
|
|
22
|
+
format: 'json',
|
|
23
|
+
concurrency: 1,
|
|
24
|
+
writeToDisk: true,
|
|
25
|
+
verbose: false,
|
|
26
|
+
userProps: { tier: ['trial', 'trial', 'enterprise'] },
|
|
27
|
+
events: [
|
|
28
|
+
{ event: 'Land', isFirstEvent: true, isStrictEvent: true },
|
|
29
|
+
{ event: 'Sign Up', isAuthEvent: true, isStrictEvent: true },
|
|
30
|
+
{ event: 'Activate', isStrictEvent: true },
|
|
31
|
+
{ event: 'Browse', weight: 5 },
|
|
32
|
+
],
|
|
33
|
+
funnels: [{
|
|
34
|
+
sequence: ['Land', 'Sign Up', 'Activate'],
|
|
35
|
+
conversionRate: 100, isFirstFunnel: true, timeToConvert: 4,
|
|
36
|
+
}],
|
|
37
|
+
hook: function (record, type, meta) {
|
|
38
|
+
if (type !== 'funnel-post' || !Array.isArray(record)) return;
|
|
39
|
+
if (!meta.isFirstFunnel) return;
|
|
40
|
+
applyTTCBySegment(record, meta.profile || {}, {
|
|
41
|
+
segmentKey: 'tier',
|
|
42
|
+
factors: { trial: 4, enterprise: 0.5 },
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "promptforge";
|
|
3
3
|
const num_days = 120;
|
|
4
|
-
const num_users =
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 0.83;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -112,7 +112,7 @@ const chance = u.initChance(SEED);
|
|
|
112
112
|
* 3. AGENTIC LOOP POWER USERS (everything)
|
|
113
113
|
* ---------------------------------------------------------------
|
|
114
114
|
*
|
|
115
|
-
* PATTERN: Users with
|
|
115
|
+
* PATTERN: Users with 3+ "tool use call" AND 3+ api-call events with
|
|
116
116
|
* multi_turn=true get 8x tokens_used on api calls plus 2 extra cloned
|
|
117
117
|
* api-call events per existing (3x rate). Cloned events with unique
|
|
118
118
|
* offset timestamps. No flag — discover via cohort builder.
|
|
@@ -121,7 +121,7 @@ const chance = u.initChance(SEED);
|
|
|
121
121
|
*
|
|
122
122
|
* Report 1: Tokens per User — Agentic Cohort
|
|
123
123
|
* - Report type: Insights (with cohort)
|
|
124
|
-
* - Cohort A: users with
|
|
124
|
+
* - Cohort A: users with >= 3 "tool use call" AND >= 3 api-call with multi_turn=true
|
|
125
125
|
* - Cohort B: rest
|
|
126
126
|
* - Event: "api call"
|
|
127
127
|
* - Measure: Average of "tokens_used"
|
|
@@ -272,23 +272,23 @@ const chance = u.initChance(SEED);
|
|
|
272
272
|
* ---------------------------------------------------------------
|
|
273
273
|
*
|
|
274
274
|
* PATTERN: Count "docs searched" events between organization-created and
|
|
275
|
-
* first billing-payment. Sweet
|
|
276
|
-
* payment events. Over
|
|
275
|
+
* first billing-payment. Sweet 2-4 → +35% on amount_usd of billing
|
|
276
|
+
* payment events. Over 5+ → drop 30% of billing payment events. No flag.
|
|
277
277
|
*
|
|
278
278
|
* HOW TO FIND IT IN MIXPANEL:
|
|
279
279
|
*
|
|
280
280
|
* Report 1: Avg Billing Amount by Docs-Searched Bucket
|
|
281
281
|
* - Report type: Insights (with cohort)
|
|
282
|
-
* - Cohort A: users with
|
|
283
|
-
* - Cohort B: users with 0-
|
|
282
|
+
* - Cohort A: users with 2-4 "docs searched" between sign-up and first billing
|
|
283
|
+
* - Cohort B: users with 0-1
|
|
284
284
|
* - Event: "billing payment"
|
|
285
285
|
* - Measure: Average of "amount_usd"
|
|
286
286
|
* - Expected: A ~ 1.35x B
|
|
287
287
|
*
|
|
288
288
|
* Report 2: Billing Payments per User on Heavy Searchers
|
|
289
289
|
* - Report type: Insights (with cohort)
|
|
290
|
-
* - Cohort C: users with >=
|
|
291
|
-
* - Cohort A: users with
|
|
290
|
+
* - Cohort C: users with >= 5 "docs searched" between sign-up and billing
|
|
291
|
+
* - Cohort A: users with 2-4
|
|
292
292
|
* - Event: "billing payment"
|
|
293
293
|
* - Measure: Total per user
|
|
294
294
|
* - Expected: C ~ 30% fewer billing payments per user
|
|
@@ -317,15 +317,17 @@ const chance = u.initChance(SEED);
|
|
|
317
317
|
|
|
318
318
|
/** @type {Config} */
|
|
319
319
|
const config = {
|
|
320
|
+
version: 2,
|
|
320
321
|
token,
|
|
321
322
|
seed: SEED,
|
|
322
323
|
datasetStart: "2026-01-01T00:00:00Z",
|
|
323
|
-
datasetEnd: "2026-
|
|
324
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
324
325
|
// numDays: num_days,
|
|
325
326
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
326
327
|
numUsers: num_users,
|
|
327
|
-
hasAnonIds:
|
|
328
|
-
|
|
328
|
+
hasAnonIds: true,
|
|
329
|
+
avgDevicePerUser: 2,
|
|
330
|
+
hasSessionIds: true,
|
|
329
331
|
format: "json",
|
|
330
332
|
gzip: true,
|
|
331
333
|
alsoInferFunnels: false,
|
|
@@ -364,6 +366,7 @@ const config = {
|
|
|
364
366
|
event: "organization created",
|
|
365
367
|
weight: 1,
|
|
366
368
|
isFirstEvent: true,
|
|
369
|
+
isAuthEvent: true,
|
|
367
370
|
properties: {
|
|
368
371
|
org_size: ["solo", "startup", "growth", "enterprise"],
|
|
369
372
|
referral_source: ["docs", "blog", "github", "word_of_mouth", "search", "conference"],
|
|
@@ -588,38 +591,6 @@ const config = {
|
|
|
588
591
|
|
|
589
592
|
// -- Hook Function ----------------------------------------
|
|
590
593
|
hook: function (record, type, meta) {
|
|
591
|
-
// ─────────────────────────────────────────────────────────
|
|
592
|
-
// Hook #6: OUTAGE DAY (event)
|
|
593
|
-
// Days 40-41: 40% of api calls get is_error=true with
|
|
594
|
-
// service error types
|
|
595
|
-
// ─────────────────────────────────────────────────────────
|
|
596
|
-
if (type === "event") {
|
|
597
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
598
|
-
if (record.event === "api call") {
|
|
599
|
-
const eventTime = dayjs(record.time);
|
|
600
|
-
const dayInDataset = eventTime.diff(datasetStart, "days", true);
|
|
601
|
-
|
|
602
|
-
// Hook #6: Outage day errors
|
|
603
|
-
if (dayInDataset >= 40 && dayInDataset < 42) {
|
|
604
|
-
if (chance.bool({ likelihood: 40 })) {
|
|
605
|
-
record.is_error = true;
|
|
606
|
-
record.error_type = chance.pickone([
|
|
607
|
-
"service_overloaded",
|
|
608
|
-
"internal_server_error",
|
|
609
|
-
"gateway_timeout",
|
|
610
|
-
]);
|
|
611
|
-
record.latency_ms = Math.floor((record.latency_ms || 1500) * 3);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Hook #2 (model migration wave) MOVED to everything hook —
|
|
616
|
-
// at event-hook time `record.api_tier` is the random per-event
|
|
617
|
-
// value, not the user's profile tier. Stamping happens later in
|
|
618
|
-
// the everything hook.
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
return record;
|
|
622
|
-
}
|
|
623
594
|
|
|
624
595
|
// ─────────────────────────────────────────────────────────
|
|
625
596
|
// Hook 9 (T2C): API-TO-EVAL TIME-TO-CONVERT (funnel-post)
|
|
@@ -661,6 +632,26 @@ const config = {
|
|
|
661
632
|
if (profile.sdk_language) e.sdk_language = profile.sdk_language;
|
|
662
633
|
});
|
|
663
634
|
|
|
635
|
+
// ─────────────────────────────────────────────────────
|
|
636
|
+
// Hook #6: OUTAGE DAY (days 40-41)
|
|
637
|
+
// 40% of api calls get is_error=true with service errors
|
|
638
|
+
// ─────────────────────────────────────────────────────
|
|
639
|
+
events.forEach(e => {
|
|
640
|
+
if (e.event !== "api call") return;
|
|
641
|
+
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
642
|
+
if (dayInDataset >= 40 && dayInDataset < 42) {
|
|
643
|
+
if (chance.bool({ likelihood: 40 })) {
|
|
644
|
+
e.is_error = true;
|
|
645
|
+
e.error_type = chance.pickone([
|
|
646
|
+
"service_overloaded",
|
|
647
|
+
"internal_server_error",
|
|
648
|
+
"gateway_timeout",
|
|
649
|
+
]);
|
|
650
|
+
e.latency_ms = Math.floor((e.latency_ms || 1500) * 3);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
664
655
|
// Determine first event time for relative day calculations
|
|
665
656
|
const sortedByTime = [...events].sort((a, b) => dayjs(a.time).valueOf() - dayjs(b.time).valueOf());
|
|
666
657
|
const firstEventTime = sortedByTime.length > 0 ? dayjs(sortedByTime[0].time) : datasetStart;
|
|
@@ -727,11 +718,13 @@ const config = {
|
|
|
727
718
|
|
|
728
719
|
// ─────────────────────────────────────────────────────
|
|
729
720
|
// Hook #3: AGENTIC LOOP POWER USERS (BEHAVIORS TOGETHER)
|
|
730
|
-
// Users with tool use + multi_turn
|
|
721
|
+
// Users with 3+ tool use calls + 3+ multi_turn api calls
|
|
722
|
+
// get 8x tokens, 3x events. Threshold ensures a meaningful
|
|
723
|
+
// agentic cohort (~20-30% of users).
|
|
731
724
|
// ─────────────────────────────────────────────────────
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
const isAgenticUser =
|
|
725
|
+
const toolUseCount = events.filter(e => e.event === "tool use call").length;
|
|
726
|
+
const multiTurnCount = events.filter(e => e.event === "api call" && e.multi_turn === true).length;
|
|
727
|
+
const isAgenticUser = toolUseCount >= 3 && multiTurnCount >= 3;
|
|
735
728
|
|
|
736
729
|
if (isAgenticUser) {
|
|
737
730
|
events.forEach(e => {
|
|
@@ -819,8 +812,8 @@ const config = {
|
|
|
819
812
|
// ─────────────────────────────────────────────────────
|
|
820
813
|
// Hook 10: DOCS-SEARCHED MAGIC NUMBER (in-funnel, no flags)
|
|
821
814
|
// Count "docs searched" events between first "organization
|
|
822
|
-
// created" (sign-up) and any "billing payment". Sweet
|
|
823
|
-
// on amount_usd of billing-payment events. Over
|
|
815
|
+
// created" (sign-up) and any "billing payment". Sweet 2-4 → +35%
|
|
816
|
+
// on amount_usd of billing-payment events. Over 5+ → drop 30%
|
|
824
817
|
// of billing-payment events.
|
|
825
818
|
// ─────────────────────────────────────────────────────
|
|
826
819
|
const orgEvent = events.find(e => e.event === "organization created");
|
|
@@ -833,13 +826,13 @@ const config = {
|
|
|
833
826
|
dayjs(e.time).isAfter(aTime) &&
|
|
834
827
|
dayjs(e.time).isBefore(bTime)
|
|
835
828
|
).length;
|
|
836
|
-
if (docsBetween >=
|
|
829
|
+
if (docsBetween >= 2 && docsBetween <= 4) {
|
|
837
830
|
events.forEach(e => {
|
|
838
831
|
if (e.event === "billing payment" && typeof e.amount_usd === "number") {
|
|
839
832
|
e.amount_usd = Math.round(e.amount_usd * 1.35);
|
|
840
833
|
}
|
|
841
834
|
});
|
|
842
|
-
} else if (docsBetween >=
|
|
835
|
+
} else if (docsBetween >= 5) {
|
|
843
836
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
844
837
|
if (events[i].event === "billing payment" && chance.bool({ likelihood: 30 })) {
|
|
845
838
|
events.splice(i, 1);
|