1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

re-implement google analytics

This commit is contained in:
checktheroads
2021-01-10 01:14:57 -07:00
parent ccf9bb1fc0
commit 4137286ca4
11 changed files with 148 additions and 14 deletions

View File

@@ -54,6 +54,7 @@ The following global settings can be set in `hyperglass.yaml`:
| `listen_port` | Integer | `8001` | Local TCP port the hyperglass application listens on to serve web traffic. | | `listen_port` | Integer | `8001` | Local TCP port the hyperglass application listens on to serve web traffic. |
| `cors_origins` | List | `[]` | Allowed [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) hosts. By default, no CORS hosts are allowed. | | `cors_origins` | List | `[]` | Allowed [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) hosts. By default, no CORS hosts are allowed. |
| `netmiko_delay_factor` | Integer \| Float | `0.1` | Override the [Netmiko global delay factor](https://ktbyers.github.io/netmiko/docs/netmiko/index.html). | | `netmiko_delay_factor` | Integer \| Float | `0.1` | Override the [Netmiko global delay factor](https://ktbyers.github.io/netmiko/docs/netmiko/index.html). |
| `google_analytics` | String | | Google Analytics Tracking ID |
:::note :::note
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. 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.

View File

@@ -115,6 +115,7 @@ class Params(HyperglassModel):
title="Netmiko Delay Factor", title="Netmiko Delay Factor",
description="Override the netmiko global delay factor.", description="Override the netmiko global delay factor.",
) )
google_analytics: Optional[StrictStr]
# Sub Level Params # Sub Level Params
cache: Cache = Cache() cache: Cache = Cache()

View File

@@ -2,6 +2,7 @@ export * from './useASNDetail';
export * from './useBooleanValue'; export * from './useBooleanValue';
export * from './useDevice'; export * from './useDevice';
export * from './useDNSQuery'; export * from './useDNSQuery';
export * from './useGoogleAnalytics';
export * from './useGreeting'; export * from './useGreeting';
export * from './useLGQuery'; export * from './useLGQuery';
export * from './useLGState'; export * from './useLGState';

View File

