feat: Support feature flag evaluation server side (#5511)

* feat(editor): roll out schema view

* feat(editor): add posthog tracking

* refactor: use composables

* refactor: clean up console log

* refactor: clean up impl

* chore: clean up impl

* fix: fix demo var

* chore: add comment

* refactor: clean up

* chore: wrap error func

* refactor: clean up import

* refactor: make store

* feat: enable rudderstack usebeacon, move event to unload

* chore: clean up alert

* refactor: move tracking from hooks

* fix: reload flags on login

* fix: add func to setup

* fix: clear duplicate import

* chore: add console to tesT

* chore: add console to tesT

* fix: try reload

* chore: randomize instnace id for testing

* chore: randomize instnace id for testing

* chore: add console logs for testing

* chore: move random id to fe

* chore: use query param for testing

* feat: update PostHog api endpoint

* feat: update rs host

* feat: update rs host

* feat: update rs endpoints

* refactor: use api host for BE events as well

* refactor: refactor out posthog client

* feat: add feature flags to login

* feat: add feature flags to login

* feat: get feature flags to work

* feat: add created at to be events

* chore: add todos

* chore: clean up store

* chore: add created at to identify

* feat: add posthog config to settings

* feat: add bootstrapping

* chore: clean up

* chore: fix build

* fix: get dates to work

* fix: get posthog to recognize dates

* chore: refactor

* fix: update back to number

* fix: update key

* fix: get experiment evals to work

* feat: add posthog to signup router

* feat: add feature flags on sign up

* chore: clean up

* fix: fix import

* chore: clean up loading script

* feat: add timeout, fix: script loader

* fix: test timeout and get working on 8080

* refactor: move out posthog

* feat: add experiment tracking

* fix: clear tracked on reset

* fix: fix signup bug

* fix: handle errors when telmetry is disabled

* refactor: remove redundant await

* fix: add back posthog to telemetry

* test: fix test

* test: fix test

* test: add tests for posthog client

* lint: fix

* fix: fix issue with slow decide endpoint

* lint: fix

* lint: fix

* lint: fix

* lint: fix

* chore: address PR feedback

* chore: address PR feedback

* feat: add onboarding experiment
This commit is contained in:
Mutasem Aldmour
2023-02-21 11:35:35 +03:00
committed by GitHub
parent ee21b7a1cf
commit 26a20ed47e
29 changed files with 513 additions and 122 deletions

View File

@@ -0,0 +1,124 @@
import { ref, Ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useUsersStore } from '@/stores/users';
import { useRootStore } from '@/stores/n8nRootStore';
import { useSettingsStore } from './settings';
import { FeatureFlags } from 'n8n-workflow';
import { EXPERIMENTS_TO_TRACK } from '@/constants';
import { useTelemetryStore } from './telemetry';
export const usePostHogStore = defineStore('posthog', () => {
const usersStore = useUsersStore();
const settingsStore = useSettingsStore();
const telemetryStore = useTelemetryStore();
const rootStore = useRootStore();
const featureFlags: Ref<FeatureFlags | null> = ref(null);
const initialized: Ref<boolean> = ref(false);
const trackedDemoExp: Ref<FeatureFlags> = ref({});
const reset = () => {
window.posthog?.reset?.();
featureFlags.value = null;
trackedDemoExp.value = {};
};
const getVariant = (experiment: keyof FeatureFlags): FeatureFlags[keyof FeatureFlags] => {
return featureFlags.value?.[experiment];
};
const isVariantEnabled = (experiment: string, variant: string) => {
return getVariant(experiment) === variant;
};
const identify = () => {
const instanceId = rootStore.instanceId;
const user = usersStore.currentUser;
const traits: Record<string, string | number> = { instance_id: instanceId };
if (user && typeof user.createdAt === 'string') {
traits.created_at_timestamp = new Date(user.createdAt).getTime();
}
// For PostHog, main ID _cannot_ be `undefined` as done for RudderStack.
const id = user ? `${instanceId}#${user.id}` : instanceId;
window.posthog?.identify?.(id, traits);
};
const init = (evaluatedFeatureFlags?: FeatureFlags) => {
if (!window.posthog) {
return;
}
const config = settingsStore.settings.posthog;
if (!config.enabled) {
return;
}
const userId = usersStore.currentUserId;
if (!userId) {
return;
}
const instanceId = rootStore.instanceId;
const distinctId = `${instanceId}#${userId}`;
const options: Parameters<typeof window.posthog.init>[1] = {
api_host: config.apiHost,
autocapture: config.autocapture,
disable_session_recording: config.disableSessionRecording,
debug: config.debug,
};
if (evaluatedFeatureFlags) {
featureFlags.value = evaluatedFeatureFlags;
options.bootstrap = {
distinctId,
featureFlags: evaluatedFeatureFlags,
};
}
window.posthog?.init(config.apiKey, options);
identify();
initialized.value = true;
};
const trackExperiment = (name: string) => {
const curr = featureFlags.value;
const prev = trackedDemoExp.value;
if (!curr || curr[name] === undefined) {
return;
}
if (curr[name] === prev[name]) {
return;
}
const variant = curr[name];
telemetryStore.track('User is part of experiment', {
name,
variant,
});
trackedDemoExp.value[name] = variant;
};
watch(
() => featureFlags.value,
() => {
setTimeout(() => {
EXPERIMENTS_TO_TRACK.forEach(trackExperiment);
}, 0);
},
);
return {
init,
isVariantEnabled,
getVariant,
reset,
};
});

View File

@@ -0,0 +1,21 @@
import type { Telemetry } from '@/plugins/telemetry';
import { ITelemetryTrackProperties } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { ref, Ref } from 'vue';
export const useTelemetryStore = defineStore('telemetry', () => {
const telemetry: Ref<Telemetry | undefined> = ref();
const init = (tel: Telemetry) => {
telemetry.value = tel;
};
const track = (event: string, properties?: ITelemetryTrackProperties) => {
telemetry.value?.track(event, properties);
};
return {
init,
track,
};
});

View File

@@ -34,6 +34,7 @@ import { getPersonalizedNodeTypes, isAuthorized, PERMISSIONS, ROLE } from '@/uti
import { defineStore } from 'pinia';
import Vue from 'vue';
import { useRootStore } from './n8nRootStore';
import { usePostHogStore } from './posthog';
import { useSettingsStore } from './settings';
import { useUIStore } from './ui';
@@ -141,23 +142,32 @@ export const useUsersStore = defineStore(STORES.USERS, {
async loginWithCookie(): Promise<void> {
const rootStore = useRootStore();
const user = await loginCurrentUser(rootStore.getRestApiContext);
if (user) {
this.addUsers([user]);
this.currentUserId = user.id;
if (!user) {
return;
}
this.addUsers([user]);
this.currentUserId = user.id;
usePostHogStore().init(user.featureFlags);
},
async loginWithCreds(params: { email: string; password: string }): Promise<void> {
const rootStore = useRootStore();
const user = await login(rootStore.getRestApiContext, params);
if (user) {
this.addUsers([user]);
this.currentUserId = user.id;
if (!user) {
return;
}
this.addUsers([user]);
this.currentUserId = user.id;
usePostHogStore().init(user.featureFlags);
},
async logout(): Promise<void> {
const rootStore = useRootStore();
await logout(rootStore.getRestApiContext);
this.currentUserId = null;
usePostHogStore().reset();
},
async preOwnerSetup() {
return preOwnerSetup(useRootStore().getRestApiContext);
@@ -197,6 +207,8 @@ export const useUsersStore = defineStore(STORES.USERS, {
this.addUsers([user]);
this.currentUserId = user.id;
}
usePostHogStore().init(user.featureFlags);
},
async sendForgotPasswordEmail(params: { email: string }): Promise<void> {
const rootStore = useRootStore();