@wrangled/cli 0.3.151 → 0.3.152
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 +1 -0
- package/dist/main.js +25 -4
- package/dist/unattributed-calls.js +17 -0
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -155,6 +155,7 @@ 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.
|
|
158
159
|
- \`${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
160
|
- 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
161
|
- 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,7 @@ 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 { renderUnattributedCalls } from "./unattributed-calls.js";
|
|
12
13
|
import { apiSetupPath, parseAuthFlag, parseQueryFlags, setupText, submittedText, testText, tryText, waitingText, } from "./api-setup.js";
|
|
13
14
|
import { answerRow, batches, ENRICH_EMAIL_USAGE, parsePeople, parseSteps } from "./enrich.js";
|
|
14
15
|
import { sendCopies, sendStatusChanges } from "./meta-progress.js";
|
|
@@ -577,6 +578,21 @@ const commands = {
|
|
|
577
578
|
printLeftOut(campaignResults.left_out, "the totals (the rows keep it)");
|
|
578
579
|
},
|
|
579
580
|
},
|
|
581
|
+
"calls unattributed": {
|
|
582
|
+
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)",
|
|
583
|
+
async run(args, options) {
|
|
584
|
+
if (args.length > 0)
|
|
585
|
+
throw new CliError("Usage: calls unattributed [--from <date>] [--to <date>] [--attribution first_touch|last_touch] [--limit <n>]", exitCodes.usage);
|
|
586
|
+
const touch = options.attribution === undefined ? undefined : CALL_TOUCHES.get(options.attribution);
|
|
587
|
+
if (options.attribution !== undefined && touch === undefined)
|
|
588
|
+
throw new CliError("--attribution is first_touch or last_touch.", exitCodes.usage);
|
|
589
|
+
const query = reportQuery(options, { touch, limit: options.limit });
|
|
590
|
+
const { unattributedCalls } = await api(await resolveConnection(options), `/api/v1/reports/unattributed-calls${query}`);
|
|
591
|
+
if (options.format === "json")
|
|
592
|
+
return printJson({ unattributedCalls });
|
|
593
|
+
process.stdout.write(renderUnattributedCalls(unattributedCalls));
|
|
594
|
+
},
|
|
595
|
+
},
|
|
580
596
|
"tools list": {
|
|
581
597
|
usage: "tools list",
|
|
582
598
|
async run(_args, options) {
|
|
@@ -4449,6 +4465,10 @@ function amountOption(flag, value) {
|
|
|
4449
4465
|
throw new CliError(`${flag} must be a number above 0, such as 10000.`, exitCodes.usage);
|
|
4450
4466
|
return n;
|
|
4451
4467
|
}
|
|
4468
|
+
const CALL_TOUCHES = new Map([
|
|
4469
|
+
["first_touch", "first"],
|
|
4470
|
+
["last_touch", "last"],
|
|
4471
|
+
]);
|
|
4452
4472
|
function reportQuery(options, extra) {
|
|
4453
4473
|
const query = new URLSearchParams();
|
|
4454
4474
|
for (const [name, value] of Object.entries({ from: options.from, to: options.to, ...extra }))
|
|
@@ -5039,8 +5059,8 @@ Options:
|
|
|
5039
5059
|
--where <json | @file.json> tables delete-rows: {column: value} pairs a row must all match to be deleted
|
|
5040
5060
|
--mode append|replace tables import: add the file's rows (default) or replace every row the table has
|
|
5041
5061
|
--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);
|
|
5062
|
+
--from <YYYY-MM-DD> first day funnel, campaigns, money, calls unattributed, metrics query, brief, pages results and messages results cover (default: 29 days before --to)
|
|
5063
|
+
--to <YYYY-MM-DD> last day funnel, campaigns, money, calls unattributed, metrics query, brief, pages results and messages results cover (default: today in the workspace reporting time zone);
|
|
5044
5064
|
for outbound push, where the leads go: instantly:<campaign-id>
|
|
5045
5065
|
--grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
|
|
5046
5066
|
--month <YYYY-MM> the month tools usage covers, in the workspace reporting time zone (default: this month)
|
|
@@ -5050,7 +5070,8 @@ Options:
|
|
|
5050
5070
|
Facebook ad, ad set or ad name (ad, adset, creative), or by a confirmed custom label (metrics custom list); repeat for
|
|
5051
5071
|
several
|
|
5052
5072
|
--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
|
|
5073
|
+
--attribution first_touch|last_touch metrics query and feeds create: which ad touch gets the credit for leads and sales by channel or campaign;
|
|
5074
|
+
calls unattributed: which touch credits each booked call
|
|
5054
5075
|
--last-days <n> feeds create: how many days the sheet link covers, ending today and counting it (default 30, at most 400)
|
|
5055
5076
|
(default: first_touch)
|
|
5056
5077
|
--channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
|
|
@@ -5176,7 +5197,7 @@ Options:
|
|
|
5176
5197
|
first; repeat for several
|
|
5177
5198
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
5178
5199
|
--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,
|
|
5200
|
+
--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
5201
|
content to-check and seo page-two return, how many ads ads autotag tags and how many items content autotag
|
|
5181
5202
|
tags, or how many ads each ads tag-formats batch looks at (default 2000; seo page-two: 50)
|
|
5182
5203
|
--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
|
+
}
|