Skip to content

Commit 6caed10

Browse files
committed
initial
Change-Id: I8a927a6b4d41ade5bd6b77996f27a4b1850f4316
1 parent 796aed7 commit 6caed10

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

src/tools/performance.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,76 @@ async function stopTracingAndAppendOutput(
185185
context.setIsRunningPerformanceTrace(false);
186186
}
187187
}
188+
189+
const CRUX_API_KEY = 'AIzaSyCCSOx25vrb5z0tbedCB3_JRzzbVW6Uwgw';
190+
const CRUX_ENDPOINT = `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${CRUX_API_KEY}`;
191+
192+
export const queryChromeUXReport = defineTool({
193+
name: 'performance_query_chrome_ux_report',
194+
description:
195+
'Queries the Chrome UX Report (CrUX) API to get real-user experience metrics (like Core Web Vitals) for a given URL or origin. You must provide EITHER "origin" OR "url", but not both. You can optionally filter by "formFactor".',
196+
annotations: {
197+
category: ToolCategory.PERFORMANCE,
198+
readOnlyHint: true,
199+
},
200+
schema: {
201+
origin: zod
202+
.string()
203+
.describe(
204+
'The origin to query, e.g., "https://www.google.com". Do not provide this if "url" is specified.',
205+
)
206+
.optional(),
207+
url: zod
208+
.string()
209+
.describe(
210+
'The specific page URL to query, e.g., "https://www.google.com/search?q=puppies". Do not provide this if "origin" is specified.',
211+
)
212+
.optional(),
213+
formFactor: zod
214+
.enum(['DESKTOP', 'PHONE', 'TABLET'])
215+
.describe(
216+
'The form factor to filter by. If omitted, data for all form factors is aggregated.',
217+
)
218+
.optional(),
219+
},
220+
handler: async (request, response) => {
221+
const {origin, url, formFactor} = request.params;
222+
223+
if ((!origin && !url) || (origin && url)) {
224+
response.appendResponseLine(
225+
'Error: you must provide either "origin" or "url", but not both.',
226+
);
227+
return;
228+
}
229+
230+
const body = JSON.stringify({
231+
origin,
232+
url,
233+
formFactor,
234+
metrics: [
235+
'first_contentful_paint',
236+
'largest_contentful_paint',
237+
'cumulative_layout_shift',
238+
'interaction_to_next_paint',
239+
],
240+
});
241+
242+
try {
243+
const cruxResponse = await fetch(CRUX_ENDPOINT, {
244+
method: 'POST',
245+
headers: {
246+
'Content-Type': 'application/json',
247+
},
248+
body,
249+
});
250+
251+
const data = await cruxResponse.json();
252+
response.appendResponseLine(JSON.stringify(data, null, 2));
253+
} catch (e) {
254+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
255+
logger(`Error fetching CrUX data: ${errorText}`);
256+
response.appendResponseLine('An error occurred fetching CrUX data:');
257+
response.appendResponseLine(errorText);
258+
}
259+
},
260+
});

tests/tools/performance.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import sinon from 'sinon';
1010

1111
import {
1212
analyzeInsight,
13+
queryChromeUXReport,
1314
startTrace,
1415
stopTrace,
1516
} from '../../src/tools/performance.js';
@@ -272,4 +273,113 @@ describe('performance', () => {
272273
});
273274
});
274275
});
276+
277+
describe('performance_query_chrome_ux_report', () => {
278+
it('successfully queries with origin', async () => {
279+
const mockResponse = {record: {key: {origin: 'https://example.com'}}};
280+
const fetchStub = sinon.stub(global, 'fetch').resolves(
281+
new Response(JSON.stringify(mockResponse), {
282+
status: 200,
283+
headers: {'Content-Type': 'application/json'},
284+
}),
285+
);
286+
287+
await withBrowser(async response => {
288+
await queryChromeUXReport.handler(
289+
{params: {origin: 'https://example.com'}},
290+
response,
291+
{} as any,
292+
);
293+
294+
assert.ok(fetchStub.calledOnce);
295+
assert.strictEqual(
296+
response.responseLines[0],
297+
JSON.stringify(mockResponse, null, 2),
298+
);
299+
});
300+
});
301+
302+
it('successfully queries with url', async () => {
303+
const mockResponse = {record: {key: {url: 'https://example.com'}}};
304+
const fetchStub = sinon.stub(global, 'fetch').resolves(
305+
new Response(JSON.stringify(mockResponse), {
306+
status: 200,
307+
headers: {'Content-Type': 'application/json'},
308+
}),
309+
);
310+
311+
await withBrowser(async response => {
312+
await queryChromeUXReport.handler(
313+
{params: {url: 'https://example.com'}},
314+
response,
315+
{} as any,
316+
);
317+
318+
assert.ok(fetchStub.calledOnce);
319+
assert.strictEqual(
320+
response.responseLines[0],
321+
JSON.stringify(mockResponse, null, 2),
322+
);
323+
});
324+
});
325+
326+
it('errors if both origin and url are provided', async () => {
327+
await withBrowser(async response => {
328+
await queryChromeUXReport.handler(
329+
{
330+
params: {
331+
origin: 'https://example.com',
332+
url: 'https://example.com',
333+
},
334+
},
335+
response,
336+
{} as any,
337+
);
338+
339+
assert.ok(
340+
response.responseLines[0]?.includes(
341+
'Error: you must provide either "origin" or "url", but not both.',
342+
),
343+
);
344+
});
345+
});
346+
347+
it('errors if neither origin nor url are provided', async () => {
348+
await withBrowser(async response => {
349+
await queryChromeUXReport.handler(
350+
{params: {}},
351+
response,
352+
{} as any,
353+
);
354+
355+
assert.ok(
356+
response.responseLines[0]?.includes(
357+
'Error: you must provide either "origin" or "url", but not both.',
358+
),
359+
);
360+
});
361+
});
362+
363+
it('handles fetch API error', async () => {
364+
const fetchStub = sinon
365+
.stub(global, 'fetch')
366+
.rejects(new Error('API is down'));
367+
368+
await withBrowser(async response => {
369+
await queryChromeUXReport.handler(
370+
{params: {origin: 'https://example.com'}},
371+
response,
372+
{} as any,
373+
);
374+
375+
assert.ok(fetchStub.calledOnce);
376+
assert.ok(
377+
response.responseLines[0]?.includes(
378+
'An error occurred fetching CrUX data:',
379+
),
380+
);
381+
assert.strictEqual(response.responseLines[1], 'API is down');
382+
});
383+
});
384+
});
275385
});

0 commit comments

Comments
 (0)