diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 4f644a46c..34b2bae65 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -29,6 +29,7 @@ const OTHER = 'other'; const TYPE_TO_TAB = { [TYPES.XHR]: XHR, + [TYPES.FETCH]: XHR, [TYPES.JS]: JS, [TYPES.CSS]: CSS, [TYPES.IMG]: IMG, diff --git a/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTabs.tsx b/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTabs.tsx index 837a61ec6..1b49cc1fe 100644 --- a/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTabs.tsx +++ b/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTabs.tsx @@ -8,6 +8,11 @@ const REQUEST = 'REQUEST'; const RESPONSE = 'RESPONSE'; const TABS = [HEADERS, REQUEST, RESPONSE].map((tab) => ({ text: tab, key: tab })); + +function isValidJSON(o: any): o is Object { + return typeof o === "object" && o != null +} + interface Props { resource: any; } @@ -15,37 +20,41 @@ function FetchTabs(props: Props) { const { resource } = props; const [activeTab, setActiveTab] = useState(HEADERS); const onTabClick = (tab: string) => setActiveTab(tab); - const [jsonPayload, setJsonPayload] = useState(null); - const [jsonResponse, setJsonResponse] = useState(null); + const [jsonRequest, setJsonRequest] = useState(null); + const [jsonResponse, setJsonResponse] = useState(null); const [requestHeaders, setRequestHeaders] = useState(null); const [responseHeaders, setResponseHeaders] = useState(null); useEffect(() => { - const { payload, response } = resource; + const { request, response } = resource; try { - let jsonPayload = typeof payload === 'string' ? JSON.parse(payload) : payload; - let requestHeaders = jsonPayload.headers; - jsonPayload.body = - typeof jsonPayload.body === 'string' ? JSON.parse(jsonPayload.body) : jsonPayload.body; - delete jsonPayload.headers; - setJsonPayload(jsonPayload); - setRequestHeaders(requestHeaders); - } catch (e) {} + let jRequest = JSON.parse(request) + setRequestHeaders(jRequest.headers); + try { + let jBody = JSON.parse(jRequest.body) + jBody = isValidJSON(jBody) ? jBody : jRequest.body + setJsonRequest(jBody) + } catch { + setJsonRequest(jRequest.body) + } + } catch {} try { - let jsonResponse = typeof response === 'string' ? JSON.parse(response) : response; - let responseHeaders = jsonResponse.headers; - jsonResponse.body = - typeof jsonResponse.body === 'string' ? JSON.parse(jsonResponse.body) : jsonResponse.body; - delete jsonResponse.headers; - setJsonResponse(jsonResponse); - setResponseHeaders(responseHeaders); - } catch (e) {} - }, [resource, activeTab]); + let jResponse = JSON.parse(response) + setResponseHeaders(jResponse.headers); + try { + let jBody = JSON.parse(jResponse.body) + jBody = isValidJSON(jBody) ? jBody : jResponse.body + setJsonResponse(jBody) + } catch { + setJsonResponse(jResponse.body) + } + } catch {} + }, [resource]); const renderActiveTab = () => { - const { payload, response } = resource; + const { request, response } = resource; switch (activeTab) { case REQUEST: return ( @@ -57,15 +66,15 @@ function FetchTabs(props: Props) { } size="small" - show={!payload} + show={!request} // animatedIcon="no-results" >
- {jsonPayload === undefined ? ( -
{payload}
+ { !isValidJSON(jsonRequest) ? ( +
{jsonRequest || request}
) : ( - + )}
@@ -87,8 +96,8 @@ function FetchTabs(props: Props) { >
- {jsonResponse === undefined ? ( -
{response}
+ { !isValidJSON(jsonResponse) ? ( +
{jsonResponse || response}
) : ( )} diff --git a/frontend/app/player/common/ListWalker.ts b/frontend/app/player/common/ListWalker.ts index 7d92fa285..43cf88ea9 100644 --- a/frontend/app/player/common/ListWalker.ts +++ b/frontend/app/player/common/ListWalker.ts @@ -12,6 +12,15 @@ export default class ListWalker { this.list.push(m); } + insert(m: T): void { + let index = this.list.findIndex(om => om.time > m.time) + if (index === -1) { + index = this.length + } + const oldList = this.list + this._list = [...oldList.slice(0, index), m, ...oldList.slice(index)] + } + reset(): void { this.p = 0 } diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index 8e4543489..d58143362 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -2,7 +2,7 @@ import { Decoder } from "syncod"; import logger from 'App/logger'; -import Resource, { TYPES } from 'Types/session/resource'; +import Resource, { TYPES as RES_TYPES } from 'Types/session/resource'; import { TYPES as EVENT_TYPES } from 'Types/session/event'; import { Log } from './types'; @@ -187,13 +187,12 @@ export default class MessageManager { const stateToUpdate : Partial= { performanceChartData: this.performanceTrackManager.chartData, performanceAvaliability: this.performanceTrackManager.avaliability, - ...this.lists.getFullListsState() + ...this.lists.getFullListsState(), } if (this.activityManager) { this.activityManager.end() stateToUpdate.skipIntervals = this.activityManager.list } - this.state.update(stateToUpdate) } private onFileReadFailed = (e: any) => { @@ -227,26 +226,18 @@ export default class MessageManager { this.setMessagesLoading(true) this.waitingForFiles = true - try { - if (this.session.domURL && this.session.domURL.length > 0) { - await loadFiles(this.session.domURL, createNewParser()) - this.onFileReadSuccess() - } else { - const file = await requestEFSDom(this.session.sessionId) - const parser = createNewParser(false) - await parser(file) - } - } catch (e) { - console.error('Cant get session replay file:', e) - try { - // back compat with old mobsUrl - await loadFiles(this.session.mobsUrl, createNewParser(false)) - } catch (e) { - this.onFileReadFailed(e) - } - } finally { - this.onFileReadFinally() - } + let fileReadPromise = this.session.domURL && this.session.domURL.length > 0 + ? loadFiles(this.session.domURL, createNewParser()) + : requestEFSDom(this.session.sessionId) + .then(createNewParser(false)) + fileReadPromise.catch(e => { + logger.error('Can not get normal session replay file:', e) + // back compat fallback to an old mobsUrl + return loadFiles(this.session.mobsUrl, createNewParser(false)) + }) + .then(this.onFileReadSuccess) + .catch(this.onFileReadFailed) + .finally(this.onFileReadFinally) // load devtools if (this.session.devtoolsURL.length) { @@ -256,7 +247,10 @@ export default class MessageManager { requestEFSDevtools(this.session.sessionId) .then(createNewParser(false)) ) - //.catch() // not able to download the devtools file + .then(() => { + this.state.update(this.lists.getFullListsState()) + }) + .catch(e => logger.error("Can not download the devtools file", e)) .finally(() => this.state.update({ devtoolsLoading: false })) } } @@ -439,15 +433,16 @@ export default class MessageManager { ) break; case MType.Fetch: + case MType.NetworkRequest: // @ts-ignore burn immutable - this.lists.lists.fetch.append(Resource({ + this.lists.lists.fetch.insert(Resource({ method: msg.method, url: msg.url, - payload: msg.request, + request: msg.request, response: msg.response, status: msg.status, duration: msg.duration, - type: TYPES.XHR, + type: msg.type === "xhr" ? RES_TYPES.XHR : RES_TYPES.FETCH, time: Math.max(msg.timestamp - this.sessionStart, 0), // !!! doesn't look good. TODO: find solution to show negative timings index, }) as Timed) diff --git a/frontend/app/types/session/resource.js b/frontend/app/types/session/resource.js index 74cbceaf6..3b0523f78 100644 --- a/frontend/app/types/session/resource.js +++ b/frontend/app/types/session/resource.js @@ -3,6 +3,7 @@ import Record from 'Types/Record'; import { getResourceName } from 'App/utils'; const XHR = 'xhr'; +const FETCH = 'fetch'; const JS = 'script'; const CSS = 'css'; const IMG = 'img'; @@ -32,7 +33,6 @@ const OTHER = 'other'; const TYPES_MAP = { "stylesheet": CSS, - "fetch": XHR, } function getResourceStatus(status, success) { @@ -53,6 +53,7 @@ function getResourceSuccess(success, status) { export const TYPES = { XHR, + FETCH, JS, CSS, IMG, @@ -85,7 +86,7 @@ export default Record({ // initiator: "other", // pagePath: "", method: '', - payload:'', + request:'', response: '', headerSize: 0, encodedBodySize: 0, @@ -93,14 +94,13 @@ export default Record({ responseBodySize: 0, timings: List(), }, { - fromJS: ({ responseBody, response, type, initiator, status, success, time, datetime, timestamp, timings, ...resource }) => ({ + fromJS: ({ type, initiator, status, success, time, datetime, timestamp, timings, ...resource }) => ({ ...resource, type: TYPES_MAP[type] || type, name: getResourceName(resource.url), status: getResourceStatus(status, success), success: getResourceSuccess(success, status), time: typeof time === 'number' ? time : datetime || timestamp, - response: responseBody || response, ttfb: timings && timings.ttfb, timewidth: timings && timings.timewidth, timings,