diff --git a/app/api/package.json b/app/api/package.json index 2925f9e..4028911 100644 --- a/app/api/package.json +++ b/app/api/package.json @@ -5,6 +5,7 @@ "dependencies": { "@redwoodjs/api": "^0.33.0", "@redwoodjs/api-server": "^0.33.0", + "@sentry/node": "^6.5.1", "cloudinary": "^1.23.0" } -} \ No newline at end of file +} diff --git a/app/api/src/functions/graphql.js b/app/api/src/functions/graphql.js index 57ba262..45ae461 100644 --- a/app/api/src/functions/graphql.js +++ b/app/api/src/functions/graphql.js @@ -3,6 +3,7 @@ import { makeMergedSchema, makeServices, } from '@redwoodjs/api' +import { createSentryApolloPlugin } from 'src/lib/sentry' import schemas from 'src/graphql/**/*.{js,ts}' import services from 'src/services/**/*.{js,ts}' @@ -16,6 +17,9 @@ export const handler = createGraphQLHandler({ schemas, services: makeServices({ services }), }), + plugins: [ + createSentryApolloPlugin(), + ], onException: () => { // Disconnect from your database with an unhandled exception. db.$disconnect() diff --git a/app/api/src/functions/identity-signup.js b/app/api/src/functions/identity-signup.js index 7d772d0..192f49d 100644 --- a/app/api/src/functions/identity-signup.js +++ b/app/api/src/functions/identity-signup.js @@ -1,8 +1,9 @@ import { createUserInsecure } from 'src/services/users/users.js' import { db } from 'src/lib/db' +import { sentryWrapper } from 'src/lib/sentry' import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers' -export const handler = async (req, _context) => { +const unWrappedHandler = async (req, _context) => { const body = JSON.parse(req.body) console.log(body) console.log(_context) @@ -82,3 +83,5 @@ export const handler = async (req, _context) => { } } } + +export const handler = sentryWrapper(unWrappedHandler) diff --git a/app/api/src/lib/sentry.ts b/app/api/src/lib/sentry.ts new file mode 100644 index 0000000..34c583a --- /dev/null +++ b/app/api/src/lib/sentry.ts @@ -0,0 +1,106 @@ +import * as Sentry from '@sentry/node' +import { context, Config, ApolloError } from '@redwoodjs/api' + +let sentryInitialized = false +if (process.env.SENTRY_DSN && !sentryInitialized) { + Sentry.init({ + dsn: process.env.SENTRY_DSN, + environment: process.env.CONTEXT, + release: process.env.COMMIT_REF, + }) + sentryInitialized = true +} + +async function reportError(error) { + if (!sentryInitialized) return + // If you do have authentication set up, we can add + // some user data to help debug issues + // if (context.currentUser) { + // Sentry.configureScope((scope) => { + // scope.setUser({ + // id: context?.currentUser?.id, + // email: context?.currentUser?.email, + // }) + // }) + // } + if (typeof error === 'string') { + Sentry.captureMessage(error) + } else { + Sentry.captureException(error) + } + await Sentry.flush() +} + +export const sentryWrapper = (handler) => async (event, lambdaContext) => { + lambdaContext.callbackWaitsForEmptyEventLoop = false + try { + return await new Promise((resolve, reject) => { + const callback = (err, result) => { + if (err) { + reject(err) + } else { + resolve(result) + } + } + const resp = handler(event, lambdaContext, callback) + if (resp?.then) { + resp.then(resolve, reject) + } + }) + } catch (e) { + // This catches both sync errors & promise + // rejections, because we 'await' on the handler + await reportError(e) + throw e + } +} + +export const createSentryApolloPlugin: Config['plugins'][number] = () => ({ + requestDidStart: () => { + return { + didEncounterErrors(ctx) { + // If we couldn't parse the operation, don't + // do anything here + if (!ctx.operation) { + return; + } + + for (const err of ctx.errors) { + // Only report internal server errors, + // all errors extending ApolloError should be user-facing + if (err instanceof ApolloError) { + continue; + } + + // Add scoped report details and send to Sentry + Sentry.withScope(scope => { + // Annotate whether failing operation was query/mutation/subscription + scope.setTag("kind", ctx.operation.operation); + + // Log query and variables as extras (make sure to strip out sensitive data!) + scope.setExtra("query", ctx.request.query); + scope.setExtra("variables", ctx.request.variables); + + if (err.path) { + // We can also add the path as breadcrumb + scope.addBreadcrumb({ + category: "query-path", + message: err.path.join(" > "), + level: Sentry.Severity.Debug + }); + } + + const transactionId = ctx.request.http.headers.get( + "x-transaction-id" + ); + if (transactionId) { + scope.setTransaction(transactionId); + } + + Sentry.captureException(err); + }); + } + } + } + } +}) diff --git a/app/redwood.toml b/app/redwood.toml index b7944e7..03bf1fa 100644 --- a/app/redwood.toml +++ b/app/redwood.toml @@ -8,7 +8,16 @@ [web] port = 8910 apiProxyPath = "/.netlify/functions" - includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET', 'CAD_LAMBDA_BASE_URL'] + includeEnvironmentVariables = [ + 'GOOGLE_ANALYTICS_ID', + 'CLOUDINARY_API_KEY', + 'CLOUDINARY_API_SECRET', + 'CAD_LAMBDA_BASE_URL', + 'SENTRY_DSN', + 'SENTRY_AUTH_TOKEN', + 'SENTRY_ORG', + 'SENTRY_PROJECT' + ] # experimentalFastRefresh = true # this seems to break cascadeStudio [api] port = 8911 @@ -18,3 +27,10 @@ [experimental] esbuild = false + +[[plugins]] + package = "@sentry/netlify-build-plugin" + + [plugins.inputs] + sentryOrg = "cadhub-org" + sentryProject = "cadhub" diff --git a/app/web/package.json b/app/web/package.json index b0db7bd..92b9ba2 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -20,6 +20,7 @@ "@redwoodjs/forms": "^0.33.0", "@redwoodjs/router": "^0.33.0", "@redwoodjs/web": "^0.33.0", + "@sentry/browser": "^6.5.1", "browser-fs-access": "^0.17.2", "cloudinary-react": "^1.6.7", "controlkit": "^0.1.9", @@ -56,4 +57,4 @@ "tailwindcss": "^2.1.2", "worker-loader": "^3.0.7" } -} \ No newline at end of file +} diff --git a/app/web/src/components/FatalErrorBoundary/FatalErrorBoundary.tsx b/app/web/src/components/FatalErrorBoundary/FatalErrorBoundary.tsx new file mode 100644 index 0000000..6004437 --- /dev/null +++ b/app/web/src/components/FatalErrorBoundary/FatalErrorBoundary.tsx @@ -0,0 +1,12 @@ +import { FatalErrorBoundary as FatalErrorBoundaryBase } from '@redwoodjs/web' +import * as Sentry from '@sentry/browser' + +class FatalErrorBoundary extends FatalErrorBoundaryBase { + componentDidCatch(error, errorInfo) { + Sentry.withScope((scope) => { + scope.setExtras(errorInfo) + Sentry.captureException(error) + }) + } +} +export default FatalErrorBoundary diff --git a/app/yarn.lock b/app/yarn.lock index 19dd750..46bdf77 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2985,6 +2985,84 @@ dependencies: any-observable "^0.3.0" +"@sentry/browser@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.5.1.tgz#9a6ed5607b3b0f4e83f38720e3e202906f8c5bdb" + integrity sha512-iVLCdEFwsoWAzE/hNknexPQjjDpMQV7mmaq9Z1P63bD6MfhwVTx4hG4pHn8HEvC38VvCVf1wv0v/LxtoODAYXg== + dependencies: + "@sentry/core" "6.5.1" + "@sentry/types" "6.5.1" + "@sentry/utils" "6.5.1" + tslib "^1.9.3" + +"@sentry/core@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.5.1.tgz#c8b6c3ed86ed07b193c95d599c1b9a4a161e500e" + integrity sha512-Mh3sl/iUOT1myHmM6RlDy2ARzkUClx/g4DAt1rJ/IpQBOlDYQraplXSIW80i/hzRgQDfwhwgf4wUa5DicKBjKw== + dependencies: + "@sentry/hub" "6.5.1" + "@sentry/minimal" "6.5.1" + "@sentry/types" "6.5.1" + "@sentry/utils" "6.5.1" + tslib "^1.9.3" + +"@sentry/hub@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.5.1.tgz#135ef09d07d32e87a53f664c0ae8fcc4f5963519" + integrity sha512-lBRMBVMYP8B4PfRiM70murbtJAXiIAao/asDEMIRNGMP6pI2ArqXfJCBYDkStukhikYD0Kqb4trXq+JYF07Hbg== + dependencies: + "@sentry/types" "6.5.1" + "@sentry/utils" "6.5.1" + tslib "^1.9.3" + +"@sentry/minimal@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.5.1.tgz#b8c1b382c2ea788eec3d32d203e5081b00eb6838" + integrity sha512-q9Do/oreu1RP695CXCLowVDuQyk7ilE6FGdz2QLpTXAfx8247qOwk6+zy9Kea/Djk93+BoSDVQUSneNiVwl0nQ== + dependencies: + "@sentry/hub" "6.5.1" + "@sentry/types" "6.5.1" + tslib "^1.9.3" + +"@sentry/node@^6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.5.1.tgz#a572b380858de5aeaf98eade6d8d3afcba13d364" + integrity sha512-Yh8J/QJ5e8gRBVL9VLCDpUvmiaxsxVZm0CInPHw3V/smgMkrzSKEiqxSeMq8ImPlaJrCFECqdpv4gnvYKI+mQQ== + dependencies: + "@sentry/core" "6.5.1" + "@sentry/hub" "6.5.1" + "@sentry/tracing" "6.5.1" + "@sentry/types" "6.5.1" + "@sentry/utils" "6.5.1" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/tracing@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.5.1.tgz#a5f3e497d4f1f319f36475df050e135cf65af750" + integrity sha512-y1W/xFC2hAuKqSuuaovkElHY4pbli3XoXrreesg8PtO7ilX6ZbatOQbHsEsHQyoUv0F6aVA+MABOxWH2jt7tfw== + dependencies: + "@sentry/hub" "6.5.1" + "@sentry/minimal" "6.5.1" + "@sentry/types" "6.5.1" + "@sentry/utils" "6.5.1" + tslib "^1.9.3" + +"@sentry/types@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.5.1.tgz#0a34ecfd1ae9275a416a105640eb4bed45a46a1d" + integrity sha512-b/7a6CMoytaeFPx4IBjfxPw3nPvsQh7ui1C8Vw0LxNNDgBwVhPLzUOWeLWbo5YZCVbGEMIWwtCUQYWxneceZSA== + +"@sentry/utils@6.5.1": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.5.1.tgz#046baf7d1a6564d6d555437ad3674dba9bc0806a" + integrity sha512-Wv86JYGQH+ZJ5XGFQX7h6ijl32667ikenoL9EyXMn8UoOYX/MLwZoQZin1P60wmKkYR9ifTNVmpaI9OoTaH+UQ== + dependencies: + "@sentry/types" "6.5.1" + tslib "^1.9.3" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -12660,6 +12738,11 @@ lru-memoizer@^2.1.2: lodash.clonedeep "^4.5.0" lru-cache "~4.0.0" +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"