Skip to content

Commit 8d48ff8

Browse files
powdercloudDevtools-frontend LUCI CQ
authored andcommitted
Add CrUX Vis link to live metrics view (perf panel landing page).
This adds a link to the CrUX Vis viewer (https://cruxvis.withgoogle.com/) for field data to the live metrics view. To be consistent with PageSpeed Insights, the link is placed in parenthesis after the collection period. E.g., Collection period: Sep 15, 2025 - Oct 12, 2025 (View more history) Note that when the collection period begin / end aren't rendered, then this "(View more history)" link also won't be rendered. Also, the actual link will use the same "scope" that the user has made in the panel, that is url-level vs. origin level data as well as device type. To get this done I added a normalizedUrl field to the CrUXManager; this makes it so that we use the same URL for CrUX vis that we're using to make CrUX API requests. Bug: 405467537 Change-Id: I63271c6eab7ba30c69b948a07954b09a6e807a8c Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7040531 Commit-Queue: Johannes Henkel <johannes@chromium.org> Reviewed-by: Connor Clark <cjamcl@chromium.org> Auto-Submit: Johannes Henkel <johannes@chromium.org> Reviewed-by: Paul Irish <paulirish@chromium.org>
1 parent 15d6638 commit 8d48ff8

File tree

6 files changed

+82
-5
lines changed

6 files changed

+82
-5
lines changed

front_end/models/crux-manager/CrUXManager.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ describeWithMockConnection('CrUXManager', () => {
146146
'url-PHONE': mockResponse(),
147147
'url-TABLET': null,
148148
warnings: [],
149+
normalizedUrl: 'https://example.com/',
149150
});
150151

151152
assert.deepEqual(fetchBodies, [
@@ -261,6 +262,7 @@ describeWithMockConnection('CrUXManager', () => {
261262
'url-PHONE': null,
262263
'url-TABLET': null,
263264
warnings: [],
265+
normalizedUrl: 'https://example.com/',
264266
});
265267
});
266268

@@ -352,6 +354,7 @@ describeWithMockConnection('CrUXManager', () => {
352354
'url-PHONE': mockResponse({pageScope: 'url', deviceScope: 'PHONE'}),
353355
'url-TABLET': null,
354356
warnings: [],
357+
normalizedUrl: '',
355358
});
356359
});
357360

@@ -528,6 +531,7 @@ describeWithMockConnection('CrUXManager', () => {
528531
'url-PHONE': null,
529532
'url-TABLET': null,
530533
warnings: [],
534+
normalizedUrl: '',
531535
});
532536
});
533537

