Rework Continue On telemetry (#167124)

* Rework Continue On telemetry

* Fix tests

* Fix line endings

* More line endings
This commit is contained in:
Joyce Er 2022-11-23 20:10:27 -08:00 committed by GitHub
parent 00b1383034
commit 2eca6d38de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 81 deletions

View file

@ -10,7 +10,7 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc
import { Action2, IAction2Options, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { IEditSessionsStorageService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_DATA_VIEW_ID, decodeEditSessionFileContent } from 'vs/workbench/contrib/editSessions/common/editSessions';
import { IEditSessionsStorageService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_DATA_VIEW_ID, decodeEditSessionFileContent, hashedEditSessionId } from 'vs/workbench/contrib/editSessions/common/editSessions';
import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
@ -83,7 +83,7 @@ const showOutputChannelCommand: IAction2Options = {
const resumingProgressOptions = {
location: ProgressLocation.Window,
type: 'syncing',
title: `[${localize('resuming edit session window', 'Resuming edit session...')}](command:${showOutputChannelCommand.id})`
title: `[${localize('resuming working changes window', 'Resuming working changes...')}](command:${showOutputChannelCommand.id})`
};
const queryParamName = 'editSessionId';
@ -138,66 +138,54 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
this._register(this.editSessionsStorageService.onDidSignOut(() => this.updateAccountsMenuBadge()));
}
private autoResumeEditSession() {
void this.progressService.withProgress(resumingProgressOptions, async () => {
performance.mark('code/willResumeEditSessionFromIdentifier');
private async autoResumeEditSession() {
const shouldAutoResumeOnReload = this.configurationService.getValue('workbench.editSessions.autoResume') === 'onReload';
type ResumeEvent = {};
type ResumeClassification = {
owner: 'joyceerhl'; comment: 'Reporting when an action is resumed from an edit session identifier.';
if (this.environmentService.editSessionId !== undefined) {
this.logService.info(`Resuming cloud changes, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`);
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined));
} else if (shouldAutoResumeOnReload && this.editSessionsStorageService.isSignedIn) {
this.logService.info('Resuming cloud changes, reason: cloud changes enabled...');
// Attempt to resume edit session based on edit workspace identifier
// Note: at this point if the user is not signed into edit sessions,
// we don't want them to be prompted to sign in and should just return early
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(undefined, true));
} else if (shouldAutoResumeOnReload) {
// The application has previously launched via a protocol URL Continue On flow
const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false);
const handlePendingEditSessions = () => {
// display a badge in the accounts menu but do not prompt the user to sign in again
this.updateAccountsMenuBadge();
// attempt a resume if we are in a pending state and the user just signed in
const disposable = this.editSessionsStorageService.onDidSignIn(async () => {
disposable.dispose();
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(undefined, true));
this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION);
this.environmentService.continueOn = undefined;
});
};
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.continue.resume');
const shouldAutoResumeOnReload = this.configurationService.getValue('workbench.editSessions.autoResume') === 'onReload';
if (this.environmentService.editSessionId !== undefined) {
this.logService.info(`Resuming cloud changes, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`);
await this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined);
} else if (shouldAutoResumeOnReload && this.editSessionsStorageService.isSignedIn) {
this.logService.info('Resuming cloud changes, reason: cloud changes enabled...');
// Attempt to resume edit session based on edit workspace identifier
// Note: at this point if the user is not signed into edit sessions,
// we don't want them to be prompted to sign in and should just return early
await this.resumeEditSession(undefined, true);
} else if (shouldAutoResumeOnReload) {
// The application has previously launched via a protocol URL Continue On flow
const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false);
const handlePendingEditSessions = () => {
// display a badge in the accounts menu but do not prompt the user to sign in again
this.updateAccountsMenuBadge();
// attempt a resume if we are in a pending state and the user just signed in
const disposable = this.editSessionsStorageService.onDidSignIn(async () => {
disposable.dispose();
this.resumeEditSession(undefined, true);
this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION);
this.environmentService.continueOn = undefined;
});
};
if ((this.environmentService.continueOn !== undefined) &&
!this.editSessionsStorageService.isSignedIn &&
// and user has not yet been prompted to sign in on this machine
hasApplicationLaunchedFromContinueOnFlow === false
) {
this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
await this.editSessionsStorageService.initialize(true);
if (this.editSessionsStorageService.isSignedIn) {
await this.resumeEditSession(undefined, true);
} else {
handlePendingEditSessions();
}
// store the fact that we prompted the user
} else if (!this.editSessionsStorageService.isSignedIn &&
// and user has been prompted to sign in on this machine
hasApplicationLaunchedFromContinueOnFlow === true
) {
if ((this.environmentService.continueOn !== undefined) &&
!this.editSessionsStorageService.isSignedIn &&
// and user has not yet been prompted to sign in on this machine
hasApplicationLaunchedFromContinueOnFlow === false
) {
this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
await this.editSessionsStorageService.initialize(true);
if (this.editSessionsStorageService.isSignedIn) {
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(undefined, true));
} else {
handlePendingEditSessions();
}
// store the fact that we prompted the user
} else if (!this.editSessionsStorageService.isSignedIn &&
// and user has been prompted to sign in on this machine
hasApplicationLaunchedFromContinueOnFlow === true
) {
handlePendingEditSessions();
}
performance.mark('code/didResumeEditSessionFromIdentifier');
});
}
}
private updateAccountsMenuBadge() {
@ -292,17 +280,19 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
}
async run(accessor: ServicesAccessor, workspaceUri: URI | undefined, destination: string | undefined): Promise<void> {
type ContinueEditSessionEvent = {};
type ContinueEditSessionClassification = {
owner: 'joyceerhl'; comment: 'Reporting when the continue edit session action is run.';
type ContinueOnEventOutcome = { outcome: string; hashedId?: string };
type ContinueOnClassificationOutcome = {
owner: 'joyceerhl'; comment: 'Reporting the outcome of invoking the Continue On action.';
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of invoking continue edit session.' };
hashedId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The hash of the stored edit session id, for correlating success of stores and resumes.' };
};
that.telemetryService.publicLog2<ContinueEditSessionEvent, ContinueEditSessionClassification>('editSessions.continue.store');
// First ask the user to pick a destination, if necessary
let uri: URI | 'noDestinationUri' | undefined = workspaceUri;
if (!destination && !uri) {
destination = await that.pickContinueEditSessionDestination();
if (!destination) {
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.pick.outcome', { outcome: 'noSelection' });
return;
}
}
@ -313,16 +303,36 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
// Run the store action to get back a ref
let ref: string | undefined;
if (shouldStoreEditSession) {
type ContinueWithEditSessionEvent = {};
type ContinueWithEditSessionClassification = {
owner: 'joyceerhl'; comment: 'Reporting when storing an edit session as part of the Continue On flow.';
};
that.telemetryService.publicLog2<ContinueWithEditSessionEvent, ContinueWithEditSessionClassification>('continueOn.editSessions.store');
const cancellationTokenSource = new CancellationTokenSource();
ref = await that.progressService.withProgress({
location: ProgressLocation.Notification,
cancellable: true,
type: 'syncing',
title: localize('store your working changes', 'Storing your working changes...')
}, async () => that.storeEditSession(false, cancellationTokenSource.token), () => {
cancellationTokenSource.cancel();
cancellationTokenSource.dispose();
});
try {
ref = await that.progressService.withProgress({
location: ProgressLocation.Notification,
cancellable: true,
type: 'syncing',
title: localize('store your working changes', 'Storing your working changes...')
}, async () => {
const ref = await that.storeEditSession(false, cancellationTokenSource.token);
if (ref !== undefined) {
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeSucceeded', hashedId: hashedEditSessionId(ref) });
} else {
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeSkipped' });
}
return ref;
}, () => {
cancellationTokenSource.cancel();
cancellationTokenSource.dispose();
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeCancelledByUser' });
});
} catch (ex) {
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeFailed' });
throw ex;
}
}
// Append the ref to the URI
@ -364,15 +374,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
}
async run(accessor: ServicesAccessor, editSessionId?: string): Promise<void> {
await that.progressService.withProgress(resumingProgressOptions, async () => {
type ResumeEvent = {};
type ResumeClassification = {
owner: 'joyceerhl'; comment: 'Reporting when the resume edit session action is invoked.';
};
that.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume');
await that.resumeEditSession(editSessionId);
});
await that.progressService.withProgress(resumingProgressOptions, async () => await that.resumeEditSession(editSessionId));
}
}));
}
@ -423,6 +425,16 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
return;
}
type ResumeEvent = { outcome: string; hashedId?: string };
type ResumeClassification = {
owner: 'joyceerhl'; comment: 'Reporting when an edit session is resumed from an edit session identifier.';
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of resuming the edit session.' };
hashedId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The hash of the stored edit session id, for correlating success of stores and resumes.' };
};
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume');
performance.mark('code/willResumeEditSessionFromIdentifier');
const data = await this.editSessionsStorageService.read(ref);
if (!data) {
if (ref === undefined && !silent) {
@ -438,6 +450,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
if (editSession.version > EditSessionSchemaVersion) {
this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to resume your working changes from the cloud.", this.productService.nameLong));
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'clientUpdateNeeded' });
return;
}
@ -480,10 +493,14 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`);
await this.editSessionsStorageService.delete(ref);
this.logService.info(`Deleted edit session with ref ${ref}.`);
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'resumeSucceeded' });
} catch (ex) {
this.logService.error('Failed to resume edit session, reason: ', (ex as Error).toString());
this.notificationService.error(localize('resume failed', "Failed to resume your working changes from the cloud."));
}
performance.mark('code/didResumeEditSessionFromIdentifier');
}
private async generateChanges(editSession: EditSession, ref: string, force = false) {
@ -693,6 +710,12 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
}
private async shouldContinueOnWithEditSession(): Promise<boolean> {
type EditSessionsAuthCheckEvent = { outcome: string };
type EditSessionsAuthCheckClassification = {
owner: 'joyceerhl'; comment: 'Reporting whether we can and should store edit session as part of Continue On.';
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of checking whether we can store an edit session as part of the Continue On flow.' };
};
// If the user is already signed in, we should store edit session
if (this.editSessionsStorageService.isSignedIn) {
return this.hasEditSession();
@ -700,12 +723,17 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
// If the user has been asked before and said no, don't use edit sessions
if (this.configurationService.getValue(useEditSessionsWithContinueOn) === 'off') {
this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'disabledEditSessionsViaSetting' });
return false;
}
// Prompt the user to use edit sessions if they currently could benefit from using it
if (this.hasEditSession()) {
return this.editSessionsStorageService.initialize(true);
const initialized = await this.editSessionsStorageService.initialize(true);
if (!initialized) {
this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'didNotEnableEditSessionsWhenPrompted' });
}
return initialized;
}
return false;
@ -827,16 +855,33 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
}
private async resolveDestination(command: string): Promise<URI | 'noDestinationUri' | undefined> {
type EvaluateContinueOnDestinationEvent = { outcome: string; selection: string };
type EvaluateContinueOnDestinationClassification = {
owner: 'joyceerhl'; comment: 'Reporting the outcome of evaluating a selected Continue On destination option.';
selection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The selected Continue On destination option.' };
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of evaluating the selected Continue On destination option.' };
};
try {
const uri = await this.commandService.executeCommand(command);
// Some continue on commands do not return a URI
// to support extensions which want to be in control
// of how the destination is opened
if (uri === undefined) { return 'noDestinationUri'; }
if (uri === undefined) {
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'noDestinationUri' });
return 'noDestinationUri';
}
return URI.isUri(uri) ? uri : undefined;
if (URI.isUri(uri)) {
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'resolvedUri' });
return uri;
}
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'invalidDestination' });
return undefined;
} catch (ex) {
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'unknownError' });
return undefined;
}
}

View file

@ -13,6 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { StringSHA1 } from 'vs/base/common/hash';
export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = {
original: 'Cloud Changes',
@ -100,3 +101,9 @@ export function decodeEditSessionFileContent(version: number, content: string):
throw new Error('Upgrade to a newer version to decode this content.');
}
}
export function hashedEditSessionId(editSessionId: string) {
const sha1 = new StringSHA1();
sha1.update(editSessionId);
return sha1.digest();
}

View file

@ -38,6 +38,8 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IEditorService, ISaveAllEditorsOptions } from 'vs/workbench/services/editor/common/editorService';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
const folderName = 'test-folder';
const folderUri = URI.file(`/${folderName}`);
@ -75,6 +77,7 @@ suite('Edit session sync', () => {
instantiationService.stub(IProgressService, ProgressService);
instantiationService.stub(ISCMService, SCMService);
instantiationService.stub(IEnvironmentService, TestEnvironmentService);
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IDialogService, new class extends mock<IDialogService>() {
override async show() {
return { choice: 1 };