BFCM Tracking & Attribution Preflight
Check your Meta, Google and TikTok purchase tracking and UTMs against Shopify before Black Friday, see last year's claim gap, and get a dated fix list.
What it does
- Compares 28 days of Meta, Google Ads and TikTok purchase events with Shopify orders to flag possible duplicate delivery, missing Conversions API, zero-event days and value gaps
- Audits UTMs on real orders: untagged ad clicks, "direct" share, spelling variants like facebook / fb / ig, and unfilled macros
- Shows descriptive attribution gaps between platforms and Shopify last BFCM, per day and per platform, with blended MER
- Gives a READY / FIX BEFORE BFCM / NOT READY verdict and a fix list dated back from Black Friday so you get two weeks of clean data
- Read-only and runs on local exports, with a tested sample store and 16 end-to-end checks on Python 3.8 and the default runtime
Before you start
- A Shopify store with an online store and at least one of Meta, Google Ads or TikTok ads running
- Access to Shopify Analytics and each platform's events or conversions reporting (Meta Events Manager, Google Ads Goals, TikTok Events Manager)
- 28 days of daily numbers, exported to CSV in the format in DATA_CONTRACT.md (about 30 minutes by hand)
- Optional: an orders export with landing site and referrer, and last year's BFCM week by day with each platform's claimed revenue and spend
- Python 3.8 or newer. Nothing to install and no API keys needed
See an example
Example output from a sample run. Company names and figures are sample data.
Fictional sample store: Fernhill Goods. Offline demonstration only.
BFCM Tracking Preflight: Fernhill Goods, as of 2026-10-05
Verdict: NOT READY. 1 critical issue and 6 warnings. Black Friday is Nov 27, 2026 (53 days away) and Cyber Monday is Nov 30. Fix the items below by Nov 10 so you have two weeks of clean data before Black Friday.
Issues, most severe first (all detected, none changed)
- CRITICAL: Meta deduplication appears to have failed since Sep 24 (12 days; server and browser events both counted). In the last 7 days Meta counted 422 purchases for 233 Shopify orders (1.81x).
- WARNING: Meta browser pixel sent 0 purchase events on Sep 15–Sep 16 (2 days) while Shopify booked 62 orders. It came back on Sep 17.
- WARNING: Google Ads reports $117.07 per purchase conversion against Shopify net sales of $88.75 per order (1.32x).
- WARNING: 279 of 901 orders (31.0%) have no utm_source.
- WARNING: 73 orders (8.1%) carry an ad-platform click ID in the landing URL but no UTMs: Meta 64, TikTok 9.
- WARNING: utm_source is spelled inconsistently for 2 channels, so reports split one channel into several rows.
- WARNING: 12 orders carry an unfilled ad-platform macro in their UTMs:
{{campaign.name}}in utm_campaign (12).
1. Event coverage and deduplication (Sep 8–Oct 5, 28 days)
Shopify booked 901 orders and $79,959 net sales (233 orders in the last 7 days). Meta and TikTok count every site purchase, so they should land at 0.85–1.10x Shopify orders. Google Ads counts only ad-attributed conversions, so it is checked for gaps and value, not count.
- CRITICAL: Meta deduplication appears to have failed since Sep 24 (12 days; server and browser events both counted). In the last 7 days Meta counted 422 purchases for 233 Shopify orders (1.81x). Browser pixel 205 + Conversions API 223 = 428 events. Meta merged only 6 as duplicates (3% of the 205 it could have). Before Sep 24 it merged 97% of possible duplicates and counted 0.99x Shopify orders. These aggregate counts suggest duplicate delivery; verify matching event IDs. Reported metrics may be distorted; counts alone do not establish the effect on attributed ROAS or CPA.
- WARNING: Meta browser pixel sent 0 purchase events on Sep 15–Sep 16 (2 days) while Shopify booked 62 orders. It came back on Sep 17. Conversions API events kept arriving on those days (59), so only the browser pixel broke.
- WARNING: Google Ads reports $117.07 per purchase conversion against Shopify net sales of $88.75 per order (1.32x). Aggregate averages can differ because attributed customers and revenue definitions differ. This ratio cannot establish a tax/shipping error or a ROAS correction factor.
| Source | Last 7 days | Per Shopify order | Value per event vs Shopify | Zero-event days | Status |
|---|---|---|---|---|---|
| Shopify orders | 233 | — | $88.75 net sales per order | — | — |
| Meta purchases (after dedup) | 422 | 1.81x | 1.00x | — | CRITICAL |
| Meta browser pixel | 205 | 0.88x | — | 2 | WARNING |
| Meta Conversions API | 223 | 0.96x | — | 0 | OK |
| Google Ads (attributed only) | 61 | 0.26x attributed | 1.32x | 0 | WARNING |
| TikTok | 212 | 0.91x | 0.99x | 0 | OK |
2. UTM hygiene (901 orders, Sep 8–Oct 5)
622 of 901 orders (69.0%) have a utm_source. 119 (13.2%) have no UTM, no referrer and no click ID.
- WARNING: 279 of 901 orders (31.0%) have no utm_source. 119 of them (13.2% of all orders) also had no referrer and no click ID, so every report will call them direct.
- WARNING: 73 orders (8.1%) carry an ad-platform click ID in the landing URL but no UTMs: Meta 64, TikTok 9. 64 carry a Meta click ID (fbclid), from ads or organic posts: Meta adds fbclid to organic Facebook/Instagram link clicks too, so these are not all untagged ads. TikTok click IDs come from ad clicks, so those ads appear to be live without URL parameters and Shopify can't credit the platform or campaign. Catalog, Advantage+, Spark and boosted-post ads are the usual gaps.
- INFO: 41 orders have a Google click ID (gclid/gbraid/wbraid) and no UTMs. Fine for Google Ads and GA4 with auto-tagging. Shopify's own reports need UTMs: add a final URL suffix if you report from Shopify (references/playbook.md §4).
- WARNING: utm_source is spelled inconsistently for 2 channels, so reports split one channel into several rows.
Meta: 5 spellings across 308 orders:
facebook171,fb45,Facebook43,ig28,instagram21. Usefacebook. Google: 3 spellings across 139 orders:google117,Google13,adwords9. Usegoogle. - WARNING: 12 orders carry an unfilled ad-platform macro in their UTMs:
{{campaign.name}}in utm_campaign (12). By utm_source: tiktok 12.{{…}}is Meta's macro syntax. TikTok uses__CAMPAIGN_NAME__and Google uses{campaignid}, so a template copied from Meta arrives unfilled.
| Landing page (orders without UTMs) | Orders | With an ad click ID |
|---|---|---|
| / | 79 | 14 |
| /products/linen-apron | 52 | 30 |
| /pages/gift-guide | 47 | 16 |
| /products/ceramic-pour-over-set | 29 | 26 |
| /products/waxed-canvas-tote | 28 | 18 |
3. Last BFCM: platform claims vs Shopify (Nov 24–Dec 2, 2025)
Platforms together reported $252,000 of attributed sales vs $120,000 in Shopify net sales (2.10x). This is a reporting gap, not a return. Summing platform claims is not total sales. Each platform counts every sale it touched, so one order is claimed by several platforms at once. View-through credit, modelled conversions, different attribution windows and gross-vs-net revenue definitions widen the gap further.
| Date | Shopify net sales | Meta claimed | Google Ads claimed | TikTok claimed | All claims | Claims ÷ actual |
|---|---|---|---|---|---|---|
| Mon Nov 24 | $5,280 | $3,960 | $3,480 | $2,070 | $9,510 | 1.80x |
| Tue Nov 25 | $5,760 | $4,240 | $3,760 | $2,190 | $10,190 | 1.77x |
| Wed Nov 26 | $7,440 | $5,570 | $4,610 | $2,750 | $12,930 | 1.74x |
| Thu Nov 27 (Thanksgiving) | $12,480 | $10,470 | $9,530 | $5,560 | $25,560 | 2.05x |
| Fri Nov 28 (Black Friday) | $28,320 | $32,130 | $23,650 | $14,350 | $70,130 | 2.48x |
| Sat Nov 29 | $17,760 | $17,020 | $15,660 | $9,220 | $41,900 | 2.36x |
| Sun Nov 30 | $14,640 | $12,440 | $10,710 | $6,620 | $29,770 | 2.03x |
| Mon Dec 1 (Cyber Monday) | $18,960 | $14,480 | $12,630 | $7,730 | $34,840 | 1.84x |
| Tue Dec 2 | $9,360 | $7,690 | $5,970 | $3,510 | $17,170 | 1.83x |
| Total | $120,000 | $108,000 | $90,000 | $54,000 | $252,000 | 2.10x |
The aggregate attribution gap peaked on Nov 28 (Black Friday) at 2.48x.
- Meta claimed $108,000 (90.0% of all Shopify sales on its own). Shopify booked $67,500 from orders with Meta UTMs, so Meta attribution-gap ratio is 1.60x (platform revenue / UTM revenue). Reported ROAS 6.67 on $16,200 spend; Shopify UTM (last-click) ROAS 4.17.
- Google Ads claimed $90,000 (75.0% of all Shopify sales on its own). Shopify booked $36,000 from orders with Google Ads UTMs, so Google Ads attribution-gap ratio is 2.50x (platform revenue / UTM revenue). Reported ROAS 9.26 on $9,720 spend; Shopify UTM (last-click) ROAS 3.70.
- TikTok claimed $54,000 (45.0% of all Shopify sales on its own). Shopify booked $10,800 from orders with TikTok UTMs, so TikTok attribution-gap ratio is 5.00x (platform revenue / UTM revenue). Reported ROAS 13.24 on $4,080 spend; Shopify UTM (last-click) ROAS 2.65.
- Shopify UTM sales and platform-attributed revenue use different attribution methods. Each ratio is descriptive, not a planning discount, correction multiplier or measure of incremental contribution.
- Blended MER: 4.00 ($120,000 Shopify net sales ÷ $30,000 total ad spend).
Future gaps may differ. Use blended MER with margin and cash constraints; it includes non-ad-driven sales and is not causal ad ROI.
4. Fix list and timeline (nothing has been changed)
| # | Severity | What to do | State | By |
|---|---|---|---|---|
| 1 | CRITICAL | Restore Meta deduplication: the browser and server Purchase events must carry the same event_name and event_id. Look for what changed on Sep 24 (a second server-side app, a GTM server container, or a pixel/app reinstall) and keep one CAPI source. Confirm in Events Manager → Purchase → Event deduplication. | recommended | Nov 10 |
| 2 | WARNING | Find what changed on or just before Sep 15 (theme publish, app install or update, checkout change). Place a test order after every theme or app change from now on. | recommended | Nov 10 |
| 3 | WARNING | Reconcile matched Google Ads orders, currency, discounts, tax, shipping, refunds and attribution/reporting dates before proposing any purchase-value change. Do not divide ROAS by the aggregate ratio. | recommended | Nov 10 |
| 4 | WARNING | Put UTMs on every paid ad, email and SMS link using the naming standard in references/playbook.md §4 (Meta URL parameters at ad level, Google final URL suffix, TikTok URL parameters, Klaviyo UTM tracking on). | recommended | Nov 10 |
| 5 | WARNING | Add URL parameters to every Meta and TikTok ad, including catalog, Advantage+, Spark and boosted-post ads. The click ID alone is not enough for Shopify reports. | recommended | Nov 10 |
| 6 | WARNING | Standardise utm_source to one lowercase value per platform (facebook, google) in every ad template. Put placement (fb/ig) in utm_content, not utm_source. | recommended | Nov 10 |
| 7 | WARNING | Fix the URL templates so each platform uses its own macro syntax (references/playbook.md §4), then click a live ad and check the landing URL. | recommended | Nov 10 |
| 8 | PLAN | Re-run this preflight. If it says READY, Nov 13–Nov 26 is your two weeks of clean data before Black Friday. | recommended | Nov 13 |
| 9 | PLAN | Set BFCM budgets and scaling rules off blended MER (4.00 last year), not platform ROAS. Combine MER with contribution margin and cash constraints; attribution-gap ratios are not ROAS correction factors. | recommended | Nov 17 |
| 10 | PLAN | Change freeze until Dec 1: no theme publishes, new apps, pixel, consent-banner or checkout changes. Any change after this needs a test order. | recommended | Nov 20 |
| 11 | PLAN | Final preflight run. Then check Meta, Google Ads and TikTok purchases against Shopify orders every morning through Cyber Monday. | recommended | Nov 24 |
Thresholds: Meta/TikTok purchases 0.85–1.10x Shopify orders (critical outside 0.70–1.25x); dedup broken when under 50% of duplicates merge; value per event 0.85–1.15x Shopify net sales per order; no-UTM orders warning ≥ 20%, critical ≥ 40%. States: detected → recommended → applied → verified. Read-only: no pixels, ad accounts or Shopify settings were changed.
Related guide
Browse the technical files
---
name: bfcm-tracking-preflight
description: Pre-BFCM audit for Shopify DTC brands of purchase tracking and attribution. Compares Meta, Google Ads and TikTok purchase events with Shopify orders to catch broken pixels, missing Conversions API and failed deduplication, checks UTM hygiene, shows how far last year's platform claims were from real Shopify sales, and gives a READY / FIX BEFORE BFCM / NOT READY verdict with a dated fix list. Read-only.
---
# BFCM Tracking & Attribution Preflight
Black Friday 2026 is **Nov 27** and Cyber Monday is **Nov 30**. The worst time to find out a pixel is broken is on Black Friday, when every hour of bad data steers budget and the ad platforms' algorithms in the wrong direction. A widely shared r/FacebookAds checklist ("BFCM Prep you should do THIS WEEK") puts it simply: audit your pixel, server-side events and UTMs now; pull last year's numbers and compare what Meta said with what actually landed in Shopify, to describe differing attribution methods; and set budgets off MER, not platform ROAS, because several platforms can credit the same sale.
This skill does that audit from local exports and puts a date on every fix: **events complete and deduplicated → UTMs clean → last year's claim gap known → budgets set off MER → change freeze.**
## Before you run
This skill ships scripts and sample data alongside this SKILL.md. Before running any command:
1. **Get the files.** Make sure the skill's other files (`scripts/`, `examples/`, `tests/`, `references/` and `DATA_CONTRACT.md`) are in your working folder at the same relative paths. Some environments load only SKILL.md. If yours did, fetch each file from this skill's published files and write it to the matching path. In type.com, read them with the skill-file tools. Anywhere else, the type.com Skills Library API lists every file with its path, content and `sha256`: GET `https://api.type.com/api/public/library/skills` and take the entry with slug `bfcm-tracking-preflight`.
2. **Check the copies are exact.** Compare each file's size in bytes, not characters (and its hash, where your tools report one), with the published version before running. A copy written out from the published file is fine once its byte size and hash match. Never run a script you summarised or reconstructed from memory.
3. **Run from the skill's folder**, calling interpreters explicitly: `python3 scripts/…` and `bash examples/run.sh`. Python 3.8+ and the standard library only; there is nothing to install.
4. **Try the sample first.** Run `bash examples/run.sh`. Its output must match `examples/expected_output.txt` exactly. If it doesn't, stop and report the first differing line rather than running on real data. `bash tests/run_tests.sh` runs the full check suite.
## When to use this
- October to mid-November, before BFCM, on a Shopify store running Meta, Google Ads or TikTok ads. Or on each store an agency manages.
- Someone asks "is our tracking ready for Black Friday?", "why does Meta show more purchases than Shopify?", or "how should we set BFCM budgets?".
- After a theme publish, new app, checkout change or tracking-app install, to make sure nothing broke.
- On a schedule: weekly from October, then on the re-check and final-run dates in the report. Stay silent when the verdict is READY and nothing changed.
## Operating rules
1. **Read-only.** The script reads local files only. Never change pixels, Conversions API settings, conversion actions, ad URL parameters, ad accounts or Shopify settings unless the user approves that specific change. When they do, make it, read it back, place a test order, and re-run the preflight once 3–7 days of new data exist.
2. **Recommend, don't fix silently.** Some fixes move reported numbers (dedup repair can reduce event counts; a value-definition change can alter reported ROAS). Say so before anyone changes anything, and never change conversion settings in the middle of BFCM.
3. **Platform claims are not sales.** Never add platform-reported revenue together and call it revenue, and never divide summed claims by total spend (that is not a ROAS). Shopify net sales is the actual; MER is the budget metric. **Net sales** throughout this skill means gross sales − discounts, before returns/refunds, excluding tax and shipping.
4. **Quote exact numbers**: X of Y, the ratio or percentage, and the date something started. Those are what the team and the platforms' support need.
5. **Keep exports local.** The orders file has order numbers and landing URLs with click IDs. Don't paste it into chat; delete it when done.
6. Say which state each item is in: **detected**, **recommended**, **applied**, or **verified**.
## Gathering the inputs
Exact columns and sources are in `DATA_CONTRACT.md`. All three are CSVs you can export by hand in under 30 minutes.
1. **Daily events, required** (`events_daily.csv`): 28 days of Shopify online-store orders and net sales, next to Meta Events Manager Purchase counts (total after dedup, plus Browser and Server), Google Ads purchase conversions, and TikTok Complete Payment events, with values where available.
2. **Orders with landing site, optional** (`orders_utm.csv`): one row per order with landing URL (query string included) and referrer, from the Shopify Admin API or an export app. Without it, UTM hygiene is not checked.
3. **Last BFCM, optional** (`last_bfcm.csv`): last year's Monday-before-Thanksgiving to Tuesday-after-Cyber-Monday, by day: Shopify net sales, each platform's claimed revenue, and optionally spend and Shopify sales by UTM source. Without it, there is no claim gap or MER baseline.
If the user has API access to these platforms through connected tools, you may pull the same numbers that way, but write them into these CSVs and run the analyzer on the files. Never ask the user to paste API keys or tokens into chat.
## Running it
```bash
python3 scripts/preflight.py --account "Store name" --events events_daily.csv \
[--orders orders_utm.csv] [--last-bfcm last_bfcm.csv] [--as-of YYYY-MM-DD] [--json analysis.json]
```
`bash examples/run.sh` runs it on the bundled sample store (Fernhill Goods, fictional).
What it does:
1. **Event coverage and dedup.** Meta and TikTok site-event coverage is screened against a configurable 0.85–1.10x Shopify order band over the last 7 days. Consent, reporting scope and latency affect coverage; this band is a heuristic, not proof of correct tracking. It flags Meta deduplication that appears to have failed (browser + server events both counted, with the start date; aggregate counts suggest this, matching event IDs confirm it), missing or partial Conversions API, streams that dropped to zero on days Shopify had orders (ongoing or recovered), and purchase values that don't match Shopify net sales per order (a flag for matched-order reconciliation, not proof of a tax/shipping error). Google Ads counts only ad-attributed conversions, so it is checked for gaps and value, not count.
2. **UTM hygiene.** Share of orders with no `utm_source` and the share that look "direct", click IDs arriving without UTMs (ttclid and msclkid come from ad clicks; fbclid is also added to organic Facebook/Instagram link clicks, so it is reported as "ads or organic posts"), `utm_source` spelling variants (`facebook` / `fb` / `Facebook` / `ig`), unfilled macros such as a literal `{{campaign.name}}`, and the top landing pages for untagged orders.
3. **Last BFCM claim gap.** Sum of platform claims vs Shopify net sales, per day and overall; each platform's claim vs Shopify sales with its UTMs; reported vs Shopify UTM (last-click) ROAS; blended MER. Summed platform ROAS is never printed. If a platform reported attributed sales but has no spend column, the report names it, marks MER as overstated, and does not use it for budgets.
4. **Verdict and dated fix list.** `NOT READY` (any critical), `FIX BEFORE BFCM` (warnings only) or `READY`. Fixes are due 17 days before Black Friday (Nov 10 in 2026) so two weeks of clean data follow; then the re-check (Nov 13), budgets from MER (Nov 17), change freeze (Nov 20) and final run (Nov 24).
Exit codes: 0 means the report was written, 2 means invalid input (with the file, line, column and reason). Numbers are parsed strictly: comma decimals (`2255,74`), European-formatted files (`1.234,56`), `nan`, `inf`, exponents, negatives and duplicate order IDs are rejected rather than guessed; `$1,234.56`, `€1,234.56` and `1234.56` are fine.
## Acting on the report
Present the verdict and the ranked issues, then walk through the fixes in this order. Each needs the user's approval. Exact UI paths and templates are in `references/playbook.md`.
1. **Meta deduplication and Conversions API.** One server source per dataset, the same `event_id` on browser and server Purchase events, data sharing consistent with merchant-approved consent/privacy settings; never automatically raise it. Verify with a test order in Events Manager → Test events. Explain that reported event counts may change; aggregate counts do not establish true attributed ROAS.
2. **Streams at zero.** Find the change on the start date (theme publish, app, checkout or consent banner), fix it, place a test order.
3. **Purchase value.** Reconcile matched orders and agree on revenue definitions before proposing changes. Aggregate purchase-value ratios cannot diagnose a tax/shipping error or correct ROAS. Avoid unreviewed changes during BFCM.
4. **UTMs.** Apply the naming standard to every ad, including catalog, Advantage+, Spark and boosted posts; fix macros per platform; standardise `utm_source` spellings.
5. **Budgets off MER.** Use last year's blended MER with contribution margin and cash constraints to inform budgets and scaling rules. Attribution-gap ratios are descriptive comparisons, never ROAS correction multipliers or evidence of incremental channel contribution. Present this as a plan, not a change to ad accounts.
6. **Freeze and re-check.** Agree the change freeze, then re-run on the re-check and final dates. Mark items **verified** only when a later run shows them fixed.
## Files
- `scripts/preflight.py`: the analyzer. Local files only, deterministic output, optional `--json`.
- `DATA_CONTRACT.md`: the three CSV formats, where each column comes from, and every threshold.
- `references/playbook.md`: Meta dedup and Event Match Quality, Shopify customer events and web pixels, Google Ads value and enhanced conversions, the UTM naming standard with per-platform macros, TikTok, budgeting off MER, the BFCM 2026 timeline, and a status-update template.
- `examples/run.sh`, `examples/expected_output.txt`, `examples/data/`: sample run. `examples/make_fixtures.py` regenerates the sample data.
- `tests/run_tests.sh`, `tests/make_cases.py`: 16 end-to-end checks on hand-checkable inputs. No network needed.