[issues] Introduce RelatedIssue for associating issues with objects
This introduces a mechanism to associate issues with other objects in
DevTools such as NetworkRequests. Whether an issue is associated may
be used for, e.g., filtering or providing links to the issue.
Bug: chromium:1057979
Change-Id: I208ac146b1f576a19d377f29adf266cacc1ef091
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2116439
Commit-Queue: Sigurd Schneider <[email protected]>
Reviewed-by: Tim van der Lippe <[email protected]>
Reviewed-by: Simon Zünd <[email protected]>
diff --git a/BUILD.gn b/BUILD.gn
index ea488a3..4383138 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1404,6 +1404,7 @@
"sdk/IsolateManager.js",
"sdk/Issue.js",
"sdk/IssuesModel.js",
+ "sdk/RelatedIssue.js",
"sdk/LayerTreeBase.js",
"sdk/LogModel.js",
"sdk/NetworkLog.js",
diff --git a/front_end/network/NetworkLogView.js b/front_end/network/NetworkLogView.js
index 2863f27..c0097db 100644
--- a/front_end/network/NetworkLogView.js
+++ b/front_end/network/NetworkLogView.js
@@ -1578,7 +1578,7 @@
if (this._dataURLFilterUI.checked() && (request.parsedURL.isDataURL() || request.parsedURL.isBlobURL())) {
return false;
}
- if (this._onlyIssuesFilterUI.checked() && !SDK.IssuesModel.IssuesModel.hasIssues(request)) {
+ if (this._onlyIssuesFilterUI.checked() && !SDK.RelatedIssue.hasIssues(request)) {
return false;
}
if (this._onlyBlockedRequestsUI.checked() && !request.wasBlocked()) {
diff --git a/front_end/resources/CookieItemsView.js b/front_end/resources/CookieItemsView.js
index 02464d7..fd2bee4 100644
--- a/front_end/resources/CookieItemsView.js
+++ b/front_end/resources/CookieItemsView.js
@@ -207,7 +207,7 @@
*/
filter(items, keyFunction) {
return super.filter(items, keyFunction)
- .filter(cookie => !this._onlyIssuesFilterUI.checked() || SDK.IssuesModel.IssuesModel.hasIssues(cookie));
+ .filter(cookie => !this._onlyIssuesFilterUI.checked() || SDK.RelatedIssue.hasIssues(cookie));
}
/**
diff --git a/front_end/sdk/BUILD.gn b/front_end/sdk/BUILD.gn
index 8d99d1a..74e4091 100644
--- a/front_end/sdk/BUILD.gn
+++ b/front_end/sdk/BUILD.gn
@@ -10,6 +10,7 @@
"CookieParser.js",
"NetworkManager.js",
"NetworkRequest.js",
+ "RelatedIssue.js",
"SDKModel.js",
"SecurityOriginManager.js",
"ServerTiming.js",
diff --git a/front_end/sdk/Issue.js b/front_end/sdk/Issue.js
index f609ecb..fcb2a85 100644
--- a/front_end/sdk/Issue.js
+++ b/front_end/sdk/Issue.js
@@ -3,7 +3,7 @@
// found in the LICENSE file.
import * as Common from '../common/common.js';
-import {IssuesModel} from './IssuesModel.js';
+import {IssueCategory} from './RelatedIssue.js';
/**
* @unrestricted
@@ -52,6 +52,20 @@
}
return false;
}
+
+ /**
+ * @return {symbol}
+ */
+ getCategory() {
+ const code = this.code();
+ if (code === 'SameSiteCookieIssue') {
+ return IssueCategory.SameSiteCookie;
+ }
+ if (code.startsWith('CrossOriginEmbedderPolicy')) {
+ return IssueCategory.CrossOriginEmbedderPolicy;
+ }
+ return IssueCategory.Other;
+ }
}
/**
@@ -107,7 +121,6 @@
}
if (resources.cookies) {
for (const cookie of resources.cookies) {
- IssuesModel.connectWithIssue(cookie, issue);
const key = JSON.stringify(cookie);
if (!this._cookies.has(key)) {
this._cookies.set(key, cookie);
diff --git a/front_end/sdk/IssuesModel.js b/front_end/sdk/IssuesModel.js
index 43781c1..01b34df 100644
--- a/front_end/sdk/IssuesModel.js
+++ b/front_end/sdk/IssuesModel.js
@@ -7,10 +7,11 @@
import {CookieModel} from './CookieModel.js';
import {AggregatedIssue, Issue} from './Issue.js';
import {NetworkManager} from './NetworkManager.js';
+import {NetworkRequest} from './NetworkRequest.js'; // eslint-disable-line no-unused-vars
+import * as RelatedIssue from './RelatedIssue.js';
import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTreeModel.js';
import {Capability, SDKModel, Target} from './SDKModel.js'; // eslint-disable-line no-unused-vars
-const connectedIssueSymbol = Symbol('issue');
/**
* @implements {Protocol.AuditsDispatcher}
@@ -43,7 +44,15 @@
*/
_onMainFrameNavigated(event) {
const mainFrame = /** @type {!SDK.ResourceTreeFrame} */ (event.data);
- this._issues = this._issues.filter(issue => issue.isAssociatedWithRequestId(mainFrame.loaderId));
+ const keptIssues = [];
+ for (const issue of this._issues) {
+ if (issue.isAssociatedWithRequestId(mainFrame.loaderId)) {
+ keptIssues.push(issue);
+ } else {
+ this._disconnectIssue(issue);
+ }
+ }
+ this._issues = keptIssues;
this._aggregatedIssuesByCode.clear();
for (const issue of this._issues) {
this._aggregateIssue(issue);
@@ -83,48 +92,69 @@
issueAdded(inspectorIssue) {
const issue = new Issue(inspectorIssue.code, inspectorIssue.resources);
this._issues.push(issue);
+ this._connectIssue(issue);
const aggregatedIssue = this._aggregateIssue(issue);
this.dispatchEventToListeners(Events.AggregatedIssueUpdated, aggregatedIssue);
}
/**
+ *
+ * @param {!Issue} issue
+ */
+ _connectIssue(issue) {
+ const resources = issue.resources();
+ if (!resources) {
+ return;
+ }
+ if (resources.requests) {
+ for (const resourceRequest of resources.requests) {
+ const request =
+ /** @type {?NetworkRequest} */ (
+ self.SDK.networkLog.requests().find(r => r.requestId() === resourceRequest.requestId));
+ if (request) {
+ // Connect the real network request with this issue and vice versa.
+ RelatedIssue.connect(request, issue.getCategory(), issue);
+ resourceRequest.request = request;
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * @param {!Issue} issue
+ */
+ _disconnectIssue(issue) {
+ const resources = issue.resources();
+ if (!resources) {
+ return;
+ }
+ if (resources.requests) {
+ for (const resourceRequest of resources.requests) {
+ const request =
+ /** @type {?NetworkRequest} */ (
+ self.SDK.networkLog.requests().find(r => r.requestId() === resourceRequest.requestId));
+ if (request) {
+ // Disconnect the real network request from this issue;
+ RelatedIssue.disconnect(request, issue.getCategory(), issue);
+ }
+ }
+ }
+ }
+
+ /**
* @returns {!Iterable<AggregatedIssue>}
*/
aggregatedIssues() {
return this._aggregatedIssuesByCode.values();
}
- /**
+ /**
* @return {number}
*/
numberOfAggregatedIssues() {
return this._aggregatedIssuesByCode.size;
}
-
- /**
- * @param {!*} obj
- * TODO(chromium:1063765): Strengthen types.
- * @param {!Issue} issue
- */
- static connectWithIssue(obj, issue) {
- if (!obj) {
- return;
- }
-
- if (!obj[connectedIssueSymbol]) {
- obj[connectedIssueSymbol] = new Set();
- }
-
- obj[connectedIssueSymbol].add(issue);
- }
-
- /**
- * @param {!*} obj
- * @returns {boolean}
- */
- static hasIssues(obj) {
- return !!obj && obj[connectedIssueSymbol] && obj[connectedIssueSymbol].size;
- }
}
/** @enum {symbol} */
@@ -133,4 +163,4 @@
FullUpdateRequired: Symbol('FullUpdateRequired'),
};
-SDKModel.register(IssuesModel, Capability.None, true);
+SDKModel.register(IssuesModel, Capability.Network, true);
diff --git a/front_end/sdk/RelatedIssue.js b/front_end/sdk/RelatedIssue.js
new file mode 100644
index 0000000..6f54d28
--- /dev/null
+++ b/front_end/sdk/RelatedIssue.js
@@ -0,0 +1,107 @@
+// 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 '../common/common.js'; // eslint-disable-line no-unused-vars
+
+const connectedIssueSymbol = Symbol('issue');
+
+export const IssueCategory = {
+ CrossOriginEmbedderPolicy: Symbol('CrossOriginEmbedderPolicy'),
+ SameSiteCookie: Symbol('SameSiteCookie'),
+ Other: Symbol('Other')
+};
+
+/**
+ * @param {*} obj
+ * @param {symbol} category
+ * @param {*} issue
+ */
+export function connect(obj, category, issue) {
+ if (!obj) {
+ return;
+ }
+ if (!obj[connectedIssueSymbol]) {
+ obj[connectedIssueSymbol] = new Map();
+ }
+ const map = obj[connectedIssueSymbol];
+ if (!map.has(category)) {
+ map.set(category, new Set());
+ }
+ const set = map.get(category);
+ set.add(issue);
+}
+
+/**
+ * @param {*} obj
+ * @param {symbol} category
+ * @param {*} issue
+ */
+export function disconnect(obj, category, issue) {
+ if (!obj || !obj[connectedIssueSymbol]) {
+ return;
+ }
+ const map = obj[connectedIssueSymbol];
+ if (!map.has(category)) {
+ return;
+ }
+ const set = map.get(category);
+ set.delete(issue);
+}
+
+/**
+ * @param {*} obj
+ * @return {boolean}
+ */
+export function hasIssues(obj) {
+ if (!obj || !obj[connectedIssueSymbol]) {
+ return false;
+ }
+ const map = obj[connectedIssueSymbol];
+ if (map.size === 0) {
+ return false;
+ }
+ for (const set of map.values()) {
+ if (set.size > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * @param {*} obj
+ * @param {symbol} category
+ * @return {boolean}
+ */
+export function hasIssueOfCategory(obj, category) {
+ if (!obj || !obj[connectedIssueSymbol]) {
+ return false;
+ }
+ const map = obj[connectedIssueSymbol];
+ if (!map.has(category)) {
+ return false;
+ }
+ const set = map.get(category);
+ return set.size > 0;
+}
+
+/**
+ * @param {*} obj
+ * @param {symbol} category
+ * @return {!Promise<undefined>}
+ */
+export async function reveal(obj, category) {
+ if (!obj || !obj[connectedIssueSymbol]) {
+ return;
+ }
+ const map = obj[connectedIssueSymbol];
+ if (!map.has(category)) {
+ return;
+ }
+ const set = map.get(category);
+ if (set.size === 0) {
+ return;
+ }
+ return Common.Revealer.reveal(set.values().next().value);
+}
diff --git a/front_end/sdk/module.json b/front_end/sdk/module.json
index 46b1987..2e27ff2 100644
--- a/front_end/sdk/module.json
+++ b/front_end/sdk/module.json
@@ -420,6 +420,7 @@
"ProfileTreeModel.js",
"Issue.js",
"IssuesModel.js",
+ "RelatedIssue.js",
"ServerTiming.js",
"CPUProfileDataModel.js",
"CPUProfilerModel.js",
diff --git a/front_end/sdk/sdk.js b/front_end/sdk/sdk.js
index 8a7ab19..f70ff35 100644
--- a/front_end/sdk/sdk.js
+++ b/front_end/sdk/sdk.js
@@ -47,6 +47,7 @@
import * as PaintProfiler from './PaintProfiler.js';
import * as PerformanceMetricsModel from './PerformanceMetricsModel.js';
import * as ProfileTreeModel from './ProfileTreeModel.js';
+import * as RelatedIssue from './RelatedIssue.js';
import * as RemoteObject from './RemoteObject.js';
import * as Resource from './Resource.js';
import * as ResourceTreeModel from './ResourceTreeModel.js';
@@ -100,6 +101,7 @@
PaintProfiler,
PerformanceMetricsModel,
ProfileTreeModel,
+ RelatedIssue,
RemoteObject,
Resource,
ResourceTreeModel,
diff --git a/test/unittests/front_end/sdk/BUILD.gn b/test/unittests/front_end/sdk/BUILD.gn
index 1e9670c..2546751 100644
--- a/test/unittests/front_end/sdk/BUILD.gn
+++ b/test/unittests/front_end/sdk/BUILD.gn
@@ -4,6 +4,7 @@
testonly = true
sources = [
"Cookie_test.ts",
+ "RelatedIssue_test.ts",
"ServerTiming_test.ts",
]
diff --git a/test/unittests/front_end/sdk/RelatedIssue_test.ts b/test/unittests/front_end/sdk/RelatedIssue_test.ts
new file mode 100644
index 0000000..acbdb2e
--- /dev/null
+++ b/test/unittests/front_end/sdk/RelatedIssue_test.ts
@@ -0,0 +1,93 @@
+// 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.
+
+const {assert} = chai;
+
+import {connect, disconnect, hasIssues, hasIssueOfCategory, IssueCategory} from '../../../../front_end/sdk/RelatedIssue.js';
+
+describe('hasIssues', () => {
+ it('should be false for fresh objects', () => {
+ const obj = {};
+ assert.isFalse(hasIssues(obj), 'obj should not have an issue');
+ });
+});
+
+describe('connect', () => {
+ it('should cause object to have issues', () => {
+ const obj = {};
+ const issue = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue);
+ assert.isTrue(hasIssues(obj), 'obj should have an issue');
+ });
+
+ it('should cause object to have a issue of a specific category', () => {
+ const obj = {};
+ const issue = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue);
+ assert.isTrue(hasIssueOfCategory(obj, IssueCategory.Other), 'obj should have an issue of the specified category');
+ assert.isFalse(
+ hasIssueOfCategory(obj, IssueCategory.CrossOriginEmbedderPolicy),
+ 'obj should not have an issue of a different category');
+ });
+
+ it('should cause object to have a issue after being called twice', () => {
+ const obj = {};
+ const issue = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue);
+ connect(obj, IssueCategory.Other, issue);
+ assert.isTrue(hasIssues(obj), 'obj should have an issue');
+ assert.isTrue(hasIssueOfCategory(obj, IssueCategory.Other), 'obj should have an issue of the specified category');
+ assert.isFalse(
+ hasIssueOfCategory(obj, IssueCategory.CrossOriginEmbedderPolicy),
+ 'obj should not have an issue of a different category');
+ });
+});
+
+describe('disconnect', () => {
+ it('should remove an issue', () => {
+ const obj = {};
+ const issue = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue);
+ assert.isTrue(hasIssues(obj), 'obj should have an issue');
+ disconnect(obj, IssueCategory.Other, issue);
+ assert.isFalse(hasIssues(obj), 'obj should not have an issue');
+ });
+
+ it('should remove an issue of a specific category', () => {
+ const obj = {};
+ const issue = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue);
+ connect(obj, IssueCategory.CrossOriginEmbedderPolicy, issue);
+ assert.isTrue(hasIssueOfCategory(obj, IssueCategory.Other), 'obj should have an issue of this category');
+ assert.isTrue(
+ hasIssueOfCategory(obj, IssueCategory.CrossOriginEmbedderPolicy),
+ 'obj should not have an issue of this category');
+ disconnect(obj, IssueCategory.Other, issue);
+ assert.isFalse(hasIssueOfCategory(obj, IssueCategory.Other), 'obj should no longer have an issue of this category');
+ assert.isTrue(
+ hasIssueOfCategory(obj, IssueCategory.CrossOriginEmbedderPolicy),
+ 'obj should still have an issue of this category');
+ });
+
+ it('should remove an issue even if connected twice', () => {
+ const obj = {};
+ const issue = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue);
+ connect(obj, IssueCategory.Other, issue);
+ disconnect(obj, IssueCategory.Other, issue);
+ assert.isFalse(hasIssues(obj), 'obj should not have an issue');
+ assert.isFalse(hasIssueOfCategory(obj, IssueCategory.Other), 'obj should not have an issue');
+ });
+
+ it('should remove only the specified issue', () => {
+ const obj = {};
+ const issue1 = {}; // Use a dummy object;
+ const issue2 = {}; // Use a dummy object;
+ connect(obj, IssueCategory.Other, issue1);
+ connect(obj, IssueCategory.Other, issue2);
+ disconnect(obj, IssueCategory.Other, issue1);
+ assert.isTrue(hasIssues(obj), 'obj should still have an issue');
+ assert.isTrue(hasIssueOfCategory(obj, IssueCategory.Other), 'obj should still have an issue');
+ });
+});