Skip to content

Commit abfb3c3

Browse files
committed
chore: structured content for console
1 parent a5134c6 commit abfb3c3

5 files changed

Lines changed: 240 additions & 28 deletions

File tree

src/McpResponse.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
432432
tabId?: string;
433433
networkRequest?: object;
434434
networkRequests?: object[];
435+
detailedConsoleMessage?: object;
436+
consoleMessages?: object[];
435437
} = {};
436438

437439
if (this.#tabId) {
@@ -457,6 +459,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
457459
response.push(
458460
...this.#formatConsoleData(context, data.detailedConsoleMessage),
459461
);
462+
if (data.detailedConsoleMessage) {
463+
structuredContent.detailedConsoleMessage =
464+
data.detailedConsoleMessage.toJSONDetailed();
465+
}
460466

461467
if (this.#networkRequestsOptions?.include) {
462468
let requests = context.getNetworkRequests(
@@ -511,6 +517,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
511517
return message.toString();
512518
}),
513519
);
520+
structuredContent.consoleMessages = data.items.map(message =>
521+
message.toJSON(),
522+
);
514523
} else {
515524
response.push('<no console messages found>');
516525
}

src/formatters/ConsoleFormatter.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,27 @@ export class ConsoleFormatter {
171171
}
172172
return result;
173173
}
174+
toJSON(): object {
175+
return {
176+
type: this.#getType(),
177+
text: this.#getText(),
178+
argsCount:
179+
this.#msg instanceof Error
180+
? 0
181+
: this.#resolvedArgs.length || this.#msg.args().length,
182+
id: this.#id,
183+
};
184+
}
185+
186+
toJSONDetailed(): object {
187+
return {
188+
id: this.#id,
189+
type: this.#getType(),
190+
text: this.#getText(),
191+
args: this.#getArgs().map(arg =>
192+
typeof arg === 'object' ? arg : String(arg),
193+
),
194+
stackTrace: this.#resolvedStackTrace,
195+
};
196+
}
174197
}

src/formatters/IssueFormatter.ts

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export interface IssueFormatterOptions {
1414
id?: number;
1515
}
1616

17+
export interface AffectedResource {
18+
uid?: string;
19+
data?: unknown;
20+
request?: string | number;
21+
}
22+
1723
export class IssueFormatter {
1824
#issue: DevTools.AggregatedIssue;
1925
#options: IssueFormatterOptions;
@@ -59,6 +65,55 @@ export class IssueFormatter {
5965
}
6066
}
6167

68+
const affectedResources = this.#getAffectedResources();
69+
if (affectedResources.length) {
70+
bodyParts.push('### Affected resources');
71+
bodyParts.push(
72+
...affectedResources.map(item => {
73+
const details = [];
74+
if (item.uid) {
75+
details.push(`uid=${item.uid}`);
76+
}
77+
if (item.request) {
78+
details.push(
79+
(typeof item.request === 'number' ? `reqid=` : 'url=') +
80+
item.request,
81+
);
82+
}
83+
if (item.data) {
84+
details.push(`data=${JSON.stringify(item.data)}`);
85+
}
86+
return details.join(' ');
87+
}),
88+
);
89+
}
90+
91+
result.push(`Message: issue> ${bodyParts.join('\n')}`);
92+
93+
return result.join('\n');
94+
}
95+
96+
toJSON(): object {
97+
return {
98+
type: 'issue',
99+
title: this.#getTitle(),
100+
count: this.#issue.getAggregatedIssuesCount(),
101+
id: this.#options.id,
102+
};
103+
}
104+
105+
toJSONDetailed(): object {
106+
return {
107+
id: this.#options.id,
108+
type: 'issue',
109+
title: this.#getTitle(),
110+
description: this.#getDescription(),
111+
links: this.#issue.getDescription()?.links,
112+
affectedResources: this.#getAffectedResources(),
113+
};
114+
}
115+
116+
#getAffectedResources(): AffectedResource[] {
62117
const issues = this.#issue.getAllIssues();
63118
const affectedResources: Array<{
64119
uid?: string;
@@ -73,8 +128,10 @@ export class IssueFormatter {
73128

74129
// We send the remaining details as untyped JSON because the DevTools
75130
// frontend code is currently not re-usable.
76-
// eslint-disable-next-line
77-
const data = structuredClone(details) as any;
131+
const data = structuredClone(details) as unknown as Record<
132+
string,
133+
unknown
134+
>;
78135

79136
let uid;
80137
let request: number | string | undefined;
@@ -111,7 +168,8 @@ export class IssueFormatter {
111168
);
112169
if (resolvedId) {
113170
request = resolvedId;
114-
delete data.request.requestId;
171+
const requestData = data.request as Record<string, unknown>;
172+
delete requestData.requestId;
115173
}
116174
}
117175
}
@@ -125,31 +183,7 @@ export class IssueFormatter {
125183
request,
126184
});
127185
}
128-
if (affectedResources.length) {
129-
bodyParts.push('### Affected resources');
130-
bodyParts.push(
131-
...affectedResources.map(item => {
132-
const details = [];
133-
if (item.uid) {
134-
details.push(`uid=${item.uid}`);
135-
}
136-
if (item.request) {
137-
details.push(
138-
(typeof item.request === 'number' ? `reqid=` : 'url=') +
139-
item.request,
140-
);
141-
}
142-
if (item.data) {
143-
details.push(`data=${JSON.stringify(item.data)}`);
144-
}
145-
return details.join(' ');
146-
}),
147-
);
148-
}
149-
150-
result.push(`Message: issue> ${bodyParts.join('\n')}`);
151-
152-
return result.join('\n');
186+
return affectedResources;
153187
}
154188

