Sanity AB testing compared: plugin, GrowthBook, and Croct


Sanity is a great CMS. The structured content model is genuinely well designed, the developer tooling is excellent, and the first-party @sanity/personalization-plugin does exactly what a content tool should do: it gives editors a clean way to author variants without inventing their own conventions.

It also ships with a direct GrowthBook integration, and GrowthBook has published a detailed guide for wiring the two together. That is the documented, recommended path, and it is the reason GrowthBook comes up in almost every conversation about Sanity experimentation.

So this comparison isn't about whether GrowthBook is good. It is, and for some teams it is the right answer. This article is actually about how much of the experimentation pipeline each option covers, and where the remaining work lands.

The three options at a glance

There are really three paths, not two. The plugin on its own is a legitimate choice if you are happy to build the delivery layer. Adding GrowthBook closes the biggest gap in it. Croct replaces the stack with a single layer.

Sanity plugin aloneSanity + GrowthBookCroct
Variant content authored in Sanity✅✅Dynamic content in Croct, default in Sanity
Variant assignment and bucketing❌✅✅
Same-device stickiness❌✅✅
Cross-device stickiness❌✅✅
Exposure tracking❌❌✅
Statistical engine❌✅✅
Analytics destination❌❌✅
GranularityField and documentField and documentComponent or module
No schema changes❌❌✅
Server-side, zero-flicker delivery✅✅✅
Real-time behavioral personalization❌❌✅
Built-in user profiles❌❌✅
Launch a new tests without a developer❌❌✅
Go beyond simple AB testing and feature flags

Test individual elements or full journeys with advanced AI-powered segmentation, great performance, no-code rollouts, and flicker-free experiments.

Experimentation workflow

The useful question is how much of the pipeline each option hands you finished.

StepSanity plugin aloneSanity + GrowthBookCroct
Experiment and variant configuration✅✅✅
Variant content definition✅✅✅
Variant assignment and bucketing❌✅✅
Same-device stickiness❌✅✅
Cross-device stickiness❌✅✅
Exposure tracking❌❌✅
Metric definitions❌✅ (against your warehouse)✅
Statistical analysis❌✅✅

Sanity plugin alone

Sanity is upfront about the boundary in its own documentation: for experiments to work, your frontend must assign users to variants and pass the correct variant ID when querying content.

That is an honest and defensible scope for a content tool. It also means that choosing the plugin on its own commits you to building identity, bucketing, persistence, exposure tracking, metric definitions, and the statistics. That is not a plugin integration. That is owning an experimentation platform.

Sanity plugin + GrowthBook

This is the path most Sanity teams end up on, and it closes the biggest gap. You hover a field in Studio, click the flask icon, and link it to an experiment you created in GrowthBook. GrowthBook handles bucketing and the statistics, and the statistics are genuinely excellent. It offers warehouse-native analysis, sequential testing, CUPED variance reduction, and bandits on the Pro tier.

What it does not close is the measurement plumbing.

Following GrowthBook's guide, you still write a lib/growthbookServer.ts with a cache-tagged fetch override, a middleware.ts that sets a gb-user-id cookie, and a tracking component.

That tracking component is where the gap shows: the guide's onExperimentView callback logs to the console, with a comment telling you to replace it with your own event tracking. The guide also lists metrics setup as a next step rather than part of the walkthrough.

Exposure is the denominator of every result you will ever report. It is left as an exercise.

So the realistic stack is Sanity for content, GrowthBook for assignment and statistics, an analytics platform or warehouse for events, and your own code holding the three together.

Croct

Croct sits alongside Sanity rather than inside it. Your component fetches a Croct slot and passes the Sanity content in as the fallback and default. When an experience or experiment targets the visitor, Croct returns the variant. When nothing is running, your Sanity content renders unchanged.

Assignment, same-device and cross-device stickiness, exposure tracking, conversion attribution, and the statistics are all handled. There is no separate analytics tool to wire up and no CDP to buy before the first audience works.

Go beyond simple AB testing and feature flags

Integration and code customization

Marketing agility is mostly a function of how much custom code sits between a hypothesis and a live test.

