Skip to content

Commit 21354de

Browse files
authored
enhance(runtime): log unexpected errors to the console with their stack traces (ardatan#6798)
* enhance(runtime): log unexpected errors to the console with their stack traces * Print original stack trace in production * Go
1 parent 76baa44 commit 21354de

File tree

4 files changed

+169
-2
lines changed

4 files changed

+169
-2
lines changed

.changeset/quiet-drinks-taste.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@graphql-mesh/runtime": patch
3+
---
4+
5+
Log unexpected non GraphQL errors with the stack trace
6+
7+
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.

packages/legacy/runtime/src/get-mesh.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
DocumentNode,
33
getOperationAST,
4+
GraphQLError,
45
GraphQLObjectType,
56
GraphQLSchema,
67
OperationTypeNode,
@@ -36,6 +37,7 @@ import {
3637
import { CreateProxyingResolverFn, Subschema, SubschemaConfig } from '@graphql-tools/delegate';
3738
import { normalizedExecutor } from '@graphql-tools/executor';
3839
import {
40+
createGraphQLError,
3941
ExecutionResult,
4042
getRootTypeMap,
4143
isAsyncIterable,
@@ -48,7 +50,7 @@ import { MESH_CONTEXT_SYMBOL } from './constants.js';
4850
import { getInContextSDK } from './in-context-sdk.js';
4951
import { ExecuteMeshFn, GetMeshOptions, MeshExecutor, SubscribeMeshFn } from './types.js';
5052
import { useSubschema } from './useSubschema.js';
51-
import { isGraphQLJitCompatible, isStreamOperation } from './utils.js';
53+
import { getOriginalError, isGraphQLJitCompatible, isStreamOperation } from './utils.js';
5254

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

@@ -303,6 +305,46 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
303305
useExtendedValidation({
304306
rules: [OneOfInputObjectsRule],
305307
}),
308+
{
309+
onExecute() {
310+
return {
311+
onExecuteDone({ result, setResult }) {
312+
if (result.errors) {
313+
// Print errors with stack trace in development
314+
if (process.env.NODE_ENV === 'production') {
315+
for (const error of result.errors) {
316+
const origError = getOriginalError(error);
317+
if (origError) {
318+
logger.error(origError);
319+
}
320+
}
321+
} else {
322+
setResult({
323+
...result,
324+
errors: result.errors.map(error => {
325+
const origError = getOriginalError(error);
326+
if (origError) {
327+
return createGraphQLError(error.message, {
328+
...error,
329+
extensions: {
330+
...error.extensions,
331+
originalError: {
332+
name: origError.name,
333+
message: origError.message,
334+
stack: origError.stack,
335+
},
336+
},
337+
});
338+
}
339+
return error;
340+
}),
341+
});
342+
}
343+
}
344+
},
345+
};
346+
},
347+
},
306348
...initialPluginList,
307349
];
308350

packages/legacy/runtime/src/utils.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
GraphQLSchema,
88
visit,
99
} from 'graphql';
10-
import { getDocumentString } from '@envelop/core';
10+
import { getDocumentString, isGraphQLError } from '@envelop/core';
1111
import { MapperKind, mapSchema, memoize1 } from '@graphql-tools/utils';
1212

1313
export const isStreamOperation = memoize1(function isStreamOperation(astNode: ASTNode): boolean {
@@ -74,3 +74,10 @@ export const isGraphQLJitCompatible = memoize1(function isGraphQLJitCompatible(
7474
}
7575
return false;
7676
});
77+
78+
export function getOriginalError(error: Error) {
79+
if (isGraphQLError(error)) {
80+
return getOriginalError(error.originalError);
81+
}
82+
return error;
83+
}

packages/legacy/runtime/test/getMesh.test.ts

+111
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('getMesh', () => {
3131
pubsub,
3232
logger,
3333
});
34+
process.env.NODE_ENV = 'test';
3435
});
3536

3637
interface CreateSchemaConfiguration {
@@ -213,4 +214,114 @@ describe('getMesh', () => {
213214
}
214215
`);
215216
});
217+
218+
it('logs the unexpected errors with stack traces in production', async () => {
219+
process.env.NODE_ENV = 'production';
220+
const errorLogSpy = jest.spyOn(logger, 'error');
221+
const mesh = await getMesh({
222+
cache,
223+
pubsub,
224+
logger,
225+
merger,
226+
sources: [
227+
createGraphQLSource({
228+
suffix: 'Foo',
229+
suffixRootTypeNames: false,
230+
suffixFieldNames: true,
231+
suffixResponses: true,
232+
}),
233+
],
234+
additionalTypeDefs: [
235+
parse(/* GraphQL */ `
236+
extend type Query {
237+
throwMe: String
238+
}
239+
`),
240+
],
241+
additionalResolvers: {
242+
Query: {
243+
throwMe: () => {
244+
throw new Error('This is an error');
245+
},
246+
},
247+
},
248+
});
249+
250+
const result = await mesh.execute(
251+
/* GraphQL */ `
252+
query {
253+
throwMe
254+
}
255+
`,
256+
{},
257+
);
258+
259+
expect(result).toMatchInlineSnapshot(`
260+
{
261+
"data": {
262+
"throwMe": null,
263+
},
264+
"errors": [
265+
[GraphQLError: This is an error],
266+
],
267+
"stringify": [Function],
268+
}
269+
`);
270+
271+
const firstErrorWithStack = errorLogSpy.mock.calls[0][0].stack;
272+
expect(firstErrorWithStack).toContain('This is an error');
273+
expect(firstErrorWithStack).toContain('at Object.throwMe (');
274+
});
275+
276+
it('prints errors with stack traces of the original errors in development', async () => {
277+
process.env.NODE_ENV = 'development';
278+
const mesh = await getMesh({
279+
cache,
280+
pubsub,
281+
logger,
282+
merger,
283+
sources: [
284+
createGraphQLSource({
285+
suffix: 'Foo',
286+
suffixRootTypeNames: false,
287+
suffixFieldNames: true,
288+
suffixResponses: true,
289+
}),
290+
],
291+
additionalTypeDefs: [
292+
parse(/* GraphQL */ `
293+
extend type Query {
294+
throwMe: String
295+
}
296+
`),
297+
],
298+
additionalResolvers: {
299+
Query: {
300+
throwMe: () => {
301+
throw new Error('This is an error');
302+
},
303+
},
304+
},
305+
});
306+
307+
const result = await mesh.execute(
308+
/* GraphQL */ `
309+
query {
310+
throwMe
311+
}
312+
`,
313+
{},
314+
);
315+
316+
const error = result.errors[0];
317+
expect(error.message).toContain('This is an error');
318+
const serializedOriginalError = error.extensions?.originalError as {
319+
name: string;
320+
message: string;
321+
stack: string[];
322+
};
323+
expect(serializedOriginalError?.message).toContain('This is an error');
324+
expect(serializedOriginalError?.stack?.[0]).toBe('Error: This is an error');
325+
expect(serializedOriginalError?.stack?.[1]).toContain('at Object.throwMe (');
326+
});
216327
});

0 commit comments

Comments
 (0)