@ak--47/dungeon-master 1.6.4 → 1.7.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 +201 -0
- package/HOOKS.md +49 -0
- package/README.md +129 -8
- package/docs/guides/1.7.0-upgrade-guide.md +154 -0
- package/index.js +16 -1
- package/lib/core/config-validator.js +243 -13
- package/lib/core/context.js +39 -0
- package/lib/core/dungeon-loader.js +4 -1
- package/lib/generators/events.js +53 -7
- package/lib/generators/funnels.js +85 -11
- package/lib/generators/profiles.js +9 -4
- package/lib/orchestrators/mixpanel-sender.js +13 -1
- package/lib/orchestrators/user-loop.js +239 -9
- package/lib/utils/conditions.js +62 -0
- package/lib/utils/json-evaluator.js +12 -2
- package/lib/utils/utils.js +78 -7
- package/lib/verify/schema-validator.js +8 -0
- package/package.json +2 -10
- package/types.d.ts +303 -34
package/types.d.ts
CHANGED
|
@@ -7,10 +7,60 @@
|
|
|
7
7
|
type Primitives = string | number | boolean | Date | Record<string, any>;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* v1.7.0 (P1-1) — context handed to every property value function. Every member
|
|
11
|
+
* is optional: group profiles, lookup tables, ad spend and mirror props have no
|
|
12
|
+
* user, and `event` / `time` exist only while an event is being built.
|
|
13
|
+
*
|
|
14
|
+
* Existing zero-arity value functions keep working untouched — JavaScript ignores
|
|
15
|
+
* the extra argument. A function that DECLARES a parameter (`(ctx) => …`) is
|
|
16
|
+
* treated as context-aware and is never served from the source-string cache, so
|
|
17
|
+
* its result may legitimately differ per user or per event.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* userProps: {
|
|
21
|
+
* plan: ['free', 'pro'],
|
|
22
|
+
* revenue: (ctx) => ctx.profile.plan === 'pro' ? 100 : 10, // profile keys resolve in declaration order
|
|
23
|
+
* },
|
|
24
|
+
* superProps: {
|
|
25
|
+
* plan_on_event: (ctx) => ctx.profile.plan, // or use `stickyEventProps: ['plan']`
|
|
26
|
+
* total: (ctx) => ctx.event.price * ctx.event.quantity, // event props resolve in declaration order
|
|
27
|
+
* }
|
|
12
28
|
*/
|
|
13
|
-
export
|
|
29
|
+
export interface ValueContext {
|
|
30
|
+
/** The user's resolved profile (partially built while userProps resolve). Undefined for group/lookup/ad-spend/mirror values. */
|
|
31
|
+
profile?: Record<string, any>;
|
|
32
|
+
/** The partially-built event record (identity + time set; earlier properties already resolved). Undefined outside event generation. */
|
|
33
|
+
event?: Record<string, any>;
|
|
34
|
+
/** The event's timestamp in unix milliseconds, when an event is being built. */
|
|
35
|
+
time?: number;
|
|
36
|
+
/** The validated dungeon config. */
|
|
37
|
+
config: Dungeon;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* v1.7.0 (P2-1) — declarative weighted value. The numbers ARE the distribution:
|
|
42
|
+
* `{ __weights: { free: 60, pro: 30, enterprise: 10 } }` draws `free` 60% of the
|
|
43
|
+
* time. No automatic power law, no per-run winner; zero-weight keys never draw.
|
|
44
|
+
* Keys are strings (object keys), so numeric values come back as strings.
|
|
45
|
+
*/
|
|
46
|
+
export interface WeightedValue {
|
|
47
|
+
__weights: Record<string, number>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A "validValue" can be a primitive, an array of valid values, a declarative
|
|
52
|
+
* weighted form, or a function that returns one. Configs use this everywhere
|
|
53
|
+
* properties are user-defined.
|
|
54
|
+
*
|
|
55
|
+
* **Arrays of 3–19 unique strings get an automatic power-law draw** (~45% / 25% /
|
|
56
|
+
* 15% / decaying tail, one stable winner per array per run). Repeated entries are
|
|
57
|
+
* the weights (`['card', 'card', 'apple_pay']` = 2:1) and skip the power law.
|
|
58
|
+
* Opt out globally with `autoPowerLaw: false`, or state the distribution with
|
|
59
|
+
* `{ __weights }`.
|
|
60
|
+
*
|
|
61
|
+
* v1.7.0: the function arm receives a `ValueContext`. Zero-arity functions still work.
|
|
62
|
+
*/
|
|
63
|
+
export type ValueValid = Primitives | ValueValid[] | WeightedValue | ((ctx?: ValueContext) => ValueValid);
|
|
14
64
|
|
|
15
65
|
/**
|
|
16
66
|
* Mixpanel data residency region. Matches the set `mixpanel-import` accepts.
|
|
@@ -76,6 +126,12 @@ export interface DungeonSwitches {
|
|
|
76
126
|
hasBrowser?: boolean;
|
|
77
127
|
isAnonymous?: boolean;
|
|
78
128
|
alsoInferFunnels?: boolean;
|
|
129
|
+
/** v1.7.0 — see `Dungeon.singleCountry`. */
|
|
130
|
+
singleCountry?: string;
|
|
131
|
+
/** v1.7.0 — see `Dungeon.campaignPerUser`. */
|
|
132
|
+
campaignPerUser?: boolean;
|
|
133
|
+
/** v1.7.0 — see `Dungeon.stickyEventProps`. */
|
|
134
|
+
stickyEventProps?: string[];
|
|
79
135
|
}
|
|
80
136
|
|
|
81
137
|
/**
|
|
@@ -309,10 +365,63 @@ export interface Dungeon {
|
|
|
309
365
|
sessionTimeout?: number;
|
|
310
366
|
/** If true, auto-generates funnels from the events array in addition to any explicit funnels. */
|
|
311
367
|
alsoInferFunnels?: boolean;
|
|
312
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* Restrict all location data (`hasLocation`) to one country. Accepts the ISO
|
|
370
|
+
* code (`"US"`, `"GB"`) or the full name (`"United States"`), case-insensitive.
|
|
371
|
+
*
|
|
372
|
+
* v1.7.0: a value that matches no country in the location template THROWS at
|
|
373
|
+
* validation. Before 1.7.0 a miss (including `"US"`, which only matched by full
|
|
374
|
+
* name) silently emptied the location pool and deleted every geo property from
|
|
375
|
+
* events and profiles. Also accepted inside `switches`.
|
|
376
|
+
*/
|
|
313
377
|
singleCountry?: string;
|
|
314
|
-
/**
|
|
378
|
+
/**
|
|
379
|
+
* If true, the run delivers exactly `numEvents` events (forces `concurrency: 1`).
|
|
380
|
+
*
|
|
381
|
+
* v1.7.0: exact. Per-user budgets are scaled during the run so the total lands on
|
|
382
|
+
* the target, and the last user's stream is trimmed with a seeded uniform sample
|
|
383
|
+
* so the count never exceeds `numEvents`. If the users' capacity (rate × active
|
|
384
|
+
* days, minus drops) cannot reach the target, the run stops short and
|
|
385
|
+
* `result.warnings` carries a `numEvents` entry with `requested` / `applied`.
|
|
386
|
+
* Before 1.7.0 the flag stopped on the generated count (drops included) and
|
|
387
|
+
* never topped up, landing ~6% short with 4x headroom available.
|
|
388
|
+
*
|
|
389
|
+
* Without this flag the count is approximate (rate × users × days, thinned by
|
|
390
|
+
* born-late users and drops).
|
|
391
|
+
*/
|
|
315
392
|
strictEventCount?: boolean;
|
|
393
|
+
/**
|
|
394
|
+
* v1.7.0 (P1-2) — profile properties projected onto every event of the user.
|
|
395
|
+
* Each key must be declared in `userProps`, a persona's `properties`, or
|
|
396
|
+
* `superProps` (schema-first; the validator throws otherwise). Keys from the
|
|
397
|
+
* profile copy the profile's value (after the `user` hook). Keys declared only
|
|
398
|
+
* in `superProps` are resolved ONCE per user and held constant instead of
|
|
399
|
+
* re-rolled per event. Sticky values land after `superProps` and before the
|
|
400
|
+
* `event` hook, so hooks remain the final authority.
|
|
401
|
+
*
|
|
402
|
+
* The declarative alternative to `superProps: { plan: (ctx) => ctx.profile.plan }`.
|
|
403
|
+
* Also accepted inside `switches`.
|
|
404
|
+
*/
|
|
405
|
+
stickyEventProps?: string[];
|
|
406
|
+
/**
|
|
407
|
+
* v1.7.0 (P1-4) — one acquisition campaign per user. With `hasCampaigns: true`,
|
|
408
|
+
* each user draws one campaign template at birth; its `utm_source`,
|
|
409
|
+
* `utm_campaign`, `utm_medium`, `utm_content`, `utm_term` are stamped on the
|
|
410
|
+
* profile and every sampled touchpoint carries those same values instead of a
|
|
411
|
+
* fresh random template per event. Any UTM key already on the profile (from a
|
|
412
|
+
* persona's `properties` or the `user` hook) wins over the draw, so
|
|
413
|
+
* `personas: [{ name: 'paid', properties: { utm_source: 'google', utm_medium: 'cpc' } }]`
|
|
414
|
+
* makes "paid search converts better" declarative. Default `false`. Also
|
|
415
|
+
* accepted inside `switches`.
|
|
416
|
+
*/
|
|
417
|
+
campaignPerUser?: boolean;
|
|
418
|
+
/**
|
|
419
|
+
* v1.7.0 (P2-1) — set `false` to turn off the automatic power-law draw on
|
|
420
|
+
* arrays of 3–19 unique strings for the whole run (uniform picks instead).
|
|
421
|
+
* Default `true` (pre-1.7 behavior). Prefer `{ __weights }` when you want a
|
|
422
|
+
* specific distribution.
|
|
423
|
+
*/
|
|
424
|
+
autoPowerLaw?: boolean;
|
|
316
425
|
/** Internal flag for UI-triggered jobs (affects SCD credential handling). */
|
|
317
426
|
isUIJob?: boolean;
|
|
318
427
|
|
|
@@ -441,6 +550,16 @@ export interface Dungeon {
|
|
|
441
550
|
*
|
|
442
551
|
* Example: `{ day1: 0.40, day7: 0.20, day30: 0.08 }` produces a curve that
|
|
443
552
|
* approximates a typical product's 30-day retention.
|
|
553
|
+
*
|
|
554
|
+
* **Day 1 has a floor the curve cannot move (v1.7.0 doc, P1-6).** The curve
|
|
555
|
+
* picks which UTC days a user gets a SESSION; retention counts EVENTS. A funnel
|
|
556
|
+
* opened on the birth day spills its later steps across the following
|
|
557
|
+
* `timeToConvert` hours regardless of the day plan, so day-1 retention sits
|
|
558
|
+
* near 0.85 for funnel-driven dungeons no matter what `day1` says (measured
|
|
559
|
+
* 0.885 for an asked 0.15; days 7 and 30 followed the curve). Treat `day1` as
|
|
560
|
+
* governed by session and funnel structure and verify the curve from day 7 on.
|
|
561
|
+
* To lower day 1, shorten `timeToConvert` on funnels users enter on birth, or
|
|
562
|
+
* use an `everything` hook to drop next-day spill.
|
|
444
563
|
*/
|
|
445
564
|
retentionCurve?: {
|
|
446
565
|
type?: 'logarithmic' | 'linear';
|
|
@@ -532,13 +651,27 @@ export type MacroPreset = "flat" | "steady" | "growth" | "viral" | "decline";
|
|
|
532
651
|
|
|
533
652
|
/**
|
|
534
653
|
* Macro configuration object — fine-grained big-picture trend control.
|
|
654
|
+
*
|
|
655
|
+
* **Canonical spelling:** `macro: { preset, ...overrides }`. The top-level
|
|
656
|
+
* `bornRecentBias` / `percentUsersBornInDataset` / `preExistingSpread` keys are
|
|
657
|
+
* a legacy alias and win over the object when both are set.
|
|
658
|
+
*
|
|
659
|
+
* **Two shapes, two contracts (v1.7.0, R2-1):**
|
|
660
|
+
* - `{ preset: 'growth', percentUsersBornInDataset: 50 }` — a NAMED preset is a
|
|
661
|
+
* shape contract, so its born% cap applies (flat 12, steady 12, growth 30,
|
|
662
|
+
* viral 55, decline 5). Values above the cap are clamped and reported in
|
|
663
|
+
* `result.warnings` (`key: 'percentUsersBornInDataset'`).
|
|
664
|
+
* - `{ bornRecentBias: 0.3, percentUsersBornInDataset: 50 }` — NO `preset` is a
|
|
665
|
+
* custom macro: you own the shape, no cap applies, overrides are honored as
|
|
666
|
+
* written (missing fields fill from `flat`). Before 1.7.0 this shape was
|
|
667
|
+
* silently capped at 12.
|
|
535
668
|
*/
|
|
536
669
|
export type MacroConfig = {
|
|
537
|
-
/** Use a named macro preset as the base, then override individual fields. */
|
|
670
|
+
/** Use a named macro preset as the base, then override individual fields. Omit for a custom, uncapped macro. */
|
|
538
671
|
preset?: MacroPreset;
|
|
539
|
-
/** Bias for birth dates. -1..1; negative = early skew, positive = recent skew, 0 = uniform. */
|
|
672
|
+
/** Bias for birth dates. -1..1; negative = early skew, positive = recent skew, 0 = uniform. User-explicit values are clamped to [-0.5, 0.5]. */
|
|
540
673
|
bornRecentBias?: number;
|
|
541
|
-
/** Percentage of users born in dataset window (0..100). */
|
|
674
|
+
/** Percentage of users born in dataset window (0..100). Capped per named preset; uncapped without `preset`. */
|
|
542
675
|
percentUsersBornInDataset?: number;
|
|
543
676
|
/** "pinned" = pre-existing users stack at FIXED_BEGIN; "uniform" = spread across [FIXED_BEGIN-30d, FIXED_BEGIN]. */
|
|
544
677
|
preExistingSpread?: "pinned" | "uniform";
|
|
@@ -835,6 +968,10 @@ export interface RuntimeState {
|
|
|
835
968
|
eventCount: number;
|
|
836
969
|
storedEventCount: number;
|
|
837
970
|
userCount: number;
|
|
971
|
+
/** v1.7.0 (R2-5): profiles pushed to storage (ticks at push time; batch-mode safe). */
|
|
972
|
+
profilesGenerated: number;
|
|
973
|
+
/** v1.7.0 (R2-5): `_drop`-flagged profiles pushed to storage (never sent to /engage). */
|
|
974
|
+
profilesDropped: number;
|
|
838
975
|
isBatchMode: boolean;
|
|
839
976
|
verbose: boolean;
|
|
840
977
|
}
|
|
@@ -897,6 +1034,14 @@ export interface Context {
|
|
|
897
1034
|
incrementUsers(): void;
|
|
898
1035
|
incrementStoredEvents(count?: number): void;
|
|
899
1036
|
setStorage(storage: Storage): void;
|
|
1037
|
+
/** v1.7.0 (P2-2): record or bump an aggregated runtime warning (keyed by `entry.key`). */
|
|
1038
|
+
addWarning(entry: EngineWarning): void;
|
|
1039
|
+
/** v1.7.0 (P2-2): aggregated runtime warnings collected so far. */
|
|
1040
|
+
getWarnings(): EngineWarning[];
|
|
1041
|
+
/** v1.7.0 (R2-5) */
|
|
1042
|
+
incrementProfilesGenerated(): void;
|
|
1043
|
+
/** v1.7.0 (R2-5) */
|
|
1044
|
+
incrementProfilesDropped(): void;
|
|
900
1045
|
|
|
901
1046
|
// State getter methods
|
|
902
1047
|
getOperations(): number;
|
|
@@ -926,7 +1071,15 @@ export interface EventConfig {
|
|
|
926
1071
|
properties?: Record<string, ValueValid>;
|
|
927
1072
|
/** If true, this is the user's first-ever event (e.g., "sign up"). Used to create onboarding funnels. */
|
|
928
1073
|
isFirstEvent?: boolean;
|
|
929
|
-
/**
|
|
1074
|
+
/**
|
|
1075
|
+
* If true, generating this event signals the user has churned. The user stops
|
|
1076
|
+
* producing further events unless returnLikelihood allows them to come back.
|
|
1077
|
+
*
|
|
1078
|
+
* A churn event in the standalone pool is drawn by weight like any other, so it
|
|
1079
|
+
* ends every user after roughly `total weight / its weight` events — which caps
|
|
1080
|
+
* per-user volume and washes out `personas[].eventMultiplier` (measured 1.04x
|
|
1081
|
+
* for an asked 3x). Keep its weight low, or drive churn from a hook.
|
|
1082
|
+
*/
|
|
930
1083
|
isChurnEvent?: boolean;
|
|
931
1084
|
/** Probability (0-1) that a churned user returns and continues generating events. 0 = permanent churn, 1 = always returns. Only used when isChurnEvent is true. Default: 0 */
|
|
932
1085
|
returnLikelihood?: number;
|
|
@@ -1070,10 +1223,33 @@ export interface Funnel {
|
|
|
1070
1223
|
*/
|
|
1071
1224
|
props?: Record<string, ValueValid>;
|
|
1072
1225
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1226
|
+
* Profile conditions a user must satisfy to be offered this funnel (AND across
|
|
1227
|
+
* keys). The only engine mechanism that makes ONE segment convert differently on
|
|
1228
|
+
* ONE funnel — `personas[].conversionModifier` applies to every funnel.
|
|
1229
|
+
*
|
|
1230
|
+
* Each value is a scalar (strict equality) or an operator map:
|
|
1231
|
+
* `eq`, `neq`, `in`, `nin`, `gt`, `gte`, `lt`, `lte` (v1.7.0). Operators within
|
|
1232
|
+
* one key AND together. No `or`.
|
|
1233
|
+
*
|
|
1234
|
+
* The duplicate-funnel idiom: two funnels with the same `name` and `sequence`,
|
|
1235
|
+
* different `conditions` and rates.
|
|
1236
|
+
*
|
|
1237
|
+
* @example
|
|
1238
|
+
* userProps: { platform: ['iOS', 'Android'], seats: [1, 5, 10, 20] },
|
|
1239
|
+
* funnels: [
|
|
1240
|
+
* { name: 'Checkout', sequence: [...], conditions: { platform: 'iOS' }, conversionRate: 80 },
|
|
1241
|
+
* { name: 'Checkout', sequence: [...], conditions: { platform: 'Android' }, conversionRate: 40 },
|
|
1242
|
+
* { name: 'Upgrade', sequence: [...], conditions: { seats: { gte: 10 }, platform: { in: ['iOS', 'Android'] } } },
|
|
1243
|
+
* ]
|
|
1244
|
+
*
|
|
1245
|
+
* Validation (v1.7.0): function values, bare arrays, unknown operators, and
|
|
1246
|
+
* `in`/`nin` without an array THROW — those shapes silently never matched before.
|
|
1247
|
+
* A key not declared in `userProps` / `superProps` / any persona's `properties`
|
|
1248
|
+
* produces a `result.warnings` entry (only a `user` hook could supply it). Users
|
|
1249
|
+
* who match no funnel at all fall through to standalone events; the run reports
|
|
1250
|
+
* how many under `key: 'funnels.conditions'`.
|
|
1075
1251
|
*/
|
|
1076
|
-
conditions?:
|
|
1252
|
+
conditions?: FunnelConditions;
|
|
1077
1253
|
/**
|
|
1078
1254
|
* Experiment configuration for this funnel.
|
|
1079
1255
|
*
|
|
@@ -1131,7 +1307,9 @@ export interface Funnel {
|
|
|
1131
1307
|
}>;
|
|
1132
1308
|
|
|
1133
1309
|
/** @internal Resolved experiment config set by config-validator. */
|
|
1134
|
-
_experiment?: { name: string; variants: Array<{ name: string; conversionMultiplier: number; ttcMultiplier: number; weight: number }>; startUnix: number | null; sticky: boolean };
|
|
1310
|
+
_experiment?: { name: string; variants: Array<{ name: string; conversionMultiplier: number; ttcMultiplier: number; weight: number }>; startUnix: number | null; sticky: boolean; stampProfile: boolean };
|
|
1311
|
+
/** @internal v1.7.0 — set by the validator on the engine-synthesized catch-all funnel. */
|
|
1312
|
+
_catchAll?: boolean;
|
|
1135
1313
|
/** @internal Set by funnels.js during experiment handling. */
|
|
1136
1314
|
_experimentName?: string;
|
|
1137
1315
|
/** @internal Set by funnels.js during experiment handling. */
|
|
@@ -1214,8 +1392,43 @@ export interface ExperimentConfig {
|
|
|
1214
1392
|
* variants across passes.
|
|
1215
1393
|
*/
|
|
1216
1394
|
sticky?: boolean;
|
|
1395
|
+
/**
|
|
1396
|
+
* v1.7.0 (P0-2) — write the assigned variant onto the user profile as
|
|
1397
|
+
* `"Experiment: <name>": "<variant>"` so a funnel can be broken down by variant
|
|
1398
|
+
* without building a cohort from the exposure event. Default `true`. Stamped
|
|
1399
|
+
* lazily when the user is first exposed (respects `startDaysBeforeEnd`), so
|
|
1400
|
+
* never-exposed users carry no property and the `user` hook does not see it;
|
|
1401
|
+
* the `everything` hook does. Ignored when `sticky: false` (a re-rolled variant
|
|
1402
|
+
* has no single per-user value). The variant is NOT stamped on downstream funnel
|
|
1403
|
+
* step events (that would add undeclared columns).
|
|
1404
|
+
*/
|
|
1405
|
+
stampProfile?: boolean;
|
|
1217
1406
|
}
|
|
1218
1407
|
|
|
1408
|
+
/**
|
|
1409
|
+
* v1.7.0 (P0-1) — one funnel condition: a scalar (strict equality) or an operator map.
|
|
1410
|
+
*/
|
|
1411
|
+
export type FunnelCondition = Primitives | FunnelConditionOperators;
|
|
1412
|
+
|
|
1413
|
+
/** Operator map for a funnel condition. Every operator present must hold (AND). */
|
|
1414
|
+
export interface FunnelConditionOperators {
|
|
1415
|
+
/** strict equality */
|
|
1416
|
+
eq?: Primitives;
|
|
1417
|
+
/** strict inequality (a missing profile key satisfies it) */
|
|
1418
|
+
neq?: Primitives;
|
|
1419
|
+
/** value is one of these */
|
|
1420
|
+
in?: Primitives[];
|
|
1421
|
+
/** value is none of these (a missing profile key satisfies it) */
|
|
1422
|
+
nin?: Primitives[];
|
|
1423
|
+
gt?: number | string;
|
|
1424
|
+
gte?: number | string;
|
|
1425
|
+
lt?: number | string;
|
|
1426
|
+
lte?: number | string;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** Map of profile key → condition. AND across keys. */
|
|
1430
|
+
export type FunnelConditions = Record<string, FunnelCondition>;
|
|
1431
|
+
|
|
1219
1432
|
/** A single variant in an experiment. */
|
|
1220
1433
|
export interface ExperimentVariant {
|
|
1221
1434
|
/** Display name — appears in the "Variant name" property on $experiment_started. */
|
|
@@ -1328,11 +1541,47 @@ export interface GroupProfileSchema {
|
|
|
1328
1541
|
*/
|
|
1329
1542
|
export interface ImportResults {
|
|
1330
1543
|
events: ImportResult;
|
|
1331
|
-
|
|
1544
|
+
/**
|
|
1545
|
+
* v1.7.0 (R2-5): mixpanel-import's receipt plus two engine counters, so the
|
|
1546
|
+
* consumer can reconcile without guessing:
|
|
1547
|
+
* `generated - dropped_anonymous - failed === success`.
|
|
1548
|
+
*/
|
|
1549
|
+
users: ImportResult & {
|
|
1550
|
+
/** Profiles the engine pushed to storage (bots included). */
|
|
1551
|
+
generated: number;
|
|
1552
|
+
/** `_drop`-flagged anonymous non-converters that were never sent to /engage. */
|
|
1553
|
+
dropped_anonymous: number;
|
|
1554
|
+
};
|
|
1332
1555
|
groups: ImportResult[];
|
|
1333
1556
|
}
|
|
1334
1557
|
type ImportResult = import("mixpanel-import").ImportResults;
|
|
1335
1558
|
|
|
1559
|
+
/**
|
|
1560
|
+
* v1.7.0 (P2-2) — one value the engine changed or flagged. `result.warnings` is
|
|
1561
|
+
* always present (empty array when nothing was touched), regardless of `verbose`.
|
|
1562
|
+
*
|
|
1563
|
+
* Validator clamps come first (`severity: 'clamp'`, `applied !== requested`),
|
|
1564
|
+
* then run-level aggregates: conversionRate saturation per funnel and source
|
|
1565
|
+
* (`key: 'funnels[<name>].conversionRate:<source>'`), users matching no
|
|
1566
|
+
* conditioned funnel (`'funnels.conditions'`), churn washing out a persona
|
|
1567
|
+
* multiplier (`'personas.eventMultiplier'`), and a strictEventCount shortfall
|
|
1568
|
+
* (`'numEvents'`). Aggregated entries carry `count`.
|
|
1569
|
+
*/
|
|
1570
|
+
export interface EngineWarning {
|
|
1571
|
+
/** Config path the entry is about, e.g. `percentUsersBornInDataset`, `funnels[Checkout].conversionRate:persona "power" conversionModifier`. */
|
|
1572
|
+
key: string;
|
|
1573
|
+
/** What the config (or a modifier) asked for. Undefined when nothing was requested (auto-set). */
|
|
1574
|
+
requested?: unknown;
|
|
1575
|
+
/** What the engine used. Equals `requested` for pure warnings. */
|
|
1576
|
+
applied?: unknown;
|
|
1577
|
+
/** Plain-language explanation and what to change. */
|
|
1578
|
+
reason: string;
|
|
1579
|
+
/** `clamp` = a value was changed; `warn` = flagged, nothing changed. */
|
|
1580
|
+
severity: 'clamp' | 'warn';
|
|
1581
|
+
/** Number of occurrences folded into this entry (runtime aggregates only). */
|
|
1582
|
+
count?: number;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1336
1585
|
/**
|
|
1337
1586
|
* the end result of the data generation
|
|
1338
1587
|
*/
|
|
@@ -1353,6 +1602,11 @@ export type Result = {
|
|
|
1353
1602
|
lookupTableData: LookupTableData[][];
|
|
1354
1603
|
/** Mixpanel import results (only populated when a token was provided). */
|
|
1355
1604
|
importResults?: ImportResults;
|
|
1605
|
+
/**
|
|
1606
|
+
* v1.7.0 (P2-2): every value the engine clamped or flagged this run. Always
|
|
1607
|
+
* present, even when empty. See `EngineWarning`.
|
|
1608
|
+
*/
|
|
1609
|
+
warnings: EngineWarning[];
|
|
1356
1610
|
/** Absolute paths of all files written to disk. */
|
|
1357
1611
|
files?: string[];
|
|
1358
1612
|
/** Timing information. */
|
|
@@ -1454,34 +1708,38 @@ export interface Persona {
|
|
|
1454
1708
|
name: string;
|
|
1455
1709
|
/** Relative weight for persona assignment (higher = more users get this persona). */
|
|
1456
1710
|
weight: number;
|
|
1457
|
-
/**
|
|
1711
|
+
/**
|
|
1712
|
+
* Multiplier on the persona's whole per-user event budget (1.0 = normal). The
|
|
1713
|
+
* budget drives BOTH usage-funnel passes and standalone events, so a 3x persona
|
|
1714
|
+
* runs ~3x as many funnel passes (measured 2.9–3.2x).
|
|
1715
|
+
*
|
|
1716
|
+
* **`isChurnEvent` caps it.** A churn event in the standalone pool ends each
|
|
1717
|
+
* user after roughly the same number of events regardless of budget, so the
|
|
1718
|
+
* multiplier washes out (measured 1.04x for an asked 3x with a weight-1 churn
|
|
1719
|
+
* event among 16 weight units). When more than half the users churn and a
|
|
1720
|
+
* persona multiplier is in play, `result.warnings` carries
|
|
1721
|
+
* `key: 'personas.eventMultiplier'`. Lower the churn event's weight, raise
|
|
1722
|
+
* `returnLikelihood`, or drive churn from a hook.
|
|
1723
|
+
*/
|
|
1458
1724
|
eventMultiplier?: number;
|
|
1459
|
-
/** Multiplier for funnel conversion rates (1.0 = normal, 1.3 = 30% better). */
|
|
1725
|
+
/** Multiplier for funnel conversion rates (1.0 = normal, 1.3 = 30% better). Applies to every funnel; for one segment on one funnel use `Funnel.conditions`. */
|
|
1460
1726
|
conversionModifier?: number;
|
|
1461
1727
|
/**
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
* atoms + `engagementDecay`).
|
|
1728
|
+
* v1.7.0 (P1-3) — multiplier for funnel `timeToConvert` (1.0 = normal,
|
|
1729
|
+
* 0.5 = converts twice as fast). Composes after an experiment's `ttcMultiplier`
|
|
1730
|
+
* and before the `funnel-pre` hook. Must be a positive number.
|
|
1466
1731
|
*/
|
|
1467
|
-
|
|
1732
|
+
ttcModifier?: number;
|
|
1468
1733
|
/** Properties merged into user profiles for this persona. */
|
|
1469
1734
|
properties?: Record<string, ValueValid>;
|
|
1470
1735
|
/**
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1736
|
+
* Per-persona engagement decay override. This one IS implemented
|
|
1737
|
+
* (`user-loop.js` reads `persona.engagementDecay` before the global one).
|
|
1738
|
+
*
|
|
1739
|
+
* v1.7.0 removed the never-implemented `churnRate`, `activeWindow`, and
|
|
1740
|
+
* `soupOverride` from this type. The validator still accepts and warns on them.
|
|
1475
1741
|
*/
|
|
1476
|
-
activeWindow?: { maxDays: number };
|
|
1477
|
-
/** Per-persona engagement decay override. */
|
|
1478
1742
|
engagementDecay?: EngagementDecay;
|
|
1479
|
-
/**
|
|
1480
|
-
* Per-persona soup/timing override.
|
|
1481
|
-
* @deprecated — unimplemented; no-op. Declared surface only; nothing in lib/
|
|
1482
|
-
* reads it. Use the top-level `soup` config for timing shape.
|
|
1483
|
-
*/
|
|
1484
|
-
soupOverride?: SoupConfig;
|
|
1485
1743
|
}
|
|
1486
1744
|
|
|
1487
1745
|
/**
|
|
@@ -1496,7 +1754,18 @@ export interface WorldEvent {
|
|
|
1496
1754
|
startDay: number;
|
|
1497
1755
|
/** Duration in days (0.25 = 6 hours, null = permanent from startDay onward). */
|
|
1498
1756
|
duration?: number | null;
|
|
1499
|
-
/**
|
|
1757
|
+
/**
|
|
1758
|
+
* Volume multiplier during this event (3.0 = 3x events, 0.1 = 90% drop).
|
|
1759
|
+
*
|
|
1760
|
+
* Below 1: affected events are dropped at random so volume falls to the multiple.
|
|
1761
|
+
* Above 1 (v1.7.0, P0-3): affected in-window events are CLONED — `floor(m - 1)`
|
|
1762
|
+
* copies plus one more with probability `frac(m)` (2.5 = one guaranteed clone
|
|
1763
|
+
* and a 50% second) — each with a fresh `insert_id` and a timestamp spread
|
|
1764
|
+
* uniformly across the window (never past the dataset end). Measured 3.06x for
|
|
1765
|
+
* an asked 3x; before 1.7.0 values above 1 were a silent no-op (measured 1.08x).
|
|
1766
|
+
* Clones exist before `engagementDecay` and every hook, so they are visible to
|
|
1767
|
+
* `everything`. The same rule applies to `aftermath.volumeMultiplier`.
|
|
1768
|
+
*/
|
|
1500
1769
|
volumeMultiplier?: number;
|
|
1501
1770
|
/** Conversion rate modifier during this event. */
|
|
1502
1771
|
conversionModifier?: number;
|