Reland “De-obfuscate X-Client-Data header values in Network tab”

Reland of: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2316062

Screenshot: https://i.imgur.com/LJHFIp9.png
Note that the screenshot shows this as a response header instead of a
request header, since that was easier to test. In real-world scenarios
it would be a request header instead, but the UI looks the same.

CL preparing the ClientVariationsParser for use in the DevTools UI:
https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2316065

[email protected],[email protected],[email protected],[email protected]

Bug: chromium:1103854
Change-Id: I6b959a1d57bcbc84f8571257a88fcbec805bf330
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2318257
Reviewed-by: Mathias Bynens <[email protected]>
Reviewed-by: Ilya Sherman <[email protected]>
Reviewed-by: Paul Lewis <[email protected]>
Commit-Queue: Mathias Bynens <[email protected]>
diff --git a/BUILD.gn b/BUILD.gn
index 24e4008..a1a3d06 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -157,9 +157,9 @@
 generated_non_autostart_non_remote_modules = [
   "$resources_out_dir/accessibility/accessibility_module.js",
   "$resources_out_dir/animation/animation_module.js",
-  "$resources_out_dir/lighthouse/lighthouse_module.js",
   "$resources_out_dir/browser_debugger/browser_debugger_module.js",
   "$resources_out_dir/changes/changes_module.js",
+  "$resources_out_dir/client_variations/client_variations_module.js",
   "$resources_out_dir/cm_modes/cm_modes_module.js",
   "$resources_out_dir/cm/cm_module.js",
   "$resources_out_dir/color_picker/color_picker_module.js",
@@ -182,6 +182,7 @@
   "$resources_out_dir/js_profiler/js_profiler_module.js",
   "$resources_out_dir/layer_viewer/layer_viewer_module.js",
   "$resources_out_dir/layers/layers_module.js",
+  "$resources_out_dir/lighthouse/lighthouse_module.js",
   "$resources_out_dir/marked/marked_module.js",
   "$resources_out_dir/media/media_module.js",
   "$resources_out_dir/network/network_module.js",
diff --git a/all_devtools_files.gni b/all_devtools_files.gni
index 6e4ba5a..78ceb6c 100644
--- a/all_devtools_files.gni
+++ b/all_devtools_files.gni
@@ -49,6 +49,7 @@
   "front_end/changes/changesSidebar.css",
   "front_end/changes/changesView.css",
   "front_end/changes/module.json",
+  "front_end/client_variations/module.json",
   "front_end/cm_headless/module.json",
   "front_end/cm_modes/module.json",
   "front_end/cm/codemirror.css",
diff --git a/all_devtools_modules.gni b/all_devtools_modules.gni
index 05448e2..bf844ff 100644
--- a/all_devtools_modules.gni
+++ b/all_devtools_modules.gni
@@ -501,9 +501,6 @@
   "elements/StylePropertyHighlighter.js",
   "elements/StylePropertyTreeElement.js",
   "elements/StylesSidebarPane.js",
-  "formatter/FormatterWorkerPool.js",
-  "formatter/ScriptFormatter.js",
-  "formatter/SourceFormatter.js",
   "formatter_worker/AcornTokenizer.js",
   "formatter_worker/CSSFormatter.js",
   "formatter_worker/CSSRuleParser.js",
@@ -514,9 +511,15 @@
   "formatter_worker/IdentityFormatter.js",
   "formatter_worker/JavaScriptFormatter.js",
   "formatter_worker/JavaScriptOutline.js",
+  "formatter/FormatterWorkerPool.js",
+  "formatter/ScriptFormatter.js",
+  "formatter/SourceFormatter.js",
   "generated/ARIAProperties.js",
   "generated/InspectorBackendCommands.js",
   "generated/SupportedCSSProperties.js",
+  "har_importer/HARFormat.js",
+  "har_importer/HARImporter.js",
+  "heap_snapshot_model/HeapSnapshotModel.js",
   "host/InspectorFrontendHost.js",
   "host/InspectorFrontendHostAPI.js",
   "host/Platform.js",
@@ -525,6 +528,8 @@
   "issues/IssueAggregator.js",
   "issues/IssueRevealer.js",
   "issues/IssuesPane.js",
+  "javascript_metadata/JavaScriptMetadata.js",
+  "javascript_metadata/NativeFunctions.js",
   "platform/array-utilities.js",
   "platform/date-utilities.js",
   "platform/number-utilities.js",
@@ -605,13 +610,11 @@
   "text_utils/TextCursor.js",
   "text_utils/TextRange.js",
   "text_utils/TextUtils.js",
-  "workspace/FileManager.js",
-  "workspace/UISourceCode.js",
-  "workspace/WorkspaceImpl.js",
   "third_party/acorn-logical-assignment/package/dist/acorn-logical-assignment.mjs",
   "third_party/acorn-loose/package/dist/acorn-loose.mjs",
   "third_party/acorn-numeric-separator/package/dist/acorn-numeric-separator.mjs",
   "third_party/acorn/package/dist/acorn.mjs",
+  "third_party/chromium/client-variations/ClientVariationsParser.js",
   "third_party/codemirror/package/addon/comment/comment.js",
   "third_party/codemirror/package/addon/edit/closebrackets.js",
   "third_party/codemirror/package/addon/edit/matchbrackets.js",
@@ -661,13 +664,10 @@
   "timeline_model/TimelineModelFilter.js",
   "timeline_model/TimelineProfileTree.js",
   "timeline_model/TracingLayerTree.js",
-  "workspace_diff/WorkspaceDiff.js",
   "worker_service/ServiceDispatcher.js",
-  "har_importer/HARFormat.js",
-  "har_importer/HARImporter.js",
-  "heap_snapshot_model/HeapSnapshotModel.js",
-  "javascript_metadata/JavaScriptMetadata.js",
-  "javascript_metadata/NativeFunctions.js",
+  "workspace/FileManager.js",
+  "workspace/UISourceCode.js",
+  "workspace/WorkspaceImpl.js",
 ]
 
 foreach(module, all_typescript_modules) {
diff --git a/devtools_module_entrypoints.gni b/devtools_module_entrypoints.gni
index a61c710..143d7f9 100644
--- a/devtools_module_entrypoints.gni
+++ b/devtools_module_entrypoints.gni
@@ -160,6 +160,7 @@
   "$resources_out_dir/third_party/acorn/acorn.js",
   "$resources_out_dir/third_party/lit-html/lit-html.js",
   "$resources_out_dir/marked/marked.js",
+  "$resources_out_dir/client_variations/client_variations.js",
   "$resources_out_dir/root/root.js",
   "$resources_out_dir/sdk/sdk.js",
   "$resources_out_dir/search/search.js",
diff --git a/front_end/BUILD.gn b/front_end/BUILD.gn
index d9faa65..b316be3 100644
--- a/front_end/BUILD.gn
+++ b/front_end/BUILD.gn
@@ -12,6 +12,7 @@
     "accessibility:bundle",
     "bindings:bundle",
     "browser_sdk:bundle",
+    "client_variations:bundle",
     "color_picker:bundle",
     "common:bundle",
     "component_docs",
diff --git a/front_end/client_variations/BUILD.gn b/front_end/client_variations/BUILD.gn
new file mode 100644
index 0000000..f9174a9
--- /dev/null
+++ b/front_end/client_variations/BUILD.gn
@@ -0,0 +1,13 @@
+# 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("../../scripts/build/ninja/devtools_entrypoint.gni")
+
+devtools_entrypoint("bundle") {
+  entrypoint = "client_variations.js"
+
+  is_legacy_javascript_entrypoint = [ "crbug.com/1011811" ]
+
+  deps = [ "../third_party/chromium" ]
+}
diff --git a/front_end/client_variations/client_variations.js b/front_end/client_variations/client_variations.js
new file mode 100644
index 0000000..5c67eea
--- /dev/null
+++ b/front_end/client_variations/client_variations.js
@@ -0,0 +1,7 @@
+// 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 ClientVariationsParser from '../third_party/chromium/client-variations/ClientVariationsParser.js';
+
+export const parseClientVariations = ClientVariationsParser.parseClientVariations;
diff --git a/front_end/client_variations/module.json b/front_end/client_variations/module.json
new file mode 100644
index 0000000..0d413f5
--- /dev/null
+++ b/front_end/client_variations/module.json
@@ -0,0 +1,10 @@
+{
+  "skip_rollup": true,
+  "modules": [
+    "client_variations.js",
+    "../third_party/chromium/client-variations/ClientVariationsParser.js"
+  ],
+  "skip_compilation": [
+    "../third_party/chromium/client-variations/ClientVariationsParser.js"
+  ]
+}
diff --git a/front_end/network/RequestHeadersView.js b/front_end/network/RequestHeadersView.js
index 44a8c3f..3c10522 100644
--- a/front_end/network/RequestHeadersView.js
+++ b/front_end/network/RequestHeadersView.js
@@ -29,6 +29,7 @@
  */
 
 import * as BrowserSDK from '../browser_sdk/browser_sdk.js';
+import * as ClientVariationsParser from '../client_variations/client_variations.js';
 import * as Common from '../common/common.js';
 import * as Host from '../host/host.js';
 import * as ObjectUI from '../object_ui/object_ui.js';
@@ -736,7 +737,7 @@
       if (this._request.cachedInMemory() || this._request.cached()) {
         cautionText = ls`Provisional headers are shown. Disable cache to see full headers.`;
         cautionTitle = ls
-        `Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn't store the original request headers. Disable cache to see full request headers.`;
+        `Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn’t store the original request headers. Disable cache to see full request headers.`;
       } else {
         cautionText = ls`Provisional headers are shown`;
       }
@@ -757,12 +758,14 @@
     }
 
     headersTreeElement.hidden = !length && !provisionalHeaders;
-    for (let i = 0; i < length; ++i) {
-      const headerTreeElement = new UI.TreeOutline.TreeElement(this._formatHeaderObject(headers[i]));
-      headerTreeElement[_headerNameSymbol] = headers[i].name;
+    for (const header of headers) {
+      const headerTreeElement = new UI.TreeOutline.TreeElement(this._formatHeaderObject(header));
+      headerTreeElement[_headerNameSymbol] = header.name;
 
-      if (headers[i].name.toLowerCase() === 'set-cookie') {
-        const matchingBlockedReasons = blockedCookieLineToReasons.get(headers[i].value);
+      const headerId = header.name.toLowerCase();
+
+      if (headerId === 'set-cookie') {
+        const matchingBlockedReasons = blockedCookieLineToReasons.get(header.value);
         if (matchingBlockedReasons) {
           const icon = UI.Icon.Icon.create('smallicon-warning', '');
           headerTreeElement.listItemElement.appendChild(icon);
@@ -779,6 +782,25 @@
       }
 
       headersTreeElement.appendChild(headerTreeElement);
+
+      if (headerId === 'x-client-data') {
+        // https://source.chromium.org/chromium/chromium/src/+/master:components/variations/proto/client_variations.proto;l=14-21
+        const {variationIds, triggerVariationIds} = ClientVariationsParser.parseClientVariations(header.value);
+        if (variationIds.length || triggerVariationIds.length) {
+          const element = createElement('div');
+          element.classList.add('x-client-data-details');
+          if (variationIds.length) {
+            element.createChild('div').textContent =
+                ls`Active client experiment variation IDs: ${variationIds.join(', ')}`;
+          }
+          if (triggerVariationIds.length) {
+            element.createChild('div').textContent =
+                ls`Active client experiment variation IDs that trigger server-side behavior: ${
+                    triggerVariationIds.join(', ')}`;
+          }
+          headerTreeElement.listItemElement.appendChild(element);
+        }
+      }
     }
   }
 
diff --git a/front_end/network/module.json b/front_end/network/module.json
index 2df8cde..f53a3ce 100644
--- a/front_end/network/module.json
+++ b/front_end/network/module.json
@@ -176,6 +176,7 @@
   ],
   "dependencies": [
     "browser_sdk",
+    "client_variations",
     "common",
     "components",
     "cookie_table",
diff --git a/front_end/network/network_strings.grdp b/front_end/network/network_strings.grdp
index add97b6..af310c8 100644
--- a/front_end/network/network_strings.grdp
+++ b/front_end/network/network_strings.grdp
@@ -48,6 +48,9 @@
   <message name="IDS_DEVTOOLS_0db377921f4ce762c62526131097968f" desc="Text in Request Headers View of the Network panel">
     General
   </message>
+  <message name="IDS_DEVTOOLS_0edec22265ba6e09745b283f6f54ceba" desc="Text for X-Client-Data HTTP headers in Headers View of the Network panel">
+    Active client experiment variation IDs: <ph name="VARIATION_IDS">$1s<ex>111, 222, 333</ex></ph>
+  </message>
   <message name="IDS_DEVTOOLS_0f591b1d87c91e77d4fa11dc6e44288c" desc="Text in Network Item View of the Network panel">
     Request and response timeline
   </message>
@@ -141,9 +144,6 @@
   <message name="IDS_DEVTOOLS_2ad7f735dd6d0dedb0d010a612854a17" desc="An option in the user agent dropdown menu in the Network conditions tool">
     <ph name="LOCKED_1">Internet Explorer 8</ph>
   </message>
-  <message name="IDS_DEVTOOLS_2cc979d2cc5eda80f702a242ad5242cf" desc="Tooltip to explain lack of raw headers for a particular network request">
-    Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn&apos;t store the original request headers. Disable cache to see full request headers.
-  </message>
   <message name="IDS_DEVTOOLS_2d13df6f8b5e4c5af9f87e0dc39df69d" desc="Text in Network Data Grid Node of the Network panel">
     Pending
   </message>
@@ -168,6 +168,9 @@
   <message name="IDS_DEVTOOLS_324c6483c84f17f8af6b9a0d20145dda" desc="Text in Network Log View of the Network panel">
     <ph name="RESOURCESIZE">$1s<ex>10</ex></ph> B resources loaded by the page
   </message>
+  <message name="IDS_DEVTOOLS_32ac16c44f3339f7da811c289390d4fe" desc="Text for X-Client-Data HTTP headers in Headers View of the Network panel">
+    Active client experiment variation IDs that trigger server-side behavior: <ph name="TRIGGER_VARIATION_IDS">$1s<ex>111, 222, 333</ex></ph>
+  </message>
   <message name="IDS_DEVTOOLS_336439019ce67912717a20d54298bf24" desc="Text in Request Timing View of the Network panel">
     Receiving Push
   </message>
@@ -432,6 +435,9 @@
   <message name="IDS_DEVTOOLS_9b5e92790ca7fec879dfd28355a224d1" desc="Text used to show that serviceworker fetch response source is HTTP cache">
     From HTTP cache
   </message>
+  <message name="IDS_DEVTOOLS_a1725c50b8a42b8b7af43912a64eb7d2" desc="Tooltip to explain lack of raw headers for a particular network request">
+    Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn’t store the original request headers. Disable cache to see full request headers.
+  </message>
   <message name="IDS_DEVTOOLS_bb9fd331bd610fd877425ceff23708a7" desc="Time at which a response was retrieved">
     Retrieval Time: <ph name="THIS__REQUEST_RESPONSERETRIEVALTIME">$1s<ex>Fri Apr 10 2020 17:20:27 GMT-0700 (Pacific Daylight Time)</ex></ph>
   </message>
diff --git a/front_end/network/requestHeadersTree.css b/front_end/network/requestHeadersTree.css
index 99237dc..9b2a30c 100644
--- a/front_end/network/requestHeadersTree.css
+++ b/front_end/network/requestHeadersTree.css
@@ -161,6 +161,12 @@
   background-color: #ffff78;
 }
 
+.x-client-data-details {
+  padding-left: 10px;
+  word-break: break-word;
+  white-space: initial;
+}
+
 @media (forced-colors: active) {
   :host-context(.request-headers-tree) ol.tree-outline:not(.hide-selection-when-blurred) li.selected:focus {
     background: Highlight;
diff --git a/front_end/shell.json b/front_end/shell.json
index 66b3fd3..085258b 100644
--- a/front_end/shell.json
+++ b/front_end/shell.json
@@ -1,6 +1,7 @@
 {
   "modules" : [
     { "name": "bindings", "type": "autostart" },
+    { "name": "browser_sdk", "type": "autostart" },
     { "name": "common", "type": "autostart" },
     { "name": "components", "type": "autostart"},
     { "name": "console_counters", "type": "autostart" },
@@ -11,17 +12,17 @@
     { "name": "persistence", "type": "autostart" },
     { "name": "platform", "type": "autostart" },
     { "name": "protocol_client", "type": "autostart" },
-    { "name": "sdk", "type": "autostart" },
-    { "name": "browser_sdk", "type": "autostart" },
     { "name": "root", "type": "autostart" },
+    { "name": "sdk", "type": "autostart" },
     { "name": "services", "type": "autostart" },
     { "name": "text_utils", "type": "autostart" },
     { "name": "ui", "type": "autostart" },
     { "name": "workspace", "type": "autostart" },
 
     { "name": "changes" },
-    { "name": "cm" },
+    { "name": "client_variations" },
     { "name": "cm_modes" },
+    { "name": "cm" },
     { "name": "color_picker" },
     { "name": "console" },
     { "name": "coverage" },
@@ -31,10 +32,13 @@
     { "name": "formatter" },
     { "name": "heap_snapshot_model" },
     { "name": "inline_editor" },
+    { "name": "input"},
     { "name": "javascript_metadata" },
+    { "name": "marked" },
     { "name": "object_ui" },
     { "name": "perf_ui" },
     { "name": "profiler" },
+    { "name": "protocol_monitor"},
     { "name": "quick_open" },
     { "name": "search" },
     { "name": "settings" },
@@ -42,9 +46,6 @@
     { "name": "source_frame" },
     { "name": "sources" },
     { "name": "text_editor" },
-    { "name": "workspace_diff" },
-    { "name": "protocol_monitor"},
-    { "name": "input"},
-    { "name": "marked" }
+    { "name": "workspace_diff" }
   ]
 }
diff --git a/scripts/eslint_rules/lib/es_modules_import.js b/scripts/eslint_rules/lib/es_modules_import.js
index bc5c658..63e58a9 100644
--- a/scripts/eslint_rules/lib/es_modules_import.js
+++ b/scripts/eslint_rules/lib/es_modules_import.js
@@ -24,6 +24,8 @@
   path.join(FRONT_END_DIRECTORY, 'third_party', 'acorn-loose'),
   // marked is exempt as it doesn't expose all its modules from the root file
   path.join(FRONT_END_DIRECTORY, 'third_party', 'marked'),
+  // client-variations is exempt as it doesn't expose all its modules from the root file
+  path.join(FRONT_END_DIRECTORY, 'third_party', 'chromium', 'client-variations'),
 ]);
 
 const CROSS_NAMESPACE_MESSAGE =
diff --git a/test/unittests/front_end/common/BUILD.gn b/test/unittests/front_end/common/BUILD.gn
index b0506b9..17e9554 100644
--- a/test/unittests/front_end/common/BUILD.gn
+++ b/test/unittests/front_end/common/BUILD.gn
@@ -23,7 +23,7 @@
   ]
 
   deps = [
+    "../../../../front_end/client_variations:bundle",
     "../../../../front_end/common:bundle",
-    "../../../../front_end/third_party/chromium",
   ]
 }
diff --git a/test/unittests/front_end/common/ClientVariationsParser_test.ts b/test/unittests/front_end/common/ClientVariationsParser_test.ts
index 08764e5..7cddab5 100644
--- a/test/unittests/front_end/common/ClientVariationsParser_test.ts
+++ b/test/unittests/front_end/common/ClientVariationsParser_test.ts
@@ -4,7 +4,7 @@
 
 const {assert} = chai;
 
-import {parseClientVariations} from '../../../../front_end/third_party/chromium/client-variations/ClientVariationsParser.js';
+import {parseClientVariations} from '../../../../front_end/client_variations/client_variations.js';
 
 describe('parseClientVariations', () => {
   it('returns empty lists for unparseable text', () => {