Add Sentry config and ErrorBoundary Component (#1528)

* Add Sentry config and ErrorBoundary Component

* Update travis file to upload sentry sourcemaps

* Add design implementation for ErrorBoundary

* Add Sentry DSN configuration for all networks

* Push sourcemaps to sentry only on staging/production build

* move isProdction to constants file

* change Button for Link

* fix redirect when safeAddress is provided but not a subpath for it.

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
nicolas 2020-10-30 16:55:40 -03:00 committed by GitHub
parent 503b99973e
commit df42b36194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 266 additions and 29 deletions

View File

@ -6,6 +6,7 @@ REACT_APP_GOOGLE_ANALYTICS=
REACT_APP_INFURA_TOKEN=
REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs
PUBLIC_URL=/app/
REACT_APP_SENTRY_DSN=
# For production environments
REACT_APP_BLOCKNATIVE_KEY=

View File

@ -10,27 +10,38 @@ matrix:
include:
- env:
- REACT_APP_NETWORK='mainnet'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_MAINNET}
- SENTRY_PROJECT=${SENTRY_PROJECT_MAINNET}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='rinkeby'
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_RINKEBY}
- SENTRY_PROJECT=${SENTRY_PROJECT_RINKEBY}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
- env:
- REACT_APP_NETWORK='xdai'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
- STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME}
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_XDAI}
- SENTRY_PROJECT=${SENTRY_PROJECT_XDAI}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='volta'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
- STAGING_BUCKET_NAME=${STAGING_VOLTA_BUCKET_NAME}
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA}
- SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='energy_web_chain'
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
- STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_EWC}
- SENTRY_PROJECT=${SENTRY_PROJECT_EWC}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
if: ((branch = master OR branch = release/v2.14.0) AND NOT type = pull_request) OR tag IS present
cache:
@ -48,7 +59,12 @@ script:
- yarn prettier:check
- yarn test:coverage
- yarn build
#- bash ./config/travis/build.sh
- if [[ $TRAVIS_BRANCH == "master" && $TRAVIS_PULL_REQUEST == "false" ]] || [ -n "$TRAVIS_TAG" ]; then
echo "Upload sentry source maps"
yarn sentry-upload-sourcemaps;
else
echo "Skip source map upload";
fi;
after_success:
# Pull Request - Deploy it to a review environment
# Travis doesn't do deploy step with pull requests builds

View File

@ -1,10 +0,0 @@
#!/bin/bash
export NODE_ENV=production;
if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi
yarn lint:check
yarn prettier:check
yarn test:coverage
yarn build

View File

