-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DEVPROD-7857: Send Sentry alerts for 422 errors in Parsley (#473)
- Loading branch information
Showing
19 changed files
with
234 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,74 +1,13 @@ | ||
import { | ||
ApolloClient, | ||
ApolloProvider, | ||
HttpLink, | ||
InMemoryCache, | ||
from, | ||
} from "@apollo/client"; | ||
import { onError } from "@apollo/client/link/error"; | ||
import { RetryLink } from "@apollo/client/link/retry"; | ||
import { graphqlURL } from "utils/environmentVariables"; | ||
import { reportError } from "utils/errorReporting"; | ||
|
||
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), { | ||
context: { | ||
gqlErr, | ||
variables: operation.variables, | ||
}, | ||
fingerprint, | ||
tags: { operationName: operation.operationName }, | ||
}).warning(); | ||
}); | ||
import { ApolloProvider } from "@apollo/client"; | ||
import { FullPageLoad } from "@evg-ui/lib/components/FullPageLoad"; | ||
import { useCreateGQLClient } from "gql/client/useCreateGQLClient"; | ||
|
||
const GQLProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { | ||
const client = useCreateGQLClient(); | ||
if (!client) { | ||
return <FullPageLoad />; | ||
} | ||
}); | ||
|
||
const retryLink = new RetryLink({ | ||
attempts: { | ||
max: 5, | ||
retryIf: (error): boolean => | ||
error && error.response && error.response.status >= 500, | ||
}, | ||
delay: { | ||
initial: 300, | ||
jitter: true, | ||
max: 3000, | ||
}, | ||
}); | ||
|
||
interface ClientLinkParams { | ||
credentials?: string; | ||
gqlURL?: string; | ||
} | ||
|
||
const getGQLClient = ({ credentials, gqlURL }: ClientLinkParams) => { | ||
const cache = new InMemoryCache(); | ||
const link = new HttpLink({ | ||
credentials, | ||
uri: gqlURL, | ||
}); | ||
const client = new ApolloClient({ | ||
cache, | ||
link: from([logErrorsLink, retryLink, link]), | ||
}); | ||
return client; | ||
return <ApolloProvider client={client}>{children}</ApolloProvider>; | ||
}; | ||
|
||
const GQLProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => ( | ||
<ApolloProvider | ||
client={getGQLClient({ | ||
credentials: "include", | ||
gqlURL: graphqlURL, | ||
})} | ||
> | ||
{children} | ||
</ApolloProvider> | ||
); | ||
|
||
export default GQLProvider; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { RetryLink } from "@apollo/client/link/retry"; | ||
import { logGQLErrorsLink } from "./logGQLErrorsLink"; | ||
|
||
const retryLink = new RetryLink({ | ||
attempts: { | ||
max: 5, | ||
retryIf: (error): boolean => | ||
error && error.response && error.response.status >= 500, | ||
}, | ||
delay: { | ||
initial: 300, | ||
jitter: true, | ||
max: 3000, | ||
}, | ||
}); | ||
|
||
export { retryLink, logGQLErrorsLink }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Operation } from "@apollo/client"; | ||
import { GraphQLError } from "graphql"; | ||
import * as ErrorReporting from "utils/errorReporting"; | ||
import { reportingFn } from "./logGQLErrorsLink"; | ||
|
||
describe("reportingFn", () => { | ||
beforeEach(() => { | ||
vi.spyOn(ErrorReporting, "reportError"); | ||
}); | ||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
it("reportError should be called with secret fields redacted", () => { | ||
const secretFields = ["password", "creditCard"]; | ||
const operation: Operation = { | ||
extensions: {}, | ||
getContext: vi.fn(), | ||
operationName: "exampleOperation", | ||
// @ts-ignore-error: It's not necessary to run an actual query. | ||
query: null, | ||
setContext: vi.fn(), | ||
variables: { | ||
input: { creditCard: "1234567890123456", password: "password123" }, | ||
}, | ||
}; | ||
const gqlErr = new GraphQLError("An error occurred", { | ||
path: ["a", "path", "1"], | ||
}); | ||
|
||
reportingFn(secretFields, operation)(gqlErr); | ||
|
||
expect(ErrorReporting.reportError).toHaveBeenCalledTimes(1); | ||
expect(ErrorReporting.reportError).toHaveBeenCalledWith(expect.any(Error), { | ||
context: { | ||
gqlErr, | ||
variables: { input: { creditCard: "REDACTED", password: "REDACTED" } }, | ||
}, | ||
fingerprint: ["exampleOperation", "a", "path", "1"], | ||
tags: { operationName: "exampleOperation" }, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Operation } from "@apollo/client"; | ||
import { onError } from "@apollo/client/link/error"; | ||
import { ApolloServerErrorCode } from "@apollo/server/errors"; | ||
import { GraphQLError } from "graphql"; | ||
import { deleteNestedKey } from "@evg-ui/lib/utils/object"; | ||
import { reportError } from "utils/errorReporting"; | ||
|
||
export const reportingFn = | ||
(secretFields: string[], operation: Operation) => (gqlErr: GraphQLError) => { | ||
const fingerprint = [operation.operationName]; | ||
const path = gqlErr?.path?.map((v) => v.toString()); | ||
if (path) { | ||
fingerprint.push(...path); | ||
} | ||
|
||
const isValidationError = | ||
gqlErr.extensions?.code === | ||
ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED; | ||
|
||
const err = isValidationError | ||
? new GraphQLError( | ||
`GraphQL validation error in '${operation.operationName}': ${gqlErr.message}`, | ||
) | ||
: new Error( | ||
`Error occurred in '${operation.operationName}': ${gqlErr.message}`, | ||
); | ||
|
||
const sendError = reportError(err, { | ||
context: { | ||
gqlErr, | ||
variables: deleteNestedKey( | ||
operation.variables, | ||
secretFields, | ||
"REDACTED", | ||
), | ||
}, | ||
fingerprint, | ||
tags: { operationName: operation.operationName }, | ||
}); | ||
|
||
if (isValidationError) { | ||
sendError.severe(); | ||
} else { | ||
sendError.warning(); | ||
} | ||
}; | ||
|
||
export const logGQLErrorsLink = (secretFields: string[]) => | ||
onError(({ graphQLErrors, operation }) => | ||
graphQLErrors?.forEach(reportingFn(secretFields, operation)), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { useEffect, useState } from "react"; | ||
import { | ||
ApolloClient, | ||
HttpLink, | ||
InMemoryCache, | ||
NormalizedCacheObject, | ||
from, | ||
} from "@apollo/client"; | ||
import { | ||
fetchWithRetry, | ||
shouldLogoutAndRedirect, | ||
} from "@evg-ui/lib/utils/request"; | ||
import { useAuthContext } from "context/auth"; | ||
import { logGQLErrorsLink, retryLink } from "gql/client/link"; | ||
import { secretFieldsReq } from "gql/fetch"; | ||
import { SecretFieldsQuery } from "gql/generated/types"; | ||
import { graphqlURL, isDevelopmentBuild } from "utils/environmentVariables"; | ||
import { SentryBreadcrumb, leaveBreadcrumb } from "utils/errorReporting"; | ||
|
||
export const useCreateGQLClient = (): ApolloClient<NormalizedCacheObject> => { | ||
const { logoutAndRedirect } = useAuthContext(); | ||
const [gqlClient, setGQLClient] = useState<any>(); | ||
|
||
// SecretFields are not necessary for development builds because nothing is logged. | ||
const [secretFields, setSecretFields] = useState<string[] | undefined>( | ||
isDevelopmentBuild() ? [] : undefined, | ||
); | ||
|
||
useEffect(() => { | ||
fetchWithRetry<SecretFieldsQuery>(graphqlURL ?? "", secretFieldsReq) | ||
.then(({ data }) => { | ||
setSecretFields(data?.spruceConfig?.secretFields); | ||
}) | ||
.catch((err) => { | ||
leaveBreadcrumb( | ||
"SecretFields Query Error", | ||
{ | ||
err, | ||
}, | ||
SentryBreadcrumb.HTTP, | ||
); | ||
if (shouldLogoutAndRedirect(err?.cause?.statusCode)) { | ||
logoutAndRedirect(); | ||
} | ||
}); | ||
}, [logoutAndRedirect]); | ||
|
||
useEffect(() => { | ||
if (secretFields && !gqlClient) { | ||
const cache = new InMemoryCache(); | ||
const client = new ApolloClient({ | ||
cache, | ||
link: from([ | ||
logGQLErrorsLink(secretFields), | ||
retryLink, | ||
new HttpLink({ | ||
credentials: "include", | ||
uri: graphqlURL, | ||
}), | ||
]), | ||
}); | ||
setGQLClient(client); | ||
} | ||
}, [secretFields, gqlClient, logoutAndRedirect]); | ||
|
||
return gqlClient; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { OperationDefinitionNode } from "graphql"; | ||
import { SECRET_FIELDS } from "gql/queries"; | ||
|
||
export const secretFieldsReq: RequestInit = { | ||
body: JSON.stringify({ | ||
operationName: (SECRET_FIELDS.definitions[0] as OperationDefinitionNode) | ||
?.name?.value, | ||
query: SECRET_FIELDS.loc?.source.body, | ||
variables: {}, | ||
}), | ||
credentials: "include" as RequestCredentials, | ||
headers: { | ||
"content-type": "application/json", | ||
}, | ||
method: "POST", | ||
mode: "cors" as RequestMode, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
query SecretFields { | ||
spruceConfig { | ||
secretFields | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
Oops, something went wrong.