diff --git a/docs/api/index.md b/docs/api/index.md
index d680e73f456..7b44f36e489 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -91,6 +91,7 @@ sidebar_label: API
| [Credentials](./puppeteer.credentials.md) | |
| [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for CSS coverage. |
| [CustomQueryHandler](./puppeteer.customqueryhandler.md) | |
+| [DebugInfo](./puppeteer.debuginfo.md) | |
| [Device](./puppeteer.device.md) | |
| [ElementScreenshotOptions](./puppeteer.elementscreenshotoptions.md) | |
| [FrameAddScriptTagOptions](./puppeteer.frameaddscripttagoptions.md) | |
diff --git a/docs/api/puppeteer.browser.md b/docs/api/puppeteer.browser.md
index f7191c54892..68c24dce2d5 100644
--- a/docs/api/puppeteer.browser.md
+++ b/docs/api/puppeteer.browser.md
@@ -56,9 +56,10 @@ await browser2.close();
## Properties
-| Property | Modifiers | Type | Description |
-| --------- | --------------------- | ------- | ------------------------------------------------------------------------- |
-| connected | readonly
| boolean | Whether Puppeteer is connected to this [browser](./puppeteer.browser.md). |
+| Property | Modifiers | Type | Description |
+| --------- | --------------------- | ------------------------------------- | ------------------------------------------------------------------------- |
+| connected | readonly
| boolean | Whether Puppeteer is connected to this [browser](./puppeteer.browser.md). |
+| debugInfo | readonly
| [DebugInfo](./puppeteer.debuginfo.md) | Get debug information from Puppeteer. |
## Methods
diff --git a/docs/api/puppeteer.debuginfo.md b/docs/api/puppeteer.debuginfo.md
new file mode 100644
index 00000000000..f20ed755451
--- /dev/null
+++ b/docs/api/puppeteer.debuginfo.md
@@ -0,0 +1,17 @@
+---
+sidebar_label: DebugInfo
+---
+
+# DebugInfo interface
+
+#### Signature:
+
+```typescript
+export interface DebugInfo
+```
+
+## Properties
+
+| Property | Modifiers | Type | Description | Default |
+| --------------------- | --------- | --------- | ----------- | ------- |
+| pendingProtocolErrors | | Error\[\] | | |
diff --git a/packages/puppeteer-core/src/api/Browser.ts b/packages/puppeteer-core/src/api/Browser.ts
index cd7b25f5409..e3b465c80e2 100644
--- a/packages/puppeteer-core/src/api/Browser.ts
+++ b/packages/puppeteer-core/src/api/Browser.ts
@@ -186,6 +186,14 @@ export interface BrowserEvents extends Record {
[BrowserEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
}
+/**
+ * @public
+ * @experimental
+ */
+export interface DebugInfo {
+ pendingProtocolErrors: Error[];
+}
+
/**
* {@link Browser} represents a browser instance that is either:
*
@@ -431,4 +439,16 @@ export abstract class Browser extends EventEmitter {
* @internal
*/
abstract get protocol(): ProtocolType;
+
+ /**
+ * Get debug information from Puppeteer.
+ *
+ * @remarks
+ *
+ * Currently, includes pending protocol calls. In the future, we might add more info.
+ *
+ * @public
+ * @experimental
+ */
+ abstract get debugInfo(): DebugInfo;
}
diff --git a/packages/puppeteer-core/src/bidi/Browser.ts b/packages/puppeteer-core/src/bidi/Browser.ts
index 51d0a057d60..42979790c92 100644
--- a/packages/puppeteer-core/src/bidi/Browser.ts
+++ b/packages/puppeteer-core/src/bidi/Browser.ts
@@ -13,6 +13,7 @@ import {
BrowserEvent,
type BrowserCloseCallback,
type BrowserContextOptions,
+ type DebugInfo,
} from '../api/Browser.js';
import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
@@ -307,4 +308,10 @@ export class BidiBrowser extends Browser {
this.connection.dispose();
}
}
+
+ override get debugInfo(): DebugInfo {
+ return {
+ pendingProtocolErrors: this.connection.getPendingProtocolErrors(),
+ };
+ }
}
diff --git a/packages/puppeteer-core/src/bidi/Connection.ts b/packages/puppeteer-core/src/bidi/Connection.ts
index 6cbd4723e74..bce952ba39f 100644
--- a/packages/puppeteer-core/src/bidi/Connection.ts
+++ b/packages/puppeteer-core/src/bidi/Connection.ts
@@ -234,6 +234,10 @@ export class BidiConnection
this.unbind();
this.#transport.close();
}
+
+ getPendingProtocolErrors(): Error[] {
+ return this.#callbacks.getPendingProtocolErrors();
+ }
}
/**
diff --git a/packages/puppeteer-core/src/cdp/Browser.ts b/packages/puppeteer-core/src/cdp/Browser.ts
index ea7e8b82915..7698acd1642 100644
--- a/packages/puppeteer-core/src/cdp/Browser.ts
+++ b/packages/puppeteer-core/src/cdp/Browser.ts
@@ -8,6 +8,7 @@ import type {ChildProcess} from 'child_process';
import type {Protocol} from 'devtools-protocol';
+import type {DebugInfo} from '../api/Browser.js';
import {
Browser as BrowserBase,
BrowserEvent,
@@ -417,6 +418,12 @@ export class CdpBrowser extends BrowserBase {
#getVersion(): Promise {
return this.#connection.send('Browser.getVersion');
}
+
+ override get debugInfo(): DebugInfo {
+ return {
+ pendingProtocolErrors: this.#connection.getPendingProtocolErrors(),
+ };
+ }
}
/**
diff --git a/packages/puppeteer-core/src/cdp/CDPSession.ts b/packages/puppeteer-core/src/cdp/CDPSession.ts
index 324d092a552..fe5faa56473 100644
--- a/packages/puppeteer-core/src/cdp/CDPSession.ts
+++ b/packages/puppeteer-core/src/cdp/CDPSession.ts
@@ -157,4 +157,11 @@ export class CdpCDPSession extends CDPSession {
override id(): string {
return this.#sessionId;
}
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ return this.#callbacks.getPendingProtocolErrors();
+ }
}
diff --git a/packages/puppeteer-core/src/cdp/Connection.ts b/packages/puppeteer-core/src/cdp/Connection.ts
index 0a11cf4ead9..3c565341b38 100644
--- a/packages/puppeteer-core/src/cdp/Connection.ts
+++ b/packages/puppeteer-core/src/cdp/Connection.ts
@@ -251,6 +251,18 @@ export class Connection extends EventEmitter {
): Promise {
return await this._createSession(targetInfo, false);
}
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ const result: Error[] = [];
+ result.push(...this.#callbacks.getPendingProtocolErrors());
+ for (const session of this.#sessions.values()) {
+ result.push(...session.getPendingProtocolErrors());
+ }
+ return result;
+ }
}
/**
diff --git a/packages/puppeteer-core/src/common/CallbackRegistry.ts b/packages/puppeteer-core/src/common/CallbackRegistry.ts
index fc8b18a5cbf..ea9f3d5abb5 100644
--- a/packages/puppeteer-core/src/common/CallbackRegistry.ts
+++ b/packages/puppeteer-core/src/common/CallbackRegistry.ts
@@ -94,6 +94,19 @@ export class CallbackRegistry {
}
this.#callbacks.clear();
}
+
+ /**
+ * @internal
+ */
+ getPendingProtocolErrors(): Error[] {
+ const result: Error[] = [];
+ for (const callback of this.#callbacks.values()) {
+ result.push(
+ new Error(`${callback.label} timed out. Trace: ${callback.error.stack}`)
+ );
+ }
+ return result;
+ }
}
/**
* @internal
diff --git a/test/TestExpectations.json b/test/TestExpectations.json
index c53ec37f747..18b5b5ec988 100644
--- a/test/TestExpectations.json
+++ b/test/TestExpectations.json
@@ -41,6 +41,12 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
+ {
+ "testIdPattern": "[debugInfo.spec] *",
+ "platforms": ["darwin", "linux", "win32"],
+ "parameters": ["webDriverBiDi"],
+ "expectations": ["PASS"]
+ },
{
"testIdPattern": "[device-request-prompt.spec] *",
"platforms": ["darwin", "linux", "win32"],
@@ -3366,7 +3372,7 @@
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
- "expectations": ["PASS"]
+ "expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
diff --git a/test/src/debugInfo.spec.ts b/test/src/debugInfo.spec.ts
new file mode 100644
index 00000000000..079107cab7c
--- /dev/null
+++ b/test/src/debugInfo.spec.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2024 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import expect from 'expect';
+
+import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
+
+describe('DebugInfo', function () {
+ setupTestBrowserHooks();
+
+ describe('Browser.debugInfo', function () {
+ it('should work', async () => {
+ const {page, browser} = await getTestState();
+
+ const promise = page.evaluate(() => {
+ return new Promise(resolve => {
+ // @ts-expect-error another context
+ window.resolve = resolve;
+ });
+ });
+ try {
+ expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(1);
+ } finally {
+ await page.evaluate(() => {
+ // @ts-expect-error another context
+ window.resolve();
+ });
+ }
+ await promise;
+ expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(0);
+ });
+ });
+});