chrome-devtools-frontend
Version:
Chrome DevTools UI
406 lines (351 loc) • 13.9 kB
text/typescript
// Copyright 2020 The Chromium Authors
// 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 type * as Protocol from '../../generated/protocol.js';
import {AttributionReportingIssue} from './AttributionReportingIssue.js';
import {ContentSecurityPolicyIssue} from './ContentSecurityPolicyIssue.js';
import {CookieDeprecationMetadataIssue} from './CookieDeprecationMetadataIssue.js';
import {CookieIssue} from './CookieIssue.js';
import {CorsIssue} from './CorsIssue.js';
import {DeprecationIssue} from './DeprecationIssue.js';
import {ElementAccessibilityIssue} from './ElementAccessibilityIssue.js';
import {GenericIssue} from './GenericIssue.js';
import {HeavyAdIssue} from './HeavyAdIssue.js';
import {Issue, IssueCategory, IssueKind, unionIssueKind} from './Issue.js';
import type {EventTypes as IssuesManagerEventsTypes, IssueAddedEvent} from './IssuesManager.js';
import {Events as IssuesManagerEvents} from './IssuesManagerEvents.js';
import {LowTextContrastIssue} from './LowTextContrastIssue.js';
import type {MarkdownIssueDescription} from './MarkdownIssueDescription.js';
import {MixedContentIssue} from './MixedContentIssue.js';
import {PartitioningBlobURLIssue} from './PartitioningBlobURLIssue.js';
import {PermissionElementIssue} from './PermissionElementIssue.js';
import {QuirksModeIssue} from './QuirksModeIssue.js';
import {SharedArrayBufferIssue} from './SharedArrayBufferIssue.js';
export interface IssuesProvider extends Common.EventTarget.EventTarget<IssuesManagerEventsTypes> {
issues(): Iterable<Issue>;
}
interface 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 Issue {
#allIssues = new Set<Issue>();
#affectedCookies = new Map<string, {
cookie: Protocol.Audits.AffectedCookie,
hasRequest: boolean,
}>();
#affectedRawCookieLines = new Map<string, {rawCookieLine: string, hasRequest: boolean}>();
#affectedRequests: Protocol.Audits.AffectedRequest[] = [];
#affectedRequestIds = new Set<Protocol.Network.RequestId>();
#affectedLocations = new Map<string, Protocol.Audits.SourceCodeLocation>();
#heavyAdIssues = new Set<HeavyAdIssue>();
#blockedByResponseDetails = new Map<string, Protocol.Audits.BlockedByResponseIssueDetails>();
#bounceTrackingSites = new Set<string>();
#corsIssues = new Set<CorsIssue>();
#cspIssues = new Set<ContentSecurityPolicyIssue>();
#deprecationIssues = new Set<DeprecationIssue>();
#issueKind = IssueKind.IMPROVEMENT;
#lowContrastIssues = new Set<LowTextContrastIssue>();
#cookieDeprecationMetadataIssues = new Set<CookieDeprecationMetadataIssue>();
#mixedContentIssues = new Set<MixedContentIssue>();
#partitioningBlobURLIssues = new Set<PartitioningBlobURLIssue>();
#permissionElementIssues = new Set<PermissionElementIssue>();
#sharedArrayBufferIssues = new Set<SharedArrayBufferIssue>();
#quirksModeIssues = new Set<QuirksModeIssue>();
#attributionReportingIssues = new Set<AttributionReportingIssue>();
#genericIssues = new Set<GenericIssue>();
#elementAccessibilityIssues = new Set<ElementAccessibilityIssue>();
#representative?: Issue;
#aggregatedIssuesCount = 0;
#key: AggregationKey;
constructor(code: string, aggregationKey: AggregationKey) {
super(code, null);
this.#key = aggregationKey;
}
override primaryKey(): string {
throw new Error('This should never be called');
}
aggregationKey(): AggregationKey {
return this.#key;
}
override getBlockedByResponseDetails(): Iterable<Protocol.Audits.BlockedByResponseIssueDetails> {
return this.#blockedByResponseDetails.values();
}
override 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();
}
override sources(): Iterable<Protocol.Audits.SourceCodeLocation> {
return this.#affectedLocations.values();
}
getBounceTrackingSites(): Iterable<string> {
return this.#bounceTrackingSites.values();
}
cookiesWithRequestIndicator(): Iterable<{
cookie: Protocol.Audits.AffectedCookie,
hasRequest: boolean,
}> {
return this.#affectedCookies.values();
}
getHeavyAdIssues(): Iterable<HeavyAdIssue> {
return this.#heavyAdIssues;
}
getCookieDeprecationMetadataIssues(): Iterable<CookieDeprecationMetadataIssue> {
return this.#cookieDeprecationMetadataIssues;
}
getMixedContentIssues(): Iterable<MixedContentIssue> {
return this.#mixedContentIssues;
}
getCorsIssues(): Set<CorsIssue> {
return this.#corsIssues;
}
getCspIssues(): Iterable<ContentSecurityPolicyIssue> {
return this.#cspIssues;
}
getDeprecationIssues(): Iterable<DeprecationIssue> {
return this.#deprecationIssues;
}
getLowContrastIssues(): Iterable<LowTextContrastIssue> {
return this.#lowContrastIssues;
}
override requests(): Iterable<Protocol.Audits.AffectedRequest> {
return this.#affectedRequests.values();
}
getSharedArrayBufferIssues(): Iterable<SharedArrayBufferIssue> {
return this.#sharedArrayBufferIssues;
}
getQuirksModeIssues(): Iterable<QuirksModeIssue> {
return this.#quirksModeIssues;
}
getAttributionReportingIssues(): ReadonlySet<AttributionReportingIssue> {
return this.#attributionReportingIssues;
}
getGenericIssues(): ReadonlySet<GenericIssue> {
return this.#genericIssues;
}
getElementAccessibilityIssues(): Iterable<ElementAccessibilityIssue> {
return this.#elementAccessibilityIssues;
}
getDescription(): MarkdownIssueDescription|null {
if (this.#representative) {
return this.#representative.getDescription();
}
return null;
}
getCategory(): IssueCategory {
if (this.#representative) {
return this.#representative.getCategory();
}
return IssueCategory.OTHER;
}
getAggregatedIssuesCount(): number {
return this.#aggregatedIssuesCount;
}
getPartitioningBlobURLIssues(): Iterable<PartitioningBlobURLIssue> {
return this.#partitioningBlobURLIssues;
}
getPermissionElementIssues(): Iterable<PermissionElementIssue> {
return this.#permissionElementIssues;
}
/**
* Produces a primary key for a cookie. Use this instead of `JSON.stringify` in
* case new fields are added to `AffectedCookie`.
*/
#keyForCookie(cookie: Protocol.Audits.AffectedCookie): string {
const {domain, path, name} = cookie;
return `${domain};${path};${name}`;
}
addInstance(issue: Issue): void {
this.#aggregatedIssuesCount++;
if (!this.#representative) {
this.#representative = issue;
}
this.#allIssues.add(issue);
this.#issueKind = unionIssueKind(this.#issueKind, issue.getKind());
let hasRequest = false;
for (const request of issue.requests()) {
const {requestId} = request;
hasRequest = true;
if (requestId === undefined) {
this.#affectedRequests.push(request);
} else if (!this.#affectedRequestIds.has(requestId)) {
this.#affectedRequests.push(request);
this.#affectedRequestIds.add(requestId);
}
}
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 site of issue.trackingSites()) {
if (!this.#bounceTrackingSites.has(site)) {
this.#bounceTrackingSites.add(site);
}
}
for (const location of issue.sources()) {
const key = JSON.stringify(location);
if (!this.#affectedLocations.has(key)) {
this.#affectedLocations.set(key, location);
}
}
if (issue instanceof CookieDeprecationMetadataIssue) {
this.#cookieDeprecationMetadataIssues.add(issue);
}
if (issue instanceof MixedContentIssue) {
this.#mixedContentIssues.add(issue);
}
if (issue instanceof 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 ContentSecurityPolicyIssue) {
this.#cspIssues.add(issue);
}
if (issue instanceof DeprecationIssue) {
this.#deprecationIssues.add(issue);
}
if (issue instanceof SharedArrayBufferIssue) {
this.#sharedArrayBufferIssues.add(issue);
}
if (issue instanceof LowTextContrastIssue) {
this.#lowContrastIssues.add(issue);
}
if (issue instanceof CorsIssue) {
this.#corsIssues.add(issue);
}
if (issue instanceof QuirksModeIssue) {
this.#quirksModeIssues.add(issue);
}
if (issue instanceof AttributionReportingIssue) {
this.#attributionReportingIssues.add(issue);
}
if (issue instanceof GenericIssue) {
this.#genericIssues.add(issue);
}
if (issue instanceof ElementAccessibilityIssue) {
this.#elementAccessibilityIssues.add(issue);
}
if (issue instanceof PartitioningBlobURLIssue) {
this.#partitioningBlobURLIssues.add(issue);
}
if (issue instanceof PermissionElementIssue) {
this.#permissionElementIssues.add(issue);
}
}
getKind(): IssueKind {
return this.#issueKind;
}
getAllIssues(): Issue[] {
return Array.from(this.#allIssues);
}
override isHidden(): boolean {
return this.#representative?.isHidden() || false;
}
override setHidden(_value: boolean): void {
throw new Error('Should not call setHidden on aggregatedIssue');
}
}
export class IssueAggregator extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
readonly #aggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>();
readonly #hiddenAggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>();
constructor(private readonly issuesManager: IssuesProvider) {
super();
this.issuesManager.addEventListener(IssuesManagerEvents.ISSUE_ADDED, this.#onIssueAdded, this);
this.issuesManager.addEventListener(IssuesManagerEvents.FULL_UPDATE_REQUIRED, this.#onFullUpdateRequired, this);
for (const issue of this.issuesManager.issues()) {
this.#aggregateIssue(issue);
}
}
#onIssueAdded(event: Common.EventTarget.EventTargetEvent<IssueAddedEvent>): void {
this.#aggregateIssue(event.data.issue);
}
#onFullUpdateRequired(): void {
this.#aggregatedIssuesByKey.clear();
this.#hiddenAggregatedIssuesByKey.clear();
for (const issue of this.issuesManager.issues()) {
this.#aggregateIssue(issue);
}
this.dispatchEventToListeners(Events.FULL_UPDATE_REQUIRED);
}
#aggregateIssue(issue: Issue): AggregatedIssue|undefined {
if (CookieIssue.isThirdPartyCookiePhaseoutRelatedIssue(issue)) {
return;
}
const map = issue.isHidden() ? this.#hiddenAggregatedIssuesByKey : this.#aggregatedIssuesByKey;
const aggregatedIssue = this.#aggregateIssueByStatus(map, issue);
this.dispatchEventToListeners(Events.AGGREGATED_ISSUE_UPDATED, aggregatedIssue);
return aggregatedIssue;
}
#aggregateIssueByStatus(aggregatedIssuesMap: Map<AggregationKey, AggregatedIssue>, 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()];
}
aggregatedIssueCodes(): Set<AggregationKey> {
return new Set([...this.#aggregatedIssuesByKey.keys(), ...this.#hiddenAggregatedIssuesByKey.keys()]);
}
aggregatedIssueCategories(): Set<IssueCategory> {
const result = new Set<IssueCategory>();
for (const issue of this.#aggregatedIssuesByKey.values()) {
result.add(issue.getCategory());
}
return result;
}
aggregatedIssueKinds(): Set<IssueKind> {
const result = new Set<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: Issue): AggregationKey {
return issue.code() as unknown as AggregationKey;
}
}
export const enum Events {
AGGREGATED_ISSUE_UPDATED = 'AggregatedIssueUpdated',
FULL_UPDATE_REQUIRED = 'FullUpdateRequired',
}
export interface EventTypes {
[Events.AGGREGATED_ISSUE_UPDATED]: AggregatedIssue;
[Events.FULL_UPDATE_REQUIRED]: void;
}