155189
isValid(): boolean {

tests/formatters/ConsoleFormatter.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import assert from 'node:assert';
78
import {describe, it} from 'node:test';
89

910
import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js';
@@ -163,4 +164,77 @@ describe('ConsoleFormatter', () => {
163164
t.assert.snapshot?.(result);
164165
});
165166
});
167+
describe('toJSON', () => {
168+
it('formats a console.log message', async () => {
169+
const message = createMockMessage({
170+
type: () => 'log',
171+
text: () => 'Hello, world!',
172+
});
173+
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
174+
assert.deepStrictEqual(result, {
175+
type: 'log',
176+
text: 'Hello, world!',
177+
argsCount: 0,
178+
id: 1,
179+
});
180+
});
181+
182+
it('formats a console.log message with args', async () => {
183+
const message = createMockMessage({
184+
type: () => 'log',
185+
text: () => 'Processing file:',
186+
args: () => [
187+
{jsonValue: async () => 'file.txt'},
188+
{jsonValue: async () => 'another file'},
189+
],
190+
});
191+
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
192+
assert.deepStrictEqual(result, {
193+
type: 'log',
194+
text: 'Processing file:',
195+
argsCount: 2,
196+
id: 1,
197+
});
198+
});
199+
});
200+
201+
describe('toJSONDetailed', () => {
202+
it('formats a console.log message', async () => {
203+
const message = createMockMessage({
204+
type: () => 'log',
205+
text: () => 'Hello, world!',
206+
});
207+
const result = (
208+
await ConsoleFormatter.from(message, {id: 1})
209+
).toJSONDetailed();
210+
assert.deepStrictEqual(result, {
211+
id: 1,
212+
type: 'log',
213+
text: 'Hello, world!',
214+
args: [],
215+
stackTrace: undefined,
216+
});
217+
});
218+
219+
it('formats a console.log message with args', async () => {
220+
const message = createMockMessage({
221+
type: () => 'log',
222+
text: () => 'Processing file:',
223+
args: () => [
224+
{jsonValue: async () => 'file.txt'},
225+
{jsonValue: async () => 'another file'},
226+
],
227+
});
228+
const result = (
229+
await ConsoleFormatter.from(message, {id: 2, fetchDetailedData: true})
230+
).toJSONDetailed();
231+
assert.deepStrictEqual(result, {
232+
id: 2,
233+
type: 'log',
234+
text: 'Processing file:',
235+
args: ['file.txt', 'another file'],
236+
stackTrace: undefined,
237+
});
238+
});
239+
});
166240
});

tests/formatters/IssueFormatter.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,76 @@ describe('IssueFormatter', () => {
134134
assert.ok(detailed.includes('Valid Title'));
135135
});
136136
});
137+
describe('toJSON', () => {
138+
it('formats a simplified issue', () => {
139+
const mockAggregatedIssue = getMockAggregatedIssue();
140+
mockAggregatedIssue.getDescription.returns({
141+
file: 'mock.md',
142+
links: [],
143+
});
144+
mockAggregatedIssue.getAggregatedIssuesCount.returns(5);
145+
getIssueDescriptionStub
146+
.withArgs('mock.md')
147+
.returns('# Issue Title\n\nIssue content');
148+
149+
const formatter = new IssueFormatter(mockAggregatedIssue, {id: 1});
150+
assert.deepStrictEqual(formatter.toJSON(), {
151+
type: 'issue',
152+
title: 'Issue Title',
153+
count: 5,
154+
id: 1,
155+
});
156+
});
157+
});
158+
159+
describe('toJSONDetailed', () => {
160+
it('formats a detailed issue', () => {
161+
const testGenericIssue = {
162+
details: () => {
163+
return {
164+
violatingNodeId: 2,
165+
violatingNodeAttribute: 'test',
166+
};
167+
},
168+
};
169+
const mockAggregatedIssue = getMockAggregatedIssue();
170+
const mockDescription = {
171+
file: 'mock.md',
172+
links: [{link: 'http://example.com', linkTitle: 'Link 1'}],
173+
substitutions: new Map([['PLACEHOLDER_VALUE', 'sub value']]),
174+
};
175+
mockAggregatedIssue.getDescription.returns(mockDescription);
176+
// @ts-expect-error stubbed generic issue does not match the complete type.
177+
mockAggregatedIssue.getAllIssues.returns([testGenericIssue]);
178+
179+
const mockDescriptionFileContent =
180+
'# Mock Issue Title\n\nThis is a mock issue description {PLACEHOLDER_VALUE}';
181+
182+
getIssueDescriptionStub
183+
.withArgs('mock.md')
184+
.returns(mockDescriptionFileContent);
185+
186+
const formatter = new IssueFormatter(mockAggregatedIssue, {
187+
id: 5,
188+
});
189+
190+
const detailedResult = formatter.toJSONDetailed() as unknown as Record<
191+
string,
192+
object
193+
> & {affectedResources: Array<{data: object}>};
194+
assert.strictEqual(detailedResult.id, 5);
195+
assert.strictEqual(detailedResult.type, 'issue');
196+
assert.strictEqual(detailedResult.title, 'Mock Issue Title');
197+
assert.strictEqual(
198+
detailedResult.description,
199+
'# Mock Issue Title\n\nThis is a mock issue description sub value',
200+
);
201+
assert.deepStrictEqual(detailedResult.links, mockDescription.links);
202+
assert.strictEqual(detailedResult.affectedResources.length, 1);
203+
assert.deepStrictEqual(detailedResult.affectedResources[0].data, {
204+
violatingNodeAttribute: 'test',
205+
violatingNodeId: 2,
206+
});
207+
});
208+
});
137209
});

0 commit comments

Comments
 (0)