[go: nahoru, domu]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(entity-classification): classify chrome extensions into separate entities #15017

Merged
merged 14 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions core/computed/entity-classification.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,51 @@ import thirdPartyWeb from '../lib/third-party-web.js';
/** @typedef {Map<string, LH.Artifacts.Entity>} EntityCache */

class EntityClassification {
/**
* @param {EntityCache} entityCache
* @param {string} url
* @param {string=} optionalName
Copy link
Member
@adamraine adamraine May 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
* @param {string=} optionalName
* @param {string=} name

* @return {LH.Artifacts.Entity}
*/
static makupChromeExtensionEntity(entityCache, url, optionalName) {
const origin = Util.getChromeExtensionOrigin(url);
const host = new URL(origin).host;
const name = optionalName || host;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm defaulting unknown chrome extensions' names to the host part of the extension origin URL, i.e., foobar in chrome-extension://foobar/baz.js. This should help searching on the web with that string to reach the extension.


const cachedEntity = entityCache.get(origin);
if (cachedEntity) return cachedEntity;

const chromeExtensionEntity = {
name,
company: name,
category: 'Chrome Extension',
homepage: 'https://chromewebstore.google.com/detail/' + host,
categories: [],
domains: [],
averageExecutionTime: 0,
totalExecutionTime: 0,
totalOccurrences: 0,
};

entityCache.set(origin, chromeExtensionEntity);
return chromeExtensionEntity;
}

/**
* @param {EntityCache} entityCache
* @param {string} url
* @return {LH.Artifacts.Entity | undefined}
*/
static makeUpAnEntity(entityCache, url) {
if (!UrlUtils.isValid(url)) return;
// We can make up an entity only for those URLs with a valid domain attached.
// So we further restrict from allowed URLs to (http/https).
if (!Util.createOrReturnURL(url).protocol.startsWith('http')) return;

const parsedUrl = Util.createOrReturnURL(url);
if (parsedUrl.protocol === 'chrome-extension:') {
return EntityClassification.makupChromeExtensionEntity(entityCache, url);
}

// Make up an entity only for valid http/https URLs.
if (!parsedUrl.protocol.startsWith('http')) return;

const rootDomain = Util.getRootDomain(url);
if (!rootDomain) return;
Expand All @@ -43,6 +78,24 @@ class EntityClassification {
return unrecognizedEntity;
}

/**
* Preload Chrome extensions found in the devtoolsLog into cache.
* @param {EntityCache} entityCache
* @param {LH.DevtoolsLog} devtoolsLog
*/
static preloadChromeExtensionsToCache_(entityCache, devtoolsLog) {
for (const entry of devtoolsLog.values()) {
if (entry.method !== 'Runtime.executionContextCreated') continue;

const origin = entry.params.context.origin;
if (!origin.startsWith('chrome-extension:')) continue;
if (entityCache.has(origin)) continue;

EntityClassification.makupChromeExtensionEntity(entityCache, origin,
entry.params.context.name);
}
}

/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
Expand All @@ -57,6 +110,8 @@ class EntityClassification {
/** @type {Map<LH.Artifacts.Entity, Set<string>>} */
const urlsByEntity = new Map();

EntityClassification.preloadChromeExtensionsToCache_(madeUpEntityCache, data.devtoolsLog);

for (const record of networkRecords) {
const {url} = record;
if (entityByUrl.has(url)) continue;
Expand Down
4 changes: 4 additions & 0 deletions core/lib/url-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
static getOrigin(url) {
try {
const urlInfo = new URL(url);
if (urlInfo.protocol === 'chrome-extension:') {
// Chrome extensions return string "null" as origin, so we reconstruct the extension origin.
return Util.getChromeExtensionOrigin(url);
}

Check warning on line 93 in core/lib/url-utils.js

View check run for this annotation

Codecov / codecov/patch

core/lib/url-utils.js#L91-L93

Added lines #L91 - L93 were not covered by tests
// check for both host and origin since some URLs schemes like data and file set origin to the
// string "null" instead of the object
return (urlInfo.host && urlInfo.origin) || null;
Expand Down
52 changes: 51 additions & 1 deletion core/test/computed/entity-classification-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,58 @@ describe('Entity Classification computed artifact', () => {
// Make sure only valid network urls with a domain is recognized.
expect(entities).toEqual(['third-party.com']);
expect(result.entityByUrl.size).toBe(1);
// First party check returns false for non-DT-log URLs.
expect(result.isFirstParty('chrome://version')).toEqual(false);
});

it('classifies chrome-extension URLs and resolves their names', async () => {
artifacts.URL = {
mainDocumentUrl: 'http://third-party.com',
};
artifacts.devtoolsLog = networkRecordsToDevtoolsLog([
{url: 'http://third-party.com'},
{url: 'data:foobar'},
{'url': 'chrome-extension://abcdefghijklmnopqrstuvwxyz/foo/bar.js'},
{'url': 'chrome-extension://nonresolvablechromextension/bar/baz.js'},
]);

// Inject an executionContextCreated entry to resolve extension names
artifacts.devtoolsLog.push({
method: 'Runtime.executionContextCreated',
params: {
context: {
origin: 'chrome-extension://abcdefghijklmnopqrstuvwxyz',
name: 'Sample Chrome Extension',
},
},
});

const result = await EntityClassification.request(artifacts, context);
const entities = Array.from(result.urlsByEntity.keys()).map(e => e.name);
// Make sure first party is identified.
expect(result.firstParty.name).toBe('third-party.com');
// Make sure only valid network urls with a domain is recognized.
expect(entities).toEqual(['third-party.com', 'Sample Chrome Extension',
'nonresolvablechromextension']);

const extensionEntity = result.entityByUrl
.get('chrome-extension://abcdefghijklmnopqrstuvwxyz/foo/bar.js');
expect(extensionEntity).toHaveProperty('category', 'Chrome Extension');
expect(extensionEntity).toHaveProperty('name', 'Sample Chrome Extension');
expect(extensionEntity).toHaveProperty('homepage',
'https://chromewebstore.google.com/detail/abcdefghijklmnopqrstuvwxyz');

const extensionUnknownEntity = result.entityByUrl
.get('chrome-extension://nonresolvablechromextension/bar/baz.js');
expect(extensionUnknownEntity).toHaveProperty('category', 'Chrome Extension');
expect(extensionUnknownEntity).toHaveProperty('name', 'nonresolvablechromextension');
expect(extensionUnknownEntity).toHaveProperty('homepage',
'https://chromewebstore.google.com/detail/nonresolvablechromextension');

expect(result.entityByUrl.size).toBe(3);
// First party check fails for non-DT-log URLs.
expect(result.isFirstParty('chrome-extension://abcdefghijklmnopqrstuvwxyz/foo/bar.js')).toEqual(false);
expect(result.isFirstParty('chrome-extension://abcdefghijklmnopqrstuvwxyz/foo/bar.js'))
.toEqual(false);
expect(result.isFirstParty('chrome://new-tab-page')).toEqual(false);
});
});
13 changes: 10 additions & 3 deletions report/test/renderer/details-renderer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,8 @@ describe('DetailsRenderer', () => {
entities: [
{name: 'example.com', category: 'Cat', isFirstParty: true},
{name: 'cdn.com', category: 'CDN'},
{name: 'Sample Chrome Extension', category: 'Chrome Extension',
origins: ['chrome-extension://abcdefghijklmnopqrstuvwxyz']},
],
});

Expand Down Expand Up @@ -583,7 +585,7 @@ describe('DetailsRenderer', () => {
{url: 'https://example.com/1', totalBytes: 100, wastedBytes: 500, entity: 'example.com'},
{url: 'https://cdn.com/1', totalBytes: 300, wastedBytes: 700, entity: 'cdn.com'},
{url: 'https://cdn.com/2', totalBytes: 400, wastedBytes: 800, entity: 'cdn.com'},
{url: 'chrome-extension://abcdefghijklmnopqrstuvwxyz/foo/bar.js', totalBytes: 300, wastedBytes: 700},
{url: 'chrome-extension://abcdefghijklmnopqrstuvwxyz/foo/bar.js', totalBytes: 300, wastedBytes: 700, entity: 'Sample Chrome Extension'},
{url: 'chrome://new-tab-page', totalBytes: 300, wastedBytes: 700},
{url: 'Unattributable', totalBytes: 500, wastedBytes: 500}, // entity not marked.
],
Expand All @@ -601,10 +603,15 @@ describe('DetailsRenderer', () => {
);
assert.deepStrictEqual(
[...el.querySelectorAll('.lh-row--group')[2].children].map(td => td.textContent),
['Unattributable', '1.1 KiB', '1.9 KiB'],
['Sample Chrome Extension Chrome Extension', '0.3 KiB', '0.7 KiB'],
'did not render Chrome Extensions row'
);
assert.deepStrictEqual(
[...el.querySelectorAll('.lh-row--group')[3].children].map(td => td.textContent),
['Unattributable', '0.8 KiB', '1.2 KiB'],
'did not render all Unattributable row'
);
assert.equal(el.querySelectorAll('tr').length, 10, `did not render ${tableType} rows`);
assert.equal(el.querySelectorAll('tr').length, 11, `did not render ${tableType} rows`);
});

it('does not group if entity classification is absent', () => {
Expand Down
13 changes: 12 additions & 1 deletion shared/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,16 @@ class Util {
return name;
}

/**
* Returns the origin portion of a Chrome extension URL.
* @param {string} url
* @return {string}
*/
static getChromeExtensionOrigin(url) {
const parsedUrl = new URL(url);
return parsedUrl.protocol + '//' + parsedUrl.host;
}

/**
* Split a URL into a file, hostname and origin for easy display.
* @param {string} url
Expand All @@ -270,7 +280,8 @@ class Util {
return {
file: Util.getURLDisplayName(parsedUrl),
hostname: parsedUrl.hostname,
origin: parsedUrl.origin,
origin: parsedUrl.protocol === 'chrome-extension:' ?
alexnj marked this conversation as resolved.
Show resolved Hide resolved
adamraine marked this conversation as resolved.
Show resolved Hide resolved
Util.getChromeExtensionOrigin(url) : parsedUrl.origin,
};
}

Expand Down
Loading