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:
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
81
hyperglass/ui/hooks/useGoogleAnalytics.tsx
Normal file
81
hyperglass/ui/hooks/useGoogleAnalytics.tsx
Normal 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 };
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
7
hyperglass/ui/package.json
vendored
7
hyperglass/ui/package.json
vendored
@@ -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",
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
13
hyperglass/ui/yarn.lock
vendored
13
hyperglass/ui/yarn.lock
vendored
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user