From e057dca33f33af7b440556c3d3136e683fb48519 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 13 Dec 2022 15:22:44 +0200 Subject: [PATCH 1/3] feat: Add workflow sharing telemetry --- packages/editor-ui/src/Interface.ts | 2 ++ .../components/DuplicateWorkflowDialog.vue | 7 +++++ .../components/MainHeader/WorkflowDetails.vue | 10 ++++++- .../src/components/NodeDetailsView.vue | 1 + .../editor-ui/src/components/WorkflowCard.vue | 5 ++++ .../src/components/WorkflowShareModal.ee.vue | 26 +++++++++++++++++++ .../editor-ui/src/mixins/workflowHelpers.ts | 9 +++++++ 7 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 1e0c2fbf84653..f1f343911d176 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -930,6 +930,8 @@ export interface IUsedCredential { name: string; credentialType: string; currentUserHasAccess: boolean; + ownedBy: Partial; + sharedWith: Array>; } export interface WorkflowsState { diff --git a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue index cd63c8e719def..19ce306999922 100644 --- a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -51,6 +51,8 @@ import { mapStores } from "pinia"; import { useSettingsStore } from "@/stores/settings"; import { useWorkflowsStore } from "@/stores/workflows"; import { IWorkflowDataUpdate } from "@/Interface"; +import {getWorkflowPermissions, IPermissions} from "@/permissions"; +import {useUsersStore} from "@/stores/users"; export default mixins(showMessage, workflowHelpers, restApi).extend({ components: { TagsDropdown, Modal }, @@ -75,9 +77,13 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ }, computed: { ...mapStores( + useUsersStore, useSettingsStore, useWorkflowsStore, ), + workflowPermissions(): IPermissions { + return getWorkflowPermissions(this.usersStore.currentUser, this.workflowsStore.getWorkflowById(this.data.id)); + }, }, watch: { isActive(active) { @@ -145,6 +151,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ this.$telemetry.track('User duplicated workflow', { old_workflow_id: currentWorkflowId, workflow_id: this.data.id, + sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee', }); } } catch (error) { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 099eabed5fc94..ee2fd008a89de 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -133,7 +133,7 @@ import SaveButton from "@/components/SaveButton.vue"; import TagsDropdown from "@/components/TagsDropdown.vue"; import InlineTextEdit from "@/components/InlineTextEdit.vue"; import BreakpointsObserver from "@/components/BreakpointsObserver.vue"; -import {IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare, NestedRecord} from "@/Interface"; +import {IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare, NestedRecord} from "@/Interface"; import { saveAs } from 'file-saver'; import { titleChange } from "@/mixins/titleChange"; @@ -188,6 +188,9 @@ export default mixins(workflowHelpers, titleChange).extend({ useWorkflowsStore, useUsersStore, ), + currentUser (): IUser | null { + return this.usersStore.currentUser; + }, dynamicTranslations(): NestedRecord { return this.uiStore.dynamicTranslations; }, @@ -276,6 +279,11 @@ export default mixins(workflowHelpers, titleChange).extend({ }, onShareButtonClick() { this.uiStore.openModalWithData({ name: WORKFLOW_SHARE_MODAL_KEY, data: { id: this.currentWorkflowId } }); + this.$telemetry.track('User opened sharing modale', { + workflow_id: this.currentWorkflowId, + user_id_sharer: this.currentUser?.id, + sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', + }); }, onTagsEditEnable() { this.$data.appliedTagIds = this.currentWorkflowTagIds; diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index d95610d730cfd..4e4b176b8ff4c 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -417,6 +417,7 @@ export default mixins( node_type: this.activeNodeType ? this.activeNodeType.name : '', workflow_id: this.workflowsStore.workflowId, session_id: this.sessionId, + is_editable: !this.hasForeignCredential, parameters_pane_position: this.mainPanelPosition, input_first_connector_runs: this.maxInputRun, output_first_connector_runs: this.maxOutputRun, diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue index 5ae8ec100a2e5..f869f76ec84ac 100644 --- a/packages/editor-ui/src/components/WorkflowCard.vue +++ b/packages/editor-ui/src/components/WorkflowCard.vue @@ -189,6 +189,11 @@ export default mixins( }); } else if (action === WORKFLOW_LIST_ITEM_ACTIONS.SHARE) { this.uiStore.openModalWithData({ name: WORKFLOW_SHARE_MODAL_KEY, data: { id: this.data.id } }); + this.$telemetry.track('User opened sharing modale', { + workflow_id: this.data.id, + user_id_sharer: this.currentUser.id, + sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', + }); } else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DELETE) { const deleteConfirmed = await this.confirmMessage( this.$locale.baseText( diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index a9a8a2f777489..04cb16ef9269d 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -129,6 +129,7 @@ import {useUIStore} from "@/stores/ui"; import {useUsersStore} from "@/stores/users"; import {useWorkflowsStore} from "@/stores/workflows"; import useWorkflowsEEStore from "@/stores/workflows.ee"; +import {ITelemetryTrackProperties} from "n8n-workflow"; export default mixins( showMessage, @@ -228,9 +229,17 @@ export default mixins( }; try { + const shareesAdded = this.sharedWith.filter((sharee) => !this.workflow.sharedWith?.find((previousSharee) => sharee.id === previousSharee.id)); + const shareesRemoved = this.workflow.sharedWith?.filter((previousSharee) => !this.sharedWith.find((sharee) => sharee.id === previousSharee.id)) || []; + const workflowId = await saveWorkflowPromise(); await this.workflowsEEStore.saveWorkflowSharedWith({ workflowId, sharedWith: this.sharedWith }); + this.trackTelemetry({ + user_ids_sharees_added: shareesAdded.map((sharee) => sharee.id), + sharees_removed: shareesRemoved.length, + }); + this.$showMessage({ title: this.$locale.baseText('workflows.shareModal.onSave.success.title'), type: 'success', @@ -247,6 +256,10 @@ export default mixins( const sharee = { id, firstName, lastName, email }; this.sharedWith = this.sharedWith.concat(sharee); + + this.trackTelemetry({ + user_id_sharee: userId, + }); }, async onRemoveSharee(userId: string) { const user = this.usersStore.getUserById(userId)!; @@ -304,6 +317,11 @@ export default mixins( this.sharedWith = this.sharedWith.filter((sharee: Partial) => { return sharee.id !== user.id; }); + + this.trackTelemetry({ + user_id_sharee: userId, + warning_orphan_credentials: isLastUserWithAccessToCredentials + }); } }, onRoleAction(user: IUser, action: string) { @@ -337,6 +355,14 @@ export default mixins( this.$router.push({ name: VIEWS.USERS_SETTINGS }); this.modalBus.$emit('close'); }, + trackTelemetry(data: ITelemetryTrackProperties) { + this.$telemetry.track('User selected sharee to remove', { + workflow_id: this.workflow.id, + user_id_sharer: this.currentUser?.id, + sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', + ...data, + }); + }, }, mounted() { if (this.isSharingAvailable) { diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index d7f7cc282b18f..4ab21fa0c39f1 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -68,6 +68,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes'; import useWorkflowsEEStore from "@/stores/workflows.ee"; import {useUsersStore} from "@/stores/users"; import {ICredentialMap, ICredentialsResponse, IUsedCredential} from "@/Interface"; +import {getWorkflowPermissions, IPermissions} from "@/permissions"; let cachedWorkflowKey: string | null = ''; let cachedWorkflow: Workflow | null = null; @@ -90,6 +91,9 @@ export const workflowHelpers = mixins( useUsersStore, useUIStore, ), + workflowPermissions(): IPermissions { + return getWorkflowPermissions(this.usersStore.currentUser, this.workflowsStore.workflow); + }, }, methods: { executeData(parentNode: string[], currentNode: string, inputName: string, runIndex: number): IExecuteData { @@ -749,6 +753,11 @@ export const workflowHelpers = mixins( this.uiStore.removeActiveAction('workflowSaving'); if (error.errorCode === 100) { + this.$telemetry.track('User attempted to save locked workflow', { + workflowId: currentWorkflow, + sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee', + }); + const overwrite = await this.confirmMessage( this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message'), this.$locale.baseText('workflows.concurrentChanges.confirmMessage.title'), From 2ccde061b72bd46deb597a239288066ff9e721bc Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 14 Dec 2022 16:16:07 +0200 Subject: [PATCH 2/3] chore: fix linting issue --- .../src/components/WorkflowShareModal.ee.vue | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index 84dc31b9180dd..f6987f17290df 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -133,7 +133,7 @@ import { useUIStore } from '@/stores/ui'; import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsEEStore } from '@/stores/workflows.ee'; -import { ITelemetryTrackProperties } from "n8n-workflow"; +import { ITelemetryTrackProperties } from 'n8n-workflow'; export default mixins(showMessage).extend({ name: 'workflow-share-modal', @@ -249,8 +249,14 @@ export default mixins(showMessage).extend({ }; try { - const shareesAdded = this.sharedWith.filter((sharee) => !this.workflow.sharedWith?.find((previousSharee) => sharee.id === previousSharee.id)); - const shareesRemoved = this.workflow.sharedWith?.filter((previousSharee) => !this.sharedWith.find((sharee) => sharee.id === previousSharee.id)) || []; + const shareesAdded = this.sharedWith.filter( + (sharee) => + !this.workflow.sharedWith?.find((previousSharee) => sharee.id === previousSharee.id), + ); + const shareesRemoved = + this.workflow.sharedWith?.filter( + (previousSharee) => !this.sharedWith.find((sharee) => sharee.id === previousSharee.id), + ) || []; const workflowId = await saveWorkflowPromise(); await this.workflowsEEStore.saveWorkflowSharedWith({ @@ -364,7 +370,7 @@ export default mixins(showMessage).extend({ this.trackTelemetry({ user_id_sharee: userId, - warning_orphan_credentials: isLastUserWithAccessToCredentials + warning_orphan_credentials: isLastUserWithAccessToCredentials, }); } }, From 64526c1f84ac05a47c15cfdafa7e62b89b9e6f6a Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 14 Dec 2022 16:24:26 +0200 Subject: [PATCH 3/3] fix: fix telemetry typo --- .../editor-ui/src/components/MainHeader/WorkflowDetails.vue | 2 +- packages/editor-ui/src/components/WorkflowCard.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index dc0be01daea92..de0568c139333 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -312,7 +312,7 @@ export default mixins(workflowHelpers, titleChange).extend({ data: { id: this.currentWorkflowId }, }); - this.$telemetry.track('User opened sharing modale', { + this.$telemetry.track('User opened sharing modal', { workflow_id: this.currentWorkflowId, user_id_sharer: this.currentUser?.id, sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue index 97d3fc4f7dacb..20737a4f49b16 100644 --- a/packages/editor-ui/src/components/WorkflowCard.vue +++ b/packages/editor-ui/src/components/WorkflowCard.vue @@ -207,7 +207,7 @@ export default mixins(showMessage, restApi).extend({ data: { id: this.data.id }, }); - this.$telemetry.track('User opened sharing modale', { + this.$telemetry.track('User opened sharing modal', { workflow_id: this.data.id, user_id_sharer: this.currentUser.id, sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor',