Web Inspector: Please support HAR Export for network traffic
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 23 Oct 2017 21:34:59 +0000 (21:34 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 23 Oct 2017 21:34:59 +0000 (21:34 +0000)
https://bugs.webkit.org/show_bug.cgi?id=146692
<rdar://problem/7463672>

Reviewed by Brian Burg.

Source/JavaScriptCore:

* inspector/protocol/Network.json:
Add a walltime to each send request.

Source/WebCore:

Tests: http/tests/inspector/network/har/har-basic.html
       http/tests/inspector/network/har/har-page.html

* inspector/InspectorNetworkAgent.cpp:
(WebCore::InspectorNetworkAgent::willSendRequest):
Include the wall time when sending a request. This is needed for HAR to
include a wall time, and can be used for Cookie expiration time calculation
as well.

Source/WebInspectorUI:

* UserInterface/Main.html:
* UserInterface/Test.html:
New resources.

* UserInterface/Base/Platform.js:
Include a build number as well.

* UserInterface/Base/URLUtilities.js:
(parseLocationQueryParameters): Deleted.
Remove unused function.

* UserInterface/Controllers/FrameResourceManager.js:
(WI.FrameResourceManager.prototype.frameDidNavigate):
(WI.FrameResourceManager.prototype.resourceRequestWillBeSent):
(WI.FrameResourceManager.prototype.resourceRequestWasServedFromMemoryCache):
(WI.FrameResourceManager.prototype.resourceRequestDidReceiveResponse):
(WI.FrameResourceManager.prototype._addNewResourceToFrameOrTarget):
Pass along a walltime.

* UserInterface/Protocol/NetworkObserver.js:
(WI.NetworkObserver.prototype.requestWillBeSent):
Pass along a walltime. This new parameter shifts old parameters.

* UserInterface/Controllers/HARBuilder.js: Added.
(WI.HARBuilder.async.buildArchive):
(WI.HARBuilder.creator):
(WI.HARBuilder.pages):
(WI.HARBuilder.pageTimings):
(WI.HARBuilder.entry):
(WI.HARBuilder.request):
(WI.HARBuilder.response):
(WI.HARBuilder.cookies):
(WI.HARBuilder.headers):
(WI.HARBuilder.content):
(WI.HARBuilder.postData):
(WI.HARBuilder.cache):
(WI.HARBuilder.timings):
(WI.HARBuilder.ipAddress):
(WI.HARBuilder.date):
(WI.HARBuilder.fetchType):
HAR construction and helpers.

* UserInterface/Models/Cookie.js:
(WI.Cookie.prototype.expirationDate):
* UserInterface/Models/Resource.js:
(WI.Resource.prototype.get queryStringParameters):
(WI.Resource.prototype.get requestFormParameters):
(WI.Resource.prototype.get requestSentWalltime):
(WI.Resource.prototype.get requestSentDate):
(WI.Resource.prototype.hasRequestFormParameters):
Helpers for HAR generation and sub-sets of data.

* UserInterface/Models/SourceCode.js:
(WI.SourceCode.prototype._processContent):
Capture the raw, unmodified, base64 encoded flag and content. This ends
up getting used by HAR generation and is otherwise lost.

* UserInterface/Test/TestHarness.js:
(TestHarness.prototype.json):
Helper for just logging JSON data with a filter. This defaults to
a reasonable 2 space indent for JSON logs in our test output.

* UserInterface/Views/DOMTreeContentView.js:
(WI.DOMTreeContentView.prototype.get saveData):
(WI.DOMTreeContentView.get saveData.saveHandler): Deleted.
Drive-by simplify while looking at other save handlers.

* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView.prototype.get supportsSave):
(WI.NetworkTableContentView.prototype.get saveData):
(WI.NetworkTableContentView.prototype.tableCellContextMenuClicked):
(WI.NetworkTableContentView.prototype._HARResources):
(WI.NetworkTableContentView.prototype._exportHAR):
Provide a context menu and save keyboard handler to export a HAR.
This matches other browsers.

* UserInterface/Views/ResourceClusterContentView.js:
(WI.ResourceClusterContentView.prototype._canShowRequestContentView):
Use code that is now available in Resource.

LayoutTests:

* http/tests/inspector/network/har/har-basic-expected.txt: Added.
* http/tests/inspector/network/har/har-basic.html: Added.
* http/tests/inspector/network/har/har-page-expected.txt: Added.
* http/tests/inspector/network/har/har-page.html: Added.
Tests with mock resources / data and real resources.

* platform/mac-wk1/TestExpectations:
* platform/mac/TestExpectations:
* platform/win/TestExpectations:
Skip on platforms that cannot provide complete metrics, so some optional
fields may be missing.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@223856 268f45cc-cd09-0410-ab3c-d52691b4dbfc

27 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/inspector/network/har/har-basic-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/inspector/network/har/har-basic.html [new file with mode: 0644]
LayoutTests/http/tests/inspector/network/har/har-page-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/inspector/network/har/har-page.html [new file with mode: 0644]
LayoutTests/platform/mac-wk1/TestExpectations
LayoutTests/platform/mac/TestExpectations
LayoutTests/platform/win/TestExpectations
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/protocol/Network.json
Source/WebCore/ChangeLog
Source/WebCore/inspector/InspectorNetworkAgent.cpp
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Base/Platform.js
Source/WebInspectorUI/UserInterface/Base/URLUtilities.js
Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js
Source/WebInspectorUI/UserInterface/Controllers/HARBuilder.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Cookie.js
Source/WebInspectorUI/UserInterface/Models/Resource.js
Source/WebInspectorUI/UserInterface/Models/SourceCode.js
Source/WebInspectorUI/UserInterface/Protocol/NetworkObserver.js
Source/WebInspectorUI/UserInterface/Test.html
Source/WebInspectorUI/UserInterface/Test/TestHarness.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeContentView.js
Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js
Source/WebInspectorUI/UserInterface/Views/ResourceClusterContentView.js

index 67bfdf7..62be165 100644 (file)
@@ -1,3 +1,23 @@
+2017-10-23  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Please support HAR Export for network traffic
+        https://bugs.webkit.org/show_bug.cgi?id=146692
+        <rdar://problem/7463672>
+
+        Reviewed by Brian Burg.
+
+        * http/tests/inspector/network/har/har-basic-expected.txt: Added.
+        * http/tests/inspector/network/har/har-basic.html: Added.
+        * http/tests/inspector/network/har/har-page-expected.txt: Added.
+        * http/tests/inspector/network/har/har-page.html: Added.
+        Tests with mock resources / data and real resources.
+
+        * platform/mac-wk1/TestExpectations:
+        * platform/mac/TestExpectations:
+        * platform/win/TestExpectations:
+        Skip on platforms that cannot provide complete metrics, so some optional
+        fields may be missing.
+
 2017-10-23  Andy Estes  <aestes@apple.com>
 
         [Payment Request] Resolve PaymentRequest.show()'s accept promise when a payment is authorized
