@koda-sl/baker-cli 0.238.0-dev.7fb8e0c69 → 0.238.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;
@@ -13171,10 +13154,74 @@ var analyticsMappingResponseSchema = z24.object({
13171
13154
  * turn configuring `keyword` to mean keyword.
13172
13155
  */
13173
13156
  builtIn: z24.array(z24.string()),
13157
+ /**
13158
+ * What the client's own ad account says about each parameter just staged.
13159
+ *
13160
+ * Present only when a Session staged a `set` on a platform whose tracking
13161
+ * configuration Baker can read. `contradicted` is the entry worth stopping
13162
+ * for: the account fills that name with a placeholder meaning something
13163
+ * else, so the answer just given would produce a report that is wrong and
13164
+ * looks entirely plausible.
13165
+ */
13166
+ evidence: z24.record(
13167
+ z24.string(),
13168
+ z24.object({
13169
+ status: z24.enum(["confirmed", "contradicted", "unseen", "not_read"]),
13170
+ macro: z24.string().optional(),
13171
+ suggestedRole: adParamRoleSchema.optional(),
13172
+ source: z24.string().optional(),
13173
+ reason: z24.string().optional()
13174
+ })
13175
+ ).optional(),
13174
13176
  updatedAt: z24.number().nullable()
13175
13177
  }),
13176
13178
  hints: z24.array(z24.string()).optional()
13177
13179
  });
