@ak--47/dungeon-master 1.5.4 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/powertools/SKILL.md +75 -0
- package/.claude/skills/powertools/pt.mjs +91 -0
- package/.claude/skills/powertools/snapshot-project.mjs +124 -0
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +224 -0
- package/HOOKS.md +566 -112
- package/README.md +30 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +7 -3
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +151 -59
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +298 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: analyze-soup
|
|
3
3
|
description: Use when investigating TimeSoup parameters, diagnosing event-distribution shape, or comparing soup configs — runs a dungeon locally and analyzes time distribution at week/day/hour/minute granularities, producing a soup-analysis.md diagnostic report.
|
|
4
|
-
argument-hint: [dungeon path, e.g. dungeons/
|
|
4
|
+
argument-hint: [dungeon path, e.g. dungeons/technical/simplest.js]
|
|
5
5
|
model: claude-opus-4-6
|
|
6
6
|
effort: max
|
|
7
7
|
---
|
|
@@ -10,7 +10,7 @@ effort: max
|
|
|
10
10
|
|
|
11
11
|
Run a dungeon and analyze the time distribution of generated events to evaluate TimeSoup parameters.
|
|
12
12
|
|
|
13
|
-
**Dungeon file:** `$ARGUMENTS` (default: `dungeons/
|
|
13
|
+
**Dungeon file:** `$ARGUMENTS` (default: `dungeons/technical/simplest.js`)
|
|
14
14
|
|
|
15
15
|
## Step 1: Run the Dungeon
|
|
16
16
|
|
|
@@ -32,10 +32,19 @@ Wait for generation to complete. Note the event count and EPS.
|
|
|
32
32
|
|
|
33
33
|
Run these DuckDB queries against the generated JSONL file. Use `duckdb` CLI.
|
|
34
34
|
|
|
35
|
+
**All bucketing is UTC — do not convert timezones.** TimeSoup applies its
|
|
36
|
+
day-of-week / hour-of-day weights in UTC, the engine's calendar-day logic
|
|
37
|
+
(`avgActiveDaysPerUser`, lifecycle periods, session midnight splits) is UTC,
|
|
38
|
+
and `/create-project` pins Mixpanel projects to UTC. Casting `time::timestamp`
|
|
39
|
+
on the ISO strings keeps the UTC wall time. Converting to a local zone (e.g.
|
|
40
|
+
`America/Los_Angeles`, −8h) shifts late-evening UTC events onto the previous
|
|
41
|
+
calendar day — the DOW histogram you'd analyze would not be the one the soup
|
|
42
|
+
generated or the one Mixpanel reports.
|
|
43
|
+
|
|
35
44
|
### 2a. Week over Week
|
|
36
45
|
```bash
|
|
37
46
|
duckdb -c "
|
|
38
|
-
SELECT date_trunc('week',
|
|
47
|
+
SELECT date_trunc('week', time::timestamp) as week,
|
|
39
48
|
count(*) as events
|
|
40
49
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
41
50
|
GROUP BY 1 ORDER BY 1;
|
|
@@ -45,7 +54,7 @@ GROUP BY 1 ORDER BY 1;
|
|
|
45
54
|
### 2b. Day over Day
|
|
46
55
|
```bash
|
|
47
56
|
duckdb -c "
|
|
48
|
-
SELECT date_trunc('day',
|
|
57
|
+
SELECT date_trunc('day', time::timestamp) as day,
|
|
49
58
|
count(*) as events
|
|
50
59
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
51
60
|
GROUP BY 1 ORDER BY 1;
|
|
@@ -55,10 +64,10 @@ GROUP BY 1 ORDER BY 1;
|
|
|
55
64
|
### 2c. Hour over Hour (last 7 days only)
|
|
56
65
|
```bash
|
|
57
66
|
duckdb -c "
|
|
58
|
-
SELECT date_trunc('hour',
|
|
67
|
+
SELECT date_trunc('hour', time::timestamp) as hour,
|
|
59
68
|
count(*) as events
|
|
60
69
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
61
|
-
WHERE
|
|
70
|
+
WHERE time::timestamp > (SELECT max(time::timestamp) - interval '7 days' FROM read_json_auto('./data/soup-analysis-EVENTS.json'))
|
|
62
71
|
GROUP BY 1 ORDER BY 1;
|
|
63
72
|
"
|
|
64
73
|
```
|
|
@@ -66,10 +75,10 @@ GROUP BY 1 ORDER BY 1;
|
|
|
66
75
|
### 2d. Minute over Minute (last 24 hours only)
|
|
67
76
|
```bash
|
|
68
77
|
duckdb -c "
|
|
69
|
-
SELECT date_trunc('minute',
|
|
78
|
+
SELECT date_trunc('minute', time::timestamp) as minute,
|
|
70
79
|
count(*) as events
|
|
71
80
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
72
|
-
WHERE
|
|
81
|
+
WHERE time::timestamp > (SELECT max(time::timestamp) - interval '1 day' FROM read_json_auto('./data/soup-analysis-EVENTS.json'))
|
|
73
82
|
GROUP BY 1 ORDER BY 1;
|
|
74
83
|
"
|
|
75
84
|
```
|
|
@@ -78,14 +87,14 @@ GROUP BY 1 ORDER BY 1;
|
|
|
78
87
|
```bash
|
|
79
88
|
duckdb -c "
|
|
80
89
|
WITH daily AS (
|
|
81
|
-
SELECT date_trunc('day',
|
|
90
|
+
SELECT date_trunc('day', time::timestamp) as day, count(*) as events
|
|
82
91
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
83
92
|
GROUP BY 1
|
|
84
93
|
),
|
|
85
94
|
hourly AS (
|
|
86
|
-
SELECT date_trunc('hour',
|
|
95
|
+
SELECT date_trunc('hour', time::timestamp) as hour, count(*) as events
|
|
87
96
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
88
|
-
WHERE
|
|
97
|
+
WHERE time::timestamp > (SELECT max(time::timestamp) - interval '7 days' FROM read_json_auto('./data/soup-analysis-EVENTS.json'))
|
|
89
98
|
GROUP BY 1
|
|
90
99
|
)
|
|
91
100
|
SELECT 'daily' as granularity,
|
|
@@ -112,7 +121,7 @@ FROM hourly;
|
|
|
112
121
|
```bash
|
|
113
122
|
duckdb -c "
|
|
114
123
|
WITH daily AS (
|
|
115
|
-
SELECT date_trunc('day',
|
|
124
|
+
SELECT date_trunc('day', time::timestamp) as day, count(*) as events
|
|
116
125
|
FROM read_json_auto('./data/soup-analysis-EVENTS.json')
|
|
117
126
|
GROUP BY 1
|
|
118
127
|
)
|
|
@@ -51,7 +51,14 @@ Out of scope (hand off to `write-hooks`):
|
|
|
51
51
|
For an encyclopedia of hook patterns, recipes, and real-world examples:
|
|
52
52
|
see `HOOKS.md` at the project root.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
> ⚠️ **Silently-ignored config keys — do not use.** The validator STRIPS these
|
|
55
|
+
> five keys before generation: `subscription`, `attribution`, `geo`,
|
|
56
|
+
> `features`, `anomalies`. They produce **zero data** — only a verbose-mode
|
|
57
|
+
> warning. A dungeon that "configures" churn via `subscription:` or campaign
|
|
58
|
+
> lift via `attribution:` is configuring nothing, and the omission is
|
|
59
|
+
> invisible at smoke-test scale. Recreate those behaviors with hooks via
|
|
60
|
+
> `/write-hooks` (churn cohorts, UTM biasing, geo weighting, feature-flag
|
|
61
|
+
> cohorts, anomaly windows all have recipes in `HOOKS.md`).
|
|
55
62
|
|
|
56
63
|
## Reference reading
|
|
57
64
|
|
|
@@ -62,8 +69,10 @@ Before writing any code, scan:
|
|
|
62
69
|
with full JSDoc. **Treat this as the source of truth.**
|
|
63
70
|
- `lib/utils/utils.js` — `pickAWinner`, `weighNumRange`, `initChance`, `exhaust`,
|
|
64
71
|
`takeSome` for property value distributions
|
|
65
|
-
- `dungeons/vertical/sass.js` — B2B reference dungeon with full identity model
|
|
66
|
-
- `dungeons/
|
|
72
|
+
- `dungeons/vertical/sass/sass.js` — B2B reference dungeon with full identity model
|
|
73
|
+
- `dungeons/vertical/ecommerce/ecommerce.js` — canonical consumer exemplar:
|
|
74
|
+
schema, funnels, stories export, and full hook layout (what your schema
|
|
75
|
+
becomes after `/write-hooks`)
|
|
67
76
|
- `dungeons/technical/identity-model-verify.js` — minimal identity-model fixture
|
|
68
77
|
|
|
69
78
|
## File structure
|
|
@@ -88,6 +97,7 @@ import * as v from "ak-tools";
|
|
|
88
97
|
* APP: <2-4 line description: what users do, core flow, monetization>
|
|
89
98
|
* SCALE: <numUsers> users, ~<numEvents> events, <numDays> days (<start> → <end>)
|
|
90
99
|
* CORE LOOP: <event1> → <event2> → <event3> → ...
|
|
100
|
+
* VALUE MOMENT: <the one event representing realized value — lifecycle hooks anchor here>
|
|
91
101
|
*
|
|
92
102
|
* EVENTS (N):
|
|
93
103
|
* <event name (weight)> > ... (sorted by weight desc)
|
|
@@ -160,7 +170,7 @@ KNOBS (extracted tunable constants — timing, thresholds, multipliers),
|
|
|
160
170
|
HOOK STATE (module-level Maps/Sets used across users), and HELPER FUNCTIONS
|
|
161
171
|
(per-type handlers like `handleEventHooks`, `handleEverythingHooks`).
|
|
162
172
|
`config.hook` becomes a thin dispatcher delegating to the helpers. See
|
|
163
|
-
`dungeons/vertical/ecommerce.js` as the canonical exemplar.
|
|
173
|
+
`dungeons/vertical/ecommerce/ecommerce.js` as the canonical exemplar.
|
|
164
174
|
|
|
165
175
|
## Required components
|
|
166
176
|
|
|
@@ -196,6 +206,30 @@ When using experiments, include `$experiment_started` in the events array with
|
|
|
196
206
|
}}
|
|
197
207
|
```
|
|
198
208
|
|
|
209
|
+
**Design the vocabulary for the analyses hooks will target (v1.6):**
|
|
210
|
+
|
|
211
|
+
- **Session-friendly next-events.** Flows (Sankey) reads collapse when an
|
|
212
|
+
anchor event can plausibly be followed by 15 different event types — every
|
|
213
|
+
branch gets a sliver and no engineered path clears the top level. For each
|
|
214
|
+
high-traffic anchor (post-login, post-search, post-add-to-cart), keep the
|
|
215
|
+
realistic next-event vocabulary to **≤5–6 event types**; if the app
|
|
216
|
+
genuinely has more surfaces, give the rare ones weight 1 so the top paths
|
|
217
|
+
stay legible. A path-share hook needs ~20–25% share to survive a Sankey's
|
|
218
|
+
top-3 — impossible against a 15-way fan-out.
|
|
219
|
+
- **Designate a value moment.** Pick ONE event that represents realized value
|
|
220
|
+
(first workout logged, order delivered, report generated) and record it in
|
|
221
|
+
the OVERVIEW block as `VALUE MOMENT: <event>`. Lifecycle stories
|
|
222
|
+
(dormancy → resurrection waves) anchor on the value moment — hooks gate or
|
|
223
|
+
inject THAT event, not generic page views. A schema with no obvious value
|
|
224
|
+
moment forces `write-hooks` to invent one badly.
|
|
225
|
+
- **Hidden-event hygiene.** Heartbeat/telemetry events (`ping`,
|
|
226
|
+
`app foregrounded`, `position updated`) at weight ≥5 drown every Sankey
|
|
227
|
+
level — every top path becomes anchor → heartbeat → heartbeat. Mixpanel
|
|
228
|
+
Flows (and the emulator's `topPaths` via `hiddenEvents`) can hide them at
|
|
229
|
+
query time, but the default view is what demos show. If realism demands
|
|
230
|
+
heartbeats, keep them at weight 1–2 and list them in the OVERVIEW so
|
|
231
|
+
`write-hooks` knows to pass `hiddenEvents` in flows stories.
|
|
232
|
+
|
|
199
233
|
### 2. Funnels (3–6)
|
|
200
234
|
|
|
201
235
|
- First funnel: includes `isFirstEvent` AND has `isAuthEvent: true` on the
|
|
@@ -237,6 +271,32 @@ event.
|
|
|
237
271
|
|
|
238
272
|
**Funnels representing loops need `reentry: true`.** Any funnel named "X loop" / "X cycle" / "session" / per-instance recurring behavior must declare `reentry: true`. Without it, the engine emits one sequence per user and downstream "power user" / "daily active" cohorts have no behavioral signal to bin on.
|
|
239
273
|
|
|
274
|
+
**Structural trend engineering — duplicate funnels instead of reaching for
|
|
275
|
+
hooks.** Not every trend needs a hook: initial conditions can raise or lower
|
|
276
|
+
a metric by themselves. The funnel array is a set of knobs — duplicate a
|
|
277
|
+
funnel and swap steps, props, `conversionRate`, `timeToConvert`, or `weight`
|
|
278
|
+
to architect a comparison directly into the schema:
|
|
279
|
+
|
|
280
|
+
- **Exclusion-step story**: Mixpanel funnels can exclude users who did
|
|
281
|
+
`foo` between A and B. There's no `excludeSteps` config — model it with
|
|
282
|
+
two funnels, `[A, B, C]` and `[A, B, foo, C]`, giving the second a lower
|
|
283
|
+
`conversionRate`. The report shows the conversion dip for the
|
|
284
|
+
detour-takers with zero hook code.
|
|
285
|
+
- **Segment-split story**: two copies of the same sequence with different
|
|
286
|
+
`props` (`{ checkout_version: "v1" }` vs `"v2"`) and different
|
|
287
|
+
`conversionRate` — an A/B story without the experiment machinery (use
|
|
288
|
+
`experiment` when you want `$experiment_started` + variant reports).
|
|
289
|
+
- **Speed story**: same sequence, different `timeToConvert`, split by a
|
|
290
|
+
funnel-level prop — time-to-convert deltas appear structurally.
|
|
291
|
+
- **Mix-shift story**: `weight` controls how often each variant funnel is
|
|
292
|
+
chosen; a heavy-weighted low-converting path drags the blended rate.
|
|
293
|
+
|
|
294
|
+
Prefer structure when the story is a *between-path comparison* (this path
|
|
295
|
+
converts worse / slower than that one). Reach for hooks when the story is a
|
|
296
|
+
*within-cohort behavior* (these users do more of X over time, this segment's
|
|
297
|
+
values differ). Structural trends are cheaper to verify — the knob IS the
|
|
298
|
+
expected value.
|
|
299
|
+
|
|
240
300
|
### 3. SuperProps (2–3)
|
|
241
301
|
|
|
242
302
|
Properties present on EVERY event. Common picks: `Plan`, `Region`, `Platform`,
|
|
@@ -554,7 +614,7 @@ When designing event properties, always consider which Mixpanel type best repres
|
|
|
554
614
|
app/customer, then **create `dungeons/user/<name>/` if it doesn't already exist**
|
|
555
615
|
(`mkdir -p dungeons/user/<name>`) and write the dungeon to
|
|
556
616
|
`dungeons/user/<name>/<name>.js` (e.g. `dungeons/user/acme/acme.js`). Folder and
|
|
557
|
-
file share the name, matching `
|
|
617
|
+
file share the name, matching the vertical layout (`ecommerce/ecommerce.js`).
|
|
558
618
|
|
|
559
619
|
This keeps `dungeons/user/` organized — EVERYTHING about this dungeon lives in
|
|
560
620
|
the same folder: `hook-results.md` + `hook-query-log.txt` +
|
|
@@ -25,7 +25,7 @@ in order:
|
|
|
25
25
|
1. **createProject** — name derived from the dungeon's `OVERVIEW` (`NAME:` line), region `US`, timezone `UTC` (set as a follow-up by the endpoint).
|
|
26
26
|
2. **mintServiceAccount** — `admin`, expires `+30 days`, scoped to the new project. This is what the dungeon uses to **send** data.
|
|
27
27
|
3. **addGroupKey** — one per `groupKeys` entry in the dungeon (`property_name` + a titleized `display_name`). Skipped if the dungeon has no group keys.
|
|
28
|
-
4. **setBusinessContext** — markdown built from the dungeon's `OVERVIEW` + `HOOK STORIES` comment
|
|
28
|
+
4. **setBusinessContext** — markdown built from the dungeon's `OVERVIEW` comment block plus the `stories` named export (per story: `narrative` + `mixpanelReport` + intentional deviations); dungeons without stories fall back to the `HOOK STORIES` comment scrape (via the package's `extractComments`). Plus an events/funnels/props/group-keys summary, capped at 50k chars. The dry-run plan prints which source was used.
|
|
29
29
|
5. **write-back** — replaces the dungeon's `credentials: { … }` block with `{ token, projectId, serviceAccount, serviceSecret, region }`.
|
|
30
30
|
|
|
31
31
|
**Always creates a fresh project.** Re-running mints a new project and overwrites
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* 1. createProject (sets timezone UTC as a follow-up)
|
|
11
11
|
* 2. mintServiceAccount (admin, +30d) — scoped to the new project
|
|
12
12
|
* 3. addGroupKey (only if the dungeon declares groupKeys)
|
|
13
|
-
* 4. setBusinessContext (OVERVIEW +
|
|
13
|
+
* 4. setBusinessContext (OVERVIEW + the dungeon's `stories` export
|
|
14
|
+
* (narrative + mixpanelReport per story) — falls back to the HOOK
|
|
15
|
+
* STORIES comment scrape when the dungeon has no stories — + schema summary)
|
|
14
16
|
* 5. write `credentials: { token, projectId, serviceAccount, serviceSecret, region }`
|
|
15
17
|
* back into the dungeon (gitignored user dungeons — plaintext is fine)
|
|
16
18
|
*
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
|
|
28
30
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
29
31
|
import path, { dirname, resolve } from 'path';
|
|
30
|
-
import { fileURLToPath } from 'url';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
31
33
|
import dotenv from 'dotenv';
|
|
32
34
|
import { loadFromFile, extractComments } from '../../../index.js';
|
|
33
35
|
|
|
@@ -57,12 +59,26 @@ if (!existsSync(dungeonPath)) fail(`dungeon file not found: ${dungeonPath}`);
|
|
|
57
59
|
const config = await loadFromFile(dungeonPath);
|
|
58
60
|
const comments = extractComments(dungeonPath);
|
|
59
61
|
|
|
62
|
+
// stories is a NAMED export of JS dungeons (v1.6 story-spec) — the verified
|
|
63
|
+
// behavior contract. Preferred over the comment scrape for business context.
|
|
64
|
+
let stories = null;
|
|
65
|
+
if (/\.(js|mjs)$/i.test(dungeonPath)) {
|
|
66
|
+
try {
|
|
67
|
+
const mod = await import(pathToFileURL(dungeonPath).href);
|
|
68
|
+
if (Array.isArray(mod.stories) && mod.stories.length) stories = mod.stories;
|
|
69
|
+
} catch {
|
|
70
|
+
// loadFromFile above already imported the module; an error here would
|
|
71
|
+
// have surfaced there. Fall back to the comment scrape regardless.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
const name = deriveName(comments, dungeonPath);
|
|
61
76
|
const groupKeys = Array.isArray(config.groupKeys)
|
|
62
77
|
? config.groupKeys.filter(Boolean).map(([prop]) => ({ property_name: prop, display_name: titleize(prop) }))
|
|
63
78
|
: [];
|
|
64
79
|
const saName = `${slug(name)}-dungeon-sa`.slice(0, 64);
|
|
65
|
-
const
|
|
80
|
+
const ctxSource = stories ? `stories export (${stories.length} stories)` : 'comment scrape';
|
|
81
|
+
const content = buildContext(name, config, comments, groupKeys, stories);
|
|
66
82
|
|
|
67
83
|
if (dryRun) {
|
|
68
84
|
printPlan();
|
|
@@ -182,13 +198,41 @@ function slug(s) {
|
|
|
182
198
|
|
|
183
199
|
function isoInDays(days) {
|
|
184
200
|
const ms = Date.now() + days * 24 * 60 * 60 * 1000;
|
|
185
|
-
|
|
201
|
+
// Mixpanel's createServiceAccount requires %Y-%m-%dT%H:%M:%SZ — seconds
|
|
202
|
+
// precision, NO milliseconds. toISOString() emits ".sssZ", which the API
|
|
203
|
+
// rejects ("Expiration date … does not match %Y-%m-%dT%H:%M:%SZ").
|
|
204
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
186
205
|
}
|
|
187
206
|
|
|
188
|
-
function buildContext(name, config, comments, groupKeys) {
|
|
207
|
+
function buildContext(name, config, comments, groupKeys, stories) {
|
|
189
208
|
const parts = [`# ${name}`, ''];
|
|
190
209
|
if (comments.overview) parts.push(comments.overview, '');
|
|
191
|
-
|
|
210
|
+
|
|
211
|
+
// Preferred source: the stories export — each story's narrative is the
|
|
212
|
+
// human-readable behavior and mixpanelReport (free-form object) points at
|
|
213
|
+
// the report where it shows. Comment scrape is the fallback for dungeons
|
|
214
|
+
// without stories (schema-only or pre-1.6).
|
|
215
|
+
if (stories?.length) {
|
|
216
|
+
parts.push('## Engineered Behaviors', '');
|
|
217
|
+
parts.push(`${stories.length} machine-verified story patterns (from the dungeon's \`stories\` export):`, '');
|
|
218
|
+
for (const s of stories) {
|
|
219
|
+
const arch = s.archetype ? ` — ${s.archetype}` : '';
|
|
220
|
+
parts.push(`### ${s.id}${arch}`, '');
|
|
221
|
+
if (s.narrative) parts.push(String(s.narrative).trim(), '');
|
|
222
|
+
if (s.mixpanelReport && typeof s.mixpanelReport === 'object') {
|
|
223
|
+
parts.push('How to see it in Mixpanel:');
|
|
224
|
+
for (const [k, v] of Object.entries(s.mixpanelReport)) {
|
|
225
|
+
parts.push(`- **${k}**: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
|
|
226
|
+
}
|
|
227
|
+
parts.push('');
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(s.intentionalDeviations) && s.intentionalDeviations.length) {
|
|
230
|
+
parts.push('Notes:', ...s.intentionalDeviations.map((d) => `- ${d}`), '');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (comments.hookStories) {
|
|
234
|
+
parts.push('## Engineered Behaviors', '', comments.hookStories, '');
|
|
235
|
+
}
|
|
192
236
|
|
|
193
237
|
parts.push('## Schema', '');
|
|
194
238
|
const events = config.events || [];
|
|
@@ -252,7 +296,7 @@ function printPlan() {
|
|
|
252
296
|
console.log(`region: ${REGION} timezone: UTC`);
|
|
253
297
|
console.log(`service acct: ${saName} (role admin, expires +${SA_TTL_DAYS}d)`);
|
|
254
298
|
console.log(`group keys: ${groupKeys.length ? groupKeys.map((g) => `${g.property_name} → "${g.display_name}"`).join(', ') : '(none)'}`);
|
|
255
|
-
console.log(`business ctx: ${content.length} chars`);
|
|
299
|
+
console.log(`business ctx: ${content.length} chars (source: ${ctxSource})`);
|
|
256
300
|
console.log('');
|
|
257
301
|
console.log('would POST: createProject → mintServiceAccount' + (groupKeys.length ? ' → addGroupKey' : '') + ' → setBusinessContext');
|
|
258
302
|
console.log('then write credentials back into the dungeon.');
|
|
@@ -268,11 +312,14 @@ function printSummary() {
|
|
|
268
312
|
console.log(`region: ${REGION} timezone: UTC`);
|
|
269
313
|
console.log(`service acct: ${sa ? `${sa.username} (role ${sa.role}, expires ${sa.expires})` : '(FAILED — see warnings)'}`);
|
|
270
314
|
if (groupKeys.length) {
|
|
271
|
-
|
|
272
|
-
|
|
315
|
+
// Defensive: the API's `added`/`skipped` entries may be objects, strings, or
|
|
316
|
+
// null. Never let summary formatting throw — it would swallow real warnings.
|
|
317
|
+
const fmt = (g) => (g == null ? null : typeof g === 'string' ? g : (g.property_name || g.name || null));
|
|
318
|
+
const added = (groupKeyResult?.added || []).map(fmt).filter(Boolean).join(', ') || '(none)';
|
|
319
|
+
const skipped = (groupKeyResult?.skipped || []).map(fmt).filter(Boolean).join(', ') || '(none)';
|
|
273
320
|
console.log(`group keys: added [${added}] skipped [${skipped}]`);
|
|
274
321
|
}
|
|
275
|
-
console.log(`business ctx: ${content.length} chars uploaded`);
|
|
322
|
+
console.log(`business ctx: ${content.length} chars uploaded (source: ${ctxSource})`);
|
|
276
323
|
console.log(`credentials: ${wroteBack ? 'written back into dungeon ✓' : 'NOT written (see warnings)'}`);
|
|
277
324
|
if (warnings.length) {
|
|
278
325
|
console.log('');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: powertools
|
|
3
|
+
description: Use when any task needs the Mixpanel Power Tools API ("use powertools") — schema export (get-schema), event volumes, project CRUD, query methods, macros, or snapshotting a prod project's schema to copy it into a dungeon. Companion to create-project (which handles provisioning specifically).
|
|
4
|
+
argument-hint: [what to do, e.g. "get schema for project 12345" or "copy project 12345 into a dungeon"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Power Tools API
|
|
8
|
+
|
|
9
|
+
Base URL: `https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app`
|
|
10
|
+
|
|
11
|
+
## Auth model — read this first
|
|
12
|
+
|
|
13
|
+
- **GET any endpoint path = documentation, no auth.** Always `curl -s GET <base><path>` before first use of an unfamiliar endpoint — docs include exact body params and response shapes.
|
|
14
|
+
- **POST = execute.** `Authorization: Bearer <oauth-token>` (employee OAuth from repo `.env` `BEARER_TOKEN`, or a customer's OAuth token) or `Basic base64(service_acct:secret)`.
|
|
15
|
+
- **Customer OAuth tokens are accepted** (verified 2026-07-06): `/auth` and `/macro/get-schema` work with a customer token on projects that token can access. `ai_endpoints_allowed: false` for non-employees — the `ai-*` family stays employee-only.
|
|
16
|
+
- **Every POST body** should include `client_id: "dungeon-master"` and `region` (`US` default).
|
|
17
|
+
- **`/auth` accessibility ≠ data access.** `/auth {project_id}` can report `accessible: true` while `get-schema`/`query/*` on the same project return `HTTP 403: Forbidden` (token lacks data-level access, e.g. an employee token on a customer project). Diagnose with `/auth`, but don't trust it for data endpoints. Prefer the token of an actual project member.
|
|
18
|
+
|
|
19
|
+
## Tools in this skill
|
|
20
|
+
|
|
21
|
+
### `pt.mjs` — ad-hoc client
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node .claude/skills/powertools/pt.mjs <path> ['<json-body>'] [--bearer <token>] [--get] [--region US]
|
|
25
|
+
# examples
|
|
26
|
+
node .claude/skills/powertools/pt.mjs /auth '{}'
|
|
27
|
+
node .claude/skills/powertools/pt.mjs /macro/get-schema '{"project_id":"123","include_metadata":true,"verbose":true}'
|
|
28
|
+
node .claude/skills/powertools/pt.mjs /query/getTopEvents --get # docs, no auth
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Bearer defaults to `.env` `BEARER_TOKEN`. Merges `client_id`/`region` into the body. Prints pretty JSON to stdout.
|
|
32
|
+
|
|
33
|
+
### `snapshot-project.mjs` — schema + relative-volume snapshot
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node .claude/skills/powertools/snapshot-project.mjs <project_id> --bearer <token> \
|
|
37
|
+
[--region US] [--out snapshot.json]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Uses `/macro/get-schema` (`include_metadata` + `verbose`) + `/query/getTopEvents`. Token (customer or employee) needs data access to the project.
|
|
41
|
+
- Output shape: `{ projectId, projectName, fetchedAt, totalCount, events: [{ name, count, pct, properties: [{name, type, description}] }], userProps: [{name, type, description}], groups: {} }`, events sorted by count desc.
|
|
42
|
+
- **Never captures property values** — schema + volumes only. Snapshots stay privacy-safe by construction.
|
|
43
|
+
|
|
44
|
+
## Endpoint catalog (the useful subset)
|
|
45
|
+
|
|
46
|
+
GET the path for full docs. Full list: GET `/` and GET `/macro`.
|
|
47
|
+
|
|
48
|
+
**crud** — `/crud/createProject`, `/crud/deleteProject`, `/crud/getProjects`, `/crud/mintServiceAccount`, `/crud/addGroupKey`, `/crud/setBusinessContext` (all used by the create-project skill's `provision.mjs`).
|
|
49
|
+
|
|
50
|
+
**query** — `/query/getTopEvents` (per-event counts, limit≤100 default), `/query/getEventNames`, `/query/getPropertyValues`, `/query/getTopProperties`, `/query/getSegmentation`, `/query/getFunnel`, `/query/listFunnels`, `/query/listCohorts`, `/query/runJQL`. Rate limits: 5 concurrent / 60 per hour; 1h response cache.
|
|
51
|
+
|
|
52
|
+
**macros** — `/macro/get-schema` (always pass `include_metadata: true, verbose: true`; also `include_density: true` for per-(event,property) coverage % and `include_sdk_defaults: true` to keep `$browser`/`$os`-style SDK props — needed for carbon copies), `/macro/analyze-project` (volumes + cardinality + activity), `/macro/enumerate-project`, `/macro/clone-project`, `/macro/clone-boards`, `/macro/delete-entities`, `/macro/dungeon-master` (runs a dungeon config server-side and ingests), `/macro/ai-e2e-dm4` (demo build from a supplied schema, designed for dungeon-master), plus the `ai-*` family (dashboards, cohorts, metrics, schema naming).
|
|
53
|
+
|
|
54
|
+
### get-schema field notes (verified on a 2,410-event project, 2026-07-06)
|
|
55
|
+
|
|
56
|
+
- Response nests under `json`: `{ json: { events, properties, users, groups, dependencies }, duration_ms }`. Large projects are slow — 2,410 events took ~13 min; run it in the background.
|
|
57
|
+
- Event counts include custom events (`customEventId > 0`) and merged events. Filter `!merged && !(customEventId > 0)` to match the Lexicon UI event count.
|
|
58
|
+
- `properties` includes `mp_*` internals and `$custom_property:<id>` computed-prop references — exclude both when authoring a dungeon (ingestion stamps `mp_*` itself; computed props can't be tracked).
|
|
59
|
+
- `exampleValue` fields contain REAL customer values — never copy them into a dungeon.
|
|
60
|
+
- **Known issue**: `dependencies` can come back empty (`{events:{},properties:{}}`) on large projects even with `verbose: true` + `include_density: true` — the bulk dependency call silently fails. Retry the call; per-event property mapping is unavailable until it succeeds.
|
|
61
|
+
|
|
62
|
+
## Recipe: copy a prod project into a dungeon
|
|
63
|
+
|
|
64
|
+
Goal: a purely synthetic dungeon with the same events/props/user-props and matching **relative** event volumes. Safe to share — no customer data.
|
|
65
|
+
|
|
66
|
+
1. **Snapshot** the source project (`snapshot-project.mjs`, customer token unless the employee token has data access). Sanity-check event count + that counts are non-zero.
|
|
67
|
+
2. **Author the dungeon** from the snapshot (schema-first, per repo hook rules):
|
|
68
|
+
- Take the top-N events covering ≥95% of total volume (`pct` cumsum); note dropped tail in the OVERVIEW comment.
|
|
69
|
+
- `weight` per event ∝ snapshot `count` (normalize so max ≈ 100, min ≥ 1).
|
|
70
|
+
- Properties per event from snapshot; **invent all values** from name/type/description — never copy real values.
|
|
71
|
+
- `userProps` from snapshot; shared high-frequency props → `superProps`.
|
|
72
|
+
- Funnels: best-effort from event-name semantics (or `/query/listFunnels` + `/query/getFunnel` on the source if accessible).
|
|
73
|
+
3. **Provision** into our org via the create-project skill: `node .claude/skills/create-project/provision.mjs <dungeon> --dry-run` → confirm → live. Uses `.env` `BEARER_TOKEN` + `ORG_ID`, writes `credentials` back into the dungeon.
|
|
74
|
+
4. **Run**: `node scripts/run-dungeon.mjs <dungeon>`.
|
|
75
|
+
5. **Verify** relative volumes: top-10 generated events should rank in the same order as the snapshot's top-10.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pt.mjs — thin ad-hoc client for the Mixpanel Power Tools API.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node .claude/skills/powertools/pt.mjs <path> ['<json-body>'] [--bearer <token>] [--get] [--region US]
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* node .claude/skills/powertools/pt.mjs /auth '{}'
|
|
11
|
+
* node .claude/skills/powertools/pt.mjs /macro/get-schema '{"project_id":"123","include_metadata":true,"verbose":true}'
|
|
12
|
+
* node .claude/skills/powertools/pt.mjs /query/getTopEvents --get # endpoint docs, no auth
|
|
13
|
+
*
|
|
14
|
+
* POST bodies are merged with { client_id: "dungeon-master", region }.
|
|
15
|
+
* Bearer defaults to BEARER_TOKEN in the repo .env (employee OAuth). Customer
|
|
16
|
+
* OAuth tokens are accepted on non-ai endpoints; the ai-* family is
|
|
17
|
+
* employee-only (see SKILL.md).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { dirname, resolve } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import dotenv from 'dotenv';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
dotenv.config({ path: resolve(__dirname, '../../../.env') });
|
|
26
|
+
|
|
27
|
+
const BASE = 'https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app';
|
|
28
|
+
const CLIENT_ID = 'dungeon-master';
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const getMode = popFlag('--get');
|
|
32
|
+
const bearer = popOpt('--bearer') ?? process.env.BEARER_TOKEN;
|
|
33
|
+
const region = popOpt('--region') ?? 'US';
|
|
34
|
+
const [pathArg, bodyArg] = args;
|
|
35
|
+
|
|
36
|
+
if (!pathArg || !pathArg.startsWith('/')) {
|
|
37
|
+
console.error('Usage: node pt.mjs <path starting with /> [\'<json-body>\'] [--bearer <token>] [--get] [--region US]');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (getMode) {
|
|
42
|
+
const res = await fetch(BASE + pathArg);
|
|
43
|
+
console.log(JSON.stringify(await res.json(), null, 2));
|
|
44
|
+
process.exit(res.ok ? 0 : 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!bearer) {
|
|
48
|
+
console.error('No bearer token: pass --bearer or set BEARER_TOKEN in .env');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let body = {};
|
|
53
|
+
if (bodyArg) {
|
|
54
|
+
try {
|
|
55
|
+
body = JSON.parse(bodyArg);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(`Body is not valid JSON: ${err.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const res = await fetch(BASE + pathArg, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${bearer}` },
|
|
65
|
+
body: JSON.stringify({ client_id: CLIENT_ID, region, ...body }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
try {
|
|
70
|
+
console.log(JSON.stringify(JSON.parse(text), null, 2));
|
|
71
|
+
} catch {
|
|
72
|
+
console.log(text);
|
|
73
|
+
}
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(`\nHTTP ${res.status} ${res.statusText}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function popFlag(name) {
|
|
80
|
+
const i = args.indexOf(name);
|
|
81
|
+
if (i === -1) return false;
|
|
82
|
+
args.splice(i, 1);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function popOpt(name) {
|
|
87
|
+
const i = args.indexOf(name);
|
|
88
|
+
if (i === -1) return undefined;
|
|
89
|
+
const [, value] = args.splice(i, 2);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* snapshot-project.mjs — export a Mixpanel project's schema + relative event
|
|
5
|
+
* volumes into one normalized JSON snapshot, for authoring a synthetic
|
|
6
|
+
* dungeon-master "copy" of the project.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node .claude/skills/powertools/snapshot-project.mjs <project_id> --bearer <token> \
|
|
10
|
+
* [--region US|EU|IN] [--out <file>]
|
|
11
|
+
*
|
|
12
|
+
* Uses the Power Tools API: /macro/get-schema (include_metadata + verbose) +
|
|
13
|
+
* /query/getTopEvents. The bearer token (customer or employee OAuth) needs
|
|
14
|
+
* data access to the project — /auth reporting accessible: true is not enough.
|
|
15
|
+
*
|
|
16
|
+
* Output shape (events sorted by count desc):
|
|
17
|
+
* {
|
|
18
|
+
* projectId, projectName, fetchedAt, region, totalCount,
|
|
19
|
+
* events: [{ name, count, pct, properties: [{ name, type, description }] }],
|
|
20
|
+
* userProps: [{ name, type, description }],
|
|
21
|
+
* groups: {}
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Privacy: captures schema + volumes ONLY — never property values.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { writeFileSync } from 'fs';
|
|
28
|
+
import { dirname, resolve } from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import dotenv from 'dotenv';
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
dotenv.config({ path: resolve(__dirname, '../../../.env') });
|
|
34
|
+
|
|
35
|
+
const PT_BASE = 'https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app';
|
|
36
|
+
const CLIENT_ID = 'dungeon-master';
|
|
37
|
+
const REGIONS = ['US', 'EU', 'IN'];
|
|
38
|
+
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const bearer = popOpt('--bearer') ?? process.env.BEARER_TOKEN;
|
|
41
|
+
const region = (popOpt('--region') ?? 'US').toUpperCase();
|
|
42
|
+
const outArg = popOpt('--out');
|
|
43
|
+
const projectId = args.find((a) => !a.startsWith('--'));
|
|
44
|
+
|
|
45
|
+
if (!projectId || !/^\d+$/.test(projectId)) fail('Usage: snapshot-project.mjs <project_id> --bearer <token> [--region US] [--out file]');
|
|
46
|
+
if (!bearer) fail('No bearer token: pass --bearer or set BEARER_TOKEN in .env');
|
|
47
|
+
if (!REGIONS.includes(region)) fail(`Unknown --region "${region}"`);
|
|
48
|
+
|
|
49
|
+
const outPath = resolve(process.cwd(), outArg ?? `snapshot-${projectId}.json`);
|
|
50
|
+
|
|
51
|
+
const snapshot = await snapshotViaPowertools();
|
|
52
|
+
|
|
53
|
+
snapshot.events.sort((a, b) => b.count - a.count);
|
|
54
|
+
snapshot.totalCount = snapshot.events.reduce((s, e) => s + e.count, 0);
|
|
55
|
+
for (const e of snapshot.events) e.pct = snapshot.totalCount ? +(e.count / snapshot.totalCount * 100).toFixed(4) : 0;
|
|
56
|
+
|
|
57
|
+
writeFileSync(outPath, JSON.stringify(snapshot, null, 2));
|
|
58
|
+
|
|
59
|
+
const zeros = snapshot.events.filter((e) => e.count === 0).length;
|
|
60
|
+
console.log(`✓ snapshot → ${outPath}`);
|
|
61
|
+
console.log(` project: ${snapshot.projectName ?? '(name unknown)'} (${snapshot.projectId})`);
|
|
62
|
+
console.log(` events: ${snapshot.events.length} (${zeros} with zero volume)`);
|
|
63
|
+
console.log(` userProps: ${snapshot.userProps.length}`);
|
|
64
|
+
console.log(` total vol: ${snapshot.totalCount.toLocaleString()} events`);
|
|
65
|
+
console.log(' top 10:');
|
|
66
|
+
for (const e of snapshot.events.slice(0, 10)) console.log(` ${e.pct.toFixed(2).padStart(6)}% ${e.count.toLocaleString().padStart(12)} ${e.name}`);
|
|
67
|
+
|
|
68
|
+
async function snapshotViaPowertools() {
|
|
69
|
+
const schema = await ptPost('/macro/get-schema', { project_id: projectId, include_metadata: true, verbose: true });
|
|
70
|
+
const top = await ptPost('/query/getTopEvents', { project_id: projectId, limit: 500 });
|
|
71
|
+
|
|
72
|
+
const counts = {};
|
|
73
|
+
for (const r of top.results ?? []) counts[r.event] = r.count;
|
|
74
|
+
|
|
75
|
+
// dependencies.events maps eventName → [propertyNames]; property defs live in schema.properties
|
|
76
|
+
const propDefs = new Map();
|
|
77
|
+
for (const p of schema.properties ?? []) propDefs.set(p.name, p);
|
|
78
|
+
const deps = schema.dependencies?.events ?? {};
|
|
79
|
+
|
|
80
|
+
const events = (schema.events ?? []).map((ev) => {
|
|
81
|
+
const name = ev.name ?? ev;
|
|
82
|
+
const propNames = deps[name] ?? [];
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
count: counts[name] ?? 0,
|
|
86
|
+
pct: 0,
|
|
87
|
+
properties: propNames.map((pn) => {
|
|
88
|
+
const def = propDefs.get(pn) ?? {};
|
|
89
|
+
return { name: pn, type: def.type ?? 'string', description: def.description ?? '' };
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const userProps = (schema.users ?? []).map((p) => ({
|
|
95
|
+
name: p.name ?? p,
|
|
96
|
+
type: p.type ?? 'string',
|
|
97
|
+
description: p.description ?? '',
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return { projectId, projectName: null, fetchedAt: new Date().toISOString(), region, totalCount: 0, events, userProps, groups: schema.groups ?? {} };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ptPost(pathname, body) {
|
|
104
|
+
const res = await fetch(PT_BASE + pathname, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${bearer}` },
|
|
107
|
+
body: JSON.stringify({ client_id: CLIENT_ID, region, ...body }),
|
|
108
|
+
});
|
|
109
|
+
const json = await res.json().catch(() => ({}));
|
|
110
|
+
if (!res.ok) throw new Error(`POST ${pathname} → HTTP ${res.status}: ${json.error ?? JSON.stringify(json).slice(0, 300)}`);
|
|
111
|
+
return json;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function popOpt(name) {
|
|
115
|
+
const i = args.indexOf(name);
|
|
116
|
+
if (i === -1) return undefined;
|
|
117
|
+
const [, value] = args.splice(i, 2);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fail(msg) {
|
|
122
|
+
console.error(`✖ ${msg}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|