PostHog / posthog-js-lite

Reimplementation of posthog-js to be as light and modular as possible.
https://posthog.com/docs/libraries
MIT License
70 stars 36 forks source link

setPersonPropertiesForFlags not called automatically when updating person properties in RN #235

Open itsramiel opened 5 months ago

itsramiel commented 5 months ago

Bug description

I am using using user properties to match surveys to a specific user. For example i setup a survey to be shown only if survey_shown/{survey_id} is not set as a property on the user.

The issue is that when I set the user property as I capture an event like so:

      posthog.capture('survey shown', {
        $survey_id: survey.id,
        $set: {
          [`survey_shown/${survey.id}`]: true,
        },
      });

and flush the events with await posthog.flush(); the feature flags are not immediately correct and the survey matching is incorrect as a result. It takes some time(second or two) for the feature flags to become correct and correctly match the surveys. This leads to issues where I show the survey multiple times or I dont show a survey that was dependent on the previous survey.

How to reproduce

  1. Create an api survey and set the condition to be survey_shown/{survey if of created survey} is not set
  2. call the following function twice while awaiting each call to ensure flushing occurred:

    const getSurveysAndMarkAsShown = async () => {
    const response = await fetch(`${HOST_URL}/api/surveys?token=${API_KEY}`);
    const json = (await response.json()) as TGetPosthogSurveysResponse;
    
    const featureFlags = await posthog.reloadFeatureFlagsAsync();
    if (!featureFlags) return;
    
    const matchingSurveys = getMatchingSurveys(json.surveys, featureFlags);
    console.log('matchingSurveys', matchingSurveys);
    matchingSurveys.forEach(survey => {
      posthog.capture('survey shown', {
        $survey_id: survey.id,
        $set: {
        },
      });
    });
    
    await posthog.flush();
    };
    
    // ......
    
    await getSurveysAndMarkAsShown();
    await getSurveysAndMarkAsShown();
  3. Notice that even though in the first call we are setting the user property that we are matching against and we are waiting for the events to be flushed, the same survey is returned in the subsequent call. However in a second or two the survey will no longer be matching(matching took effect)

Related sub-libraries

Additional context

Complete App:

import {usePostHog, PostHogProvider} from 'posthog-react-native';
import {Button, StyleSheet, View} from 'react-native';

const HOST_URL = 'your-host';
const API_KEY = 'your-api-key';

function App() {
  return (
    <PostHogProvider
      apiKey={API_KEY}
      options={{
        // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
        host: HOST_URL, // host is optional if you use https://us.i.posthog.com
      }}>
      <MyComponent />
    </PostHogProvider>
  );
}

export default App;

type TPosthogSurvey = {
  id: string;
  linked_flag_key: string | null;
  targeting_flag_key: string | null;
  questions: Array<{
    type: string;
    choices: Array<string>;
    question: string;
    description?: string;
  }>;
  conditions: {
    selector?: string;
    seenSurveyWaitPeriodInDays?: number;
  } | null;
  start_date: string | null;
  end_date: string | null;
};

function getMatchingSurveys(
  surveys: Array<TPosthogSurvey>,
  featureFlags: {[key: string]: string | boolean},
) {
  return surveys.filter(survey => {
    const isActive =
      typeof survey.start_date === 'string' &&
      typeof survey.end_date !== 'string';
    if (!isActive) return false;

    const linkedFlagCheck = survey.linked_flag_key
      ? !!featureFlags[survey.linked_flag_key]
      : true;

    const targetingFlagCheck = survey.targeting_flag_key
      ? !!featureFlags[survey.targeting_flag_key]
      : true;

    return linkedFlagCheck && targetingFlagCheck;
  });
}

type TGetPosthogSurveysResponse = {
  surveys: Array<TPosthogSurvey>;
};

const MyComponent = () => {
  const posthog = usePostHog();

  const getSurveysAndMarkAsShown = async () => {
    const response = await fetch(`${HOST_URL}/api/surveys?token=${API_KEY}`);
    const json = (await response.json()) as TGetPosthogSurveysResponse;

    const featureFlags = await posthog.reloadFeatureFlagsAsync();
    if (!featureFlags) return;

    const matchingSurveys = getMatchingSurveys(json.surveys, featureFlags);
    console.log('matchingSurveys', matchingSurveys);
    matchingSurveys.forEach(survey => {
      posthog.capture('survey shown', {
        $survey_id: survey.id,
        $set: {
          [`survey_shown/${survey.id}`]: true,
        },
      });
    });

    await posthog.flush();
  };

  const onPress = async () => {
    await getSurveysAndMarkAsShown();
    await getSurveysAndMarkAsShown();
  };

  return (
    <View style={styles.screen}>
      <Button title="get surveys" onPress={onPress} />
    </View>
  );
};

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
marandaneto commented 5 months ago

cc @PostHog/team-feature-success

neilkakkar commented 5 months ago

Hi @itsramiel thanks for flagging - it seems like we don't yet handle automatically updating flag props in RN yet.

For now, you can get around this by calling setPersonPropertiesForFlags manually whenever you're adding $set to events - this will keep them in sync properly.

itsramiel commented 5 months ago

Hey @neilkakkar

That seems to do the job. Do I still need to call flush now or do I drop that from my implementation? It seems to work without it, but interested in knowing what you think

neilkakkar commented 5 months ago

You don't need to flush, but since this is running on the client, I'd recommend flushing anyway (no need to await it though) so you're not losing events for whatever reason (like force quit, etc. etc.). One other reason can be syncing across devices - if the user shows up on both say ios/android/web, and you don't want the survey to show again everywhere, the faster the event makes it in, the faster things will be in sync.

But if this is not a concern, you're all good 👌 .

I'll keep this ticket open anyway for now to implement this bit automatically.