From 64612ed89cf442e26dcf50c116b36f4c3f85603d Mon Sep 17 00:00:00 2001 From: Michael Mrowetz Date: Sun, 12 Feb 2017 21:47:53 +0900 Subject: [PATCH] #145 move indicators to general tab --- src/css-raw/perf-cascade.css | 1 + src/ts/helpers/heuristics.ts | 77 ----------------- src/ts/transformers/har-heuristics.ts | 84 +++++++++++++++++++ src/ts/transformers/har.ts | 25 +++--- .../details-overlay/html-details-body.ts | 42 ++++++++-- src/ts/waterfall/row/svg-indicators.ts | 6 +- 6 files changed, 135 insertions(+), 100 deletions(-) create mode 100644 src/ts/transformers/har-heuristics.ts diff --git a/src/css-raw/perf-cascade.css b/src/css-raw/perf-cascade.css index c2b27543..a0abdb66 100755 --- a/src/css-raw/perf-cascade.css +++ b/src/css-raw/perf-cascade.css @@ -148,6 +148,7 @@ svg.water-fall-chart {width:100%; overflow: visible;} .info-overlay-holder .tab {float: left; width:100%; height: 350px; padding:12px 12px 24px;} .info-overlay-holder .tab h2 {font-size: 1.2em; margin:0.5em 0 0; padding: 0.5em 0 0.5em 1em; clear: both; border-top: solid 1px #efefef;} +.info-overlay-holder .tab h2:first-child {border-top: 0; padding-top: 0;} .info-overlay-holder .tab pre {overflow-y: hidden;} .info-overlay-holder .tab .preview {width: auto; max-width: 100%; max-height: 500px; border: solid 1px #666;} .info-overlay-holder .tab dl:after {content: ""; display: table; clear: both;} diff --git a/src/ts/helpers/heuristics.ts b/src/ts/helpers/heuristics.ts index 180e5612..c144b468 100644 --- a/src/ts/helpers/heuristics.ts +++ b/src/ts/helpers/heuristics.ts @@ -1,7 +1,4 @@ import {Entry} from "../typing/har"; -import {RequestType} from "../typing/waterfall"; -import {hasHeader} from "./har"; -import * as misc from "./misc"; /** * @@ -13,77 +10,3 @@ import * as misc from "./misc"; export function isInStatusCodeRange(entry: Entry, lowerBound: number, upperBound: number) { return entry.response.status >= lowerBound && entry.response.status <= upperBound; } - -function isCompressible(entry: Entry, requestType: RequestType): boolean { - const minCompressionSize = 1000; - // small responses - if (entry.response.bodySize < minCompressionSize) { - return false; - } - - if (misc.contains(["html", "css", "javascript", "svg", "plain"], requestType)) { - return true; - } - const mime = entry.response.content.mimeType; - const compressableMimes = ["application/vnd.ms-fontobject", - "application/x-font-opentype", - "application/x-font-truetype", - "application/x-font-ttf", - "application/xml", - "font/eot", - "font/opentype", - "font/otf", - "image/vnd.microsoft.icon"]; - if (misc.contains(["text"], mime.split("/")[0]) || misc.contains(compressableMimes, mime.split(";")[0])) { - return true; - } - return false; -} - -/** - * Checks if response could be cacheable, but isn't due to lack of cache header. - * @param {Entry} entry - the waterfall entry. - * @returns {boolean} - */ -export function hasCacheIssue(entry: Entry) { - if (entry.request.method.toLowerCase() !== "get") { - return false; - } - if (entry.response.status === 204 || !isInStatusCodeRange(entry, 200, 299)) { - return false; - } - - const headers = entry.response.headers; - return !(hasHeader(headers, "Cache-Control") || hasHeader(headers, "Expires")); -} - -export function hasCompressionIssue(entry: Entry, requestType: RequestType) { - const headers = entry.response.headers; - return (!hasHeader(headers, "Content-Encoding") && isCompressible(entry, requestType)); -} - -/** Checks if the ressource uses https */ -export function isSecure(entry: Entry) { - return entry.request.url.indexOf("https://") === 0; -} - -export function isPush(entry: Entry): boolean { - function toInt(input: string | number): number { - if (typeof input === "string") { - return parseInt(input, 10); - } else { - return input; - } - } - return toInt(entry._was_pushed) === 1; -} - -/** - * Check if the document (disregarding any initial http->https redirects) is loaded over a secure connection. - * @param {Entry[]} data - the waterfall entries data. - * @returns {boolean} - */ -export function documentIsSecure(data: Entry[]) { - const rootDocument = data.filter((e) => !e.response.redirectURL)[0]; - return isSecure(rootDocument); -} diff --git a/src/ts/transformers/har-heuristics.ts b/src/ts/transformers/har-heuristics.ts new file mode 100644 index 00000000..cb79433b --- /dev/null +++ b/src/ts/transformers/har-heuristics.ts @@ -0,0 +1,84 @@ +/** + * Heuristics used at parse-time for HAR data + */ + +import { hasHeader } from "../helpers/har"; +import { isInStatusCodeRange } from "../helpers/heuristics"; +import * as misc from "../helpers/misc"; +import { Entry } from "../typing/har"; +import { RequestType } from "../typing/waterfall"; + + +function isCompressible(entry: Entry, requestType: RequestType): boolean { + const minCompressionSize = 1000; + // small responses + if (entry.response.bodySize < minCompressionSize) { + return false; + } + + if (misc.contains(["html", "css", "javascript", "svg", "plain"], requestType)) { + return true; + } + const mime = entry.response.content.mimeType; + const compressableMimes = ["application/vnd.ms-fontobject", + "application/x-font-opentype", + "application/x-font-truetype", + "application/x-font-ttf", + "application/xml", + "font/eot", + "font/opentype", + "font/otf", + "image/vnd.microsoft.icon"]; + if (misc.contains(["text"], mime.split("/")[0]) || misc.contains(compressableMimes, mime.split(";")[0])) { + return true; + } + return false; +} + +/** + * Checks if response could be cacheable, but isn't due to lack of cache header. + * @param {Entry} entry - the waterfall entry. + * @returns {boolean} + */ +export function hasCacheIssue(entry: Entry) { + if (entry.request.method.toLowerCase() !== "get") { + return false; + } + if (entry.response.status === 204 || !isInStatusCodeRange(entry, 200, 299)) { + return false; + } + + const headers = entry.response.headers; + return !(hasHeader(headers, "Cache-Control") || hasHeader(headers, "Expires")); +} + +export function hasCompressionIssue(entry: Entry, requestType: RequestType) { + const headers = entry.response.headers; + return (!hasHeader(headers, "Content-Encoding") && isCompressible(entry, requestType)); +} + +/** Checks if the ressource uses https */ +export function isSecure(entry: Entry) { + return entry.request.url.indexOf("https://") === 0; +} + +export function isPush(entry: Entry): boolean { + function toInt(input: string | number): number { + if (typeof input === "string") { + return parseInt(input, 10); + } else { + return input; + } + } + return toInt(entry._was_pushed) === 1; +} + +/** + * Check if the document (disregarding any initial http->https redirects) is loaded over a secure connection. + * @param {Entry[]} data - the waterfall entries data. + * @returns {boolean} + */ +export function documentIsSecure(data: Entry[]) { + const rootDocument = data.filter((e) => !e.response.redirectURL)[0]; + return isSecure(rootDocument); +} diff --git a/src/ts/transformers/har.ts b/src/ts/transformers/har.ts index 345ef363..d410254e 100644 --- a/src/ts/transformers/har.ts +++ b/src/ts/transformers/har.ts @@ -1,4 +1,4 @@ -import * as heuristics from "../helpers/heuristics"; +import { isInStatusCodeRange } from "../helpers/heuristics"; import { roundNumber } from "../helpers/misc"; import { Entry, Har, PageTimings } from "../typing/har"; import { @@ -11,6 +11,7 @@ import { WaterfallEntryIndicator, WaterfallEntryTiming, } from "../typing/waterfall"; +import * as harHeuristics from "./har-heuristics"; function createWaterfallEntry(name: string, start: number, @@ -95,7 +96,7 @@ function collectIndicators(entry: Entry, docIsTLS: boolean, requestType: Request // const harEntry = entry; let output: WaterfallEntryIndicator[] = []; - if (heuristics.isPush(entry)) { + if (harHeuristics.isPush(entry)) { output.push({ description: "Response was pushed by the server using HTTP2 push.", icon: "push", @@ -105,27 +106,27 @@ function collectIndicators(entry: Entry, docIsTLS: boolean, requestType: Request }); } - if (docIsTLS && !heuristics.isSecure(entry)) { + if (docIsTLS && !harHeuristics.isSecure(entry)) { output.push({ - description: "Insecure Connetion, it should uses TLS", + description: "Insecure request, it should use HTTPS.", id: "noTls", title: "Insecure Connection", type: "error", }); } - if (heuristics.hasCacheIssue(entry)) { + if (harHeuristics.hasCacheIssue(entry)) { output.push({ - description: "The response does not allow to be cached on the client. Consider setting 'Cache-Control' headers.", + description: "The response is not allow to be cached on the client. Consider setting 'Cache-Control' headers.", id: "noCache", title: "Response not cached", type: "error", }); } - if (heuristics.hasCompressionIssue(entry, requestType)) { + if (harHeuristics.hasCompressionIssue(entry, requestType)) { output.push({ - description: "The response is not compressed.", + description: "The response is not compressed. Consider enabling HTTP compression on your server.", id: "noGzip", title: "no gzip", type: "error", @@ -133,10 +134,10 @@ function collectIndicators(entry: Entry, docIsTLS: boolean, requestType: Request } if (!entry.response.content.mimeType && - heuristics.isInStatusCodeRange(entry, 200, 299) && - entry.response.status !== 204) { + isInStatusCodeRange(entry, 200, 299) && + entry.response.status !== 204) { output.push({ - description: "No MIME Type defined", + description: "Response doesn't contain a 'Content-Type' header.", id: "warning", title: "No MIME Type defined", type: "warning", @@ -186,7 +187,7 @@ export function transformPage(harData: Har, pageIndex: number = 0): WaterfallDat console.log("%s: %s of %s page(s)", currPage.title, pageIndex + 1, data.pages.length); let doneTime = 0; - const isTLS = heuristics.documentIsSecure(data.entries); + const isTLS = harHeuristics.documentIsSecure(data.entries); const entries = data.entries .filter((entry) => entry.pageref === currPage.id) .map((entry) => { diff --git a/src/ts/waterfall/details-overlay/html-details-body.ts b/src/ts/waterfall/details-overlay/html-details-body.ts index 917fc9c3..3b45e01b 100644 --- a/src/ts/waterfall/details-overlay/html-details-body.ts +++ b/src/ts/waterfall/details-overlay/html-details-body.ts @@ -36,12 +36,40 @@ function makeImgTab(accordionHeight: number, entry: WaterfallEntry) { return makeTab(imgTag, false); } -function makeIndicatorTab(entry: WaterfallEntry) { +function makeGeneralTab(generalData: KvTuple[], entry: WaterfallEntry){ + let content = makeDefinitionList(generalData); if (entry.indicators.length === 0) { - return ""; + return makeTab(content, true); + } + let general = `

