feat: Add test overrides (#5642)

* feat: Add test overrides

* feat: add more func to test with

* test: add tests for posthog store

* fix: only init once

* fix: only init once

* test: fix
This commit is contained in:
Mutasem Aldmour 2023-03-24 15:57:22 +01:00 committed by GitHub
parent 78c9707fa7
commit 696e43a919
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 215 additions and 7 deletions

View file

@ -71,6 +71,11 @@ declare global {
analytics?: {
track(event: string, proeprties?: ITelemetryTrackProperties): void;
};
featureFlags?: {
getAll: () => FeatureFlags;
getVariant: (name: string) => string | boolean | undefined;
override: (name: string, value: string) => void;
};
}
}
@ -579,6 +584,7 @@ export interface IUserResponse {
firstName?: string;
lastName?: string;
email?: string;
createdAt?: string;
globalRole?: {
name: IRole;
id: string;
@ -599,7 +605,6 @@ export interface IUser extends IUserResponse {
isOwner: boolean;
inviteAcceptUrl?: string;
fullName?: string;
createdAt?: string;
}
export interface IVersionNotificationSettings {

View file

@ -325,6 +325,7 @@ export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOV
export const LOCAL_STORAGE_MAPPING_IS_ONBOARDED = 'N8N_MAPPING_ONBOARDED';
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
export const LOCAL_STORAGE_THEME = 'N8N_THEME';
export const LOCAL_STORAGE_EXPERIMENT_OVERRIDES = 'N8N_EXPERIMENT_OVERRIDES';
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
export const HIRING_BANNER = `

View file

@ -0,0 +1,150 @@
import { createPinia, setActivePinia } from 'pinia';
import { usePostHog } from './posthog';
import { useUsersStore } from './users';
import { useSettingsStore } from './settings';
import { IN8nUISettings } from '@/Interface';
import { useRootStore } from './n8nRootStore';
import { useTelemetryStore } from './telemetry';
const DEFAULT_POSTHOG_SETTINGS: IN8nUISettings['posthog'] = {
enabled: true,
apiHost: 'host',
apiKey: 'key',
autocapture: false,
disableSessionRecording: true,
debug: false,
};
const CURRENT_USER_ID = '1';
const CURRENT_INSTANCE_ID = '456';
function setSettings(overrides?: Partial<IN8nUISettings>) {
useSettingsStore().setSettings({
posthog: DEFAULT_POSTHOG_SETTINGS,
instanceId: CURRENT_INSTANCE_ID,
...overrides,
} as IN8nUISettings);
useRootStore().setInstanceId(CURRENT_INSTANCE_ID);
}
function setCurrentUser() {
useUsersStore().addUsers([
{
id: CURRENT_USER_ID,
isPending: false,
createdAt: '2023-03-17T14:01:36.432Z',
},
]);
useUsersStore().currentUserId = CURRENT_USER_ID;
}
function resetStores() {
useSettingsStore().$reset();
useUsersStore().$reset();
}
function setup() {
setActivePinia(createPinia());
window.posthog = {
init: () => {},
identify: () => {},
};
const telemetryStore = useTelemetryStore();
vi.spyOn(window.posthog, 'init');
vi.spyOn(window.posthog, 'identify');
vi.spyOn(window.Storage.prototype, 'setItem');
vi.spyOn(telemetryStore, 'track');
}
describe('Posthog store', () => {
describe('should not init', () => {
beforeEach(() => {
setup();
});
it('should not init if posthog is not enabled', () => {
setSettings({ posthog: { ...DEFAULT_POSTHOG_SETTINGS, enabled: false } });
setCurrentUser();
const posthog = usePostHog();
posthog.init();
expect(window.posthog?.init).not.toHaveBeenCalled();
});
it('should not init if user is not logged in', () => {
setSettings();
const posthog = usePostHog();
posthog.init();
expect(window.posthog?.init).not.toHaveBeenCalled();
});
afterEach(() => {
resetStores();
});
});
describe('should init posthog', () => {
beforeEach(() => {
setup();
setSettings();
setCurrentUser();
});
it('should init store with serverside flags', () => {
const TEST = 'test';
const flags = {
[TEST]: 'variant',
};
const posthog = usePostHog();
posthog.init(flags);
expect(posthog.getVariant('test')).toEqual(flags[TEST]);
expect(window.posthog?.init).toHaveBeenCalled();
});
it('should identify user', () => {
const posthog = usePostHog();
posthog.init();
const userId = `${CURRENT_INSTANCE_ID}#${CURRENT_USER_ID}`;
expect(window.posthog?.identify).toHaveBeenCalledWith(userId, {
created_at_timestamp: 1679061696432,
instance_id: CURRENT_INSTANCE_ID,
});
});
it('sets override feature flags', () => {
const TEST = 'test';
const flags = {
[TEST]: 'variant',
};
const posthog = usePostHog();
posthog.init(flags);
window.featureFlags?.override(TEST, 'override');
expect(posthog.getVariant('test')).toEqual('override');
expect(window.posthog?.init).toHaveBeenCalled();
expect(window.localStorage.setItem).toHaveBeenCalledWith(
'N8N_EXPERIMENT_OVERRIDES',
JSON.stringify({ test: 'override' }),
);
window.featureFlags?.override('other_test', 'override');
expect(window.localStorage.setItem).toHaveBeenCalledWith(
'N8N_EXPERIMENT_OVERRIDES',
JSON.stringify({ test: 'override', other_test: 'override' }),
);
});
afterEach(() => {
resetStores();
window.localStorage.clear();
window.featureFlags = undefined;
});
});
});

