@koda-sl/baker-cli 0.236.0 → 0.237.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/README.md CHANGED
@@ -1204,6 +1204,8 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1204
1204
 
1205
1205
  **Campaign parameters are per platform, and so are the numbers.** `baker analytics ads` returns `adPlatformTraffic` (visits, conversions and *tagged* visits for each of the nine platforms), `adParams` for the platform in `--platform` only, and `adParamsUnattributed` for names arriving on visits with no click id and no recognisable source. Read `adPlatformTraffic` first: a platform with visits and zero tagged visits has untagged ad URLs, and no mapping can fix that from Baker's side. `baker analytics map` needs `--platform` for any change, accepts a parameter name **nobody has sent yet** — which is how a tracking template is configured before its campaign runs — and takes an answer back with `--remove`. `--set <name>=ignore` says a parameter is not campaign information at all, so Baker stops storing it and stops listing it.
1206
1206
 
1207
+ **The one recommendation about the ad account itself.** `baker analytics ads --platform <p>` warns when that platform's traffic carries no campaign id, ad group id or ad id — the three tiers every "which ad worked" question is grouped by — and returns the exact URL suffix, in the platform's own macro language, that would start sending the missing ones. It is the only mapping gap that cannot be fixed afterwards: naming a parameter Baker already captures corrects the history, because roles are resolved when a report is read, but a parameter the ads never sent has no stored values for a meaning to apply to. The hint stays quiet for a platform with little traffic, and defers to the untagged-account finding rather than repeating it.
1208
+
1207
1209
  An unrecognised `--timezone` is refused with a `VALIDATION_ERROR` rather than defaulting to UTC, and `--output` accepts only `json` or `md`. Pass `--timezone` whenever the client is not in UTC. Day boundaries are drawn in UTC by default, which moves two hours of every Spanish evening onto the following day — not a rounding error on a day-of-week comparison, but the wrong day.
1208
1210
 
1209
1211
  Reading the output:
package/dist/cli.js CHANGED
@@ -11347,6 +11347,62 @@ var AD_PARAM_ROLES = [
11347
11347
  "other",
11348
11348
  "ignore"
11349
11349
  ];
