microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
dev/v-zhenyuan/update-parameters

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 { URL, URLSearchParams } from "url";
5import * as vscode from "vscode";
6import { Base64 } from "js-base64";
7import { RequestParams } from "../clientDevice";
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 { InspectorView } from "./inspectorView";
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 as any)["Request Query Parameters"] =
220 this.retrieveURLSearchParams(url.searchParams);
221 }
222
223 return networkRequestConsoleView;
224 }
225
226 private retrieveURLSearchParams(searchParams: URLSearchParams): Record<string, string> {
227 const formattedSearchParams: Record<string, string> = {};
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 {
235 return `${String(Math.abs(responseTimestamp - requestTimestamp))}ms`;
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;
242 }, {} as Record<string, string>);
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 ) {
252 networkRequestData.networkRequestData["Response Body"] = `${responseBody.substring(
253 0,
254 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