Skip to content

Commit d73dce1

Browse files
Expands Start Work telemetry
1 parent 58e8b36 commit d73dce1

File tree

2 files changed

+151
-62
lines changed

2 files changed

+151
-62
lines changed

src/constants.telemetry.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { IntegrationId, SupportedCloudIntegrationIds } from './constants.in
66
import type { SubscriptionState } from './constants.subscription';
77
import type { CustomEditorTypes, TreeViewTypes, WebviewTypes, WebviewViewTypes } from './constants.views';
88
import type { GitContributionTiers } from './git/models/contributor';
9+
import type { StartWorkType } from './plus/startWork/startWork';
910
import type { Period } from './plus/webviews/timeline/protocol';
1011
import type { Flatten } from './system/object';
1112
import type { WalkthroughContextKeys } from './telemetry/walkthroughStateProvider';
@@ -317,10 +318,26 @@ export type TelemetryEvents = {
317318
'startWork/opened': StartWorkEventData & {
318319
connected: boolean;
319320
};
321+
/** Sent when the user chooses an option to start work in the first step */
322+
'startWork/type/chosen': StartWorkEventData & {
323+
connected: boolean;
324+
type: StartWorkType;
325+
};
326+
/** Sent when the user chooses an issue to start work in the second step */
327+
'startWork/issue/chosen': StartWorkEventData & {
328+
connected: boolean;
329+
type: StartWorkType;
330+
} & Partial<Record<`item.${string}`, string | number | boolean>>;
320331
/** Sent when the Start Work has "reloaded" (while open, e.g. user refreshed or back button) and is disconnected; use `instance` to correlate a Start Work "session" */
332+
'startWork/steps/type': StartWorkEventData & {
333+
connected: boolean;
334+
};
321335
'startWork/steps/connect': StartWorkEventData & {
322336
connected: boolean;
323337
};
338+
'startWork/steps/issue': StartWorkEventData & {
339+
connected: boolean;
340+
};
324341

325342
/** Sent when a PR review was started in the inspect overview */
326343
openReviewMode: {
@@ -462,13 +479,13 @@ export type CommandEventData =
462479
webview?: string;
463480
};
464481

465-
export type StartWorkTelemetryContext = StartWorkEventDataBase;
482+
export type StartWorkTelemetryContext = StartWorkEventData;
466483

467484
type StartWorkEventDataBase = {
468485
instance: number;
469-
};
486+
} & Partial<{ type: StartWorkType }>;
470487

471-
type StartWorkEventData = StartWorkEventDataBase;
488+
type StartWorkEventData = StartWorkEventDataBase & Partial<{ 'items.count': number }>;
472489

473490
export type LaunchpadTelemetryContext = LaunchpadEventData;
474491

src/plus/startWork/startWork.ts

