Conversio
Conversio.
Client documentation
Runtime tag 2.6.4

For your platform and security teams

Conversio Tag: A Better Way to Track

Component
runtime tag + delivery worker
Current Version
2.6.4
Delivery
tag.conversio.dev
Issued
15 October 2026

Introduction

We are rolling out a new version of our tracking methodology, the Conversio Tag. This direction moved the complexities of our tracking out of your Tag Management container and hosts it externally on industry standard infrastructure. This allows our tracking solution to be as light as possible (only 6KB!) to be efficiently delivered to end-user devices and to maintain the highest levels of security, data compliance and data integrity.

It also allows us to deliverer enhancements and introduce updates that offer richer data and better insight, alongside also working natively with the most up to data consent management requirements.

All of this is done with with complete transparency and with no risk to your data and the privacy of your customers. This solution has been in beta for a couple of months now, live to real visitors, and is ready to be rolled out to all of our clients.

This document is written to clearly set out what the Conversio Tag does on your site, what data moves where, and what it cannot do. It is also aimed at answering most of the questions you may have ahead of installation.

How the Conversio Tag works

Summary

The tag is a single JavaScript file loaded through your Google Tag Manager (or other tag management provider) container. It reads experiment and event information your container pushes to the dataLayer, de-duplicates it, and reports it to your own Google Analytics 4 property. It will look similar to this:

<script src="https://tag.conversio.dev/t/cvo_sG0XUKi6G-ZXmRUTehJqRLq2.js" async></script>

One thing is important to be clear about upfront with the Conversio Tag, because it isn't like other third-party measurement scripts: the tag makes no network requests of its own. It contains no fetch, no XMLHttpRequest, no navigator.sendBeacon and no image beacons. It writes to the dataLayer and calls the gtag function your page already loads. There is no channel by which visitor data reaches Conversio from the browser.

The script fetch is the only one involving Conversio-operated infrastructure, and it carries no measurement data.

FlowDestinationContents
Script deliverytag.conversio.dev, a Cloudflare WorkerAn ordinary HTTPS GET for a JavaScript file. Carries whatever any asset request carries: source IP, user agent, referring page. No measurement data, no identifiers.
MeasurementYour GA4 propertyExperiment and event names, and a random visitor identifier. Sent by your page's own gtag, pinned to the single measurement ID you nominate.
Page diagnosticsYour dataLayer onlyCore Web Vitals for the page load, available to your own container. Not transmitted anywhere by the tag.
Anything to ConversioNoneNo such flow exists. The tag issues no requests.

Answers to common questions

Can we see exactly what will execute on our pages?

Yes, and you can verify it for yourselves. The file is un-minified and readable, with original identifiers and one statement per line. It is generated from a source file by a build step that only strips comments, and the build is reproducible: the bytes we serve you can be regenerated from a specific commit and compared.

Ask us for the commit hash of the version you are served and we will walk the comparison through with you. It is a byte-for-byte match once the values we substitute for you are put back to the placeholders the published source carries. We would rather do that alongside you than hand over a recipe, because which values are substituted changes from one version to the next, and a recipe written against an older version would report a difference that is not really there.

Can you provide a Subresource Integrity (SRI) hash?

No, and the reason is structural rather than an oversight. Values specific to you, your measurement ID among them, are substituted into the file when it is served, so the bytes differ from one client to the next and no single integrity hash could cover them all. Publishing one would break your tag the moment your configuration changed.

What we offer instead is stronger for the question SRI is usually asked to answer. An integrity hash tells you a file has not changed since you reviewed it. Version pinning tells you it cannot change: the file we serve you stays exactly as it is until we move your key, on a date agreed with you, and never as a side effect of us releasing anything. That is set out under How Updates are Tested and Communicated below, and the values the tag publishes on every page load let you confirm which version and which deployment date you are on at any time, without asking us.

If an integrity hash is a firm requirement rather than a preference, for instance because the tag would sit on a page in scope for PCI DSS, please just raise this with us. It is by no mean a blocker and the right solution may simply be to keep the tag off those particular pages.

What Content Security Policy change do we need?

One host in script-src:

script-src https://tag.conversio.dev

Nothing else. The tag loads no fonts, stylesheets, images or further scripts, and opens no connections, so no other directive is affected. If your policy is nonce-based, the tag inherits the nonce your container applies to its injected scripts.

What is stored on a visitor's device?

No cookies. Two kinds of browser storage, and nothing at all is written before your consent signal arrives.

Session storage is cleared when the tab closes. The single local storage entry is the only value that persists.