front_end/models/crux-manager/CrUXManager.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export interface CrUXResponse {
8787

8888
export type PageResult = Record<`${PageScope}-${DeviceScope}`, CrUXResponse|null>&{
8989
warnings: string[],
90+
normalizedUrl: string,
9091
};
9192

9293
export interface OriginMapping {
@@ -194,10 +195,12 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
194195
'url-PHONE': null,
195196
'url-TABLET': null,
196197
warnings: [],
198+
normalizedUrl: '',
197199
};
198200

199201
try {
200202
const normalizedUrl = this.#normalizeUrl(pageUrl);
203+
pageResult.normalizedUrl = normalizedUrl.href;
201204
const promises: Array<Promise<void>> = [];
202205

203206
for (const pageScope of pageScopeList) {
@@ -250,11 +253,11 @@ export class CrUXManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
250253
*/
251254
async #getFieldDataForCurrentPage(): Promise<PageResult> {
252255
const currentUrl = this.#mainDocumentUrl || await this.#getInspectedURL();
253-
const urlForCrux = this.#configSetting.get().overrideEnabled ? this.#configSetting.get().override || '' :
254-
this.#getMappedUrl(currentUrl);
256+
const normalizedUrl = this.#configSetting.get().overrideEnabled ? this.#configSetting.get().override || '' :
257+
this.#getMappedUrl(currentUrl);
255258

256-
const result = await this.getFieldDataForPage(urlForCrux);
257-
if (currentUrl !== urlForCrux) {
259+
const result = await this.getFieldDataForPage(normalizedUrl);
260+
if (currentUrl !== normalizedUrl) {
258261
result.warnings.push(i18nString(UIStrings.fieldOverrideWarning));
259262
}
260263
return result;

front_end/panels/timeline/components/FieldSettingsDialog.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ describeWithMockConnection('FieldSettingsDialog', () => {
7979
'url-PHONE': null,
8080
'url-TABLET': null,
8181
warnings: [],
82+
normalizedUrl: '',
8283
};
8384

8485
cruxManager.getConfigSetting().set({enabled: false, override: ''});

front_end/panels/timeline/components/LiveMetricsView.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ function getFieldMessage(view: Element): HTMLElement|null {
8585
return view.shadowRoot!.querySelector('#field-setup .field-data-message');
8686
}
8787

88+
function getFieldDataHistoryLink(view: Element): HTMLElement|null {
89+
return view.shadowRoot!.querySelector<HTMLElement>('#field-setup .field-data-message .local-field-link');
90+
}
91+
8892
function getLiveMetricsTitle(view: Element): HTMLElement {
8993
// There may be multiple, but this should always be the first one.
9094
return view.shadowRoot!.querySelector('.live-metrics > .section-title')!;
@@ -709,6 +713,7 @@ describeWithMockConnection('LiveMetricsView', () => {
709713
'url-PHONE': null,
710714
'url-TABLET': null,
711715
warnings: [],
716+
normalizedUrl: '',
712717
};
713718

714719
sinon.stub(CrUXManager.CrUXManager.instance(), 'getFieldDataForPage').callsFake(async () => mockFieldData);
@@ -800,6 +805,33 @@ describeWithMockConnection('LiveMetricsView', () => {
800805
assert.match(fieldMessage!.textContent!, /Warning from crux/);
801806
});
802807

808+
it('Should display field data history link', async () => {
809+
const view = renderLiveMetrics();
810+
811+
await RenderCoordinator.done();
812+
813+
mockFieldData['url-ALL'] = createMockFieldData();
814+
mockFieldData.normalizedUrl = 'https://www.example.com/';
815+
816+
target.model(SDK.ResourceTreeModel.ResourceTreeModel)
817+
?.dispatchEventToListeners(SDK.ResourceTreeModel.Events.FrameNavigated, {
818+
url: 'https://example.com',
819+
isPrimaryFrame: () => true,
820+
} as SDK.ResourceTreeModel.ResourceTreeFrame);
821+
822+
await RenderCoordinator.done();
823+
824+
const fieldLink = getFieldDataHistoryLink(view);
825+
assert.include(fieldLink!.textContent, 'View history');
826+
assert.strictEqual(
827+
fieldLink!.getAttribute('href'),
828+
'https://cruxvis.withgoogle.com/#/?' +
829+
'view=cwvsummary&' +
830+
'url=https%3A%2F%2Fwww.example.com%2F&' +
831+
'identifier=url&' +
832+
'device=ALL');
833+
});
834+
803835
it('should make initial request on render when crux is enabled', async () => {
804836
mockFieldData['url-ALL'] = createMockFieldData();
805837

front_end/panels/timeline/components/LiveMetricsView.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ const UIStrings = {
5858
* @description Title of a view that shows performance metrics from the local environment.
5959
*/
6060
localMetrics: 'Local metrics',
61+
/**
62+
*@description Text for the link to the historical field data for the specific URL or origin that is shown. This link text appears in parenthesis after the collection period information in the field data dialog. The link opens the CrUX Vis viewer (https://cruxvis.withgoogle.com).
63+
*/
64+
fieldDataHistoryLink: 'View history',
65+
/**
66+
*@description Tooltip for the CrUX Vis viewer link which shows the history of the field data for the specific URL or origin.
67+
*/
68+
fieldDataHistoryTooltip: 'View field data history in CrUX Vis',
6169
/**
6270
* @description Accessible label for a section that logs user interactions and layout shifts. A layout shift is an event that shifts content in the layout of the page causing a jarring experience for the user.
6371
*/
@@ -840,6 +848,32 @@ export class LiveMetricsView extends LegacyWrapper.LegacyWrapper.WrappableCompon
840848
});
841849
}
842850

851+
#renderFieldDataHistoryLink(): Lit.LitTemplate {
852+
if (!this.#cruxManager.getConfigSetting().get().enabled) {
853+
return Lit.nothing;
854+
}
855+
const normalizedUrl = this.#cruxManager.pageResult?.normalizedUrl;
856+
if (!normalizedUrl) {
857+
return Lit.nothing;
858+
}
859+
const tmp = new URL('https://cruxvis.withgoogle.com/');
860+
tmp.searchParams.set('view', 'cwvsummary');
861+
tmp.searchParams.set('url', normalizedUrl);
862+
// identifier must be 'origin' or 'url'.
863+
const identifier = this.#cruxManager.fieldPageScope;
864+
tmp.searchParams.set('identifier', identifier);
865+
// device must be one 'PHONE', 'DESKTOP', 'TABLET', or 'ALL'.
866+
const device = this.#cruxManager.getSelectedDeviceScope();
867+
tmp.searchParams.set('device', device);
868+
const cruxVis = `${tmp.origin}/#/${tmp.search}`;
869+
return html`
870+
(<x-link href=${cruxVis}
871+
class="local-field-link"
872+
title=${i18nString(UIStrings.fieldDataHistoryTooltip)}
873+
>${i18nString(UIStrings.fieldDataHistoryLink)}</x-link>)
874+
`;
875+
}
876+
843877
#renderCollectionPeriod(): Lit.LitTemplate {
844878
const range = this.#getCollectionPeriodRange();
845879

@@ -851,11 +885,13 @@ export class LiveMetricsView extends LegacyWrapper.LegacyWrapper.WrappableCompon
851885
PH1: dateEl,
852886
});
853887

888+
const fieldDataHistoryLink = range ? this.#renderFieldDataHistoryLink() : Lit.nothing;
889+
854890
const warnings = this.#cruxManager.pageResult?.warnings || [];
855891

856892
return html`
857893
<div class="field-data-message">
858-
<div>${message}</div>
894+
<div>${message} ${fieldDataHistoryLink}</div>
859895
${warnings.map(warning => html`
860896
<div class="field-data-warning">${warning}</div>
861897
`)}

front_end/panels/timeline/components/OriginMap.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describeWithMockConnection('OriginMap', () => {
114114
'url-PHONE': null,
115115
'url-TABLET': null,
116116
warnings: [],
117+
normalizedUrl: '',
117118
};
118119

119120
cruxManager.getConfigSetting().set({enabled: true, override: ''});

0 commit comments

Comments
 (0)