feat(editor): Show template credential setup based on feature flag (#7989)

Replace the local storage based feature flag with posthog feature flag.

Also:
- Fix bunch of eslint warnings in posthog store
This commit is contained in:
Tomi Turtiainen 2023-12-11 20:21:10 +02:00 committed by GitHub
parent c378f60a25
commit 08ee307209
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 89 additions and 87 deletions

View file

@ -4,12 +4,11 @@ import {
visitTemplateCollectionPage, visitTemplateCollectionPage,
testData, testData,
} from '../pages/template-collection'; } from '../pages/template-collection';
import { TemplateCredentialSetupPage } from '../pages/template-credential-setup'; import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
import { TemplateWorkflowPage } from '../pages/template-workflow'; import { TemplateWorkflowPage } from '../pages/template-workflow';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowPage } from '../pages/workflow';
const templateWorkflowPage = new TemplateWorkflowPage(); const templateWorkflowPage = new TemplateWorkflowPage();
const templateCredentialsSetupPage = new TemplateCredentialSetupPage();
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
const messageBox = new MessageBox(); const messageBox = new MessageBox();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -25,7 +24,8 @@ describe('Template credentials setup', () => {
it('can be opened from template workflow page', () => { it('can be opened from template workflow page', () => {
templateWorkflowPage.actions.visit(testTemplate.id); templateWorkflowPage.actions.visit(testTemplate.id);
templateCredentialsSetupPage.actions.enableFeatureFlag(); templateWorkflowPage.getters.useTemplateButton().should('be.visible');
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
templateWorkflowPage.actions.clickUseThisWorkflowButton(); templateWorkflowPage.actions.clickUseThisWorkflowButton();
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
@ -35,7 +35,7 @@ describe('Template credentials setup', () => {
it('can be opened from template collection page', () => { it('can be opened from template collection page', () => {
visitTemplateCollectionPage(testData.ecommerceStarterPack); visitTemplateCollectionPage(testData.ecommerceStarterPack);
templateCredentialsSetupPage.actions.enableFeatureFlag(); templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram'); clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
@ -44,7 +44,7 @@ describe('Template credentials setup', () => {
}); });
it('can be opened with a direct url', () => { it('can be opened with a direct url', () => {
templateCredentialsSetupPage.actions.visit(testTemplate.id); templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`) .title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
@ -52,7 +52,7 @@ describe('Template credentials setup', () => {
}); });
it('has all the elements on page', () => { it('has all the elements on page', () => {
templateCredentialsSetupPage.actions.visit(testTemplate.id); templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters templateCredentialsSetupPage.getters
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`) .title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
@ -83,14 +83,14 @@ describe('Template credentials setup', () => {
}); });
it('can skip template creation', () => { it('can skip template creation', () => {
templateCredentialsSetupPage.actions.visit(testTemplate.id); templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters.skipLink().click(); templateCredentialsSetupPage.getters.skipLink().click();
workflowPage.getters.canvasNodes().should('have.length', 3); workflowPage.getters.canvasNodes().should('have.length', 3);
}); });
it('can create credentials and workflow from the template', () => { it('can create credentials and workflow from the template', () => {
templateCredentialsSetupPage.actions.visit(testTemplate.id); templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
// Continue button should be disabled if no credentials are created // Continue button should be disabled if no credentials are created
templateCredentialsSetupPage.getters.continueButton().should('be.disabled'); templateCredentialsSetupPage.getters.continueButton().should('be.disabled');

View file

@ -1,40 +1,35 @@
import { BasePage } from './base';
export type TemplateTestData = { export type TemplateTestData = {
id: number; id: number;
fixture: string; fixture: string;
}; };
export class TemplateCredentialSetupPage extends BasePage { export const testData = {
testData = { simpleTemplate: {
simpleTemplate: { id: 1205,
id: 1205, fixture: 'Test_Template_1.json',
fixture: 'Test_Template_1.json', },
}, };
};
getters = { export const getters = {
continueButton: () => cy.getByTestId('continue-button'), continueButton: () => cy.getByTestId('continue-button'),
skipLink: () => cy.get('a:contains("Skip")'), skipLink: () => cy.get('a:contains("Skip")'),
title: (title: string) => cy.get(`h1:contains(${title})`), title: (title: string) => cy.get(`h1:contains(${title})`),
infoCallout: () => cy.getByTestId('info-callout'), infoCallout: () => cy.getByTestId('info-callout'),
createAppCredentialsButton: (appName: string) => createAppCredentialsButton: (appName: string) =>
cy.get(`button:contains("Create new ${appName} credential")`), cy.get(`button:contains("Create new ${appName} credential")`),
appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'), appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'),
stepHeading: ($el: JQuery<HTMLElement>) => stepHeading: ($el: JQuery<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-heading'), cy.wrap($el).findChildByTestId('credential-step-heading'),
stepDescription: ($el: JQuery<HTMLElement>) => stepDescription: ($el: JQuery<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-description'), cy.wrap($el).findChildByTestId('credential-step-description'),
}; };
actions = { export const visitTemplateCredentialSetupPage = (templateId: number) => {
visit: (templateId: number) => { cy.visit(`/templates/${templateId}/setup`);
cy.visit(`/templates/${templateId}/setup`); };
},
enableFeatureFlag: () => { export const enableTemplateCredentialSetupFeatureFlag = () => {
cy.window().then((window) => { cy.window().then((win) => {
window.localStorage.setItem('template-credentials-setup', 'true'); win.featureFlags.override('016_template_credential_setup', true);
}); });
}, };
};
}

View file

@ -41,6 +41,13 @@ declare global {
draganddrop(draggableSelector: string, droppableSelector: string): void; draganddrop(draggableSelector: string, droppableSelector: string): void;
push(type: string, data: unknown): void; push(type: string, data: unknown): void;
shouldNotHaveConsoleErrors(): void; shouldNotHaveConsoleErrors(): void;
window(): Chainable<
AUTWindow & {
featureFlags: {
override: (feature: string, value: any) => void;
};
}
>;
} }
} }
} }

View file

@ -617,7 +617,9 @@ export const ASK_AI_EXPERIMENT = {
gpt4: 'gpt4', gpt4: 'gpt4',
}; };
export const EXPERIMENTS_TO_TRACK = [ASK_AI_EXPERIMENT.name]; export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '016_template_credential_setup';
export const EXPERIMENTS_TO_TRACK = [ASK_AI_EXPERIMENT.name, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT];
export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998; export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998;

View file

@ -14,6 +14,8 @@ const EVENTS = {
IS_PART_OF_EXPERIMENT: 'User is part of experiment', IS_PART_OF_EXPERIMENT: 'User is part of experiment',
}; };
export type PosthogStore = ReturnType<typeof usePostHog>;
export const usePostHog = defineStore('posthog', () => { export const usePostHog = defineStore('posthog', () => {
const usersStore = useUsersStore(); const usersStore = useUsersStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@ -39,6 +41,13 @@ export const usePostHog = defineStore('posthog', () => {
return getVariant(experiment) === variant; return getVariant(experiment) === variant;
}; };
/**
* Checks if the given feature flag is enabled. Should only be used for boolean flags
*/
const isFeatureEnabled = (experiment: keyof FeatureFlags) => {
return featureFlags.value?.[experiment] === true;
};
if (!window.featureFlags) { if (!window.featureFlags) {
// for testing // for testing
const cachedOverrides = useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value; const cachedOverrides = useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value;
@ -65,7 +74,7 @@ export const usePostHog = defineStore('posthog', () => {
}, },
getVariant, getVariant,
getAll: () => featureFlags.value || {}, getAll: () => featureFlags.value ?? {},
}; };
} }
@ -90,6 +99,25 @@ export const usePostHog = defineStore('posthog', () => {
}; };
}; };
const trackExperiment = (featFlags: FeatureFlags, name: string) => {
const variant = featFlags[name];
if (!variant || trackedDemoExp.value[name] === variant) {
return;
}
telemetryStore.track(EVENTS.IS_PART_OF_EXPERIMENT, {
name,
variant,
});
trackedDemoExp.value[name] = variant;
};
const trackExperiments = (featFlags: FeatureFlags) => {
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featFlags, name));
};
const trackExperimentsDebounced = debounce(trackExperiments, 2000);
const init = (evaluatedFeatureFlags?: FeatureFlags) => { const init = (evaluatedFeatureFlags?: FeatureFlags) => {
if (!window.posthog) { if (!window.posthog) {
return; return;
@ -143,25 +171,6 @@ export const usePostHog = defineStore('posthog', () => {
} }
}; };
const trackExperiments = (featureFlags: FeatureFlags) => {
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name));
};
const trackExperimentsDebounced = debounce(trackExperiments, 2000);
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,
});
trackedDemoExp.value[name] = variant;
};
const capture = (event: string, properties: IDataObject) => { const capture = (event: string, properties: IDataObject) => {
if (typeof window.posthog?.capture === 'function') { if (typeof window.posthog?.capture === 'function') {
window.posthog.capture(event, properties); window.posthog.capture(event, properties);
@ -181,6 +190,7 @@ export const usePostHog = defineStore('posthog', () => {
return { return {
init, init,
isFeatureEnabled,
isVariantEnabled, isVariantEnabled,
getVariant, getVariant,
reset, reset,

View file

@ -1,16 +0,0 @@
// Feature flags
export const enum FeatureFlag {
templateCredentialsSetup = 'template-credentials-setup',
}
const hasLocaleStorageKey = (key: string): boolean => {
try {
// Local storage might not be available in all envs e.g. when user has
// disabled it in their browser
return !!localStorage.getItem(key);
} catch (e) {
return false;
}
};
export const isFeatureFlagEnabled = (flag: FeatureFlag): boolean => hasLocaleStorageKey(flag);

View file

@ -1,9 +1,9 @@
import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface'; import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface';
import { getNewWorkflow } from '@/api/workflows'; import { getNewWorkflow } from '@/api/workflows';
import { VIEWS } from '@/constants'; import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
import type { useRootStore } from '@/stores/n8nRoot.store'; import type { useRootStore } from '@/stores/n8nRoot.store';
import type { PosthogStore } from '@/stores/posthog.store';
import type { useWorkflowsStore } from '@/stores/workflows.store'; import type { useWorkflowsStore } from '@/stores/workflows.store';
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
import { getFixedNodesList } from '@/utils/nodeViewUtils'; import { getFixedNodesList } from '@/utils/nodeViewUtils';
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
@ -45,13 +45,16 @@ export async function createWorkflowFromTemplate(
* if the feature flag is disabled) * if the feature flag is disabled)
*/ */
export async function openTemplateCredentialSetup(opts: { export async function openTemplateCredentialSetup(opts: {
posthogStore: PosthogStore;
templateId: string; templateId: string;
router: Router; router: Router;
inNewBrowserTab?: boolean; inNewBrowserTab?: boolean;
}) { }) {
const { router, templateId, inNewBrowserTab = false } = opts; const { router, templateId, inNewBrowserTab = false, posthogStore } = opts;
const routeLocation: RouteLocationRaw = isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup) const routeLocation: RouteLocationRaw = posthogStore.isFeatureEnabled(
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
)
? { ? {
name: VIEWS.TEMPLATE_SETUP, name: VIEWS.TEMPLATE_SETUP,
params: { id: templateId }, params: { id: templateId },

View file

@ -66,10 +66,9 @@ import type {
} from '@/Interface'; } from '@/Interface';
import { setPageTitle } from '@/utils/htmlUtils'; import { setPageTitle } from '@/utils/htmlUtils';
import { VIEWS } from '@/constants'; import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import { usePostHog } from '@/stores/posthog.store'; import { usePostHog } from '@/stores/posthog.store';
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
@ -129,7 +128,7 @@ export default defineComponent({
this.navigateTo(event, VIEWS.TEMPLATE, id); this.navigateTo(event, VIEWS.TEMPLATE, id);
}, },
async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) { async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) { if (this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
const telemetryPayload = { const telemetryPayload = {
template_id: id, template_id: id,
wf_template_repo_session_id: this.templatesStore.currentSessionId, wf_template_repo_session_id: this.templatesStore.currentSessionId,
@ -142,6 +141,7 @@ export default defineComponent({
} }
await openTemplateCredentialSetup({ await openTemplateCredentialSetup({
posthogStore: this.posthogStore,
router: this.$router, router: this.$router,
templateId: id, templateId: id,
inNewBrowserTab: event.metaKey || event.ctrlKey, inNewBrowserTab: event.metaKey || event.ctrlKey,

View file

@ -69,8 +69,8 @@ import { setPageTitle } from '@/utils/htmlUtils';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import { usePostHog } from '@/stores/posthog.store'; import { usePostHog } from '@/stores/posthog.store';
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants';
export default defineComponent({ export default defineComponent({
name: 'TemplatesWorkflowView', name: 'TemplatesWorkflowView',
@ -107,7 +107,7 @@ export default defineComponent({
}, },
methods: { methods: {
async openTemplateSetup(id: string, e: PointerEvent) { async openTemplateSetup(id: string, e: PointerEvent) {
if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) { if (!this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
const telemetryPayload = { const telemetryPayload = {
source: 'workflow', source: 'workflow',
template_id: id, template_id: id,
@ -121,6 +121,7 @@ export default defineComponent({
} }
await openTemplateCredentialSetup({ await openTemplateCredentialSetup({
posthogStore: this.posthogStore,
router: this.$router, router: this.$router,
templateId: id, templateId: id,
inNewBrowserTab: e.metaKey || e.ctrlKey, inNewBrowserTab: e.metaKey || e.ctrlKey,