amplitude / experiment-js-client

Amplitude Experiment client-side SDK for JavaScript
MIT License
8 stars 7 forks source link

Variants never update without a refresh #109

Closed BrandonHigbee closed 5 months ago

BrandonHigbee commented 5 months ago

version: "@amplitude/experiment-js-client": "^1.10.2"

goal: when I update an Amplitude feature flag from the Experiment user interface I want my app to update without needing user input such as refreshing the page

observed behavior: flags never update until I refresh the page (it seems like it may be tied to the experiment assignment event which doesnt get emitted afaik until the sdk is reinitialized?)

Example react code:

const FeatureFlagsContext = createContext<ContextProviderProps | undefined>(
  undefined,
);

const AmpliFeatureFlagsProvider: React.FC<ProviderProps> = ({
  authServiceUrl,
  accessToken,
  ampliKey,
  children,
}) => {
  const [flags, setFlags] = useState<FeatureFlags>({});
  const [loading, setLoading] = useState(true);
  const [initialized, setInitialized] = useState(false);
  const [_, setError] = React.useState<Error | undefined>();

  const experiment = React.useMemo(
    () =>
      Experiment.initializeWithAmplitudeAnalytics(ampliKey, {
        debug: true,
        automaticFetchOnAmplitudeIdentityChange: true,
      }),
    [ampliKey],
  );
  const authService = React.useMemo(
    () => new TackleAuthService(authServiceUrl, serviceName),
    [authServiceUrl],
  );
  useEffect(() => {
    const identifyUser = async (token: string): Promise<AmpliUser> => {
      const principal = await authService.whoAmI(token);
      return {
        user_id: getId(principal),
        user_properties: {
          someProp: principal.something || '',
          otherProp: principal.other || '',
        },
      };
    };

    const startAmplitude = async (user: AmpliUser) => {
      await experiment.start(user);
      console.log('amp started');
      setLoading(false);
      setInitialized(true);
    };

    if (!initialized && accessToken) {
      identifyUser(accessToken)
        .then((user) => {
          void startAmplitude(user);
        })
        .catch((error: Error) => {
          setError(error);
        });
    }
  }, [accessToken]);

  useEffect(() => {
    const grabAndUpdateFlags = (
      flags: FeatureFlags,
      experiment: ExperimentClient,
    ) => {
      console.log('Checking for updated flags');
      const evaluatedFlags = experiment.all();
      console.log('Evaluated flags:', JSON.stringify(evaluatedFlags));
      console.log('current flags', JSON.stringify(flags));
      setFlags((prev) => ({ ...prev, ...evaluatedFlags }));
    };
    if (!loading && initialized) {
      const id = setInterval(() => grabAndUpdateFlags(flags, experiment), 5000);
      return () => clearInterval(id);
    }
  }, [flags, loading, initialized, experiment]);

  return (
    <ContextProvider loading={loading} flags={flags}>
      {children}
    </ContextProvider>
  );
};

const ContextProvider: React.FC<ContextProps> = ({
  loading,
  flags,
  children,
}) => {
  const context = useMemo(() => {
    return {
      loading,
      flags,
    };
  }, [loading, flags]);

  return (
    <FeatureFlagsContext.Provider value={context}>
      {children}
    </FeatureFlagsContext.Provider>
  );
};

Log output:

amp started
amp started
Checking for updated flags
Evaluated flags: {"first":{"key":"off","metadata":{"default":true}}}
current flags {}
Checking for updated flags
Evaluated flags: {"first":{"key":"off","metadata":{"default":true}}}
current flags {"first":{"key":"off","metadata":{"default":true}}}
Checking for updated flags
Evaluated flags: {"first":{"key":"off","metadata":{"default":true}}}
current flags {"first":{"key":"off","metadata":{"default":true}}}

This repeats forever regardless of the changes I make in amplitude. Any help is appreciated. If there is an easier way to subscribe to flag changes then please advise :)

BrandonHigbee commented 5 months ago

In further testing if I update my interval to fetch the user then I see updates from the server. I was under the impression that the polling mechanism kicked off by await experiment.start(user); would handle this, otherwise it is not clear what the polling is doing.

const fetchFlags = () => {
    experiment
      .fetch()
      .then(() => {
        const evaluatedFlags = experiment.all();
        console.log('evaluatedFlags', evaluatedFlags);
        setFlags((prev) => ({ ...prev, ...evaluatedFlags }));
      })
      .catch((error: Error) => {
        setError(error);
      });
  };
bgiori commented 5 months ago

@BrandonHigbee Hey thanks for submitting this. Yes you're right, the poller is just for local evaluation flags, not remote evaluation.

To re-evaluate for remote evaluation, you will need to call fetch() manually. If you dont use local evaluation you can turn of the polling for local evaluation flag configurations from calling start using pollOnStart: false in the config.