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,
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');

View file

@ -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<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-heading'),
stepDescription: ($el: JQuery<HTMLElement>) =>
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<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-heading'),
stepDescription: ($el: JQuery<HTMLElement>) =>
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);
});
};

View file

@ -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;
};
}
>;
}
}
}

View file

@ -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;

View file

@ -14,6 +14,8 @@ const EVENTS = {
IS_PART_OF_EXPERIMENT: 'User is part of experiment',
};
export type PosthogStore = ReturnType<typeof usePostHog>;
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,

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 { 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 },

View file

@ -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,

View file

@ -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,