Running into one of those dreaded "Multiple Exposu...
# ask-questions
l
Running into one of those dreaded "Multiple Exposures" warnings. I've looked at the documentation and I'm sure there's probably an issue somewhere with how we're identifying users, but it's a bit odd overall. Trying to figure out a pathway to understanding how to debug this problem and could use some guidance.
1.8% isn't much but a previous test experiment ended up a bit over 3% after 10 days so it's concerning for sure.
My guess is the primary culprit is related to how UUIDs and anonymous ids (Segment) are handled. I'm a bit unsure if the Growthbook uuid and the Segment anonymous_id should be the same? Right now they are not set to be the same, but there's also a join table set up (image below), leaving me a bit confused on what these should be.
s
Hi Cory, I am a Data Scientist at GrowthBook. In your experiment, are you randomizing on
user_id
, and then hoping to segment by
anonymous_id
?
l
@steep-dog-1694 Good question. If you mean "Assign Variation by Attribute", it's just set as
id
, and then "Experiment Assignment Table" is
anonymous_id
s
Thanks Cory. Does each
user_id
have a unique
anonymous_id
? Or can a single
user_id
have multiple `anonymous_id`s?
f
Sounds like you're doing client side testing. Our SDK will create a unique uuid if you don't specify one, as segment might not be be ready when the SDK loads. Usually there should be a 1:1 mapping between our id and the segment id. However, sometimes cookie consent tools can block the gbuuid cookie, even through its first party, causing a user with one segment id to have multiple gbuuid cookies, and hence multiple exposures. One thing to help is if you can share your code you've added to the site.
l
Yeah with Next.js and pages and various other things we've got rolling it's only worked properly with client-side. Before I share code, am I correct then in understanding that
gbuuid
and the Segment
anonymous_id
should be the same? One shouldn't be
123456
and the other
asdfjkl
?
f
not the same, but should be consistant per user.
l
Hmm okay šŸ¤”
This is what my provider looks like:
Copy code
import { Experiment, Result } from "@growthbook/growthbook"

import {
  GrowthBookProvider as GBProvider,
  GrowthBook,
} from "@growthbook/growthbook-react"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import { getSegmentAnonymousIdWithRetry } from "src/utils/segment-utils"
import getUUID from "src/utils/user-uuid"

const onExperimentViewed = (
  experiment: Experiment<any>,
  result: Result<any>
) => {
  const experimentId = experiment.key
  const variationId = result.key
  const userId = getUUID()

  setTimeout(() => {
    // Sometimes the window object is not available, like if
    // Segment is not allowed to load because of GDPR or whatever
    // In this case we add ?. to avoid attempting to fire this
    // if the analytics object is not available
    window?.analytics?.track("Experiment Viewed", {
      experimentId,
      variationId,
      userId,
    })
  }, 2000)
}

// Create a client-side GrowthBook instance
const gb = new GrowthBook({
  apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
  clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
  decryptionKey: process.env.NEXT_PUBLIC_GROWTHBOOK_DECRYPTION_KEY,
  // Enable easier debugging of feature flags during development
  enableDevMode: true,
  trackingCallback: onExperimentViewed,
})

// Let the GrowthBook instance know when the URL changes so the active
// experiments can update accordingly
function updateGrowthBookURL() {
  gb.setURL(window.location.href)
}

const GrowthBookProvider = ({ children }: { children: React.ReactNode }) => {
  const router = useRouter()

  useEffect(() => {
    // Initialize GrowthBook and make sure the attributes are set properly
    gb.init({ streaming: true })

    // Initial setup with UUID
    const uuid = getUUID()
    gb.setAttributes({ id: uuid, anonymousId: uuid })

    // Try to get Segment's anonymousId and set it if available
    getSegmentAnonymousIdWithRetry().then((anonymousId) => {
      if (anonymousId) {
        gb.setAttributes({ id: uuid, anonymousId: anonymousId })
      }
    })

    router.events.on("routeChangeComplete", updateGrowthBookURL)
    return () => router.events.off("routeChangeComplete", updateGrowthBookURL)
  }, [])

  return <GBProvider growthbook={gb}>{children}</GBProvider>
}

