Skip to content

Commit 4137286

Browse files
committed
re-implement google analytics
1 parent ccf9bb1 commit 4137286

11 files changed

Lines changed: 148 additions & 14 deletions

File tree

docs/docs/parameters.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ The following global settings can be set in `hyperglass.yaml`:
5454
| `listen_port` | Integer | `8001` | Local TCP port the hyperglass application listens on to serve web traffic. |
5555
| `cors_origins` | List | `[]` | Allowed [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) hosts. By default, no CORS hosts are allowed. |
5656
| `netmiko_delay_factor` | Integer \| Float | `0.1` | Override the [Netmiko global delay factor](https://ktbyers.github.io/netmiko/docs/netmiko/index.html). |
57+
| `google_analytics` | String | | Google Analytics Tracking ID |
5758

5859
:::note
5960
The `netmiko_delay_factor` parameter should only be used if you're experiencing strange SSH connection issues. By default, Netmiko uses a `global_delay_factor` of `1`, which tends to be a bit slow for running a simple show command. hyperglass overrides this to `0.1` by default, but you can override this to whatever value suits your environment if needed.

hyperglass/models/config/params.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class Params(HyperglassModel):
115115
title="Netmiko Delay Factor",
116116
description="Override the netmiko global delay factor.",
117117
)
118+
google_analytics: Optional[StrictStr]
118119

119120
# Sub Level Params
120121
cache: Cache = Cache()

hyperglass/ui/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './useASNDetail';
22
export * from './useBooleanValue';
33
export * from './useDevice';
44
export * from './useDNSQuery';
5+
export * from './useGoogleAnalytics';
56
export * from './useGreeting';
67
export * from './useLGQuery';
78
export * from './useLGState';

