Skip to content

Commit 0be4f2c

Browse files
committed
fix(web): prevent history entries with mismatched URL/entity
When navigating between pages, the router updates location before the old component unmounts. This caused the history tracking effect to re-run with stale entityId/entityType props but a new URL, creating corrupted entries like "Machine learning" pointing to /works/W123. Changes: - use-user-interactions: Validate URL matches entityType/entityId before recording history entry (skip if URL doesn't contain entity path) - catalogue-db: Filter out entries where stored entityType doesn't match URL path in getHistory(), catching existing corrupted entries
1 parent 45537d9 commit 0be4f2c

File tree

2 files changed

+34
-6
lines changed

2 files changed

+34
-6
lines changed

apps/web/src/hooks/use-user-interactions.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
245245
if (autoTrackVisits && entityId && entityType) {
246246
const trackPageVisit = async () => {
247247
try {
248+
// Validate URL matches current entity to prevent race conditions during navigation
249+
// When navigating from entity A to entity B, location updates before component unmounts,
250+
// which could cause recording entity A with entity B's URL
251+
const currentUrl = location.pathname + serializeSearch(location.search);
252+
const urlMatchesEntity = currentUrl.includes(`/${entityType}/`) && currentUrl.includes(entityId);
253+
if (!urlMatchesEntity) {
254+
logger.debug(
255+
USER_INTERACTIONS_LOGGER_CONTEXT,
256+
"Skipping history record - URL doesn't match entity (navigation race condition)",
257+
{ entityId, entityType, currentUrl }
258+
);
259+
return;
260+
}
261+
248262
// Debounce: skip if same entity was recorded recently
249263
const now = Date.now();
250264
if (
@@ -254,8 +268,6 @@ export const useUserInteractions = (options: UseUserInteractionsOptions = {}): U
254268
return;
255269
}
256270

257-
const currentUrl = location.pathname + serializeSearch(location.search);
258-
259271
// Only pass displayName if it matches the current entityId
260272
// This prevents race conditions where a stale displayName is stored
261273
const safeDisplayName = displayNameEntityRef.current === entityId ? displayName : undefined;

packages/utils/src/storage/catalogue-db.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,9 @@ export class CatalogueService {
10971097
C: "/concepts/",
10981098
};
10991099

1100+
// Non-entity pages that shouldn't be validated against entity patterns
1101+
const nonEntityPatterns = ["/about", "/settings", "/history", "/bookmarks", "/catalogue", "/search"];
1102+
11001103
return entities.filter((entity) => {
11011104
if (entity.entityId.length === 0) return false;
11021105
if (entity.entityId.includes(CORRUPTED_ENTITY_ID_PATTERN)) return false;
@@ -1105,15 +1108,28 @@ export class CatalogueService {
11051108
if (entity.notes?.includes(CORRUPTED_ENTITY_ID_PATTERN)) return false;
11061109
if (entity.notes?.includes(urlEncodedPattern)) return false;
11071110

1108-
// Validate entityId matches URL path (detect mismatch like Work ID with Author URL)
1111+
// Validate entityId/entityType matches URL path (detect race condition mismatches)
11091112
const urlMatch = entity.notes?.match(/URL: ([^\n]+)/);
11101113
if (urlMatch) {
11111114
const url = urlMatch[1];
1115+
1116+
// Skip validation for non-entity pages
1117+
const isNonEntityUrl = nonEntityPatterns.some(pattern => url.includes(pattern));
1118+
if (isNonEntityUrl) {
1119+
return true;
1120+
}
1121+
1122+
// Validate entityType matches URL path
1123+
// This catches race conditions where entity A's props were recorded with entity B's URL
1124+
const entityTypeUrlPath = `/${entity.entityType}/`;
1125+
if (!url.includes(entityTypeUrlPath)) {
1126+
return false;
1127+
}
1128+
1129+
// Also validate entityId matches URL path for prefixed IDs
11121130
const entityPrefix = entity.entityId.charAt(0).toUpperCase();
11131131
const expectedPath = entityPrefixToPath[entityPrefix];
1114-
// If we have a known prefix and URL doesn't match expected path, filter it out
1115-
// Skip validation for non-entity pages like /about, /settings
1116-
if (expectedPath && !url.includes(expectedPath) && !url.startsWith("/about") && !url.startsWith("/settings")) {
1132+
if (expectedPath && !url.includes(expectedPath)) {
11171133
return false;
11181134
}
11191135
}

0 commit comments

Comments
 (0)