@wrangled/cli 0.3.149 → 0.3.150

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/guide.js CHANGED
@@ -156,6 +156,7 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
156
156
  - \`${cli} funnel [--grain day|week|month]\` prints ad spend, leads, booked calls, calls held, sales and net revenue per day, week (starting Monday) or month, with cost per lead, per booked call and per sale and ROAS, then totals for the whole range.
157
157
  - \`${cli} campaigns [--channel <channel>] [--limit <n>]\` prints each campaign's spend, leads, booked calls, sales and revenue, best first (most first-touch revenue, then most spend), then totals over every matching campaign. Each lead and sale is credited to the person's first touch and last touch side by side. \`--limit\` is 1 to 200 (default 50); \`--channel\` is meta, google, affiliate, a utm_source, unknown or recorded_in_crm.
158
158
  - \`${cli} money [--grain day|week|month]\` prints money in (gross, refunds, net, orders), ad spend, affiliate payouts, what is left after ad costs and ROAS per period, then totals for the whole range, money in per payment source (stripe, ghl) and the biggest refund days. Refunds come off on the day they were made, not the day of the sale. \`left_after_ad_costs\` is net less ad spend and affiliate payouts; it is not profit. Its \`roas\` is net over ad spend alone, so it differs from \`${cli} funnel\`'s, which divides by all spend including affiliate payouts.
159
+ - When no payment tool, store or CRM sales are connected but Triple Whale is, \`${cli} money\` takes money in from Triple Whale's store totals and \`${cli} campaigns\` adds the orders, revenue and ROAS Triple Whale credits to each Facebook campaign (\`orders_triple_whale\`, \`revenue_triple_whale\`, \`roas_triple_whale\`), counted as \`${cli} winners\` counts them. Each says so under its table. Triple Whale can credit one order to several ads, so campaign orders can add up to more than the store's. With neither, each report says what is missing and leaves out the columns that would be blank.
159
160
  - Money is never converted or added across currencies: there is one row per currency, and one total per currency. In \`${cli} funnel\`, leads and calls are counted for the whole period and repeat on every currency row, so never add them across its rows. In \`${cli} campaigns\`, each campaign's leads and calls sit on the row of the currency it spends in (or a blank-currency row when it has no spend), so they do add up across rows and totals. Ratios come from the summed numbers, not averages. An empty cell means the source is not connected, or a ratio would divide by 0.
160
161
  - \`${cli} metrics query --metric <name> [--by <dimension>]...\` gives any of the standard numbers by name, split and filtered your way, such as \`--metric spend --metric cost_per_lead --by channel --where channel!=affiliate --sort=-spend\`. It sums the parts before dividing, keeps currencies apart, credits leads and sales by channel or campaign to the first touch (\`--attribution last_touch\` for the last), and prints an empty number, never 0, for a source that is not connected. An unknown name fails with the list of real ones. Use it before writing SQL for a number these reports do not split the way you need.
161
162
  - \`${cli} metrics search "<everyday words>"\` finds the name \`metrics query\` takes from your own words, such as \`"show rate"\`, \`"cost per booked call"\` or \`"first time buyers"\`: each match with what it means, which of the client's sources feed it and whether they are connected, and for a split like channel or campaign up to five of the client's own values. \`${cli} metrics list\` prints every number and split. The client's own confirmed labels and numbers show beside the catalog's, marked "(custom)". Search before guessing a name.
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
7
7
  import { parseArgs } from "node:util";
8
8
  import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, DOCUMENT_LIMITS, META_IDS_MAX, PROFILE_MESSAGING_KINDS, OUTCOME_PARTS, FACET_WORDS, GOOGLE_ADS_IDS_MAX, IMPORT_NAME_PATTERN, AGENT_TABLE_LIMITS, AGENT_TABLE_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, OUTBOUND_PUSH_MAX, entryTagFlags, streamSigned, } from "./api.js";
9
9
  import { brand, envPrefix } from "./brand.js";
10
+ import { campaignLayout, money, moneyLayout } from "./report-columns.js";
10
11
  import { renderDataMap, renderQuestions } from "./map.js";
11
12
  import { apiSetupPath, parseAuthFlag, parseQueryFlags, setupText, submittedText, testText, tryText, waitingText, } from "./api-setup.js";
12
13
  import { answerRow, batches, ENRICH_EMAIL_USAGE, parsePeople, parseSteps } from "./enrich.js";
@@ -242,7 +243,7 @@ const commands = {
242
243
  const { funnel } = await api(await resolveConnection(options), `/api/v1/reports/funnel${query}`);
243
244
  if (options.format === "json")
244
245
  return printJson({ funnel });
245
- printReport(`Funnel by ${funnel.grain}`, funnel, ["period", "currency"], FUNNEL_NUMBERS);
246
+ printReport(`Funnel by ${funnel.grain}`, funnel, ["period", "currency"], { numbers: FUNNEL_NUMBERS, before: [], after: [] });
246
247
  printLeftOut(funnel.left_out, "sales, revenue_net and roas");
247
248
  },
248
249
  },
@@ -480,7 +481,7 @@ const commands = {
480
481
  const { money } = await api(await resolveConnection(options), `/api/v1/reports/money${query}`);
481
482
  if (options.format === "json")
482
483
  return printJson({ money });
483
- printReport(`Money by ${money.grain}`, money, ["period", "currency"], MONEY_NUMBERS);
484
+ printReport(`Money by ${money.grain}`, money, ["period", "currency"], moneyLayout(money));
484
485
  if (money.by_source.length > 0)
485
486
  console.log(`\nMoney in by payment source:\n${renderTable(["source", "currency", "gross", "refunds", "net", "orders"], money.by_source)}`);
486
487
  if (money.store_orders_in_stripe?.length) {
@@ -552,7 +553,7 @@ const commands = {
552
553
  if (options.format === "json")
553
554
  return printJson({ campaignResults });
554
555
  const title = campaignResults.channel === null ? "Campaigns, best first" : `${campaignResults.channel} campaigns, best first`;
555
- printReport(title, campaignResults, ["channel", "campaign_id", "campaign_name", "currency"], CAMPAIGN_NUMBERS);
556
+ printReport(title, campaignResults, ["channel", "campaign_id", "campaign_name", "currency"], campaignLayout(campaignResults));
556
557
  printLeftOut(campaignResults.left_out, "the totals (the rows keep it)");
557
558
  },
558
559
  },
@@ -4103,27 +4104,6 @@ function historyText(history) {
4103
4104
  return main + catchingUp.join("");
4104
4105
  }
4105
4106
  const FUNNEL_NUMBERS = ["spend", "leads", "booked_calls", "calls_held", "sales_count", "revenue_net", "new_buyers", "cost_per_lead", "cost_per_booked_call", "cost_per_sale", "cost_per_new_buyer", "roas"];
4106
- const MONEY_NUMBERS = ["gross", "refunds", "net", "orders", "ad_spend", "affiliate_payouts", "left_after_ad_costs", "roas"];
4107
- const CAMPAIGN_NUMBERS = [
4108
- "spend",
4109
- "leads_first_touch",
4110
- "leads_last_touch",
4111
- "booked_calls_first_touch",
4112
- "sales_first_touch",
4113
- "revenue_first_touch",
4114
- "revenue_last_touch",
4115
- "cost_per_lead_first_touch",
4116
- "roas_first_touch",
4117
- "roas_last_touch",
4118
- ];
4119
- function money(value, currency) {
4120
- try {
4121
- return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value);
4122
- }
4123
- catch {
4124
- return `${value.toFixed(2)} ${currency}`;
4125
- }
4126
- }
4127
4107
  function newIdeasLine({ target, proposedToday, stalled, costPerResult: cpr }, ideasPerDay) {
4128
4108
  const split = `${target} of ${ideasPerDay}${proposedToday > 0 ? `, ${proposedToday} already proposed` : ""}`;
4129
4109
  if (!cpr)
@@ -4578,13 +4558,19 @@ function printLeftOut(leftOut, figures) {
4578
4558
  const rows = leftOut.flatMap((l) => l.amounts.map((a) => ({ fact: l.fact_id, currency: a.currency, revenue: a.revenue, sales: a.sales, rule: l.summary })));
4579
4559
  console.log(`\nLeft out of ${figures} by confirmed leave-out rules:\n${renderTable(["fact", "currency", "revenue", "sales", "rule"], rows)}`);
4580
4560
  }
4581
- function printReport(title, report, labels, numbers) {
4561
+ function printReport(title, report, labels, { numbers, before, after }) {
4562
+ if (before.length > 0)
4563
+ console.log(`${before.join("\n")}\n`);
4582
4564
  if (report.rows.length === 0)
4583
- return console.log(`No numbers from ${report.from} to ${report.to}.`);
4584
- console.log(`${title}, ${report.from} to ${report.to}:`);
4585
- console.log(renderTable([...labels, ...numbers], report.rows));
4586
- console.log("\nTotals:");
4587
- console.log(renderTable(["currency", ...numbers], report.totals));
4565
+ console.log(`No numbers from ${report.from} to ${report.to}.`);
4566
+ else {
4567
+ console.log(`${title}, ${report.from} to ${report.to}:`);
4568
+ console.log(renderTable([...labels, ...numbers], report.rows));
4569
+ console.log("\nTotals:");
4570
+ console.log(renderTable(["currency", ...numbers], report.totals));
4571
+ }
4572
+ for (const line of after)
4573
+ console.log(line);
4588
4574
  }
4589
4575
  const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
4590
4576
  const FACTS_PATH = "/api/v1/facts";
@@ -0,0 +1,57 @@
1
+ import { brand } from "./brand.js";
2
+ export const MONEY_NUMBERS = ["gross", "refunds", "net", "orders", "ad_spend", "affiliate_payouts", "left_after_ad_costs", "roas"];
3
+ export const CAMPAIGN_NUMBERS = [
4
+ "spend",
5
+ "leads_first_touch",
6
+ "leads_last_touch",
7
+ "booked_calls_first_touch",
8
+ "sales_first_touch",
9
+ "revenue_first_touch",
10
+ "revenue_last_touch",
11
+ "cost_per_lead_first_touch",
12
+ "roas_first_touch",
13
+ "roas_last_touch",
14
+ ];
15
+ const MONEY_IN_NUMBERS = new Set(["gross", "refunds", "net", "orders", "left_after_ad_costs", "roas"]);
16
+ const TRIPLE_WHALE_NUMBERS = ["orders_triple_whale", "revenue_triple_whale", "roas_triple_whale"];
17
+ const NO_CRM_SALES = "no payment tool, store or CRM sales are connected";
18
+ export function money(value, currency) {
19
+ try {
20
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value);
21
+ }
22
+ catch {
23
+ return `${value.toFixed(2)} ${currency}`;
24
+ }
25
+ }
26
+ /** A server older than this CLI sends no sales_from; its money in is daily_sales'. */
27
+ export function moneyLayout({ sales_from: from }) {
28
+ if (from === undefined || from.source === "daily_sales")
29
+ return { numbers: MONEY_NUMBERS, before: [], after: [] };
30
+ if (from.source === null)
31
+ return { numbers: MONEY_NUMBERS.filter((n) => !MONEY_IN_NUMBERS.has(n)), before: [from.why], after: [] };
32
+ return { numbers: MONEY_NUMBERS, before: [], after: [`\nMoney in is Triple Whale's ${from.detail}, because ${NO_CRM_SALES}.`] };
33
+ }
34
+ const blankEverywhere = (column, rows) => rows.every((row) => row[column] === null || row[column] === undefined);
35
+ /**
36
+ * Without daily_sales, the first- and last-touch columns blank in every row and total are left out, and with Triple
37
+ * Whale its own orders, revenue and return on ad spend are added.
38
+ */
39
+ export function campaignLayout(report) {
40
+ const from = report.sales_from;
41
+ if (from === undefined || from.source === "daily_sales")
42
+ return { numbers: CAMPAIGN_NUMBERS, before: [], after: [] };
43
+ const all = [...report.rows, ...report.totals];
44
+ const touches = CAMPAIGN_NUMBERS.filter((n) => n === "spend" || !blankEverywhere(n, all));
45
+ if (from.source === null)
46
+ return { numbers: touches, before: [from.why], after: [] };
47
+ const others = (report.triple_whale_other_campaigns ?? []).map(({ currency, orders, revenue }) => `Triple Whale credits ${orders} more ${orders === 1 ? "order" : "orders"}${revenue === null || currency === null ? "" : ` (${money(revenue, currency)})`} to Facebook ads that aren't in the ad accounts ${brand.name} is connected to, so they aren't in the rows above.`);
48
+ return {
49
+ numbers: [...touches, ...TRIPLE_WHALE_NUMBERS],
50
+ before: [],
51
+ after: [
52
+ `\nOrders and revenue are what Triple Whale credits to each Facebook campaign (${from.detail}), because ${NO_CRM_SALES}. ` +
53
+ "Triple Whale can credit one order to more than one ad, so these can add up to more than the store's own orders.",
54
+ ...others,
55
+ ],
56
+ };
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.149",
3
+ "version": "0.3.150",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {