@wrangled/cli 0.3.153 → 0.3.155
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 +2 -2
- package/dist/main.js +10 -8
- package/dist/playbooks.js +2 -2
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -86,7 +86,7 @@ ${brand.name} writes and sends no emails or texts; the client's own tool (GoHigh
|
|
|
86
86
|
|
|
87
87
|
## Which Facebook ads and pages won: winners and flops
|
|
88
88
|
|
|
89
|
-
\`${cli} winners\` says which Facebook ads won and which flopped, for the dates asked for and for the 30 days ending on their last day, judged on the client's goal. Each client has an ordered list of goals, and winners uses the first one whose data is connected: \`sales\` (first-touch sales from every connected payment source: each sale goes to the Facebook ad that first brought the buyer in, and payments typed into GoHighLevel by hand never count toward an ad), \`sales:triple_whale\` (the orders Triple Whale credits to each ad), \`booked_calls\` (calls booked by someone whose first touch was the ad), \`meta_results\` (Facebook's own count of the result types set on the Meta source, such as applications, \`submit_application_website\`) or \`first_leads\`. With no goals set, a client whose Meta sources count scheduling as their result (such as \`schedule_website\`) defaults to booked calls, then first leads; every other client defaults to Triple Whale's orders, then first-touch sales, then first leads, which is how winners judged before goals could be set. \`${cli} winners goals\` shows the list, whether each goal has data and which one is used now; \`${cli} winners goals set booked_calls first_leads\` sets it, and \`${cli} winners goals reset\` goes back to the default. To judge on Stripe sales, connect Stripe, then put \`sales\` first. The report starts with a sentence saying which goal it judged on, and in the JSON every "sales" field (\`numbers.sales\`, \`costPerSale\`, \`accountCostPerSale\`, \`adSales\`, \`judgedOn: "sales"\`) holds that goal, named in \`goal\`, and every "signups" field the fallback goal, named in \`fallbackGoal\`. An ad with none of the goal that has spent less than twice the account's cost per one is too new to judge on it, so it is judged on the fallback goal, the next connected goal of another kind, and listed apart. Landing pages and ad-and-page pairs are judged on the same goal when it is \`sales\`, \`booked_calls\` or \`first_leads\`, and on first-touch sales otherwise, with first leads for pages too new to judge; \`pagesGoal\` and \`pagesFallbackGoal\` name them in the JSON. Each ad says in a sentence why it is on its list, then its headline and primary text in full (\`headline\` and \`primaryText\` in the JSON), so you need no SQL for its words. When a currency's ads run in two or more Facebook ad accounts, each ad is judged against its own account's cost per sale, and the section says so and lists each account (\`adAccounts\` in the JSON, and \`accountId\` on each ad). Each currency also says how many of its Facebook sales could be tied to a specific ad (\`adSales\` of \`facebookSales\` in the JSON): a sale whose link names no single ad counts toward Facebook but toward no ad. It judges the landing pages the ads sent people to the same way, and each ad on each page as an ad-and-page pair: a page or pair counts only once the page reader has read the page and it got at least 100 visits in the dates, and pairs are listed only for an ad that sent people to two or more pages, or a page that got people from two or more ads, each naming what the same ad cost on its other pages. \`--week <YYYY-MM-DD>\` picks any day of a Monday-to-Sunday week (default: the latest complete week), \`--days <n>\` the last n
|
|
89
|
+
\`${cli} winners\` says which Facebook ads won and which flopped, for the dates asked for and for the 30 days ending on their last day, judged on the client's goal. Each client has an ordered list of goals, and winners uses the first one whose data is connected: \`sales\` (first-touch sales from every connected payment source: each sale goes to the Facebook ad that first brought the buyer in, and payments typed into GoHighLevel by hand never count toward an ad), \`sales:triple_whale\` (the orders Triple Whale credits to each ad), \`booked_calls\` (calls booked by someone whose first touch was the ad), \`meta_results\` (Facebook's own count of the result types set on the Meta source, such as applications, \`submit_application_website\`) or \`first_leads\`. With no goals set, a client whose Meta sources count scheduling as their result (such as \`schedule_website\`) defaults to booked calls, then first leads; every other client defaults to Triple Whale's orders, then first-touch sales, then first leads, which is how winners judged before goals could be set. \`${cli} winners goals\` shows the list, whether each goal has data and which one is used now; \`${cli} winners goals set booked_calls first_leads\` sets it, and \`${cli} winners goals reset\` goes back to the default. To judge on Stripe sales, connect Stripe, then put \`sales\` first. The report starts with a sentence saying which goal it judged on, and in the JSON every "sales" field (\`numbers.sales\`, \`costPerSale\`, \`accountCostPerSale\`, \`adSales\`, \`judgedOn: "sales"\`) holds that goal, named in \`goal\`, and every "signups" field the fallback goal, named in \`fallbackGoal\`. An ad with none of the goal that has spent less than twice the account's cost per one is too new to judge on it, so it is judged on the fallback goal, the next connected goal of another kind, and listed apart. Landing pages and ad-and-page pairs are judged on the same goal when it is \`sales\`, \`booked_calls\` or \`first_leads\`, and on first-touch sales otherwise, with first leads for pages too new to judge; \`pagesGoal\` and \`pagesFallbackGoal\` name them in the JSON. Each ad says in a sentence why it is on its list, then its headline and primary text in full (\`headline\` and \`primaryText\` in the JSON), so you need no SQL for its words. When a currency's ads run in two or more Facebook ad accounts, each ad is judged against its own account's cost per sale, and the section says so and lists each account (\`adAccounts\` in the JSON, and \`accountId\` on each ad). Each currency also says how many of its Facebook sales could be tied to a specific ad (\`adSales\` of \`facebookSales\` in the JSON): a sale whose link names no single ad counts toward Facebook but toward no ad. It judges the landing pages the ads sent people to the same way, and each ad on each page as an ad-and-page pair: a page or pair counts only once the page reader has read the page and it got at least 100 visits in the dates, and pairs are listed only for an ad that sent people to two or more pages, or a page that got people from two or more ads, each naming what the same ad cost on its other pages. \`--week <YYYY-MM-DD>\` picks any day of a Monday-to-Sunday week (default: the latest complete week), \`--days <n>\` the last n days ending today, as money and campaigns count them, and \`--from <YYYY-MM-DD> --to <YYYY-MM-DD>\` any dates; use one of them, at most 90 days. Check as often as suits the client, daily or every few days: a short window holds few sales, so more ads are judged on first leads, and the report says so. \`--top <n>\` how many ads, pages or pairs each list shows (default 5, at most 20), \`--ad-format image|video|carousel\` ranks only ads with that format tag (run \`${cli} ads tag-formats\` first; the account's cost per sale still counts every ad), and \`--format json\` gives the report as data.
|
|
90
90
|
|
|
91
91
|
## What winners and flops taught: lessons
|
|
92
92
|
|
|
@@ -155,7 +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; \`
|
|
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; \`other_touch_only\`: only a visit of the other kind, such as a latest visit and no first one when crediting the first touch; \`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
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.
|
|
160
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.
|
|
161
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.
|
package/dist/main.js
CHANGED
|
@@ -4229,13 +4229,14 @@ function winnersWindowText(title, window, words) {
|
|
|
4229
4229
|
const tied = s.adSales === null || !s.facebookSales ? "" : ` ${tenths(s.adSales)} of ${countOf(s.facebookSales, facebookWords)} could be tied to a specific ad.`;
|
|
4230
4230
|
const tests = s.tests.map((t) => `- Test ${t.testId}${t.hypothesisId ? ` (idea ${t.hypothesisId})` : ""}: ${counted(t.ads.length, "ad")}, spent ${money(t.spend, s.currency)}, ${countOf(t.sales, primary)}` +
|
|
4231
4231
|
`${t.costPerSale === null ? "" : `, ${money(t.costPerSale, s.currency)} per ${primary.one}`}\n`);
|
|
4232
|
-
|
|
4232
|
+
// With the fallback not connected the too-new ads are not judged at all, and a note says why.
|
|
4233
|
+
const fallbackJudged = fallback !== null && s.signupsSource === "connected";
|
|
4234
|
+
const onFallback = fallback === null ? "" : fallbackJudged ? `, ${s.adsJudgedOnSignups} judged on ${fallback.many}` : `, ${s.adsJudgedOnSignups} not judged (${fallback.many} are not connected)`;
|
|
4233
4235
|
return (`\nIn ${s.currency}: ${account}.${tied} ${s.adsJudgedOnSales} judged on ${primary.many}${onFallback}.\n` +
|
|
4234
4236
|
s.notes.map((note) => `${note}\n`).join("") +
|
|
4235
4237
|
list("Winners", s.winners) +
|
|
4236
4238
|
list("Flops", s.flops) +
|
|
4237
|
-
list(`${s.onSignups.title}: best`, s.onSignups.best) +
|
|
4238
|
-
list(`${s.onSignups.title}: worst`, s.onSignups.worst) +
|
|
4239
|
+
(fallback !== null && !fallbackJudged ? "" : list(`${s.onSignups.title}: best`, s.onSignups.best) + list(`${s.onSignups.title}: worst`, s.onSignups.worst)) +
|
|
4239
4240
|
(tests.length > 0 ? `\nAdzara tests\n${tests.join("")}` : "") +
|
|
4240
4241
|
pages);
|
|
4241
4242
|
})
|
|
@@ -4346,6 +4347,7 @@ function followUpLines(row) {
|
|
|
4346
4347
|
const tags = "tags" in row ? followUpTags(row.tags) : "";
|
|
4347
4348
|
return `- ${name}: ${sent}, ${engaged}, ${counted(n.salesHelped, "sale")} helped, ${tenths(n.salesLastNudge)} last nudge${n.optedOut > 0 ? `, ${counted(n.optedOut, "opt-out")}` : ""}${tags ? `. Tags: ${tags}` : ""}\n ${row.pickedBecause}\n`;
|
|
4348
4349
|
}
|
|
4350
|
+
/** One window's follow-up lists, under its dates unless `title` is null (the report has one window, whose dates head the ads). */
|
|
4349
4351
|
function followUpWindowText(title, window) {
|
|
4350
4352
|
const list = (heading, rows) => `\n${heading}\n${rows.length === 0 ? "- none\n" : rows.map(followUpLines).join("")}`;
|
|
4351
4353
|
const claimed = window.toolClaimed.map((m) => {
|
|
@@ -4353,7 +4355,7 @@ function followUpWindowText(title, window) {
|
|
|
4353
4355
|
const sends = m.toolSends === null ? "" : `, ${tenths(m.toolSends)} sent`;
|
|
4354
4356
|
return `- ${m.name ?? m.subject ?? "Email"} [${m.itemId}] (${m.platform}): ${revenue} claimed${sends}\n ${m.pickedBecause}\n`;
|
|
4355
4357
|
});
|
|
4356
|
-
return (`\n### ${title}\n` +
|
|
4358
|
+
return ((title === null ? "" : `\n### ${title}\n`) +
|
|
4357
4359
|
list("Top emails", window.emails.top) +
|
|
4358
4360
|
list("Bottom emails", window.emails.bottom) +
|
|
4359
4361
|
list("Top texts", window.texts.top) +
|
|
@@ -4367,9 +4369,9 @@ function followUpText(followUp, title) {
|
|
|
4367
4369
|
if (followUp.state === "not_connected")
|
|
4368
4370
|
return "\n## Follow-up emails and texts\n\nNo email or text tool is connected yet, so there are no follow-up messages to judge.\n";
|
|
4369
4371
|
const { week, last30Days } = followUp;
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
(
|
|
4372
|
+
const once = week.from === last30Days.from && week.to === last30Days.to;
|
|
4373
|
+
return (`\n## Follow-up emails and texts\n\n${once ? `For the same dates as the ads above, ${week.from} to ${week.to}. ` : ""}A sale is helped by a message the buyer clicked or replied to in the ${counted(followUp.creditDays, "day")} before buying. Under ${tenths(followUp.minSends)} sent, a message is judged on clicks or replies, never as a flop.\n` +
|
|
4374
|
+
(once ? followUpWindowText(null, week) : followUpWindowText(title, week) + followUpWindowText(`Last 30 days, ${last30Days.from} to ${last30Days.to}`, last30Days)));
|
|
4373
4375
|
}
|
|
4374
4376
|
function printWinnersGoals(goals, format) {
|
|
4375
4377
|
if (format === "json")
|
|
@@ -5097,7 +5099,7 @@ Options:
|
|
|
5097
5099
|
--top <n> how many best ads, worst ads, pages and campaigns brief shows, or ads in each winners list (default: 5, at most 20)
|
|
5098
5100
|
--week <YYYY-MM-DD> any day of the week winners covers, Monday to Sunday (default: the latest complete week); with
|
|
5099
5101
|
messages brief, the week its winning ads come from, its message numbers the 30 days ending that Sunday
|
|
5100
|
-
--days <n> winners: the last n
|
|
5102
|
+
--days <n> winners: the last n days, ending today, instead of a week (at most 90); or give winners
|
|
5101
5103
|
--from and --to for any dates; seo page-two: how many days, ending on Search Console's newest day (default: 90)
|
|
5102
5104
|
--site <site_url> seo page-two: only this Search Console property, such as sc-domain:example.com
|
|
5103
5105
|
--min-impressions <n> seo page-two: only searches shown at least n times (default: 100)
|
package/dist/playbooks.js
CHANGED
|
@@ -748,7 +748,7 @@ Start from the shape that fits, then rename and add steps to match what you read
|
|
|
748
748
|
|
|
749
749
|
Write \`map.json\` as \`{"offers": [...], "funnels": [...]}\`, in this order.
|
|
750
750
|
|
|
751
|
-
1. **Offers**, front end first, numbered by \`stack_position\` from 1. Each has an \`offer_key\` (lowercase, digits and underscores), \`name\`, \`tier\` (\`front_end\` or \`back_end\`), \`delivery\` (course, event, coaching, service, product, membership, webinar, lead_magnet or other), \`price\`, \`currency\`, an optional \`price_note\` (plain words on the price: other prices, payment plans, doubts), and \`matches\`: how a sale is recognised. A match is \`{"platform": ..., "match_type": ..., "value": "..."}\`: platform \`stripe\`, \`ghl\`, \`shopify\` or \`kajabi\`, and match_type \`product_id\`, \`price_id\`, \`amount\` (whole units, with a \`currency\`), \`name_contains\` (any part of the product's name, ignoring case) or \`name_starts_with\` (the start of the product's name, ignoring case). Use \`name_starts_with\` for a family of products whose names begin with one code, such as event tickets named "OCA Dallas" and "OCA Austin"; \`name_contains\` "OCA" would also catch "Local Coaching". Stripe charges carry no product, so match them by \`amount\`. For Kajabi, \`product_id\` is the offer id and a name match reads the offer's title; Kajabi has no \`price_id\`. Prefer a GoHighLevel or Shopify name match on the product's name when one price is shared by two offers, as a $497 bump and a $497 guest ticket can be. An offer nobody pays for, such as a free webinar, may have no \`matches\`.
|
|
751
|
+
1. **Offers**, front end first, numbered by \`stack_position\` from 1. Each has an \`offer_key\` (lowercase, digits and underscores), \`name\`, \`tier\` (\`front_end\` or \`back_end\`), \`delivery\` (course, event, coaching, service, product, membership, webinar, lead_magnet or other), \`price\`, \`currency\`, an optional \`price_note\` (plain words on the price: other prices, payment plans, doubts), and \`matches\`: how a sale is recognised. A match is \`{"platform": ..., "match_type": ..., "value": "..."}\`: platform \`stripe\`, \`ghl\`, \`shopify\` or \`kajabi\`, and match_type \`product_id\`, \`price_id\`, \`amount\` (whole units, with a \`currency\`), \`name_contains\` (any part of the product's name, ignoring case) or \`name_starts_with\` (the start of the product's name, ignoring case). Use \`name_starts_with\` for a family of products whose names begin with one code, such as event tickets named "OCA Dallas" and "OCA Austin"; \`name_contains\` "OCA" would also catch "Local Coaching". Stripe charges carry no product, so match them by \`amount\`. For Kajabi, \`product_id\` is the offer id and a name match reads the offer's title; Kajabi has no \`price_id\`. Prefer a GoHighLevel or Shopify name match on the product's name when one price is shared by two offers, as a $497 bump and a $497 guest ticket can be. No payment counts under two offers: an order line or payment two offers' matches both find counts under the one with the lower \`stack_position\`, and the save lists it as weak on both. Two lines of one order are two sales, one for each offer that finds its line. An offer nobody pays for, such as a free webinar, may have no \`matches\`.
|
|
752
752
|
- **Price.** A free webinar's \`price\` is 0 when its page or GoHighLevel says it is free. When GoHighLevel or Kajabi and Stripe both show a sale, \`price\` is the GoHighLevel or Kajabi price that names the product, \`price_note\` gives the Stripe amount, and the offer matches both: a GoHighLevel name match or the Kajabi offer, and the Stripe \`amount\`. A sale Stripe holds counts only under Stripe. Any other price you cannot find in the evidence is \`null\`, with a \`price_note\` saying why.
|
|
753
753
|
2. **Funnels and steps.** A funnel has a \`funnel_key\`, \`name\`, \`kind\` (\`front_end\` or \`back_end\`), \`entry_offer_key\`, \`window_days\` (how many days after one step a sale still counts for the next; 365 when left out) and \`steps\`. A step has a \`step_key\`, \`position\` (1, 2, 3 in the order people walk them), \`step_type\`, \`name\`, and optionally the \`offer_key\` it sells, a \`wait_note\` (plain words on the wait before it, such as "the months after the course"), \`assets\` and \`rules\`.
|
|
754
754
|
- Step types: \`ad\`, \`landing_page\`, \`vsl\`, \`optin\`, \`application\`, \`booking\`, \`confirmation\`, \`sales_call\`, \`checkout\`, \`order_bump\`, \`upsell\`, \`downsell\`, \`thank_you\`, \`webinar_registration\`, \`webinar\`, \`event\`, \`email_nurture\`, \`sms_nurture\`, \`onboarding\`, \`other\`.
|
|
@@ -1767,7 +1767,7 @@ When the owner is in the session with you, ask about these 3 gaps, one at a time
|
|
|
1767
1767
|
|
|
1768
1768
|
${step(W.winners)}
|
|
1769
1769
|
|
|
1770
|
-
${W.winners.purpose[0].toUpperCase()}${W.winners.purpose.slice(1)}. Each ad has its \`numbers\`, \`pickedBecause\`, \`tags\` and Adzara \`test\`; each section's \`tests\` adds up each Adzara test. Leave out \`--week\` for the latest complete week. To check more often, swap \`--week\` for \`--days <n>\`, the last n
|
|
1770
|
+
${W.winners.purpose[0].toUpperCase()}${W.winners.purpose.slice(1)}. Each ad has its \`numbers\`, \`pickedBecause\`, \`tags\` and Adzara \`test\`; each section's \`tests\` adds up each Adzara test. Leave out \`--week\` for the latest complete week. To check more often, swap \`--week\` for \`--days <n>\`, the last n days ending today, or \`--from <YYYY-MM-DD> --to <YYYY-MM-DD>\` for any dates (the MCP tool takes \`week\`, \`days\` or \`from\` and \`to\`). The "sales" fields (\`numbers.sales\`, \`costPerSale\`, \`accountCostPerSale\`, \`adSales\`, \`judgedOn: "sales"\`) hold the client's goal, named in \`goal\`, and the "signups" fields the fallback goal, named in \`fallbackGoal\`. A short window holds few of the goal, so fewer ads can be judged on it and more are judged on the fallback; the report says so in \`notes\`.
|
|
1771
1771
|
|
|
1772
1772
|
## 3. Lessons
|
|
1773
1773
|
|