aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/instant/src/components/zero_ex_instant_provider.tsx14
-rw-r--r--packages/instant/src/constants.ts2
-rw-r--r--packages/instant/src/index.umd.ts3
-rw-r--r--packages/instant/src/redux/analytics_middleware.ts59
-rw-r--r--packages/instant/src/redux/store.ts7
-rw-r--r--packages/instant/src/types.ts1
-rw-r--r--packages/instant/src/util/analytics.ts64
-rw-r--r--packages/instant/src/util/heap.ts113
-rw-r--r--packages/instant/src/util/provider_state_factory.ts2
-rw-r--r--packages/instant/webpack.config.js115
-rw-r--r--packages/monorepo-scripts/src/prepublish_checks.ts11
11 files changed, 347 insertions, 44 deletions
diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx
index 8be53ee20..9435d8c7c 100644
--- a/packages/instant/src/components/zero_ex_instant_provider.tsx
+++ b/packages/instant/src/components/zero_ex_instant_provider.tsx
@@ -12,6 +12,7 @@ import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer';
import { store, Store } from '../redux/store';
import { fonts } from '../style/fonts';
import { AccountState, AffiliateInfo, AssetMetaData, Network, OrderSource } from '../types';
+import { analytics, disableAnalytics } from '../util/analytics';
import { assetUtils } from '../util/asset';
import { errorFlasher } from '../util/error_flasher';
import { gasPriceEstimator } from '../util/gas_price_estimator';
@@ -36,6 +37,7 @@ export interface ZeroExInstantProviderOptionalProps {
additionalAssetMetaDataMap: ObjectMap<AssetMetaData>;
networkId: Network;
affiliateInfo: AffiliateInfo;
+ shouldDisableAnalyticsTracking: boolean;
}
export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> {
@@ -121,6 +123,18 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
gasPriceEstimator.getGasInfoAsync();
// tslint:disable-next-line:no-floating-promises
this._flashErrorIfWrongNetwork();
+
+ // Analytics
+ disableAnalytics(this.props.shouldDisableAnalyticsTracking || false);
+ analytics.addEventProperties({
+ embeddedHost: window.location.host,
+ embeddedUrl: window.location.href,
+ networkId: state.network,
+ providerName: state.providerState.name,
+ gitSha: process.env.GIT_SHA,
+ npmVersion: process.env.NPM_PACKAGE_VERSION,
+ });
+ analytics.trackInstantOpened();
}
public componentWillUnmount(): void {
if (this._accountUpdateHeartbeat) {
diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts
index ad2d568fe..be6077ca9 100644
--- a/packages/instant/src/constants.ts
+++ b/packages/instant/src/constants.ts
@@ -16,6 +16,7 @@ export const BUY_QUOTE_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 15;
export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6);
export const DEFAULT_ESTIMATED_TRANSACTION_TIME_MS = ONE_MINUTE_MS * 2;
export const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
+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;
@@ -48,4 +49,5 @@ export const PROVIDER_TYPE_TO_NAME: { [key in ProviderType]: string } = {
[ProviderType.Mist]: 'Mist',
[ProviderType.CoinbaseWallet]: 'Coinbase Wallet',
[ProviderType.Parity]: 'Parity',
+ [ProviderType.Fallback]: 'Fallback',
};
diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts
index 0274db30c..5010347b3 100644
--- a/packages/instant/src/index.umd.ts
+++ b/packages/instant/src/index.umd.ts
@@ -35,6 +35,9 @@ export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFA
if (!_.isUndefined(props.provider)) {
assert.isWeb3Provider('props.provider', props.provider);
}
+ if (!_.isUndefined(props.shouldDisableAnalyticsTracking)) {
+ assert.isBoolean('props.shouldDisableAnalyticsTracking', props.shouldDisableAnalyticsTracking);
+ }
assert.isString('selector', selector);
const appendToIfExists = document.querySelector(selector);
assert.assert(!_.isNull(appendToIfExists), `Could not find div with selector: ${selector}`);
diff --git a/packages/instant/src/redux/analytics_middleware.ts b/packages/instant/src/redux/analytics_middleware.ts
new file mode 100644
index 000000000..f971dbd33
--- /dev/null
+++ b/packages/instant/src/redux/analytics_middleware.ts
@@ -0,0 +1,59 @@
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as _ from 'lodash';
+import { Middleware } from 'redux';
+
+import { ETH_DECIMALS } from '../constants';
+import { Account, AccountState } from '../types';
+import { analytics } from '../util/analytics';
+
+import { Action, ActionTypes } from './actions';
+
+import { State } from './reducer';
+
+const shouldTriggerWalletReady = (prevAccount: Account, curAccount: Account): boolean => {
+ const didJustTurnReady = curAccount.state === AccountState.Ready && prevAccount.state !== AccountState.Ready;
+ if (didJustTurnReady) {
+ return true;
+ }
+
+ if (curAccount.state === AccountState.Ready && prevAccount.state === AccountState.Ready) {
+ // Account was ready, and is now ready again, but address has changed
+ return curAccount.address !== prevAccount.address;
+ }
+
+ return false;
+};
+
+export const analyticsMiddleware: Middleware = store => next => middlewareAction => {
+ const prevState = store.getState() as State;
+ const prevAccount = prevState.providerState.account;
+
+ const nextAction = next(middlewareAction) as Action;
+
+ const curState = store.getState() as State;
+ const curAccount = curState.providerState.account;
+
+ switch (nextAction.type) {
+ case ActionTypes.SET_ACCOUNT_STATE_READY:
+ if (curAccount.state === AccountState.Ready && shouldTriggerWalletReady(prevAccount, curAccount)) {
+ const ethAddress = curAccount.address;
+ analytics.addUserProperties({ ethAddress });
+ analytics.trackWalletReady();
+ }
+ break;
+ case ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE:
+ if (
+ curAccount.state === AccountState.Ready &&
+ curAccount.ethBalanceInWei &&
+ !_.isEqual(curAccount, prevAccount)
+ ) {
+ const ethBalanceInUnitAmount = Web3Wrapper.toUnitAmount(
+ curAccount.ethBalanceInWei,
+ ETH_DECIMALS,
+ ).toString();
+ analytics.addUserProperties({ ethBalanceInUnitAmount });
+ }
+ }
+
+ return nextAction;
+};
diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts
index 20710765d..11bba3876 100644
--- a/packages/instant/src/redux/store.ts
+++ b/packages/instant/src/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 { analyticsMiddleware } from './analytics_middleware';
import { createReducer, State } from './reducer';
export type Store = ReduxStore<State>;
@@ -9,6 +10,6 @@ export type Store = ReduxStore<State>;
export const store = {
create: (initialState: State): Store => {
const reducer = createReducer(initialState);
- return createStore(reducer, initialState, devToolsEnhancer({}));
+ return createStore(reducer, initialState, composeWithDevTools(applyMiddleware(analyticsMiddleware)));
},
};
diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts
index 2d4a8a850..999d50fed 100644
--- a/packages/instant/src/types.ts
+++ b/packages/instant/src/types.ts
@@ -165,4 +165,5 @@ export enum ProviderType {
Mist = 'MIST',
CoinbaseWallet = 'COINBASE_WALLET',
Cipher = 'CIPHER',
+ Fallback = 'FALLBACK',
}
diff --git a/packages/instant/src/util/analytics.ts b/packages/instant/src/util/analytics.ts
new file mode 100644
index 000000000..2ffaac1dd
--- /dev/null
+++ b/packages/instant/src/util/analytics.ts
@@ -0,0 +1,64 @@
+import { ObjectMap } from '@0x/types';
+
+import { heapUtil } from './heap';
+
+let isDisabled = false;
+export const disableAnalytics = (shouldDisableAnalytics: boolean) => {
+ isDisabled = shouldDisableAnalytics;
+};
+export const evaluateIfEnabled = (fnCall: () => void) => {
+ if (isDisabled) {
+ return;
+ }
+ fnCall();
+};
+
+enum EventNames {
+ INSTANT_OPENED = 'Instant - Opened',
+ WALLET_READY = 'Wallet - Ready',
+}
+const track = (eventName: EventNames, eventData: ObjectMap<string | number> = {}): void => {
+ evaluateIfEnabled(() => {
+ heapUtil.evaluateHeapCall(heap => heap.track(eventName, eventData));
+ });
+};
+function trackingEventFnWithoutPayload(eventName: EventNames): () => void {
+ return () => {
+ track(eventName);
+ };
+}
+// 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);
+ };
+}
+
+export interface AnalyticsUserOptions {
+ ethAddress?: string;
+ ethBalanceInUnitAmount?: string;
+}
+export interface AnalyticsEventOptions {
+ embeddedHost?: string;
+ embeddedUrl?: string;
+ networkId?: number;
+ providerName?: string;
+ gitSha?: string;
+ npmVersion?: string;
+}
+export const analytics = {
+ addUserProperties: (properties: AnalyticsUserOptions): void => {
+ evaluateIfEnabled(() => {
+ heapUtil.evaluateHeapCall(heap => heap.addUserProperties(properties));
+ });
+ },
+ addEventProperties: (properties: AnalyticsEventOptions): void => {
+ evaluateIfEnabled(() => {
+ heapUtil.evaluateHeapCall(heap => heap.addEventProperties(properties));
+ });
+ },
+ trackWalletReady: trackingEventFnWithoutPayload(EventNames.WALLET_READY),
+ trackInstantOpened: trackingEventFnWithoutPayload(EventNames.INSTANT_OPENED),
+};
diff --git a/packages/instant/src/util/heap.ts b/packages/instant/src/util/heap.ts
new file mode 100644
index 000000000..78ec3b3cc
--- /dev/null
+++ b/packages/instant/src/util/heap.ts
@@ -0,0 +1,113 @@
+import { ObjectMap } from '@0x/types';
+import { logUtils } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { HEAP_ANALYTICS_ID } from '../constants';
+
+import { AnalyticsEventOptions, AnalyticsUserOptions } from './analytics';
+
+export interface HeapAnalytics {
+ loaded: boolean;
+ appid: string;
+ identify(id: string, idType: string): void;
+ track(eventName: string, eventProperties?: ObjectMap<string | number>): void;
+ resetIdentity(): void;
+ addUserProperties(properties: AnalyticsUserOptions): void;
+ addEventProperties(properties: AnalyticsEventOptions): void;
+ removeEventProperty(property: string): void;
+ clearEventProperties(): void;
+}
+interface ModifiedWindow {
+ heap?: HeapAnalytics;
+ zeroExInstantLoadedHeap?: boolean;
+}
+const getWindow = (): ModifiedWindow => {
+ return window as ModifiedWindow;
+};
+
+const setupZeroExInstantHeap = () => {
+ if (_.isUndefined(HEAP_ANALYTICS_ID)) {
+ return;
+ }
+
+ const curWindow = getWindow();
+ // Set property to specify that this is zeroEx's heap
+ curWindow.zeroExInstantLoadedHeap = true;
+
+ // Typescript-compatible version of https://docs.heapanalytics.com/docs/installation
+ /* tslint:disable */
+ ((window as any).heap = (window as any).heap || []),
+ ((window as any).heap.load = function(e: any, t: any) {
+ ((window as any).heap.appid = e), ((window as any).heap.config = t = t || {});
+ var r = t.forceSSL || 'https:' === (document.location as Location).protocol,
+ a = document.createElement('script');
+ (a.type = 'text/javascript'),
+ (a.async = !0),
+ (a.src = (r ? 'https:' : 'http:') + '//cdn.heapanalytics.com/js/heap-' + e + '.js');
+ var n = document.getElementsByTagName('script')[0];
+ (n.parentNode as Node).insertBefore(a, n);
+ for (
+ var o = function(e: any) {
+ return function() {
+ (window as any).heap.push([e].concat(Array.prototype.slice.call(arguments, 0)));
+ };
+ },
+ p = [
+ 'addEventProperties',
+ 'addUserProperties',
+ 'clearEventProperties',
+ 'identify',
+ 'resetIdentity',
+ 'removeEventProperty',
+ 'setEventProperties',
+ 'track',
+ 'unsetEventProperty',
+ ],
+ c = 0;
+ c < p.length;
+ c++
+ )
+ (window as any).heap[p[c]] = o(p[c]);
+ });
+ (window as any).heap.load(HEAP_ANALYTICS_ID);
+ /* tslint:enable */
+
+ return curWindow.heap as HeapAnalytics;
+};
+
+export const heapUtil = {
+ getHeap: (): HeapAnalytics | undefined => {
+ const curWindow = getWindow();
+ const hasOtherExistingHeapIntegration = curWindow.heap && !curWindow.zeroExInstantLoadedHeap;
+ if (hasOtherExistingHeapIntegration) {
+ return undefined;
+ }
+
+ const zeroExInstantHeapIntegration = curWindow.zeroExInstantLoadedHeap && curWindow.heap;
+ if (zeroExInstantHeapIntegration) {
+ return zeroExInstantHeapIntegration;
+ }
+
+ return setupZeroExInstantHeap();
+ },
+ evaluateHeapCall: (heapFunctionCall: (heap: HeapAnalytics) => void): void => {
+ if (_.isUndefined(HEAP_ANALYTICS_ID)) {
+ return;
+ }
+
+ const curHeap = heapUtil.getHeap();
+ if (curHeap) {
+ try {
+ if (curHeap.appid !== HEAP_ANALYTICS_ID) {
+ // Integrator has included heap after us and reset the app id
+ return;
+ }
+ heapFunctionCall(curHeap);
+ } catch (e) {
+ // We never want analytics to crash our React component
+ // TODO(sk): error reporter here
+ logUtils.log('Analytics error', e);
+ }
+ }
+ },
+};
diff --git a/packages/instant/src/util/provider_state_factory.ts b/packages/instant/src/util/provider_state_factory.ts
index 452a71460..7c788dff2 100644
--- a/packages/instant/src/util/provider_state_factory.ts
+++ b/packages/instant/src/util/provider_state_factory.ts
@@ -56,7 +56,7 @@ export const providerStateFactory = {
getInitialProviderStateFallback: (orderSource: OrderSource, network: Network): ProviderState => {
const provider = providerFactory.getFallbackNoSigningProvider(network);
const providerState: ProviderState = {
- name: envUtil.getProviderName(provider),
+ name: 'Fallback',
provider,
web3Wrapper: new Web3Wrapper(provider),
assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),
diff --git a/packages/instant/webpack.config.js b/packages/instant/webpack.config.js
index fae303712..41276809c 100644
--- a/packages/instant/webpack.config.js
+++ b/packages/instant/webpack.config.js
@@ -1,46 +1,81 @@
-const path = require('path');
+const childProcess = require('child_process');
const ip = require('ip');
+const path = require('path');
+const webpack = require('webpack');
+
// The common js bundle (not this one) is built using tsc.
// The umd bundle (this one) has a different entrypoint.
-const outputPath = process.env.WEBPACK_OUTPUT_PATH || 'umd';
-const config = {
- entry: {
- instant: './src/index.umd.ts',
- },
- output: {
- filename: '[name].js',
- path: path.resolve(__dirname, outputPath),
- library: 'zeroExInstant',
- libraryTarget: 'umd',
- },
- devtool: 'source-map',
- resolve: {
- extensions: ['.js', '.json', '.ts', '.tsx'],
- },
- module: {
- rules: [
- {
- test: /\.(ts|tsx)$/,
- loader: 'awesome-typescript-loader',
- },
- {
- test: /\.svg$/,
- loader: 'svg-react-loader',
- },
+
+const GIT_SHA = childProcess
+ .execSync('git rev-parse HEAD')
+ .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];
+ }
+
+ if (modeName === 'development') {
+ return process.env[HEAP_DEVELOPMENT_ENV_VAR_NAME];
+ }
+
+ return undefined;
+};
+
+module.exports = (env, argv) => {
+ const outputPath = process.env.WEBPACK_OUTPUT_PATH || 'umd';
+ const config = {
+ entry: {
+ instant: './src/index.umd.ts',
+ },
+ output: {
+ filename: '[name].js',
+ path: path.resolve(__dirname, outputPath),
+ 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),
+ },
+ }),
],
- },
- devServer: {
- contentBase: path.join(__dirname, 'public'),
- port: 5000,
- host: '0.0.0.0',
- after: () => {
- if (config.devServer.host === '0.0.0.0') {
- console.log(
- `webpack-dev-server can be accessed externally at: http://${ip.address()}:${config.devServer.port}`,
- );
- }
+ devtool: 'source-map',
+ resolve: {
+ extensions: ['.js', '.json', '.ts', '.tsx'],
},
- },
+ module: {
+ rules: [
+ {
+ test: /\.(ts|tsx)$/,
+ loader: 'awesome-typescript-loader',
+ },
+ {
+ test: /\.svg$/,
+ loader: 'svg-react-loader',
+ },
+ ],
+ },
+ devServer: {
+ contentBase: path.join(__dirname, 'public'),
+ port: 5000,
+ host: '0.0.0.0',
+ after: () => {
+ if (config.devServer.host === '0.0.0.0') {
+ console.log(
+ `webpack-dev-server can be accessed externally at: http://${ip.address()}:${
+ config.devServer.port
+ }`,
+ );
+ }
+ },
+ },
+ };
+ return config;
};
-
-module.exports = config;
diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts
index 5f603ebc7..fc550cf3a 100644
--- a/packages/monorepo-scripts/src/prepublish_checks.ts
+++ b/packages/monorepo-scripts/src/prepublish_checks.ts
@@ -17,6 +17,7 @@ async function prepublishChecksAsync(): Promise<void> {
await checkChangelogFormatAsync(updatedPublicPackages);
await checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicPackages);
await checkPublishRequiredSetupAsync();
+ checkRequiredEnvVariables();
}
async function checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicPackages: Package[]): Promise<void> {
@@ -183,6 +184,16 @@ async function checkPublishRequiredSetupAsync(): Promise<void> {
}
}
+const checkRequiredEnvVariables = () => {
+ utils.log('Checking required environment variables...');
+ const requiredEnvVars = ['INSTANT_HEAP_ANALYTICS_ID_PRODUCTION'];
+ requiredEnvVars.forEach(requiredEnvVarName => {
+ if (_.isUndefined(process.env[requiredEnvVarName])) {
+ throw new Error(`Must have ${requiredEnvVarName} set`);
+ }
+ });
+};
+
prepublishChecksAsync().catch(err => {
utils.log(err.message);
process.exit(1);