Commit f6804087 authored by tom goriunov's avatar tom goriunov Committed by GitHub

refine Sentry setup (#1229)

* [skip ci] reusable workflow for publishing docker image

* [skip ci] clean up

* remove sentry/nextjs

* refine config and fetch error payload

* log 404

* make ENV variables for instance and enviroment optional

* add release and enviroment to csp
parent c4bcba60
import type { Feature } from './types';
import app from '../app';
import { getEnvValue } from '../utils';
const dsn = getEnvValue('NEXT_PUBLIC_SENTRY_DSN');
const instance = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_APP_INSTANCE');
if (envValue) {
return envValue;
}
return app.host?.replace('.blockscout.com', '').replaceAll('-', '_');
})();
const environment = getEnvValue('NEXT_PUBLIC_APP_ENV') || 'production';
const release = getEnvValue('NEXT_PUBLIC_GIT_TAG');
const cspReportUrl = (() => {
try {
const url = new URL(getEnvValue('SENTRY_CSP_REPORT_URI') || '');
// https://docs.sentry.io/product/security-policy-reporting/#additional-configuration
url.searchParams.set('sentry_environment', environment);
release && url.searchParams.set('sentry_release', release);
return url.toString();
} catch (error) {
return;
}
})();
const title = 'Sentry error monitoring';
const config: Feature<{ dsn: string; environment: string | undefined; cspReportUrl: string | undefined; instance: string | undefined }> = (() => {
if (dsn) {
const config: Feature<{
dsn: string;
cspReportUrl: string | undefined;
instance: string;
release: string | undefined;
environment: string;
}> = (() => {
if (dsn && instance && environment) {
return Object.freeze({
title,
isEnabled: true,
dsn,
environment: getEnvValue('NEXT_PUBLIC_APP_ENV') || getEnvValue('NODE_ENV'),
cspReportUrl: getEnvValue('SENTRY_CSP_REPORT_URI'),
instance: getEnvValue('NEXT_PUBLIC_APP_INSTANCE'),
cspReportUrl,
instance,
release,
environment,
});
}
......
......@@ -40,7 +40,7 @@ const UI = Object.freeze({
isHidden: getEnvValue('NEXT_PUBLIC_HIDE_INDEXING_ALERT'),
},
maintenanceAlert: {
message: getEnvValue(process.env.NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE),
message: getEnvValue('NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE'),
},
explorers: {
items: parseEnvJson<Array<NetworkExplorer>>(getEnvValue('NEXT_PUBLIC_NETWORK_EXPLORERS')) || [],
......
......@@ -30,7 +30,6 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
......
......@@ -33,7 +33,6 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
......
......@@ -33,7 +33,6 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
......
......@@ -34,7 +34,6 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
......
......@@ -35,7 +35,6 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_WEB3_WALLETS=['coinbase']
......
......@@ -32,7 +32,6 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
......
......@@ -34,7 +34,6 @@ NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-c
## misc
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
# NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97fa753626b8d44011d0b9f9a947c735f20b6e895efdee49d7cda76a50001017
NEXT_PUBLIC_HAS_BEACON_CHAIN=false
......
......@@ -35,7 +35,6 @@ NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward','nonce']
## misc
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97fa753626b8d44011d0b9f9a947c735f20b6e895efdee49d7cda76a50001017
NEXT_PUBLIC_HAS_BEACON_CHAIN=false
......
import type { NextjsOptions } from '@sentry/nextjs/types/utils/nextjsOptions';
const config: NextjsOptions = {
environment: process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
ignoreErrors: [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
'Can\'t find variable: ZiteReader',
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
],
denyUrls: [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
],
};
export default config;
......@@ -155,14 +155,14 @@ const sentrySchema = yup
.string()
.when('NEXT_PUBLIC_SENTRY_DSN', {
is: (value: string) => Boolean(value),
then: (schema) => schema.required(),
then: (schema) => schema,
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_APP_INSTANCE cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
}),
NEXT_PUBLIC_APP_ENV: yup
.string()
.when('NEXT_PUBLIC_SENTRY_DSN', {
is: (value: string) => Boolean(value),
then: (schema) => schema.required(),
then: (schema) => schema,
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_APP_ENV cannot not be used without NEXT_PUBLIC_SENTRY_DSN'),
}),
});
......
......@@ -174,8 +174,8 @@ frontend:
cpu: 200m
memory: 256Mi
env:
NEXT_PUBLIC_APP_ENV: stable
NEXT_PUBLIC_APP_INSTANCE: base_goerli
NEXT_PUBLIC_APP_ENV: development
NEXT_PUBLIC_APP_INSTANCE: main_L2
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
......
......@@ -146,8 +146,8 @@ frontend:
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_APP_ENV: stable
NEXT_PUBLIC_APP_INSTANCE: eth_goerli
NEXT_PUBLIC_APP_ENV: development
NEXT_PUBLIC_APP_INSTANCE: main
NEXT_PUBLIC_STATS_API_HOST: https://stats-test.k8s-dev.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
......
......@@ -63,9 +63,9 @@ frontend:
enabled: false
environment:
NEXT_PUBLIC_APP_ENV:
_default: preview
_default: development
NEXT_PUBLIC_APP_INSTANCE:
_default: base_goerli
_default: review_L2
NEXT_PUBLIC_NETWORK_NAME:
_default: "Base Göerli"
NEXT_PUBLIC_NETWORK_SHORT_NAME:
......
......@@ -59,9 +59,9 @@ frontend:
enabled: false
environment:
NEXT_PUBLIC_APP_ENV:
_default: preview
_default: development
NEXT_PUBLIC_APP_INSTANCE:
_default: eth_goerli
_default: review
NEXT_PUBLIC_NETWORK_NAME:
_default: Blockscout
NEXT_PUBLIC_NETWORK_ID:
......
......@@ -417,8 +417,8 @@ For the smart contract addresses which are [Safe{Core} accounts](https://safe.gl
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | Required | - | `<your-secret>` |
| SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `<your-secret>` |
| NEXT_PUBLIC_APP_ENV | `string` | Current app env (e.g development, review or production). Passed as `environment` property to Sentry config | - | `process.env.NODE_ENV` | `production` |
| NEXT_PUBLIC_APP_INSTANCE | `string` | Name of app instance. Used as custom tag `app_instance` value in the main Sentry scope | - | - | `wonderful_kepler` |
| NEXT_PUBLIC_APP_ENV | `string` | App env (e.g development, review or production). Passed as `environment` property to Sentry config | - | `production` | `production` |
| NEXT_PUBLIC_APP_INSTANCE | `string` | Name of app instance. Used as custom tag `app_instance` value in the main Sentry scope. If not provided, it will be constructed from `NEXT_PUBLIC_APP_HOST` | - | - | `wonderful_kepler` |
&nbsp;
......
......@@ -52,7 +52,12 @@ export default function useFetch() {
};
if (!meta?.omitSentryErrorLog) {
Sentry.captureException(new Error('Client fetch failed'), { extra: { ...error, ...meta }, tags: { source: 'fetch' } });
Sentry.captureException(new Error('Client fetch failed'), { tags: {
source: 'fetch',
'source.resource': meta?.resource,
'status.code': error.status,
'status.text': error.statusText,
} });
}
return response.json().then(
......
......@@ -17,7 +17,12 @@ export default function useGetCsrfToken() {
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
if (!csrfFromHeader) {
Sentry.captureException(new Error('Unable to get csrf token'), { tags: { source: 'csrf_token' } });
Sentry.captureException(new Error('Client fetch failed'), { tags: {
source: 'fetch',
'source.resource': 'csrf',
'status.code': 500,
'status.text': 'Unable to obtain csrf token from header',
} });
return;
}
......
......@@ -19,7 +19,7 @@ export default function useRedirectForInvalidAuthToken() {
const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
if (apiToken && loginUrl) {
Sentry.captureException(new Error('Invalid api token'), { tags: { source: 'invalid_api_token' } });
Sentry.captureException(new Error('Invalid API token'), { tags: { source: 'invalid_api_token' } });
window.location.assign(loginUrl);
}
}
......
import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app';
......@@ -13,11 +12,8 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
return {
environment: feature.environment,
dsn: feature.dsn,
release: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
release: feature.release,
enableTracing: false,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
......
import * as Sentry from '@sentry/react';
import React from 'react';
import { config, configureScope } from 'configs/sentry/react';
import { config, configureScope } from './config';
export default function useConfigSentry() {
React.useEffect(() => {
......
......@@ -4,10 +4,10 @@ import React from 'react';
import type { Route } from 'nextjs-routes';
import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as metadata from 'lib/metadata';
import * as mixpanel from 'lib/mixpanel';
import useConfigSentry from 'lib/sentry/useConfigSentry';
type Props = Route & {
children: React.ReactNode;
......
import * as Sentry from '@sentry/react';
import React from 'react';
import type { NextPageWithLayout } from 'nextjs/types';
......@@ -10,6 +11,10 @@ import LayoutError from 'ui/shared/layout/LayoutError';
const error = new Error('Not found', { cause: { status: 404 } });
const Page: NextPageWithLayout = () => {
React.useEffect(() => {
Sentry.captureException(new Error('Page not found'), { tags: { source: '404' } });
}, []);
return (
<PageNextJs pathname="/404">
<AppError error={ error }/>
......
/**
* NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher.
*
* NOTE: If using this with `next` version 12.2.0 or lower, uncomment the
* penultimate line in `CustomErrorComponent`.
*
* This page is loaded by Nextjs:
* - on the server, when data-fetching methods throw or reject
* - on the client, when `getInitialProps` throws or rejects
* - on the client, when a React lifecycle method throws or rejects, and it's
* caught by the built-in Nextjs error boundary
*
* See:
* - https://nextjs.org/docs/basic-features/data-fetching/overview
* - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props
* - https://reactjs.org/docs/error-boundaries.html
*/
import * as Sentry from '@sentry/nextjs';
import type { GetServerSideProps } from 'next';
import NextErrorComponent from 'next/error';
import React from 'react';
import type { Props as ServerSidePropsCommon } from 'nextjs/getServerSideProps';
import { base as getServerSidePropsCommon } from 'nextjs/getServerSideProps';
import sentryConfig from 'configs/sentry/nextjs';
import * as cookies from 'lib/cookies';
type Props = ServerSidePropsCommon & {
......@@ -37,16 +15,3 @@ const CustomErrorComponent = (props: Props) => {
};
export default CustomErrorComponent;
export const getServerSideProps: GetServerSideProps = async(context) => {
Sentry.init(sentryConfig);
// In case this is running in a serverless function, await this in order to give Sentry
// time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(context);
const commonSSPResult = await getServerSidePropsCommon(context);
const commonSSProps = 'props' in commonSSPResult ? commonSSPResult.props : undefined;
return { props: { ...commonSSProps, statusCode: context.res.statusCode } };
};
......@@ -26,7 +26,7 @@ export default function useMarketplaceApps(filter: string, selectedCategoryId: s
const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(configUrl),
async() => apiFetch(configUrl, undefined, { resource: 'marketplace-apps' }),
{
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
......
......@@ -25,7 +25,7 @@ const Login = () => {
}, []);
const checkSentry = React.useCallback(() => {
Sentry.captureException(new Error('Test error'), { extra: { foo: 'bar' }, tags: { source: 'test' } });
Sentry.captureException(new Error('Test error'), { tags: { source: 'test' } });
}, []);
const checkMixpanel = React.useCallback(() => {
......
......@@ -37,7 +37,7 @@ const MarketplaceApp = () => {
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
[ 'marketplace-apps', id ],
async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl);
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
if (!Array.isArray(result)) {
throw result;
}
......
......@@ -34,7 +34,7 @@ export default function useNftMediaType(url: string | null, isEnabled: boolean)
try {
const mediaTypeResourceUrl = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url } });
const response = await fetch<{ type: MediaType | undefined }, ResourceError>(mediaTypeResourceUrl);
const response = await fetch<{ type: MediaType | undefined }, ResourceError>(mediaTypeResourceUrl, undefined, { resource: 'media-type' });
return 'type' in response ? response.type ?? 'image' : 'image';
} catch (error) {
......
......@@ -91,7 +91,7 @@ const Footer = () => {
const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>(
[ 'footer-links' ],
async() => fetch(config.UI.footer.links || ''),
async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
{
enabled: Boolean(config.UI.footer.links),
staleTime: Infinity,
......
......@@ -15,7 +15,7 @@ export default function useNetworkMenu() {
const apiFetch = useApiFetch();
const { isLoading, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>(
[ 'featured-network' ],
async() => apiFetch(config.UI.sidebar.featuredNetworks || ''),
async() => apiFetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }),
{
enabled: Boolean(config.UI.sidebar.featuredNetworks) && isOpen,
staleTime: Infinity,
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment