Hello, I'm trying to use GrowthBook in a Next.js a...
# ask-questions
w
Hello, I'm trying to use GrowthBook in a Next.js app on a server page and the gb.getDeferredTrackingCalls function never returns any data. Payload comes in just fine, it just looks like that function for tracking does not work. Getting exposure data is no problem for client pages but it breaks for server pages. Setting it up following the documentation has not helped. What could I be missing?
1
h
@strong-mouse-55694 /@happy-autumn-40938 - would appreciate it if you guys can provide any input on the above, please.
s
https://github.com/growthbook/examples/tree/main/next-js If
getDeferredTrackingCalls
is empty, it likely means that the experiment wasn't evaluated. For SSR, you need to ensure that you're also evaluating server-side. Otherwise, there won't be any data to forward. Also, when using
getDeferredTrackingCalls
, you'll also be firing the callback on the client. Sharing more of your setup will also help provide us more guidance
w
@strong-mouse-55694, thanks after looking through the example i'm still not sure what I'm missing. Is there a method I need to be calling to evaluate the experiment? If I'm able to be in the test group and see the test feature, does that not mean the experiment was evaluated on that page?
h
@strong-mouse-55694 - is it possible to set up an ad-hoc call today - might be easier to share screens and go through the code?
s
Let's first try to solve it here, because then others can benefit! If we truly get stuck, we can jump on the call. Are you able to share some of your implementation code? Things to check: • Experiment is evaluated server-side. You need to be calling
gb.isOn
or similar to generate the data for the deferred tracking call. • This also requires you to be setting up a user id. If there isn't a user id, then assignment won't happen. • The experiment in GB needs to be active and configured. • Are you debugging this locally? Or do you have anything other parts of your setup that should be noted? • Also, what are you using for event tracking? Have you tried just using console.log() to see if you anything fires?
w
Yep, calling gb.isOn, have a user id, the experiment is active and configured, debugging locally (but only after it stopped tracking when we switched to a server page from a client page), console.log() does not get fired in the tracking calls
h
@strong-mouse-55694 ^
s
Great. Can you share some of the code where you're initializing GB and running the experiment?
h
^@worried-night-96863
w
Copy code
export default async function Page({ params, searchParams }: PageProps<true>) {
  const { slug } = await params;
  const urlPath = `/${slug?.join('/') || ''}`;

  const cookieStore = await cookies();
  const headerStore = await headers();
  const rawSearchParams = await searchParams;
  const gbAttributes = getGrowthBookAttributes(headerStore, cookieStore);

  // for server side pages with growthbook devtools (refresh page to see changes)
  const { gb, trackingData } = await initGrowthBook({
    attributes: gbAttributes,
    plugins: [
      devtoolsNextjsPlugin({
        requestCookies: cookieStore,
        searchParams: rawSearchParams as { _gbdebug?: string | undefined },
      }),
    ],
  });

  console.log(gbAttributes);

  const isInTest = gb.isOn('redirect-n1-to-ed-questions');

  console.log('trackingData in page', trackingData);

  const isBmN1 = urlPath === '/bm/n1';

  if (isInTest && isBmN1) {
    return (
      <>
        <EdQuestions />
        <GrowthBookTracking data={trackingData} />
      </>
    );
  }

  const filteredSearchParams = rawSearchParams
    ? filterUndefined(rawSearchParams)
    : undefined;

  const content = await fetchOneEntry({
    apiKey: BUILDER_API_KEY,
    model: 'page',
    options: getBuilderSearchParams(filteredSearchParams),
    userAttributes: { urlPath },
  });

  const canShowContent = content || isPreviewing(filteredSearchParams);

  if (!canShowContent) {
    notFound();
  }

  return (
    <>
      <Content
        apiKey={BUILDER_API_KEY}
        content={content}
        customComponents={customComponents}
        model="page"
      />
      <GrowthBookTracking data={trackingData} />
    </>
  );
}
here's the util functions that initialize GrowthBook:
Copy code
export function growthBookInstance(options?: GrowthBookInstanceOptions) {
  return new GrowthBook({
    clientKey:
      env.NEXT_PUBLIC_VERCEL_ENV === 'production'
        ? env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY
        : 'sdk-0HcTwbYopRHy3HwL',
    enableDevMode: true,
    ...options,
  });
}

export default async function initGrowthBook(
  options?: GrowthBookInstanceOptions,
) {
  setPolyfills({
    fetch: (
      url: Parameters<typeof fetch>[0],
      opts: Parameters<typeof fetch>[1],
    ) =>
      fetch(url, {
        ...opts,
        cache: 'no-store',
      }),
  });

  configureCache({ disableCache: true });

  const gb = growthBookInstance({ ...options });

  const { error } = await gb.init({ timeout: 1000 });

  if (error) {
    console.error(error);
  }

  const payload = gb.getDecryptedPayload();
  const trackingData = gb.getDeferredTrackingCalls();

  console.log('payload', payload);
  console.log('trackingData', trackingData);

  return { gb, payload, trackingData };
}
h
Gentle nudge on the above @strong-mouse-55694
@strong-mouse-55694 - apologies for the fast follow - wondering if you had a chance to take a look at the code?
w
@strong-mouse-55694-kind of stuck here if you wouldn't mind taking a look
b
@worried-night-96863 @handsome-lighter-25056 I'm going to escalate this to one our Engineers. I'll let you know as soon as I have an update.
🙌 2
s
Sorry for not responding sooner. I can see that your
initGrowthbook
abstraction is the issue. If you move
const trackingData = gb.getDeferredTrackingCalls();
into the component, after the feature call, then it you'll see the data as expected. (If you don't need to fire your event tracking clientside, then you could replace the deferred calls with a tracking callback in your GrowthBook initialization.) However, let me think about that function to determine if it's a good approach.
w
i moved the tracking data and the payload and I still don't get tracking data
s
Where'd you put
gb.getDeferredTrackingCalls()
?
w
right after the gb.isOn() call
s
So this is what my page.tsx looks like:
Copy code
import { GrowthBookTracking } from "@/lib/GrowthBookTracking";
import { GB_UUID_COOKIE } from "@/middleware";
import { cookies } from "next/headers";
import gbInit from "./gbInit";

export default async function ServerDynamic() {
  // configureServerSideGrowthBook();
  const id = cookies().get(GB_UUID_COOKIE)?.value || "";

  const { gb, payload } = await gbInit(id);

  // Evaluate any feature flags
  const feature1Enabled = gb.isOn("feature1");

  const trackingData = gb.getDeferredTrackingCalls();
  // Cleanup
  gb.destroy();

  return (
    <div>
      <h2>Dynamic Server Rendering</h2>
      <p>
        This page renders dynamically for every request. You can use feature
        flag targeting and run A/B experiments entirely server-side.
      </p>
      <ul>
        <li>
          feature1: <strong>{feature1Enabled ? "ON" : "OFF"}</strong>
        </li>
      </ul>

      {/* <RevalidateMessage /> */}

      <GrowthBookTracking data={trackingData} />
    </div>
  );
}
and the version of the init function
Copy code
import {
  GrowthBook,
  setPolyfills,
  configureCache,
} from "@growthbook/growthbook";

export function growthBookInstance(id: string) {
  return new GrowthBook({
    apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
    clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
    enableDevMode: true,
    attributes: {
      id,
    },
  });
}

export default async function initGrowthBook(id: string) {
  setPolyfills({
    fetch: (
      url: Parameters<typeof fetch>[0],
      opts: Parameters<typeof fetch>[1]
    ) =>
      fetch(url, {
        ...opts,
        cache: "no-store",
      }),
  });

  configureCache({ disableCache: true });

  const gb = growthBookInstance(id);

  const { error } = await gb.init({ timeout: 1000 });

  if (error) {
    console.error(error);
  }

  const payload = gb.getDecryptedPayload();

  return { gb, payload };
}
w
i'm using nanoId to generate the user id, that's the only difference I can see here. does the growthbook user id need to be in a certain format?
i changed everything else to match what you're doing here and I still get no tracking data
s
nanoid should be fine. that value doesn't need to be of a particular shape. one other difference is that I didn't encrypt my payload. If you log out your payload, what does it look like?
Copy code
const payload = gb.getDecryptedPayload();
  console.log(payload);
this is mine for reference:
Copy code
{
  status: 200,
  features: {
    flag_internetmd___headline_cta: { defaultValue: false },
    flag_internetmd___headline_cta_2: { defaultValue: false },
    flag_internetmd___headline_cta_23: { defaultValue: false },
    feature1: { defaultValue: false, rules: [Array] }
  },
  experiments: [],
  dateUpdated: '2025-07-22T20:54:12.531Z'
}
w
Copy code
{
  "status": 200,
  "features": {
    "recommended_strength_variant": {
      "defaultValue": "high",
      "rules": [
        {
          "id": "fr_19g624mbyqadh1",
          "condition": {
            "url": {
              "$regex": "rugiet-men-ed"
            }
          },
          "coverage": 1,
          "hashAttribute": "id",
          "seed": "5bbb2ec7-3e55-4707-b910-ca3b64f603a0",
          "hashVersion": 2,
          "variations": [
            "high",
            "max",
            "medium",
            "low"
          ],
          "weights": [
            0.25,
            0.25,
            0.25,
            0.25
          ],
          "key": "recommended_strength_variant",
          "meta": [
            {
              "key": "0",
              "name": "high"
            },
            {
              "key": "1",
              "name": "very_high"
            },
            {
              "key": "2",
              "name": "medium"
            },
            {
              "key": "3",
              "name": "low"
            }
          ],
          "phase": "3",
          "name": "recommended_strength_variant"
        }
      ]
    },
    "recommended_quantity_variant": {
      "defaultValue": 2,
      "rules": [
        {
          "id": "fr_19g621mbyn73bk",
          "condition": {
            "url": {
              "$regex": "rugiet-men-ed"
            }
          },
          "coverage": 1,
          "hashAttribute": "id",
          "seed": "df600dc2-b529-40db-9a47-618c96b02de6",
          "hashVersion": 2,
          "variations": [
            6,
            12,
            18,
            24
          ],
          "weights": [
            0.25,
            0.25,
            0.25,
            0.25
          ],
          "key": "recommended_quantity_variant",
          "meta": [
            {
              "key": "0",
              "name": "1"
            },
            {
              "key": "1",
              "name": "2"
            },
            {
              "key": "2",
              "name": "3"
            },
            {
              "key": "3",
              "name": "4"
            }
          ],
          "phase": "3",
          "name": "recommended_quantity_variant"
        }
      ]
    },
    "product_intro": {
      "defaultValue": false
    },
    "test-pdm": {
      "defaultValue": "false"
    },
    "redirect-n1-to-ed-questions": {
      "defaultValue": false,
      "rules": [
        {
          "id": "fr_19g624md7r3s83",
          "condition": {
            "url": {
              "$regex": "utm_source=(fb|facebook|ig)"
            }
          },
          "coverage": 0.2,
          "hashAttribute": "id",
          "bucketVersion": 1,
          "seed": "3f394707-b979-471c-997e-fb3d63d09e8b",
          "hashVersion": 2,
          "variations": [
            false,
            true
          ],
          "weights": [
            0.5,
            0.5
          ],
          "key": "social-lp-bmn1-vs-ed-questions",
          "meta": [
            {
              "key": "0",
              "name": "Control"
            },
            {
              "key": "1",
              "name": "Variation 1"
            }
          ],
          "phase": "4",
          "name": "Social LP bm/n1 vs ed-questions"
        }
      ]
    },
    "rugiet-lp-comparison-chart": {
      "defaultValue": "CONTROL"
    }
  },
  "experiments": [],
  "dateUpdated": "2025-07-17T22:10:33.405Z"
}
our payload isn't encrypted either, so i can swap that out for gb.getPayload
and it still works (except the tracking data is still missing)
s
let's try to two things: first, can you try adding a trackingCallback directly to your GrowthBook instance like this:
Copy code
export function growthBookInstance(id: string) {
  return new GrowthBook({
    apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
    clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
    enableDevMode: true,
    attributes: {
      id,
    },
    trackingCallback: (data) => {
      console.log("trackingCallback", data);
    },
  });
}
and then what's the value if you:
Copy code
const isInTest = gb.isOn('redirect-n1-to-ed-questions'); 
console.log(isInTest)
and you change the ID. Do you get different values?
The other thing is that it looks like there are rules on that experiment. If those aren't met, then the experiment won't run. It may be helpful to remove those rules and use the simplest version of the flag/experiment for testing.
w
Console statements in the trackingCallback never run. I duplicated the experiment for the staging environment without the rules and still i get no tracking data
would we be able to set up a call now?
s
We can set up a call. Here are a few other steps you can try, too: 1. Set
gb.debug = true
This will console log a lot more info 2. Create a minimal example. Right now, you're doing some redirect stuff. It's unclear whether that's impacting the experiment. I'd try a really basic component/page and see if you encounter the same issue. There's something in how you're setting up your page and/or configuring GrowthBook that's preventing the experiment evaluation from happening properly. 3. Do you see different values for gb.ison when the user id changes?
w
Here's the log from the control group
Copy code
Skip because of coverage { id: 'social-lp-bmn1-vs-ed-questions-testing' }
Use default value { id: 'redirect-n1-to-ed-questions', value: false }
 GET /bm/n1 200 in 466ms
