diff options
Diffstat (limited to 'packages/instant')
-rw-r--r-- | packages/instant/package.json | 3 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_provider.tsx | 2 | ||||
-rw-r--r-- | packages/instant/src/constants.ts | 11 | ||||
-rw-r--r-- | packages/instant/src/redux/async_data.ts | 4 | ||||
-rw-r--r-- | packages/instant/src/util/asset.ts | 17 | ||||
-rw-r--r-- | packages/instant/src/util/buy_quote_updater.ts | 31 | ||||
-rw-r--r-- | packages/instant/src/util/error_reporter.ts | 62 | ||||
-rw-r--r-- | packages/instant/src/util/gas_price_estimator.ts | 5 | ||||
-rw-r--r-- | packages/instant/src/util/heap.ts | 3 | ||||
-rw-r--r-- | packages/instant/test/util/asset.test.ts | 33 | ||||
-rw-r--r-- | packages/instant/webpack.config.js | 108 |
11 files changed, 239 insertions, 40 deletions
diff --git a/packages/instant/package.json b/packages/instant/package.json index 62904949b..ed8c460d9 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -58,6 +58,7 @@ "react-redux": "^5.0.7", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", + "rollbar": "^2.5.0", "styled-components": "^4.0.2", "ts-optchain": "^0.1.1" }, @@ -82,7 +83,9 @@ "make-promises-safe": "^1.1.0", "npm-run-all": "^4.1.2", "nyc": "^11.0.1", + "rollbar-sourcemap-webpack-plugin": "^2.4.0", "shx": "^0.2.2", + "source-map-loader": "^0.2.4", "svg-react-loader": "^0.4.6", "ts-jest": "^23.10.3", "tslint": "5.11.0", diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index a4a03bbf4..46488fcc4 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -15,6 +15,7 @@ import { AccountState, AffiliateInfo, AssetMetaData, Network, OrderSource, Quote import { analytics, disableAnalytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; +import { setupRollbar } from '../util/error_reporter'; import { gasPriceEstimator } from '../util/gas_price_estimator'; import { Heartbeater } from '../util/heartbeater'; import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory'; @@ -86,6 +87,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider } constructor(props: ZeroExInstantProviderProps) { super(props); + setupRollbar(); fonts.include(); const initialAppState = ZeroExInstantProvider._mergeDefaultStateWithProps(this.props); this._store = store.create(initialAppState); diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 0dd770ec6..2fe0f4b89 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -22,6 +22,17 @@ export const HEAP_ANALYTICS_ID = process.env.HEAP_ANALYTICS_ID; export const COINBASE_API_BASE_URL = 'https://api.coinbase.com/v2'; export const PROGRESS_STALL_AT_WIDTH = '95%'; export const PROGRESS_FINISH_ANIMATION_TIME_MS = 200; +export const HOST_DOMAINS = [ + '0x-instant-staging.s3-website-us-east-1.amazonaws.com', + '0x-instant-dogfood.s3-website-us-east-1.amazonaws.com', + 'localhost', + '127.0.0.1', + '0.0.0.0', + 'unpkg.com', + 'jsdelivr.com', +]; +export const ROLLBAR_CLIENT_TOKEN = process.env.ROLLBAR_CLIENT_TOKEN; +export const ROLLBAR_ENABLED = process.env.ROLLBAR_ENABLED; export const INSTANT_DISCHARGE_TARGET = process.env.INSTANT_DISCHARGE_TARGET as | 'production' | 'dogfood' diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 9fdcea3ca..18f671cd7 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -10,6 +10,7 @@ import { assetUtils } from '../util/asset'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; import { errorFlasher } from '../util/error_flasher'; +import { errorReporter } from '../util/error_reporter'; import { actions } from './actions'; import { State } from './reducer'; @@ -23,6 +24,7 @@ export const asyncData = { const errorMessage = 'Error fetching ETH/USD price'; errorFlasher.flashNewErrorMessage(dispatch, errorMessage); dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO)); + errorReporter.report(e); } }, fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => { @@ -37,6 +39,7 @@ export const asyncData = { errorFlasher.flashNewErrorMessage(dispatch, errorMessage); // On error, just specify that none are available dispatch(actions.setAvailableAssets([])); + errorReporter.report(e); } }, fetchAccountInfoAndDispatchToStore: async ( @@ -77,6 +80,7 @@ export const asyncData = { const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address); dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei })); } catch (e) { + errorReporter.report(e); // leave balance as is return; } diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts index 40560d3eb..08f3642e3 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -1,3 +1,4 @@ +import { AssetBuyerError } from '@0x/asset-buyer'; import { AssetProxyId, ObjectMap } from '@0x/types'; import * as _ from 'lodash'; @@ -106,4 +107,20 @@ export const assetUtils = { ); return _.compact(erc20sOrUndefined); }, + assetBuyerErrorMessage: (asset: ERC20Asset, error: Error): string | undefined => { + if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + return `Not enough ${assetName} available`; + } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { + return 'Not enough ZRX available'; + } else if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + return `${assetName} is currently unavailable`; + } + + return undefined; + }, }; diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts index c1899f8c1..4229f2735 100644 --- a/packages/instant/src/util/buy_quote_updater.ts +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -1,4 +1,4 @@ -import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; @@ -10,6 +10,7 @@ import { AffiliateInfo, ERC20Asset, QuoteFetchOrigin } from '../types'; import { analytics } from '../util/analytics'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; +import { errorReporter } from '../util/error_reporter'; export const buyQuoteUpdater = { updateBuyQuoteAsync: async ( @@ -35,30 +36,18 @@ export const buyQuoteUpdater = { try { newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage }); } catch (error) { + const errorMessage = assetUtils.assetBuyerErrorMessage(asset, error); + + if (_.isUndefined(errorMessage)) { + // This is an unknown error, report it to rollbar + errorReporter.report(error); + } + if (options.dispatchErrors) { dispatch(actions.setQuoteRequestStateFailure()); analytics.trackQuoteError(error.message ? error.message : 'other', baseUnitValue, fetchOrigin); - let errorMessage; - if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { - const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); - errorMessage = `Not enough ${assetName} available`; - } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { - errorMessage = 'Not enough ZRX available'; - } else if ( - error.message === AssetBuyerError.StandardRelayerApiError || - error.message.startsWith(AssetBuyerError.AssetUnavailable) - ) { - const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); - errorMessage = `${assetName} is currently unavailable`; - } - if (!_.isUndefined(errorMessage)) { - errorFlasher.flashNewErrorMessage(dispatch, errorMessage); - } else { - throw error; - } + errorFlasher.flashNewErrorMessage(dispatch, errorMessage || 'Error fetching price, please try again'); } - // TODO: report to error reporter on else - return; } // We have a successful new buy quote diff --git a/packages/instant/src/util/error_reporter.ts b/packages/instant/src/util/error_reporter.ts new file mode 100644 index 000000000..3ec7b6daa --- /dev/null +++ b/packages/instant/src/util/error_reporter.ts @@ -0,0 +1,62 @@ +import { logUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { HOST_DOMAINS, INSTANT_DISCHARGE_TARGET, ROLLBAR_CLIENT_TOKEN, ROLLBAR_ENABLED } from '../constants'; + +// Import version of Rollbar designed for embedded components +// See https://docs.rollbar.com/docs/using-rollbarjs-inside-an-embedded-component +// tslint:disable-next-line:no-var-requires +const Rollbar = require('rollbar/dist/rollbar.noconflict.umd'); + +let rollbar: any; +// Configures rollbar and sets up error catching +export const setupRollbar = (): any => { + if (_.isUndefined(rollbar) && ROLLBAR_CLIENT_TOKEN && ROLLBAR_ENABLED) { + rollbar = new Rollbar({ + accessToken: ROLLBAR_CLIENT_TOKEN, + captureUncaught: true, + captureUnhandledRejections: true, + enabled: true, + itemsPerMinute: 10, + maxItems: 500, + payload: { + environment: INSTANT_DISCHARGE_TARGET || `Local ${process.env.NODE_ENV}`, + client: { + javascript: { + source_map_enabled: true, + code_version: process.env.GIT_SHA, + guess_uncaught_frames: true, + }, + }, + }, + hostWhiteList: HOST_DOMAINS, + uncaughtErrorLevel: 'error', + ignoredMessages: [ + // Errors from the third-party scripts + 'Script error', + // Network errors or ad-blockers + 'TypeError: Failed to fetch', + 'Exchange has not been deployed to detected network (network/artifact mismatch)', + // Source: https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/7VU0_VvC7mE + "undefined is not an object (evaluating '__gCrWeb.autofill.extractForms')", + // Source: http://stackoverflow.com/questions/43399818/securityerror-from-facebook-and-cross-domain-messaging + 'SecurityError (DOM Exception 18)', + ], + }); + } +}; + +export const errorReporter = { + report(err: Error): void { + if (!rollbar) { + logUtils.log('Not reporting to rollbar because not configured', err); + return; + } + + rollbar.error(err, (rollbarErr: Error) => { + if (rollbarErr) { + logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`); + } + }); + }, +}; diff --git a/packages/instant/src/util/gas_price_estimator.ts b/packages/instant/src/util/gas_price_estimator.ts index 6b15809a3..332c8d00a 100644 --- a/packages/instant/src/util/gas_price_estimator.ts +++ b/packages/instant/src/util/gas_price_estimator.ts @@ -7,6 +7,8 @@ import { GWEI_IN_WEI, } from '../constants'; +import { errorReporter } from './error_reporter'; + interface EthGasStationResult { average: number; fastestWait: number; @@ -42,8 +44,9 @@ export class GasPriceEstimator { let fetchedAmount: GasInfo | undefined; try { fetchedAmount = await fetchFastAmountInWeiAsync(); - } catch { + } catch (e) { fetchedAmount = undefined; + errorReporter.report(e); } if (fetchedAmount) { diff --git a/packages/instant/src/util/heap.ts b/packages/instant/src/util/heap.ts index 7c53c9918..279ff3059 100644 --- a/packages/instant/src/util/heap.ts +++ b/packages/instant/src/util/heap.ts @@ -5,6 +5,7 @@ import * as _ from 'lodash'; import { HEAP_ANALYTICS_ID } from '../constants'; import { AnalyticsEventOptions, AnalyticsUserOptions } from './analytics'; +import { errorReporter } from './error_reporter'; export type EventProperties = ObjectMap<string | number>; @@ -107,8 +108,8 @@ export const heapUtil = { heapFunctionCall(curHeap); } catch (e) { // We never want analytics to crash our React component - // TODO(sk): error reporter here logUtils.log('Analytics error', e); + errorReporter.report(e); } } }, diff --git a/packages/instant/test/util/asset.test.ts b/packages/instant/test/util/asset.test.ts index 4229b24ed..fc4e4e2e4 100644 --- a/packages/instant/test/util/asset.test.ts +++ b/packages/instant/test/util/asset.test.ts @@ -1,6 +1,7 @@ +import { AssetBuyerError } from '@0x/asset-buyer'; import { AssetProxyId, ObjectMap } from '@0x/types'; -import { Asset, AssetMetaData, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; +import { Asset, AssetMetaData, ERC20Asset, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; import { assetUtils } from '../../src/util/asset'; const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; @@ -11,7 +12,7 @@ const ZRX_META_DATA: ERC20AssetMetaData = { decimals: 18, name: '0x', }; -const ZRX_ASSET: Asset = { +const ZRX_ASSET: ERC20Asset = { assetData: ZRX_ASSET_DATA, metaData: ZRX_META_DATA, }; @@ -45,4 +46,32 @@ describe('assetDataUtil', () => { ).toThrowError(ZeroExInstantError.AssetMetaDataNotAvailable); }); }); + describe('assetBuyerErrorMessage', () => { + it('should return message for InsufficientAssetLiquidity', () => { + const insufficientAssetError = new Error(AssetBuyerError.InsufficientAssetLiquidity); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, insufficientAssetError)).toEqual( + 'Not enough ZRX available', + ); + }); + it('should return message for InsufficientAssetLiquidity', () => { + const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, insufficientZrxError)).toEqual( + 'Not enough ZRX available', + ); + }); + it('should message for StandardRelayerApiError', () => { + const standardRelayerError = new Error(AssetBuyerError.StandardRelayerApiError); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, standardRelayerError)).toEqual( + 'ZRX is currently unavailable', + ); + }); + it('should return error for AssetUnavailable error', () => { + const assetUnavailableError = new Error( + `${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET_DATA}`, + ); + expect(assetUtils.assetBuyerErrorMessage(ZRX_ASSET, assetUnavailableError)).toEqual( + 'ZRX is currently unavailable', + ); + }); + }); }); diff --git a/packages/instant/webpack.config.js b/packages/instant/webpack.config.js index 803240e76..60c07db61 100644 --- a/packages/instant/webpack.config.js +++ b/packages/instant/webpack.config.js @@ -1,10 +1,16 @@ const childProcess = require('child_process'); const ip = require('ip'); const path = require('path'); +const RollbarSourceMapPlugin = require('rollbar-sourcemap-webpack-plugin'); const webpack = require('webpack'); +const GIT_SHA = childProcess + .execSync('git rev-parse HEAD') + .toString() + .trim(); + const DISCHARGE_TARGETS_THAT_REQUIRED_HEAP = ['production', 'staging', 'dogfood']; -const getConfigForDischargeTarget = dischargeTarget => { +const getHeapConfigForDischargeTarget = dischargeTarget => { return { heapAnalyticsIdEnvName: dischargeTarget === 'production' @@ -14,14 +20,51 @@ const getConfigForDischargeTarget = dischargeTarget => { }; }; -const GIT_SHA = childProcess - .execSync('git rev-parse HEAD') - .toString() - .trim(); -const generateConfig = (dischargeTarget, configOptions) => { +const DISCHARGE_TARGETS_THAT_REQUIRE = ['production', 'staging', 'dogfood']; +const getRollbarConfigForDischargeTarget = dischargeTarget => { + if (DISCHARGE_TARGETS_THAT_REQUIRE.includes(dischargeTarget)) { + const rollbarSourceMapPublicPath = + dischargeTarget === 'production' + ? 'https://instant.0xproject.com' + : `http://0x-instant-${dischargeTarget}.s3-website-us-east-1.amazonaws.com`; + + return { + rollbarSourceMapPublicPath, + rollbarRequired: true, + }; + } + + return { + rollbarRequired: false, + }; +}; + +const ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME = 'INSTANT_ROLLBAR_CLIENT_TOKEN'; +const ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME = 'INSTANT_ROLLBAR_PUBLISH_TOKEN'; +const getRollbarTokens = (dischargeTarget, rollbarRequired) => { + const clientToken = process.env[ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME]; + const publishToken = process.env[ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME]; + + if (rollbarRequired) { + if (!clientToken) { + throw new Error( + `Rollbar client token required for ${dischargeTarget}, please set env var ${ROLLBAR_CLIENT_TOKEN_ENV_VAR_NAME}`, + ); + } + if (!publishToken) { + throw new Error( + `Rollbar publish token required for ${dischargeTarget}, please set env var ${ROLLBAR_PUBLISH_TOKEN_ENV_VAR_NAME}`, + ); + } + } + + return { clientToken, publishToken }; +}; + +const generateConfig = (dischargeTarget, heapConfigOptions, rollbarConfigOptions, nodeEnv) => { const outputPath = process.env.WEBPACK_OUTPUT_PATH || 'umd'; - const { heapAnalyticsIdEnvName, heapAnalyticsIdRequired } = configOptions; + const { heapAnalyticsIdEnvName, heapAnalyticsIdRequired } = heapConfigOptions; const heapAnalyticsId = process.env[heapAnalyticsIdEnvName]; if (heapAnalyticsIdRequired && !heapAnalyticsId) { throw new Error( @@ -29,9 +72,26 @@ const generateConfig = (dischargeTarget, configOptions) => { ); } + const rollbarTokens = getRollbarTokens(dischargeTarget, rollbarConfigOptions.rollbarRequired); + const rollbarEnabled = + rollbarTokens.clientToken && (nodeEnv !== 'development' || process.env.INSTANT_ROLLBAR_FORCE_DEVELOPMENT); + + let rollbarPlugin; + if (rollbarConfigOptions.rollbarRequired) { + if (!rollbarEnabled || !rollbarTokens.publishToken || !rollbarConfigOptions.rollbarSourceMapPublicPath) { + throw new Error(`Rollbar required for ${dischargeTarget} but not configured`); + } + rollbarPlugin = new RollbarSourceMapPlugin({ + accessToken: rollbarTokens.publishToken, + version: GIT_SHA, + publicPath: rollbarConfigOptions.rollbarSourceMapPublicPath, + }); + } + const envVars = { GIT_SHA: JSON.stringify(GIT_SHA), NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version), + ROLLBAR_ENABLED: rollbarEnabled, }; if (dischargeTarget) { envVars.INSTANT_DISCHARGE_TARGET = JSON.stringify(dischargeTarget); @@ -39,6 +99,18 @@ const generateConfig = (dischargeTarget, configOptions) => { if (heapAnalyticsId) { envVars.HEAP_ANALYTICS_ID = JSON.stringify(heapAnalyticsId); } + if (rollbarTokens.clientToken) { + envVars.ROLLBAR_CLIENT_TOKEN = JSON.stringify(rollbarTokens.clientToken); + } + + const plugins = [ + new webpack.DefinePlugin({ + 'process.env': envVars, + }), + ]; + if (rollbarPlugin) { + plugins.push(rollbarPlugin); + } const config = { entry: { @@ -50,11 +122,7 @@ const generateConfig = (dischargeTarget, configOptions) => { library: 'zeroExInstant', libraryTarget: 'umd', }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env': envVars, - }), - ], + plugins, devtool: 'source-map', resolve: { extensions: ['.js', '.json', '.ts', '.tsx'], @@ -69,6 +137,15 @@ const generateConfig = (dischargeTarget, configOptions) => { test: /\.svg$/, loader: 'svg-react-loader', }, + { + test: /\.js$/, + loader: 'source-map-loader', + exclude: [ + // instead of /\/node_modules\// + path.join(process.cwd(), 'node_modules'), + path.join(process.cwd(), '../..', 'node_modules'), + ], + }, ], }, devServer: { @@ -89,8 +166,9 @@ const generateConfig = (dischargeTarget, configOptions) => { return config; }; -module.exports = (env, _argv) => { +module.exports = (env, argv) => { const dischargeTarget = env ? env.discharge_target : undefined; - const configOptions = getConfigForDischargeTarget(dischargeTarget); - return generateConfig(dischargeTarget, configOptions); + const heapConfigOptions = getHeapConfigForDischargeTarget(dischargeTarget); + const rollbarConfigOptions = getRollbarConfigForDischargeTarget(dischargeTarget); + return generateConfig(dischargeTarget, heapConfigOptions, rollbarConfigOptions, argv.mode); }; |