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

Google Analytics Events #211

Merged
merged 12 commits into from
Aug 6, 2024
6 changes: 6 additions & 0 deletions components/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import useCheckMobile from "@/lib/client/useCheckMobile";
import { MdInfo, MdWarning } from "react-icons/md";
import Avatar from "./Avatar";
import Banner, { DiscordBanner } from "./Banner";
import Head from "next/head";

const api = new ClientAPI("gearboxiscool");

type ContainerProps = {
children: ReactNode;
requireAuthentication: boolean;
title: string;
/**
* Hides the button to open the sidebar.
*/
Expand Down Expand Up @@ -118,6 +120,10 @@ export default function Container(props: ContainerProps) {
className="w-full h-screen flex flex-col overflow-x-hidden"
data-theme={theme}
>
<Head>
<title>{props.title !== "Gearbox" ? `${props.title} | Gearbox` : props.title}</title>
</Head>

<DiscordBanner />

<div className="drawer">
Expand Down
13 changes: 12 additions & 1 deletion components/forms/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ import { CommentBox } from "./Comment";
import { IncrementButton } from "./Buttons";
import Slider from "./Sliders";
import { BlockElement, FormLayout, FormElement } from "@/lib/Layout";
import { Analytics } from "@/lib/client/Analytics";

const api = new ClientAPI("gearboxiscool");

export default function Form(props: { report: Report, layout: FormLayout<QuantData>, fieldImagePrefix: string }) {
export type FormProps = {
report: Report;
layout: FormLayout<QuantData>;
fieldImagePrefix: string;
teamNumber: number;
compName: string;
};

export default function Form(props: FormProps) {
const { session, status } = useCurrentSession();

const [page, setPage] = useState(0);
Expand All @@ -29,6 +38,8 @@ export default function Form(props: { report: Report, layout: FormLayout<QuantDa
async function submitForm() {
await api.submitForm(props.report?._id, formData, session?.user?._id);
location.href = location.href.substring(0, location.href.lastIndexOf("/"));

Analytics.quantReportSubmitted(props.report.robotNumber, props.teamNumber, props.compName, session.user?.name ?? "Unknown User");
}

const sync = async () => {
Expand Down
2 changes: 2 additions & 0 deletions enviroment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ declare global {
SLACK_STATE_SECRET: string;
SLACK_PORT: string;

NEXT_PUBLIC_GOOGLE_ANALYTICS_ID: string;

NODE_ENV: "development" | "production";
}
}
Expand Down
16 changes: 7 additions & 9 deletions lib/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,13 @@ export namespace API {

team.requests = removeDuplicates([...team.requests, data.userId]);

return res
.status(200)
.send(
await db.updateObjectById<Team>(
Collections.Teams,
new ObjectId(data.teamId),
team
)
);
await db.updateObjectById<Team>(
Collections.Teams,
new ObjectId(data.teamId),
team
)

return res.status(200).send({ result: "success" });
},

handleRequest: async (req, res, { db, data }) => {
Expand Down
22 changes: 20 additions & 2 deletions lib/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { Collections, getDatabase, clientPromise } from "./MongoDB";
import { Admin, ObjectId } from "mongodb";
import { User } from "./Types";
import { GenerateSlug } from "./Utils";
import { Analytics } from '@/lib/client/Analytics';

var db = getDatabase();

const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB });

export const AuthenticationOptions: AuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [
Expand Down Expand Up @@ -71,12 +74,27 @@ export const AuthenticationOptions: AuthOptions = {
return session;
},

async redirect({ url, baseUrl }) {
async redirect({ baseUrl }) {
return baseUrl + "/onboarding";
},

async signIn({ user }) {
Analytics.signIn(user.name ?? "Unknown User");

return true;
}
},
debug: false,
adapter: MongoDBAdapter(clientPromise, { databaseName: process.env.DB }),
adapter: {
...adapter,
createUser: async (user) => {
const createdUser = await adapter.createUser!(user);

Analytics.newSignUp(user.name ?? "Unknown User");

return createdUser;
}
},
pages: {
//signIn: "/signin",
},
Expand Down
155 changes: 155 additions & 0 deletions lib/client/Analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import ReactGA from "react-ga4";
import { League, User, Team } from '../Types';
import { UaEventOptions } from "react-ga4/types/ga4";
import { GameId } from "./GameId";

enum EventCategory {
User = "User",
Team = "Team",
Season = "Season",
Comp = "Competition",
}

/**
* Event parameters must be added as custom dimensions in Google Analytics. Go to Admin > Custom definitions.
*/
type EventParams = UaEventOptions & {
teamNumber?: number;
username?: string;
league?: League;
gameId?: GameId;
compName?: string;
usePublicData?: boolean;
teamScouted?: number;
targetName?: string;
}

if (process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID !== undefined && process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID !== "")
ReactGA.initialize(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID);

function triggerEvent(params: EventParams) {
if (!ReactGA._hasLoadedGA)
console.error("Google Analytics not loaded");

if (!ReactGA.isInitialized)
console.error("Google Analytics not initialized");

console.log("Event triggered:", params);
const { action, ...options } = params;
ReactGA.event(action, options);
}

export namespace Analytics {
export function onboardingCompleted(name: string, teamNumber: number, league: League) {
triggerEvent({
category: EventCategory.User,
action: "onboarding_complete",
username: name,
teamNumber,
league
});
}

export function newSignUp(name: string) {
triggerEvent({
category: EventCategory.User,
action: "new_sign_up",
username: name
});
}

export function signIn(name: string) {
triggerEvent({
category: EventCategory.User,
action: "sign_in",
username: name
});
}

export function teamCreated(number: number, league: League, username: string) {
triggerEvent({
category: EventCategory.Team,
action: "create_team",
teamNumber: number,
username,
league
});
}

export function requestedToJoinTeam(teamNumber: number, username: string) {
triggerEvent({
category: EventCategory.Team,
action: "request_to_join_team",
teamNumber,
username
});
}

export function teamJoinRequestHandled(teamNumber: number, league: League, requesterName: string, doneByName: string,
accepted: boolean) {
triggerEvent({
category: EventCategory.Team,
action: `team_join_request_${accepted ? "accepted" : "declined"}`,
label: accepted ? "accepted" : "declined",
teamNumber,
username: doneByName,
targetName: requesterName,
league
});
}

export function seasonCreated(gameId: GameId, teamNumber: number, username: string) {
triggerEvent({
category: EventCategory.Season,
action: "create_season",
gameId,
teamNumber,
username
});
}

export function compCreated(compName: string, gameId: GameId, usePublicData: boolean, teamNumber: number, username: string) {
triggerEvent({
category: EventCategory.Season,
action: "create_competition",
compName,
gameId,
usePublicData,
teamNumber,
username
});
}

export function quantReportSubmitted(teamScouted: number, teamNumber: number, compName: string, username: string) {
triggerEvent({
category: EventCategory.Comp,
action: "submit_quantitative_report",
teamNumber,
username,
compName,
teamScouted
});
}

export function pitReportSubmitted(teamScouted: number, teamNumber: number, compName: string, username: string) {
triggerEvent({
category: EventCategory.Comp,
action: "submit_pit_report",
teamNumber,
username,
compName,
teamScouted
});
}

export function subjectiveReportSubmitted(teamsWithComments: string[], teamNumber: number, compName: string, username: string) {
triggerEvent({
category: EventCategory.Comp,
action: "submit_subjective_report",
label: teamsWithComments.join(", "),
teamNumber,
username,
compName
});
}
}
2 changes: 1 addition & 1 deletion lib/client/ClientAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export default class ClientAPI {
end: number,
seasonId: string | undefined,
publicData: boolean
) {
): Promise<Competition> {
return await this.request("/createCompetiton", {
name,
tbaId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Container from "@/components/Container";
import { useCurrentSession } from "@/lib/client/useCurrentSession";
import Form from "@/components/forms/Form";
import Form, { FormProps } from "@/components/forms/Form";
import { GetServerSideProps } from "next";
import UrlResolver, { ResolvedUrlData } from "@/lib/UrlResolver";
import { games } from "@/lib/games";
Expand All @@ -13,7 +13,7 @@ import ClientAPI from "@/lib/client/ClientAPI";

const api = new ClientAPI("gearboxiscool");

export default function Homepage(props: { report: Report, layout: FormLayout<QuantData>, fieldImgPrefix: string }) {
export default function Homepage(props: FormProps) {
const { session, status } = useCurrentSession();
const hide = status === "authenticated";

Expand All @@ -23,9 +23,9 @@ export default function Homepage(props: { report: Report, layout: FormLayout<Qua
}, []);

return (
<Container requireAuthentication={false} hideMenu={!hide}>
<Container requireAuthentication={false} hideMenu={!hide} title={`${props.report.robotNumber} | Quant Scouting`}>
{props.report ? (
<Form report={props.report} layout={props.layout} fieldImagePrefix={props.fieldImgPrefix} />
<Form {...props} />
) : (
<p className="text-error">Welp.</p>
)}
Expand All @@ -41,7 +41,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
props: {
report: resolver.report,
layout: makeObjSerializeable(games[season?.gameId ?? defaultGameId].quantitativeReportLayout),
fieldImgPrefix: games[season?.gameId ?? defaultGameId].fieldImagePrefix
fieldImagePrefix: games[season?.gameId ?? defaultGameId].fieldImagePrefix,
teamNumber: resolver.team?.number,
compName: resolver.competition?.name,
}
};
} as { props: FormProps };
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Container from "@/components/Container";
import { Match, SubjectiveReportSubmissionType } from "@/lib/Types";
import UrlResolver, { ResolvedUrlData } from "@/lib/UrlResolver";
import { Analytics } from "@/lib/client/Analytics";
import ClientAPI from "@/lib/client/ClientAPI";
import { useCurrentSession } from "@/lib/client/useCurrentSession";
import useInterval from "@/lib/client/useInterval";
Expand Down Expand Up @@ -60,13 +61,20 @@ export default function Subjective(props: ResolvedUrlData) {
}, session.session.user?._id!, teamSlug as string).then((res) => {
window.location.href = `/${teamSlug}/${seasonSlug}/${competitonSlug}`;
});

const teamsWithComments = [...(e.target as any)].map((element: any, index: number) =>
["Whole Match"].concat(match?.blueAlliance.concat(match.redAlliance).map((n) => n.toString()) ?? [])[index]
);

Analytics.subjectiveReportSubmitted(teamsWithComments, props.team?.number ?? -1, props.competition?.name ?? "Unknown Competition",
session.session.user?.name ?? "Unknown User");
}

// We have to use router as a dependency because it is only populated after the first render (during hydration)
useInterval(() => api.checkInForSubjectiveReport(matchId as string), 5000, [router]);

return (
<Container requireAuthentication={true} hideMenu={false}>
<Container requireAuthentication={true} hideMenu={false} title={`${match?.number} | Subjective Scouting`}>
<div className="flex flex-col items-center p-12">
{ !match
?
Expand Down
2 changes: 1 addition & 1 deletion pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ export default function Home(props: ResolvedUrlData) {
}

return (
<Container requireAuthentication={true} hideMenu={false}>
<Container requireAuthentication={true} hideMenu={false} title={props.competition?.name ?? "Competition Loading"}>
<div className="min-h-screen w-screen flex flex-col sm:flex-row grow-0 items-center justify-center max-sm:content-center sm:space-x-6 space-y-2 overflow-hidden max-sm:my-4 md:ml-4">
<div className="w-[90%] sm:w-2/5 flex flex-col grow-0 space-y-14 h-full">
<div className="w-full card bg-base-200 shadow-xl">
Expand Down
Loading
Loading