diff --git a/packages/frontend/.env.production b/packages/frontend/.env.production
new file mode 100644
index 00000000..919d4a37
--- /dev/null
+++ b/packages/frontend/.env.production
@@ -0,0 +1 @@
+NEXT_PUBLIC_GOOGLE_MEASUREMENT_ID=G-66F9GF1VQ3
\ No newline at end of file
diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore
index 4da8988b..b5490c24 100644
--- a/packages/frontend/.gitignore
+++ b/packages/frontend/.gitignore
@@ -26,7 +26,7 @@ yarn-debug.log*
yarn-error.log*
# env files
-.env*
+.env
# vercel
.vercel
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 6c7b5c60..6608c61c 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -42,6 +42,7 @@
"@testing-library/react": "^14.1.0",
"@testing-library/user-event": "^14.5.1",
"@types/d3": "^7",
+ "@types/gtag.js": "^0.0.18",
"@types/jest": "^29.5.8",
"@types/node": "^20",
"@types/react": "^18",
diff --git a/packages/frontend/src/components/analytics/Scripts.tsx b/packages/frontend/src/components/analytics/Scripts.tsx
new file mode 100644
index 00000000..4b615409
--- /dev/null
+++ b/packages/frontend/src/components/analytics/Scripts.tsx
@@ -0,0 +1,28 @@
+import Script from "next/script";
+
+import { PRODUCTION } from "../../constants/config";
+import { GA_TRACKING_ID } from "../../libs/gtag";
+
+function Scripts() {
+ return (
+
+ {PRODUCTION && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
+
+export default Scripts;
diff --git a/packages/frontend/src/components/analytics/useGtagEffect.ts b/packages/frontend/src/components/analytics/useGtagEffect.ts
new file mode 100644
index 00000000..22ed4403
--- /dev/null
+++ b/packages/frontend/src/components/analytics/useGtagEffect.ts
@@ -0,0 +1,22 @@
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+
+import { PRODUCTION } from "../../constants/config";
+import * as gtag from "../../libs/gtag";
+
+const useGtagEffect = () => {
+ const router = useRouter();
+ useEffect(() => {
+ if (!PRODUCTION) return () => {};
+
+ const handleRouteChange = (url: URL) => {
+ gtag.pageview(url);
+ };
+ router.events.on("routeChangeComplete", handleRouteChange);
+ return () => {
+ router.events.off("routeChangeComplete", handleRouteChange);
+ };
+ }, [router.events]);
+ return null;
+};
+export default useGtagEffect;
diff --git a/packages/frontend/src/constants/config.ts b/packages/frontend/src/constants/config.ts
new file mode 100644
index 00000000..5ad4b667
--- /dev/null
+++ b/packages/frontend/src/constants/config.ts
@@ -0,0 +1 @@
+export const PRODUCTION = process.env.NODE_ENV === "production";
diff --git a/packages/frontend/src/libs/gtag.ts b/packages/frontend/src/libs/gtag.ts
new file mode 100644
index 00000000..153d3ae2
--- /dev/null
+++ b/packages/frontend/src/libs/gtag.ts
@@ -0,0 +1,30 @@
+export const GA_TRACKING_ID: string =
+ process.env.NEXT_PUBLIC_GOOGLE_MEASUREMENT_ID || "";
+
+// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
+export const pageview = (url: URL) => {
+ if (typeof window !== "object") return;
+ window.gtag("config", GA_TRACKING_ID, {
+ page_path: url,
+ });
+};
+
+// https://developers.google.com/analytics/devguides/collection/gtagjs/events
+export const event = ({
+ action,
+ category,
+ label,
+ value,
+}: {
+ action: string;
+ category: string;
+ label: string;
+ value: string;
+}) => {
+ if (typeof window !== "object") return;
+ window.gtag("event", action, {
+ event_category: category,
+ event_label: label,
+ value,
+ });
+};
diff --git a/packages/frontend/src/pages/_app.page.tsx b/packages/frontend/src/pages/_app.page.tsx
index a1217412..cfde5966 100644
--- a/packages/frontend/src/pages/_app.page.tsx
+++ b/packages/frontend/src/pages/_app.page.tsx
@@ -7,6 +7,8 @@ import React, { useEffect, useState } from "react";
import "react-toastify/dist/ReactToastify.min.css";
import { sessionAPI } from "../apis/session";
+import Scripts from "../components/analytics/Scripts";
+import useGtagEffect from "../components/analytics/useGtagEffect";
import { UserQuizStatusProvider } from "../contexts/UserQuizStatusContext";
import { ToastContainer, toast } from "../design-system/components/common";
import Layout from "../design-system/components/common/Layout";
@@ -31,6 +33,8 @@ export default function App({ Component, pageProps }: AppProps) {
})();
}, []);
+ useGtagEffect();
+
return (
<>
@@ -42,6 +46,7 @@ export default function App({ Component, pageProps }: AppProps) {
/>
+
diff --git a/yarn.lock b/yarn.lock
index 0943af2a..20de6849 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5457,6 +5457,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/gtag.js@npm:^0.0.18":
+ version: 0.0.18
+ resolution: "@types/gtag.js@npm:0.0.18"
+ checksum: af7a0a5769c5cfbe75e47f2b0346a65db8234b3c89775b897681548dfb6898edb3145153e2bc5dd1751a5ace221e03e6a68b7650ecacc6aa16a099aa9d925c99
+ languageName: node
+ linkType: hard
+
"@types/html-minifier-terser@npm:^6.0.0":
version: 6.1.0
resolution: "@types/html-minifier-terser@npm:6.1.0"
@@ -10951,6 +10958,7 @@ __metadata:
"@testing-library/react": "npm:^14.1.0"
"@testing-library/user-event": "npm:^14.5.1"
"@types/d3": "npm:^7"
+ "@types/gtag.js": "npm:^0.0.18"
"@types/jest": "npm:^29.5.8"
"@types/node": "npm:^20"
"@types/react": "npm:^18"