feat: Add events to enable onboarding checklist (#5536)

* feat: Add new event hooks

* fix: update event

* feat: Add more functionality for webhooks

*  Not sending onboarding checklist event from templates page

* 🔥 Removing quotes added by mistake

*  Added rest of events needed for onboarding checklist

* 💄 Hiding appcues checklist inside iframes

* 💄 Updating appcues selector

* fix: remove unnessary fix

* fix: fix schedule node

* refactor: bake events into segment store

* refactor: rename store

* refactor: use node keys

* refactor: remove unnessary

* chore: clean up store

* refactor: add key for event

* fix: allow tracking on template pages

* chore: remove comment

* fix: buidl

* refactor: block event if in iframe

* fix: fix tracking nodes

* refactor: track experiments once

* fix: ensure tracking works

* chore: remove comment

* fix: lint

* fix: lint

---------

Co-authored-by: Milorad Filipovic <milorad@n8n.io>
This commit is contained in:
Mutasem Aldmour
2023-02-28 13:44:37 +03:00
committed by GitHub
parent ae634407a4
commit 20c4919513
9 changed files with 192 additions and 54 deletions

View File

@@ -80,6 +80,11 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
return !!(nodeType && nodeType.group.includes('trigger'));
};
},
isCoreNodeType() {
return (nodeType: INodeTypeDescription) => {
return nodeType.codex?.categories?.includes('Core Nodes');
};
},
visibleNodeTypes(): INodeTypeDescription[] {
return this.allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => !nodeType.hidden);
},

View File

