From 3d892663972b3a81adf823cece0e32e454090a28 Mon Sep 17 00:00:00 2001 From: Riko Eksteen Date: Thu, 2 Apr 2020 18:08:53 +0100 Subject: [PATCH] feat(client): capture analytics (ARTP-982) --- src/client/package-lock.json | 37 +++++++++++-------- src/client/package.json | 7 ++-- src/client/src/apps/MainRoute/MainRoute.tsx | 23 ++++++++++++ .../components/app-header/Header.tsx | 11 +++++- .../spotTile/epics/executionService.ts | 23 ++++++++++-- src/client/src/index.tsx | 15 +++++++- .../src/rt-platforms/browser/browser.ts | 10 ++++- .../src/rt-platforms/defaultPlatformWindow.ts | 10 ++++- .../src/rt-platforms/finsemble/index.ts | 6 +++ src/client/src/rt-platforms/glue/glue.ts | 19 +++++++++- .../rt-platforms/openFin/adapter/window.ts | 7 ++++ .../rt-platforms/symphony/adapter/index.ts | 6 +++ 12 files changed, 145 insertions(+), 29 deletions(-) diff --git a/src/client/package-lock.json b/src/client/package-lock.json index 7f38bdb016..f9785031e7 100644 --- a/src/client/package-lock.json +++ b/src/client/package-lock.json @@ -2758,9 +2758,9 @@ } }, "@types/react-router": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.3.tgz", - "integrity": "sha512-0gGhmerBqN8CzlnDmSgGNun3tuZFXerUclWkqEhozdLaJtfcJRUTGkKaEKk+/MpHd1KDS1+o2zb/3PkBUiv2qQ==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.5.tgz", + "integrity": "sha512-RZPdCtZympi6X7EkGyaU7ISiAujDYTWgqMF9owE3P6efITw27IWQykcti0BvA5h4Mu1LLl5rxrpO3r8kHyUZ/Q==", "dev": true, "requires": { "@types/history": "*", @@ -2768,9 +2768,9 @@ } }, "@types/react-router-dom": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.5.tgz", - "integrity": "sha512-eFajSUASYbPHg2BDM1G8Btx+YqGgvROPIg6sBhl3O4kbDdYXdFdfrgQFf/pcBuQVObjfT9AL/dd15jilR5DIEA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.0.tgz", + "integrity": "sha512-YCh8r71pL5p8qDwQf59IU13hFy/41fDQG/GeOI3y+xmD4o0w3vEPxE8uBe+dvOgMoDl0W1WUZsWH0pxc1mcZyQ==", "dev": true, "requires": { "@types/history": "*", @@ -15171,6 +15171,11 @@ "react-clientside-effect": "^1.2.0" } }, + "react-ga": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-2.7.0.tgz", + "integrity": "sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA==" + }, "react-helmet": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz", @@ -15315,9 +15320,9 @@ } }, "react-router": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", - "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.0.tgz", + "integrity": "sha512-n9HXxaL/6yRlig9XPfGyagI8+bUNdqcu7FUAx0/Z+Us22Z8iHsbkyJ21Inebn9HOxI5Nxlfc8GNabkNSeXfhqw==", "requires": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", @@ -15332,15 +15337,15 @@ } }, "react-router-dom": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz", - "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.0.tgz", + "integrity": "sha512-OkxKbMKjO7IkYqnoaZNX19MnwgjhxwZE871cPUTq0YU2wpIw7QwGxSnSoNRMOa7wO1TwvJJMFpgiEB4C/gVhTw==", "requires": { "@babel/runtime": "^7.1.2", "history": "^4.9.0", "loose-envify": "^1.3.1", "prop-types": "^15.6.2", - "react-router": "5.1.2", + "react-router": "5.1.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" } @@ -18516,9 +18521,9 @@ "optional": true }, "tiny-invariant": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", - "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" }, "tiny-warning": { "version": "1.0.3", diff --git a/src/client/package.json b/src/client/package.json index 3715195f0a..ab2230ab45 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -74,11 +74,12 @@ "react": "^16.8.6", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.8.6", + "react-ga": "^2.7.0", "react-helmet": "^5.2.1", "react-hotkeys": "^2.0.0", "react-measure": "^2.3.0", "react-redux": "7.1.0", - "react-router-dom": "^5.0.1", + "react-router-dom": "^5.1.0", "react-sizeme": "2.3.6", "react-spring": "^5.5.4", "react-switch": "^5.0.1", @@ -116,7 +117,7 @@ "@types/react-dom": "^16.8.5", "@types/react-helmet": "^5.0.9", "@types/react-redux": "^7.1.1", - "@types/react-router-dom": "^4.3.4", + "@types/react-router-dom": "^5.1.0", "@types/react-test-renderer": "^16.8.3", "@types/react-virtualized": "9.7.10", "@types/recharts": "^1.1.20", @@ -127,12 +128,12 @@ "cors": "^2.8.5", "cross-env": "^6.0.3", "get-json": "^1.0.1", + "jest-styled-components": "^6.3.4", "npm-run-all": "^4.1.2", "react-scripts": "^3.3.0", "react-test-renderer": "^16.12.0", "redux-devtools-extension": "^2.13.2", "source-map-explorer": "^2.0.1", - "jest-styled-components": "^6.3.4", "typescript": "^3.6.4" }, "optionalDependencies": { diff --git a/src/client/src/apps/MainRoute/MainRoute.tsx b/src/client/src/apps/MainRoute/MainRoute.tsx index 968955c686..e698896ef1 100644 --- a/src/client/src/apps/MainRoute/MainRoute.tsx +++ b/src/client/src/apps/MainRoute/MainRoute.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import ReactGA from 'react-ga' import Helmet from 'react-helmet' import { Provider as ReduxProvider } from 'react-redux' import { ThemeProvider } from 'rt-theme' @@ -6,8 +7,11 @@ import { Router } from './data' import GlobalScrollbarStyle from './GlobalScrollbarStyle' import { getPlatformAsync, PlatformProvider } from 'rt-platforms' import { createStore } from './store' +import { useHistory } from 'react-router-dom' const MainRoute = () => { + const routeHistory = useHistory() + const [platform, setPlatform] = useState() const [store, setStore] = useState() @@ -21,6 +25,25 @@ const MainRoute = () => { getPlatform() }, []) + useEffect(() => { + if (platform) { + ReactGA.set({ + dimension1: platform.type, + dimension2: platform.name, + page: window.location.pathname, + }) + ReactGA.pageview(window.location.pathname) + } + }, [platform]) + + useEffect(() => { + const stopListening = routeHistory.listen(location => { + ReactGA.set({ page: location.pathname }) + ReactGA.pageview(location.pathname) + }) + return stopListening + }, [routeHistory]) + if (!store || !platform) { return <> } diff --git a/src/client/src/apps/MainRoute/components/app-header/Header.tsx b/src/client/src/apps/MainRoute/components/app-header/Header.tsx index 13e1078fea..3ec5d6343d 100644 --- a/src/client/src/apps/MainRoute/components/app-header/Header.tsx +++ b/src/client/src/apps/MainRoute/components/app-header/Header.tsx @@ -1,9 +1,18 @@ import React, { useCallback } from 'react' +import ReactGA from 'react-ga' import { styled, ThemeName, useTheme } from 'rt-theme' import Logo from './Logo' const Header: React.FC = ({ children }) => { - const onLogoClick = useCallback(() => window.open('https://weareadaptive.com/'), []) + const onLogoClick = useCallback(() => { + ReactGA.event({ + category: 'RT - Outbound', + action: 'click', + label: 'https://weareadaptive.com', + transport: 'beacon', + }) + window.open('https://weareadaptive.com/') + }, []) return ( diff --git a/src/client/src/apps/MainRoute/widgets/spotTile/epics/executionService.ts b/src/client/src/apps/MainRoute/widgets/spotTile/epics/executionService.ts index c3a29f7e94..60003c79fb 100644 --- a/src/client/src/apps/MainRoute/widgets/spotTile/epics/executionService.ts +++ b/src/client/src/apps/MainRoute/widgets/spotTile/epics/executionService.ts @@ -8,6 +8,7 @@ import { ExecuteTradeRequest, } from '../model/executeTradeRequest' import numeral from 'numeral' +import ReactGA from 'react-ga' interface RawTradeReponse { Trade: TradeRaw @@ -35,7 +36,15 @@ export default class ExecutionService { const executeTradeRequest = this.formatTradeRequest(rawExecuteTradeRequest) return this.limitChecker(executeTradeRequest).pipe( - tap(() => console.info(LOG_NAME, 'executing: ', executeTradeRequest)), + tap(() => { + console.info(LOG_NAME, 'executing: ', executeTradeRequest) + ReactGA.event({ + category: `RT - Trade Attempt`, + action: executeTradeRequest.Direction, + label: `${executeTradeRequest.CurrencyPair} - ${executeTradeRequest.SpotRate}`, + value: Math.round(executeTradeRequest.Notional), + }) + }), take(1), mergeMap(tradeWithinLimit => { if (!tradeWithinLimit) { @@ -50,7 +59,7 @@ export default class ExecutionService { executeTradeRequest, ) .pipe( - tap(dto => + tap(dto => { console.info( LOG_NAME, `execute response received for ${executeTradeRequest.CurrencyPair}. Status: ${dto.Trade.Status}`, @@ -58,8 +67,14 @@ export default class ExecutionService { Request: executeTradeRequest, Response: dto, }, - ), - ), + ) + ReactGA.event({ + category: `RT - Trade ${dto.Trade.Status}`, + action: executeTradeRequest.Direction, + label: `${executeTradeRequest.CurrencyPair} - ${dto.Trade.SpotRate}`, + value: Math.round(dto.Trade.Notional), + }) + }), map(dto => mapFromTradeDto(dto.Trade)), map(trade => createExecuteTradeResponse(trade, executeTradeRequest)), takeUntil(timer(EXECUTION_REQUEST_TIMEOUT_MS)), diff --git a/src/client/src/index.tsx b/src/client/src/index.tsx index ed191e6669..2ec7f695a0 100644 --- a/src/client/src/index.tsx +++ b/src/client/src/index.tsx @@ -1,5 +1,6 @@ import React, { lazy, Suspense } from 'react' import ReactDOM from 'react-dom' +import ReactGA from 'react-ga' import { BrowserRouter, Route, Switch } from 'react-router-dom' import { GlobalStyle } from 'rt-theme' import * as serviceWorker from './serviceWorker' @@ -11,6 +12,12 @@ const MainRoute = lazy(() => import('./apps/MainRoute')) const StyleguideRoute = lazy(() => import('./apps/StyleguideRoute')) const SimpleLauncher = lazy(() => import('./apps/SimpleLauncher')) +//TODO: Move to environment variables / config. +const trackingId = 'UA-46320965-5' +ReactGA.initialize(trackingId, { + debug: process.env.NODE_ENV === 'development', +}) + const { pathname } = new URL(window.location.href) const urlParams = new URLSearchParams(window.location.search) @@ -36,11 +43,17 @@ const appTitles = { async function init() { console.info('BUILD_VERSION: ', process.env.REACT_APP_BUILD_VERSION) - const intentsProvider = getProvider() + const intentsProvider = getProvider() const env = getEnvironment() + document.title = `${appTitles[pathname] || document.title} ${envTitles[env || 'unknown']}` + ReactGA.set({ + dimension3: env, + page: window.location.pathname, + }) + if (urlParams.has('startAsSymphonyController')) { const { initiateSymphony } = await getSymphonyPlatform() await initiateSymphony(urlParams.get('env') || undefined) diff --git a/src/client/src/rt-platforms/browser/browser.ts b/src/client/src/rt-platforms/browser/browser.ts index ee078db7d6..da24f969c3 100644 --- a/src/client/src/rt-platforms/browser/browser.ts +++ b/src/client/src/rt-platforms/browser/browser.ts @@ -1,3 +1,4 @@ +import ReactGA from 'react-ga' import { UAParser } from 'ua-parser-js' import { WindowConfig } from '../types' import { openBrowserWindow } from './window' @@ -36,7 +37,14 @@ export default class Browser implements Platform { window = { ...createDefaultPlatformWindow(window), - open: (config: WindowConfig, onClose?: () => void) => openBrowserWindow(config, onClose), + open: (config: WindowConfig, onClose?: () => void) => { + ReactGA.event({ + category: 'RT - Window', + action: 'open', + label: config.name, + }) + return openBrowserWindow(config, onClose) + }, } notification = { diff --git a/src/client/src/rt-platforms/defaultPlatformWindow.ts b/src/client/src/rt-platforms/defaultPlatformWindow.ts index 96b2f17233..eafdd7f628 100644 --- a/src/client/src/rt-platforms/defaultPlatformWindow.ts +++ b/src/client/src/rt-platforms/defaultPlatformWindow.ts @@ -1,7 +1,15 @@ +import ReactGA from 'react-ga' import { PlatformWindow } from './platformWindow' export function createDefaultPlatformWindow(window: Window): PlatformWindow { return { - close: () => Promise.resolve(window.close()) + close: () => { + ReactGA.event({ + category: 'RT - Window', + action: 'close', + label: window.name, + }) + return Promise.resolve(window.close()) + }, } } diff --git a/src/client/src/rt-platforms/finsemble/index.ts b/src/client/src/rt-platforms/finsemble/index.ts index 92d5a769ca..dfe155cab7 100644 --- a/src/client/src/rt-platforms/finsemble/index.ts +++ b/src/client/src/rt-platforms/finsemble/index.ts @@ -1,3 +1,4 @@ +import ReactGA from 'react-ga' import { Platform } from '../platform' import { AppConfig, WindowConfig } from '../types' import { fromEventPattern } from 'rxjs' @@ -21,6 +22,11 @@ export class Finsemble implements Platform { window = { ...createDefaultPlatformWindow(window), open: (config: WindowConfig, onClose?: () => void) => { + ReactGA.event({ + category: 'RT - Window', + action: 'open', + label: config.name, + }) const createdWindow = window.open() return Promise.resolve(createdWindow ? createDefaultPlatformWindow(createdWindow) : undefined) }, diff --git a/src/client/src/rt-platforms/glue/glue.ts b/src/client/src/rt-platforms/glue/glue.ts index fb117919f4..49c37eecd0 100644 --- a/src/client/src/rt-platforms/glue/glue.ts +++ b/src/client/src/rt-platforms/glue/glue.ts @@ -1,3 +1,4 @@ +import ReactGA from 'react-ga' import Glue, { Glue42 as GlueInterface } from '@glue42/desktop' import Glue4Office, { Glue42Office as Glue42OfficeInterface } from '@glue42/office' import { WindowConfig } from '../types' @@ -36,9 +37,23 @@ export class Glue42 implements Platform { } window = { - close: () => Promise.resolve(window.close()), + close: () => { + ReactGA.event({ + category: 'RT - Window', + action: 'close', + label: window.name, + }) + return Promise.resolve(window.close()) + }, - open: (config: WindowConfig, onClose?: () => void) => openGlueWindow(config, onClose), + open: (config: WindowConfig, onClose?: () => void) => { + ReactGA.event({ + category: 'RT - Window', + action: 'open', + label: config.name, + }) + return openGlueWindow(config, onClose) + }, /** * In order to integrate Glue42 with channels the clicked symbol needs to be published to the channel. diff --git a/src/client/src/rt-platforms/openFin/adapter/window.ts b/src/client/src/rt-platforms/openFin/adapter/window.ts index 973e9b8c83..c1ef4933eb 100644 --- a/src/client/src/rt-platforms/openFin/adapter/window.ts +++ b/src/client/src/rt-platforms/openFin/adapter/window.ts @@ -1,4 +1,5 @@ /* eslint-disable no-undef */ +import ReactGA from 'react-ga' import { WindowConfig } from '../../types' import { get as _get, last as _last } from 'lodash' import { PlatformWindow } from '../../platformWindow' @@ -112,6 +113,12 @@ export const openDesktopWindow = async ( console.info(`Creating Openfin window: ${windowName}`) + ReactGA.event({ + category: 'RT - Window', + action: 'open', + label: windowName, + }) + //TODO: move to openfin V2 version (based on promises) once they fix their bug related to getting current window // (in V2 call to ofWindow.getWebWindow() returns undefined - thus we are forced to use old callback APIs) const ofWindowPromise = new Promise(resolve => { diff --git a/src/client/src/rt-platforms/symphony/adapter/index.ts b/src/client/src/rt-platforms/symphony/adapter/index.ts index 57b197b95f..3373434b76 100644 --- a/src/client/src/rt-platforms/symphony/adapter/index.ts +++ b/src/client/src/rt-platforms/symphony/adapter/index.ts @@ -1,3 +1,4 @@ +import ReactGA from 'react-ga' import { Platform } from '../../platform' import { WindowConfig } from '../../types' import { createTileMessage, FX_ENTITY_TYPE, SYMPHONY_APP_ID, SymphonyClient } from '../index' @@ -41,6 +42,11 @@ export default class Symphony implements Platform { window = { ...createDefaultPlatformWindow(window), open: (config: WindowConfig, onClose?: () => void) => { + ReactGA.event({ + category: 'RT - Window', + action: 'open', + label: config.name, + }) return Promise.resolve(undefined) }, }