[issues] Link raw cookie lines to network panel

This CL adds a new network filter that allows users to search for
requests where the response includes a specified value in the
`Set-Cookie` header.

The issues panel uses the new filter to link issues with raw
cookie lines back to the network panel (should the issue be
associated with a network request).

Screenshots: https://imgur.com/a/zVZNrK1

[email protected]

Bug: 1179186
Change-Id: I9e275d31676a67a1b197c9aa195b65a144caeebf
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3068920
Commit-Queue: Simon Zünd <[email protected]>
Reviewed-by: Sigurd Schneider <[email protected]>
diff --git a/front_end/core/i18n/locales/en-US.json b/front_end/core/i18n/locales/en-US.json
index 658fb54..5433815 100644
--- a/front_end/core/i18n/locales/en-US.json
+++ b/front_end/core/i18n/locales/en-US.json
@@ -4742,6 +4742,9 @@
   "panels/issues/AffectedCookiesView.ts | domain": {
     "message": "Domain"
   },
+  "panels/issues/AffectedCookiesView.ts | filterSetCookieTitle": {
+    "message": "Show network requests that include this Set-Cookie header in the network panel"
+  },
   "panels/issues/AffectedCookiesView.ts | name": {
     "message": "Name"
   },
diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json
index 26a94df..f64dbe7 100644
--- a/front_end/core/i18n/locales/en-XL.json
+++ b/front_end/core/i18n/locales/en-XL.json
@@ -4742,6 +4742,9 @@
   "panels/issues/AffectedCookiesView.ts | domain": {
     "message": "D̂óm̂áîń"
   },
+  "panels/issues/AffectedCookiesView.ts | filterSetCookieTitle": {
+    "message": "Ŝh́ôẃ n̂ét̂ẃôŕk̂ ŕêq́ûéŝt́ŝ t́ĥát̂ ín̂ćl̂úd̂é t̂h́îś Set-Cookie ĥéâd́êŕ îń t̂h́ê ńêt́ŵór̂ḱ p̂án̂él̂"
+  },
   "panels/issues/AffectedCookiesView.ts | name": {
     "message": "N̂ám̂é"
   },
diff --git a/front_end/panels/issues/AffectedCookiesView.ts b/front_end/panels/issues/AffectedCookiesView.ts
index 029a851..1f401c7 100644
--- a/front_end/panels/issues/AffectedCookiesView.ts
+++ b/front_end/panels/issues/AffectedCookiesView.ts
@@ -34,6 +34,10 @@
   *@description Label for the the number of affected `Set-Cookie` lines associated with a DevTools issue. `Set-Cookie` is a specific header line in an HTTP network request and consists of a single line of text.
   */
   nRawCookieLines: '{n, plural, =1 {1 Raw `Set-Cookie` header} other {# Raw `Set-Cookie` headers}}',
+  /**
+  *@description Title for text button in the Issues panel. Clicking the button navigates the user to the Network Panel. `Set-Cookie` is a specific header line in an HTTP network request and consists of a single line of text.
+  */
+  filterSetCookieTitle: 'Show network requests that include this `Set-Cookie` header in the network panel',
 };
 const str_ = i18n.i18n.registerUIStrings('panels/issues/AffectedCookiesView.ts', UIStrings);
 const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -117,13 +121,31 @@
 
   override update(): void {
     this.clear();
-    const rawCookieLines = this.issue.getRawCookieLines();
-    for (const rawCookieLine of rawCookieLines) {
+    const cookieLinesWithRequestIndicator = this.issue.getRawCookieLines();
+    let count = 0;
+
+    for (const cookie of cookieLinesWithRequestIndicator) {
       const row = document.createElement('tr');
       row.classList.add('affected-resource-directive');
-      this.appendIssueDetailCell(row, rawCookieLine);
+      if (cookie.hasRequest) {
+        const cookieLine = document.createElement('td');
+        const textButton = UI.UIUtils.createTextButton(cookie.rawCookieLine, () => {
+          Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([
+            {
+              filterType: NetworkForward.UIFilter.FilterType.ResponseHeaderValueSetCookie,
+              filterValue: cookie.rawCookieLine,
+            },
+          ]));
+        }, 'link-style devtools-link');
+        textButton.title = i18nString(UIStrings.filterSetCookieTitle);
+        cookieLine.appendChild(textButton);
+        row.appendChild(cookieLine);
+      } else {
+        this.appendIssueDetailCell(row, cookie.rawCookieLine);
+      }
       this.affectedResources.appendChild(row);
+      count++;
     }
-    this.updateAffectedResourceCount(rawCookieLines.size);
+    this.updateAffectedResourceCount(count);
   }
 }
