aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website/ts')
-rw-r--r--packages/website/ts/blockchain.ts31
-rw-r--r--packages/website/ts/components/fill_order.tsx18
-rw-r--r--packages/website/ts/components/forms/subscribe_form.tsx4
-rw-r--r--packages/website/ts/components/generate_order/generate_order_form.tsx38
-rw-r--r--packages/website/ts/components/inputs/allowance_toggle.tsx12
-rw-r--r--packages/website/ts/components/onboarding/portal_onboarding_flow.tsx14
-rw-r--r--packages/website/ts/components/order_json.tsx4
-rw-r--r--packages/website/ts/components/portal/portal.tsx9
-rw-r--r--packages/website/ts/components/relayer_index/relayer_grid_tile.tsx8
-rw-r--r--packages/website/ts/components/relayer_index/relayer_top_tokens.tsx13
-rw-r--r--packages/website/ts/components/token_balances.tsx4
-rw-r--r--packages/website/ts/components/wallet/wallet.tsx8
-rw-r--r--packages/website/ts/components/wallet/wrap_ether_item.tsx31
-rw-r--r--packages/website/ts/index.tsx6
-rw-r--r--packages/website/ts/pages/wiki/wiki.tsx2
-rw-r--r--packages/website/ts/redux/analyticsMiddleware.ts41
-rw-r--r--packages/website/ts/redux/store.ts7
-rw-r--r--packages/website/ts/types.ts6
-rw-r--r--packages/website/ts/utils/analytics.ts104
-rw-r--r--packages/website/ts/utils/doc_utils.ts6
-rw-r--r--packages/website/ts/utils/fetch_utils.ts6
-rw-r--r--packages/website/ts/utils/utils.ts11
22 files changed, 241 insertions, 142 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts
index 1003cbd06..e8168d975 100644
--- a/packages/website/ts/blockchain.ts
+++ b/packages/website/ts/blockchain.ts
@@ -15,8 +15,9 @@ import {
ledgerEthereumBrowserClientFactoryAsync,
LedgerSubprovider,
RedundantSubprovider,
+ RPCSubprovider,
SignerSubprovider,
- Subprovider,
+ Web3ProviderEngine,
} from '@0xproject/subproviders';
import {
BlockParam,
@@ -60,9 +61,7 @@ import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';
-import ProviderEngine = require('web3-provider-engine');
import FilterSubprovider = require('web3-provider-engine/subproviders/filters');
-import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
import * as MintableArtifacts from '../contracts/Mintable.json';
@@ -148,7 +147,7 @@ export class Blockchain {
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
- const provider = new ProviderEngine();
+ const provider = new Web3ProviderEngine();
const ledgerWalletConfigs = {
networkId: networkIdIfExists,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
@@ -157,25 +156,21 @@ export class Blockchain {
provider.addProvider(ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists], publicNodeUrl => {
- return new RpcSubprovider({
- rpcUrl: publicNodeUrl,
- });
+ return new RPCSubprovider(publicNodeUrl);
});
- provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
+ provider.addProvider(new RedundantSubprovider(rpcSubproviders));
provider.start();
return [provider, ledgerSubprovider];
} else if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) {
// We catch all requests involving a users account and send it to the injectedWeb3
// instance. All other requests go to the public hosted node.
- const provider = new ProviderEngine();
+ const provider = new Web3ProviderEngine();
provider.addProvider(new SignerSubprovider(injectedWeb3.currentProvider));
provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => {
- return new RpcSubprovider({
- rpcUrl: publicNodeUrl,
- });
+ return new RPCSubprovider(publicNodeUrl);
});
- provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
+ provider.addProvider(new RedundantSubprovider(rpcSubproviders));
provider.start();
return [provider, undefined];
} else if (doesInjectedWeb3Exist) {
@@ -185,15 +180,13 @@ export class Blockchain {
// If no injectedWeb3 instance, all requests fallback to our public hosted mainnet/testnet node
// We do this so that users can still browse the 0x Portal DApp even if they do not have web3
// injected into their browser.
- const provider = new ProviderEngine();
+ const provider = new Web3ProviderEngine();
provider.addProvider(new FilterSubprovider());
const networkId = constants.NETWORK_ID_MAINNET;
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => {
- return new RpcSubprovider({
- rpcUrl: publicNodeUrl,
- });
+ return new RPCSubprovider(publicNodeUrl);
});
- provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
+ provider.addProvider(new RedundantSubprovider(rpcSubproviders));
provider.start();
return [provider, undefined];
}
@@ -794,7 +787,7 @@ export class Blockchain {
return tokenByAddress;
}
private async _onPageLoadInitFireAndForgetAsync(): Promise<void> {
- await utils.onPageLoadAsync(); // wait for page to load
+ await utils.onPageLoadPromise; // wait for page to load
const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync();
this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : constants.NETWORK_ID_MAINNET;
const injectedWeb3IfExists = Blockchain._getInjectedWeb3();
diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx
index 9360ba24c..7da2e0870 100644
--- a/packages/website/ts/components/fill_order.tsx
+++ b/packages/website/ts/components/fill_order.tsx
@@ -1,5 +1,5 @@
import { getOrderHashHex, isValidSignature } from '@0xproject/order-utils';
-import { colors, constants as sharedConstants } from '@0xproject/react-shared';
+import { colors } from '@0xproject/react-shared';
import { Order as ZeroExOrder } from '@0xproject/types';
import { BigNumber, logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
@@ -506,6 +506,10 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
await this._checkForUntrackedTokensAndAskToAddAsync();
}
+ private _trackOrderEvent(eventName: string): void {
+ const parsedOrder = this.state.parsedOrder;
+ analytics.trackOrderEvent(eventName, parsedOrder);
+ }
private async _onFillOrderClickFireAndForgetAsync(): Promise<void> {
if (this.props.blockchainErr !== BlockchainErrs.NoError || _.isEmpty(this.props.userAddress)) {
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
@@ -552,14 +556,12 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
});
return;
}
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- const eventLabel = `${parsedOrder.metadata.takerToken.symbol}-${networkName}`;
try {
const orderFilledAmount: BigNumber = await this.props.blockchain.fillOrderAsync(
signedOrder,
this.props.orderFillAmount,
);
- analytics.logEvent('Portal', 'Fill Order Success', eventLabel, parsedOrder.signedOrder.takerTokenAmount);
+ this._trackOrderEvent('Fill Order Success');
// After fill completes, let's force fetch the token balances
this.props.dispatcher.forceTokenStateRefetch();
this.setState({
@@ -573,7 +575,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
this.setState({
isFilling: false,
});
- analytics.logEvent('Portal', 'Fill Order Failure', eventLabel, parsedOrder.signedOrder.takerTokenAmount);
+ this._trackOrderEvent('Fill Order Failure');
const errMsg = `${err}`;
if (utils.didUserDenyWeb3Request(errMsg)) {
return;
@@ -628,8 +630,6 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
});
return;
}
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- const eventLabel = `${parsedOrder.metadata.makerToken.symbol}-${networkName}`;
try {
await this.props.blockchain.cancelOrderAsync(signedOrder, availableTakerTokenAmount);
this.setState({
@@ -638,7 +638,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
globalErrMsg: '',
unavailableTakerAmount: takerTokenAmount,
});
- analytics.logEvent('Portal', 'Cancel Order Success', eventLabel, parsedOrder.signedOrder.makerTokenAmount);
+ this._trackOrderEvent('Cancel Order Success');
return;
} catch (err) {
this.setState({
@@ -648,7 +648,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
if (utils.didUserDenyWeb3Request(errMsg)) {
return;
}
- analytics.logEvent('Portal', 'Cancel Order Failure', eventLabel, parsedOrder.signedOrder.makerTokenAmount);
+ this._trackOrderEvent('Cancel Order Failure');
globalErrMsg = 'Failed to cancel order, please refresh and try again';
logUtils.log(`${err}`);
this.setState({
diff --git a/packages/website/ts/components/forms/subscribe_form.tsx b/packages/website/ts/components/forms/subscribe_form.tsx
index 8ef58328e..761db7517 100644
--- a/packages/website/ts/components/forms/subscribe_form.tsx
+++ b/packages/website/ts/components/forms/subscribe_form.tsx
@@ -6,6 +6,7 @@ import { Button } from 'ts/components/ui/button';
import { Container } from 'ts/components/ui/container';
import { Input } from 'ts/components/ui/input';
import { Text } from 'ts/components/ui/text';
+import { analytics } from 'ts/utils/analytics';
import { backendClient } from 'ts/utils/backend_client';
export interface SubscribeFormProps {}
@@ -112,6 +113,9 @@ export class SubscribeForm extends React.Component<SubscribeFormProps, Subscribe
try {
const response = await backendClient.subscribeToNewsletterAsync(this.state.emailText);
const status = response.status === 200 ? SubscribeFormStatus.Success : SubscribeFormStatus.Error;
+ if (status === SubscribeFormStatus.Success) {
+ analytics.identify(this.state.emailText, 'email');
+ }
this.setState({ status, emailText: '' });
} catch (error) {
this._setStatus(SubscribeFormStatus.Error);
diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx
index 7d7df5506..72efab033 100644
--- a/packages/website/ts/components/generate_order/generate_order_form.tsx
+++ b/packages/website/ts/components/generate_order/generate_order_form.tsx
@@ -1,6 +1,6 @@
import { generatePseudoRandomSalt, getOrderHashHex } from '@0xproject/order-utils';
-import { colors, constants as sharedConstants } from '@0xproject/react-shared';
-import { ECSignature, Order } from '@0xproject/types';
+import { colors } from '@0xproject/react-shared';
+import { ECSignature, Order as ZeroExOrder } from '@0xproject/types';
import { BigNumber, logUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
@@ -20,7 +20,7 @@ import { SwapIcon } from 'ts/components/ui/swap_icon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { portalOrderSchema } from 'ts/schemas/portal_order_schema';
import { validator } from 'ts/schemas/validator';
-import { AlertTypes, BlockchainErrs, HashData, Side, SideToAssetToken, Token, TokenByAddress } from 'ts/types';
+import { AlertTypes, BlockchainErrs, HashData, Order, Side, SideToAssetToken, Token, TokenByAddress } from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
@@ -254,7 +254,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
userAddressIfExists,
debitToken.address,
);
- const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount;
+ const receiveToken = this.props.sideToAssetToken[Side.Receive];
+ const receiveAmount = receiveToken.amount;
if (
!_.isUndefined(debitToken.amount) &&
!_.isUndefined(receiveAmount) &&
@@ -264,24 +265,28 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
debitBalance.gte(debitToken.amount) &&
debitAllowance.gte(debitToken.amount)
) {
- const didSignSuccessfully = await this._signTransactionAsync();
- if (didSignSuccessfully) {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- const eventLabel = `${this.props.tokenByAddress[debitToken.address].symbol}-${networkName}`;
- analytics.logEvent('Portal', 'Sign Order Success', eventLabel, debitToken.amount.toNumber());
+ const signedOrder = await this._signTransactionAsync();
+ const doesSignedOrderExist = !_.isUndefined(signedOrder);
+ if (doesSignedOrderExist) {
+ analytics.trackOrderEvent('Sign Order Success', signedOrder);
this.setState({
globalErrMsg: '',
shouldShowIncompleteErrs: false,
});
}
- return didSignSuccessfully;
+ return doesSignedOrderExist;
} else {
let globalErrMsg = 'You must fix the above errors in order to generate a valid order';
if (this.props.userAddress === '') {
globalErrMsg = 'You must enable wallet communication';
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
}
- analytics.logEvent('Portal', 'Sign Order Failure', globalErrMsg);
+ analytics.track('Sign Order Failure', {
+ makerTokenAmount: debitToken.amount.toString(),
+ makerToken: this.props.tokenByAddress[debitToken.address].symbol,
+ takerTokenAmount: receiveToken.amount.toString(),
+ takerToken: this.props.tokenByAddress[receiveToken.address].symbol,
+ });
this.setState({
globalErrMsg,
shouldShowIncompleteErrs: true,
@@ -289,7 +294,7 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
return false;
}
}
- private async _signTransactionAsync(): Promise<boolean> {
+ private async _signTransactionAsync(): Promise<Order | undefined> {
this.setState({
signingState: SigningState.SIGNING,
});
@@ -299,11 +304,11 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
this.setState({
signingState: SigningState.UNSIGNED,
});
- return false;
+ return undefined;
}
const hashData = this.props.hashData;
- const zeroExOrder: Order = {
+ const zeroExOrder: ZeroExOrder = {
exchangeContractAddress: exchangeContractAddr,
expirationUnixTimestampSec: hashData.orderExpiryTimestamp,
feeRecipient: hashData.feeRecipientAddress,
@@ -320,9 +325,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
const orderHash = getOrderHashHex(zeroExOrder);
let globalErrMsg = '';
+ let order;
try {
const ecSignature = await this.props.blockchain.signOrderHashAsync(orderHash);
- const order = utils.generateOrder(
+ order = utils.generateOrder(
exchangeContractAddr,
this.props.sideToAssetToken,
hashData.orderExpiryTimestamp,
@@ -356,7 +362,7 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
signingState: globalErrMsg === '' ? SigningState.SIGNED : SigningState.UNSIGNED,
globalErrMsg,
});
- return globalErrMsg === '';
+ return order;
}
private _updateOrderAddress(address?: string): void {
if (!_.isUndefined(address)) {
diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx
index ff3a01136..297617bef 100644
--- a/packages/website/ts/components/inputs/allowance_toggle.tsx
+++ b/packages/website/ts/components/inputs/allowance_toggle.tsx
@@ -1,4 +1,4 @@
-import { constants as sharedConstants, Styles } from '@0xproject/react-shared';
+import { Styles } from '@0xproject/react-shared';
import { BigNumber, logUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import Toggle from 'material-ui/Toggle';
@@ -111,14 +111,16 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow
if (!this._isAllowanceSet()) {
newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
}
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- const eventLabel = `${this.props.token.symbol}-${networkName}`;
+ const logData = {
+ tokenSymbol: this.props.token.symbol,
+ newAllowance: newAllowanceAmountInBaseUnits.toNumber(),
+ };
try {
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
- analytics.logEvent('Portal', 'Set Allowance Success', eventLabel, newAllowanceAmountInBaseUnits.toNumber());
+ analytics.track('Set Allowances Success', logData);
await this.props.refetchTokenStateAsync();
} catch (err) {
- analytics.logEvent('Portal', 'Set Allowance Failure', eventLabel, newAllowanceAmountInBaseUnits.toNumber());
+ analytics.track('Set Allowance Failure', logData);
this.setState({
isSpinnerVisible: false,
});
diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx
index 20a8f0a32..a51170735 100644
--- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx
+++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx
@@ -1,4 +1,3 @@
-import { constants as sharedConstants } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
@@ -225,20 +224,21 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
(this.props.stepIndex === 0 && !this.props.isRunning && this.props.blockchainIsLoaded) ||
(!this.props.isRunning && !this.props.hasBeenClosed && this.props.blockchainIsLoaded)
) {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- analytics.logEvent('Portal', 'Onboarding Started - Automatic', networkName, this.props.stepIndex);
+ analytics.track('Onboarding Started', {
+ reason: 'automatic',
+ stepIndex: this.props.stepIndex,
+ });
this.props.updateIsRunning(true);
}
}
private _updateOnboardingStep(stepIndex: number): void {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
this.props.updateOnboardingStep(stepIndex);
- analytics.logEvent('Portal', 'Update Onboarding Step', networkName, stepIndex);
}
private _closeOnboarding(): void {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
this.props.updateIsRunning(false);
- analytics.logEvent('Portal', 'Onboarding Closed', networkName, this.props.stepIndex);
+ analytics.track('Onboarding Closed', {
+ stepIndex: this.props.stepIndex,
+ });
}
private _renderZrxAllowanceToggle(): React.ReactNode {
const zrxToken = utils.getZrxToken(this.props.tokenByAddress);
diff --git a/packages/website/ts/components/order_json.tsx b/packages/website/ts/components/order_json.tsx
index 965bd5455..c2606bd56 100644
--- a/packages/website/ts/components/order_json.tsx
+++ b/packages/website/ts/components/order_json.tsx
@@ -1,5 +1,5 @@
import { ECSignature } from '@0xproject/types';
-import { BigNumber, logUtils } from '@0xproject/utils';
+import { BigNumber, fetchAsync, logUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import Paper from 'material-ui/Paper';
import TextField from 'material-ui/TextField';
@@ -148,7 +148,7 @@ You can see and fill it here: ${this.state.shareLink}`);
const bitlyRequestUrl = `${constants.URL_BITLY_API}/v3/shorten?access_token=${
configs.BITLY_ACCESS_TOKEN
}&longUrl=${longUrl}`;
- const response = await fetch(bitlyRequestUrl);
+ const response = await fetchAsync(bitlyRequestUrl);
const responseBody = await response.text();
const bodyObj = JSON.parse(responseBody);
if (response.status !== 200 || bodyObj.status_code !== 200) {
diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx
index f983241fa..ea821d038 100644
--- a/packages/website/ts/components/portal/portal.tsx
+++ b/packages/website/ts/components/portal/portal.tsx
@@ -1,4 +1,4 @@
-import { colors, constants as sharedConstants } from '@0xproject/react-shared';
+import { colors } from '@0xproject/react-shared';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as React from 'react';
@@ -388,10 +388,11 @@ export class Portal extends React.Component<PortalProps, PortalState> {
startOnboarding
);
}
-
private _startOnboarding(): void {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- analytics.logEvent('Portal', 'Onboarding Started - Manual', networkName, this.props.portalOnboardingStep);
+ analytics.track('Onboarding Started', {
+ reason: 'manual',
+ stepIndex: this.props.portalOnboardingStep,
+ });
this.props.dispatcher.updatePortalOnboardingShowing(true);
}
private _renderWalletSection(): React.ReactNode {
diff --git a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx
index 431cf145b..193dd237a 100644
--- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx
+++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx
@@ -1,4 +1,4 @@
-import { constants as sharedConstants, Styles } from '@0xproject/react-shared';
+import { Styles } from '@0xproject/react-shared';
import * as _ from 'lodash';
import { GridTile as PlainGridTile } from 'material-ui/GridList';
import * as React from 'react';
@@ -64,10 +64,10 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (
const link = props.relayerInfo.appUrl || props.relayerInfo.url;
const topTokens = props.relayerInfo.topTokens;
const weeklyTxnVolume = props.relayerInfo.weeklyTxnVolume;
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[props.networkId];
- const eventLabel = `${props.relayerInfo.name}-${networkName}`;
const onClick = () => {
- analytics.logEvent('Portal', 'Relayer Click', eventLabel);
+ analytics.track('Relayer Click', {
+ name: props.relayerInfo.name,
+ });
utils.openUrl(link);
};
const headerImageUrl = props.relayerInfo.logoImgUrl;
diff --git a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx
index c48b672e9..f3787bd27 100644
--- a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx
+++ b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx
@@ -1,9 +1,4 @@
-import {
- colors,
- constants as sharedConstants,
- EtherscanLinkSuffixes,
- utils as sharedUtils,
-} from '@0xproject/react-shared';
+import { colors, EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared';
import * as _ from 'lodash';
import * as React from 'react';
@@ -46,11 +41,11 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> {
};
}
public render(): React.ReactNode {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
- const eventLabel = `${this.props.tokenInfo.symbol}-${networkName}`;
const onClick = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
- analytics.logEvent('Portal', 'Token Click', eventLabel);
+ analytics.track('Token Click', {
+ tokenSymbol: this.props.tokenInfo.symbol,
+ });
const tokenLink = this._tokenLinkFromToken(this.props.tokenInfo, this.props.networkId);
utils.openUrl(tokenLink);
};
diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx
index c9b313736..c8d80702b 100644
--- a/packages/website/ts/components/token_balances.tsx
+++ b/packages/website/ts/components/token_balances.tsx
@@ -5,7 +5,7 @@ import {
Styles,
utils as sharedUtils,
} from '@0xproject/react-shared';
-import { BigNumber, errorUtils, logUtils } from '@0xproject/utils';
+import { BigNumber, errorUtils, fetchAsync, logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
@@ -548,7 +548,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY);
const segment = isEtherRequest ? 'ether' : 'zrx';
- const response = await fetch(
+ const response = await fetchAsync(
`${constants.URL_TESTNET_FAUCET}/${segment}/${this.props.userAddress}?networkId=${this.props.networkId}`,
);
const responseBody = await response.text();
diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx
index 6c1c495d7..e462ab3e0 100644
--- a/packages/website/ts/components/wallet/wallet.tsx
+++ b/packages/website/ts/components/wallet/wallet.tsx
@@ -1,4 +1,4 @@
-import { constants as sharedConstants, EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared';
+import { EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared';
import { BigNumber, errorUtils } from '@0xproject/utils';
import * as _ from 'lodash';
@@ -488,19 +488,17 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
);
}
private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
const action =
wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Opened' : 'Wallet - Unwrap WETH Opened';
- analytics.logEvent('Portal', action, networkName);
+ analytics.track(action);
this.setState({
wrappedEtherDirection,
});
}
private _closeWrappedEtherActionRow(wrappedEtherDirection: Side): void {
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
const action =
wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Closed' : 'Wallet - Unwrap WETH Closed';
- analytics.logEvent('Portal', action, networkName);
+ analytics.track(action);
this.setState({
wrappedEtherDirection: undefined,
});
diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx
index 76b349c0b..fcab5d1dd 100644
--- a/packages/website/ts/components/wallet/wrap_ether_item.tsx
+++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx
@@ -1,4 +1,4 @@
-import { constants as sharedConstants, Styles } from '@0xproject/react-shared';
+import { Styles } from '@0xproject/react-shared';
import { BigNumber, logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
@@ -188,20 +188,23 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther
this.setState({
isEthConversionHappening: true,
});
- const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId];
+ const etherToken = this.props.etherToken;
+ const amountToConvert = this.state.currentInputAmount;
+ const ethAmount = Web3Wrapper.toUnitAmount(amountToConvert, constants.DECIMAL_PLACES_ETH).toString();
+ const tokenAmount = Web3Wrapper.toUnitAmount(amountToConvert, etherToken.decimals).toString();
try {
- const etherToken = this.props.etherToken;
- const amountToConvert = this.state.currentInputAmount;
if (this.props.direction === Side.Deposit) {
await this.props.blockchain.convertEthToWrappedEthTokensAsync(etherToken.address, amountToConvert);
- const ethAmount = Web3Wrapper.toUnitAmount(amountToConvert, constants.DECIMAL_PLACES_ETH);
- this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`);
- analytics.logEvent('Portal', 'Wrap ETH Successfully', networkName);
+ this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount} ETH to WETH`);
+ analytics.track('Wrap ETH Success', {
+ amount: ethAmount,
+ });
} else {
await this.props.blockchain.convertWrappedEthTokensToEthAsync(etherToken.address, amountToConvert);
- const tokenAmount = Web3Wrapper.toUnitAmount(amountToConvert, etherToken.decimals);
- this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`);
- analytics.logEvent('Portal', 'Unwrap WETH Successfully', networkName);
+ this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount} WETH to ETH`);
+ analytics.track('Unwrap WETH Success', {
+ amount: tokenAmount,
+ });
}
await this.props.refetchEthTokenStateAsync();
this.props.onConversionSuccessful();
@@ -214,10 +217,14 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther
logUtils.log(err.stack);
if (this.props.direction === Side.Deposit) {
this.props.dispatcher.showFlashMessage('Failed to wrap your ETH. Please try again.');
- analytics.logEvent('Portal', 'Wrap ETH Failed', networkName);
+ analytics.track('Wrap ETH Failure', {
+ amount: ethAmount,
+ });
} else {
this.props.dispatcher.showFlashMessage('Failed to unwrap your WETH. Please try again.');
- analytics.logEvent('Portal', 'Unwrap WETH Failed', networkName);
+ analytics.track('Unwrap WETH Failed', {
+ amount: tokenAmount,
+ });
}
errorReporter.report(err);
}
diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx
index 7ceec8c2c..2a5c5e4f1 100644
--- a/packages/website/ts/index.tsx
+++ b/packages/website/ts/index.tsx
@@ -16,11 +16,9 @@ import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
import { tradeHistoryStorage } from 'ts/local_storage/trade_history_storage';
import { store } from 'ts/redux/store';
import { WebsiteLegacyPaths, WebsitePaths } from 'ts/types';
-import { analytics } from 'ts/utils/analytics';
import { muiTheme } from 'ts/utils/mui_theme';
import { utils } from 'ts/utils/utils';
// Polyfills
-import 'whatwg-fetch';
injectTapEventPlugin();
// Check if we've introduced an update that requires us to clear the tradeHistory local storage entries
@@ -69,10 +67,6 @@ const LazyEthereumTypesDocumentation = createLazyComponent('Documentation', asyn
System.import<any>(/* webpackChunkName: "ethereumTypesDocs" */ 'ts/containers/ethereum_types_documentation'),
);
-analytics.init();
-// tslint:disable-next-line:no-floating-promises
-analytics.logProviderAsync((window as any).web3);
-
render(
<Router>
<div>
diff --git a/packages/website/ts/pages/wiki/wiki.tsx b/packages/website/ts/pages/wiki/wiki.tsx
index 9659900be..55f532b11 100644
--- a/packages/website/ts/pages/wiki/wiki.tsx
+++ b/packages/website/ts/pages/wiki/wiki.tsx
@@ -205,7 +205,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> {
articlesBySection,
},
async () => {
- await utils.onPageLoadAsync();
+ await utils.onPageLoadPromise;
const hash = this.props.location.hash.slice(1);
sharedUtils.scrollToHash(hash, sharedConstants.SCROLL_CONTAINER_ID);
},
diff --git a/packages/website/ts/redux/analyticsMiddleware.ts b/packages/website/ts/redux/analyticsMiddleware.ts
new file mode 100644
index 000000000..1ef9b6ad7
--- /dev/null
+++ b/packages/website/ts/redux/analyticsMiddleware.ts
@@ -0,0 +1,41 @@
+import { Middleware } from 'redux';
+import { State } from 'ts/redux/reducer';
+import { ActionTypes } from 'ts/types';
+import { analytics } from 'ts/utils/analytics';
+
+export const analyticsMiddleware: Middleware = store => next => action => {
+ const nextAction = next(action);
+ const nextState = (store.getState() as any) as State;
+ switch (action.type) {
+ case ActionTypes.UpdateInjectedProviderName:
+ analytics.addEventProperties({
+ injectedProviderName: nextState.injectedProviderName,
+ });
+ break;
+ case ActionTypes.UpdateNetworkId:
+ analytics.addEventProperties({
+ networkId: nextState.networkId,
+ });
+ break;
+ case ActionTypes.UpdateUserAddress:
+ analytics.addUserProperties({
+ ethAddress: nextState.userAddress,
+ });
+ break;
+ case ActionTypes.UpdateUserEtherBalance:
+ if (nextState.userEtherBalanceInWei) {
+ analytics.addUserProperties({
+ ethBalance: nextState.userEtherBalanceInWei.toString(),
+ });
+ }
+ break;
+ case ActionTypes.UpdatePortalOnboardingStep:
+ analytics.track('Update Onboarding Step', {
+ stepIndex: nextState.portalOnboardingStep,
+ });
+ break;
+ default:
+ break;
+ }
+ return nextAction;
+};
diff --git a/packages/website/ts/redux/store.ts b/packages/website/ts/redux/store.ts
index 2672e3f61..006241371 100644
--- a/packages/website/ts/redux/store.ts
+++ b/packages/website/ts/redux/store.ts
@@ -1,7 +1,8 @@
import * as _ from 'lodash';
-import { createStore, Store as ReduxStore } from 'redux';
-import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly';
+import { applyMiddleware, createStore, Store as ReduxStore } from 'redux';
+import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import { stateStorage } from 'ts/local_storage/state_storage';
+import { analyticsMiddleware } from 'ts/redux/analyticsMiddleware';
import { reducer, State } from 'ts/redux/reducer';
const ONE_SECOND = 1000;
@@ -9,7 +10,7 @@ const ONE_SECOND = 1000;
export const store: ReduxStore<State> = createStore(
reducer,
stateStorage.getPersistedDefaultState(),
- devToolsEnhancer({ name: '0x Website Redux Store' }),
+ composeWithDevTools(applyMiddleware(analyticsMiddleware)),
);
store.subscribe(
_.throttle(() => {
diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts
index a473e50c3..4d0496f6c 100644
--- a/packages/website/ts/types.ts
+++ b/packages/website/ts/types.ts
@@ -519,8 +519,10 @@ export interface OutdatedWrappedEtherByNetworkId {
};
}
-export interface ItemByAddress<T> {
- [address: string]: T;
+export type ItemByAddress<T> = ObjectMap<T>;
+
+export interface ObjectMap<T> {
+ [key: string]: T;
}
export type TokenStateByAddress = ItemByAddress<TokenState>;
diff --git a/packages/website/ts/utils/analytics.ts b/packages/website/ts/utils/analytics.ts
index f4bfa083f..e5a1ddfa4 100644
--- a/packages/website/ts/utils/analytics.ts
+++ b/packages/website/ts/utils/analytics.ts
@@ -1,27 +1,83 @@
import * as _ from 'lodash';
-import * as ReactGA from 'react-ga';
-import { InjectedWeb3 } from 'ts/types';
-import { configs } from 'ts/utils/configs';
+import { ObjectMap, Order } from 'ts/types';
import { utils } from 'ts/utils/utils';
-export const analytics = {
- init(): void {
- ReactGA.initialize(configs.GOOGLE_ANALYTICS_ID);
- },
- logEvent(category: string, action: string, label: string, value?: any): void {
- ReactGA.event({
- category,
- action,
- label,
- value,
- });
- },
- async logProviderAsync(web3IfExists: InjectedWeb3): Promise<void> {
- await utils.onPageLoadAsync();
- const providerType =
- !_.isUndefined(web3IfExists) && !_.isUndefined(web3IfExists.currentProvider)
- ? utils.getProviderType(web3IfExists.currentProvider)
- : 'NONE';
- ReactGA.ga('set', 'dimension1', providerType);
- },
-};
+export interface HeapAnalytics {
+ loaded: boolean;
+ identify(id: string, idType: string): void;
+ track(eventName: string, eventProperties?: ObjectMap<string | number>): void;
+ resetIdentity(): void;
+ addUserProperties(properties: ObjectMap<string | number>): void;
+ addEventProperties(properties: ObjectMap<string | number>): void;
+ removeEventProperty(property: string): void;
+ clearEventProperties(): void;
+}
+export class Analytics {
+ private _heap: HeapAnalytics;
+ public static init(): Analytics {
+ return new Analytics(Analytics.getHeap());
+ }
+ public static getHeap(): HeapAnalytics {
+ const heap = (window as any).heap;
+ if (!_.isUndefined(heap)) {
+ return heap;
+ } else {
+ throw new Error('Could not find the Heap SDK on the page.');
+ }
+ }
+ constructor(heap: HeapAnalytics) {
+ this._heap = heap;
+ }
+ // tslint:disable:no-floating-promises
+ // HeapAnalytics Wrappers
+ public identify(id: string, idType: string): void {
+ this._heapLoadedGuardAsync(() => this._heap.identify(id, idType));
+ }
+ public track(eventName: string, eventProperties?: ObjectMap<string | number>): void {
+ this._heapLoadedGuardAsync(() => this._heap.track(eventName, eventProperties));
+ }
+ public resetIdentity(): void {
+ this._heapLoadedGuardAsync(() => this._heap.resetIdentity());
+ }
+ public addUserProperties(properties: ObjectMap<string | number>): void {
+ this._heapLoadedGuardAsync(() => this._heap.addUserProperties(properties));
+ }
+ public addEventProperties(properties: ObjectMap<string | number>): void {
+ this._heapLoadedGuardAsync(() => this._heap.addEventProperties(properties));
+ }
+ public removeEventProperty(property: string): void {
+ this._heapLoadedGuardAsync(() => this._heap.removeEventProperty(property));
+ }
+ public clearEventProperties(): void {
+ this._heapLoadedGuardAsync(() => this._heap.clearEventProperties());
+ }
+ // tslint:enable:no-floating-promises
+ // Custom methods
+ public trackOrderEvent(eventName: string, order: Order): void {
+ const orderLoggingData = {
+ takerTokenAmount: order.signedOrder.takerTokenAmount,
+ makeTokenAmount: order.signedOrder.makerTokenAmount,
+ takerToken: order.metadata.takerToken.symbol,
+ makerToken: order.metadata.makerToken.symbol,
+ };
+ this.track(eventName, orderLoggingData);
+ }
+ /**
+ * Heap is not available as a UMD module, and additionally has the strange property of replacing itself with
+ * a different object once it's loaded.
+ * Instead of having an await call before every analytics use, we opt to have the awaiting logic in here by
+ * guarding every API call with the guard below.
+ */
+ private async _heapLoadedGuardAsync(callback: () => void): Promise<void> {
+ if (this._heap.loaded) {
+ callback();
+ return undefined;
+ }
+ await utils.onPageLoadPromise;
+ // HACK: Reset heap to loaded heap
+ this._heap = (window as any).heap;
+ callback();
+ }
+}
+
+export const analytics = Analytics.init();
diff --git a/packages/website/ts/utils/doc_utils.ts b/packages/website/ts/utils/doc_utils.ts
index 7768835fb..1627b9b0c 100644
--- a/packages/website/ts/utils/doc_utils.ts
+++ b/packages/website/ts/utils/doc_utils.ts
@@ -1,5 +1,5 @@
import { DoxityDocObj, TypeDocNode } from '@0xproject/react-docs';
-import { logUtils } from '@0xproject/utils';
+import { fetchAsync, logUtils } from '@0xproject/utils';
import findVersions = require('find-versions');
import * as _ from 'lodash';
import { S3FileObject, VersionToFilePath } from 'ts/types';
@@ -16,7 +16,7 @@ export const docUtils = {
return versionToFilePath;
},
async getVersionFileNamesAsync(s3DocJsonRoot: string, folderName: string): Promise<string[]> {
- const response = await fetch(s3DocJsonRoot);
+ const response = await fetchAsync(s3DocJsonRoot);
if (response.status !== 200) {
// TODO: Show the user an error message when the docs fail to load
const errMsg = await response.text();
@@ -73,7 +73,7 @@ export const docUtils = {
},
async getJSONDocFileAsync(filePath: string, s3DocJsonRoot: string): Promise<TypeDocNode | DoxityDocObj> {
const endpoint = `${s3DocJsonRoot}/${filePath}`;
- const response = await fetch(endpoint);
+ const response = await fetchAsync(endpoint);
if (response.status !== 200) {
// TODO: Show the user an error message when the docs fail to load
const errMsg = await response.text();
diff --git a/packages/website/ts/utils/fetch_utils.ts b/packages/website/ts/utils/fetch_utils.ts
index a56d89262..e9a88b6b3 100644
--- a/packages/website/ts/utils/fetch_utils.ts
+++ b/packages/website/ts/utils/fetch_utils.ts
@@ -1,4 +1,4 @@
-import { logUtils } from '@0xproject/utils';
+import { fetchAsync, logUtils } from '@0xproject/utils';
import * as _ from 'lodash';
import * as queryString from 'query-string';
@@ -18,14 +18,14 @@ export const fetchUtils = {
async requestAsync(baseUrl: string, path: string, queryParams?: object): Promise<any> {
const query = queryStringFromQueryParams(queryParams);
const url = `${baseUrl}${path}${query}`;
- const response = await fetch(url);
+ const response = await fetchAsync(url);
logErrorIfPresent(response, url);
const result = await response.json();
return result;
},
async postAsync(baseUrl: string, path: string, body: object): Promise<Response> {
const url = `${baseUrl}${path}`;
- const response = await fetch(url, {
+ const response = await fetchAsync(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts
index f2baeb849..739bb7b66 100644
--- a/packages/website/ts/utils/utils.ts
+++ b/packages/website/ts/utils/utils.ts
@@ -299,14 +299,13 @@ export const utils = {
const baseUrl = `https://${window.location.hostname}${hasPort ? `:${port}` : ''}`;
return baseUrl;
},
- async onPageLoadAsync(): Promise<void> {
+ onPageLoadPromise: new Promise((resolve, _reject) => {
if (document.readyState === 'complete') {
- return; // Already loaded
+ resolve();
+ return;
}
- return new Promise<void>((resolve, _reject) => {
- window.onload = () => resolve();
- });
- },
+ window.onload = resolve;
+ }),
getProviderType(provider: Provider): Providers | string {
const constructorName = provider.constructor.name;
let parsedProviderName = constructorName;