@ak--47/dungeon-master 1.5.3 → 1.6.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/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +96 -0
- package/.claude/skills/create-project/provision.mjs +336 -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 +211 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -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 +2 -2
- 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 +5 -4
- 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 +3 -2
- 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 +296 -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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# 1.4.3 Upgrade Guide
|
|
2
|
+
|
|
3
|
+
**TL;DR: `percentUsersBornInDataset` defaults raised across all macro presets. No API changes. Existing dungeons that set `macro` or `percentUsersBornInDataset` explicitly are unaffected. Dungeons relying on the implicit "flat" default will now have 50% born-in-dataset users instead of 15%.**
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
### Breaking: `percentUsersBornInDataset` defaults raised
|
|
8
|
+
|
|
9
|
+
The "flat" macro preset (the default when no `macro` is specified) changed from 15% to 50%. All other presets were raised proportionally. No preset goes below 25%.
|
|
10
|
+
|
|
11
|
+
| Preset | Before (1.4.1) | After (1.4.3) | Notes |
|
|
12
|
+
|--------|---------------|---------------|-------|
|
|
13
|
+
| `flat` | 15 | **50** | Default when no macro specified |
|
|
14
|
+
| `steady` | 10 | **35** | |
|
|
15
|
+
| `growth` | 25 | **60** | |
|
|
16
|
+
| `viral` | 50 | **95** | Nearly all users born in window |
|
|
17
|
+
| `decline` | 5 | **25** | Floor — nothing below 25% |
|
|
18
|
+
|
|
19
|
+
#### Why
|
|
20
|
+
|
|
21
|
+
The old 15% default meant only ~750 out of 5,000 users were "born" during the dataset window. The other 85% were pre-existing users with full-window event histories. This caused two problems:
|
|
22
|
+
|
|
23
|
+
1. **Retention and onboarding hooks had tiny cohorts.** A hook that splits born-in-dataset users into "activated" vs "not activated" cohorts was working with ~750 users total. After splitting, cohorts of 50-100 users produced noisy, unreliable signal in verification and Mixpanel reports.
|
|
24
|
+
|
|
25
|
+
2. **The "end-bunchiness" fix overcorrected.** The original bunchiness problem (events piling up at the right edge) was solved in 1.3.0 by `avgEventsPerUserPerDay` — born-late users now get `rate × remaining_days` events instead of compressing `numEvents/numUsers` into a tiny window. With that fix in place, there's no reason to keep born-in-dataset users artificially low. More born users = richer onboarding/retention stories without any right-edge blow-up.
|
|
26
|
+
|
|
27
|
+
#### Migration
|
|
28
|
+
|
|
29
|
+
**Dungeons that explicitly set `macro` or `percentUsersBornInDataset`:** No change needed. Explicit values override the preset.
|
|
30
|
+
|
|
31
|
+
**Dungeons relying on the implicit default:** Will now generate more born-in-dataset users. This means:
|
|
32
|
+
- More "account created" / first-event traffic spread across the window
|
|
33
|
+
- Retention hooks will have larger cohorts (good)
|
|
34
|
+
- Total event count may decrease slightly because born-late users have shorter active windows
|
|
35
|
+
- If you need the old behavior, add `macro: { preset: "flat", percentUsersBornInDataset: 15 }` to your config
|
|
36
|
+
|
|
37
|
+
**Tests:** If you have tests with hardcoded event count thresholds (e.g., `expect(events.length).toBeGreaterThan(N)`) on small configs without explicit `percentUsersBornInDataset`, you may need to lower the thresholds. More born-in-dataset users = some users have shorter windows = fewer total events.
|
|
38
|
+
|
|
39
|
+
### New: HOOKS.md Recipe 3.22 — Retention Magic Number
|
|
40
|
+
|
|
41
|
+
A new hook recipe for the "N actions in first X days predicts retention" pattern, drawn from the Twitter dungeon. This is the most requested hook pattern for consumer products.
|
|
42
|
+
|
|
43
|
+
#### The Pattern
|
|
44
|
+
|
|
45
|
+
Users who perform a threshold action (e.g., follow 5 accounts) within an activation window (e.g., first 14 days) are retained. Users who don't hit the threshold go silent after a cutoff (e.g., 36 days after their first event).
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
if (meta.userIsBornInDataset) {
|
|
49
|
+
const userStart = dayjs(record[0]?.time);
|
|
50
|
+
const windowEnd = userStart.add(14, "days").toISOString();
|
|
51
|
+
const bin = binUsersByEventInRange(
|
|
52
|
+
record, "user followed",
|
|
53
|
+
record[0]?.time, windowEnd,
|
|
54
|
+
{ retained: [5, Infinity], not_retained: [0, 5] }
|
|
55
|
+
);
|
|
56
|
+
if (bin === "not_retained") {
|
|
57
|
+
const cutoff = userStart.add(36, "days");
|
|
58
|
+
dropEventsWhere(record, e => dayjs(e.time).isAfter(cutoff));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Key Design Lessons (from the Twitter dungeon iteration)
|
|
64
|
+
|
|
65
|
+
These lessons came from three rounds of verify-hooks iteration. Each one produced a WEAK or INVERSE verdict before the fix was found.
|
|
66
|
+
|
|
67
|
+
**1. User-relative cutoff, not dataset-relative.**
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// WRONG — misses users born after day 36
|
|
71
|
+
const cutoff = datasetStart.add(36, "days");
|
|
72
|
+
|
|
73
|
+
// RIGHT — each user's cutoff is relative to their own first event
|
|
74
|
+
const cutoff = userStart.add(36, "days");
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A dataset-anchored cutoff (e.g., `datasetStart + 36 days` = Feb 6) means users born on day 60 already have their first event past the cutoff — nothing gets dropped. The hook appears to fire but produces no visible retention difference. This was the most common failure mode.
|
|
78
|
+
|
|
79
|
+
**2. Threshold calibration against actual event distribution.**
|
|
80
|
+
|
|
81
|
+
Before picking a threshold, compute what's achievable:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
events/user/day × (event_weight / total_weight) × window_days = expected_count
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For the Twitter dungeon: `5 events/day × (5/84) × 14 days ≈ 4.2 follows in 14 days`. A threshold of 5 means ~15-20% of born users qualify — large enough for signal, small enough to be non-trivial.
|
|
88
|
+
|
|
89
|
+
The original threshold (7 follows in 10 days) produced a retained cohort of only 37 users out of 3,075 born-in-dataset. Too small. Lowering to 5 follows in 14 days expanded it to 900 users and the retention signal jumped from 1.56x to 2.19x.
|
|
90
|
+
|
|
91
|
+
**3. `percentUsersBornInDataset` is load-bearing for retention hooks.**
|
|
92
|
+
|
|
93
|
+
At the old 15% default, only ~750 users are born-in-dataset. After splitting into retained/not-retained, cohorts can be <100 users. The new 50% default ensures ~2,500 born users at 5K total — enough for both cohorts to produce clean signal.
|
|
94
|
+
|
|
95
|
+
**4. Verify with pct_past_dN, not avg_active_days.**
|
|
96
|
+
|
|
97
|
+
`avg_active_days` is confounded by birth time — users born late in the dataset naturally have fewer active days regardless of the hook. The correct metric is `pct_past_d36`: what fraction of each cohort has events beyond 36 days after their first event. This isolates the hook's effect from the birth-time distribution.
|
|
98
|
+
|
|
99
|
+
```sql
|
|
100
|
+
-- RIGHT metric: what % of each cohort survives past day 36
|
|
101
|
+
SELECT cohort,
|
|
102
|
+
COUNT(*) FILTER (WHERE active_days > 36) * 100.0 / COUNT(*) AS pct_past_d36
|
|
103
|
+
FROM ...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### New: Twitter/X Dungeon
|
|
107
|
+
|
|
108
|
+
`dungeons/user/twitter.js` — consumer social media platform with 4 verified hooks:
|
|
109
|
+
|
|
110
|
+
| Hook | Pattern | Verdict |
|
|
111
|
+
|------|---------|---------|
|
|
112
|
+
| 5 follows in 14 days → retention | Retention magic number | STRONG (2.19x) |
|
|
113
|
+
| Web ad bidding bug (25-day window) | Degradation and recovery | NAILED (6.0x latency, 0.56x conversion) |
|
|
114
|
+
| 5-8 tweets/week → verified purchase | Inverted-U sweet spot | NAILED (8333x signal) |
|
|
115
|
+
| Top business advertisers → premium placement | Profile-segment boost | STRONG (100% timeline, 1.62x quality) |
|
|
116
|
+
|
|
117
|
+
The dungeon also includes:
|
|
118
|
+
- Server-side ad auction pipeline (ad bid → ad impression → ad click) with heavy bid properties (auction ID, bid/winning amounts, keyword targeting, quality scores, bid latency)
|
|
119
|
+
- Group analytics on `advertiser_id` (business account_type users only)
|
|
120
|
+
- SCDs for membership_status, follower_count, following_count
|
|
121
|
+
- Multi-device identity model (iOS + Android + Desktop)
|
|
122
|
+
|
|
123
|
+
### Lesson: `advertiser_id` (group key) is NOT available during `everything` hook
|
|
124
|
+
|
|
125
|
+
The `everything` hook fires at `user-loop.js:280`, but group key stamping happens during storage push (after the everything hook completes). Any hook that needs to condition on `advertiser_id` (or any `groupKeys` value) cannot read it from events during the everything hook — it will be `undefined`.
|
|
126
|
+
|
|
127
|
+
**Workaround:** Use profile-based conditions (`meta.profile.account_type`) combined with deterministic hash splitting to simulate group-level cohort effects within the everything hook. The group key will be stamped correctly on the output events by the storage phase, but the hook logic must use a proxy.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// WRONG — advertiser_id not yet stamped during everything hook
|
|
131
|
+
const advId = userEvents.find(e => e.advertiser_id)?.advertiser_id;
|
|
132
|
+
|
|
133
|
+
// RIGHT — use profile + hash as proxy for group membership
|
|
134
|
+
if (profile.account_type === "business") {
|
|
135
|
+
const hash = String(uid).split("").reduce((a, c) => a + c.charCodeAt(0), 0);
|
|
136
|
+
const isTopAdvertiser = (hash % 5) < 2;
|
|
137
|
+
// ... modify events for top advertisers
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### New: Schema Validation (`lib/verify/schema-validator.js`)
|
|
142
|
+
|
|
143
|
+
Catches hooks that introduce undeclared columns (flag stamping). Integrated into `verifyDungeon()` — runs automatically.
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
import { deriveExpectedSchema, validateSchema } from '@ak--47/dungeon-master/verify';
|
|
147
|
+
|
|
148
|
+
const schema = deriveExpectedSchema(config); // Map<eventName, Set<propKey>>
|
|
149
|
+
const report = validateSchema(events, config); // { pass, eventTypes, summary, flagStamping }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Rule:** Hook-introduced columns with 100% coverage within their event type = PASS. Partial coverage = FAIL (flag stamping).
|
|
153
|
+
|
|
154
|
+
`verifyDungeon()` now returns `schemaReport` alongside `results`:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const { pass, results, schemaReport } = await verifyDungeon(config, checks);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### New: Property Type Helpers (complete Mixpanel coverage)
|
|
161
|
+
|
|
162
|
+
Three new helpers for the three missing Mixpanel property data types. All exported from `@ak--47/dungeon-master/utils`.
|
|
163
|
+
|
|
164
|
+
| Helper | Mixpanel Type | Example | Output |
|
|
165
|
+
|--------|--------------|---------|--------|
|
|
166
|
+
| `dateRange(start?, end?)` | Date | `dateRange()` | `"2024-03-15T14:22:33"` |
|
|
167
|
+
| `listOf(pool, {min, max})` | List | `listOf(["A","B","C"], {min:1, max:3})` | `["A","C"]` |
|
|
168
|
+
| `objectList(template, {min, max})` | List of Objects | `objectList({id: weighNumRange(1,100), cat: ["A","B"]}, {min:1, max:3})` | `[{id:42, cat:"A"}]` |
|
|
169
|
+
|
|
170
|
+
**Implementation detail:** `ListValue extends Array` marker class prevents `choose()` from picking a single element from the returned array. Serializes identically to plain arrays in JSON.
|
|
171
|
+
|
|
172
|
+
**`dateRange()` defaults to the dataset window** (`datasetStart` → `datasetEnd`). Accepts ISO strings, unix seconds, or dayjs objects for custom bounds.
|
|
173
|
+
|
|
174
|
+
### Skill Rename: `verify-hooks` → `verify-dungeon`
|
|
175
|
+
|
|
176
|
+
Reflects broader scope (schema + hooks + identity + experiments). All active code references updated; historical docs left as-is.
|
|
177
|
+
|
|
178
|
+
- Slash command: `/verify-dungeon <dungeon-path>`
|
|
179
|
+
- Default verify-runner.mjs run name: `verify-dungeon`
|
|
180
|
+
|
|
181
|
+
## Full Changelog
|
|
182
|
+
|
|
183
|
+
- `lib/templates/macro-presets.js`: Raised `percentUsersBornInDataset` across all 5 presets (floor 25%, flat default 50%)
|
|
184
|
+
- `HOOKS.md`: Added recipe 3.22 (Retention Magic Number), renumbered 3.22→3.23, 3.23→3.24
|
|
185
|
+
- `CLAUDE.md`: Updated default references from 15 to 50, added Property Type Helpers section, updated public API imports, updated skill references
|
|
186
|
+
- `types.d.ts`: Updated JSDoc on `percentUsersBornInDataset`, added `SchemaReport` interface, added `@ak--47/dungeon-master/utils` module declaration, updated `verifyDungeon` return type
|
|
187
|
+
- `.claude/skills/create-dungeon/SKILL.md`: Updated macro section, added Property Type Reference
|
|
188
|
+
- `.claude/skills/verify-dungeon/SKILL.md`: **Renamed** from verify-hooks, added Step 2b Schema Validation
|
|
189
|
+
- `.claude/skills/write-hooks/SKILL.md`: Updated verify-hooks → verify-dungeon references
|
|
190
|
+
- `dungeons/user/twitter.js`: New Twitter/X dungeon with 4 verified hooks
|
|
191
|
+
- `lib/verify/schema-validator.js`: **New** — `deriveExpectedSchema()`, `validateSchema()`
|
|
192
|
+
- `lib/verify/verify-dungeon.js`: Integrated schema validation, updated return type
|
|
193
|
+
- `lib/verify/index.js`: Added schema validator exports
|
|
194
|
+
- `lib/utils/utils.js`: Added `dateRange()`, `listOf()`, `objectList()`, `ListValue`, `DATASET_BEGIN` + `setDatasetBegin()`, `choose()` ListValue passthrough
|
|
195
|
+
- `index.js`: Import + call `setDatasetBegin()`
|
|
196
|
+
- `scripts/verify-runner.mjs`: Default run name → `verify-dungeon`
|
|
197
|
+
- `tests/schema-validator.test.js`: **New** — 17 tests
|
|
198
|
+
- `tests/property-helpers.test.js`: **New** — 21 tests
|
|
199
|
+
- `tests/macro-and-rate.test.js`: Added pinned-value snapshot test for all presets, added floor guard (≥25%)
|
|
200
|
+
- `tests/int.test.js`, `tests/sanity.test.js`: Adjusted event count thresholds for new default
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# 1.4.4 Upgrade Guide
|
|
2
|
+
|
|
3
|
+
**TL;DR: Fixes GCS-sourced imports hanging indefinitely. GCS writes no longer use `Content-Encoding: gzip`. Default format for GCS is now JSONL (was CSV). Group profile batch imports fixed. No API changes.**
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
### Fixed: GCS import hang (critical)
|
|
8
|
+
|
|
9
|
+
GCS-sourced imports (`writeToDisk: 'gs://...'` + `token`) hung indefinitely — the `DUNGEON_MASTER()` promise never resolved, zero events arrived in Mixpanel, and cleanup never ran. Three independent production runs on Cloud Run (Node 24) reproduced the issue.
|
|
10
|
+
|
|
11
|
+
**Root cause:** The three stream-write functions (`streamJSON`, `streamCSV`, `streamParquet`) used `createWriteStream({ gzip: true })` for all GCS paths. This set `Content-Encoding: gzip` on the GCS object — HTTP-level transparent compression. When `mixpanel-import` read the files back:
|
|
12
|
+
|
|
13
|
+
1. The HTTP transport layer automatically decompressed the data (transparent `Content-Encoding` handling)
|
|
14
|
+
2. But `mixpanel-import` checked the file extension (`.csv`, `.json`) and found no `.gz` → no gunzip applied
|
|
15
|
+
3. This happened to work when the HTTP layer decompressed, but **broke silently on environments where it didn't** (Cloud Run internal networking)
|
|
16
|
+
4. On failure: raw gzip bytes reached the parser, stream errors didn't propagate through `.pipe()` chains, downstream Transform streams never received `'end'`, the pipeline promise never resolved
|
|
17
|
+
|
|
18
|
+
**Fix:** GCS writes no longer use `createWriteStream({ gzip: true })`. Gzip is now handled at the application level (same as local writes):
|
|
19
|
+
|
|
20
|
+
- **No `gzip` config (default):** plain JSONL files written to GCS, no compression metadata
|
|
21
|
+
- **`gzip: true`:** data piped through `zlib.createGzip()` before writing to GCS. File gets `.gz` extension. No `Content-Encoding` header. `mixpanel-import` detects `.gz` extension, applies gunzip, parses correctly
|
|
22
|
+
|
|
23
|
+
Both modes verified end-to-end: write → GCS → `mixpanel-import` stream → parse → records.
|
|
24
|
+
|
|
25
|
+
### Changed: GCS default format is now JSONL
|
|
26
|
+
|
|
27
|
+
When `writeToDisk` is a `gs://` path and no `format` is specified, the default is now `"json"` (JSONL) instead of `"csv"`. This ensures `mixpanel-import` can stream-parse all data types (events, users, groups, SCDs, ad spend) without format-specific edge cases.
|
|
28
|
+
|
|
29
|
+
If you explicitly set `format: "csv"` or `format: "json"` in your config, nothing changes.
|
|
30
|
+
|
|
31
|
+
### Fixed: Group profiles skipped in batch mode
|
|
32
|
+
|
|
33
|
+
After `flushStorageToDisk()`, all HookedArray containers have `length === 0` (data is on disk). The group profile import loop had an early-exit guard:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
if (!groupEntity || groupEntity.length === 0) continue;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This skipped group profiles even when batch files existed on disk. Events, users, and ad spend all had `|| isBATCH_MODE` fallbacks — groups didn't. Fixed:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
if (!groupEntity) continue;
|
|
43
|
+
if (groupEntity.length === 0 && !isBATCH_MODE) continue;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Fixed: Group events batch mode fallback (latent)
|
|
47
|
+
|
|
48
|
+
The group events import block (`groupEventData?.length > 0`) had no `isBATCH_MODE` fallback. Currently latent (group events aren't initialized in storage), but fixed defensively for when that changes.
|
|
49
|
+
|
|
50
|
+
### Fixed: GCS gzip finish-event timing
|
|
51
|
+
|
|
52
|
+
When writing gzipped data to GCS, the promise resolved on the gzip transform's `finish` event (compression done) rather than the GCS stream's `finish` event (upload complete). This could produce truncated files on GCS. Fixed — the promise now resolves when the GCS upload stream finishes.
|
|
53
|
+
|
|
54
|
+
### Added: GCS round-trip tests
|
|
55
|
+
|
|
56
|
+
Three new tests in `tests/file-tracking.test.js`:
|
|
57
|
+
|
|
58
|
+
1. **JSONL round-trip** — write to GCS with default config, verify `.json` extension, stream back through `mixpanel-import`, verify records parse correctly
|
|
59
|
+
2. **Gzipped JSONL round-trip** — write with `gzip: true`, verify `.json.gz` extension, verify no `Content-Encoding: gzip` on GCS object, stream back and verify parsing
|
|
60
|
+
3. **Full dungeon round-trip** — events + users + groups + SCDs + ad spend, all written to GCS, all verified readable
|
|
61
|
+
|
|
62
|
+
### Added: `data/.gitkeep`
|
|
63
|
+
|
|
64
|
+
Ensures the `./data` directory exists for tests that write to the default local output path.
|
|
65
|
+
|
|
66
|
+
### Included: HOOKS.md in npm package
|
|
67
|
+
|
|
68
|
+
`HOOKS.md` (the hook encyclopedia — 24 recipes, 20 principles, atom/pattern reference) is now shipped with the npm package.
|
|
69
|
+
|
|
70
|
+
## Migration
|
|
71
|
+
|
|
72
|
+
### If you use `writeToDisk: 'gs://...'`
|
|
73
|
+
|
|
74
|
+
**Your imports will now work.** Previously they hung. No config changes needed.
|
|
75
|
+
|
|
76
|
+
If you were passing `format: 'csv'` explicitly, that still works — but JSONL is now the recommended format for GCS because `mixpanel-import` handles it more reliably across all data types.
|
|
77
|
+
|
|
78
|
+
### If you relied on GCS gzip compression
|
|
79
|
+
|
|
80
|
+
Previously, all GCS writes were silently gzipped via `Content-Encoding: gzip` regardless of `config.gzip`. Now:
|
|
81
|
+
|
|
82
|
+
- **Default (no `gzip`):** files are written uncompressed. Slightly larger on GCS, but these are temporary files that get cleaned up after import (`cleanup: true`).
|
|
83
|
+
- **`gzip: true`:** files are properly gzipped at the application level with `.gz` extension. Works correctly with `mixpanel-import`'s extension-based format detection.
|
|
84
|
+
|
|
85
|
+
### If you use group analytics with GCS
|
|
86
|
+
|
|
87
|
+
Group profiles were silently not imported in batch mode. They will now be imported correctly. You may see group profiles appear in Mixpanel that were previously missing.
|
|
88
|
+
|
|
89
|
+
## Files Changed
|
|
90
|
+
|
|
91
|
+
- `lib/utils/utils.js` — removed `{ gzip: true }` from all GCS `createWriteStream()` calls; added application-level gzip for GCS (pipe through `zlib.createGzip()`); fixed finish-event to wait on GCS stream
|
|
92
|
+
- `lib/core/config-validator.js` — GCS paths default to `format: 'json'`
|
|
93
|
+
- `lib/orchestrators/mixpanel-sender.js` — group profile batch guard; group events batch guard
|
|
94
|
+
- `tests/file-tracking.test.js` — 3 new GCS round-trip tests, `mixpanel-import` stream verification
|
|
95
|
+
- `tests/sanity.test.js` — ensure `./data` directory exists in test setup
|