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 with client-side GTM, Method 3 is for web tracking with direct gtag (no GTM), and Method 4 is for server-side tracking (server-side GTM).
Method 1: Duplicate GA4 requests in GTM (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:
D8A GA4 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-02-19T22:31:19.302Z */
"use strict";
(() => {
// src/version.ts
var version = "dev-26-02" ? "dev-26-02" : devVersionUtc();
// 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);
const convertToGet = ctx.getConvertToGet(requestUrl);
if (upperMethod === "GET") {
originalFetch(duplicateUrl, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug) console.error("gtm interceptor: error duplicating GET fetch:", error);
});
} else if (upperMethod === "POST") {
prepareBodyPromise.then((dupBody) => {
if (convertToGet) {
let bodyStr = "";
if (typeof dupBody === "string") {
bodyStr = dupBody;
} else if (dupBody instanceof Blob) {
originalFetch(duplicateUrl, {
method: "POST",
body: dupBody,
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating POST fetch (convert_to_get with Blob):",
error
);
});
return;
}
const lines = bodyStr.split("\n");
let sentAny = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line) {
const mergedUrl = ctx.buildDuplicateUrl(requestUrl);
const urlWithMergedLine = mergeBodyLineWithUrl(mergedUrl, line);
originalFetch(urlWithMergedLine, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating GET fetch (from convert_to_get):",
error
);
});
sentAny = true;
}
}
if (!sentAny) {
originalFetch(duplicateUrl, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating GET fetch (empty body convert_to_get):",
error
);
});
}
} else {
originalFetch(duplicateUrl, {
method: "POST",
body: dupBody,
keepalive: true,
credentials: "include"
}).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);
const convertToGet = ctx.getConvertToGet(this._requestUrl);
if (method === "GET") {
fetch(duplicateUrl, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug) console.error("gtm interceptor: error duplicating GET xhr:", error);
});
} else if (method === "POST") {
if (convertToGet) {
let bodyStr = "";
if (typeof body === "string") {
bodyStr = body;
} else if (body && typeof body === "object") {
try {
bodyStr = String(body);
} catch (e) {
bodyStr = "";
}
}
const lines = bodyStr.split("\n");
let sentAny = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line) {
const mergedUrl = ctx.buildDuplicateUrl(this._requestUrl);
const urlWithMergedLine = mergeBodyLineWithUrl(mergedUrl, line);
fetch(urlWithMergedLine, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating GET xhr (from convert_to_get):",
error
);
});
sentAny = true;
}
}
if (!sentAny) {
fetch(duplicateUrl, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating GET xhr (empty body convert_to_get):",
error
);
});
}
} else {
fetch(duplicateUrl, {
method: "POST",
body,
keepalive: true,
credentials: "include"
}).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 {
const duplicateUrl = ctx.buildDuplicateUrl(url);
const convertToGet = ctx.getConvertToGet(url);
if (convertToGet) {
let bodyStr = "";
if (typeof data === "string") {
bodyStr = data;
} else if (data && typeof data === "object") {
try {
bodyStr = String(data);
} catch (e) {
bodyStr = "";
}
}
const lines = bodyStr.split("\n");
let sentAny = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line) {
const mergedUrl = ctx.buildDuplicateUrl(url);
const urlWithMergedLine = mergeBodyLineWithUrl(mergedUrl, line);
fetch(urlWithMergedLine, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating GET beacon (from convert_to_get):",
error
);
});
sentAny = true;
}
}
if (!sentAny) {
fetch(duplicateUrl, {
method: "GET",
keepalive: true,
credentials: "include"
}).catch((error) => {
if (ctx.debug)
console.error(
"gtm interceptor: error duplicating GET beacon (empty body convert_to_get):",
error
);
});
}
} else {
originalSendBeacon.call(navigator, duplicateUrl, 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,
credentials: "include"
}).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++) {
const dest = options.destinations[i];
destinations.push({
measurement_id: dest.measurement_id,
server_container_url: dest.server_container_url,
convert_to_get: dest.convert_to_get !== void 0 ? dest.convert_to_get : options.convert_to_get
});
}
}
if (options.server_container_url) {
destinations.push({
measurement_id: "*",
server_container_url: options.server_container_url,
convert_to_get: options.convert_to_get
});
}
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 ensureBareQueryFlag(url, flag) {
const u = String(url || "");
const f = String(flag || "").trim();
if (!u || !f) return u;
const hashIdx = u.indexOf("#");
const beforeHash = hashIdx >= 0 ? u.slice(0, hashIdx) : u;
const hash = hashIdx >= 0 ? u.slice(hashIdx) : "";
const qIdx = beforeHash.indexOf("?");
const base = qIdx >= 0 ? beforeHash.slice(0, qIdx) : beforeHash;
const rawQuery = qIdx >= 0 ? beforeHash.slice(qIdx + 1) : "";
const parts = rawQuery ? rawQuery.split("&").map((p) => p.trim()).filter(Boolean) : [];
const kept = [];
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === f) continue;
if (p.startsWith(f + "=")) continue;
kept.push(p);
}
const newQuery = kept.length > 0 ? kept.join("&") + "&" + f : f;
return base + "?" + newQuery + hash;
}
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 mergeBodyLineWithUrl(originalUrl, bodyLine) {
try {
const url = new URL(originalUrl, location.href);
const lineParams = new URLSearchParams(bodyLine);
for (const [key] of lineParams.entries()) {
url.searchParams.delete(key);
}
for (const [key, value] of lineParams.entries()) {
url.searchParams.append(key, value);
}
return ensureBareQueryFlag(url.toString(), "richsstsse");
} catch (e) {
const urlWithoutQuery = originalUrl.split("?")[0];
const originalParams = originalUrl.match(/\?(.*)/) ? originalUrl.match(/\?(.*)/)[1] : "";
const merged = originalParams + (originalParams && bodyLine ? "&" : "") + bodyLine;
return ensureBareQueryFlag(urlWithoutQuery + (merged ? "?" + merged : ""), "richsstsse");
}
}
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;
dst.searchParams.set("_dtv", version);
dst.searchParams.set("_dtn", "gd");
} catch (e) {
}
return ensureBareQueryFlag(dst.toString(), "richsstsse");
}
function getConvertToGet(url) {
const id = getMeasurementId(url);
const dest = getDestinationForId(id);
return dest ? !!dest.convert_to_get : false;
}
const context = {
debug: !!options.debug,
isTargetUrl,
buildDuplicateUrl,
getConvertToGet
};
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=gd.js.map
window.createGA4Duplicator({
server_container_url: trackingURL,
});
</script>
-
Save the tag
- Save your D8A GA4 Duplicator tag so it is available when you configure the Google Tag in the next step
- Note: You do not need to add a trigger to this tag
-
Fire the duplicator before the Google Tag
- Open your Google Tag (your GA4 configuration tag)
- Expand Advanced Settings → Tag Sequencing
- Under Fire a tag before Google Tag Name fires, select your D8A GA4 Duplicator tag
-
Save and Publish
- Save your tag
- Submit the changes and publish your container
You can now test your web tracking setup.
Method 2: Redirect all GA4 requests in GTM
⚠️ 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
Method 3: Duplicate GA4 requests with gtag directly
This method duplicates requests at the Google Tag (gtag) level by adding service_container_url as an additional destination.
This method assumes gtag is already installed directly in your HTML (without web GTM).
Steps:
-
Confirm your direct
gtagsetup- Verify the standard Google tag snippet is already present in your page (for example, in
<head>)
- Verify the standard Google tag snippet is already present in your page (for example, in
-
Keep your existing GA4 config call
- Leave your primary
gtag('config', 'G-XXXXXXX')call unchanged
- Leave your primary
-
Add a second config call with
service_container_url- Add another
gtag('config', ...)call for the same Google tag ID - Set
service_container_urlto your d8a endpoint URL, for example:https://global.t.d8a.tech/13370000-1984-0042-0069-c0ffee123456/g/collect - Example:
- Add another
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
// Existing GA4 destination
gtag("config", "G-XXXXXXX");
// Additional destination for duplicated traffic
gtag("config", "<property_id>", {
service_container_url: "https://global.t.d8a.tech/<property_id>/g/collect",
});
</script>
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
- Method 3: Check GA4 traffic is duplicated to your
service_container_urlendpoint
-
Optional: Use GTM Preview Mode (Methods 1 and 2 only)
- Enable Preview mode in GTM
- Verify tags fire in the correct sequence
- Check for any errors in the console
Method 4: 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.
Prefer a native tracker instead?
If you do not want to intercept existing GA4 tracking and prefer a setup that is independent from Google, use our dedicated gtag-compatible web tracker: Web tracker.