@@ -2,21 +2,25 @@ import { ref, Ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useUsersStore } from '@/stores/users';
import { useRootStore } from '@/stores/n8nRootStore';
import { useSettingsStore } from './settings';
import { useSettingsStore } from '@/stores/settings';
import { FeatureFlags } from 'n8n-workflow';
import { EXPERIMENTS_TO_TRACK } from '@/constants';
import { EXPERIMENTS_TO_TRACK, ONBOARDING_EXPERIMENT } from '@/constants';
import { useTelemetryStore } from './telemetry';
import { runExternalHook } from '@/mixins/externalHooks';
import { useWebhooksStore } from './webhooks';
import { useSegment } from './segment';
import { debounce } from 'lodash-es';
export const usePostHogStore = defineStore('posthog', () => {
const EVENTS = {
IS_PART_OF_EXPERIMENT: 'User is part of experiment',
};
export const usePostHog = defineStore('posthog', () => {
const usersStore = useUsersStore();
const settingsStore = useSettingsStore();
const telemetryStore = useTelemetryStore();
const rootStore = useRootStore();
const segmentStore = useSegment();
const featureFlags: Ref<FeatureFlags | null> = ref(null);
const initialized: Ref<boolean> = ref(false);
const trackedDemoExp: Ref<FeatureFlags> = ref({});
const reset = () => {
@@ -72,54 +76,42 @@ export const usePostHogStore = defineStore('posthog', () => {
debug: config.debug,
};
if (evaluatedFeatureFlags) {
window.posthog?.init(config.apiKey, options);
identify();
if (evaluatedFeatureFlags && Object.keys(evaluatedFeatureFlags).length) {
featureFlags.value = evaluatedFeatureFlags;
options.bootstrap = {
distinctId,
featureFlags: evaluatedFeatureFlags,
};
trackExperiments(evaluatedFeatureFlags);
} else {
// depend on client side evaluation if serverside evaluation fails
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
featureFlags.value = map;
trackExperiments(map);
});
}
window.posthog?.init(config.apiKey, options);
identify();
initialized.value = true;
};
const trackExperiment = (name: string) => {
const curr = featureFlags.value;
const prev = trackedDemoExp.value;
const trackExperiments = debounce((featureFlags: FeatureFlags) => {
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name));
}, 2000);
if (!curr || curr[name] === undefined) {
return;
}
if (curr[name] === prev[name]) {
return;
}
const variant = curr[name];
telemetryStore.track('User is part of experiment', {
const trackExperiment = (featureFlags: FeatureFlags, name: string) => {
const variant = featureFlags[name];
telemetryStore.track(EVENTS.IS_PART_OF_EXPERIMENT, {
name,
variant,
});
trackedDemoExp.value[name] = variant;
runExternalHook('posthog.featureFlagsUpdated', useWebhooksStore(), {
name,
variant,
});
};
watch(
() => featureFlags.value,
() => {
setTimeout(() => {
EXPERIMENTS_TO_TRACK.forEach(trackExperiment);
}, 0);
},
);
if (name === ONBOARDING_EXPERIMENT.name && variant === ONBOARDING_EXPERIMENT.variant) {
segmentStore.showAppCuesChecklist();
}
};
return {
init,

View File

@@ -0,0 +1,133 @@
import {
CODE_NODE_TYPE,
HTTP_REQUEST_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
SCHEDULE_TRIGGER_NODE_TYPE,
SET_NODE_TYPE,
WEBHOOK_NODE_TYPE,
} from '@/constants';
import { ITelemetryTrackProperties } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { useSettingsStore } from '@/stores/settings';
import { INodeTypeDescription, IRun } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows';
import { useNodeTypesStore } from '@/stores/nodeTypes';
const EVENTS = {
SHOW_CHECKLIST: 'Show checklist',
ADDED_MANUAL_TRIGGER: 'User added manual trigger',
ADDED_SCHEDULE_TRIGGER: 'User added schedule trigger',
ADDED_DATA_TRIGGER: 'User added data trigger',
RECEIEVED_MULTIPLE_DATA_ITEMS: 'User received multiple data items',
EXECUTED_MANUAL_TRIGGER: 'User executed manual trigger successfully',
EXECUTED_SCHEDULE_TRIGGER: 'User executed schedule trigger successfully',
EXECUTED_DATA_NODE_TRIGGER: 'User executed data node successfully',
MAPPED_DATA: 'User mapped data',
};
export const useSegment = defineStore('segment', () => {
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const settingsStore = useSettingsStore();
const track = (eventName: string, properties?: ITelemetryTrackProperties) => {
if (settingsStore.telemetry.enabled) {
window.analytics?.track(eventName, properties);
}
};
const showAppCuesChecklist = () => {
const isInIframe = window.location !== window.parent.location;
if (isInIframe) {
return;
}
track(EVENTS.SHOW_CHECKLIST);
};
const trackAddedTrigger = (nodeTypeName: string) => {
if (!nodeTypesStore.isTriggerNode(nodeTypeName)) {
return;
}
if (nodeTypeName === MANUAL_TRIGGER_NODE_TYPE) {
track(EVENTS.ADDED_MANUAL_TRIGGER);
} else if (nodeTypeName === SCHEDULE_TRIGGER_NODE_TYPE) {
track(EVENTS.ADDED_SCHEDULE_TRIGGER);
} else {
track(EVENTS.ADDED_DATA_TRIGGER);
}
};
const trackSuccessfulWorkflowExecution = (runData: IRun) => {
const dataNodeTypes: Set<string> = new Set<string>();
const multipleOutputNodes: Set<string> = new Set<string>();
let hasManualTrigger = false;
let hasScheduleTrigger = false;
for (const nodeName of Object.keys(runData.data.resultData.runData)) {
const nodeRunData = runData.data.resultData.runData[nodeName];
const node = workflowsStore.getNodeByName(nodeName);
const nodeTypeName = node ? node.type : 'unknown';
if (nodeRunData[0].data && nodeRunData[0].data.main.some((out) => out && out?.length > 1)) {
multipleOutputNodes.add(nodeTypeName);
}
if (node && !node.disabled) {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (isDataNodeType(nodeType)) {
dataNodeTypes.add(nodeTypeName);
}
if (isManualTriggerNode(nodeType)) {
hasManualTrigger = true;
}
if (isScheduleTriggerNode(nodeType)) {
hasScheduleTrigger = true;
}
}
}
if (multipleOutputNodes.size > 0) {
track(EVENTS.RECEIEVED_MULTIPLE_DATA_ITEMS, {
nodeTypes: Array.from(multipleOutputNodes),
});
}
if (dataNodeTypes.size > 0) {
track(EVENTS.EXECUTED_DATA_NODE_TRIGGER, {
nodeTypes: Array.from(dataNodeTypes),
});
}
if (hasManualTrigger) {
track(EVENTS.EXECUTED_MANUAL_TRIGGER);
}
if (hasScheduleTrigger) {
track(EVENTS.EXECUTED_SCHEDULE_TRIGGER);
}
};
const isManualTriggerNode = (nodeType: INodeTypeDescription | null): boolean => {
return !!nodeType && nodeType.name === MANUAL_TRIGGER_NODE_TYPE;
};
const isScheduleTriggerNode = (nodeType: INodeTypeDescription | null): boolean => {
return !!nodeType && nodeType.name === SCHEDULE_TRIGGER_NODE_TYPE;
};
const isDataNodeType = (nodeType: INodeTypeDescription | null): boolean => {
if (!nodeType) {
return false;
}
const includeCoreNodes = [
HTTP_REQUEST_NODE_TYPE,
CODE_NODE_TYPE,
SET_NODE_TYPE,
WEBHOOK_NODE_TYPE,
];
return !nodeTypesStore.isCoreNodeType(nodeType) || includeCoreNodes.includes(nodeType.name);
};
return {
showAppCuesChecklist,
track,
trackAddedTrigger,
trackSuccessfulWorkflowExecution,
EVENTS,
};
});

View File

@@ -34,7 +34,7 @@ import { getPersonalizedNodeTypes, isAuthorized, PERMISSIONS, ROLE } from '@/uti
import { defineStore } from 'pinia';
import Vue from 'vue';
import { useRootStore } from './n8nRootStore';
import { usePostHogStore } from './posthog';
import { usePostHog } from './posthog';
import { useSettingsStore } from './settings';
import { useUIStore } from './ui';
@@ -149,7 +149,7 @@ export const useUsersStore = defineStore(STORES.USERS, {
this.addUsers([user]);
this.currentUserId = user.id;
usePostHogStore().init(user.featureFlags);
usePostHog().init(user.featureFlags);
},
async loginWithCreds(params: { email: string; password: string }): Promise<void> {
const rootStore = useRootStore();
@@ -161,13 +161,13 @@ export const useUsersStore = defineStore(STORES.USERS, {
this.addUsers([user]);
this.currentUserId = user.id;
usePostHogStore().init(user.featureFlags);
usePostHog().init(user.featureFlags);
},
async logout(): Promise<void> {
const rootStore = useRootStore();
await logout(rootStore.getRestApiContext);
this.currentUserId = null;
usePostHogStore().reset();
usePostHog().reset();
},
async preOwnerSetup() {
return preOwnerSetup(useRootStore().getRestApiContext);
@@ -208,7 +208,7 @@ export const useUsersStore = defineStore(STORES.USERS, {
this.currentUserId = user.id;
}
usePostHogStore().init(user.featureFlags);
usePostHog().init(user.featureFlags);
},
async sendForgotPasswordEmail(params: { email: string }): Promise<void> {
const rootStore = useRootStore();