Skip to content

Commit 4a19046

Browse files
author
Anthony Sennett
authored
First-pass sentry integration (#822)
* add sentry * add sentry * linting * adding env to sentry * adding new envs vars to docker * PR branch name as release for sentry * let's try this * maybe this works * maybe this works * source maps * tidyup * tidyup * linting * handle sentry release in higher environments * removing release name form pr releases * removing debugging code * linting * document env varas in readme * formattign * fix typos
1 parent 2ca4c23 commit 4a19046

22 files changed

+438
-12
lines changed

.env

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ INFURA_PROJECT_ID_BACKEND="de82b2d602264e4fbc0929dec0c45baa"
1616
ETHERSCAN_API_KEY="34JVYM6RPM3J1SK8QXQFRNSHD9XG4UHXVU"
1717
USE_TERMS_OF_SERVICE=1
1818

19+
NEXT_PUBLIC_SENTRY_ENV="development"
20+
SENTRY_RELEASE="development"
21+
1922
MULTIPLY_PROXY_ACTIONS=0x2a49eae5cca3f050ebec729cf90cc910fadaf7a2
2023
EXCHANGE=0xb5eB8cB6cED6b6f8E13bcD502fb489Db4a726C7B
2124
DUMMY_EXCHANGE=0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ yalc.lock
1111
.idea
1212
.log
1313
.DS_Store
14+
.sentryclirc
1415

1516
public/precache.*.*.js
1617
public/sw.js

Dockerfile

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ ARG ETHERSCAN_API_KEY
2222
ARG BLOCKNATIVE_API_KEY
2323
ARG INFURA_PROJECT_ID
2424
ARG NODE_ENV
25+
ARG NEXT_PUBLIC_SENTRY_ENV
26+
ARG SENTRY_AUTH_TOKEN
2527

2628
ENV COMMIT_SHA=$COMMIT_SHA \
2729
API_HOST=$API_HOST \
@@ -34,7 +36,10 @@ ENV COMMIT_SHA=$COMMIT_SHA \
3436
INFURA_PROJECT_ID=$INFURA_PROJECT_ID \
3537
USE_TERMS_OF_SERVICE=1 \
3638
SHOW_BUILD_INFO=$SHOW_BUILD_INFO \
37-
NODE_ENV=$NODE_ENV
39+
NODE_ENV=$NODE_ENV \
40+
SENTRY_RELEASE=$COMMIT_SHA \
41+
NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV \
42+
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
3843

3944
COPY . .
4045

Procfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web: yarn start:prod
1+
web: yarn start:prod

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ Some of the values that are used you can check in the `.env` file.
147147
- `ETHERSCAN_API_KEY` - The value is used to create the corresponding etherscan endpoint. For each
148148
transaction, there is a url that leads to that TX details in etherscan.
149149

150+
- `SENTRY_RELEASE` - The release in sentry.io. Used by sentry.io to generate and upload source maps for a given release at build time, and tie those source maps to errors sent to sentry at run time.
151+
152+
- `SENTRY_AUTH_TOKEN` - auth token used by sentry.io to upload source maps.
153+
150154
As mentioned previously, there is also the custom express server part which uses the env variables
151155
at _run time_
152156

@@ -165,6 +169,10 @@ at _run time_
165169

166170
- `MAILCHIMP_API_KEY` - Mailchimp API Key used to integrate Mailchimp newsletter.
167171

172+
- `SENTRY_RELEASE` - The release in sentry.io. Used by sentry.io to generate and upload source maps for a given release at build time, and tie those source maps to errors sent to sentry at run time.
173+
174+
- `NEXT_PUBLIC_SENTRY_ENV` - The environment that sentry events are tagged as. `production` | `staging` | `pullrequest` | `development`
175+
168176
_Note: Make sure that you call the process that build the project with the `build-time` vars and
169177
make sure that you call the proces that runs the application with the `run-time` vars._
170178

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react'
2+
import { of } from 'rxjs'
3+
import { flatMap } from 'rxjs/operators'
4+
5+
import { useObservable } from '../../helpers/observableHook'
6+
7+
const streamThatErrors$ = of(1).pipe(flatMap(() => fetch('https://fetch-unhandled-url')))
8+
9+
export function TriggerErrorWithUseObservable() {
10+
const value = useObservable(streamThatErrors$)
11+
return <>TriggerErrorWithUseObservable {JSON.stringify(value)}</>
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react'
2+
import { of } from 'rxjs'
3+
import { flatMap } from 'rxjs/operators'
4+
5+
import { useObservableWithError } from '../../helpers/observableHook'
6+
const streamThatErrors$ = of(1).pipe(flatMap(() => fetch('https://fetch-handled-url')))
7+
export function TriggerErrorWithUseObservableWithError() {
8+
const { value, error } = useObservableWithError(streamThatErrors$)
9+
return (
10+
<>
11+
TriggerErrorWithUseObservableWithError. value: {JSON.stringify(value)} <br /> error:{' '}
12+
{JSON.stringify(error)}
13+
</>
14+
)
15+
}

helpers/observableHook.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Sentry from '@sentry/nextjs'
12
import { useAppContext } from 'components/AppContextProvider'
23
import { useEffect, useReducer, useState } from 'react'
34
import { Observable } from 'rxjs'
@@ -31,7 +32,10 @@ export function useObservable<O extends Observable<any>>(o$: O): Unpack<O> | und
3132
useEffect(() => {
3233
const subscription = o$.subscribe(
3334
(v: Unpack<O>) => setValue(v),
34-
(error) => console.log('error', error),
35+
(error) => {
36+
console.log('error', error)
37+
Sentry.captureException(error)
38+
},
3539
)
3640
return () => subscription.unsubscribe()
3741
}, [o$])
@@ -48,7 +52,10 @@ export function useObservableWithError<O extends Observable<any>>(
4852
useEffect(() => {
4953
const subscription = o$.subscribe(
5054
(v: Unpack<O>) => setValue(v),
51-
(e) => setError(e),
55+
(e) => {
56+
setError(e)
57+
Sentry.captureException(e)
58+
},
5259
)
5360
return () => subscription.unsubscribe()
5461
}, [o$])

next.config.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
88
})
99
const MomentLocalesPlugin = require('moment-locales-webpack-plugin')
1010
const { i18n } = require('./next-i18next.config')
11+
const { withSentryConfig } = require('@sentry/nextjs')
1112

1213
const isProduction = process.env.NODE_ENV === 'production'
1314
const basePath = ''
1415

15-
module.exports = withBundleAnalyzer(
16+
const conf = withBundleAnalyzer(
1617
withPWA(
1718
withMDX(
1819
withSass({
@@ -42,6 +43,7 @@ module.exports = withBundleAnalyzer(
4243
showBuildInfo: process.env.SHOW_BUILD_INFO === '1',
4344
infuraProjectId: process.env.INFURA_PROJECT_ID,
4445
etherscanAPIKey: process.env.ETHERSCAN_API_KEY,
46+
sentryRelease: process.env.SENTRY_RELEASE,
4547
exchangeAddress:
4648
process.env.USE_DUMMY === '1' ? process.env.DUMMY_EXCHANGE : process.env.EXCHANGE,
4749
multiplyProxyActions: process.env.MULTIPLY_PROXY_ACTIONS,
@@ -115,3 +117,10 @@ module.exports = withBundleAnalyzer(
115117
),
116118
),
117119
)
120+
121+
// sentry needs to be last for accurate sourcemaps
122+
module.exports = withSentryConfig(conf, {
123+
org: 'oazo-apps',
124+
project: 'oazo-apps',
125+
url: 'https://sentry.io/',
126+
})

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@oasisdex/utils": "^0.0.8",
5353
"@oasisdex/web3-context": "^0.1.0",
5454
"@prisma/client": "^3.9.1",
55+
"@sentry/nextjs": "^6.17.7",
5556
"@storybook/storybook-deployer": "^2.8.10",
5657
"@theme-ui/style-guide": "^0.3.5",
5758
"@types/crypto-js": "^4.0.2",

pages/_error.jsx

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import NextErrorComponent from 'next/error'
2+
3+
import * as Sentry from '@sentry/nextjs'
4+
5+
const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
6+
if (!hasGetInitialPropsRun && err) {
7+
// getInitialProps is not called in case of
8+
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
9+
// err via _app.js so it can be captured
10+
Sentry.captureException(err)
11+
// Flushing is not required in this case as it only happens on the client
12+
}
13+
14+
return <NextErrorComponent statusCode={statusCode} />
15+
}
16+
17+
MyError.getInitialProps = async ({ res, err, asPath }) => {
18+
const errorInitialProps = await NextErrorComponent.getInitialProps({
19+
res,
20+
err,
21+
})
22+
23+
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
24+
// getInitialProps has run
25+
errorInitialProps.hasGetInitialPropsRun = true
26+
27+
// Running on the server, the response object (`res`) is available.
28+
//
29+
// Next.js will pass an err on the server if a page's data fetching methods
30+
// threw or returned a Promise that rejected
31+
//
32+
// Running on the client (browser), Next.js will provide an err if:
33+
//
34+
// - a page's `getInitialProps` threw or returned a Promise that rejected
35+
// - an exception was thrown somewhere in the React lifecycle (render,
36+
// componentDidMount, etc) that was caught by Next.js's React Error
37+
// Boundary. Read more about what types of exceptions are caught by Error
38+
// Boundaries: https://reactjs.org/docs/error-boundaries.html
39+
40+
if (err) {
41+
Sentry.captureException(err)
42+
43+
// Flushing before returning is necessary if deploying to Vercel, see
44+
// https://vercel.com/docs/platform/limits#streaming-responses
45+
await Sentry.flush(2000)
46+
47+
return errorInitialProps
48+
}
49+
50+
// If this point is reached, getInitialProps was called without any
51+
// information about what the error might be. This is unexpected and may
52+
// indicate a bug introduced in Next.js, so record it in Sentry
53+
Sentry.captureException(new Error(`_error.js getInitialProps missing data at path: ${asPath}`))
54+
await Sentry.flush(2000)
55+
56+
return errorInitialProps
57+
}
58+
59+
export default MyError

pages/api/deliberateError.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { withSentry } from '@sentry/nextjs'
2+
3+
const handler = async () => {
4+
throw new Error('API throw error test')
5+
}
6+
7+
export default withSentry(handler)

pages/api/gasPrice.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { withSentry } from '@sentry/nextjs'
12
import axios from 'axios'
23
import { NextApiRequest, NextApiResponse } from 'next'
34

45
const NodeCache = require('node-cache')
56
const cache = new NodeCache({ stdTTL: 9 })
67

7-
export default async function (_req: NextApiRequest, res: NextApiResponse) {
8+
const handler = async function (_req: NextApiRequest, res: NextApiResponse) {
89
const time = cache.get('time')
910
if (!time) {
1011
axios({
@@ -51,3 +52,5 @@ export default async function (_req: NextApiRequest, res: NextApiResponse) {
5152
})
5253
}
5354
}
55+
56+
export default withSentry(handler)

pages/api/health.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { withSentry } from '@sentry/nextjs'
12
import { NextApiRequest, NextApiResponse } from 'next'
23

3-
export default async function (_req: NextApiRequest, res: NextApiResponse) {
4+
const handler = async function (_req: NextApiRequest, res: NextApiResponse) {
45
const response = { status: 200, message: 'Everything is okay!' }
56
res.status(200).json(response)
67
}
8+
9+
export default withSentry(handler)

pages/api/newsletter-subscribe.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { withSentry } from '@sentry/nextjs'
12
import md5 from 'crypto-js/md5'
23
import { NextApiRequest, NextApiResponse } from 'next'
34

@@ -13,7 +14,7 @@ type UserStatus = 'pending' | 'subscribed' | 'unsubscribed' | 'cleaned' | 'trans
1314
// change to subscribed if there is no need for opt-in
1415
const INITIAL_USER_STATUS: UserStatus = 'subscribed'
1516

16-
export default async function (req: NextApiRequest, res: NextApiResponse) {
17+
const handler = async function (req: NextApiRequest, res: NextApiResponse) {
1718
const { email } = req.body
1819

1920
try {
@@ -69,3 +70,5 @@ export default async function (req: NextApiRequest, res: NextApiResponse) {
6970
return res.status(500).json({ error: error.message || error.toString() })
7071
}
7172
}
73+
74+
export default withSentry(handler)

pages/api/t.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { withSentry } from '@sentry/nextjs'
12
import { enableMixpanelDevelopmentMode, MixpanelDevelopmentType } from 'analytics/analytics'
23
import { config } from 'analytics/mixpanel'
34
import Mixpanel from 'mixpanel'
@@ -8,7 +9,7 @@ let mixpanel: MixpanelType = Mixpanel.init(config.mixpanel.token, config.mixpane
89

910
mixpanel = enableMixpanelDevelopmentMode(mixpanel)
1011

11-
export default async function (req: NextApiRequest, res: NextApiResponse<{ status: number }>) {
12+
const handler = async function (req: NextApiRequest, res: NextApiResponse<{ status: number }>) {
1213
try {
1314
const { eventName, eventBody, distinctId } = req.body
1415

@@ -23,3 +24,5 @@ export default async function (req: NextApiRequest, res: NextApiResponse<{ statu
2324
res.json({ status: 500 })
2425
}
2526
}
27+
28+
export default withSentry(handler)

pages/errors/index.tsx

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { MarketingLayout } from 'components/Layouts'
2+
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
3+
import React, { useState } from 'react'
4+
5+
import { TriggerErrorWithUseObservable } from '../../components/errorTriggeringComponents/TriggerErrorWithUseObservable'
6+
import { TriggerErrorWithUseObservableWithError } from '../../components/errorTriggeringComponents/TriggerErrorWithUseObservableWithError'
7+
8+
export const getStaticProps = async ({ locale }: { locale: string }) => ({
9+
props: {
10+
...(await serverSideTranslations(locale, ['common'])),
11+
},
12+
})
13+
14+
export default function ServerError() {
15+
const [
16+
showComponentThatErrorsWithUnhandledError,
17+
setShowComponentThatErrorsWithUnhandledError,
18+
] = useState(false)
19+
20+
const [
21+
showComponentThatErrorsWithHandledError,
22+
setShowComponentThatErrorsWithHandledError,
23+
] = useState(false)
24+
return (
25+
<ul>
26+
<li>
27+
<button
28+
type="button"
29+
onClick={() => {
30+
throw new Error('Sentry Frontend Error')
31+
}}
32+
>
33+
Trigger error on client in this component
34+
</button>
35+
</li>
36+
<li>
37+
<button
38+
type="button"
39+
onClick={() => {
40+
setShowComponentThatErrorsWithUnhandledError(
41+
(showComponentThatTriggersUnhandledError) => !showComponentThatTriggersUnhandledError,
42+
)
43+
}}
44+
>
45+
Trigger handled error in observable with useObservable
46+
</button>
47+
{showComponentThatErrorsWithUnhandledError && <TriggerErrorWithUseObservable />}
48+
</li>
49+
<li>
50+
<button
51+
type="button"
52+
onClick={() => {
53+
setShowComponentThatErrorsWithHandledError(
54+
(showComponentThatErrors) => !showComponentThatErrors,
55+
)
56+
}}
57+
>
58+
Trigger handled error in observable with useObservableWithError
59+
</button>
60+
{showComponentThatErrorsWithHandledError && <TriggerErrorWithUseObservableWithError />}
61+
</li>
62+
<li>
63+
<a href="/errors/server-error">trigger error on page on server</a>
64+
</li>
65+
<li>
66+
<a href="/api/deliberateError">trigger error on API endpoint</a>
67+
</li>
68+
</ul>
69+
)
70+
}
71+
72+
ServerError.layout = MarketingLayout
73+
ServerError.theme = 'Landing'

pages/errors/server-error.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MarketingLayout } from 'components/Layouts'
2+
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
3+
4+
export const getStaticProps = async ({ locale }: { locale: string }) => ({
5+
props: {
6+
...(await serverSideTranslations(locale, ['common'])),
7+
},
8+
})
9+
10+
export default function ServerError() {
11+
throw new Error('test error from page')
12+
}
13+
14+
ServerError.layout = MarketingLayout
15+
ServerError.layoutProps = {
16+
variant: 'termsContainer',
17+
}
18+
ServerError.theme = 'Landing'

0 commit comments

Comments
 (0)