Skip to content

Commit e82d377

Browse files
FrankLiu4138claude
andcommitted
fix(upgrade): check extension version and add comprehensive telemetry
- Check appmod extension version (>= 1.15.0) before calling gotoAgentMode, fixing 67% of upgrade failures caused by outdated extensions - Add extensionState (up-to-date/outdated/not-installed) and issueType (cve/upgrade) dimensions to all telemetry events for funnel analysis - Distinguish notification button text and message body based on extension state (Upgrade Now / Update Extension and Upgrade / Install Extension and Upgrade) - Track extension install/update success rate and reload prompt engagement - Extract pure functions (getExtensionState, buildNotificationContent) for unit testability - Centralize telemetry operation names in telemetryConstants.ts Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
1 parent e9ae840 commit e82d377

6 files changed

Lines changed: 313 additions & 39 deletions

File tree

src/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ export namespace ExtensionName {
3636
export const APP_MODERNIZATION_FOR_JAVA = "vscjava.migrate-java-to-azure";
3737
// Java upgrade extension is merged into app modernization extension
3838
export const APP_MODERNIZATION_UPGRADE_FOR_JAVA = APP_MODERNIZATION_FOR_JAVA;
39-
export const APP_MODERNIZATION_EXTENSION_NAME = "GitHub Copilot app modernization";
39+
export const APP_MODERNIZATION_EXTENSION_NAME = "GitHub Copilot modernization";
4040
}
4141

4242
export namespace Upgrade {
4343
export const PACKAGE_ID_FOR_JAVA_RUNTIME = "java:*";
44+
/** Minimum version of the appmod extension that supports gotoAgentMode command */
45+
export const MIN_APPMOD_VERSION = "1.15.0";
4446
}
4547

4648
/**

src/upgrade/display/notificationManager.ts

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// Licensed under the MIT license.
33

44
import { commands, ExtensionContext, extensions, window } from "vscode";
5+
import * as semver from "semver";
56
import { UpgradeReason, type IUpgradeIssuesRenderer, type UpgradeIssue } from "../type";
6-
import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage } from "../utility";
7+
import { buildCVENotificationMessage, buildFixPrompt, buildNotificationMessage, type ExtensionState } from "../utility";
78
import { Commands } from "../../commands";
89
import { Settings } from "../../settings";
910
import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
10-
import { ExtensionName } from "../../constants";
11+
import { ExtensionName, Upgrade } from "../../constants";
1112
import { CveUpgradeIssue } from "../cve";
13+
import { UpgradeTelemetry } from "../telemetryConstants";
1214

1315
const KEY_PREFIX = 'javaupgrade.notificationManager';
1416
const NEXT_SHOW_TS_KEY = `${KEY_PREFIX}.nextShowTs`;
@@ -17,6 +19,8 @@ const BUTTON_TEXT_UPGRADE = "Upgrade Now";
1719
const BUTTON_TEXT_FIX_CVE = "Fix Now";
1820
const BUTTON_TEXT_INSTALL_AND_UPGRADE = "Install Extension and Upgrade";
1921
const BUTTON_TEXT_INSTALL_AND_FIX_CVE = "Install Extension and Fix";
22+
const BUTTON_TEXT_UPDATE_AND_UPGRADE = "Update Extension and Upgrade";
23+
const BUTTON_TEXT_UPDATE_AND_FIX_CVE = "Update Extension and Fix";
2024
const BUTTON_TEXT_NOT_NOW = "Not Now";
2125

2226
const SECONDS_IN_A_DAY = 24 * 60 * 60;
@@ -26,6 +30,61 @@ function getNowTs() {
2630
return Number(new Date()) / 1000;
2731
}
2832

33+
export type { ExtensionState } from "../utility";
34+
35+
export interface NotificationContent {
36+
message: string;
37+
upgradeButtonText: string;
38+
fixCVEButtonText: string;
39+
}
40+
41+
export function getExtensionState(extensionVersion: string | undefined): ExtensionState {
42+
if (!extensionVersion) {
43+
return "not-installed";
44+
}
45+
if (semver.gte(extensionVersion, Upgrade.MIN_APPMOD_VERSION)) {
46+
return "up-to-date";
47+
}
48+
return "outdated";
49+
}
50+
51+
export function buildNotificationContent(
52+
issues: UpgradeIssue[],
53+
extensionState: ExtensionState,
54+
): NotificationContent {
55+
const cveIssues = issues.filter(
56+
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
57+
);
58+
const nonCVEIssues = issues.filter(
59+
(i) => i.reason !== UpgradeReason.CVE
60+
);
61+
const hasCVEIssue = cveIssues.length > 0;
62+
63+
const message = hasCVEIssue
64+
? buildCVENotificationMessage(cveIssues, extensionState)
65+
: buildNotificationMessage(nonCVEIssues[0], extensionState);
66+
67+
let upgradeButtonText: string;
68+
let fixCVEButtonText: string;
69+
70+
switch (extensionState) {
71+
case "up-to-date":
72+
upgradeButtonText = BUTTON_TEXT_UPGRADE;
73+
fixCVEButtonText = BUTTON_TEXT_FIX_CVE;
74+
break;
75+
case "outdated":
76+
upgradeButtonText = BUTTON_TEXT_UPDATE_AND_UPGRADE;
77+
fixCVEButtonText = BUTTON_TEXT_UPDATE_AND_FIX_CVE;
78+
break;
79+
case "not-installed":
80+
upgradeButtonText = BUTTON_TEXT_INSTALL_AND_UPGRADE;
81+
fixCVEButtonText = BUTTON_TEXT_INSTALL_AND_FIX_CVE;
82+
break;
83+
}
84+
85+
return { message, upgradeButtonText, fixCVEButtonText };
86+
}
87+
2988
class NotificationManager implements IUpgradeIssuesRenderer {
3089
private hasShown = false;
3190
private context?: ExtensionContext;
@@ -42,16 +101,6 @@ class NotificationManager implements IUpgradeIssuesRenderer {
42101
return;
43102
}
44103

45-
// Filter to only CVE issues and cast to CveUpgradeIssue[]
46-
const cveIssues = issues.filter(
47-
(i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE
48-
);
49-
const nonCVEIssues = issues.filter(
50-
(i) => i.reason !== UpgradeReason.CVE
51-
);
52-
const hasCVEIssue = cveIssues.length > 0;
53-
const issue = hasCVEIssue ? cveIssues[0] : nonCVEIssues[0];
54-
55104
if (!this.shouldShow()) {
56105
return;
57106
}
@@ -61,39 +110,42 @@ class NotificationManager implements IUpgradeIssuesRenderer {
61110
}
62111
this.hasShown = true;
63112

64-
const hasExtension = !!extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
65-
const prompt = buildFixPrompt(issue);
113+
const ext = extensions.getExtension(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
114+
const extensionState = getExtensionState(ext?.packageJSON?.version);
115+
const { message, upgradeButtonText, fixCVEButtonText } = buildNotificationContent(issues, extensionState);
66116

67-
let notificationMessage = "";
117+
const hasCVEIssue = issues.some(i => i.reason === UpgradeReason.CVE);
118+
const issueType = hasCVEIssue ? "cve" : "upgrade";
119+
const issue = hasCVEIssue
120+
? issues.find((i): i is CveUpgradeIssue => i.reason === UpgradeReason.CVE)!
121+
: issues.find(i => i.reason !== UpgradeReason.CVE)!;
122+
const prompt = buildFixPrompt(issue);
68123

69-
if (hasCVEIssue) {
70-
notificationMessage = buildCVENotificationMessage(cveIssues, hasExtension);
71-
} else {
72-
notificationMessage = buildNotificationMessage(issue, hasExtension);
73-
}
74-
const upgradeButtonText = hasExtension ? BUTTON_TEXT_UPGRADE : BUTTON_TEXT_INSTALL_AND_UPGRADE;
75-
const fixCVEButtonText = hasExtension ? BUTTON_TEXT_FIX_CVE : BUTTON_TEXT_INSTALL_AND_FIX_CVE;
76124
sendInfo(operationId, {
77-
operationName: "java.dependency.upgradeNotification.show",
125+
operationName: UpgradeTelemetry.NOTIFICATION_SHOW,
126+
extensionState,
127+
issueType,
78128
});
79129

80130
const buttons = hasCVEIssue
81131
? [fixCVEButtonText, BUTTON_TEXT_NOT_NOW]
82132
: [upgradeButtonText, BUTTON_TEXT_NOT_NOW];
83133

84134
const selection = await window.showInformationMessage(
85-
notificationMessage,
135+
message,
86136
...buttons
87137
);
88138
sendInfo(operationId, {
89-
operationName: "java.dependency.upgradeNotification.runUpgrade",
139+
operationName: UpgradeTelemetry.NOTIFICATION_CLICK,
140+
extensionState,
141+
issueType,
90142
choice: selection ?? "",
91143
});
92144

93145
switch (selection) {
94146
case fixCVEButtonText:
95147
case upgradeButtonText: {
96-
commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt);
148+
commands.executeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, prompt, issueType, extensionState);
97149
break;
98150
}
99151
case BUTTON_TEXT_NOT_NOW: {

src/upgrade/telemetryConstants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Telemetry operation names for the upgrade flow.
3+
*/
4+
export const UpgradeTelemetry = {
5+
NOTIFICATION_SHOW: "java.dependency.upgradeNotification.show",
6+
NOTIFICATION_CLICK: "java.dependency.upgradeNotification.runUpgrade",
7+
EXECUTE_START: "java.dependency.upgrade.execute.start",
8+
EXECUTE_END: "java.dependency.upgrade.execute.end",
9+
EXTENSION_INSTALL_START: "java.dependency.upgrade.extensionInstall.start",
10+
EXTENSION_INSTALL_END: "java.dependency.upgrade.extensionInstall.end",
11+
RELOAD_PROMPT_SHOW: "java.dependency.upgrade.reloadPrompt.show",
12+
RELOAD_PROMPT_CLICK: "java.dependency.upgrade.reloadPrompt.click",
13+
} as const;

