Skip to content

Commit

Permalink
Output stack trace for errors in debug mode (#137)
Browse files Browse the repository at this point in the history
* add debug option to runQuery

* add test for integrations

* update changelog

* fix lint error
  • Loading branch information
nnance authored and helfer committed Sep 12, 2016
1 parent 9724594 commit f4546e7
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
[PR #131](https://github.com/apollostack/apollo-server/pull/131)
* Improve logging function. Issue #79. ([@nnance](https://github.com/nnance)) in
[PR #136](https://github.com/apollostack/apollo-server/pull/136)
* Output stack trace for errors in debug mode. Issue #111. ([@nnance](https://github.com/nnance)) in
[PR #137](https://github.com/apollostack/apollo-server/pull/137)

### v0.2.6
* Expose the OperationStore as part of the public API. ([@nnance](https://github.com/nnance))
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"mocha": "^3.0.0",
"multer": "^1.1.0",
"remap-istanbul": "^0.6.4",
"sinon": "^1.17.5",
"supertest": "^2.0.0",
"supertest-as-promised": "^4.0.0",
"tslint": "^3.13.0",
Expand Down
63 changes: 48 additions & 15 deletions src/core/runQuery.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
expect,
} from 'chai';
import { expect } from 'chai';
import { stub } from 'sinon';

import {
GraphQLSchema,
Expand Down Expand Up @@ -62,6 +61,12 @@ const QueryType = new GraphQLObjectType({
return 'it ' + (<any>Promise).await('works');
},
},
testError: {
type: GraphQLString,
resolve() {
throw new Error('Secret error message');
},
},
},
});

Expand All @@ -88,18 +93,46 @@ describe('runQuery', () => {
});
});

it('returns a syntax error if the query string contains one', () => {
const query = `query { test`;
const expected = /Syntax Error GraphQL/;
return runQuery({
schema: Schema,
query: query,
variables: { base: 1 },
}).then((res) => {
expect(res.data).to.be.undefined;
expect(res.errors.length).to.equal(1);
return expect(res.errors[0].message).to.match(expected);
});
it('returns a syntax error if the query string contains one', () => {
const query = `query { test `;
const expected = /Syntax Error GraphQL/;
return runQuery({
schema: Schema,
query: query,
variables: { base: 1 },
}).then((res) => {
expect(res.data).to.be.undefined;
expect(res.errors.length).to.equal(1);
return expect(res.errors[0].message).to.match(expected);
});
});

it('sends stack trace to error if in an error occurs and debug mode is set', () => {
const query = `query { testError }`;
const expected = /at resolveOrError/;
const logStub = stub(console, 'error');
return runQuery({
schema: Schema,
query: query,
debug: true,
}).then((res) => {
logStub.restore();
expect(logStub.callCount).to.equal(1);
return expect(logStub.getCall(0).args[0]).to.match(expected);
});
});

it('does not send stack trace if in an error occurs and not in debug mode', () => {
const query = `query { testError }`;
const logStub = stub(console, 'error');
return runQuery({
schema: Schema,
query: query,
debug: false,
}).then((res) => {
logStub.restore();
return expect(logStub.callCount).to.equal(0);
});
});

it('returns a validation error if the query string does not pass validation', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/core/runQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface QueryOptions {

formatError?: Function;
formatResponse?: Function;
debug?: boolean;
}

const resolvedPromise = Promise.resolve();
Expand All @@ -62,6 +63,8 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResult> {
let documentAST: Document;

const logFunction = options.logFunction || function(){ return null; };
const debugDefault = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
const debug = typeof options.debug !== 'undefined' ? options.debug : debugDefault;

logFunction({action: LogAction.request, step: LogStep.start});

Expand All @@ -73,6 +76,10 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResult> {
return errors.map(options.formatError || formatError as any) as Array<Error>;
}

function printStackTrace(error: Error) {
console.error(error.stack);
}

const qry = typeof options.query === 'string' ? options.query : print(options.query);
logFunction({action: LogAction.request, step: LogStep.status, key: 'query', data: qry});
logFunction({action: LogAction.request, step: LogStep.status, key: 'variables', data: options.variables});
Expand Down Expand Up @@ -123,6 +130,9 @@ function doRunQuery(options: QueryOptions): Promise<GraphQLResult> {
};
if (gqlResponse.errors) {
response['errors'] = format(gqlResponse.errors);
if (debug) {
gqlResponse.errors.map(printStackTrace);
}
}
if (options.formatResponse) {
response = options.formatResponse(response, options);
Expand Down
2 changes: 2 additions & 0 deletions src/integrations/apolloOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LogFunction } from '../core/runQuery';
* - (optional) formatParams: a function applied to the parameters of every invocation of runQuery
* - (optional) validationRules: extra validation rules applied to requests
* - (optional) formatResponse: a function applied to each graphQL execution result
* - (optional) debug: a boolean that will print additional debug logging if execution errors occur
*
*/
interface ApolloOptions {
Expand All @@ -23,6 +24,7 @@ interface ApolloOptions {
formatParams?: Function;
validationRules?: Array<ValidationRule>;
formatResponse?: Function;
debug?: boolean;
}

export default ApolloOptions;
1 change: 1 addition & 0 deletions src/integrations/expressApollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function apolloExpress(options: ApolloOptions | ExpressApolloOptionsFunct
validationRules: optionsObject.validationRules,
formatError: formatErrorFn,
formatResponse: optionsObject.formatResponse,
debug: optionsObject.debug,
};

if (optionsObject.formatParams) {
Expand Down
1 change: 1 addition & 0 deletions src/integrations/hapiApollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ async function processQuery(graphqlParams, optionsObject: ApolloOptions, reply)
validationRules: optionsObject.validationRules,
formatError: formatErrorFn,
formatResponse: optionsObject.formatResponse,
debug: optionsObject.debug,
};

if (optionsObject.formatParams) {
Expand Down
44 changes: 41 additions & 3 deletions src/integrations/integrations.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
expect,
} from 'chai';
import { expect } from 'chai';
import { stub } from 'sinon';

import {
GraphQLSchema,
Expand Down Expand Up @@ -425,6 +424,45 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
});

it('sends stack trace to error if debug mode is set', () => {
const expected = /at resolveOrError/;
const stackTrace = [];
const origError = console.error;
console.error = (...args) => stackTrace.push(args);
app = createApp({apolloOptions: {
schema: Schema,
debug: true,
}});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then((res) => {
console.error = origError;
return expect(stackTrace[0][0]).to.match(expected);
});
});

it('sends stack trace to error log if debug mode is set', () => {
const logStub = stub(console, 'error');
const expected = /at resolveOrError/;
app = createApp({apolloOptions: {
schema: Schema,
debug: true,
}});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then((res) => {
logStub.restore();
expect(logStub.callCount).to.equal(1);
return expect(logStub.getCall(0).args[0]).to.match(expected);
});
});

it('applies additional validationRules', () => {
const expected = 'AlwaysInvalidRule was really invalid!';
const AlwaysInvalidRule = function (context) {
Expand Down
1 change: 1 addition & 0 deletions src/integrations/koaApollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function apolloKoa(options: ApolloOptions | KoaApolloOptionsFunction): Ko
validationRules: optionsObject.validationRules,
formatError: formatErrorFn,
formatResponse: optionsObject.formatResponse,
debug: optionsObject.debug,
};

if (optionsObject.formatParams) {
Expand Down
3 changes: 2 additions & 1 deletion typings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"dependencies": {
"chai": "registry:npm/chai#3.5.0+20160723033700",
"graphql": "github:nitintutlani/typed-graphql#ffe7e46e2249cc8f3824a5d15a44938f4354afe9",
"http-errors": "registry:npm/http-errors#1.4.0+20160723033700"
"http-errors": "registry:npm/http-errors#1.4.0+20160723033700",
"sinon": "registry:npm/sinon#1.16.0+20160723033700"
},
"globalDependencies": {
"body-parser": "registry:dt/body-parser#0.0.0+20160619023215",
Expand Down

0 comments on commit f4546e7

Please sign in to comment.