Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Google Analytics #1049

Merged
merged 3 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@swc/helpers": "^0.4.11",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/gtag.js": "^0.0.11",
"@types/jest": "^29.0.0",
"@types/node": "^18.7.16",
"@types/rimraf": "^3.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,6 @@ export default /** @type {import('next').NextConfig} */ ({
source: '/:path*',
headers: securityHeaders,
},
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apedroferreira Any idea on how this passed the prettier CI check up until now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with docs/next.config.js. It gets updated when I run yarn prettier:all

Copy link
Member Author

@apedroferreira apedroferreira Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, maybe it's not running for files with the .mjs extension?
i probably just saved the file and prettier added this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird... if i run it locally this file always gets fixed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did prettier get updated recently? CI only checks for changed files I believe. Perhaps we should run prettier:all in CI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, i merged this from renovatebot 735011e

that was probably it

];
},
});
21 changes: 20 additions & 1 deletion packages/toolpad-app/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import * as React from 'react';
import Head from 'next/head';
import App, { AppContext, AppProps } from 'next/app';
import App, { AppContext, AppProps, NextWebVitalsMetric } from 'next/app';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { LicenseInfo } from '@mui/x-data-grid-pro';
import { useRouter } from 'next/router';
import theme from '../src/theme';
import createEmotionCache from '../src/createEmotionCache';
import { MUI_X_PRO_LICENSE } from '../src/constants';
import { queryClient } from '../src/api';
import { reportWebVitalsToGA, setGAPage } from '../src/utils/ga';

LicenseInfo.setLicenseKey(MUI_X_PRO_LICENSE);

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

export const reportWebVitals = (metric: NextWebVitalsMetric): void => {
reportWebVitalsToGA(metric);
};

interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
}

export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;

const router = useRouter();

React.useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apedroferreira

  • How useful would this really be? We also have react-router on the editor, so the data wouldn't be exactly a good representation of user navigation throughout the app.
  • Did you verify we only send one event on hard reload?

Copy link
Member Author

@apedroferreira apedroferreira Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took it from this example https://github.com/vercel/next.js/tree/canary/examples/with-google-analytics, just rechecked again and as far as I can tell:

  • hard reload only triggers one page view
  • client side router changes (creating a new connection for example) trigger page views too, probably because of hashChangeComplete

Copy link
Member Author

@apedroferreira apedroferreira Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to run for the iframe routes too, not sure if that's helpful

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might make sense to disable for the app canvas

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok actually it turns out this effect is doing nothing at all... the page view events are being sent automatically.
i'll remove this effect and see if i can filter out events from the iframe

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and because the page view events are sent automatically doesn't seem simple to filter out the ones from the canvas... - we can just ignore that data in the google analytics dashboard itself if we want to use page view data, im gonna make a PR that just removes the effect

router.events.on('routeChangeComplete', setGAPage);
router.events.on('hashChangeComplete', setGAPage);

return () => {
router.events.off('routeChangeComplete', setGAPage);
router.events.off('hashChangeComplete', setGAPage);
};
}, [router.events]);

return (
<CacheProvider value={emotionCache}>
<Head>
Expand Down
23 changes: 23 additions & 0 deletions packages/toolpad-app/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import Document, {
} from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import serializeJavascript from 'serialize-javascript';
import Script from 'next/script';
import theme from '../src/theme';
import createEmotionCache from '../src/createEmotionCache';
import config, { RuntimeConfig } from '../src/config';
import { RUNTIME_CONFIG_WINDOW_PROPERTY } from '../src/constants';
import { GA_ID } from '../src/utils/ga';

interface ToolpadDocumentProps {
config: RuntimeConfig;
Expand Down Expand Up @@ -95,6 +97,27 @@ export default class MyDocument extends Document<ToolpadDocumentProps> {
)}] = ${serializeJavascript(this.props.config, { ignoreFunction: true })}`,
}}
/>

{/* Global site tag (gtag.js) - Google Analytics */}
<Script
async
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
</Head>
{/* https://github.com/facebook/react/issues/11538 */}
<body className="notranslate">
Expand Down
3 changes: 3 additions & 0 deletions packages/toolpad-app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface RuntimeConfig {
// Enable input field for seeding a dom in the app creation dialog
// (For testing purposes)
enableCreateByDom?: boolean;
// Google Analytics measurement ID
gaId?: string;
}

declare global {
Expand Down Expand Up @@ -68,6 +70,7 @@ const runtimeConfig: RuntimeConfig =
? {
// Define runtime config here
enableCreateByDom: !!process.env.TOOLPAD_ENABLE_CREATE_BY_DOM,
gaId: process.env.TOOLPAD_GA_ID,
}
: getBrowsersideRuntimeConfig();

Expand Down
21 changes: 21 additions & 0 deletions packages/toolpad-app/src/utils/ga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { NextWebVitalsMetric } from 'next/app';
import config from '../config';

export const GA_ID = config.gaId;

export const setGAPage = (pagePath: string): void => {
if (GA_ID) {
window.gtag('config', GA_ID, { page_path: pagePath });
}
};

export const reportWebVitalsToGA = ({ id, label, name, value }: NextWebVitalsMetric): void => {
if (GA_ID) {
window.gtag('event', name, {
event_category: label === 'web-vital' ? 'Web Vitals' : 'Next.js custom metric',
value: Math.round(name === 'CLS' ? value * 1000 : value), // values must be integers
event_label: id, // id unique to current page load
non_interaction: true, // avoids affecting bounce rate.
});
}
};
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2709,6 +2709,11 @@
dependencies:
"@types/node" "*"

"@types/gtag.js@^0.0.11":
version "0.0.11"
resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.11.tgz#0c384ea32e4e40043a2dca82db79b5e48d681ad5"
integrity sha512-rUuSDedDjcuUpoc2zf6eX6zRrxqALNgwrmMBfVFopkLH7YGM52C7tt6j9GsYIvaxn+ioVRpOKoHnN1DXzHEqIg==

"@types/har-format@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.8.tgz#e6908b76d4c88be3db642846bb8b455f0bfb1c4e"
Expand Down