Skip to content

fix(sessions): keep earlier linked identities visible after a colliding identify - #4518

Open
atirna wants to merge 2 commits into
umami-software:devfrom
atirna:fix/session-identify-collision
Open

atirna wants to merge 2 commits into
umami-software:devfrom
atirna:fix/session-identify-collision

Conversation

@atirna

@atirna atirna commented Sep 6, 2026

Copy link
Copy Markdown

Summary

When two different real users collide on the same session hash (shared corporate NAT IP + identical User-Agent), the second identify() reassigns the session's primary distinctId. The session profile then showed only the latest identity, and the previously linked one disappeared with no indication two people ever shared that session.

The link history in session_link already records every identity ever linked to the session, but the session GET route only consulted it while session.distinct_id was still null. Once the first identify set the primary, every later collision skipped the history entirely.

This changes the route to always union the primary distinctId with getLinkedDistinctIds, so:

  • stitching resolves sessions through every linked identity, not just the current primary
  • the response now carries distinctIds (all identities ever linked, primary first)
  • the session profile's Distinct ID cell renders every linked identity when there is more than one

The primary distinctId keeps its current last-wins behavior, matching the ClickHouse read path (argMax(distinctId, max_time)), so no data semantics change on the write side.

Fixes #4512

Why

Without this there is no way to distinguish "this session was always User B" from "this session used to be User A" on a colliding session, which is the exact data-integrity problem reported: events keep the right per-event distinctId, but the stitched profile silently rewrites history.

Verification

  • before, on current dev: the session GET with primary user-b and link history [user-a, user-b] returned stitching for user-b only; GET keeps earlier linked identities visible after a colliding identify fails on the pre-fix tree
  • after: npx vitest run src/app/api/websites/[websiteId]/sessions/[sessionId]/route.test.ts: union + distinctIds: ['user-b', 'user-a'], both links drive stitchedSessionCount
  • npx vitest run src/app/(main)/websites/[websiteId]/sessions/SessionInfo.test.tsx: profile renders both user-b and user-a
  • npx biome check clean on all four changed files

Notes

  • distinctIds is additive on the response; existing consumers of distinctId are unchanged
  • repeated identifies of the same user are unchanged: the send-path cache token already skips the identity writes

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…ng identify

A session hash collision (shared NAT IP + identical user agent) lets a
second user's identify() reassign the session's primary distinctId. The
session GET route only consulted the session_link history when the
primary was unset, so the previously linked identity disappeared from
the profile with no record that two people shared the session.

The route now unions the primary distinctId with the link history for
both stitching and the response, and the profile renders every linked
identity instead of only the latest one.

Fixes umami-software#4512
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

@atirna is attempting to deploy a commit to the Umami Software Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR preserves identity history when multiple users become associated with the same session, returning the current primary identity first and rendering all linked identities in the session profile.

  • Always unions the current distinctId with identities from session_link.
  • Calculates the stitched-session count across the complete identity set.
  • Adds distinctIds to the session response and renders multiple IDs in the profile.
  • Adds route and component coverage for colliding and single-identity sessions.
  • The generated public API contract has not been updated for the new response field.

Confidence Score: 4/5

The implementation should not merge until the generated public API contract includes the newly added distinctIds response field.

The session behavior and UI coverage are coherent, but the advertised additive response field is absent from the OpenAPI contract and generated client, leaving typed API consumers unable to access it.

Files Needing Attention: src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts and its generated contract

Important Files Changed

Filename Overview
src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts Unions the primary and historical identities for stitching and exposes them as distinctIds, but the generated public response contract was not updated.
src/app/api/websites/[websiteId]/sessions/[sessionId]/route.test.ts Adds coverage for preserving earlier identities, primary-first ordering, deduplication, and stitched-session counts.
src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx Renders every linked identity when the response contains more than one while preserving the existing single-ID display.
src/app/(main)/websites/[websiteId]/sessions/SessionInfo.test.tsx Covers multiple linked identities and backward-compatible rendering when only distinctId is supplied.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S[Session record] --> P[Primary distinctId]
  L[session_link history] --> U[Deduplicated identity union]
  P --> U
  U --> Q[getLinkedSessionIds per identity]
  Q --> C[stitchedSessionCount]
  U --> R[GET session response]
  R --> UI[SessionInfo renders linked identities]
Loading

Reviews (1): Last reviewed commit: "fix(sessions): keep earlier linked ident..." | Re-trigger Greptile

Comment thread src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
…ests

The session GET response gained distinctIds but the generated contract,
openapi spec and api-client types still declared only canDelete and
stitchedSessionCount, and CI runs openapi:check --explicit.

The two union cases now share one test.each setup.

Signed-off-by: Atirna <288419661+atirna@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant