Skip to content

Commit

Permalink
Add ability to view introspection request/response timeline upon error (
Browse files Browse the repository at this point in the history
#970)

* Add ability to view introspection request/response timeline upon error

* Fix tests
  • Loading branch information
gschier authored Jun 6, 2018
1 parent 3e77cd4 commit c82e163
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('requestCreate()', () => {
};

const r = await models.request.create(patch);
expect(Object.keys(r).length).toBe(20);
expect(Object.keys(r).length).toBe(21);

expect(r._id).toMatch(/^req_[a-zA-Z0-9]{32}$/);
expect(r.created).toBeGreaterThanOrEqual(now);
Expand Down
1 change: 1 addition & 0 deletions packages/insomnia-app/app/common/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export async function getRenderedRequestAndContext (
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies,
settingRebuildPath: renderedRequest.settingRebuildPath,
settingMaxTimelineDataSize: renderedRequest.settingMaxTimelineDataSize,
type: renderedRequest.type,
url: renderedRequest.url
}
Expand Down
9 changes: 6 additions & 3 deletions packages/insomnia-app/app/models/__tests__/request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ describe('init()', () => {
settingSendCookies: true,
settingDisableRenderRequestBody: false,
settingEncodeUrl: true,
settingRebuildPath: true
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000
});
});
});
Expand Down Expand Up @@ -55,7 +56,8 @@ describe('create()', async () => {
settingSendCookies: true,
settingDisableRenderRequestBody: false,
settingEncodeUrl: true,
settingRebuildPath: true
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000
};

expect(request).toEqual(expected);
Expand Down Expand Up @@ -318,7 +320,8 @@ describe('migrate()', () => {
settingSendCookies: true,
settingDisableRenderRequestBody: false,
settingEncodeUrl: true,
settingRebuildPath: true
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000
};

const migrated = await models.initModel(models.request.type, original);
Expand Down
6 changes: 4 additions & 2 deletions packages/insomnia-app/app/models/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ type BaseRequest = {
settingSendCookies: boolean,
settingDisableRenderRequestBody: boolean,
settingEncodeUrl: boolean,
settingRebuildPath: boolean
settingRebuildPath: boolean,
settingMaxTimelineDataSize: number
};

export type Request = BaseModel & BaseRequest;
Expand All @@ -85,7 +86,8 @@ export function init (): BaseRequest {
settingSendCookies: true,
settingDisableRenderRequestBody: false,
settingEncodeUrl: true,
settingRebuildPath: true
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000
};
}

Expand Down
50 changes: 28 additions & 22 deletions packages/insomnia-app/app/network/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ export async function _actuallySend (
const curl = new Curl();

/** Helper function to respond with a success */
function respond (patch: ResponsePatch, bodyPath: ?string): void {
const response = Object.assign(({
async function respond (patch: ResponsePatch, bodyPath: string | null, noPlugins: boolean = false): Promise<void> {
const responsePatchBeforeHooks = Object.assign(({
parentId: renderedRequest._id,
bodyCompression: null, // Will default to .zip otherwise
timeline: timeline,
Expand All @@ -111,17 +111,20 @@ export async function _actuallySend (
settingStoreCookies: renderedRequest.settingStoreCookies
}: ResponsePatch), patch);

resolve(response);
if (noPlugins) {
resolve(responsePatchBeforeHooks);
return;
}

// Apply plugin hooks and don't wait for them and don't throw from them
process.nextTick(async () => {
try {
await _applyResponsePluginHooks(response, renderedRequest, renderContext);
} catch (err) {
// TODO: Better error handling here
console.warn('Response plugin failed', err);
}
});
let responsePatch: ?ResponsePatch;
try {
responsePatch = await _applyResponsePluginHooks(responsePatchBeforeHooks, renderedRequest, renderContext);
} catch (err) {
handleError(new Error(`[plugin] Response hook failed plugin=${err.plugin.name} err=${err.message}`));
return;
}

resolve(responsePatch);
}

/** Helper function to respond with an error */
Expand All @@ -134,7 +137,7 @@ export async function _actuallySend (
statusMessage: 'Error',
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies
});
}, null, true);
}

/** Helper function to set Curl options */
Expand Down Expand Up @@ -164,7 +167,7 @@ export async function _actuallySend (
url: curl.getInfo(Curl.info.EFFECTIVE_URL),
statusMessage: 'Cancelled',
error: 'Request was cancelled'
});
}, null, true);

// Kill it!
curl.close();
Expand Down Expand Up @@ -222,7 +225,7 @@ export async function _actuallySend (
if (infoType === Curl.info.debug.DATA_OUT) {
if (content.length === 0) {
// Sometimes this happens, but I'm not sure why. Just ignore it.
} else if (content.length < 1000) {
} else if (content.length < renderedRequest.settingMaxTimelineDataSize) {
timeline.push({name, value: content});
} else {
timeline.push({name, value: `(${describeByteSize(content.length)} hidden)`});
Expand Down Expand Up @@ -705,7 +708,7 @@ export async function _actuallySend (
statusMessage = 'Abort';
}

respond({statusMessage, error});
respond({statusMessage, error}, null, true);
});

curl.perform();
Expand Down Expand Up @@ -827,10 +830,8 @@ async function _applyRequestPluginHooks (
renderedRequest: RenderedRequest,
renderedContext: Object
): Promise<RenderedRequest> {
let newRenderedRequest = renderedRequest;
const newRenderedRequest = clone(renderedRequest);
for (const {plugin, hook} of await plugins.getRequestHooks()) {
newRenderedRequest = clone(newRenderedRequest);

const context = {
...pluginContexts.app.init(),
...pluginContexts.request.init(newRenderedRequest, renderedContext)
Expand All @@ -851,12 +852,15 @@ async function _applyResponsePluginHooks (
response: ResponsePatch,
request: RenderedRequest,
renderContext: Object
): Promise<void> {
): Promise<ResponsePatch> {
const newResponse = clone(response);
const newRequest = clone(request);

for (const {plugin, hook} of await plugins.getResponseHooks()) {
const context = {
...pluginContexts.app.init(),
...pluginContexts.response.init(response),
...pluginContexts.request.init(request, renderContext, true)
...pluginContexts.response.init(newResponse),
...pluginContexts.request.init(newRequest, renderContext, true)
};

try {
Expand All @@ -866,6 +870,8 @@ async function _applyResponsePluginHooks (
throw err;
}
}

return newResponse;
}

export function _parseHeaders (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('app.import.*', () => {
settingSendCookies: true,
settingStoreCookies: true,
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000,
type: 'Request',
url: 'https://insomnia.rest'
}]);
Expand Down Expand Up @@ -128,6 +129,7 @@ describe('app.import.*', () => {
settingSendCookies: true,
settingStoreCookies: true,
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000,
type: 'Request',
url: 'https://insomnia.rest'
}]);
Expand Down Expand Up @@ -199,6 +201,7 @@ describe('app.export.*', () => {
settingSendCookies: true,
settingStoreCookies: true,
settingRebuildPath: true,
settingMaxTimelineDataSize: 1000,
url: 'https://insomnia.rest'
}]
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ class CodeEditor extends React.Component {
try {
jsonString = JSON.stringify(jq.query(obj, this.state.filter));
} catch (err) {
console.log('[jsonpath] Error: ', err);
jsonString = '[]';
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ import {jsonParseOr} from '../../../../common/misc';
import HelpTooltip from '../../help-tooltip';
import {CONTENT_TYPE_JSON, DEBOUNCE_MILLIS} from '../../../../common/constants';
import prettify from 'insomnia-prettify';
import type {ResponsePatch} from '../../../../network/network';
import * as network from '../../../../network/network';
import type {Workspace} from '../../../../models/workspace';
import type {Settings} from '../../../../models/settings';
import TimeFromNow from '../../time-from-now';
import * as models from '../../../../models/index';
import * as db from '../../../../common/database';
import {showModal} from '../../modals';
import WrapperModal from '../../modals/wrapper-modal';
import ResponseTimelineViewer from '../../viewers/response-timeline-viewer';
import Tooltip from '../../tooltip';

type GraphQLBody = {
query: string,
Expand All @@ -43,7 +48,10 @@ type Props = {
type State = {
body: GraphQLBody,
schema: Object | null,
schemaFetchError: string,
schemaFetchError: {
message: string,
response: ResponsePatch | null
} | null,
schemaLastFetchTime: number,
schemaIsFetching: boolean,
hideSchemaFetchErrors: boolean,
Expand All @@ -62,7 +70,7 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
this.state = {
body: GraphQLEditor._stringToGraphQL(props.content),
schema: null,
schemaFetchError: '',
schemaFetchError: null,
schemaLastFetchTime: 0,
schemaIsFetching: false,
hideSchemaFetchErrors: false,
Expand All @@ -71,6 +79,31 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
};
}

_handleViewResponse () {
const {settings} = this.props;
const {schemaFetchError} = this.state;
if (!schemaFetchError || !schemaFetchError.response) {
return;
}

const {response} = schemaFetchError;

showModal(WrapperModal, {
title: 'Introspection Request',
tall: true,
body: (
<div style={{display: 'grid'}} className="tall pad-top">
<ResponseTimelineViewer
editorFontSize={settings.editorFontSize}
editorIndentSize={settings.editorIndentSize}
editorLineWrapping={settings.editorLineWrapping}
timeline={response.timeline}
/>
</div>
)
});
}

_hideSchemaFetchError () {
this.setState({hideSchemaFetchErrors: true});
}
Expand All @@ -82,11 +115,12 @@ class GraphQLEditor extends React.PureComponent<Props, State> {

const newState = {
schema: this.state.schema,
schemaFetchError: '',
schemaFetchError: (null: any),
schemaLastFetchTime: this.state.schemaLastFetchTime,
schemaIsFetching: false
};

let responsePatch: ResponsePatch | null = null;
try {
const bodyJson = JSON.stringify({
query: introspectionQuery,
Expand All @@ -95,32 +129,45 @@ class GraphQLEditor extends React.PureComponent<Props, State> {

const introspectionRequest = await db.upsert(Object.assign({}, rawRequest, {
_id: rawRequest._id + '.graphql',
settingMaxTimelineDataSize: 5000,
parentId: rawRequest._id,
isPrivate: true, // So it doesn't get synced or exported
body: newBodyRaw(bodyJson, CONTENT_TYPE_JSON)
}));

const responsePatch = await network.send(introspectionRequest._id, environmentId);
responsePatch = await network.send(introspectionRequest._id, environmentId);
const bodyBuffer = models.response.getBodyBuffer(responsePatch);

const status = typeof responsePatch.statusCode === 'number' ? responsePatch.statusCode : 0;
const error = typeof responsePatch.error === 'string' ? responsePatch.error : '';

if (error) {
newState.schemaFetchError = error;
newState.schemaFetchError = {
message: error,
response: responsePatch
};
} else if (status < 200 || status >= 300) {
const renderedURL = responsePatch.url || rawRequest.url;
newState.schemaFetchError = `Got status ${status} fetching schema from "${renderedURL}"`;
newState.schemaFetchError = {
message: `Got status ${status} fetching schema from "${renderedURL}"`,
response: responsePatch
};
} else if (bodyBuffer) {
const {data} = JSON.parse(bodyBuffer.toString());
newState.schema = buildClientSchema(data);
newState.schemaLastFetchTime = Date.now();
} else {
newState.schemaFetchError = 'No response body received when fetching schema';
newState.schemaFetchError = {
message: 'No response body received when fetching schema',
response: responsePatch
};
}
} catch (err) {
console.warn('Failed to fetch GraphQL schema', err);
newState.schemaFetchError = `Failed to to fetch schema: ${err.message}`;
newState.schemaFetchError = {
message: `Failed to to fetch schema: ${err.message}`,
response: responsePatch
};
}

if (this._isMounted) {
Expand Down Expand Up @@ -348,10 +395,19 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
<div className="graphql-editor__schema-error">
{!hideSchemaFetchErrors && schemaFetchError && (
<div className="notice error margin no-margin-top margin-bottom-sm">
<button className="pull-right icon" onClick={this._hideSchemaFetchError}>
<i className="fa fa-times"/>
</button>
{schemaFetchError}
<div className="pull-right">
<Tooltip position="top" message="View introspection request/response timeline">
<button className="icon icon--success" onClick={this._handleViewResponse}>
<i className="fa fa-bug"/>
</button>
</Tooltip>
{' '}
<button className="icon" onClick={this._hideSchemaFetchError}>
<i className="fa fa-times"/>
</button>
</div>
{schemaFetchError.message}
<br/>
</div>
)}
</div>
Expand Down
Loading

0 comments on commit c82e163

Please sign in to comment.