diff --git a/backend/src/main.ts b/backend/src/main.ts index be1538a..e4e2573 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -19,6 +19,8 @@ import UserRouter from "./api/user"; import ClassRouter from "./api/class"; import LevelRouter from "./api/level"; +import { ITrackerDBModel } from "./models/tracker"; + const baseRouter = express.Router(); const authRouter = express.Router(); @@ -66,6 +68,40 @@ const password = encodeURIComponent(env["LATEINICUS_USER_PW"]); app.use("/api/level", LevelRouter); app.use("/api/class", ClassRouter); app.use("/api/user", UserRouter); + app.post("/api/tracker", async (req, res) => { + // Did we get any data + if (!req.body) { + res.send({ + error: "403", + data: { + msg: "No request body provided", + }, + }); + return; + } + + // Did we get all arguments? + if (!("session" in req.body) || !("event" in req.body)) { + res.send({ + error: "403", + data: { + msg: "Invalid request", + }, + }); + return; + } + + // Insert it into the database + const tracker: ITrackerDBModel = Object.assign({}, req.body, { + timestamp: Date.now(), + }); + await db.collection("tracker").insertOne(tracker); + + res.send({ + error: "200", + data: {}, + }); + }); app.get("/api/levels", async (req, res) => { // TODO: if (levels) const levels = (await db.collection("levels").find({}, { diff --git a/backend/src/models/tracker.ts b/backend/src/models/tracker.ts new file mode 100644 index 0000000..ea292be --- /dev/null +++ b/backend/src/models/tracker.ts @@ -0,0 +1,16 @@ +export enum TrackerEvent { + LOG_IN = "LOG_IN", + LOG_OUT = "LOG_OUT", + START_LEARNING = "START_LEARNING", + CANCEL_LEARNING = "CANCEL_LEARNING", + FINISH_LEARNING = "FINISH_LEARNING", +}; + +export interface ITrackerRequest { + session: string; + event: TrackerEvent; +}; + +export type ITrackerDBModel = ITrackerRequest & { + timestamp: number; +}; diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index 77adba5..8f35d58 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -54,6 +54,19 @@ export default class Application extends React.Component { } checkAuthStatus = (token: string): Promise => { + // Track the end of a review + console.log("Sending trcaker request"); + fetch(`${BACKEND_URL}/api/tracker`, { + headers: new Headers({ + "Content-Type": "application/json", + }), + method: "POST", + body: JSON.stringify({ + session: window.sessionStorage.getItem("tracker_session"), + event: "LOG_IN", + }), + }); + return new Promise((res, rej) => { fetch(`${BACKEND_URL}/api/user/me`, { headers: new Headers({ @@ -128,6 +141,18 @@ export default class Application extends React.Component { }).then(resp => resp.json(), err => { console.log("Application::setLastReview: POSTing last results failed"); }); + + // Track the end of a review + fetch(`${BACKEND_URL}/api/tracker`, { + headers: new Headers({ + "Content-Type": "application/json", + }), + method: "POST", + body: JSON.stringify({ + session: window.sessionStorage.getItem("tracker_session"), + event: "FINISH_LEARNING", + }), + }); } getReviewQueue = (): Promise => { @@ -256,6 +281,18 @@ export default class Application extends React.Component { } login = (username: string, password: string): Promise => { + // Track the login + fetch(`${BACKEND_URL}/api/tracker`, { + headers: new Headers({ + "Content-Type": "application/json", + }), + method: "POST", + body: JSON.stringify({ + session: window.sessionStorage.getItem("tracker_session"), + event: "LOG_IN", + }), + }); + return new Promise((res, rej) => { fetch(`${BACKEND_URL}/api/login`, { method: "POST", @@ -288,6 +325,18 @@ export default class Application extends React.Component { } logout = () => { + // Track the logout + fetch(`${BACKEND_URL}/api/tracker`, { + headers: new Headers({ + "Content-Type": "application/json", + }), + method: "POST", + body: JSON.stringify({ + session: window.sessionStorage.getItem("tracker_session"), + event: "LOG_OUT", + }), + }); + // NOTE: No promise, since we don't care about the result fetch(`${BACKEND_URL}/api/user/logout`, { headers: new Headers({ diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index d159382..473da4b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -10,6 +10,11 @@ import Application from "./containers/Application"; const store = createStore(LateinicusApp); +// Generate a tracker session +let array = new Uint32Array(1); +window.crypto.getRandomValues(array); +window.sessionStorage.setItem("tracker_session", array[0].toString()); + ReactDOM.render(( diff --git a/frontend/src/pages/review.tsx b/frontend/src/pages/review.tsx index 6b2539f..161e6bc 100644 --- a/frontend/src/pages/review.tsx +++ b/frontend/src/pages/review.tsx @@ -34,6 +34,8 @@ import { REVIEW_HELP_MOD } from "../config"; +import { BACKEND_URL } from "../config"; + import { Queue } from "../utils/queue"; interface IProps { @@ -103,6 +105,18 @@ const ReviewPageWithRouter = withRouter( [ReviewType.QUEUE]: () => vocabByQueueW(), }[reviewType]; + // Track the start of a session + fetch(`${BACKEND_URL}/api/tracker`, { + headers: new Headers({ + "Content-Type": "application/json", + }), + method: "POST", + body: JSON.stringify({ + session: window.sessionStorage.getItem("tracker_session"), + event: "START_LEARNING", + }), + }); + getVocab().then((res: IVocab[]) => { // Check if we received any vocabulary if (res.length === 0) { @@ -145,6 +159,18 @@ const ReviewPageWithRouter = withRouter( cancelReview = () => { this.closeDialog(); + // Track the cancellation of a learning session + fetch(`${BACKEND_URL}/api/tracker`, { + headers: new Headers({ + "Content-Type": "application/json", + }), + method: "POST", + body: JSON.stringify({ + session: window.sessionStorage.getItem("tracker_session"), + event: "CANCEL_LEARNING", + }), + }); + // Show the drawer button again this.props.drawerButtonState(true);