Skip to content

Commit 2eca6d3

Browse files
authored
Rework Continue On telemetry (microsoft#167124)
* Rework Continue On telemetry * Fix tests * Fix line endings * More line endings
1 parent 00b1383 commit 2eca6d3

File tree

3 files changed

+137
-82
lines changed

3 files changed

+137
-82
lines changed

src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts

Lines changed: 127 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc
1010
import { Action2, IAction2Options, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
1111
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
1212
import { localize } from 'vs/nls';
13-
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';
13+
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';
1414
import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm';
1515
import { IFileService } from 'vs/platform/files/common/files';
1616
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
@@ -83,7 +83,7 @@ const showOutputChannelCommand: IAction2Options = {
8383
const resumingProgressOptions = {
8484
location: ProgressLocation.Window,
8585
type: 'syncing',
86-
title: `[${localize('resuming edit session window', 'Resuming edit session...')}](command:${showOutputChannelCommand.id})`
86+
title: `[${localize('resuming working changes window', 'Resuming working changes...')}](command:${showOutputChannelCommand.id})`
8787
};
8888
const queryParamName = 'editSessionId';
8989

@@ -138,66 +138,54 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
138138
this._register(this.editSessionsStorageService.onDidSignOut(() => this.updateAccountsMenuBadge()));
139139
}
140140

141-
private autoResumeEditSession() {
142-
void this.progressService.withProgress(resumingProgressOptions, async () => {
143-
performance.mark('code/willResumeEditSessionFromIdentifier');
144-
145-
type ResumeEvent = {};
146-
type ResumeClassification = {
147-
owner: 'joyceerhl'; comment: 'Reporting when an action is resumed from an edit session identifier.';
141+
private async autoResumeEditSession() {
142+
const shouldAutoResumeOnReload = this.configurationService.getValue('workbench.editSessions.autoResume') === 'onReload';
143+
144+
if (this.environmentService.editSessionId !== undefined) {
145+
this.logService.info(`Resuming cloud changes, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`);
146+
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined));
147+
} else if (shouldAutoResumeOnReload && this.editSessionsStorageService.isSignedIn) {
148+
this.logService.info('Resuming cloud changes, reason: cloud changes enabled...');
149+
// Attempt to resume edit session based on edit workspace identifier
150+
// Note: at this point if the user is not signed into edit sessions,
151+
// we don't want them to be prompted to sign in and should just return early
152+
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(undefined, true));
153+
} else if (shouldAutoResumeOnReload) {
154+
// The application has previously launched via a protocol URL Continue On flow
155+
const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false);
156+
157+
const handlePendingEditSessions = () => {
158+
// display a badge in the accounts menu but do not prompt the user to sign in again
159+
this.updateAccountsMenuBadge();
160+
// attempt a resume if we are in a pending state and the user just signed in
161+
const disposable = this.editSessionsStorageService.onDidSignIn(async () => {
162+
disposable.dispose();
163+
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(undefined, true));
164+
this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION);
165+
this.environmentService.continueOn = undefined;
166+
});
148167
};
149-
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.continue.resume');
150-
151-
const shouldAutoResumeOnReload = this.configurationService.getValue('workbench.editSessions.autoResume') === 'onReload';
152-
153-
if (this.environmentService.editSessionId !== undefined) {
154-
this.logService.info(`Resuming cloud changes, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`);
155-
await this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined);
156-
} else if (shouldAutoResumeOnReload && this.editSessionsStorageService.isSignedIn) {
157-
this.logService.info('Resuming cloud changes, reason: cloud changes enabled...');
158-
// Attempt to resume edit session based on edit workspace identifier
159-
// Note: at this point if the user is not signed into edit sessions,
160-
// we don't want them to be prompted to sign in and should just return early
161-
await this.resumeEditSession(undefined, true);
162-
} else if (shouldAutoResumeOnReload) {
163-
// The application has previously launched via a protocol URL Continue On flow
164-
const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false);
165-
166-
const handlePendingEditSessions = () => {
167-
// display a badge in the accounts menu but do not prompt the user to sign in again
168-
this.updateAccountsMenuBadge();
169-
// attempt a resume if we are in a pending state and the user just signed in
170-
const disposable = this.editSessionsStorageService.onDidSignIn(async () => {
171-
disposable.dispose();
172-
this.resumeEditSession(undefined, true);
173-
this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION);
174-
this.environmentService.continueOn = undefined;
175-
});
176-
};
177168