Sanity plugin aloneSanity + GrowthBookCroct
No Sanity schema changes❌❌✅
No GROQ query changes❌❌✅
One-time SDK setup❌✅✅
Built-in experiment setup❌❌✅
Built-in bucketing❌✅✅
Built-in exposure tracking❌❌✅
Built-in statistics❌✅✅

Croct

The setup is one command. The CLI detects your project, installs the SDK, and wires the provider and middleware.

1
npx croct@latest init

Then each component you want to make dynamic fetches a slot and passes its Sanity content as the fallback:

12345678910
import {fetchContent} from '@croct/plug-next/server';
export async function HomeHero() {  const sanityHero = await getSanityHero();  const {content} = await fetchContent('home-hero', {    fallback: sanityHero,  });
  return <Hero {...content} />;}

If you want Sanity to keep serving the default and only defer to Croct when something is actually running, check where the content came from:

123456
const {content, metadata} = await fetchContent('home-hero', {  fallback: sanityHero,});
// Render Sanity content unless Croct returned dynamic contentconst hero = metadata?.contentSource === 'slot' ? sanityHero : content;

That is the whole integration. Your schemas are untouched, your GROQ query stays a content query, and rolling back means deleting a line.

Use Claude Code, Cursor, and Codex to build with Croct.

Sanity plugin + GrowthBook

Four pieces of setup, then recurring work per experiment.

Plugin configuration

12345
fieldLevelExperiments({  fields: ["string", "image"],  environment: "<SPECIFY_ENVIRONMENT>",  project: "<SPECIFY_PROJECT_ID>",}),

Schema changes

Every field you want to test changes type, permanently:

1
defineField({ name: "title", type: "experimentString" })

Each experiment field then stores a default value plus a variants array, where each entry carries an experimentId, a variantId, and a value.

Identity

Assignment needs a stable ID, so middleware sets a cookie:

123456789101112131415161718192021222324252627282930313233343536373839
import { NextResponse } from "next/server";import type { NextRequest } from "next/server";
// Ensure a UUID cookie is always set to enable experimentationexport const GB_UUID_COOKIE = "gb-user-id";
export async function middleware(request: NextRequest) {  // Generate a UUID if it doesn't exist yet  let uuid = request.cookies.get(GB_UUID_COOKIE)?.value;  let needsUpdate = false;  if (!uuid) {    uuid = crypto.randomUUID();    needsUpdate = true;
    // Set the cookie on the current request    request.cookies.set(GB_UUID_COOKIE, uuid);  }
  // Forward the request to the server  const response = NextResponse.next({    request: {      headers: new Headers(request.headers),    },  });
  // Add the newly created UUID to the response headers to persist in the browser  if (needsUpdate) {    response.cookies.set(GB_UUID_COOKIE, uuid);  }
  return response;}
// Run for all pages that need experimentsexport const config = {  matcher: [    '/((?!api|_next/static|_next/image|favicon.ico|studio).*)',  ],};

Clearing that cookie reassigns the visitor. Carrying identity across devices means supplying your own user ID and, for guaranteed stability, GrowthBook's sticky bucketing, which is a Pro feature.

Variant resolution

On the server, you read the flag and thread the result into the Sanity query:

1
const variation = gb.getFeatureValue("flag-sony-a7-iv", "default");

The GROQ query then uses coalesce to select the matching variant and fall back to the default.

This is where field-level granularity starts to cost. GrowthBook's own example tests three fields on a product page (title, description, and image) and threads six parameters through the query: titleExperiment, titleVariant, descriptionExperiment, descriptionVariant, imageExperiment, and imageVariant.

Three fields, six parameters, one page, on the documented happy path. A hero with a headline, subheadline, image, and button label is four fields that must be varied and queried in lockstep to make one coherent variant.

Exposure tracking

1234567891011121314151617181920212223242526
"use client";
import type { TrackingCallback, TrackingData } from "@growthbook/growthbook";import { useEffect } from "react";
export const onExperimentView: TrackingCallback = (experiment, result) => {
  // Example: Google Analytics 4  if (typeof window !== 'undefined' && window.gtag) {    window.gtag('event', 'experiment_view', {      experiment_id: experiment.key,       variant_id: result.key,     });   }};
// Helper component to track experiment views from server componentsexport function GrowthBookTracking({ data }: { data: TrackingData[] }) {  useEffect(() => {    data.forEach(({ experiment, result }) => {      onExperimentView(experiment, result);    });  }, [data]);
  return null;}

