@koda-sl/baker-cli 0.287.0-dev.93be96120 → 0.290.2
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/README.md +30 -10
- package/dist/{chunk-LN5O53BG.js → chunk-NNQDWCFG.js} +60 -14
- package/dist/chunk-NNQDWCFG.js.map +1 -0
- package/dist/cli.js +1605 -539
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LN5O53BG.js.map +0 -1
package/README.md
CHANGED
|
@@ -1256,10 +1256,15 @@ baker analytics tracking # do the ad URLs carry the campaign, a
|
|
|
1256
1256
|
baker analytics tracking --platform meta # the same for one platform, read from their own ad account
|
|
1257
1257
|
baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
|
|
1258
1258
|
baker analytics map --platform google --remove kw # take that answer back
|
|
1259
|
-
baker analytics conversions # what counts
|
|
1259
|
+
baker analytics conversions # what counts, what each produced, and which Forms count nothing
|
|
1260
1260
|
baker analytics conversions --candidates # everything these pages do, and which of it is counted
|
|
1261
|
+
baker analytics conversions --flow contact # one Form's endings as event keys, traffic or not
|
|
1261
1262
|
baker analytics conversions --event page:request_demo --name "Demo requested"
|
|
1263
|
+
baker analytics conversions --event page:a --event submit:b --name "Lead" # both, as one number
|
|
1264
|
+
baker analytics conversions --event page:a --count-mode every_time # re-mode, keeping its name
|
|
1265
|
+
baker analytics conversions --rename "Lead form" --to "Lead" # merge two into one row
|
|
1262
1266
|
baker analytics conversions --remove page:request_demo
|
|
1267
|
+
baker analytics conversions --remove-name "Lead"
|
|
1263
1268
|
baker analytics delivery --page 2 # the next page of a long list
|
|
1264
1269
|
baker analytics submissions --flow contact # every attempt at a Form, delivered or not
|
|
1265
1270
|
```
|
|
@@ -1274,7 +1279,11 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
|
|
|
1274
1279
|
|
|
1275
1280
|
**Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
|
|
1276
1281
|
|
|
1277
|
-
**A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
|
|
1282
|
+
**A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
|
|
1283
|
+
|
|
1284
|
+
**The name is the grouping, so the CLI defends it.** Repeat `--event` to count several events under one `--name` in a single call; `--rename "<old>" --to "<new>"` renames every event under an outcome, and renaming *onto* a name that already exists **merges** them into one row and one number — the fix for a company whose Forms each invented their own word for the same thing. `--remove-name` drops an outcome entirely, `--remove` a single event key. A name that differs from one already counted only by case, accent, punctuation or a plural is **refused**: Baker groups by exact name, so "Booked a Call" beside "Booked a call" is a number split in half with both halves plausible. Every answer leads with the names already in use, and a company that has named nothing gets a standard set to start from — after which the rule is reuse, in that company's own language.
|
|
1285
|
+
|
|
1286
|
+
**`--flow <slug>` reads it from the Form's side**, listing that Form's endings already spelled as event keys with the name each is counted under, built from the Form's own file so it works before a single visitor arrives. With no `--flow`, the answer carries `flows[]` — every Form in the workspace with the step that counts for it, or nothing — which exists nowhere else: on every other report a Form that counts nothing looks exactly like a Form nobody converts on. The list of definitions is read from the store rather than from the reporting mirror, so an outcome named a second ago is in the reply that named it.
|
|
1278
1287
|
|
|
1279
1288
|
**Campaign parameters are per platform, and so are the numbers.** `baker analytics ads` returns `adPlatformTraffic` (visits, conversions and *tagged* visits for each of the nine platforms), `adParams` for the platform in `--platform` only, and `adParamsUnattributed` for names arriving on visits with no click id and no recognisable source. Read `adPlatformTraffic` first: a platform with visits and zero tagged visits has untagged ad URLs, and no mapping can fix that from Baker's side. `baker analytics map` needs `--platform` for any change, accepts a parameter name **nobody has sent yet** — which is how a tracking template is configured before its campaign runs — and takes an answer back with `--remove`. `--set <name>=ignore` says a parameter is not campaign information at all, so Baker stops storing it and stops listing it.
|
|
1280
1289
|
|
|
@@ -1315,25 +1324,33 @@ Two versions of one landing page, split 50/50 at the edge, read on one of the co
|
|
|
1315
1324
|
|
|
1316
1325
|
```bash
|
|
1317
1326
|
baker experiment plan --landing oferta --variant oferta--1 # FIRST — can this page settle the question at all?
|
|
1318
|
-
baker experiment plan --landing oferta --variant oferta--1 --goal
|
|
1327
|
+
baker experiment plan --landing oferta --variant oferta--1 --goal submit:book-a-call
|
|
1319
1328
|
baker experiment start --landing oferta --variant oferta--1 --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
|
|
1329
|
+
baker experiment update --id temp_exp_1 --goal submit:book-a-call # a staged test can still change what it measures
|
|
1330
|
+
baker experiment update --id a3f91c2b --because "…" # a live one can only change its wording
|
|
1320
1331
|
baker experiment status # the verdict on every test
|
|
1321
|
-
baker experiment status --id a3f91c2b --full # one test, with the posteriors behind it
|
|
1332
|
+
baker experiment status --id a3f91c2b --full # one test, with the confidence and posteriors behind it
|
|
1333
|
+
baker experiment history --landing oferta # every finished test on a page: claim, numbers, decision, learning
|
|
1322
1334
|
baker experiment pause --id a3f91c2b # everyone sees the original for now
|
|
1323
1335
|
baker experiment resume --id a3f91c2b # the split picks up with the same sides
|
|
1324
|
-
baker experiment finish --id a3f91c2b --keep variant
|
|
1336
|
+
baker experiment finish --id a3f91c2b --keep variant --learning "…" # end it now; say which version stays and what it taught
|
|
1337
|
+
baker experiment cancel --id a3f91c2b # call it off: staged → gone, live → ended, original kept
|
|
1325
1338
|
baker experiment fold # make the page's own content the version that was kept
|
|
1326
1339
|
```
|
|
1327
1340
|
|
|
1328
|
-
**A page has variants. A variant is never a page.** The variant is a real built folder at `src/pages/oferta--1/`, so it gets every quality gate a page gets — which matters, because it is about to be shown to half the client's visitors. What it does not get is a public address: it is absent from the Landings list and the sitemap,
|
|
1341
|
+
**A page has variants. A variant is never a page.** The variant is a real built folder at `src/pages/oferta--1/`, so it gets every quality gate a page gets — which matters, because it is about to be shown to half the client's visitors. What it does not get is a public address: it is absent from the Landings list and the sitemap, names the page's URL as its canonical, and `/oferta--1/` returns **404** at the edge. It is as indexable as the page it is a version of, deliberately — half the crawls of `/oferta/` are served the variant, so a `noindex` on it is a `noindex` on the live page. Both versions answer at `/oferta/`. Assignment happens while the HTML is served, so there is no flicker and nothing for an ad blocker to suppress, and the split is a path rewrite and never a redirect. To look at one side deliberately, `/oferta/?_bkr_preview=a` and `?_bkr_preview=b` serve it exactly, with no assignment and no cookie.
|
|
1342
|
+
|
|
1343
|
+
**The goal is one event, named by its key.** `--goal submit:book-a-call` — the same vocabulary the event stream uses (`form:<flow>:<node>:<trigger>`, `submit:<flow>`, `page:<name>`, `exit:<host>`), and `baker analytics conversions --candidates` lists the keys these pages produce. Not a conversion *name*, which groups several keys: a variant that swaps the page's Form for a shorter one converts on a different key, and a name would either count both endings as one outcome or count the new version as never converting at all — `--variant-goal` gives the new version its own event when it has one. The goal is fixed for the life of the test and the verdict is read on it alone. Every other conversion the company counts comes back under `secondary` on every result, with the same interval and a one-word reading, and decides nothing. A conversion counts for a visitor wherever it happens after they were shown a version — on the page, on another page, in a delivered lead, or in a deal the CRM posted a week later by email.
|
|
1329
1344
|
|
|
1330
|
-
**
|
|
1345
|
+
**Nothing ends a test on its own.** `status` returns a verdict; `finish --keep original|variant` is the decision, and it takes effect within seconds with nothing to publish. Ending a `keep_running` test records `inconclusive`. Going against the verdict is allowed and recorded. `pause` and `resume` switch the split off and on without losing the test; `cancel` is `finish --keep original` for a live test and takes a staged one back off the session. `update` changes what a test *says* — `--because`, `--change`, `--evidence` (repeatable, `source:what it showed`, optionally `|link`) — at any time, writes down what a finished one taught (`--learning`), and changes what it *measures* — `--goal`, `--lift`, `--expect` — only while it is still staged.
|
|
1331
1346
|
|
|
1332
|
-
**
|
|
1347
|
+
**A finished test is a record.** Its counts are frozen when it ends, so it reads the same next year as it did that day; its `learning` is what it taught, in a person's words. `history` returns every finished test — the claim, what was different, the numbers with their interval and confidence, what was kept and whether that went against the numbers, whether the belief held, and the learning — and `plan` returns the same entries as `pastTests` for the page it sizes, so a hypothesis is chosen against the page's record rather than in ignorance of it.
|
|
1348
|
+
|
|
1349
|
+
**The confidence is anytime-valid.** `--full` carries `evidence.confidence`, how sure the numbers are that the two versions differ at all, read off the same construction as the verdict — so it holds however often it is read, and a winner is called exactly when it crosses 95%. Beside it, `probabilityVariantBetter` is the Bayesian chance the new variant is the better one, which is for printing and never for deciding.
|
|
1333
1350
|
|
|
1334
1351
|
**Finishing with the variant takes two beats, and `fold` is the second.** `finish --keep variant` records which version is live and the edge serves it to everyone at once. `baker experiment fold` moves that version into the page's own folder, archives the one that lost as a self-contained snapshot under `src/_variants/<page id>/<n>/`, and deletes the variant folder. Until it runs, the page is served by a rewrite and cannot be tested again. Run `fold` with no arguments after any test ends, then publish; Baker drops the rewrite by itself once the new pages are live.
|
|
1335
1352
|
|
|
1336
|
-
**`plan` before building anything.** At a 3% conversion rate a +20% lift needs roughly 14,000 visitors *per variant*;
|
|
1353
|
+
**`plan` before building anything.** At a 3% conversion rate a +20% lift needs roughly 14,000 visitors *per variant*; `plan` says how long that is at this page's traffic, lists what this page's visitors actually went on to do, and what a 14, 28, 56 or 90-day window could settle. The duration is a guide and never a finish line — the test ends when the numbers are clear, and a page with no traffic yet still runs, sized on a stand-in that `warning` names (`--baseline 0.05` states the rate to expect). What `plan` refuses is what cannot be measured: no conversion defined, or a goal the page has demonstrably never produced. `--lift` sets the smallest lift worth detecting (default `0.2`) — halving it roughly quadruples the traffic needed.
|
|
1337
1354
|
|
|
1338
1355
|
### Google Analytics 4 (`baker ga4`)
|
|
1339
1356
|
|
|
@@ -5818,9 +5835,12 @@ Creates a new **version of a page** for an A/B test. It **imports the page's sec
|
|
|
5818
5835
|
|
|
5819
5836
|
```bash
|
|
5820
5837
|
baker landing variant oferta --fork Hero.astro
|
|
5821
|
-
baker landing variant oferta --fork Hero.astro
|
|
5838
|
+
baker landing variant oferta --fork Hero.astro --fork Proof.astro --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
|
|
5839
|
+
baker landing variant oferta --fork Hero.astro,Proof.astro --because "…" --change "…"
|
|
5822
5840
|
```
|
|
5823
5841
|
|
|
5842
|
+
- **`--fork` takes several components, repeated or comma-separated.** Both forms keep every name.
|
|
5843
|
+
|
|
5824
5844
|
- **You do not choose the slug.** It is `<page>--<n>`, numbered per page and never reused — so it cannot collide with an archived version, and it cannot merge with an older variant's analytics history. `--` is also what every tool decides by: which analytics id the page stamps, whether the sitemap lists it, and whether the edge answers the URL. It replaced a `<page>-b` convention that nine live pages across the fleet would have been misread by.
|
|
5825
5845
|
|
|
5826
5846
|
- **Sharing, not copying, is what keeps the test honest.** A duplicated page drifts from its control on every later edit — a price, a typo, a new testimonial — and the test then reports that drift plus the hypothesis as one result. A shared section is one file, so an edit reaches both arms at once. Baker follows the sharing too: both pages redeploy together, and both are re-critiqued when a shared section changes.
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
shouldEscalate
|
|
17
17
|
} from "./chunk-DZUVUGEP.js";
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
|
|
20
20
|
var require_safe_stable_stringify = __commonJS({
|
|
21
|
-
"
|
|
21
|
+
"../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
22
22
|
"use strict";
|
|
23
23
|
var { hasOwnProperty } = Object.prototype;
|
|
24
24
|
var stringify = configure2();
|
|
@@ -1092,7 +1092,7 @@ function resolveAdaptFormats(params) {
|
|
|
1092
1092
|
return params.formats ?? [];
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
|
-
//
|
|
1095
|
+
// ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
1096
1096
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
1097
1097
|
var configure = import__.default.configure;
|
|
1098
1098
|
var wrapper_default = import__.default;
|
|
@@ -6022,9 +6022,8 @@ async function ingestImageUrl(url, ctx) {
|
|
|
6022
6022
|
} catch (e) {
|
|
6023
6023
|
throw localExecError(ctx, `${url}: ${e.message}`);
|
|
6024
6024
|
}
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
}
|
|
6025
|
+
const note = normalizationNote(normalized);
|
|
6026
|
+
if (note) ctx.log(`ingest: ${url} ${note}`);
|
|
6028
6027
|
return uploadAndIngest({
|
|
6029
6028
|
bytes: normalized.bytes,
|
|
6030
6029
|
kind: "image",
|
|
@@ -6134,23 +6133,69 @@ async function rasterizeSvgToPng(bytes) {
|
|
|
6134
6133
|
return await sharp(bytes, { density }).png({ force: true, palette: false }).toBuffer();
|
|
6135
6134
|
}
|
|
6136
6135
|
var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
6137
|
-
|
|
6136
|
+
var MAX_MODEL_REFERENCE_BYTES = 30 * 1024 * 1024;
|
|
6137
|
+
async function fitWithinReferenceLimit(bytes, mime, maxBytes) {
|
|
6138
|
+
if (bytes.byteLength <= maxBytes) return { bytes, mime };
|
|
6139
|
+
const { default: sharp } = await import("sharp");
|
|
6140
|
+
const read = () => sharp(bytes, { animated: true });
|
|
6141
|
+
const meta = await read().metadata();
|
|
6142
|
+
const animated = (meta.pages ?? 1) > 1;
|
|
6143
|
+
const keepAlpha = meta.hasAlpha === true;
|
|
6144
|
+
const width = meta.width ?? 0;
|
|
6145
|
+
const encode = async (scale2) => {
|
|
6146
|
+
let pipeline2 = read();
|
|
6147
|
+
if (scale2 < 1 && width > 0) pipeline2 = pipeline2.resize({ width: Math.max(256, Math.round(width * scale2)) });
|
|
6148
|
+
if (animated && mime === "image/gif") return { bytes: await pipeline2.gif().toBuffer(), mime: "image/gif" };
|
|
6149
|
+
if (animated || keepAlpha) return { bytes: await pipeline2.webp({ quality: 90 }).toBuffer(), mime: "image/webp" };
|
|
6150
|
+
return { bytes: await pipeline2.jpeg({ quality: 90, mozjpeg: true }).toBuffer(), mime: "image/jpeg" };
|
|
6151
|
+
};
|
|
6152
|
+
let out = await encode(1);
|
|
6153
|
+
let scale = 1;
|
|
6154
|
+
for (let pass = 0; pass < 4 && out.bytes.byteLength > maxBytes; pass++) {
|
|
6155
|
+
scale *= Math.min(0.9, Math.sqrt(maxBytes / out.bytes.byteLength));
|
|
6156
|
+
out = await encode(scale);
|
|
6157
|
+
}
|
|
6158
|
+
return out;
|
|
6159
|
+
}
|
|
6160
|
+
async function toModelSafeImage(bytes, opts) {
|
|
6161
|
+
const maxBytes = opts?.maxBytes ?? MAX_MODEL_REFERENCE_BYTES;
|
|
6162
|
+
const fit = async (candidate, mime, rasterizedFrom) => {
|
|
6163
|
+
const fitted = await fitWithinReferenceLimit(candidate, mime, maxBytes);
|
|
6164
|
+
return {
|
|
6165
|
+
bytes: fitted.bytes,
|
|
6166
|
+
mime: fitted.mime,
|
|
6167
|
+
...rasterizedFrom ? { rasterizedFrom } : {},
|
|
6168
|
+
...fitted.bytes === candidate ? {} : { shrunkFromBytes: candidate.byteLength }
|
|
6169
|
+
};
|
|
6170
|
+
};
|
|
6138
6171
|
const safe = sniffImageMime(bytes);
|
|
6139
6172
|
if (safe && MODEL_SAFE_IMAGE_MIMES.has(safe)) {
|
|
6140
|
-
return
|
|
6173
|
+
return await fit(bytes, safe);
|
|
6141
6174
|
}
|
|
6142
6175
|
if (sniffSvg(bytes)) {
|
|
6143
|
-
return
|
|
6176
|
+
return await fit(await rasterizeSvgToPng(bytes), "image/png", "svg");
|
|
6144
6177
|
}
|
|
6145
6178
|
const { default: sharp } = await import("sharp");
|
|
6179
|
+
let png;
|
|
6180
|
+
let format;
|
|
6146
6181
|
try {
|
|
6147
6182
|
const img = sharp(bytes);
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
return { bytes: png, mime: "image/png", rasterizedFrom: format ?? "unknown" };
|
|
6183
|
+
format = (await img.metadata()).format ?? "unknown";
|
|
6184
|
+
png = await img.png({ force: true }).toBuffer();
|
|
6151
6185
|
} catch (e) {
|
|
6152
6186
|
throw new Error(`bytes are not a decodable image (${e.message})`);
|
|
6153
6187
|
}
|
|
6188
|
+
return await fit(png, "image/png", format);
|
|
6189
|
+
}
|
|
6190
|
+
function normalizationNote(image) {
|
|
6191
|
+
const parts = [];
|
|
6192
|
+
if (image.rasterizedFrom) parts.push(`normalized ${image.rasterizedFrom} -> ${image.mime}`);
|
|
6193
|
+
if (image.shrunkFromBytes !== void 0) {
|
|
6194
|
+
parts.push(
|
|
6195
|
+
`fitted ${image.shrunkFromBytes}B -> ${image.bytes.length}B as ${image.mime} (providers refuse a reference over ${MAX_MODEL_REFERENCE_BYTES}B)`
|
|
6196
|
+
);
|
|
6197
|
+
}
|
|
6198
|
+
return parts.length > 0 ? parts.join("; ") : null;
|
|
6154
6199
|
}
|
|
6155
6200
|
function hasAscii(buf, offset, sig) {
|
|
6156
6201
|
return buf.length >= offset + sig.length && buf.toString("ascii", offset, offset + sig.length) === sig;
|
|
@@ -6280,7 +6325,8 @@ async function execLocalFile(params, ctx) {
|
|
|
6280
6325
|
outBytes = normalized.bytes;
|
|
6281
6326
|
outMime = normalized.mime;
|
|
6282
6327
|
rasterizedFrom = normalized.rasterizedFrom;
|
|
6283
|
-
|
|
6328
|
+
const note = normalizationNote(normalized);
|
|
6329
|
+
if (note) ctx.log(`ingest: ${note}`);
|
|
6284
6330
|
}
|
|
6285
6331
|
const durationMs = probeVideoDurationMs(params.expect, outBytes, ctx);
|
|
6286
6332
|
const ref = await uploadAndIngest({
|
|
@@ -9153,4 +9199,4 @@ export {
|
|
|
9153
9199
|
defaultRegistry,
|
|
9154
9200
|
createEngineFromEnv
|
|
9155
9201
|
};
|
|
9156
|
-
//# sourceMappingURL=chunk-
|
|
9202
|
+
//# sourceMappingURL=chunk-NNQDWCFG.js.map
|