Root cause: In apps/backend/src/routes/public.ts, the GET /:username handler (and the /:username/card/:cardId handler) attempts to suppress self-view tracking with the guard if (viewerId !== user.id). However, when the request has no Authorization header, viewerId is set to null (line 92). The condition null !== user.id is always true (since user.id is a non-null UUID string), so the view is tracked even for unauthenticated requests. More importantly, when an authenticated owner visits their own profile without a token (e.g., logged out browser tab, direct URL), the same null-path fires and records a self-view. The comment on line 91 says "Only log if they aren't the owner" but the null branch at line 92 does not enforce this intent. This silently inflates totalViews and uniqueViewers in the analytics dashboard for every profile owner who views their own card while unauthenticated.
Why it matters: Analytics data is a core product feature. Inflated self-view counts corrupt the metrics users see on their dashboard. The bug is completely silent — no error, no log, just wrong data. It is also present in both the profile view handler and the card view handler (/:username/card/:cardId), making it a two-location fix.
Affected files/functions: apps/backend/src/routes/public.ts, GET /:username handler lines 84–108, GET /:username/card/:cardId handler lines 213–232.
Current behavior: An unauthenticated GET to /api/public/johndoe records a cardView row in the database even though the viewer is unknown. The condition null !== 'some-uuid' is always true, so the guard never blocks tracking.
Expected behavior: Unauthenticated requests should still be tracked (with viewerId: null and viewerIp) — that is intentional for analytics. The real fix is: the owner self-view check should only apply to authenticated requests where viewerId is set. The current structure already handles this correctly for the authenticated self-view case (line 89 skips setting viewerId if decoded.id === user.id). The issue is the redundant and misleading guard on line 99 which combines the two cases incorrectly. The condition should be refactored so it is clear: always track if viewerId is null (unauthenticated), always skip if viewerId === user.id (owner).
Minimal fix plan:
Change the condition on line 99 from if (viewerId !== user.id) to if (viewerId === null || viewerId !== user.id). Wait — null !== user.id is already true, so unauthenticated views ARE being tracked, which is correct. The real documentation/logic problem is: the code comment on line 91 (// Only log if they aren't the owner) is misleading, and the guard on line 99 is redundant because the authenticated owner path already sets viewerId = null (not decoded.id) when decoded.id === user.id — wait, re-reading line 89: it sets viewerId = decoded.id only when decoded.id !== user.id. So for an authenticated owner, viewerId stays null, and null !== user.id is true, causing an owner self-view to still be tracked.
The actual fix: in the authenticated branch where decoded.id === user.id, set a sentinel like viewerId = user.id (or use a separate boolean flag isSelfView), and change the outer guard to if (viewerId !== user.id). This correctly blocks the authenticated owner self-view. Apply the same two-line fix to the /:username/card/:cardId handler.
Suggested tests: Add unit tests for the public route: (1) authenticated request where decoded.id === user.id — assert prisma.cardView.create is never called, (2) authenticated request where decoded.id !== user.id — assert cardView.create IS called with viewerId = otherUserId, (3) unauthenticated request — assert cardView.create IS called with viewerId: null.
Root cause: In
apps/backend/src/routes/public.ts, theGET /:usernamehandler (and the/:username/card/:cardIdhandler) attempts to suppress self-view tracking with the guardif (viewerId !== user.id). However, when the request has noAuthorizationheader,viewerIdis set tonull(line 92). The conditionnull !== user.idis alwaystrue(sinceuser.idis a non-null UUID string), so the view is tracked even for unauthenticated requests. More importantly, when an authenticated owner visits their own profile without a token (e.g., logged out browser tab, direct URL), the same null-path fires and records a self-view. The comment on line 91 says "Only log if they aren't the owner" but the null branch at line 92 does not enforce this intent. This silently inflatestotalViewsanduniqueViewersin the analytics dashboard for every profile owner who views their own card while unauthenticated.Why it matters: Analytics data is a core product feature. Inflated self-view counts corrupt the metrics users see on their dashboard. The bug is completely silent — no error, no log, just wrong data. It is also present in both the profile view handler and the card view handler (
/:username/card/:cardId), making it a two-location fix.Affected files/functions:
apps/backend/src/routes/public.ts,GET /:usernamehandler lines 84–108,GET /:username/card/:cardIdhandler lines 213–232.Current behavior: An unauthenticated GET to
/api/public/johndoerecords acardViewrow in the database even though the viewer is unknown. The conditionnull !== 'some-uuid'is always true, so the guard never blocks tracking.Expected behavior: Unauthenticated requests should still be tracked (with
viewerId: nullandviewerIp) — that is intentional for analytics. The real fix is: the owner self-view check should only apply to authenticated requests whereviewerIdis set. The current structure already handles this correctly for the authenticated self-view case (line 89 skips settingviewerIdifdecoded.id === user.id). The issue is the redundant and misleading guard on line 99 which combines the two cases incorrectly. The condition should be refactored so it is clear: always track ifviewerIdis null (unauthenticated), always skip ifviewerId === user.id(owner).Minimal fix plan:
Change the condition on line 99 from
if (viewerId !== user.id)toif (viewerId === null || viewerId !== user.id). Wait —null !== user.idis already true, so unauthenticated views ARE being tracked, which is correct. The real documentation/logic problem is: the code comment on line 91 (// Only log if they aren't the owner) is misleading, and the guard on line 99 is redundant because the authenticated owner path already setsviewerId = null(notdecoded.id) whendecoded.id === user.id— wait, re-reading line 89: it setsviewerId = decoded.idonly whendecoded.id !== user.id. So for an authenticated owner,viewerIdstaysnull, andnull !== user.idis true, causing an owner self-view to still be tracked.The actual fix: in the authenticated branch where
decoded.id === user.id, set a sentinel likeviewerId = user.id(or use a separate boolean flagisSelfView), and change the outer guard toif (viewerId !== user.id). This correctly blocks the authenticated owner self-view. Apply the same two-line fix to the/:username/card/:cardIdhandler.Suggested tests: Add unit tests for the public route: (1) authenticated request where
decoded.id === user.id— assertprisma.cardView.createis never called, (2) authenticated request wheredecoded.id !== user.id— assertcardView.createIS called withviewerId = otherUserId, (3) unauthenticated request — assertcardView.createIS called withviewerId: null.