fix: Adjust 'use template' feature telemetry (no-changelog) (#8232)

- Add extra params to 'User closed cred setup' event
- Fire 'User closed cred setup' only when template has creds
- Skip cred setup page if template has no creds required
- Fire 'User inserted workflow template' also in cred setup
This commit is contained in:
Tomi Turtiainen 2024-01-05 11:52:10 +02:00 committed by GitHub
parent cda49a4747
commit 6e78d2346e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 2366 additions and 92 deletions

View file

@ -28,6 +28,8 @@ function getSearchKey(query: ITemplatesQuery): string {
return JSON.stringify([query.search || '', [...query.categories].sort()]); return JSON.stringify([query.search || '', [...query.categories].sort()]);
} }
export type TemplatesStore = ReturnType<typeof useTemplatesStore>;
export const useTemplatesStore = defineStore(STORES.TEMPLATES, { export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
state: (): ITemplateState => ({ state: (): ITemplateState => ({
categories: {}, categories: {},

View file

@ -0,0 +1,158 @@
import { VIEWS } from '@/constants';
import { Telemetry } from '@/plugins/telemetry';
import type { NodeTypesStore } from '@/stores/nodeTypes.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { PosthogStore } from '@/stores/posthog.store';
import { usePostHog } from '@/stores/posthog.store';
import type { TemplatesStore } from '@/stores/templates.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useTemplateWorkflow } from '@/utils/templates/templateActions';
import {
nodeTypeRespondToWebhookV1,
nodeTypeShopifyTriggerV1,
nodeTypeTelegramV1,
nodeTypeTwitterV1,
nodeTypeWebhookV1,
nodeTypeWebhookV1_1,
nodeTypesSet,
} from '@/utils/testData/nodeTypeTestData';
import {
fullCreateApiEndpointTemplate,
fullShopifyTelegramTwitterTemplate,
} from '@/utils/testData/templateTestData';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { vi } from 'vitest';
import type { Router } from 'vue-router';
describe('templateActions', () => {
describe('useTemplateWorkflow', () => {
const telemetry = new Telemetry();
const externalHooks = {
run: vi.fn(),
};
const router: Router = {
push: vi.fn(),
resolve: vi.fn(),
} as unknown as Router;
let nodeTypesStore: NodeTypesStore;
let posthogStore: PosthogStore;
let templatesStore: TemplatesStore;
beforeEach(() => {
vi.resetAllMocks();
setActivePinia(
createTestingPinia({
stubActions: false,
}),
);
vi.spyOn(telemetry, 'track').mockImplementation(() => {});
nodeTypesStore = useNodeTypesStore();
posthogStore = usePostHog();
templatesStore = useTemplatesStore();
});
describe('When feature flag is disabled', () => {
const templateId = '1';
beforeEach(async () => {
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(false);
await useTemplateWorkflow({
externalHooks,
posthogStore,
nodeTypesStore,
telemetry,
templateId,
templatesStore,
router,
});
});
it('should navigate to correct url', async () => {
expect(router.push).toHaveBeenCalledWith({
name: VIEWS.TEMPLATE_IMPORT,
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', () => {
const templateId = fullShopifyTelegramTwitterTemplate.id.toString();
beforeEach(async () => {
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true);
templatesStore.addWorkflows([fullShopifyTelegramTwitterTemplate]);
nodeTypesStore.setNodeTypes([
nodeTypeTelegramV1,
nodeTypeTwitterV1,
nodeTypeShopifyTriggerV1,
]);
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
await useTemplateWorkflow({
externalHooks,
posthogStore,
nodeTypesStore,
telemetry,
templateId,
templatesStore,
router,
});
});
it('should navigate to correct url', async () => {
expect(router.push).toHaveBeenCalledWith({
name: VIEWS.TEMPLATE_SETUP,
params: { id: templateId },
});
});
});
describe("When feature flag is enabled and template doesn't have nodes requiring credentials", () => {
const templateId = fullCreateApiEndpointTemplate.id.toString();
beforeEach(async () => {
posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true);
templatesStore.addWorkflows([fullCreateApiEndpointTemplate]);
nodeTypesStore.setNodeTypes([
nodeTypeWebhookV1,
nodeTypeWebhookV1_1,
nodeTypeRespondToWebhookV1,
...Object.values(nodeTypesSet),
]);
vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue();
await useTemplateWorkflow({
externalHooks,
posthogStore,
nodeTypesStore,
telemetry,
templateId,
templatesStore,
router,
});
});
it('should navigate to correct url', async () => {
expect(router.push).toHaveBeenCalledWith({
name: VIEWS.TEMPLATE_IMPORT,
params: { id: templateId },
});
});
});
});
});

View file

@ -1,4 +1,9 @@
import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface'; import type {
INodeUi,
ITemplatesWorkflowFull,
IWorkflowData,
IWorkflowTemplate,
} from '@/Interface';
import { getNewWorkflow } from '@/api/workflows'; import { getNewWorkflow } from '@/api/workflows';
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, 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';
@ -7,9 +12,19 @@ import type { useWorkflowsStore } from '@/stores/workflows.store';
import { getFixedNodesList } from '@/utils/nodeViewUtils'; import { getFixedNodesList } from '@/utils/nodeViewUtils';
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; import {
getNodesRequiringCredentials,
replaceAllTemplateNodeCredentials,
} from '@/utils/templates/templateTransforms';
import type { INodeCredentialsDetails } from 'n8n-workflow'; import type { INodeCredentialsDetails } from 'n8n-workflow';
import type { RouteLocationRaw, Router } from 'vue-router'; import type { RouteLocationRaw, Router } from 'vue-router';
import type { TemplatesStore } from '@/stores/templates.store';
import type { NodeTypesStore } from '@/stores/nodeTypes.store';
import type { Telemetry } from '@/plugins/telemetry';
import type { useExternalHooks } from '@/composables/useExternalHooks';
import { assert } from '@/utils/assert';
type ExternalHooks = ReturnType<typeof useExternalHooks>;
/** /**
* Creates a new workflow from a template * Creates a new workflow from a template
@ -49,27 +64,18 @@ export async function createWorkflowFromTemplate(opts: {
} }
/** /**
* Opens the template credential setup view (or workflow view * Opens the template credential setup view
* if the feature flag is disabled)
*/ */
export async function openTemplateCredentialSetup(opts: { async function openTemplateCredentialSetup(opts: {
posthogStore: PosthogStore;
templateId: string; templateId: string;
router: Router; router: Router;
inNewBrowserTab?: boolean; inNewBrowserTab?: boolean;
}) { }) {
const { router, templateId, inNewBrowserTab = false, posthogStore } = opts; const { router, templateId, inNewBrowserTab = false } = opts;
const routeLocation: RouteLocationRaw = posthogStore.isFeatureEnabled( const routeLocation: RouteLocationRaw = {
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
)
? {
name: VIEWS.TEMPLATE_SETUP, name: VIEWS.TEMPLATE_SETUP,
params: { id: templateId }, params: { id: templateId },
}
: {
name: VIEWS.TEMPLATE_IMPORT,
params: { id: templateId },
}; };
if (inNewBrowserTab) { if (inNewBrowserTab) {
@ -79,3 +85,93 @@ export async function openTemplateCredentialSetup(opts: {
await router.push(routeLocation); await router.push(routeLocation);
} }
} }
/**
* Opens the given template's workflow on NodeView. Fires necessary
* telemetry events.
*/
async function openTemplateWorkflowOnNodeView(opts: {
externalHooks: ExternalHooks;
templateId: string;
templatesStore: TemplatesStore;
router: Router;
inNewBrowserTab?: boolean;
telemetry: Telemetry;
}) {
const { externalHooks, templateId, templatesStore, telemetry, inNewBrowserTab, router } = opts;
const routeLocation: RouteLocationRaw = {
name: VIEWS.TEMPLATE_IMPORT,
params: { id: templateId },
};
const telemetryPayload = {
source: 'workflow',
template_id: templateId,
wf_template_repo_session_id: templatesStore.currentSessionId,
};
telemetry.track('User inserted workflow template', telemetryPayload, {
withPostHog: true,
});
await externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload);
if (inNewBrowserTab) {
const route = router.resolve(routeLocation);
window.open(route.href, '_blank');
} else {
await router.push(routeLocation);
}
}
function hasTemplateCredentials(
nodeTypeProvider: NodeTypeProvider,
template: ITemplatesWorkflowFull,
) {
const nodesRequiringCreds = getNodesRequiringCredentials(nodeTypeProvider, template);
return nodesRequiringCreds.length > 0;
}
async function getFullTemplate(templatesStore: TemplatesStore, templateId: string) {
const template = templatesStore.getFullTemplateById(templateId);
if (template) {
return template;
}
await templatesStore.fetchTemplateById(templateId);
return templatesStore.getFullTemplateById(templateId);
}
/**
* Uses the given template by opening the template workflow on NodeView
* or the template credential setup view. Fires necessary telemetry events.
*/
export async function useTemplateWorkflow(opts: {
externalHooks: ExternalHooks;
nodeTypesStore: NodeTypesStore;
posthogStore: PosthogStore;
templateId: string;
templatesStore: TemplatesStore;
router: Router;
inNewBrowserTab?: boolean;
telemetry: Telemetry;
}) {
const { nodeTypesStore, posthogStore, templateId, templatesStore } = opts;
const openCredentialSetup = posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT);
if (!openCredentialSetup) {
await openTemplateWorkflowOnNodeView(opts);
return;
}
const [template] = await Promise.all([
getFullTemplate(templatesStore, templateId),
nodeTypesStore.loadNodeTypesIfNotLoaded(),
]);
assert(template);
if (hasTemplateCredentials(nodeTypesStore, template)) {
await openTemplateCredentialSetup(opts);
} else {
await openTemplateWorkflowOnNodeView(opts);
}
}

View file

@ -1,8 +1,16 @@
import type { IWorkflowTemplateNode, IWorkflowTemplateNodeCredentials } from '@/Interface'; import type {
ITemplatesWorkflowFull,
IWorkflowTemplateNode,
IWorkflowTemplateNodeCredentials,
} from '@/Interface';
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms'; import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms';
import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes'; import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes';
import type { INodeCredentials, INodeCredentialsDetails } from 'n8n-workflow'; import type {
INodeCredentialDescription,
INodeCredentials,
INodeCredentialsDetails,
} from 'n8n-workflow';
export type IWorkflowTemplateNodeWithCredentials = IWorkflowTemplateNode & export type IWorkflowTemplateNodeWithCredentials = IWorkflowTemplateNode &
Required<Pick<IWorkflowTemplateNode, 'credentials'>>; Required<Pick<IWorkflowTemplateNode, 'credentials'>>;
@ -17,6 +25,11 @@ const credentialKeySymbol = Symbol('credentialKey');
*/ */
export type TemplateCredentialKey = string & { [credentialKeySymbol]: never }; export type TemplateCredentialKey = string & { [credentialKeySymbol]: never };
export type TemplateNodeWithRequiredCredential = {
node: IWorkflowTemplateNode;
requiredCredentials: INodeCredentialDescription[];
};
/** /**
* Forms a key from credential type name and credential name * Forms a key from credential type name and credential name
*/ */
@ -120,3 +133,25 @@ export const replaceAllTemplateNodeCredentials = (
}; };
}); });
}; };
/**
* Returns the nodes in the template that require credentials
* and the required credentials for each node.
*/
export const getNodesRequiringCredentials = (
nodeTypeProvider: NodeTypeProvider,
template: ITemplatesWorkflowFull,
): TemplateNodeWithRequiredCredential[] => {
if (!template) {
return [];
}
const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes
.map((node) => ({
node,
requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node),
}))
.filter(({ requiredCredentials }) => requiredCredentials.length > 0);
return nodesWithCredentials;
};

