diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index c8c0bedca27..9b434b7e0c8 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -57,18 +57,7 @@ export const getClientString = () => `${getAppEnvironment()}::${getAppPlatform() // Global Stuff export const DEBOUNCE_MILLIS = 100; -export const REQUEST_TIME_TO_SHOW_COUNTER = 1; // Seconds - -/** - * A number in milliseconds representing the time required to setup and teardown a request. - * - * Should not be used for anything a user may rely on for performance metrics of any kind. - * - * While this isn't a perfect "magic-number" (it can be as low as 120ms and as high as 300) it serves as a rough average. - * - * For initial introduction, see https://github.com/Kong/insomnia/blob/8aa274d21b351c4710f0bb833cba7deea3d56c29/app/ui/components/ResponsePane.js#L100 -*/ -export const REQUEST_SETUP_TEARDOWN_COMPENSATION = 200; + export const STATUS_CODE_PLUGIN_ERROR = -222; export const LARGE_RESPONSE_MB = 5; export const HUGE_RESPONSE_MB = 100; diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index a892267b75d..8b17633f4fb 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -14,6 +14,7 @@ export type HandleChannels = | 'curl.readyState' | 'curlRequest' | 'database.caCertificate.create' + | 'getExecution' | 'grpc.loadMethods' | 'grpc.loadMethodsFromReflection' | 'installPlugin' @@ -63,7 +64,10 @@ export type MainOnChannels = | 'trackSegmentEvent' | 'webSocket.close' | 'webSocket.closeAll' - | 'writeText'; + | 'writeText' + | 'addExecutionStep' + | 'completeExecutionStep' + | 'startExecution'; export type RendererOnChannels = 'clear-all-models' | 'clear-model' diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index 800ee7529fa..18b31acbf5b 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -9,6 +9,7 @@ import { backup, restoreBackup } from '../backup'; import installPlugin from '../install-plugin'; import { CurlBridgeAPI } from '../network/curl'; import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise'; +import { addExecutionStep, completeExecutionStep, getExecution, startExecution, StepName, TimingStep } from '../network/request-timing'; import { WebSocketBridgeAPI } from '../network/websocket'; import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron'; import { gRPCBridgeAPI } from './grpc'; @@ -41,8 +42,24 @@ export interface RendererToMainBridgeAPI { }; }; hiddenBrowserWindow: HiddenBrowserWindowBridgeAPI; + getExecution: (options: { requestId: string }) => Promise; + addExecutionStep: (options: { requestId: string; stepName: StepName }) => void; + startExecution: (options: { requestId: string }) => void; + completeExecutionStep: (options: { requestId: string }) => void; } export function registerMainHandlers() { + ipcMainOn('addExecutionStep', (_, options: { requestId: string; stepName: StepName }) => { + addExecutionStep(options.requestId, options.stepName); + }); + ipcMainOn('startExecution', (_, options: { requestId: string }) => { + return startExecution(options.requestId); + }); + ipcMainOn('completeExecutionStep', (_, options: { requestId: string }) => { + return completeExecutionStep(options.requestId); + }); + ipcMainHandle('getExecution', (_, options: { requestId: string }) => { + return getExecution(options.requestId); + }); ipcMainHandle('database.caCertificate.create', async (_, options: { parentId: string; path: string }) => { return models.caCertificate.create(options); }); diff --git a/packages/insomnia/src/main/network/request-timing.ts b/packages/insomnia/src/main/network/request-timing.ts new file mode 100644 index 00000000000..8f4d25b14ec --- /dev/null +++ b/packages/insomnia/src/main/network/request-timing.ts @@ -0,0 +1,39 @@ +import { BrowserWindow } from 'electron'; + +export type StepName = 'Executing pre-request script' + | 'Rendering request' + | 'Sending request' + | 'Executing after-response script'; + +export interface TimingStep { + stepName: StepName; + startedAt: number; + duration?: number; +} +export const executions = new Map(); +export const getExecution = (requestId?: string) => requestId ? executions.get(requestId) : []; +export const startExecution = (requestId: string) => executions.set(requestId, []); +export function addExecutionStep( + requestId: string, + stepName: StepName, +) { + // append to new step to execution + const record: TimingStep = { + stepName, + startedAt: Date.now(), + }; + const execution = [...(executions.get(requestId) || []), record]; + executions.set(requestId, execution); + for (const window of BrowserWindow.getAllWindows()) { + window.webContents.send(`syncTimers.${requestId}`, { executions: executions.get(requestId) }); + } +} +export function completeExecutionStep(requestId: string) { + const latest = executions.get(requestId)?.at(-1); + if (latest) { + latest.duration = (Date.now() - latest.startedAt); + } + for (const window of BrowserWindow.getAllWindows()) { + window.webContents.send(`syncTimers.${requestId}`, { executions: executions.get(requestId) }); + } +} diff --git a/packages/insomnia/src/network/cancellation.ts b/packages/insomnia/src/network/cancellation.ts index ebcba8ede09..1588cd96185 100644 --- a/packages/insomnia/src/network/cancellation.ts +++ b/packages/insomnia/src/network/cancellation.ts @@ -7,6 +7,7 @@ import { Request } from '../models/request'; const cancelRequestFunctionMap = new Map void>(); export async function cancelRequestById(requestId: string) { + window.main.completeExecutionStep({ requestId }); const cancel = cancelRequestFunctionMap.get(requestId); if (cancel) { return cancel(); diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index a56a5daac7b..e22b1267815 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -41,6 +41,10 @@ const grpc: gRPCBridgeAPI = { loadMethodsFromReflection: options => ipcRenderer.invoke('grpc.loadMethodsFromReflection', options), }; const main: Window['main'] = { + startExecution: options => ipcRenderer.send('startExecution', options), + addExecutionStep: options => ipcRenderer.send('addExecutionStep', options), + completeExecutionStep: options => ipcRenderer.send('completeExecutionStep', options), + getExecution: options => ipcRenderer.invoke('getExecution', options), loginStateChange: () => ipcRenderer.send('loginStateChange'), restart: () => ipcRenderer.send('restart'), openInBrowser: options => ipcRenderer.send('openInBrowser', options), diff --git a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx index 69da1b265bb..a70aa31e735 100644 --- a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx @@ -58,6 +58,7 @@ const updateRequestAuth = 'bearer' );`; const requireAModule = "const atob = require('atob');"; +const delay = 'new Promise((resolve)=>setTimeout(resolve, 1000));'; const getStatusCode = 'const statusCode = insomnia.response.code;'; const getStatusMsg = 'const status = insomnia.response.status;'; @@ -336,6 +337,11 @@ const miscMenu: SnippetMenuItem = { 'name': 'Require a module', 'snippet': requireAModule, }, + { + 'id': 'delay', + 'name': 'Delay', + 'snippet': delay, + }, ], }; diff --git a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx index d59b1fb5a49..094ddf63c9c 100644 --- a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx +++ b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx @@ -15,6 +15,7 @@ import { Response } from '../../../models/response'; import { cancelRequestById } from '../../../network/cancellation'; import { insomniaFetch } from '../../../ui/insomniaFetch'; import { jsonPrettify } from '../../../utils/prettify/json'; +import { useExecutionState } from '../../hooks/use-execution-state'; import { MockRouteLoaderData } from '../../routes/mock-route'; import { useRootLoaderData } from '../../routes/root'; import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; @@ -54,6 +55,7 @@ export const MockResponsePane = () => { const [timeline, setTimeline] = useState([]); const [previewMode, setPreviewMode] = useState(PREVIEW_MODE_FRIENDLY); const requestFetcher = useFetcher({ key: 'mock-request-fetcher' }); + const { steps } = useExecutionState({ requestId: activeResponse?.parentId }); useEffect(() => { const fn = async () => { @@ -69,6 +71,8 @@ export const MockResponsePane = () => { { activeResponse && cancelRequestById(activeResponse.parentId)} + activeRequestId={mockRoute._id} + steps={steps} />} ); @@ -79,7 +83,7 @@ export const MockResponsePane = () => {
- +
diff --git a/packages/insomnia/src/ui/components/panes/request-pane.tsx b/packages/insomnia/src/ui/components/panes/request-pane.tsx index 15f9b1b59e1..93433373565 100644 --- a/packages/insomnia/src/ui/components/panes/request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-pane.tsx @@ -31,14 +31,12 @@ import { PlaceholderRequestPane } from './placeholder-request-pane'; interface Props { environmentId: string; settings: Settings; - setLoading: (l: boolean) => void; onPaste: (text: string) => void; } export const RequestPane: FC = ({ environmentId, settings, - setLoading, onPaste, }) => { const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData; @@ -104,7 +102,6 @@ export const RequestPane: FC = ({ uniquenessKey={uniqueKey} handleAutocompleteUrls={() => queryAllWorkspaceUrls(workspaceId, models.request.type, requestId)} nunjucksPowerUserMode={settings.nunjucksPowerUserMode} - setLoading={setLoading} onPaste={onPaste} /> diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index f6f137f4daf..850a8b84444 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -8,6 +8,7 @@ import { getSetCookieHeaders } from '../../../common/misc'; import * as models from '../../../models'; import { cancelRequestById } from '../../../network/cancellation'; import { jsonPrettify } from '../../../utils/prettify/json'; +import { useExecutionState } from '../../hooks/use-execution-state'; import { useRequestMetaPatcher } from '../../hooks/use-request'; import { RequestLoaderData } from '../../routes/request'; import { useRootLoaderData } from '../../routes/root'; @@ -30,10 +31,10 @@ import { Pane, PaneHeader } from './pane'; import { PlaceholderResponsePane } from './placeholder-response-pane'; interface Props { - runningRequests: Record; + activeRequestId: string; } export const ResponsePane: FC = ({ - runningRequests, + activeRequestId, }) => { const { activeRequest, activeRequestMeta, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData; const filterHistory = activeRequestMeta.responseFilterHistory || []; @@ -73,6 +74,9 @@ export const ResponsePane: FC = ({ window.clipboard.writeText(bodyBuffer.toString('utf8')); } }, [handleGetResponseBody]); + + const { isExecuting, steps } = useExecutionState({ requestId: activeRequest._id }); + const handleDownloadResponseBody = useCallback(async (prettify: boolean) => { if (!activeResponse || !activeRequest) { console.warn('Nothing to download'); @@ -128,12 +132,15 @@ export const ResponsePane: FC = ({ if (!activeResponse) { return ( - {runningRequests[activeRequest._id] && cancelRequestById(activeRequest._id)} + activeRequestId={activeRequestId} + steps={steps} />} ); } + const timeline = models.response.getTimeline(activeResponse); const cookieHeaders = getSetCookieHeaders(activeResponse.headers); return ( @@ -142,7 +149,7 @@ export const ResponsePane: FC = ({
- +
= ({ - {runningRequests[activeRequest._id] && cancelRequestById(activeRequest._id)} + activeRequestId={activeRequestId} + steps={steps} />} diff --git a/packages/insomnia/src/ui/components/request-url-bar.tsx b/packages/insomnia/src/ui/components/request-url-bar.tsx index 871894fe326..5f1ef3d8e05 100644 --- a/packages/insomnia/src/ui/components/request-url-bar.tsx +++ b/packages/insomnia/src/ui/components/request-url-bar.tsx @@ -42,7 +42,6 @@ interface Props { handleAutocompleteUrls: () => Promise; nunjucksPowerUserMode: boolean; uniquenessKey: string; - setLoading: (l: boolean) => void; onPaste: (text: string) => void; } @@ -53,7 +52,6 @@ export interface RequestUrlBarHandle { export const RequestUrlBar = forwardRef(({ handleAutocompleteUrls, uniquenessKey, - setLoading, onPaste, }, ref) => { const [searchParams, setSearchParams] = useSearchParams(); @@ -105,14 +103,7 @@ export const RequestUrlBar = forwardRef(({ const [currentInterval, setCurrentInterval] = useState(null); const [currentTimeout, setCurrentTimeout] = useState(undefined); const fetcher = useFetcher(); - // TODO: unpick this loading hack. This could be simplified if submit provides a way to update state when it finishes. https://github.com/remix-run/remix/discussions/9020 - useEffect(() => { - if (fetcher.state !== 'idle') { - setLoading(true); - } else { - setLoading(false); - } - }, [fetcher.state, setLoading]); + const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string }; const connect = useCallback((connectParams: ConnectActionParams) => { fetcher.submit(JSON.stringify(connectParams), diff --git a/packages/insomnia/src/ui/components/response-timer.tsx b/packages/insomnia/src/ui/components/response-timer.tsx index a4352c5143f..a44432e649d 100644 --- a/packages/insomnia/src/ui/components/response-timer.tsx +++ b/packages/insomnia/src/ui/components/response-timer.tsx @@ -1,20 +1,21 @@ import React, { DOMAttributes, FunctionComponent, useEffect, useState } from 'react'; -import { REQUEST_SETUP_TEARDOWN_COMPENSATION, REQUEST_TIME_TO_SHOW_COUNTER } from '../../common/constants'; +import type { TimingStep } from '../../main/network/request-timing'; interface Props { handleCancel: DOMAttributes['onClick']; + activeRequestId: string; + steps: TimingStep[]; } - -export const ResponseTimer: FunctionComponent = ({ handleCancel }) => { +// triggers a 100 ms render in order to show a incrementing counter +const MillisecondTimer = () => { const [milliseconds, setMilliseconds] = useState(0); - useEffect(() => { let interval: NodeJS.Timeout | null = null; const loadStartTime = Date.now(); - interval = setInterval(() => { - setMilliseconds(Date.now() - loadStartTime - REQUEST_SETUP_TEARDOWN_COMPENSATION); + const delta = Date.now() - loadStartTime; + setMilliseconds(delta); }, 100); return () => { if (interval !== null) { @@ -23,16 +24,35 @@ export const ResponseTimer: FunctionComponent = ({ handleCancel }) => { } }; }, []); - - const seconds = milliseconds / 1000; + const ms = (milliseconds / 1000); + return ms > 0 ? `${ms.toFixed(1)} s` : '0 s'; +}; +export const ResponseTimer: FunctionComponent = ({ handleCancel, activeRequestId, steps }) => { return (
-

- {seconds >= REQUEST_TIME_TO_SHOW_COUNTER ? `${seconds.toFixed(1)} seconds` : 'Loading'}... -

-
- +
+ {steps.map((record: TimingStep) => ( +
+
+ + { + record.duration ? + () : + () + } + + + {record.stepName} + +
+ {record.duration ? `${((record.duration) / 1000).toFixed(1)} s` : ()} +
+ ))}
+
diff --git a/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx b/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx index a156d22b1d5..6fdfb074148 100644 --- a/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx +++ b/packages/insomnia/src/ui/components/websockets/realtime-response-pane.tsx @@ -128,7 +128,7 @@ const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }>
- +
([]); + + useEffect(() => { + let isMounted = true; + const fn = async () => { + if (!requestId) { + return; + } + const targetSteps = await window.main.getExecution({ requestId }); + if (targetSteps) { + isMounted && setSteps(targetSteps); + } + }; + fn(); + return () => { + isMounted = false; + }; + }, [requestId]); + + useEffect(() => { + let isMounted = true; + // @ts-expect-error -- we use a dynamic channel here + const unsubscribe = window.main.on(`syncTimers.${requestId}`, + (_, { executions }: { executions: TimingStep[] }) => { + isMounted && setSteps(executions); + }); + return () => { + isMounted = false; + unsubscribe(); + }; + }, [requestId]); + + const isExecuting = () => { + const hasSteps = steps && steps.length > 0; + if (!hasSteps) { + return false; + } + const latest = steps[steps.length - 1]; + return latest.duration === undefined; + }; + + return { steps, isExecuting: isExecuting() }; +} diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 88d69a48abc..8db322f5bfd 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -91,6 +91,7 @@ import { getMethodShortHand } from '../components/tags/method-tag'; import { ConnectionCircle } from '../components/websockets/action-bar'; import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane'; import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane'; +import { useExecutionState } from '../hooks/use-execution-state'; import { useReadyState } from '../hooks/use-ready-state'; import { CreateRequestType, @@ -164,6 +165,11 @@ const getRequestNameOrFallback = (doc: Request | RequestGroup | GrpcRequest | We return !isRequestGroup(doc) ? doc.name || doc.url || 'Untitled request' : doc.name || 'Untitled folder'; }; +const RequestTiming = ({ requestId }: { requestId: string }) => { + const { isExecuting } = useExecutionState({ requestId }); + return isExecuting ? : null; +}; + export const Debug: FC = () => { const { activeWorkspace, @@ -227,18 +233,6 @@ export const Debug: FC = () => { }, []); const { settings } = useRootLoaderData(); - const [runningRequests, setRunningRequests] = useState< - Record - >({}); - const setLoading = (isLoading: boolean) => { - invariant(requestId, 'No active request'); - if (Boolean(runningRequests?.[requestId]) !== isLoading) { - setRunningRequests({ - ...runningRequests, - [requestId]: isLoading ? true : false, - }); - } - }; const grpcState = grpcStates.find(s => s.requestId === requestId); const setGrpcState = (newState: GrpcRequestState) => @@ -1252,6 +1246,7 @@ export const Debug: FC = () => { }} /> {isWebSocketRequest(item.doc) && } + {isRequest(item.doc) && } {isEventStreamRequest(item.doc) && } {item.pinned && ( @@ -1348,7 +1343,6 @@ export const Debug: FC = () => { { setPastedCurl(text); setPasteCurlModalOpen(true); @@ -1376,7 +1370,7 @@ export const Debug: FC = () => { )} {activeRequest && isRequest(activeRequest) && !isRealtimeRequest && ( - + )} diff --git a/packages/insomnia/src/ui/routes/request.tsx b/packages/insomnia/src/ui/routes/request.tsx index 7bf84bb1029..ea7f3665a1b 100644 --- a/packages/insomnia/src/ui/routes/request.tsx +++ b/packages/insomnia/src/ui/routes/request.tsx @@ -365,8 +365,11 @@ export const sendAction: ActionFunction = async ({ request, params }) => { invariant(workspaceId, 'Workspace ID is required'); const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams; try { + window.main.startExecution({ requestId }); const requestData = await fetchRequestData(requestId); + window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' }); const mutatedContext = await getPreRequestScriptOutput(requestData, workspaceId); + window.main.completeExecutionStep({ requestId }); if (mutatedContext === null) { return null; } @@ -374,6 +377,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { const afterResponseScript = `${mutatedContext.request.afterResponseScript}`; mutatedContext.request.afterResponseScript = ''; + window.main.addExecutionStep({ requestId, stepName: 'Rendering request' }); const renderedResult = await tryToInterpolateRequest( mutatedContext.request, mutatedContext.environment, @@ -383,6 +387,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { ignoreUndefinedEnvVariable, ); const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult); + window.main.completeExecutionStep({ requestId }); // TODO: remove this temporary hack to support GraphQL variables in the request body properly if (renderedRequest && renderedRequest.body?.text && renderedRequest.body?.mimeType === 'application/graphql') { @@ -397,6 +402,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { } } + window.main.addExecutionStep({ requestId, stepName: 'Sending request' }); const response = await sendCurlAndWriteTimeline( renderedRequest, mutatedContext.clientCertificates, @@ -405,6 +411,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { requestData.timelinePath, requestData.responseId ); + window.main.completeExecutionStep({ requestId }); const requestMeta = await models.requestMeta.getByParentId(requestId); invariant(requestMeta, 'RequestMeta not found'); @@ -416,7 +423,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { if (requestData.request.afterResponseScript) { const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId); const cookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId); - + window.main.addExecutionStep({ requestId, stepName: 'Executing after-response script' }); const postMutatedContext = await tryToExecuteAfterResponseScript({ ...requestData, ...mutatedContext, @@ -424,6 +431,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { cookieJar, response, }); + window.main.completeExecutionStep({ requestId }); if (!postMutatedContext?.request) { // exiy early if there was a problem with the pre-request script // TODO: improve error message? @@ -434,7 +442,6 @@ export const sendAction: ActionFunction = async ({ request, params }) => { if (!shouldWriteToFile) { const response = await models.response.create(responsePatch, requestData.settings.maxHistoryResponses); await models.requestMeta.update(requestMeta, { activeResponseId: response._id }); - // setLoading(false); return null; } @@ -453,7 +460,6 @@ export const sendAction: ActionFunction = async ({ request, params }) => { ...(defaultPath ? { defaultPath } : {}), }); if (!filePath) { - // setLoading(false); return null; } window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath); @@ -461,6 +467,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => { } } catch (e) { console.log('Failed to send request', e); + window.main.completeExecutionStep({ requestId }); const url = new URL(request.url); url.searchParams.set('error', e); if (e?.extraInfo && e?.extraInfo?.subType === RenderErrorSubType.EnvironmentVariable) { @@ -493,9 +500,22 @@ export const createAndSendToMockbinAction: ActionFunction = async ({ request }) timelinePath, responseId, } = await fetchRequestData(req._id); + window.main.startExecution({ requestId: req._id }); + window.main.addExecutionStep({ + requestId: req._id, + stepName: 'Rendering request', + } + ); const renderResult = await tryToInterpolateRequest(req, environment._id, RENDER_PURPOSE_SEND); const renderedRequest = await tryToTransformRequestWithPlugins(renderResult); + + window.main.completeExecutionStep({ requestId: req._id }); + window.main.addExecutionStep({ + requestId: req._id, + stepName: 'Sending request', + }); + const res = await sendCurlAndWriteTimeline( renderedRequest, clientCertificates, @@ -504,8 +524,10 @@ export const createAndSendToMockbinAction: ActionFunction = async ({ request }) timelinePath, responseId, ); + const response = await responseTransform(res, activeEnvironmentId, renderedRequest, renderResult.context); await models.response.create(response); + window.main.completeExecutionStep({ requestId: req._id }); return null; }; export const deleteAllResponsesAction: ActionFunction = async ({ params }) => {