diff --git a/LayoutTests/http/tests/inspector/network/har/har-basic-expected.txt b/LayoutTests/http/tests/inspector/network/har/har-basic-expected.txt
new file mode 100644 (file)
index 0000000..6e99eba
--- /dev/null
@@ -0,0 +1,144 @@
+Basic tests for HAR.
+
+
+== Running test suite: HAR.Basic
+-- Running test case: HAR.Basic.Empty
+{
+  "log": {
+    "version": "1.2",
+    "creator": {
+      "name": "WebKit Web Inspector",
+      "version": "<filtered>"
+    },
+    "pages": [
+      {
+        "startedDateTime": "",
+        "id": "page_0",
+        "title": "http://127.0.0.1:8000/inspector/network/har/har-basic.html",
+        "pageTimings": {}
+      }
+    ],
+    "entries": []
+  }
+}
+
+-- Running test case: HAR.Basic.FakeResources
+{
+  "log": {
+    "version": "1.2",
+    "creator": {
+      "name": "WebKit Web Inspector",
+      "version": "<filtered>"
+    },
+    "pages": [
+      {
+        "startedDateTime": "",
+        "id": "page_0",
+        "title": "http://127.0.0.1:8000/inspector/network/har/har-basic.html",
+        "pageTimings": {}
+      }
+    ],
+    "entries": [
+      {
+        "pageref": "page_0",
+        "startedDateTime": "2017-10-23T01:55:52.694Z",
+        "time": 0,
+        "request": {
+          "method": "GET",
+          "url": "https://example.com/fake.js",
+          "httpVersion": "",
+          "cookies": [],
+          "headers": [
+            {
+              "name": "Test-Request-Header",
+              "value": "Test Request Header Value"
+            }
+          ],
+          "queryString": [],
+          "headersSize": -1,
+          "bodySize": -1
+        },
+        "response": {
+          "status": 0,
+          "statusText": "",
+          "httpVersion": "",
+          "cookies": [],
+          "headers": [],
+          "content": {
+            "size": 0,
+            "compression": 0,
+            "mimeType": "text/javascript"
+          },
+          "redirectURL": "",
+          "headersSize": -1,
+          "bodySize": -1
+        },
+        "cache": {},
+        "timings": {
+          "blocked": -1,
+          "dns": -1,
+          "connect": -1,
+          "ssl": -1,
+          "send": 0,
+          "wait": 0,
+          "receive": 0
+        }
+      },
+      {
+        "pageref": "page_0",
+        "startedDateTime": "2017-10-23T01:55:52.694Z",
+        "time": 700,
+        "request": {
+          "method": "GET",
+          "url": "https://example.com/fake.js",
+          "httpVersion": "HTTP/1.1",
+          "cookies": [],
+          "headers": [
+            {
+              "name": "Test-Request-Header",
+              "value": "Test Request Header Value"
+            }
+          ],
+          "queryString": [],
+          "headersSize": 100,
+          "bodySize": 0
+        },
+        "response": {
+          "status": 200,
+          "statusText": "OK",
+          "httpVersion": "HTTP/1.1",
+          "cookies": [],
+          "headers": [
+            {
+              "name": "Test-Response-Header",
+              "value": "Test Response Header Value"
+            }
+          ],
+          "content": {
+            "size": 1234,
+            "compression": 434,
+            "mimeType": "text/javascript"
+          },
+          "redirectURL": "",
+          "headersSize": 200,
+          "bodySize": 800,
+          "_transferSize": 1000
+        },
+        "cache": {},
+        "timings": {
+          "blocked": 100.00000000000009,
+          "dns": 99.99999999999987,
+          "connect": 99.99999999999987,
+          "ssl": 49.99999999999982,
+          "send": 100.00000000000009,
+          "wait": 100.00000000000009,
+          "receive": 99.99999999999987
+        },
+        "serverIPAddress": "12.34.56.78",
+        "connection": "1",
+        "_fetchType": "Network Load"
+      }
+    ]
+  }
+}
+
diff --git a/LayoutTests/http/tests/inspector/network/har/har-basic.html b/LayoutTests/http/tests/inspector/network/har/har-basic.html
new file mode 100644 (file)
index 0000000..3c8fef9
--- /dev/null
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="../../resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    function HARJSONFilter(key, value) {
+        // Filter out the creator.version / browser.version but leave a top level version.
+        if ((key === "creator" || key === "browser") && value.version) {
+            value.version = "<filtered>";
+            return value;
+        }
+        return value;
+    }
+
+    let suite = InspectorTest.createAsyncSuite("HAR.Basic");
+
+    suite.addTestCase({
+        name: "HAR.Basic.Empty",
+        description: "Should be able to generate a HAR with no resources.",
+        async test() {
+            let har = await WI.HARBuilder.buildArchive([]);
+            InspectorTest.json(har, HARJSONFilter);
+        }
+    });
+
+    suite.addTestCase({
+        name: "HAR.Basic.FakeResources",
+        description: "Should be able to generate a HAR with resources.",
+        async test() {
+            // FIXME: We should have an easier way to construct a Resource with mock data.
+            const url = "https://example.com/fake.js";
+            const mimeType = "text/javascript";
+            const type = WI.Resource.Type.Script;
+            const loaderIdentifier = undefined;
+            const targetId = undefined;
+            const requestIdentifier = undefined;
+            const requestMethod = "GET";
+            const requestHeaders = {"Test-Request-Header": "Test Request Header Value"};
+            const responseHeaders = {"Test-Response-Header": "Test Response Header Value"};
+            const statusCode = 200;
+            const statusText = "OK";
+            const source = "network";
+            const requestData = null;
+            const requestSentWalltime = 1508723752694 / 1000; // Sun Oct 22 2017 18:55:52 GMT-0700, when this test was written.
+            const initiatorSourceCodeLocation = null;
+            const timestamp = undefined;
+            const size = 1234;
+            const timingData = {
+                startTime: 1,
+                domainLookupStart: 100,
+                domainLookupEnd: 200,
+                connectStart: 300,
+                connectEnd: 400,
+                secureConnectionStart: 350,
+                requestStart: 500,
+                responseStart: 600,
+                responseEnd: 700,
+            };
+            const metrics = {
+                protocol: "http/1.1",
+                priority: "medium",
+                remoteAddress: "12.34.56.78:443",
+                connectionIdentifier: 123,
+                requestHeaderBytesSent: 100,
+                requestBodyBytesSent: 0,
+                responseHeaderBytesReceived: 200,
+                responseBodyBytesReceived: 800,
+                responseBodyDecodedSize: 1234,
+                requestHeaders,
+            };
+
+            let bareResource = new WI.Resource(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, timestamp, requestSentWalltime, initiatorSourceCodeLocation, timestamp);
+            bareResource.markAsFinished(undefined);
+
+            let fullResource = new WI.Resource(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, timestamp, requestSentWalltime, initiatorSourceCodeLocation, timestamp);
+            fullResource.updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, timestamp, timingData, source);
+            fullResource.increaseSize(size);
+            fullResource.updateWithMetrics(metrics);
+            fullResource.markAsFinished(1.7);
+
+            let har = await WI.HARBuilder.buildArchive([bareResource, fullResource]);
+            InspectorTest.json(har, HARJSONFilter);
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>Basic tests for HAR.</p>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/inspector/network/har/har-page-expected.txt b/LayoutTests/http/tests/inspector/network/har/har-page-expected.txt
new file mode 100644 (file)
index 0000000..759e6ab
--- /dev/null
@@ -0,0 +1,116 @@
+HAR Page Test.
+
+
+== Running test suite: HAR.Page
+-- Running test case: HAR.Basic.Page
+{
+  "log": {
+    "version": "1.2",
+    "creator": {
+      "name": "WebKit Web Inspector",
+      "version": "<filtered>"
+    },
+    "pages": [
+      {
+        "startedDateTime": "<filtered>",
+        "id": "page_0",
+        "title": "http://127.0.0.1:8000/inspector/network/har/har-page.html",
+        "pageTimings": {
+          "onContentLoad": "<filtered>",
+          "onLoad": "<filtered>"
+        }
+      }
+    ],
+    "entries": [
+      {
+        "pageref": "page_0",
+        "startedDateTime": "<filtered>",
+        "time": "<filtered>",
+        "request": {
+          "method": "GET",
+          "url": "http://127.0.0.1:8000/inspector/network/har/har-page.html",
+          "httpVersion": "HTTP/1.1",
+          "cookies": [],
+          "headers": "<filtered>",
+          "queryString": [],
+          "headersSize": "<filtered>",
+          "bodySize": "<filtered>"
+        },
+        "response": {
+          "status": 200,
+          "statusText": "OK",
+          "httpVersion": "HTTP/1.1",
+          "cookies": [],
+          "headers": "<filtered>",
+          "content": {
+            "size": 2658,
+            "compression": 0,
+            "mimeType": "text/html",
+            "text": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<script src=\"../../resources/inspector-test.js\"></script>\n<script>\nfunction test()\n{\n    function HARJSONFilter(key, value) {\n        // Filter out the creator.version / browser.version but leave a top level version.\n        if ((key === \"creator\" || key === \"browser\") && value.version) {\n            value.version = \"<filtered>\";\n            return value;\n        }\n\n        // Headers include dynamic data.\n        if (key === \"headers\")\n            return \"<filtered>\";\n\n        // Dates would change between test runs.\n        if (key.endsWith(\"DateTime\"))\n            return \"<filtered>\";\n\n        // Size data may or may not be available, but could change based on headers.\n        if (key.endsWith(\"Size\"))\n            return \"<filtered>\";\n\n        // Connection identifier could be different.\n        if (key === \"connection\")\n            return \"<filtered>\";\n\n        // Cache may or may not have been used.\n        if (key === \"_fetchType\")\n            return \"<filtered>\";\n\n        // Since cache may or may not be used, timing data may be variable.\n        // NOTE: SSL should always be -1 for this test case.\n        if (key === \"time\")\n            return \"<filtered>\";\n        if (key === \"timings\") {\n            value.blocked = \"<filtered>\";\n            value.dns = \"<filtered>\";\n            value.connect = \"<filtered>\";\n            value.send = \"<filtered>\";\n            value.wait = \"<filtered>\";\n            value.receive = \"<filtered>\";\n        }\n\n        // PageTimings can be variable.\n        if (key === \"onContentLoad\" || key === \"onLoad\")\n            return \"<filtered>\";\n\n        return value;\n    }\n\n    let suite = InspectorTest.createAsyncSuite(\"HAR.Page\");\n\n    suite.addTestCase({\n        name: \"HAR.Basic.Page\",\n        description: \"Should be able to generate a HAR with all of this test page's resources.\",\n        async test() {\n            InspectorTest.reloadPage({ignoreCache: true});\n            await InspectorTest.awaitEvent(\"LoadComplete\");\n\n            let resources = [];\n            resources.push(WI.frameResourceManager.mainFrame.mainResource);\n            for (let resource of WI.frameResourceManager.mainFrame.resourceCollection.items)\n                resources.push(resource);\n\n            let har = await WI.HARBuilder.buildArchive(resources);\n            InspectorTest.json(har, HARJSONFilter);\n        }\n    });\n\n    suite.runTestCasesAndFinish();\n}\n</script>\n</head>\n<body onload=\"runTest()\">\n<p>HAR Page Test.</p>\n<script>\nwindow.addEventListener(\"load\", () => {\n    TestPage.dispatchEventToFrontend(\"LoadComplete\");\n});\n</script>\n</body>\n</html>\n"
+          },
+          "redirectURL": "",
+          "headersSize": "<filtered>",
+          "bodySize": "<filtered>",
+          "_transferSize": "<filtered>"
+        },
+        "cache": {},
+        "timings": {
+          "blocked": "<filtered>",
+          "dns": "<filtered>",
+          "connect": "<filtered>",
+          "ssl": -1,
+          "send": "<filtered>",
+          "wait": "<filtered>",
+          "receive": "<filtered>"
+        },
+        "serverIPAddress": "127.0.0.1",
+        "connection": "<filtered>",
+        "_fetchType": "<filtered>"
+      },
+      {
+        "pageref": "page_0",
+        "startedDateTime": "<filtered>",
+        "time": "<filtered>",
+        "request": {
+          "method": "GET",
+          "url": "http://127.0.0.1:8000/inspector/resources/inspector-test.js",
+          "httpVersion": "",
+          "cookies": [],
+          "headers": "<filtered>",
+          "queryString": [],
+          "headersSize": "<filtered>",
+          "bodySize": "<filtered>"
+        },
+        "response": {
+          "status": 200,
+          "statusText": "OK",
+          "httpVersion": "",
+          "cookies": [],
+          "headers": "<filtered>",
+          "content": {
+            "size": 0,
+            "compression": 0,
+            "mimeType": "application/x-javascript",
+            "text": "/*\n * Copyright (C) 2013-2015 Apple Inc. All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\n * IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\n * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\n * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n// This namespace is injected into every test page. Its functions are invoked by\n// InspectorTest methods on the inspector page via a TestHarness subclass.\nTestPage = {};\nTestPage._initializers = [];\n\n// Helper scripts like `debugger-test.js` must register their initialization\n// function with this method so it will be marshalled to the inspector page.\nTestPage.registerInitializer = function(initializer)\n{\n    if (typeof initializer === \"function\")\n        this._initializers.push(initializer.toString());\n}\n\n// This function is called by the test document's body onload handler.\n\n// It initializes the inspector and loads any `*-test.js` helper scripts\n// into the inspector page context.\nfunction runTest()\n{\n    // Don't try to use testRunner if running through the browser.\n    if (!window.testRunner)\n        return;\n\n    // Set up the test page before the load event fires.\n    testRunner.dumpAsText();\n    testRunner.waitUntilDone();\n\n    window.internals.setInspectorIsUnderTest(true);\n    testRunner.showWebInspector();\n\n    let testFunction = window.test;\n    if (typeof testFunction !== \"function\") {\n        alert(\"Failed to send test() because it is not a function.\");\n        testRunner.notifyDone();\n    }\n\n    function runInitializationMethodsInFrontend(initializersArray)\n    {\n        InspectorTest.testPageDidLoad();\n\n        // If the test page reloaded but we started running the test in a previous\n        // navigation, then don't initialize the inspector frontend again.\n        if (InspectorTest.didInjectTestCode)\n            return;\n\n        for (let initializer of initializersArray) {\n            try {\n                initializer();\n            } catch (e) {\n                console.error(\"Exception in test initialization: \" + e, e.stack || \"(no stack trace)\");\n                InspectorTest.completeTest();\n            }\n        }\n    }\n\n    function runTestMethodInFrontend(testFunction)\n    {\n        if (InspectorTest.didInjectTestCode)\n            return;\n\n        InspectorTest.didInjectTestCode = true;\n\n        try {\n            testFunction();\n        } catch (e) {\n            // Using this instead of window.onerror will preserve the stack trace.\n            e.code = testFunction.toString();\n            InspectorTest.reportUncaughtException(e);\n        }\n    }\n\n    let initializationCodeString = `(${runInitializationMethodsInFrontend.toString()})([${TestPage._initializers}]);`;\n    let testFunctionCodeString = `(${runTestMethodInFrontend.toString()})(${testFunction.toString()});`;\n\n    testRunner.evaluateInWebInspector(initializationCodeString);\n    testRunner.evaluateInWebInspector(testFunctionCodeString);\n}\n\nfunction runTestHTTPS()\n{\n    if (window.testRunner) {\n        testRunner.dumpAsText();\n        testRunner.waitUntilDone();\n    }\n\n    let url = new URL(document.URL);\n    if (url.protocol !== \"https:\") {\n        url.protocol = \"https:\";\n        url.port = \"8443\";\n        window.location.href = url.toString();\n        return;\n    }\n\n    runTest();\n}\n\nTestPage.completeTest = function()\n{\n    // Don't try to use testRunner if running through the browser.\n    if (!window.testRunner)\n        return;\n\n    // Close inspector asynchrously in case we want to test tear-down behavior.\n    setTimeout(() => {\n        testRunner.closeWebInspector();\n        setTimeout(() => { testRunner.notifyDone(); }, 0);\n    }, 0);\n}\n\n// Logs message to unbuffered process stdout, avoiding timeouts.\n// only be used to debug tests and not to produce normal test output.\nTestPage.debugLog = function(message)\n{\n    window.alert(message);\n}\n\n// Add and clear test output from the results window.\nTestPage.addResult = function(text)\n{\n    // For early errors triggered when loading the test page, write to stderr.\n    if (!document.body) {\n        this.debugLog(text);\n        this.completeTest();\n    }\n\n    if (!this._resultElement) {\n        this._resultElement = document.createElement(\"pre\");\n        this._resultElement.id = \"output\";\n        document.body.appendChild(this._resultElement);\n    }\n\n    this._resultElement.append(text, document.createElement(\"br\"));\n}\n\nTestPage.log = TestPage.addResult;\n\nTestPage.dispatchEventToFrontend = function(eventName, data)\n{\n    let dispatchEventCodeString = `InspectorTest.dispatchEventToListeners(${JSON.stringify(eventName)}, ${JSON.stringify(data)});`;\n    testRunner.evaluateInWebInspector(dispatchEventCodeString);\n};\n\nTestPage.allowUncaughtExceptions = false;\nTestPage.needToSanitizeUncaughtExceptionURLs = false;\n\nTestPage.reportUncaughtException = function(message, url, lineNumber)\n{\n    if (TestPage.needToSanitizeUncaughtExceptionURLs) {\n        if (typeof url === \"string\") {\n            let lastSlash = url.lastIndexOf(\"/\");\n            let lastBackSlash = url.lastIndexOf(\"\\\\\");\n            let lastPathSeparator = Math.max(lastSlash, lastBackSlash);\n            if (lastPathSeparator > 0)\n                url = url.substr(lastPathSeparator + 1);\n        }\n    }\n\n    let result = `Uncaught exception in test page: ${message} [${url}:${lineNumber}]`;\n    TestPage.addResult(result);\n\n    if (!TestPage.allowUncaughtExceptions)\n        TestPage.completeTest();\n}\n\n// Catch syntax errors, type errors, and other exceptions. Run this before loading other files.\nwindow.onerror = TestPage.reportUncaughtException.bind(TestPage);\n"
+          },
+          "redirectURL": "",
+          "headersSize": "<filtered>",
+          "bodySize": "<filtered>",
+          "_transferSize": "<filtered>"
+        },
+        "cache": {},
+        "timings": {
+          "blocked": "<filtered>",
+          "dns": "<filtered>",
+          "connect": "<filtered>",
+          "ssl": -1,
+          "send": "<filtered>",
+          "wait": "<filtered>",
+          "receive": "<filtered>"
+        },
+        "_fetchType": "<filtered>"
+      }
+    ]
+  }
+}
+
diff --git a/LayoutTests/http/tests/inspector/network/har/har-page.html b/LayoutTests/http/tests/inspector/network/har/har-page.html
new file mode 100644 (file)
index 0000000..322332e
--- /dev/null
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="../../resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    function HARJSONFilter(key, value) {
+        // Filter out the creator.version / browser.version but leave a top level version.
+        if ((key === "creator" || key === "browser") && value.version) {
+            value.version = "<filtered>";
+            return value;
+        }
+
+        // Headers include dynamic data.
+        if (key === "headers")
+            return "<filtered>";
+
+        // Dates would change between test runs.
+        if (key.endsWith("DateTime"))
+            return "<filtered>";
+
+        // Size data may or may not be available, but could change based on headers.
+        if (key.endsWith("Size"))
+            return "<filtered>";
+
+        // Connection identifier could be different.
+        if (key === "connection")
+            return "<filtered>";
+
+        // Cache may or may not have been used.
+        if (key === "_fetchType")
+            return "<filtered>";
+
+        // Since cache may or may not be used, timing data may be variable.
+        // NOTE: SSL should always be -1 for this test case.
+        if (key === "time")
+            return "<filtered>";
+        if (key === "timings") {
+            value.blocked = "<filtered>";
+            value.dns = "<filtered>";
+            value.connect = "<filtered>";
+            value.send = "<filtered>";
+            value.wait = "<filtered>";
+            value.receive = "<filtered>";
+        }
+
+        // PageTimings can be variable.
+        if (key === "onContentLoad" || key === "onLoad")
+            return "<filtered>";
+
+        return value;
+    }
+
+    let suite = InspectorTest.createAsyncSuite("HAR.Page");
+
+    suite.addTestCase({
+        name: "HAR.Basic.Page",
+        description: "Should be able to generate a HAR with all of this test page's resources.",
+        async test() {
+            InspectorTest.reloadPage({ignoreCache: true});
+            await InspectorTest.awaitEvent("LoadComplete");
+
+            let resources = [];
+            resources.push(WI.frameResourceManager.mainFrame.mainResource);
+            for (let resource of WI.frameResourceManager.mainFrame.resourceCollection.items)
+                resources.push(resource);
+
+            let har = await WI.HARBuilder.buildArchive(resources);
+            InspectorTest.json(har, HARJSONFilter);
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>HAR Page Test.</p>
+<script>
+window.addEventListener("load", () => {
+    TestPage.dispatchEventToFrontend("LoadComplete");
+});
+</script>
+</body>
+</html>
index fa0eed9..480b672 100644 (file)
@@ -340,6 +340,7 @@ http/wpt/resource-timing/rt-nextHopProtocol.worker.html [ Failure ]
 http/tests/inspector/network/resource-request-headers.html [ Failure ]
 http/tests/inspector/network/resource-sizes-network.html [ Failure ]
 http/tests/inspector/network/resource-sizes-memory-cache.html [ Failure ]
+http/tests/inspector/network/har/har-page.html [ Failure ]
 
 webkit.org/b/164491 [ Yosemite ElCapitan ] fast/visual-viewport/rtl-zoomed-rects.html [ Failure ]
 
index 6248990..89ababd 100644 (file)
@@ -1429,6 +1429,7 @@ webkit.org/b/168415 [ ElCapitan ] http/wpt/resource-timing/rt-cors.worker.html [
 
 # Request Header networking data not available without Network Session.
 [ ElCapitan ] http/tests/inspector/network/resource-request-headers.html [ Failure ]
+[ ElCapitan ] http/tests/inspector/network/har/har-page.html [ Failure ]
 
 [ ElCapitan ] http/tests/inspector/network/ping-type.html [ Skip ]
 
index 7eafdc7..28edb35 100644 (file)
@@ -3599,6 +3599,7 @@ fast/shadow-dom/shadow-at-root-during-disconnect.html [ Skip ]
 fast/text/font-weight-fallback.html [ Skip ]
 http/tests/cache/cache-control-immutable-https.html [ Skip ]
 http/tests/inspector/network/resource-request-headers.html [ Skip ]
+http/tests/inspector/network/har/har-page.html [ Skip ]
 http/tests/local/blob/send-hybrid-blob-using-open-panel.html [ Skip ]
 http/tests/security/contentSecurityPolicy/cross-origin-plugin-document-allowed-in-child-window.html [ Skip ]
 http/tests/security/contentSecurityPolicy/same-origin-plugin-document-blocked-in-child-window.html [ Skip ]
index 643815a..c4b9e49 100644 (file)
@@ -1,3 +1,14 @@
+2017-10-23  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Please support HAR Export for network traffic
+        https://bugs.webkit.org/show_bug.cgi?id=146692
+        <rdar://problem/7463672>
+
+        Reviewed by Brian Burg.
+
+        * inspector/protocol/Network.json:
+        Add a walltime to each send request.
+
 2017-10-23  Matt Lewis  <jlewis3@apple.com>
 
         Unreviewed, rolling out r223820.
index 2a4d8c3..abe479c 100644 (file)
                 { "name": "loaderId", "$ref": "LoaderId", "description": "Loader identifier." },
                 { "name": "documentURL", "type": "string", "description": "URL of the document this request is loaded for." },
                 { "name": "request", "$ref": "Request", "description": "Request data." },
-                { "name": "timestamp", "$ref": "Timestamp", "description": "Timestamp." },
+                { "name": "timestamp", "$ref": "Timestamp" },
+                { "name": "walltime", "$ref": "Walltime" },
                 { "name": "initiator", "$ref": "Initiator", "description": "Request initiator." },
                 { "name": "redirectResponse", "optional": true, "$ref": "Response", "description": "Redirect response data." },
                 { "name": "type", "$ref": "Page.ResourceType", "optional": true, "description": "Resource type." },
index 5dd5d0f..42c40f7 100644 (file)
@@ -1,3 +1,20 @@
+2017-10-23  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Please support HAR Export for network traffic
+        https://bugs.webkit.org/show_bug.cgi?id=146692
+        <rdar://problem/7463672>
+
+        Reviewed by Brian Burg.
+
+        Tests: http/tests/inspector/network/har/har-basic.html
+               http/tests/inspector/network/har/har-page.html
+
+        * inspector/InspectorNetworkAgent.cpp:
+        (WebCore::InspectorNetworkAgent::willSendRequest):
+        Include the wall time when sending a request. This is needed for HAR to
+        include a wall time, and can be used for Cookie expiration time calculation
+        as well.
+
 2017-10-23  Andy Estes  <aestes@apple.com>
 
         [Payment Request] Resolve PaymentRequest.show()'s accept promise when a payment is authorized
index 3c985c5..80f89d4 100644 (file)
@@ -345,6 +345,9 @@ void InspectorNetworkAgent::willSendRequest(unsigned long identifier, DocumentLo
         return;
     }
 
+    double sendTimestamp = timestamp();
+    double walltime = currentTime();
+
     String requestId = IdentifiersFactory::requestId(identifier);
     m_resourcesData->resourceCreated(requestId, m_pageAgent->loaderId(&loader));
 
@@ -373,7 +376,7 @@ void InspectorNetworkAgent::willSendRequest(unsigned long identifier, DocumentLo
     RefPtr<Inspector::Protocol::Network::Initiator> initiatorObject = buildInitiatorObject(loader.frame() ? loader.frame()->document() : nullptr);
     String targetId = request.initiatorIdentifier();
 
-    m_frontendDispatcher->requestWillBeSent(requestId, m_pageAgent->frameId(loader.frame()), m_pageAgent->loaderId(&loader), loader.url().string(), buildObjectForResourceRequest(request), timestamp(), initiatorObject, buildObjectForResourceResponse(redirectResponse, nullptr), type != InspectorPageAgent::OtherResource ? &protocolResourceType : nullptr, targetId.isEmpty() ? nullptr : &targetId);
+    m_frontendDispatcher->requestWillBeSent(requestId, m_pageAgent->frameId(loader.frame()), m_pageAgent->loaderId(&loader), loader.url().string(), buildObjectForResourceRequest(request), sendTimestamp, walltime, initiatorObject, buildObjectForResourceResponse(redirectResponse, nullptr), type != InspectorPageAgent::OtherResource ? &protocolResourceType : nullptr, targetId.isEmpty() ? nullptr : &targetId);
 }
 
 static InspectorPageAgent::ResourceType resourceTypeForCachedResource(CachedResource* resource)
index 505ea86..9854478 100644 (file)
@@ -1,3 +1,91 @@
+2017-10-23  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: Please support HAR Export for network traffic
+        https://bugs.webkit.org/show_bug.cgi?id=146692
+        <rdar://problem/7463672>
+
+        Reviewed by Brian Burg.
+
+        * UserInterface/Main.html:
+        * UserInterface/Test.html:
+        New resources.
+
+        * UserInterface/Base/Platform.js:
+        Include a build number as well.
+        
+        * UserInterface/Base/URLUtilities.js:
+        (parseLocationQueryParameters): Deleted.
+        Remove unused function.
+
+        * UserInterface/Controllers/FrameResourceManager.js:
+        (WI.FrameResourceManager.prototype.frameDidNavigate):
+        (WI.FrameResourceManager.prototype.resourceRequestWillBeSent):
+        (WI.FrameResourceManager.prototype.resourceRequestWasServedFromMemoryCache):
+        (WI.FrameResourceManager.prototype.resourceRequestDidReceiveResponse):
+        (WI.FrameResourceManager.prototype._addNewResourceToFrameOrTarget):
+        Pass along a walltime.
+
+        * UserInterface/Protocol/NetworkObserver.js:
+        (WI.NetworkObserver.prototype.requestWillBeSent):
+        Pass along a walltime. This new parameter shifts old parameters.
+
+        * UserInterface/Controllers/HARBuilder.js: Added.
+        (WI.HARBuilder.async.buildArchive):
+        (WI.HARBuilder.creator):
+        (WI.HARBuilder.pages):
+        (WI.HARBuilder.pageTimings):
+        (WI.HARBuilder.entry):
+        (WI.HARBuilder.request):
+        (WI.HARBuilder.response):
+        (WI.HARBuilder.cookies):
+        (WI.HARBuilder.headers):
+        (WI.HARBuilder.content):
+        (WI.HARBuilder.postData):
+        (WI.HARBuilder.cache):
+        (WI.HARBuilder.timings):
+        (WI.HARBuilder.ipAddress):
+        (WI.HARBuilder.date):
+        (WI.HARBuilder.fetchType):
+        HAR construction and helpers.
+
+        * UserInterface/Models/Cookie.js:
+        (WI.Cookie.prototype.expirationDate):
+        * UserInterface/Models/Resource.js:
+        (WI.Resource.prototype.get queryStringParameters):
+        (WI.Resource.prototype.get requestFormParameters):
+        (WI.Resource.prototype.get requestSentWalltime):
+        (WI.Resource.prototype.get requestSentDate):
+        (WI.Resource.prototype.hasRequestFormParameters):
+        Helpers for HAR generation and sub-sets of data.
+
+        * UserInterface/Models/SourceCode.js:
+        (WI.SourceCode.prototype._processContent):
+        Capture the raw, unmodified, base64 encoded flag and content. This ends
+        up getting used by HAR generation and is otherwise lost.
+
+        * UserInterface/Test/TestHarness.js:
+        (TestHarness.prototype.json):
+        Helper for just logging JSON data with a filter. This defaults to
+        a reasonable 2 space indent for JSON logs in our test output.
+
+        * UserInterface/Views/DOMTreeContentView.js:
+        (WI.DOMTreeContentView.prototype.get saveData):
+        (WI.DOMTreeContentView.get saveData.saveHandler): Deleted.
+        Drive-by simplify while looking at other save handlers.
+
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView.prototype.get supportsSave):
+        (WI.NetworkTableContentView.prototype.get saveData):
+        (WI.NetworkTableContentView.prototype.tableCellContextMenuClicked):
+        (WI.NetworkTableContentView.prototype._HARResources):
+        (WI.NetworkTableContentView.prototype._exportHAR):
+        Provide a context menu and save keyboard handler to export a HAR.
+        This matches other browsers.
+
+        * UserInterface/Views/ResourceClusterContentView.js:
+        (WI.ResourceClusterContentView.prototype._canShowRequestContentView):
+        Use code that is now available in Resource.
+
 2017-10-20  Matt Baker  <mattbaker@apple.com>
 
         Web Inspector: scrolling the editor while debugging shouldn't trigger popovers
index 6ab082c..c82e012 100644 (file)
@@ -29,22 +29,30 @@ WI.Platform = {
     version: {
         base: 0,
         release: 0,
-        name: ""
+        name: "",
+        build: "",
     }
 };
 
 (function () {
-    // Check for a nightly build by looking for a plus in the version number and a small number of stylesheets (indicating combined resources).
-    var versionMatch = / AppleWebKit\/([^ ]+)/.exec(navigator.userAgent);
-    if (versionMatch && versionMatch[1].indexOf("+") !== -1 && document.styleSheets.length < 10)
-        WI.Platform.isNightlyBuild = true;
+    let versionMatch = / AppleWebKit\/([^ ]+)/.exec(navigator.userAgent);
+    if (versionMatch) {
+        WI.Platform.version.build = versionMatch[1];
 
-    var osVersionMatch = / Mac OS X (\d+)_(\d+)/.exec(navigator.appVersion);
+        // Check for a nightly build by looking for a plus in the version number and a small number of stylesheets (indicating combined resources).
+        if (versionMatch[1].indexOf("+") !== -1 && document.styleSheets.length < 10)
+            WI.Platform.isNightlyBuild = true;
+    }
+
+    let osVersionMatch = / Mac OS X (\d+)_(\d+)/.exec(navigator.appVersion);
     if (osVersionMatch && osVersionMatch[1] === "10") {
         WI.Platform.version.base = 10;
         WI.Platform.version.release = parseInt(osVersionMatch[2]);
         switch (osVersionMatch[2]) {
         case "12":
+            WI.Platform.version.name = "high-sierra";
+            break;
+        case "12":
             WI.Platform.version.name = "sierra";
             break;
         case "11":
index 6ae0c9d..bc93298 100644 (file)
@@ -186,12 +186,6 @@ function absoluteURL(partialURL, baseURL)
     return baseURLPrefix + resolveDotsInPath(basePath + partialURL);
 }
 
-function parseLocationQueryParameters(arrayResult)
-{
-    // The first character is always the "?".
-    return parseQueryString(window.location.search.substring(1), arrayResult);
-}
-
 function parseQueryString(queryString, arrayResult)
 {
     if (!queryString)
index cf10978..b3a01e9 100644 (file)
@@ -85,7 +85,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
             // If the frame wasn't known before now, then the main resource was loaded instantly (about:blank, etc.)
             // Make a new resource (which will make the frame). Mark will mark it as loaded at the end too since we
             // don't expect any more events about the load finishing for these frames.
-            var frameResource = this._addNewResourceToFrameOrTarget(null, framePayload.id, framePayload.loaderId, framePayload.url, null, null, null, null, null, framePayload.name, framePayload.securityOrigin);
+            var frameResource = this._addNewResourceToFrameOrTarget(null, framePayload.id, framePayload.loaderId, framePayload.url, null, null, null, null, null, null, framePayload.name, framePayload.securityOrigin);
             frame = frameResource.parentFrame;
             frameWasLoadedInstantly = true;
 
@@ -163,7 +163,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
             this._mainFrameDidChange(oldMainFrame);
     }
 
-    resourceRequestWillBeSent(requestIdentifier, frameIdentifier, loaderIdentifier, request, type, redirectResponse, timestamp, initiator, targetId)
+    resourceRequestWillBeSent(requestIdentifier, frameIdentifier, loaderIdentifier, request, type, redirectResponse, timestamp, walltime, initiator, targetId)
     {
         // Called from WI.NetworkObserver.
 
@@ -191,7 +191,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
         var initiatorSourceCodeLocation = this._initiatorSourceCodeLocationFromPayload(initiator);
 
         // This is a new request, make a new resource and add it to the right frame.
-        resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, request.url, type, request.method, request.headers, request.postData, elapsedTime, null, null, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId);
+        resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, request.url, type, request.method, request.headers, request.postData, elapsedTime, walltime, null, null, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId);
 
         // Associate the resource with the requestIdentifier so it can be found in future loading events.
         this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
@@ -327,7 +327,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
         let response = cachedResourcePayload.response;
         const responseSource = NetworkAgent.ResponseSource.MemoryCache;
 
-        let resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload.url, cachedResourcePayload.type, "GET", null, null, elapsedTime, null, null, initiatorSourceCodeLocation);
+        let resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload.url, cachedResourcePayload.type, "GET", null, null, elapsedTime, null, null, null, initiatorSourceCodeLocation);
         resource.updateForResponse(cachedResourcePayload.url, response.mimeType, cachedResourcePayload.type, response.headers, response.status, response.statusText, elapsedTime, response.timing, responseSource);
         resource.increaseSize(cachedResourcePayload.bodySize, elapsedTime);
         resource.increaseTransferSize(cachedResourcePayload.bodySize);
@@ -374,7 +374,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
         // If we haven't found an existing Resource by now, then it is a resource that was loading when the inspector
         // opened and we just missed the resourceRequestWillBeSent for it. So make a new resource and add it.
         if (!resource) {
-            resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, response.url, type, null, response.requestHeaders, null, elapsedTime, null, null, null);
+            resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, response.url, type, null, response.requestHeaders, null, elapsedTime, null, null, null, null);
 
             // Associate the resource with the requestIdentifier so it can be found in future loading events.
             this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
@@ -498,7 +498,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
 
     // Private
 
-    _addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, url, type, requestMethod, requestHeaders, requestData, elapsedTime, frameName, frameSecurityOrigin, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId)
+    _addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, url, type, requestMethod, requestHeaders, requestData, elapsedTime, walltime, frameName, frameSecurityOrigin, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId)
     {
         console.assert(!this._waitingForMainFrameResourceTreePayload);
 
@@ -512,7 +512,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
             else if (type === PageAgent.ResourceType.Document && frame.provisionalMainResource && frame.provisionalMainResource.url === url && frame.provisionalLoaderIdentifier === loaderIdentifier)
                 resource = frame.provisionalMainResource;
             else {
-                resource = new WI.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp);
+                resource = new WI.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, walltime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp);
                 if (resource.target === WI.mainTarget)
                     this._addResourceToFrame(frame, resource);
                 else if (resource.target)
@@ -523,7 +523,7 @@ WI.FrameResourceManager = class FrameResourceManager extends WI.Object
         } else {
             // This is a new request for a new frame, which is always the main resource.
             console.assert(!targetId);
-            resource = new WI.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp);
+            resource = new WI.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, walltime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp);
             frame = new WI.Frame(frameIdentifier, frameName, frameSecurityOrigin, loaderIdentifier, resource);
             this._frameIdentifierMap.set(frame.id, frame);
 
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/HARBuilder.js b/Source/WebInspectorUI/UserInterface/Controllers/HARBuilder.js
new file mode 100644 (file)
index 0000000..d994627
--- /dev/null
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// HTTP Archive (HAR) format - Version 1.2
+// https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html#sec-har-object-types-creator
+// http://www.softwareishard.com/blog/har-12-spec/
+
+WI.HARBuilder = class HARBuilder
+{
+    static async buildArchive(resources)
+    {
+        let promises = [];
+        for (let resource of resources) {
+            console.assert(resource.finished);
+            promises.push(new Promise((resolve, reject) => {
+                // Always resolve.
+                resource.requestContent().then(
+                    (x) => resolve(x),
+                    () => resolve(null)
+                );
+            }));
+        }
+
+        let contents = await Promise.all(promises);
+        console.assert(contents.length === resources.length);
+
+        return {
+            log: {
+                version: "1.2",
+                creator: HARBuilder.creator(),
+                pages: HARBuilder.pages(),
+                entries: resources.map((resource, index) => HARBuilder.entry(resource, contents[index])),
+            }
+        };
+    }
+
+    static creator()
+    {
+        return {
+            name: "WebKit Web Inspector",
+            version: WI.Platform.version.build || "1.0",
+        };
+    }
+
+    static pages()
+    {
+        return [{
+            startedDateTime: HARBuilder.date(WI.frameResourceManager.mainFrame.mainResource.requestSentDate),
+            id: "page_0",
+            title: WI.frameResourceManager.mainFrame.url || "",
+            pageTimings: HARBuilder.pageTimings(),
+        }];
+    }
+
+    static pageTimings()
+    {
+        let result = {};
+
+        let domContentReadyEventTimestamp = WI.frameResourceManager.mainFrame.domContentReadyEventTimestamp;
+        if (!isNaN(domContentReadyEventTimestamp))
+            result.onContentLoad = domContentReadyEventTimestamp * 1000;
+
+        let loadEventTimestamp = WI.frameResourceManager.mainFrame.loadEventTimestamp;
+        if (!isNaN(loadEventTimestamp))
+            result.onLoad = loadEventTimestamp * 1000;
+
+        return result;
+    }
+
+    static entry(resource, content)
+    {
+        let entry = {
+            pageref: "page_0",
+            startedDateTime: HARBuilder.date(resource.requestSentDate),
+            time: 0,
+            request: HARBuilder.request(resource),
+            response: HARBuilder.response(resource, content),
+            cache: HARBuilder.cache(resource),
+            timings: HARBuilder.timings(resource),
+        };
+
+        if (resource.timingData.startTime && resource.timingData.responseEnd)
+            entry.time = (resource.timingData.responseEnd - resource.timingData.startTime) * 1000;
+        if (resource.remoteAddress)
+            entry.serverIPAddress = HARBuilder.ipAddress(resource.remoteAddress);
+        if (resource.connectionIdentifier)
+            entry.connection = "" + resource.connectionIdentifier;
+
+        // CFNetwork Custom Field `_fetchType`.
+        if (resource.responseSource !== WI.Resource.ResponseSource.Unknown)
+            entry._fetchType = HARBuilder.fetchType(resource.responseSource);
+
+        return entry;
+    }
+
+    static request(resource)
+    {
+        let result = {
+            method: resource.requestMethod || "",
+            url: resource.url || "",
+            httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
+            cookies: HARBuilder.cookies(resource.requestCookies, null),
+            headers: HARBuilder.headers(resource.requestHeaders),
+            queryString: resource.queryStringParameters || [],
+            headersSize: !isNaN(resource.requestHeadersTransferSize) ? resource.requestHeadersTransferSize : -1,
+            bodySize: !isNaN(resource.requestBodyTransferSize) ? resource.requestBodyTransferSize : -1,
+        };
+
+        if (resource.requestData)
+            result.postData = HARBuilder.postData(resource);
+
+        return result;
+    }
+
+    static response(resource, content)
+    {
+        let result = {
+            status: resource.statusCode || 0,
+            statusText: resource.statusText || "",
+            httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
+            cookies: HARBuilder.cookies(resource.responseCookies, resource.requestSentDate),
+            headers: HARBuilder.headers(resource.responseHeaders),
+            content: HARBuilder.content(resource, content),
+            redirectURL: resource.responseHeaders.valueForCaseInsensitiveKey("Location") || "",
+            headersSize: !isNaN(resource.responseHeadersTransferSize) ? resource.responseHeadersTransferSize : -1,
+            bodySize: !isNaN(resource.responseBodyTransferSize) ? resource.responseBodyTransferSize : -1,
+        };
+
+        // Chrome Custom Field `_transferSize`.
+        if (!isNaN(resource.networkTotalTransferSize))
+            result._transferSize = resource.networkTotalTransferSize;
+
+        // Chrome Custom Field `_error`.
+        if (resource.failureReasonText)
+            result._error = resource.failureReasonText;
+
+        return result;
+    }
+
+    static cookies(cookies, requestSentDate)
+    {
+        let result = [];
+
+        for (let cookie of cookies) {
+            let json = {
+                name: cookie.name,
+                value: cookie.value,
+            };
+
+            if (cookie.type === WI.Cookie.Type.Response) {
+                if (cookie.path)
+                    json.path = cookie.path;
+                if (cookie.domain)
+                    json.domain = cookie.domain;
+                json.expires = HARBuilder.date(cookie.expirationDate(requestSentDate));
+                json.httpOnly = cookie.httpOnly;
+                json.secure = cookie.secure;
+            }
+
+            result.push(json);
+        }
+
+        return result;
+    }
+
+    static headers(headers)
+    {
+        let result = [];
+
+        for (let key in headers)
+            result.push({name: key, value: headers[key]});
+
+        return result;
+    }
+
+    static content(resource, content)
+    {
+        let encodedSize = !isNaN(resource.networkEncodedSize) ? resource.networkEncodedSize : resource.estimatedNetworkEncodedSize;
+        let decodedSize = !isNaN(resource.networkDecodedSize) ? resource.networkDecodedSize : resource.size;
+
+        if (isNaN(decodedSize))
+            decodedSize = 0;
+        if (isNaN(encodedSize))
+            encodedSize = 0;
+
+        let result = {
+            size: decodedSize,
+            compression: decodedSize - encodedSize,
+            mimeType: resource.mimeType || "x-unknown",
+        };
+
+        if (content) {
+            if (content.rawContent)
+                result.text = content.rawContent;
+            if (content.rawBase64Encoded)
+                result.encoding = "base64";
+        }
+
+        return result;
+    }
+
+    static postData(resource)
+    {
+        return {
+            mimeType: resource.requestDataContentType || "",
+            text: resource.requestData,
+            params: resource.requestFormParameters || [],
+        };
+    }
+
+    static cache(resource)
+    {
+        // FIXME: <https://webkit.org/b/178682> Web Inspector: Include <cache> details in HAR Export
+        // http://www.softwareishard.com/blog/har-12-spec/#cache
+        return {};
+    }
+
+    static timings(resource)
+    {
+        // Chrome has Custom Fields `_blocked_queueing` and `_blocked_proxy`.
+
+        let result = {
+            blocked: -1,
+            dns: -1,
+            connect: -1,
+            ssl: -1,
+            send: 0,
+            wait: 0,
+            receive: 0,
+        };
+
+        if (resource.timingData.startTime && resource.timingData.responseEnd) {
+            let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
+            result.blocked = ((domainLookupStart || connectStart || requestStart) - startTime) * 1000;
+            if (domainLookupStart)
+                result.dns = ((domainLookupEnd || connectStart || requestStart) - domainLookupStart) * 1000;
+            if (connectStart)
+                result.connect = ((connectEnd || requestStart) - connectStart) * 1000;
+            if (secureConnectionStart)
+                result.ssl = ((connectEnd || requestStart) - secureConnectionStart) * 1000;
+            result.send = (requestStart - (connectEnd || domainLookupEnd || startTime)) * 1000;
+            result.wait = (responseStart - requestStart) * 1000;
+            result.receive = (responseEnd - responseStart) * 1000;
+        }
+
+        return result;
+    }
+
+    // Helpers
+
+    static ipAddress(remoteAddress)
+    {
+        // IP Address, without port.
+        if (!remoteAddress)
+            return "";
+
+        // NOTE: Resource.remoteAddress always includes the port at the end.
+        // So this always strips the last part.
+        return remoteAddress.replace(/:\d+$/, "");
+    }
+
+    static date(date)
+    {
+        // ISO 8601
+        if (!date)
+            return "";
+
+        return date.toISOString();
+    }
+
+    static fetchType(responseSource)
+    {
+        switch (responseSource) {
+        case WI.Resource.ResponseSource.Network:
+            return "Network Load";
+        case WI.Resource.ResponseSource.MemoryCache:
+            return "Memory Cache";
+        case WI.Resource.ResponseSource.DiskCache:
+            return "Disk Cache";
+        }
+
+        console.assert(false);
+        return undefined;
+    }
+}
index 9308a4e..e4fb40d 100644 (file)
     <script src="Controllers/Formatter.js"></script>
     <script src="Controllers/FormatterSourceMap.js"></script>
     <script src="Controllers/FrameResourceManager.js"></script>
