Skip to content

Commit

Permalink
enhance(runtime): log unexpected errors to the console with their sta…
Browse files Browse the repository at this point in the history
…ck traces (#6798)

* enhance(runtime): log unexpected errors to the console with their stack traces

* Print original stack trace in production

* Go
  • Loading branch information
ardatan committed Apr 17, 2024
1 parent 76baa44 commit 666b79c
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/quiet-drinks-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@graphql-mesh/runtime": patch
---

Log unexpected non GraphQL errors with the stack trace

Previously, it was not possible to see the stack trace of unexpected errors that were not related to GraphQL. This change logs the stack trace of such errors.
44 changes: 43 additions & 1 deletion packages/legacy/runtime/src/get-mesh.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DocumentNode,
getOperationAST,
GraphQLError,
GraphQLObjectType,
GraphQLSchema,
OperationTypeNode,
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
import { CreateProxyingResolverFn, Subschema, SubschemaConfig } from '@graphql-tools/delegate';
import { normalizedExecutor } from '@graphql-tools/executor';
import {
createGraphQLError,
ExecutionResult,
getRootTypeMap,
isAsyncIterable,
Expand All @@ -48,7 +50,7 @@ import { MESH_CONTEXT_SYMBOL } from './constants.js';
import { getInContextSDK } from './in-context-sdk.js';
import { ExecuteMeshFn, GetMeshOptions, MeshExecutor, SubscribeMeshFn } from './types.js';
import { useSubschema } from './useSubschema.js';
import { isGraphQLJitCompatible, isStreamOperation } from './utils.js';
import { getOriginalError, isGraphQLJitCompatible, isStreamOperation } from './utils.js';

type SdkRequester = (document: DocumentNode, variables?: any, operationContext?: any) => any;

Expand Down Expand Up @@ -303,6 +305,46 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
useExtendedValidation({
rules: [OneOfInputObjectsRule],
}),
{
onExecute() {
return {
onExecuteDone({ result, setResult }) {
if (result.errors) {
// Print errors with stack trace in development
if (process.env.NODE_ENV === 'production') {
for (const error of result.errors) {
const origError = getOriginalError(error);
if (origError) {
logger.error(origError);
}
}
} else {
setResult({
...result,
errors: result.errors.map(error => {
const origError = getOriginalError(error);
if (origError) {
return createGraphQLError(error.message, {
...error,
extensions: {
...error.extensions,
originalError: {
name: origError.name,
message: origError.message,
stack: origError.stack,
},
},
});
}
return error;
}),
});
}
}
},
};
},
},
...initialPluginList,
];

Expand Down
9 changes: 8 additions & 1 deletion packages/legacy/runtime/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
GraphQLSchema,
visit,
} from 'graphql';
import { getDocumentString } from '@envelop/core';
import { getDocumentString, isGraphQLError } from '@envelop/core';
import { MapperKind, mapSchema, memoize1 } from '@graphql-tools/utils';

export const isStreamOperation = memoize1(function isStreamOperation(astNode: ASTNode): boolean {
Expand Down Expand Up @@ -74,3 +74,10 @@ export const isGraphQLJitCompatible = memoize1(function isGraphQLJitCompatible(
}
return false;
});

export function getOriginalError(error: Error) {
if (isGraphQLError(error)) {
return getOriginalError(error.originalError);
}
return error;
}
110 changes: 110 additions & 0 deletions packages/legacy/runtime/test/getMesh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('getMesh', () => {
pubsub,
logger,
});
process.env.NODE_ENV = 'test';
});

interface CreateSchemaConfiguration {
Expand Down Expand Up @@ -213,4 +214,113 @@ describe('getMesh', () => {
}
`);
});

it('logs the unexpected errors with stack traces in production', async () => {
process.env.NODE_ENV = 'production';
const errorLogSpy = jest.spyOn(logger, 'error');
const mesh = await getMesh({
cache,
pubsub,
logger,
merger,
sources: [
createGraphQLSource({
suffix: 'Foo',
suffixRootTypeNames: false,
suffixFieldNames: true,
suffixResponses: true,
}),
],
additionalTypeDefs: [
parse(/* GraphQL */ `
extend type Query {
throwMe: String
}
`),
],
additionalResolvers: {
Query: {
throwMe: () => {
throw new Error('This is an error');
},
},
},
});

const result = await mesh.execute(
/* GraphQL */ `
query {
throwMe
}
`,
{},
);

expect(result).toMatchInlineSnapshot(`
{
"data": {
"throwMe": null,
},
"errors": [
[GraphQLError: This is an error],
],
"stringify": [Function],
}
`);

const firstErrorWithStack = errorLogSpy.mock.calls[0][0].stack;
expect(firstErrorWithStack).toContain('This is an error');
expect(firstErrorWithStack).toContain('at Object.throwMe (');
});

it('prints errors with stack traces of the original errors in development', async () => {
process.env.NODE_ENV = 'development';
const mesh = await getMesh({
cache,
pubsub,
logger,
merger,
sources: [
createGraphQLSource({
suffix: 'Foo',
suffixRootTypeNames: false,
suffixFieldNames: true,
suffixResponses: true,
}),
],
additionalTypeDefs: [
parse(/* GraphQL */ `
extend type Query {
throwMe: String
}
`),
],
additionalResolvers: {
Query: {
throwMe: () => {
throw new Error('This is an error');
},
},
},
});

const result = await mesh.execute(
/* GraphQL */ `
query {
throwMe
}
`,
{},
);

const error = result.errors[0];
expect(error.message).toContain('This is an error');
const serializedOriginalError = error.extensions?.originalError as {
name: string;
message: string;
stack: string[];
};
expect(serializedOriginalError?.message).toContain('This is an error');
expect(serializedOriginalError?.stack).toContain('at Object.throwMe (');
});
});

0 comments on commit 666b79c

Please sign in to comment.