QuackChat with Google Analytics (no Tag Manager)
Your chatbot records booking and cart activity in your QuackChat dashboard. This guide forwards those same moments into your GA4 property using gtag, with no tag manager involved.
Have Google Tag Manager on your site? Use the Google Tag Manager guide instead. It is less code, the mapping is visible and editable without touching your site, and the protections this page has to build by hand come free there.
Quick Start (TL;DR)
- Confirm your site loads
gtag.jsdirectly, not through a tag manager. - Paste the snippet below after your existing GA4 tag, replacing the measurement ID.
- Register the custom dimensions you want to report on.
- Mark the booking event as a key event.
Prerequisites
- The QuackChat widget live on your site, via the WordPress plugin, the Shopify app embed or a script tag
- A GA4 property with
gtag.jsalready on the page, and your measurement ID (it looks likeG-XXXXXXXXXX) - A connected practice management system or store. Booking events need the former, cart events the latter, and both are Pro features, so no QuackChat event fires on Free or Starter
Why a data layer push is not enough
This is the part that catches almost everyone, so it is worth being explicit.
The widget publishes every event two ways: it pushes onto window.dataLayer, and it fires a browser event called quackchat:event. Tag Manager users only need the first, because reading the data layer is exactly what a tag manager does.
gtag.js does not work that way. It shares the dataLayer array as a transport for its own internal calls, but it ignores plain objects that other scripts push onto it. So on a site with GA4 but no tag manager, our events land on the page, sit in the array, and nothing ever sends them to Google.
That is what the snippet below fixes. It listens for the browser event instead and calls gtag itself.
The snippet
Replace G-XXXXXXXXXX with your own measurement ID. Placement on the page does not matter: the snippet checks for gtag at the moment an event fires, not when it loads, so it works above or below your GA4 tag.
<script>
(function () {
if (window.__qcGtagBridge) return;
window.__qcGtagBridge = 1;
var NAMES = ['quackchat_booking_link_clicked', 'quackchat_booking_completed',
'quackchat_product_clicked', 'quackchat_add_to_cart'];
window.addEventListener('quackchat:event', function (e) {
var d = (e && e.detail) || {};
if (typeof window.gtag !== 'function') return;
if (NAMES.indexOf(d.event) === -1) return;
var params = {};
for (var k in d) {
if (Object.prototype.hasOwnProperty.call(d, k) &&
k.indexOf('quackchat_') === 0 && d[k] != null) {
params[k] = d[k];
}
}
if (d.quackchat_value != null && d.quackchat_currency) {
params.value = d.quackchat_value;
params.currency = d.quackchat_currency;
}
params.send_to = 'G-XXXXXXXXXX';
window.gtag('event', d.event, params);
});
})();
</script>
What each part is doing
Five things in there are load-bearing. Changing them has consequences worth knowing about.
The __qcGtagBridge flag stops double counting. If the snippet ever runs twice, you get two listeners and every conversion is counted twice, silently and permanently. That is easy to do by accident on a single-page app, where a component can mount more than once, or if the snippet ends up in both a layout and a page template. The flag makes a second run a no-op.
The NAMES list filters out event names that are not ours. quackchat:event is an ordinary browser event, so any other script on your page can fire one. Tag Manager users get this filtering for free, because a Custom Event trigger matches one exact name and ignores everything else; written by hand it has to be explicit, or a stray event from another tag lands in your GA4 as a conversion. Be clear on what this does and does not buy you: it stops an unrelated name, it cannot stop a script that deliberately imitates ours. Treat these numbers as page-level analytics, not as tamper-proof accounting. Anything on your page can read these events, and anything on your page can emit them.
Only quackchat_ keys are copied, and send_to is set last. Both matter. Copying arbitrary keys off the event would let a forged event carry its own send_to and redirect the hit somewhere you did not choose, and hasOwnProperty keeps inherited properties from other scripts out. Setting send_to after the copy rather than before means nothing on the page can overwrite it.
send_to decides which Google products receive the event. Leave it out and gtag sends to every product configured on the page. If you run both a GA4 tag and a Google Ads tag, that means every chat booking is forwarded into Google Ads automatically, which is a decision you should make deliberately rather than inherit from a copied snippet. Pinning it to your measurement ID keeps the event in GA4 until you choose otherwise.
d[k] != null is what makes one snippet work for both clinics and stores. Clinic events carry only the five shared fields and nothing else, by design. Store events add product detail. Skipping empty values means each event sends exactly what it has, with no blank product fields on a booking.
The value and currency lines are conditional because GA4 requires a currency whenever you send a value and discards the amount if it is missing. A store that has not finished its first sync has no currency yet, so the snippet holds the amount back rather than sending a number Google will throw away.
The events and their fields
Four events, and every one carries the same five identifying fields. Rather than repeat the tables, the full reference lives on the other page and applies identically here:
- The four event names and when each fires
- The shared fields on every event
- The extra fields on the two commerce events
One difference worth noting. In Tag Manager you pick fields one at a time as Data Layer Variables. The snippet above forwards whatever the event carries, so you do not have to keep it in step with us when a field is added.
Registering custom dimensions
GA4 will accept the parameters immediately but will not report on them until you register each one. Go to Admin, Custom definitions, Create custom dimension, scope Event, and set the parameter name exactly.
Worth registering:
quackchat_bot_id, if you run more than one botquackchat_integration, to split clinic bookings from store activityquackchat_placement, on stores, to separate normal product cards from cross-sell suggestions
Do not register
quackchat_event_id. It is a fresh unique value on every single event, so it is useless as a dimension and actively harmful as one. GA4 allows 50 event-scoped custom dimensions, and a high-cardinality one pushes your reports into the(other)row, which degrades everything else in the same report. It exists so you can de-duplicate if you buffer or retry events, nothing more.
Marking a booking as a key event
Admin, Events, find quackchat_booking_completed once it has arrived at least once, and toggle Mark as key event. That is what makes it available to import into Google Ads.
Before you import a clinic booking into Ads
If you run a health or medical practice, stop and think about this one. The event carries no patient details, and that is enforced in our code rather than left to convention. But "this visitor booked an appointment at a physio clinic" is still health-adjacent once it is attached to an advertising profile, and Google's policies restrict advertising based on sensitive categories. The full note is on the Tag Manager guide, and it is worth a conversation with whoever runs your ad account before you turn it on.
This is also why send_to is pinned in the snippet. Removing that line is what makes the decision for you.
Consent Mode
If you use Consent Mode or a cookie banner, gate this snippet along with your other analytics. Do not gate the widget the same way. A support chat is a functional part of your site, and gating it means visitors get no chat until they accept marketing cookies.
Verification
- Turn on debug mode. The simplest way is Google Tag Assistant, which connects to your site and enables it for you. Alternatively add
debug_modeto your own tag:gtag('config', 'G-XXXXXXXXXX', { 'debug_mode': true }). - Use the chat. Click the booking link, or add a product to your cart.
- In GA4, go to Admin, DebugView. The event appears by name within a few seconds.
- Click it to check the parameters came through.
Remember to take
debug_modeback out when you are done. Setting it tofalsedoes not turn it off, you have to remove the parameter.
Troubleshooting
Nothing appears in DebugView
Cause, in the order worth checking:
- The
send_toID does not match your property. This is the most common one, because it is the single string you had to edit by hand. A mismatched measurement ID produces total silence, with no console error and nothing in DebugView. gtagis not on the page at all, because a consent banner has not been accepted yet or an ad blocker removed it.- The integration is not connected. Booking events need a practice management connection, cart events need a connected store, and both need Pro.
Solution: Compare the ID in the snippet character by character against the one in your gtag('config', ...) line. Then open your browser console and run typeof gtag; if it is not function, the tag itself never loaded and nothing downstream can work. Finally check Settings, Integrations in your bot's workspace shows an active connection.
Note that where you paste the snippet is not a cause. It checks for gtag when an event fires, not when it loads, so it is safe above or below your GA4 tag.
The listener never fires
Cause: The event is dispatched on window. A listener bound to document will never hear it.
Solution: Use window.addEventListener, exactly as written above.
Events reach GA4 but not Google Ads
Cause: This is the expected behaviour of send_to. The snippet deliberately sends to GA4 only.
Solution: Mark the event as a key event, then import it as a conversion from GA4 inside Google Ads. Read the clinic note above first if you are a health practice.
The amount is missing on cart events
Cause: Your store's currency has not synced, so we withhold the amount rather than send a value GA4 would discard for having no currency. It is also empty for a product with no price.
Solution: Press Sync on the store connection under Settings, Integrations. Testing the connection is not enough, only a sync saves the currency.
Events fire twice
Cause: Either the snippet is on the page twice, or you have this snippet and a tag manager container forwarding the same events. The first is easy to do on a single-page app, where a component that remounts runs its scripts again, or by pasting into both a layout and a page template.
Solution: If you copied the snippet before the __qcGtagBridge guard was added at the top, take the current version above, which makes a second run a no-op. Otherwise pick one path: if you have a tag manager, delete this snippet and use the Tag Manager guide.
Known Limitations
- You are maintaining code on your site. The Tag Manager route keeps the mapping outside your codebase where a marketer can change it. This route means a developer edits the page to change anything.
- The events can't be switched off. They're part of the widget. There's no setting, and turning them off means removing the widget.
- Anything on the page can read them. Both the data layer and the browser event are shared with every script on your site. They carry no personal details, which is why this is safe, but it isn't private.
- Purchases aren't tracked. We report the add-to-cart, not the completed order.
- Bookings made outside the chat aren't counted. Only what happens in the chat is reported, so a booking made on your own booking page fires nothing from us.