File diff suppressed because it is too large Load diff

View file

@ -290,3 +290,148 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = {
image: [], image: [],
full: true, full: true,
} satisfies ITemplatesWorkflowFull; } satisfies ITemplatesWorkflowFull;
/** Template that doesn't contain nodes requiring credentials */
export const fullCreateApiEndpointTemplate = {
id: 1750,
name: 'Creating an API endpoint',
views: 13265,
recentViews: 9899,
totalViews: 13265,
createdAt: '2022-07-06T14:45:19.659Z',
description:
'**Task:**\nCreate a simple API endpoint using the Webhook and Respond to Webhook nodes\n\n**Why:**\nYou can prototype or replace a backend process with a single workflow\n\n**Main use cases:**\nReplace backend logic with a workflow',
workflow: {
meta: { instanceId: '8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd' },
nodes: [
{
id: 'f80aceed-b676-42aa-bf25-f7a44408b1bc',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
position: [375, 115],
webhookId: '6f7b288e-1efe-4504-a6fd-660931327269',
parameters: {
path: '6f7b288e-1efe-4504-a6fd-660931327269',
options: {},
responseMode: 'responseNode',
},
typeVersion: 1,
},
{
id: '3b9ec913-0bbe-4906-bf8e-da352b556655',
name: 'Note1',
type: 'n8n-nodes-base.stickyNote',
position: [355, -25],
parameters: {
width: 600,
height: 280,
content:
'## Create a simple API endpoint\n\nIn this workflow we show how to create a simple API endpoint with `Webhook` and `Respond to Webhook` nodes\n\n',
},
typeVersion: 1,
},
{
id: '9c36dae5-0700-450c-9739-e9f3eff31bfe',
name: 'Respond to Webhook',
type: 'n8n-nodes-base.respondToWebhook',
position: [815, 115],
parameters: {
options: {},
respondWith: 'text',
responseBody:
'=The URL of the Google search query for the term "{{$node["Webhook"].json["query"]["first_name"]}} {{$node["Webhook"].json["query"]["last_name"]}}" is: {{$json["product"]}}',
},
typeVersion: 1,
},
{
id: '5a228fcb-78b9-4a28-95d2-d7c9fdf1d4ea',
name: 'Create URL string',
type: 'n8n-nodes-base.set',
position: [595, 115],
parameters: {
values: {
string: [
{
name: 'product',
value:
'=https://www.google.com/search?q={{$json["query"]["first_name"]}}+{{$json["query"]["last_name"]}}',
},
],
},
options: {},
keepOnlySet: true,
},
typeVersion: 1,
},
{
id: 'e7971820-45a8-4dc8-ba4c-b3220d65307a',
name: 'Note3',
type: 'n8n-nodes-base.stickyNote',
position: [355, 275],
parameters: {
width: 600,
height: 220,
content:
'### How to use it\n1. Execute the workflow so that the webhook starts listening\n2. Make a test request by pasting, **in a new browser tab**, the test URL from the `Webhook` node and appending the following test at the end `?first_name=bob&last_name=dylan`\n\nYou will receive the following output in the new tab `The URL of the Google search query for the term "bob dylan" is: https://www.google.com/search?q=bob+dylan`\n\n',
},
typeVersion: 1,
},
],
connections: {
Webhook: { main: [[{ node: 'Create URL string', type: 'main', index: 0 }]] },
'Create URL string': { main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]] },
},
},
lastUpdatedBy: 1,
workflowInfo: null,
user: { username: 'jon-n8n' },
nodes: [
{
id: 38,
icon: 'fa:pen',
name: 'n8n-nodes-base.set',
defaults: { name: 'Edit Fields', color: '#0000FF' },
iconData: { icon: 'pen', type: 'icon' },
categories: [{ id: 9, name: 'Core Nodes' }],
displayName: 'Edit Fields (Set)',
typeVersion: 3,
},
{
id: 47,
icon: 'file:webhook.svg',
name: 'n8n-nodes-base.webhook',
defaults: { name: 'Webhook' },
iconData: {
type: 'file',
fileBuffer:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0zNSwzN2MtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMzcuMiwzNywzNSwzN3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUsNDNjLTMsMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEsMS40LDIuOSwyLjMsNC43LDIuM2MzLjMsMCw2LTIuNyw2LTZzLTIuNy02LTYtNiBjLTEsMC0yLDAuMy0yLjksMC43bC0xLjcsMUwyMy4zLDE2bDMuNS0xLjlsNS4zLDkuNGMxLTAuMywyLTAuNSwzLTAuNWM1LjUsMCwxMCw0LjUsMTAsMTBTNDAuNSw0MywzNSw0M3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMTQsNDNDOC41LDQzLDQsMzguNSw0LDMzYzAtNC42LDMuMS04LjUsNy41LTkuN2wxLDMuOUM5LjksMjcuOSw4LDMwLjMsOCwzM2MwLDMuMywyLjcsNiw2LDYgczYtMi43LDYtNnYtMmgxNXY0SDIzLjhDMjIuOSwzOS42LDE4LjgsNDMsMTQsNDN6Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0iTTE0LDM3Yy0yLjIsMC00LTEuOC00LTRzMS44LTQsNC00czQsMS44LDQsNFMxNi4yLDM3LDE0LDM3eiIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSwxOWMtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMjcuMiwxOSwyNSwxOXoiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTUuNywzNEwxMi4zLDMybDUuOS05LjdjLTItMS45LTMuMi00LjUtMy4yLTcuM2MwLTUuNSw0LjUtMTAsMTAtMTBjNS41LDAsMTAsNC41LDEwLDEwIGMwLDAuOS0wLjEsMS43LTAuMywyLjVsLTMuOS0xYzAuMS0wLjUsMC4yLTEsMC4yLTEuNWMwLTMuMy0yLjctNi02LTZzLTYsMi43LTYsNmMwLDIuMSwxLjEsNCwyLjksNS4xbDEuNywxTDE1LjcsMzR6Ii8+PC9zdmc+Cg==',
},
categories: [
{ id: 5, name: 'Development' },
{ id: 9, name: 'Core Nodes' },
],
displayName: 'Webhook',
typeVersion: 1,
},
{
id: 535,
icon: 'file:webhook.svg',
name: 'n8n-nodes-base.respondToWebhook',
defaults: { name: 'Respond to Webhook' },
iconData: {
type: 'file',
fileBuffer:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0zNSwzN2MtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMzcuMiwzNywzNSwzN3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUsNDNjLTMsMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEsMS40LDIuOSwyLjMsNC43LDIuM2MzLjMsMCw2LTIuNyw2LTZzLTIuNy02LTYtNiBjLTEsMC0yLDAuMy0yLjksMC43bC0xLjcsMUwyMy4zLDE2bDMuNS0xLjlsNS4zLDkuNGMxLTAuMywyLTAuNSwzLTAuNWM1LjUsMCwxMCw0LjUsMTAsMTBTNDAuNSw0MywzNSw0M3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMTQsNDNDOC41LDQzLDQsMzguNSw0LDMzYzAtNC42LDMuMS04LjUsNy41LTkuN2wxLDMuOUM5LjksMjcuOSw4LDMwLjMsOCwzM2MwLDMuMywyLjcsNiw2LDYgczYtMi43LDYtNnYtMmgxNXY0SDIzLjhDMjIuOSwzOS42LDE4LjgsNDMsMTQsNDN6Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0iTTE0LDM3Yy0yLjIsMC00LTEuOC00LTRzMS44LTQsNC00czQsMS44LDQsNFMxNi4yLDM3LDE0LDM3eiIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSwxOWMtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMjcuMiwxOSwyNSwxOXoiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTUuNywzNEwxMi4zLDMybDUuOS05LjdjLTItMS45LTMuMi00LjUtMy4yLTcuM2MwLTUuNSw0LjUtMTAsMTAtMTBjNS41LDAsMTAsNC41LDEwLDEwIGMwLDAuOS0wLjEsMS43LTAuMywyLjVsLTMuOS0xYzAuMS0wLjUsMC4yLTEsMC4yLTEuNWMwLTMuMy0yLjctNi02LTZzLTYsMi43LTYsNmMwLDIuMSwxLjEsNCwyLjksNS4xbDEuNywxTDE1LjcsMzR6Ii8+PC9zdmc+Cg==',
},
categories: [
{ id: 7, name: 'Utility' },
{ id: 9, name: 'Core Nodes' },
],
displayName: 'Respond to Webhook',
typeVersion: 1,
},
],
categories: [{ id: 20, name: 'Building Blocks' }],
image: [],
full: true,
} satisfies ITemplatesWorkflowFull;

