[go: nahoru, domu]

Skip to content

Commit

Permalink
core(entity-classification): classify chrome extensions into separate…
Browse files Browse the repository at this point in the history
… entities (#15017)
  • Loading branch information
alexnj committed May 16, 2023
1 parent fa2e00c commit 8ecbb94
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 11 deletions.
67 changes: 61 additions & 6 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=} extensionName
* @return {LH.Artifacts.Entity}
*/
static makeupChromeExtensionEntity_(entityCache, url, extensionName) {
const origin = Util.getChromeExtensionOrigin(url);
const host = new URL(origin).host;
const name = extensionName || host;

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) {
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.makeupChromeExtensionEntity_(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.makeupChromeExtensionEntity_(entityCache, origin,
entry.params.context.name);
}
}

/**
* @param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
Expand All @@ -57,12 +110,14 @@ 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;

const entity = thirdPartyWeb.getEntity(url) ||
EntityClassification.makeUpAnEntity(madeUpEntityCache, url);
EntityClassification._makeUpAnEntity(madeUpEntityCache, url);
if (!entity) continue;

const entityURLs = urlsByEntity.get(entity) || new Set();
Expand All @@ -76,7 +131,7 @@ class EntityClassification {
// See https://github.com/GoogleChrome/lighthouse/issues/13706
const firstPartyUrl = data.URL.mainDocumentUrl || data.URL.finalDisplayedUrl;
const firstParty = thirdPartyWeb.getEntity(firstPartyUrl) ||
EntityClassification.makeUpAnEntity(madeUpEntityCache, firstPartyUrl);
EntityClassification._makeUpAnEntity(madeUpEntityCache, firstPartyUrl);

/**
* Convenience function to check if a URL belongs to first party.
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 @@ class UrlUtils {
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 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
15 changes: 14 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,10 @@ class Util {
return {
file: Util.getURLDisplayName(parsedUrl),
hostname: parsedUrl.hostname,
origin: parsedUrl.origin,
// Node's URL parsing behavior is different than Chrome and returns 'null'
// for chrome-extension:// URLs. See https://github.com/nodejs/node/issues/21955.
origin: parsedUrl.protocol === 'chrome-extension:' ?
Util.getChromeExtensionOrigin(url) : parsedUrl.origin,
};
}

Expand Down

0 comments on commit 8ecbb94

Please sign in to comment.