View file

@ -4,7 +4,11 @@ import { useUsersStore } from '@/stores/users';
import { useRootStore } from '@/stores/n8nRootStore';
import { useSettingsStore } from '@/stores/settings';
import { FeatureFlags } from 'n8n-workflow';
import { EXPERIMENTS_TO_TRACK, ONBOARDING_EXPERIMENT } from '@/constants';
import {
EXPERIMENTS_TO_TRACK,
LOCAL_STORAGE_EXPERIMENT_OVERRIDES,
ONBOARDING_EXPERIMENT,
} from '@/constants';
import { useTelemetryStore } from './telemetry';
import { useSegment } from './segment';
import { debounce } from 'lodash-es';
@ -23,6 +27,8 @@ export const usePostHog = defineStore('posthog', () => {
const featureFlags: Ref<FeatureFlags | null> = ref(null);
const trackedDemoExp: Ref<FeatureFlags> = ref({});
const overrides: Ref<Record<string, string | boolean>> = ref({});
const reset = () => {
window.posthog?.reset?.();
featureFlags.value = null;
@ -37,6 +43,33 @@ export const usePostHog = defineStore('posthog', () => {
return getVariant(experiment) === variant;
};
if (!window.featureFlags) {
// for testing
const cachedOverrdies = localStorage.getItem(LOCAL_STORAGE_EXPERIMENT_OVERRIDES);
if (cachedOverrdies) {
try {
overrides.value = JSON.parse(cachedOverrdies);
} catch (e) {}
}
window.featureFlags = {
// since features are evaluated serverside, regular posthog mechanism to override clientside does not work
override: (name: string, value: string | boolean) => {
overrides.value[name] = value;
featureFlags.value = {
...featureFlags.value,
[name]: value,
};
try {
localStorage.setItem(LOCAL_STORAGE_EXPERIMENT_OVERRIDES, JSON.stringify(overrides.value));
} catch (e) {}
},
getVariant,
getAll: () => featureFlags.value || {},
};
}
const identify = () => {
const instanceId = rootStore.instanceId;
const user = usersStore.currentUser;
@ -51,6 +84,13 @@ export const usePostHog = defineStore('posthog', () => {
window.posthog?.identify?.(id, traits);
};
const addExperimentOverrides = () => {
featureFlags.value = {
...featureFlags.value,
...overrides.value,
};
};
const init = (evaluatedFeatureFlags?: FeatureFlags) => {
if (!window.posthog) {
return;
@ -86,10 +126,12 @@ export const usePostHog = defineStore('posthog', () => {
featureFlags: evaluatedFeatureFlags,
};
trackExperiments(evaluatedFeatureFlags);
addExperimentOverrides();
} else {
// depend on client side evaluation if serverside evaluation fails
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
featureFlags.value = map;
addExperimentOverrides();
trackExperiments(map);
});
}
@ -101,6 +143,10 @@ export const usePostHog = defineStore('posthog', () => {
const trackExperiment = (featureFlags: FeatureFlags, name: string) => {
const variant = featureFlags[name];
if (!variant || trackedDemoExp.value[name] === variant) {
return;
}
telemetryStore.track(EVENTS.IS_PART_OF_EXPERIMENT, {
name,
variant,

View file

@ -179,13 +179,19 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
setSettings(settings: IN8nUISettings): void {
this.settings = settings;
this.userManagement = settings.userManagement;
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
if (this.userManagement) {
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
}
this.api = settings.publicApi;
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;
this.ldap.loginLabel = settings.sso.ldap.loginLabel;
this.saml.loginEnabled = settings.sso.saml.loginEnabled;
this.saml.loginLabel = settings.sso.saml.loginLabel;
if (settings.sso?.ldap) {
this.ldap.loginEnabled = settings.sso.ldap.loginEnabled;
this.ldap.loginLabel = settings.sso.ldap.loginLabel;
}
if (settings.sso?.saml) {
this.saml.loginEnabled = settings.sso.saml.loginEnabled;
this.saml.loginLabel = settings.sso.saml.loginLabel;
}
},
async getSettings(): Promise<void> {
const rootStore = useRootStore();