hyperglass/ui/hooks/types.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { State } from '@hookstate/core';
1+
import type { State } from '@hookstate/core';
22
import type { QueryFunctionContext } from 'react-query';
3+
import type * as ReactGA from 'react-ga';
34
import type {
45
TDevice,
56
Families,
@@ -89,9 +90,14 @@ export type TLGStateHandlers = {
8990
stateExporter<O extends unknown>(o: O): O | null;
9091
};
9192

92-
export type UseStrfArgs = { [k: string]: any } | string;
93+
export type UseStrfArgs = { [k: string]: unknown } | string;
9394

94-
export type TTableToStringFormatter = (v: any) => string;
95+
export type TTableToStringFormatter =
96+
| ((v: string) => string)
97+
| ((v: number) => string)
98+
| ((v: number[]) => string)
99+
| ((v: string[]) => string)
100+
| ((v: boolean) => string);
95101

96102
export type TTableToStringFormatted = {
97103
age: (v: number) => string;
@@ -100,3 +106,13 @@ export type TTableToStringFormatted = {
100106
communities: (v: string[]) => string;
101107
rpki_state: (v: number, n: TRPKIStates) => string;
102108
};
109+
110+
export type GAEffect = (ga: typeof ReactGA) => void;
111+
112+
export interface GAReturn {
113+
ga: typeof ReactGA;
114+
initialize(trackingId: string | null, debug: boolean): void;
115+
trackPage(path: string): void;
116+
trackModal(path: string): void;
117+
trackEvent(event: ReactGA.EventArgs): void;
118+
}

hyperglass/ui/hooks/useDNSQuery.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useQuery } from 'react-query';
22
import { useConfig } from '~/context';
33
import { fetchWithTimeout } from '~/util';
4+
import { useGoogleAnalytics } from './useGoogleAnalytics';
45

56
import type { QueryObserverResult } from 'react-query';
67
import type { DnsOverHttps } from '~/types';
@@ -49,6 +50,12 @@ export function useDNSQuery(
4950
family: 4 | 6,
5051
): QueryObserverResult<DnsOverHttps.Response> {
5152
const { cache, web } = useConfig();
53+
const { trackEvent } = useGoogleAnalytics();
54+
55+
if (typeof target === 'string') {
56+
trackEvent({ category: 'DNS', action: 'Query', label: target, dimension1: `IPv${family}` });
57+
}
58+
5259
return useQuery([web.dns_provider.url, { target, family }], dnsQuery, {
5360
cacheTime: cache.timeout * 1000,
5461
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useCallback } from 'react';
2+
import { createState, useState } from '@hookstate/core';
3+
import * as ReactGA from 'react-ga';
4+
5+
import type { GAEffect, GAReturn } from './types';
6+
7+
const enabledState = createState<boolean>(false);
8+
9+
export function useGoogleAnalytics(): GAReturn {
10+
const enabled = useState<boolean>(enabledState);
11+
12+
const useAnalytics = useCallback((effect: GAEffect): void => {
13+
if (typeof window !== 'undefined' && enabled.value) {
14+
if (typeof effect === 'function') {
15+
effect(ReactGA);
16+
}
17+
}
18+
}, []);
19+
20+
const trackEvent = useCallback((e: ReactGA.EventArgs) => {
21+
useAnalytics(ga => {
22+
if (process.env.NODE_ENV === 'production') {
23+
ga.event(e);
24+
} else {
25+
console.log(
26+
`%cEvent %c${JSON.stringify(e)}`,
27+
'background: green; color: black; padding: 0.5rem; font-size: 0.75rem;',
28+
'background: black; color: green; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
29+
);
30+
}
31+
});
32+
}, []);
33+
34+
const trackPage = useCallback((path: string) => {
35+
useAnalytics(ga => {
36+
if (process.env.NODE_ENV === 'production') {
37+
ga.pageview(path);
38+
} else {
39+
console.log(
40+
`%cPage View %c${path}`,
41+
'background: blue; color: white; padding: 0.5rem; font-size: 0.75rem;',
42+
'background: white; color: blue; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
43+
);
44+
}
45+
});
46+
}, []);
47+
48+
const trackModal = useCallback((path: string) => {
49+
useAnalytics(ga => {
50+
if (process.env.NODE_ENV === 'production') {
51+
ga.modalview(path);
52+
} else {
53+
console.log(
54+
`%cModal View %c${path}`,
55+
'background: red; color: white; padding: 0.5rem; font-size: 0.75rem;',
56+
'background: white; color: red; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
57+
);
58+
}
59+
});
60+
}, []);
61+
62+
const initialize = useCallback((trackingId: string, debug: boolean) => {
63+
if (typeof trackingId !== 'string') {
64+
return;
65+
}
66+
67+
enabled.set(true);
68+
69+
const initializeOpts = { titleCase: false } as ReactGA.InitializeOptions;
70+
71+
if (debug) {
72+
initializeOpts.debug = true;
73+
}
74+
75+
useAnalytics(ga => {
76+
ga.initialize(trackingId, initializeOpts);
77+
});
78+
}, []);
79+
80+
return { trackEvent, trackModal, trackPage, initialize, ga: ReactGA };
81+
}

hyperglass/ui/hooks/useLGQuery.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect } from 'react';
22
import { useQuery } from 'react-query';
33
import { useConfig } from '~/context';
4+
import { useGoogleAnalytics } from './useGoogleAnalytics';
45
import { fetchWithTimeout } from '~/util';
56

67
import type { QueryObserverResult } from 'react-query';
@@ -14,6 +15,17 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
1415
const { request_timeout, cache } = useConfig();
1516
const controller = new AbortController();
1617

18+
const { trackEvent } = useGoogleAnalytics();
19+
20+
trackEvent({
21+
category: 'Query',
22+
action: 'submit',
23+
dimension1: query.queryLocation,
24+
dimension2: query.queryTarget,
25+
dimension3: query.queryType,
26+
dimension4: query.queryVrf,
27+
});
28+
1729
async function runQuery(ctx: TUseLGQueryFn): Promise<TQueryResponse> {
1830
const [url, data] = ctx.queryKey;
1931
const { queryLocation, queryTarget, queryType, queryVrf } = data;

hyperglass/ui/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"license": "BSD-3-Clause-Clear",
77
"private": true,
88
"scripts": {
9-
"lint": "eslint .",
9+
"lint": "eslint . --ext .ts --ext .tsx",
1010
"dev": "node nextdev",
1111
"start": "next start",
1212
"typecheck": "tsc --noEmit",
13-
"format": "prettier -c -w .",
13+
"format": "prettier -c .",
1414
"clean": "rimraf --no-glob ./.next ./out",
1515
"check:es:export": "es-check es5 './out/**/*.js' -v",
1616
"check:es:build": "es-check es5 './.next/static/**/*.js' -v",
@@ -22,7 +22,7 @@
2222
"@emotion/react": "^11.1.4",
2323
"@emotion/styled": "^11.0.0",
2424
"@hookform/resolvers": "^1.2.0",
25-
"@hookstate/core": "^3.0.1",
25+
"@hookstate/core": "^3.0.3",
2626
"@hookstate/persistence": "^3.0.0",
2727
"@meronex/icons": "^4.0.0",
2828
"color2k": "^1.1.1",
@@ -36,6 +36,7 @@
3636
"react-dom": "^17.0.1",
3737
"react-fast-compare": "^3.2.0",
3838
"react-flow-renderer": "^8.2.3",
39+
"react-ga": "^3.3.0",
3940
"react-hook-form": "^6.13.1",
4041
"react-markdown": "^5.0.3",
4142
"react-query": "^3.5.6",

hyperglass/ui/pages/_app.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useEffect } from 'react';
12
import Head from 'next/head';
23
import { HyperglassProvider } from '~/context';
4+
import { useGoogleAnalytics } from '~/hooks';
35
import { IConfig } from '~/types';
46

57
import type { AppProps, AppInitialProps, AppContext } from 'next/app';
@@ -12,13 +14,20 @@ type TApp = { config: IConfig };
1214

1315
type GetInitialPropsReturn<IP> = AppProps & AppInitialProps & { appProps: IP };
1416

15-
type Temp<IP> = React.FC<GetInitialPropsReturn<IP>> & {
17+
type NextApp<IP> = React.FC<GetInitialPropsReturn<IP>> & {
1618
getInitialProps(c?: AppContext): Promise<{ appProps: IP }>;
1719
};
1820

19-
const App: Temp<TApp> = (props: GetInitialPropsReturn<TApp>) => {
20-
const { Component, pageProps, appProps } = props;
21+
const App: NextApp<TApp> = (props: GetInitialPropsReturn<TApp>) => {
22+
const { Component, pageProps, appProps, router } = props;
2123
const { config } = appProps;
24+
const { initialize, trackPage } = useGoogleAnalytics();
25+
26+
initialize(config.google_analytics, config.developer_mode);
27+
28+
useEffect(() => {
29+
router.events.on('routeChangeComplete', trackPage);
30+
}, []);
2231

2332
return (
2433
<>

hyperglass/ui/types/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export interface IConfig {
169169
primary_asn: string;
170170
request_timeout: number;
171171
org_name: string;
172-
google_analytics?: string;
172+
google_analytics: string | null;
173173
site_title: string;
174174
site_keywords: string[];
175175
site_description: string;

0 commit comments

Comments
 (0)