@wrangled/cli 0.3.38 → 0.3.40
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/api.js +2 -0
- package/dist/guide.js +4 -3
- package/dist/main.js +67 -5
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -21,6 +21,8 @@ export class UnreadableCredentialsError extends CliError {
|
|
|
21
21
|
* in src/lib/knowledge/tags.ts. format's flag is --ad-format, because --format picks the output.
|
|
22
22
|
*/
|
|
23
23
|
export const entryTagFlags = { angle: "angle", hook_kind: "hook-kind", format: "ad-format", awareness: "awareness" };
|
|
24
|
+
/** The days before a sale in which a click or reply helps it, as the app counts them (MESSAGE_CREDIT_DAYS). */
|
|
25
|
+
export const MESSAGE_CREDIT_DAYS = 7;
|
|
24
26
|
/** Copies of the server's IMPORT_LIMITS and import name rule; src/lib/cli-consistency.test.ts keeps them in step. */
|
|
25
27
|
export const IMPORT_LIMITS = { bytes: 50 * 1024 * 1024, rows: 1_000_000, columns: 500 };
|
|
26
28
|
export const IMPORT_NAME_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
package/dist/guide.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IMPORT_LIMITS } from "./api.js";
|
|
1
|
+
import { IMPORT_LIMITS, MESSAGE_CREDIT_DAYS } from "./api.js";
|
|
2
2
|
import { brand, envPrefix } from "./brand.js";
|
|
3
3
|
import { BACK_TAG_SKILL_NAME, CRO_SKILL_NAME, GRAMMAR_SKILL_NAME, LEARN_NAMES_SKILL_NAME, LESSONS_SKILL_NAME, MAP_OFFERS_SKILL_NAME, PLAYBOOK_CHOICES, STORY_SKILL_NAME, WEEKLY_LOOP_SKILL_NAME } from "./playbooks.js";
|
|
4
4
|
import { PULL_STATE_FILE } from "./pull.js";
|
|
@@ -113,7 +113,7 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
|
|
|
113
113
|
|
|
114
114
|
## Webhook addresses
|
|
115
115
|
|
|
116
|
-
A webhook address lets a tool such as a form or webinar platform push events into the warehouse's \`raw_events\` table. \`${cli} intake list\` shows each address with whether it is on, how many days it keeps events, the events of the last 24 hours, the last event's time, and the rows it holds. \`${cli} intake create <label>\` makes one and prints its link, address and token. The link is the one URL to paste into the sending tool: the address with the token on the end, or for Mailgun the bare address. Add \`--kind ghl_workflow\` for a GoHighLevel workflow's Webhook step: its events also land, typed, in \`ghl_events\`. Add \`--kind aloware\` for Aloware's call and text webhooks: they also land, typed, in \`aloware_calls\` and \`aloware_texts\`. Add \`--kind webinarfuel\` or \`--kind stealth_seminar\` for a webinar tool: its sign-ups, attendance, replays and offer clicks land in \`webinar_events\`. Add \`--kind mailgun --signing-key-stdin\`, with Mailgun's HTTP webhook signing key piped on stdin, for a Mailgun account's webhooks: each event's signature is checked and alone proves the sender, so Mailgun posts to the address itself with no token, and its events also land, typed, in \`mailgun_events\`. When the account's key changes, pipe the new one to \`${cli} intake signing-key <id> --signing-key-stdin\`; deleting the workspace's last Mailgun address deletes the key. Add \`--kind thrivecart_events\` for a ThriveCart account's webhook: affiliate commissions earned, taken back and paid out also land, typed, in \`thrivecart_affiliate_commissions\`, where they stay after their events pass the retention, and \`daily_spend\` counts them. The link and token are shown only then and after \`${cli} intake rotate <id>\`. Paste the link into the sending tool, or, when the tool can set a header, give it the address and \`Authorization: Bearer <token>\` instead, which keeps the token out of request logs. Never paste either anywhere else. Events are kept for 90 days unless \`--retention-days\` or \`${cli} intake retention <id> <days>\` says otherwise (1 to 3650). \`${cli} intake delete <id> --yes\` deletes the address and every event it saved. A website visit tag shows in \`${cli} intake list\` as an address of kind \`visit_tag\`, and each page view it records lands in \`site_visits\` (host, path, referrer host, UTM tags, click ids, country, device type, a bot flag and a random visitor id). \`${cli} intake create\` cannot make one: a tag has no token and needs its list of allowed sites, so an operator makes it with the server's \`npm run visit-tag:create\` script.
|
|
116
|
+
A webhook address lets a tool such as a form or webinar platform push events into the warehouse's \`raw_events\` table. \`${cli} intake list\` shows each address with whether it is on, how many days it keeps events, the events of the last 24 hours, the last event's time, and the rows it holds. \`${cli} intake create <label>\` makes one and prints its link, address and token. The link is the one URL to paste into the sending tool: the address with the token on the end, or for Mailgun the bare address. Add \`--kind ghl_workflow\` for a GoHighLevel workflow's Webhook step: its events also land, typed, in \`ghl_events\`. Add \`--kind aloware\` for Aloware's call and text webhooks: they also land, typed, in \`aloware_calls\` and \`aloware_texts\`. Add \`--kind webinarfuel\` or \`--kind stealth_seminar\` for a webinar tool: its sign-ups, attendance, replays and offer clicks land in \`webinar_events\`. Add \`--kind mailgun --signing-key-stdin\`, with Mailgun's HTTP webhook signing key piped on stdin, for a Mailgun account's webhooks: each event's signature is checked and alone proves the sender, so Mailgun posts to the address itself with no token, and its events also land, typed, in \`mailgun_events\`. When the account's key changes, pipe the new one to \`${cli} intake signing-key <id> --signing-key-stdin\`; deleting the workspace's last Mailgun address deletes the key. Add \`--kind thrivecart_events\` for a ThriveCart account's webhook: affiliate commissions earned, taken back and paid out also land, typed, in \`thrivecart_affiliate_commissions\`, where they stay after their events pass the retention, and \`daily_spend\` counts them. Add \`--kind roezan\` once the workspace's Roezan source is connected: the server uses the source's key to ask Roezan to send each new broadcast to the link (\`subscription\` in the answer says whether that worked; if not, add the link in Roezan's Webhooks tab), each broadcast an event names lands in \`roezan_broadcast_events\`, and the Roezan source reads it on its next hourly sync. The link and token are shown only then and after \`${cli} intake rotate <id>\`. Paste the link into the sending tool, or, when the tool can set a header, give it the address and \`Authorization: Bearer <token>\` instead, which keeps the token out of request logs. Never paste either anywhere else. Events are kept for 90 days unless \`--retention-days\` or \`${cli} intake retention <id> <days>\` says otherwise (1 to 3650). \`${cli} intake delete <id> --yes\` deletes the address and every event it saved. A website visit tag shows in \`${cli} intake list\` as an address of kind \`visit_tag\`, and each page view it records lands in \`site_visits\` (host, path, referrer host, UTM tags, click ids, country, device type, a bot flag and a random visitor id). \`${cli} intake create\` cannot make one: a tag has no token and needs its list of allowed sites, so an operator makes it with the server's \`npm run visit-tag:create\` script.
|
|
117
117
|
|
|
118
118
|
## Limits
|
|
119
119
|
|
|
@@ -298,6 +298,7 @@ ${cli} guide what this workspace's data holds, how fresh
|
|
|
298
298
|
${cli} brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>] the offer, voice, best and worst ads, pages, results and audience: start here before writing ads
|
|
299
299
|
${cli} pages brief --step <key> [--funnel <key>] | --page <ref> the offer, step, current page, best and worst pages of its kind, its ads and how to hand a page back: start here before writing a landing page
|
|
300
300
|
${cli} pages results [--from <date>] [--to <date>] [--page <ref>] each landing page's, and each ad and page pair's, real first-touch sales, cost per sale and sales per page view
|
|
301
|
+
${cli} messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <id>] real sales after each follow-up email, text and sequence: every message the buyer clicked or replied to in the ${MESSAGE_CREDIT_DAYS} days before buying (helped), and the latest one (last nudge)
|
|
301
302
|
${cli} winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>] which Facebook ads and landing pages won and flopped in a week or any dates and over 30 days, judged on first-touch sales
|
|
302
303
|
${cli} winners send-to-exodus [--week <date> | --days <n> | --from <date> --to <date>] [--dry-run] send the winners, with their reasons, lessons and customer words, to Exodus; safe to send again
|
|
303
304
|
${cli} warehouse schema [--summary] [--table <name> ...] [--search <words>] list tables and columns; --summary for one row per table
|
|
@@ -309,7 +310,7 @@ ${cli} sources list list data sources and their health
|
|
|
309
310
|
${cli} sources show <id> show one source's health, tables and last sync runs
|
|
310
311
|
${cli} sources settings <id> [<key>=<value> ...] show a key source's settings, or check them with its saved key and save them
|
|
311
312
|
${cli} intake list list webhook addresses and their health
|
|
312
|
-
${cli} intake create <label> [--kind generic|ghl_workflow|aloware|webinarfuel|stealth_seminar|mailgun|thrivecart_events] [--retention-days <n>] [--signing-key-stdin] make a webhook address; prints its link and token once
|
|
313
|
+
${cli} intake create <label> [--kind generic|ghl_workflow|aloware|webinarfuel|stealth_seminar|mailgun|thrivecart_events|roezan] [--retention-days <n>] [--signing-key-stdin] make a webhook address; prints its link and token once
|
|
313
314
|
${cli} intake rotate <id> replace an address's token; prints the new link and token once
|
|
314
315
|
${cli} intake signing-key <id> --signing-key-stdin replace the workspace's signing key for that address's kind (mailgun)
|
|
315
316
|
${cli} intake disable <id> | enable <id> turn an address off or back on
|
package/dist/main.js
CHANGED
|
@@ -669,6 +669,17 @@ const commands = {
|
|
|
669
669
|
printPageResults(results);
|
|
670
670
|
},
|
|
671
671
|
},
|
|
672
|
+
"messages results": {
|
|
673
|
+
usage: "messages results [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--channel email|text] [--sequence <workflow id>] (which follow-up emails, texts and sequences bring real sales)",
|
|
674
|
+
async run(args, options) {
|
|
675
|
+
if (args.length > 0)
|
|
676
|
+
throw new CliError("Usage: messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <workflow id>]", exitCodes.usage);
|
|
677
|
+
const results = await api(await resolveConnection(options.apiUrl), `/api/v1/messages/results${reportQuery(options, { channel: options.channel, sequence: options.sequence })}`);
|
|
678
|
+
if (options.format === "json")
|
|
679
|
+
return printJson(results);
|
|
680
|
+
printMessageResults(results);
|
|
681
|
+
},
|
|
682
|
+
},
|
|
672
683
|
winners: {
|
|
673
684
|
usage: "winners [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--top <n>] (which Facebook ads and landing pages won and flopped, judged on real first-touch sales)",
|
|
674
685
|
async run(args, options) {
|
|
@@ -2170,6 +2181,51 @@ function printPageResults(results) {
|
|
|
2170
2181
|
console.log(renderTable(["page", "ad", "ad name", "currency", ...PAGE_NUMBERS], results.adPages.map((r) => ({ ...pageNumbers(r), ad: r.adId ?? "(no Meta ad)", "ad name": r.adName === null ? "" : excerpt(r.adName, 30) }))));
|
|
2171
2182
|
console.log('\n"by ad link" counts sales whose buyer\'s landing page was not recorded, credited to the page the ad links to now.');
|
|
2172
2183
|
}
|
|
2184
|
+
const MESSAGE_NUMBERS = ["currency", "helped", "helped revenue", "last nudge", "last nudge revenue", "first from Facebook"];
|
|
2185
|
+
/** One row per currency the sales are in, or one row with no sales. */
|
|
2186
|
+
function salesRows(label, sales) {
|
|
2187
|
+
if (sales.length === 0)
|
|
2188
|
+
return [{ ...label, helped: 0, "last nudge": 0 }];
|
|
2189
|
+
return sales.map((s) => ({
|
|
2190
|
+
...label,
|
|
2191
|
+
currency: s.currency,
|
|
2192
|
+
helped: s.salesHelped,
|
|
2193
|
+
"helped revenue": s.revenueHelped,
|
|
2194
|
+
"last nudge": s.salesLastNudge,
|
|
2195
|
+
"last nudge revenue": s.revenueLastNudge,
|
|
2196
|
+
"first from Facebook": s.buyersFirstTouchMeta,
|
|
2197
|
+
}));
|
|
2198
|
+
}
|
|
2199
|
+
const messageLabel = (m) => excerpt(m.name ?? m.subject ?? (m.realSales === "available" ? m.messageKey : (m.itemId ?? "")), 40);
|
|
2200
|
+
function printMessageResults(results) {
|
|
2201
|
+
const real = results.messages.flatMap((m) => (m.realSales === "available" ? [m] : []));
|
|
2202
|
+
const claimed = results.messages.flatMap((m) => (m.realSales === "not_available" ? [m] : []));
|
|
2203
|
+
if (results.messages.length === 0 && results.sequences.length === 0)
|
|
2204
|
+
return console.log(`No follow-up results from ${results.from} to ${results.to}.`);
|
|
2205
|
+
console.log(`Follow-up emails and texts, most helped sales first, ${results.from} to ${results.to}:`);
|
|
2206
|
+
console.log(renderTable(["message", "channel", "platform", "sent", "clicked", "replied", ...MESSAGE_NUMBERS, "per 1,000 sent"], real.flatMap((m) => salesRows({ message: messageLabel(m), channel: m.channel, platform: m.platform, sent: m.peopleSent ?? (m.toolNumbersInRange === "unknown" ? "unknown" : m.toolSends), clicked: m.peopleClicked, replied: m.peopleReplied, "per 1,000 sent": m.salesPer1000Sent }, m.sales))));
|
|
2207
|
+
if (results.sequences.length > 0) {
|
|
2208
|
+
console.log("\nSequences:");
|
|
2209
|
+
console.log(renderTable(["sequence", "name", "messages", ...MESSAGE_NUMBERS], results.sequences.flatMap((q) => salesRows({ sequence: q.sequenceKey, name: q.name ?? "", messages: q.messages }, q.sales))));
|
|
2210
|
+
}
|
|
2211
|
+
if (claimed.length > 0) {
|
|
2212
|
+
console.log("\nFrom tools that never say who clicked, so no real sales; revenue is the tool's own claim:");
|
|
2213
|
+
console.log(renderTable(["message", "channel", "platform", "sends", "clicks", "tool-claimed revenue"], claimed.map((m) => m.toolNumbersInRange === "unknown"
|
|
2214
|
+
? { message: messageLabel(m), channel: m.channel, platform: m.platform, sends: "unknown", clicks: "unknown", "tool-claimed revenue": "unknown" }
|
|
2215
|
+
: {
|
|
2216
|
+
message: messageLabel(m),
|
|
2217
|
+
channel: m.channel,
|
|
2218
|
+
platform: m.platform,
|
|
2219
|
+
sends: m.toolSends,
|
|
2220
|
+
clicks: m.toolClicks,
|
|
2221
|
+
"tool-claimed revenue": (m.toolClaimedRevenue ?? []).map((r) => `${r.revenue} ${r.currency ?? ""}`.trim()).join(", "),
|
|
2222
|
+
})));
|
|
2223
|
+
}
|
|
2224
|
+
if (results.messages.some((m) => m.toolNumbersInRange === "unknown")) {
|
|
2225
|
+
console.log("\nunknown: the tool keeps only running totals, and none was read before these dates for a message sent earlier, so how much fell in them cannot be told.");
|
|
2226
|
+
}
|
|
2227
|
+
console.log(`\n"helped" counts every message the buyer clicked or replied to in the ${results.creditDays} days before buying, so one sale counts for several messages; "last nudge" gives each sale only to the latest one, so those add up.`);
|
|
2228
|
+
}
|
|
2173
2229
|
/** The rows, then the totals, which are one row per currency. */
|
|
2174
2230
|
/** What confirmed leave-out rules took out of `figures`, one line per rule and currency. */
|
|
2175
2231
|
function printLeftOut(leftOut, figures) {
|
|
@@ -2385,11 +2441,13 @@ Options:
|
|
|
2385
2441
|
--force content unpublish: take the page down even while delivering Meta ads link to it
|
|
2386
2442
|
--plan <json> meta adset publish: the ad set to make, {"kind": "copy", ...} or {"kind": "create", ...}
|
|
2387
2443
|
--yes confirm intake delete or import remove, which delete saved data for good
|
|
2388
|
-
--from <YYYY-MM-DD> first day funnel, campaigns, money, brief and
|
|
2389
|
-
--to <YYYY-MM-DD> last day funnel, campaigns, money, brief and
|
|
2444
|
+
--from <YYYY-MM-DD> first day funnel, campaigns, money, brief, pages results and messages results cover (default: 29 days before --to)
|
|
2445
|
+
--to <YYYY-MM-DD> last day funnel, campaigns, money, brief, pages results and messages results cover (default: today in the workspace reporting time zone)
|
|
2390
2446
|
--grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
|
|
2391
2447
|
--channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
|
|
2392
|
-
for content track-link, where the link goes: facebook, email, sms or web (default)
|
|
2448
|
+
for content track-link, where the link goes: facebook, email, sms or web (default);
|
|
2449
|
+
with messages results, email or text
|
|
2450
|
+
--sequence <id> with messages results, only the messages in this GoHighLevel workflow, and only that sequence
|
|
2393
2451
|
--campaign <id> the campaign content track-link puts in utm_campaign; with brief, the one campaign it covers
|
|
2394
2452
|
--top <n> how many best ads, worst ads, pages and campaigns brief shows, or ads in each winners list (default: 5, at most 20)
|
|
2395
2453
|
--week <YYYY-MM-DD> any day of the week winners covers, Monday to Sunday (default: the latest complete week)
|
|
@@ -2544,6 +2602,7 @@ function parseOptions(argv) {
|
|
|
2544
2602
|
funnel: { type: "string" },
|
|
2545
2603
|
step: { type: "string" },
|
|
2546
2604
|
page: { type: "string" },
|
|
2605
|
+
sequence: { type: "string" },
|
|
2547
2606
|
workspace: { type: "string" },
|
|
2548
2607
|
"max-seconds": { type: "string" },
|
|
2549
2608
|
angle: { type: "string" },
|
|
@@ -2653,6 +2712,7 @@ function parseOptions(argv) {
|
|
|
2653
2712
|
funnel: values.funnel,
|
|
2654
2713
|
step: values.step,
|
|
2655
2714
|
page: values.page,
|
|
2715
|
+
sequence: values.sequence,
|
|
2656
2716
|
workspace: values.workspace,
|
|
2657
2717
|
maxSeconds,
|
|
2658
2718
|
entryTags: Object.fromEntries(Object.entries(entryTagFlags).flatMap(([field, flag]) => (values[flag] === undefined ? [] : [[field, values[flag]]]))),
|
|
@@ -3400,15 +3460,17 @@ function parseRetentionDays(text) {
|
|
|
3400
3460
|
throw new CliError("Retention must be a whole number of days, at least 1.", exitCodes.usage);
|
|
3401
3461
|
return days;
|
|
3402
3462
|
}
|
|
3403
|
-
function printIntakeToken({ endpoint, address, token, link }, format) {
|
|
3463
|
+
function printIntakeToken({ endpoint, address, token, link, subscription }, format) {
|
|
3404
3464
|
if (format === "json")
|
|
3405
|
-
printJson({ endpoint, address, token, link });
|
|
3465
|
+
printJson({ endpoint, address, token, link, ...(subscription ? { subscription } : {}) });
|
|
3406
3466
|
else {
|
|
3407
3467
|
if (link)
|
|
3408
3468
|
console.log(`Link: ${link}`);
|
|
3409
3469
|
console.log(`Address: ${address}`);
|
|
3410
3470
|
console.log(`Header: Authorization: Bearer ${token}`);
|
|
3411
3471
|
console.log(`Token: ${token}`);
|
|
3472
|
+
if (subscription)
|
|
3473
|
+
console.log(`Setup: ${subscription.message}`);
|
|
3412
3474
|
}
|
|
3413
3475
|
console.error(`The token for ${endpoint.label} is shown only now. Rotate it if it is lost.`);
|
|
3414
3476
|
}
|