178-
if ((this.environmentService.continueOn !== undefined) &&
179-
!this.editSessionsStorageService.isSignedIn &&
180-
// and user has not yet been prompted to sign in on this machine
181-
hasApplicationLaunchedFromContinueOnFlow === false
182-
) {
183-
this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
184-
await this.editSessionsStorageService.initialize(true);
185-
if (this.editSessionsStorageService.isSignedIn) {
186-
await this.resumeEditSession(undefined, true);
187-
} else {
188-
handlePendingEditSessions();
189-
}
190-
// store the fact that we prompted the user
191-
} else if (!this.editSessionsStorageService.isSignedIn &&
192-
// and user has been prompted to sign in on this machine
193-
hasApplicationLaunchedFromContinueOnFlow === true
194-
) {
169+
if ((this.environmentService.continueOn !== undefined) &&
170+
!this.editSessionsStorageService.isSignedIn &&
171+
// and user has not yet been prompted to sign in on this machine
172+
hasApplicationLaunchedFromContinueOnFlow === false
173+
) {
174+
this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
175+
await this.editSessionsStorageService.initialize(true);
176+
if (this.editSessionsStorageService.isSignedIn) {
177+
await this.progressService.withProgress(resumingProgressOptions, async () => await this.resumeEditSession(undefined, true));
178+
} else {
195179
handlePendingEditSessions();
196180
}
181+
// store the fact that we prompted the user
182+
} else if (!this.editSessionsStorageService.isSignedIn &&
183+
// and user has been prompted to sign in on this machine
184+
hasApplicationLaunchedFromContinueOnFlow === true
185+
) {
186+
handlePendingEditSessions();
197187
}
198-
199-
performance.mark('code/didResumeEditSessionFromIdentifier');
200-
});
188+
}
201189
}
202190

203191
private updateAccountsMenuBadge() {
@@ -292,17 +280,19 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
292280
}
293281

294282
async run(accessor: ServicesAccessor, workspaceUri: URI | undefined, destination: string | undefined): Promise<void> {
295-
type ContinueEditSessionEvent = {};
296-
type ContinueEditSessionClassification = {
297-
owner: 'joyceerhl'; comment: 'Reporting when the continue edit session action is run.';
283+
type ContinueOnEventOutcome = { outcome: string; hashedId?: string };
284+
type ContinueOnClassificationOutcome = {
285+
owner: 'joyceerhl'; comment: 'Reporting the outcome of invoking the Continue On action.';
286+
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of invoking continue edit session.' };
287+
hashedId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The hash of the stored edit session id, for correlating success of stores and resumes.' };
298288
};
299-
that.telemetryService.publicLog2<ContinueEditSessionEvent, ContinueEditSessionClassification>('editSessions.continue.store');
300289

301290
// First ask the user to pick a destination, if necessary
302291
let uri: URI | 'noDestinationUri' | undefined = workspaceUri;
303292
if (!destination && !uri) {
304293
destination = await that.pickContinueEditSessionDestination();
305294
if (!destination) {
295+
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.pick.outcome', { outcome: 'noSelection' });
306296
return;
307297
}
308298
}
@@ -313,16 +303,36 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
313303
// Run the store action to get back a ref
314304
let ref: string | undefined;
315305
if (shouldStoreEditSession) {
306+
type ContinueWithEditSessionEvent = {};
307+
type ContinueWithEditSessionClassification = {
308+
owner: 'joyceerhl'; comment: 'Reporting when storing an edit session as part of the Continue On flow.';
309+
};
310+
that.telemetryService.publicLog2<ContinueWithEditSessionEvent, ContinueWithEditSessionClassification>('continueOn.editSessions.store');
311+
316312
const cancellationTokenSource = new CancellationTokenSource();
317-
ref = await that.progressService.withProgress({
318-
location: ProgressLocation.Notification,
319-
cancellable: true,
320-
type: 'syncing',
321-
title: localize('store your working changes', 'Storing your working changes...')
322-
}, async () => that.storeEditSession(false, cancellationTokenSource.token), () => {
323-
cancellationTokenSource.cancel();
324-
cancellationTokenSource.dispose();
325-
});
313+
try {
314+
ref = await that.progressService.withProgress({
315+
location: ProgressLocation.Notification,
316+
cancellable: true,
317+
type: 'syncing',
318+
title: localize('store your working changes', 'Storing your working changes...')
319+
}, async () => {
320+
const ref = await that.storeEditSession(false, cancellationTokenSource.token);
321+
if (ref !== undefined) {
322+
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeSucceeded', hashedId: hashedEditSessionId(ref) });
323+
} else {
324+
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeSkipped' });
325+
}
326+
return ref;
327+
}, () => {
328+
cancellationTokenSource.cancel();
329+
cancellationTokenSource.dispose();
330+
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeCancelledByUser' });
331+
});
332+
} catch (ex) {
333+
that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeFailed' });
334+
throw ex;
335+
}
326336
}
327337

328338
// Append the ref to the URI
@@ -364,15 +374,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
364374
}
365375

366376
async run(accessor: ServicesAccessor, editSessionId?: string): Promise<void> {
367-
await that.progressService.withProgress(resumingProgressOptions, async () => {
368-
type ResumeEvent = {};
369-
type ResumeClassification = {
370-
owner: 'joyceerhl'; comment: 'Reporting when the resume edit session action is invoked.';
371-
};
372-
that.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume');
373-
374-
await that.resumeEditSession(editSessionId);
375-
});
377+
await that.progressService.withProgress(resumingProgressOptions, async () => await that.resumeEditSession(editSessionId));
376378
}
377379
}));
378380
}
@@ -423,6 +425,16 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
423425
return;
424426
}
425427

