Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function showUpdatePasswordModal(): void {
showSimpleModal({
title: "Update password",
schema: z.object({
previousPass: getPasswordSchema(),
previousPass: z.string().min(1, "Current password is required"),
newPassword: getPasswordSchema(),
newPassConfirm: getPasswordSchema(),
}),
Expand Down
116 changes: 42 additions & 74 deletions frontend/src/ts/test/events/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from "./types";
import { keysToTrack } from "./helpers";
import { Keycode } from "../../constants/keys";
import { mean, roundTo2 } from "@monkeytype/util/numbers";
import { isSafeNumber, mean, roundTo2 } from "@monkeytype/util/numbers";
import { bailedOut, koreanStatus, resultCalculating } from "../test-state";
import * as TestWords from "../test-words";
import { Config } from "../../config/store";
Expand Down Expand Up @@ -78,6 +78,10 @@ export function logTestEvent(

now = roundTo2(now);

if (!isSafeNumber(now)) {
throw new Error(`Invalid timestamp: ${now}`);
}

//strip undefined values from eventData
eventData = Object.fromEntries(
Object.entries(eventData).filter(([_, v]) => v !== undefined),
Expand Down Expand Up @@ -188,84 +192,51 @@ function invalidateCache(): void {
}

export function cleanupData(): void {
invalidateCache();
getAllTestEvents();

if (cachedAllEvents === undefined) {
throw new Error(
"cachedAllEvents should not be undefined after getAllTestEvents",
);
}

//remove all pre-start keydown/keyup events except the last keydown
const timerStartIndex = cachedAllEvents.findIndex(
(e) => e.type === "timer" && e.data.event === "start",
);
if (timerStartIndex !== -1) {
// find the last keydown before timer start
let lastPreStartKeydownIndex = -1;
for (let i = timerStartIndex - 1; i >= 0; i--) {
if (cachedAllEvents[i]?.type === "keydown") {
lastPreStartKeydownIndex = i;
break;
}
const timerStart = timerEvents.find((e) => e.data.event === "start");
const timerEnd = timerEvents.find((e) => e.data.event === "end");

if (timerStart !== undefined) {
// keep only the last pre-start keydown; drop all pre-start keyups
let lastPreStartKeydown: KeydownEvent | undefined;
for (const e of keydownEvents) {
if (e.ms < timerStart.ms) lastPreStartKeydown = e;
else break;
}
cachedAllEvents = cachedAllEvents.filter((e, index) => {
if (index >= timerStartIndex) return true;
if (e.type === "keydown") return index === lastPreStartKeydownIndex;
if (e.type === "keyup") return false;
return true;
});
}

//remove all input events after timer end
const timerEndIndex = cachedAllEvents.findIndex(
(e) => e.type === "timer" && e.data.event === "end",
);
if (timerEndIndex !== -1) {
cachedAllEvents = cachedAllEvents.filter(
(e, index) => !(e.type === "input" && index > timerEndIndex),
keydownEvents = keydownEvents.filter(
(e) => e.ms >= timerStart.ms || e === lastPreStartKeydown,
);
keyupEvents = keyupEvents.filter((e) => e.ms >= timerStart.ms);
}

//remove keydowns after timer end, and their associated keyups
if (timerEndIndex !== -1) {
const keydownsAfterTimerEnd = new Set(
cachedAllEvents
.filter((e, index) => e.type === "keydown" && index > timerEndIndex)
.map((e) => (e.data as KeydownEventData).code),
if (timerEnd !== undefined) {
inputEvents = inputEvents.filter((e) => e.ms <= timerEnd.ms);
const postEndKeydownCodes = new Set(
keydownEvents.filter((e) => e.ms > timerEnd.ms).map((e) => e.data.code),
);
keydownEvents = keydownEvents.filter((e) => e.ms <= timerEnd.ms);
keyupEvents = keyupEvents.filter(
(e) => e.ms <= timerEnd.ms || !postEndKeydownCodes.has(e.data.code),
);
cachedAllEvents = cachedAllEvents.filter((e, index) => {
if (index <= timerEndIndex) return true;
if (e.type === "keydown") return false;
if (e.type === "keyup") {
return !keydownsAfterTimerEnd.has(e.data.code);
}
return true;
});
}

// sync source arrays back from cleaned cache
keydownEvents = cachedAllEvents.filter(
(e): e is KeydownEvent => e.type === "keydown",
);
keyupEvents = cachedAllEvents.filter(
(e): e is KeyupEvent => e.type === "keyup",
);
timerEvents = cachedAllEvents.filter(
(e): e is TimerEvent => e.type === "timer",
);
inputEvents = cachedAllEvents.filter(
(e): e is InputEvent => e.type === "input",
);
compositionEvents = cachedAllEvents.filter(
(e): e is CompositionTestEvent => e.type === "composition",
);
invalidateCache();
}

export function getAllTestEvents(): TestEventNoMs[] {
if (cachedAllEvents !== undefined) return cachedAllEvents;

const total =
keydownEvents.length +
keyupEvents.length +
timerEvents.length +
inputEvents.length +
compositionEvents.length;

if (total === 0) {
cachedAllEvents = [];
return cachedAllEvents;
}

const firstEventMs = Math.min(
...[
keydownEvents[0]?.ms,
Expand All @@ -277,14 +248,11 @@ export function getAllTestEvents(): TestEventNoMs[] {
);

const startEventMs =
timerEvents.find((e) => e.data.event === "start")?.ms ?? firstEventMs ?? 0;
timerEvents.find((e) => e.data.event === "start")?.ms ?? firstEventMs;

const total =
keydownEvents.length +
keyupEvents.length +
timerEvents.length +
inputEvents.length +
compositionEvents.length;
if (!isSafeNumber(startEventMs)) {
throw new Error(`Invalid startEventMs: ${startEventMs}`);
}

const merged = new Array<TestEvent>(total);
let p = 0;
Expand Down
60 changes: 54 additions & 6 deletions frontend/src/ts/test/test-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,10 @@ function compareCompletedEvents(
key === "timestamp" ||
key === "keyDuration" ||
key === "keySpacing" ||
key === "chartData"
key === "chartData" ||
key === "consistency" ||
key === "keyConsistency" ||
key === "keyOverlap"
) {
continue;
}
Expand Down Expand Up @@ -1024,6 +1027,7 @@ function compareCompletedEvents(
continue;
}

///@ts-expect-error temp
if (key === "keyOverlap") {
val1 = Numbers.roundTo2(val1 as number);
val2 = Numbers.roundTo2(val2 as number);
Expand Down Expand Up @@ -1274,9 +1278,31 @@ function compareCompletedEvents(
if (a.length === b.length && a.every((val, i) => val === b[i])) {
console.debug(`Completed event match on rawHistory:`, a);
} else {
notMatching.push(`rawHistory (values differ)`);
const len = Math.min(a.length, b.length);
const diffs: number[] = [];
for (let i = 0; i < len; i++) {
const av = a[i] as number;
const bv = b[i] as number;
const denom = Math.abs(av);
if (denom === 0) {
if (bv !== 0) diffs.push(100);
continue;
}
diffs.push((Math.abs(av - bv) / denom) * 100);
}
const avg = diffs.length
? diffs.reduce((acc, v) => acc + v, 0) / diffs.length
: 0;
const avgRounded = Numbers.roundTo2(avg);
notMatching.push(
`rawHistory (avg ${avgRounded}% difference): ${JSON.stringify(a)} vs ${JSON.stringify(b)}`,
);
mismatchedKeys.push("rawHistory");
console.error(`Completed event mismatch on rawHistory:`, a, b);
console.error(
`Completed event mismatch on rawHistory (avg ${avgRounded}% difference):`,
a,
b,
);
}
}

Expand All @@ -1287,9 +1313,31 @@ function compareCompletedEvents(
if (a.length === b.length && a.every((val, i) => val === b[i])) {
console.debug(`Completed event match on chartData.wpm:`, a);
} else {
notMatching.push(`chartData.wpm (values differ)`);
const len = Math.min(a.length, b.length);
const diffs: number[] = [];
for (let i = 0; i < len; i++) {
const av = a[i] as number;
const bv = b[i] as number;
const denom = Math.abs(av);
if (denom === 0) {
if (bv !== 0) diffs.push(100);
continue;
}
diffs.push((Math.abs(av - bv) / denom) * 100);
}
const avg = diffs.length
? diffs.reduce((acc, v) => acc + v, 0) / diffs.length
: 0;
const avgRounded = Numbers.roundTo2(avg);
notMatching.push(
`chartData.wpm (avg ${avgRounded}% difference): ${JSON.stringify(a)} vs ${JSON.stringify(b)}`,
);
mismatchedKeys.push("chartData.wpm");
console.error(`Completed event mismatch on chartData.wpm:`, a, b);
console.error(
`Completed event mismatch on chartData.wpm (avg ${avgRounded}% difference):`,
a,
b,
);
}
}
}
Expand Down Expand Up @@ -1386,7 +1434,7 @@ function compareCompletedEvents(
difficulty: ce.difficulty,
duration: ce.testDuration,
funboxes: getActiveFunboxNames().join(","),
version: 28,
version: 29,
eventLog,
// ce: ce as Record<string, unknown>,
// ce2: ce2 as Record<string, unknown>,
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const ReportCompletedEventMismatchRequestSchema = z.object({
difficulty: DifficultySchema.optional(),
duration: z.number().max(200).optional(),
funboxes: z.string().max(100).optional(),
version: z.literal(28),
version: z.literal(29),
eventLog: z.object({
version: z.number(),
context: z.record(z.unknown()),
Expand Down
Loading