Skip to content

Commit

Permalink
First-pass sentry integration (#822)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Anthony Sennett authored Feb 17, 2022
1 parent 2ca4c23 commit 4a19046
Show file tree
Hide file tree
Showing 22 changed files with 438 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ INFURA_PROJECT_ID_BACKEND="de82b2d602264e4fbc0929dec0c45baa"
ETHERSCAN_API_KEY="34JVYM6RPM3J1SK8QXQFRNSHD9XG4UHXVU"
USE_TERMS_OF_SERVICE=1

NEXT_PUBLIC_SENTRY_ENV="development"
SENTRY_RELEASE="development"

MULTIPLY_PROXY_ACTIONS=0x2a49eae5cca3f050ebec729cf90cc910fadaf7a2
EXCHANGE=0xb5eB8cB6cED6b6f8E13bcD502fb489Db4a726C7B
DUMMY_EXCHANGE=0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ yalc.lock
.idea
.log
.DS_Store
.sentryclirc

public/precache.*.*.js
public/sw.js
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ ARG ETHERSCAN_API_KEY
ARG BLOCKNATIVE_API_KEY
ARG INFURA_PROJECT_ID
ARG NODE_ENV
ARG NEXT_PUBLIC_SENTRY_ENV
ARG SENTRY_AUTH_TOKEN

ENV COMMIT_SHA=$COMMIT_SHA \
API_HOST=$API_HOST \
Expand All @@ -34,7 +36,10 @@ ENV COMMIT_SHA=$COMMIT_SHA \
INFURA_PROJECT_ID=$INFURA_PROJECT_ID \
USE_TERMS_OF_SERVICE=1 \
SHOW_BUILD_INFO=$SHOW_BUILD_INFO \
NODE_ENV=$NODE_ENV
NODE_ENV=$NODE_ENV \
SENTRY_RELEASE=$COMMIT_SHA \
NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV \
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN

COPY . .

Expand Down
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
web: yarn start:prod
web: yarn start:prod
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ Some of the values that are used you can check in the `.env` file.
- `ETHERSCAN_API_KEY` - The value is used to create the corresponding etherscan endpoint. For each
transaction, there is a url that leads to that TX details in etherscan.

- `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.

- `SENTRY_AUTH_TOKEN` - auth token used by sentry.io to upload source maps.

As mentioned previously, there is also the custom express server part which uses the env variables
at _run time_

Expand All @@ -165,6 +169,10 @@ at _run time_

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

- `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.

- `NEXT_PUBLIC_SENTRY_ENV` - The environment that sentry events are tagged as. `production` | `staging` | `pullrequest` | `development`

_Note: Make sure that you call the process that build the project with the `build-time` vars and
make sure that you call the proces that runs the application with the `run-time` vars._

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import { of } from 'rxjs'
import { flatMap } from 'rxjs/operators'

import { useObservable } from '../../helpers/observableHook'

const streamThatErrors$ = of(1).pipe(flatMap(() => fetch('https://fetch-unhandled-url')))

export function TriggerErrorWithUseObservable() {
const value = useObservable(streamThatErrors$)
return <>TriggerErrorWithUseObservable {JSON.stringify(value)}</>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import { of } from 'rxjs'
import { flatMap } from 'rxjs/operators'

import { useObservableWithError } from '../../helpers/observableHook'
const streamThatErrors$ = of(1).pipe(flatMap(() => fetch('https://fetch-handled-url')))
export function TriggerErrorWithUseObservableWithError() {
const { value, error } = useObservableWithError(streamThatErrors$)
return (
<>
TriggerErrorWithUseObservableWithError. value: {JSON.stringify(value)} <br /> error:{' '}
{JSON.stringify(error)}
</>
)
}
11 changes: 9 additions & 2 deletions helpers/observableHook.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/nextjs'
import { useAppContext } from 'components/AppContextProvider'
import { useEffect, useReducer, useState } from 'react'
import { Observable } from 'rxjs'
Expand Down Expand Up @@ -31,7 +32,10 @@ export function useObservable<O extends Observable<any>>(o$: O): Unpack<O> | und
useEffect(() => {
const subscription = o$.subscribe(
(v: Unpack<O>) => setValue(v),
(error) => console.log('error', error),
(error) => {
console.log('error', error)
Sentry.captureException(error)
},
)
return () => subscription.unsubscribe()
}, [o$])
Expand All @@ -48,7 +52,10 @@ export function useObservableWithError<O extends Observable<any>>(
useEffect(() => {
const subscription = o$.subscribe(
(v: Unpack<O>) => setValue(v),
(e) => setError(e),
(e) => {
setError(e)
Sentry.captureException(e)
},
)
return () => subscription.unsubscribe()
}, [o$])
Expand Down
11 changes: 10 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
})
const MomentLocalesPlugin = require('moment-locales-webpack-plugin')
const { i18n } = require('./next-i18next.config')
const { withSentryConfig } = require('@sentry/nextjs')

const isProduction = process.env.NODE_ENV === 'production'
const basePath = ''

module.exports = withBundleAnalyzer(
const conf = withBundleAnalyzer(
withPWA(
withMDX(
withSass({
Expand Down Expand Up @@ -42,6 +43,7 @@ module.exports = withBundleAnalyzer(
showBuildInfo: process.env.SHOW_BUILD_INFO === '1',
infuraProjectId: process.env.INFURA_PROJECT_ID,
etherscanAPIKey: process.env.ETHERSCAN_API_KEY,
sentryRelease: process.env.SENTRY_RELEASE,
exchangeAddress:
process.env.USE_DUMMY === '1' ? process.env.DUMMY_EXCHANGE : process.env.EXCHANGE,
multiplyProxyActions: process.env.MULTIPLY_PROXY_ACTIONS,
Expand Down Expand Up @@ -115,3 +117,10 @@ module.exports = withBundleAnalyzer(
),
),
)

// sentry needs to be last for accurate sourcemaps
module.exports = withSentryConfig(conf, {
org: 'oazo-apps',
project: 'oazo-apps',
url: 'https://sentry.io/',
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@oasisdex/utils": "^0.0.8",
"@oasisdex/web3-context": "^0.1.0",
"@prisma/client": "^3.9.1",
"@sentry/nextjs": "^6.17.7",
"@storybook/storybook-deployer": "^2.8.10",
"@theme-ui/style-guide": "^0.3.5",
"@types/crypto-js": "^4.0.2",
Expand Down
59 changes: 59 additions & 0 deletions pages/_error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import NextErrorComponent from 'next/error'

import * as Sentry from '@sentry/nextjs'

const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
if (!hasGetInitialPropsRun && err) {
// getInitialProps is not called in case of
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
// err via _app.js so it can be captured
Sentry.captureException(err)
// Flushing is not required in this case as it only happens on the client
}

return <NextErrorComponent statusCode={statusCode} />
}

MyError.getInitialProps = async ({ res, err, asPath }) => {
const errorInitialProps = await NextErrorComponent.getInitialProps({
res,
err,
})

// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
errorInitialProps.hasGetInitialPropsRun = true

// Running on the server, the response object (`res`) is available.
//
// Next.js will pass an err on the server if a page's data fetching methods
// threw or returned a Promise that rejected
//
// Running on the client (browser), Next.js will provide an err if:
//
// - a page's `getInitialProps` threw or returned a Promise that rejected
// - an exception was thrown somewhere in the React lifecycle (render,
// componentDidMount, etc) that was caught by Next.js's React Error
// Boundary. Read more about what types of exceptions are caught by Error
// Boundaries: https://reactjs.org/docs/error-boundaries.html

if (err) {
Sentry.captureException(err)

// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await Sentry.flush(2000)

return errorInitialProps
}

// If this point is reached, getInitialProps was called without any
// information about what the error might be. This is unexpected and may
// indicate a bug introduced in Next.js, so record it in Sentry
Sentry.captureException(new Error(`_error.js getInitialProps missing data at path: ${asPath}`))
await Sentry.flush(2000)

return errorInitialProps
}

export default MyError
7 changes: 7 additions & 0 deletions pages/api/deliberateError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { withSentry } from '@sentry/nextjs'

const handler = async () => {
throw new Error('API throw error test')
}

export default withSentry(handler)
5 changes: 4 additions & 1 deletion pages/api/gasPrice.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { withSentry } from '@sentry/nextjs'
import axios from 'axios'
import { NextApiRequest, NextApiResponse } from 'next'

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

export default async function (_req: NextApiRequest, res: NextApiResponse) {
const handler = async function (_req: NextApiRequest, res: NextApiResponse) {
const time = cache.get('time')
if (!time) {
axios({
Expand Down Expand Up @@ -51,3 +52,5 @@ export default async function (_req: NextApiRequest, res: NextApiResponse) {
})
}
}

export default withSentry(handler)
5 changes: 4 additions & 1 deletion pages/api/health.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { withSentry } from '@sentry/nextjs'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function (_req: NextApiRequest, res: NextApiResponse) {
const handler = async function (_req: NextApiRequest, res: NextApiResponse) {
const response = { status: 200, message: 'Everything is okay!' }
res.status(200).json(response)
}

export default withSentry(handler)
5 changes: 4 additions & 1 deletion pages/api/newsletter-subscribe.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { withSentry } from '@sentry/nextjs'
import md5 from 'crypto-js/md5'
import { NextApiRequest, NextApiResponse } from 'next'

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

export default async function (req: NextApiRequest, res: NextApiResponse) {
const handler = async function (req: NextApiRequest, res: NextApiResponse) {
const { email } = req.body

try {
Expand Down Expand Up @@ -69,3 +70,5 @@ export default async function (req: NextApiRequest, res: NextApiResponse) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

export default withSentry(handler)
5 changes: 4 additions & 1 deletion pages/api/t.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { withSentry } from '@sentry/nextjs'
import { enableMixpanelDevelopmentMode, MixpanelDevelopmentType } from 'analytics/analytics'
import { config } from 'analytics/mixpanel'
import Mixpanel from 'mixpanel'
Expand All @@ -8,7 +9,7 @@ let mixpanel: MixpanelType = Mixpanel.init(config.mixpanel.token, config.mixpane

mixpanel = enableMixpanelDevelopmentMode(mixpanel)

export default async function (req: NextApiRequest, res: NextApiResponse<{ status: number }>) {
const handler = async function (req: NextApiRequest, res: NextApiResponse<{ status: number }>) {
try {
const { eventName, eventBody, distinctId } = req.body

Expand All @@ -23,3 +24,5 @@ export default async function (req: NextApiRequest, res: NextApiResponse<{ statu
res.json({ status: 500 })
}
}

export default withSentry(handler)
73 changes: 73 additions & 0 deletions pages/errors/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { MarketingLayout } from 'components/Layouts'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import React, { useState } from 'react'

import { TriggerErrorWithUseObservable } from '../../components/errorTriggeringComponents/TriggerErrorWithUseObservable'
import { TriggerErrorWithUseObservableWithError } from '../../components/errorTriggeringComponents/TriggerErrorWithUseObservableWithError'

export const getStaticProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
})

export default function ServerError() {
const [
showComponentThatErrorsWithUnhandledError,
setShowComponentThatErrorsWithUnhandledError,
] = useState(false)

const [
showComponentThatErrorsWithHandledError,
setShowComponentThatErrorsWithHandledError,
] = useState(false)
return (
<ul>
<li>
<button
type="button"
onClick={() => {
throw new Error('Sentry Frontend Error')
}}
>
Trigger error on client in this component
</button>
</li>
<li>
<button
type="button"
onClick={() => {
setShowComponentThatErrorsWithUnhandledError(
(showComponentThatTriggersUnhandledError) => !showComponentThatTriggersUnhandledError,
)
}}
>
Trigger handled error in observable with useObservable
</button>
{showComponentThatErrorsWithUnhandledError && <TriggerErrorWithUseObservable />}
</li>
<li>
<button
type="button"
onClick={() => {
setShowComponentThatErrorsWithHandledError(
(showComponentThatErrors) => !showComponentThatErrors,
)
}}
>
Trigger handled error in observable with useObservableWithError
</button>
{showComponentThatErrorsWithHandledError && <TriggerErrorWithUseObservableWithError />}
</li>
<li>
<a href="/errors/server-error">trigger error on page on server</a>
</li>
<li>
<a href="/api/deliberateError">trigger error on API endpoint</a>
</li>
</ul>
)
}

ServerError.layout = MarketingLayout
ServerError.theme = 'Landing'
18 changes: 18 additions & 0 deletions pages/errors/server-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MarketingLayout } from 'components/Layouts'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'

export const getStaticProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
})

export default function ServerError() {
throw new Error('test error from page')
}

ServerError.layout = MarketingLayout
ServerError.layoutProps = {
variant: 'termsContainer',
}
ServerError.theme = 'Landing'
Loading

0 comments on commit 4a19046

Please sign in to comment.