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

User Analytics (CS IA) #424

Merged
merged 36 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bad3c43
Started CS IA
renatodellosso Oct 28, 2024
c6db312
Finished LinkedList and wrote its unit tests
renatodellosso Oct 30, 2024
9724961
Finished sign in date tracking
renatodellosso Nov 11, 2024
c3ed7fb
Merge branch 'user-analytics-cs-ia' into updating-cs-ia
renatodellosso Nov 11, 2024
f10aeac
Merge pull request #272 from Decatur-Robotics/updating-cs-ia
renatodellosso Nov 11, 2024
f1f7e61
Fix missing newline at end of useranalytics.tsx file
renatodellosso Nov 12, 2024
dd3e2ae
Basic user analytics work now
renatodellosso Nov 13, 2024
603e070
Merge branch 'user-analytics-cs-ia' into updating-cs-ia
renatodellosso Nov 15, 2024
2988c8c
Merge pull request #287 from Decatur-Robotics/updating-cs-ia
renatodellosso Nov 15, 2024
f49dce8
Charts no longer fill the whole page
renatodellosso Nov 15, 2024
521e74e
Merge pull request #289 from Decatur-Robotics/main
renatodellosso Nov 18, 2024
9af6b6f
Started checks for whether teams are active
renatodellosso Nov 18, 2024
e35505d
Fixed chart direction flipping
renatodellosso Nov 18, 2024
93e8b6e
Merge pull request #345 from Decatur-Robotics/main
renatodellosso Dec 2, 2024
1cb5528
Refactor LinkedList tests: consolidate test cases into a single file
renatodellosso Dec 2, 2024
0067c0c
Refactor Auth module: change db variable to a constant for improved c…
renatodellosso Dec 2, 2024
926c170
Update test reference in LinkedList class documentation
renatodellosso Dec 2, 2024
72ab92d
Fix indentation in ClientApi
renatodellosso Dec 4, 2024
66be7c2
Merge branch 'main' into user-analytics-cs-ia
renatodellosso Jan 29, 2025
47a0b21
Fix build issues
renatodellosso Jan 29, 2025
4133067
Fix formatting
renatodellosso Jan 29, 2025
74aec2e
Fake users now have sign in dates
renatodellosso Jan 29, 2025
d982941
Merge branch 'main' into user-analytics-cs-ia
renatodellosso Jan 29, 2025
694a250
Merge branch 'main' into user-analytics-cs-ia
renatodellosso Jan 29, 2025
4a5bf42
1.1.15
gearbox4026 Jan 29, 2025
b70f6b5
Merge branch 'user-analytics-cs-ia' into updating-cs-ia
renatodellosso Feb 3, 2025
f55a19f
Merge pull request #426 from Decatur-Robotics/updating-cs-ia
renatodellosso Feb 3, 2025
d292c95
Fixed sign in
renatodellosso Feb 3, 2025
9662ee5
Fix login times for email users, sort bars in user analytics
renatodellosso Feb 3, 2025
83f63db
Merge branch 'main' into user-analytics-cs-ia
renatodellosso Feb 4, 2025
487809a
Fix formatting
renatodellosso Feb 5, 2025
36686ed
Merge pull request #434 from Decatur-Robotics/main
renatodellosso Feb 5, 2025
4e2c26a
Merge branch 'main' into user-analytics-cs-ia
renatodellosso Feb 10, 2025
934a11a
Comment user analytics route
renatodellosso Feb 10, 2025
d6d6b4d
Merge branch 'user-analytics-cs-ia' of github.com:Decatur-Robotics/Ge…
renatodellosso Feb 10, 2025
836371c
Fix formatting
renatodellosso Feb 10, 2025
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
27 changes: 21 additions & 6 deletions lib/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import CollectionId from "./client/CollectionId";
import { AdapterUser } from "next-auth/adapters";
import { wait } from "./client/ClientUtils";

var db = getDatabase();
const db = getDatabase();

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

Expand Down Expand Up @@ -123,21 +123,36 @@ export const AuthenticationOptions: AuthOptions = {
if (!foundUser) await wait(50);
}

console.log(
"User is incomplete, filling in missing fields. User:",
typedUser,
);
console.log("User is incomplete, filling in missing fields.");

typedUser._id = foundUser._id;
typedUser.lastSignInDateTime = new Date();

typedUser = await repairUser(await db, typedUser);

console.log("User updated:", typedUser);
console.log("User updated:", typedUser._id?.toString());
};

repairUserOnceItIsInDb();
}

const today = new Date();
if (
(typedUser as User).lastSignInDateTime?.toDateString() !==
today.toDateString()
) {
// We use user.id since user._id strangely doesn't exist on user.
await getDatabase().then((db) =>
db.updateObjectById(
CollectionId.Users,
new ObjectId(typedUser._id?.toString()),
{
lastSignInDateTime: today,
},
),
);
}

new ResendUtils().createContact(typedUser as User);

return true;
Expand Down
121 changes: 121 additions & 0 deletions lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class User implements NextAuthUser {
level: number = 1;
onboardingComplete: boolean = false;
resendContactId: string | undefined = undefined;
lastSignInDateTime: Date | undefined = undefined;

constructor(
name: string | undefined,
Expand Down Expand Up @@ -567,6 +568,126 @@ export type CompPicklistGroup = {
strikethroughs: number[];
};

type LinkedNode<T> = T & {
prev?: LinkedNode<T>;
next?: LinkedNode<T>;
};

/**
* @tested_by tests/lib/Types.test.ts
*/
export class LinkedList<T> {
private head?: LinkedNode<T> = undefined;

constructor(head?: T | T[]) {
if (Array.isArray(head) && head.length > 0) {
let node: LinkedNode<T>;

for (const element of head) {
if (!this.head) {
this.head = {
...element,
next: undefined,
prev: undefined,
};

node = this.head;
} else node = this.insertAfter(node!, element);
}
} else if (head)
this.head = {
...(head as T),
next: undefined,
prev: undefined,
};
}

size() {
let count = 0;

for (let node = this.head; node !== undefined; node = node.next) count++;

return count;
}

isEmpty() {
return this.head === undefined;
}

first() {
return this.head;
}

last() {
let node = this.head;
while (node?.next) node = node.next;

return node;
}

// Add to criterion B
/**
* Will reset the list to just be head
*/
setHead(insertedVal: T) {
this.head = {
...insertedVal,
prev: undefined,
next: undefined,
};
}

insertBefore(existingNode: LinkedNode<T>, insertedVal: T) {
const insertedNode: LinkedNode<T> = {
...insertedVal,
next: existingNode,
};

if (existingNode.prev) {
existingNode.prev.next = insertedNode;
insertedNode.prev = existingNode.prev;
}
existingNode.prev = insertedNode;

if (this.head === existingNode) this.head = insertedNode;

return insertedNode;
}

insertAfter(existingNode: LinkedNode<T>, insertedVal: T) {
const insertedNode: LinkedNode<T> = {
...insertedVal,
prev: existingNode,
};

if (existingNode.next) {
existingNode.next.prev = insertedNode;
insertedNode.next = existingNode.next;
}
existingNode.next = insertedNode;

return insertedNode;
}

// Add to criterion B
forEach(func: (node: LinkedNode<T>) => any) {
for (let node = this.head; node; node = node.next) {
func(node);
}
}

// Add to criterion B
map<TMap>(func: (node: LinkedNode<T>) => TMap) {
const array: TMap[] = [];

for (let node = this.head; node; node = node.next) {
array.push(func(node));
}

return array;
}
}

/**
* DO NOT GIVE TO CLIENTS!
*/
Expand Down
100 changes: 100 additions & 0 deletions lib/api/ClientApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
WebhookHolder,
LeaderboardUser,
LeaderboardTeam,
LinkedList,
} from "@/lib/Types";
import { NotLinkedToTba, removeDuplicates } from "../client/ClientUtils";
import {
Expand Down Expand Up @@ -2179,4 +2180,103 @@ export default class ClientApi extends NextApiTemplate<ApiDependencies> {
.send({ users: leaderboardUsers, teams: leaderboardTeamsArray });
},
});

getUserAnalyticsData = createNextRoute<
[],
{ [team: string]: { date: Date; count: number }[] },
ApiDependencies,
void
>({
isAuthorized: AccessLevels.IfDeveloper,
handler: async (req, res, { db: dbPromise }, authData, args) => {
const db = await dbPromise;

// Find data from DB
const [teams, users] = await Promise.all([
db.findObjects(CollectionId.Teams, {}),
db.findObjects(CollectionId.Users, {
lastSignInDateTime: { $exists: true },
}),
]);

// Create a linked list for each team
const signInDatesByTeam: {
[team: string]: LinkedList<{ date: string; count: number }>;
} = teams.reduce(
(acc, team) => {
acc[team._id!.toString()] = new LinkedList<{
date: string;
count: number;
}>();
return acc;
},
{ All: new LinkedList() } as {
[team: string]: LinkedList<{ date: string; count: number }>;
},
);

for (const user of users) {
// Add the user to each of their teams
for (const team of [...user.teams, "All"]) {
const signInDates = signInDatesByTeam[team];

// Iterate through the team's linked list
for (let node = signInDates.first(); true; node = node.next) {
if (!node) {
// We're either at the end of the list, or the list is empty

// Can't just update signInDates, as that will reference a new object and not change the old one!
signInDates.setHead({
date: user.lastSignInDateTime!.toDateString(),
count: 1,
});
break;
}

if (
node &&
node?.date === user.lastSignInDateTime!.toDateString()
) {
// Found the node with the same date
node.count++;
break;
}

if (
!node?.next ||
new Date(user.lastSignInDateTime!.toDateString())! <
new Date(node.next.date)
) {
// The next node's date is after the user's sign-in date
signInDates.insertAfter(node!, {
date: user.lastSignInDateTime!.toDateString(),
count: 1,
});
break;
}
}
}
}

// Convert linked lists to arrays
const responseObj: { [team: string]: { date: Date; count: number }[] } =
{};
for (const obj of [...teams, "All"]) {
// Convert ObjectIds to strings
const id = typeof obj === "object" ? obj._id!.toString() : obj;
// Pull relevant data from the team to create a label
const label =
typeof obj === "object" ? `${obj.league} ${obj.number}` : obj;

// Convert date strings to Date objects
responseObj[label] = signInDatesByTeam[id].map((node) => ({
date: new Date(node.date),
count: node.count,
}));
}

// Send the response
res.status(200).send(responseObj);
},
});
}
1 change: 1 addition & 0 deletions lib/dev/FakeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function fakeUser(
"",
10,
);
user.lastSignInDateTime = new Date();
return await db.addObject(CollectionId.Users, user);
}

Expand Down
Loading