This is the piece to budget for. Everything above it is configuration. This one is an integration with whatever analytics system you intend to measure against.

Developer dependency

Testing velocity is usually capped by sprint cadence rather than by ideas, so the question of who can ship a test matters more than any individual feature.

Sanity plugin aloneSanity + GrowthBookCroct
Initial setup requires engineering✅✅✅
New tests on an integrated module without a developer❌❌✅
New tests on a new module without a developer❌❌❌
Adding a field to an existing test without a developer❌❌❌
Traffic allocation changes without a developer❌✅✅
Conversion tracking changes without a developer❌❌✅

The honest version of this table: with the plugin, every new test means a developer adds variant fields to a schema, threads parameters through a query, and ships a deploy. With Croct, that cost is paid once per component rather than once per test, which means that a surface that has never been made dynamic still needs a developer to add the slot.

Availability and cost

Sanity personalization plugin is free and open source. Works on any Sanity plan.

GrowthBook offers a free cloud Starter tier that covers 3 users and 1 project with unlimited flags, experiments, and traffic. Pro is US$40 per seat per month for up to 30 users and unlocks sticky bucketing, sequential testing, CUPED, and bandits. The open-source edition is free to self-host. Analysis is warehouse-native, so a warehouse is a prerequisite rather than an optional extra.

Croct is free up to 10,000 monthly active users with AB testing, Bayesian analysis, and personalization included. Growth starts at US$1,200 per year for 20,000 MAU, and it works on any Sanity plan.

Worth noting the cost that isn't on a pricing page: the GrowthBook path assumes you already run a data warehouse and an analytics pipeline. If you do, that is free leverage. If you don't, it is the largest line item in the comparison.

Beyond AB testing: the path to personalization

Most teams start with AB testing and end up wanting targeted experiences. The three options diverge sharply here.

Sanity plugin aloneSanity + GrowthBookCroct
A/B/n testingContent only✅✅
Feature flags❌✅✅
Warehouse-native analysis❌✅❌
Real-time audience segmentation❌✅✅
Built-in user profiles❌❌✅
Visitor events timeline❌❌✅
Behavioral targeting without a CDP❌❌✅
Geolocation and contextual targeting❌❌✅
Zero-party and custom attribute targeting❌❌✅
B2B firmographic integrations❌❌✅
Built-in dashboards❌✅✅

The pattern is consistent. GrowthBook will target on any attribute you give it, which is powerful and completely open-ended. But you have to know the visitor viewed pricing twice this session, which means collecting, storing, and serving that signal yourself. That is the CDP-shaped hole in the stack.

Switch content based on the user context

Which should you choose?

It all depends on your needs.

Choose the Sanity plugin on its own if your variants are editorial rather than experimental: you want editors authoring alternate copy for known segments, and you already have a delivery mechanism. As an experimentation solution by itself, it is incomplete by design, and Sanity says so.

Choose the Sanity plugin with GrowthBook if you have engineering bandwidth, you already run a data warehouse, and you want experiment analysis on data you own. It is open source, well documented, the statistics are first-rate, and you keep full control of the pipeline. Accept that you are building the exposure-tracking layer, that your schemas will carry variant fields permanently, and that each new test is a developer task.

Choose Croct if your experimentation is driven by growth and marketing teams who need to launch tests without a deploy, if you would rather not add a CDP and an analytics platform before your first result, and if you want to work at the component level instead of wiring fields together. Your Sanity content stays exactly where it is and keeps rendering when nothing is running.

The decision is really one question: how much of the experimentation pipeline do you want to own? Both answers are legitimate. Just make it deliberately, rather than discovering the answer three sprints in when someone asks where the exposure numbers come from.

Want to try the Croct path? Create a free account and connect your first Sanity component. The free tier covers 10,000 monthly active users, and the integration is one CLI command and one fallback.

Let's grow together!

Learn practical tactics our customers use to grow by 20% or more.

By continuing, you agree to our Terms & Privacy Policy.