diff --git a/front_end/panels/issues/IssueAggregator.ts b/front_end/panels/issues/IssueAggregator.ts
index 885a22d..762332d 100644
--- a/front_end/panels/issues/IssueAggregator.ts
+++ b/front_end/panels/issues/IssueAggregator.ts
@@ -16,7 +16,7 @@
     cookie: Protocol.Audits.AffectedCookie,
     hasRequest: boolean,
   }>();
-  private affectedRawCookieLines = new Set<string>();
+  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>();
@@ -47,8 +47,8 @@
     return Array.from(this.affectedCookies.values()).map(x => x.cookie);
   }
 
-  getRawCookieLines(): ReadonlySet<string> {
-    return this.affectedRawCookieLines;
+  getRawCookieLines(): Iterable<{rawCookieLine: string, hasRequest: boolean}> {
+    return this.affectedRawCookieLines.values();
   }
 
   sources(): Iterable<Protocol.Audits.SourceCodeLocation> {
@@ -154,7 +154,9 @@
       }
     }
     for (const rawCookieLine of issue.rawCookieLines()) {
-      this.affectedRawCookieLines.add(rawCookieLine);
+      if (!this.affectedRawCookieLines.has(rawCookieLine)) {
+        this.affectedRawCookieLines.set(rawCookieLine, {rawCookieLine, hasRequest});
+      }
     }
     for (const location of issue.sources()) {
       const key = JSON.stringify(location);
diff --git a/front_end/panels/network/NetworkLogView.ts b/front_end/panels/network/NetworkLogView.ts
index 2ba36dc..5e14fb8 100644
--- a/front_end/panels/network/NetworkLogView.ts
+++ b/front_end/panels/network/NetworkLogView.ts
@@ -620,6 +620,12 @@
     return request.responseHeaderValue(value) !== undefined;
   }
 
+  static _requestResponseHeaderSetCookieFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean {
+    // Multiple Set-Cookie headers in the request are concatenated via space. Only
+    // filter via `includes` instead of strict equality.
+    return Boolean(request.responseHeaderValue('Set-Cookie')?.includes(value));
+  }
+
   static _requestMethodFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean {
     return request.requestMethod === value;
   }
@@ -1431,8 +1437,11 @@
     }
 
     const responseHeaders = request.responseHeaders;
-    for (let i = 0, l = responseHeaders.length; i < l; ++i) {
-      this._suggestionBuilder.addItem(NetworkForward.UIFilter.FilterType.HasResponseHeader, responseHeaders[i].name);
+    for (const responseHeader of responseHeaders) {
+      this._suggestionBuilder.addItem(NetworkForward.UIFilter.FilterType.HasResponseHeader, responseHeader.name);
+      if (responseHeader.name === 'Set-Cookie') {
+        this._suggestionBuilder.addItem(NetworkForward.UIFilter.FilterType.ResponseHeaderValueSetCookie);
+      }
     }
 
     for (const cookie of request.responseCookies) {
@@ -1734,6 +1743,9 @@
       case NetworkForward.UIFilter.FilterType.HasResponseHeader:
         return NetworkLogView._requestResponseHeaderFilter.bind(null, value);
 
+      case NetworkForward.UIFilter.FilterType.ResponseHeaderValueSetCookie:
+        return NetworkLogView._requestResponseHeaderSetCookieFilter.bind(null, value);
+
       case NetworkForward.UIFilter.FilterType.Is:
         if (value.toLowerCase() === NetworkForward.UIFilter.IsFilterType.Running) {
           return NetworkLogView._runningRequestFilter;
diff --git a/front_end/panels/network/forward/UIFilter.ts b/front_end/panels/network/forward/UIFilter.ts
index d3c4a9b..db9a497 100644
--- a/front_end/panels/network/forward/UIFilter.ts
+++ b/front_end/panels/network/forward/UIFilter.ts
@@ -7,6 +7,7 @@
 export enum FilterType {
   Domain = 'domain',
   HasResponseHeader = 'has-response-header',
+  ResponseHeaderValueSetCookie = 'response-header-set-cookie',
   Is = 'is',
   LargerThan = 'larger-than',
   Method = 'method',