+    <script src="Controllers/HARBuilder.js"></script>
     <script src="Controllers/HeapManager.js"></script>
     <script src="Controllers/IssueManager.js"></script>
     <script src="Controllers/JavaScriptLogViewController.js"></script>
index 3cfc06f..84c1a10 100644 (file)
@@ -53,6 +53,18 @@ WI.Cookie = class Cookie
         }
     }
 
+    // Public
+
+    expirationDate(requestSentDate)
+    {
+        if (this.maxAge) {
+            let startDate = requestSentDate || new Date;
+            return new Date(startDate.getTime() + (this.maxAge * 1000));
+        }
+
+        return this.expires;
+    }
+
     // Static
 
     // RFC 6265 defines the HTTP Cookie and Set-Cookie header fields:
index 69063b1..2cc2bf3 100644 (file)
@@ -26,7 +26,7 @@
 
 WI.Resource = class Resource extends WI.SourceCode
 {
-    constructor(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp)
+    constructor(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, requestSentWalltime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp)
     {
         super();
 
@@ -42,6 +42,8 @@ WI.Resource = class Resource extends WI.SourceCode
         this._type = type || WI.Resource.typeFromMIMEType(mimeType);
         this._loaderIdentifier = loaderIdentifier || null;
         this._requestIdentifier = requestIdentifier || null;
+        this._queryStringParameters = undefined;
+        this._requestFormParameters = undefined;
         this._requestMethod = requestMethod || null;
         this._requestData = requestData || null;
         this._requestHeaders = requestHeaders || {};
@@ -53,6 +55,7 @@ WI.Resource = class Resource extends WI.SourceCode
         this._initiatedResources = [];
         this._originalRequestWillBeSentTimestamp = originalRequestWillBeSentTimestamp || null;
         this._requestSentTimestamp = requestSentTimestamp || NaN;
+        this._requestSentWalltime = requestSentWalltime || NaN;
         this._responseReceivedTimestamp = NaN;
         this._lastRedirectReceivedTimestamp = NaN;
         this._lastDataReceivedTimestamp = NaN;
@@ -410,6 +413,20 @@ WI.Resource = class Resource extends WI.SourceCode
         return this._failureReasonText;
     }
 
+    get queryStringParameters()
+    {
+        if (this._queryStringParameters === undefined)
+            this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true);
+        return this._queryStringParameters;
+    }
+
+    get requestFormParameters()
+    {
+        if (this._requestFormParameters === undefined)
+            this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null;
+        return this._requestFormParameters;
+    }
+
     get requestDataContentType()
     {
         return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
@@ -460,6 +477,16 @@ WI.Resource = class Resource extends WI.SourceCode
         return this._requestSentTimestamp;
     }
 
