diff options
Diffstat (limited to 'packages/instant')
-rw-r--r-- | packages/instant/.dogfood.discharge.json | 4 | ||||
-rw-r--r-- | packages/instant/.staging.discharge.json | 4 | ||||
-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 | 8 | ||||
-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 | 29 | ||||
-rw-r--r-- | packages/instant/src/util/error_reporter.ts | 61 | ||||
-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 | 116 |
13 files changed, 245 insertions, 44 deletions
diff --git a/packages/instant/.dogfood.discharge.json b/packages/instant/.dogfood.discharge.json index ca36b3861..cea579c27 100644 --- a/packages/instant/.dogfood.discharge.json +++ b/packages/instant/.dogfood.discharge.json @@ -1,12 +1,12 @@ { "domain": "0x-instant-dogfood", - "build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod", + "build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod --env.dogfood", "upload_directory": "public", "index_key": "index.html", "error_key": "index.html", "trailing_slashes": true, "cache": 3600, - "aws_profile": "default", + "aws_profile": "0xproject", "aws_region": "us-east-1", "cdn": false, "dns_configured": true diff --git a/packages/instant/.staging.discharge.json b/packages/instant/.staging.discharge.json index c917a650b..9e63cb110 100644 --- a/packages/instant/.staging.discharge.json +++ b/packages/instant/.staging.discharge.json @@ -1,12 +1,12 @@ { "domain": "0x-instant-staging", - "build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod", + "build_command": "WEBPACK_OUTPUT_PATH=public yarn build:umd:prod --env.staging", "upload_directory": "public", "index_key": "index.html", "error_key": "index.html", "trailing_slashes": true, "cache": 3600, - "aws_profile": "default", + "aws_profile": "0xproject", "aws_region": "us-east-1", "cdn": false, "dns_configured": true diff --git a/packages/instant/package.json b/packages/instant/package.json index 69319e854..e37ca1ea2 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -63,6 +63,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" }, @@ -87,7 +88,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 9814aabf8..1f53f2d96 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 } from 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 be6077ca9..dfa7520f8 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -20,6 +20,14 @@ 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 ROLLBAR_CLIENT_TOKEN = process.env.ROLLBAR_CLIENT_TOKEN; +export const INSTANT_ENVIRONMENT = process.env.INSTANT_ENVIRONMENT as + | 'dogfood' + | 'staging' + | 'development' + | 'production' + | undefined; +export const ROLLBAR_ENABLED = process.env.ROLLBAR_ENABLED; export const COINBASE_WALLET_IOS_APP_STORE_URL = 'https://itunes.apple.com/us/app/coinbase-wallet/id1278383455?mt=8'; export const COINBASE_WALLET_ANDROID_APP_STORE_URL = 'https://play.google.com/store/apps/details?id=org.toshi&hl=en'; export const COINBASE_WALLET_SITE_URL = 'https://wallet.coinbase.com/'; diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 5d30388b8..5382494f2 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -9,6 +9,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'; @@ -22,6 +23,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) => { @@ -36,6 +38,7 @@ export const asyncData = { errorFlasher.flashNewErrorMessage(dispatch, errorMessage); // On error, just specify that none are available dispatch(actions.setAvailableAssets([])); + errorReporter.report(e); } }, fetchAccountInfoAndDispatchToStore: async ( @@ -75,6 +78,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 2fd16d781..4f97622e1 100644 --- a/packages/instant/src/util/buy_quote_updater.ts +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -9,6 +9,7 @@ import { Action, actions } from '../redux/actions'; import { AffiliateInfo, ERC20Asset } from '../types'; import { assetUtils } from '../util/asset'; import { errorFlasher } from '../util/error_flasher'; +import { errorReporter } from '../util/error_reporter'; export const buyQuoteUpdater = { updateBuyQuoteAsync: async ( @@ -29,29 +30,17 @@ 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()); - 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..89c5b6bd3 --- /dev/null +++ b/packages/instant/src/util/error_reporter.ts @@ -0,0 +1,61 @@ +import { logUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { INSTANT_ENVIRONMENT, 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 && INSTANT_ENVIRONMENT && ROLLBAR_ENABLED) { + rollbar = new Rollbar({ + accessToken: ROLLBAR_CLIENT_TOKEN, + captureUncaught: true, + captureUnhandledRejections: true, + enabled: true, + itemsPerMinute: 10, + maxItems: 500, + payload: { + environment: INSTANT_ENVIRONMENT, + client: { + javascript: { + source_map_enabled: true, + code_version: process.env.GIT_SHA, + guess_uncaught_frames: true, + }, + }, + }, + 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 78ec3b3cc..10670b278 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 interface HeapAnalytics { loaded: boolean; @@ -105,8 +106,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 41276809c..7149793c4 100644 --- a/packages/instant/webpack.config.js +++ b/packages/instant/webpack.config.js @@ -1,6 +1,7 @@ const childProcess = require('child_process'); const ip = require('ip'); const path = require('path'); +const RollbarSourceMapPlugin = require('rollbar-sourcemap-webpack-plugin'); const webpack = require('webpack'); // The common js bundle (not this one) is built using tsc. @@ -11,22 +12,104 @@ const GIT_SHA = childProcess .toString() .trim(); -const HEAP_PRODUCTION_ENV_VAR_NAME = 'INSTANT_HEAP_ANALYTICS_ID_PRODUCTION'; -const HEAP_DEVELOPMENT_ENV_VAR_NAME = 'INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT'; -const getHeapAnalyticsId = modeName => { - if (modeName === 'production') { - return process.env[HEAP_PRODUCTION_ENV_VAR_NAME]; +const getEnvironmentName = (env, argv) => { + if (env && env.dogfood) { + return 'dogfood'; + } else if (env && env.staging) { + return 'staging'; } - if (modeName === 'development') { - return process.env[HEAP_DEVELOPMENT_ENV_VAR_NAME]; + // argv.mode should be 'development' or 'production' + return argv.mode; +}; + +const getHeapAnalyticsId = environmentName => { + if (environmentName === 'production') { + return process.env['INSTANT_HEAP_ANALYTICS_ID_PRODUCTION']; + } + + if (environmentName === 'development' || environmentName === 'dogfood' || environmentName === 'staging') { + return process.env['INSTANT_HEAP_ANALYTICS_ID_DEVELOPMENT']; } return undefined; }; +const ROLLBAR_PUBLISH_TOKEN_ENV_NAME = 'INSTANT_ROLLBAR_PUBLISH_TOKEN'; +const ROLLBAR_CLIENT_TOKEN_ENV_NAME = 'INSTANT_ROLLBAR_CLIENT_TOKEN'; +const getRollbarSourceMapPlugin = environmentName => { + if (!environmentName) { + return undefined; + } + + const publishToken = process.env[ROLLBAR_PUBLISH_TOKEN_ENV_NAME]; + if (!publishToken) { + return undefined; + } + + let rollbarPublicPath; + if (environmentName === 'dogfood') { + rollbarPublicPath = 'http://0x-instant-dogfood.s3-website-us-east-1.amazonaws.com'; + } else if (environmentName === 'staging') { + rollbarPublicPath = 'http://0x-instant-staging.s3-website-us-east-1.amazonaws.com'; + } // TODO(sk): When we decide on JS cdn, add public path here + + if (!rollbarPublicPath) { + console.log('No rollbar public path'); + return undefined; + } + + const rollbarPluginOptions = { + accessToken: publishToken, + version: GIT_SHA, + publicPath: rollbarPublicPath, + }; + return new RollbarSourceMapPlugin(rollbarPluginOptions); +}; +const validateRollbarPresence = (environmentName, rollbarEnabled, rollbarSourceMapPlugin) => { + const requiresRollbar = environmentName === 'dogfood' || environmentName === 'staging'; + if (!requiresRollbar) { + return; + } + if (!rollbarEnabled || !rollbarSourceMapPlugin) { + throw new Error( + `Rollbar env vars must be set to build for ${environmentName}. Please set ${ROLLBAR_CLIENT_TOKEN_ENV_NAME} to a rollbar access token with post_client_item permissions, and ${ROLLBAR_PUBLISH_TOKEN_ENV_NAME} to a rollbar access token with post_server_item permissions.`, + ); + } +}; + module.exports = (env, argv) => { + const environmentName = getEnvironmentName(env, argv); const outputPath = process.env.WEBPACK_OUTPUT_PATH || 'umd'; + + const envVars = { + GIT_SHA: JSON.stringify(GIT_SHA), + NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version), + HEAP_ANALYTICS_ID: getHeapAnalyticsId(environmentName), + INSTANT_ENVIRONMENT: JSON.stringify(environmentName), + ROLLBAR_CLIENT_TOKEN: JSON.stringify(process.env[ROLLBAR_CLIENT_TOKEN_ENV_NAME]), + }; + + const canRollbarBeEnabled = + environmentName === 'development' ? process.env.INSTANT_ROLLBAR_FORCE_DEVELOPMENT_REPORT : true; + if (envVars.INSTANT_ENVIRONMENT && envVars.ROLLBAR_CLIENT_TOKEN && canRollbarBeEnabled) { + envVars['ROLLBAR_ENABLED'] = JSON.stringify(true); + } + + let plugins = [ + new webpack.DefinePlugin({ + 'process.env': envVars, + }), + ]; + const rollbarSourceMapPlugin = getRollbarSourceMapPlugin(environmentName); + if (rollbarSourceMapPlugin) { + console.log('Using rollbar source map plugin'); + plugins = plugins.concat(rollbarSourceMapPlugin); + } else { + console.log('Not using rollbar source map plugin'); + } + validateRollbarPresence(environmentName, envVars['ROLLBAR_ENABLED'], rollbarSourceMapPlugin); + const config = { entry: { instant: './src/index.umd.ts', @@ -37,15 +120,7 @@ module.exports = (env, argv) => { library: 'zeroExInstant', libraryTarget: 'umd', }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env': { - GIT_SHA: JSON.stringify(GIT_SHA), - HEAP_ANALYTICS_ID: getHeapAnalyticsId(argv.mode), - NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version), - }, - }), - ], + plugins, devtool: 'source-map', resolve: { extensions: ['.js', '.json', '.ts', '.tsx'], @@ -60,6 +135,15 @@ module.exports = (env, argv) => { 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: { |