@@ -1,5 +1,6 @@
import { State } from '@hookstate/core'; import type { State } from '@hookstate/core';
import type { QueryFunctionContext } from 'react-query'; import type { QueryFunctionContext } from 'react-query';
import type * as ReactGA from 'react-ga';
import type { import type {
TDevice, TDevice,
Families, Families,
@@ -89,9 +90,14 @@ export type TLGStateHandlers = {
stateExporter<O extends unknown>(o: O): O | null; stateExporter<O extends unknown>(o: O): O | null;
}; };
export type UseStrfArgs = { [k: string]: any } | string; export type UseStrfArgs = { [k: string]: unknown } | string;
export type TTableToStringFormatter = (v: any) => string; export type TTableToStringFormatter =
| ((v: string) => string)
| ((v: number) => string)
| ((v: number[]) => string)
| ((v: string[]) => string)
| ((v: boolean) => string);
export type TTableToStringFormatted = { export type TTableToStringFormatted = {
age: (v: number) => string; age: (v: number) => string;
@@ -100,3 +106,13 @@ export type TTableToStringFormatted = {
communities: (v: string[]) => string; communities: (v: string[]) => string;
rpki_state: (v: number, n: TRPKIStates) => string; rpki_state: (v: number, n: TRPKIStates) => string;
}; };
export type GAEffect = (ga: typeof ReactGA) => void;
export interface GAReturn {
ga: typeof ReactGA;
initialize(trackingId: string | null, debug: boolean): void;
trackPage(path: string): void;
trackModal(path: string): void;
trackEvent(event: ReactGA.EventArgs): void;
}

View File

@@ -1,6 +1,7 @@
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import { fetchWithTimeout } from '~/util'; import { fetchWithTimeout } from '~/util';
import { useGoogleAnalytics } from './useGoogleAnalytics';
import type { QueryObserverResult } from 'react-query'; import type { QueryObserverResult } from 'react-query';
import type { DnsOverHttps } from '~/types'; import type { DnsOverHttps } from '~/types';
@@ -49,6 +50,12 @@ export function useDNSQuery(
family: 4 | 6, family: 4 | 6,
): QueryObserverResult<DnsOverHttps.Response> { ): QueryObserverResult<DnsOverHttps.Response> {
const { cache, web } = useConfig(); const { cache, web } = useConfig();
const { trackEvent } = useGoogleAnalytics();
if (typeof target === 'string') {
trackEvent({ category: 'DNS', action: 'Query', label: target, dimension1: `IPv${family}` });
}
return useQuery([web.dns_provider.url, { target, family }], dnsQuery, { return useQuery([web.dns_provider.url, { target, family }], dnsQuery, {
cacheTime: cache.timeout * 1000, cacheTime: cache.timeout * 1000,
}); });

View File

@@ -0,0 +1,81 @@
import { useCallback } from 'react';
import { createState, useState } from '@hookstate/core';
import * as ReactGA from 'react-ga';
import type { GAEffect, GAReturn } from './types';
const enabledState = createState<boolean>(false);
export function useGoogleAnalytics(): GAReturn {
const enabled = useState<boolean>(enabledState);
const useAnalytics = useCallback((effect: GAEffect): void => {
if (typeof window !== 'undefined' && enabled.value) {
if (typeof effect === 'function') {
effect(ReactGA);
}
}
}, []);
const trackEvent = useCallback((e: ReactGA.EventArgs) => {
useAnalytics(ga => {
if (process.env.NODE_ENV === 'production') {
ga.event(e);
} else {
console.log(
`%cEvent %c${JSON.stringify(e)}`,
'background: green; color: black; padding: 0.5rem; font-size: 0.75rem;',
'background: black; color: green; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
}, []);
const trackPage = useCallback((path: string) => {
useAnalytics(ga => {
if (process.env.NODE_ENV === 'production') {
ga.pageview(path);
} else {
console.log(
`%cPage View %c${path}`,
'background: blue; color: white; padding: 0.5rem; font-size: 0.75rem;',
'background: white; color: blue; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
}, []);
const trackModal = useCallback((path: string) => {
useAnalytics(ga => {
if (process.env.NODE_ENV === 'production') {
ga.modalview(path);
} else {
console.log(
`%cModal View %c${path}`,
'background: red; color: white; padding: 0.5rem; font-size: 0.75rem;',
'background: white; color: red; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
}, []);
const initialize = useCallback((trackingId: string, debug: boolean) => {
if (typeof trackingId !== 'string') {
return;
}
enabled.set(true);
const initializeOpts = { titleCase: false } as ReactGA.InitializeOptions;
if (debug) {
initializeOpts.debug = true;
}
useAnalytics(ga => {
ga.initialize(trackingId, initializeOpts);
});
}, []);
return { trackEvent, trackModal, trackPage, initialize, ga: ReactGA };
}

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import { useGoogleAnalytics } from './useGoogleAnalytics';
import { fetchWithTimeout } from '~/util'; import { fetchWithTimeout } from '~/util';
import type { QueryObserverResult } from 'react-query'; import type { QueryObserverResult } from 'react-query';
@@ -14,6 +15,17 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
const { request_timeout, cache } = useConfig(); const { request_timeout, cache } = useConfig();
const controller = new AbortController(); const controller = new AbortController();
const { trackEvent } = useGoogleAnalytics();
trackEvent({
category: 'Query',
action: 'submit',
dimension1: query.queryLocation,
dimension2: query.queryTarget,
dimension3: query.queryType,
dimension4: query.queryVrf,
});
async function runQuery(ctx: TUseLGQueryFn): Promise<TQueryResponse> { async function runQuery(ctx: TUseLGQueryFn): Promise<TQueryResponse> {
const [url, data] = ctx.queryKey; const [url, data] = ctx.queryKey;
const { queryLocation, queryTarget, queryType, queryVrf } = data; const { queryLocation, queryTarget, queryType, queryVrf } = data;

View File

@@ -6,11 +6,11 @@
"license": "BSD-3-Clause-Clear", "license": "BSD-3-Clause-Clear",
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint . --ext .ts --ext .tsx",
"dev": "node nextdev", "dev": "node nextdev",
"start": "next start", "start": "next start",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"format": "prettier -c -w .", "format": "prettier -c .",
"clean": "rimraf --no-glob ./.next ./out", "clean": "rimraf --no-glob ./.next ./out",
"check:es:export": "es-check es5 './out/**/*.js' -v", "check:es:export": "es-check es5 './out/**/*.js' -v",
"check:es:build": "es-check es5 './.next/static/**/*.js' -v", "check:es:build": "es-check es5 './.next/static/**/*.js' -v",
@@ -22,7 +22,7 @@
"@emotion/react": "^11.1.4", "@emotion/react": "^11.1.4",
"@emotion/styled": "^11.0.0", "@emotion/styled": "^11.0.0",
"@hookform/resolvers": "^1.2.0", "@hookform/resolvers": "^1.2.0",
"@hookstate/core": "^3.0.1", "@hookstate/core": "^3.0.3",
"@hookstate/persistence": "^3.0.0", "@hookstate/persistence": "^3.0.0",
"@meronex/icons": "^4.0.0", "@meronex/icons": "^4.0.0",
"color2k": "^1.1.1", "color2k": "^1.1.1",
@@ -36,6 +36,7 @@
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-flow-renderer": "^8.2.3", "react-flow-renderer": "^8.2.3",
"react-ga": "^3.3.0",
"react-hook-form": "^6.13.1", "react-hook-form": "^6.13.1",
"react-markdown": "^5.0.3", "react-markdown": "^5.0.3",
"react-query": "^3.5.6", "react-query": "^3.5.6",

View File

@@ -1,5 +1,7 @@
import { useEffect } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import { HyperglassProvider } from '~/context'; import { HyperglassProvider } from '~/context';
import { useGoogleAnalytics } from '~/hooks';
import { IConfig } from '~/types'; import { IConfig } from '~/types';
import type { AppProps, AppInitialProps, AppContext } from 'next/app'; import type { AppProps, AppInitialProps, AppContext } from 'next/app';
@@ -12,13 +14,20 @@ type TApp = { config: IConfig };
type GetInitialPropsReturn<IP> = AppProps & AppInitialProps & { appProps: IP }; type GetInitialPropsReturn<IP> = AppProps & AppInitialProps & { appProps: IP };
type Temp<IP> = React.FC<GetInitialPropsReturn<IP>> & { type NextApp<IP> = React.FC<GetInitialPropsReturn<IP>> & {
getInitialProps(c?: AppContext): Promise<{ appProps: IP }>; getInitialProps(c?: AppContext): Promise<{ appProps: IP }>;
}; };
const App: Temp<TApp> = (props: GetInitialPropsReturn<TApp>) => { const App: NextApp<TApp> = (props: GetInitialPropsReturn<TApp>) => {
const { Component, pageProps, appProps } = props; const { Component, pageProps, appProps, router } = props;
const { config } = appProps; const { config } = appProps;
const { initialize, trackPage } = useGoogleAnalytics();
initialize(config.google_analytics, config.developer_mode);
useEffect(() => {
router.events.on('routeChangeComplete', trackPage);
}, []);
return ( return (
<> <>

View File

@@ -169,7 +169,7 @@ export interface IConfig {
primary_asn: string; primary_asn: string;
request_timeout: number; request_timeout: number;
org_name: string; org_name: string;
google_analytics?: string; google_analytics: string | null;
site_title: string; site_title: string;
site_keywords: string[]; site_keywords: string[];
site_description: string; site_description: string;

View File

@@ -926,10 +926,10 @@
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-1.2.0.tgz#3a429b4bd3ec9981764fcdcc2491202f9f72d847" resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-1.2.0.tgz#3a429b4bd3ec9981764fcdcc2491202f9f72d847"
integrity sha512-YCKEj/3Kdo3uNt+zrWKV8txaiuATtvgHyz+KYmun3n5JDjxdI0HcVQgfcmJabmkBXBzKuIIrYfxaV8sRuAPZ8w== integrity sha512-YCKEj/3Kdo3uNt+zrWKV8txaiuATtvgHyz+KYmun3n5JDjxdI0HcVQgfcmJabmkBXBzKuIIrYfxaV8sRuAPZ8w==
"@hookstate/core@^3.0.1": "@hookstate/core@^3.0.3":
version "3.0.1" version "3.0.3"
resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.1.tgz#dc46ea71e3bf0ab5c2dc024029c9210ed12fbb84" resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.3.tgz#9fdd0e29d3cd4f3ce70ec209071d9d31b3a33677"
integrity sha512-buRie83l3FYPLCuaBE68puE3XS19r2O+jwC/kH2ikIW7ww8AavndR3MspzMMkNpq2zL8pZtIcpiWJBbn4Uq2Vw== integrity sha512-tlcBxWrOZCEw3ExHTlvLK93q+JPV6d1BayjvFkXIPDCNC0fSc/aXOWjE7gywTnUYrrsidYNiOBjAROHUIWHoUw==
"@hookstate/devtools@^3.0.0": "@hookstate/devtools@^3.0.0":
version "3.0.0" version "3.0.0"
@@ -6189,6 +6189,11 @@ react-focus-lock@2.4.1:
use-callback-ref "^1.2.1" use-callback-ref "^1.2.1"
use-sidecar "^1.0.1" use-sidecar "^1.0.1"
react-ga@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca"
integrity sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==
react-hook-form@^6.13.1: react-hook-form@^6.13.1:
version "6.13.1" version "6.13.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.13.1.tgz#b9c0aa61f746db8169ed5e1050de21cacb1947d6" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.13.1.tgz#b9c0aa61f746db8169ed5e1050de21cacb1947d6"