+    get requestSentWalltime()
+    {
+        return this._requestSentWalltime;
+    }
+
+    get requestSentDate()
+    {
+        return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000);
+    }
+
     get lastRedirectReceivedTimestamp()
     {
         return this._lastRedirectReceivedTimestamp;
@@ -656,6 +683,12 @@ WI.Resource = class Resource extends WI.SourceCode
         return !isNaN(this._statusCode) || this._finished;
     }
 
+    hasRequestFormParameters()
+    {
+        let requestDataContentType = this.requestDataContentType;
+        return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i);
+    }
+
     updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source)
     {
         console.assert(!this._finished);
index 83371ec..8343336 100644 (file)
@@ -194,12 +194,13 @@ WI.SourceCode = class SourceCode extends WI.Object
     _processContent(parameters)
     {
         // Different backend APIs return one of `content, `body`, `text`, or `scriptSource`.
-        var content = parameters.content || parameters.body || parameters.text || parameters.scriptSource;
-        var error = parameters.error;
+        let rawContent = parameters.content || parameters.body || parameters.text || parameters.scriptSource;
+        let content = rawContent;
+        let error = parameters.error;
         if (parameters.base64Encoded)
             content = content ? decodeBase64ToBlob(content, this.mimeType) : "";
 
-        var revision = this.revisionForRequestedContent;
+        let revision = this.revisionForRequestedContent;
 
         this._ignoreRevisionContentDidChangeEvent = true;
         revision.content = content || null;
@@ -213,6 +214,8 @@ WI.SourceCode = class SourceCode extends WI.Object
             error,
             sourceCode: this,
             content,
+            rawContent,
+            rawBase64Encoded: parameters.base64Encoded,
         });
     }
 };
