microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.5.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/networkInspector/views/inspectorConsoleView.ts

272lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import { InspectorView } from "./inspectorView";
5import { RequestParams } from "../clientDevice";
6import { URL, URLSearchParams } from "url";
7import * as vscode from "vscode";
8import {
9 Request,
10 Response,
11 Header,
12 ResponseFollowupChunk,
13 PartialResponse,
14} from "../networkMessageData";
15import { EditorColorThemesHelper, SystemColorTheme } from "../../../common/editorColorThemesHelper";
16import { SettingsHelper } from "../../settingsHelper";
17import { combineBase64Chunks } from "../requestBodyFormatters/utils";
18import { FormattedBody } from "../requestBodyFormatters/requestBodyFormatter";
19import { Base64 } from "js-base64";
20
21interface 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
36export 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) {
219 networkRequestConsoleView.networkRequestData[
220 "Request Query Parameters"
221 ] = this.retrieveURLSearchParams(url.searchParams);
222 }
223
224 return networkRequestConsoleView;
225 }
226
227 private retrieveURLSearchParams(searchParams: URLSearchParams): Record<string, string> {
228 let formattedSearchParams: Record<string, string> = {};
229 searchParams.forEach((value: string, key: string) => {
230 formattedSearchParams[key] = value;
231 });
232 return formattedSearchParams;
233 }
234
235 private getRequestDurationString(requestTimestamp: number, responseTimestamp: number): string {
236 return Math.abs(responseTimestamp - requestTimestamp) + "ms";
237 }
238
239 private prepareHeadersViewObject(headers: Header[]): Record<string, string> {
240 return headers.reduce((headersViewObject, header) => {
241 headersViewObject[header.key] = header.value;
242 return headersViewObject;
243 }, {});
244 }
245
246 private printNetworkRequestData(networkRequestData: ConsoleNetworkRequestDataView): void {
247 const responseBody = networkRequestData.networkRequestData["Response Body"];
248 if (
249 responseBody &&
250 typeof responseBody === "string" &&
251 responseBody.length > this.maxResponseBodyLength
252 ) {
253 networkRequestData.networkRequestData["Response Body"] =
254 responseBody.substring(0, this.maxResponseBodyLength) +
255 "... (Response body exceeds output limit, the rest its part is omitted)";
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}
273