feat: Add workflow sharing telemetry (#4906)

* feat: Add workflow sharing telemetry

* chore: fix linting issue

* fix: fix telemetry typo
This commit is contained in:
Alex Grozav
2022-12-15 10:05:54 +02:00
committed by GitHub
parent 9956547504
commit ac066fc9f3
7 changed files with 84 additions and 6 deletions

View File

@@ -953,6 +953,8 @@ export interface IUsedCredential {
name: string; name: string;
credentialType: string; credentialType: string;
currentUserHasAccess: boolean; currentUserHasAccess: boolean;
ownedBy: Partial<IUser>;
sharedWith: Array<Partial<IUser>>;
} }
export interface WorkflowsState { export interface WorkflowsState {

View File

@@ -62,6 +62,8 @@ import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
import { IWorkflowDataUpdate } from '@/Interface'; import { IWorkflowDataUpdate } from '@/Interface';
import { getWorkflowPermissions, IPermissions } from '@/permissions';
import { useUsersStore } from '@/stores/users';
export default mixins(showMessage, workflowHelpers, restApi).extend({ export default mixins(showMessage, workflowHelpers, restApi).extend({
components: { TagsDropdown, Modal }, components: { TagsDropdown, Modal },
@@ -85,7 +87,13 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$nextTick(() => this.focusOnNameInput()); this.$nextTick(() => this.focusOnNameInput());
}, },
computed: { computed: {
...mapStores(useSettingsStore, useWorkflowsStore), ...mapStores(useUsersStore, useSettingsStore, useWorkflowsStore),
workflowPermissions(): IPermissions {
return getWorkflowPermissions(
this.usersStore.currentUser,
this.workflowsStore.getWorkflowById(this.data.id),
);
},
}, },
watch: { watch: {
isActive(active) { isActive(active) {
@@ -157,6 +165,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$telemetry.track('User duplicated workflow', { this.$telemetry.track('User duplicated workflow', {
old_workflow_id: currentWorkflowId, old_workflow_id: currentWorkflowId,
workflow_id: this.data.id, workflow_id: this.data.id,
sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee',
}); });
} }
} catch (error) { } catch (error) {

View File

@@ -139,7 +139,13 @@ import SaveButton from '@/components/SaveButton.vue';
import TagsDropdown from '@/components/TagsDropdown.vue'; import TagsDropdown from '@/components/TagsDropdown.vue';
import InlineTextEdit from '@/components/InlineTextEdit.vue'; import InlineTextEdit from '@/components/InlineTextEdit.vue';
import BreakpointsObserver from '@/components/BreakpointsObserver.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 { saveAs } from 'file-saver';
import { titleChange } from '@/mixins/titleChange'; import { titleChange } from '@/mixins/titleChange';
@@ -194,6 +200,9 @@ export default mixins(workflowHelpers, titleChange).extend({
useWorkflowsStore, useWorkflowsStore,
useUsersStore, useUsersStore,
), ),
currentUser(): IUser | null {
return this.usersStore.currentUser;
},
dynamicTranslations(): NestedRecord<string> { dynamicTranslations(): NestedRecord<string> {
return this.uiStore.dynamicTranslations; return this.uiStore.dynamicTranslations;
}, },
@@ -302,6 +311,12 @@ export default mixins(workflowHelpers, titleChange).extend({
name: WORKFLOW_SHARE_MODAL_KEY, name: WORKFLOW_SHARE_MODAL_KEY,
data: { id: this.currentWorkflowId }, data: { id: this.currentWorkflowId },
}); });
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',
});
}, },
onTagsEditEnable() { onTagsEditEnable() {
this.$data.appliedTagIds = this.currentWorkflowTagIds; this.$data.appliedTagIds = this.currentWorkflowTagIds;

View File

@@ -421,6 +421,7 @@ export default mixins(
node_type: this.activeNodeType ? this.activeNodeType.name : '', node_type: this.activeNodeType ? this.activeNodeType.name : '',
workflow_id: this.workflowsStore.workflowId, workflow_id: this.workflowsStore.workflowId,
session_id: this.sessionId, session_id: this.sessionId,
is_editable: !this.hasForeignCredential,
parameters_pane_position: this.mainPanelPosition, parameters_pane_position: this.mainPanelPosition,
input_first_connector_runs: this.maxInputRun, input_first_connector_runs: this.maxInputRun,
output_first_connector_runs: this.maxOutputRun, output_first_connector_runs: this.maxOutputRun,

View File

@@ -206,6 +206,12 @@ export default mixins(showMessage, restApi).extend({
name: WORKFLOW_SHARE_MODAL_KEY, name: WORKFLOW_SHARE_MODAL_KEY,
data: { id: this.data.id }, data: { id: this.data.id },
}); });
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',
});
} else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DELETE) { } else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DELETE) {
const deleteConfirmed = await this.confirmMessage( const deleteConfirmed = await this.confirmMessage(
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', { this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', {

View File

@@ -132,7 +132,8 @@ import { useSettingsStore } from '@/stores/settings';
import { useUIStore } from '@/stores/ui'; import { useUIStore } from '@/stores/ui';
import { useUsersStore } from '@/stores/users'; import { useUsersStore } from '@/stores/users';
import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsStore } from '@/stores/workflows';
import useWorkflowsEEStore from '@/stores/workflows.ee'; import { useWorkflowsEEStore } from '@/stores/workflows.ee';
import { ITelemetryTrackProperties } from 'n8n-workflow';
export default mixins(showMessage).extend({ export default mixins(showMessage).extend({
name: 'workflow-share-modal', name: 'workflow-share-modal',
@@ -248,12 +249,26 @@ export default mixins(showMessage).extend({
}; };
try { 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(); const workflowId = await saveWorkflowPromise();
await this.workflowsEEStore.saveWorkflowSharedWith({ await this.workflowsEEStore.saveWorkflowSharedWith({
workflowId, workflowId,
sharedWith: this.sharedWith, sharedWith: this.sharedWith,
}); });
this.trackTelemetry({
user_ids_sharees_added: shareesAdded.map((sharee) => sharee.id),
sharees_removed: shareesRemoved.length,
});
this.$showMessage({ this.$showMessage({
title: this.$locale.baseText('workflows.shareModal.onSave.success.title'), title: this.$locale.baseText('workflows.shareModal.onSave.success.title'),
type: 'success', type: 'success',
@@ -270,6 +285,10 @@ export default mixins(showMessage).extend({
const sharee = { id, firstName, lastName, email }; const sharee = { id, firstName, lastName, email };
this.sharedWith = this.sharedWith.concat(sharee); this.sharedWith = this.sharedWith.concat(sharee);
this.trackTelemetry({
user_id_sharee: userId,
});
}, },
async onRemoveSharee(userId: string) { async onRemoveSharee(userId: string) {
const user = this.usersStore.getUserById(userId)!; const user = this.usersStore.getUserById(userId)!;
@@ -348,6 +367,11 @@ export default mixins(showMessage).extend({
this.sharedWith = this.sharedWith.filter((sharee: Partial<IUser>) => { this.sharedWith = this.sharedWith.filter((sharee: Partial<IUser>) => {
return sharee.id !== user.id; return sharee.id !== user.id;
}); });
this.trackTelemetry({
user_id_sharee: userId,
warning_orphan_credentials: isLastUserWithAccessToCredentials,
});
} }
}, },
onRoleAction(user: IUser, action: string) { onRoleAction(user: IUser, action: string) {
@@ -379,6 +403,14 @@ export default mixins(showMessage).extend({
this.$router.push({ name: VIEWS.USERS_SETTINGS }); this.$router.push({ name: VIEWS.USERS_SETTINGS });
this.modalBus.$emit('close'); 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() { mounted() {
if (this.isSharingAvailable) { if (this.isSharingAvailable) {

View File

@@ -66,8 +66,10 @@ import { IWorkflowSettings } from 'n8n-workflow';
import { useNDVStore } from '@/stores/ndv'; import { useNDVStore } from '@/stores/ndv';
import { useTemplatesStore } from '@/stores/templates'; import { useTemplatesStore } from '@/stores/templates';
import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useWorkflowsEEStore } from "@/stores/workflows.ee"; import { useUsersStore } from '@/stores/users';
import { useUsersStore } from "@/stores/users"; import { useWorkflowsEEStore } from '@/stores/workflows.ee';
import { ICredentialMap, ICredentialsResponse, IUsedCredential } from '@/Interface';
import { getWorkflowPermissions, IPermissions } from '@/permissions';
import { ICredentialsResponse } from '@/Interface'; import { ICredentialsResponse } from '@/Interface';
let cachedWorkflowKey: string | null = ''; let cachedWorkflowKey: string | null = '';
@@ -85,6 +87,9 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
useUsersStore, useUsersStore,
useUIStore, useUIStore,
), ),
workflowPermissions(): IPermissions {
return getWorkflowPermissions(this.usersStore.currentUser, this.workflowsStore.workflow);
},
}, },
methods: { methods: {
executeData( executeData(
@@ -827,7 +832,15 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
this.uiStore.removeActiveAction('workflowSaving'); this.uiStore.removeActiveAction('workflowSaving');
if (error.errorCode === 100) { if (error.errorCode === 100) {
const url = this.$router.resolve({ name: VIEWS.WORKFLOW, params: { name: currentWorkflow }}).href; this.$telemetry.track('User attempted to save locked workflow', {
workflowId: currentWorkflow,
sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee',
});
const url = this.$router.resolve({
name: VIEWS.WORKFLOW,
params: { name: currentWorkflow },
}).href;
const overwrite = await this.confirmMessage( const overwrite = await this.confirmMessage(
this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message', { this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message', {