diff --git a/.env.example b/.env.example
index 9717ca010..890165976 100644
--- a/.env.example
+++ b/.env.example
@@ -58,13 +58,6 @@ NEXT_PUBLIC_VALIDATOR=
NODE_ENV=
-########## Umami ##########
-## Umami: self-hosted analytics service
-## Required only in production.
-NEXT_PUBLIC_UMAMI_URL=
-NEXT_PUBLIC_UMAMI_WEBSITE_ID=
-
-
########## Sentry ##########
## Sentry: error tracking service
## Not required in any environments.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e592ec0cf..c5d1ac924 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -80,7 +80,3 @@ jobs:
NODE_ENV: production
VERCEL_ENV: preview
-
- # UMAMI
- NEXT_PUBLIC_UMAMI_URL: hi
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: bye
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ea5cf7edc..09e4346f5 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -64,10 +64,6 @@ jobs:
NODE_ENV: test
- # UMAMI
- NEXT_PUBLIC_UMAMI_URL: hi
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: bye
-
steps:
- name: Checkout
uses: actions/checkout@v3
diff --git a/docs/WORKFLOWS_AND_DEPLOYMENT.md b/docs/WORKFLOWS_AND_DEPLOYMENT.md
index 4f760d99d..a05d563ba 100644
--- a/docs/WORKFLOWS_AND_DEPLOYMENT.md
+++ b/docs/WORKFLOWS_AND_DEPLOYMENT.md
@@ -24,8 +24,6 @@ All deployment/external service:
- Web server: Vercel
- TRPC Routes: Vercel Edge Functions
- Planner Postgres: Neon
-- Umami (User analytics): Railway
-- Umami Postgres: Railway
- Sentry (Crash analytics)
- Auth Providers: Discord, Google, Facebook
- Mailtrap (Email "magic link" auth)
diff --git a/package-lock.json b/package-lock.json
index 0d40458d5..3e61693ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@mui/material": "^5.4.0",
"@next-auth/prisma-adapter": "^1.0.5",
"@next/bundle-analyzer": "^13.1.6",
+ "@next/third-parties": "^15.0.2",
"@prisma/client": "^5.0.0",
"@radix-ui/react-checkbox": "^1.0.1",
"@radix-ui/react-dialog": "^1.0.3",
@@ -2223,6 +2224,18 @@
"node": ">= 10"
}
},
+ "node_modules/@next/third-parties": {
+ "version": "15.0.2",
+ "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-15.0.2.tgz",
+ "integrity": "sha512-Ohlh0KKfag3Vrx+yuSMJ/fSoCVvRoVG9wRiz8jvYelmg+l0970d41VoGzF2UeKwh9s5qXVRDVqiN/mIeiJ4iLg==",
+ "dependencies": {
+ "third-party-capital": "1.0.20"
+ },
+ "peerDependencies": {
+ "next": "^13.0.0 || ^14.0.0 || ^15.0.0",
+ "react": "^18.2.0 || 19.0.0-rc-02c0e824-20241028"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"license": "MIT",
@@ -15639,6 +15652,11 @@
"version": "0.2.0",
"license": "MIT"
},
+ "node_modules/third-party-capital": {
+ "version": "1.0.20",
+ "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz",
+ "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA=="
+ },
"node_modules/throttleit": {
"version": "1.0.0",
"dev": true,
@@ -18501,6 +18519,14 @@
"integrity": "sha512-Ls2OL9hi3YlJKGNdKv8k3X/lLgc3VmLG3a/DeTkAd+lAituJp8ZHmRmm9f9SL84fT3CotlzcgbdaCDfFwFA6bA==",
"optional": true
},
+ "@next/third-parties": {
+ "version": "15.0.2",
+ "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-15.0.2.tgz",
+ "integrity": "sha512-Ohlh0KKfag3Vrx+yuSMJ/fSoCVvRoVG9wRiz8jvYelmg+l0970d41VoGzF2UeKwh9s5qXVRDVqiN/mIeiJ4iLg==",
+ "requires": {
+ "third-party-capital": "1.0.20"
+ }
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"requires": {
@@ -27268,6 +27294,11 @@
"text-table": {
"version": "0.2.0"
},
+ "third-party-capital": {
+ "version": "1.0.20",
+ "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz",
+ "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA=="
+ },
"throttleit": {
"version": "1.0.0",
"dev": true
diff --git a/package.json b/package.json
index 004b78a8d..365c9b48f 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@mui/material": "^5.4.0",
"@next-auth/prisma-adapter": "^1.0.5",
"@next/bundle-analyzer": "^13.1.6",
+ "@next/third-parties": "^15.0.2",
"@prisma/client": "^5.0.0",
"@radix-ui/react-checkbox": "^1.0.1",
"@radix-ui/react-dialog": "^1.0.3",
diff --git a/src/components/common/AnalyticsWrapper.tsx b/src/components/common/AnalyticsWrapper.tsx
deleted file mode 100644
index 1fc4bc563..000000000
--- a/src/components/common/AnalyticsWrapper.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { cloneElement, Children } from 'react';
-
-/**
- * Wrapper component that injects ``analyticsClass`` class into its child
- *
- * Errors when more than 1 child
- *
- * Child component must take ``className`` props
- */
-export default function AnalyticsWrapper({
- analyticsClass,
- children,
-}: {
- analyticsClass: string;
- children: React.ReactElement;
-}) {
- return cloneElement(Children.only(children), {
- ...children.props,
- className: children.props.className
- ? `${children.props.className} ${analyticsClass}`
- : analyticsClass,
- });
-}
diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx
index bb2e05022..984b4267e 100644
--- a/src/components/home/Home.tsx
+++ b/src/components/home/Home.tsx
@@ -7,7 +7,6 @@ import ChevronIcon from '@/icons/ChevronIcon';
import PlusIcon from '@/icons/PlusIcon';
import { trpc } from '@utils/trpc';
-import AnalyticsWrapper from '../common/AnalyticsWrapper';
import PlanCard from '../landing/PlanCard';
import TemplateModal from '../template/Modal';
@@ -122,42 +121,36 @@ export default function PlansPage(): JSX.Element {
-
- {
- setPlanPage(0);
- setOpenTemplateModal(true);
- }}
- />
-
+ {
+ setPlanPage(0);
+ setOpenTemplateModal(true);
+ }}
+ />
-
- {
- setPlanPage(1);
- setOpenTemplateModal(true);
- }}
- />
-
+ {
+ setPlanPage(1);
+ setOpenTemplateModal(true);
+ }}
+ />
-
- {
- setPlanPage(2);
- setOpenTemplateModal(true);
- }}
- />
-
+ {
+ setPlanPage(2);
+ setOpenTemplateModal(true);
+ }}
+ />
diff --git a/src/components/planner/Sidebar/Sidebar.tsx b/src/components/planner/Sidebar/Sidebar.tsx
index 449080316..00509e231 100644
--- a/src/components/planner/Sidebar/Sidebar.tsx
+++ b/src/components/planner/Sidebar/Sidebar.tsx
@@ -4,7 +4,6 @@ import Skeleton from 'react-loading-skeleton';
import { v4 as uuidv4 } from 'uuid';
import Button from '@/components/Button';
-import AnalyticsWrapper from '@/components/common/AnalyticsWrapper';
import RequirementsContainer from '@/components/planner/Sidebar/RequirementsContainer';
import SearchBar from '@/components/planner/Sidebar/SearchBar';
import ChevronIcon from '@/icons/ChevronIcon';
@@ -164,21 +163,19 @@ function CourseSelectorContainer({
-
- setDisplay(true)}
- updateQuery={(q) => {
- updateQuery(q);
- setDisplay(true);
- }}
- className={`${
- displayResults
- ? 'rounded-b-none border-b-transparent'
- : 'rounded-b-[10px] border-b-inherit'
- }`}
- placeholder="Search courses"
- />
-
+
setDisplay(true)}
+ updateQuery={(q) => {
+ updateQuery(q);
+ setDisplay(true);
+ }}
+ className={`${
+ displayResults
+ ? 'rounded-b-none border-b-transparent'
+ : 'rounded-b-[10px] border-b-inherit'
+ }`}
+ placeholder="Search courses"
+ />
{displaySemesterCode(semester.code)}
-
-
-
+
{
>
{Object.entries(tagColors).map(([color, classes]) => (
-
- {
- e.stopPropagation();
- toggleColorFilter(color as keyof typeof tagColors);
- }}
- >
-
filter.type === 'color' && filter.color === color,
- )}
- onClick={(e) => e.stopPropagation()}
- onCheckedChange={() => toggleColorFilter(color as keyof typeof tagColors)}
- />
-
-
- {color.substring(0, 1).toUpperCase() + color.substring(1) || 'None'}
-
-
-
+ {
+ e.stopPropagation();
+ toggleColorFilter(color as keyof typeof tagColors);
+ }}
+ >
+
filter.type === 'color' && filter.color === color,
+ )}
+ onClick={(e) => e.stopPropagation()}
+ onCheckedChange={() => toggleColorFilter(color as keyof typeof tagColors)}
+ />
+
+
+ {color.substring(0, 1).toUpperCase() + color.substring(1) || 'None'}
+
+
))}
@@ -130,25 +127,23 @@ const FilterByDropdown: FC = ({ children }) => {
.sort()
.map((year) => (
-
- {
- toggleYearFilter(year);
- e.stopPropagation();
- }}
- >
- e.stopPropagation()}
- checked={filters.some(
- (filter) => filter.type === 'year' && filter.year === year,
- )}
- onCheckedChange={() => toggleYearFilter(year)}
- />
- {year}
-
-
+ {
+ toggleYearFilter(year);
+ e.stopPropagation();
+ }}
+ >
+ e.stopPropagation()}
+ checked={filters.some(
+ (filter) => filter.type === 'year' && filter.year === year,
+ )}
+ onCheckedChange={() => toggleYearFilter(year)}
+ />
+ {year}
+
))}
@@ -180,26 +175,24 @@ const FilterByDropdown: FC = ({ children }) => {
>
{Object.keys(semestersDisplayMap).map((semesterType) => (
-
- {
- toggleSemesterFilter(semesterType as SemesterType);
- e.stopPropagation();
- }}
- >
- e.stopPropagation()}
- checked={filters.some(
- (filter) =>
- filter.type === 'semester' && semesterType === filter.semester,
- )}
- onCheckedChange={() => toggleSemesterFilter(semesterType as SemesterType)}
- />
- {semestersDisplayMap[semesterType as SemesterType] + ' semester'}
-
-
+ {
+ toggleSemesterFilter(semesterType as SemesterType);
+ e.stopPropagation();
+ }}
+ >
+ e.stopPropagation()}
+ checked={filters.some(
+ (filter) =>
+ filter.type === 'semester' && semesterType === filter.semester,
+ )}
+ onCheckedChange={() => toggleSemesterFilter(semesterType as SemesterType)}
+ />
+ {semestersDisplayMap[semesterType as SemesterType] + ' semester'}
+
))}
diff --git a/src/components/planner/Toolbar/Toolbar.tsx b/src/components/planner/Toolbar/Toolbar.tsx
index 936f6135a..279478e96 100644
--- a/src/components/planner/Toolbar/Toolbar.tsx
+++ b/src/components/planner/Toolbar/Toolbar.tsx
@@ -3,7 +3,6 @@ import { usePDF } from '@react-pdf/renderer';
import Link from 'next/link';
import { FC, useEffect, useState } from 'react';
-import AnalyticsWrapper from '@/components/common/AnalyticsWrapper';
import DownloadIcon from '@/icons/DownloadIcon';
import SettingsIcon from '@/icons/SettingsIcon';
import SwitchVerticalIcon from '@/icons/SwitchVerticalIcon';
@@ -88,41 +87,37 @@ const Toolbar: FC = ({
-
-
+ }
+ id="tutorial-editor-7"
+ className="whitespace-nowrap"
>
- }
- id="tutorial-editor-7"
- className="whitespace-nowrap"
- >
- Export Degree Plan
-
-
-
+ Export Degree Plan
+
+
-
- }
- >
-
- Sort By
-
-
-
+ }
+ >
+
+ Sort By
+
+
diff --git a/src/env/schema.mjs b/src/env/schema.mjs
index 250ceb214..8db1905b4 100644
--- a/src/env/schema.mjs
+++ b/src/env/schema.mjs
@@ -40,8 +40,6 @@ export const serverSchema = z.object({
*/
export const clientSchema = z.object({
NEXT_PUBLIC_NODE_ENV: z.enum(['development', 'test', 'production']),
- NEXT_PUBLIC_UMAMI_URL: z.string(),
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string(),
NEXT_PUBLIC_VALIDATOR: z.string(),
});
@@ -53,7 +51,5 @@ export const clientSchema = z.object({
*/
export const clientEnv = {
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
- NEXT_PUBLIC_UMAMI_URL: process.env.NEXT_PUBLIC_UMAMI_URL,
- NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_VALIDATOR: process.env.NEXT_PUBLIC_VALIDATOR,
};
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 2702a2c5a..91679fccb 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -5,6 +5,7 @@ import '../styles/introjs.css';
import { createTheme, StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { GoogleAnalytics } from '@next/third-parties/google';
import { Analytics } from '@vercel/analytics/react';
import { AnimateSharedLayout } from 'framer-motion';
import { type AppType, AppProps } from 'next/app';
@@ -84,6 +85,7 @@ const NebulaApp: AppType<{ session: Session | null }> = ({
return (
+
@@ -105,30 +107,7 @@ const NebulaApp: AppType<{ session: Session | null }> = ({
-
- {process.env.VERCEL_ENV === 'production' && (
-
- )}
- {env.NEXT_PUBLIC_NODE_ENV === 'production' && (
- <>
-
-
- >
- )}
setHasWarned(true)}
diff --git a/src/pages/api/umami/[uri].ts b/src/pages/api/umami/[uri].ts
deleted file mode 100644
index 7ad7bf244..000000000
--- a/src/pages/api/umami/[uri].ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import axios from 'axios';
-import { NextApiRequest, NextApiResponse } from 'next';
-
-import { env } from '@/env/client.mjs';
-
-const scriptName = 'test';
-const endpointName = 'endpoint-name';
-const umamiUrl = env.NEXT_PUBLIC_UMAMI_URL;
-const corsHeaders = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
- 'Access-Control-Max-Age': '86400',
-};
-
-/***
- * Purpose of this NextJS api endpoint is to allow umami to
- * track website usage in spite of adblockers
- *
- * More information can be found here:
- * https://github.com/umami-software/umami/discussions/1026
- *
- */
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- const { uri } = req.query;
-
- if ((uri as string).endsWith(scriptName)) {
- return getScript(req, res);
- } else if ((uri as string).endsWith(endpointName)) {
- return postData(req, res);
- }
- res.status(404).send(null);
-}
-
-async function getScript(req: NextApiRequest, res: NextApiResponse) {
- // Uses axios because I don't feel like changing it rn
- // Also host field causes issues when converting to fetch
- const response = await axios(umamiUrl + '/umami.js', {
- headers: {
- ...req.headers,
- ...corsHeaders,
- 'accept-encoding': 'gzip',
- host: null, // not removing host header will result in a weird SSL error that leads to a 500 code (EPROTO SSL alert number 80)
- } as unknown as Record,
- });
-
- const originalScript = await response.data;
- const obfuscatedScript = originalScript.replace(
- new RegExp('/api/collect', 'g'),
- `/${endpointName}`,
- );
- res.status(response.status ?? 200).send(obfuscatedScript);
-}
-
-async function postData(req: NextApiRequest, res: NextApiResponse) {
- const response = await axios.post(umamiUrl + '/api/collect', req.body, {
- headers: {
- ...req.headers,
- ...corsHeaders,
- host: null, // not removing host header will result in a weird SSL error that leads to a 500 code (EPROTO SSL alert number 80)
- } as unknown as Record,
- });
- res.status(response.status ?? 201).send(response.data);
-}