@wrangled/cli 0.3.151 → 0.3.153

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.
@@ -0,0 +1,20 @@
1
+ import { renderTable } from "./table.js";
2
+ const COLUMNS = ["ad_id", "ad_name", "meta", "first_touch", "first_delta", "last_touch", "last_delta"];
3
+ const count = (n) => (n === null ? "n/a" : String(n));
4
+ const signed = (n) => (n !== null && n > 0 ? `+${n}` : count(n));
5
+ function counted(report) {
6
+ if (report.event !== null)
7
+ return `Meta's ${report.event} events`;
8
+ if (report.result_types.length === 0)
9
+ return "Meta's results";
10
+ return `Meta's results (${report.result_types.join(", ")})`;
11
+ }
12
+ export function renderAdCalls(report) {
13
+ const header = `Calls per Facebook ad ${report.from} to ${report.to}: ${counted(report)} against booked calls credited to the first and last touch. Deltas are booked minus Meta.`;
14
+ if (report.ads.length === 0)
15
+ return `${header}\nNo ad has calls in either source.\n`;
16
+ const row = (c) => ({ meta: count(c.meta_calls), first_touch: count(c.first_touch), first_delta: signed(c.first_touch_delta), last_touch: count(c.last_touch), last_delta: signed(c.last_touch_delta) });
17
+ const table = renderTable(COLUMNS, [...report.ads.map((a) => ({ ad_id: a.ad_id, ad_name: a.ad_name, ...row(a) })), { ad_id: "total", ...row(report.totals) }]);
18
+ const unconnected = report.booked_calls_connected ? "" : "No booked-call source is connected, so booked calls and deltas are n/a.\n";
19
+ return `${header}\n${table}\n${unconnected}`;
20
+ }
package/dist/guide.js CHANGED
@@ -155,6 +155,8 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
155
155
 
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
+ - \`${cli} calls unattributed [--attribution first_touch|last_touch] [--limit <n>]\` answers "why do these booked calls have no ad?": it counts the booked calls in the range (by day booked), how many were credited to a Facebook ad and how many were not, as \`ad_results_daily\` credits them (to the first touch by default), splits the calls with no ad by why (\`no_person\`: the booking's contact has no email or phone to match a person; \`meta_without_ad\`: a Facebook visit whose link named no single ad; \`not_meta\`: another channel; \`visit_after_call\`: the visits are dated after the booking; \`last_visit_only\`: only a latest visit, no first; \`no_visit\`: no tagged visit at all), then lists those calls newest first with the booking's contact id, the person id and the touch's channel, campaign, ad and landing page. With and without add up to every booked call. \`--limit\` is 1 to 1000 (default 200); the counts always cover every call. The rows are in the \`booked_call_credits\` view; join \`touches\` on \`person_id\` to see each visit's tags.
159
+ - \`${cli} calls compare [--event <event>]\` answers "do Meta's call numbers match the booked calls?": per Facebook ad over the range, the calls Meta reports (its results, the workspace's chosen result type as Ads Manager shows it, or with \`--event\` that one Meta action type, such as schedule_website), the booked calls \`ad_results_daily\` credits to the ad on the first touch and on the last touch, and each delta, booked minus Meta (negative when Meta reports more). An ad missing from one source counts 0 there. Most Meta calls first, then a total row; \`--format json\` gives the same numbers.
158
160
  - \`${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
161
  - 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.
160
162
  - 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.
package/dist/main.js CHANGED
@@ -9,6 +9,8 @@ import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, ge
9
9
  import { brand, envPrefix } from "./brand.js";
10
10
  import { campaignLayout, money, moneyLayout } from "./report-columns.js";
11
11
  import { renderDataMap, renderQuestions } from "./map.js";
12
+ import { renderAdCalls } from "./ad-calls.js";
13
+ import { renderUnattributedCalls } from "./unattributed-calls.js";
12
14
  import { apiSetupPath, parseAuthFlag, parseQueryFlags, setupText, submittedText, testText, tryText, waitingText, } from "./api-setup.js";
13
15
  import { answerRow, batches, ENRICH_EMAIL_USAGE, parsePeople, parseSteps } from "./enrich.js";
14
16
  import { sendCopies, sendStatusChanges } from "./meta-progress.js";
@@ -577,6 +579,33 @@ const commands = {
577
579
  printLeftOut(campaignResults.left_out, "the totals (the rows keep it)");
578
580
  },
579
581
  },
582
+ "calls unattributed": {
583
+ usage: "calls unattributed [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--attribution first_touch|last_touch] [--limit <n>] (booked calls credited to no Facebook ad, and why each one has none)",
584
+ async run(args, options) {
585
+ if (args.length > 0)
586
+ throw new CliError("Usage: calls unattributed [--from <date>] [--to <date>] [--attribution first_touch|last_touch] [--limit <n>]", exitCodes.usage);
587
+ const touch = options.attribution === undefined ? undefined : CALL_TOUCHES.get(options.attribution);
588
+ if (options.attribution !== undefined && touch === undefined)
589
+ throw new CliError("--attribution is first_touch or last_touch.", exitCodes.usage);
590
+ const query = reportQuery(options, { touch, limit: options.limit });
591
+ const { unattributedCalls } = await api(await resolveConnection(options), `/api/v1/reports/unattributed-calls${query}`);
592
+ if (options.format === "json")
593
+ return printJson({ unattributedCalls });
594
+ process.stdout.write(renderUnattributedCalls(unattributedCalls));
595
+ },
596
+ },
597
+ "calls compare": {
598
+ usage: "calls compare [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--event <event>] (calls per Facebook ad: what Meta reports next to the booked calls credited on the first and last touch)",
599
+ async run(args, options) {
600
+ if (args.length > 0)
601
+ throw new CliError("Usage: calls compare [--from <date>] [--to <date>] [--event <event>]", exitCodes.usage);
602
+ const query = reportQuery(options, { event: options.event });
603
+ const { adCalls } = await api(await resolveConnection(options), `/api/v1/reports/ad-calls${query}`);
604
+ if (options.format === "json")
605
+ return printJson({ adCalls });
606
+ process.stdout.write(renderAdCalls(adCalls));
607
+ },
608
+ },
580
609
  "tools list": {
581
610
  usage: "tools list",
582
611
  async run(_args, options) {
@@ -4449,6 +4478,10 @@ function amountOption(flag, value) {
4449
4478
  throw new CliError(`${flag} must be a number above 0, such as 10000.`, exitCodes.usage);
4450
4479
  return n;
4451
4480
  }
4481
+ const CALL_TOUCHES = new Map([
4482
+ ["first_touch", "first"],
4483
+ ["last_touch", "last"],
4484
+ ]);
4452
4485
  function reportQuery(options, extra) {
4453
4486
  const query = new URLSearchParams();
4454
4487
  for (const [name, value] of Object.entries({ from: options.from, to: options.to, ...extra }))
@@ -5039,8 +5072,8 @@ Options:
5039
5072
  --where <json | @file.json> tables delete-rows: {column: value} pairs a row must all match to be deleted
5040
5073
  --mode append|replace tables import: add the file's rows (default) or replace every row the table has
5041
5074
  --yes confirm intake delete, import remove, tables drop, content collections remove or remove-item, which delete saved data for good
5042
- --from <YYYY-MM-DD> first day funnel, campaigns, money, metrics query, brief, pages results and messages results cover (default: 29 days before --to)
5043
- --to <YYYY-MM-DD> last day funnel, campaigns, money, metrics query, brief, pages results and messages results cover (default: today in the workspace reporting time zone);
5075
+ --from <YYYY-MM-DD> first day funnel, campaigns, money, calls unattributed, calls compare, metrics query, brief, pages results and messages results cover (default: 29 days before --to)
5076
+ --to <YYYY-MM-DD> last day funnel, campaigns, money, calls unattributed, calls compare, metrics query, brief, pages results and messages results cover (default: today in the workspace reporting time zone);
5044
5077
  for outbound push, where the leads go: instantly:<campaign-id>
5045
5078
  --grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
5046
5079
  --month <YYYY-MM> the month tools usage covers, in the workspace reporting time zone (default: this month)
@@ -5050,7 +5083,8 @@ Options:
5050
5083
  Facebook ad, ad set or ad name (ad, adset, creative), or by a confirmed custom label (metrics custom list); repeat for
5051
5084
  several
5052
5085
  --where <dim>=<v,v> metrics query and feeds create: only rows with one of these values; <dim>!=<v,v> keeps the rest; repeat for several
5053
- --attribution first_touch|last_touch metrics query and feeds create: which ad touch gets the credit for leads and sales by channel or campaign
5086
+ --attribution first_touch|last_touch metrics query and feeds create: which ad touch gets the credit for leads and sales by channel or campaign;
5087
+ calls unattributed: which touch credits each booked call
5054
5088
  --last-days <n> feeds create: how many days the sheet link covers, ending today and counting it (default 30, at most 400)
5055
5089
  (default: first_touch)
5056
5090
  --channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
@@ -5114,6 +5148,7 @@ Options:
5114
5148
  --pixel <id> meta audience create --kind website: the pixel that saw the visits
5115
5149
  --url-contains <text> meta audience create --kind website: only visits to pages whose address contains this
5116
5150
  --event <event> meta audience create --kind website: only visits that fired this pixel event, such as Purchase or Lead
5151
+ calls compare: count the Meta action type <event>, such as schedule_website, as Meta's calls instead of its results
5117
5152
  --video <id> meta audience create --kind video: a video watched; repeat for several
5118
5153
  --watched <how much> meta audience create --kind video: 3s, 10s, thruplay, 25%, 50%, 75% or 95%
5119
5154
  --page <id> meta audience create --kind page: the Facebook Page
@@ -5176,7 +5211,7 @@ Options:
5176
5211
  first; repeat for several
5177
5212
  --order desc|asc content search's sort direction (default: desc, best or newest first)
5178
5213
  --offset <n> api setup, api try: where in the guide or the answer the slice starts, in characters
5179
- --limit <n> api setup, api try: how many characters of the guide (default 20,000) or the answer (default 65,536); how many results content search, creatives list, media list, campaigns, voice, ads tags, content tags,
5214
+ --limit <n> api setup, api try: how many characters of the guide (default 20,000) or the answer (default 65,536); how many results content search, creatives list, media list, campaigns, calls unattributed (default 200, at most 1000), voice, ads tags, content tags,
5180
5215
  content to-check and seo page-two return, how many ads ads autotag tags and how many items content autotag
5181
5216
  tags, or how many ads each ads tag-formats batch looks at (default 2000; seo page-two: 50)
5182
5217
  --library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
@@ -0,0 +1,17 @@
1
+ import { renderTable } from "./table.js";
2
+ const CALL_COLUMNS = ["booked_on", "started_on", "credit", "record_source", "contact_id", "outcome", "channel", "campaign_id", "ad_id", "landing_page_key"];
3
+ export function renderUnattributedCalls(report) {
4
+ const range = `${report.from} to ${report.to}`;
5
+ if (report.booked_calls === null)
6
+ return `No booked-call source is connected, so there are no booked calls from ${range}.\n`;
7
+ const lines = [
8
+ `Booked calls ${range}, credited to the ${report.touch} touch: ${report.booked_calls} in all, ${report.with_ad} with a Facebook ad, ${report.without_ad} without one.`,
9
+ ];
10
+ if (report.without_ad === 0)
11
+ return `${lines[0]}\n`;
12
+ lines.push("", "Why calls have no ad:", renderTable(["credit", "calls"], report.by_credit.filter((c) => c.calls > 0)));
13
+ lines.push("", "Calls with no ad, newest first:", renderTable(CALL_COLUMNS, report.calls));
14
+ if (report.truncated)
15
+ lines.push(`Showing the newest ${report.calls.length} of ${report.without_ad}; --limit shows up to 1000.`);
16
+ return `${lines.join("\n")}\n`;
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.151",
3
+ "version": "0.3.153",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {