Repository: microsoft/vscode-edge-devtools
Pull request #2431 (2024-09-18 18:15:32)

Feature: Sets different fallback chains.
This PR introduces two different ways to fallback to the devtools version displayed by the extension:

Before:
We got the version from the storage, if that did not existed or failed, we got the one from the browser and if that failed we got the one from the CDN. This whole process was async and could take up to ~5s leaving the users with the question if the tool was working or not.

After:
User has the ability to select the fallback chain by selecting the browser flavor. If the user wants a specific version (browserFlavor) the extension will behave in the same way as before, otherwise the first thing we will try to get is a working version of the tools from the CDN fallback address. This process takes less than 1s, making it faster for most of the users


Patch:
diff --git a/src/devtoolsPanel.ts b/src/devtoolsPanel.ts
index 6b82504..4d11f9f 100644
--- a/src/devtoolsPanel.ts
+++ b/src/devtoolsPanel.ts
@@ -57,6 +57,8 @@ export class DevToolsPanel {
     private collectConsoleMessages = true;
     private currentRevision: string | undefined;
     private cssWarningActive: boolean;
+    private fallbackChain: (() => void)[] = [];
+    private getFallbackRevisionFunction: (() => void) = () => {};
 
     private constructor(
         panel: vscode.WebviewPanel,
@@ -72,7 +74,7 @@ export class DevToolsPanel {
         this.config = config;
         this.timeStart = null;
         this.devtoolsBaseUri = this.config.devtoolsBaseUri || null;
-        this.isHeadless = false;
+        this.isHeadless = SettingsProvider.instance.getHeadlessSettings();
         this.cssWarningActive = false;
 
         // Hook up the socket events
@@ -103,7 +105,12 @@ export class DevToolsPanel {
         // This Websocket is only used on initial connection to determine the browser version.
         // The browser version is used to select the correct hashed version of the devtools
         this.versionDetectionSocket = new BrowserVersionDetectionSocket(this.targetUrl);
-        this.versionDetectionSocket.on('setCdnParameters', (msg: {revision: string; isHeadless: boolean}) => this.setCdnParameters(msg));
+
+        // Gets an array of functions that will be tried to get the right Devtools revision.
+        this.fallbackChain = this.determineVersionFallback();
+        if (this.fallbackChain.length > 0) {
+            this.getFallbackRevisionFunction = this.fallbackChain.pop() || this.getFallbackRevisionFunction;
+        }
 
         // Handle closing
         this.panel.onDidDispose(() => {
@@ -117,8 +124,7 @@ export class DevToolsPanel {
                     // Connection type determined already
                     this.update();
                 } else {
-                    // Use version socket to determine which Webview/Tools to use
-                    this.versionDetectionSocket.detectVersion();
+                    this.getFallbackRevisionFunction();
                 }
             }
         }, this, this.disposables);
@@ -137,12 +143,52 @@ export class DevToolsPanel {
         });
     }
 
+    /**
+     * Allows multiple fallbacks, allowing the user to select between stability
+     * or latest features.
+     * @returns A function array that has the fallback chain.
+     */
+    determineVersionFallback() {
+        const browserFlavor = this.config.browserFlavor;
+        const storedRevision = this.context.globalState.get<string>('fallbackRevision') || '';
+        const callWrapper = (revision: string) => {
+            this.setCdnParameters({revision, isHeadless: this.isHeadless});
+        };
+
+        // Use version socket to determine which Webview/Tools to use
+        const detectedVersion = () => {
+            this.versionDetectionSocket.on('setCdnParameters', (msg: {revision: string; isHeadless: boolean}) => {
+                this.setCdnParameters(msg);
+            });
+
+            this.versionDetectionSocket.detectVersion.bind(this.versionDetectionSocket)();
+        };
+
+        // we reverse the array so that it behaves like a stack.
+        switch (browserFlavor) {
+            case 'Beta':
+            case 'Canary':
+            case 'Dev':
+            case 'Stable': {
+                return [ detectedVersion,
+                        () => callWrapper(CDN_FALLBACK_REVISION),
+                        () => callWrapper(storedRevision)].reverse();
+            }
+
+            case 'Default':
+            default: {
+                return [() => callWrapper(CDN_FALLBACK_REVISION),
+                        detectedVersion,
+                        () => callWrapper(storedRevision)].reverse();
+            }
+        }
+    }
+
     dispose(): void {
         DevToolsPanel.instance = undefined;
 
         this.panel.dispose();
         this.panelSocket.dispose();
-        this.versionDetectionSocket.dispose();
         if (this.timeStart !== null) {
             const timeEnd = performance.now();
             const sessionTime = timeEnd - this.timeStart;
@@ -406,41 +452,18 @@ export class DevToolsPanel {
     private onSocketDevToolsConnection(success: string) {
         if (success === 'true') {
             void this.context.globalState.update('fallbackRevision', this.currentRevision);
-            this.context.globalState.update('retryAttemptToLoadCDN', '1');
+            this.fallbackChain = this.determineVersionFallback();
         } else {
-            let retryNumber: number;
-            try {
-                retryNumber = parseInt(this.context.globalState.get<string>('retryAttemptToLoadCDN') || '1', 10);
-            } catch {
-                retryNumber = 1;
-            }
-
-            let fallbackRevision;
-            switch (retryNumber) {
-                case 1: {
-                    // Always try the latest specified revision first, this will keep it updated.
-                    fallbackRevision = CDN_FALLBACK_REVISION;
-                    this.context.globalState.update('retryAttemptToLoadCDN', ++retryNumber);
-                    break;
-                }
-                case 2: {
-                     // Retry connection with latest well known fallback that this environment knows.
-                    fallbackRevision = this.context.globalState.get<string>('fallbackRevision') ?? '';
-                    this.context.globalState.update('retryAttemptToLoadCDN', ++retryNumber);
-                    break;
-                }
-                default: {
-                    // Could not find suitable version.
-                    this.context.globalState.update('retryAttemptToLoadCDN', '1');
-                    return;
-                }
-            }
-
             if (this.currentRevision) {
                 this.telemetryReporter.sendTelemetryEvent('websocket/failedConnection', {revision: this.currentRevision});
             }
 
-            this.setCdnParameters({revision: fallbackRevision, isHeadless: this.isHeadless});
+            // We failed trying to retrieve the specified revision
+            // we fallback to the next option if available.
+            if (this.fallbackChain.length > 0) {
+                this.getFallbackRevisionFunction = this.fallbackChain.pop() || (() => {});
+                this.getFallbackRevisionFunction();
+            }
         }
     }
 
@@ -574,7 +597,7 @@ export class DevToolsPanel {
     }
 
     private setCdnParameters(msg: {revision: string, isHeadless: boolean}) {
-        this.currentRevision = msg.revision || CDN_FALLBACK_REVISION;
+        this.currentRevision = msg.revision;
         this.devtoolsBaseUri = `https://devtools.azureedge.net/serve_file/${this.currentRevision}/vscode_app.html`;
         this.isHeadless = msg.isHeadless;
         this.update();
diff --git a/src/utils.ts b/src/utils.ts
index 98960ee..6e4bafc 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -63,6 +63,7 @@ export interface IRuntimeConfig {
     useLocalEdgeWatch: boolean;
     devtoolsBaseUri?: string;
     defaultEntrypoint?: string;
+    browserFlavor: BrowserFlavor;
 }
 export interface IStringDictionary<T> {
     [name: string]: T;
@@ -454,6 +455,7 @@ export function removeTrailingSlash(uri: string): string {
 export function getRuntimeConfig(config: Partial<IUserConfig> = {}): IRuntimeConfig {
     const settings = vscode.workspace.getConfiguration(SETTINGS_STORE_NAME);
     const pathMapping = config.pathMapping || settings.get('pathMapping') || SETTINGS_DEFAULT_PATH_MAPPING;
+    const browserFlavor = config.browserFlavor || settings.get('browserFlavor') || 'Default';
     const sourceMapPathOverrides =
         config.sourceMapPathOverrides || settings.get('sourceMapPathOverrides') || SETTINGS_DEFAULT_PATH_OVERRIDES;
     const webRoot = config.webRoot || settings.get('webRoot') || SETTINGS_DEFAULT_WEB_ROOT;
@@ -499,6 +501,7 @@ export function getRuntimeConfig(config: Partial<IUserConfig> = {}): IRuntimeCon
     return {
         pathMapping: resolvedMappingOverrides,
         sourceMapPathOverrides: resolvedOverrides,
+        browserFlavor,
         sourceMaps,
         webRoot: resolvedWebRoot,
         isJsDebugProxiedCDPConnection: false,
diff --git a/src/versionSocketConnection.ts b/src/versionSocketConnection.ts
index 02301bf..6b67a08 100644
--- a/src/versionSocketConnection.ts
+++ b/src/versionSocketConnection.ts
@@ -17,7 +17,7 @@ export interface BrowserVersionCdpResponse {
 }
 
 // Minimum supported version of Edge
-export const MIN_SUPPORTED_VERSION = '120.0.2210.181';
+export const MIN_SUPPORTED_VERSION = '127.0.2592.0';
 export const MIN_SUPPORTED_REVISION = CDN_FALLBACK_REVISION;
 
 export class BrowserVersionDetectionSocket extends EventEmitter {


Test patch:
diff --git a/test/devtoolsPanel.test.ts b/test/devtoolsPanel.test.ts
index 0d62465..74097f3 100644
--- a/test/devtoolsPanel.test.ts
+++ b/test/devtoolsPanel.test.ts
@@ -48,6 +48,7 @@ describe("devtoolsPanel", () => {
             webRoot: "",
             isJsDebugProxiedCDPConnection: false,
             useLocalEdgeWatch: false,
+            browserFlavor: "Default",
         };
 
         mockPanel = {
@@ -173,6 +174,7 @@ describe("devtoolsPanel", () => {
     describe("update", () => {
         it("adds attempts to detect browser version only when visible", async () => {
             const dtp = await import("../src/devtoolsPanel");
+            mockRuntimeConfig.browserFlavor = 'Stable';
             dtp.DevToolsPanel.createOrShow(context, mockTelemetry, "", mockRuntimeConfig);
             expect(mockPanel.onDidChangeViewState).toHaveBeenCalled();
 
diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts
index e81da00..19781d7 100644
--- a/test/helpers/helpers.ts
+++ b/test/helpers/helpers.ts
@@ -87,6 +87,9 @@ export function createFakeVSCode() {
             showTextDocument: jest.fn(),
             showInformationMessage: jest.fn(),
             showWarningMessage: jest.fn().mockResolvedValue({}),
+            activeColorTheme: {
+                kind: 1
+            }
         },
         workspace: {
             createFileSystemWatcher: jest.fn(),
@@ -140,9 +143,11 @@ export function createFakeVSCode() {
  * Create a fake VS Code extension context that can be used in tests
  */
 export function createFakeExtensionContext() {
+    const mockedGlobalState = new Map();
     return {
         extensionPath: "",
         subscriptions: [],
+        globalState: mockedGlobalState,
         workspaceState: {
             get: jest.fn(),
             update: jest.fn(),