@koda-sl/baker-cli 0.307.0-dev.ce3b0e7c2 → 0.307.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/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
validateCanvasDeep,
|
|
68
68
|
withoutCastName,
|
|
69
69
|
ytDlpBlockSignal
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-WONWYE7B.js";
|
|
71
71
|
import {
|
|
72
72
|
csvOrJson,
|
|
73
73
|
daysAgoIso,
|
|
@@ -12237,13 +12237,25 @@ registerSchema({
|
|
|
12237
12237
|
}
|
|
12238
12238
|
});
|
|
12239
12239
|
function extractFields(rows) {
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
return
|
|
12240
|
+
const keys = /* @__PURE__ */ new Set();
|
|
12241
|
+
foldObservedFields(rows, keys);
|
|
12242
|
+
return [...keys];
|
|
12243
|
+
}
|
|
12244
|
+
function resultColumns(gaql, rows) {
|
|
12245
|
+
const observed = extractFields(rows);
|
|
12246
|
+
const columns = [];
|
|
12247
|
+
for (const field of parseSelectedFields(gaql)) {
|
|
12248
|
+
const leaves = observed.filter((key) => key.startsWith(`${field}.`));
|
|
12249
|
+
columns.push(...leaves.length > 0 ? leaves : [field]);
|
|
12250
|
+
}
|
|
12251
|
+
for (const key of observed) columns.push(key);
|
|
12252
|
+
return [...new Set(columns)];
|
|
12253
|
+
}
|
|
12254
|
+
function isCsvPath(filePath) {
|
|
12255
|
+
return filePath.split(".").pop()?.toLowerCase() === "csv";
|
|
12243
12256
|
}
|
|
12244
12257
|
function writeRowsToFile(filePath, rows, fields, append) {
|
|
12245
|
-
|
|
12246
|
-
if (ext === "csv") {
|
|
12258
|
+
if (isCsvPath(filePath)) {
|
|
12247
12259
|
if (!append || !existsSync2(filePath)) {
|
|
12248
12260
|
writeFileSync(filePath, `${toCsvRow(fields)}
|
|
12249
12261
|
`, "utf-8");
|
|
@@ -12251,7 +12263,7 @@ function writeRowsToFile(filePath, rows, fields, append) {
|
|
|
12251
12263
|
const csvRows = rows.map((row) => toCsvRow(fields.map((f) => String(row[f] ?? ""))));
|
|
12252
12264
|
appendFileSync(filePath, `${csvRows.join("\n")}
|
|
12253
12265
|
`, "utf-8");
|
|
12254
|
-
} else if (
|
|
12266
|
+
} else if (filePath.split(".").pop()?.toLowerCase() === "jsonl") {
|
|
12255
12267
|
const lines = rows.map((row) => JSON.stringify(row));
|
|
12256
12268
|
if (append && existsSync2(filePath)) {
|
|
12257
12269
|
appendFileSync(filePath, `${lines.join("\n")}
|
|
@@ -12341,16 +12353,25 @@ var writeJsonlRows = (rows, _fields, notices) => {
|
|
|
12341
12353
|
for (const line of notices) writeLine(line);
|
|
12342
12354
|
for (const row of rows) writeLine(JSON.stringify(row));
|
|
12343
12355
|
};
|
|
12356
|
+
var OMITTED = "(omitted)";
|
|
12357
|
+
var OMITTED_FALSE = `false ${OMITTED}`;
|
|
12358
|
+
var OMITTED_LEGEND = `> ${OMITTED} = Google left this field off the row, which is how it sends false, 0 and "not set": the field WAS read. A field with no column here was not read.`;
|
|
12359
|
+
function mdCell(row, field, booleanColumns) {
|
|
12360
|
+
if (!(field in row)) return booleanColumns.has(field) ? OMITTED_FALSE : OMITTED;
|
|
12361
|
+
return String(row[field] ?? "").replace(/\|/g, "\\|");
|
|
12362
|
+
}
|
|
12344
12363
|
var writeMdRows = (rows, fields, notices) => {
|
|
12364
|
+
const booleanColumns = new Set(fields.filter((f) => rows.some((row) => typeof row[f] === "boolean")));
|
|
12345
12365
|
for (const line of notices) writeLine(line);
|
|
12346
12366
|
writeLine(`| ${fields.join(" | ")} |`);
|
|
12347
12367
|
writeLine(`| ${fields.map(() => "---").join(" | ")} |`);
|
|
12348
|
-
for (const row of rows) writeLine(`| ${fields.map((f) =>
|
|
12368
|
+
for (const row of rows) writeLine(`| ${fields.map((f) => mdCell(row, f, booleanColumns)).join(" | ")} |`);
|
|
12369
|
+
if (rows.some((row) => fields.some((f) => !(f in row)))) writeLine(OMITTED_LEGEND);
|
|
12349
12370
|
};
|
|
12350
12371
|
var ROW_WRITERS = { csv: writeCsvRows, jsonl: writeJsonlRows, md: writeMdRows };
|
|
12351
|
-
function writeRows(format, rows, notices) {
|
|
12372
|
+
function writeRows(format, rows, columns, notices) {
|
|
12352
12373
|
const write2 = ROW_WRITERS[format] ?? writeMdRows;
|
|
12353
|
-
write2(rows,
|
|
12374
|
+
write2(rows, columns, notices);
|
|
12354
12375
|
if (rows.length > REPEAT_NOTICE_ABOVE_ROWS) {
|
|
12355
12376
|
for (const line of notices) writeLine(line);
|
|
12356
12377
|
}
|
|
@@ -12377,7 +12398,7 @@ function outputResults(rows, fieldDescs, warnings, args4, cached, read) {
|
|
|
12377
12398
|
);
|
|
12378
12399
|
return;
|
|
12379
12400
|
}
|
|
12380
|
-
writeRows(format, rows, warningLines(allWarnings, format));
|
|
12401
|
+
writeRows(format, rows, resultColumns(read.gaql, rows), warningLines(allWarnings, format));
|
|
12381
12402
|
}
|
|
12382
12403
|
function resolveGaql(args4, limit) {
|
|
12383
12404
|
if (args4.preset) {
|
|
@@ -12496,15 +12517,27 @@ function missingFieldWarningsFor(gaql, observed, rowCount2) {
|
|
|
12496
12517
|
}
|
|
12497
12518
|
];
|
|
12498
12519
|
}
|
|
12520
|
+
function unwrittenColumnWarnings(header, observed) {
|
|
12521
|
+
const written = new Set(header);
|
|
12522
|
+
const unwritten = [...observed].filter((key) => !written.has(key));
|
|
12523
|
+
if (unwritten.length === 0) return [];
|
|
12524
|
+
return [
|
|
12525
|
+
{
|
|
12526
|
+
code: "UNWRITTEN_COLUMNS",
|
|
12527
|
+
message: `The file has no column for ${unwritten.join(", ")}: the header was fixed on the first page, which carried none of them, so their values from later pages are NOT in the file. Do not read those columns as empty. SELECT these fields by name instead of their parent, or write to a .jsonl file, then re-run.`
|
|
12528
|
+
}
|
|
12529
|
+
];
|
|
12530
|
+
}
|
|
12499
12531
|
function keywordServingOptions(gaql) {
|
|
12500
12532
|
return { statusReasonsSelected: parseSelectedFields(gaql).includes("ad_group_criterion.primary_status_reasons") };
|
|
12501
12533
|
}
|
|
12502
|
-
async function fetchAllPages(body, args4, servingOptions) {
|
|
12534
|
+
async function fetchAllPages(gaql, body, args4, servingOptions) {
|
|
12503
12535
|
let allRows = [];
|
|
12504
12536
|
let pageToken = args4.cursor;
|
|
12505
12537
|
let totalRowsWritten = 0;
|
|
12506
12538
|
let keywordLimits;
|
|
12507
12539
|
const observedFields = /* @__PURE__ */ new Set();
|
|
12540
|
+
let fileColumns;
|
|
12508
12541
|
do {
|
|
12509
12542
|
const requestBody2 = { ...body };
|
|
12510
12543
|
if (pageToken) requestBody2.pageToken = pageToken;
|
|
@@ -12513,7 +12546,8 @@ async function fetchAllPages(body, args4, servingOptions) {
|
|
|
12513
12546
|
pageToken = response.pageToken;
|
|
12514
12547
|
if (args4.out) {
|
|
12515
12548
|
const shouldAppend = args4.append || totalRowsWritten > 0;
|
|
12516
|
-
|
|
12549
|
+
fileColumns ??= resultColumns(gaql, rows);
|
|
12550
|
+
writeRowsToFile(resolve(args4.out), rows, fileColumns, shouldAppend);
|
|
12517
12551
|
totalRowsWritten += rows.length;
|
|
12518
12552
|
keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
|
|
12519
12553
|
foldObservedFields(rows, observedFields);
|
|
@@ -12527,7 +12561,8 @@ async function fetchAllPages(body, args4, servingOptions) {
|
|
|
12527
12561
|
lastPageToken: pageToken,
|
|
12528
12562
|
totalRowsWritten,
|
|
12529
12563
|
keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions),
|
|
12530
|
-
observedFields
|
|
12564
|
+
observedFields,
|
|
12565
|
+
fileColumns: fileColumns ?? []
|
|
12531
12566
|
};
|
|
12532
12567
|
}
|
|
12533
12568
|
async function executeQuery(finalQuery, args4, customerId, limit, warnings, useCache, cacheKey) {
|
|
@@ -12537,7 +12572,8 @@ async function executeQuery(finalQuery, args4, customerId, limit, warnings, useC
|
|
|
12537
12572
|
if (args4.cursor) body.pageToken = args4.cursor;
|
|
12538
12573
|
if (!useCache) body.skipCache = true;
|
|
12539
12574
|
const servingOptions = keywordServingOptions(finalQuery);
|
|
12540
|
-
const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields } = await fetchAllPages(
|
|
12575
|
+
const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields, fileColumns } = await fetchAllPages(
|
|
12576
|
+
finalQuery,
|
|
12541
12577
|
body,
|
|
12542
12578
|
args4,
|
|
12543
12579
|
servingOptions
|
|
@@ -12553,6 +12589,8 @@ async function executeQuery(finalQuery, args4, customerId, limit, warnings, useC
|
|
|
12553
12589
|
...warnings,
|
|
12554
12590
|
...renderKeywordLimits(keywordLimits),
|
|
12555
12591
|
...missingFieldWarningsFor(finalQuery, observedFields, totalRowsWritten),
|
|
12592
|
+
// Only a CSV has a header to fall out of; json and jsonl files carry every row whole.
|
|
12593
|
+
...isCsvPath(args4.out) ? unwrittenColumnWarnings(fileColumns, observedFields) : [],
|
|
12556
12594
|
...conversionMixWarnings(finalQuery),
|
|
12557
12595
|
...truncation
|
|
12558
12596
|
];
|
|
@@ -12584,6 +12622,7 @@ async function executeQuery(finalQuery, args4, customerId, limit, warnings, useC
|
|
|
12584
12622
|
args4,
|
|
12585
12623
|
false,
|
|
12586
12624
|
{
|
|
12625
|
+
gaql: finalQuery,
|
|
12587
12626
|
effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args4.all)),
|
|
12588
12627
|
all: Boolean(args4.all),
|
|
12589
12628
|
nextCursor: lastPageToken
|
|
@@ -12701,7 +12740,11 @@ Examples:
|
|
|
12701
12740
|
],
|
|
12702
12741
|
args4,
|
|
12703
12742
|
true,
|
|
12704
|
-
{
|
|
12743
|
+
{
|
|
12744
|
+
gaql: finalQuery,
|
|
12745
|
+
effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args4.all)),
|
|
12746
|
+
all: Boolean(args4.all)
|
|
12747
|
+
}
|
|
12705
12748
|
);
|
|
12706
12749
|
return;
|
|
12707
12750
|
}
|
|
@@ -13611,7 +13654,7 @@ var SEND_PLATFORM_SPECS = {
|
|
|
13611
13654
|
},
|
|
13612
13655
|
googleAds: {
|
|
13613
13656
|
title: "Google Ads",
|
|
13614
|
-
eventMeans: "The
|
|
13657
|
+
eventMeans: "The id of one of the account's conversion actions \u2014 copy it from `conversionActions`, never type one",
|
|
13615
13658
|
eventKind: "id",
|
|
13616
13659
|
credentialFields: ["oauthProviderId"],
|
|
13617
13660
|
setupHint: "Connect the Google Ads conversion tag to a Google Ads account in Tracking"
|
|
@@ -13808,7 +13851,14 @@ var sendsResponseSchema = z28.object({
|
|
|
13808
13851
|
title: z28.string(),
|
|
13809
13852
|
eventMeans: z28.string(),
|
|
13810
13853
|
setupHint: z28.string(),
|
|
13811
|
-
tags: z28.array(z28.object({ tagId: z28.string(), label: z28.string() }))
|
|
13854
|
+
tags: z28.array(z28.object({ tagId: z28.string(), label: z28.string() })),
|
|
13855
|
+
/**
|
|
13856
|
+
* The conversions the platform already holds, by the id a send's `event`
|
|
13857
|
+
* takes and the name the person gave them. Google Ads only: its `event`
|
|
13858
|
+
* is an id Google issued, and an id the account does not hold is
|
|
13859
|
+
* accepted and files against nothing.
|
|
13860
|
+
*/
|
|
13861
|
+
conversionActions: z28.array(z28.object({ id: z28.string(), name: z28.string(), primaryForGoal: z28.boolean() })).optional()
|
|
13812
13862
|
})
|
|
13813
13863
|
),
|
|
13814
13864
|
/** Every field a send forwards. Nothing to map. */
|
|
@@ -22235,7 +22285,26 @@ var OPTIMIZATION_GOALS = [
|
|
|
22235
22285
|
"THRUPLAY",
|
|
22236
22286
|
"VALUE",
|
|
22237
22287
|
"CONVERSATIONS",
|
|
22238
|
-
"DERIVED_EVENTS"
|
|
22288
|
+
"DERIVED_EVENTS",
|
|
22289
|
+
// Added 2026-09-18 from Meta's own generated enum spec
|
|
22290
|
+
// (facebook-business-sdk-codegen › AdSet_optimization_goal). Every one of these
|
|
22291
|
+
// is a goal Meta accepts and Baker used to refuse at stage time — the messaging
|
|
22292
|
+
// and Instagram-profile goals are the ones that cost real campaigns, because a
|
|
22293
|
+
// click-to-WhatsApp ad set cannot be expressed without them.
|
|
22294
|
+
"ADVERTISER_SILOED_VALUE",
|
|
22295
|
+
"APP_INSTALLS_AND_OFFSITE_CONVERSIONS",
|
|
22296
|
+
"AUTOMATIC_OBJECTIVE",
|
|
22297
|
+
"ENGAGED_PAGE_VIEWS",
|
|
22298
|
+
"IN_APP_VALUE",
|
|
22299
|
+
"MEANINGFUL_CALL_ATTEMPT",
|
|
22300
|
+
"MESSAGING_APPOINTMENT_CONVERSION",
|
|
22301
|
+
"MESSAGING_DEEP_CONVERSATION_AND_FOLLOW",
|
|
22302
|
+
"MESSAGING_PURCHASE_CONVERSION",
|
|
22303
|
+
"PROFILE_AND_PAGE_ENGAGEMENT",
|
|
22304
|
+
"PROFILE_VISIT",
|
|
22305
|
+
"REMINDERS_SET",
|
|
22306
|
+
"SUBSCRIBERS",
|
|
22307
|
+
"VISIT_INSTAGRAM_PROFILE"
|
|
22239
22308
|
];
|
|
22240
22309
|
var OPTIMIZATION_GOALS_REQUIRING_PAGE = [
|
|
22241
22310
|
"LEAD_GENERATION",
|
|
@@ -22257,7 +22326,23 @@ var DESTINATION_TYPES = [
|
|
|
22257
22326
|
"WHATSAPP",
|
|
22258
22327
|
"PHONE_CALL",
|
|
22259
22328
|
"INSTAGRAM_PROFILE",
|
|
22260
|
-
"SHOP_AUTOMATIC"
|
|
22329
|
+
"SHOP_AUTOMATIC",
|
|
22330
|
+
// Added 2026-09-18 from adcampaign_destination_type. The four MESSAGING_* values
|
|
22331
|
+
// are the click-to-message destinations (Messenger / Instagram Direct / WhatsApp
|
|
22332
|
+
// in every combination Meta ships); without them Baker could not stage a
|
|
22333
|
+
// click-to-WhatsApp ad set at all.
|
|
22334
|
+
"APPLINKS_AUTOMATIC",
|
|
22335
|
+
"FACEBOOK",
|
|
22336
|
+
"FACEBOOK_LIVE",
|
|
22337
|
+
"FACEBOOK_PAGE",
|
|
22338
|
+
"IMAGINE",
|
|
22339
|
+
"INSTAGRAM_LIVE",
|
|
22340
|
+
"INSTAGRAM_PROFILE_AND_FACEBOOK_PAGE",
|
|
22341
|
+
"MESSAGING_INSTAGRAM_DIRECT_MESSENGER",
|
|
22342
|
+
"MESSAGING_INSTAGRAM_DIRECT_MESSENGER_WHATSAPP",
|
|
22343
|
+
"MESSAGING_INSTAGRAM_DIRECT_WHATSAPP",
|
|
22344
|
+
"MESSAGING_MESSENGER_WHATSAPP",
|
|
22345
|
+
"ON_EVENT"
|
|
22261
22346
|
];
|
|
22262
22347
|
var CUSTOM_EVENT_TYPES = [
|
|
22263
22348
|
"PURCHASE",
|
|
@@ -22271,7 +22356,28 @@ var CUSTOM_EVENT_TYPES = [
|
|
|
22271
22356
|
"SUBSCRIBE",
|
|
22272
22357
|
"START_TRIAL",
|
|
22273
22358
|
"CONTACT",
|
|
22274
|
-
"OTHER"
|
|
22359
|
+
"OTHER",
|
|
22360
|
+
// Added 2026-09-18 from AdPromotedObject_custom_event_type. SCHEDULE,
|
|
22361
|
+
// SUBMIT_APPLICATION, SERVICE_BOOKING_REQUEST and
|
|
22362
|
+
// MESSAGING_CONVERSATION_STARTED_7D are the conversions service businesses and
|
|
22363
|
+
// click-to-message campaigns actually optimize toward.
|
|
22364
|
+
"ACHIEVEMENT_UNLOCKED",
|
|
22365
|
+
"ADD_TO_WISHLIST",
|
|
22366
|
+
"AD_IMPRESSION",
|
|
22367
|
+
"CUSTOMIZE_PRODUCT",
|
|
22368
|
+
"D2_RETENTION",
|
|
22369
|
+
"D7_RETENTION",
|
|
22370
|
+
"DONATE",
|
|
22371
|
+
"FIND_LOCATION",
|
|
22372
|
+
"LEVEL_ACHIEVED",
|
|
22373
|
+
"LISTING_INTERACTION",
|
|
22374
|
+
"MESSAGING_CONVERSATION_STARTED_7D",
|
|
22375
|
+
"RATE",
|
|
22376
|
+
"SCHEDULE",
|
|
22377
|
+
"SERVICE_BOOKING_REQUEST",
|
|
22378
|
+
"SPENT_CREDITS",
|
|
22379
|
+
"SUBMIT_APPLICATION",
|
|
22380
|
+
"TUTORIAL_COMPLETION"
|
|
22275
22381
|
];
|
|
22276
22382
|
var ATTRIBUTION_EVENT_TYPES = ["CLICK_THROUGH", "VIEW_THROUGH", "ENGAGED_VIDEO_VIEW"];
|
|
22277
22383
|
var ADVANTAGE_AUDIENCE_OPEN_AGE_MAX = 65;
|
|
@@ -22390,7 +22496,37 @@ var CTA_TYPES2 = [
|
|
|
22390
22496
|
"LINK_CARD",
|
|
22391
22497
|
"DIAL_CODE",
|
|
22392
22498
|
"FIND_YOUR_GROUPS",
|
|
22393
|
-
"START_ORDER"
|
|
22499
|
+
"START_ORDER",
|
|
22500
|
+
// Added 2026-09-18 from Meta's generated enum spec
|
|
22501
|
+
// (AdCreative_call_to_action_type). Additive on purpose: 37 of the values above
|
|
22502
|
+
// are absent from that spec and present in the live 400, which is the
|
|
22503
|
+
// disagreement the note above is about — so the spec widens this list and never
|
|
22504
|
+
// prunes it.
|
|
22505
|
+
"AUDIO_CALL",
|
|
22506
|
+
"BROWSE_SHOP",
|
|
22507
|
+
"CONTACT",
|
|
22508
|
+
"FIND_A_GROUP",
|
|
22509
|
+
"FIND_OUT_MORE",
|
|
22510
|
+
"FOLLOW_NEWS_STORYLINE",
|
|
22511
|
+
"FOLLOW_PAGE",
|
|
22512
|
+
"FOLLOW_USER",
|
|
22513
|
+
"GET_DETAILS",
|
|
22514
|
+
"GET_STARTED",
|
|
22515
|
+
"SAY_THANKS",
|
|
22516
|
+
"SEE_SHOP",
|
|
22517
|
+
"SEND_A_GIFT",
|
|
22518
|
+
"SEND_GIFT_MONEY",
|
|
22519
|
+
"SHARE",
|
|
22520
|
+
"SHOP_WITH_AI",
|
|
22521
|
+
"SOTTO_SUBSCRIBE",
|
|
22522
|
+
"TRY_ON_WITH_AI",
|
|
22523
|
+
"VIDEO_ANNOTATION",
|
|
22524
|
+
"VIDEO_CALL",
|
|
22525
|
+
"VIEW_CART",
|
|
22526
|
+
"VIEW_IN_CART",
|
|
22527
|
+
"VISIT_PAGES_FEED",
|
|
22528
|
+
"VISIT_WEBSITE",
|
|
22529
|
+
"WOODHENGE_SUPPORT"
|
|
22394
22530
|
];
|
|
22395
22531
|
var ASSET_FEED_CTA_TYPES = [
|
|
22396
22532
|
"OPEN_LINK",
|
|
@@ -22509,6 +22645,11 @@ var ENROLL_STATUSES = ["OPT_IN", "OPT_OUT"];
|
|
|
22509
22645
|
var STAGEABLE_CREATE_STATUSES4 = ["ACTIVE", "PAUSED"];
|
|
22510
22646
|
var UPDATE_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED"];
|
|
22511
22647
|
var CUSTOM_AUDIENCE_SUBTYPES = ["CUSTOM", "WEBSITE", "ENGAGEMENT", "LOOKALIKE"];
|
|
22648
|
+
var CUSTOMER_FILE_SOURCES = [
|
|
22649
|
+
"USER_PROVIDED_ONLY",
|
|
22650
|
+
"PARTNER_PROVIDED_ONLY",
|
|
22651
|
+
"BOTH_USER_AND_PARTNER_PROVIDED"
|
|
22652
|
+
];
|
|
22512
22653
|
var MEDIA_KINDS = ["image", "video"];
|
|
22513
22654
|
var CURRENCY_MINIMUMS2 = {
|
|
22514
22655
|
USD: { dailyBudgetMin: 1 },
|
|
@@ -22526,6 +22667,119 @@ import { z as z31 } from "zod";
|
|
|
22526
22667
|
// ../api/src/ads-meta/placements.ts
|
|
22527
22668
|
var FACEBOOK_POSITIONS_REQUIRING_FEED = ["marketplace", "search", "profile_feed", "notification"];
|
|
22528
22669
|
var PLACEMENT_TARGETING_DOC = "https://developers.facebook.com/docs/marketing-api/audiences/reference/placement-targeting/";
|
|
22670
|
+
var PUBLISHER_PLATFORMS = ["facebook", "instagram", "threads", "messenger", "audience_network"];
|
|
22671
|
+
var FACEBOOK_POSITIONS = [
|
|
22672
|
+
"feed",
|
|
22673
|
+
"right_hand_column",
|
|
22674
|
+
"marketplace",
|
|
22675
|
+
"video_feeds",
|
|
22676
|
+
"story",
|
|
22677
|
+
"search",
|
|
22678
|
+
"instream_video",
|
|
22679
|
+
"facebook_reels",
|
|
22680
|
+
"facebook_reels_overlay",
|
|
22681
|
+
"profile_feed",
|
|
22682
|
+
"notification"
|
|
22683
|
+
];
|
|
22684
|
+
var INSTAGRAM_POSITIONS = [
|
|
22685
|
+
"stream",
|
|
22686
|
+
"story",
|
|
22687
|
+
"explore",
|
|
22688
|
+
"explore_home",
|
|
22689
|
+
"reels",
|
|
22690
|
+
"profile_feed",
|
|
22691
|
+
"ig_search",
|
|
22692
|
+
"profile_reels"
|
|
22693
|
+
];
|
|
22694
|
+
var AUDIENCE_NETWORK_POSITIONS = ["classic", "rewarded_video"];
|
|
22695
|
+
var MESSENGER_POSITIONS = ["sponsored_messages", "story"];
|
|
22696
|
+
var THREADS_POSITIONS = ["threads_stream"];
|
|
22697
|
+
var DEVICE_PLATFORMS = ["mobile", "desktop", "connected_tv"];
|
|
22698
|
+
var PLACEMENT_VOCABULARIES = {
|
|
22699
|
+
publisher_platforms: PUBLISHER_PLATFORMS,
|
|
22700
|
+
facebook_positions: FACEBOOK_POSITIONS,
|
|
22701
|
+
instagram_positions: INSTAGRAM_POSITIONS,
|
|
22702
|
+
audience_network_positions: AUDIENCE_NETWORK_POSITIONS,
|
|
22703
|
+
messenger_positions: MESSENGER_POSITIONS,
|
|
22704
|
+
threads_positions: THREADS_POSITIONS,
|
|
22705
|
+
device_platforms: DEVICE_PLATFORMS
|
|
22706
|
+
};
|
|
22707
|
+
var SAME_SURFACE_ON_THIS_PLATFORM = {
|
|
22708
|
+
facebook_positions: { reels: "facebook_reels", stream: "feed", stories: "story", ig_search: "search" },
|
|
22709
|
+
instagram_positions: {
|
|
22710
|
+
feed: "stream",
|
|
22711
|
+
facebook_reels: "reels",
|
|
22712
|
+
stories: "story",
|
|
22713
|
+
search: "ig_search"
|
|
22714
|
+
}
|
|
22715
|
+
};
|
|
22716
|
+
function placementFieldsAccepting(value, except) {
|
|
22717
|
+
return Object.keys(PLACEMENT_VOCABULARIES).filter(
|
|
22718
|
+
(field) => field !== except && PLACEMENT_VOCABULARIES[field].includes(value)
|
|
22719
|
+
);
|
|
22720
|
+
}
|
|
22721
|
+
function placementValueIssue(field, value) {
|
|
22722
|
+
const renamed = SAME_SURFACE_ON_THIS_PLATFORM[field]?.[value];
|
|
22723
|
+
if (renamed) {
|
|
22724
|
+
return {
|
|
22725
|
+
field,
|
|
22726
|
+
value,
|
|
22727
|
+
kind: "wrong-field",
|
|
22728
|
+
fix: `Meta calls that surface "${renamed}" in ${field} \u2014 use "${renamed}"`
|
|
22729
|
+
};
|
|
22730
|
+
}
|
|
22731
|
+
const elsewhere = placementFieldsAccepting(value, field);
|
|
22732
|
+
if (elsewhere.length > 0) {
|
|
22733
|
+
return {
|
|
22734
|
+
field,
|
|
22735
|
+
value,
|
|
22736
|
+
kind: "wrong-field",
|
|
22737
|
+
fix: `"${value}" belongs to ${elsewhere.join(" / ")}, not ${field} \u2014 move it there or drop it`
|
|
22738
|
+
};
|
|
22739
|
+
}
|
|
22740
|
+
return {
|
|
22741
|
+
field,
|
|
22742
|
+
value,
|
|
22743
|
+
kind: "unknown",
|
|
22744
|
+
fix: `"${value}" is not a placement Baker knows; ${field} accepts ${PLACEMENT_VOCABULARIES[field].join(", ")}`
|
|
22745
|
+
};
|
|
22746
|
+
}
|
|
22747
|
+
function invalidPlacementValues(targeting) {
|
|
22748
|
+
const issues = [];
|
|
22749
|
+
for (const field of Object.keys(PLACEMENT_VOCABULARIES)) {
|
|
22750
|
+
const allowed = PLACEMENT_VOCABULARIES[field];
|
|
22751
|
+
const selected = targeting?.[field] ?? [];
|
|
22752
|
+
for (const value of selected) {
|
|
22753
|
+
if (!allowed.includes(value)) {
|
|
22754
|
+
issues.push(placementValueIssue(field, value));
|
|
22755
|
+
}
|
|
22756
|
+
}
|
|
22757
|
+
}
|
|
22758
|
+
return issues;
|
|
22759
|
+
}
|
|
22760
|
+
function placementCoRequirementIssues(targeting) {
|
|
22761
|
+
const facebook = targeting?.facebook_positions ?? [];
|
|
22762
|
+
const instagram = targeting?.instagram_positions ?? [];
|
|
22763
|
+
const threads = targeting?.threads_positions ?? [];
|
|
22764
|
+
const devices = targeting?.device_platforms;
|
|
22765
|
+
const issues = [];
|
|
22766
|
+
if (facebook.includes("story")) {
|
|
22767
|
+
if (!facebook.includes("feed") && !instagram.includes("story")) {
|
|
22768
|
+
issues.push(
|
|
22769
|
+
'Meta refuses Facebook "story" on its own \u2014 select "feed" in facebook_positions or "story" in instagram_positions alongside it'
|
|
22770
|
+
);
|
|
22771
|
+
}
|
|
22772
|
+
if (devices && !devices.includes("mobile")) {
|
|
22773
|
+
issues.push(
|
|
22774
|
+
'Facebook "story" only runs on mobile \u2014 add "mobile" to device_platforms or drop the story placement'
|
|
22775
|
+
);
|
|
22776
|
+
}
|
|
22777
|
+
}
|
|
22778
|
+
if (threads.includes("threads_stream") && !instagram.includes("stream")) {
|
|
22779
|
+
issues.push('Threads "threads_stream" cannot be selected alone \u2014 add "stream" to instagram_positions');
|
|
22780
|
+
}
|
|
22781
|
+
return issues;
|
|
22782
|
+
}
|
|
22529
22783
|
var CONSTRAINED_PLACEMENTS = [
|
|
22530
22784
|
{
|
|
22531
22785
|
position: "facebook_reels_overlay",
|
|
@@ -22640,6 +22894,7 @@ var metaTargetingSchema = z31.object({
|
|
|
22640
22894
|
instagram_positions: z31.array(z31.string()).optional(),
|
|
22641
22895
|
audience_network_positions: z31.array(z31.string()).optional(),
|
|
22642
22896
|
messenger_positions: z31.array(z31.string()).optional(),
|
|
22897
|
+
threads_positions: z31.array(z31.string()).optional(),
|
|
22643
22898
|
device_platforms: z31.array(z31.string()).optional(),
|
|
22644
22899
|
targeting_automation: z31.object({ advantage_audience: z31.union([z31.literal(0), z31.literal(1)]) }).partial().optional()
|
|
22645
22900
|
}).catchall(z31.unknown());
|
|
@@ -22771,6 +23026,13 @@ var adSetFields = {
|
|
|
22771
23026
|
start_time: z31.number().int().positive().optional(),
|
|
22772
23027
|
end_time: z31.number().int().positive().optional(),
|
|
22773
23028
|
targeting: metaTargetingSchema,
|
|
23029
|
+
/**
|
|
23030
|
+
* The live ad set this one was copied from, verbatim, by `duplicate`. Baker
|
|
23031
|
+
* only — `adSetCommonBody` never sends it to Meta. Set exclusively when the
|
|
23032
|
+
* targeting IS the source's targeting, because that is what it licenses:
|
|
23033
|
+
* placement validation is skipped for it (see `validateAdSetPlacements`).
|
|
23034
|
+
*/
|
|
23035
|
+
clonedFrom: z31.string().regex(NUMERIC_ID_REGEX3).optional(),
|
|
22774
23036
|
/** EU Digital Services Act: who benefits from / pays for the ad. Auto-filled from the account for EU geo when omitted. */
|
|
22775
23037
|
dsa_beneficiary: z31.string().min(1).max(100).optional(),
|
|
22776
23038
|
dsa_payor: z31.string().min(1).max(100).optional()
|
|
@@ -22871,6 +23133,9 @@ function validateAdvantageAudience(p, ctx) {
|
|
|
22871
23133
|
}
|
|
22872
23134
|
}
|
|
22873
23135
|
function validateAdSetPlacements(p, ctx) {
|
|
23136
|
+
if (p.clonedFrom) {
|
|
23137
|
+
return;
|
|
23138
|
+
}
|
|
22874
23139
|
const missingFeed = positionsMissingRequiredFeed(p.targeting);
|
|
22875
23140
|
if (missingFeed.length > 0) {
|
|
22876
23141
|
ctx.addIssue({
|
|
@@ -22879,6 +23144,19 @@ function validateAdSetPlacements(p, ctx) {
|
|
|
22879
23144
|
message: `Meta requires "feed" alongside ${missingFeed.join(", ")} \u2014 add "feed" to facebook_positions or drop those positions (${PLACEMENT_TARGETING_DOC})`
|
|
22880
23145
|
});
|
|
22881
23146
|
}
|
|
23147
|
+
for (const issue of invalidPlacementValues(p.targeting)) {
|
|
23148
|
+
if (issue.kind !== "wrong-field") {
|
|
23149
|
+
continue;
|
|
23150
|
+
}
|
|
23151
|
+
ctx.addIssue({
|
|
23152
|
+
code: "custom",
|
|
23153
|
+
path: ["targeting", issue.field],
|
|
23154
|
+
message: `${issue.field} does not accept "${issue.value}". ${issue.fix} (${PLACEMENT_TARGETING_DOC})`
|
|
23155
|
+
});
|
|
23156
|
+
}
|
|
23157
|
+
for (const message of placementCoRequirementIssues(p.targeting)) {
|
|
23158
|
+
ctx.addIssue({ code: "custom", path: ["targeting"], message: `${message} (${PLACEMENT_TARGETING_DOC})` });
|
|
23159
|
+
}
|
|
22882
23160
|
}
|
|
22883
23161
|
var adSetCreateSchema = z31.object(adSetFields).superRefine((p, ctx) => {
|
|
22884
23162
|
validateAdSetBudgetAndBid(p, ctx);
|
|
@@ -23195,7 +23473,7 @@ var customAudienceCreateSchema = z31.object({
|
|
|
23195
23473
|
name: z31.string().min(1).max(META_LIMITS.audience.nameMax),
|
|
23196
23474
|
subtype: z31.enum(CUSTOM_AUDIENCE_SUBTYPES),
|
|
23197
23475
|
description: z31.string().max(500).optional(),
|
|
23198
|
-
customer_file_source: z31.
|
|
23476
|
+
customer_file_source: z31.enum(CUSTOMER_FILE_SOURCES).optional(),
|
|
23199
23477
|
retention_days: z31.number().int().min(1).max(META_LIMITS.audience.retentionDaysMax).optional(),
|
|
23200
23478
|
lookalike_spec: lookalikeSpecSchema.optional(),
|
|
23201
23479
|
/** Website/engagement rule — structural passthrough validated by Meta. */
|
|
@@ -29296,7 +29574,7 @@ function stagedHints(definitions) {
|
|
|
29296
29574
|
const staged = definitions.filter((row) => row.staged === true);
|
|
29297
29575
|
if (staged.length === 0) return [];
|
|
29298
29576
|
return [
|
|
29299
|
-
`Staged on this chat, not counted yet: ${staged.map((row) => `"${row.name}"`).join(", ")}. The person sees ${staged.length === 1 ? "it" : "them"} under
|
|
29577
|
+
`Staged on this chat, not counted yet: ${staged.map((row) => `"${row.name}"`).join(", ")}. The person sees ${staged.length === 1 ? "it" : "them"} under Sources in the chat's changes, on the event's own row, and ${staged.length === 1 ? "it counts" : "they count"} from when they publish the chat; discarding the chat drops ${staged.length === 1 ? "it" : "them"}. Publishing also rescores events Baker ALREADY RECORDED under the key \u2014 never anything that happened in HubSpot or elsewhere before the event existed, so an event this chat just made starts at zero. Tell them that, not that it is already counted.`,
|
|
29300
29578
|
'When you talk to the person, call each event by its name ("Appointment scheduled"), never its key (`page:Appointment scheduled`): the key is how Baker spells it internally, and the `page:` part means nothing to them.'
|
|
29301
29579
|
];
|
|
29302
29580
|
}
|
|
@@ -30044,7 +30322,7 @@ var args = {
|
|
|
30044
30322
|
},
|
|
30045
30323
|
event: {
|
|
30046
30324
|
type: "string",
|
|
30047
|
-
description: 'What the platform calls it \u2014 `platforms[].eventMeans` says what each one takes: a Meta or Google Analytics event name (Purchase, Lead, generate_lead), a Google Ads conversion action id, a LinkedIn conversion rule id, an X event tag id. On Meta and Google Analytics it may be WRITTEN FROM THE EVENT with the same {{path}} mentions --parameter takes: --event {{properties.plan}} sends each event under the plan it carries, --event "Purchase {{properties.tier}}" mixes text and a field. An event carrying nothing there is not uploaded. Google Ads, LinkedIn and X take an id THEY issued, so a template is refused there \u2014 pass what varies as a --parameter instead. For a GOOGLE ADS id, list the account\'s conversion actions first and pick by name \u2014 `baker ads google query "SELECT conversion_action.id, conversion_action.name, conversion_action.primary_for_goal FROM conversion_action WHERE conversion_action.status = \'ENABLED\'"` \u2014 never guess: Google accepts an upload against an id the account does not hold and files it against nothing',
|
|
30325
|
+
description: 'What the platform calls it \u2014 `platforms[].eventMeans` says what each one takes: a Meta or Google Analytics event name (Purchase, Lead, generate_lead), a Google Ads conversion action id copied from `platforms[].conversionActions` (never typed: an id the account does not hold is refused), a LinkedIn conversion rule id, an X event tag id. On Meta and Google Analytics it may be WRITTEN FROM THE EVENT with the same {{path}} mentions --parameter takes: --event {{properties.plan}} sends each event under the plan it carries, --event "Purchase {{properties.tier}}" mixes text and a field. An event carrying nothing there is not uploaded. Google Ads, LinkedIn and X take an id THEY issued, so a template is refused there \u2014 pass what varies as a --parameter instead. For a GOOGLE ADS id, list the account\'s conversion actions first and pick by name \u2014 `baker ads google query "SELECT conversion_action.id, conversion_action.name, conversion_action.primary_for_goal FROM conversion_action WHERE conversion_action.status = \'ENABLED\'"` \u2014 never guess: Google accepts an upload against an id the account does not hold and files it against nothing',
|
|
30048
30326
|
required: false
|
|
30049
30327
|
},
|
|
30050
30328
|
account: {
|
|
@@ -31472,7 +31750,7 @@ var conversionsCommand3 = (() => {
|
|
|
31472
31750
|
const args4 = {
|
|
31473
31751
|
event: {
|
|
31474
31752
|
type: "string",
|
|
31475
|
-
description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Inside a chat it is STAGED: the chat lists it under
|
|
31753
|
+
description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Inside a chat it is STAGED: the chat lists it under Sources, on its event, and it counts from when the chat is published \u2014 then it scores the WHOLE history, not just from then on, so counting the right event also corrects last month's reports. An event a client's system sends (the API, HubSpot) counts here too: it is credited to the LAST LANDING its person viewed, up to 90 days before, so a deal won weeks later is that landing's conversion. A person who never saw a landing is not counted at all. Such an event is usually also a funnel step (`baker funnel`) and a send (`baker analytics sends`)",
|
|
31476
31754
|
required: false
|
|
31477
31755
|
},
|
|
31478
31756
|
name: {
|
|
@@ -31529,7 +31807,7 @@ var conversionsCommand3 = (() => {
|
|
|
31529
31807
|
return defineCommand97({
|
|
31530
31808
|
meta: {
|
|
31531
31809
|
name: "conversions",
|
|
31532
|
-
description: "What this company counts as a conversion \u2014 the numerator of every conversion rate on Measure. A page event (a Form ending, a data-baker-* event) and an event one of the client's systems sends (a HubSpot event made with `baker analytics sources --create-hubspot`, an order the server posts) both count; a system's event is credited to the last landing its person viewed. A HubSpot event is `page:<its name>` here \u2014 `--event 'page:Deal won' --name 'Deal won'`. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. Inside a chat a change is STAGED: the person sees it under
|
|
31810
|
+
description: "What this company counts as a conversion \u2014 the numerator of every conversion rate on Measure. A page event (a Form ending, a data-baker-* event) and an event one of the client's systems sends (a HubSpot event made with `baker analytics sources --create-hubspot`, an order the server posts) both count; a system's event is credited to the last landing its person viewed. A HubSpot event is `page:<its name>` here \u2014 `--event 'page:Deal won' --name 'Deal won'`. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. Inside a chat a change is STAGED: the person sees it under Sources in the chat's changes, on the event's own row and it applies when they publish, retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers. A read shows a proposal marked `staged`. Call an event by its name when you talk to the person, never by its key. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
|
|
31533
31811
|
},
|
|
31534
31812
|
args: args4,
|
|
31535
31813
|
run: async ({ args: raw }) => {
|
|
@@ -46440,6 +46718,9 @@ function inertSideEffectHints(tree) {
|
|
|
46440
46718
|
"Fix each by setting the id its own step raises: `baker flows schema node <nodeType>` lists them."
|
|
46441
46719
|
];
|
|
46442
46720
|
}
|
|
46721
|
+
function funnelPlacementHint(slug) {
|
|
46722
|
+
return `Whether this Form moves anyone along the company funnel is not in _data.json either: \`baker funnel\` names every Form no step counts. If it names this one, place its ending on the step it moves people to \u2014 \`submit:${slug}\`, or its booking trigger, both listed by \`baker funnel --moments\` \u2014 with \`baker funnel --step\`, which replaces the step it names, so keep that step's other ways in.`;
|
|
46723
|
+
}
|
|
46443
46724
|
function displayName(slug) {
|
|
46444
46725
|
const tree = readFlowTree(slug);
|
|
46445
46726
|
const name = tree.displayName;
|
|
@@ -46503,7 +46784,8 @@ var showCommand4 = defineCommand127({
|
|
|
46503
46784
|
// an answer that is not in this file at all. What a Form counts is a
|
|
46504
46785
|
// named event in Baker, and a Form can be perfectly configured, fire
|
|
46505
46786
|
// every pixel it has, and still report converting nobody.
|
|
46506
|
-
`What this Form counts as a conversion is NOT in _data.json and not in any side effect here: it is a named event held in Baker. Read it, and this Form's own endings spelled as event keys, with \`baker analytics conversions --flow ${slug}
|
|
46787
|
+
`What this Form counts as a conversion is NOT in _data.json and not in any side effect here: it is a named event held in Baker. Read it, and this Form's own endings spelled as event keys, with \`baker analytics conversions --flow ${slug}\`.`,
|
|
46788
|
+
funnelPlacementHint(slug)
|
|
46507
46789
|
]
|
|
46508
46790
|
});
|
|
46509
46791
|
}
|
|
@@ -47107,7 +47389,7 @@ function parsePreset(raw) {
|
|
|
47107
47389
|
var args3 = {
|
|
47108
47390
|
steps: {
|
|
47109
47391
|
type: "string",
|
|
47110
|
-
description: 'The WHOLE funnel, as a JSON array. One object per step: {"key":"meeting_held","displayName":"Demo held","stepClass":"engaged","after":["lead"],"triggers":[{"id":"t1","kind":"event","eventKey":"page:demo_held"}]}. It REPLACES the funnel rather than adding to it, so read first and restate everything you want to keep. `key` is permanent and reaches reporting and URLs (lowercase, digits, underscores); `displayName` is the client\'s own words and can be anything. `stepClass` is the stage, one of visited, lead, engaged, customer, repeat, lost \u2014 six plain words every business has, used to compare one company\'s funnel with another\'s. `after` is a LIST, so two ways into one step is expressible and normal. `triggers` is what MOVES somebody onto the step, any one of them being enough, and there are
|
|
47392
|
+
description: 'The WHOLE funnel, as a JSON array. One object per step: {"key":"meeting_held","displayName":"Demo held","stepClass":"engaged","after":["lead"],"triggers":[{"id":"t1","kind":"event","eventKey":"page:demo_held"}]}. It REPLACES the funnel rather than adding to it, so read first and restate everything you want to keep. `key` is permanent and reaches reporting and URLs (lowercase, digits, underscores); `displayName` is the client\'s own words and can be anything. `stepClass` is the stage, one of visited, lead, engaged, customer, repeat, lost \u2014 six plain words every business has, used to compare one company\'s funnel with another\'s. `after` is a LIST, so two ways into one step is expressible and normal. `triggers` is what MOVES somebody onto the step, any one of them being enough, and there are three kinds: `{"id":"t1","kind":"event","eventKey":"submit:contact"}` for anything the company\'s Forms, pages or systems produced, used by its own key with nothing named first; `{"id":"t2","kind":"landing","landingId":"<from --moments>"}` for one landing page being viewed; `{"id":"t3","kind":"any_landing"}` for any landing being viewed. Take every key and id from `--moments`. A trigger carries no amount: an event is worth what it says, and a step is worth the sum. Where a step SENDS its people is not written here: a send belongs to the system\'s event (`baker analytics sends`), and the step reads it back as `sends[]`',
|
|
47111
47393
|
required: false
|
|
47112
47394
|
},
|
|
47113
47395
|
step: {
|
|
@@ -47122,12 +47404,12 @@ var args3 = {
|
|
|
47122
47404
|
},
|
|
47123
47405
|
preset: {
|
|
47124
47406
|
type: "string",
|
|
47125
|
-
description: "Draw one of the ready-made graphs instead of stating steps: b2b_sales, b2b_inbound, saas_self_serve, saas_sales_assisted, ecommerce_purchase.
|
|
47407
|
+
description: "Draw one of the ready-made graphs instead of stating steps: b2b_sales, b2b_inbound, saas_self_serve, saas_sales_assisted, ecommerce_purchase. It draws over the whole funnel \u2014 every company already has one, so prefer --step to change a step. Run --templates first to see what each one contains",
|
|
47126
47408
|
required: false
|
|
47127
47409
|
},
|
|
47128
47410
|
moments: {
|
|
47129
47411
|
type: "boolean",
|
|
47130
|
-
description: "Also return everything a trigger can name, so a trigger is PICKED rather than invented: `
|
|
47412
|
+
description: "Also return everything a trigger can name, so a trigger is PICKED rather than invented: `events` (every event this company's Forms, pages and systems actually produced in the window, with volume, the Form it belongs to, `origin` \u2014 `site` for a page or Form, `systems` for the client's own server, `both` \u2014 and `countedAs` when the company also counts it as a page conversion, which a trigger does not need), `sources` (the client's own systems that post events to Baker, by the `page:<name>` key each raises, pickable before its first send arrives) and `landings` (its pages, by the id a `landing` trigger takes). Use this before writing any `triggers`: a key typed from memory saves, publishes and counts nobody forever",
|
|
47131
47413
|
required: false
|
|
47132
47414
|
},
|
|
47133
47415
|
days: {
|
|
@@ -47137,7 +47419,7 @@ var args3 = {
|
|
|
47137
47419
|
},
|
|
47138
47420
|
templates: {
|
|
47139
47421
|
type: "boolean",
|
|
47140
|
-
description: "Also return the libraries: the suggested steps with their stages, and the
|
|
47422
|
+
description: "Also return the libraries: the suggested steps with their stages, the presets, and `stages[]` (the six stages with the test to apply). Where a step can be sent is not here \u2014 a send belongs to the system's event, and `baker analytics sends` lists the platforms and the tags that can send",
|
|
47141
47423
|
required: false
|
|
47142
47424
|
}
|
|
47143
47425
|
};
|
|
@@ -47149,7 +47431,7 @@ registerSchema({
|
|
|
47149
47431
|
var funnelCommand2 = defineCommand128({
|
|
47150
47432
|
meta: {
|
|
47151
47433
|
name: "funnel",
|
|
47152
|
-
description: 'Start here: run with no flags to see the funnel this company has, with the last 30 days of PEOPLE counted beside every step \u2014 `people` reached it, `atLeast` got at least that far, `before` got as far as the step before, `share` is the ratio, and `worstStep` names where the most people are lost.
|
|
47434
|
+
description: 'Start here: run with no flags to see the funnel this company has, with the last 30 days of PEOPLE counted beside every step \u2014 `people` reached it, `atLeast` got at least that far, `before` got as far as the step before, `share` is the ratio, and `worstStep` names where the most people are lost. Every company has one from the day it exists: Visited (any landing) \u2192 Became a lead \u2192 Became a customer, the last two with no way in until somebody gives them one. Then `--moments` for everything a trigger can name (every event the company\'s Forms, pages and systems actually produced, its sources, its landings), and `--step \'<json>\'` to add or change ONE step in the client\'s own words \u2014 it replaces the step with that key, so restate its `after` and the triggers it keeps. A step is reached by three kinds of thing and nothing else: an event by its own key (`{"id":"t1","kind":"event","eventKey":"<from moments.events or moments.sources>"}`), one landing being viewed (`{"id":"t2","kind":"landing","landingId":"<from moments.landings>"}`) or any landing (`{"id":"t3","kind":"any_landing"}`). Nothing has to be named first. Pick every trigger from `--moments`, never from memory. A step with no `triggers` advances NOBODY and reports zero forever without warning \u2014 that is what the hints keep pointing at. There is no default door: a Form is only counted where a trigger names it. Keep it true: a chat that adds a Form, a Source or an event that moves people forward places it on its step in the same chat. Each step comes back with `sends[]`, read-only: where the people who reach it are sent and what happened, read off the system events behind its ways in. A send is set on the event with `baker analytics sends`, never on a step, and once published it spends ad budget \u2014 a failed one is named in the hints. Everything here is STAGED: it becomes the company\'s funnel in one piece when the chat is published \u2014 the client sees it on the canvas in this chat and can edit it there too \u2014 and the previous version stays on file so what was already measured under it is not lost.'
|
|
47153
47435
|
},
|
|
47154
47436
|
args: args3,
|
|
47155
47437
|
run: async ({ args: raw }) => {
|