src/upgrade/upgradeManager.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import notificationManager from "./display/notificationManager";
1212
import { Settings } from "../settings";
1313
import assessmentManager, { getDirectDependencies } from "./assessmentManager";
1414
import { checkOrInstallAppModExtensionForUpgrade, checkOrPopupToInstallAppModExtensionForModernization } from "./utility";
15+
import { UpgradeTelemetry } from "./telemetryConstants";
1516

16-
const DEFAULT_UPGRADE_PROMPT = "Upgrade Java project dependency to latest version.";
17+
const DEFAULT_UPGRADE_PROMPT = "Upgrade Java runtime and frameworks to the latest stable version.";
1718

1819

1920
function shouldRunCheckup() {
@@ -25,10 +26,26 @@ class UpgradeManager {
2526
notificationManager.initialize(context);
2627

2728
// Upgrade project
28-
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, async (promptText?: string) => {
29-
await checkOrInstallAppModExtensionForUpgrade(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
30-
const promptToUse = promptText ?? DEFAULT_UPGRADE_PROMPT;
31-
await commands.executeCommand(Commands.GOTO_AGENT_MODE, { prompt: promptToUse, useCustomAgent: true });
29+
context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.JAVA_UPGRADE_WITH_COPILOT, async (promptText?: string, issueType?: string, extensionState?: string) => {
30+
const dimensions = {
31+
issueType: issueType ?? "unknown",
32+
extensionState: extensionState ?? "unknown",
33+
};
34+
sendInfo("", { operationName: UpgradeTelemetry.EXECUTE_START, ...dimensions });
35+
try {
36+
await checkOrInstallAppModExtensionForUpgrade(ExtensionName.APP_MODERNIZATION_UPGRADE_FOR_JAVA);
37+
const promptToUse = promptText ?? DEFAULT_UPGRADE_PROMPT;
38+
await commands.executeCommand(Commands.GOTO_AGENT_MODE, { prompt: promptToUse, useCustomAgent: true });
39+
sendInfo("", { operationName: UpgradeTelemetry.EXECUTE_END, ...dimensions, result: "success" });
40+
} catch (e) {
41+
sendInfo("", {
42+
operationName: UpgradeTelemetry.EXECUTE_END,
43+
...dimensions,
44+
result: "failure",
45+
error: e instanceof Error ? e.message : String(e),
46+
});
47+
throw e;
48+
}
3249
}));
3350

3451
// Show modernization view

src/upgrade/utility.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { UpgradeReason, type UpgradeIssue } from "./type";
77
import { ExtensionName, Upgrade } from "../constants";
88
import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper";
99
import { CveUpgradeIssue } from "./cve";
10+
import { UpgradeTelemetry } from "./telemetryConstants";
1011

1112

1213
function findEolDate(currentVersion: string, eolDate: Record<string, string>): string | null {
@@ -22,7 +23,20 @@ function findEolDate(currentVersion: string, eolDate: Record<string, string>): s
2223
return null;
2324
}
2425

25-
export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: boolean): string {
26+
export type ExtensionState = "up-to-date" | "outdated" | "not-installed";
27+
28+
function getActionWord(extensionState: ExtensionState, verb: string): string {
29+
switch (extensionState) {
30+
case "up-to-date":
31+
return verb;
32+
case "outdated":
33+
return `update ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and ${verb}`;
34+
case "not-installed":
35+
return `install ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and ${verb}`;
36+
}
37+
}
38+
39+
export function buildNotificationMessage(issue: UpgradeIssue, extensionState: ExtensionState): string {
2640
const {
2741
packageId,
2842
currentVersion,
@@ -31,7 +45,7 @@ export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: bool
3145
packageDisplayName
3246
} = issue;
3347

34-
const upgradeWord = hasExtension ? "upgrade" : `install ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and upgrade`;
48+
const upgradeWord = getActionWord(extensionState, "upgrade");
3549

3650
if (packageId === Upgrade.PACKAGE_ID_FOR_JAVA_RUNTIME) {
3751
return `This project is using an older Java runtime (${currentVersion}). Would you like to ${upgradeWord} it to the latest LTS version?`;
@@ -51,7 +65,7 @@ export function buildNotificationMessage(issue: UpgradeIssue, hasExtension: bool
5165
}
5266
}
5367

54-
export function buildCVENotificationMessage(issues: CveUpgradeIssue[], hasExtension: boolean): string {
68+
export function buildCVENotificationMessage(issues: CveUpgradeIssue[], extensionState: ExtensionState): string {
5569

5670
if (issues.length === 0) {
5771
return "No CVE issues found.";
@@ -81,7 +95,7 @@ export function buildCVENotificationMessage(issues: CveUpgradeIssue[], hasExtens
8195
CVESeverityDistribution: severityText,
8296
});
8397

84-
const fixWord = hasExtension ? "fix" : `install ${ExtensionName.APP_MODERNIZATION_EXTENSION_NAME} extension and fix`;
98+
const fixWord = getActionWord(extensionState, "fix");
8599

86100
if (issues.length === 1) {
87101
return `${severityText} CVE vulnerability is detected in this project. Would you like to ${fixWord} it now?`;
@@ -102,7 +116,7 @@ export function buildFixPrompt(issue: UpgradeIssue): string {
102116
return `upgrade ${packageDisplayName} to ${suggestedVersionName}`;
103117
}
104118
case UpgradeReason.CVE: {
105-
return `fix all critical and high-severity CVE vulnerabilities in this project by invoking #appmod-validate-cves-for-java`;
119+
return `fix all CVE vulnerabilities in this project`;
106120
}
107121
}
108122
}
@@ -155,10 +169,43 @@ export async function checkOrPopupToInstallAppModExtensionForModernization(
155169

156170
export async function checkOrInstallAppModExtensionForUpgrade(
157171
extensionIdToCheck: string): Promise<void> {
158-
if (extensions.getExtension(extensionIdToCheck)) {
172+
const ext = extensions.getExtension(extensionIdToCheck);
173+
174+
if (ext) {
175+
const installedVersion = ext.packageJSON?.version;
176+
if (installedVersion && semver.gte(installedVersion, Upgrade.MIN_APPMOD_VERSION)) {
177+
return;
178+
}
179+
}
180+
181+
const action = ext ? "update" : "install";
182+
sendInfo("", { operationName: UpgradeTelemetry.EXTENSION_INSTALL_START, action });
183+
try {
184+
await commands.executeCommand("workbench.extensions.installExtension", ExtensionName.APP_MODERNIZATION_FOR_JAVA);
185+
sendInfo("", { operationName: UpgradeTelemetry.EXTENSION_INSTALL_END, action, result: "success" });
186+
} catch (e) {
187+
sendInfo("", {
188+
operationName: UpgradeTelemetry.EXTENSION_INSTALL_END,
189+
action,
190+
result: "failure",
191+
error: e instanceof Error ? e.message : String(e),
192+
});
193+
throw e;
194+
}
195+
196+
if (action === "update") {
197+
// Reload is required for the updated extension to take effect
198+
sendInfo("", { operationName: UpgradeTelemetry.RELOAD_PROMPT_SHOW });
199+
const reload = await window.showInformationMessage(
200+
"GitHub Copilot modernization extension has been updated. Reload VS Code to start the modernize experience.",
201+
"Reload Now"
202+
);
203+
sendInfo("", { operationName: UpgradeTelemetry.RELOAD_PROMPT_CLICK, choice: reload ?? "dismissed" });
204+
if (reload === "Reload Now") {
205+
await commands.executeCommand("workbench.action.reloadWindow");
206+
}
159207
return;
160208
}
161209

162-
await commands.executeCommand("workbench.extensions.installExtension", ExtensionName.APP_MODERNIZATION_FOR_JAVA);
163210
await checkOrPromptToEnableAppModExtension("upgrade");
164211
}

0 commit comments

Comments
 (0)