From 0ec3086c672b17abc754de595bae243bd23c3812 Mon Sep 17 00:00:00 2001
From: thatmattlove
Date: Wed, 8 Dec 2021 16:23:59 -0700
Subject: [PATCH] Closes #176: Deprecate native google analytics support; Add
support for custom JS and HTML
---
hyperglass/models/config/web.py | 27 +----
hyperglass/ui/.eslintignore | 1 +
hyperglass/ui/.gitignore | 1 +
hyperglass/ui/.prettierignore | 1 +
hyperglass/ui/components/custom.tsx | 25 +++++
hyperglass/ui/components/index.ts | 1 +
hyperglass/ui/components/layout/frame.tsx | 19 +---
hyperglass/ui/hooks/index.ts | 1 -
hyperglass/ui/hooks/useDNSQuery.ts | 6 --
hyperglass/ui/hooks/useGoogleAnalytics.tsx | 110 ---------------------
hyperglass/ui/hooks/useLGQuery.ts | 11 ---
hyperglass/ui/pages/_document.tsx | 18 +++-
hyperglass/ui/types/config.ts | 1 -
hyperglass/util/frontend.py | 21 ++++
14 files changed, 69 insertions(+), 174 deletions(-)
create mode 100644 hyperglass/ui/components/custom.tsx
delete mode 100644 hyperglass/ui/hooks/useGoogleAnalytics.tsx
diff --git a/hyperglass/models/config/web.py b/hyperglass/models/config/web.py
index 29bdaff..4704f00 100644
--- a/hyperglass/models/config/web.py
+++ b/hyperglass/models/config/web.py
@@ -36,31 +36,6 @@ Side = constr(regex=r"left|right")
LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"]
-class Analytics(HyperglassModel):
- """Validation model for Google Analytics."""
-
- enable: StrictBool = False
- id: t.Optional[StrictStr]
-
- @validator("id")
- def validate_id(cls, value, values):
- """Ensure ID is set if analytics is enabled.
-
- Arguments:
- value {str|None} -- Google Analytics ID
- values {[type]} -- Already-validated model parameters
-
- Raises:
- ValueError: Raised if analytics is enabled but no ID is set.
-
- Returns:
- {str|None} -- Google Analytics ID if enabled.
- """
- if values["enable"] and value is None:
- raise ValueError("Analytics is enabled, but no ID is set.")
- return value
-
-
class Credit(HyperglassModel):
"""Validation model for developer credit."""
@@ -270,6 +245,8 @@ class Web(HyperglassModel):
text: Text = Text()
theme: Theme = Theme()
location_display_mode: LocationDisplayMode = "auto"
+ custom_javascript: t.Optional[FilePath]
+ custom_html: t.Optional[FilePath]
class WebPublic(Web):
diff --git a/hyperglass/ui/.eslintignore b/hyperglass/ui/.eslintignore
index 4c7afc3..e06f250 100644
--- a/hyperglass/ui/.eslintignore
+++ b/hyperglass/ui/.eslintignore
@@ -2,3 +2,4 @@ node_modules
dist
.next/
favicon-formats.ts
+custom.*[js, html]
diff --git a/hyperglass/ui/.gitignore b/hyperglass/ui/.gitignore
index 4a9306a..3e347d9 100644
--- a/hyperglass/ui/.gitignore
+++ b/hyperglass/ui/.gitignore
@@ -1,5 +1,6 @@
.DS_Store
.env*
+custom.*[js, html]
*.tsbuildinfo
# dev/test files
TODO.txt
diff --git a/hyperglass/ui/.prettierignore b/hyperglass/ui/.prettierignore
index 6b48447..fc2cfe7 100644
--- a/hyperglass/ui/.prettierignore
+++ b/hyperglass/ui/.prettierignore
@@ -7,3 +7,4 @@ package-lock.json
tsconfig.json
.next/
favicon-formats.ts
+custom.*[js, html]
diff --git a/hyperglass/ui/components/custom.tsx b/hyperglass/ui/components/custom.tsx
new file mode 100644
index 0000000..e012d54
--- /dev/null
+++ b/hyperglass/ui/components/custom.tsx
@@ -0,0 +1,25 @@
+/**
+ * Render a generic script tag in the `` that contains any custom-defined Javascript, if
+ * defined. It no custom JS is defined, an empty fragment is rendered, which will not appear in
+ * the DOM.
+ */
+export const CustomJavascript = (props: React.PropsWithChildren): JSX.Element => {
+ const { children } = props;
+ if (typeof children === 'string' && children !== '') {
+ return ;
+ }
+ return <>>;
+};
+
+/**
+ * Render a generic div outside of the main application that contains any custom-defined HTML, if
+ * defined. If no custom HTML is defined, an empty fragment is rendered, which will not appear in
+ * the DOM.
+ */
+export const CustomHtml = (props: React.PropsWithChildren): JSX.Element => {
+ const { children } = props;
+ if (typeof children === 'string' && children !== '') {
+ return ;
+ }
+ return <>>;
+};
diff --git a/hyperglass/ui/components/index.ts b/hyperglass/ui/components/index.ts
index ee7d018..4084e00 100644
--- a/hyperglass/ui/components/index.ts
+++ b/hyperglass/ui/components/index.ts
@@ -1,6 +1,7 @@
export * from './card';
export * from './codeBlock';
export * from './countdown';
+export * from './custom';
export * from './debugger';
export * from './favicon';
export * from './footer';
diff --git a/hyperglass/ui/components/layout/frame.tsx b/hyperglass/ui/components/layout/frame.tsx
index 17e66f3..e6e8502 100644
--- a/hyperglass/ui/components/layout/frame.tsx
+++ b/hyperglass/ui/components/layout/frame.tsx
@@ -1,23 +1,19 @@
-import { useCallback, useEffect, useRef } from 'react';
-import { useRouter } from 'next/router';
+import { useCallback, useRef } from 'react';
import { Flex } from '@chakra-ui/react';
import { isSafari } from 'react-device-detect';
import { If, Debugger, Greeting, Footer, Header } from '~/components';
import { useConfig } from '~/context';
-import { useGoogleAnalytics, useFormState } from '~/hooks';
+import { useFormState } from '~/hooks';
import { ResetButton } from './resetButton';
import type { TFrame } from './types';
export const Frame = (props: TFrame): JSX.Element => {
- const router = useRouter();
- const { developerMode, googleAnalytics } = useConfig();
+ const { developerMode } = useConfig();
const { setStatus, reset } = useFormState(
useCallback(({ setStatus, reset }) => ({ setStatus, reset }), []),
);
- const { initialize, trackPage } = useGoogleAnalytics();
-
const containerRef = useRef({} as HTMLDivElement);
function handleReset(): void {
@@ -26,15 +22,6 @@ export const Frame = (props: TFrame): JSX.Element => {
reset();
}
- useEffect(() => {
- initialize(googleAnalytics, developerMode);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- trackPage(router.pathname);
- }, [router.pathname, trackPage]);
-
return (
<>
{
const { cache, web } = useConfig();
- const { trackEvent } = useGoogleAnalytics();
-
- if (typeof target === 'string') {
- trackEvent({ category: 'DNS', action: 'Query', label: target, dimension1: `IPv${family}` });
- }
return useQuery({
queryKey: [web.dnsProvider.url, { target, family }],
diff --git a/hyperglass/ui/hooks/useGoogleAnalytics.tsx b/hyperglass/ui/hooks/useGoogleAnalytics.tsx
deleted file mode 100644
index 13c90d2..0000000
--- a/hyperglass/ui/hooks/useGoogleAnalytics.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import create from 'zustand';
-import { useCallback } from 'react';
-import * as ReactGA from 'react-ga';
-
-import type { GAEffect, GAReturn } from './types';
-
-interface EnabledState {
- enabled: boolean;
- enable(): void;
- disable(): void;
-}
-
-const useEnabled = create(set => ({
- enabled: false,
- enable() {
- set({ enabled: true });
- },
- disable() {
- set({ enabled: false });
- },
-}));
-
-export function useGoogleAnalytics(): GAReturn {
- const { enabled, enable } = useEnabled(({ enable, enabled }) => ({ enable, enabled }));
-
- const runEffect = useCallback(
- (effect: GAEffect): void => {
- if (typeof window !== 'undefined' && enabled) {
- if (typeof effect === 'function') {
- effect(ReactGA);
- }
- }
- },
- [enabled],
- );
-
- const trackEvent = useCallback(
- (e: ReactGA.EventArgs) => {
- runEffect(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;',
- );
- }
- });
- },
- [runEffect],
- );
-
- const trackPage = useCallback(
- (path: string) => {
- runEffect(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;',
- );
- }
- });
- },
- [runEffect],
- );
-
- const trackModal = useCallback(
- (path: string) => {
- runEffect(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;',
- );
- }
- });
- },
- [runEffect],
- );
-
- const initialize = useCallback(
- (trackingId: string, debug: boolean) => {
- if (typeof trackingId !== 'string') {
- return;
- }
-
- enable();
-
- const initializeOpts = { titleCase: false } as ReactGA.InitializeOptions;
-
- if (debug) {
- initializeOpts.debug = true;
- }
-
- runEffect(ga => {
- ga.initialize(trackingId, initializeOpts);
- });
- },
- [runEffect, enable],
- );
-
- return { trackEvent, trackModal, trackPage, initialize, ga: ReactGA };
-}
diff --git a/hyperglass/ui/hooks/useLGQuery.ts b/hyperglass/ui/hooks/useLGQuery.ts
index 48a886f..b478407 100644
--- a/hyperglass/ui/hooks/useLGQuery.ts
+++ b/hyperglass/ui/hooks/useLGQuery.ts
@@ -1,7 +1,6 @@
import { useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
-import { useGoogleAnalytics } from './useGoogleAnalytics';
import { fetchWithTimeout } from '~/util';
import type { QueryFunction, QueryFunctionContext, QueryObserverResult } from 'react-query';
@@ -18,16 +17,6 @@ export function useLGQuery(
const { requestTimeout, cache } = useConfig();
const controller = useMemo(() => new AbortController(), []);
- const { trackEvent } = useGoogleAnalytics();
-
- trackEvent({
- category: 'Query',
- action: 'submit',
- dimension1: query.queryLocation,
- dimension2: query.queryTarget,
- dimension3: query.queryType,
- });
-
const runQuery: QueryFunction = async (
ctx: QueryFunctionContext,
): Promise => {
diff --git a/hyperglass/ui/pages/_document.tsx b/hyperglass/ui/pages/_document.tsx
index 5949556..8865bde 100644
--- a/hyperglass/ui/pages/_document.tsx
+++ b/hyperglass/ui/pages/_document.tsx
@@ -1,13 +1,21 @@
+import fs from 'fs';
import Document, { Html, Head, Main, NextScript } from 'next/document';
-import { Favicon } from '~/components';
+import { Favicon, CustomJavascript, CustomHtml } from '~/components';
import favicons from '../favicon-formats';
import type { DocumentContext, DocumentInitialProps } from 'next/document';
-class MyDocument extends Document {
- static async getInitialProps(ctx: DocumentContext): Promise {
+interface DocumentExtra extends DocumentInitialProps {
+ customJs: string;
+ customHtml: string;
+}
+
+class MyDocument extends Document {
+ static async getInitialProps(ctx: DocumentContext): Promise {
const initialProps = await Document.getInitialProps(ctx);
- return { ...initialProps };
+ const customJs = fs.readFileSync('custom.js').toString();
+ const customHtml = fs.readFileSync('custom.html').toString();
+ return { customJs, customHtml, ...initialProps };
}
render(): JSX.Element {
@@ -31,9 +39,11 @@ class MyDocument extends Document {
{favicons.map((favicon, idx) => (
))}
+ {this.props.customJs}
+ {this.props.customHtml}