diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html
index 726e0fffb..f12372d3e 100644
--- a/frontend/src/html/popups.html
+++ b/frontend/src/html/popups.html
@@ -166,8 +166,19 @@
analytics
- We use Google Analytics cookies to check how users interact with our
- website and use this data to improve our site design.
+ We use Google Analytics to track the overall traffic and
+ demographics of our site.
+
+
+
+
+
+
+ sentry
+
+ We use Sentry to track errors and performance issues on our site, as
+ well as record annonymized user sessions to help us debug issues and
+ improve our product.
diff --git a/frontend/src/privacy-policy.html b/frontend/src/privacy-policy.html
index 81b44fcb0..3167e82cc 100644
--- a/frontend/src/privacy-policy.html
+++ b/frontend/src/privacy-policy.html
@@ -111,7 +111,7 @@
Effective date: September 8, 2021
-
Last updated: May 20, 2024
+
Last updated: May 12, 2025
Thanks for trusting Monkeytype ('Monkeytype', 'we', 'us', 'our') with
your personal information! We take our responsibility to you very
@@ -274,13 +274,13 @@
-
What log data do we collect?
+
Analytics
- Like most websites, Monkeytype collects information that your browser
- sends whenever you visit the website. This data may include internet
- protocol (IP) addresses, browser type, Internet Service Provider
- (ISP), date and time stamp, referring/exit pages, and time spent on
- each page.
+ Like most websites, we use Google Analytics, which collects
+ information that your browser sends whenever you visit the website.
+ This data may include internet protocol (IP) addresses, browser type,
+ Internet Service Provider (ISP), date and time stamp, referring/exit
+ pages, and time spent on each page.
THIS DATA DOES NOT CONTAIN ANY PERSONALLY IDENTIFIABLE INFORMATION.
@@ -288,7 +288,36 @@
tracking users' movement on the website, and gathering demographic
information.
-
In our case, this service is provided by Google Analytics.
+
+ For more information on Google Analytics' privacy policy, please
+ visit:
+
+ https://support.google.com/analytics/answer/6004245?hl=en
+
+
+
+
Sentry
+
+ Sentry is a crash reporting service that helps us track errors and
+ crashes on the website. It collects information about your device,
+ browser, and the error that occurred. Sometimes it might also include
+ an annonymized replay of your session. This information is used to
+ track down bugs faster and improve our website.
+
+
+ For more information on Sentry's privacy policy, please visit:
+
+ https://sentry.io/privacy/
+
+
Advertisements
diff --git a/frontend/src/ts/controllers/analytics-controller.ts b/frontend/src/ts/controllers/analytics-controller.ts
index afb7df31c..157d2348e 100644
--- a/frontend/src/ts/controllers/analytics-controller.ts
+++ b/frontend/src/ts/controllers/analytics-controller.ts
@@ -6,18 +6,9 @@ import {
} from "firebase/analytics";
import { app as firebaseApp } from "../firebase";
import { createErrorMessage } from "../utils/misc";
-import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
-import { z } from "zod";
let analytics: AnalyticsType;
-const AcceptedCookiesSchema = z.object({
- security: z.boolean(),
- analytics: z.boolean(),
-});
-
-type AcceptedCookies = z.infer;
-
export async function log(
eventName: string,
params?: Record
@@ -29,26 +20,12 @@ export async function log(
}
}
-const lsString = localStorage.getItem("acceptedCookies");
-let acceptedCookies: AcceptedCookies | null;
-if (lsString !== undefined && lsString !== null && lsString !== "") {
- try {
- acceptedCookies = parseJsonWithSchema(lsString, AcceptedCookiesSchema);
- } catch (e) {
- console.error("Failed to parse accepted cookies:", e);
- acceptedCookies = null;
- }
-} else {
- acceptedCookies = null;
-}
-
-if (acceptedCookies !== null) {
- if (acceptedCookies.analytics) {
- activateAnalytics();
- }
-}
-
export function activateAnalytics(): void {
+ if (analytics !== undefined) {
+ console.warn("Analytics already activated");
+ return;
+ }
+ console.log("Activating Analytics");
try {
analytics = getAnalytics(firebaseApp);
setAnalyticsCollectionEnabled(analytics, true);
diff --git a/frontend/src/ts/cookies.ts b/frontend/src/ts/cookies.ts
new file mode 100644
index 000000000..01e654dfb
--- /dev/null
+++ b/frontend/src/ts/cookies.ts
@@ -0,0 +1,42 @@
+import { z } from "zod";
+import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
+import { activateAnalytics } from "./controllers/analytics-controller";
+import { activateSentry } from "./sentry";
+
+const AcceptedCookiesSchema = z
+ .object({
+ security: z.boolean(),
+ analytics: z.boolean(),
+ sentry: z.boolean(),
+ })
+ .strict()
+ .nullable();
+
+export type AcceptedCookies = z.infer;
+
+export function getAcceptedCookies(): AcceptedCookies | null {
+ return cookies.get();
+}
+
+export function setAcceptedCookies(accepted: AcceptedCookies): void {
+ cookies.set(accepted);
+
+ activateWhatsAccepted();
+}
+
+const cookies = new LocalStorageWithSchema({
+ key: "acceptedCookies",
+ schema: AcceptedCookiesSchema,
+ fallback: null,
+ // no migration here, if cookies changed, we need to ask the user again
+});
+
+export function activateWhatsAccepted(): void {
+ const accepted = getAcceptedCookies();
+ if (accepted?.analytics) {
+ activateAnalytics();
+ }
+ if (accepted?.sentry) {
+ activateSentry();
+ }
+}
diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts
index 8baa45d80..b06f16873 100644
--- a/frontend/src/ts/index.ts
+++ b/frontend/src/ts/index.ts
@@ -29,6 +29,7 @@ import "./controllers/account-controller";
import { enable } from "./states/glarses-mode";
import "./test/caps-warning";
import "./modals/simple-modals";
+import * as CookiesModal from "./modals/cookies";
import "./controllers/input-controller";
import "./ready";
import "./controllers/route-controller";
@@ -46,6 +47,7 @@ import * as VersionButton from "./elements/version-button";
import * as Focus from "./test/focus";
import { getDevOptionsModal } from "./utils/async-modules";
import * as Sentry from "./sentry";
+import * as Cookies from "./cookies";
function addToGlobal(items: Record): void {
for (const [name, item] of Object.entries(items)) {
@@ -57,7 +59,13 @@ function addToGlobal(items: Record): void {
void loadFromLocalStorage();
void VersionButton.update();
Focus.set(true, true);
-Sentry.init();
+
+const accepted = Cookies.getAcceptedCookies();
+if (accepted === null) {
+ CookiesModal.show();
+} else {
+ Cookies.activateWhatsAccepted();
+}
addToGlobal({
snapshot: DB.getSnapshot,
diff --git a/frontend/src/ts/modals/cookies.ts b/frontend/src/ts/modals/cookies.ts
index 62bf22750..58d6f1749 100644
--- a/frontend/src/ts/modals/cookies.ts
+++ b/frontend/src/ts/modals/cookies.ts
@@ -1,39 +1,20 @@
-import { activateAnalytics } from "../controllers/analytics-controller";
import * as Notifications from "../elements/notifications";
import { isPopupVisible } from "../utils/misc";
import * as AdController from "../controllers/ad-controller";
import AnimatedModal from "../utils/animated-modal";
import { focusWords } from "../test/test-ui";
-import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
-import { z } from "zod";
-
-const AcceptedSchema = z.object({
- security: z.boolean(),
- analytics: z.boolean(),
-});
-type Accepted = z.infer;
-
-const acceptedCookiesLS = new LocalStorageWithSchema({
- key: "acceptedCookies",
- schema: AcceptedSchema,
- fallback: undefined,
-});
-
-function setAcceptedObject(obj: Accepted): void {
- acceptedCookiesLS.set(obj);
-}
-
-export function check(): void {
- if (acceptedCookiesLS.get() === undefined) {
- show();
- }
-}
+import {
+ AcceptedCookies,
+ getAcceptedCookies,
+ setAcceptedCookies,
+} from "../cookies";
export function show(goToSettings?: boolean): void {
void modal.show({
beforeAnimation: async () => {
if (goToSettings) {
- showSettings();
+ const currentAcceptedCookies = getAcceptedCookies();
+ showSettings(currentAcceptedCookies);
}
},
afterAnimation: async () => {
@@ -44,9 +25,26 @@ export function show(goToSettings?: boolean): void {
});
}
-function showSettings(): void {
+function showSettings(currentAcceptedCookies?: AcceptedCookies): void {
modal.getModal().querySelector(".main")?.classList.add("hidden");
modal.getModal().querySelector(".settings")?.classList.remove("hidden");
+
+ if (currentAcceptedCookies) {
+ if (currentAcceptedCookies.analytics) {
+ (
+ modal
+ .getModal()
+ .querySelector(".cookie.analytics input") as HTMLInputElement
+ ).checked = true;
+ }
+ if (currentAcceptedCookies.sentry) {
+ (
+ modal
+ .getModal()
+ .querySelector(".cookie.sentry input") as HTMLInputElement
+ ).checked = true;
+ }
+ }
}
async function hide(): Promise {
@@ -57,14 +55,6 @@ async function hide(): Promise {
});
}
-// function verifyVisible(): void {
-// if (!modal.isOpen()) return;
-// if (!isPopupVisible("cookiePopup")) {
-// //removed by cookie popup blocking extension
-// modal.destroy();
-// }
-// }
-
const modal = new AnimatedModal({
dialogId: "cookiesModal",
customEscapeHandler: (): void => {
@@ -78,17 +68,18 @@ const modal = new AnimatedModal({
const accepted = {
security: true,
analytics: true,
+ sentry: true,
};
- setAcceptedObject(accepted);
- activateAnalytics();
+ setAcceptedCookies(accepted);
void hide();
});
modalEl.querySelector(".rejectAll")?.addEventListener("click", () => {
const accepted = {
security: true,
analytics: false,
+ sentry: false,
};
- setAcceptedObject(accepted);
+ setAcceptedCookies(accepted);
void hide();
});
modalEl.querySelector(".openSettings")?.addEventListener("click", () => {
@@ -111,16 +102,16 @@ const modal = new AnimatedModal({
const analyticsChecked = (
modalEl.querySelector(".cookie.analytics input") as HTMLInputElement
).checked;
+ const sentryChecked = (
+ modalEl.querySelector(".cookie.sentry input") as HTMLInputElement
+ ).checked;
const accepted = {
security: true,
analytics: analyticsChecked,
+ sentry: sentryChecked,
};
- setAcceptedObject(accepted);
+ setAcceptedCookies(accepted);
void hide();
-
- if (analyticsChecked) {
- activateAnalytics();
- }
});
},
});
diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts
index 7b20937d2..13c32a150 100644
--- a/frontend/src/ts/ready.ts
+++ b/frontend/src/ts/ready.ts
@@ -1,7 +1,6 @@
import * as Misc from "./utils/misc";
import * as MonkeyPower from "./elements/monkey-power";
import * as MerchBanner from "./elements/merch-banner";
-import * as CookiesModal from "./modals/cookies";
import * as ConnectionState from "./states/connection";
import * as AccountButton from "./elements/account-button";
//@ts-expect-error no types for this package
@@ -12,7 +11,6 @@ import { loadPromise } from "./config";
$(async (): Promise => {
await loadPromise;
- CookiesModal.check();
//this line goes back to pretty much the beginning of the project and im pretty sure its here
//to make sure the initial theme application doesnt animate the background color
diff --git a/frontend/src/ts/sentry.ts b/frontend/src/ts/sentry.ts
index c07eb5557..157ef7ae1 100644
--- a/frontend/src/ts/sentry.ts
+++ b/frontend/src/ts/sentry.ts
@@ -3,7 +3,15 @@ import { envConfig } from "./constants/env-config";
let debug = false;
-export function init(): void {
+let activated = false;
+
+export function activateSentry(): void {
+ if (activated) {
+ console.warn("Sentry already activated");
+ return;
+ }
+ activated = true;
+ console.log("Activating Sentry");
Sentry.init({
release: envConfig.clientVersion,
dsn: "https://f50c25dc9dd75304a63776063896a39b@o4509236448133120.ingest.us.sentry.io/4509237217394688",