export default GrowthBookProvider
I'm generating and storing a
gbuuid
cookie like so:
Copy code
import Cookies from "js-cookie"

const getUUID = (): string => {
  const COOKIE_NAME = "gbuuid"
  const COOKIE_DAYS = 400 // 400 days is the max cookie duration for Chrome

  // Only run on client side
  if (typeof window === "undefined") return ""

  // Generate a UUID using the most appropriate method available
  const genUUID = (): string => {
    if (window?.crypto?.randomUUID) return window.crypto.randomUUID()

    return `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`.replace(/[018]/g, (c) =>
      (
        Number(c) ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4)))
      ).toString(16)
    )
  }

  // Check for existing Growthbook UUID
  // If it exists, refresh the expiration date
  const existingUUID = Cookies.get(COOKIE_NAME)

  if (existingUUID) {
    // Refresh the expiration date when retrieved
    Cookies.set(COOKIE_NAME, existingUUID, {
      expires: COOKIE_DAYS,
      path: "/",
      sameSite: "strict",
      secure: process.env.NODE_ENV === "production",
    })
    return existingUUID
  }

  const uuid = genUUID()

  if (uuid) {
    Cookies.set(COOKIE_NAME, uuid, {
      expires: COOKIE_DAYS,
      path: "/",
      sameSite: "strict",
      secure: process.env.NODE_ENV === "production",
    })
    return uuid
  }

  return ""
}

export default getUUID
Maybe I've been staring at all of this too much.
•
gbuuid
and
anonymous_id
are different. •
Experiment Viewed
only fires if Segment has loaded. ā—¦ This contains the uuid as a property and the
anonymous_id
as part of the normal event.
f
you are tracking the uuid
so can you see if one uuid is getting multiple segment ids? or is it the other way around - or some combination
l
Good question. Checking with my data guy, will get back
f
segment sometimes will do some post - reconciliation of ids if they're able to figure out the user is the same later
I Wonder if this could be it
l
Copy code
|anonymous_id                        |variation|timestamp                    |
|------------------------------------|---------|-----------------------------|
|01453618-0363-4ea1-bb41-e6576a44a6c1|0        |2025-03-17 17:08:12.898 +0000|
|01453618-0363-4ea1-bb41-e6576a44a6c1|1        |2025-03-17 18:26:25.120 +0000|
|01453618-0363-4ea1-bb41-e6576a44a6c1|1        |2025-03-17 19:19:17.262 +0000|
|01453618-0363-4ea1-bb41-e6576a44a6c1|1        |2025-03-17 20:30:38.444 +0000|
|01453618-0363-4ea1-bb41-e6576a44a6c1|1        |2025-03-17 21:44:30.400 +0000|
|01453618-0363-4ea1-bb41-e6576a44a6c1|1        |2025-03-17 23:45:35.510 +0000|
|01453618-0363-4ea1-bb41-e6576a44a6c1|1        |2025-03-18 11:57:48.383 +0000|
Here's an example. It's interesting that it showed a 0 variant, but then the rest of the time it's 1.
Is it possible I'm overengineering this and shouldn't be spinning up my own uuid function? In the Segment event it looks sort of like this:
Copy code
{
  "anonymousId": "123456",
  "userId": "abcdefg",
  "properties": {
    "userId": "qwerty"
  }
}
Update from me: we cleaned up a bunch of the logic, including removing the custom uuid from the properties but still allowing for a client-side generation of a uuid if one was not provided. I also realized I had left in some extra code in our middleware which was also setting a gbuuid cookie for whatever reason. Removed that and started a new test. 14 hours in and zero multiple exposures detected šŸ‘€ šŸ¤ž
s
thanks for the update, I hope your issue is resolved.