13180
+ var analyticsTrackingRequestSchema = z24.object({
13181
+ platform: z24.enum(AD_PLATFORMS).optional(),
13182
+ days: z24.number().int().min(1).max(365).optional()
13183
+ });
13184
+ var trackingDimensionSchema = z24.object({
13185
+ role: adParamRoleSchema,
13186
+ /** The platform's own macro for it — what goes in the URL. */
13187
+ macro: z24.string(),
13188
+ /** The account's own spelling where it has one, else the canonical name. */
13189
+ name: z24.string(),
13190
+ /**
13191
+ * What has to happen for this dimension to reach a report.
13192
+ *
13193
+ * Four states rather than a boolean, because two of them need opposite fixes
13194
+ * and one of them is not a finding at all. `unmapped` is one
13195
+ * `baker analytics map` call and repairs the history with it; `missing` is a
13196
+ * change to the ad account and everything before it stays unattributable;
13197
+ * `unknown` means no visit arrived and the account could not be read, which
13198
+ * must never be reported as untagged.
13199
+ */
13200
+ state: z24.enum(["covered", "unmapped", "missing", "unknown"]),
13201
+ evidence: z24.string().optional(),
13202
+ note: z24.string().optional()
13203
+ });
13204
+ var analyticsTrackingPlatformSchema = z24.object({
13205
+ platform: z24.enum(AD_PLATFORMS),
13206
+ label: z24.string(),
13207
+ sessions: z24.number(),
13208
+ /** Whether the ad account itself could be read, and why not when it could not. */
13209
+ account: z24.object({ read: z24.boolean(), templates: z24.number(), reason: z24.string().optional() }),
13210
+ dimensions: z24.array(trackingDimensionSchema),
13211
+ /** Names the URLs already carry that Baker does not read yet. */
13212
+ unmapped: z24.record(z24.string(), adParamRoleSchema),
13213
+ mapCommand: z24.string(),
13214
+ unexplained: z24.array(z24.object({ name: z24.string(), value: z24.string() })),
13215
+ /** The pairs to append to the account's tagging, or `""`. */
13216
+ suffix: z24.string(),
13217
+ whereItGoes: z24.string(),
13218
+ fix: z24.string().optional()
13219
+ });
13220
+ var analyticsTrackingResponseSchema = z24.object({
13221
+ ok: z24.literal(true),
13222
+ data: z24.object({ days: z24.number(), platforms: z24.array(analyticsTrackingPlatformSchema) }),
13223
+ hints: z24.array(z24.string()).optional()
13224
+ });
13178
13225
  var submissionRecordRequestSchema = z24.object({
13179
13226
  /** Injected by the Worker from the host or the site key. Never from the page. */
13180
13227
  companyId: z24.string().min(1),
@@ -23883,13 +23930,13 @@ var mapCommand = (() => {
23883
23930
  };
23884
23931
  registerSchema({
23885
23932
  command: "analytics.map",
23886
- description: "Set how this company's campaign URLs spell each ad dimension",
23933
+ description: "Stage how this company's campaign URLs spell each ad dimension, applied on publish",
23887
23934
  args
23888
23935
  });
23889
23936
  return defineCommand88({
23890
23937
  meta: {
23891
23938
  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."
23939
+ 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
23940
  },
23894
23941
  args,
23895
23942
  run: async ({ args: raw }) => {
@@ -23907,6 +23954,61 @@ var mapCommand = (() => {
23907
23954
  }
23908
23955
  });
23909
23956
  })();
23957
+ var trackingCommand = (() => {
23958
+ const args = {
23959
+ platform: {
23960
+ type: "string",
23961
+ 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",
23962
+ required: false
23963
+ },
23964
+ days: { type: "string", description: "Lookback window for the traffic half (default: 30)", required: false }
23965
+ };
23966
+ registerSchema({
23967
+ command: "analytics.tracking",
23968
+ description: "Check whether a platform's ad URLs carry the campaign, ad group and ad a report needs",
23969
+ args
23970
+ });
23971
+ return defineCommand88({
23972
+ meta: {
23973
+ name: "tracking",
23974
+ 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.
23975
+
23976
+ 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:
23977
+ covered \u2014 arriving and understood, nothing to do
23978
+ 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
23979
+ 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
23980
+ unknown \u2014 no visit arrived AND the account could not be read. Not a finding; do not report it as untagged
23981
+
23982
+ 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.
23983
+
23984
+ Examples:
23985
+ baker analytics tracking
23986
+ baker analytics tracking --platform meta
23987
+ baker analytics tracking --platform google --days 90`
23988
+ },
23989
+ args,
23990
+ run: async ({ args: raw }) => {
23991
+ try {
23992
+ const platform = raw.platform === void 0 ? void 0 : String(raw.platform);
23993
+ if (platform !== void 0 && !isAdPlatform(platform)) {
23994
+ writeJsonEnvelope({
23995
+ ok: false,
23996
+ error: { code: "BAD_REQUEST", message: `Unknown platform "${platform}"` }
23997
+ });
23998
+ process.exit(1);
23999
+ }
24000
+ const days = raw.days === void 0 ? void 0 : Number(raw.days);
24001
+ const response = await apiPost("/api/analytics/tracking", {
24002
+ ...platform ? { platform } : {},
24003
+ ...days !== void 0 && Number.isFinite(days) ? { days } : {}
24004
+ });
24005
+ writeJsonEnvelope(response);
24006
+ } catch (err) {
24007
+ handleError(err);
24008
+ }
24009
+ }
24010
+ });
24011
+ })();
23910
24012
  function parsePairs(raw) {
23911
24013
  if (raw === void 0) return void 0;
23912
24014
  const out = {};
@@ -23955,7 +24057,9 @@ Examples:
23955
24057
  baker analytics ads --role keyword \u2014 keywords by conversions AND by how many bounced
23956
24058
  baker analytics ads --platform google --role keyword \u2014 the same, in Google's vocabulary and from Google's traffic only
23957
24059
  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
24060
+ baker analytics tracking \u2014 do the ad URLs carry the campaign, ad group and ad at all
24061
+ baker analytics tracking --platform meta \u2014 the same for one platform, read from their own ad account
24062
+ baker analytics map --platform google --set kw=keyword \u2014 stage a parameter name, seen or not yet
23959
24063
  baker analytics map --platform google --remove kw \u2014 take that answer back
23960
24064
  baker analytics delivery --page 2 --page-size 50 \u2014 the next page of a long list
23961
24065
  baker analytics overview --compare \u2014 this window against the one before it
@@ -23980,6 +24084,7 @@ Full guide: __tooling__/docs/tools/baker/analytics.md`
23980
24084
  devices: devicesCommand,
23981
24085
  ads: adsCommand3,
23982
24086
  map: mapCommand,
24087
+ tracking: trackingCommand,
23983
24088
  presets: presetsCommand
23984
24089
  }
23985
24090
  });
@@ -33840,10 +33945,6 @@ var MAPPING_SLOTS = {
33840
33945
  twitterAds: [
33841
33946
  { kind: "record", key: "userMatching", allowed: ["em", "ph"] },
33842
33947
  { kind: "scalar", keys: ["value", "currency"] }
33843
- ],
33844
- microsoftAds: [
33845
- { kind: "record", key: "userMatching", allowed: ["em", "ph"] },
33846
- { kind: "scalar", keys: ["value", "currency", "transactionId"] }
33847
33948
  ]
33848
33949
  };
33849
33950
  var UNMAPPABLE = {