mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat: Redirect users without feature flag from template cred setup (no-changelog) (#8302)
This commit is contained in:
parent
c2748802a2
commit
135553bd6b
12
cypress/composables/featureFlags.ts
Normal file
12
cypress/composables/featureFlags.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export const overrideFeatureFlag = (name: string, value: boolean | string) => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
// If feature flags hasn't been initialized yet, we store the override
|
||||||
|
// in local storage and it gets loaded when the feature flags are
|
||||||
|
// initialized.
|
||||||
|
win.localStorage.setItem('N8N_EXPERIMENT_OVERRIDES', JSON.stringify({ [name]: value }));
|
||||||
|
|
||||||
|
if (win.featureFlags) {
|
||||||
|
win.featureFlags.override(name, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -38,6 +38,7 @@ 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.enableTemplateCredentialSetupFeatureFlag();
|
||||||
templateWorkflowPage.getters.useTemplateButton().should('be.visible');
|
templateWorkflowPage.getters.useTemplateButton().should('be.visible');
|
||||||
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
|
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
|
||||||
templateWorkflowPage.actions.clickUseThisWorkflowButton();
|
templateWorkflowPage.actions.clickUseThisWorkflowButton();
|
||||||
|
@ -57,14 +58,6 @@ describe('Template credentials setup', () => {
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be opened with a direct url', () => {
|
|
||||||
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters
|
|
||||||
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has all the elements on page', () => {
|
it('has all the elements on page', () => {
|
||||||
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { CredentialsModal, MessageBox } from './modals';
|
import { CredentialsModal, MessageBox } from './modals';
|
||||||
import * as formStep from '../composables/setup-template-form-step';
|
import * as formStep from '../composables/setup-template-form-step';
|
||||||
|
import { overrideFeatureFlag } from '../composables/featureFlags';
|
||||||
|
|
||||||
export type TemplateTestData = {
|
export type TemplateTestData = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -28,15 +29,14 @@ export const getters = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enableTemplateCredentialSetupFeatureFlag = () => {
|
export const enableTemplateCredentialSetupFeatureFlag = () => {
|
||||||
cy.window().then((win) => {
|
overrideFeatureFlag('017_template_credential_setup_v2', true);
|
||||||
win.featureFlags.override('017_template_credential_setup_v2', true);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
export const visitTemplateCredentialSetupPage = (templateId: number) => {
|
||||||
cy.visit(`/templates/${templateId}/setup`);
|
cy.visit(`templates/${templateId}/setup`);
|
||||||
formStep.getFormStep().eq(0).should('be.visible');
|
|
||||||
enableTemplateCredentialSetupFeatureFlag();
|
enableTemplateCredentialSetupFeatureFlag();
|
||||||
|
|
||||||
|
formStep.getFormStep().eq(0).should('be.visible');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,7 +5,7 @@ export class TemplateWorkflowPage extends BasePage {
|
||||||
|
|
||||||
getters = {
|
getters = {
|
||||||
useTemplateButton: () => cy.get('[data-test-id="use-template-button"]'),
|
useTemplateButton: () => cy.get('[data-test-id="use-template-button"]'),
|
||||||
description: () => cy.get('[data-test-id="template-description"]')
|
description: () => cy.get('[data-test-id="template-description"]'),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
@ -17,7 +17,15 @@ export class TemplateWorkflowPage extends BasePage {
|
||||||
this.getters.useTemplateButton().click();
|
this.getters.useTemplateButton().click();
|
||||||
},
|
},
|
||||||
|
|
||||||
openTemplate: (template: {workflow: {id: number, name: string, description: string, user: {username: string}, image: {id: number, url: string}[] }}) => {
|
openTemplate: (template: {
|
||||||
|
workflow: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
user: { username: string };
|
||||||
|
image: { id: number; url: string }[];
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${template.workflow.id}`, {
|
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${template.workflow.id}`, {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: template,
|
body: template,
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVariant = (experiment: keyof FeatureFlags): FeatureFlags[keyof FeatureFlags] => {
|
const getVariant = (experiment: keyof FeatureFlags): FeatureFlags[keyof FeatureFlags] => {
|
||||||
return featureFlags.value?.[experiment];
|
return overrides.value[experiment] ?? featureFlags.value?.[experiment];
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVariantEnabled = (experiment: string, variant: string) => {
|
const isVariantEnabled = (experiment: string, variant: string) => {
|
||||||
|
@ -46,7 +46,7 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
* Checks if the given feature flag is enabled. Should only be used for boolean flags
|
* Checks if the given feature flag is enabled. Should only be used for boolean flags
|
||||||
*/
|
*/
|
||||||
const isFeatureEnabled = (experiment: keyof FeatureFlags) => {
|
const isFeatureEnabled = (experiment: keyof FeatureFlags) => {
|
||||||
return featureFlags.value?.[experiment] === true;
|
return getVariant(experiment) === true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!window.featureFlags) {
|
if (!window.featureFlags) {
|
||||||
|
@ -55,7 +55,10 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
if (cachedOverrides) {
|
if (cachedOverrides) {
|
||||||
try {
|
try {
|
||||||
console.log('Overriding feature flags', cachedOverrides);
|
console.log('Overriding feature flags', cachedOverrides);
|
||||||
|
const parsedOverrides = JSON.parse(cachedOverrides);
|
||||||
|
if (typeof parsedOverrides === 'object') {
|
||||||
overrides.value = JSON.parse(cachedOverrides);
|
overrides.value = JSON.parse(cachedOverrides);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Could not override experiment', e);
|
console.log('Could not override experiment', e);
|
||||||
}
|
}
|
||||||
|
@ -65,10 +68,6 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
// since features are evaluated serverside, regular posthog mechanism to override clientside does not work
|
// since features are evaluated serverside, regular posthog mechanism to override clientside does not work
|
||||||
override: (name: string, value: string | boolean) => {
|
override: (name: string, value: string | boolean) => {
|
||||||
overrides.value[name] = value;
|
overrides.value[name] = value;
|
||||||
featureFlags.value = {
|
|
||||||
...featureFlags.value,
|
|
||||||
[name]: value,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value = JSON.stringify(overrides.value);
|
useStorage(LOCAL_STORAGE_EXPERIMENT_OVERRIDES).value = JSON.stringify(overrides.value);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
@ -93,13 +92,6 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
window.posthog?.identify?.(id, traits);
|
window.posthog?.identify?.(id, traits);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addExperimentOverrides = () => {
|
|
||||||
featureFlags.value = {
|
|
||||||
...featureFlags.value,
|
|
||||||
...overrides.value,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackExperiment = (featFlags: FeatureFlags, name: string) => {
|
const trackExperiment = (featFlags: FeatureFlags, name: string) => {
|
||||||
const variant = featFlags[name];
|
const variant = featFlags[name];
|
||||||
if (!variant || trackedDemoExp.value[name] === variant) {
|
if (!variant || trackedDemoExp.value[name] === variant) {
|
||||||
|
@ -160,13 +152,11 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// does not need to be debounced really, but tracking does not fire without delay on page load
|
// does not need to be debounced really, but tracking does not fire without delay on page load
|
||||||
addExperimentOverrides();
|
|
||||||
trackExperimentsDebounced(featureFlags.value);
|
trackExperimentsDebounced(featureFlags.value);
|
||||||
} else {
|
} else {
|
||||||
// depend on client side evaluation if serverside evaluation fails
|
// depend on client side evaluation if serverside evaluation fails
|
||||||
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
|
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
|
||||||
featureFlags.value = map;
|
featureFlags.value = map;
|
||||||
addExperimentOverrides();
|
|
||||||
|
|
||||||
// must be debounced because it is called multiple times by posthog
|
// must be debounced because it is called multiple times by posthog
|
||||||
trackExperimentsDebounced(featureFlags.value);
|
trackExperimentsDebounced(featureFlags.value);
|
||||||
|
|
|
@ -67,6 +67,7 @@ describe('templateActions', () => {
|
||||||
templateId,
|
templateId,
|
||||||
templatesStore,
|
templatesStore,
|
||||||
router,
|
router,
|
||||||
|
source: 'workflow',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -76,18 +77,6 @@ describe('templateActions', () => {
|
||||||
params: { id: templateId },
|
params: { id: templateId },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should track 'User inserted workflow template'", async () => {
|
|
||||||
expect(telemetry.track).toHaveBeenCalledWith(
|
|
||||||
'User inserted workflow template',
|
|
||||||
{
|
|
||||||
source: 'workflow',
|
|
||||||
template_id: templateId,
|
|
||||||
wf_template_repo_session_id: '',
|
|
||||||
},
|
|
||||||
{ withPostHog: true },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When feature flag is enabled and template has nodes requiring credentials', () => {
|
describe('When feature flag is enabled and template has nodes requiring credentials', () => {
|
||||||
|
@ -111,6 +100,7 @@ describe('templateActions', () => {
|
||||||
templateId,
|
templateId,
|
||||||
templatesStore,
|
templatesStore,
|
||||||
router,
|
router,
|
||||||
|
source: 'workflow',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -144,6 +134,7 @@ describe('templateActions', () => {
|
||||||
templateId,
|
templateId,
|
||||||
templatesStore,
|
templatesStore,
|
||||||
router,
|
router,
|
||||||
|
source: 'workflow',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -98,9 +98,8 @@ async function openTemplateWorkflowOnNodeView(opts: {
|
||||||
templatesStore: TemplatesStore;
|
templatesStore: TemplatesStore;
|
||||||
router: Router;
|
router: Router;
|
||||||
inNewBrowserTab?: boolean;
|
inNewBrowserTab?: boolean;
|
||||||
telemetry: Telemetry;
|
|
||||||
}) {
|
}) {
|
||||||
const { externalHooks, templateId, templatesStore, telemetry, inNewBrowserTab, router } = opts;
|
const { externalHooks, templateId, templatesStore, inNewBrowserTab, router } = opts;
|
||||||
const routeLocation: RouteLocationRaw = {
|
const routeLocation: RouteLocationRaw = {
|
||||||
name: VIEWS.TEMPLATE_IMPORT,
|
name: VIEWS.TEMPLATE_IMPORT,
|
||||||
params: { id: templateId },
|
params: { id: templateId },
|
||||||
|
@ -111,9 +110,6 @@ async function openTemplateWorkflowOnNodeView(opts: {
|
||||||
wf_template_repo_session_id: templatesStore.currentSessionId,
|
wf_template_repo_session_id: templatesStore.currentSessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
telemetry.track('User inserted workflow template', telemetryPayload, {
|
|
||||||
withPostHog: true,
|
|
||||||
});
|
|
||||||
await externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload);
|
await externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload);
|
||||||
|
|
||||||
if (inNewBrowserTab) {
|
if (inNewBrowserTab) {
|
||||||
|
|
|
@ -1308,6 +1308,18 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$telemetry.track(
|
||||||
|
'User inserted workflow template',
|
||||||
|
{
|
||||||
|
source: 'workflow',
|
||||||
|
template_id: templateId,
|
||||||
|
wf_template_repo_session_id: this.templatesStore.previousSessionId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withPostHog: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.blankRedirect = true;
|
this.blankRedirect = true;
|
||||||
await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
await this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, watch } from 'vue';
|
import { computed, onBeforeMount, onMounted, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useSetupTemplateStore } from './setupTemplate.store';
|
import { useSetupTemplateStore } from './setupTemplate.store';
|
||||||
import N8nHeading from 'n8n-design-system/components/N8nHeading';
|
import N8nHeading from 'n8n-design-system/components/N8nHeading';
|
||||||
|
@ -7,14 +7,14 @@ import N8nLink from 'n8n-design-system/components/N8nLink';
|
||||||
import AppsRequiringCredsNotice from './AppsRequiringCredsNotice.vue';
|
import AppsRequiringCredsNotice from './AppsRequiringCredsNotice.vue';
|
||||||
import SetupTemplateFormStep from './SetupTemplateFormStep.vue';
|
import SetupTemplateFormStep from './SetupTemplateFormStep.vue';
|
||||||
import TemplatesView from '../TemplatesView.vue';
|
import TemplatesView from '../TemplatesView.vue';
|
||||||
import { VIEWS } from '@/constants';
|
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
|
||||||
// Store
|
// Store
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const telemetry = useTelemetry();
|
const posthogStore = usePostHog();
|
||||||
|
|
||||||
// Router
|
// Router
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -79,6 +79,15 @@ const skipIfTemplateHasNoCreds = async () => {
|
||||||
|
|
||||||
setupTemplateStore.setTemplateId(templateId.value);
|
setupTemplateStore.setTemplateId(templateId.value);
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
if (!posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
|
||||||
|
void router.replace({
|
||||||
|
name: VIEWS.TEMPLATE_IMPORT,
|
||||||
|
params: { id: templateId.value },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await setupTemplateStore.init();
|
await setupTemplateStore.init();
|
||||||
await skipIfTemplateHasNoCreds();
|
await skipIfTemplateHasNoCreds();
|
||||||
|
|
|
@ -153,16 +153,6 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
||||||
wf_template_repo_session_id: templatesStore.currentSessionId,
|
wf_template_repo_session_id: templatesStore.currentSessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
telemetry.track(
|
|
||||||
'User inserted workflow template',
|
|
||||||
{
|
|
||||||
source: 'workflow',
|
|
||||||
template_id: templateId.value,
|
|
||||||
wf_template_repo_session_id: templatesStore.currentSessionId,
|
|
||||||
},
|
|
||||||
{ withPostHog: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
telemetry.track('User closed cred setup', {
|
telemetry.track('User closed cred setup', {
|
||||||
completed: false,
|
completed: false,
|
||||||
creds_filled: 0,
|
creds_filled: 0,
|
||||||
|
|
Loading…
Reference in a new issue