@koda-sl/baker-cli 0.307.0-dev.ddf2902ef → 0.307.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,
|
|
@@ -4816,6 +4816,8 @@ var chatChangeTypeSchema = z13.enum([
|
|
|
4816
4816
|
"conversion-sends",
|
|
4817
4817
|
// The company's sources: a HubSpot event made or changed. Staged, applied at Publish.
|
|
4818
4818
|
"event-sources",
|
|
4819
|
+
// What the company counts as a conversion, when an agent changed it. Staged, applied at Publish.
|
|
4820
|
+
"conversions",
|
|
4819
4821
|
// An A/B test between a landing and its alternative. Staged, not immediate.
|
|
4820
4822
|
"experiment",
|
|
4821
4823
|
"briefs"
|
|
@@ -12235,13 +12237,25 @@ registerSchema({
|
|
|
12235
12237
|
}
|
|
12236
12238
|
});
|
|
12237
12239
|
function extractFields(rows) {
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
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";
|
|
12241
12256
|
}
|
|
12242
12257
|
function writeRowsToFile(filePath, rows, fields, append) {
|
|
12243
|
-
|
|
12244
|
-
if (ext === "csv") {
|
|
12258
|
+
if (isCsvPath(filePath)) {
|
|
12245
12259
|
if (!append || !existsSync2(filePath)) {
|
|
12246
12260
|
writeFileSync(filePath, `${toCsvRow(fields)}
|
|
12247
12261
|
`, "utf-8");
|
|
@@ -12249,7 +12263,7 @@ function writeRowsToFile(filePath, rows, fields, append) {
|
|
|
12249
12263
|
const csvRows = rows.map((row) => toCsvRow(fields.map((f) => String(row[f] ?? ""))));
|
|
12250
12264
|
appendFileSync(filePath, `${csvRows.join("\n")}
|
|
12251
12265
|
`, "utf-8");
|
|
12252
|
-
} else if (
|
|
12266
|
+
} else if (filePath.split(".").pop()?.toLowerCase() === "jsonl") {
|
|
12253
12267
|
const lines = rows.map((row) => JSON.stringify(row));
|
|
12254
12268
|
if (append && existsSync2(filePath)) {
|
|
12255
12269
|
appendFileSync(filePath, `${lines.join("\n")}
|
|
@@ -12339,16 +12353,25 @@ var writeJsonlRows = (rows, _fields, notices) => {
|
|
|
12339
12353
|
for (const line of notices) writeLine(line);
|
|
12340
12354
|
for (const row of rows) writeLine(JSON.stringify(row));
|
|
12341
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
|
+
}
|
|
12342
12363
|
var writeMdRows = (rows, fields, notices) => {
|
|
12364
|
+
const booleanColumns = new Set(fields.filter((f) => rows.some((row) => typeof row[f] === "boolean")));
|
|
12343
12365
|
for (const line of notices) writeLine(line);
|
|
12344
12366
|
writeLine(`| ${fields.join(" | ")} |`);
|
|
12345
12367
|
writeLine(`| ${fields.map(() => "---").join(" | ")} |`);
|
|
12346
|
-
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);
|
|
12347
12370
|
};
|
|
12348
12371
|
var ROW_WRITERS = { csv: writeCsvRows, jsonl: writeJsonlRows, md: writeMdRows };
|
|
12349
|
-
function writeRows(format, rows, notices) {
|
|
12372
|
+
function writeRows(format, rows, columns, notices) {
|
|
12350
12373
|
const write2 = ROW_WRITERS[format] ?? writeMdRows;
|
|
12351
|
-
write2(rows,
|
|
12374
|
+
write2(rows, columns, notices);
|
|
12352
12375
|
if (rows.length > REPEAT_NOTICE_ABOVE_ROWS) {
|
|
12353
12376
|
for (const line of notices) writeLine(line);
|
|
12354
12377
|
}
|
|
@@ -12375,7 +12398,7 @@ function outputResults(rows, fieldDescs, warnings, args4, cached, read) {
|
|
|
12375
12398
|
);
|
|
12376
12399
|
return;
|
|
12377
12400
|
}
|
|
12378
|
-
writeRows(format, rows, warningLines(allWarnings, format));
|
|
12401
|
+
writeRows(format, rows, resultColumns(read.gaql, rows), warningLines(allWarnings, format));
|
|
12379
12402
|
}
|
|
12380
12403
|
function resolveGaql(args4, limit) {
|
|
12381
12404
|
if (args4.preset) {
|
|
@@ -12494,15 +12517,27 @@ function missingFieldWarningsFor(gaql, observed, rowCount2) {
|
|
|
12494
12517
|
}
|
|
12495
12518
|
];
|
|
12496
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
|
+
}
|
|
12497
12531
|
function keywordServingOptions(gaql) {
|
|
12498
12532
|
return { statusReasonsSelected: parseSelectedFields(gaql).includes("ad_group_criterion.primary_status_reasons") };
|
|
12499
12533
|
}
|
|
12500
|
-
async function fetchAllPages(body, args4, servingOptions) {
|
|
12534
|
+
async function fetchAllPages(gaql, body, args4, servingOptions) {
|
|
12501
12535
|
let allRows = [];
|
|
12502
12536
|
let pageToken = args4.cursor;
|
|
12503
12537
|
let totalRowsWritten = 0;
|
|
12504
12538
|
let keywordLimits;
|
|
12505
12539
|
const observedFields = /* @__PURE__ */ new Set();
|
|
12540
|
+
let fileColumns;
|
|
12506
12541
|
do {
|
|
12507
12542
|
const requestBody2 = { ...body };
|
|
12508
12543
|
if (pageToken) requestBody2.pageToken = pageToken;
|
|
@@ -12511,7 +12546,8 @@ async function fetchAllPages(body, args4, servingOptions) {
|
|
|
12511
12546
|
pageToken = response.pageToken;
|
|
12512
12547
|
if (args4.out) {
|
|
12513
12548
|
const shouldAppend = args4.append || totalRowsWritten > 0;
|
|
12514
|
-
|
|
12549
|
+
fileColumns ??= resultColumns(gaql, rows);
|
|
12550
|
+
writeRowsToFile(resolve(args4.out), rows, fileColumns, shouldAppend);
|
|
12515
12551
|
totalRowsWritten += rows.length;
|
|
12516
12552
|
keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
|
|
12517
12553
|
foldObservedFields(rows, observedFields);
|
|
@@ -12525,7 +12561,8 @@ async function fetchAllPages(body, args4, servingOptions) {
|
|
|
12525
12561
|
lastPageToken: pageToken,
|
|
12526
12562
|
totalRowsWritten,
|
|
12527
12563
|
keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions),
|
|
12528
|
-
observedFields
|
|
12564
|
+
observedFields,
|
|
12565
|
+
fileColumns: fileColumns ?? []
|
|
12529
12566
|
};
|
|
12530
12567
|
}
|
|
12531
12568
|
async function executeQuery(finalQuery, args4, customerId, limit, warnings, useCache, cacheKey) {
|
|
@@ -12535,7 +12572,8 @@ async function executeQuery(finalQuery, args4, customerId, limit, warnings, useC
|
|
|
12535
12572
|
if (args4.cursor) body.pageToken = args4.cursor;
|
|
12536
12573
|
if (!useCache) body.skipCache = true;
|
|
12537
12574
|
const servingOptions = keywordServingOptions(finalQuery);
|
|
12538
|
-
const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields } = await fetchAllPages(
|
|
12575
|
+
const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields, fileColumns } = await fetchAllPages(
|
|
12576
|
+
finalQuery,
|
|
12539
12577
|
body,
|
|
12540
12578
|
args4,
|
|
12541
12579
|
servingOptions
|
|
@@ -12551,6 +12589,8 @@ async function executeQuery(finalQuery, args4, customerId, limit, warnings, useC
|
|
|
12551
12589
|
...warnings,
|
|
12552
12590
|
...renderKeywordLimits(keywordLimits),
|
|
12553
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) : [],
|
|
12554
12594
|
...conversionMixWarnings(finalQuery),
|
|
12555
12595
|
...truncation
|
|
12556
12596
|
];
|
|
@@ -12582,6 +12622,7 @@ async function executeQuery(finalQuery, args4, customerId, limit, warnings, useC
|
|
|
12582
12622
|
args4,
|
|
12583
12623
|
false,
|
|
12584
12624
|
{
|
|
12625
|
+
gaql: finalQuery,
|
|
12585
12626
|
effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args4.all)),
|
|
12586
12627
|
all: Boolean(args4.all),
|
|
12587
12628
|
nextCursor: lastPageToken
|
|
@@ -12699,7 +12740,11 @@ Examples:
|
|
|
12699
12740
|
],
|
|
12700
12741
|
args4,
|
|
12701
12742
|
true,
|
|
12702
|
-
{
|
|
12743
|
+
{
|
|
12744
|
+
gaql: finalQuery,
|
|
12745
|
+
effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args4.all)),
|
|
12746
|
+
all: Boolean(args4.all)
|
|
12747
|
+
}
|
|
12703
12748
|
);
|
|
12704
12749
|
return;
|
|
12705
12750
|
}
|
|
@@ -13609,7 +13654,7 @@ var SEND_PLATFORM_SPECS = {
|
|
|
13609
13654
|
},
|
|
13610
13655
|
googleAds: {
|
|
13611
13656
|
title: "Google Ads",
|
|
13612
|
-
eventMeans: "The
|
|
13657
|
+
eventMeans: "The id of one of the account's conversion actions \u2014 copy it from `conversionActions`, never type one",
|
|
13613
13658
|
eventKind: "id",
|
|
13614
13659
|
credentialFields: ["oauthProviderId"],
|
|
13615
13660
|
setupHint: "Connect the Google Ads conversion tag to a Google Ads account in Tracking"
|
|
@@ -13806,7 +13851,14 @@ var sendsResponseSchema = z28.object({
|
|
|
13806
13851
|
title: z28.string(),
|
|
13807
13852
|
eventMeans: z28.string(),
|
|
13808
13853
|
setupHint: z28.string(),
|
|
13809
|
-
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()
|
|
13810
13862
|
})
|
|
13811
13863
|
),
|
|
13812
13864
|
/** Every field a send forwards. Nothing to map. */
|
|
@@ -14950,7 +15002,9 @@ var analyticsConversionRowSchema = z30.object({
|
|
|
14950
15002
|
* The doors its events came in by in the window, in
|
|
14951
15003
|
* {@link ANALYTICS_EVENT_DOORS} order. Empty when it has not fired.
|
|
14952
15004
|
*/
|
|
14953
|
-
doors: z30.array(z30.enum(ANALYTICS_EVENT_DOORS)).default([])
|
|
15005
|
+
doors: z30.array(z30.enum(ANALYTICS_EVENT_DOORS)).default([]),
|
|
15006
|
+
/** Proposed in this chat: counted from when it is published, so every report reads it as nothing until then. */
|
|
15007
|
+
staged: z30.boolean().optional()
|
|
14954
15008
|
});
|
|
14955
15009
|
var analyticsEventCatalogRowSchema = z30.object({
|
|
14956
15010
|
/** `form:…` | `submit:…` | `page:…` | `exit:…` — parse with `parseEventKey`. */
|
|
@@ -15558,14 +15612,24 @@ var analyticsConversionsRequestSchema = z30.object({
|
|
|
15558
15612
|
var analyticsConversionsResponseSchema = z30.object({
|
|
15559
15613
|
ok: z30.literal(true),
|
|
15560
15614
|
data: z30.object({
|
|
15615
|
+
/**
|
|
15616
|
+
* What the company counts — inside a chat, with this chat's proposal on
|
|
15617
|
+
* top: `staged` marks a definition that applies when the chat is published.
|
|
15618
|
+
*/
|
|
15561
15619
|
definitions: z30.array(
|
|
15562
15620
|
z30.object({
|
|
15563
15621
|
eventKey: z30.string(),
|
|
15564
15622
|
name: z30.string(),
|
|
15565
15623
|
countMode: z30.string(),
|
|
15566
|
-
updatedAt: z30.number()
|
|
15624
|
+
updatedAt: z30.number(),
|
|
15625
|
+
staged: z30.boolean().optional()
|
|
15567
15626
|
})
|
|
15568
|
-
)
|
|
15627
|
+
),
|
|
15628
|
+
/** This chat's proposal, when it has one: counted from Publish, and gone if the chat is discarded. */
|
|
15629
|
+
staged: z30.object({
|
|
15630
|
+
set: z30.array(z30.object({ eventKey: z30.string(), name: z30.string() })),
|
|
15631
|
+
remove: z30.array(z30.string())
|
|
15632
|
+
}).optional()
|
|
15569
15633
|
}),
|
|
15570
15634
|
hints: z30.array(z30.string()).optional()
|
|
15571
15635
|
});
|
|
@@ -22221,7 +22285,26 @@ var OPTIMIZATION_GOALS = [
|
|
|
22221
22285
|
"THRUPLAY",
|
|
22222
22286
|
"VALUE",
|
|
22223
22287
|
"CONVERSATIONS",
|
|
22224
|
-
"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"
|
|
22225
22308
|
];
|
|
22226
22309
|
var OPTIMIZATION_GOALS_REQUIRING_PAGE = [
|
|
22227
22310
|
"LEAD_GENERATION",
|
|
@@ -22243,7 +22326,23 @@ var DESTINATION_TYPES = [
|
|
|
22243
22326
|
"WHATSAPP",
|
|
22244
22327
|
"PHONE_CALL",
|
|
22245
22328
|
"INSTAGRAM_PROFILE",
|
|
22246
|
-
"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"
|
|
22247
22346
|
];
|
|
22248
22347
|
var CUSTOM_EVENT_TYPES = [
|
|
22249
22348
|
"PURCHASE",
|
|
@@ -22257,7 +22356,28 @@ var CUSTOM_EVENT_TYPES = [
|
|
|
22257
22356
|
"SUBSCRIBE",
|
|
22258
22357
|
"START_TRIAL",
|
|
22259
22358
|
"CONTACT",
|
|
22260
|
-
"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"
|
|
22261
22381
|
];
|
|
22262
22382
|
var ATTRIBUTION_EVENT_TYPES = ["CLICK_THROUGH", "VIEW_THROUGH", "ENGAGED_VIDEO_VIEW"];
|
|
22263
22383
|
var ADVANTAGE_AUDIENCE_OPEN_AGE_MAX = 65;
|
|
@@ -22376,7 +22496,37 @@ var CTA_TYPES2 = [
|
|
|
22376
22496
|
"LINK_CARD",
|
|
22377
22497
|
"DIAL_CODE",
|
|
22378
22498
|
"FIND_YOUR_GROUPS",
|
|
22379
|
-
"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"
|
|
22380
22530
|
];
|
|
22381
22531
|
var ASSET_FEED_CTA_TYPES = [
|
|
22382
22532
|
"OPEN_LINK",
|
|
@@ -22495,6 +22645,11 @@ var ENROLL_STATUSES = ["OPT_IN", "OPT_OUT"];
|
|
|
22495
22645
|
var STAGEABLE_CREATE_STATUSES4 = ["ACTIVE", "PAUSED"];
|
|
22496
22646
|
var UPDATE_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED"];
|
|
22497
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
|
+
];
|
|
22498
22653
|
var MEDIA_KINDS = ["image", "video"];
|
|
22499
22654
|
var CURRENCY_MINIMUMS2 = {
|
|
22500
22655
|
USD: { dailyBudgetMin: 1 },
|
|
@@ -22512,6 +22667,132 @@ import { z as z31 } from "zod";
|
|
|
22512
22667
|
// ../api/src/ads-meta/placements.ts
|
|
22513
22668
|
var FACEBOOK_POSITIONS_REQUIRING_FEED = ["marketplace", "search", "profile_feed", "notification"];
|
|
22514
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
|
+
"story",
|
|
22676
|
+
"search",
|
|
22677
|
+
"instream_video",
|
|
22678
|
+
"facebook_reels",
|
|
22679
|
+
"facebook_reels_overlay",
|
|
22680
|
+
"profile_feed",
|
|
22681
|
+
"notification"
|
|
22682
|
+
];
|
|
22683
|
+
var INSTAGRAM_POSITIONS = [
|
|
22684
|
+
"stream",
|
|
22685
|
+
"story",
|
|
22686
|
+
"explore",
|
|
22687
|
+
"explore_home",
|
|
22688
|
+
"reels",
|
|
22689
|
+
"profile_feed",
|
|
22690
|
+
"ig_search",
|
|
22691
|
+
"profile_reels"
|
|
22692
|
+
];
|
|
22693
|
+
var AUDIENCE_NETWORK_POSITIONS = ["classic", "rewarded_video"];
|
|
22694
|
+
var MESSENGER_POSITIONS = ["sponsored_messages", "story"];
|
|
22695
|
+
var THREADS_POSITIONS = ["threads_stream"];
|
|
22696
|
+
var DEVICE_PLATFORMS = ["mobile", "desktop", "connected_tv"];
|
|
22697
|
+
var PLACEMENT_VOCABULARIES = {
|
|
22698
|
+
publisher_platforms: PUBLISHER_PLATFORMS,
|
|
22699
|
+
facebook_positions: FACEBOOK_POSITIONS,
|
|
22700
|
+
instagram_positions: INSTAGRAM_POSITIONS,
|
|
22701
|
+
audience_network_positions: AUDIENCE_NETWORK_POSITIONS,
|
|
22702
|
+
messenger_positions: MESSENGER_POSITIONS,
|
|
22703
|
+
threads_positions: THREADS_POSITIONS,
|
|
22704
|
+
device_platforms: DEVICE_PLATFORMS
|
|
22705
|
+
};
|
|
22706
|
+
var SAME_SURFACE_ON_THIS_PLATFORM = {
|
|
22707
|
+
facebook_positions: { reels: "facebook_reels", stream: "feed", stories: "story", ig_search: "search" },
|
|
22708
|
+
instagram_positions: {
|
|
22709
|
+
feed: "stream",
|
|
22710
|
+
facebook_reels: "reels",
|
|
22711
|
+
stories: "story",
|
|
22712
|
+
search: "ig_search"
|
|
22713
|
+
}
|
|
22714
|
+
};
|
|
22715
|
+
var RETIRED_PLACEMENTS = {
|
|
22716
|
+
facebook_positions: {
|
|
22717
|
+
video_feeds: 'Meta retired Facebook video feeds \u2014 use "instream_video" for in-stream video, or "facebook_reels", and drop "video_feeds"'
|
|
22718
|
+
}
|
|
22719
|
+
};
|
|
22720
|
+
function placementFieldsAccepting(value, except) {
|
|
22721
|
+
return Object.keys(PLACEMENT_VOCABULARIES).filter(
|
|
22722
|
+
(field) => field !== except && PLACEMENT_VOCABULARIES[field].includes(value)
|
|
22723
|
+
);
|
|
22724
|
+
}
|
|
22725
|
+
function placementValueIssue(field, value) {
|
|
22726
|
+
const retired = RETIRED_PLACEMENTS[field]?.[value];
|
|
22727
|
+
if (retired) {
|
|
22728
|
+
return { field, value, kind: "retired", fix: retired };
|
|
22729
|
+
}
|
|
22730
|
+
const renamed = SAME_SURFACE_ON_THIS_PLATFORM[field]?.[value];
|
|
22731
|
+
if (renamed) {
|
|
22732
|
+
return {
|
|
22733
|
+
field,
|
|
22734
|
+
value,
|
|
22735
|
+
kind: "wrong-field",
|
|
22736
|
+
fix: `Meta calls that surface "${renamed}" in ${field} \u2014 use "${renamed}"`
|
|
22737
|
+
};
|
|
22738
|
+
}
|
|
22739
|
+
const elsewhere = placementFieldsAccepting(value, field);
|
|
22740
|
+
if (elsewhere.length > 0) {
|
|
22741
|
+
return {
|
|
22742
|
+
field,
|
|
22743
|
+
value,
|
|
22744
|
+
kind: "wrong-field",
|
|
22745
|
+
fix: `"${value}" belongs to ${elsewhere.join(" / ")}, not ${field} \u2014 move it there or drop it`
|
|
22746
|
+
};
|
|
22747
|
+
}
|
|
22748
|
+
return {
|
|
22749
|
+
field,
|
|
22750
|
+
value,
|
|
22751
|
+
kind: "unknown",
|
|
22752
|
+
fix: `"${value}" is not a placement Baker knows; ${field} accepts ${PLACEMENT_VOCABULARIES[field].join(", ")}`
|
|
22753
|
+
};
|
|
22754
|
+
}
|
|
22755
|
+
function invalidPlacementValues(targeting) {
|
|
22756
|
+
const issues = [];
|
|
22757
|
+
for (const field of Object.keys(PLACEMENT_VOCABULARIES)) {
|
|
22758
|
+
const allowed = PLACEMENT_VOCABULARIES[field];
|
|
22759
|
+
const selected = targeting?.[field] ?? [];
|
|
22760
|
+
for (const value of selected) {
|
|
22761
|
+
if (!allowed.includes(value)) {
|
|
22762
|
+
issues.push(placementValueIssue(field, value));
|
|
22763
|
+
}
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22766
|
+
return issues;
|
|
22767
|
+
}
|
|
22768
|
+
function placementCoRequirementIssues(targeting) {
|
|
22769
|
+
const facebook = targeting?.facebook_positions ?? [];
|
|
22770
|
+
const instagram = targeting?.instagram_positions ?? [];
|
|
22771
|
+
const threads = targeting?.threads_positions ?? [];
|
|
22772
|
+
const devices = targeting?.device_platforms;
|
|
22773
|
+
const issues = [];
|
|
22774
|
+
if (facebook.includes("story")) {
|
|
22775
|
+
if (!facebook.includes("feed") && !instagram.includes("story")) {
|
|
22776
|
+
issues.push(
|
|
22777
|
+
'Meta refuses Facebook "story" on its own \u2014 select "feed" in facebook_positions or "story" in instagram_positions alongside it'
|
|
22778
|
+
);
|
|
22779
|
+
}
|
|
22780
|
+
if (devices && !devices.includes("mobile")) {
|
|
22781
|
+
issues.push(
|
|
22782
|
+
'Facebook "story" only runs on mobile \u2014 add "mobile" to device_platforms or drop the story placement'
|
|
22783
|
+
);
|
|
22784
|
+
}
|
|
22785
|
+
}
|
|
22786
|
+
if (instagram.includes("explore_home") && !instagram.includes("explore")) {
|
|
22787
|
+
issues.push(
|
|
22788
|
+
'Meta refuses Instagram "explore_home" without "explore" \u2014 add "explore" to instagram_positions or drop explore_home'
|
|
22789
|
+
);
|
|
22790
|
+
}
|
|
22791
|
+
if (threads.includes("threads_stream") && !instagram.includes("stream")) {
|
|
22792
|
+
issues.push('Threads "threads_stream" cannot be selected alone \u2014 add "stream" to instagram_positions');
|
|
22793
|
+
}
|
|
22794
|
+
return issues;
|
|
22795
|
+
}
|
|
22515
22796
|
var CONSTRAINED_PLACEMENTS = [
|
|
22516
22797
|
{
|
|
22517
22798
|
position: "facebook_reels_overlay",
|
|
@@ -22549,6 +22830,10 @@ var FACEBOOK_PREVIEW_FORMATS = {
|
|
|
22549
22830
|
facebook_reels: "FACEBOOK_REELS_MOBILE",
|
|
22550
22831
|
facebook_reels_overlay: "FACEBOOK_REELS_BANNER",
|
|
22551
22832
|
instream_video: "INSTREAM_VIDEO_MOBILE",
|
|
22833
|
+
// Retired for targeting (RETIRED_PLACEMENTS) and still reported by a breakdown
|
|
22834
|
+
// over any window that predates the retirement — an ad set created before it
|
|
22835
|
+
// keeps delivering there. Previewing what a historic row measured has to keep
|
|
22836
|
+
// working, so this stays.
|
|
22552
22837
|
video_feeds: "INSTREAM_VIDEO_MOBILE"
|
|
22553
22838
|
};
|
|
22554
22839
|
var INSTAGRAM_PREVIEW_FORMATS = {
|
|
@@ -22626,6 +22911,7 @@ var metaTargetingSchema = z31.object({
|
|
|
22626
22911
|
instagram_positions: z31.array(z31.string()).optional(),
|
|
22627
22912
|
audience_network_positions: z31.array(z31.string()).optional(),
|
|
22628
22913
|
messenger_positions: z31.array(z31.string()).optional(),
|
|
22914
|
+
threads_positions: z31.array(z31.string()).optional(),
|
|
22629
22915
|
device_platforms: z31.array(z31.string()).optional(),
|
|
22630
22916
|
targeting_automation: z31.object({ advantage_audience: z31.union([z31.literal(0), z31.literal(1)]) }).partial().optional()
|
|
22631
22917
|
}).catchall(z31.unknown());
|
|
@@ -22757,6 +23043,13 @@ var adSetFields = {
|
|
|
22757
23043
|
start_time: z31.number().int().positive().optional(),
|
|
22758
23044
|
end_time: z31.number().int().positive().optional(),
|
|
22759
23045
|
targeting: metaTargetingSchema,
|
|
23046
|
+
/**
|
|
23047
|
+
* The live ad set this one was copied from, verbatim, by `duplicate`. Baker
|
|
23048
|
+
* only — `adSetCommonBody` never sends it to Meta. Set exclusively when the
|
|
23049
|
+
* targeting IS the source's targeting, because that is what it licenses:
|
|
23050
|
+
* placement validation is skipped for it (see `validateAdSetPlacements`).
|
|
23051
|
+
*/
|
|
23052
|
+
clonedFrom: z31.string().regex(NUMERIC_ID_REGEX3).optional(),
|
|
22760
23053
|
/** EU Digital Services Act: who benefits from / pays for the ad. Auto-filled from the account for EU geo when omitted. */
|
|
22761
23054
|
dsa_beneficiary: z31.string().min(1).max(100).optional(),
|
|
22762
23055
|
dsa_payor: z31.string().min(1).max(100).optional()
|
|
@@ -22857,6 +23150,18 @@ function validateAdvantageAudience(p, ctx) {
|
|
|
22857
23150
|
}
|
|
22858
23151
|
}
|
|
22859
23152
|
function validateAdSetPlacements(p, ctx) {
|
|
23153
|
+
if (p.clonedFrom) {
|
|
23154
|
+
for (const issue of invalidPlacementValues(p.targeting)) {
|
|
23155
|
+
if (issue.kind === "retired") {
|
|
23156
|
+
ctx.addIssue({
|
|
23157
|
+
code: "custom",
|
|
23158
|
+
path: ["targeting", issue.field],
|
|
23159
|
+
message: `${issue.field} no longer accepts "${issue.value}". ${issue.fix} (${PLACEMENT_TARGETING_DOC})`
|
|
23160
|
+
});
|
|
23161
|
+
}
|
|
23162
|
+
}
|
|
23163
|
+
return;
|
|
23164
|
+
}
|
|
22860
23165
|
const missingFeed = positionsMissingRequiredFeed(p.targeting);
|
|
22861
23166
|
if (missingFeed.length > 0) {
|
|
22862
23167
|
ctx.addIssue({
|
|
@@ -22865,6 +23170,19 @@ function validateAdSetPlacements(p, ctx) {
|
|
|
22865
23170
|
message: `Meta requires "feed" alongside ${missingFeed.join(", ")} \u2014 add "feed" to facebook_positions or drop those positions (${PLACEMENT_TARGETING_DOC})`
|
|
22866
23171
|
});
|
|
22867
23172
|
}
|
|
23173
|
+
for (const issue of invalidPlacementValues(p.targeting)) {
|
|
23174
|
+
if (issue.kind === "unknown") {
|
|
23175
|
+
continue;
|
|
23176
|
+
}
|
|
23177
|
+
ctx.addIssue({
|
|
23178
|
+
code: "custom",
|
|
23179
|
+
path: ["targeting", issue.field],
|
|
23180
|
+
message: issue.kind === "retired" ? `${issue.field} no longer accepts "${issue.value}". ${issue.fix} (${PLACEMENT_TARGETING_DOC})` : `${issue.field} does not accept "${issue.value}". ${issue.fix} (${PLACEMENT_TARGETING_DOC})`
|
|
23181
|
+
});
|
|
23182
|
+
}
|
|
23183
|
+
for (const message of placementCoRequirementIssues(p.targeting)) {
|
|
23184
|
+
ctx.addIssue({ code: "custom", path: ["targeting"], message: `${message} (${PLACEMENT_TARGETING_DOC})` });
|
|
23185
|
+
}
|
|
22868
23186
|
}
|
|
22869
23187
|
var adSetCreateSchema = z31.object(adSetFields).superRefine((p, ctx) => {
|
|
22870
23188
|
validateAdSetBudgetAndBid(p, ctx);
|
|
@@ -23163,11 +23481,25 @@ var lookalikeSpecSchema = z31.object({
|
|
|
23163
23481
|
ratio: z31.number().min(0.01).max(0.2).optional(),
|
|
23164
23482
|
country: z31.string().length(2).optional()
|
|
23165
23483
|
});
|
|
23484
|
+
function valuesOfKey(node, key) {
|
|
23485
|
+
if (Array.isArray(node)) return node.flatMap((entry) => valuesOfKey(entry, key));
|
|
23486
|
+
if (!node || typeof node !== "object") return [];
|
|
23487
|
+
const record = node;
|
|
23488
|
+
return [
|
|
23489
|
+
...key in record ? [record[key]] : [],
|
|
23490
|
+
...Object.values(record).flatMap((value) => valuesOfKey(value, key))
|
|
23491
|
+
];
|
|
23492
|
+
}
|
|
23493
|
+
function mixedEventSourceTypes(rule) {
|
|
23494
|
+
const types = valuesOfKey(rule, "event_sources").flatMap((sources) => Array.isArray(sources) ? sources : []).map((source) => source?.type).filter((type) => typeof type === "string");
|
|
23495
|
+
const distinct = [...new Set(types)].sort();
|
|
23496
|
+
return distinct.length > 1 ? distinct : [];
|
|
23497
|
+
}
|
|
23166
23498
|
var customAudienceCreateSchema = z31.object({
|
|
23167
23499
|
name: z31.string().min(1).max(META_LIMITS.audience.nameMax),
|
|
23168
23500
|
subtype: z31.enum(CUSTOM_AUDIENCE_SUBTYPES),
|
|
23169
23501
|
description: z31.string().max(500).optional(),
|
|
23170
|
-
customer_file_source: z31.
|
|
23502
|
+
customer_file_source: z31.enum(CUSTOMER_FILE_SOURCES).optional(),
|
|
23171
23503
|
retention_days: z31.number().int().min(1).max(META_LIMITS.audience.retentionDaysMax).optional(),
|
|
23172
23504
|
lookalike_spec: lookalikeSpecSchema.optional(),
|
|
23173
23505
|
/** Website/engagement rule — structural passthrough validated by Meta. */
|
|
@@ -23179,6 +23511,21 @@ var customAudienceCreateSchema = z31.object({
|
|
|
23179
23511
|
if ((p.subtype === "WEBSITE" || p.subtype === "ENGAGEMENT") && !p.rule) {
|
|
23180
23512
|
ctx.addIssue({ code: "custom", path: ["rule"], message: `${p.subtype} audiences need a rule (use --file)` });
|
|
23181
23513
|
}
|
|
23514
|
+
if (p.rule && p.retention_days !== void 0) {
|
|
23515
|
+
ctx.addIssue({
|
|
23516
|
+
code: "custom",
|
|
23517
|
+
path: ["retention_days"],
|
|
23518
|
+
message: "a rule carries its own window in retention_seconds, so drop retention_days \u2014 Meta takes it only on a customer-file audience, and sending both leaves the review showing a window the audience will not have"
|
|
23519
|
+
});
|
|
23520
|
+
}
|
|
23521
|
+
const mixed = mixedEventSourceTypes(p.rule);
|
|
23522
|
+
if (mixed.length > 0) {
|
|
23523
|
+
ctx.addIssue({
|
|
23524
|
+
code: "custom",
|
|
23525
|
+
path: ["rule"],
|
|
23526
|
+
message: `one audience covers one kind of source, but this rule mixes ${mixed.join(" and ")} \u2014 split it into one audience per source`
|
|
23527
|
+
});
|
|
23528
|
+
}
|
|
23182
23529
|
});
|
|
23183
23530
|
var customAudienceUpdateSchema = z31.object({
|
|
23184
23531
|
name: z31.string().min(1).max(META_LIMITS.audience.nameMax).optional(),
|
|
@@ -23919,6 +24266,7 @@ var audiencesCreateCommand2 = defineCommand57({
|
|
|
23919
24266
|
name: "create",
|
|
23920
24267
|
description: `Stage a new custom audience shell. ${STAGED_NOTE2}
|
|
23921
24268
|
LOOKALIKE/WEBSITE/ENGAGEMENT specifics go via --file (lookalike_spec / rule). Member-list upload is not staged in v1.
|
|
24269
|
+
A WEBSITE/ENGAGEMENT rule carries its own window in retention_seconds, so --retention-days is refused next to one, and one audience covers one kind of event source (a Page and an Instagram profile are two audiences).
|
|
23922
24270
|
Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WEBSITE --file rule.json`
|
|
23923
24271
|
},
|
|
23924
24272
|
args: {
|
|
@@ -23926,7 +24274,10 @@ Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WE
|
|
|
23926
24274
|
name: { type: "string", description: "Audience name" },
|
|
23927
24275
|
subtype: { type: "string", description: "CUSTOM|WEBSITE|ENGAGEMENT|LOOKALIKE" },
|
|
23928
24276
|
description: { type: "string", description: "Description" },
|
|
23929
|
-
"retention-days": {
|
|
24277
|
+
"retention-days": {
|
|
24278
|
+
type: "string",
|
|
24279
|
+
description: "Retention window in days \u2014 customer-file audiences only; a rule carries its own retention_seconds"
|
|
24280
|
+
},
|
|
23930
24281
|
file: { type: "string", description: "JSON file with lookalike_spec / rule / full payload" }
|
|
23931
24282
|
},
|
|
23932
24283
|
run: async ({ args: args4 }) => {
|
|
@@ -29245,6 +29596,14 @@ function namingHints(definitions) {
|
|
|
29245
29596
|
`Names in use here: ${names.join(", ")}. Reuse one EXACTLY when another Form produces the same outcome \u2014 that is what makes them one number \u2014 and use \`--rename "<old>" --to "<existing>"\` to merge two that should always have been one.`
|
|
29246
29597
|
];
|
|
29247
29598
|
}
|
|
29599
|
+
function stagedHints(definitions) {
|
|
29600
|
+
const staged = definitions.filter((row) => row.staged === true);
|
|
29601
|
+
if (staged.length === 0) return [];
|
|
29602
|
+
return [
|
|
29603
|
+
`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.`,
|
|
29604
|
+
'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.'
|
|
29605
|
+
];
|
|
29606
|
+
}
|
|
29248
29607
|
|
|
29249
29608
|
// src/commands/analytics/conversionList.ts
|
|
29250
29609
|
function mergeStoredWithCounts(stored, reported) {
|
|
@@ -29270,7 +29629,8 @@ function mergeStoredWithCounts(stored, reported) {
|
|
|
29270
29629
|
conversions: counted?.conversions ?? 0,
|
|
29271
29630
|
convertingVisits: counted?.convertingVisits ?? 0,
|
|
29272
29631
|
lastSeen: counted?.lastSeen ?? null,
|
|
29273
|
-
doors: counted?.doors ?? []
|
|
29632
|
+
doors: counted?.doors ?? [],
|
|
29633
|
+
...rows.some((row) => row.staged === true) ? { staged: true } : {}
|
|
29274
29634
|
};
|
|
29275
29635
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
29276
29636
|
}
|
|
@@ -29988,7 +30348,7 @@ var args = {
|
|
|
29988
30348
|
},
|
|
29989
30349
|
event: {
|
|
29990
30350
|
type: "string",
|
|
29991
|
-
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',
|
|
30351
|
+
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',
|
|
29992
30352
|
required: false
|
|
29993
30353
|
},
|
|
29994
30354
|
account: {
|
|
@@ -31397,6 +31757,7 @@ function conversionsEnvelope(args4, definitions, candidates) {
|
|
|
31397
31757
|
const hints2 = [
|
|
31398
31758
|
...flow ? flowConversionHints(flow.slug, flow.endings, flow.noConversionReason) : [],
|
|
31399
31759
|
...flowCoverageHints(coverage),
|
|
31760
|
+
...stagedHints(definitions),
|
|
31400
31761
|
...page.hints,
|
|
31401
31762
|
...namingHints(definitions)
|
|
31402
31763
|
];
|
|
@@ -31415,7 +31776,7 @@ var conversionsCommand3 = (() => {
|
|
|
31415
31776
|
const args4 = {
|
|
31416
31777
|
event: {
|
|
31417
31778
|
type: "string",
|
|
31418
|
-
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.
|
|
31779
|
+
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`)",
|
|
31419
31780
|
required: false
|
|
31420
31781
|
},
|
|
31421
31782
|
name: {
|
|
@@ -31472,7 +31833,7 @@ var conversionsCommand3 = (() => {
|
|
|
31472
31833
|
return defineCommand97({
|
|
31473
31834
|
meta: {
|
|
31474
31835
|
name: "conversions",
|
|
31475
|
-
description: "What this company counts as a conversion
|
|
31836
|
+
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."
|
|
31476
31837
|
},
|
|
31477
31838
|
args: args4,
|
|
31478
31839
|
run: async ({ args: raw }) => {
|
|
@@ -31503,12 +31864,8 @@ var conversionsCommand3 = (() => {
|
|
|
31503
31864
|
}
|
|
31504
31865
|
});
|
|
31505
31866
|
})();
|
|
31506
|
-
var SYSTEM_EVENTS_HINT = "Events your systems send are not listed: a system's event is a funnel step (`baker funnel`) and is sent to an ad platform with `baker analytics sends`, never counted as a page conversion.";
|
|
31507
31867
|
function pageCandidates(definitions, catalogue, listing) {
|
|
31508
|
-
|
|
31509
|
-
const hints2 = conversionHints(definitions, candidates, listing);
|
|
31510
|
-
if (listing && candidates.length < catalogue.length) hints2.push(SYSTEM_EVENTS_HINT);
|
|
31511
|
-
return { candidates, hints: hints2 };
|
|
31868
|
+
return { candidates: [...catalogue], hints: conversionHints(definitions, catalogue, listing) };
|
|
31512
31869
|
}
|
|
31513
31870
|
function parsePairs(raw) {
|
|
31514
31871
|
if (raw === void 0) return void 0;
|
|
@@ -45255,12 +45612,37 @@ function splitParts(raw) {
|
|
|
45255
45612
|
}
|
|
45256
45613
|
return parts.map((part) => part.trim()).filter((part) => part.length > 0);
|
|
45257
45614
|
}
|
|
45615
|
+
var TRANSFORM_REFUSAL = "A value cannot run code. Map the field as it is (firstname=full_name), or split it in the step with a codeExecution that exposes first_name and last_name and map code:first_name.";
|
|
45616
|
+
var METHOD_CALL = /\.\s*[A-Za-z_$][\w$]*\s*\(/;
|
|
45617
|
+
var TRAILING_INDEX = /\[[^\]]*\]$/;
|
|
45618
|
+
function mentionPart(inner) {
|
|
45619
|
+
if (inner.startsWith("tracking.")) return { kind: "tracking", name: inner.slice("tracking.".length) };
|
|
45620
|
+
if (inner.startsWith("global.")) return { kind: "global", name: inner.slice("global.".length) };
|
|
45621
|
+
return referencePart(inner);
|
|
45622
|
+
}
|
|
45623
|
+
function referencePart(ref) {
|
|
45624
|
+
return /\.codeExecution\.[^.]+$/.test(ref) ? { kind: "code", ref } : { kind: "field", ref };
|
|
45625
|
+
}
|
|
45258
45626
|
function parsePart(part) {
|
|
45259
45627
|
if (part.startsWith('"') && part.endsWith('"') && part.length >= 2) {
|
|
45260
45628
|
return { kind: "text", text: part.slice(1, -1) };
|
|
45261
45629
|
}
|
|
45630
|
+
if (METHOD_CALL.test(part) || TRAILING_INDEX.test(part)) {
|
|
45631
|
+
throw new MappingError(TRANSFORM_REFUSAL);
|
|
45632
|
+
}
|
|
45633
|
+
if (part.startsWith("{{")) {
|
|
45634
|
+
const close = part.indexOf("}}");
|
|
45635
|
+
if (close < 0) {
|
|
45636
|
+
throw new MappingError(
|
|
45637
|
+
`Unclosed \`{{\` in value \`${part}\`.`,
|
|
45638
|
+
"A mention is written {{<stepId>.customForm.<field>}}."
|
|
45639
|
+
);
|
|
45640
|
+
}
|
|
45641
|
+
if (part.slice(close + 2).trim()) throw new MappingError(TRANSFORM_REFUSAL);
|
|
45642
|
+
return mentionPart(part.slice(2, close).trim());
|
|
45643
|
+
}
|
|
45262
45644
|
const colon = part.indexOf(":");
|
|
45263
|
-
if (colon <= 0) return
|
|
45645
|
+
if (colon <= 0) return referencePart(part);
|
|
45264
45646
|
const prefix = part.slice(0, colon);
|
|
45265
45647
|
const rest = part.slice(colon + 1).trim();
|
|
45266
45648
|
if (!PREFIXES.includes(prefix)) {
|
|
@@ -45318,7 +45700,12 @@ function fieldOwners(tree) {
|
|
|
45318
45700
|
];
|
|
45319
45701
|
});
|
|
45320
45702
|
}
|
|
45321
|
-
function splitRef(ref) {
|
|
45703
|
+
function splitRef(ref, segment) {
|
|
45704
|
+
const marker = `.${segment}.`;
|
|
45705
|
+
const at = ref.lastIndexOf(marker);
|
|
45706
|
+
if (at > 0 && !ref.slice(at + marker.length).includes(".")) {
|
|
45707
|
+
return { step: ref.slice(0, at), name: ref.slice(at + marker.length) };
|
|
45708
|
+
}
|
|
45322
45709
|
const dot = ref.lastIndexOf(".");
|
|
45323
45710
|
if (dot <= 0) return { name: ref };
|
|
45324
45711
|
return { step: ref.slice(0, dot), name: ref.slice(dot + 1) };
|
|
@@ -45330,7 +45717,7 @@ function titleize(value) {
|
|
|
45330
45717
|
return value.split(/[_-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
45331
45718
|
}
|
|
45332
45719
|
function resolveField(tree, ref) {
|
|
45333
|
-
const { step, name } = splitRef(ref);
|
|
45720
|
+
const { step, name } = splitRef(ref, "customForm");
|
|
45334
45721
|
const owners = fieldOwners(tree);
|
|
45335
45722
|
const inventory = owners.map((owner2) => ` ${owner2.name} (${owner2.id}): ${owner2.fields.join(", ") || "(no fields)"}`).join("\n");
|
|
45336
45723
|
const candidates = owners.filter(
|
|
@@ -45373,7 +45760,7 @@ function resolveGlobal(tree, name) {
|
|
|
45373
45760
|
return mention(id, id);
|
|
45374
45761
|
}
|
|
45375
45762
|
function resolveCode(tree, ref) {
|
|
45376
|
-
const { step, name } = splitRef(ref);
|
|
45763
|
+
const { step, name } = splitRef(ref, "codeExecution");
|
|
45377
45764
|
const owners = walkNodes(tree).flatMap((node) => {
|
|
45378
45765
|
const exposed = externalOf(node).codeExecution?.exposedValues ?? [];
|
|
45379
45766
|
const names = exposed.map((value) => value?.name).filter((value) => typeof value === "string");
|
|
@@ -45917,7 +46304,7 @@ function describeDestination(chosen) {
|
|
|
45917
46304
|
// two different questions with two different answers, and the type is the
|
|
45918
46305
|
// one an agent does not know it is being asked until a mapping is refused.
|
|
45919
46306
|
...slots.flatMap((slot) => slot.fieldTypes ? [`${slot.slot} field types \u2014 ${slot.fieldTypes}`] : []),
|
|
45920
|
-
'Values (right of the `=`) are the same for every slot: a form field, tracking:<var>, global:<name>, code:<name>, a "literal", or those joined with +.'
|
|
46307
|
+
'Values (right of the `=`) are the same for every slot: a form field, tracking:<var>, global:<name>, code:<name>, a "literal", or those joined with +. A field may also be written as the file stores it, {{<stepId>.customForm.<field>}}. A value cannot run code (no .split(), .slice(), [0]).'
|
|
45921
46308
|
]
|
|
45922
46309
|
});
|
|
45923
46310
|
}
|
|
@@ -46357,6 +46744,9 @@ function inertSideEffectHints(tree) {
|
|
|
46357
46744
|
"Fix each by setting the id its own step raises: `baker flows schema node <nodeType>` lists them."
|
|
46358
46745
|
];
|
|
46359
46746
|
}
|
|
46747
|
+
function funnelPlacementHint(slug) {
|
|
46748
|
+
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.`;
|
|
46749
|
+
}
|
|
46360
46750
|
function displayName(slug) {
|
|
46361
46751
|
const tree = readFlowTree(slug);
|
|
46362
46752
|
const name = tree.displayName;
|
|
@@ -46420,7 +46810,8 @@ var showCommand4 = defineCommand127({
|
|
|
46420
46810
|
// an answer that is not in this file at all. What a Form counts is a
|
|
46421
46811
|
// named event in Baker, and a Form can be perfectly configured, fire
|
|
46422
46812
|
// every pixel it has, and still report converting nobody.
|
|
46423
|
-
`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}
|
|
46813
|
+
`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}\`.`,
|
|
46814
|
+
funnelPlacementHint(slug)
|
|
46424
46815
|
]
|
|
46425
46816
|
});
|
|
46426
46817
|
}
|
|
@@ -47024,7 +47415,7 @@ function parsePreset(raw) {
|
|
|
47024
47415
|
var args3 = {
|
|
47025
47416
|
steps: {
|
|
47026
47417
|
type: "string",
|
|
47027
|
-
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
|
|
47418
|
+
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[]`',
|
|
47028
47419
|
required: false
|
|
47029
47420
|
},
|
|
47030
47421
|
step: {
|
|
@@ -47039,12 +47430,12 @@ var args3 = {
|
|
|
47039
47430
|
},
|
|
47040
47431
|
preset: {
|
|
47041
47432
|
type: "string",
|
|
47042
|
-
description: "Draw one of the ready-made graphs instead of stating steps: b2b_sales, b2b_inbound, saas_self_serve, saas_sales_assisted, ecommerce_purchase.
|
|
47433
|
+
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",
|
|
47043
47434
|
required: false
|
|
47044
47435
|
},
|
|
47045
47436
|
moments: {
|
|
47046
47437
|
type: "boolean",
|
|
47047
|
-
description: "Also return everything a trigger can name, so a trigger is PICKED rather than invented: `
|
|
47438
|
+
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",
|
|
47048
47439
|
required: false
|
|
47049
47440
|
},
|
|
47050
47441
|
days: {
|
|
@@ -47054,7 +47445,7 @@ var args3 = {
|
|
|
47054
47445
|
},
|
|
47055
47446
|
templates: {
|
|
47056
47447
|
type: "boolean",
|
|
47057
|
-
description: "Also return the libraries: the suggested steps with their stages, and the
|
|
47448
|
+
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",
|
|
47058
47449
|
required: false
|
|
47059
47450
|
}
|
|
47060
47451
|
};
|
|
@@ -47066,7 +47457,7 @@ registerSchema({
|
|
|
47066
47457
|
var funnelCommand2 = defineCommand128({
|
|
47067
47458
|
meta: {
|
|
47068
47459
|
name: "funnel",
|
|
47069
|
-
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.
|
|
47460
|
+
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.'
|
|
47070
47461
|
},
|
|
47071
47462
|
args: args3,
|
|
47072
47463
|
run: async ({ args: raw }) => {
|
|
@@ -59508,7 +59899,7 @@ registerSchema({
|
|
|
59508
59899
|
},
|
|
59509
59900
|
sort: {
|
|
59510
59901
|
type: "string",
|
|
59511
|
-
description: "Sort:
|
|
59902
|
+
description: "Sort: search_volume, competition, cpc (default: search_volume)",
|
|
59512
59903
|
required: false
|
|
59513
59904
|
},
|
|
59514
59905
|
type: { type: "string", description: "Filter: paid, organic, all (default: all)", required: false },
|
|
@@ -59537,7 +59928,7 @@ Examples:
|
|
|
59537
59928
|
target: { type: "positional", description: "Domain or URL to analyze", required: true },
|
|
59538
59929
|
location: { type: "string", description: "Country code or location code (default: us)", required: false },
|
|
59539
59930
|
language: { type: "string", description: "Language code or name (default: en)", required: false },
|
|
59540
|
-
sort: { type: "string", description: "Sort:
|
|
59931
|
+
sort: { type: "string", description: "Sort: search_volume, competition, cpc", required: false },
|
|
59541
59932
|
type: { type: "string", description: "Filter: paid, organic, all (default: all)", required: false },
|
|
59542
59933
|
limit: { type: "string", description: "Max results (default: 50, max: 1000)", required: false },
|
|
59543
59934
|
output: { type: "string", description: "Format: json|csv|md|jsonl", required: false, default: "json" },
|