aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/instant/src/util')
-rw-r--r--packages/instant/src/util/analytics.ts218
-rw-r--r--packages/instant/src/util/asset.ts24
-rw-r--r--packages/instant/src/util/buy_quote_updater.ts40
-rw-r--r--packages/instant/src/util/env.ts9
-rw-r--r--packages/instant/src/util/error_reporter.ts79
-rw-r--r--packages/instant/src/util/format.ts30
-rw-r--r--packages/instant/src/util/gas_price_estimator.ts5
-rw-r--r--packages/instant/src/util/heap.ts7
-rw-r--r--packages/instant/src/util/heartbeater_factory.ts12
-rw-r--r--packages/instant/src/util/provider_state_factory.ts33
10 files changed, 398 insertions, 59 deletions
diff --git a/packages/instant/src/util/analytics.ts b/packages/instant/src/util/analytics.ts
index 2ffaac1dd..4faeaaf5a 100644
--- a/packages/instant/src/util/analytics.ts
+++ b/packages/instant/src/util/analytics.ts
@@ -1,25 +1,73 @@
-import { ObjectMap } from '@0x/types';
+import { BuyQuote } from '@0x/asset-buyer';
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
-import { heapUtil } from './heap';
+import { GIT_SHA, HEAP_ENABLED, INSTANT_DISCHARGE_TARGET, NODE_ENV, NPM_PACKAGE_VERSION } from '../constants';
+import {
+ AffiliateInfo,
+ Asset,
+ BaseCurrency,
+ Network,
+ OrderProcessState,
+ OrderSource,
+ ProviderState,
+ QuoteFetchOrigin,
+ WalletSuggestion,
+} from '../types';
-let isDisabled = false;
+import { EventProperties, heapUtil } from './heap';
+
+let isDisabledViaConfig = false;
export const disableAnalytics = (shouldDisableAnalytics: boolean) => {
- isDisabled = shouldDisableAnalytics;
+ isDisabledViaConfig = shouldDisableAnalytics;
};
export const evaluateIfEnabled = (fnCall: () => void) => {
- if (isDisabled) {
+ if (isDisabledViaConfig) {
return;
}
- fnCall();
+ if (HEAP_ENABLED) {
+ fnCall();
+ }
};
enum EventNames {
INSTANT_OPENED = 'Instant - Opened',
- WALLET_READY = 'Wallet - Ready',
+ INSTANT_CLOSED = 'Instant - Closed',
+ ACCOUNT_LOCKED = 'Account - Locked',
+ ACCOUNT_READY = 'Account - Ready',
+ ACCOUNT_UNLOCK_REQUESTED = 'Account - Unlock Requested',
+ ACCOUNT_UNLOCK_DENIED = 'Account - Unlock Denied',
+ ACCOUNT_ADDRESS_CHANGED = 'Account - Address Changed',
+ BASE_CURRENCY_CHANGED = 'Base Currency - Changed',
+ PAYMENT_METHOD_DROPDOWN_OPENED = 'Payment Method - Dropdown Opened',
+ PAYMENT_METHOD_OPENED_ETHERSCAN = 'Payment Method - Opened Etherscan',
+ PAYMENT_METHOD_COPIED_ADDRESS = 'Payment Method - Copied Address',
+ BUY_NOT_ENOUGH_ETH = 'Buy - Not Enough Eth',
+ BUY_STARTED = 'Buy - Started',
+ BUY_SIGNATURE_DENIED = 'Buy - Signature Denied',
+ BUY_SIMULATION_FAILED = 'Buy - Simulation Failed',
+ BUY_UNKNOWN_ERROR = 'Buy - Unknown Error',
+ BUY_TX_SUBMITTED = 'Buy - Tx Submitted',
+ BUY_TX_SUCCEEDED = 'Buy - Tx Succeeded',
+ BUY_TX_FAILED = 'Buy - Tx Failed',
+ USD_PRICE_FETCH_FAILED = 'USD Price - Fetch Failed',
+ INSTALL_WALLET_CLICKED = 'Install Wallet - Clicked',
+ INSTALL_WALLET_MODAL_OPENED = 'Install Wallet - Modal - Opened',
+ INSTALL_WALLET_MODAL_CLICKED_EXPLANATION = 'Install Wallet - Modal - Clicked Explanation',
+ INSTALL_WALLET_MODAL_CLICKED_GET = 'Install Wallet - Modal - Clicked Get',
+ INSTALL_WALLET_MODAL_CLOSED = 'Install Wallet - Modal - Closed',
+ TOKEN_SELECTOR_OPENED = 'Token Selector - Opened',
+ TOKEN_SELECTOR_CLOSED = 'Token Selector - Closed',
+ TOKEN_SELECTOR_CHOSE = 'Token Selector - Chose',
+ TOKEN_SELECTOR_SEARCHED = 'Token Selector - Searched',
+ TRANSACTION_VIEWED = 'Transaction - Viewed',
+ QUOTE_FETCHED = 'Quote - Fetched',
+ QUOTE_ERROR = 'Quote - Error',
}
-const track = (eventName: EventNames, eventData: ObjectMap<string | number> = {}): void => {
+
+const track = (eventName: EventNames, eventProperties: EventProperties = {}): void => {
evaluateIfEnabled(() => {
- heapUtil.evaluateHeapCall(heap => heap.track(eventName, eventData));
+ heapUtil.evaluateHeapCall(heap => heap.track(eventName, eventProperties));
});
};
function trackingEventFnWithoutPayload(eventName: EventNames): () => void {
@@ -28,25 +76,57 @@ function trackingEventFnWithoutPayload(eventName: EventNames): () => void {
};
}
// tslint:disable-next-line:no-unused-variable
-function trackingEventFnWithPayload<T extends ObjectMap<string | number>>(
- eventName: EventNames,
-): (eventDataProperties: T) => void {
- return (eventDataProperties: T) => {
- track(eventName, eventDataProperties);
+function trackingEventFnWithPayload(eventName: EventNames): (eventProperties: EventProperties) => void {
+ return (eventProperties: EventProperties) => {
+ track(eventName, eventProperties);
};
}
+const buyQuoteEventProperties = (buyQuote: BuyQuote) => {
+ const assetBuyAmount = buyQuote.assetBuyAmount.toString();
+ const assetEthAmount = buyQuote.worstCaseQuoteInfo.assetEthAmount.toString();
+ const feeEthAmount = buyQuote.worstCaseQuoteInfo.feeEthAmount.toString();
+ const totalEthAmount = buyQuote.worstCaseQuoteInfo.totalEthAmount.toString();
+ const feePercentage = !_.isUndefined(buyQuote.feePercentage) ? buyQuote.feePercentage.toString() : 0;
+ const hasFeeOrders = !_.isEmpty(buyQuote.feeOrders) ? 'true' : 'false';
+ return {
+ assetBuyAmount,
+ assetEthAmount,
+ feeEthAmount,
+ totalEthAmount,
+ feePercentage,
+ hasFeeOrders,
+ };
+};
+
export interface AnalyticsUserOptions {
- ethAddress?: string;
- ethBalanceInUnitAmount?: string;
+ lastKnownEthAddress?: string;
+ lastEthBalanceInUnitAmount?: string;
}
export interface AnalyticsEventOptions {
embeddedHost?: string;
embeddedUrl?: string;
+ ethBalanceInUnitAmount?: string;
+ ethAddress?: string;
networkId?: number;
providerName?: string;
+ providerDisplayName?: string;
gitSha?: string;
npmVersion?: string;
+ instantEnvironment?: string;
+ orderSource?: string;
+ affiliateAddress?: string;
+ affiliateFeePercent?: number;
+ numberAvailableAssets?: number;
+ selectedAssetName?: string;
+ selectedAssetSymbol?: string;
+ selectedAssetData?: string;
+ selectedAssetDecimals?: number;
+ baseCurrency?: string;
+}
+export enum TokenSelectorClosedVia {
+ ClickedX = 'Clicked X',
+ TokenChose = 'Token Chose',
}
export const analytics = {
addUserProperties: (properties: AnalyticsUserOptions): void => {
@@ -59,6 +139,110 @@ export const analytics = {
heapUtil.evaluateHeapCall(heap => heap.addEventProperties(properties));
});
},
- trackWalletReady: trackingEventFnWithoutPayload(EventNames.WALLET_READY),
+ generateEventProperties: (
+ network: Network,
+ orderSource: OrderSource,
+ providerState: ProviderState,
+ window: Window,
+ selectedAsset?: Asset,
+ affiliateInfo?: AffiliateInfo,
+ baseCurrency?: BaseCurrency,
+ ): AnalyticsEventOptions => {
+ const affiliateAddress = affiliateInfo ? affiliateInfo.feeRecipient : 'none';
+ const affiliateFeePercent = affiliateInfo ? parseFloat(affiliateInfo.feePercentage.toFixed(4)) : 0;
+ const orderSourceName = typeof orderSource === 'string' ? orderSource : 'provided';
+ const eventOptions: AnalyticsEventOptions = {
+ embeddedHost: window.location.host,
+ embeddedUrl: window.location.href,
+ networkId: network,
+ providerName: providerState.name,
+ providerDisplayName: providerState.displayName,
+ gitSha: GIT_SHA,
+ npmVersion: NPM_PACKAGE_VERSION,
+ orderSource: orderSourceName,
+ affiliateAddress,
+ affiliateFeePercent,
+ selectedAssetName: selectedAsset ? selectedAsset.metaData.name : 'none',
+ selectedAssetData: selectedAsset ? selectedAsset.assetData : 'none',
+ instantEnvironment: INSTANT_DISCHARGE_TARGET || `Local ${NODE_ENV}`,
+ baseCurrency,
+ };
+ return eventOptions;
+ },
trackInstantOpened: trackingEventFnWithoutPayload(EventNames.INSTANT_OPENED),
+ trackInstantClosed: trackingEventFnWithoutPayload(EventNames.INSTANT_CLOSED),
+ trackAccountLocked: trackingEventFnWithoutPayload(EventNames.ACCOUNT_LOCKED),
+ trackAccountReady: (address: string) => trackingEventFnWithPayload(EventNames.ACCOUNT_READY)({ address }),
+ trackAccountUnlockRequested: trackingEventFnWithoutPayload(EventNames.ACCOUNT_UNLOCK_REQUESTED),
+ trackAccountUnlockDenied: trackingEventFnWithoutPayload(EventNames.ACCOUNT_UNLOCK_DENIED),
+ trackAccountAddressChanged: (address: string) =>
+ trackingEventFnWithPayload(EventNames.ACCOUNT_ADDRESS_CHANGED)({ address }),
+ trackBaseCurrencyChanged: (currencyChangedTo: BaseCurrency) =>
+ trackingEventFnWithPayload(EventNames.BASE_CURRENCY_CHANGED)({ currencyChangedTo }),
+ trackPaymentMethodDropdownOpened: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_DROPDOWN_OPENED),
+ trackPaymentMethodOpenedEtherscan: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_OPENED_ETHERSCAN),
+ trackPaymentMethodCopiedAddress: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_COPIED_ADDRESS),
+ trackBuyNotEnoughEth: (buyQuote: BuyQuote) =>
+ trackingEventFnWithPayload(EventNames.BUY_NOT_ENOUGH_ETH)(buyQuoteEventProperties(buyQuote)),
+ trackBuyStarted: (buyQuote: BuyQuote) =>
+ trackingEventFnWithPayload(EventNames.BUY_STARTED)(buyQuoteEventProperties(buyQuote)),
+ trackBuySignatureDenied: (buyQuote: BuyQuote) =>
+ trackingEventFnWithPayload(EventNames.BUY_SIGNATURE_DENIED)(buyQuoteEventProperties(buyQuote)),
+ trackBuySimulationFailed: (buyQuote: BuyQuote) =>
+ trackingEventFnWithPayload(EventNames.BUY_SIMULATION_FAILED)(buyQuoteEventProperties(buyQuote)),
+ trackBuyUnknownError: (buyQuote: BuyQuote, errorMessage: string) =>
+ trackingEventFnWithPayload(EventNames.BUY_UNKNOWN_ERROR)({
+ ...buyQuoteEventProperties(buyQuote),
+ errorMessage,
+ }),
+ trackBuyTxSubmitted: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) =>
+ trackingEventFnWithPayload(EventNames.BUY_TX_SUBMITTED)({
+ ...buyQuoteEventProperties(buyQuote),
+ txHash,
+ expectedTxTimeMs: expectedEndTimeUnix - startTimeUnix,
+ }),
+ trackBuyTxSucceeded: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) =>
+ trackingEventFnWithPayload(EventNames.BUY_TX_SUCCEEDED)({
+ ...buyQuoteEventProperties(buyQuote),
+ txHash,
+ expectedTxTimeMs: expectedEndTimeUnix - startTimeUnix,
+ actualTxTimeMs: new Date().getTime() - startTimeUnix,
+ }),
+ trackBuyTxFailed: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) =>
+ trackingEventFnWithPayload(EventNames.BUY_TX_FAILED)({
+ ...buyQuoteEventProperties(buyQuote),
+ txHash,
+ expectedTxTimeMs: expectedEndTimeUnix - startTimeUnix,
+ actualTxTimeMs: new Date().getTime() - startTimeUnix,
+ }),
+ trackInstallWalletClicked: (walletSuggestion: WalletSuggestion) =>
+ trackingEventFnWithPayload(EventNames.INSTALL_WALLET_CLICKED)({ walletSuggestion }),
+ trackInstallWalletModalClickedExplanation: trackingEventFnWithoutPayload(
+ EventNames.INSTALL_WALLET_MODAL_CLICKED_EXPLANATION,
+ ),
+ trackInstallWalletModalClickedGet: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLICKED_GET),
+ trackInstallWalletModalOpened: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_OPENED),
+ trackInstallWalletModalClosed: trackingEventFnWithoutPayload(EventNames.INSTALL_WALLET_MODAL_CLOSED),
+ trackTokenSelectorOpened: trackingEventFnWithoutPayload(EventNames.TOKEN_SELECTOR_OPENED),
+ trackTokenSelectorClosed: (closedVia: TokenSelectorClosedVia) =>
+ trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CLOSED)({ closedVia }),
+ trackTokenSelectorChose: (payload: { assetName: string; assetData: string }) =>
+ trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_CHOSE)(payload),
+ trackTokenSelectorSearched: (searchText: string) =>
+ trackingEventFnWithPayload(EventNames.TOKEN_SELECTOR_SEARCHED)({ searchText }),
+ trackTransactionViewed: (orderProcesState: OrderProcessState) =>
+ trackingEventFnWithPayload(EventNames.TRANSACTION_VIEWED)({ orderState: orderProcesState }),
+ trackQuoteFetched: (buyQuote: BuyQuote, fetchOrigin: QuoteFetchOrigin) =>
+ trackingEventFnWithPayload(EventNames.QUOTE_FETCHED)({
+ ...buyQuoteEventProperties(buyQuote),
+ fetchOrigin,
+ }),
+ trackQuoteError: (errorMessage: string, assetBuyAmount: BigNumber, fetchOrigin: QuoteFetchOrigin) => {
+ trackingEventFnWithPayload(EventNames.QUOTE_ERROR)({
+ errorMessage,
+ assetBuyAmount: assetBuyAmount.toString(),
+ fetchOrigin,
+ });
+ },
+ trackUsdPriceFailed: trackingEventFnWithoutPayload(EventNames.USD_PRICE_FETCH_FAILED),
};
diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts
index 40560d3eb..faaeb7c22 100644
--- a/packages/instant/src/util/asset.ts
+++ b/packages/instant/src/util/asset.ts
@@ -1,6 +1,8 @@
+import { AssetBuyerError } from '@0x/asset-buyer';
import { AssetProxyId, ObjectMap } from '@0x/types';
import * as _ from 'lodash';
+import { DEFAULT_UNKOWN_ASSET_NAME } from '../constants';
import { assetDataNetworkMapping } from '../data/asset_data_network_mapping';
import { Asset, AssetMetaData, ERC20Asset, Network, ZeroExInstantError } from '../types';
@@ -25,7 +27,7 @@ export const assetUtils = {
return;
}
return {
- assetData,
+ assetData: assetData.toLowerCase(),
metaData,
};
},
@@ -35,7 +37,7 @@ export const assetUtils = {
network: Network,
): Asset => {
return {
- assetData,
+ assetData: assetData.toLowerCase(),
metaData: assetUtils.getMetaDataOrThrow(assetData, assetMetaDataMap, network),
};
},
@@ -70,7 +72,7 @@ export const assetUtils = {
}
return metaData;
},
- bestNameForAsset: (asset?: Asset, defaultName: string = '???'): string => {
+ bestNameForAsset: (asset?: Asset, defaultName: string = DEFAULT_UNKOWN_ASSET_NAME): string => {
if (_.isUndefined(asset)) {
return defaultName;
}
@@ -106,4 +108,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..6191c92e3 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';
@@ -6,9 +6,11 @@ import { Dispatch } from 'redux';
import { oc } from 'ts-optchain';
import { Action, actions } from '../redux/actions';
-import { AffiliateInfo, ERC20Asset } from '../types';
+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 (
@@ -16,7 +18,12 @@ export const buyQuoteUpdater = {
dispatch: Dispatch<Action>,
asset: ERC20Asset,
assetUnitAmount: BigNumber,
- options: { setPending: boolean; dispatchErrors: boolean; affiliateInfo?: AffiliateInfo },
+ fetchOrigin: QuoteFetchOrigin,
+ options: {
+ setPending: boolean;
+ dispatchErrors: boolean;
+ affiliateInfo?: AffiliateInfo;
+ },
): Promise<void> => {
// get a new buy quote.
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals);
@@ -29,34 +36,21 @@ export const buyQuoteUpdater = {
try {
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
} catch (error) {
+ const errorMessage = assetUtils.assetBuyerErrorMessage(asset, error);
+
+ errorReporter.report(error);
+ analytics.trackQuoteError(error.message ? error.message : 'other', baseUnitValue, fetchOrigin);
+
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
errorFlasher.clearError(dispatch);
// invalidate the last buy quote.
dispatch(actions.updateLatestBuyQuote(newBuyQuote));
+ analytics.trackQuoteFetched(newBuyQuote, fetchOrigin);
},
};
diff --git a/packages/instant/src/util/env.ts b/packages/instant/src/util/env.ts
index 4a32f9cb1..aedf4f5d6 100644
--- a/packages/instant/src/util/env.ts
+++ b/packages/instant/src/util/env.ts
@@ -44,6 +44,8 @@ export const envUtil = {
getProviderType(provider: Provider): ProviderType | undefined {
if (provider.constructor.name === 'EthereumProvider') {
return ProviderType.Mist;
+ } else if ((provider as any).isTrust) {
+ return ProviderType.TrustWallet;
} else if ((provider as any).isParity) {
return ProviderType.Parity;
} else if ((provider as any).isMetaMask) {
@@ -62,4 +64,11 @@ export const envUtil = {
}
return PROVIDER_TYPE_TO_NAME[providerTypeIfExists];
},
+ getProviderDisplayName(provider: Provider): string {
+ const providerTypeIfExists = envUtil.getProviderType(provider);
+ if (_.isUndefined(providerTypeIfExists)) {
+ return 'Wallet';
+ }
+ return PROVIDER_TYPE_TO_NAME[providerTypeIfExists];
+ },
};
diff --git a/packages/instant/src/util/error_reporter.ts b/packages/instant/src/util/error_reporter.ts
new file mode 100644
index 000000000..8d7481684
--- /dev/null
+++ b/packages/instant/src/util/error_reporter.ts
@@ -0,0 +1,79 @@
+import { logUtils } from '@0x/utils';
+import * as _ from 'lodash';
+
+import {
+ GIT_SHA,
+ HOST_DOMAINS_EXTERNAL,
+ HOST_DOMAINS_LOCAL,
+ INSTANT_DISCHARGE_TARGET,
+ NODE_ENV,
+ 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');
+
+const getRollbarHostDomains = (): string[] => {
+ if (NODE_ENV === 'development') {
+ return HOST_DOMAINS_EXTERNAL.concat(HOST_DOMAINS_LOCAL);
+ } else {
+ return HOST_DOMAINS_EXTERNAL;
+ }
+};
+
+let rollbar: any;
+// Configures rollbar and sets up error catching
+export const setupRollbar = (): any => {
+ if (_.isUndefined(rollbar) && ROLLBAR_CLIENT_TOKEN && ROLLBAR_ENABLED) {
+ const hostDomains = getRollbarHostDomains();
+ rollbar = new Rollbar({
+ accessToken: ROLLBAR_CLIENT_TOKEN,
+ captureUncaught: true,
+ captureUnhandledRejections: true,
+ enabled: true,
+ itemsPerMinute: 10,
+ maxItems: 500,
+ payload: {
+ environment: INSTANT_DISCHARGE_TARGET || `Local ${NODE_ENV}`,
+ client: {
+ javascript: {
+ source_map_enabled: true,
+ code_version: GIT_SHA,
+ guess_uncaught_frames: true,
+ },
+ },
+ },
+ hostWhiteList: hostDomains,
+ 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/format.ts b/packages/instant/src/util/format.ts
index e9c432b2f..4adb63e21 100644
--- a/packages/instant/src/util/format.ts
+++ b/packages/instant/src/util/format.ts
@@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
-import { ETH_DECIMALS } from '../constants';
+import { BIG_NUMBER_ZERO, ETH_DECIMALS } from '../constants';
export const format = {
ethBaseUnitAmount: (
@@ -20,24 +20,38 @@ export const format = {
ethUnitAmount?: BigNumber,
decimalPlaces: number = 4,
defaultText: React.ReactNode = '0 ETH',
+ minUnitAmountToDisplay: BigNumber = new BigNumber('0.00001'),
): React.ReactNode => {
if (_.isUndefined(ethUnitAmount)) {
return defaultText;
}
- const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
- return `${roundedAmount} ETH`;
+ let roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
+
+ if (roundedAmount.eq(BIG_NUMBER_ZERO) && ethUnitAmount.greaterThan(BIG_NUMBER_ZERO)) {
+ // Sometimes for small ETH amounts (i.e. 0.000045) the amount rounded to 4 decimalPlaces is 0
+ // If that is the case, show to 1 significant digit
+ roundedAmount = new BigNumber(ethUnitAmount.toPrecision(1));
+ }
+
+ const displayAmount =
+ roundedAmount.greaterThan(BIG_NUMBER_ZERO) && roundedAmount.lessThan(minUnitAmountToDisplay)
+ ? `< ${minUnitAmountToDisplay.toString()}`
+ : roundedAmount.toString();
+
+ return `${displayAmount} ETH`;
},
ethBaseUnitAmountInUsd: (
ethBaseUnitAmount?: BigNumber,
ethUsdPrice?: BigNumber,
decimalPlaces: number = 2,
defaultText: React.ReactNode = '$0.00',
+ minUnitAmountToDisplay: BigNumber = new BigNumber('0.00001'),
): React.ReactNode => {
if (_.isUndefined(ethBaseUnitAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText;
}
const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseUnitAmount, ETH_DECIMALS);
- return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces);
+ return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces, minUnitAmountToDisplay);
},
ethUnitAmountInUsd: (
ethUnitAmount?: BigNumber,
@@ -48,7 +62,13 @@ export const format = {
if (_.isUndefined(ethUnitAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText;
}
- return `$${ethUnitAmount.mul(ethUsdPrice).toFixed(decimalPlaces)}`;
+ const rawUsdPrice = ethUnitAmount.mul(ethUsdPrice);
+ const roundedUsdPrice = rawUsdPrice.toFixed(decimalPlaces);
+ if (roundedUsdPrice === '0.00' && rawUsdPrice.gt(BIG_NUMBER_ZERO)) {
+ return '<$0.01';
+ } else {
+ return `$${roundedUsdPrice}`;
+ }
},
ethAddress: (address: string): string => {
return `0x${address.slice(2, 7)}…${address.slice(-5)}`;
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..279ff3059 100644
--- a/packages/instant/src/util/heap.ts
+++ b/packages/instant/src/util/heap.ts
@@ -5,12 +5,15 @@ 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>;
export interface HeapAnalytics {
loaded: boolean;
appid: string;
identify(id: string, idType: string): void;
- track(eventName: string, eventProperties?: ObjectMap<string | number>): void;
+ track(eventName: string, eventProperties?: EventProperties): void;
resetIdentity(): void;
addUserProperties(properties: AnalyticsUserOptions): void;
addEventProperties(properties: AnalyticsEventOptions): void;
@@ -105,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/src/util/heartbeater_factory.ts b/packages/instant/src/util/heartbeater_factory.ts
index 2b852fb0d..cf29bf3ea 100644
--- a/packages/instant/src/util/heartbeater_factory.ts
+++ b/packages/instant/src/util/heartbeater_factory.ts
@@ -1,5 +1,6 @@
import { asyncData } from '../redux/async_data';
import { Store } from '../redux/store';
+import { QuoteFetchOrigin } from '../types';
import { Heartbeater } from './heartbeater';
@@ -17,8 +18,13 @@ export const generateAccountHeartbeater = (options: HeartbeatFactoryOptions): He
export const generateBuyQuoteHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => {
const { store, shouldPerformImmediatelyOnStart } = options;
return new Heartbeater(async () => {
- await asyncData.fetchCurrentBuyQuoteAndDispatchToStore(store.getState(), store.dispatch, {
- updateSilently: true,
- });
+ await asyncData.fetchCurrentBuyQuoteAndDispatchToStore(
+ store.getState(),
+ store.dispatch,
+ QuoteFetchOrigin.Heartbeat,
+ {
+ updateSilently: true,
+ },
+ );
}, shouldPerformImmediatelyOnStart);
};
diff --git a/packages/instant/src/util/provider_state_factory.ts b/packages/instant/src/util/provider_state_factory.ts
index 7c788dff2..bd2d6dad5 100644
--- a/packages/instant/src/util/provider_state_factory.ts
+++ b/packages/instant/src/util/provider_state_factory.ts
@@ -10,27 +10,40 @@ import { assetBuyerFactory } from './asset_buyer_factory';
import { providerFactory } from './provider_factory';
export const providerStateFactory = {
- getInitialProviderState: (orderSource: OrderSource, network: Network, provider?: Provider): ProviderState => {
+ getInitialProviderState: (
+ orderSource: OrderSource,
+ network: Network,
+ provider?: Provider,
+ walletDisplayName?: string,
+ ): ProviderState => {
if (!_.isUndefined(provider)) {
- return providerStateFactory.getInitialProviderStateFromProvider(orderSource, network, provider);
+ return providerStateFactory.getInitialProviderStateFromProvider(
+ orderSource,
+ network,
+ provider,
+ walletDisplayName,
+ );
}
const providerStateFromWindowIfExits = providerStateFactory.getInitialProviderStateFromWindowIfExists(
orderSource,
network,
+ walletDisplayName,
);
if (providerStateFromWindowIfExits) {
return providerStateFromWindowIfExits;
} else {
- return providerStateFactory.getInitialProviderStateFallback(orderSource, network);
+ return providerStateFactory.getInitialProviderStateFallback(orderSource, network, walletDisplayName);
}
},
getInitialProviderStateFromProvider: (
orderSource: OrderSource,
network: Network,
provider: Provider,
+ walletDisplayName?: string,
): ProviderState => {
const providerState: ProviderState = {
name: envUtil.getProviderName(provider),
+ displayName: walletDisplayName || envUtil.getProviderDisplayName(provider),
provider,
web3Wrapper: new Web3Wrapper(provider),
assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),
@@ -38,11 +51,16 @@ export const providerStateFactory = {
};
return providerState;
},
- getInitialProviderStateFromWindowIfExists: (orderSource: OrderSource, network: Network): Maybe<ProviderState> => {
+ getInitialProviderStateFromWindowIfExists: (
+ orderSource: OrderSource,
+ network: Network,
+ walletDisplayName?: string,
+ ): Maybe<ProviderState> => {
const injectedProviderIfExists = providerFactory.getInjectedProviderIfExists();
if (!_.isUndefined(injectedProviderIfExists)) {
const providerState: ProviderState = {
name: envUtil.getProviderName(injectedProviderIfExists),
+ displayName: walletDisplayName || envUtil.getProviderDisplayName(injectedProviderIfExists),
provider: injectedProviderIfExists,
web3Wrapper: new Web3Wrapper(injectedProviderIfExists),
assetBuyer: assetBuyerFactory.getAssetBuyer(injectedProviderIfExists, orderSource, network),
@@ -53,10 +71,15 @@ export const providerStateFactory = {
return undefined;
}
},
- getInitialProviderStateFallback: (orderSource: OrderSource, network: Network): ProviderState => {
+ getInitialProviderStateFallback: (
+ orderSource: OrderSource,
+ network: Network,
+ walletDisplayName?: string,
+ ): ProviderState => {
const provider = providerFactory.getFallbackNoSigningProvider(network);
const providerState: ProviderState = {
name: 'Fallback',
+ displayName: walletDisplayName || envUtil.getProviderDisplayName(provider),
provider,
web3Wrapper: new Web3Wrapper(provider),
assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),