Experiments with Plasmic

Design both versions of a page in Plasmic. Apex decides which visitor sees which version and measures the result. Plasmic only draws the version Apex picked.

How you set it up depends on how your site ships Plasmic:

  • Codegen (plasmic sync writes components into your repo): follow Codegen below.
  • Headless API (@plasmicapp/loader-nextjs or @plasmicapp/loader-react fetches designs at runtime): follow Headless API.
  • Plasmic hosting (no repo of your own): use the tracking snippet and snippet experiments. There is no code to put the hook in.

Let Apex pick the version, not Plasmic

Plasmic has its own A/B test feature (Split content). It picks a version for each visitor by itself. If Apex is also picking, the two disagree about who saw what, and your results count the wrong people.

So do not create the test under Plasmic's Split content tab. With codegen, design the change as a plain global variant. With the Headless API, give the Plasmic test external IDs that match the Apex experiment, and pass Apex's pick in.

Codegen

Codegen turns every global variant group into a React context. Apex's useApexVariant hook picks the version, and your code turns that context on for visitors in the treatment.

Install Apex in the app

If you use Next.js:

npm install @apex-inc/next @apex-inc/react
// middleware.ts
import { apexVisitorMiddleware } from "@apex-inc/next";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  return apexVisitorMiddleware(request);
}

The middleware gives each visitor a stable id on the page request, so the server can pick their version before any HTML is sent.

Design the treatment in Plasmic

In Plasmic Studio, open the page and go to the Page Data tab. Under Global Variants, add a group (for example, "Hero Test") with one variant named treatment.

Turn the treatment variant on and make your changes. Everything you leave alone stays linked to the original design. One group can change several pages and shared components at once, such as a homepage hero and a site-wide banner.

Sync

Run plasmic sync. Plasmic writes a file like components/plasmic/my_site/PlasmicGlobalVariant__HeroTest.tsx that exports HeroTestContext. Use the variant value exactly as that file's type spells it.

Create the experiment in Apex

Create the experiment and choose Your code changes the page. Copy the experiment id. Plasmic's original design is Control. The treatment variant is Variant B.

Turn the variant on for the treatment

Put the hook in a component you own. Do not edit files under components/plasmic/. plasmic sync overwrites them.

// components/HeroTest.tsx
"use client";

import type { ReactNode } from "react";
import { useApexVariant, isTreatmentVariant } from "@apex-inc/react";
import { HeroTestContext } from "@/components/plasmic/my_site/PlasmicGlobalVariant__HeroTest";

export function HeroTest(props: { experimentId: string; children: ReactNode }) {
  const arm = useApexVariant(props.experimentId);
  return (
    <HeroTestContext.Provider value={isTreatmentVariant(arm) ? "treatment" : undefined}>
      {props.children}
    </HeroTestContext.Provider>
  );
}

useApexVariant records that the visitor saw the test. You do not send an exposure event yourself.

Pick the version on the server

The hook alone is not flicker-free. Without a server pick, the first HTML is the original design and the treatment swaps in after the page loads. Assign on the server and pass the result to <ApexProvider assignments>.

App Router:

// app/page.tsx
import { cookies, headers } from "next/headers";
import { assignExperiments } from "@apex-inc/next";
import { ApexProvider } from "@apex-inc/react";
import { HeroTest } from "@/components/HeroTest";
import Homepage from "@/components/Homepage";

const HERO_TEST = "exp_123";
const APEX = "https://app.apex.inc";
const WORKSPACE = process.env.NEXT_PUBLIC_APEX_WORKSPACE_KEY;

export default async function Page(props: {
  searchParams: Promise<Record<string, string>>;
}) {
  const cookieStore = await cookies();
  const headerStore = await headers();
  const { assignments } = await assignExperiments([HERO_TEST], {
    cookie: cookieStore.toString(),
    host: headerStore.get("host") ?? "",
    workspaceKey: WORKSPACE,
    apiBase: APEX,
  });
  const search = new URLSearchParams(await props.searchParams).toString();

  return (
    <ApexProvider apiBase={APEX} workspaceKey={WORKSPACE} assignments={assignments} search={search}>
      <HeroTest experimentId={HERO_TEST}>
        <Homepage />
      </HeroTest>
    </ApexProvider>
  );
}

Pages Router: Plasmic's Next.js setup usually builds pages ahead of time with getStaticProps. A page built ahead of time cannot know which visitor is asking, so switch the tested page to getServerSideProps:

export const getServerSideProps: GetServerSideProps = async ({ req, query }) => {
  const { assignments } = await assignExperiments([HERO_TEST], {
    cookie: req.headers.cookie ?? "",
    host: req.headers.host ?? "",
    workspaceKey: WORKSPACE,
    apiBase: APEX,
  });
  const search = new URLSearchParams(query as Record<string, string>).toString();
  return { props: { assignments, search } };
};

Then render the same <ApexProvider assignments={assignments} search={search}> and <HeroTest> in the page.

Passing search makes preview links (?_apex_preview=variant_a&_apex_exp=exp_123) show the treatment in the first HTML, which is how Apex captures a screenshot of each version.

Deploy, then activate

Ship the code first. Then activate the experiment in Apex. Visitors are split from that moment.

Headless API

The Headless API accepts another tool's pick directly.

  1. In Plasmic Studio, create the test under Split content and open its external IDs.
  2. Set the experiment's external ID to your Apex experiment id.
  3. Set the original's external ID to control and the override's to variant_a.
  4. Pass Apex's pick to Plasmic:
const arm = useApexVariant(HERO_TEST);

<PlasmicRootProvider loader={PLASMIC} variation={{ [`ext.${HERO_TEST}`]: arm === "control" ? "control" : "variant_a" }}>
  <PlasmicComponent component="Homepage" />
</PlasmicRootProvider>

Pick on the server the same way as codegen (assignExperiments and <ApexProvider assignments>) so the first HTML is already the right version. Do not also call Plasmic's getActiveVariation for this test. That is Plasmic picking a second time.

Before you start the test

  • Two versions per test. Apex splits each experiment between Control and one treatment. A Plasmic variant group with more than one variant cannot be tested as a single Apex experiment.
  • A Plasmic sync can change a live test. If a designer edits the treatment variant and someone runs plasmic sync while the test is running, visitors in Variant B start seeing a different design partway through. Apex's live-test check runs when someone edits code in an editor. It does not see a Plasmic sync. Agree with your designers to leave the test's variant alone until the test ends.
  • The winner is made permanent in Plasmic. Apex cannot edit your Plasmic project. If the treatment wins, copy its changes into the base design in Plasmic Studio and sync. Then remove the HeroTest wrapper from your code. If Control wins, delete the variant group in Plasmic and remove the wrapper. See Graduation Pipeline for marking the test graduated.

Next steps