Skip to content

Commit

Permalink
Setup Error Monitoring and Analytics (#114)
Browse files Browse the repository at this point in the history
* Add Sentry error capture
* Add Vercel analytics
* Setup bundle analyzer
* Capture errors from logger
  • Loading branch information
jmrossy authored Jan 16, 2024
1 parent f2f5e98 commit 7ac7dcc
Show file tree
Hide file tree
Showing 21 changed files with 682 additions and 74 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ coverage
postcss.config.js
next.config.js
tailwind.config.js
jest.config.js
jest.config.js
sentry.*
57 changes: 50 additions & 7 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
/** @type {import('next').NextConfig} */

const { version } = require('./package.json')
const { withSentryConfig } = require("@sentry/nextjs");
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})

const isDev = process.env.NODE_ENV !== 'production'

// Sometimes useful to disable this during development
const ENABLE_CSP_HEADER = true;
const FRAME_SRC_HOSTS = ['https://*.walletconnect.com', 'https://*.walletconnect.org','https://*.solflare.com'];
const STYLE_SRC_HOSTS = ['https://*.googleapis.com']
const IMG_SRC_HOSTS = ['https://*.walletconnect.com'];
const cspHeader = `
default-src 'self';
script-src 'self'${isDev ? " 'unsafe-eval'" : ''};
style-src 'self' 'unsafe-inline' ${STYLE_SRC_HOSTS.join(' ')};
connect-src *;
img-src 'self' blob: data: ${IMG_SRC_HOSTS.join(' ')};
font-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-src 'self' ${FRAME_SRC_HOSTS.join(' ')};
frame-ancestors 'none';
${!isDev ? 'block-all-mixed-content;' : ''}
${!isDev ? 'upgrade-insecure-requests;' : ''}
`.replace(/\s{2,}/g, ' ').trim();

const securityHeaders = [
{
key: 'X-XSS-Protection',
Expand All @@ -22,12 +47,14 @@ const securityHeaders = [
value: 'strict-origin-when-cross-origin',
},
// Note, causes a problem for firefox: https://github.com/MetaMask/metamask-extension/issues/3133
{
key: 'Content-Security-Policy',
value: `default-src 'self'; script-src 'self'${
isDev ? " 'unsafe-eval'" : ''
}; connect-src *; img-src 'self' data: https://*.walletconnect.com; style-src 'self' 'unsafe-inline' https://*.googleapis.com; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-src 'self' https://*.solflare.com https://*.walletconnect.com https://*.walletconnect.org;`,
},
...(ENABLE_CSP_HEADER
? [
{
key: 'Content-Security-Policy',
value: cspHeader,
},
]
: [])
]

const nextConfig = {
Expand Down Expand Up @@ -64,6 +91,22 @@ const nextConfig = {

reactStrictMode: true,
swcMinify: true,

sentry: {
hideSourceMaps: true,
tunnelRoute: "/monitoring-tunnel",
},
}

module.exports = nextConfig
const sentryWebpackPluginOptions = {
org: "hyperlane",
project: "warp-ui",
authToken: process.env.SENTRY_AUTH_TOKEN,
bundleSizeOptimizations: {
excludeDebugStatements: true,
excludeReplayIframe: true,
excludeReplayShadowDom: true,
},
};

module.exports = withBundleAnalyzer(withSentryConfig(nextConfig, sentryWebpackPluginOptions));
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
"@hyperlane-xyz/widgets": "^3.1.4",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "1.3.0",
"@sentry/nextjs": "^7.93.0",
"@solana/spl-token": "^0.3.8",
"@solana/wallet-adapter-base": "^0.9.22",
"@solana/wallet-adapter-react": "^0.15.32",
"@solana/wallet-adapter-react-ui": "^0.9.31",
"@solana/wallet-adapter-wallets": "^0.19.16",
"@solana/web3.js": "^1.77.0",
"@tanstack/react-query": "^4.29.7",
"@vercel/analytics": "^1.1.1",
"bignumber.js": "^9.1.1",
"buffer": "^6.0.3",
"cosmjs-types": "^0.9.0",
Expand All @@ -43,6 +45,7 @@
"zustand": "^4.3.9"
},
"devDependencies": {
"@next/bundle-analyzer": "^14.0.4",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/jest": "^29.5.3",
"@types/node": "^18.11.18",
Expand Down
22 changes: 22 additions & 0 deletions sentry.client.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { sentryDefaultConfig } from './sentry.default.config';
import * as Sentry from '@sentry/nextjs';

if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init({
...sentryDefaultConfig,
integrations: [
new Sentry.Integrations.Breadcrumbs({
console: false,
dom: false,
fetch: false,
history: false,
sentry: false,
xhr: false,
}),
new Sentry.Integrations.Dedupe(),
new Sentry.Integrations.FunctionToString(),
new Sentry.Integrations.GlobalHandlers(),
new Sentry.Integrations.HttpContext(),
],
});
}
13 changes: 13 additions & 0 deletions sentry.default.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const sentryDefaultConfig = {
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
maxBreadcrumbs: 1,
sendClientReports: false,
attachStacktrace: false,
defaultIntegrations: false,
integrations: [],
beforeSend(event) {
delete event.user;
return event;
},
};
6 changes: 6 additions & 0 deletions sentry.edge.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { sentryDefaultConfig } from './sentry.default.config';
import * as Sentry from '@sentry/nextjs';

if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init(sentryDefaultConfig)
}
6 changes: 6 additions & 0 deletions sentry.server.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { sentryDefaultConfig } from './sentry.default.config';
import * as Sentry from '@sentry/nextjs';

