Repository: microsoft/vscode-edge-devtools
Pull request #391 (2021-05-14 20:43:18)

Add ability to attach to active debug target using shared CDP connection
This Pull request registers a new command for the extension that allows VSCode's JavaScript Debugger to attach the devtools to the currently active debug target.

It also leverages the new CDPProxy that allows our extension to connect to the client using the same websocket as the JsDebugger, giving us the benefits of:
- Appearing as a single debugger to the debug client page
- Allowing us to attach to debugging instances of Edge that are connected via annonymous pipes from VSCode debugger, instead of only remote-debugging-port launched Edge instances

![image](https://user-images.githubusercontent.com/42152559/118722572-26b73e80-b7e1-11eb-9827-c69a3e1fb909.png)

![image](https://user-images.githubusercontent.com/42152559/118722623-3767b480-b7e1-11eb-859d-1bde0d528d38.png)

![image](https://user-images.githubusercontent.com/42152559/118722662-42224980-b7e1-11eb-9d99-3563ba282f63.png)


Testing instructions:
To test the new features, Follow the instructions to install the Nightly flight of the JavaScript Debugger into VSCode Insiders from: https://github.com/microsoft/vscode-js-debug#nightly-extension
Then, debug a build from this branch.
In the resulting Extension host environment:
1. Attach the JsDebugger to a target page using an Edge launch.json:
        {
            "name": "Launch Edge",
            "request": "launch",
            "type": "pwa-msedge",
            "url": "http://localhost:8080",
            "webRoot": "${workspaceFolder}"
        },

2. Click the new "inspect icon" in the Vscode debugger bar
2a. An alternative is to use the 'Debug: Open Browser Devtools' command prompt

bgoddar: A bit more context on the weird case of missing events: I can fairly consistenly repro by:
1) Attaching a debug session of js-debugger
2) running the command to attach devtools from Ctrl+shift+P
3) Closing devtools
4) Reopening devtools.

The issue then resolves if I reload the page. I'm not sure if it's something weird on our end about remembering an old session? Or if there is an issue in the js-debugger side of things
mliao95: Took a look at the tests and they look good.  Thanks!

Patch:
diff --git a/package-lock.json b/package-lock.json
index fee550f..bcad061 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2556,6 +2556,14 @@
             "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=",
             "dev": true
         },
+        "bufferutil": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz",
+            "integrity": "sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==",
+            "requires": {
+                "node-gyp-build": "~3.7.0"
+            }
+        },
         "cache-base": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@@ -7789,6 +7797,11 @@
             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
             "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
         },
+        "node-gyp-build": {
+            "version": "3.7.0",
+            "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz",
+            "integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w=="
+        },
         "node-int64": {
             "version": "0.4.0",
             "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -10242,6 +10255,14 @@
             "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
             "dev": true
         },
