@koda-sl/baker-cli 0.344.0-dev.9890d9fcc → 0.345.0-dev.9f4314fa9

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
@@ -2362,7 +2362,6 @@ var GOOGLE_ADS_LIMITS = {
2362
2362
  businessNameMax: 25
2363
2363
  }
2364
2364
  };
2365
- var GOOGLE_ADS_QUERY_MAX_PAGE_SIZE = 1e4;
2366
2365
  var KEYWORD_MATCH_TYPES = ["EXACT", "PHRASE", "BROAD"];
2367
2366
  var ADVERTISING_CHANNEL_TYPES = [
2368
2367
  "SEARCH",
@@ -10080,72 +10079,7 @@ function urlOptionsHints(report) {
10080
10079
  return hints2;
10081
10080
  }
10082
10081
 
10083
- // src/commands/ads/google/during-literals.ts
10084
- var GAQL_DURING_LITERALS = [
10085
- "TODAY",
10086
- "YESTERDAY",
10087
- "LAST_7_DAYS",
10088
- "LAST_14_DAYS",
10089
- "LAST_30_DAYS",
10090
- "LAST_BUSINESS_WEEK",
10091
- "THIS_MONTH",
10092
- "LAST_MONTH",
10093
- "THIS_WEEK_SUN_TODAY",
10094
- "THIS_WEEK_MON_TODAY",
10095
- "LAST_WEEK_SUN_SAT",
10096
- "LAST_WEEK_MON_SUN"
10097
- ];
10098
- function isDuringLiteral(value) {
10099
- return GAQL_DURING_LITERALS.includes(value.toUpperCase());
10100
- }
10101
- function isoDay(date) {
10102
- return date.toISOString().slice(0, 10);
10103
- }
10104
- function utcDate(year, month, day) {
10105
- return new Date(Date.UTC(year, month, day));
10106
- }
10107
- function equivalentBetween(literal, now) {
10108
- const upper2 = literal.toUpperCase();
10109
- const today = utcDate(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
10110
- const yesterday = utcDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 1);
10111
- const range = (from, to) => `BETWEEN '${isoDay(from)}' AND '${isoDay(to)}'`;
10112
- const year = today.getUTCFullYear();
10113
- const quarterStart = Math.floor(today.getUTCMonth() / 3) * 3;
10114
- const rolling = upper2.match(/^LAST_(\d+)_(DAYS|WEEKS|MONTHS|YEARS)$/);
10115
- if (rolling) {
10116
- const n = Number(rolling[1]);
10117
- const unit = rolling[2];
10118
- if (n < 1) return null;
10119
- if (unit === "DAYS" || unit === "WEEKS") {
10120
- const days = unit === "DAYS" ? n : n * 7;
10121
- return range(utcDate(year, today.getUTCMonth(), today.getUTCDate() - days), yesterday);
10122
- }
10123
- const months = unit === "MONTHS" ? n : n * 12;
10124
- return range(utcDate(year, today.getUTCMonth() - months, today.getUTCDate()), yesterday);
10125
- }
10126
- switch (upper2) {
10127
- case "THIS_YEAR":
10128
- return range(utcDate(year, 0, 1), today);
10129
- case "LAST_YEAR":
10130
- return range(utcDate(year - 1, 0, 1), utcDate(year - 1, 11, 31));
10131
- case "THIS_QUARTER":
10132
- return range(utcDate(year, quarterStart, 1), today);
10133
- case "LAST_QUARTER":
10134
- return range(utcDate(year, quarterStart - 3, 1), utcDate(year, quarterStart, 0));
10135
- default:
10136
- return null;
10137
- }
10138
- }
10139
- function duringLiteralExplanation(literal, between) {
10140
- const accepted = GAQL_DURING_LITERALS.join(", ");
10141
- const lead = between ? `Google does not accept ${literal} after DURING. The same window as explicit dates is ${between}.` : `Google does not accept ${literal} after DURING, and it names no fixed window \u2014 use BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD', or drop the date filter.`;
10142
- return `${lead} DURING accepts only: ${accepted}. Any other window needs BETWEEN.`;
10143
- }
10144
-
10145
10082
  // src/commands/ads/google/changes-window.ts
10146
- function monthlySpendTrendQuery(now = /* @__PURE__ */ new Date()) {
10147
- return `SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date ${equivalentBetween("LAST_12_MONTHS", now)}`;
10148
- }
10149
10083
  function isChangeScope(value) {
10150
10084
  return CHANGE_SCOPES.includes(value);
10151
10085
  }
@@ -10199,7 +10133,7 @@ function lookbackTooLong(scope, days, max) {
10199
10133
  message: `Google's account history API reaches back ${CHANGE_SCOPE_MAX_DAYS.summary} days at most; ${days} days is outside that window.`,
10200
10134
  fix: {
10201
10135
  action: "use_different_resource",
10202
- explanation: `Do not abort \u2014 infer the change from performance instead. Query monthly metrics per campaign (${monthlySpendTrendQuery()}) to see which campaigns stopped or started spending. That shows what changed and when, though not who changed it. Older history exists only in the Google Ads web UI, which has no API \u2014 ask the user to check it if the 'who' matters.`
10136
+ explanation: "Do not abort \u2014 infer the change from performance instead. Query monthly metrics per campaign (SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_12_MONTHS) to see which campaigns stopped or started spending. That shows what changed and when, though not who changed it. Older history exists only in the Google Ads web UI, which has no API \u2014 ask the user to check it if the 'who' matters."
10203
10137
  }
10204
10138
  };
10205
10139
  }
