Flagsmith / flagsmith-js-client

Javascript Client for Flagsmith. Ship features with confidence using feature flags and remote config. Host yourself or use our hosted version at https://www.flagsmith.com/

Home Page:https://www.flagsmith.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

useFlags() race condition

EcksDy opened this issue · comments

commented

Submitted empty by mistake, here are the details :)

I've experienced an issue with segment overrides.

I have the following setup:

  • Version: "flagsmith": "^3.18.3"
  • Next.js app
  • Feature flag X that is off
  • Segment for different traits on identities
  • Flag X enabled for the segment

Flagsmith init in _app.tsx

MyApp.getServerSideProps = async () => {
  await flagsmith.init({
    // fetches flags on the server and passes them to the App
    environmentID: process.env.NEXT_PUBLIC_FLAGSMITH_ID ?? '',
  });
  return { flagsmithState: flagsmith.getState() };
};

When I login with the identity and navigate to the page with useFlags(['X']), it will send 2 requests to the backend:

  • GET https://edge.api.flagsmith.com/api/v1/flags/
[...
  {
	"14": {
		"enabled": false,
		"feature": {
			"type": "STANDARD",
			"id": 62834,
			"name": "X"
		},
		"feature_state_value": null
	}
 }
]
  • POST https://edge.api.flagsmith.com/api/v1/identities/
{
  "identifier": "yury@preql.com",
  "flags": [...
    {
      "14": {
        "feature": {
	  "name": "X",
	  "id": 62834,
	  "type": "STANDARD"
        },
	"feature_state_value": null,
	"enabled": true
      }
    }
  ] 
}

When these requests come in this order (flags > identities), everything works as expected.
But if the network gods decide otherwise, flags with their enabled: false state will override the enabled: true that comes from the identity.

I assumed identities should have precedence over the feature flags.

Edit

@matthewelwell provided the details, thank you

Hi @EcksDy, can you provide more information here please?

commented

@matthewelwell
Ping in case the edit didnt notify ✨

Thanks @EcksDy, you're right - I hadn't received any notification. We will take a look into this in more detail now and get back to you.

@EcksDy , reading through this, I think the first step is to diagnose why there's a call to "/flags".

Looking at the above implementation, flagsmith.init({}) is being called on getServerSideProps, this means that the client has the environment flags.

If the 2 requests you mention are on the client, I have a feeling that you're calling flagsmith.identify pretty much synchronously to the mounting of the provider, by default this provider fetches flags on the client to ensure freshness regardless of SSR calls.

The way I see it there's a couple options here:

1 - If you know whether the user is logged in, pass identity as a prop to the Flagsmith provider, this will tell the SDK to initialise with the logged in user.
2 - Prevent the FlagsmithProvider from causing a fetch on the client, you can use the preventFetch option for that. This will also have the additional benefit of reducing API calls.

Regarding the latter, it seems there's probably an improvement we can make, if we receive an API response where the identity has changed since then just ignore the response. I'll check this out now.

commented

Hey @kyle-ssg , thanks for responding, I've poked at it more and found the following:

The getServerSideProps wasn't triggered at all, and the FlagsmithProvider is initiated with the flagsmith id on the client.

In the custom AuthProvider we have, we call to flagsmith.identify(...) with all the relevant data. I'm guessing that's what triggered /identities requests.

Currently I've removed the flagsmith.init and the flagsmith.identify and changed to the following setup:

const flagsmithRef = useRef(createFlagsmithInstance()); // createFlagsmithInstance from `flagsmith/isomorphic`
const auth = useAuth();

return ( 
  <FlagsmithProvider
    flagsmith={flagsmithRef.current}
    key={auth.user?.email} 
    options={{
      environmentID: process.env.NEXT_PUBLIC_FLAGSMITH_ID ?? '',
      identity: auth.user?.email,
    }}
  >
  ...

Interesting observation is that using key/options.identity alone does nothing. Only adds requests to /flags.
Using them together will add requests to /api/v1/identities/?identifier=yury%40preql.com.

So now it looks like:
image

This still results in identity flakiness.
What else am I missing?

"Interesting observation is that using key/options.identity alone does nothing. Only adds requests to /flags."

I've just verified this and I do not believe that to be the case.

The following code requests /identities

  <FlagsmithProvider options={{
      environmentID: "API_KEY",
      identity:"test"
  }} flagsmith={flagsmith}>

I believe the reason why you're seeing multiple requests is due to "auth.user?.email" changing in quick succession, remounting the Provider since you've provided a key.

I don't think adding a key here and remounting the provider is the best approach. Instead I think you should

  • Detect whether the user is already logged in before mounting any component in order to cut out that confusion
  • When the auth.user?.email changes call identify or logout

@EcksDy , reading through this, I think the first step is to diagnose why there's a call to "/flags".

Looking at the above implementation, flagsmith.init({}) is being called on getServerSideProps, this means that the client has the environment flags.

If the 2 requests you mention are on the client, I have a feeling that you're calling flagsmith.identify pretty much synchronously to the mounting of the provider, by default this provider fetches flags on the client to ensure freshness regardless of SSR calls.

The way I see it there's a couple options here:

1 - If you know whether the user is logged in, pass identity as a prop to the Flagsmith provider, this will tell the SDK to initialise with the logged in user. 2 - Prevent the FlagsmithProvider from causing a fetch on the client, you can use the preventFetch option for that. This will also have the additional benefit of reducing API calls.

Regarding the latter, it seems there's probably an improvement we can make, if we receive an API response where the identity has changed since then just ignore the response. I'll check this out now.

We have now deployed #205 as part of version 3.22.0. If I'm not mistaken, it will resolve the initial issue you were seeing where rapid requests to unidentified / identified users could interfere due to race condition. That being said, if an application faces this it signifies that identity resolution probably should happen before initialising Flagsmith to avoid multiple simultaneous API calls.

Please feel free to re-open this if you think there's still an issue.