UNPKG

chrome-devtools-frontend

Version:
342 lines (294 loc) • 12.8 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as IssuesManager from '../../models/issues_manager/issues_manager.js'; import type * as Protocol from '../../generated/protocol.js'; type AggregationKeyTag = { aggregationKeyTag: undefined, }; /** * An opaque type for the key which we use to aggregate issues. The key must be * chosen such that if two aggregated issues have the same aggregation key, then * they also have the same issue code. */ export type AggregationKey = { toString(): string, }&AggregationKeyTag; /** * An `AggregatedIssue` representes a number of `IssuesManager.Issue.Issue` objects that are displayed together. * Currently only grouping by issue code, is supported. The class provides helpers to support displaying * of all resources that are affected by the aggregated issues. */ export class AggregatedIssue extends IssuesManager.Issue.Issue { private affectedCookies = new Map<string, { cookie: Protocol.Audits.AffectedCookie, hasRequest: boolean, }>(); private affectedRawCookieLines = new Map<string, {rawCookieLine: string, hasRequest: boolean}>(); private affectedRequests = new Map<string, Protocol.Audits.AffectedRequest>(); private affectedLocations = new Map<string, Protocol.Audits.SourceCodeLocation>(); private heavyAdIssues = new Set<IssuesManager.HeavyAdIssue.HeavyAdIssue>(); private blockedByResponseDetails = new Map<string, Protocol.Audits.BlockedByResponseIssueDetails>(); private corsIssues = new Set<IssuesManager.CorsIssue.CorsIssue>(); private cspIssues = new Set<IssuesManager.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue>(); private issueKind = IssuesManager.Issue.IssueKind.Improvement; private lowContrastIssues = new Set<IssuesManager.LowTextContrastIssue.LowTextContrastIssue>(); private mixedContentIssues = new Set<IssuesManager.MixedContentIssue.MixedContentIssue>(); private sharedArrayBufferIssues = new Set<IssuesManager.SharedArrayBufferIssue.SharedArrayBufferIssue>(); private trustedWebActivityIssues = new Set<IssuesManager.TrustedWebActivityIssue.TrustedWebActivityIssue>(); private quirksModeIssues = new Set<IssuesManager.QuirksModeIssue.QuirksModeIssue>(); private attributionReportingIssues = new Set<IssuesManager.AttributionReportingIssue.AttributionReportingIssue>(); private wasmCrossOriginModuleSharingIssues = new Set<IssuesManager.WasmCrossOriginModuleSharingIssue.WasmCrossOriginModuleSharingIssue>(); private genericIssues = new Set<IssuesManager.GenericIssue.GenericIssue>(); private representative?: IssuesManager.Issue.Issue; private aggregatedIssuesCount = 0; private key: AggregationKey; constructor(code: string, aggregationKey: AggregationKey) { super(code); this.key = aggregationKey; } override primaryKey(): string { throw new Error('This should never be called'); } aggregationKey(): AggregationKey { return this.key; } getBlockedByResponseDetails(): Iterable<Protocol.Audits.BlockedByResponseIssueDetails> { return this.blockedByResponseDetails.values(); } cookies(): Iterable<Protocol.Audits.AffectedCookie> { return Array.from(this.affectedCookies.values()).map(x => x.cookie); } getRawCookieLines(): Iterable<{rawCookieLine: string, hasRequest: boolean}> { return this.affectedRawCookieLines.values(); } sources(): Iterable<Protocol.Audits.SourceCodeLocation> { return this.affectedLocations.values(); } cookiesWithRequestIndicator(): Iterable<{ cookie: Protocol.Audits.AffectedCookie, hasRequest: boolean, }> { return this.affectedCookies.values(); } getHeavyAdIssues(): Iterable<IssuesManager.HeavyAdIssue.HeavyAdIssue> { return this.heavyAdIssues; } getMixedContentIssues(): Iterable<IssuesManager.MixedContentIssue.MixedContentIssue> { return this.mixedContentIssues; } getTrustedWebActivityIssues(): Iterable<IssuesManager.TrustedWebActivityIssue.TrustedWebActivityIssue> { return this.trustedWebActivityIssues; } getCorsIssues(): Set<IssuesManager.CorsIssue.CorsIssue> { return this.corsIssues; } getCspIssues(): Iterable<IssuesManager.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue> { return this.cspIssues; } getLowContrastIssues(): Iterable<IssuesManager.LowTextContrastIssue.LowTextContrastIssue> { return this.lowContrastIssues; } requests(): Iterable<Protocol.Audits.AffectedRequest> { return this.affectedRequests.values(); } getSharedArrayBufferIssues(): Iterable<IssuesManager.SharedArrayBufferIssue.SharedArrayBufferIssue> { return this.sharedArrayBufferIssues; } getQuirksModeIssues(): Iterable<IssuesManager.QuirksModeIssue.QuirksModeIssue> { return this.quirksModeIssues; } getAttributionReportingIssues(): ReadonlySet<IssuesManager.AttributionReportingIssue.AttributionReportingIssue> { return this.attributionReportingIssues; } getWasmCrossOriginModuleSharingIssue(): ReadonlySet<IssuesManager.WasmCrossOriginModuleSharingIssue.WasmCrossOriginModuleSharingIssue> { return this.wasmCrossOriginModuleSharingIssues; } getGenericIssues(): ReadonlySet<IssuesManager.GenericIssue.GenericIssue> { return this.genericIssues; } getDescription(): IssuesManager.MarkdownIssueDescription.MarkdownIssueDescription|null { if (this.representative) { return this.representative.getDescription(); } return null; } getCategory(): IssuesManager.Issue.IssueCategory { if (this.representative) { return this.representative.getCategory(); } return IssuesManager.Issue.IssueCategory.Other; } getAggregatedIssuesCount(): number { return this.aggregatedIssuesCount; } /** * Produces a primary key for a cookie. Use this instead of `JSON.stringify` in * case new fields are added to `AffectedCookie`. */ private keyForCookie(cookie: Protocol.Audits.AffectedCookie): string { const {domain, path, name} = cookie; return `${domain};${path};${name}`; } addInstance(issue: IssuesManager.Issue.Issue): void { this.aggregatedIssuesCount++; if (!this.representative) { this.representative = issue; } this.issueKind = IssuesManager.Issue.unionIssueKind(this.issueKind, issue.getKind()); let hasRequest = false; for (const request of issue.requests()) { hasRequest = true; if (!this.affectedRequests.has(request.requestId)) { this.affectedRequests.set(request.requestId, request); } } for (const cookie of issue.cookies()) { const key = this.keyForCookie(cookie); if (!this.affectedCookies.has(key)) { this.affectedCookies.set(key, {cookie, hasRequest}); } } for (const rawCookieLine of issue.rawCookieLines()) { if (!this.affectedRawCookieLines.has(rawCookieLine)) { this.affectedRawCookieLines.set(rawCookieLine, {rawCookieLine, hasRequest}); } } for (const location of issue.sources()) { const key = JSON.stringify(location); if (!this.affectedLocations.has(key)) { this.affectedLocations.set(key, location); } } if (issue instanceof IssuesManager.MixedContentIssue.MixedContentIssue) { this.mixedContentIssues.add(issue); } if (issue instanceof IssuesManager.HeavyAdIssue.HeavyAdIssue) { this.heavyAdIssues.add(issue); } for (const details of issue.getBlockedByResponseDetails()) { const key = JSON.stringify(details, ['parentFrame', 'blockedFrame', 'requestId', 'frameId', 'reason', 'request']); this.blockedByResponseDetails.set(key, details); } if (issue instanceof IssuesManager.TrustedWebActivityIssue.TrustedWebActivityIssue) { this.trustedWebActivityIssues.add(issue); } if (issue instanceof IssuesManager.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue) { this.cspIssues.add(issue); } if (issue instanceof IssuesManager.SharedArrayBufferIssue.SharedArrayBufferIssue) { this.sharedArrayBufferIssues.add(issue); } if (issue instanceof IssuesManager.LowTextContrastIssue.LowTextContrastIssue) { this.lowContrastIssues.add(issue); } if (issue instanceof IssuesManager.CorsIssue.CorsIssue) { this.corsIssues.add(issue); } if (issue instanceof IssuesManager.QuirksModeIssue.QuirksModeIssue) { this.quirksModeIssues.add(issue); } if (issue instanceof IssuesManager.AttributionReportingIssue.AttributionReportingIssue) { this.attributionReportingIssues.add(issue); } if (issue instanceof IssuesManager.WasmCrossOriginModuleSharingIssue.WasmCrossOriginModuleSharingIssue) { this.wasmCrossOriginModuleSharingIssues.add(issue); } if (issue instanceof IssuesManager.GenericIssue.GenericIssue) { this.genericIssues.add(issue); } } getKind(): IssuesManager.Issue.IssueKind { return this.issueKind; } isHidden(): boolean { return this.representative?.isHidden() || false; } setHidden(_value: boolean): void { throw new Error('Should not call setHidden on aggregatedIssue'); } } export class IssueAggregator extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { private readonly aggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>(); private readonly hiddenAggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>(); constructor(private readonly issuesManager: IssuesManager.IssuesManager.IssuesManager) { super(); this.issuesManager.addEventListener(IssuesManager.IssuesManager.Events.IssueAdded, this.onIssueAdded, this); this.issuesManager.addEventListener( IssuesManager.IssuesManager.Events.FullUpdateRequired, this.onFullUpdateRequired, this); for (const issue of this.issuesManager.issues()) { this.aggregateIssue(issue); } } private onIssueAdded(event: Common.EventTarget.EventTargetEvent<IssuesManager.IssuesManager.IssueAddedEvent>): void { this.aggregateIssue(event.data.issue); } private onFullUpdateRequired(): void { this.aggregatedIssuesByKey.clear(); this.hiddenAggregatedIssuesByKey.clear(); for (const issue of this.issuesManager.issues()) { this.aggregateIssue(issue); } this.dispatchEventToListeners(Events.FullUpdateRequired); } private aggregateIssue(issue: IssuesManager.Issue.Issue): AggregatedIssue { const map = issue.isHidden() ? this.hiddenAggregatedIssuesByKey : this.aggregatedIssuesByKey; const aggregatedIssue = this.aggregateIssueByStatus(map, issue); this.dispatchEventToListeners(Events.AggregatedIssueUpdated, aggregatedIssue); return aggregatedIssue; } private aggregateIssueByStatus( aggregatedIssuesMap: Map<AggregationKey, AggregatedIssue>, issue: IssuesManager.Issue.Issue): AggregatedIssue { const key = issue.code() as unknown as AggregationKey; let aggregatedIssue = aggregatedIssuesMap.get(key); if (!aggregatedIssue) { aggregatedIssue = new AggregatedIssue(issue.code(), key); aggregatedIssuesMap.set(key, aggregatedIssue); } aggregatedIssue.addInstance(issue); return aggregatedIssue; } aggregatedIssues(): Iterable<AggregatedIssue> { return [...this.aggregatedIssuesByKey.values(), ...this.hiddenAggregatedIssuesByKey.values()]; } hiddenAggregatedIssues(): Iterable<AggregatedIssue> { return this.hiddenAggregatedIssuesByKey.values(); } aggregatedIssueCodes(): Set<AggregationKey> { return new Set([...this.aggregatedIssuesByKey.keys(), ...this.hiddenAggregatedIssuesByKey.keys()]); } aggregatedIssueCategories(): Set<IssuesManager.Issue.IssueCategory> { const result = new Set<IssuesManager.Issue.IssueCategory>(); for (const issue of this.aggregatedIssuesByKey.values()) { result.add(issue.getCategory()); } return result; } aggregatedIssueKinds(): Set<IssuesManager.Issue.IssueKind> { const result = new Set<IssuesManager.Issue.IssueKind>(); for (const issue of this.aggregatedIssuesByKey.values()) { result.add(issue.getKind()); } return result; } numberOfAggregatedIssues(): number { return this.aggregatedIssuesByKey.size; } numberOfHiddenAggregatedIssues(): number { return this.hiddenAggregatedIssuesByKey.size; } keyForIssue(issue: IssuesManager.Issue.Issue<string>): AggregationKey { return issue.code() as unknown as AggregationKey; } } export const enum Events { AggregatedIssueUpdated = 'AggregatedIssueUpdated', FullUpdateRequired = 'FullUpdateRequired', } export type EventTypes = { [Events.AggregatedIssueUpdated]: AggregatedIssue, [Events.FullUpdateRequired]: void, };