microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
src/extension/networkInspector/views/inspectorConsoleView.ts
272lines · modeblame
4bb0956eRedMickey5 years ago | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. | |
| 3 | | |
| 4 | import { URL, URLSearchParams } from "url"; | |
| 5 | import * as vscode from "vscode"; | |
09f6024fHeniker4 years ago | 6 | import { Base64 } from "js-base64"; |
| 7 | import { RequestParams } from "../clientDevice"; | |
4bb0956eRedMickey5 years ago | 8 | import { |
| 9 | Request, | |
| 10 | Response, | |
| 11 | Header, | |
| 12 | ResponseFollowupChunk, | |
| 13 | PartialResponse, | |
| 14 | } from "../networkMessageData"; | |
| 15 | import { EditorColorThemesHelper, SystemColorTheme } from "../../../common/editorColorThemesHelper"; | |
| 16 | import { SettingsHelper } from "../../settingsHelper"; | |
| 17 | import { combineBase64Chunks } from "../requestBodyFormatters/utils"; | |
| 18 | import { FormattedBody } from "../requestBodyFormatters/requestBodyFormatter"; | |
09f6024fHeniker4 years ago | 19 | import { InspectorView } from "./inspectorView"; |
4bb0956eRedMickey5 years ago | 20 | |
| 21 | interface ConsoleNetworkRequestDataView { | |
| 22 | title: string; | |
| 23 | networkRequestData: { | |
| 24 | URL: string; | |
| 25 | Method: string; | |
| 26 | Status: number; | |
| 27 | Duration: string; | |
| 28 | "Request Headers": Record<string, string>; | |
| 29 | "Response Headers": Record<string, string>; | |
| 30 | "Request Body": FormattedBody | null | undefined; | |
| 31 | "Request Query Parameters"?: Record<string, string> | undefined; | |
| 32 | "Response Body": FormattedBody | null | undefined; | |
| 33 | }; | |
| 34 | } | |
| 35 | | |
| 36 | export class InspectorConsoleView extends InspectorView { | |
| 37 | private readonly maxResponseBodyLength = 75000; | |
| 38 | private readonly openDeveloperToolsCommand = "workbench.action.toggleDevTools"; | |
| 39 | private readonly consoleLogsColors = { | |
| 40 | Blue: "#0000ff", | |
| 41 | Orange: "#f28b54", | |
| 42 | }; | |
| 43 | | |
| 44 | private consoleLogsColor: string; | |
| 45 | | |
| 46 | public async init(): Promise<void> { | |
| 47 | if (!this.isInitialized) { | |
| 48 | this.isInitialized = true; | |
| 49 | await vscode.commands.executeCommand(this.openDeveloperToolsCommand); | |
| 50 | if (EditorColorThemesHelper.isAutoDetectColorSchemeEnabled()) { | |
| 51 | this.setupConsoleLogsColor(EditorColorThemesHelper.getCurrentSystemColorTheme()); | |
| 52 | } else { | |
| 53 | this.setupConsoleLogsColor( | |
| 54 | SettingsHelper.getNetworkInspectorConsoleLogsColorTheme(), | |
| 55 | ); | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | | |
| 60 | public handleMessage(data: RequestParams): void { | |
| 61 | if (data.params) { | |
| 62 | switch (data.method) { | |
| 63 | case "newRequest": | |
| 64 | this.handleRequest(data.params as Request); | |
| 65 | break; | |
| 66 | case "newResponse": | |
| 67 | this.handleResponse(data.params as Response); | |
| 68 | break; | |
| 69 | case "partialResponse": | |
| 70 | this.handlePartialResponse(data.params as Response | ResponseFollowupChunk); | |
| 71 | break; | |
| 72 | } | |
| 73 | } | |
| 74 | } | |
| 75 | | |
| 76 | private setupConsoleLogsColor(systemColorTheme: SystemColorTheme): void { | |
| 77 | if (systemColorTheme === SystemColorTheme.Light) { | |
| 78 | this.consoleLogsColor = this.consoleLogsColors.Blue; | |
| 79 | } else { | |
| 80 | this.consoleLogsColor = this.consoleLogsColors.Orange; | |
| 81 | } | |
| 82 | } | |
| 83 | | |
| 84 | private handleRequest(data: Request): void { | |
| 85 | this.requests.set(data.id, data); | |
| 86 | } | |
| 87 | | |
| 88 | private handleResponse(data: Response): void { | |
| 89 | this.responses.set(data.id, data); | |
| 90 | if (this.requests.has(data.id)) { | |
| 91 | this.printNetworkRequestData( | |
| 92 | this.createNetworkRequestData(this.requests.get(data.id) as Request, data), | |
| 93 | ); | |
| 94 | } | |
| 95 | } | |
| 96 | | |
| 97 | private handlePartialResponse(data: Response | ResponseFollowupChunk): void { | |
| 98 | /* Some clients (such as low end Android devices) struggle to serialise large payloads in one go, so partial responses allow them | |
| 99 | to split payloads into chunks and serialise each individually. | |
| 100 | | |
| 101 | Such responses will be distinguished between normal responses by both: | |
| 102 | * Being sent to the partialResponse method. | |
| 103 | * Having a totalChunks value > 1. | |
| 104 | | |
| 105 | The first chunk will always be included in the initial response. This response must have index 0. | |
| 106 | The remaining chunks will be sent in ResponseFollowupChunks, which each contain another piece of the payload, along with their index from 1 onwards. | |
| 107 | The payload of each chunk is individually encoded in the same way that full responses are. | |
| 108 | | |
| 109 | The order that initialResponse, and followup chunks are recieved is not guaranteed to be in index order. | |
| 110 | */ | |
| 111 | | |
| 112 | let newPartialResponseEntry; | |
| 113 | let responseId; | |
| 114 | if (data.index !== undefined && data.index > 0) { | |
| 115 | // It's a follow up chunk | |
| 116 | const followupChunk: ResponseFollowupChunk = data as ResponseFollowupChunk; | |
| 117 | const partialResponseEntry = this.partialResponses.get(followupChunk.id) ?? { | |
| 118 | followupChunks: {}, | |
| 119 | }; | |
| 120 | | |
| 121 | newPartialResponseEntry = Object.assign({}, partialResponseEntry); | |
| 122 | newPartialResponseEntry.followupChunks[followupChunk.index] = followupChunk.data; | |
| 123 | responseId = followupChunk.id; | |
| 124 | } else { | |
| 125 | // It's an initial chunk | |
| 126 | const partialResponse: Response = data as Response; | |
| 127 | const partialResponseEntry = this.partialResponses.get(partialResponse.id) ?? { | |
| 128 | followupChunks: {}, | |
| 129 | }; | |
| 130 | newPartialResponseEntry = { | |
| 131 | ...partialResponseEntry, | |
| 132 | initialResponse: partialResponse, | |
| 133 | }; | |
| 134 | responseId = partialResponse.id; | |
| 135 | } | |
| 136 | const response = this.assembleChunksIfResponseIsComplete(newPartialResponseEntry); | |
| 137 | if (response) { | |
| 138 | this.handleResponse(response); | |
| 139 | this.partialResponses.delete(responseId); | |
| 140 | } else { | |
| 141 | this.partialResponses.set(responseId, newPartialResponseEntry); | |
| 142 | } | |
| 143 | } | |
| 144 | | |
| 145 | /** | |
| 146 | * @preserve | |
| 147 | * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/v0.79.1/desktop/plugins/network/index.tsx#L276-L324 | |
| 148 | * | |
| 149 | * Copyright (c) Facebook, Inc. and its affiliates. | |
| 150 | * | |
| 151 | * This source code is licensed under the MIT license found in the | |
| 152 | * LICENSE file in the root directory of this source tree. | |
| 153 | * | |
| 154 | * @format | |
| 155 | */ | |
| 156 | private assembleChunksIfResponseIsComplete( | |
| 157 | partialResponseEntry: PartialResponse, | |
| 158 | ): Response | null { | |
| 159 | const numChunks = partialResponseEntry.initialResponse?.totalChunks; | |
| 160 | if ( | |
| 161 | !partialResponseEntry.initialResponse || | |
| 162 | !numChunks || | |
| 163 | Object.keys(partialResponseEntry.followupChunks).length + 1 < numChunks | |
| 164 | ) { | |
| 165 | // Partial response not yet complete, do nothing. | |
| 166 | return null; | |
| 167 | } | |
| 168 | // Partial response has all required chunks, convert it to a full Response. | |
| 169 | | |
| 170 | const response: Response = partialResponseEntry.initialResponse; | |
| 171 | const allChunks: string[] = | |
| 172 | response.data != null | |
| 173 | ? [ | |
| 174 | response.data, | |
| 175 | ...Object.entries(partialResponseEntry.followupChunks) | |
| 176 | // It's important to parseInt here or it sorts lexicographically | |
| 177 | .sort((a, b) => parseInt(a[0], 10) - parseInt(b[0], 10)) | |
| 178 | .map(([_k, v]: [string, string]) => v), | |
| 179 | ] | |
| 180 | : []; | |
| 181 | const data = combineBase64Chunks(allChunks); | |
| 182 | | |
| 183 | const newResponse = { | |
| 184 | ...response, | |
| 185 | // Currently data is always decoded at render time, so re-encode it to match the single response format. | |
| 186 | data: Base64.btoa(data), | |
| 187 | }; | |
| 188 | | |
| 189 | return newResponse; | |
| 190 | } | |
| 191 | | |
| 192 | /** | |
| 193 | * @preserve | |
| 194 | * End region: https://github.com/facebook/flipper/blob/v0.79.1/desktop/plugins/network/index.tsx#L276-L324 | |
| 195 | */ | |
| 196 | | |
| 197 | private createNetworkRequestData( | |
| 198 | request: Request, | |
| 199 | response: Response, | |
| 200 | ): ConsoleNetworkRequestDataView { | |
| 201 | const url = new URL(request.url); | |
| 202 | const networkRequestConsoleView = { | |
| 203 | title: `%cNetwork request: ${request.method} ${ | |
| 204 | url ? url.host + url.pathname : "<unknown>" | |
| 205 | }`, | |
| 206 | networkRequestData: { | |
| 207 | URL: request.url, | |
| 208 | Method: request.method, | |
| 209 | Status: response.status, | |
| 210 | Duration: this.getRequestDurationString(request.timestamp, response.timestamp), | |
| 211 | "Request Headers": this.prepareHeadersViewObject(request.headers), | |
| 212 | "Response Headers": this.prepareHeadersViewObject(response.headers), | |
| 213 | "Request Body": this.requestBodyDecoder.formatBody(request), | |
| 214 | "Response Body": this.requestBodyDecoder.formatBody(response), | |
| 215 | }, | |
| 216 | }; | |
| 217 | | |
| 218 | if (url.search) { | |
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago | 219 | (networkRequestConsoleView.networkRequestData as any)["Request Query Parameters"] = |
09f6024fHeniker4 years ago | 220 | this.retrieveURLSearchParams(url.searchParams); |
4bb0956eRedMickey5 years ago | 221 | } |
| 222 | | |
| 223 | return networkRequestConsoleView; | |
| 224 | } | |
| 225 | | |
| 226 | private retrieveURLSearchParams(searchParams: URLSearchParams): Record<string, string> { | |
09f6024fHeniker4 years ago | 227 | const formattedSearchParams: Record<string, string> = {}; |
4bb0956eRedMickey5 years ago | 228 | searchParams.forEach((value: string, key: string) => { |
| 229 | formattedSearchParams[key] = value; | |
| 230 | }); | |
| 231 | return formattedSearchParams; | |
| 232 | } | |
| 233 | | |
| 234 | private getRequestDurationString(requestTimestamp: number, responseTimestamp: number): string { | |
09f6024fHeniker4 years ago | 235 | return `${String(Math.abs(responseTimestamp - requestTimestamp))}ms`; |
4bb0956eRedMickey5 years ago | 236 | } |
| 237 | | |
| 238 | private prepareHeadersViewObject(headers: Header[]): Record<string, string> { | |
| 239 | return headers.reduce((headersViewObject, header) => { | |
| 240 | headersViewObject[header.key] = header.value; | |
| 241 | return headersViewObject; | |
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago | 242 | }, {} as Record<string, string>); |
4bb0956eRedMickey5 years ago | 243 | } |
| 244 | | |
| 245 | private printNetworkRequestData(networkRequestData: ConsoleNetworkRequestDataView): void { | |
| 246 | const responseBody = networkRequestData.networkRequestData["Response Body"]; | |
| 247 | if ( | |
| 248 | responseBody && | |
| 249 | typeof responseBody === "string" && | |
| 250 | responseBody.length > this.maxResponseBodyLength | |
| 251 | ) { | |
09f6024fHeniker4 years ago | 252 | networkRequestData.networkRequestData["Response Body"] = `${responseBody.substring( |
| 253 | 0, | |
| 254 | this.maxResponseBodyLength, | |
| 255 | )}... (Response body exceeds output limit, the rest its part is omitted)`; | |
4bb0956eRedMickey5 years ago | 256 | } |
| 257 | | |
| 258 | console.log( | |
| 259 | networkRequestData.title, | |
| 260 | `color: ${this.consoleLogsColor}`, | |
| 261 | networkRequestData.networkRequestData, | |
| 262 | ); | |
| 263 | | |
| 264 | this.logger.debug( | |
| 265 | `${networkRequestData.title}\ncolor: ${this.consoleLogsColor}\n${JSON.stringify( | |
| 266 | networkRequestData.networkRequestData, | |
| 267 | null, | |
| 268 | 2, | |
| 269 | )}`, | |
| 270 | ); | |
| 271 | } | |
| 272 | } |