Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to view introspection request/response timeline upon error #970

Merged
merged 2 commits into from
Jun 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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