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:
124
packages/editor-ui/src/stores/posthog.ts
Normal file
124
packages/editor-ui/src/stores/posthog.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
21
packages/editor-ui/src/stores/telemetry.ts
Normal file
21
packages/editor-ui/src/stores/telemetry.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user