feat(bf): fire the trial conversion behind the same consent gate

Adds the second Google Ads label to /odoo-black-friday/: the free-trial
form (#trialForm) now fires AW-18065522009/AtAPCIjjvrAdENn6p6ZD (action
7886320008 "Registrierung") when it is submitted.

Reuses the existing gate in static/ads-consent.js rather than a second
mechanism: the label is read from the page (data-ads-trial-send-to on
#adsConsent), so the script stays generic. The page hands the gate its
real submit function via window.o4pAds.trialConversion; the gate fires
the conversion and runs the submit from the event callback, with the same
700 ms hard timeout that runs it anyway. Nothing fires and no cookie is
set before consent (identical default-deny gate).

The buy conversion is untouched (comments only).
This commit is contained in:
webmaster committed 2026-11-02 09:23:14 +00:00
1 parent 6d486ad954
commit 7497fbf588
3 files changed
+88 -28

No files matched your search

+4 -1
View File
@@ -1063,7 +1063,9 @@
<!-- Google Ads consent banner — default DENY. The tag loads only after "Accept".
Markup lives in the page; JS (static/ads-consent.js) reveals it and drives it.
With JavaScript off it stays hidden and the native buy forms submit untouched.
Two conversions are wired here: the buy forms (data-ads-send-to) and the free-trial
form (data-ads-trial-send-to). With JavaScript off it stays hidden and both actions
(the native buy POSTs, the trial's own submit) run untouched.
See wiki/concepts/conversion-tracking-contract. -->
<div
id="adsConsent"
@@ -1072,6 +1074,7 @@
aria-label="Advertising cookie consent"
data-ads-id="AW-18065522009"
data-ads-send-to="AW-18065522009/vnDUCPDxt7AdENn6p6ZD"
data-ads-trial-send-to="AW-18065522009/AtAPCIjjvrAdENn6p6ZD"
hidden
>
<div class="ads-consent-inner">
+53 -8
View File
@@ -9,20 +9,24 @@
* _gcl_au / _gcl_aw. So nothing loads and no request fires until the visitor accepts.
* The default is DENY. Declining genuinely prevents the tag from loading.
*
* WHAT THE CONVERSION IS: a CLICK PROXY, not a sale. It fires when a buy form on this page
* is submitted. The real purchase is measured on the checkout host (20.odoo4projects.com)
* by odoo-dev. See wiki/concepts/conversion-tracking-contract. Never label it a sale.
* WHAT THE CONVERSIONS ARE: both are CLICK PROXIES, not sales.
* - a buy form on this page (POST /shop/post_basket) -> data-ads-send-to
* - the free-trial form (#trialForm, the n8n webhook) -> data-ads-trial-send-to
* The real purchase is measured on the checkout host (20.odoo4projects.com) by odoo-dev.
* See wiki/concepts/conversion-tracking-contract. Never label either a sale.
*
* SAFETY: a conversion tag must never swallow a buy. On submit we fire the event and submit
* the form from the event callback, with a hard timeout that submits anyway. With JavaScript
* off the native POST forms submit untouched (no banner, no conversion, no error).
* SAFETY: a conversion tag must never swallow an action. On submit we fire the event and run the
* action from the event callback, with a hard timeout that runs it anyway. For a buy form the
* action is form.submit(); for the trial form the page hands us its send function via
* window.o4pAds.trialConversion. With JavaScript off the native POST forms submit untouched
* (no banner, no conversion, no error) and the trial form does its native thing.
*/
(function () {
"use strict";
var STORE_KEY = "o4p_ads_consent"; // "granted" | "denied"
var SUBMIT_FALLBACK_MS = 700; // hard cap: submit even if the tag never answers
var cfg = null; // { id, sendTo } — read from the banner, never hard-coded
var cfg = null; // { id, sendTo, trialSendTo } — from the banner, never hard-coded
/* ---- the visitor's stored choice ---- */
function readChoice() {
@@ -79,7 +83,28 @@
document.head.appendChild(s);
}
/* ---- fire one conversion, then run the action from the callback or the hard timeout ---- */
function fireConversion(sendTo, action) {
var done = false;
function run() {
if (done) return;
done = true;
action(); // the action always runs: at most SUBMIT_FALLBACK_MS after the event
}
try {
window.gtag("event", "conversion", {
send_to: sendTo,
event_callback: run,
});
} catch (err) {
run();
}
window.setTimeout(run, SUBMIT_FALLBACK_MS);
}
/* ---- the conversion proxy, fired on a buy-form submit ---- */
/* NOTE: the buy path below is deliberately left byte-identical to what shipped and was
* verified on 2026-11-02 — its label, its one mark and its 700 ms fallback are unchanged. */
function onSubmit(e) {
var form = e.target;
if (!form || form.tagName !== "FORM") return;
@@ -141,7 +166,11 @@
var id = banner.getAttribute("data-ads-id");
var sendTo = banner.getAttribute("data-ads-send-to");
if (!id || !sendTo) return;
cfg = { id: id, sendTo: sendTo };
cfg = {
id: id,
sendTo: sendTo,
trialSendTo: banner.getAttribute("data-ads-trial-send-to"),
};
var choice = readChoice();
if (choice === "granted") {
@@ -165,6 +194,22 @@
}
}
/* ---- the trial conversion: the page hands us its real submit function ---- *
* Called by /static/black-friday.js when #trialForm is submitted. With consent we fire the Ads
* conversion and run the page's submit from the callback; a blocked/slow tag is covered by the
* hard timeout, so the trial always completes. Without consent (or without the tag) we run the
* submit immediately and fire nothing — the trial is never gated on the tag.
*/
window.o4pAds = window.o4pAds || {};
window.o4pAds.trialConversion = function (action) {
if (typeof action !== "function") return;
if (!cfg || !cfg.trialSendTo || !isGranted() || typeof window.gtag !== "function") {
action();
return;
}
fireConversion(cfg.trialSendTo, action);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
+31 -19
View File
@@ -241,25 +241,37 @@
var location = trialLoc ? trialLoc.value : '';
if (!email) return;
var btn = trialForm.querySelector('.btn-brass');
btn.textContent = 'Sending...';
btn.disabled = true;
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(UTM.inject({ email: email, location: location }))
}).then(function () {
if (window.plausible) window.plausible('Trial');
trialForm.style.display = 'none';
document.getElementById('trialSuccess').classList.add('visible');
}).catch(function () {
btn.textContent = 'Send';
btn.disabled = false;
var errorMsg = document.createElement('div');
errorMsg.className = 'trial-error visible';
errorMsg.textContent = 'Something went wrong. Please try again.';
trialForm.appendChild(errorMsg);
setTimeout(function () { errorMsg.classList.remove('visible'); }, 3000);
});
function sendTrial() {
btn.textContent = 'Sending...';
btn.disabled = true;
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/trial', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(UTM.inject({ email: email, location: location }))
}).then(function () {
if (window.plausible) window.plausible('Trial');
trialForm.style.display = 'none';
document.getElementById('trialSuccess').classList.add('visible');
}).catch(function () {
btn.textContent = 'Send';
btn.disabled = false;
var errorMsg = document.createElement('div');
errorMsg.className = 'trial-error visible';
errorMsg.textContent = 'Something went wrong. Please try again.';
trialForm.appendChild(errorMsg);
setTimeout(function () { errorMsg.classList.remove('visible'); }, 3000);
});
}
/* When the consent gate is present it fires the Google Ads trial conversion and runs
sendTrial from the tag's callback, with a hard timeout that runs it anyway — so a
blocked or slow tag can never swallow the trial. Without the gate, send directly. */
if (window.o4pAds && typeof window.o4pAds.trialConversion === 'function') {
window.o4pAds.trialConversion(sendTrial);
} else {
sendTrial();
}
});
}
})();