mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 20:11:38 +08:00
We weren't testing aggregation of deprecation issues before and this was causing breaks: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3587009 Bug: 1264960 Change-Id: Ib8a387457bdd80a1ac455f8c7b02cd8f276f10b9 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3587163 Auto-Submit: Ari Chivukula <arichiv@chromium.org> Reviewed-by: Danil Somsikov <dsv@chromium.org> Commit-Queue: Danil Somsikov <dsv@chromium.org>
340 lines
12 KiB
TypeScript
340 lines
12 KiB
TypeScript
// 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 {
|
|
#affectedCookies = new Map<string, {
|
|
cookie: Protocol.Audits.AffectedCookie,
|
|
hasRequest: boolean,
|
|
}>();
|
|
#affectedRawCookieLines = new Map<string, {rawCookieLine: string, hasRequest: boolean}>();
|
|
#affectedRequests = new Map<string, Protocol.Audits.AffectedRequest>();
|
|
#affectedLocations = new Map<string, Protocol.Audits.SourceCodeLocation>();
|
|
#heavyAdIssues = new Set<IssuesManager.HeavyAdIssue.HeavyAdIssue>();
|
|
#blockedByResponseDetails = new Map<string, Protocol.Audits.BlockedByResponseIssueDetails>();
|
|
#corsIssues = new Set<IssuesManager.CorsIssue.CorsIssue>();
|
|
#cspIssues = new Set<IssuesManager.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue>();
|
|
#deprecationIssues = new Set<IssuesManager.DeprecationIssue.DeprecationIssue>();
|
|
#issueKind = IssuesManager.Issue.IssueKind.Improvement;
|
|
#lowContrastIssues = new Set<IssuesManager.LowTextContrastIssue.LowTextContrastIssue>();
|
|
#mixedContentIssues = new Set<IssuesManager.MixedContentIssue.MixedContentIssue>();
|
|
#sharedArrayBufferIssues = new Set<IssuesManager.SharedArrayBufferIssue.SharedArrayBufferIssue>();
|
|
#trustedWebActivityIssues = new Set<IssuesManager.TrustedWebActivityIssue.TrustedWebActivityIssue>();
|
|
#quirksModeIssues = new Set<IssuesManager.QuirksModeIssue.QuirksModeIssue>();
|
|
#attributionReportingIssues = new Set<IssuesManager.AttributionReportingIssue.AttributionReportingIssue>();
|
|
#genericIssues = new Set<IssuesManager.GenericIssue.GenericIssue>();
|
|
#representative?: IssuesManager.Issue.Issue;
|
|
#aggregatedIssuesCount = 0;
|
|
#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;
|
|
}
|
|
|
|
getDeprecationIssues(): Iterable<IssuesManager.DeprecationIssue.DeprecationIssue> {
|
|
return this.#deprecationIssues;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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`.
|
|
*/
|
|
#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.DeprecationIssue.DeprecationIssue) {
|
|
this.#deprecationIssues.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.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> {
|
|
readonly #aggregatedIssuesByKey = new Map<AggregationKey, AggregatedIssue>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
#onIssueAdded(event: Common.EventTarget.EventTargetEvent<IssuesManager.IssuesManager.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.FullUpdateRequired);
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
#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,
|
|
};
|