11350
+ var AD_PARAM_ROLE_SPECS = {
11351
+ campaign_id: { role: "campaign_id", label: "Campaign ID", description: "The top level of this account's structure." },
11352
+ campaign_name: { role: "campaign_name", label: "Campaign name", description: "The campaign's name." },
11353
+ adgroup_id: { role: "adgroup_id", label: "Ad group ID", description: "The targeting container inside a campaign." },
11354
+ adgroup_name: { role: "adgroup_name", label: "Ad group name", description: "The ad group's name." },
11355
+ ad_id: { role: "ad_id", label: "Ad ID", description: "The individual ad that was clicked." },
11356
+ ad_name: { role: "ad_name", label: "Ad name", description: "The individual ad's name." },
11357
+ keyword: {
11358
+ role: "keyword",
11359
+ label: "Keyword",
11360
+ description: "The keyword that matched, as it is written in the account \u2014 not what the visitor typed."
11361
+ },
11362
+ match_type: {
11363
+ role: "match_type",
11364
+ label: "Match type",
11365
+ description: "How loosely the keyword matched: broad, phrase or exact."
11366
+ },
11367
+ search_query: { role: "search_query", label: "Search query", description: "What the visitor actually typed." },
11368
+ audience: {
11369
+ role: "audience",
11370
+ label: "Audience",
11371
+ description: "The remarketing list or interest segment the visitor was in."
11372
+ },
11373
+ placement: {
11374
+ role: "placement",
11375
+ label: "Placement",
11376
+ description: "Where the ad appeared \u2014 a feed, a story, a site on the display network."
11377
+ },
11378
+ network: {
11379
+ role: "network",
11380
+ label: "Network",
11381
+ description: "Which network served it: search, a search partner, display, or a video surface."
11382
+ },
11383
+ site_source: {
11384
+ role: "site_source",
11385
+ label: "App",
11386
+ description: "Which app the click came from, on platforms that span several."
11387
+ },
11388
+ device: { role: "device", label: "Device", description: "Mobile, tablet or desktop, as the platform saw it." },
11389
+ product_id: { role: "product_id", label: "Product ID", description: "The shopping item that was clicked." },
11390
+ account_name: {
11391
+ role: "account_name",
11392
+ label: "Account name",
11393
+ description: "Which ad account paid for the click, where an agency runs several."
11394
+ },
11395
+ other: {
11396
+ role: "other",
11397
+ label: "Something else",
11398
+ description: "Keep the values and let me filter by this name, without saying what it means."
11399
+ },
11400
+ ignore: {
11401
+ role: "ignore",
11402
+ label: "Ignore it",
11403
+ description: "Not campaign information. Baker stops storing it and stops asking about it."
11404
+ }
11405
+ };
11350
11406
  var RESERVED_PARAM_NAMES = [
11351
11407
  "utm_source",
11352
11408
  "utm_medium",
@@ -11365,6 +11421,9 @@ var AD_PLATFORMS = [
11365
11421
  "pinterest",
11366
11422
  "snapchat"
11367
11423
  ];
11424
+ function isAdPlatform(value) {
11425
+ return AD_PLATFORMS.includes(value);
11426
+ }
11368
11427
  var GOOGLE = {
11369
11428
  platform: "google",
11370
11429
  label: "Google Ads",
@@ -11557,6 +11616,10 @@ var AD_PLATFORM_DEFINITIONS = {
11557
11616
  pinterest: PINTEREST,
11558
11617
  snapchat: SNAPCHAT
11559
11618
  };
11619
+ var ESSENTIAL_ROLES = ["campaign_id", "adgroup_id", "ad_id", "keyword", "match_type"];
11620
+ function essentialAdDimensions(platform) {
11621
+ return AD_PLATFORM_DEFINITIONS[platform].dimensions.filter((entry) => ESSENTIAL_ROLES.includes(entry.role));
11622
+ }
11560
11623
  function normalizeAdParamName(raw) {
11561
11624
  return raw.trim().toLowerCase().slice(0, 64);
11562
11625
  }
@@ -12361,10 +12424,19 @@ var analyticsLandingRowSchema = z24.object({
12361
12424
  *
12362
12425
  * Derived from the click ids a visit arrived with, never from `utm_source`:
12363
12426
  * a label is whatever somebody typed into a tracking template, a `gclid` is a
12364
- * click. Thresholded, so one stray click from a test URL does not describe a
12365
- * page as advertised.
12427
+ * click. Ordered by traffic, busiest first.
12366
12428
  */
12367
- channels: z24.array(z24.string())
12429
+ channels: z24.array(z24.string()),
12430
+ /**
12431
+ * Visits per entry of {@link channels}, in the same order.
12432
+ *
12433
+ * The half that makes a mark readable. A mark on its own says "advertised
12434
+ * here", which is why it used to be withheld until three visits had arrived —
12435
+ * and withholding it is what made a page whose whole day was five Meta clicks
12436
+ * show nothing at all. The count says how much, so one test click and a live
12437
+ * campaign are distinguishable by reading rather than by absence.
12438
+ */
12439
+ channelSessions: z24.array(z24.number().int().nonnegative())
12368
12440
  });
12369
12441
  var analyticsTagRowSchema = z24.object({
12370
12442
  tag: z24.string(),
@@ -12923,7 +12995,14 @@ var analyticsVisitProfileSchema = z24.object({
12923
12995
  utmSource: z24.string(),
12924
12996
  utmMedium: z24.string(),
12925
12997
  utmCampaign: z24.string(),
12926
- /** Whichever click id the visit arrived with, and which platform it belongs to. */
12998
+ /**
12999
+ * Which platform the click id proves, in the `AD_PLATFORMS` vocabulary —
13000
+ * `google` | `meta` | `tiktok` | `x` | `linkedin` | `microsoft` | `""`.
13001
+ *
13002
+ * The same spelling every mark, mapping and report uses. It briefly said
13003
+ * `google_ads` / `meta_ads` here alone, which meant every logo lookup on the
13004
+ * People screen missed and a paid visit drew a neutral globe.
13005
+ */
12927
13006
  clickPlatform: z24.string(),
12928
13007
  /** Every ValueTrack-style parameter the URL carried, unresolved. */
12929
13008
  campaignParams: z24.record(z24.string(), z24.string()),
@@ -23060,7 +23139,22 @@ function shapeHints(data) {
23060
23139
  }
23061
23140
  return hints;
23062
23141
  }
23063
- function buildAnalyticsHints(data) {
23142
+ function adHierarchyHints(data, platform) {
23143
+ if (platform === void 0) return [];
23144
+ const traffic = (data.adPlatformTraffic ?? []).find((entry) => entry.platform === platform);
23145
+ if (traffic === void 0 || traffic.sessions < MIN_SESSIONS_FOR_TAGGING_CLAIM) return [];
23146
+ if (traffic.taggedSessions === 0) return [];
23147
+ const carried = new Set(
23148
+ (data.adParams ?? []).filter((row) => row.role !== void 0 && row.awaitingTraffic !== true).map((row) => row.role)
23149
+ );
23150
+ const missing = essentialAdDimensions(platform).filter((entry) => !carried.has(entry.role));
23151
+ if (missing.length === 0) return [];
23152
+ const suffix = missing.map((entry) => `${entry.names[0] ?? entry.role}=${entry.macro}`).join("&");
23153
+ return [
23154
+ `${platform} traffic carries no ${missing.map((entry) => AD_PARAM_ROLE_SPECS[entry.role].label.toLowerCase()).join(", ")}, so no report can say which ad sent a visit \u2014 only which platform. Add \`${suffix}\` to that account's tracking template. This cannot be backfilled: a parameter's values are only stored once it starts arriving.`
23155
+ ];
23156
+ }
23157
+ function buildAnalyticsHints(data, platform) {
23064
23158
  const hints = [];
23065
23159
  for (const funnel of data.funnels ?? []) {
23066
23160
  const worst = funnel.worstStep;
@@ -23086,6 +23180,7 @@ function buildAnalyticsHints(data) {
23086
23180
  );
23087
23181
  }
23088
23182
  hints.push(...shapeHints(data));
23183
+ hints.push(...adHierarchyHints(data, platform));
23089
23184
  if ((data.topCampaigns ?? []).length === 0 && (data.topSources ?? []).length > 0) {
23090
23185
  hints.push(
23091
23186
  "No campaign tagging on any traffic \u2014 ad clicks cannot be attributed to a campaign until the destination URLs carry utm parameters."
@@ -23514,6 +23609,8 @@ function requestBody(args, options) {
23514
23609
  async function runPreset(args, options) {
23515
23610
  try {
23516
23611
  const response = await apiPost("/api/analytics/query", requestBody(args, options));
23612
+ const requestedPlatform = args.platform === void 0 ? "" : String(args.platform);
23613
+ const adPlatformScope = isAdPlatform(requestedPlatform) ? requestedPlatform : void 0;
23517
23614
  const format = args.output === void 0 ? "json" : String(args.output);
23518
23615
  if (!isAnalyticsOutputFormat(format)) {
23519
23616
  writeJsonEnvelope({
@@ -23527,7 +23624,11 @@ async function runPreset(args, options) {
23527
23624
  }
23528
23625
  const data = pruneForAgent(response.data, args.full === true);
23529
23626
  const hints = [
23530
- ...buildAnalyticsHints(response.data),
23627
+ // The platform the ad blocks were scoped to, so the tagging hint can
23628
+ // only speak about the inventory it was actually given. `adParams` is one
23629
+ // platform's list; a hint drawn from it without knowing which platform
23630
+ // would be a claim about Meta made from Google's URLs.
23631
+ ...buildAnalyticsHints(response.data, adPlatformScope),
23531
23632
  ...missingBreakdownHint(response.data, {
23532
23633
  eventName: options.eventName,
23533
23634
  property: options.property