Merge pull request #1272 from 0xProject/feature/instant/heap

[instant] Base heap integration
This commit is contained in:
Steve Klebanoff 2018-11-20 09:23:34 -08:00 committed by GitHub
commit ba41fc9275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 348 additions and 45 deletions

View File

@ -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) {

View File

@ -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',
};

View File

@ -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}`);

View File

@ -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;
};

View File

@ -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)));
},
};

View File

@ -165,4 +165,5 @@ export enum ProviderType {
Mist = 'MIST',
CoinbaseWallet = 'COINBASE_WALLET',
Cipher = 'CIPHER',
Fallback = 'FALLBACK',
}

View File

@ -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),
};

View File

@ -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);
}
}
},
};

View File

@ -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),

View File

@ -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',
},
],
},
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}`,
);
}
},
},
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 = config;
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),
},
}),
],
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;
};

View File

@ -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);