Plugin screenshot thumbnail 1/7
Plugin screenshot thumbnail 2/7
Plugin screenshot thumbnail 3/7
Plugin screenshot thumbnail 4/7
Plugin screenshot thumbnail 5/7
Plugin screenshot thumbnail 6/7
Plugin screenshot thumbnail 7/7

Tape

Conversion tracking for Craft CMS, wired up properly.

Tape puts Google Ads, GA4, Meta, TikTok and a dozen other pixels on your Craft site — with the full ecommerce funnel behind them, Consent Mode v2 in front of them, and server-side tracking underneath so the conversions your customers' browsers lose still get counted.

It is the plugin the WooCommerce world calls a pixel manager, for Craft. If you have moved a store off WooCommerce and miss having every platform's events set up correctly from one screen, this is that screen.


Requirements

Craft CMS 5.3+ and PHP 8.2+. Craft Commerce is optional — without it you get page views, custom events, form and click conversions and everything else; with it you get the whole checkout funnel.

Installation

composer require justinholtweb/craft-tape
php craft plugin/install tape

Then Tape → Destinations → New destination, pick a platform, paste in the ID. That is the whole setup for a browser pixel.


What it tracks

Every pagepage_view
Product pagesview_item
Listings and searchview_item_list, select_item, search
The basketadd_to_cart, remove_from_cart, view_cart, add_to_wishlist
Checkoutbegin_checkout, add_shipping_info, add_payment_info
The moneypurchase, refund
Whatever else you wantany event name, from Twig, JavaScript, or a trigger

With Commerce installed, add-to-cart, remove-from-cart, purchase and refund are wired to Commerce's own events. No template changes at all — including across a redirect, so a basket updated by a form post still reports the right event on the page the customer lands on. The other funnel steps are one line of Twig each.

A custom name like brochure_download goes to the platforms that take one — GA4, GTM, Meta, Microsoft Ads, Clarity, Hotjar, TikTok and Reddit in the browser, the custom snippet and the webhook — and is skipped by those that only count conversion IDs set up on their side.

Where it sends

PlatformBrowserServer-sideEdition
Google Ads — conversions, dynamic remarketing, cart data, enhanced conversions✓Lite
Google Analytics 4 — enhanced ecommerce✓Measurement ProtocolLite
Google Tag Manager — container plus a real data layer✓Lite
Meta (Facebook and Instagram)✓Conversions APILite
Microsoft Clarity✓Lite
Hotjar✓Lite
Microsoft Advertising (Bing UET)✓Pro
TikTok✓Events APIPro
Pinterest✓Conversions APIPro
LinkedIn✓Pro
Snapchat✓Conversions APIPro
Reddit✓Conversions APIPro
X (Twitter)✓Pro
Any other pixel — your own HTML plus a JavaScript event queue✓Pro
Webhook — the event as JSON, to a URL of yours✓Pro

Two of the same platform is normal and supported: an agency pixel and a client pixel, two brands on one install.


The bit that pays for itself

Browser conversion tracking loses somewhere between a tenth and nearly half of real orders. It happens for three completely ordinary reasons:

  • Redirect payment gateways. The customer pays at the bank's page, the order completes, and they close the tab instead of coming back. The order exists. The confirmation page never rendered. No tag ever fired.
  • Ad and tracking blockers, which is most of a young audience.
  • Tabs closed early, before the vendor script finished loading.

None of that loss is visible from inside Google Ads or Meta — they only know what reached them, so the campaign simply looks worse than it is and gets its budget cut.

Tape keeps a ledger of every conversion and what became of it, and the browser reports back when it has actually dispatched one. That gives two things nothing else can:

The accuracy report. Orders you took, against conversions anybody was told about, per day. If it reads 62%, that is worth more than any amount of campaign tuning.

Recovery. Orders whose conversion never reached anyone are sent again from the server, carrying the same event ID the browser tag would have carried — so a platform that did get it deduplicates rather than double-counting. Put it on a schedule:

*/15 * * * * cd /path/to/craft && php craft tape/conversions/recover

Recovery repairs failures. It does not override decisions: an order whose visitor withheld consent is left alone.


Consent

Google Consent Mode v2 is on by default, with all seven signals, region defaults, URL passthrough and ads data redaction.

Tape reads the answer from your consent platform — Cookiebot, CookieYes, Complianz, Iubenda, OneTrust, Osano, Termly, Klaro, Usercentrics, Civic, or a JavaScript expression of your own — and mirrors it into a first-party cookie, which is what lets server-side conversions honour consent too. If you have your own banner, one call is enough:

