---
title: "Sanity AB testing compared: plugin, GrowthBook, and Croct"
slug: "sanity-plugin-vs-growthbook-vs-croct"
locale: "en-us"
description: "GrowthBook is the documented path for AB testing in Sanity. Here's how it compares with Croct on architecture, integration effort, developer dependency, and the path to personalization."
published_at: "2026-08-11"
updated_at: "2026-10-26"
image: "https://storage.googleapis.com/croct-assets-b931d070/blog/Sanity_AB_testing_compared_plugin_Growth_Book_and_Croct_a7ac6eb949/Sanity_AB_testing_compared_plugin_Growth_Book_and_Croct_a7ac6eb949.png"
authors: ["Juliana Amorim"]
category: "Experimentation"
tags: ["A/B testing","Comparison"]
canonical: "https://blog.croct.com/post/sanity-plugin-vs-growthbook-vs-croct"
---

# 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](https://docs.growthbook.io/guide/sanity) 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.

> 💬 Summary
>
> The Sanity plugin covers content modeling. GrowthBook covers assignment and statistics. Exposure tracking and metrics are still yours to build, and both work at the field level, so a four-field hero means four experiment fields queried in lockstep. Croct works at the component level, sits alongside Sanity without touching your schemas, and covers the full pipeline. This article compares them on architecture, integration effort, developer dependency, plan availability, and the path from testing to personalization.

## 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 alone | Sanity + GrowthBook | Croct                                       |
| -------------------------------------- | ------------------- | ------------------- | ------------------------------------------- |
| 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                  | ❌                   | ❌                   | ✅                                           |
| Granularity                            | Field and document  | Field and document  | Component 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 | ❌                   | ❌                   | ✅                                           |

## Experimentation workflow

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

| Step                                 | Sanity plugin alone | Sanity + GrowthBook        | Croct |
| ------------------------------------ | ------------------- | -------------------------- | ----- |
| 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](https://docs.growthbook.io/guide/sanity#add-growthbook-to-your-next-js-app), 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.

## 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 alone | Sanity + GrowthBook | Croct |
| -------------------------- | ------------------- | ------------------- | ----- |
| 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.

```sh
npx croct@latest init
```

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

```jsx
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:

```jsx
const {content, metadata} = await fetchContent('home-hero', {
  fallback: sanityHero,
});

// Render Sanity content unless Croct returned dynamic content
const 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.

### Sanity plugin + GrowthBook

Four pieces of setup, then recurring work per experiment.

#### Plugin configuration

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

#### Schema changes

Every field you want to test changes type, permanently:

```ts
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:

```ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Ensure a UUID cookie is always set to enable experimentation
export 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 experiments
export 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:

```ts
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

```tsx
"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 components
export 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 alone | Sanity + GrowthBook | Croct |
| ------------------------------------------------------ | ------------------- | ------------------- | ----- |
| 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 alone | Sanity + GrowthBook | Croct |
| ----------------------------------------- | ------------------- | ------------------- | ----- |
| A/B/n testing                             | Content 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.

> ✨
>
> For a deeper treatment of that specific problem, see [how to personalize Sanity content based on visitor behavior](/post/personalization-sanity-visitor-behavior.md).

## 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](https://croct.com/signup) 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.
