From 35c451b8033713b8277cf6fca0603c74eb4fd9b5 Mon Sep 17 00:00:00 2001 From: Wils Dawson Date: Wed, 19 Jan 2022 13:33:21 -0800 Subject: [PATCH] Git Sync Events (#4365) * Cleanup old plugin events * Add VCS lifecyle events * Git action events * Apply Opender's Suggestions Co-authored-by: Opender Singh * Apply review changes for stage/unstage & rollback * Update error messages for clone events Co-authored-by: Opender Singh --- packages/insomnia-app/app/common/analytics.ts | 25 ++++++++++++++++--- .../insomnia-app/app/plugins/context/data.ts | 6 ++--- .../dropdowns/git-sync-dropdown.tsx | 6 ++++- .../components/modals/git-branches-modal.tsx | 5 ++++ .../components/modals/git-staging-modal.tsx | 18 +++++++++++-- .../ui/redux/modules/__tests__/git.test.tsx | 2 +- .../insomnia-app/app/ui/redux/modules/git.tsx | 21 ++++++++++++++-- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/packages/insomnia-app/app/common/analytics.ts b/packages/insomnia-app/app/common/analytics.ts index 5d90d1981..3fd593018 100644 --- a/packages/insomnia-app/app/common/analytics.ts +++ b/packages/insomnia-app/app/common/analytics.ts @@ -117,8 +117,6 @@ let segmentClient: Analytics | null = null; export enum SegmentEvent { collectionCreate = 'Collection Created', documentCreate = 'Document Created', - pluginExportLoadAllWokspace = 'Plugin export loading all workspace', - pluginExportLoadWorkspacesInProject = 'Plugin export loading workspaces for active project', requestCreate = 'Request Created', requestExecute = 'Request Executed', projectLocalCreate = 'Local Project Created', @@ -129,6 +127,26 @@ export enum SegmentEvent { unitTestDelete = 'Unit Test Deleted', unitTestRun = 'Ran Individual Unit Test', unitTestRunAll = 'Ran All Unit Tests', + vcsSyncStart = 'VCS Sync Started', + vcsSyncComplete = 'VCS Sync Completed', + vcsAction = 'VCS Action Executed', +} + +type PushPull = 'push' | 'pull'; + +export function vcsSegmentEventProperties( + type: 'git', + action: PushPull | `force_${PushPull}` | + 'create_branch' | 'merge_branch' | 'delete_branch' | 'checkout_branch' | + 'commit' | 'stage_all' | 'stage' | 'unstage_all' | 'unstage' | 'rollback' | 'rollback_all' | + 'update' | 'setup' | 'clone', + error?: string +) { + return { + 'type': type, + 'action': action, + 'error': error, + }; } export async function trackSegmentEvent(event: SegmentEvent, properties?: Record) { @@ -152,8 +170,7 @@ export async function trackSegmentEvent(event: SegmentEvent, properties?: Record } const anonymousId = await getDeviceId(); - // TODO: This currently always returns an empty string in the main process - // This is due to the session data being stored in localStorage + // This may return an empty string or undefined when a user is not logged in const userId = getAccountId(); segmentClient.track({ anonymousId, diff --git a/packages/insomnia-app/app/plugins/context/data.ts b/packages/insomnia-app/app/plugins/context/data.ts index 062f7bdeb..9da6198bd 100644 --- a/packages/insomnia-app/app/plugins/context/data.ts +++ b/packages/insomnia-app/app/plugins/context/data.ts @@ -1,4 +1,3 @@ -import { SegmentEvent, trackSegmentEvent } from '../../common/analytics'; import { exportWorkspacesData, exportWorkspacesHAR } from '../../common/export'; import type { ImportRawConfig } from '../../common/import'; import { importRaw, importUri } from '../../common/import'; @@ -29,10 +28,11 @@ const buildImportRawConfig = (options: PluginImportOptions, activeProjectId: str const getWorkspaces = (activeProjectId?: string) => { if (activeProjectId) { - trackSegmentEvent(SegmentEvent.pluginExportLoadWorkspacesInProject); return models.workspace.findByParentId(activeProjectId); } else { - trackSegmentEvent(SegmentEvent.pluginExportLoadAllWokspace); + // This code path was kept in case there was ever a time when the app wouldn't have an active project. + // In over 5 months of monitoring in production, we never saw this happen. + // Keeping it for defensive purposes, but it's not clear if it's necessary. return models.workspace.all(); } }; diff --git a/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.tsx b/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.tsx index 2478c0dff..17c46eda7 100644 --- a/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.tsx +++ b/packages/insomnia-app/app/ui/components/dropdowns/git-sync-dropdown.tsx @@ -4,7 +4,7 @@ import React, { Fragment, PureComponent, ReactNode } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { trackEvent } from '../../../common/analytics'; +import { SegmentEvent, trackEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics'; import { AUTOBIND_CFG } from '../../../common/constants'; import { database as db } from '../../../common/database'; import { docsGitSync } from '../../../common/documentation'; @@ -130,11 +130,13 @@ class GitSyncDropdown extends PureComponent { try { await vcs.pull(gitRepository.credentials); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'pull')); } catch (err) { showError({ title: 'Error Pulling Repository', error: err, }); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'pull', err.message)); } await db.flushChanges(bufferId); @@ -186,6 +188,7 @@ class GitSyncDropdown extends PureComponent { try { await vcs.push(gitRepository.credentials, force); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', force ? 'force_push' : 'push')); } catch (err) { if (err.code === 'PushRejectedError') { this._dropdown?.hide(); @@ -203,6 +206,7 @@ class GitSyncDropdown extends PureComponent { title: 'Error Pushing Repository', error: err, }); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', force ? 'force_push' : 'push', err.message)); } } diff --git a/packages/insomnia-app/app/ui/components/modals/git-branches-modal.tsx b/packages/insomnia-app/app/ui/components/modals/git-branches-modal.tsx index 7834ddbab..d535ad57c 100644 --- a/packages/insomnia-app/app/ui/components/modals/git-branches-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/git-branches-modal.tsx @@ -2,6 +2,7 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator'; import classnames from 'classnames'; import React, { Fragment, PureComponent } from 'react'; +import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics'; import { AUTOBIND_CFG } from '../../../common/constants'; import { database as db } from '../../../common/database'; import type { GitRepository } from '../../../models/git-repository'; @@ -113,6 +114,7 @@ export class GitBranchesModal extends PureComponent { const { vcs } = this.props; const { newBranchName } = this.state; await vcs.checkout(newBranchName); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'create_branch')); await this._refreshState({ newBranchName: '', }); @@ -125,6 +127,7 @@ export class GitBranchesModal extends PureComponent { await vcs.merge(branch); // Apparently merge doesn't update the working dir so need to checkout too await this._handleCheckout(branch); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'merge_branch')); }); } @@ -132,6 +135,7 @@ export class GitBranchesModal extends PureComponent { await this._errorHandler(async () => { const { vcs } = this.props; await vcs.deleteBranch(branch); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'delete_branch')); await this._refreshState(); }); } @@ -150,6 +154,7 @@ export class GitBranchesModal extends PureComponent { const { vcs, handleInitializeEntities } = this.props; const bufferId = await db.bufferChanges(); await vcs.checkout(branch); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'checkout_branch')); await db.flushChanges(bufferId, true); await handleInitializeEntities(); await this._refreshState(); diff --git a/packages/insomnia-app/app/ui/components/modals/git-staging-modal.tsx b/packages/insomnia-app/app/ui/components/modals/git-staging-modal.tsx index 5b6778eab..3c8d8c4e2 100644 --- a/packages/insomnia-app/app/ui/components/modals/git-staging-modal.tsx +++ b/packages/insomnia-app/app/ui/components/modals/git-staging-modal.tsx @@ -4,6 +4,7 @@ import path from 'path'; import React, { Fragment, PureComponent } from 'react'; import YAML from 'yaml'; +import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics'; import { AUTOBIND_CFG } from '../../../common/constants'; import { database as db } from '../../../common/database'; import { strings } from '../../../common/strings'; @@ -96,6 +97,7 @@ export class GitStagingModal extends PureComponent { } await vcs.commit(message); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'commit')); this.modal?.hide(); if (typeof this.onCommit === 'function') { @@ -120,6 +122,7 @@ export class GitStagingModal extends PureComponent { newItems[p].staged = doStage || forceAdd; } + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', doStage ? 'stage_all' : 'unstage_all')); this.setState({ items: newItems, }); @@ -134,6 +137,7 @@ export class GitStagingModal extends PureComponent { } newItems[gitPath].staged = !newItems[gitPath].staged; + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', newItems[gitPath].staged ? 'stage' : 'unstage')); this.setState({ items: newItems, }); @@ -306,6 +310,16 @@ export class GitStagingModal extends PureComponent { await this._refresh(); } + async _handleRollbackSingle(item: Item) { + await this._handleRollback([item]); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'rollback')); + } + + async _handleRollbackAll(items: Item[]) { + await this._handleRollback(items); + trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'rollback_all')); + } + renderItem(item: Item) { const { path: gitPath, staged, editable } = item; const docName = this.statusNames[gitPath] || 'n/a'; @@ -328,7 +342,7 @@ export class GitStagingModal extends PureComponent { {item.editable && @@ -351,7 +365,7 @@ export class GitStagingModal extends PureComponent { {title} this._handleRollback(items)} + onClick={() => this._handleRollbackAll(items)} > {rollbackLabel} diff --git a/packages/insomnia-app/app/ui/redux/modules/__tests__/git.test.tsx b/packages/insomnia-app/app/ui/redux/modules/__tests__/git.test.tsx index a33cbadd5..d860e331c 100644 --- a/packages/insomnia-app/app/ui/redux/modules/__tests__/git.test.tsx +++ b/packages/insomnia-app/app/ui/redux/modules/__tests__/git.test.tsx @@ -425,7 +425,7 @@ describe('git', () => { const alertArgs = getAndClearShowAlertMockArgs(); expect(alertArgs.title).toBe('Setup Problem'); expect(alertArgs.message).toBe( - 'This repository already contains a workspace; create a fresh clone from the dashboard.', + 'This repository is already connected to Insomnia; try creating a clone from the dashboard instead.', ); // Ensure activity is activated expect(store.getActions()).toEqual([ diff --git a/packages/insomnia-app/app/ui/redux/modules/git.tsx b/packages/insomnia-app/app/ui/redux/modules/git.tsx index 3b4c29a58..805294bae 100644 --- a/packages/insomnia-app/app/ui/redux/modules/git.tsx +++ b/packages/insomnia-app/app/ui/redux/modules/git.tsx @@ -3,7 +3,7 @@ import path from 'path'; import React, { ReactNode } from 'react'; import YAML from 'yaml'; -import { trackEvent } from '../../../common/analytics'; +import { SegmentEvent, trackEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../../../common/analytics'; import { database as db } from '../../../common/database'; import { strings } from '../../../common/strings'; import * as models from '../../../models'; @@ -29,10 +29,12 @@ export type UpdateGitRepositoryCallback = (arg0: { gitRepository: GitRepository * */ export const updateGitRepository: UpdateGitRepositoryCallback = ({ gitRepository }) => { return () => { + trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'update')); showModal(GitRepositorySettingsModal, { gitRepository, onSubmitEdits: async gitRepoPatch => { await models.gitRepository.update(gitRepository, gitRepoPatch); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'update')); }, }); }; @@ -47,6 +49,7 @@ export type SetupGitRepositoryCallback = (arg0: { * */ export const setupGitRepository: SetupGitRepositoryCallback = ({ createFsClient, workspace }) => { return dispatch => { + trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'setup')); showModal(GitRepositorySettingsModal, { gitRepository: null, onSubmitEdits: async gitRepoPatch => { @@ -67,6 +70,7 @@ export const setupGitRepository: SetupGitRepositoryCallback = ({ createFsClient, message: err.message, error: err, }); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'setup', err.message)); return; } @@ -79,13 +83,17 @@ export const setupGitRepository: SetupGitRepositoryCallback = ({ createFsClient, showAlert({ title: 'Setup Problem', message: - 'This repository already contains a workspace; create a fresh clone from the dashboard.', + 'This repository is already connected to Insomnia; try creating a clone from the dashboard instead.', }); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'setup', 'existing insomnia data')); return; } } await createGitRepository(workspace._id, gitRepoPatch); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'setup')); + } catch (err) { + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'setup', err.message)); } finally { dispatch(loadStop()); } @@ -148,6 +156,8 @@ export const cloneGitRepository = ({ createFsClient }: { return (dispatch, getState: () => RootState) => { // TODO: in the future we should ask which project to clone into...? const activeProject = selectActiveProject(getState()); + + trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'clone')); showModal(GitRepositorySettingsModal, { gitRepository: null, onSubmitEdits: async repoSettingsPatch => { @@ -169,6 +179,7 @@ export const cloneGitRepository = ({ createFsClient }: { message: originalUriError.message, }); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone', originalUriError.message)); return; } @@ -188,6 +199,7 @@ export const cloneGitRepository = ({ createFsClient }: { message: `Failed to clone with original url (${repoSettingsPatch.uri}): ${originalUriError.message};\n\nAlso failed to clone with \`.git\` suffix added (${dotGitUri}): ${dotGitError.message}`, }); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone', dotGitError.message)); return; } } @@ -196,6 +208,7 @@ export const cloneGitRepository = ({ createFsClient }: { if (!(await containsInsomniaWorkspaceDir(fsClient))) { dispatch(noDocumentFound(repoSettingsPatch)); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone', 'no directory found')); return; } @@ -205,12 +218,14 @@ export const cloneGitRepository = ({ createFsClient }: { if (workspaces.length === 0) { dispatch(noDocumentFound(repoSettingsPatch)); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone', 'no workspaces found')); return; } if (workspaces.length > 1) { cloneProblem('Multiple workspaces found in repository; expected one.'); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found')); return; } @@ -229,6 +244,7 @@ export const cloneGitRepository = ({ createFsClient }: { , ); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone', 'workspace already exists')); return; } @@ -271,6 +287,7 @@ export const cloneGitRepository = ({ createFsClient }: { // Flush DB changes await db.flushChanges(bufferId); dispatch(loadStop()); + trackSegmentEvent(SegmentEvent.vcsSyncComplete, vcsSegmentEventProperties('git', 'clone')); }, }); },