From e5f39d6cb19974bf774d9f4d9bf58fbacfa9cdb8 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Thu, 15 Feb 2024 12:27:48 -0500 Subject: [PATCH 1/4] Add tag and fingerprint to graphql errors --- src/components/ErrorHandling/Sentry.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/ErrorHandling/Sentry.tsx b/src/components/ErrorHandling/Sentry.tsx index 09defe0d5a..01403d1ead 100644 --- a/src/components/ErrorHandling/Sentry.tsx +++ b/src/components/ErrorHandling/Sentry.tsx @@ -4,6 +4,7 @@ import { getCurrentHub, init, Replay, + setTag, withScope, } from "@sentry/react"; import type { Scope, SeverityLevel } from "@sentry/react"; @@ -46,6 +47,21 @@ const sendError = ( withScope((scope) => { setScope(scope, { level: severity, context: metadata }); + const { gqlErr, operationName } = metadata ?? {}; + + // Add additional sorting for GraphQL errors + if (operationName) { + // A custom fingerprint allows for more intelligent grouping + const fingerprint = [operationName]; + if (gqlErr?.path && Array.isArray(gqlErr.path)) { + fingerprint.push(...gqlErr.path); + } + scope.setFingerprint(fingerprint); + + // Apply tag, which is a searchable/filterable property + setTag("operationName", metadata.operationName); + } + captureException(err); }); }; From a9c2f319f70b2ec22a7745686665ccacf22d7392 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Fri, 16 Feb 2024 11:24:21 -0500 Subject: [PATCH 2/4] Address CR comments --- src/components/ErrorHandling/Sentry.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/ErrorHandling/Sentry.tsx b/src/components/ErrorHandling/Sentry.tsx index 01403d1ead..1124486a89 100644 --- a/src/components/ErrorHandling/Sentry.tsx +++ b/src/components/ErrorHandling/Sentry.tsx @@ -1,3 +1,4 @@ +import { ErrorResponse } from "@apollo/client/link/error"; import { captureException, ErrorBoundary as SentryErrorBoundary, @@ -39,10 +40,16 @@ const initializeSentry = () => { const isInitialized = () => !!getCurrentHub().getClient(); +type ErrorMetadata = { + gqlErr?: ErrorResponse["graphQLErrors"][0]; + operationName?: ErrorResponse["operation"]["operationName"]; + variables?: ErrorResponse["operation"]["variables"]; +}; + const sendError = ( err: Error, severity: SeverityLevel, - metadata?: { [key: string]: any }, + metadata?: ErrorMetadata, ) => { withScope((scope) => { setScope(scope, { level: severity, context: metadata }); @@ -59,7 +66,7 @@ const sendError = ( scope.setFingerprint(fingerprint); // Apply tag, which is a searchable/filterable property - setTag("operationName", metadata.operationName); + setTag("operationName", operationName); } captureException(err); From 1873ac189d3547a3efe3d53a5614734bc991d543 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Fri, 16 Feb 2024 13:24:55 -0500 Subject: [PATCH 3/4] Update metadata typing --- src/components/ErrorHandling/Sentry.tsx | 2 +- src/utils/errorReporting.test.ts | 2 +- src/utils/errorReporting.ts | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/ErrorHandling/Sentry.tsx b/src/components/ErrorHandling/Sentry.tsx index 1124486a89..62d81cab97 100644 --- a/src/components/ErrorHandling/Sentry.tsx +++ b/src/components/ErrorHandling/Sentry.tsx @@ -40,7 +40,7 @@ const initializeSentry = () => { const isInitialized = () => !!getCurrentHub().getClient(); -type ErrorMetadata = { +export type ErrorMetadata = { gqlErr?: ErrorResponse["graphQLErrors"][0]; operationName?: ErrorResponse["operation"]["operationName"]; variables?: ErrorResponse["operation"]["variables"]; diff --git a/src/utils/errorReporting.test.ts b/src/utils/errorReporting.test.ts index be8688bf57..fdff8e659d 100644 --- a/src/utils/errorReporting.test.ts +++ b/src/utils/errorReporting.test.ts @@ -54,7 +54,7 @@ describe("error reporting", () => { name: "Error Name", }; - const metadata = { customField: "foo" }; + const metadata = { operationName: "foo" }; const result = reportError(err, metadata); result.severe(); expect(Sentry.captureException).toHaveBeenCalledWith(err); diff --git a/src/utils/errorReporting.ts b/src/utils/errorReporting.ts index c13c580a0b..f34dd56cf2 100644 --- a/src/utils/errorReporting.ts +++ b/src/utils/errorReporting.ts @@ -1,5 +1,8 @@ import { addBreadcrumb, Breadcrumb } from "@sentry/react"; -import { sendError as sentrySendError } from "components/ErrorHandling/Sentry"; +import { + ErrorMetadata, + sendError as sentrySendError, +} from "components/ErrorHandling/Sentry"; import { isProductionBuild } from "./environmentVariables"; interface reportErrorResult { @@ -9,7 +12,7 @@ interface reportErrorResult { const reportError = ( err: Error, - metadata?: { [key: string]: any }, + metadata?: ErrorMetadata, ): reportErrorResult => { if (!isProductionBuild()) { return { From eb7335b311767177243a43bdf4e493f4c721c919 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Fri, 16 Feb 2024 16:27:47 -0500 Subject: [PATCH 4/4] Improve fingerprint/tag handling --- src/components/ErrorHandling/Sentry.tsx | 45 ++++++++++++------------- src/gql/GQLWrapper.tsx | 13 +++++-- src/utils/errorReporting.test.ts | 27 ++++++++++++--- src/utils/errorReporting.ts | 30 +++++++++++++---- 4 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/components/ErrorHandling/Sentry.tsx b/src/components/ErrorHandling/Sentry.tsx index 62d81cab97..b26f48272a 100644 --- a/src/components/ErrorHandling/Sentry.tsx +++ b/src/components/ErrorHandling/Sentry.tsx @@ -1,14 +1,14 @@ -import { ErrorResponse } from "@apollo/client/link/error"; import { captureException, ErrorBoundary as SentryErrorBoundary, getCurrentHub, init, Replay, - setTag, + setTags, withScope, } from "@sentry/react"; import type { Scope, SeverityLevel } from "@sentry/react"; +import type { Context, Primitive } from "@sentry/types"; import { environmentVariables } from "utils"; import ErrorFallback from "./ErrorFallback"; @@ -40,33 +40,32 @@ const initializeSentry = () => { const isInitialized = () => !!getCurrentHub().getClient(); -export type ErrorMetadata = { - gqlErr?: ErrorResponse["graphQLErrors"][0]; - operationName?: ErrorResponse["operation"]["operationName"]; - variables?: ErrorResponse["operation"]["variables"]; +export type ErrorInput = { + err: Error; + fingerprint?: string[]; + context?: Context; + severity: SeverityLevel; + tags?: { [key: string]: Primitive }; }; -const sendError = ( - err: Error, - severity: SeverityLevel, - metadata?: ErrorMetadata, -) => { +const sendError = ({ + context, + err, + fingerprint, + severity, + tags, +}: ErrorInput) => { withScope((scope) => { - setScope(scope, { level: severity, context: metadata }); + setScope(scope, { level: severity, context }); - const { gqlErr, operationName } = metadata ?? {}; - - // Add additional sorting for GraphQL errors - if (operationName) { + if (fingerprint) { // A custom fingerprint allows for more intelligent grouping - const fingerprint = [operationName]; - if (gqlErr?.path && Array.isArray(gqlErr.path)) { - fingerprint.push(...gqlErr.path); - } scope.setFingerprint(fingerprint); + } - // Apply tag, which is a searchable/filterable property - setTag("operationName", operationName); + if (tags) { + // Apply tags, which are a searchable/filterable property + setTags(tags); } captureException(err); @@ -75,7 +74,7 @@ const sendError = ( type ScopeOptions = { level?: SeverityLevel; - context?: { [key: string]: any }; + context?: Context; }; const setScope = (scope: Scope, { context, level }: ScopeOptions = {}) => { diff --git a/src/gql/GQLWrapper.tsx b/src/gql/GQLWrapper.tsx index 4618249005..6c2e5e857e 100644 --- a/src/gql/GQLWrapper.tsx +++ b/src/gql/GQLWrapper.tsx @@ -144,10 +144,17 @@ const authLink = (logout: () => void): ApolloLink => const logErrorsLink = onError(({ graphQLErrors, operation }) => { if (Array.isArray(graphQLErrors)) { graphQLErrors.forEach((gqlErr) => { + const fingerprint = [operation.operationName]; + if (gqlErr?.path?.length) { + fingerprint.push(...gqlErr.path); + } reportError(new Error(gqlErr.message), { - gqlErr, - operationName: operation.operationName, - variables: operation.variables, + fingerprint, + tags: { operationName: operation.operationName }, + context: { + gqlErr, + variables: operation.variables, + }, }).warning(); }); } diff --git a/src/utils/errorReporting.test.ts b/src/utils/errorReporting.test.ts index fdff8e659d..1e8eb1cf89 100644 --- a/src/utils/errorReporting.test.ts +++ b/src/utils/errorReporting.test.ts @@ -34,7 +34,7 @@ describe("error reporting", () => { expect(Sentry.captureException).not.toHaveBeenCalled(); }); - it("should report errors to Sentry when in production", () => { + it("reports errors to Sentry when in production", () => { mockEnv("NODE_ENV", "production"); jest.spyOn(Sentry, "captureException").mockImplementation(jest.fn()); @@ -46,7 +46,7 @@ describe("error reporting", () => { expect(Sentry.captureException).toHaveBeenCalledWith(err); }); - it("supports metadata field", () => { + it("supports context field", () => { mockEnv("NODE_ENV", "production"); jest.spyOn(Sentry, "captureException").mockImplementation(jest.fn()); const err = { @@ -54,13 +54,32 @@ describe("error reporting", () => { name: "Error Name", }; - const metadata = { operationName: "foo" }; - const result = reportError(err, metadata); + const context = { anything: "foo" }; + const result = reportError(err, { context }); result.severe(); expect(Sentry.captureException).toHaveBeenCalledWith(err); result.warning(); expect(Sentry.captureException).toHaveBeenCalledWith(err); }); + + it("supports tags", () => { + mockEnv("NODE_ENV", "production"); + jest.spyOn(Sentry, "captureException").mockImplementation(jest.fn()); + jest.spyOn(Sentry, "setTags").mockImplementation(jest.fn()); + const err = { + message: "GraphQL Error", + name: "Error Name", + }; + + const tags = { spruce: "true" }; + const result = reportError(err, { tags }); + result.severe(); + expect(Sentry.captureException).toHaveBeenCalledWith(err); + expect(Sentry.setTags).toHaveBeenCalledWith(tags); + result.warning(); + expect(Sentry.captureException).toHaveBeenCalledWith(err); + expect(Sentry.setTags).toHaveBeenCalledWith(tags); + }); }); describe("breadcrumbs", () => { diff --git a/src/utils/errorReporting.ts b/src/utils/errorReporting.ts index f34dd56cf2..5e20e345d5 100644 --- a/src/utils/errorReporting.ts +++ b/src/utils/errorReporting.ts @@ -1,6 +1,6 @@ import { addBreadcrumb, Breadcrumb } from "@sentry/react"; import { - ErrorMetadata, + ErrorInput, sendError as sentrySendError, } from "components/ErrorHandling/Sentry"; import { isProductionBuild } from "./environmentVariables"; @@ -10,27 +10,45 @@ interface reportErrorResult { warning: () => void; } +type ErrorMetadata = { + fingerprint?: ErrorInput["fingerprint"]; + tags?: ErrorInput["tags"]; + context?: ErrorInput["context"]; +}; + const reportError = ( err: Error, - metadata?: ErrorMetadata, + { context, fingerprint, tags }: ErrorMetadata = {}, ): reportErrorResult => { if (!isProductionBuild()) { return { severe: () => { - console.error({ err, severity: "severe", metadata }); + console.error({ err, severity: "severe", context, fingerprint, tags }); }, warning: () => { - console.error({ err, severity: "warning", metadata }); + console.error({ err, severity: "warning", context, fingerprint, tags }); }, }; } return { severe: () => { - sentrySendError(err, "error", metadata); + sentrySendError({ + context, + err, + fingerprint, + severity: "error", + tags, + }); }, warning: () => { - sentrySendError(err, "warning", metadata); + sentrySendError({ + context, + err, + fingerprint, + severity: "warning", + tags, + }); }, }; };