KeyStorePurpose
conversio_idlocal (consent gated)A random identifier, so one visitor's page views can be related to each other inside your GA4 property. Generated only after consent. Contains nothing derived from the visitor or their device.
conversioEmissionEnabled clientEmissionEnabledsessionWhether your consent signal has been received, per reporting stream.
conversioExperienceMap conversioExperienceFired conversioExperienceListsessionWhich experiments this visitor has seen in this session, so a repeat is reported once rather than on every page.
conversioEventBuffer conversioEventListsessionEvents observed before consent, held so they can be reported if consent is given, and discarded if it is not.
conversioVitalsPendingsessionPage performance measurements awaiting the consent decision.
client* equivalentssessionThe same set again for your own reporting stream, which shares no storage with ours.

Reporting is off by default and the tag never turns it on by itself. It stays off unless the Conversio Session Initiator tag, set to fire based on your own consent trigger, signals otherwise:

(window.conversioConsentQueue = window.conversioConsentQueue || []).push('granted');

A withdrawing is as easy as running the same call with 'denied'. This, therefore, can be set within the push object dynamically using a GTM variable. The signal is a queue rather than a function call so load-order cannot disrupt it; if it is pushed before the tag has loaded it waits, if it gets pushed afterwards it applies immediately. Both orders behave identically, which is important to maintain a consistent behaviour despite the speed and configuration of your consent platform.

Anything the tag does not recognise is ignored rather than treated as granted. An error in the signal results in no reporting, never in reporting the visitor did not agree to.

What happens if our service has a problem?

Every failure path returns a valid, inert JavaScript file. Your pages continue to work and your console stays clean and error-free; measurement simply stops until the problem is resolved. That covers our storage being unavailable, a configuration record being unreadable, the file being missing, and the request being rate limited.

This is fully tested. A fourteen-case fault matrix is run against a staging deployment before a release, and every case asserts a JavaScript content type, this ensures the service never returns a HTML error page into a <script> tag.

How quickly can we have it switched off?

Immediately, from our side, with no change to your container and no deployment. Your key is deactivated and the tag stops being served, with your script tag left harmlessly in place. Propagation is a matter of seconds, plus up to five minutes for any copy already cached in a visitor's browser.

You can of course also remove the tag from your GTM container at any time.

Hosting and logging

Delivery runs on Cloudflare Workers, executing at Cloudflare's edge. Your configuration, which is your specific version and your measurement ID, is held in Cloudflare Workers KV. Cloudflare is the only subprocessor involved in delivering the tag.

Our application logging records nothing on the normal path. All fourteen recorded conditions are faults, such as an unrecognised key, a deactivated key, or a rate limit being hit, and the key is truncated in every one of them so that no complete credential is written to a log. A visitor being served the tag normally produces no application log entry. We cover this in more detail in the next section What Conversio logs.

Retention periods, and our data processing agreement are contractual and covered by our overarching Terms & Conditions. They are not set out in this document. Ask us for them directly; this pack covers what the software does, not what we have committed to contractually.

Checks you can run yourself

We would rather you verified this than accepted it. Each of these can be run in your browser's console on your own site, in an ordinary window.

  1. Confirm nothing is stored before consent. Load a page with a fresh profile and inspect Application → Storage. It should be empty of any conversio or client key.
  2. Confirm the tag opens no connections. Filter the Network panel to tag.conversio.dev. You should see exactly one request, for the script itself, and never a second.
  3. Confirm where measurement goes. With reporting active, look for requests to google-analytics.com carrying your own measurement ID, and confirm no request goes to any Conversio host other than the single script fetch.
  4. Confirm which version and configuration you are actually being served. Run window.conversioSettings in the console: it reports the version, the measurement ID we hold for you, and the date your key was last moved. If you want a full byte comparison against the published source as well, ask us and we will run it with you.

You can also validate the tag is successfully loading, as well as get details on the current version and its release credentials by running the command window.conversioSettings in the browser console.

What Conversio logs

The short answer

The tag sends us none of your data. It makes no network requests to Conversio databases at all. The runtime tag contains no fetch, no XMLHttpRequest, no navigator.sendBeacon and no image beacon.

The only thing that reaches us is the browser's request for the script file itself, exactly as with any externally hosted script. We record nothing about that request unless it fails, and when it does fail we record a reason code and which client it concerned, never anything about the person browsing.

What the tag sends to you

Everything it produces is handed to the client's own dataLayer and their own Google Analytics property.

Measurements, experiment data and Core Web Vitals are pushed to window.dataLayer and sent to the client's own GA4 property through the client's own gtag instance. That traffic goes from the visitor's browser to Google, under the client's own tag configuration and consent framework.

What else we do log

The tag has the ability to send failure logs to Cloudflare for stability monitoring on our end.