@@ -10558,7 +10492,7 @@ var CORRECTION_RULES = [
10558
10492
  matchApiError: /changeStatusError\W{0,4}START_DATE_TOO_OLD/i,
10559
10493
  fix: () => ({
10560
10494
  action: "use_different_resource",
10561
- explanation: `Google's account history reaches back 90 days at most, and this query asks for more. Do not abort \u2014 infer the change from performance: ${monthlySpendTrendQuery()} shows which campaigns started or stopped spending and when, though not who changed them. Anything older with attribution exists only in the Google Ads web UI, which has no API.`
10495
+ explanation: "Google's account history reaches back 90 days at most, and this query asks for more. Do not abort \u2014 infer the change from performance: SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_12_MONTHS shows which campaigns started or stopped spending and when, though not who changed them. Anything older with attribution exists only in the Google Ads web UI, which has no API."
10562
10496
  })
10563
10497
  },
10564
10498
  // 18c. change_status rejects a date range it cannot bound. Google raises this both for
@@ -12385,16 +12319,9 @@ import { resolve } from "path";
12385
12319
  import { defineCommand as defineCommand28 } from "citty";
12386
12320
 
12387
12321
  // src/commands/ads/google/presets.ts
12388
- var BETWEEN_RE = /^BETWEEN\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\s+AND\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'$/i;
12322
+ var GAQL_DATE_RANGE_RE = /^(?:TODAY|YESTERDAY|LAST_7_DAYS|LAST_14_DAYS|LAST_30_DAYS|LAST_90_DAYS|THIS_MONTH|LAST_MONTH|THIS_QUARTER|LAST_QUARTER|THIS_YEAR|LAST_YEAR|ALL_TIME|BETWEEN\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\s+AND\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}')$/i;
12389
12323
  function isValidDateRange(value) {
12390
- const trimmed = value.trim();
12391
- return isDuringLiteral(trimmed) || BETWEEN_RE.test(trimmed);
12392
- }
12393
- function dateRangeError(dateRange, now) {
12394
- if (/^[A-Za-z0-9_]+$/.test(dateRange.trim())) {
12395
- return `Invalid date range: "${dateRange}". ${duringLiteralExplanation(dateRange, equivalentBetween(dateRange, now))} Pass it as --date-range "BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'".`;
12396
- }
12397
- return `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`;
12324
+ return GAQL_DATE_RANGE_RE.test(value.trim());
12398
12325
  }
12399
12326
  var SERVING_CHAINS = {
12400
12327
  keyword_view: ["campaign.status", "ad_group.status", "ad_group_criterion.status"],
@@ -12530,15 +12457,17 @@ function buildStatusScope(statusFields, includePaused) {
12530
12457
  const predicate = includePaused ? "!= 'REMOVED'" : "= 'ENABLED'";
12531
12458
  return statusFields.map((field) => ` AND ${field} ${predicate}`).join("");
12532
12459
  }
12533
- function expandPreset(preset, params, now = /* @__PURE__ */ new Date()) {
12460
+ function expandPreset(preset, params) {
12534
12461
  const dateRange = params.dateRange ?? preset.defaultDateRange;
12535
12462
  const limit = params.limit ?? preset.defaultLimit;
12536
- if (preset.gaqlTemplate.includes("{dateRange}") && !isValidDateRange(dateRange)) {
12537
- return { query: "", dateRangeError: dateRangeError(dateRange, now) };
12463
+ if (!isValidDateRange(dateRange)) {
12464
+ return {
12465
+ query: "",
12466
+ dateRangeError: `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`
12467
+ };
12538
12468
  }
12539
12469
  const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);
12540
- const range = dateRange.trim();
12541
- const query = preset.gaqlTemplate.replace(/DURING\s+\{dateRange\}/g, BETWEEN_RE.test(range) ? range : `DURING ${range}`).replace(/\{dateRange\}/g, range).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
12470
+ const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
12542
12471
  return { query };
12543
12472
  }
12544
12473
 
@@ -12697,28 +12626,6 @@ function applyAutoFixes(query, limit) {
12697
12626
  }
12698
12627
  return { query: corrected, warnings };
12699
12628
  }
12700
- function rejectUnsupportedDuring(query, customerId) {
12701
- for (const match of query.matchAll(/(?<=[a-z_]\.[a-z_.]+\s+)DURING\s+([A-Za-z0-9_]+)/gi)) {
12702
- const literal = match[1] ?? "";
12703
- if (isDuringLiteral(literal)) continue;
12704
- const between = equivalentBetween(literal, /* @__PURE__ */ new Date());
12705
- const fixed = between ? query.replace(match[0], between) : void 0;
12706
- return {
12707
- ok: false,
12708
- error: {
12709
- code: "INVALID_DATE_RANGE",
12710
- message: `DURING ${literal} is not a Google Ads date range, so Google would refuse this query.`,
12711
- fix: {
12712
- action: "narrow_date_range",
12713
- ...fixed ? { correctedCommand: buildCommand2(fixed, customerId) } : {},
12714
- explanation: duringLiteralExplanation(literal, between)
12715
- },
12716
- retryable: false
12717
- }
12718
- };
12719
- }
12720
- return null;
12721
- }
12722
12629
  function validatePreflight(query, customerId, limit) {
12723
12630
  const warnings = [];
12724
12631
  const corrected = query;
@@ -12790,8 +12697,6 @@ function validatePreflight(query, customerId, limit) {
12790
12697
  }
12791
12698
  };
12792
12699
  }