if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init(sentryDefaultConfig)
}
15 changes: 15 additions & 0 deletions src/components/toast/useToastError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { toast } from 'react-toastify';

import { errorToString } from '@hyperlane-xyz/utils';

import { logger } from '../../utils/logger';

export function useToastError(error: any, errorMsg?: string) {
useEffect(() => {
if (!error) return;
const message = errorMsg || errorToString(error, 500);
logger.error(message, error);
toast.error(errorMsg);
}, [error, errorMsg]);
}
4 changes: 2 additions & 2 deletions src/consts/config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ADDRESS_BLACKLIST } from './blacklist';

const isDevMode = process?.env?.NODE_ENV === 'development';
const version = process?.env?.NEXT_PUBLIC_VERSION ?? null;
const version = process?.env?.NEXT_PUBLIC_VERSION || '0.0.0';
const explorerApiKeys = JSON.parse(process?.env?.EXPLORER_API_KEYS || '{}');
const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || '';
const withdrawalWhitelist = process?.env?.NEXT_PUBLIC_BLOCK_WITHDRAWAL_WHITELIST || '';
const transferBlacklist = process?.env?.NEXT_PUBLIC_TRANSFER_BLACKLIST || '';

interface Config {
debug: boolean; // Enables some debug features in the app
version: string | null; // Matches version number in package.json
version: string; // Matches version number in package.json
explorerApiKeys: Record<string, string>; // Optional map of API keys for block explorer
showTipBox: boolean; // Show/Hide the blue tip box above the transfer form
showDisabledTokens: boolean; // Show/Hide invalid token options in the selection modal
Expand Down
2 changes: 1 addition & 1 deletion src/features/caip/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function tryParseCaip2Id(id?: ChainCaip2Id) {
try {
return parseCaip2Id(id);
} catch (err) {
logger.error('Error parsing caip2 id', err);
logger.error(`Error parsing caip2 id ${id}`, err);
return undefined;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/caip/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function tryParseCaip19Id(id?: TokenCaip19Id) {
try {
return parseCaip19Id(id);
} catch (err) {
logger.error('Error parsing caip2 id', err);
logger.error(`Error parsing caip2 id ${id}`, err);
return undefined;
}
}
Expand Down
11 changes: 5 additions & 6 deletions src/features/tokens/approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';

import { ProtocolType, eqAddress } from '@hyperlane-xyz/utils';

import { useToastError } from '../../components/toast/useToastError';
import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { getTokenAddress, isNativeToken, isNonFungibleToken } from '../caip/tokens';
Expand All @@ -20,11 +21,7 @@ export function useIsApproveRequired(
) {
const owner = useAccountAddressForChain(route?.originCaip2Id);

const {
isLoading,
isError: hasError,
data,
} = useQuery({
const { isLoading, isError, error, data } = useQuery({
queryKey: ['useIsApproveRequired', route, tokenCaip19Id, owner, amount],
queryFn: async () => {
if (!route || !tokenCaip19Id || !owner || !amount) return false;
Expand All @@ -33,7 +30,9 @@ export function useIsApproveRequired(
enabled,
});

return { isLoading, hasError, isApproveRequired: !!data };
useToastError(error, 'Error fetching approval status');

return { isLoading, isError, isApproveRequired: !!data };
}

export async function isApproveRequired(
Expand Down
42 changes: 24 additions & 18 deletions src/features/tokens/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect } from 'react';

import { eqAddress, isValidAddress } from '@hyperlane-xyz/utils';

import { useToastError } from '../../components/toast/useToastError';
import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens';
Expand All @@ -23,11 +24,7 @@ export function useOriginBalance(
const address = useAccountAddressForChain(originCaip2Id);
const setSenderBalances = useStore((state) => state.setSenderBalances);

const {
isLoading,
isError: hasError,
data,
} = useQuery({
const { isLoading, isError, error, data } = useQuery({
queryKey: [
'useOriginBalance',
address,
Expand Down Expand Up @@ -58,13 +55,15 @@ export function useOriginBalance(
refetchInterval: 5000,
});

useToastError(error, 'Error fetching origin balance');

useEffect(() => {
setSenderBalances(data?.tokenBalance || '0', data?.nativeBalance || '0');
}, [data, setSenderBalances]);

return {
isLoading,
hasError,
isError,
tokenBalance: data?.tokenBalance,
tokenDecimals: data?.tokenDecimals,
nativeBalance: data?.nativeBalance,
Expand All @@ -75,11 +74,7 @@ export function useDestinationBalance(
{ originCaip2Id, destinationCaip2Id, tokenCaip19Id, recipientAddress }: TransferFormValues,
tokenRoutes: RoutesMap,
) {
const {
isLoading,
isError: hasError,
data,
} = useQuery({
const { isLoading, isError, error, data } = useQuery({
queryKey: [
'useDestinationBalance',
recipientAddress,
Expand All @@ -101,7 +96,9 @@ export function useDestinationBalance(
refetchInterval: 5000,
});

return { isLoading, hasError, balance: data?.balance, decimals: data?.decimals };
useToastError(error, 'Error fetching destination balance');

return { isLoading, isError, balance: data?.balance, decimals: data?.decimals };
}

// TODO solana support
Expand All @@ -112,7 +109,8 @@ export function useOriginTokenIdBalance(tokenCaip19Id: TokenCaip19Id) {

const {
isLoading,
isError: hasError,
isError,
error,
data: tokenIds,
} = useQuery({
queryKey: ['useOriginTokenIdBalance', tokenCaip19Id, accountAddress],
Expand All @@ -123,11 +121,13 @@ export function useOriginTokenIdBalance(tokenCaip19Id: TokenCaip19Id) {
refetchInterval: 5000,
});

useToastError(error, 'Error fetching origin token IDs');

useEffect(() => {
setSenderNftIds(tokenIds && Array.isArray(tokenIds) ? tokenIds : null);
}, [tokenIds, setSenderNftIds]);

return { isLoading, hasError, tokenIds };
return { isLoading, isError, tokenIds };
}

// TODO solana support
Expand Down Expand Up @@ -159,7 +159,8 @@ export function useContractSupportsTokenByOwner(
) {
const {
isLoading,
isError: hasError,
isError,
error,
data: isContractAllowToGetTokenIds,
} = useQuery({
queryKey: ['useContractSupportsTokenByOwner', tokenCaip19Id, accountAddress],
Expand All @@ -169,7 +170,9 @@ export function useContractSupportsTokenByOwner(
},
});

return { isLoading, hasError, isContractAllowToGetTokenIds };
useToastError(error, 'Error ERC721 contract details');

return { isLoading, isError, isContractAllowToGetTokenIds };
}

// TODO solana support
Expand All @@ -195,7 +198,8 @@ export function useIsSenderNftOwner(tokenCaip19Id: TokenCaip19Id, tokenId: strin

const {
isLoading,
isError: hasError,
isError,
error,
data: owner,
} = useQuery({
queryKey: ['useOwnerOfErc721', tokenCaip19Id, tokenId],
Expand All @@ -205,12 +209,14 @@ export function useIsSenderNftOwner(tokenCaip19Id: TokenCaip19Id, tokenId: strin
},
});

useToastError(error, 'Error ERC721 owner');

useEffect(() => {
if (!senderAddress || !owner) setIsSenderNftOwner(null);
else setIsSenderNftOwner(eqAddress(senderAddress, owner));
}, [owner, senderAddress, setIsSenderNftOwner]);

return { isLoading, hasError, owner };
return { isLoading, isError, owner };
}

// TODO solana support
Expand Down
Loading

1 comment on commit 7ac7dcc

@vercel
Copy link

@vercel vercel bot commented on 7ac7dcc Jan 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.