aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/instant/src/components/zero_ex_instant_provider.tsx12
-rw-r--r--packages/instant/src/constants.ts3
-rw-r--r--packages/instant/src/redux/analytics_middleware.ts45
-rw-r--r--packages/instant/src/redux/store.ts7
-rw-r--r--packages/instant/src/util/analytics.ts49
-rw-r--r--packages/instant/src/util/heap.ts115
-rw-r--r--packages/instant/webpack.config.js20
7 files changed, 247 insertions, 4 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..5fa64aa45 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 } from '../util/analytics';
import { assetUtils } from '../util/asset';
import { errorFlasher } from '../util/error_flasher';
import { gasPriceEstimator } from '../util/gas_price_estimator';
@@ -121,6 +122,17 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
gasPriceEstimator.getGasInfoAsync();
// tslint:disable-next-line:no-floating-promises
this._flashErrorIfWrongNetwork();
+
+ // Analytics
+ 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.widgetOpened();
}
public componentWillUnmount(): void {
if (this._accountUpdateHeartbeat) {
diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts
index 5bd2349b3..80c93c431 100644
--- a/packages/instant/src/constants.ts
+++ b/packages/instant/src/constants.ts
@@ -16,6 +16,9 @@ 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 ANALYTICS_ENABLED = process.env.NODE_ENV === 'production' || process.env.ENABLE_HEAP;
+export const HEAP_ANALYTICS_DEVELOPMENT_APP_ID = '507265531';
+export const HEAP_ANALYTICS_PRODUCTION_APP_ID = '2323640988';
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;
diff --git a/packages/instant/src/redux/analytics_middleware.ts b/packages/instant/src/redux/analytics_middleware.ts
new file mode 100644
index 000000000..fb6d7eff1
--- /dev/null
+++ b/packages/instant/src/redux/analytics_middleware.ts
@@ -0,0 +1,45 @@
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as _ from 'lodash';
+import { Middleware } from 'redux';
+
+import { ETH_DECIMALS } from '../constants';
+import { AccountState } from '../types';
+import { analytics } from '../util/analytics';
+
+import { Action, ActionTypes } from './actions';
+
+import { State } from './reducer';
+
+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 && prevAccount.state !== AccountState.Ready) {
+ const ethAddress = curAccount.address;
+ analytics.addUserProperties({ ethAddress });
+ analytics.walletReady();
+ }
+ 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/util/analytics.ts b/packages/instant/src/util/analytics.ts
new file mode 100644
index 000000000..e5f3635f2
--- /dev/null
+++ b/packages/instant/src/util/analytics.ts
@@ -0,0 +1,49 @@
+import { ObjectMap } from '@0x/types';
+
+import { heapUtil } from './heap';
+
+enum EventNames {
+ WALLET_OPENED = 'Wallet - Opened',
+ WALLET_READY = 'Wallet - Ready',
+ WIDGET_OPENED = 'Widget - Opened',
+}
+const track = (eventName: EventNames, eventData: ObjectMap<string | number> = {}): void => {
+ 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 => {
+ heapUtil.evaluateHeapCall(heap => heap.addUserProperties(properties));
+ },
+ addEventProperties: (properties: AnalyticsEventOptions): void => {
+ heapUtil.evaluateHeapCall(heap => heap.addEventProperties(properties));
+ },
+ walletOpened: trackingEventFnWithoutPayload(EventNames.WALLET_OPENED),
+ walletReady: trackingEventFnWithoutPayload(EventNames.WALLET_READY),
+ widgetOpened: trackingEventFnWithoutPayload(EventNames.WIDGET_OPENED),
+};
diff --git a/packages/instant/src/util/heap.ts b/packages/instant/src/util/heap.ts
new file mode 100644
index 000000000..1871c4abc
--- /dev/null
+++ b/packages/instant/src/util/heap.ts
@@ -0,0 +1,115 @@
+import { ObjectMap } from '@0x/types';
+import { logUtils } from '@0x/utils';
+
+import { ANALYTICS_ENABLED, HEAP_ANALYTICS_DEVELOPMENT_APP_ID, HEAP_ANALYTICS_PRODUCTION_APP_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 getHeapAppId = (): string => {
+ if (process.env.NODE_ENV === 'production') {
+ return HEAP_ANALYTICS_PRODUCTION_APP_ID;
+ }
+ return HEAP_ANALYTICS_DEVELOPMENT_APP_ID;
+};
+
+const setupZeroExInstantHeap = () => {
+ 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(getHeapAppId());
+ /* 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 (!ANALYTICS_ENABLED) {
+ return;
+ }
+
+ const curHeap = heapUtil.getHeap();
+ if (curHeap) {
+ try {
+ if (curHeap.appid !== getHeapAppId()) {
+ // 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/webpack.config.js b/packages/instant/webpack.config.js
index 3129e13a6..ccbbe7359 100644
--- a/packages/instant/webpack.config.js
+++ b/packages/instant/webpack.config.js
@@ -1,7 +1,15 @@
+const childProcess = require('child_process');
const path = require('path');
-const ip = require('ip');
+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 GIT_SHA = childProcess
+ .execSync('git rev-parse HEAD')
+ .toString()
+ .trim();
+const ip = require('ip');
const config = {
entry: './src/index.umd.ts',
output: {
@@ -10,6 +18,16 @@ const config = {
library: 'zeroExInstant',
libraryTarget: 'umd',
},
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env': {
+ NODE_ENV: JSON.stringify(process.env.NODE_ENV),
+ GIT_SHA: JSON.stringify(GIT_SHA),
+ ENABLE_HEAP: JSON.stringify(process.env.ENABLE_HEAP),
+ NPM_PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version),
+ },
+ }),
+ ],
devtool: 'source-map',
resolve: {
extensions: ['.js', '.json', '.ts', '.tsx'],