This is as technical as it gets. After this, there should be no more scary setup work on your side. We
give you the code you need, and we can help along the way at any step if you want us to.
Once you are accepted as a user on FreeCROTool.com, a tag needs to be implemented on each page of your
site. This allows testing changes to happen and lets us track the paths visitors take after they have seen
a control or variant.
We can help with the implementation, and the platform provides the code so it can be copied and pasted
easily.
Ideally, the tag should be added exactly as supplied and placed as close to the top of the <head>
section as possible. If it must be injected asynchronously, we recommend adding a pagehide script too,
and we can help with that.
On Shopify, WordPress and many similar platforms, this can usually be done in one place and that is it -
all done. For example, that might be a theme file such as Shopify's theme.liquid, a site-wide header
include, or a single custom code area in WordPress.
2
Set up Google Analytics once
For Google Analytics reporting to work properly, a small set of custom definitions
must be added in Admin → Custom definitions. This must be done early and only once.
If the definitions are missed, FreeCROTool reporting will not be able to pull in the specific testing
events that make the reports easier to read.
3
Shopify optional but recommended
If you are on Shopify and want tracking on the checkout or thank you page, we usually need a small extra
piece of code there as well, because Shopify checkout testing is limited.
Without that extra piece, confirmation and order value tracking may not be available.
Shopify checkout tracking details
This is only needed for Shopify stores that want confirmation and order value tracking. If you do not
need checkout tracking, you can ignore this.
The GA_MEASUREMENT_ID comes from Google Analytics 4. Open your GA4 property, go to
Admin → Data streams, open your web stream, and copy the Measurement ID that starts
with G-. The GA_API_SECRET also comes from that same web stream, under
Measurement Protocol API secrets, where you create a new secret string.
const GA_MEASUREMENT_ID = "**G-??????????**"; // Your Google Analytics Measurement ID
const GA_API_SECRET = "**A random string of characters**"; // A secret key that we need to make in GA
const DEFAULT_CURRENCY = "GBP";
const ENABLE_GA_DEBUG_MODE = true;
function safeParseJSON(value, fallback) {
try {
return JSON.parse(value);
} catch (e) {
return fallback;
}
}
function makeClientId() {
return String(Math.floor(Math.random() * 1e10)) + "." + String(Math.floor(Date.now() / 1000));
}
async function getOrCreateClientId(browser) {
const existing = await browser.localStorage.getItem("fcro_ga4_client_id");
if (existing) return existing;
const created = makeClientId();
await browser.localStorage.setItem("fcro_ga4_client_id", created);
return created;
}
async function getOrCreateSessionId(browser) {
const existing = await browser.localStorage.getItem("fcro_ga4_session_id");
if (existing) return existing;
const created = String(Math.floor(Date.now() / 1000));
await browser.localStorage.setItem("fcro_ga4_session_id", created);
return created;
}
async function getActiveAssignments(browser) {
const raw = await browser.cookie.get("_cro_active_tests");
if (!raw) return [];
const parsed = safeParseJSON(decodeURIComponent(raw), []);
if (!Array.isArray(parsed)) return [];
return parsed
.map((x) => ({
testID: String((x && x.testID) || ""),
variantNumber: String((x && x.variantNumber) || ""),
status: String((x && x.status) || "")
}))
.filter((x) => x.testID.match(/^[0-9]+$/) && x.variantNumber.match(/^[0-9]+$/));
}
async function wasAlreadySent(browser, transactionId, testID) {
const key = "fcro_purchase_sent_" + transactionId + "_" + testID;
const existing = await browser.localStorage.getItem(key);
if (existing === "1") return true;
await browser.localStorage.setItem(key, "1");
return false;
}
async function sendGa4Event(clientId, eventName, params) {
const url =
"https://www.google-analytics.com/mp/collect?measurement_id=" +
encodeURIComponent(GA_MEASUREMENT_ID) +
"&api_secret=" +
encodeURIComponent(GA_API_SECRET);
const payload = JSON.stringify({
client_id: clientId,
events: [{ name: eventName, params }]
});
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
try {
var beaconBody = new Blob([payload], { type: "text/plain;charset=UTF-8" });
var sent = navigator.sendBeacon(url, beaconBody);
if (sent) {
return;
}
} catch (e) {
}
}
await fetch(url, {
method: "POST",
mode: "no-cors",
keepalive: true,
headers: { "Content-Type": "text/plain;charset=UTF-8" },
body: payload
});
}
function buildGaEventParams(baseParams, sessionId) {
const params = Object.assign({}, baseParams || {});
params.session_id = String(sessionId || "");
params.engagement_time_msec = Number(params.engagement_time_msec || 1);
if (ENABLE_GA_DEBUG_MODE) {
params.debug_mode = 1;
}
return params;
}
function subscribeToCheckoutCompleted(deps) {
var analytics = deps.analytics;
var browser = deps.browser;
if (!analytics || typeof analytics.subscribe !== "function") {
throw new Error("Shopify analytics API is not available in this runtime.");
}
if (!browser || !browser.localStorage || !browser.cookie) {
throw new Error("Shopify browser API is not available in this runtime.");
}
analytics.subscribe("checkout_completed", async (event) => {
const clientId = await getOrCreateClientId(browser);
const sessionId = await getOrCreateSessionId(browser);
const assignments = await getActiveAssignments(browser);
if (!assignments.length) return;
const checkout = (event && event.data && event.data.checkout) || {};
const amount =
Number(
(checkout.totalPrice && checkout.totalPrice.amount) ||
(checkout.total_price && checkout.total_price.amount) ||
0
) || 0;
const currency =
String(checkout.currencyCode || checkout.currency_code || DEFAULT_CURRENCY).toUpperCase();
const transactionId = String(
(checkout.order && (checkout.order.id || checkout.order.name)) ||
checkout.orderId ||
checkout.id ||
""
);
for (const a of assignments) {
if (transactionId) {
const dup = await wasAlreadySent(browser, transactionId, a.testID);
if (dup) continue;
}
try {
await sendGa4Event(clientId, "freecro_purchase", {
...buildGaEventParams({
name: "confirmation",
value: amount,
currency: currency,
transaction_id: transactionId,
fcro_eID: a.testID,
fcro_vNum: a.variantNumber,
fcro_vID: a.testID + "v" + a.variantNumber,
fcro_status: a.status || ""
}, sessionId)
});
} catch (e) {
console.error("freecro_purchase send failed", e);
}
}
});
}
if (typeof register === "function") {
register(({ analytics, browser }) => {
subscribeToCheckoutCompleted({ analytics, browser });
});
} else if (typeof analytics !== "undefined" && typeof browser !== "undefined") {
subscribeToCheckoutCompleted({ analytics, browser });
} else {
throw new Error("Shopify pixel APIs were not found. Run this inside a Shopify pixel context.");
}
4
We can help with both
We can help with the tag implementation and the Google Analytics setup. To reduce errors, we recommend
copying and pasting the details exactly as provided.
If you need support, we can guide you through the steps or point you to the exact code to copy.
Google Analytics custom definitions
Add these in GA Admin → Custom definitions. Ignore the last changed date column below; the
important parts are the parameter name, the friendly label and the event parameter.
Dimension name
Friendly name / Description
Scope
Event parameter
fcro_event_name
FreeCRO event name
Event
name
fcro_event_value
FreeCRO event value (for revenue)
Event
value
fcro_exp_and_variant
FreeCRO Experiment(v)VariantNumber
Event
fcro_vID
fcro_experiment_id
FreeCRO Experiment ID
Event
fcro_eID
fcro_impression_path
FreeCRO Page path where experiment was entered
Event
fcro_path
fcro_variant_number
FreeCRO Variant Number
Event
fcro_vNum
We can help you set these up, and we strongly suggest copying and pasting the values directly to avoid
mistakes.
No data will appear in reports without doing the above, so this is quite important, if you need help with this let us know and we'll setup a meeting to help :)
Example code snippet
This is the type of tag the platform provides for copying into your site:
All testing platforms need something on your site so they can run the test. There is no way to make it
work without adding the snippet, but we keep that part as simple as possible.
The tag should be on every page where visitors may travel during or after a test.
The correct GA4 property should be selected before reporting is expected to work.
Some GA4 data can take time to appear, so first reports may not be immediate.
Need help getting set up?
If you already have access to your site and GA account, you may be very close to going live. We can help
make the setup simple and keep the copy/paste steps accurate.