Merge pull request #1272 from 0xProject/feature/instant/heap
[instant] Base heap integration
This commit is contained in:
commit
ba41fc9275
@ -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) {
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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}`);
|
||||
|
59
packages/instant/src/redux/analytics_middleware.ts
Normal file
59
packages/instant/src/redux/analytics_middleware.ts
Normal 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;
|
||||
};
|
@ -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)));
|
||||
},
|
||||
};
|
||||
|
@ -165,4 +165,5 @@ export enum ProviderType {
|
||||
Mist = 'MIST',
|
||||
CoinbaseWallet = 'COINBASE_WALLET',
|
||||
Cipher = 'CIPHER',
|
||||
Fallback = 'FALLBACK',
|
||||
}
|
||||
|
64
packages/instant/src/util/analytics.ts
Normal file
64
packages/instant/src/util/analytics.ts
Normal 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),
|
||||
};
|
113
packages/instant/src/util/heap.ts
Normal file
113
packages/instant/src/util/heap.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -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),
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user