@koda-sl/baker-cli 0.238.0-dev.7fb8e0c69 → 0.239.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
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-CMPAHYLB.js";
61
+ } from "./chunk-EKLAHWSF.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -4461,24 +4461,6 @@ var FLOW_SIDE_EFFECT_CATALOG = [
4461
4461
  gotcha: "`eventTagId` comes from X Events Manager and is passed verbatim. `eventName` is an optional label only.",
4462
4462
  reference: TAGS_REF
4463
4463
  },
4464
- {
4465
- kind: "side-effect",
4466
- type: "microsoftAds",
4467
- summary: "Fires a Microsoft Ads (UET) conversion event.",
4468
- skeleton: {
4469
- ...SIDE_EFFECT_ENVELOPE,
4470
- type: "microsoftAds",
4471
- tagIds: TAG_REF_SLOT,
4472
- eventName: "<UET event action, e.g. lead_submitted>",
4473
- userMatching: matchingSlots("em", "ph"),
4474
- value: null,
4475
- currency: null,
4476
- transactionId: null
4477
- },
4478
- owners: { tagIds: "tags" },
4479
- gotcha: "`eventName` is the UET *event action*, and it only counts as a conversion once someone creates a matching custom-event conversion goal in Microsoft Advertising \u2014 the tag alone records nothing. `page_view` is refused outright (UET reserves it for SPA navigation), so pick any other action. `eventCategory` and `eventLabel` are optional plain strings (not mapped slots) \u2014 add them only when the goal filters on them. `userMatching` needs Enhanced conversions turned on for that goal, and `ph` must carry a country code (`+34\u2026`/`0034\u2026`) \u2014 map it from a phone field with the country picker, not a plain text field, or it is dropped rather than guessed at.",
4480
- reference: TAGS_REF
4481
- },
4482
4464
  eventSideEffect("posthog", "Captures a PostHog event.", { eventParams: [] }),
