Skip to content

Commit 9b040d6

Browse files
nextlevelshitMichael Czechowski
authored andcommitted
Umami: drop duplicate + close 4 tracking gaps (#80)
Co-authored-by: Michael Czechowski <mail@dailysh.it> Co-committed-by: Michael Czechowski <mail@dailysh.it>
1 parent a4479df commit 9b040d6

3 files changed

Lines changed: 68 additions & 2 deletions

File tree

src/app.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,44 @@ function track(eventName, eventData = {}) {
2424
}
2525
}
2626

27+
// Global error handlers — capture uncaught JS + CSP violations.
28+
// Strip PII: only filename (basename), line/col, truncated message.
29+
// Throttle to avoid event-storms (max 1 per error signature per minute).
30+
const _errorThrottle = new Map();
31+
function _shouldEmitError(sig) {
32+
const now = Date.now();
33+
const last = _errorThrottle.get(sig) || 0;
34+
if (now - last < 60_000) return false;
35+
_errorThrottle.set(sig, now);
36+
return true;
37+
}
38+
window.addEventListener("error", (e) => {
39+
const file = (e.filename || "").split("/").pop() || "unknown";
40+
const msg = (e.message || "").slice(0, 140);
41+
const sig = `${file}:${e.lineno}:${msg.slice(0, 40)}`;
42+
if (_shouldEmitError(sig)) {
43+
track("js_error", { file, line: e.lineno, col: e.colno, message: msg });
44+
}
45+
});
46+
window.addEventListener("unhandledrejection", (e) => {
47+
const reason = e.reason;
48+
const msg = (reason?.message || String(reason || "unknown")).slice(0, 140);
49+
const sig = `promise:${msg.slice(0, 40)}`;
50+
if (_shouldEmitError(sig)) {
51+
track("js_unhandled_rejection", { message: msg });
52+
}
53+
});
54+
document.addEventListener("securitypolicyviolation", (e) => {
55+
const sig = `csp:${e.violatedDirective}:${e.blockedURI}`;
56+
if (_shouldEmitError(sig)) {
57+
track("csp_violation", {
58+
directive: e.violatedDirective,
59+
blocked_uri: (e.blockedURI || "").slice(0, 200),
60+
source_file: (e.sourceFile || "").split("/").pop() || ""
61+
});
62+
}
63+
});
64+
2765
// Simplified state - LessonEngine now manages lesson state and progress
2866
const state = {
2967
userSettings: {
@@ -913,6 +951,18 @@ function runCode() {
913951
lesson: engineState.lessonIndex
914952
});
915953

954+
// Derive module_complete: validateCode() already marked progress, so refetch state
955+
const updatedState = lessonEngine.getCurrentState();
956+
const moduleId = updatedState.module?.id;
957+
const completedCount = lessonEngine.userProgress?.[moduleId]?.completed?.length ?? 0;
958+
const lessonCount = updatedState.module?.lessons?.length ?? 0;
959+
if (moduleId && lessonCount > 0 && completedCount === lessonCount) {
960+
track("module_complete", {
961+
module: moduleId,
962+
lessons: lessonCount
963+
});
964+
}
965+
916966
// Show success hint
917967
showSuccessHint(validationResult.message || t("successMessage"));
918968

@@ -970,6 +1020,15 @@ function runCode() {
9701020
const step = validationResult.validCases + 1;
9711021
const total = validationResult.totalCases;
9721022

1023+
// Track partial / failed validation — funnel signal for which lessons trip users
1024+
track("lesson_fail", {
1025+
module: engineState.module?.id,
1026+
lesson: engineState.lessonIndex,
1027+
step,
1028+
total,
1029+
progress: total > 0 ? Math.round((step - 1) / total * 100) : 0
1030+
});
1031+
9731032
// Only show hints if enabled
9741033
if (!state.userSettings.disableFeedbackErrors) {
9751034
showHint(validationResult.message || t("keepTrying"), step, total);

src/auth.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,10 @@ function setupAuthForms() {
340340
if (currentHash && !currentHash.includes("access_token")) {
341341
localStorage.setItem("codeCrispies.oauthReturnRoute", currentHash);
342342
}
343+
track("auth_oauth_click", { provider: "google" });
343344
const { error } = await authModule?.signInWithGoogle() ?? { error: null };
344345
if (error) {
346+
track("auth_oauth_failed", { provider: "google", error_code: error.status || error.code });
345347
showOAuthError(error.message);
346348
}
347349
});
@@ -352,8 +354,10 @@ function setupAuthForms() {
352354
if (currentHash && !currentHash.includes("access_token")) {
353355
localStorage.setItem("codeCrispies.oauthReturnRoute", currentHash);
354356
}
357+
track("auth_oauth_click", { provider: "github" });
355358
const { error } = await authModule?.signInWithGitHub() ?? { error: null };
356359
if (error) {
360+
track("auth_oauth_failed", { provider: "github", error_code: error.status || error.code });
357361
showOAuthError(error.message);
358362
}
359363
});
@@ -386,6 +390,7 @@ async function handleLoginSubmit(e) {
386390
if (error) {
387391
errorEl.textContent = error.message;
388392
errorEl.classList.remove("hidden");
393+
track("auth_login_failed", { method: "email", error_code: error.status || error.code });
389394
} else {
390395
errorEl.classList.add("hidden");
391396
document.getElementById("auth-dialog").close();
@@ -418,6 +423,7 @@ async function handleSignupSubmit(e) {
418423
errorEl.textContent = error.message;
419424
errorEl.classList.remove("hidden");
420425
document.getElementById("signup-success")?.classList.add("hidden");
426+
track("auth_signup_failed", { method: "email", error_code: error.status || error.code });
421427
} else {
422428
errorEl.classList.add("hidden");
423429
// Show success message
@@ -449,9 +455,11 @@ async function handleResetSubmit(e) {
449455
errorEl.textContent = error.message;
450456
errorEl.classList.remove("hidden");
451457
successEl.classList.add("hidden");
458+
track("auth_password_reset_failed", { error_code: error.status || error.code });
452459
} else {
453460
errorEl.classList.add("hidden");
454461
successEl.classList.remove("hidden");
462+
track("auth_password_reset_request");
455463
}
456464
}
457465

src/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" href="./favicon.ico" type="image/x-icon" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://librete.ch https://umami.cloud.librete.ch https://liberapay.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://umami.cloud.librete.ch; img-src 'self' https://liberapay.com data:; font-src 'self'; frame-src 'self' blob:" />
7+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://umami.cloud.librete.ch https://liberapay.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://umami.cloud.librete.ch; img-src 'self' https://liberapay.com data:; font-src 'self'; frame-src 'self' blob:" />
88

99
<!-- Umami analytics (self-hosted at umami.cloud.librete.ch) -->
1010
<script defer src="https://umami.cloud.librete.ch/script.js" data-website-id="4ad21991-5d01-4cf9-9f06-2c0c00ffbab1"></script>
@@ -58,7 +58,6 @@
5858
</script>
5959

6060
<link rel="stylesheet" href="main.css" />
61-
<script defer src="https://librete.ch/u/script.js" data-website-id="2189049f-80c1-4ca0-b0ff-b5cc49276b5f"></script>
6261
</head>
6362
<body>
6463
<a href="#main-content" class="skip-link" data-i18n="skipLink">Skip to main content</a>

0 commit comments

Comments
 (0)