statsig-io / react-sdk

An SDK for using Statsig Feature Management and Experimentation platform in React js clients
ISC License
6 stars 6 forks source link

Initializing Statsig SDK (was : always fail in feature gate) #1

Closed youminkim closed 2 years ago

youminkim commented 3 years ago

Hi, I am using NextJs and adopting feature gate with react-sdk. However, my gate value is always fail although my setting has 50% fail rate. I suspect this is related to my asynchronous init of StatsigProvider. Can you verify my implementation is alright?

_app.js

function MyApp(props) {

  const [userID, setUserID] = useState('')
  useEffect(()=>{
    let u = localStorage.getItem('take-app-user-id') || ''
    if (!u) {
      u = Math.random().toString(36)
      localStorage.setItem('take-app-user-id', u)
    }
    setUserID(u)
    console.log(u)
  }, [])

...

  return (
    <React.Fragment key="react_fragment">
      <StatsigProvider  
        sdkKey={process.env.NEXT_PUBLIC_STATSIG_CLIENT}
        user={{
          userID,
        }}
      >
  ...
)

Main.js

export default function Shop(props) {
    ...
    const statsig = useStatsig();
    useEffect(()=>{
        statsig.logEvent("page_view", shop_name)
    })

   ...

    const gate = useGate('adsense2');
    const [showAdsense, setShowAdsense] = useState(true)
    useEffect(()=>{
        setShowAdsense(gate.value)
    }, [gate])

In this example, showAdsense value is always false

vijaye-statsig commented 3 years ago

@youminkim - you're right this could be due to the async initialization. We will debug this; @jkw-statsig understands your requirements, so we'll respond with a solution that fits your needs in the morning.

tore-statsig commented 3 years ago

On the provider:

<StatsigProvider  
        sdkKey={process.env.NEXT_PUBLIC_STATSIG_CLIENT}
        user={{
          userID,
        }}
      >

You should specify the waitForInitialization=true property if you want to only render the remainder of the app after Statsig has initialized. The other option is to not wait for initialization at the root, and instead use fetchGate to await asynchronously at the callsite.

youminkim commented 3 years ago

waitForInitialization=true means blocking the website rendering?

fetchGate may I know how to use?

I am not sure if this is possible but I prefer the way to update userID. For example, a customer who initially did not login can login in the website and we need to update userID

vijaye-statsig commented 3 years ago

@youminkim is there a way for you to generate a unique id on server side and set it as a cookie so subsequent requests from the client would carry that same id?

jkw-statsig commented 3 years ago

@youminkim if you do not want to wait for initialization in StatsigProvider, you can set the value to be false there so that it does not block rendering, and replace your current useGate call with fetchGate, because fetchGate is async, you should call it within useEffect like this:

    const [showAdsense, setShowAdsense] = useState(true)
    useEffect(()=>{
        fetchGate('adsense2').then((gate) => setShowAdsense(gate.value))
    })

Regarding your question about updating userID, I think there are 3 scenarios and here are my thoughts:

  1. user A (new user) visits your site while logged out, and then registers during the session - ideally you use the same user ID you generated for them before & after registration.

  2. user A (returning user) visits your site while logged out, then logs in, and the IDs are different - you can call updateUser to switch the user and we will log future events under the new ID. However, currently there isn't a way to associate events from the previous ID with the new ID. We have plans in place to introduce an API to do that so you can associate events from two different IDs together.

  3. user A visits your site, then switches to user B. In this case you can just use updateUser mentioned above.

Is #2 the scenario you are talking about here? We'll provide an update once we have that feature implemented.

Hope this helps! Let us know if you have any more questions.

tore-statsig commented 3 years ago

We have discovered an issue with the SDK when you do not pass waitForInitialization=true and we are working on a fix

tore-statsig commented 3 years ago

Published SDK version v0.3.0 this afternoon. This removes the fetchGate/fetchConfig/fetchExperiment parameters. I am updating the documentation now, but my suggestion is:

If you dont want to use waitForInitialization=true to render your entire app once the sdk is initialized, you can instead leave that off and still add useGate('gate_name') in the component where you want to check a gate value.

That will initialially give you false, until the SDK initialization completes, and then will trigger a re-render of the component because the value has changed.

Hope this helps! I'll keep this issue open until you can verify this is working for your use case

tore-statsig commented 3 years ago

My suggestion, based on your initial code:

Leave the provider untouched.

Then, in your Shop component

export default function Shop(props) {
    ...
    const statsig = useStatsig();
    useEffect(()=>{
        statsig.logEvent("page_view", shop_name)
    })

   ...

    const showAddSenseGate = useGate('adsense2');

then in your component:

{showAdSenseGate.value ? <Ad /> : null }

This is in line with what @jkw-statsig suggested as well - leave the default for the gate off, and once the value is set to true, the ad will be rendered

youminkim commented 3 years ago

Thanks all for helping this.

@tore-statsig

  1. I updated the react sdk but still same result. In the original code, I am not sure my updated userID state is applied or not. Is there way to check current userID?

  2. Should I call explicitly call statsig.updateUser function to update userID?

jkw-statsig commented 3 years ago

Are you still always getting false with const showAddSenseGate = useGate('adsense2');?

You shouldn't need to manually call updateUser yourself, unless some time in the session a new user logs in.

Would be great if there is a way for us to see more of your code to help with debugging, e.g. if your code is on GitHub we can take a look. In the meantime one thing you can try is hardcoding the userID in StatsigProvider call with a bunch of different values, e.g. 1 - 20, and if you get true sometimes. If so, then it's probably related to how you are setting the userID, but if you are still only getting false, it's likely related to other part of the code.

youminkim commented 3 years ago

Hi, yes still getting false.

I just created a test code but same https://codesandbox.io/s/lively-wind-759uw?file=/pages/_app.js

jkw-statsig commented 3 years ago

Thanks for sharing @youminkim! That was helpful and I was able to find out the issues you have in your code. Here is my fork that fixed those issues and it's working so you can give it a try first https://codesandbox.io/s/recursing-wright-yy91s?file=/pages/_app.js . Here are some tips/recommendations:

  1. The main problem is with how you were setting userID. As is, your code always passed an empty string as the userID to StatsigProvider, and later the userID is updated by useEffect. You were right earlier about needing to use updateUser when you want to change userID after the initialization, but I recommend using the correct userID for the initial rendering.

Because window.localStorage is not always immediately available during your initial rendering (it won't take long though), I suggest that you wait for it to become available, get userID from it, then return StatsigProvider (do something similar to my code above). This should not delay your initial rendering by much at all.

  1. Math.random generates a "pseudo random" number that uses your computer's time, so when you test locally by refreshing at a fast speed, you can get many consecutive "false" in a row, so you might have to try 10+ times to get a true if you are unlucky (I did and thought something was wrong :) ). I would recommend using something like uuid to generate IDs for users in the future.

  2. When you are testing the new changes, because you are caching the userID, you will get the same result every time if you use your localStorage, which is the intended behavior. Make sure to disable that if you want to test the "randomness" locally, which I did to record the demo video attached below.

Hope this helps!

https://user-images.githubusercontent.com/77478330/123520350-1e1a1980-d665-11eb-9202-04cb57b440a2.mov

youminkim commented 3 years ago

Thank you @jkw-statsig for detailed explanation.

needing to use updateUser when you want to change userID after the initialization

Can we explore this more? I tried with useStatsig but seems not working https://codesandbox.io/s/crimson-rgb-6vzrw?file=/pages/_app.js

In your example, I need to wait until useEffect execution meaning I cannot use pre-rendering anymore. In same reason, I could not use waitForInitialization=true. Attaching NextJS pre-rendering details https://nextjs.org/learn/basics/data-fetching/pre-rendering

jkw-statsig commented 3 years ago

Hmm I'm not very sure about the requirement for pre-rendering, but if your goal is to only render a static page on the first rendering and update to use user-specific values (gates) later, there is actually no reason to use StatsigProvider in the very initial rendering. You can make a small adjustment to the code I shared earlier and make it work the way you want:

if (userID != null) {
    return (
      <StatsigProvider
        sdkKey={...}
        user={{
          userID
        }}
        waitForInitialization={true} // you can actually wait for initialization here now because this is designed to be rendered after the initial rendering
      >
        <Component {...pageProps} />
      </StatsigProvider>
    );
  } else {
    return <Component {...pageProps} />;
  }

Hope this works for you!

Meanwhile we'll look into the issue with updateUser separately, and potentially make changes to allow the user be updated by simply calling StatsigProvider again with a different userID.

youminkim commented 3 years ago

Without StatsigProvider provider, I am not sure how my child component's useGate hook works. I may need to manually check if the context exists which is not ideal.

I believe the best way is that StatsigProvider accepts user changes. Otherwise, in my context, calling functions directly from JS SDK would be more straightforward.

jkw-statsig commented 3 years ago

@youminkim in the case where StatsigProvider isn't called yet, calling useGate will simply return false, which is actually better than providing a placeholder userID first only to switch it soon after. I think there are 3 good solutions for you:

1. If you need/prefer to provide your own userID, so that you can use it for other purposes: You can do what I suggested above and return your child component first without StatsigProvider until you retrieve the userID from localStorage. useGate will simply return false until StatsigProvider is initialized later, no context needed from you. Give it a try :).

if (userID != null) {
    return (
      <StatsigProvider
        sdkKey={...}
        user={{
          userID
        }}
        waitForInitialization={true}
      >
        <Component {...pageProps} />
      </StatsigProvider>
    );
  } else {
    return <Component {...pageProps} />;
  }

2. If you do not need to create your own userID for any reason: You can simply wrap StatsigProvider around your child component during the initial rendering, and provide no userID at all. Statsig SDK creates a "stableID" that is stored in the localStorage and will use it whenever you do not provide a userID. So if you don't need your own userID for anything else, you can simply rely on Statsig.

  return (
    <StatsigProvider
      sdkKey={...}
    >
      <Component {...pageProps} />
    </StatsigProvider>
  );

NOTE: when you test this, your gate value on your device will be consistent across sessions, so you will always get either false or true. You need to clear your local storage to try to get a different value if you wish.

  1. If you don't like any of the above options, you can simply use pure JavaScript SDK like you did before. Though I believe either option above is better.

We think the first two options should work for your scenario, depending on whether you need to create your own userID or not, 2) is slightly simpler than 1). We'd be happy to chat about this via a VC call if you still have issues or questions. Feel free to ping me on Slack if that's needed!

youminkim commented 3 years ago

Sorry for the late reply! Busy with other feature launch. Let me try soon the recommended path

vijaye-statsig commented 3 years ago

No worries Youmin - let us know what works, we want to make sure the SDK fits your needs.

ShruthiNicheSoft commented 3 years ago

Hi @jkw-statsig I am also facing the same issue. Refer https://stackoverflow.com/questions/69113756/statsig-how-to-get-true-value-from-feature-gate. Can u help me out for this issue asap?

vijaye-statsig commented 3 years ago

@rshruthir, could you post the entire file? One of our engineers will be up in a few hours to help you debug.

Meanwhile, please also feel free to join our slack channel: https://statsig.com/slack

tore-statsig commented 2 years ago

I think this is very out of date, and is being referenced by more folks for unrelated issues. I'd recommend trying v1.0.0 of the react SDK. Feel free to open a new issue or ask in slack if there are additional problems using the SDK. @youminkim if you feel this was closed before your issue was resolved, feel free to reopen with the latest status, but first I would try the latest version of the SDK.

Thanks!