General

+
${content}
`; + content = ""; + + // Make indicator sections + let errors = entry.indicators + .filter((i) => i.type === "error") + .map((i) => [i.title, i.description] as KvTuple); + let warnings = entry.indicators + .filter((i) => i.type === "warning") + .map((i) => [i.title, i.description] as KvTuple); + // all others + let info = entry.indicators + .filter((i) => i.type !== "error" && i.type !== "warning") + .map((i) => [i.title, i.description] as KvTuple); + + if (errors.length > 0) { + content += `

Error${errors.length > 1 ? "s" : ""}

+
${makeDefinitionList(errors)}
`; + } + if (warnings.length > 0) { + content += `

Warning${warnings.length > 1 ? "s" : ""}

+
${makeDefinitionList(warnings)}
`; + } + if (info.length > 0) { + content += `

Info

+
${makeDefinitionList(info)}
`; } - let content = makeDefinitionList(entry.indicators.map((i) => [i.title, i.description] as KvTuple)); - return makeTab(content, false); + return makeTab(content + general, false); } function makeTabBtn(name: string, tab: string) { @@ -56,7 +84,7 @@ export function createDetailsBody(requestID: number, entry: WaterfallEntry, acco html.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.w3.org/2000/xmlns/"); const tabsData = getKeys(requestID, entry); - const generalTab = makeTab(makeDefinitionList(tabsData.general)); + const generalTab = makeGeneralTab(tabsData.general, entry); const timingsTab = makeTab(makeDefinitionList(tabsData.timings, true)); const requestDl = makeDefinitionList(tabsData.request); @@ -64,7 +92,7 @@ export function createDetailsBody(requestID: number, entry: WaterfallEntry, acco const responseDl = makeDefinitionList(tabsData.response); const responseHeadersDl = makeDefinitionList(tabsData.responseHeaders); const imgTab = makeImgTab(accordeonHeight, entry); - const indicatorTab = makeIndicatorTab(entry); + // const indicatorTab = makeIndicatorTab(entry); body.innerHTML = `
@@ -72,7 +100,6 @@ export function createDetailsBody(requestID: number, entry: WaterfallEntry, acco

#${requestID} ${entry.name}

- ${indicatorTab} ${generalTab}
diff --git a/src/ts/waterfall/row/svg-indicators.ts b/src/ts/waterfall/row/svg-indicators.ts index 82e34e60..2ee51b7e 100644 --- a/src/ts/waterfall/row/svg-indicators.ts +++ b/src/ts/waterfall/row/svg-indicators.ts @@ -2,7 +2,7 @@ * Creation of sub-components used in a resource request row */ -import * as heuristics from "../../helpers/heuristics"; +import { isInStatusCodeRange } from "../../helpers/heuristics"; import { WaterfallEntry } from "../../typing/waterfall"; /** @@ -30,9 +30,9 @@ export function getMimeTypeIcon(entry: WaterfallEntry): Icon { if (!!harEntry.response.redirectURL) { const url = encodeURI(harEntry.response.redirectURL.split("?")[0] || ""); return makeIcon("err3xx", `${harEntry.response.status} response status: Redirect to ${url}...`); - } else if (heuristics.isInStatusCodeRange(harEntry, 400, 499)) { + } else if (isInStatusCodeRange(harEntry, 400, 499)) { return makeIcon("err4xx", `${harEntry.response.status} response status: ${harEntry.response.statusText}`); - } else if (heuristics.isInStatusCodeRange(harEntry, 500, 599)) { + } else if (isInStatusCodeRange(harEntry, 500, 599)) { return makeIcon("err5xx", `${harEntry.response.status} response status: ${harEntry.response.statusText}`); } else if (harEntry.response.status === 204) { return makeIcon("plain", "No content");