A request that succeeds produces no log line from our code at all. Every one of the log statements in the Worker sits on an error path, and they exist so that a client's tracking breaking is something we find out about rather than something they discover later from missing data.

Beyond these failure logs nothing else is logged in Cloudflare.

Every field we can record in these logs

This is the complete list of details that are captured in our logs. No client performance data or user behaviour data is captured as part of these logs:

FieldWhat it isExample
eventAlways the same marker, so our lines can be filtered from platform noiseconversio_loader
reasonWhich failure this was, from the fixed list belowasset_missing
keyThe first 12 characters of the client's key, so a problem can be traced to a clientcvo_7Hq2mNx9
clientThe client's own company name, as we hold it in their configurationAcme Co
versionWhich version of the tag bundle was involved2.6.4
hostnameThe hostname only of the site the request came fromstaging.someothersite.com
methodThe HTTP method, when it was one we do not servePOST

These logs capture no IP address, no visitor identifier, no cookie, no user agent, no location, no page URL, no referring path.

Two of these parameters we capture are also deliberately truncated:

The complete list of reason codes

Every line below is a real example of the output from the Worker failure logs.

Access and configuration

{"event":"conversio_loader","reason":"unknown_key","key":"cvo_7Hq2mNx9"}
{"event":"conversio_loader","reason":"record_malformed","key":"cvo_7Hq2mNx9"}
{"event":"conversio_loader","reason":"revoked_key","key":"cvo_7Hq2mNx9","client":"Acme Co"}
{"event":"conversio_loader","reason":"domain_mismatch","key":"cvo_7Hq2mNx9","client":"Acme Co","hostname":"staging.someothersite.com"}
{"event":"conversio_loader","reason":"method_not_allowed","method":"POST"}
CodeWhat happened
unknown_keyA request carrying a key that is not in our records
record_malformedA configuration record that is not readable as a client record
revoked_keyA key we have withdrawn, still being requested by a site
domain_mismatchA domain-locked key requested from a site not on its allow-list
method_not_allowedSomething other than a GET or HEAD against a tag URL

Serving the tag

{"event":"conversio_loader","reason":"version_invalid","key":"cvo_7Hq2mNx9","client":"Acme Co","version":"latest"}
{"event":"conversio_loader","reason":"asset_missing","key":"cvo_7Hq2mNx9","version":"9.9"}
{"event":"conversio_loader","reason":"asset_error","key":"cvo_7Hq2mNx9","version":"2.6.4"}
{"event":"conversio_loader","reason":"asset_unreadable","key":"cvo_7Hq2mNx9","version":"2.6.4"}
{"event":"conversio_loader","reason":"tracking_id_rejected","key":"cvo_7Hq2mNx9"}
{"event":"conversio_loader","reason":"deployed_at_rejected","key":"cvo_7Hq2mNx9"}
CodeWhat happened
version_invalidA record naming a version that is not version-shaped
asset_missingA client pinned to a bundle version nobody deployed
asset_errorThe bundle store threw while being read
asset_unreadableThe bundle was found but could not be read to the end
tracking_id_rejectedA client's GA property ID failed our safety check and was dropped rather than injected
deployed_at_rejectedA client's deployment date was not a valid ISO date and was dropped

Capacity and dependencies

{"event":"conversio_loader","reason":"rate_limited","key":"cvo_7Hq2mNx9"}
{"event":"conversio_loader","reason":"rate_limiter_error","key":"cvo_7Hq2mNx9"}
{"event":"conversio_loader","reason":"key_lookup_error","key":"cvo_7Hq2mNx9"}
{"event":"conversio_loader","reason":"serve_error","key":"cvo_7Hq2mNx9"}
CodeWhat happened
rate_limitedOne key exceeded its per-location request cap
rate_limiter_errorThe rate limiter was unavailable. We fail open, so the tag keeps working
key_lookup_errorOur configuration store was unreachable, or held invalid data
serve_errorA last-resort catch-all, so an unexpected fault cannot reach a client's page as an error

Where this is stored, and for how long

These lines are written to Cloudflare Workers Logs, retained for 7 days and then automatically deleted. We hold no other copy and export them nowhere.

Separately, our configuration store holds one record per client: their name, which tag version they are on, their GA property ID, their allowed domains and the date they were moved to their current version. It holds no visitor data.

How Updates are Tested and Communicated

No update reaches your site automatically from a deployment to our Cloudflare instance. Publishing a new version makes it available without moving a client on to it. Your site changes only when we move your key to it, as a separate, deliberate step we will clearly communicate to you first.

How a change reaches your site

