Intercepting GA4 events
This guide covers methods for intercepting Google Analytics 4 (GA4) events. Choose the method that best fits your needs.
Methods 1 and 2 are for web tracking (client-side GTM), while Method 3 is for server-side tracking (server-side GTM).
Method 1: Duplicate GA4 requests (recommended)
This method keeps your existing GA4 setup intact while duplicating events to a custom endpoint. Your data will continue flowing to Google Analytics as normal.
Steps:
-
Open Google Tag Manager
- Navigate to your GTM container
- Go to the Tags section
-
Create a Custom HTML Tag
- Click New to create a new tag
- Choose Custom HTML as the tag type
- Name it:
GA4 Request Duplicator
-
Add Your Interception Code
- Paste your custom code generated below into the HTML field
The duplicator copies GA4 tracking requests to your endpoint while keeping the original Google Analytics requests intact. It does this by intercepting common request mechanisms (XMLHttpRequest, fetch, navigator.sendBeacon, and script src assignments).
Use the minified snippet for production, or the non-minified version for auditing. You can also review the full implementation in the source code.
- Minified (for production)
- Non-minified (for auditing)
<script>
var trackingURL = "https://global.t.d8a.tech/<property_id>/g/collect"; // Replace it with your tracking URL
/* ga4-duplicator - built 2026-01-11T21:18:24.380Z */
"use strict";
(() => {
// src/ga4-duplicator.ts
window.createGA4Duplicator = function(options) {
class FetchInterceptor {
install(ctx) {
const originalFetch = window.fetch;
window.fetch = function(resource, config) {
const requestUrl = typeof resource === "string" ? resource : resource instanceof URL ? resource.toString() : resource.url;
const method = config && config.method || resource.method || "GET";
if (ctx.isTargetUrl(requestUrl)) {
const upperMethod = (method || "GET").toUpperCase();
let prepareBodyPromise = Promise.resolve(void 0);
if (upperMethod === "POST") {
if (config && Object.prototype.hasOwnProperty.call(config, "body")) {
prepareBodyPromise = Promise.resolve(config.body);
} else if (typeof Request !== "undefined" && resource instanceof Request) {
try {
const clonedReq = resource.clone();
prepareBodyPromise = clonedReq.blob().catch(() => void 0);
} catch (e) {
prepareBodyPromise = Promise.resolve(void 0);
}
}
}
const originalPromise = originalFetch.apply(this, arguments);
const duplicateUrl = ctx.buildDuplicateUrl(requestUrl);
if (upperMethod === "GET") {
originalFetch(duplicateUrl, { method: "GET", keepalive: true }).catch((error) => {
if (ctx.debug) console.error("gtm interceptor: error duplicating GET fetch:", error);
});
} else if (upperMethod === "POST") {
prepareBodyPromise.then((dupBody) => {
originalFetch(duplicateUrl, { method: "POST", body: dupBody, keepalive: true }).catch(
(error) => {
if (ctx.debug)
console.error("gtm interceptor: error duplicating POST fetch:", error);
}
);
});
}
return originalPromise;
}
return originalFetch.apply(this, arguments);
};
}
}
class XhrInterceptor {
install(ctx) {
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._requestMethod = method;
this._requestUrl = url;
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
if (this._requestUrl && ctx.isTargetUrl(this._requestUrl)) {
const originalResult = originalXHRSend.apply(this, arguments);
try {
const method = (this._requestMethod || "GET").toUpperCase();
const duplicateUrl = ctx.buildDuplicateUrl(this._requestUrl);
if (method === "GET") {
fetch(duplicateUrl, { method: "GET", keepalive: true }).catch((error) => {
if (ctx.debug) console.error("gtm interceptor: error duplicating GET xhr:", error);
});
} else if (method === "POST") {
fetch(duplicateUrl, { method: "POST", body, keepalive: true }).catch(
(error) => {
if (ctx.debug)
console.error("gtm interceptor: error duplicating POST xhr:", error);
}
);
}
} catch (dupErr) {
if (ctx.debug) console.error("gtm interceptor: xhr duplication failed:", dupErr);
}
return originalResult;
}
return originalXHRSend.apply(this, arguments);
};
}
}
class BeaconInterceptor {
install(ctx) {
if (!navigator.sendBeacon) return;
const originalSendBeacon = navigator.sendBeacon;
navigator.sendBeacon = function(url, data) {
if (ctx.isTargetUrl(url)) {
const originalResult = originalSendBeacon.apply(this, arguments);
try {
originalSendBeacon.call(navigator, ctx.buildDuplicateUrl(url), data);
} catch (e) {
if (ctx.debug) console.error("gtm interceptor: error duplicating sendBeacon:", e);
}
return originalResult;
}
return originalSendBeacon.apply(this, arguments);
};
}
}
class ScriptInterceptor {
install(ctx) {
try {
const scriptSrcDescriptor = Object.getOwnPropertyDescriptor(
HTMLScriptElement.prototype,
"src"
);
const originalScriptSrcSetter = scriptSrcDescriptor && scriptSrcDescriptor.set;
const originalScriptSrcGetter = scriptSrcDescriptor && scriptSrcDescriptor.get;
const originalScriptSetAttribute = HTMLScriptElement.prototype.setAttribute;
const duplicateIfGA4Url = (urlString) => {
try {
if (!ctx.isTargetUrl(urlString)) return;
fetch(ctx.buildDuplicateUrl(urlString), { method: "GET", keepalive: true }).catch(
(error) => {
if (ctx.debug)
console.error("gtm interceptor: error duplicating script GET:", error);
}
);
} catch (e) {
}
};
if (originalScriptSrcSetter && originalScriptSrcGetter) {
const setter = originalScriptSrcSetter;
const getter = originalScriptSrcGetter;
Object.defineProperty(HTMLScriptElement.prototype, "src", {
configurable: true,
enumerable: true,
get: function() {
return getter.call(this);
},
set: function(value) {
try {
const last = this.__ga4LastSrcDuplicated;
if (value && value !== last) {
duplicateIfGA4Url(String(value));
this.__ga4LastSrcDuplicated = String(value);
}
const self = this;
const onloadOnce = function() {
try {
const finalUrl = self.src;
if (finalUrl && finalUrl !== self.__ga4LastSrcDuplicated) {
duplicateIfGA4Url(finalUrl);
self.__ga4LastSrcDuplicated = finalUrl;
}
} catch (e) {
}
self.removeEventListener("load", onloadOnce);
};
this.addEventListener("load", onloadOnce);
} catch (e) {
}
setter.call(this, value);
}
});
}
HTMLScriptElement.prototype.setAttribute = function(name, value) {
try {
if (String(name).toLowerCase() === "src") {
const v = String(value);
const last = this.__ga4LastSrcDuplicated;
if (v && v !== last) {
duplicateIfGA4Url(v);
this.__ga4LastSrcDuplicated = v;
}
const selfAttr = this;
const onloadOnceAttr = function() {
try {
const finalUrlAttr = selfAttr.src;
if (finalUrlAttr && finalUrlAttr !== selfAttr.__ga4LastSrcDuplicated) {
duplicateIfGA4Url(finalUrlAttr);
selfAttr.__ga4LastSrcDuplicated = finalUrlAttr;
}
} catch (e) {
}
selfAttr.removeEventListener("load", onloadOnceAttr);
};
this.addEventListener("load", onloadOnceAttr);
}
} catch (e) {
}
return originalScriptSetAttribute.apply(this, arguments);
};
} catch (e) {
}
}
}
if (window.__ga4DuplicatorInitialized) {
if (options.debug) console.warn("GA4 Duplicator: already initialized.");
return;
}
const destinations = [];
if (options.destinations && Array.isArray(options.destinations)) {
for (let i = 0; i < options.destinations.length; i++) {
destinations.push(options.destinations[i]);
}
}
if (options.server_container_url) {
destinations.push({
measurement_id: "*",
server_container_url: options.server_container_url
});
}
if (destinations.length === 0) {
console.error("GA4 Duplicator: either server_container_url or destinations array is required");
return;
}
function normalizePath(p) {
p = String(p || "");
p = p.replace(/\/+$/, "");
return p === "" ? "/" : p;
}
function matchesId(pattern, id) {
if (!pattern || pattern === "*") return true;
try {
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
return new RegExp("^" + regexStr + "$", "i").test(id);
} catch (e) {
return pattern.toLowerCase() === id.toLowerCase();
}
}
function getMeasurementId(url) {
try {
const parsed = new URL(url, location.href);
return parsed.searchParams.get("tid") || parsed.searchParams.get("id") || "";
} catch (e) {
const match = url.match(/[?&](?:tid|id)=([^&?#]+)/);
return match ? decodeURIComponent(match[1]) : "";
}
}
function getDestinationForId(id) {
for (let i = 0; i < destinations.length; i++) {
if (matchesId(destinations[i].measurement_id, id)) {
return destinations[i];
}
}
return null;
}
function getDuplicateEndpointUrl(dest) {
const trackingURL = String(dest.server_container_url || "").trim();
const u = new URL(trackingURL, location.href);
u.search = "";
u.hash = "";
return u;
}
function isTargetUrl(url) {
if (!url || typeof url !== "string") return false;
try {
const parsed = new URL(url, location.href);
for (let i = 0; i < destinations.length; i++) {
const duplicateTarget = getDuplicateEndpointUrl(destinations[i]);
if (parsed.origin === duplicateTarget.origin && normalizePath(parsed.pathname) === normalizePath(duplicateTarget.pathname)) {
return false;
}
}
const params = parsed.searchParams;
const hasGtm = params.has("gtm");
const hasTagExp = params.has("tag_exp");
const measurementId = params.get("tid") || params.get("id") || "";
const isMeasurementIdGA4 = /^G-[A-Z0-9]+$/i.test(measurementId);
return hasGtm && hasTagExp && isMeasurementIdGA4;
} catch (e) {
if (typeof url === "string") {
for (let j = 0; j < destinations.length; j++) {
try {
const target = getDuplicateEndpointUrl(destinations[j]);
const targetNoQuery = target.origin + target.pathname;
if (url.indexOf(targetNoQuery) !== -1) return false;
} catch (e2) {
}
}
const hasGtmFallback = url.indexOf("gtm=") !== -1;
const hasTagExpFallback = url.indexOf("tag_exp=") !== -1;
const idMatch = url.match(/[?&](?:tid|id)=G-[A-Za-z0-9]+/);
return !!(hasGtmFallback && hasTagExpFallback && idMatch);
}
return false;
}
}
function buildDuplicateUrl(originalUrl) {
const id = getMeasurementId(originalUrl);
const dest = getDestinationForId(id);
if (!dest) return "";
const dst = getDuplicateEndpointUrl(dest);
try {
const src = new URL(originalUrl, location.href);
dst.search = src.search;
} catch (e) {
}
return dst.toString();
}
const context = {
debug: !!options.debug,
isTargetUrl,
buildDuplicateUrl
};
const interceptors = [
new FetchInterceptor(),
new XhrInterceptor(),
new BeaconInterceptor(),
new ScriptInterceptor()
];
for (let i = 0; i < interceptors.length; i++) {
try {
interceptors[i].install(context);
} catch (e) {
if (options.debug) console.error("GA4 Duplicator: failed to install interceptor", e);
}
}
window.__ga4DuplicatorInitialized = true;
};
})();
//# sourceMappingURL=ga4-duplicator.js.map
window.createGA4Duplicator({
server_container_url: trackingURL,
});
</script>
-
Configure the Trigger
- In the Triggering section, click to add a trigger
- Select Initialization - All Pages
- This ensures the code runs before any GA4 events fire
-
Set Up Tag Sequencing
- In the tag configuration, expand Advanced Settings
- Go to Tag Sequencing
- Under "Setup Tag", select your main Google Tag (your GA4 configuration tag)
- This ensures the GA4 tag loads first, then your duplicator runs
-
Save and Publish
- Save your tag
- Submit the changes and publish your container
- Test thoroughly to ensure both GA4 and your custom endpoint receive events
You can now test your web tracking setup.
Method 2: Redirect all GA4 requests
⚠️ Warning: This method will stop sending data to your GA4 property. Use only if you want to completely redirect tracking to your d8a instance.
Steps:
-
Open Google Tag Manager
- Navigate to your GTM container
- Go to the Tags section
-
Modify Your Google Tag
- Find your existing Google Tag (GA4 Configuration tag)
- Edit the tag configuration
-
Change the Server Container URL
- In the tag Configuration settings, add a new parameter called
server_container_url - Set its value to your d8a tracking domain, e.g.
https://example.org/noteGoogle automatically appends
/g/collectto this value, so do not include/g/collectinserver_container_url.
- In the tag Configuration settings, add a new parameter called
Testing your web tracking setup
-
Open Browser DevTools
- Go to Network tab
- Filter by "collect" or your endpoint domain
-
Trigger Test Events
- Navigate pages, click buttons, submit forms
- Watch for outgoing requests
-
Verify Data Flow
- Method 1: Check both GA4 and your endpoint receive data
- Method 2: Check only your endpoint receives data
-
Use GTM Preview Mode
- Enable Preview mode in GTM
- Verify tags fire in the correct sequence
- Check for any errors in the console
Method 3: Duplicate requests in Google Tag Manager server-side
This method duplicates GA4 requests at the server-side container level, which is useful for server-side tracking implementations.
This method assumes you already have a working GA4 client configured in your Google Tag Manager server-side container. If you haven't set up server-side GTM yet, please configure your GA4 client first.
Steps:
-
Create a User-Agent Variable
- Go to Variables in your server-side GTM container
- Click New to create a new variable
- Choose Request Header as the variable type
- Set the Variable Name to:
User-Agent - Set the Header Name to:
User-Agent - Save the variable
-
Create an X-Forwarded-For Variable
- Create another new variable
- Choose Request Header as the variable type
- Set the Variable Name to:
X-Forwarded-For - Set the Header Name to:
X-Forwarded-For - Save the variable
-
Add the JSON HTTP Request Tag Template
- Go to Templates in your server-side GTM container
- Click Search Gallery
- Search for and add the JSON HTTP request tag template from the community gallery
-
Create a New Tag
- Go to Tags and click New
- Choose the JSON HTTP request tag template you just added
- Configure the tag:
- Destination URL:
<your-d8a-instance-url>{{Request Path}}?{{Query String}}- Replace
<your-d8a-installation-url>with your d8a endpoint URL (e.g.,https://example.org) - For example, your completed Destination URL should look like:
https://example.org{{Request Path}}?{{Query String}}note{{Request Path}}and{{Query String}}are built-in variables available in server-side GTM.
- Replace
- Destination URL:
- In the Request Headers section, add two entries:
- First entry:
- Key:
User-Agent - Value:
{{User-Agent}}
- Key:
- Second entry:
- Key:
X-Forwarded-For - Value:
{{X-Forwarded-For}}
- Key:
- First entry:
-
Configure the Trigger
- In the Triggering section, select the same trigger that you use for your GA4 traffic. This ensures the tag fires whenever GA4 events are processed
-
Save and Publish
- Save your tag
- Submit the changes and publish your server-side container
Testing your server-side tracking setup
To test your server-side setup, use the debug mode in Google Tag Manager server-side. This enables you to verify that requests are duplicated correctly to your d8a endpoint. The final URL and d8a server response will be available in the Console tab.