tape.consent.update({ ads: true, analytics: true, functionality: false });

Every destination declares a consent category, and its pixel does not load until that category is granted. Events collected before then are held, not dropped — so accepting a banner starts tracking immediately rather than on the next page. Google's own tags are the deliberate exception: under Consent Mode they load first and throttle themselves, which is what makes conversion modelling work.

Three states, not two: granted, denied and not yet answered, which is a different thing and is treated as one.


Triggers

"Count it as a lead when somebody clicks the phone number" is a marketing request, not a development ticket. Tape → Triggers answers it from the control panel:

  • a click on any CSS selector
  • a click on a phone number, an email address, or a link to another site
  • a form submission
  • scroll depth — 25, 50, 75, 90, each firing its own event
  • an element scrolling into view
  • time spent on the page

The condition is evaluated in the browser. The payload is not: Tape builds each trigger's event for every destination when the page renders, so a visitor cannot talk it into sending a conversion worth ten thousand pounds.


Templates

Nothing is required. With Commerce installed and auto-tracking on, the funnel works out of the box. Everything below is for when a site does something unusual, or for events Commerce has no opinion about.

{# Any event, with anything on it #}
{% do craft.tape.track('generate_lead', { value: 50, currency: 'GBP' }) %}

{# The funnel, by hand #}
{% do craft.tape.viewItem(variant) %}
{% do craft.tape.viewItemList(products, 'summer-sale', 'Summer sale') %}
{% do craft.tape.addToCart(variant, 2) %}
{% do craft.tape.purchase(order) %}

{# Place the tags yourself instead of having them injected #}
{{ tape.head() }}
...
{{ tape.body() }}

And from JavaScript, for anything that happens after the page has rendered:

tape.track('add_to_cart', { id: variantId, qty: 2 });

The payload is always built on the server, because that is where the platform adapters live — and because it is the only arrangement in which a visitor cannot dictate what gets sent. The endpoint refuses purchase and refund outright, and ignores any value in the request: prices come from Commerce, looked up from a product ID.


Editions

Lite (free)Pro
Google Ads, GA4, GTM, Meta, Clarity, Hotjar✓✓
Full ecommerce funnel and Commerce auto-tracking✓✓
Consent Mode v2 and CMP integration✓✓
Order deduplication and the event log✓✓
Twig and JavaScript event APIs✓✓
TikTok, Pinterest, LinkedIn, Snapchat, Reddit, X, Microsoft Ads✓
Custom snippets and webhooks✓
Server-side tracking (Conversions API, Measurement Protocol)✓
Enhanced conversions and advanced matching✓
Conversion recovery and the accuracy report✓
Triggers✓

A licence that lapses breaks nothing. Pro destinations stop firing and their configuration is left untouched, so renewing restores exactly what was there.


Privacy

Worth stating plainly, because a tracking plugin should be specific about this:

  • The event ledger holds nothing about the customer. No email, no hashes, no payloads. It records what fired, not who it fired for.
  • Matching data is hashed in Craft, never written to the page in the clear, and only ever put on a page that carries a conversion — never on a product page, where it could be cached and served to the next hundred visitors.
  • The queue carries identifiers, not identities. An order-backed job stores an order ID and re-derives everything when it runs.
  • Google's enhanced-conversions format sends the postal address unhashed. Tape leaves that off unless you explicitly ask for it, and says so on the switch.

Diagnosing it

php craft tape/destinations            # what is configured, and what is broken
php craft tape/destinations/doctor     # the whole picture, including Commerce wiring
php craft tape/destinations/test       # send a real event to every server-side API
php craft tape/conversions/recover     # look for missed conversions now
php craft tape/conversions/prune       # trim the event log

test is the one that matters. A revoked access token, a retired API version and a pixel ID belonging to somebody else's account all look exactly like success from a browser: the tag fires, nothing errors, and no conversion ever appears in the platform's reporting. One round trip with the answer printed is worth more than any amount of validation.

Caching

Tape is safe behind a full-page cache. Nothing it writes into a page varies by visitor: the consent answer is read from a cookie by the runtime rather than baked in, custom snippets sit inert inside a <template> until consent allows them, and matching data appears only on pages that carry a conversion — which are the pages nobody caches.


Licence

See LICENSE.md. © Justin Holt.

Installation Instructions

To install this plugin, copy the command above to your terminal.

Reviews

This plugin doesn't have any reviews.

Active Installs
0
Version
5.0.0
License
Craft
Compatibility
Craft 5
Last release
October 4, 2026