useFlags() race condition
EcksDy opened this issue · comments
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?
@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.
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
.
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.