12793
- const duringRejection = rejectUnsupportedDuring(corrected, customerId);
12794
- if (duringRejection) return { valid: false, warnings: [], error: duringRejection };
12795
12700
  if (/\bsegments\.conversion_action_name\b/i.test(corrected) && /\bmetrics\.cost_micros\b/i.test(corrected)) {
12796
12701
  return {
12797
12702
  valid: false,
@@ -13000,7 +12905,7 @@ registerSchema({
13000
12905
  },
13001
12906
  "date-range": {
13002
12907
  type: "string",
13003
- description: "Date range for presets: a Google date literal (TODAY, YESTERDAY, LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH \u2026) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'. Google has no LAST_90_DAYS \u2014 use BETWEEN for any other window.",
12908
+ description: "Date range for presets (LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, or BETWEEN 'X' AND 'Y')",
13004
12909
  required: false
13005
12910
  },
13006
12911
  "include-paused": {
@@ -13439,35 +13344,9 @@ async function executeQuery(finalQuery, args5, customerId, limit, warnings, useC
13439
13344
  function shouldReadCache(args5) {
13440
13345
  return !args5["no-cache"] && !args5.out;
13441
13346
  }
13442
- var DEFAULT_QUERY_LIMIT = 200;
13443
- function parseQueryLimit(raw) {
13444
- if (raw === void 0 || raw === null || raw === "") return { ok: true, limit: DEFAULT_QUERY_LIMIT };
13445
- const limit = Number(raw);
13446
- if (Number.isInteger(limit) && limit >= 1 && limit <= GOOGLE_ADS_QUERY_MAX_PAGE_SIZE) return { ok: true, limit };
13447
- return {
13448
- ok: false,
13449
- error: {
13450
- ok: false,
13451
- error: {
13452
- code: "VALIDATION_ERROR",
13453
- message: `--limit must be a whole number from 1 to ${GOOGLE_ADS_QUERY_MAX_PAGE_SIZE}, got '${String(raw)}'.`,
13454
- fix: {
13455
- action: "retry_with_flag",
13456
- explanation: `--limit is the size of one page, capped at ${GOOGLE_ADS_QUERY_MAX_PAGE_SIZE}. To read more rows, pass --limit ${GOOGLE_ADS_QUERY_MAX_PAGE_SIZE} with --all --out <file> and put your own LIMIT in the statement above the row count you expect.`
13457
- },
13458
- retryable: false
13459
- }
13460
- }
13461
- };
13462
- }
13463
13347
  async function validateArgs(args5) {
13464
- const parsedLimit = parseQueryLimit(args5.limit);
13465
- if (!parsedLimit.ok) {
13466
- writeAdsJson(parsedLimit.error);
13467
- process.exit(1);
13468
- }
13469
- const { limit } = parsedLimit;
13470
13348
  const customerId = await resolveCustomerId(args5);
13349
+ const limit = args5.limit ? Number(args5.limit) : 200;
13471
13350
  const gaql = resolveGaql(args5, limit);
13472
13351
  if (!gaql) {
13473
13352
  process.exit(1);
@@ -31153,6 +31032,278 @@ function mergeStoredWithCounts(stored, reported) {
31153
31032
  }).sort((a, b) => a.name.localeCompare(b.name));
31154
31033
  }
31155
31034
 
31035
+ // src/commands/analytics/render.ts
31036
+ var SPARKLINE_POINTS = 30;
31037
+ var DASHBOARD_ONLY_FIELDS = /* @__PURE__ */ new Set([
31038
+ // The Convex row id. It exists so a dashboard can link to the row; an agent
31039
+ // addresses a landing by `landingId`, which is the id in its `_definition.md`.
31040
+ "landingRef"
31041
+ ]);
31042
+ function isAbsent(value) {
31043
+ if (value === "") return true;
31044
+ if (Array.isArray(value)) return value.length === 0;
31045
+ if (typeof value === "object" && value !== null) return Object.keys(value).length === 0;
31046
+ return false;
31047
+ }
31048
+ function resample(series, points) {
31049
+ if (series.length <= points || points < 1) return [...series];
31050
+ const out = [];
31051
+ for (let i = 0; i < points; i += 1) {
31052
+ const from = Math.floor(i * series.length / points);
31053
+ const to = Math.floor((i + 1) * series.length / points);
31054
+ let sum = 0;
31055
+ for (let j = from; j < to; j += 1) sum += series[j] ?? 0;
31056
+ out.push(sum);
31057
+ }
31058
+ return out;
31059
+ }
31060
+ function pruneValue(value, full) {
31061
+ if (Array.isArray(value)) return value.map((entry) => pruneValue(entry, full));
31062
+ if (typeof value === "object" && value !== null) {
31063
+ return pruneRow(value, full);
31064
+ }
31065
+ return value;
31066
+ }
31067
+ function pruneRow(row, full) {
31068
+ const out = {};
31069
+ for (const [key, value] of Object.entries(row)) {
31070
+ if (DASHBOARD_ONLY_FIELDS.has(key)) continue;
31071
+ if (key === "sparkline" && Array.isArray(value)) {
31072
+ if (!full) continue;
31073
+ out[key] = resample(value, SPARKLINE_POINTS);
31074
+ continue;
31075
+ }
31076
+ if (isAbsent(value)) continue;
31077
+ out[key] = pruneValue(value, full);
31078
+ }
31079
+ return out;
31080
+ }
31081
+ function pruneForAgent(data, full) {
31082
+ return pruneRow(data, full);
31083
+ }
31084
+ function isTabular(rows) {
31085
+ return rows.every(
31086
+ (row) => typeof row === "object" && row !== null && !Array.isArray(row) && Object.values(row).every(
31087
+ (value) => value === null || typeof value !== "object" || Array.isArray(value) && value.every((e) => typeof e !== "object")
31088
+ )
31089
+ );
31090
+ }
31091
+ function cell2(value, key) {
31092
+ if (value === null || value === void 0) return "";
31093
+ if (Array.isArray(value)) return value.join(" ");
31094
+ if (typeof value === "number" && !Number.isInteger(value)) {
31095
+ if (key?.endsWith("Ms")) return String(Math.round(value));
31096
+ return value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
31097
+ }
31098
+ return String(value);
31099
+ }
31100
+ var IDENTITY_COLUMNS = [
31101
+ "landingId",
31102
+ "name",
31103
+ "slug",
31104
+ "path",
31105
+ "flowSlug",
31106
+ "stepId",
31107
+ "value",
31108
+ "key",
31109
+ "bucket",
31110
+ "tag",
31111
+ "eventName",
31112
+ "release",
31113
+ "place",
31114
+ "bot",
31115
+ "source",
31116
+ "medium",
31117
+ "campaign",
31118
+ "platform",
31119
+ "sessionId",
31120
+ "eventId"
31121
+ ];
31122
+ function byIdentityFirst(a, b) {
31123
+ const ia = IDENTITY_COLUMNS.indexOf(a);
31124
+ const ib = IDENTITY_COLUMNS.indexOf(b);
31125
+ if (ia !== -1 || ib !== -1)
31126
+ return (ia === -1 ? Number.MAX_SAFE_INTEGER : ia) - (ib === -1 ? Number.MAX_SAFE_INTEGER : ib);
31127
+ return 0;
31128
+ }
31129
+ var BLOCK_ORDER = ["window", "totals", "previous", "pageInfo"];
31130
+ function byBlockOrder(a, b) {
31131
+ const rank = (key) => {
31132
+ const at = BLOCK_ORDER.indexOf(key);
31133
+ if (at !== -1) return at;
31134
+ return key === "preset" ? BLOCK_ORDER.length + 1 : BLOCK_ORDER.length;
31135
+ };
31136
+ return rank(a) - rank(b);
31137
+ }
31138
+ function columnsOf(rows) {
31139
+ const seen = [];
31140
+ for (const row of rows) {
31141
+ for (const key of Object.keys(row)) {
31142
+ if (!seen.includes(key)) seen.push(key);
31143
+ }
31144
+ }
31145
+ return seen.filter((key) => rows.some((row) => !isAbsent(row[key]) && row[key] !== null && row[key] !== void 0)).sort(byIdentityFirst);
31146
+ }
31147
+ function table2(rows) {
31148
+ const cols = columnsOf(rows);
31149
+ if (cols.length === 0) return "";
31150
+ const head = `| ${cols.join(" | ")} |`;
31151
+ const rule = `| ${cols.map(() => "---").join(" | ")} |`;
31152
+ const body = rows.map((row) => `| ${cols.map((col) => cell2(row[col], col)).join(" | ")} |`);
31153
+ return [head, rule, ...body].join("\n");
31154
+ }
31155
+ function isRowList(value) {
31156
+ return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
31157
+ }
31158
+ function labelOf(row, index) {
31159
+ for (const key of IDENTITY_COLUMNS) {
31160
+ const value = row[key];
31161
+ if (typeof value === "string" && value !== "") return value;
31162
+ }
31163
+ return `#${index + 1}`;
31164
+ }
31165
+ function nestedBlock(name, rows) {
31166
+ const parents = rows.map(
31167
+ (row) => Object.fromEntries(Object.entries(row).filter(([, value]) => !isRowList(value) && !isPlainRow(value)))
31168
+ );
31169
+ const parts = [`### ${name}`, table2(parents)];
31170
+ rows.forEach((row, index) => {
31171
+ for (const [key, value] of Object.entries(row)) {
31172
+ if (isRowList(value)) parts.push(`#### ${name}[${labelOf(row, index)}].${key}`, table2(value));
31173
+ else if (isPlainRow(value)) parts.push(`#### ${name}[${labelOf(row, index)}].${key}`, table2([value]));
31174
+ }
31175
+ });
31176
+ return parts.filter((part) => part !== "").join("\n");
31177
+ }
31178
+ function isPlainRow(value) {
31179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31180
+ }
31181
+ function block(name, value) {
31182
+ if (Array.isArray(value)) {
31183
+ if (value.length === 0) return "";
31184
+ if (isTabular(value)) return `### ${name}
31185
+ ${table2(value)}`;
31186
+ if (isRowList(value)) return nestedBlock(name, value);
31187
+ return `### ${name}
31188
+ ${JSON.stringify(value)}`;
31189
+ }
31190
+ if (typeof value === "object" && value !== null) {
31191
+ const entries = Object.entries(value);
31192
+ if (entries.length === 0) return "";
31193
+ if (entries.every(([, v]) => v === null || typeof v !== "object")) {
31194
+ return `### ${name}
31195
+ ${entries.map(([k, v]) => `- ${k}: ${v === null ? "null" : cell2(v, k)}`).join("\n")}`;
31196
+ }
31197
+ return `### ${name}
31198
+ ${JSON.stringify(value)}`;
31199
+ }
31200
+ return `### ${name}
31201
+ ${cell2(value)}`;
31202
+ }
31203
+ function renderMarkdown3(envelope) {
31204
+ const parts = [];
31205
+ for (const warning of envelope.warnings ?? []) parts.push(`> **${warning.code}** \u2014 ${warning.message}`);
31206
+ for (const hint of envelope.hints ?? []) parts.push(`> ${hint}`);
31207
+ for (const [name, value] of Object.entries(envelope.data).sort(([a], [b]) => byBlockOrder(a, b))) {
31208
+ const rendered = block(name, value);
31209
+ if (rendered !== "") parts.push(rendered);
31210
+ }
31211
+ return `${parts.join("\n\n")}
31212
+ `;
31213
+ }
31214
+ function missingBreakdownHint(data, asked) {
31215
+ if (asked.eventName === void 0) return [];
31216
+ const breakdown = data.eventBreakdown;
31217
+ if (Array.isArray(breakdown) && breakdown.length > 0) return [];
31218
+ const known = Array.isArray(data.customEvents) ? data.customEvents.flatMap((row) => isPlainRow(row) && typeof row.eventName === "string" ? [row.eventName] : []) : [];
31219
+ return [
31220
+ `No rows came back for --event ${asked.eventName}${asked.property ? ` --property ${asked.property}` : ""}. That event or property does not exist on these pages${known.length > 0 ? `; the ones that do are: ${known.slice(0, 12).join(", ")}${known.length > 12 ? ", \u2026" : ""}` : ""}.`
31221
+ ];
31222
+ }
31223
+ var ANALYTICS_OUTPUT_FORMATS = ["json", "md"];
31224
+ function isAnalyticsOutputFormat(value) {
31225
+ return ANALYTICS_OUTPUT_FORMATS.includes(value);
31226
+ }
31227
+ function writeAnalyticsReport(envelope, format) {
31228
+ if (format === "md") {
31229
+ process.stdout.write(renderMarkdown3(envelope));
31230
+ return;
31231
+ }
31232
+ process.stdout.write(`${JSON.stringify(envelope)}
31233
+ `);
31234
+ }
31235
+ var ROWS_BEFORE_FORMAT_HINT = 25;
31236
+ function rowCount(data) {
31237
+ let total = 0;
31238
+ for (const value of Object.values(data)) {
31239
+ if (Array.isArray(value)) total += value.length;
31240
+ }
31241
+ return total;
31242
+ }
31243
+ function formatHint(data, format) {
31244
+ if (format === "md") return [];
31245
+ const rows = rowCount(data);
31246
+ if (rows < ROWS_BEFORE_FORMAT_HINT) return [];
31247
+ return [
31248
+ `This report is ${rows} rows. If you are going to read the numbers rather than pick one field out of them, --output md returns the same answer as tables, typically a third to a half smaller.`
31249
+ ];
31250
+ }
31251
+
31252
+ // src/commands/analytics/conversionsMarkdown.ts
31253
+ function doorsCell(doors) {
31254
+ return doors.map((door) => `${door.door}=${door.events}/${door.sessions}`).join(" ");
31255
+ }
31256
+ function eventKeysCell(eventKeys) {
31257
+ const formKeys = eventKeys.filter((key) => key.startsWith("form:")).length;
31258
+ const others = eventKeys.filter((key) => !key.startsWith("form:"));
31259
+ return [...others, ...formKeys > 0 ? [`${formKeys} Form ending(s)`] : []].join(" + ");
31260
+ }
31261
+ function trafficByForm(candidates) {
31262
+ const byForm = /* @__PURE__ */ new Map();
31263
+ for (const row of candidates) {
31264
+ const form = byForm.get(row.flowSlug) ?? { "endings seen": 0, "endings counted": 0, events: 0, lastSeen: "" };
31265
+ form["endings seen"] += 1;
31266
+ if (row.isCounted) form["endings counted"] += 1;
31267
+ form.events += row.events;
31268
+ if (row.lastSeen && row.lastSeen.slice(0, 10) > form.lastSeen) form.lastSeen = row.lastSeen.slice(0, 10);
31269
+ byForm.set(row.flowSlug, form);
31270
+ }
31271
+ return byForm;
31272
+ }
31273
+ var NO_TRAFFIC = { "endings seen": 0, "endings counted": 0, events: 0, lastSeen: "" };
31274
+ var FORM_KEYS_HINT = "A Form's candidates are one row per Form: `baker analytics conversions --flow <slug>` lists that Form's event keys, and `--candidates --output json` lists every key.";
31275
+ function renderConversionsMarkdown(envelope) {
31276
+ const { definitions, flow, flows, candidates } = envelope.data;
31277
+ const data = {
31278
+ definitions: definitions.map(({ name, eventKeys, ...row }) => ({ name, eventKeys: eventKeysCell(eventKeys), ...row }))
31279
+ };
31280
+ if (flow) {
31281
+ data.flow = { slug: flow.slug, noConversionReason: flow.noConversionReason };
31282
+ data.flowEndings = flow.endings;
31283
+ }
31284
+ const traffic = candidates ? trafficByForm(candidates.filter((row) => row.flowSlug !== "")) : /* @__PURE__ */ new Map();
31285
+ if (flows) {
31286
+ data.flows = flows.map((row) => ({
31287
+ slug: row.slug,
31288
+ counted: row.counted.map((entry) => `${entry.name} @ ${entry.step}`).join("; "),
31289
+ noConversion: row.noConversion,
31290
+ ...candidates ? traffic.get(row.slug) ?? NO_TRAFFIC : {}
31291
+ }));
31292
+ }
31293
+ const hints2 = [...envelope.hints ?? []];
31294
+ if (candidates) {
31295
+ const listed2 = new Set(flows?.map((row) => row.slug));
31296
+ const unlisted = [...traffic].filter(([slug]) => !listed2.has(slug));
31297
+ if (unlisted.length > 0) data.formCandidates = unlisted.map(([form, row]) => ({ form, ...row }));
31298
+ data.candidates = candidates.filter((row) => row.flowSlug === "").map(({ doors, flowSlug: _flowSlug, nodeType: _nodeType, ...row }) => ({
31299
+ ...row,
31300
+ "doors (events/sessions)": doorsCell(doors)
31301
+ }));
31302
+ if (traffic.size > 0) hints2.push(FORM_KEYS_HINT);
31303
+ }
31304
+ return renderMarkdown3({ data, ...hints2.length > 0 ? { hints: hints2 } : {} });
31305
+ }
31306
+
31156
31307
  // src/commands/analytics/errorEnvelope.ts
31157
31308
  function analyticsErrorEnvelope(err) {
31158
31309
  if (err instanceof ApiError) {
@@ -31644,223 +31795,6 @@ var ANALYTICS_PRESET_INFO = [
31644
31795
  }
31645
31796
  ];
31646
31797
 
31647
- // src/commands/analytics/render.ts
31648
- var SPARKLINE_POINTS = 30;
31649
- var DASHBOARD_ONLY_FIELDS = /* @__PURE__ */ new Set([
31650
- // The Convex row id. It exists so a dashboard can link to the row; an agent
31651
- // addresses a landing by `landingId`, which is the id in its `_definition.md`.
31652
- "landingRef"
31653
- ]);
31654
- function isAbsent(value) {
31655
- if (value === "") return true;
31656
- if (Array.isArray(value)) return value.length === 0;
31657
- if (typeof value === "object" && value !== null) return Object.keys(value).length === 0;
31658
- return false;
31659
- }
31660
- function resample(series, points) {
31661
- if (series.length <= points || points < 1) return [...series];
31662
- const out = [];
31663
- for (let i = 0; i < points; i += 1) {
31664
- const from = Math.floor(i * series.length / points);
31665
- const to = Math.floor((i + 1) * series.length / points);
31666
- let sum = 0;
31667
- for (let j = from; j < to; j += 1) sum += series[j] ?? 0;
31668
- out.push(sum);
31669
- }
31670
- return out;
31671
- }
31672
- function pruneValue(value, full) {
31673
- if (Array.isArray(value)) return value.map((entry) => pruneValue(entry, full));
31674
- if (typeof value === "object" && value !== null) {
31675
- return pruneRow(value, full);
31676
- }
31677
- return value;
31678
- }
31679
- function pruneRow(row, full) {
31680
- const out = {};
31681
- for (const [key, value] of Object.entries(row)) {
31682
- if (DASHBOARD_ONLY_FIELDS.has(key)) continue;
31683
- if (key === "sparkline" && Array.isArray(value)) {
31684
- if (!full) continue;
31685
- out[key] = resample(value, SPARKLINE_POINTS);
31686
- continue;
31687
- }
31688
- if (isAbsent(value)) continue;
31689
- out[key] = pruneValue(value, full);
31690
- }
31691
- return out;
31692
- }
31693
- function pruneForAgent(data, full) {
31694
- return pruneRow(data, full);
31695
- }
31696
- function isTabular(rows) {
31697
- return rows.every(
31698
- (row) => typeof row === "object" && row !== null && !Array.isArray(row) && Object.values(row).every(
31699
- (value) => value === null || typeof value !== "object" || Array.isArray(value) && value.every((e) => typeof e !== "object")
31700
- )
31701
- );
31702
- }
31703
- function cell2(value, key) {
31704
- if (value === null || value === void 0) return "";
31705
- if (Array.isArray(value)) return value.join(" ");
31706
- if (typeof value === "number" && !Number.isInteger(value)) {
31707
- if (key?.endsWith("Ms")) return String(Math.round(value));
31708
- return value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
31709
- }
31710
- return String(value);
31711
- }
31712
- var IDENTITY_COLUMNS = [
31713
- "landingId",
31714
- "name",
31715
- "slug",
31716
- "path",
31717
- "flowSlug",
31718
- "stepId",
31719
- "value",
31720
- "key",
31721
- "bucket",
31722
- "tag",
31723
- "eventName",
31724
- "release",
31725
- "place",
31726
- "bot",
31727
- "source",
31728
- "medium",
31729
- "campaign",
31730
- "platform",
31731
- "sessionId",
31732
- "eventId"
31733
- ];
31734
- function byIdentityFirst(a, b) {
31735
- const ia = IDENTITY_COLUMNS.indexOf(a);
31736
- const ib = IDENTITY_COLUMNS.indexOf(b);
31737
- if (ia !== -1 || ib !== -1)
31738
- return (ia === -1 ? Number.MAX_SAFE_INTEGER : ia) - (ib === -1 ? Number.MAX_SAFE_INTEGER : ib);
31739
- return 0;
31740
- }
31741
- var BLOCK_ORDER = ["window", "totals", "previous", "pageInfo"];
31742
- function byBlockOrder(a, b) {
31743
- const rank = (key) => {
31744
- const at = BLOCK_ORDER.indexOf(key);
31745
- if (at !== -1) return at;
31746
- return key === "preset" ? BLOCK_ORDER.length + 1 : BLOCK_ORDER.length;
31747
- };
31748
- return rank(a) - rank(b);
31749
- }
31750
- function columnsOf(rows) {
31751
- const seen = [];
31752
- for (const row of rows) {
31753
- for (const key of Object.keys(row)) {
31754
- if (!seen.includes(key)) seen.push(key);
31755
- }
31756
- }
31757
- return seen.filter((key) => rows.some((row) => !isAbsent(row[key]) && row[key] !== null && row[key] !== void 0)).sort(byIdentityFirst);
31758
- }
31759
- function table2(rows) {
31760
- const cols = columnsOf(rows);
31761
- if (cols.length === 0) return "";
31762
- const head = `| ${cols.join(" | ")} |`;
31763
- const rule = `| ${cols.map(() => "---").join(" | ")} |`;
31764
- const body = rows.map((row) => `| ${cols.map((col) => cell2(row[col], col)).join(" | ")} |`);
31765
- return [head, rule, ...body].join("\n");
31766
- }
31767
- function isRowList(value) {
31768
- return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
31769
- }
31770
- function labelOf(row, index) {
31771
- for (const key of IDENTITY_COLUMNS) {
31772
- const value = row[key];
31773
- if (typeof value === "string" && value !== "") return value;
31774
- }
31775
- return `#${index + 1}`;
31776
- }
31777
- function nestedBlock(name, rows) {
31778
- const parents = rows.map(
31779
- (row) => Object.fromEntries(Object.entries(row).filter(([, value]) => !isRowList(value) && !isPlainRow(value)))
31780
- );
31781
- const parts = [`### ${name}`, table2(parents)];
31782
- rows.forEach((row, index) => {
31783
- for (const [key, value] of Object.entries(row)) {
31784
- if (isRowList(value)) parts.push(`#### ${name}[${labelOf(row, index)}].${key}`, table2(value));
31785
- else if (isPlainRow(value)) parts.push(`#### ${name}[${labelOf(row, index)}].${key}`, table2([value]));
31786
- }
31787
- });
31788
- return parts.filter((part) => part !== "").join("\n");
31789
- }
31790
- function isPlainRow(value) {
31791
- return typeof value === "object" && value !== null && !Array.isArray(value);
31792
- }
31793
- function block(name, value) {
31794
- if (Array.isArray(value)) {
31795
- if (value.length === 0) return "";
31796
- if (isTabular(value)) return `### ${name}
31797
- ${table2(value)}`;
31798
- if (isRowList(value)) return nestedBlock(name, value);
31799
- return `### ${name}
31800
- ${JSON.stringify(value)}`;
31801
- }
31802
- if (typeof value === "object" && value !== null) {
31803
- const entries = Object.entries(value);
31804
- if (entries.length === 0) return "";
31805
- if (entries.every(([, v]) => v === null || typeof v !== "object")) {
31806
- return `### ${name}
31807
- ${entries.map(([k, v]) => `- ${k}: ${v === null ? "null" : cell2(v, k)}`).join("\n")}`;
31808
- }
31809
- return `### ${name}
31810
- ${JSON.stringify(value)}`;
31811
- }
31812
- return `### ${name}
31813
- ${cell2(value)}`;
31814
- }
31815
- function renderMarkdown3(envelope) {
31816
- const parts = [];
31817
- for (const warning of envelope.warnings ?? []) parts.push(`> **${warning.code}** \u2014 ${warning.message}`);
31818
- for (const hint of envelope.hints ?? []) parts.push(`> ${hint}`);
31819
- for (const [name, value] of Object.entries(envelope.data).sort(([a], [b]) => byBlockOrder(a, b))) {
31820
- const rendered = block(name, value);
31821
- if (rendered !== "") parts.push(rendered);
31822
- }
31823
- return `${parts.join("\n\n")}
31824
- `;
31825
- }
31826
- function missingBreakdownHint(data, asked) {
31827
- if (asked.eventName === void 0) return [];
31828
- const breakdown = data.eventBreakdown;
31829
- if (Array.isArray(breakdown) && breakdown.length > 0) return [];
31830
- const known = Array.isArray(data.customEvents) ? data.customEvents.flatMap((row) => isPlainRow(row) && typeof row.eventName === "string" ? [row.eventName] : []) : [];
31831
- return [
31832
- `No rows came back for --event ${asked.eventName}${asked.property ? ` --property ${asked.property}` : ""}. That event or property does not exist on these pages${known.length > 0 ? `; the ones that do are: ${known.slice(0, 12).join(", ")}${known.length > 12 ? ", \u2026" : ""}` : ""}.`
31833
- ];
31834
- }
31835
- var ANALYTICS_OUTPUT_FORMATS = ["json", "md"];
31836
- function isAnalyticsOutputFormat(value) {
31837
- return ANALYTICS_OUTPUT_FORMATS.includes(value);
31838
- }
31839
- function writeAnalyticsReport(envelope, format) {
31840
- if (format === "md") {
31841
- process.stdout.write(renderMarkdown3(envelope));
31842
- return;
31843
- }
31844
- process.stdout.write(`${JSON.stringify(envelope)}
31845
- `);
31846
- }
31847
- var ROWS_BEFORE_FORMAT_HINT = 25;
31848
- function rowCount(data) {
31849
- let total = 0;
31850
- for (const value of Object.values(data)) {
31851
- if (Array.isArray(value)) total += value.length;
31852
- }
31853
- return total;
31854
- }
31855
- function formatHint(data, format) {
31856
- if (format === "md") return [];
31857
- const rows = rowCount(data);
31858
- if (rows < ROWS_BEFORE_FORMAT_HINT) return [];
31859
- return [
31860
- `This report is ${rows} rows. If you are going to read the numbers rather than pick one field out of them, --output md returns the same answer as tables, typically a third to a half smaller.`
31861
- ];
31862
- }
31863
-
31864
31798
  // src/commands/analytics/sends.ts
31865
31799
  import { defineCommand as defineCommand97 } from "citty";
31866
31800
  var SET_FLAGS = ["--source-event", "--platform", "--tag", "--event"];
@@ -33035,17 +32969,7 @@ async function runPreset(args5, options) {
33035
32969
  const response = await apiPost("/api/analytics/query", requestBody(args5, options));
33036
32970
  const requestedPlatform = args5.platform === void 0 ? "" : String(args5.platform);
33037
32971
  const adPlatformScope = isAdPlatform(requestedPlatform) ? requestedPlatform : void 0;
33038
- const format = args5.output === void 0 ? "json" : String(args5.output);
33039
- if (!isAnalyticsOutputFormat(format)) {
33040
- writeJsonEnvelope({
33041
- ok: false,
33042
- error: {
33043
- code: "VALIDATION_ERROR",
33044
- message: `Unknown --output "${format}". Use one of: ${ANALYTICS_OUTPUT_FORMATS.join(", ")}.`
33045
- }
33046
- });
33047
- process.exit(1);
33048
- }
32972
+ const format = outputFormatOrExit(args5.output);
33049
32973
  const data = pruneForAgent(response.data, args5.full === true);
33050
32974
  const feed = "stream" in data ? shapeFeed(data.stream, args5.everything === true) : void 0;
33051
32975
  if (feed !== void 0) data.stream = feed.rows;
@@ -33075,6 +32999,20 @@ async function runPreset(args5, options) {
33075
32999
  handleError3(err);
33076
33000
  }
33077
33001
  }
33002
+ function outputFormatOrExit(raw) {
33003
+ const format = raw === void 0 ? "json" : String(raw);
33004
+ if (!isAnalyticsOutputFormat(format)) {
33005
+ writeJsonEnvelope({
33006
+ ok: false,
33007
+ error: {
33008
+ code: "VALIDATION_ERROR",
33009
+ message: `Unknown --output "${format}". Use one of: ${ANALYTICS_OUTPUT_FORMATS.join(", ")}.`
33010
+ }
33011
+ });
33012
+ process.exit(1);
33013
+ }
33014
+ return format;
33015
+ }
33078
33016
  function presetCommand(options) {
33079
33017
  registerSchema({
33080
33018
  command: `analytics.${options.name}`,
@@ -33576,7 +33514,8 @@ var conversionsCommand3 = (() => {
33576
33514
  type: "string",
33577
33515
  description: "Lookback for the counts and for --candidates. Default 30",
33578
33516
  required: false
33579
- }
33517
+ },
33518
+ output: SHARED_ARGS.output
33580
33519
  };
33581
33520
  registerSchema({
33582
33521
  command: "analytics.conversions",
@@ -33591,6 +33530,7 @@ var conversionsCommand3 = (() => {
33591
33530
  args: args5,
33592
33531
  run: async ({ args: raw }) => {
33593
33532
  try {
33533
+ const format = outputFormatOrExit(raw.output);
33594
33534
  const args6 = parseConversionsArgs(raw, process.argv);
33595
33535
  for (const eventKey of args6.eventKeys) refuseUnmatchableKey(eventKey);
33596
33536
  const before = await apiPost("/api/analytics/conversions", {});
@@ -33600,13 +33540,13 @@ var conversionsCommand3 = (() => {
33600
33540
  preset: "conversions",
33601
33541
  days: args6.days
33602
33542
  });
33603
- writeJsonEnvelope(
33604
- conversionsEnvelope(
33605
- args6,
33606
- mergeStoredWithCounts(stored, report.data.conversions ?? []),
33607
- report.data.eventCatalog ?? []
33608
- )
33543
+ const envelope = conversionsEnvelope(
33544
+ args6,
33545
+ mergeStoredWithCounts(stored, report.data.conversions ?? []),
33546
+ report.data.eventCatalog ?? []
33609
33547
  );
33548
+ if (format === "md") process.stdout.write(renderConversionsMarkdown(envelope));
33549
+ else writeJsonEnvelope(envelope);
33610
33550
  } catch (err) {
33611
33551
  if (err instanceof ConversionArgsError) {
33612
33552
  handleError3(new ApiError("VALIDATION_ERROR", err.message));