@ -42,7 +42,8 @@
"test:coverage": "yarn test --coverage --watchAll=false",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public"
"build-storybook": "build-storybook -s public",
"sentry-upload-sourcemaps": "sentry-cli --auth-token $SENTRY_AUTH_TOKEN releases -o $SENTRY_ORG -p $SENTRY_PROJECT files $npm_package_version upload-sourcemaps ./build/static/js/"
},
"husky": {
"hooks": {
@ -167,13 +168,15 @@
"dependencies": {
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#3f0689f",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#8d8508e",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.26.0",
"@material-ui/core": "4.11.0",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.56",
"@openzeppelin/contracts": "3.1.0",
"@sentry/react": "^5.27.1",
"@sentry/tracing": "^5.27.1",
"@truffle/contract": "4.2.26",
"async-sema": "^3.1.0",
"axios": "0.20.0",
@ -232,6 +235,7 @@
"web3-utils": "^1.2.11"
},
"devDependencies": {
"@sentry/cli": "^1.58.0",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
"@storybook/addons": "^5.3.19",

View File

@ -0,0 +1,103 @@
import React from 'react'
import styled from 'styled-components'
import { Text, Link, Icon, FixedIcon, Title } from '@gnosis.pm/safe-react-components'
import { IS_PRODUCTION } from 'src/utils/constants'
const Wrapper = styled.div`
width: 100%;
margin-top: 50px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
`
const Content = styled.div`
width: 400px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
> * {
margin-top: 10px;
}
`
const LinkWrapper = styled.div`
display: inline-flex;
margin-bottom: 10px;
> :first-of-type {
margin-right: 5px;
}
`
const LinkContent = styled.div`
display: flex;
align-items: center;
> span {
margin-right: 5px;
}
`
type Props = {
error: Error
componentStack: string
resetError: () => void
}
const GlobalErrorBoundaryFallback = ({ error, componentStack }: Props): React.ReactElement => {
return (
<Wrapper>
<Content>
<Title size="md">Something went wrong, please try again.</Title>
<FixedIcon type="networkError" />
{IS_PRODUCTION && (
<div>
<Text size="xl" as="span">
In case the problem persists, please reach out to us via{' '}
</Text>
<LinkWrapper>
<a target="_blank" href="email: mailto:safe@gnosis.io" rel="noopener noreferrer">
<Text color="primary" size="lg" as="span">
Email
</Text>
</a>
<Icon type="externalLink" color="primary" size="sm" />
</LinkWrapper>
or{' '}
<LinkWrapper>
<a target="_blank" href="https://discordapp.com/invite/FPMRAwK" rel="noopener noreferrer">
<Text color="primary" size="lg" as="span">
Discord
</Text>
</a>
<Icon type="externalLink" color="primary" size="sm" />
</LinkWrapper>
</div>
)}
{!IS_PRODUCTION && (
<>
<Text size="xl" color="error">
{error.toString()}
</Text>
<Text size="md" color="error">
{componentStack}
</Text>
</>
)}
<Link size="lg" color="primary" href="/app/">
<LinkContent>
<Icon size="md" type="home" color="primary" />
Go to Home
</LinkContent>
</Link>
</Content>
</Wrapper>
)
}
export default GlobalErrorBoundaryFallback

View File

@ -4,10 +4,11 @@ import { ConnectedRouter } from 'connected-react-router'
import React from 'react'
import { Provider } from 'react-redux'
import { ThemeProvider } from 'styled-components'
import * as Sentry from '@sentry/react'
import Loader from 'src/components/Loader'
import App from 'src/components/App'
import GlobalErrorBoundary from 'src/components/GlobalErrorBoundary'
import AppRoutes from 'src/routes'
import { history, store } from 'src/store'
import theme from 'src/theme/mui'
@ -20,7 +21,11 @@ const Root = (): React.ReactElement => (
<ThemeProvider theme={styledTheme}>
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ConnectedRouter history={history}>{<App>{wrapInSuspense(<AppRoutes />, <Loader />)}</App>}</ConnectedRouter>
<ConnectedRouter history={history}>
<Sentry.ErrorBoundary fallback={GlobalErrorBoundary}>
<App>{wrapInSuspense(<AppRoutes />, <Loader />)}</App>
</Sentry.ErrorBoundary>
</ConnectedRouter>
</MuiThemeProvider>
</Provider>
</ThemeProvider>

View File

@ -1,6 +1,8 @@
import { BigNumber } from 'bignumber.js'
import React from 'react'
import ReactDOM from 'react-dom'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
import Root from 'src/components/Root'
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
@ -8,6 +10,7 @@ import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store'
import { SENTRY_DSN } from './utils/constants'
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
@ -16,6 +19,13 @@ store.dispatch(loadSafesFromStorage())
store.dispatch(loadDefaultSafe())
store.dispatch(loadCurrentSessionFromStorage())
Sentry.init({
dsn: SENTRY_DSN,
release: `safe-react@${process.env.REACT_APP_APP_VERSION}`,
integrations: [new Integrations.BrowserTracing()],
sampleRate: 1,
})
const root = document.getElementById('root')
if (root !== null) {

View File

@ -101,7 +101,7 @@ const Container = (): React.ReactElement => {
path={`${matchSafeWithAddress?.path}/address-book`}
render={() => wrapInSuspense(<AddressBookTable />, null)}
/>
<Redirect to={`${matchSafeWithAddress?.path}/balances`} />
<Redirect to={`${matchSafeWithAddress?.url}/balances`} />
</Switch>
{modal.isOpen && <GenericModal {...modal} onClose={closeGenericModal} />}
</>

View File

@ -1,8 +1,10 @@
export const APP_ENV = process.env.REACT_APP_ENV
export const NODE_ENV = process.env.NODE_ENV
export const IS_PRODUCTION = process.env.NODE_ENV === 'production'
export const NETWORK = process.env.REACT_APP_NETWORK?.toUpperCase() || 'RINKEBY'
export const INTERCOM_ID = APP_ENV === 'production' ? process.env.REACT_APP_INTERCOM_ID : 'plssl1fl'
export const GOOGLE_ANALYTICS_ID = process.env.REACT_APP_GOOGLE_ANALYTICS || ''
export const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN || ''
export const PORTIS_ID = process.env.REACT_APP_PORTIS_ID ?? '852b763d-f28b-4463-80cb-846d7ec5806b'
export const FORTMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY ?? 'pk_test_CAD437AA29BE0A40'
export const BLOCKNATIVE_KEY = process.env.REACT_APP_BLOCKNATIVE_KEY ?? '7fbb9cee-7e97-4436-8770-8b29a9a8814c'

124
yarn.lock
View File

@ -1484,9 +1484,9 @@
solc "0.5.14"
truffle "^5.1.21"
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28":
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#8d8508e":
version "0.4.0"
resolved "https://github.com/gnosis/safe-react-components.git#70e57bdd1e0fd5dfdf5768076577c1e000b5fe28"
resolved "https://github.com/gnosis/safe-react-components.git#8d8508ea01bf660bfd75a95ed7fff277caa9ac30"
dependencies:
classnames "^2.2.6"
polished "3.6.5"
@ -1997,6 +1997,92 @@
resolved "https://registry.yarnpkg.com/@restless/sanitizers/-/sanitizers-0.2.5.tgz#96a5cfa3edb52abd8fa14e77798738f3a067dbec"
integrity sha512-utsOFwv5owNnbj8HijF7uML/AURgUl5YvY4S2gpxQsrp2D1EP/4rQU/HSyYdIQaL89BoZ/5NHveRJrcFyuHo/w==
"@sentry/browser@5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.27.1.tgz#67da0cb9680ed54ecdb56a66abd8183b5a8ee174"
integrity sha512-OPBtKKJDgpJOJILaXntGp0z5KT2I1fmtePnHDdgPd7uNqXfTw0E6bvSjY9bR0pSJSooSwqZAAnsAZg8t4772ow==
dependencies:
"@sentry/core" "5.27.1"
"@sentry/types" "5.27.1"
"@sentry/utils" "5.27.1"
tslib "^1.9.3"
"@sentry/cli@^1.58.0":
version "1.58.0"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.58.0.tgz#b1609f10e71539951499866502b13bf3a270fe79"
integrity sha512-bUBKBYyKVzjNhQpAfPJ3XAvAyNNvrD2Rtpo6B0MR3Okw3prdLFgv9Ta8TN19IXT7u9w13B2EdMnNA6dQDtrD4g==
dependencies:
https-proxy-agent "^5.0.0"
mkdirp "^0.5.5"
node-fetch "^2.6.0"
progress "^2.0.3"
proxy-from-env "^1.1.0"
"@sentry/core@5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.27.1.tgz#489604054d821e1de155f80fe650085b37cad235"
integrity sha512-n5CxzMbOAT6HZK4U4cOUAAikkRnnHhMNhInrjfZh7BoiuX1k63Hru2H5xk5WDuEaTTr5RaBA/fqPl7wxHySlwQ==
dependencies:
"@sentry/hub" "5.27.1"
"@sentry/minimal" "5.27.1"
"@sentry/types" "5.27.1"
"@sentry/utils" "5.27.1"
tslib "^1.9.3"
"@sentry/hub@5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.27.1.tgz#c95faaf18257c365acc09246fafd27276bfd6a2f"
integrity sha512-RBHo3T92s6s4Ian1pZcPlmNtFqB+HAP6xitU+ZNA48bYUK+R1vvqEcI8Xs83FyNaRGCgclp9erDFQYyAuxY4vw==
dependencies:
"@sentry/types" "5.27.1"
"@sentry/utils" "5.27.1"
tslib "^1.9.3"
"@sentry/minimal@5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.27.1.tgz#d6ce881ba3c262db29520177a4c1f0e0f5388697"
integrity sha512-MHXCeJdA1NAvaJuippcM8nrWScul8iTN0Q5nnFkGctGIGmmiZHTXAYkObqJk7H3AK+CP7r1jqN2aQj5Nd9CtyA==
dependencies:
"@sentry/hub" "5.27.1"
"@sentry/types" "5.27.1"
tslib "^1.9.3"
"@sentry/react@^5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.27.1.tgz#1accad75dd7302d6486b8d4657673d56ebfb7fa7"
integrity sha512-iKJgF3ZfIbKC9pCTip+xnu7JYAYryDgobknj/NmT7nbfeSE2oJHFZYsMk+BzxxKaEFcYfMeJvtm3Ijq1Nm1Khw==
dependencies:
"@sentry/browser" "5.27.1"
"@sentry/minimal" "5.27.1"
"@sentry/types" "5.27.1"
"@sentry/utils" "5.27.1"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/tracing@^5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.27.1.tgz#198cd97514363369d29eef9b597be9332ab170c4"
integrity sha512-GBmdR8Ky/nv4KOa6+DEnOSBkFOFhM+asR8Y/gw2qSUWCwzKuWHh9BEnDwxtSI8CMvgUwOIZ5wiiqJGc1unYfCw==
dependencies:
"@sentry/hub" "5.27.1"
"@sentry/minimal" "5.27.1"
"@sentry/types" "5.27.1"
"@sentry/utils" "5.27.1"
tslib "^1.9.3"
"@sentry/types@5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.27.1.tgz#031480a4cf8f0b6e6337fb03ee884deedcef6f40"
integrity sha512-g1aX0V0fz5BTo0mjgSVY9XmPLGZ6p+8OEzq3ubKzDUf59VHl+Vt8viZ8VXw/vsNtfAjBHn7BzSuzJo7cXJJBtA==
"@sentry/utils@5.27.1":
version "5.27.1"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.27.1.tgz#0ed9d9685aae6f4ef9eb6b9ebb81e361fd1c5452"
integrity sha512-VIzK8utuvFO9EogZcKJPgmLnlJtYbaPQ0jCw7od9HRw1ckrSBc84sA0uuuY6pB6KSM+7k6EjJ5IdIBaCz5ep/A==
dependencies:
"@sentry/types" "5.27.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"
@ -3780,6 +3866,13 @@ aes-js@3.1.2, aes-js@^3.1.1:
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@ -7213,6 +7306,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
dependencies:
ms "2.1.2"
debug@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@ -7234,13 +7334,6 @@ debug@^3.0.0, debug@^3.1.1, debug@^3.2.5:
dependencies:
ms "^2.1.1"
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
dependencies:
ms "2.1.2"
decamelize-keys@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
@ -10666,6 +10759,14 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@ -15728,6 +15829,11 @@ proxy-addr@~2.0.5:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"