Client-side plumbing for Google Tag Manager, GDPR cookie consent, and A/B testing in UXF Next.js apps.
Use this package to manage the consent cookie, inject the GTM loader with a consent-aware default state, and assign/read A/B test variants. It is not an analytics dashboard or a data-collection SDK — event data still flows through GTM/GA that you configure yourself.
Each concern is a separate subpath import — there is no root @uxf/analytics entry:
@uxf/analytics/consent — read/write the cookie-consent cookie.@uxf/analytics/gtm — GTM bootstrap script and consent updates.@uxf/analytics/ab-testing — experiment assignment, provider, and hooks.yarn add @uxf/analytics
npm install @uxf/analytics
Peer dependencies: @uxf/core, @uxf/core-react, and react >=18.0.2.
@uxf/analytics/consent stores the four Google consent flags plus a version in a base64-encoded cookieConsent cookie (90-day TTL by default). Bump the version to invalidate old consent and re-prompt users.
Client-only — throws if called on the server.
import { storeConsentToCookie } from "@uxf/analytics/consent";
storeConsentToCookie(
{
ad_personalization: true,
ad_storage: false,
ad_user_data: false,
analytics_storage: false,
},
1, // version
);
import { readConsentFromCookie } from "@uxf/analytics/consent";
const consent = readConsentFromCookie();
// { ad_personalization?, ad_storage?, ad_user_data?, analytics_storage?, version }
Returns true only when all four flags are booleans and the stored version matches.
import { isConsentCookieSet } from "@uxf/analytics/consent";
const isSet = isConsentCookieSet(null, 1); // (ctx, version) -> boolean
useGtmScript reads the consent cookie, builds the inline bootstrap script (sets the gtag default consent state, then loads GTM for the given container id), and returns it as a string. Render it in your document <head> — not directly in _app.
import { useGtmScript } from "@uxf/analytics/gtm";
const gtmScript = useGtmScript("GTM-YOURID");
// in your head component
<script dangerouslySetInnerHTML={{ __html: gtmScript }} />;
Stores the consent to cookie and pushes a gtag consent: "update" call plus a consent_resolved event to the dataLayer. Client-only.
import { updateGtmConsent } from "@uxf/analytics/gtm";
updateGtmConsent(
{
ad_personalization: true,
ad_storage: false,
ad_user_data: false,
analytics_storage: false,
},
1, // version
);
A set of helpers, a provider, and hooks for running A/B tests in a Next.js app. Assignment happens in the proxy/middleware (a per-experiment uxf-experiment-* cookie), and variants are exposed to components through ABTestingProvider.
Use as const satisfies ExperimentConfig[] so useABTestingVariant can infer literal variant names.
import type { ExperimentConfig } from "@uxf/analytics/ab-testing";
export const experiments = [
{
id: "1",
traffic: 1,
variants: [
{ name: "Control", traffic: 0.5 },
{ name: "B", traffic: 0.5 },
],
},
{
id: "2",
traffic: 0.5,
variants: [
{ name: "Control", traffic: 0.5 },
{ name: "B", traffic: 0.5 },
],
},
] as const satisfies ExperimentConfig[];
proxy.tshandleABTesting sets a cookie per experiment (value not-participate for excluded users) and removes cookies for experiments no longer in the config.
import { handleABTesting } from "@uxf/analytics/ab-testing";
import { NextRequest, NextResponse } from "next/server";
import { experiments } from "./app/examples/ab-testing/constants";
export async function proxy(request: NextRequest) {
const nextResponse = NextResponse.next();
handleABTesting(request, nextResponse, experiments);
return nextResponse;
}
ABTestingProviderThe provider takes the assigned variants as [experimentId, variantName][] and fires an experience_impression GTM event on mount.
App Router — read the cookies via next/headers and map them with getExperimentsFromContext:
import { ABTestingProvider, getExperimentsFromContext } from "@uxf/analytics/ab-testing";
import { cookies } from "next/headers";
async function Layout(props: LayoutProps<"/examples/ab-testing">) {
return (
<ABTestingProvider
experiments={getExperimentsFromContext(
Object.fromEntries((await cookies()).getAll().map((v) => [v.name, v.value])),
)}
>
{props.children}
</ABTestingProvider>
);
}
export default Layout;
Pages Router — inject the variants in getServerSideProps with addExperimentsSSR, then read them from pageProps:
import { addExperimentsSSR } from "@uxf/analytics/ab-testing";
import type { GetServerSideProps } from "next";
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return addExperimentsSSR(ctx, { props: {} });
};
import { ABTestingProvider, AB_TESTING_VARIANT_PROP_NAME } from "@uxf/analytics/ab-testing";
export default function App({ Component, pageProps }) {
return (
<ABTestingProvider experiments={pageProps[AB_TESTING_VARIANT_PROP_NAME]}>
<Component {...pageProps} />
</ABTestingProvider>
);
}
useABTestingVariant returns the variant name, or null when the experiment id is unknown or the user does not participate. Client component only.
"use client";
import { useABTestingVariant } from "@uxf/analytics/ab-testing";
import type { experiments } from "./constants";
function Page() {
const variant = useABTestingVariant<typeof experiments>("1");
return <div>Experiment 1 variant: {variant}</div>; // "Control" | "B" | null
}
export default Page;
@uxf/analytics/consent| Export | Signature | Description |
|---|---|---|
storeConsentToCookie |
(consent: CookiesConsentType, version: number, cookieTtl?: number) => void |
Writes the consent cookie (default TTL 90 days). Throws on the server. |
readConsentFromCookie |
(ctx?: AnyObject | null) => CookieConsentTypeWithVersion |
Reads and decodes the consent cookie. Pass a request-like ctx to read server-side. |
isConsentCookieSet |
(ctx: AnyObject | null, version: number) => boolean |
true when all four flags are set and the stored version matches. |
CookiesConsentType |
{ ad_personalization?, ad_storage?, ad_user_data?, analytics_storage?: boolean } |
The four Google consent flags. |
CookieConsentTypeWithVersion |
CookiesConsentType & { version: number } |
Shape stored in the cookie. |
@uxf/analytics/gtm| Export | Signature | Description |
|---|---|---|
useGtmScript |
(gtmId: string) => string |
Builds the inline GTM bootstrap script (default consent from the cookie + container loader). |
updateGtmConsent |
(consent: CookiesConsentType, version: number) => void |
Stores consent and pushes gtag consent: "update" + consent_resolved. Client-only. |
ConsentType |
"granted" | "denied" |
gtag consent value. |
GtmConsentData |
type | gtag consent payload / event union. |
GtmDataLayer |
{ push: (gtmEventData: unknown) => void } |
window.dataLayer shape (augments Window). |
@uxf/analytics/ab-testing| Export | Signature | Description |
|---|---|---|
handleABTesting |
(request, response, experiments: ExperimentConfig[], options?: { domain?: string }) => void |
Proxy/middleware: assigns and prunes experiment cookies. |
getExperimentVariant |
(config: ExperimentConfig, randomNumberForTesting?: number | null) => ExperimentVariant | null |
Picks a variant by weighted traffic; null = not participating. |
getExperimentsFromContext |
(cookies: Partial<{ [key: string]: string }>) => [string, string][] |
Extracts [id, variant] pairs from a server cookie map. |
getExperimentsFromClient |
() => [string, string][] |
Extracts [id, variant] pairs from document.cookie. |
addExperimentsSSR |
(ctx, pageProps) => pageProps |
Pages Router getServerSideProps helper; injects variants under AB_TESTING_VARIANT_PROP_NAME. |
sendABTestingEvent |
(getExpVariantString?: GetExpVariantString) => void |
Pushes an experience_impression GTM event per experiment cookie. Client-only. |
ABTestingProvider |
(props: { children; experiments: [string, string][]; getExpVariantString? }) => JSX |
Provides variants to the tree and fires sendABTestingEvent on mount. |
useABTesting |
() => [string, string][] |
Returns all [id, variant] pairs from context. |
useABTestingVariant |
<Config extends ExperimentConfig[]>(experimentId) => variantName | null |
Returns the variant name for one experiment, or null. |
AB_TESTING_VARIANT_PROP_NAME |
"__AB_TESTING_VARIANT__" |
Page-prop key used by addExperimentsSSR. |
EXPERIMENT_COOKIE_PREFIX |
"uxf-experiment-" |
Prefix of every experiment cookie. |
ExperimentVariant |
{ name: string; traffic: number; label?: string } |
A single variant. |
ExperimentConfig |
{ id: string; traffic: number; variants: ExperimentVariant[] } |
One experiment. |
GetExpVariantString |
(cookie: { name: string; value: string }) => string |
Maps a cookie to the exp_variant_string sent to GTM. |
@uxf/analytics/consent, /gtm, or /ab-testing.storeConsentToCookie throws when window is undefined (server). updateGtmConsent and sendABTestingEvent also run on the client only.readConsentFromCookie works on both sides — pass a request-like ctx to read server-side; omit it on the client.ABTestingProvider, useABTesting, and useABTestingVariant are client components ("use client").cookieConsent cookie; the version argument lets you re-request consent — isConsentCookieSet returns false on a version mismatch.uxf-experiment-; excluded users get the value not-participate, so a variant is only meaningful when it matches a configured variants[].name.experiments prop is [experimentId, variantName][] — build it with getExperimentsFromContext (server) or getExperimentsFromClient (client), not the raw ExperimentConfig[].