Skip because of coverage { id: 'social-lp-bmn1-vs-ed-questions-testing' }
Use default value { id: 'redirect-n1-to-ed-questions', value: false }
when I change the user id I got the test experiment which gave me a lot of output including the logs from the trackingCallback:
Copy code
trackingCallback called on server side
experiment {
  variations: [ false, true ],
  key: 'social-lp-bmn1-vs-ed-questions-testing',
  coverage: 0.2,
  weights: [ 0.5, 0.5 ],
  hashAttribute: 'id',
  meta: [ { key: '0', name: 'Control' }, { key: '1', name: 'Variation 1' } ],
  name: 'Social LP bm/n1 vs ed-questions (Testing)',
  phase: '0',
  seed: '3f394707-b979-471c-997e-fb3d63d09e8b',
  hashVersion: 2
}
result {
  key: '1',
  featureId: 'redirect-n1-to-ed-questions',
  inExperiment: true,
  hashUsed: true,
  variationId: 1,
  value: true,
  hashAttribute: 'id',
  hashValue: 'SnvrzILKYO7Eda9L7miUX',
  stickyBucketUsed: false,
  name: 'Variation 1',
  bucket: 0.5601
}
In experiment { id: 'social-lp-bmn1-vs-ed-questions-testing', variation: 1 }
 GET /bm/n1 200 in 331ms
trackingCallback called on server side
experiment {
  variations: [ false, true ],
  key: 'social-lp-bmn1-vs-ed-questions-testing',
  coverage: 0.2,
  weights: [ 0.5, 0.5 ],
  hashAttribute: 'id',
  meta: [ { key: '0', name: 'Control' }, { key: '1', name: 'Variation 1' } ],
  name: 'Social LP bm/n1 vs ed-questions (Testing)',
  phase: '0',
  seed: '3f394707-b979-471c-997e-fb3d63d09e8b',
  hashVersion: 2
}
result {
  key: '1',
  featureId: 'redirect-n1-to-ed-questions',
  inExperiment: true,
  hashUsed: true,
  variationId: 1,
  value: true,
  hashAttribute: 'id',
  hashValue: 'SnvrzILKYO7Eda9L7miUX',
  stickyBucketUsed: false,
  name: 'Variation 1',
  bucket: 0.5601
}
In experiment { id: 'social-lp-bmn1-vs-ed-questions-testing', variation: 1 }
so it looks like it works fine when it's in the test group but doesn't evaluate the experiment in the control group
i just changed the experiment to include all traffic and it appears to be working in the control group as well now
i think the problem was attempting to get the tracking data before evaluating the feature, then completely obfuscated in dev by the experiment only running for 20% of traffic
s
Glad you figured it out. It looks like it was primarily because of the 20% coverage, which was preventing most users from ever entering the experiment.
w
yeah, that made it impossible to see that we had fixed it with rearranging the feature evaluation and getting the tracking data
h
Yup, we seem to be getting experiment related events now. Thanks for the support @strong-mouse-55694, appreciate it 🙌
1