428+
type ResumeEvent = { outcome: string; hashedId?: string };
429+
type ResumeClassification = {
430+
owner: 'joyceerhl'; comment: 'Reporting when an edit session is resumed from an edit session identifier.';
431+
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of resuming the edit session.' };
432+
hashedId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The hash of the stored edit session id, for correlating success of stores and resumes.' };
433+
};
434+
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume');
435+
436+
performance.mark('code/willResumeEditSessionFromIdentifier');
437+
426438
const data = await this.editSessionsStorageService.read(ref);
427439
if (!data) {
428440
if (ref === undefined && !silent) {
@@ -438,6 +450,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
438450

439451
if (editSession.version > EditSessionSchemaVersion) {
440452
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));
453+
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'clientUpdateNeeded' });
441454
return;
442455
}
443456

@@ -480,10 +493,14 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
480493
this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`);
481494
await this.editSessionsStorageService.delete(ref);
482495
this.logService.info(`Deleted edit session with ref ${ref}.`);
496+
497+
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'resumeSucceeded' });
483498
} catch (ex) {
484499
this.logService.error('Failed to resume edit session, reason: ', (ex as Error).toString());
485500
this.notificationService.error(localize('resume failed', "Failed to resume your working changes from the cloud."));
486501
}
502+
503+
performance.mark('code/didResumeEditSessionFromIdentifier');
487504
}
488505

489506
private async generateChanges(editSession: EditSession, ref: string, force = false) {
@@ -693,19 +710,30 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
693710
}
694711

695712
private async shouldContinueOnWithEditSession(): Promise<boolean> {
713+
type EditSessionsAuthCheckEvent = { outcome: string };
714+
type EditSessionsAuthCheckClassification = {
715+
owner: 'joyceerhl'; comment: 'Reporting whether we can and should store edit session as part of Continue On.';
716+
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.' };
717+
};
718+
696719
// If the user is already signed in, we should store edit session
697720
if (this.editSessionsStorageService.isSignedIn) {
698721
return this.hasEditSession();
699722
}
700723

701724
// If the user has been asked before and said no, don't use edit sessions
702725
if (this.configurationService.getValue(useEditSessionsWithContinueOn) === 'off') {
726+
this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'disabledEditSessionsViaSetting' });
703727
return false;
704728
}
705729

706730
// Prompt the user to use edit sessions if they currently could benefit from using it
707731
if (this.hasEditSession()) {
708-
return this.editSessionsStorageService.initialize(true);
732+
const initialized = await this.editSessionsStorageService.initialize(true);
733+
if (!initialized) {
734+
this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'didNotEnableEditSessionsWhenPrompted' });
735+
}
736+
return initialized;
709737
}
710738

711739
return false;
@@ -827,16 +855,33 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
827855
}
828856

829857
private async resolveDestination(command: string): Promise<URI | 'noDestinationUri' | undefined> {
858+
type EvaluateContinueOnDestinationEvent = { outcome: string; selection: string };
859+
type EvaluateContinueOnDestinationClassification = {
860+
owner: 'joyceerhl'; comment: 'Reporting the outcome of evaluating a selected Continue On destination option.';
861+
selection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The selected Continue On destination option.' };
862+
outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The outcome of evaluating the selected Continue On destination option.' };
863+
};
864+
830865
try {
831866
const uri = await this.commandService.executeCommand(command);
832867

833868
// Some continue on commands do not return a URI
834869
// to support extensions which want to be in control
835870
// of how the destination is opened
836-
if (uri === undefined) { return 'noDestinationUri'; }
871+
if (uri === undefined) {
872+
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'noDestinationUri' });
873+
return 'noDestinationUri';
874+
}
837875

838-
return URI.isUri(uri) ? uri : undefined;
876+
if (URI.isUri(uri)) {
877+
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'resolvedUri' });
878+
return uri;
879+
}
880+
881+
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'invalidDestination' });
882+
return undefined;
839883
} catch (ex) {
884+
this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'unknownError' });
840885
return undefined;
841886
}
842887
}

src/vs/workbench/contrib/editSessions/common/editSessions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log';
1313
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
1414
import { IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
1515
import { Event } from 'vs/base/common/event';
16+
import { StringSHA1 } from 'vs/base/common/hash';
1617

1718
export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = {
1819
original: 'Cloud Changes',
@@ -100,3 +101,9 @@ export function decodeEditSessionFileContent(version: number, content: string):
100101
throw new Error('Upgrade to a newer version to decode this content.');
101102
}
102103
}
104+
105+
export function hashedEditSessionId(editSessionId: string) {
106+
const sha1 = new StringSHA1();
107+
sha1.update(editSessionId);
108+
return sha1.digest();
109+
}

0 commit comments

Comments
 (0)