4483
4465
  {
4484
4466
  kind: "side-effect",
@@ -5920,8 +5902,7 @@ var TAG_TYPES = [
5920
5902
  "posthog",
5921
5903
  "datafast",
5922
5904
  "recaptcha",
5923
- "twitterAds",
5924
- "microsoftAds"
5905
+ "twitterAds"
5925
5906
  ];
5926
5907
  var tagTypeSchema = z19.enum(TAG_TYPES);
5927
5908
  var TAG_IDENTIFYING_FIELD = {
@@ -5942,8 +5923,7 @@ var TAG_IDENTIFYING_FIELD = {
5942
5923
  posthog: "projectToken",
5943
5924
  datafast: "websiteId",
5944
5925
  recaptcha: "siteKey",
5945
- twitterAds: "pixelId",
5946
- microsoftAds: "tagId"
5926
+ twitterAds: "pixelId"
5947
5927
  };
5948
5928
  var tagDraftOpKindSchema = z19.enum(["create", "update", "delete"]);
5949
5929
  var tagDraftOpViewSchema = z19.object({
@@ -11922,19 +11902,22 @@ var analyticsBeaconPayloadSchema = z23.object({
11922
11902
  });
11923
11903
 
11924
11904
  // ../api/src/analytics/trackingTemplate.ts
11925
- var VALUETRACK_ROLES = {
11926
- keyword: "keyword",
11927
- matchtype: "match_type",
11928
- campaignid: "campaign_id",
11929
- adgroupid: "adgroup_id",
11930
- creative: "ad_id",
11931
- placement: "placement",
11932
- network: "network",
11933
- device: "device",
11934
- product_id: "product_id",
11935
- merchant_id: "product_id",
11936
- product_partition_id: "product_id"
11905
+ function placeholderKey(raw) {
11906
+ return raw.replace(/[{}]/g, "").replace(/^_+|_+$/g, "").trim().toLowerCase();
11907
+ }
11908
+ var EXTRA_PLACEHOLDER_ROLES = {
11909
+ google: {
11910
+ merchant_id: "product_id",
11911
+ product_partition_id: "product_id"
11912
+ }
11937
11913
  };
11914
+ function placeholderRoles(platform) {
11915
+ const roles = {};
11916
+ for (const dimension of AD_PLATFORM_DEFINITIONS[platform].dimensions) {
11917
+ roles[placeholderKey(dimension.macro)] = dimension.role;
11918
+ }
11919
+ return { ...roles, ...EXTRA_PLACEHOLDER_ROLES[platform] ?? {} };
11920
+ }
11938
11921
  function paramsFromTemplate(template) {
11939
11922
  const start = template.indexOf("?");
11940
11923
  const query = start === -1 ? template : template.slice(start + 1);
@@ -11950,13 +11933,13 @@ function paramsFromTemplate(template) {
11950
11933
  }
11951
11934
  function proposeMappingFromTemplate(template, platform) {
11952
11935
  const builtIn = builtinAdParamMapping(platform);
11936
+ const vocabulary = placeholderRoles(platform);
11953
11937
  const proposed = {};
11954
11938
  const unexplained = [];
11955
11939
  for (const param of paramsFromTemplate(template)) {
11956
11940
  if (RESERVED_PARAM_NAMES.includes(param.name)) continue;
11957
11941
  if (param.name in builtIn) continue;
11958
- const placeholder = param.value.startsWith("{") && param.value.endsWith("}") ? param.value.slice(1, -1).toLowerCase() : "";
11959
- const role = VALUETRACK_ROLES[placeholder];
11942
+ const role = vocabulary[placeholderKey(param.value)];
11960
11943
  if (role) {
11961
11944
  proposed[param.name] = role;
11962
11945
  continue;
@@ -12579,7 +12562,9 @@ var analyticsDimensionRowSchema = z24.object({
12579
12562
  var analyticsFunnelStepSchema = z24.object({
12580
12563
  stepId: z24.string(),
12581
12564
  stepIndex: z24.number().int().nonnegative(),
12565
+ /** Visits that moved forward into this step. Never events — see `visits`. */
12582
12566
  views: z24.number().int().nonnegative(),
12567
+ /** Visits that finished this step and moved on. */
12583
12568
  completions: z24.number().int().nonnegative(),
12584
12569
  /** Share of viewers who did not complete this step, 0–1. */
12585
12570
  dropRate: z24.number().min(0).max(1).nullable(),
@@ -12591,13 +12576,37 @@ var analyticsFunnelStepSchema = z24.object({
12591
12576
  * conversion, and knowing which step owns it is what stops a drop-off chart
12592
12577
  * treating the booking widget's "profile viewed" as a funnel stage.
12593
12578
  */
12594
- convertedSessions: z24.number().int().nonnegative()
12579
+ convertedSessions: z24.number().int().nonnegative(),
12580
+ /**
12581
+ * This step is where the Form ends — a confirmation screen, not a loss.
12582
+ *
12583
+ * Stated rather than left to be inferred. The dashboard used to read it off
12584
+ * `dropRate === null`, which is also how a step nobody viewed arrives, so a
12585
+ * converting link node in the middle of a Form was labelled its ending.
12586
+ */
12587
+ isEnding: z24.boolean()
12595
12588
  });
12596
12589
  var analyticsFunnelSchema = z24.object({
12597
12590
  flowSlug: z24.string(),
12591
+ /**
12592
+ * Visits that saw the Form at all — the denominator for every share here.
12593
+ *
12594
+ * Read this, not `starts`, whenever the question is "out of how many". A
12595
+ * start is recorded at the visitor's FIRST INTERACTION, so the two differ by
12596
+ * exactly the people who looked at the Form and never touched it — usually
12597
+ * most of them, and always the group a drop-off chart exists to find.
12598
+ *
12599
+ * Every count in this object is distinct visits, including each step's
12600
+ * `views` and `completions`. That is load-bearing rather than incidental:
12601
+ * the screen divides these by each other, and while the steps counted events
12602
+ * a Form four people had opened reported one step reached fifteen times and
12603
+ * fourteen people lost.
12604
+ */
12605
+ visits: z24.number().int().nonnegative(),
12606
+ /** Visits that interacted with the Form at all — typed, picked, or advanced. */
12598
12607
  starts: z24.number().int().nonnegative(),
12599
12608
  submits: z24.number().int().nonnegative(),
12600
- /** Submits per start, 0–1. */
12609
+ /** Conversions per visit that opened the Form, 0–1. */
12601
12610
  completionRate: z24.number().min(0).max(1).nullable(),
12602
12611
  /** The step losing the most people. The single most useful field here. */
12603
12612
  worstStep: analyticsFunnelStepSchema.nullable(),
@@ -13171,10 +13180,74 @@ var analyticsMappingResponseSchema = z24.object({
13171
13180
  * turn configuring `keyword` to mean keyword.
13172
13181
  */
13173
13182
  builtIn: z24.array(z24.string()),
13183
+ /**
13184
+ * What the client's own ad account says about each parameter just staged.
13185
+ *
13186
+ * Present only when a Session staged a `set` on a platform whose tracking
13187
+ * configuration Baker can read. `contradicted` is the entry worth stopping
13188
+ * for: the account fills that name with a placeholder meaning something
13189
+ * else, so the answer just given would produce a report that is wrong and
13190
+ * looks entirely plausible.
13191
+ */
13192
+ evidence: z24.record(
13193
+ z24.string(),
13194
+ z24.object({
13195
+ status: z24.enum(["confirmed", "contradicted", "unseen", "not_read"]),
13196
+ macro: z24.string().optional(),
13197
+ suggestedRole: adParamRoleSchema.optional(),
13198
+ source: z24.string().optional(),
13199
+ reason: z24.string().optional()
13200
+ })
13201
+ ).optional(),
13174
13202
  updatedAt: z24.number().nullable()
13175
13203
  }),
13176
13204
  hints: z24.array(z24.string()).optional()
13177
13205
  });
13206
+ var analyticsTrackingRequestSchema = z24.object({
13207
+ platform: z24.enum(AD_PLATFORMS).optional(),
13208
+ days: z24.number().int().min(1).max(365).optional()
13209
+ });
13210
+ var trackingDimensionSchema = z24.object({
13211
+ role: adParamRoleSchema,
13212
+ /** The platform's own macro for it — what goes in the URL. */
13213
+ macro: z24.string(),
13214
+ /** The account's own spelling where it has one, else the canonical name. */
13215
+ name: z24.string(),
13216
+ /**
13217
+ * What has to happen for this dimension to reach a report.
13218
+ *
13219
+ * Four states rather than a boolean, because two of them need opposite fixes
13220
+ * and one of them is not a finding at all. `unmapped` is one
13221
+ * `baker analytics map` call and repairs the history with it; `missing` is a
13222
+ * change to the ad account and everything before it stays unattributable;
13223
+ * `unknown` means no visit arrived and the account could not be read, which
13224
+ * must never be reported as untagged.
13225
+ */
13226
+ state: z24.enum(["covered", "unmapped", "missing", "unknown"]),
13227
+ evidence: z24.string().optional(),
13228
+ note: z24.string().optional()
13229
+ });
13230
+ var analyticsTrackingPlatformSchema = z24.object({
13231
+ platform: z24.enum(AD_PLATFORMS),
13232
+ label: z24.string(),
13233
+ sessions: z24.number(),
13234
+ /** Whether the ad account itself could be read, and why not when it could not. */
13235
+ account: z24.object({ read: z24.boolean(), templates: z24.number(), reason: z24.string().optional() }),
13236
+ dimensions: z24.array(trackingDimensionSchema),
13237
+ /** Names the URLs already carry that Baker does not read yet. */
13238
+ unmapped: z24.record(z24.string(), adParamRoleSchema),
13239
+ mapCommand: z24.string(),
13240
+ unexplained: z24.array(z24.object({ name: z24.string(), value: z24.string() })),
13241
+ /** The pairs to append to the account's tagging, or `""`. */
13242
+ suffix: z24.string(),
13243
+ whereItGoes: z24.string(),
13244
+ fix: z24.string().optional()
13245
+ });
13246
+ var analyticsTrackingResponseSchema = z24.object({
13247
+ ok: z24.literal(true),
13248
+ data: z24.object({ days: z24.number(), platforms: z24.array(analyticsTrackingPlatformSchema) }),
13249
+ hints: z24.array(z24.string()).optional()
13250
+ });
13178
13251
  var submissionRecordRequestSchema = z24.object({
13179
13252
  /** Injected by the Worker from the host or the site key. Never from the page. */
13180
13253
  companyId: z24.string().min(1),
@@ -23707,7 +23780,7 @@ var funnelCommand = presetCommand({
23707
23780
  var flowCommand = presetCommand({
23708
23781
  name: "flow",
23709
23782
  preset: "flow",
23710
- description: "One Form in depth: how many people it converted, where they went between steps, the per-step table, and every trigger it raised. Read `flowSummary.convertedSessions`, not `submits` \u2014 a Form that books a call on a scheduling node in the MIDDLE of the flow never emits a submit, so it reports zero submits and every one of its real bookings as conversions. `flowPaths` is the flow as edges between steps, bracketed by __flow_start__, __flow_converted__ and __flow_exit__, which is how a branching Form is read at all: a per-step table cannot say that of the people who left step two, forty went to the booking branch and ninety went nowhere.",
23783
+ description: "One Form in depth: how many people it converted, where they went between steps, the per-step table, and every trigger it raised. Every count is DISTINCT VISITS, so quote shares against `funnels[].visits` \u2014 the visits that opened the Form \u2014 and never against `starts`, which is only the visits that touched it. The gap between the two is people who read the Form and left, and on most Forms it is the largest loss there is. Read `flowSummary.convertedSessions`, not `submits` \u2014 a Form that books a call on a scheduling node in the MIDDLE of the flow never emits a submit, so it reports zero submits and every one of its real bookings as conversions. `flowPaths` is the flow as edges between steps, bracketed by __flow_start__, __flow_converted__ and __flow_exit__, which is how a branching Form is read at all: a per-step table cannot say that of the people who left step two, forty went to the booking branch and ninety went nowhere.",
23711
23784
  extraArgs: { flow: { type: "string", description: "Form slug (default: every Form)", required: false } },
23712
23785
  resolve: (args) => ({ preset: "flow", flowSlug: args.flow ? String(args.flow) : void 0 })
23713
23786
  });
@@ -23883,13 +23956,13 @@ var mapCommand = (() => {
23883
23956
  };
23884
23957
  registerSchema({
23885
23958
  command: "analytics.map",
23886
- description: "Set how this company's campaign URLs spell each ad dimension",
23959
+ description: "Stage how this company's campaign URLs spell each ad dimension, applied on publish",
23887
23960
  args
23888
23961
  });
23889
23962
  return defineCommand88({
23890
23963
  meta: {
23891
23964
  name: "map",
23892
- description: "Teach Baker how this company's campaign URLs spell each ad dimension on one platform \u2014 `--platform google --set kw=keyword`. Run it with no flags to see the whole mapping. Mappings are always per platform, because the platforms do not describe the same world: `builtIn` in the response is what that platform is already understood to send, so you do not spend a turn mapping `keyword` to keyword. Takes effect within seconds. Renaming a parameter Baker already captured fixes the history too, because roles are applied when a report is read; naming one it never read only starts collecting it, since no value was stored. You do NOT need to have seen a parameter to map it: a name nobody has sent yet is a valid --set, which is how a tracking template gets configured before the campaign that uses it runs. To drop an answer, --remove takes it back to whatever Baker knows by itself, and --set <name>=ignore says it is not campaign information at all so it stops being listed."
23965
+ description: "Teach Baker how this company's campaign URLs spell each ad dimension on one platform \u2014 `--platform google --set kw=keyword`. Run it with no flags to see the whole mapping, including what this chat has already staged. STAGED, not applied: the answers are reviewed in the chat and take effect when it is published, so nothing you map here changes a report or what the edge stores until then; discarding the chat takes them all back. Mappings are always per platform, because the platforms do not describe the same world: `builtIn` in the response is what that platform is already understood to send, so you do not spend a turn mapping `keyword` to keyword. Each --set is checked against the client's OWN ad account \u2014 `evidence` in the response says whether their tracking template really fills that name with what you claimed. Read it: `contradicted` means the account disagrees, and mapping it your way would produce a report that is wrong and looks plausible. Renaming a parameter Baker already captured fixes the history too, because roles are applied when a report is read; naming one it never read only starts collecting it from the publish onward, since no value was stored. You do NOT need to have seen a parameter to map it: a name nobody has sent yet is a valid --set, which is how a tracking template gets configured before the campaign that uses it runs. To drop an answer, --remove takes it back to whatever Baker knows by itself, and --set <name>=ignore says it is not campaign information at all so it stops being listed."
23893
23966
  },
23894
23967
  args,
23895
23968
  run: async ({ args: raw }) => {
@@ -23907,6 +23980,61 @@ var mapCommand = (() => {
23907
23980
  }
23908
23981
  });
23909
23982
  })();
23983
+ var trackingCommand = (() => {
23984
+ const args = {
23985
+ platform: {
23986
+ type: "string",
23987
+ description: "One platform: google, meta, microsoft, linkedin, tiktok, reddit, pinterest, snapchat. Omit to check the ones worth checking \u2014 every platform sending visits, plus Google and Meta, whose accounts Baker can read directly",
23988
+ required: false
23989
+ },
23990
+ days: { type: "string", description: "Lookback window for the traffic half (default: 30)", required: false }
23991
+ };
23992
+ registerSchema({
23993
+ command: "analytics.tracking",
23994
+ description: "Check whether a platform's ad URLs carry the campaign, ad group and ad a report needs",
23995
+ args
23996
+ });
23997
+ return defineCommand88({
23998
+ meta: {
23999
+ name: "tracking",
24000
+ description: `Check whether this company's ad URLs carry the dimensions any budget decision needs \u2014 the campaign, the ad group, the ad, and the keyword where the platform has one.
24001
+
24002
+ Read this before auditing an ad account and before answering any "which ad performed best" question. Each dimension comes back in one of four states, and they need different things:
24003
+ covered \u2014 arriving and understood, nothing to do
24004
+ unmapped \u2014 the URLs carry it under a name Baker does not read yet. Free to fix, and it repairs the traffic already collected: run the \`mapCommand\` in the response
24005
+ missing \u2014 nothing carries it. Add \`suffix\` to the account's tagging (\`whereItGoes\` says where, \`fix\` says whether Baker can stage it). This one cannot be filled in later \u2014 a parameter's values are only stored once it starts arriving
24006
+ unknown \u2014 no visit arrived AND the account could not be read. Not a finding; do not report it as untagged
24007
+
24008
+ Baker reads the client's own Google tracking templates and Meta URL parameters, so a platform that has not run yet still gets a real answer.
24009
+
24010
+ Examples:
24011
+ baker analytics tracking
24012
+ baker analytics tracking --platform meta
24013
+ baker analytics tracking --platform google --days 90`
24014
+ },
24015
+ args,
24016
+ run: async ({ args: raw }) => {
24017
+ try {
24018
+ const platform = raw.platform === void 0 ? void 0 : String(raw.platform);
24019
+ if (platform !== void 0 && !isAdPlatform(platform)) {
24020
+ writeJsonEnvelope({
24021
+ ok: false,
24022
+ error: { code: "BAD_REQUEST", message: `Unknown platform "${platform}"` }
24023
+ });
24024
+ process.exit(1);
24025
+ }
24026
+ const days = raw.days === void 0 ? void 0 : Number(raw.days);
24027
+ const response = await apiPost("/api/analytics/tracking", {
24028
+ ...platform ? { platform } : {},
24029
+ ...days !== void 0 && Number.isFinite(days) ? { days } : {}
24030
+ });
24031
+ writeJsonEnvelope(response);
24032
+ } catch (err) {
24033
+ handleError(err);
24034
+ }
24035
+ }
24036
+ });
24037
+ })();
23910
24038
  function parsePairs(raw) {
23911
24039
  if (raw === void 0) return void 0;
23912
24040
  const out = {};
@@ -23955,7 +24083,9 @@ Examples:
23955
24083
  baker analytics ads --role keyword \u2014 keywords by conversions AND by how many bounced
23956
24084
  baker analytics ads --platform google --role keyword \u2014 the same, in Google's vocabulary and from Google's traffic only
23957
24085
  baker analytics overview --role keyword --value "solar panels" \u2014 the whole report for those visits only
23958
- baker analytics map --platform google --set kw=keyword \u2014 teach it a parameter name, seen or not yet
24086
+ baker analytics tracking \u2014 do the ad URLs carry the campaign, ad group and ad at all
24087
+ baker analytics tracking --platform meta \u2014 the same for one platform, read from their own ad account
24088
+ baker analytics map --platform google --set kw=keyword \u2014 stage a parameter name, seen or not yet
23959
24089
  baker analytics map --platform google --remove kw \u2014 take that answer back
23960
24090
  baker analytics delivery --page 2 --page-size 50 \u2014 the next page of a long list
23961
24091
  baker analytics overview --compare \u2014 this window against the one before it
@@ -23980,6 +24110,7 @@ Full guide: __tooling__/docs/tools/baker/analytics.md`
23980
24110
  devices: devicesCommand,
23981
24111
  ads: adsCommand3,
23982
24112
  map: mapCommand,
24113
+ tracking: trackingCommand,
23983
24114
  presets: presetsCommand
23984
24115
  }
23985
24116
  });
@@ -33840,10 +33971,6 @@ var MAPPING_SLOTS = {
33840
33971
  twitterAds: [
33841
33972
  { kind: "record", key: "userMatching", allowed: ["em", "ph"] },
33842
33973
  { kind: "scalar", keys: ["value", "currency"] }
33843
- ],
33844
- microsoftAds: [
33845
- { kind: "record", key: "userMatching", allowed: ["em", "ph"] },
33846
- { kind: "scalar", keys: ["value", "currency", "transactionId"] }
33847
33974
  ]
33848
33975
  };
33849
33976
  var UNMAPPABLE = {