Git Sync Events (#4365)

* Cleanup old plugin events

* Add VCS lifecyle events

* Git action events

* Apply Opender's Suggestions

Co-authored-by: Opender Singh <opender.singh@konghq.com>

* Apply review changes for stage/unstage & rollback

* Update error messages for clone events

Co-authored-by: Opender Singh <opender.singh@konghq.com>
This commit is contained in:
Wils Dawson 2022-01-19 13:33:21 -08:00 committed by GitHub
parent 1ac24bf980
commit 35c451b803
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 13 deletions

View File

@ -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<string, any>) {
@ -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,

View File

@ -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();
}
};

View File

@ -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<Props, State> {
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<Props, State> {
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<Props, State> {
title: 'Error Pushing Repository',
error: err,
});
trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', force ? 'force_push' : 'push', err.message));
}
}

View File

@ -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<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
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();

View File

@ -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<Props, State> {
}
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<Props, State> {
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<Props, State> {
}
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<Props, State> {
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<Props, State> {
{item.editable && <Tooltip message={item.added ? 'Delete' : 'Rollback'}>
<button
className="btn btn--micro space-right"
onClick={() => this._handleRollback([item])}
onClick={() => this._handleRollbackSingle(item)}
>
<i className={classnames('fa', item.added ? 'fa-trash' : 'fa-undo')} />
</button>
@ -351,7 +365,7 @@ export class GitStagingModal extends PureComponent<Props, State> {
<strong>{title}</strong>
<PromptButton
className="btn pull-right btn--micro"
onClick={() => this._handleRollback(items)}
onClick={() => this._handleRollbackAll(items)}
>
{rollbackLabel}
</PromptButton>

View File

@ -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([

View File

@ -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'));
},
});
},