Next.js integration
Two files. Grab your project ID from the dashboard
(Get your script) and replace
{YOUR_PROJECT_ID} below.
npm install @testa-soft/next1. Proxy
proxy.ts at the project root (or under src/). Buckets the visitor, writes
the cookie, and issues split-URL redirects before any HTML is sent.
// proxy.ts — Next.js 16
import type { NextFetchEvent, NextRequest } from "next/server";
import { createTestaProxy } from "@testa-soft/next";
const testa = createTestaProxy({
projectId: "{YOUR_PROJECT_ID}",
secureCookies: process.env.NODE_ENV === "production",
});
export function proxy(request: NextRequest, event: NextFetchEvent) {
return testa(request, event);
}Next.js 13–15: name the file
middleware.tsand the exportmiddleware.Pages Router? Same proxy, different client half — see Next.js (Pages Router).
Already have middleware? Put your logic in the handler option, or await the
proxy and use the response it returns.
2. Layout
// app/layout.tsx
import { TestaGuard, TestaProvider } from "@testa-soft/next/server";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<TestaGuard />
</head>
<body>
{children}
<TestaProvider projectId="{YOUR_PROJECT_ID}" />
</body>
</html>
);
}<TestaProvider/> is the client half of every experiment type: it applies HTML
changes, tracks goals, reports which variation the visitor saw, re-applies on
soft navigation, and decides for itself on the rare pageview the server
couldn't. Mount it even if you only run split-URL tests — that's how their
conversions get counted.
<TestaGuard/> hides the page until the variant is on it, and only renders when
this visitor has something to hide on this page — split-URL-only pages are never
hidden. It reveals itself after 4 seconds regardless, so a failed load can't
leave a page blank.
Done. That's a working integration.
Optional: faster publishes
By default the proxy caches the config for 60s and refreshes in the background. To pick up dashboard changes on the next page load instead:
createTestaProxy({ projectId: "{YOUR_PROJECT_ID}", cache: "per-pageload" });Goals
Create goals in the dashboard (Goals tracking). Page-view and click goals need no code. Custom-event goals fire from yours:
import { pushEvent } from "@testa-soft/next";
pushEvent("signup_completed", { plan: "pro" });Also window.testa.pushEvent(...) and window.Analytica.pushEvent(...). Each
goal counts once per visitor.
Analytics events
Whenever a visitor is shown a variation, Testa fires variation_applied in the
browser — subscribe to forward it wherever your analytics live.
Subscribe from a client component, in an effect. onVariationApplied
returns its own unsubscribe function, so returning it from the effect is the
whole cleanup — without that, Fast Refresh and StrictMode's double mount stack
duplicate handlers:
// app/components/TestaAnalytics.tsx
"use client";
import { onVariationApplied } from "@testa-soft/next";
import { useEffect } from "react";
export function TestaAnalytics(): null {
useEffect(
() =>
onVariationApplied((data) => {
// Send `data` wherever your analytics live.
console.log(data);
// {
// project_id: 123, // your project ID
// experiment: 456, // experiment ID
// variation: 1, // variation ID (0 = control)
// uuid: "0198f2c1-4b7a-7f3e-9d21-...", // the visitor's unique ID
// title: "Homepage Hero Test", // experiment name, when set
// url: "https://example.com/pricing", // page the variation applied on
// }
}),
[],
);
return null;
}Render it once, anywhere under your root layout.
experiment and variation are the identifiers you see in the dashboard —
variation 0 is always the control. title is present only when the experiment
has a name. Forward the whole object, or pick fields out of it:
// `track` here is whatever you already use — your analytics SDK, or your own fetch.
useEffect(
() =>
onVariationApplied((data) =>
track("Experiment Viewed", {
experiment: data.experiment,
variation: data.variation,
name: data.title,
}),
),
[],
);
// Or forward the payload as it comes.
useEffect(() => onVariationApplied((data) => track("Experiment Viewed", data)), []);Register as many handlers as you like. Two guarantees make this safe to wire into a tag manager:
- A handler registered after the event fired still receives it, so a slow-loading analytics script never misses an exposure.
- Each handler sees a given experiment/variation once, so re-renders and client-side navigations can't double-count.
Outside a bundle — GTM Custom HTML, or any plain <script> — use the global
instead. (window.testa is also importable as import { testa } from "@testa-soft/next" if you prefer a namespace to named imports; it is the same
two functions.)
<script>
window.testa.onVariationApplied(function (d) {
// send d to your analytics
});
</script>GTM dataLayer — pushed automatically on every exposure, no setup:
window.dataLayer.push({
event: "Analytica", // the trigger name
ExperimentId: 456,
ExperimentName: "Homepage Hero Test", // "" when the experiment has no name
VariationId: 1,
VariationName: "Variation1", // "Control" for variation 0
});Add a Custom Event trigger on Analytica.
The bucketing moment
onVariationAssigned fires when the visitor is bucketed, with the same
payload and the same subscribe-in-an-effect shape. In the browser it fires only when the client made the decision —
a cold proxy instance, or a soft navigation. When the proxy decided (the normal
case), the assignment happened server-side and the browser never sees it, so use
the proxy's own hook instead:
createTestaProxy({
projectId: "{YOUR_PROJECT_ID}",
onVariationAssigned(event, ctx) {
// event = { experimentId, variationId, title?, redirected, destinationUrl?,
// visitorId, url, firstAssignment }
// firstAssignment is true only on the visit that bucketed them.
ctx.waitUntil(warehouse.track(event));
},
});variation_applied is the one to use for exposure tracking — it fires wherever
the decision was made.