index ab05456..a52d1e1 100644 (file)
@@ -27,9 +27,18 @@ WI.NetworkObserver = class NetworkObserver
 {
     // Events defined by the "Network" domain.
 
-    requestWillBeSent(requestId, frameId, loaderId, documentURL, request, timestamp, initiator, redirectResponse, type, targetId)
+    requestWillBeSent(requestId, frameId, loaderId, documentURL, request, timestamp, walltime, initiator, redirectResponse, type, targetId)
     {
-        WI.frameResourceManager.resourceRequestWillBeSent(requestId, frameId, loaderId, request, type, redirectResponse, timestamp, initiator, targetId);
+        // COMPATIBILITY(iOS 11.0): `walltime` did not exist in 11.0 and earlier.
+        if (!NetworkAgent.hasEventParameter("requestWillBeSent", "walltime")) {
+            walltime = undefined;
+            initiator = arguments[6];
+            redirectResponse = arguments[7];
+            type = arguments[8];
+            targetId = arguments[9];
+        }
+
+        WI.frameResourceManager.resourceRequestWillBeSent(requestId, frameId, loaderId, request, type, redirectResponse, timestamp, walltime, initiator, targetId);
     }
 
     requestServedFromCache(requestId)
index 75fc2a7..c716d98 100644 (file)
     <script src="Controllers/DOMTreeManager.js"></script>
     <script src="Controllers/DebuggerManager.js"></script>
     <script src="Controllers/FrameResourceManager.js"></script>
+    <script src="Controllers/HARBuilder.js"></script>
     <script src="Controllers/HeapManager.js"></script>
     <script src="Controllers/IssueManager.js"></script>
     <script src="Controllers/LayerTreeManager.js"></script>
index 2652e84..7ad586c 100644 (file)
@@ -90,6 +90,11 @@ TestHarness = class TestHarness extends WI.Object
             this.addResult(message);
     }
 