Lines changed: 131 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { md5 } from '@env/crypto';
12
import slug from 'slug';
23
import type { QuickPick } from 'vscode';
34
import { Uri } from 'vscode';
@@ -52,11 +53,12 @@ interface Context {
5253

5354
interface State {
5455
item?: StartWorkItem;
55-
action?: StartWorkAction;
56+
type?: StartWorkType;
5657
inWorktree?: boolean;
5758
}
5859

59-
export type StartWorkAction = 'start';
60+
export type StartWorkType = 'branch' | 'branch-worktree' | 'issue' | 'issue-worktree';
61+
type StartWorkTypeItem = { type: StartWorkType; inWorktree?: boolean };
6062

6163
export interface StartWorkCommandArgs {
6264
readonly command: 'startWork';
@@ -99,30 +101,57 @@ export class StartWorkCommand extends QuickCommand<State> {
99101
connectedIntegrations: await this.getConnectedIntegrations(),
100102
};
101103

102-
const opened = false;
104+
let opened = false;
103105
while (this.canStepsContinue(state)) {
106+
const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c);
104107
context.title = this.title;
105108

106109
if (state.counter < 1) {
107-
const result = yield* this.selectCommandStep(state);
110+
if (this.container.telemetry.enabled) {
111+
this.container.telemetry.sendEvent(
112+
opened ? 'startWork/steps/type' : 'startWork/opened',
113+
{
114+
...context.telemetryContext!,
115+
connected: hasConnectedIntegrations,
116+
},
117+
this.source,
118+
);
119+
}
120+
121+
opened = true;
122+
const result = yield* this.selectTypeStep(state);
108123
if (result === StepResultBreak) continue;
109-
state.action = result.action;
124+
state.type = result.type;
110125
state.inWorktree = result.inWorktree;
126+
if (this.container.telemetry.enabled) {
127+
this.container.telemetry.sendEvent(
128+
'startWork/type/chosen',
129+
{
130+
...context.telemetryContext!,
131+
connected: hasConnectedIntegrations,
132+
type: state.type,
133+
},
134+
this.source,
135+
);
136+
}
111137
}
112138

113-
if (state.counter < 2 && !state.action) {
114-
const hasConnectedIntegrations = [...context.connectedIntegrations.values()].some(c => c);
139+
if ((state.counter < 2 && state.type === 'issue') || state.type === 'issue-worktree') {
115140
if (!hasConnectedIntegrations) {
116141
if (this.container.telemetry.enabled) {
117142
this.container.telemetry.sendEvent(
118143
opened ? 'startWork/steps/connect' : 'startWork/opened',
119144
{
120145
...context.telemetryContext!,
121146
connected: false,
147+
type: state.type,
122148
},
123149
this.source,
124150
);
125151
}
152+
153+
opened = true;
154+
126155
const isUsingCloudIntegrations = configuration.get('cloudIntegrations.enabled', undefined, false);
127156
const result = isUsingCloudIntegrations
128157
? yield* this.confirmCloudIntegrationsConnectStep(state, context)
@@ -133,67 +162,99 @@ export class StartWorkCommand extends QuickCommand<State> {
133162
}
134163

135164
await updateContextItems(this.container, context);
165+
if (this.container.telemetry.enabled) {
166+
this.container.telemetry.sendEvent(
167+
opened ? 'startWork/steps/issue' : 'startWork/opened',
168+
{
169+
...context.telemetryContext!,
170+
connected: true,
171+
type: state.type,
172+
},
173+
this.source,
174+
);
175+
}
176+
177+
opened = true;
178+
136179
const result = yield* this.pickIssueStep(state, context);
137180
if (result === StepResultBreak) continue;
138-
if (typeof result !== 'string') {
181+
if (!isStartWorkTypeItem(result)) {
139182
state.item = result;
140-
state.action = 'start';
183+
if (this.container.telemetry.enabled) {
184+
this.container.telemetry.sendEvent(
185+
'startWork/issue/chosen',
186+
{
187+
...context.telemetryContext!,
188+
connected: true,
189+
type: state.type,
190+
'item.id': getStartWorkItemIdHash(result),
191+
'item.type': result.item.issue.type,
192+
'item.provider': result.item.issue.provider.id,
193+
'item.assignees.count': result.item.issue.assignees?.length ?? undefined,
194+
'item.createdDate': result.item.issue.createdDate.getTime(),
195+
'item.updatedDate': result.item.issue.updatedDate.getTime(),
196+
197+
'item.comments.count': result.item.issue.commentsCount ?? undefined,
198+
'item.upvotes.count': result.item.issue.thumbsUpCount ?? undefined,
199+
200+
'item.issue.state': result.item.issue.state,
201+
},
202+
this.source,
203+
);
204+
}
141205
} else {
142-
state.action = result;
206+
state.type = result.type;
207+
state.inWorktree = result.inWorktree;
143208
}
144209
}
145210

146211
const issue = state.item?.item?.issue;
147212
const repo = issue && (await this.getIssueRepositoryIfExists(issue));
148213

149-
if (typeof state.action === 'string') {
150-
switch (state.action) {
151-
case 'start': {
152-
const result = yield* getSteps(
153-
this.container,
154-
{
155-
command: 'branch',
156-
state: {
157-
subcommand: 'create',
158-
repo: repo,
159-
name: issue
160-
? `${slug(issue.id, { lower: false })}-${slug(issue.title)}`
161-
: undefined,
162-
suggestNameOnly: true,
163-
suggestRepoOnly: true,
164-
flags: state.inWorktree ? ['--worktree'] : ['--switch'],
165-
},
166-
confirm: false,
167-
},
168-
this.pickedVia,
169-
);
170-
if (result === StepResultBreak) {
171-
endSteps(state);
172-
} else {
173-
state.counter--;
174-
state.action = undefined;
175-
}
176-
break;
177-
}
178-
}
214+
const result = yield* getSteps(
215+
this.container,
216+
{
217+
command: 'branch',
218+
state: {
219+
subcommand: 'create',
220+
repo: repo,
221+
name: issue ? `${slug(issue.id, { lower: false })}-${slug(issue.title)}` : undefined,
222+
suggestNameOnly: true,
223+
suggestRepoOnly: true,
224+
flags: state.inWorktree ? ['--worktree'] : ['--switch'],
225+
},
226+
confirm: false,
227+
},
228+
this.pickedVia,
229+
);
230+
if (result === StepResultBreak) {
231+
endSteps(state);
232+
} else {
233+
state.counter--;
179234
}
180235
}
181236

182237
return state.counter < 0 ? StepResultBreak : undefined;
183238
}
184239

185-
private *selectCommandStep(
240+
private *selectTypeStep(
186241
state: StepState<State>,
187-
): StepResultGenerator<{ action?: StartWorkAction; inWorktree?: boolean }> {
242+
): StepResultGenerator<{ type: StartWorkType; inWorktree?: boolean }> {
188243
const step = createPickStep({
189244
placeholder: 'Start work by creating a new branch',
190245
items: [
191-
createQuickPickItemOfT('Create a Branch', {
192-
action: 'start',
246+
createQuickPickItemOfT<StartWorkTypeItem>('Create a Branch', {
247+
type: 'branch',
248+
}),
249+
createQuickPickItemOfT<StartWorkTypeItem>('Create a Branch in a Worktree', {
250+
type: 'branch-worktree',
251+
inWorktree: true,
252+
}),
253+
createQuickPickItemOfT<StartWorkTypeItem>('Create a Branch from an Issue', { type: 'issue' }),
254+
createQuickPickItemOfT<StartWorkTypeItem>('Create a Branch from an Issue in a Worktree', {
255+
type: 'issue-worktree',
256+
inWorktree: true,
193257
}),
194-
createQuickPickItemOfT('Create a Branch in a Worktree', { action: 'start', inWorktree: true }),
195-
createQuickPickItemOfT('Create a Branch from an Issue', {}),
196-
createQuickPickItemOfT('Create a Branch from an Issue in a Worktree', { inWorktree: true }),
197258
],
198259
});
199260
const selection: StepSelection<typeof step> = yield step;
@@ -336,7 +397,7 @@ export class StartWorkCommand extends QuickCommand<State> {
336397
private *pickIssueStep(
337398
state: StepState<State>,
338399
context: Context,
339-
): StepResultGenerator<StartWorkItem | StartWorkAction> {
400+
): StepResultGenerator<StartWorkItem | StartWorkTypeItem> {
340401
const buildIssueItem = (i: StartWorkItem) => {
341402
const buttons = i.item.issue.url ? [OpenOnGitHubQuickInputButton] : [];
342403
return {
@@ -366,15 +427,15 @@ export class StartWorkCommand extends QuickCommand<State> {
366427

367428
function getItemsAndPlaceholder(): {
368429
placeholder: string;
369-
items: QuickPickItemOfT<StartWorkItem | StartWorkAction>[];
430+
items: QuickPickItemOfT<StartWorkItem | StartWorkTypeItem>[];
370431
} {
371432
if (!context.result.items.length) {
372433
return {
373434
placeholder: 'No issues found. Start work anyway.',
374435
items: [
375-
createQuickPickItemOfT(
436+
createQuickPickItemOfT<StartWorkTypeItem>(
376437
state.inWorktree ? 'Create a branch on a worktree' : 'Create a branch',
377-
'start',
438+
{ type: state.inWorktree ? 'branch-worktree' : 'branch', inWorktree: state.inWorktree },
378439
),
379440
],
380441
};
@@ -395,7 +456,7 @@ export class StartWorkCommand extends QuickCommand<State> {
395456
matchOnDetail: true,
396457
items: items,
397458
onDidClickItemButton: (_quickpick, button, { item }) => {
398-
if (button === OpenOnGitHubQuickInputButton && typeof item !== 'string') {
459+
if (button === OpenOnGitHubQuickInputButton && !isStartWorkTypeItem(item)) {
399460
this.open(item);
400461
return true;
401462
}
@@ -411,13 +472,6 @@ export class StartWorkCommand extends QuickCommand<State> {
411472
return typeof element.item === 'string' ? element.item : { ...element.item };
412473
}
413474

414-
private startWork(state: PartialStepState<State>, item?: StartWorkItem) {
415-
state.action = 'start';
416-
if (item != null) {
417-
state.item = item;
418-
}
419-
}
420-
421475
private open(item: StartWorkItem): void {
422476
if (item.item.issue.url == null) return;
423477
void openUrl(item.item.issue.url);
@@ -451,4 +505,22 @@ async function updateContextItems(container: Container, context: Context) {
451505
}),
452506
) ?? [],
453507
};
508+
if (container.telemetry.enabled) {
509+
updateTelemetryContext(context);
510+
}
511+
}
512+
513+
function updateTelemetryContext(context: Context) {
514+
context.telemetryContext = {
515+
...context.telemetryContext!,
516+
'items.count': context.result.items.length,
517+
};
518+
}
519+
520+
function isStartWorkTypeItem(item: unknown): item is StartWorkTypeItem {
521+
return item != null && typeof item === 'object' && 'type' in item;
522+
}
523+
524+
export function getStartWorkItemIdHash(item: StartWorkItem) {
525+
return md5(item.item.issue.id);
454526
}

0 commit comments

Comments
 (0)