aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant
diff options
context:
space:
mode:
Diffstat (limited to 'packages/instant')
-rw-r--r--packages/instant/package.json3
-rw-r--r--packages/instant/src/components/zero_ex_instant_provider.tsx2
-rw-r--r--packages/instant/src/constants.ts11
-rw-r--r--packages/instant/src/redux/async_data.ts4
-rw-r--r--packages/instant/src/util/asset.ts17
-rw-r--r--packages/instant/src/util/buy_quote_updater.ts31
-rw-r--r--packages/instant/src/util/error_reporter.ts62
-rw-r--r--packages/instant/src/util/gas_price_estimator.ts5
-rw-r--r--packages/instant/src/util/heap.ts3
-rw-r--r--packages/instant/test/util/asset.test.ts33
-rw-r--r--packages/instant/webpack.config.js108
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);
};