+    json(object, filter)
+    {
+        this.log(JSON.stringify(object, filter || null, 2));
+    }
+
     assert(condition, message)
     {
         if (condition)
index 93a9a6b..cbbf483 100644 (file)
@@ -214,12 +214,7 @@ WI.DOMTreeContentView = class DOMTreeContentView extends WI.ContentView
 
     get saveData()
     {
-        function saveHandler(forceSaveAs)
-        {
-            WI.archiveMainFrame();
-        }
-
-        return {customSaveHandler: saveHandler};
+        return {customSaveHandler: () => { WI.archiveMainFrame(); }};
     }
 
     get supportsSearch()
index ab39682..4c3bd29 100644 (file)
@@ -184,6 +184,16 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         return items;
     }
 
+    get supportsSave()
+    {
+        return this._filteredEntries.some((entry) => entry.resource.finished);
+    }
+
+    get saveData()
+    {
+        return {customSaveHandler: () => { this._exportHAR(); }};
+    }
+
     shown()
     {
         super.shown();
@@ -295,6 +305,9 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         let entry = this._filteredEntries[rowIndex];
         let contextMenu = WI.ContextMenu.createFromEvent(event);
         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
+
+        contextMenu.appendSeparator();
+        contextMenu.appendItem(WI.UIString("Export HAR"), this._exportHAR);
     }
 
     tableSelectedRowChanged(table, rowIndex)
@@ -1360,6 +1373,33 @@ WI.NetworkTableContentView = class NetworkTableContentView extends WI.ContentVie
         this._table.selectRow(rowIndex);
     }
 
+    _HARResources()
+    {
+        let resources = this._filteredEntries.map((x) => x.resource);
+        const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
+        return resources.filter((resource) => resource.finished && supportedHARSchemes.has(resource.urlComponents.scheme));
+    }
+
+    _exportHAR()
+    {
+        let resources = this._HARResources();
+        if (!resources.length) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        WI.HARBuilder.buildArchive(resources).then((har) => {
+            let mainFrame = WI.frameResourceManager.mainFrame;
+            let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
+            let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
+            WI.saveDataToFile({
+                url,
+                content: JSON.stringify(har, null, 2),
+                forceSaveAs: true,
+            });
+        }).catch(handlePromiseException);
+    }
+
     _waterfallPopoverContentForResource(resource)
     {
         let contentElement = document.createElement("div");
index 655f776..e84346d 100644 (file)
@@ -213,8 +213,7 @@ WI.ResourceClusterContentView = class ResourceClusterContentView extends WI.Clus
         if (!requestData)
             return false;
 
-        var requestDataContentType = this._resource.requestDataContentType;
-        if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
+        if (!this._resource.hasRequestFormParameters())
             return false;
 
         return true;