diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index 670b94d942..a50c66714e 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -4,12 +4,11 @@ import { visitTemplateCollectionPage, testData, } 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 { WorkflowPage } from '../pages/workflow'; const templateWorkflowPage = new TemplateWorkflowPage(); -const templateCredentialsSetupPage = new TemplateCredentialSetupPage(); const credentialsModal = new CredentialsModal(); const messageBox = new MessageBox(); const workflowPage = new WorkflowPage(); @@ -25,7 +24,8 @@ describe('Template credentials setup', () => { it('can be opened from template workflow page', () => { templateWorkflowPage.actions.visit(testTemplate.id); - templateCredentialsSetupPage.actions.enableFeatureFlag(); + templateWorkflowPage.getters.useTemplateButton().should('be.visible'); + templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); templateWorkflowPage.actions.clickUseThisWorkflowButton(); templateCredentialsSetupPage.getters @@ -35,7 +35,7 @@ describe('Template credentials setup', () => { it('can be opened from template collection page', () => { visitTemplateCollectionPage(testData.ecommerceStarterPack); - templateCredentialsSetupPage.actions.enableFeatureFlag(); + templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram'); templateCredentialsSetupPage.getters @@ -44,7 +44,7 @@ describe('Template credentials setup', () => { }); it('can be opened with a direct url', () => { - templateCredentialsSetupPage.actions.visit(testTemplate.id); + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.getters .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', () => { - templateCredentialsSetupPage.actions.visit(testTemplate.id); + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.getters .title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`) @@ -83,14 +83,14 @@ describe('Template credentials setup', () => { }); it('can skip template creation', () => { - templateCredentialsSetupPage.actions.visit(testTemplate.id); + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.getters.skipLink().click(); workflowPage.getters.canvasNodes().should('have.length', 3); }); 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 templateCredentialsSetupPage.getters.continueButton().should('be.disabled'); diff --git a/cypress/pages/template-credential-setup.ts b/cypress/pages/template-credential-setup.ts index 2ccaf5f807..b015285da5 100644 --- a/cypress/pages/template-credential-setup.ts +++ b/cypress/pages/template-credential-setup.ts @@ -1,40 +1,35 @@ -import { BasePage } from './base'; - export type TemplateTestData = { id: number; fixture: string; }; -export class TemplateCredentialSetupPage extends BasePage { - testData = { - simpleTemplate: { - id: 1205, - fixture: 'Test_Template_1.json', - }, - }; +export const testData = { + simpleTemplate: { + id: 1205, + fixture: 'Test_Template_1.json', + }, +}; - getters = { - continueButton: () => cy.getByTestId('continue-button'), - skipLink: () => cy.get('a:contains("Skip")'), - title: (title: string) => cy.get(`h1:contains(${title})`), - infoCallout: () => cy.getByTestId('info-callout'), - createAppCredentialsButton: (appName: string) => - cy.get(`button:contains("Create new ${appName} credential")`), - appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'), - stepHeading: ($el: JQuery) => - cy.wrap($el).findChildByTestId('credential-step-heading'), - stepDescription: ($el: JQuery) => - cy.wrap($el).findChildByTestId('credential-step-description'), - }; +export const getters = { + continueButton: () => cy.getByTestId('continue-button'), + skipLink: () => cy.get('a:contains("Skip")'), + title: (title: string) => cy.get(`h1:contains(${title})`), + infoCallout: () => cy.getByTestId('info-callout'), + createAppCredentialsButton: (appName: string) => + cy.get(`button:contains("Create new ${appName} credential")`), + appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'), + stepHeading: ($el: JQuery) => + cy.wrap($el).findChildByTestId('credential-step-heading'), + stepDescription: ($el: JQuery) => + cy.wrap($el).findChildByTestId('credential-step-description'), +}; - actions = { - visit: (templateId: number) => { - cy.visit(`/templates/${templateId}/setup`); - }, - enableFeatureFlag: () => { - cy.window().then((window) => { - window.localStorage.setItem('template-credentials-setup', 'true'); - }); - }, - }; -} +export const visitTemplateCredentialSetupPage = (templateId: number) => { + cy.visit(`/templates/${templateId}/setup`); +}; + +export const enableTemplateCredentialSetupFeatureFlag = () => { + cy.window().then((win) => { + win.featureFlags.override('016_template_credential_setup', true); + }); +}; diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 13bd1ff0ef..09246697ce 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -41,6 +41,13 @@ declare global { draganddrop(draggableSelector: string, droppableSelector: string): void; push(type: string, data: unknown): void; shouldNotHaveConsoleErrors(): void; + window(): Chainable< + AUTWindow & { + featureFlags: { + override: (feature: string, value: any) => void; + }; + } + >; } } } diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 2c43be87f7..d78c9eb5f1 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -617,7 +617,9 @@ export const ASK_AI_EXPERIMENT = { 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; diff --git a/packages/editor-ui/src/stores/posthog.store.ts b/packages/editor-ui/src/stores/posthog.store.ts index 576fe42c5c..a33c9ccf06 100644 --- a/packages/editor-ui/src/stores/posthog.store.ts +++ b/packages/editor-ui/src/stores/posthog.store.ts @@ -14,6 +14,8 @@ const EVENTS = { IS_PART_OF_EXPERIMENT: 'User is part of experiment', }; +export type PosthogStore = ReturnType; + export const usePostHog = defineStore('posthog', () => { const usersStore = useUsersStore(); const settingsStore = useSettingsStore(); @@ -39,6 +41,13 @@ export const usePostHog = defineStore('posthog', () => { 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) { // for testing const cachedOverrides = useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value; @@ -65,7 +74,7 @@ export const usePostHog = defineStore('posthog', () => { }, 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) => { if (!window.posthog) { 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) => { if (typeof window.posthog?.capture === 'function') { window.posthog.capture(event, properties); @@ -181,6 +190,7 @@ export const usePostHog = defineStore('posthog', () => { return { init, + isFeatureEnabled, isVariantEnabled, getVariant, reset, diff --git a/packages/editor-ui/src/utils/featureFlag.ts b/packages/editor-ui/src/utils/featureFlag.ts deleted file mode 100644 index 989496bfd4..0000000000 --- a/packages/editor-ui/src/utils/featureFlag.ts +++ /dev/null @@ -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); diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts index 220d037666..4290f19642 100644 --- a/packages/editor-ui/src/utils/templates/templateActions.ts +++ b/packages/editor-ui/src/utils/templates/templateActions.ts @@ -1,9 +1,9 @@ import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface'; 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 { PosthogStore } from '@/stores/posthog.store'; import type { useWorkflowsStore } from '@/stores/workflows.store'; -import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag'; import { getFixedNodesList } from '@/utils/nodeViewUtils'; import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; @@ -45,13 +45,16 @@ export async function createWorkflowFromTemplate( * if the feature flag is disabled) */ export async function openTemplateCredentialSetup(opts: { + posthogStore: PosthogStore; templateId: string; router: Router; 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, params: { id: templateId }, diff --git a/packages/editor-ui/src/views/TemplatesCollectionView.vue b/packages/editor-ui/src/views/TemplatesCollectionView.vue index 9ac801caab..1d69834dcf 100644 --- a/packages/editor-ui/src/views/TemplatesCollectionView.vue +++ b/packages/editor-ui/src/views/TemplatesCollectionView.vue @@ -66,10 +66,9 @@ import type { } from '@/Interface'; import { setPageTitle } from '@/utils/htmlUtils'; -import { VIEWS } from '@/constants'; +import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants'; import { useTemplatesStore } from '@/stores/templates.store'; import { usePostHog } from '@/stores/posthog.store'; -import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag'; import { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; import { useExternalHooks } from '@/composables/useExternalHooks'; @@ -129,7 +128,7 @@ export default defineComponent({ this.navigateTo(event, VIEWS.TEMPLATE, id); }, async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) { - if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) { + if (this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) { const telemetryPayload = { template_id: id, wf_template_repo_session_id: this.templatesStore.currentSessionId, @@ -142,6 +141,7 @@ export default defineComponent({ } await openTemplateCredentialSetup({ + posthogStore: this.posthogStore, router: this.$router, templateId: id, inNewBrowserTab: event.metaKey || event.ctrlKey, diff --git a/packages/editor-ui/src/views/TemplatesWorkflowView.vue b/packages/editor-ui/src/views/TemplatesWorkflowView.vue index 6156baa74c..6ba14934dc 100644 --- a/packages/editor-ui/src/views/TemplatesWorkflowView.vue +++ b/packages/editor-ui/src/views/TemplatesWorkflowView.vue @@ -69,8 +69,8 @@ import { setPageTitle } from '@/utils/htmlUtils'; import { useTemplatesStore } from '@/stores/templates.store'; import { usePostHog } from '@/stores/posthog.store'; import { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; -import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag'; import { useExternalHooks } from '@/composables/useExternalHooks'; +import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants'; export default defineComponent({ name: 'TemplatesWorkflowView', @@ -107,7 +107,7 @@ export default defineComponent({ }, methods: { async openTemplateSetup(id: string, e: PointerEvent) { - if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) { + if (!this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) { const telemetryPayload = { source: 'workflow', template_id: id, @@ -121,6 +121,7 @@ export default defineComponent({ } await openTemplateCredentialSetup({ + posthogStore: this.posthogStore, router: this.$router, templateId: id, inNewBrowserTab: e.metaKey || e.ctrlKey,