+        "utf-8-validate": {
+            "version": "5.0.2",
+            "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz",
+            "integrity": "sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==",
+            "requires": {
+                "node-gyp-build": "~3.7.0"
+            }
+        },
         "util-deprecate": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 0d124f4..8c3da76 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
     "activationEvents": [
         "onCommand:vscode-edge-devtools.attach",
         "onCommand:vscode-edge-devtools.launch",
+        "onCommand:vscode-edge-devtools.attachToCurrentDebugTarget",
         "onWebviewPanel:vscode-edge-devtools",
         "onView:vscode-edge-devtools-view.targets",
         "onDebug"
@@ -596,7 +597,9 @@
         "download-edge": "node scripts/downloadAndExtractEdge.js"
     },
     "dependencies": {
+        "bufferutil": "4.0.1",
         "puppeteer-core": "9.1.1",
+        "utf-8-validate": "5.0.2",
         "vscode-chrome-debug-core": "6.8.9",
         "vscode-extension-telemetry": "0.1.7",
         "ws": "7.4.5",
diff --git a/src/JsDebugProxyPanelSocket.ts b/src/JsDebugProxyPanelSocket.ts
new file mode 100644
index 0000000..de77edb
--- /dev/null
+++ b/src/JsDebugProxyPanelSocket.ts
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { WebSocketEvent } from './common/webviewEvents';
+import { PanelSocket } from './panelSocket';
+
+export type IDevToolsPostMessageCallback = (e: WebSocketEvent, message?: string) => void;
+
+export class JsDebugProxyPanelSocket extends PanelSocket {
+    /**
+     * @override
+     */
+    protected onOpen(): void {
+        if (this.socket) {
+            this.registerForJsDebugSharedCDPEvents();
+        }
+        super.onOpen();
+    }
+
+    private registerForJsDebugSharedCDPEvents() {
+        // Register for CDP events from jsdebug:
+        // https://github.com/microsoft/vscode-js-debug/blob/main/CDP_SHARE.md
+        const registrationMessage = {
+            method: 'JsDebug.subscribe',
+            params: {
+                events: [
+                'Runtime.*',
+                'DOM.*',
+                'CSS.*',
+                'DOMDebugger.*',
+                'Network.*',
+                'Page.*',
+                'Target.*',
+                'Overlay.*',
+                ],
+            },
+        };
+        if (this.socket) {
+            this.socket.send(JSON.stringify(registrationMessage));
+        }
+    }
+}
diff --git a/src/devtoolsPanel.ts b/src/devtoolsPanel.ts
index b890052..b2fcd5a 100644
--- a/src/devtoolsPanel.ts
+++ b/src/devtoolsPanel.ts
@@ -14,6 +14,7 @@ import {
     TelemetryData,
     WebSocketEvent,
 } from './common/webviewEvents';
+import { JsDebugProxyPanelSocket } from './JsDebugProxyPanelSocket';
 import { PanelSocket } from './panelSocket';
 import {
     applyPathMapping,
@@ -55,7 +56,11 @@ export class DevToolsPanel {
         this.consoleOutput.appendLine('');
 
         // Hook up the socket events
-        this.panelSocket = new PanelSocket(this.targetUrl, (e, msg) => this.postToDevTools(e, msg));
+        if (this.config.isJsDebugProxiedCDPConnection) {
+            this.panelSocket = new JsDebugProxyPanelSocket(this.targetUrl, (e, msg) => this.postToDevTools(e, msg));
+        } else {
+            this.panelSocket = new PanelSocket(this.targetUrl, (e, msg) => this.postToDevTools(e, msg));
+        }
         this.panelSocket.on('ready', () => this.onSocketReady());
         this.panelSocket.on('websocket', () => this.onSocketMessage());
         this.panelSocket.on('telemetry', msg => this.onSocketTelemetry(msg));
diff --git a/src/extension.ts b/src/extension.ts
index 36fd922..381e319 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -27,6 +27,8 @@ import {
     SETTINGS_DEFAULT_ATTACH_INTERVAL,
     SETTINGS_STORE_NAME,
     SETTINGS_VIEW_NAME,
+    getActiveDebugSessionId,
+    getJsDebugCDPProxyWebsocketUrl,
 } from './utils';
 
 let telemetryReporter: Readonly<TelemetryReporter>;
@@ -34,6 +36,7 @@ let browserInstance: Browser;
 let launchConfig: LaunchConfig;
 let cdpTargetsProvider: CDPTargetsProvider;
 
+
 export function activate(context: vscode.ExtensionContext): void {
     if (!telemetryReporter) {
         telemetryReporter = createTelemetryReporter(context);
@@ -50,6 +53,10 @@ export function activate(context: vscode.ExtensionContext): void {
         void launch(context);
     }));
 
+    context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_STORE_NAME}.attachToCurrentDebugTarget`, (debugSessionId): void => {
+        void attachToCurrentDebugTarget(context, debugSessionId);
+    }));
+
     // Register the launch provider
     vscode.debug.registerDebugConfigurationProvider(`${SETTINGS_STORE_NAME}.debug`,
         new LaunchDebugProvider(context, telemetryReporter, attach, launch));
@@ -249,6 +256,39 @@ export async function attach(
     } while (useRetry && Date.now() - startTime < timeout);
 }
 
+export async function attachToCurrentDebugTarget(context: vscode.ExtensionContext, debugSessionId?: string): Promise<void> {
+    if (!telemetryReporter) {
+        telemetryReporter = createTelemetryReporter(context);
+    }
+
+    telemetryReporter.sendTelemetryEvent('command/attachToCurrentDebugTarget');
+    const sessionId = debugSessionId || getActiveDebugSessionId();
+
+    if (!sessionId) {
+        const errorMessage = 'No active debug session';
+        telemetryReporter.sendTelemetryErrorEvent('command/attachToCurrentDebugTarget/devtools', {message: errorMessage});
+        void vscode.window.showErrorMessage(errorMessage);
+        return;
+    }
+
+    const targetWebsocketUrl = await getJsDebugCDPProxyWebsocketUrl(sessionId);
+
+    if (targetWebsocketUrl instanceof Error) {
+        telemetryReporter.sendTelemetryErrorEvent('command/attachToCurrentDebugTarget/devtools', {message: targetWebsocketUrl.message});
+        void vscode.window.showErrorMessage(targetWebsocketUrl.message);
+    } else if (targetWebsocketUrl) {
+        // Auto connect to found target
+        telemetryReporter.sendTelemetryEvent('command/attachToCurrentDebugTarget/devtools');
+        const runtimeConfig = getRuntimeConfig();
+        runtimeConfig.isJsDebugProxiedCDPConnection = true;
+        DevToolsPanel.createOrShow(context, telemetryReporter, targetWebsocketUrl, runtimeConfig);
+    } else {
+        const errorMessage = 'Unable to attach DevTools to current debug session.';
+        telemetryReporter.sendTelemetryErrorEvent('command/attachToCurrentDebugTarget/devtools', {message: errorMessage});
+        void vscode.window.showErrorMessage(errorMessage);
+    }
+}
+
 export async function launch(context: vscode.ExtensionContext, launchUrl?: string, config?: Partial<IUserConfig>): Promise<void> {
     if (!telemetryReporter) {
         telemetryReporter = createTelemetryReporter(context);
diff --git a/src/panelSocket.ts b/src/panelSocket.ts
index 5648cbc..3a18940 100644
--- a/src/panelSocket.ts
+++ b/src/panelSocket.ts
@@ -10,7 +10,7 @@ export type IDevToolsPostMessageCallback = (e: WebSocketEvent, message?: string)
 export class PanelSocket extends EventEmitter {
     private readonly targetUrl: string;
     private readonly postMessageToDevTools: IDevToolsPostMessageCallback;
-    private socket: WebSocket | undefined;
+    protected socket: WebSocket | undefined;
     private isConnected = false;
     private messages: string[] = [];
 
@@ -76,7 +76,7 @@ export class PanelSocket extends EventEmitter {
         this.socket.onclose = () => this.onClose();
     }
 
-    private onOpen() {
+    protected onOpen(): void {
         this.isConnected = true;
 
         this.postMessageToDevTools('open');
diff --git a/src/utils.ts b/src/utils.ts
index 3035659..83edf24 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -55,11 +55,18 @@ export interface IRuntimeConfig {
     sourceMapPathOverrides: IStringDictionary<string>;
     sourceMaps: boolean;
     webRoot: string;
+    isJsDebugProxiedCDPConnection: boolean;
 }
 export interface IStringDictionary<T> {
     [name: string]: T;
 }
 
+export interface IRequestCDPProxyResult {
+    host: string;
+    port: number;
+    path: string;
+}
+
 export type Platform = 'Windows' | 'OSX' | 'Linux';
 
 export const SETTINGS_STORE_NAME = 'vscode-edge-devtools';
@@ -260,6 +267,40 @@ export function getRemoteEndpointSettings(config: Partial<IUserConfig> = {}): ID
     return { hostname, port, useHttps, defaultUrl, userDataDir, timeout };
 }
 
+/**
+ * Get the session id for the currently active VSCode debugging session
+ */
+export function getActiveDebugSessionId(): string|undefined {
+    // Attempt to attach to active CDP target
+    const session = vscode.debug.activeDebugSession;
+    return session ? session.id : undefined;
+}
+
+/**
+ * Create the target websocket url for attaching to the shared CDP instance exposed by
+ * the JavaScript Debugging Extension for VSCode.
+ * https://github.com/microsoft/vscode-js-debug/blob/main/CDP_SHARE.md
+ *
+ * @param debugSessionId The session id of the active VSCode debugging session
+ */
+export async function getJsDebugCDPProxyWebsocketUrl(debugSessionId: string): Promise<string|Error|undefined> {
+    try {
+        const addr: IRequestCDPProxyResult|undefined = await vscode.commands.executeCommand(
+        'extension.js-debug.requestCDPProxy',
+        debugSessionId,
+        );
+        if (addr) {
+            return `ws://${addr.host}:${addr.port}${addr.path || ''}`;
+        }
+    } catch (e) {
+        if (e instanceof Error) {
+            return e;
+        }
+        // Throw remaining unhandled exceptions
+        throw e;
+    }
+}
+
 /**
  * Create a telemetry reporter that can be used for this extension
  *
@@ -472,6 +513,7 @@ export function getRuntimeConfig(config: Partial<IUserConfig> = {}): IRuntimeCon
         sourceMapPathOverrides: resolvedOverrides,
         sourceMaps,
         webRoot: resolvedWebRoot,
+        isJsDebugProxiedCDPConnection: false,
     };
 }
 


Test patch:
diff --git a/test/devtoolsPanel.test.ts b/test/devtoolsPanel.test.ts
index d0a63fb..06f38e5 100644
--- a/test/devtoolsPanel.test.ts
+++ b/test/devtoolsPanel.test.ts
@@ -8,6 +8,7 @@ import { Disposable, ExtensionContext, WebviewPanel } from "vscode";
 import TelemetryReporter from "vscode-extension-telemetry";
 import { TelemetryData, webviewEventNames } from "../src/common/webviewEvents";
 import { PanelSocket } from "../src/panelSocket";
+import { JsDebugProxyPanelSocket } from "../src/JsDebugProxyPanelSocket";
 import {
     createFakeExtensionContext,
     createFakeTelemetryReporter,
@@ -26,6 +27,8 @@ describe("devtoolsPanel", () => {
     let mockPanel: Mocked<WebviewPanel>;
     let mockPanelSocket: Mocked<PanelSocket>;
     let mockPanelSocketFactory: { PanelSocket: jest.Mock };
+    let mockJsDebugProxyPanelSocket: Mocked<JsDebugProxyPanelSocket>;
+    let mockJsDebugProxyPanelSocketFactory: { JsDebugProxyPanelSocket: jest.Mock };
     let mockWebviewEvents: { encodeMessageForChannel: jest.Mock };
     let mockRuntimeConfig: IRuntimeConfig;
 
@@ -39,6 +42,7 @@ describe("devtoolsPanel", () => {
             sourceMapPathOverrides: {},
             sourceMaps: true,
             webRoot: "",
+            isJsDebugProxiedCDPConnection: false,
         };
 
         mockPanel = {
@@ -65,6 +69,17 @@ describe("devtoolsPanel", () => {
         };
         jest.doMock("../src/panelSocket", () => mockPanelSocketFactory);
 
+        mockJsDebugProxyPanelSocket = {
+            dispose: jest.fn(),
+            on: jest.fn(),
+            onMessageFromWebview: jest.fn(),
+        } as Mocked<JsDebugProxyPanelSocket>;
+
+        mockJsDebugProxyPanelSocketFactory = {
+            JsDebugProxyPanelSocket: jest.fn(() => mockJsDebugProxyPanelSocket),
+        };
+        jest.doMock("../src/JsDebugProxyPanelSocket", () => mockJsDebugProxyPanelSocketFactory);
+
         mockWebviewEvents = {
             encodeMessageForChannel: jest.fn(),
         };
@@ -519,6 +534,14 @@ describe("devtoolsPanel", () => {
                 expect(mockVsCode.window.showErrorMessage).toHaveBeenCalledWith(
                     expect.stringContaining(expectedRequest.url));
             });
+
+            it("creates a JsDebugProxyPanelSocket when config requires it", async () => {
+                mockRuntimeConfig.isJsDebugProxiedCDPConnection = true;
+                const dtp = await import("../src/devtoolsPanel");
+                dtp.DevToolsPanel.createOrShow(context, mockTelemetry, "", mockRuntimeConfig);
+
+                expect(mockJsDebugProxyPanelSocket.on).toHaveBeenCalled();
+            });
         });
     });
 });
diff --git a/test/extension.test.ts b/test/extension.test.ts
index 4f89b22..2fd19a8 100644
--- a/test/extension.test.ts
+++ b/test/extension.test.ts
@@ -42,6 +42,8 @@ describe("extension", () => {
                 getRuntimeConfig: jest.fn(),
                 removeTrailingSlash: jest.fn(removeTrailingSlash),
                 getLaunchJson: jest.fn(),
+                getJsDebugCDPProxyWebsocketUrl: jest.fn(),
+                getActiveDebugSessionId: jest.fn(),
             };
             jest.doMock("../src/utils", () => mockUtils);
             jest.doMock("../src/launchDebugProvider");
@@ -77,32 +79,34 @@ describe("extension", () => {
             // Activation should add the commands as subscriptions on the context
             newExtension.activate(context);
 
-            expect(context.subscriptions.length).toBe(13);
-            expect(commandMock).toHaveBeenCalledTimes(12);
+            expect(context.subscriptions.length).toBe(14);
+            expect(commandMock).toHaveBeenCalledTimes(13);
             expect(commandMock)
                 .toHaveBeenNthCalledWith(1, `${SETTINGS_STORE_NAME}.attach`, expect.any(Function));
             expect(commandMock)
                 .toHaveBeenNthCalledWith(2, `${SETTINGS_STORE_NAME}.launch`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(3, `${SETTINGS_VIEW_NAME}.launch`, expect.any(Function));
+                .toHaveBeenNthCalledWith(3, `${SETTINGS_STORE_NAME}.attachToCurrentDebugTarget`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(4, `${SETTINGS_VIEW_NAME}.refresh`, expect.any(Function));
+                .toHaveBeenNthCalledWith(4, `${SETTINGS_VIEW_NAME}.launch`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(5, `${SETTINGS_VIEW_NAME}.attach`, expect.any(Function));
+                .toHaveBeenNthCalledWith(5, `${SETTINGS_VIEW_NAME}.refresh`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(6, `${SETTINGS_VIEW_NAME}.openSettings`, expect.any(Function));
+                .toHaveBeenNthCalledWith(6, `${SETTINGS_VIEW_NAME}.attach`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(7, `${SETTINGS_VIEW_NAME}.viewChangelog`, expect.any(Function));
+                .toHaveBeenNthCalledWith(7, `${SETTINGS_VIEW_NAME}.openSettings`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(8, `${SETTINGS_VIEW_NAME}.close-instance`, expect.any(Function));
+                .toHaveBeenNthCalledWith(8, `${SETTINGS_VIEW_NAME}.viewChangelog`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(9, `${SETTINGS_VIEW_NAME}.copyItem`, expect.any(Function));
+                .toHaveBeenNthCalledWith(9, `${SETTINGS_VIEW_NAME}.close-instance`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(10, `${SETTINGS_VIEW_NAME}.configureLaunchJson`, expect.any(Function));
+                .toHaveBeenNthCalledWith(10, `${SETTINGS_VIEW_NAME}.copyItem`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(11, `${SETTINGS_VIEW_NAME}.launchProject`, expect.any(Function));
+                .toHaveBeenNthCalledWith(11, `${SETTINGS_VIEW_NAME}.configureLaunchJson`, expect.any(Function));
             expect(commandMock)
-                .toHaveBeenNthCalledWith(12, `${SETTINGS_VIEW_NAME}.viewDocumentation`, expect.any(Function)); 
+                .toHaveBeenNthCalledWith(12, `${SETTINGS_VIEW_NAME}.launchProject`, expect.any(Function));
+            expect(commandMock)
+                .toHaveBeenNthCalledWith(13, `${SETTINGS_VIEW_NAME}.viewDocumentation`, expect.any(Function));
             expect(mockRegisterTree)
                 .toHaveBeenNthCalledWith(1, `${SETTINGS_VIEW_NAME}.targets`, expect.any(Object));
         });
@@ -151,15 +155,15 @@ describe("extension", () => {
                 return { callback: commandMock.mock.calls[index][1], thisObj: commandMock.mock.instances[index] };
             }
 
-            const refresh = getCommandCallback(3);
+            const refresh = getCommandCallback(4);
             refresh.callback.call(refresh.thisObj);
             expect(mockProviderRefresh).toHaveBeenCalled();
 
-            const attach = getCommandCallback(4);
+            const attach = getCommandCallback(5);
             attach.callback.call(attach.thisObj, { websocketUrl: "" });
             expect(mockPanelShow).toHaveBeenCalled();
 
-            const copy = getCommandCallback(8);
+            const copy = getCommandCallback(9);
             copy.callback.call(copy.thisObj, { tooltip: "something" });
             expect(mockClipboard).toHaveBeenCalledWith("something");
         });
@@ -397,6 +401,7 @@ describe("extension", () => {
                 openNewTab: jest.fn().mockResolvedValue(null),
                 removeTrailingSlash: jest.fn(removeTrailingSlash),
                 getLaunchJson: jest.fn(),
+                getJsDebugCDPProxyWebsocketUrl: jest.fn(),
                 buttonCode: { launch: '' },
             };
 
@@ -535,4 +540,93 @@ describe("extension", () => {
             }
         });
     });
+    describe("attachToCurrentDebugTarget", () => {
+        let mocks: {
+            panel: any,
+            utils: Partial<Mocked<typeof import("../src/utils")>>,
+            vscode: any,
+        };
+        let mockTelemetry: Mocked<Readonly<TelemetryReporter>>;
+        const websocketUrl = 'ws://127.0.0.1:9222/uniquePath';
+
+        beforeEach(() => {
+            mockTelemetry = createFakeTelemetryReporter();
+
+            mocks = {
+                panel: {
+                    DevToolsPanel: {
+                        createOrShow: jest.fn(),
+                    },
+                },
+                utils: {
+                    createTelemetryReporter: jest.fn((_: ExtensionContext) => mockTelemetry),
+                    getRuntimeConfig: jest.fn().mockReturnValue(fakeRuntimeConfig),
+                    getActiveDebugSessionId: jest.fn().mockReturnValue('vscode-active-debug-session-id'),
+                    getJsDebugCDPProxyWebsocketUrl: jest.fn().mockResolvedValue(websocketUrl),
+                },
+                vscode: createFakeVSCode(),
+            };
+
+            jest.doMock("vscode", () => mocks.vscode, { virtual: true });
+            jest.doMock("../src/devtoolsPanel", () => mocks.panel);
+            jest.doMock("../src/utils", () => mocks.utils);
+            jest.resetModules();
+        });
+
+        it("creates a telemetry reporter", async () => {
+            const newExtension = await import("../src/extension");
+
+            // Activation should create a new reporter
+            await newExtension.attachToCurrentDebugTarget(createFakeExtensionContext());
+            expect(mocks.utils.createTelemetryReporter).toHaveBeenCalled();
+        });
+
+        it("finds the active debug session id if one is not provided", async () => {
+            const newExtension = await import("../src/extension");
+
+            await newExtension.attachToCurrentDebugTarget(createFakeExtensionContext());
+            expect(mocks.utils.getActiveDebugSessionId).toHaveBeenCalled();
+        });
+
+        it("throws an error if there is no active debug session", async () => {
+            const expectedErrorMessage = 'No active debug session';
+
+            mocks.utils.getActiveDebugSessionId!.mockReturnValueOnce(undefined);
+            const newExtension = await import("../src/extension");
+
+            await newExtension.attachToCurrentDebugTarget(createFakeExtensionContext());
+            expect(mocks.vscode.window.showErrorMessage).toBeCalledWith(expect.stringContaining(expectedErrorMessage));
+        });
+
+        it("creates a panel with a constructed url", async () => {
+            const newExtension = await import("../src/extension");
+
+            await newExtension.attachToCurrentDebugTarget(createFakeExtensionContext());
+            expect(mocks.utils.getJsDebugCDPProxyWebsocketUrl).toHaveBeenCalled();
+            expect(mocks.panel.DevToolsPanel!.createOrShow).toHaveBeenCalledWith(
+                expect.any(Object),
+                expect.any(Object),
+                websocketUrl,
+                {...fakeRuntimeConfig, isJsDebugProxiedCDPConnection: true},
+            );
+        });
+
+        it("throws an error when unable to resolve the JsDebugCDPProxyWebSocketUrl", async () => {
+            mocks.utils.getJsDebugCDPProxyWebsocketUrl?.mockResolvedValueOnce(Error('Error Message'));
+            const newExtension = await import("../src/extension");
+
+            await newExtension.attachToCurrentDebugTarget(createFakeExtensionContext());
+            expect(mocks.utils.getJsDebugCDPProxyWebsocketUrl).toHaveBeenCalled();
+            expect(mocks.vscode.window.showErrorMessage).toBeCalledWith(expect.stringContaining('Error Message'));
+        });
+
+        it("shows an error if JsDebugCDPProxyWebSocketUrl is undefined", async () => {
+            mocks.utils.getJsDebugCDPProxyWebsocketUrl?.mockResolvedValueOnce(undefined);
+            const newExtension = await import("../src/extension");
+
+            await newExtension.attachToCurrentDebugTarget(createFakeExtensionContext());
+            expect(mocks.utils.getJsDebugCDPProxyWebsocketUrl).toHaveBeenCalled();
+            expect(mocks.vscode.window.showErrorMessage).toBeCalledWith(expect.stringContaining('Unable to attach DevTools to current debug session.'));
+        });
+    });
 });
diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts
index f8eca62..b1c512c 100644
--- a/test/helpers/helpers.ts
+++ b/test/helpers/helpers.ts
@@ -45,6 +45,9 @@ export function createFakeVSCode() {
         },
         debug: {
             registerDebugConfigurationProvider: jest.fn(),
+            activeDebugSession: {
+                id: 'vscode-session-debug-id'
+            }
         },
         env: {
             clipboard: { writeText: jest.fn() },
diff --git a/test/utils.test.ts b/test/utils.test.ts
index d3a2795..c682206 100644
--- a/test/utils.test.ts
+++ b/test/utils.test.ts
@@ -986,4 +986,40 @@ describe("utils", () => {
             })).toEqual(pathResolve("/project/src/app.js"));
         });
     });
+
+    describe("getActiveDebugSessionId", () => {
+        it("retrieves the debug session id from vscode properly", () => {
+            expect(utils.getActiveDebugSessionId()).toBe('vscode-session-debug-id');
+        });
+
+        it("returns undefined when there is not an active vscode session", async () => {
+            const vscodeMock = await jest.requireMock("vscode");
+            vscodeMock.debug.activeDebugSession = undefined;
+            expect(utils.getActiveDebugSessionId()).toBe(undefined);
+        });
+    });
+
+    describe("getJsDebugCDPProxyWebsocketUrl", () => {
+        it("creates a proper websocket url from a debug session id", async () => {
+            const expectedAddr = {
+                host: '127.0.0.1',
+                port: '9222',
+                path: '/uniquePath'
+            };
+            const vscodeMock = await jest.requireMock("vscode");
+            vscodeMock.commands.executeCommand.mockImplementationOnce((name: string, sessionId: string) => {return expectedAddr;});
+
+            const result = await utils.getJsDebugCDPProxyWebsocketUrl('debugSessionId');
+            expect(result).toBe('ws://127.0.0.1:9222/uniquePath');
+        });
+
+        it("returns an error if the VSCode execute command throws an error", async () => {
+            const vscodeMock = await jest.requireMock("vscode");
+            vscodeMock.commands.executeCommand.mockImplementationOnce((name: string, sessionId: string) => {throw new Error("error message")});
+
+            const result = await utils.getJsDebugCDPProxyWebsocketUrl('debugSessionId') as Error;
+            expect(result).toBeInstanceOf(Error);
+            expect(result.message).toBe('error message');
+        });
+    });
 });