View file

@ -8,28 +8,21 @@ import { useRootStore } from '@/stores/n8nRoot.store';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils'; import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils';
import type { import type { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow';
INodeCredentialDescription, import type { ICredentialsResponse, INodeUi, IWorkflowTemplateNode } from '@/Interface';
INodeCredentialsDetails,
INodeTypeDescription,
} from 'n8n-workflow';
import type {
ICredentialsResponse,
INodeUi,
ITemplatesWorkflowFull,
IWorkflowTemplateNode,
} from '@/Interface';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions'; import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import type {
TemplateCredentialKey,
TemplateNodeWithRequiredCredential,
} from '@/utils/templates/templateTransforms';
import { import {
getNodesRequiringCredentials,
keyFromCredentialTypeAndName, keyFromCredentialTypeAndName,
normalizeTemplateNodeCredentials, normalizeTemplateNodeCredentials,
} from '@/utils/templates/templateTransforms'; } from '@/utils/templates/templateTransforms';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms';
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
export type NodeAndType = { export type NodeAndType = {
node: INodeUi; node: INodeUi;
@ -64,35 +57,8 @@ export type AppCredentialCount = {
count: number; count: number;
}; };
export type TemplateNodeWithRequiredCredential = {
node: IWorkflowTemplateNode;
requiredCredentials: INodeCredentialDescription[];
};
//#region Getter functions //#region Getter functions
/**
* Returns the nodes in the template that require credentials
* and the required credentials for each node.
*/
export const getNodesRequiringCredentials = (
nodeTypeProvider: NodeTypeProvider,
template: ITemplatesWorkflowFull,
): TemplateNodeWithRequiredCredential[] => {
if (!template) {
return [];
}
const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes
.map((node) => ({
node,
requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node),
}))
.filter(({ requiredCredentials }) => requiredCredentials.length > 0);
return nodesWithCredentials;
};
export const groupNodeCredentialsByKey = ( export const groupNodeCredentialsByKey = (
nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[], nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[],
) => { ) => {
@ -343,6 +309,9 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
telemetry.track('User closed cred setup', { telemetry.track('User closed cred setup', {
completed: false, completed: false,
creds_filled: 0,
creds_needed: credentialUsages.value.length,
workflow_id: null,
}); });
// Replace the URL so back button doesn't come back to this setup view // Replace the URL so back button doesn't come back to this setup view
@ -376,6 +345,19 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
telemetry.track('User closed cred setup', { telemetry.track('User closed cred setup', {
completed: true, completed: true,
creds_filled: numFilledCredentials.value,
creds_needed: credentialUsages.value.length,
workflow_id: createdWorkflow.id,
});
const telemetryPayload = {
source: 'workflow',
template_id: template.value.id,
wf_template_repo_session_id: templatesStore.currentSessionId,
};
telemetry.track('User inserted workflow template', telemetryPayload, {
withPostHog: true,
}); });
// Replace the URL so back button doesn't come back to this setup view // Replace the URL so back button doesn't come back to this setup view

View file

@ -66,11 +66,12 @@ import type {
} from '@/Interface'; } from '@/Interface';
import { setPageTitle } from '@/utils/htmlUtils'; import { setPageTitle } from '@/utils/htmlUtils';
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants'; import { 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 { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; import { useTemplateWorkflow } from '@/utils/templates/templateActions';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
export default defineComponent({ export default defineComponent({
name: 'TemplatesCollectionView', name: 'TemplatesCollectionView',
@ -152,23 +153,15 @@ 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 (this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) { await useTemplateWorkflow({
const telemetryPayload = {
template_id: id,
wf_template_repo_session_id: this.templatesStore.currentSessionId,
source: 'collection',
};
await this.externalHooks.run('templatesCollectionView.onUseWorkflow', telemetryPayload);
this.$telemetry.track('User inserted workflow template', telemetryPayload, {
withPostHog: true,
});
}
await openTemplateCredentialSetup({
posthogStore: this.posthogStore, posthogStore: this.posthogStore,
router: this.$router, router: this.$router,
templateId: id, templateId: id,
inNewBrowserTab: event.metaKey || event.ctrlKey, inNewBrowserTab: event.metaKey || event.ctrlKey,
templatesStore: useTemplatesStore(),
externalHooks: this.externalHooks,
nodeTypesStore: useNodeTypesStore(),
telemetry: this.$telemetry,
}); });
}, },
navigateTo(e: MouseEvent, page: string, id: string) { navigateTo(e: MouseEvent, page: string, id: string) {

View file

@ -68,9 +68,9 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
import { setPageTitle } from '@/utils/htmlUtils'; 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 { useTemplateWorkflow } from '@/utils/templates/templateActions';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
export default defineComponent({ export default defineComponent({
name: 'TemplatesWorkflowView', name: 'TemplatesWorkflowView',
@ -132,24 +132,15 @@ export default defineComponent({
}, },
methods: { methods: {
async openTemplateSetup(id: string, e: PointerEvent) { async openTemplateSetup(id: string, e: PointerEvent) {
if (!this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) { await useTemplateWorkflow({
const telemetryPayload = {
source: 'workflow',
template_id: id,
wf_template_repo_session_id: this.templatesStore.currentSessionId,
};
this.$telemetry.track('User inserted workflow template', telemetryPayload, {
withPostHog: true,
});
await this.externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload);
}
await openTemplateCredentialSetup({
posthogStore: this.posthogStore, posthogStore: this.posthogStore,
router: this.$router, router: this.$router,
templateId: id, templateId: id,
inNewBrowserTab: e.metaKey || e.ctrlKey, inNewBrowserTab: e.metaKey || e.ctrlKey,
externalHooks: this.externalHooks,
nodeTypesStore: useNodeTypesStore(),
telemetry: this.$telemetry,
templatesStore: useTemplatesStore(),
}); });
}, },
onHidePreview() { onHidePreview() {