feat(editor, core): Integrate PostHog (#3865)

* Integrate PostHog - Part 1: Groundwork (#3753)

* Integrate PostHog - Part 2: Event capture (#3779)

* Integrate PostHog - Part 3: Session recordings (#3789)

* Integrate PostHog - Part 4: Experiments (#3825)

* Finalize PostHog integration (#3866)

* 📦 Update `package-lock.json`

* 🐛 Account for absent PH hooks file

*  Create new env `EXTERNAL_FRONTEND_HOOKS_FILES`

*  Adjust env used for injecting PostHog

* 🐛 Switch to semicolon delimiter

*  Simplify to `externalFrontendHookPath`

* Refactor FE hooks flow (#3884)

* Add env var for session recordings

* inject frontend hooks even when telemetry is off

* allow multiple hooks files

* cr

* 🐛 Handle missing ref errors

* 🔥 Remove outdated `continue`

* 🎨 Change one-liners to blocks

* 📦 Update `package-lock.json`

Co-authored-by: Ahsan Virani <ahsan.virani@gmail.com>
This commit is contained in:
Iván Ovejero
2022-08-19 15:35:39 +02:00
committed by GitHub
parent 2b4f5c6c78
commit 43e054f5ab
37 changed files with 676 additions and 217 deletions

View File

@@ -2,7 +2,7 @@
<div>
<n8n-input-label :label="label">
<div :class="{[$style.copyText]: true, [$style[size]]: true, [$style.collapsed]: collapse}" @click="copy">
<span>{{ value }}</span>
<span ref="copyInputValue">{{ value }}</span>
<div :class="$style.copyButton"><span>{{ copyButtonText }}</span></div>
</div>
</n8n-input-label>
@@ -62,6 +62,9 @@ export default mixins(copyPaste, showMessage).extend({
});
},
},
mounted() {
this.$externalHooks().run('copyInput.mounted', { copyInputValueRef: this.$refs.copyInputValue });
},
});
</script>

View File

@@ -702,6 +702,7 @@ export default mixins(showMessage, nodeHelpers).extend({
}
this.$telemetry.track('User saved credentials', trackProperties);
this.$externalHooks().run('credentialEdit.saveCredential', trackProperties);
}
return credential;

View File

@@ -16,7 +16,7 @@
/>
</div>
<el-table :data="credentialsToDisplay" v-loading="loading" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential">
<el-table :data="credentialsToDisplay" v-loading="loading" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential" ref="table">
<el-table-column property="name" :label="$locale.baseText('credentialsList.name')" class-name="clickable" sortable></el-table-column>
<el-table-column property="type" :label="$locale.baseText('credentialsList.type')" class-name="clickable" sortable></el-table-column>
<el-table-column property="createdAt" :label="$locale.baseText('credentialsList.created')" class-name="clickable" sortable></el-table-column>
@@ -96,8 +96,9 @@ export default mixins(
this.$showError(e, this.$locale.baseText('credentialsList.errorLoadingCredentials'));
}
this.loading = false;
this.$externalHooks().run('credentialsList.mounted');
this.$externalHooks().run('credentialsList.mounted', {
tableRef: this.$refs['table'],
});
this.$telemetry.track('User opened Credentials panel', { workflow_id: this.$store.getters.workflowId });
},
destroyed() {

View File

@@ -51,11 +51,13 @@
<script lang="ts">
import Vue from 'vue';
import { mapGetters } from "vuex";
import mixins from 'vue-typed-mixins';
import Modal from './Modal.vue';
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
import { externalHooks } from '@/components/mixins/externalHooks';
export default Vue.extend({
export default mixins(externalHooks).extend({
name: 'CredentialsSelectModal',
components: {
Modal,
@@ -92,7 +94,16 @@ export default Vue.extend({
openCredentialType () {
this.modalBus.$emit('close');
this.$store.dispatch('ui/openNewCredential', { type: this.selected });
this.$telemetry.track('User opened Credential modal', { credential_type: this.selected, source: 'primary_menu', new_credential: true, workflow_id: this.$store.getters.workflowId });
const telemetryPayload = {
credential_type: this.selected,
source: 'primary_menu',
new_credential: true,
workflow_id: this.$store.getters.workflowId,
};
this.$telemetry.track('User opened Credential modal', telemetryPayload);
this.$externalHooks().run('credentialsSelectModal.openCredentialType', telemetryPayload);
},
},
});

View File

@@ -7,7 +7,7 @@
>
<template v-slot:content>
<div class="filters">
<div class="filters" ref="filters">
<el-row>
<el-col :span="2" class="filter-headline">
{{ $locale.baseText('executionsList.filters') }}:
@@ -45,7 +45,7 @@
</span>
</div>
<el-table :data="combinedExecutions" stripe v-loading="isDataLoading" :row-class-name="getRowClass">
<el-table :data="combinedExecutions" stripe v-loading="isDataLoading" :row-class-name="getRowClass" ref="table">
<el-table-column label="" width="30">
<!-- eslint-disable-next-line vue/no-unused-vars -->
<template slot="header" slot-scope="scope">
@@ -245,6 +245,11 @@ export default mixins(
this.$externalHooks().run('executionsList.openDialog');
this.$telemetry.track('User opened Executions log', { workflow_id: this.$store.getters.workflowId });
this.$externalHooks().run('executionsList.created', {
tableRef: this.$refs['table'],
filtersRef: this.$refs['filters'],
});
},
beforeDestroy() {
if (this.autoRefreshInterval) {

View File

@@ -21,7 +21,7 @@
<div class="editor-description">
{{ $locale.baseText('expressionEdit.expression') }}
</div>
<div class="expression-editor">
<div class="expression-editor" ref="expressionInput">
<expression-input :parameter="parameter" ref="inputFieldExpression" rows="8" :value="value" :path="path" @change="valueChanged" @keydown.stop="noOp"></expression-input>
</div>
</div>
@@ -30,7 +30,9 @@
<div class="editor-description">
{{ $locale.baseText('expressionEdit.result') }}
</div>
<expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="displayValue" :path="path"></expression-input>
<div ref="expressionOutput">
<expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="displayValue" :path="path"></expression-input>
</div>
</div>
</el-col>
@@ -74,6 +76,17 @@ export default mixins(
latestValue: '',
};
},
updated() {
if (this.$refs.expressionInput && this.$refs.expressionOutput) {
this.$externalHooks().run(
'expressionEdit.mounted',
{
expressionInputRef: this.$refs.expressionInput,
expressionOutputRef: this.$refs.expressionOutput,
},
);
}
},
methods: {
valueChanged (value: string, forceUpdate = false) {
this.latestValue = value;
@@ -167,14 +180,16 @@ export default mixins(
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue });
if (!newValue) {
this.$telemetry.track('User closed Expression Editor', {
const telemetryPayload = {
empty_expression: (this.value === '=') || (this.value === '={{}}') || !this.value,
workflow_id: this.$store.getters.workflowId,
source: this.eventSource,
session_id: this.$store.getters['ui/ndvSessionId'],
has_parameter: this.value.includes('$parameter'),
has_mapping: hasExpressionMapping(this.value),
});
};
this.$telemetry.track('User closed Expression Editor', telemetryPayload);
this.$externalHooks().run('expressionEdit.closeDialog', telemetryPayload);
}
},
},

View File

@@ -146,14 +146,16 @@
<span slot="title" class="item-title-root">{{nextVersions.length > 99 ? '99+' : nextVersions.length}} update{{nextVersions.length > 1 ? 's' : ''}} available</span>
</n8n-menu-item>
<el-dropdown placement="right-end" trigger="click" @command="onUserActionToggle" v-if="canUserAccessSidebarUserInfo && currentUser">
<n8n-menu-item class="user">
<div class="avatar">
<n8n-avatar :firstName="currentUser.firstName" :lastName="currentUser.lastName" size="small" />
</div>
<span slot="title" class="item-title-root" v-if="!isCollapsed">
{{currentUser.fullName}}
</span>
</n8n-menu-item>
<div ref="user">
<n8n-menu-item class="user">
<div class="avatar">
<n8n-avatar :firstName="currentUser.firstName" :lastName="currentUser.lastName" size="small" />
</div>
<span slot="title" class="item-title-root" v-if="!isCollapsed">
{{currentUser.fullName}}
</span>
</n8n-menu-item>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
command="settings"
@@ -359,6 +361,11 @@ export default mixins(
return this.$route.meta && this.$route.meta.nodeView;
},
},
mounted() {
if (this.$refs.user) {
this.$externalHooks().run('mainSidebar.mounted', { userRef: this.$refs.user });
}
},
methods: {
trackHelpItemClick (itemType: string) {
this.$telemetry.track('User clicked help resource', { type: itemType, workflow_id: this.$store.getters.workflowId });

View File

@@ -179,7 +179,14 @@ export default mixins(
}
if (!this.hasPinData || shouldUnpinAndExecute) {
this.$telemetry.track('User clicked execute node button', { node_type: this.nodeType ? this.nodeType.name : null, workflow_id: this.$store.getters.workflowId, source: this.telemetrySource });
const telemetryPayload = {
node_type: this.nodeType ? this.nodeType.name : null,
workflow_id: this.$store.getters.workflowId,
source: this.telemetrySource,
};
this.$telemetry.track('User clicked execute node button', telemetryPayload);
this.$externalHooks().run('nodeExecuteButton.onClick', telemetryPayload);
this.runWorkflow(this.nodeName, 'RunData.ExecuteNodeButton');
this.$emit('execute');
}

View File

@@ -200,7 +200,10 @@
:label="getOptionsOptionDisplayName(option)"
>
<div class="list-option">
<div class="option-headline">
<div
class="option-headline"
:class="{ 'remote-parameter-option': isRemoteParameterOption(option) }"
>
{{ getOptionsOptionDisplayName(option) }}
</div>
<div
@@ -689,6 +692,9 @@ export default mixins(
},
},
methods: {
isRemoteParameterOption(option: INodePropertyOptions) {
return this.remoteParameterOptions.map(o => o.name).includes(option.name);
},
credentialSelected (updateInformation: INodeUpdatePropertiesInformation) {
// Update the values on the node
this.$store.commit('updateNodeProperties', updateInformation);
@@ -941,7 +947,7 @@ export default mixins(
}
if (this.node && (command === 'addExpression' || command === 'removeExpression')) {
this.$telemetry.track('User switched parameter mode', {
const telemetryPayload = {
node_type: this.node.type,
parameter: this.path,
old_mode: command === 'addExpression' ? 'fixed': 'expression',
@@ -949,10 +955,21 @@ export default mixins(
was_parameter_empty: prevValue === '' || prevValue === undefined,
had_mapping: hasExpressionMapping(prevValue),
had_parameter: typeof prevValue === 'string' && prevValue.includes('$parameter'),
});
};
this.$telemetry.track('User switched parameter mode', telemetryPayload);
this.$externalHooks().run('parameterInput.modeSwitch', telemetryPayload);
}
},
},
updated () {
this.$nextTick(() => {
const remoteParameterOptions = this.$el.querySelectorAll('.remote-parameter-option');
if (remoteParameterOptions.length > 0) {
this.$externalHooks().run('parameterInput.updated', { remoteParameterOptions });
}
});
},
mounted () {
this.$on('optionSelected', this.optionSelected);

View File

@@ -465,7 +465,11 @@ export default mixins(showMessage, workflowHelpers).extend({
this.$data.isSaving = true;
try {
await this.$store.dispatch('users/submitPersonalizationSurvey', {...values, version: SURVEY_VERSION});
const survey = { ...values, version: SURVEY_VERSION };
this.$externalHooks().run('personalizationModal.onSubmit', survey);
await this.$store.dispatch('users/submitPersonalizationSurvey', survey);
if (Object.keys(values).length === 0) {
this.closeDialog();

View File

@@ -488,6 +488,18 @@ export default mixins(
}
}
},
updated() {
this.$nextTick(() => {
const jsonValues = this.$el.querySelectorAll('.vjs-value');
const tableRows = this.$el.querySelectorAll('tbody tr');
const elements = [...jsonValues, ...tableRows].reduce<Element[]>((acc, cur) => [...acc, cur], []);
if (elements.length > 0) {
this.$externalHooks().run('runData.updated', { elements });
}
});
},
destroyed() {
this.hidePinDataDiscoveryTooltip();
this.eventBus.$off('data-pinning-error', this.onDataPinningError);
@@ -802,14 +814,16 @@ export default mixins(
});
},
onDataPinningSuccess({ source }: { source: 'pin-icon-click' | 'save-edit' }) {
this.$telemetry.track('Ndv data pinning success', {
const telemetryPayload = {
pinning_source: source,
node_type: this.activeNode.type,
session_id: this.sessionId,
data_size: stringSizeInBytes(this.pinData),
view: this.displayMode,
run_index: this.runIndex,
});
};
this.$externalHooks().run('runData.onDataPinningSuccess', telemetryPayload);
this.$telemetry.track('Ndv data pinning success', telemetryPayload);
},
onDataPinningError(
{ errorType, source }: {
@@ -831,12 +845,15 @@ export default mixins(
{ source }: { source: 'banner-link' | 'pin-icon-click' | 'unpin-and-execute-modal' },
) {
if (source === 'pin-icon-click') {
this.$telemetry.track('User clicked pin data icon', {
const telemetryPayload = {
node_type: this.activeNode.type,
session_id: this.sessionId,
run_index: this.runIndex,
view: !this.hasNodeRun && !this.hasPinData ? 'none' : this.displayMode,
});
};
this.$externalHooks().run('runData.onTogglePinData', telemetryPayload);
this.$telemetry.track('User clicked pin data icon', telemetryPayload);
}
this.updateNodeParameterIssues(this.node);

View File

@@ -65,10 +65,12 @@
import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants';
import { INodeUi, ITableData } from '@/Interface';
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
import Draggable from './Draggable.vue';
import { shorten } from './helpers';
import { externalHooks } from './mixins/externalHooks';
export default Vue.extend({
export default mixins(externalHooks).extend({
name: 'RunDataTable',
components: { Draggable },
props: {
@@ -151,7 +153,7 @@ export default Vue.extend({
onDragEnd(column: string) {
setTimeout(() => {
const mappingTelemetry = this.$store.getters['ui/mappingTelemetry'];
this.$telemetry.track('User dragged data for mapping', {
const telemetryPayload = {
src_node_type: this.node.type,
src_field_name: column,
src_nodes_back: this.distanceFromActive,
@@ -161,7 +163,11 @@ export default Vue.extend({
src_element: 'column',
success: false,
...mappingTelemetry,
});
};
this.$externalHooks().run('runDataTable.onDragEnd', telemetryPayload);
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
}, 1000); // ensure dest data gets set if drop
},
},

View File

@@ -1,5 +1,5 @@
<template>
<div class="sticky-wrapper" :style="stickyPosition" :id="nodeId">
<div class="sticky-wrapper" :style="stickyPosition" :id="nodeId" ref="sticky">
<div
:class="{'sticky-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}"
:style="stickySize"
@@ -67,6 +67,9 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
type: Number,
},
},
mounted() {
this.$externalHooks().run('sticky.mounted', { stickyRef: this.$refs['sticky'] });
},
computed: {
defaultText (): string {
if (!this.nodeType) {

View File

@@ -4,20 +4,23 @@
<script lang="ts">
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
import { mapGetters } from 'vuex';
import { externalHooks } from './mixins/externalHooks';
export default Vue.extend({
export default mixins(externalHooks).extend({
name: 'Telemetry',
data() {
return {
initialised: false,
isTelemetryInitialized: false,
};
},
computed: {
...mapGetters('settings', ['telemetry']),
...mapGetters('users', ['currentUserId']),
isTelemeteryEnabledOnRoute(): boolean {
...mapGetters(['instanceId']),
isTelemetryEnabledOnRoute(): boolean {
return this.$route.meta && this.$route.meta.telemetry ? !this.$route.meta.telemetry.disabled: true;
},
},
@@ -26,17 +29,24 @@ export default Vue.extend({
},
methods: {
init() {
if (this.initialised || !this.isTelemeteryEnabledOnRoute) {
if (this.isTelemetryInitialized || !this.isTelemetryEnabledOnRoute) return;
const telemetrySettings = this.telemetry;
if (!telemetrySettings || !telemetrySettings.enabled) {
return;
}
const opts = this.telemetry;
if (opts && opts.enabled) {
this.initialised = true;
const instanceId = this.$store.getters.instanceId;
const userId = this.$store.getters['users/currentUserId'];
const logLevel = this.$store.getters['settings/logLevel'];
this.$telemetry.init(opts, { instanceId, logLevel, userId, store: this.$store });
}
this.$telemetry.init(
telemetrySettings,
{
instanceId: this.instanceId,
userId: this.currentUserId,
store: this.$store,
},
);
this.isTelemetryInitialized = true;
},
},
watch: {
@@ -44,10 +54,13 @@ export default Vue.extend({
this.init();
},
currentUserId(userId) {
const instanceId = this.$store.getters.instanceId;
this.$telemetry.identify(instanceId, userId);
this.$telemetry.identify(this.instanceId, userId);
this.$externalHooks().run('telemetry.currentUserIdChanged', {
instanceId: this.instanceId,
userId,
});
},
isTelemeteryEnabledOnRoute(enabled) {
isTelemetryEnabledOnRoute(enabled) {
if (enabled) {
this.init();
}

View File

@@ -25,7 +25,7 @@
</div>
</div>
<div v-else class="value clickable" @click="selectItem(item)">
<div class="item-title" :title="item.key">
<div class="item-title" :title="item.key" ref="variableSelectorItem">
{{item.name}}:
<font-awesome-icon icon="dot-circle" title="Select Item" />
</div>
@@ -41,8 +41,10 @@ import {
IVariableSelectorOption,
IVariableItemSelected,
} from '@/Interface';
import { externalHooks } from "@/components/mixins/externalHooks";
import mixins from 'vue-typed-mixins';
export default Vue.extend({
export default mixins(externalHooks).extend({
name: 'VariableSelectorItem',
props: [
'allowParentSelect',
@@ -85,6 +87,14 @@ export default Vue.extend({
extended: false,
};
},
mounted() {
if (this.$refs.variableSelectorItem) {
this.$externalHooks().run(
'variableSelectorItem.mounted',
{ variableSelectorItemRef: this.$refs.variableSelectorItem },
);
}
},
methods: {
optionSelected (command: string, item: IVariableSelectorOption) {
// By default it is raw

View File

@@ -29,7 +29,7 @@
</template>
<template v-slot:content>
<el-table class="search-table" :data="filteredWorkflows" stripe @cell-click="openWorkflow" :default-sort = "{prop: 'updatedAt', order: 'descending'}" v-loading="isDataLoading">
<el-table class="search-table" :data="filteredWorkflows" stripe @cell-click="openWorkflow" :default-sort = "{prop: 'updatedAt', order: 'descending'}" v-loading="isDataLoading" ref="table">
<el-table-column property="name" :label="$locale.baseText('workflowOpen.name')" class-name="clickable" sortable>
<template slot-scope="scope">
<div :key="scope.row.id">
@@ -127,6 +127,10 @@ export default mixins(
// Make sure that users can directly type in the filter
(this.$refs.inputFieldFilter as HTMLInputElement).focus();
});
this.$externalHooks().run('workflowOpen.mounted', {
tableRef: this.$refs['table'],
});
},
methods: {
getIds(tags: ITag[] | undefined) {

View File

@@ -41,7 +41,14 @@ export const workflowActivate = mixins(
const activeWorkflows = this.$store.getters.getActiveWorkflows;
const isWorkflowActive = activeWorkflows.includes(currWorkflowId);
this.$telemetry.track('User set workflow active status', { workflow_id: currWorkflowId, is_active: newActiveState, previous_status: isWorkflowActive, ndv_input: telemetrySource === 'ndv' });
const telemetryPayload = {
workflow_id: currWorkflowId,
is_active: newActiveState,
previous_status: isWorkflowActive,
ndv_input: telemetrySource === 'ndv',
};
this.$telemetry.track('User set workflow active status', telemetryPayload);
this.$externalHooks().run('workflowActivate.updateWorkflowActivation', telemetryPayload);
try {
if (isWorkflowActive && newActiveState) {

View File

@@ -95,6 +95,9 @@ const module: Module<IUsersState, IRootState> = {
getUserById(state: IUsersState): (userId: string) => IUser | null {
return (userId: string): IUser | null => state.users[userId];
},
globalRoleName(state: IUsersState, getters: any) { // tslint:disable-line:no-any
return getters.currentUser.globalRole.name;
},
canUserDeleteTags(state: IUsersState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any
const currentUser = getters.currentUser;

View File

@@ -4,15 +4,11 @@ import {
ITelemetryTrackProperties,
IDataObject,
} from 'n8n-workflow';
import { ILogLevel, INodeCreateElement, IRootState } from "@/Interface";
import { Route } from "vue-router";
import { Store } from "vuex";
declare module 'vue/types/vue' {
interface Vue {
$telemetry: Telemetry;
}
}
import type { INodeCreateElement, IRootState } from "@/Interface";
import type { Store } from "vuex";
import type { IUserNodesPanelSession } from "./telemetry.types";
export function TelemetryPlugin(vue: typeof _Vue): void {
const telemetry = new Telemetry();
@@ -25,25 +21,13 @@ export function TelemetryPlugin(vue: typeof _Vue): void {
});
}
interface IUserNodesPanelSessionData {
nodeFilter: string;
resultsNodes: string[];
filterMode: string;
}
interface IUserNodesPanelSession {
sessionId: string;
data: IUserNodesPanelSessionData;
}
class Telemetry {
export class Telemetry {
private pageEventQueue: Array<{route: Route}>;
private previousPath: string;
private store: Store<IRootState> | null;
private get telemetry() {
// @ts-ignore
private get rudderStack() {
return window.rudderanalytics;
}
@@ -62,45 +46,64 @@ class Telemetry {
this.store = null;
}
init(options: ITelemetrySettings, { instanceId, logLevel, userId, store }: { instanceId: string, logLevel?: ILogLevel, userId?: string, store: Store<IRootState> }) {
if (options.enabled && !this.telemetry) {
if(!options.config) {
return;
}
init(
telemetrySettings: ITelemetrySettings,
{ instanceId, userId, store }: {
instanceId: string;
userId?: string;
store: Store<IRootState>;
},
) {
if (!telemetrySettings.enabled || !telemetrySettings.config || this.rudderStack) return;
this.store = store;
const logging = logLevel === 'debug' ? { logLevel: 'DEBUG'} : {};
this.loadTelemetryLibrary(options.config.key, options.config.url, { integrations: { All: false }, loadIntegration: false, ...logging});
this.identify(instanceId, userId);
this.flushPageEvents();
this.track('Session started', { session_id: store.getters.sessionId });
}
const { config: { key, url } } = telemetrySettings;
this.store = store;
const logLevel = store.getters['settings/logLevel'];
const logging = logLevel === 'debug' ? { logLevel: 'DEBUG' } : {};
this.initRudderStack(
key,
url,
{
integrations: { All: false },
loadIntegration: false,
...logging,
},
);
this.identify(instanceId, userId);
this.flushPageEvents();
this.track('Session started', { session_id: store.getters.sessionId });
}
identify(instanceId: string, userId?: string) {
const traits = { instance_id: instanceId };
if (userId) {
this.telemetry.identify(`${instanceId}#${userId}`, traits);
this.rudderStack.identify(`${instanceId}#${userId}`, traits);
}
else {
this.telemetry.reset();
this.telemetry.identify(undefined, traits);
this.rudderStack.reset();
this.rudderStack.identify(undefined, traits);
}
}
track(event: string, properties?: ITelemetryTrackProperties) {
if (this.telemetry) {
const updatedProperties = {
...properties,
version_cli: this.store && this.store.getters.versionCli,
};
if (!this.rudderStack) return;
this.telemetry.track(event, updatedProperties);
}
const updatedProperties = {
...properties,
version_cli: this.store && this.store.getters.versionCli,
};
this.rudderStack.track(event, updatedProperties);
}
page(route: Route) {
if (this.telemetry) {
if (this.rudderStack) {
if (route.path === this.previousPath) { // avoid duplicate requests query is changed for example on search page
return;
}
@@ -113,7 +116,7 @@ class Telemetry {
}
const category = (route.meta && route.meta.telemetry && route.meta.telemetry.pageCategory) || 'Editor';
this.telemetry.page(category, pageName, properties);
this.rudderStack.page(category, pageName!, properties);
}
else {
this.pageEventQueue.push({
@@ -131,7 +134,7 @@ class Telemetry {
}
trackNodesPanel(event: string, properties: IDataObject = {}) {
if (this.telemetry) {
if (this.rudderStack) {
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
switch (event) {
case 'nodeView.createNodeActiveChanged':
@@ -203,36 +206,50 @@ class Telemetry {
};
}
private loadTelemetryLibrary(key: string, url: string, options: IDataObject) {
// @ts-ignore
private initRudderStack(key: string, url: string, options: IDataObject) {
window.rudderanalytics = window.rudderanalytics || [];
this.telemetry.methods = ["load", "page", "track", "identify", "alias", "group", "ready", "reset", "getAnonymousId", "setAnonymousId"];
this.telemetry.factory = (t: any) => { // tslint:disable-line:no-any
return (...args: any[]) => { // tslint:disable-line:no-any
const r = Array.prototype.slice.call(args);
r.unshift(t);
this.telemetry.push(r);
return this.telemetry;
this.rudderStack.methods = [
"load",
"page",
"track",
"identify",
"alias",
"group",
"ready",
"reset",
"getAnonymousId",
"setAnonymousId",
];
this.rudderStack.factory = (method: string) => {
return (...args: unknown[]) => {
const argsCopy = [method, ...args];
this.rudderStack.push(argsCopy);
return this.rudderStack;
};
};
for (let t = 0; t < this.telemetry.methods.length; t++) {
const r = this.telemetry.methods[t];
this.telemetry[r] = this.telemetry.factory(r);
for (const method of this.rudderStack.methods) {
this.rudderStack[method] = this.rudderStack.factory(method);
}
this.telemetry.loadJS = () => {
const r = document.createElement("script");
r.type = "text/javascript";
r.async = !0;
r.src = "https://cdn.rudderlabs.com/v1/rudder-analytics.min.js";
const a = document.getElementsByTagName("script")[0];
if(a && a.parentNode) {
a.parentNode.insertBefore(r, a);
this.rudderStack.loadJS = () => {
const script = document.createElement("script");
script.type = "text/javascript";
script.async = !0;
script.src = "https://cdn.rudderlabs.com/v1/rudder-analytics.min.js";
const element: Element = document.getElementsByTagName("script")[0];
if (element && element.parentNode) {
element.parentNode.insertBefore(script, element);
}
};
this.telemetry.loadJS();
this.telemetry.load(key, url, options);
this.rudderStack.loadJS();
this.rudderStack.load(key, url, options);
}
}

View File

@@ -1 +0,0 @@
declare module 'rudder-sdk-js';

View File

@@ -0,0 +1,95 @@
import type { Telemetry } from '.';
declare module 'vue/types/vue' {
interface Vue {
$telemetry: Telemetry;
}
}
declare global {
interface Window {
rudderanalytics: RudderStack;
featureFlag: FeatureFlag;
}
}
export interface IUserNodesPanelSession {
sessionId: string;
data: IUserNodesPanelSessionData;
}
interface IUserNodesPanelSessionData {
nodeFilter: string;
resultsNodes: string[];
filterMode: string;
}
interface FeatureFlag {
getAll(): string[];
get(flagName: string): boolean | undefined;
isEnabled(flagName: string): boolean | undefined;
reload(): void;
}
/**
* Simplified version of:
* https://github.com/rudderlabs/rudder-sdk-js/blob/master/dist/rudder-sdk-js/index.d.ts
*/
interface RudderStack extends Array<unknown> {
[key: string]: unknown;
methods: string[];
factory: (method: string) => (...args: unknown[]) => RudderStack;
loadJS(): void;
/**
* Native methods
*/
load(
writeKey: string,
dataPlaneUrl: string,
options?: object,
): void;
ready(): void;
page(
category?: string,
name?: string,
properties?: object,
options?: object,
): void;
track(
event: string,
properties?: object,
options?: object,
): void;
identify(
id?: string,
traits?: object,
options?: object,
): void;
alias(
to: string,
from?: string,
options?: object,
): void;
group(
group: string,
traits?: object,
options?: object,
): void;
getAnonymousId(): void;
setAnonymousId(id?: string): void;
reset(): void;
}

View File

@@ -435,15 +435,23 @@ export default mixins(
methods: {
onRunNode(nodeName: string, source: string) {
const node = this.$store.getters.getNodeByName(nodeName);
this.$telemetry.track('User clicked execute node button', { node_type: node ? node.type : null, workflow_id: this.$store.getters.workflowId, source: 'canvas' });
const telemetryPayload = {
node_type: node ? node.type : null,
workflow_id: this.$store.getters.workflowId,
source: 'canvas',
};
this.$telemetry.track('User clicked execute node button', telemetryPayload);
this.$externalHooks().run('nodeView.onRunNode', telemetryPayload);
this.runWorkflow(nodeName, source);
},
onRunWorkflow() {
this.getWorkflowDataToSave().then((workflowData) => {
this.$telemetry.track('User clicked execute workflow button', {
const telemetryPayload = {
workflow_id: this.$store.getters.workflowId,
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
});
};
this.$telemetry.track('User clicked execute workflow button', telemetryPayload);
this.$externalHooks().run('nodeView.onRunWorkflow', telemetryPayload);
});
this.runWorkflow();
@@ -2042,6 +2050,9 @@ export default mixins(
this.$store.commit('setStateDirty', false);
this.setZoomLevel(1);
if (window.featureFlag && !window.featureFlag.isEnabled('show-welcome-note')) return;
setTimeout(() => {
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [0, 0]});
// For novice users (onboardingFlowEnabled == true)

View File

@@ -180,7 +180,9 @@ export default mixins(
},
methods: {
openInstallModal(event: MouseEvent) {
this.$telemetry.track('user clicked cnr install button', { is_empty_state: this.getInstalledPackages.length === 0 });
const telemetryPayload = { is_empty_state: this.getInstalledPackages.length === 0 };
this.$telemetry.track('user clicked cnr install button', telemetryPayload);
this.$externalHooks().run('settingsCommunityNodesView.openInstallModal', telemetryPayload);
this.$store.dispatch('ui/openModal', COMMUNITY_PACKAGE_INSTALL_MODAL_KEY);
},
onDescriptionTextClick(event: MouseEvent) {

View File

@@ -3,7 +3,7 @@
<div :class="$style.container">
<div :class="$style.header">
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.personal.personalSettings') }}</n8n-heading>
<div :class="$style.user">
<div :class="$style.user" ref="user">
<span :class="$style.username">
<n8n-text color="text-light">{{currentUser.fullName}}</n8n-text>
</span>
@@ -14,14 +14,16 @@
<div :class="$style.sectionHeader">
<n8n-heading size="large">{{ $locale.baseText('settings.personal.basicInformation') }}</n8n-heading>
</div>
<n8n-form-inputs
v-if="formInputs"
:inputs="formInputs"
:eventBus="formBus"
@input="onInput"
@ready="onReadyToSubmit"
@submit="onSubmit"
/>
<div>
<n8n-form-inputs
v-if="formInputs"
:inputs="formInputs"
:eventBus="formBus"
@input="onInput"
@ready="onReadyToSubmit"
@submit="onSubmit"
/>
</div>
</div>
<div>
<div :class="$style.sectionHeader">
@@ -101,6 +103,10 @@ export default mixins(
},
},
];
if (this.$refs.user) {
this.$externalHooks().run('settingsPersonalView.mounted', { userRef: this.$refs.user });
}
},
computed: {
currentUser() {

View File

@@ -106,11 +106,13 @@ export default mixins(workflowHelpers).extend({
this.navigateTo(event, VIEWS.TEMPLATE, id);
},
onUseWorkflow({event, id}: {event: MouseEvent, id: string}) {
this.$telemetry.track('User inserted workflow template', {
const telemetryPayload = {
template_id: id,
wf_template_repo_session_id: this.$store.getters['templates/currentSessionId'],
source: 'collection',
});
};
this.$externalHooks().run('templatesCollectionView.onUseWorkflow', telemetryPayload);
this.$telemetry.track('User inserted workflow template', telemetryPayload);
this.navigateTo(event, VIEWS.TEMPLATE_IMPORT, id);
},

View File

@@ -89,11 +89,14 @@ export default mixins(workflowHelpers).extend({
},
methods: {
openWorkflow(id: string, e: PointerEvent) {
this.$telemetry.track('User inserted workflow template', {
const telemetryPayload = {
source: 'workflow',
template_id: id,
wf_template_repo_session_id: this.$store.getters['templates/currentSessionId'],
});
};
this.$externalHooks().run('templatesWorkflowView.openWorkflow', telemetryPayload);
this.$telemetry.track('User inserted workflow template', telemetryPayload);
if (e.metaKey || e.ctrlKey) {
const route = this.$router.resolve({ name: VIEWS.TEMPLATE_IMPORT, params: { id } });