Skip to content

Implement sticky referrer tracking with first-touch attribution#271

Merged
yosriady merged 2 commits into
mainfrom
claude/sweet-cannon-fA4W7
May 28, 2026
Merged

Implement sticky referrer tracking with first-touch attribution#271
yosriady merged 2 commits into
mainfrom
claude/sweet-cannon-fA4W7

Conversation

@yosriady
Copy link
Copy Markdown
Contributor

@yosriady yosriady commented May 28, 2026

Summary

This PR implements sticky referrer tracking to ensure first-touch attribution is preserved across a user's session. Previously, the referrer value could be overwritten during internal navigation because the browser automatically updates document.referrer to the previous page URL. Now, the stored referrer value takes precedence, maintaining accurate session source attribution.

Key Changes

  • EventFactory.ts: Reversed the precedence order for referrer resolution to prioritize stored values over context values, ensuring the initial session referrer is never overwritten by internal navigation
  • PagePropertiesParsing.spec.ts:
    • Enhanced setMockLocation() helper to accept an optional referrer parameter for testing different referrer scenarios
    • Added comprehensive test suite "Referrer (sticky across session)" with 5 test cases covering:
      • Capturing initial referrer on landing pageview
      • Persisting entry referrer across internal navigation
      • Preventing mid-session external referrer from overwriting initial value
      • Falling back to document.referrer when no stored value exists
      • Ensuring referrer doesn't leak into fresh sessions

Implementation Details

The fix leverages session storage to maintain the entry referrer across page navigations. The logic now follows: storedTrafficSources?.referrer || contextTrafficSources.referrer || "", ensuring that once a referrer is captured at session start, it remains sticky throughout the session while still allowing fallback to document.referrer for fresh sessions with no stored value.

https://claude.ai/code/session_018WEzHcE3XDMxo5pqn27vLw


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

document.referrer is set by the browser on every internal navigation
with the previous page URL, so the existing event-time precedence
overwrote the session's entry referrer the moment a user clicked any
internal link. Flip the OR fallback so the stored value wins, matching
the sticky behaviour already used for ref, UTM, and click-ID fields.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the traffic source attribution logic in EventFactory.ts to make the referrer sticky across a session, preferring the stored referrer over the context referrer. It also adds comprehensive unit tests to verify this behavior. The review feedback highlights a critical bug where a direct visit followed by internal navigation can incorrectly attribute the session to an internal referrer, and suggests filtering out same-domain referrers along with adding a corresponding test case.

Comment thread src/event/EventFactory.ts
Comment on lines 196 to +197
referrer:
contextTrafficSources.referrer || storedTrafficSources?.referrer || "",
storedTrafficSources?.referrer || contextTrafficSources.referrer || "",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a subtle but critical bug when a user lands on the site directly (with no referrer).

Because a direct visit has an empty referrer, nothing is stored in the session storage. On the subsequent internal navigation, storedTrafficSources?.referrer is empty, so the logic falls back to contextTrafficSources.referrer (which the browser automatically populates with the previous internal page URL). This internal URL then gets stored in the session storage, incorrectly attributing the rest of the session to an internal referrer.

To prevent this, we should filter out same-domain (internal) referrers so they are never captured or stored.

      referrer: (() => {
        if (storedTrafficSources?.referrer) return storedTrafficSources.referrer;
        const ref = contextTrafficSources.referrer;
        if (!ref) return "";
        try {
          const refUrl = new URL(ref);
          const currentHost = globalThis.location?.hostname;
          if (currentHost && refUrl.hostname === currentHost) return "";
        } catch {}
        return ref;
      })(),

setMockLocation("https://formo.so/dashboard");
const freshContext = await getPageContext();
expect(freshContext.referrer).to.equal("");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a test case to verify that direct landing followed by internal navigation does not incorrectly capture the internal page as the referrer.

    });

    it("should not treat internal navigation as referrer when landing directly", async () => {
      // Direct landing: no referrer.
      setMockLocation("https://formo.so/");
      const landingContext = await getPageContext();
      expect(landingContext.referrer).to.equal("");

      // Internal navigation: browser sets document.referrer to the previous page.
      setMockLocation("https://formo.so/dashboard", "https://formo.so/");
      const secondContext = await getPageContext();
      expect(secondContext.referrer).to.equal("");
    });

@yosriady
Copy link
Copy Markdown
Contributor Author

@codex review

After a direct landing (empty document.referrer), the next internal
navigation reports document.referrer as the previous same-host page,
which the prior change would have stored as the session's entry
referrer for the rest of the session. Strip same-host referrers at
the source via getExternalReferrer so they never enter the data path.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@yosriady yosriady merged commit c7a37b7 into main May 28, 2026
11 checks passed
@yosriady yosriady deleted the claude/sweet-cannon-fA4W7 branch May 28, 2026 11:07
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.

2 participants