Your site changes only at the final step, on a date you agreedHow an update to the hosted tag reaches a clientEvery changeFull test suite runs automatically, before and after mergeAbout 7,900 checks, run against the file you are servedA failing check blocks the changeStagingExercised on an isolated deployment, never your hostnameEach known failure case replayed and re-checkedMust serve JavaScript, never an errorVersion publishedThe new version becomes available to move clients toNothing moves. Every site stays on the version it hadNo client moves, including youYour siteYour key is moved, on the date agreed with youOne client at a time, never everyone at onceVerified live, reversible in minutes
Update pipeline: four stages, each with its gate.

Each stage has to pass before the next begins. The first three change nothing on your site; only the last one does.

Before anything ships

Every proposed change runs the full test suite automatically before it can be accepted, and again when it is. Roughly 7,900 behavioural checks across 15 suites, covering consent handling, event shape, deduplication, the GA4 payload and each documented failure mode.

Two things about how the testing suite runs are important to highlight:

It tests the file your visitors actually download. Each version's suite runs twice: once against the readable source we edit, and once against the exact bytes served from our hostname. Testing a copy and shipping a different file doesn't happen.

A separate check proves those two are in step. If the served file ever fell behind its source, the bytes reaching your visitors would no longer be the bytes that passed the suite. That check fails the build rather than warning, so a mismatch cannot be merged.

Older versions keep their own suites and are kept passing for as long as any client is still on them, so a fix for one client cannot impact the version another is pinned to.

None of this depends on anyone remembering to run it. It runs on every change and every merge, and a failure blocks the change rather than producing a note that can be missed.

Staging

Changes are exercised on a second, fully separate deployment before they go near the hostname your site calls. It runs on its own address, with its own limits, deliberately isolated so that nothing done there can touch a live client.

Staging is seeded with records that reproduce each way the service can fail. These are all relevant, real-world scenarios and are cases we can trigger on demand and re-check after any change:

CaseWhat it reproduces
Normal requestThe expected path: the script is served, correctly configured
Revoked clientA client whose access has been withdrawn
Domain-locked keyA key used from a site it was not issued for
Missing versionA client pointed at a version that was never published
No version, or an invalid oneA record that lost or corrupted its version
Corrupted recordA record that is no longer readable at all
Unsafe configuration valueA setting that would be unsafe to place into the script

Every one of them has to answer with a JavaScript response, not an error. Nothing our service depends on can put an error page into the <script> tag on your site. A failure on our side serves a harmless empty script, so the worst case is that tracking pauses, not that your page breaks.

How a version reaches your site

Your key is pinned to one specific version. Publishing a new version makes it available to be moved to; it does not move anybody, including you. This is the single most important thing about the rollout model, because it means a release that turns out to be wrong cannot reach every client at once. It cannot reach you at all until someone moves your key.

Moving you is a separate, deliberate step that names your key alone. One client at a time.

  1. We publish the version. Nothing changes on any site, including yours.
  2. We move one lower-stakes client first and confirm real tracking events on a live site, not just that the file is being served.
  3. We move you, on the date agreed with you.
  4. We verify against your live site that the version and configuration being served are the ones intended, rather than trusting cache timing to have caught up.

If something is wrong, rolling you back is the same operation with the previous version number, and it takes effect as soon as the cached copy expires, within minutes. Nothing needs to change on your side to roll back, and nothing needs to change on your side to be moved in the first place: the one line in your tag manager stays exactly as it is, permanently.

The same mechanism is how your access ends cleanly if we ever stop working together. Your key stops being served, your tag goes quiet, and there is nothing for you to find and remove.

What we tell you, and what you can check yourself

WhenWhat you get
At least 5 working days beforeWhat is changing and why, the version number, and the date we propose moving you
On the dayConfirmation that your site has been moved, with the date recorded against your key
If you ask us to waitYou stay on your current version. Nothing moves without your agreement
Urgent fixesA security or data-correctness fix may need to move faster. We tell you before where we can, and within one working day after where we cannot, with what changed and why it could not wait

Checking it yourself

You can confirm all of this from your own side. From version 2.6.4 the tag publishes three values on every page load, readable by your own tag manager or analytics:

window.conversioSettings.version          // which version is running
window.conversioSettings.versionReleased  // when that version was cut
window.conversioSettings.deployedAt       // when YOUR site was moved to it

The last one is the useful one. It is set automatically when your key is moved and is specific to you, so it answers "when did our tag last change?" without asking us. Re-confirming a version you already run does not move the date, so it only ever changes when your site genuinely did.

Conversio Ltd. This document describes runtime tag 2.6.4 as served from tag.conversio.dev. Version-specific details change between releases: ask us for a current copy if you are reviewing a version other than the one named above.