Skip to content

Cut over computed-posthog-events to using Keycloak's sub over email as the user's distinct_id#2805

Merged
Will-Howard merged 1 commit into
masterfrom
wh-2713-posthog-sub-distinct-ids
Jul 19, 2026
Merged

Cut over computed-posthog-events to using Keycloak's sub over email as the user's distinct_id#2805
Will-Howard merged 1 commit into
masterfrom
wh-2713-posthog-sub-distinct-ids

Conversation

@Will-Howard

Copy link
Copy Markdown
Collaborator

Description

See the migration plan, this covers "PR 4" and "PR 5":

  • PR 4: Flip the 'identify_applicants' event definition from distinctId: user.email to distinctId: user.keycloakIdentifier. This submits identify events for both new users and new applications by existing users. It's safe to flip without any alias step because:
    a. For new users: They will have already hit the posthog.alias(auth.sub, auth.email) in step 2
    b. For new applications against existing users: They will have hit the posthog.alias(auth.sub, auth.email) from the backfill in step 3
  • PR 5 (could potentially be combined with PR 4): Flip all the other events in computed-posthog-events to use keycloakIdentifier as the distinctId

Issue

#2713

Developer checklist

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56571f21-59ca-4656-bb90-239adae56a78

📥 Commits

Reviewing files that changed from the base of the PR and between 6049e6d and 1cd5bed.

📒 Files selected for processing (2)
  • libraries/computed-posthog-events/src/definitions.test.ts
  • libraries/computed-posthog-events/src/definitions.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • libraries/computed-posthog-events/src/definitions.ts
  • libraries/computed-posthog-events/src/definitions.test.ts

📝 Walkthrough

Walkthrough

PostHog event projections now derive distinct_id from linked users’ keycloakIdentifier rather than email. The change covers attendance, dropout, application lifecycle, certificate, exercise, resource, project submission, and applicant-identification events. Tests update fixtures, identity-resolution cases, incremental scans, write-once behaviour, and expected sub-* identifiers.

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: switching computed PostHog distinct_id from email to Keycloak sub.
Description check ✅ Passed It includes the required Description and Issue sections, and the checklist is filled in; no screenshot is needed for a non-visual change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wh-2713-posthog-sub-distinct-ids

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Will-Howard

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review @greptileai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

@Will-Howard: I’ll review the changes, with particular attention to consistent use of keycloakIdentifier as the PostHog distinct_id and migration safety.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes the cut-over of all computed PostHog events from using email to using the Keycloak sub (via keycloakIdentifier) as the distinctId, building on the alias/identify backfill from earlier migration PRs. The identity-resolution helper getEmailByUserId is replaced by getKeycloakIdentifierByUserId, which builds a sparse map (omitting users who have no keycloakIdentifier) so that unresolved users cleanly produce no entry rather than a stale email.

  • Application-funnel events (accepted, rejected, withdrawn, course_dropped_out/deferred) fall back to courseRegistrationAnonDistinctId when no keycloak identity is available, preserving funnel coherence for applicants who have not yet logged in.
  • Post-login events (certificate_issued, discussion_attended/absent, exercise_completed, resource_completed, project_submitted) resolve to null or continue when no keycloak sub exists, silently dropping those events — semantically correct because these actions require an authenticated session.
  • New tests cover the end-to-end identity model (logged-in vs. never-logged-in applicant funnels) and the no keycloakIdentifier → skip path for identify_applicants and discussion events.

Confidence Score: 5/5

Safe to merge — the identity cut-over is complete and consistent across all event types, with clear fallback semantics.

The helper rename and distinctId swap are mechanically straightforward. The two-tier fallback strategy (anon anchor for application-funnel events, null/drop for post-login events) is deliberate, well-commented, and exercised by a comprehensive test suite including new end-to-end identity-model tests. No event type is left in a mixed state.

No files require special attention.

Important Files Changed

Filename Overview
libraries/computed-posthog-events/src/definitions.ts Replaces getEmailByUserId with getKeycloakIdentifierByUserId across all event projections; application-funnel events fall back to the anon anchor while post-login events drop to null when no keycloak sub is present.
libraries/computed-posthog-events/src/definitions.test.ts Test fixtures updated to include keycloakIdentifier; all distinct_id assertions updated to keycloak subs; new identity-model integration tests added covering logged-in vs never-logged-in funnels and the skip-when-no-sub path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Event projection triggered] --> B{Has userId?}
    B -- No --> C{Application funnel event?}
    B -- Yes --> D[getKeycloakIdentifierByUserId lookup]
    D --> E{keycloakIdentifier found?}
    E -- Yes --> F[distinctId = keycloakIdentifier]
    E -- No --> C
    C -- Yes --> G[distinctId = courseRegistrationAnonDistinctId]
    C -- No --> H[distinctId = null - event dropped]
    F --> I[Event sent to PostHog]
    G --> I
    H --> J[Event silently skipped]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Event projection triggered] --> B{Has userId?}
    B -- No --> C{Application funnel event?}
    B -- Yes --> D[getKeycloakIdentifierByUserId lookup]
    D --> E{keycloakIdentifier found?}
    E -- Yes --> F[distinctId = keycloakIdentifier]
    E -- No --> C
    C -- Yes --> G[distinctId = courseRegistrationAnonDistinctId]
    C -- No --> H[distinctId = null - event dropped]
    F --> I[Event sent to PostHog]
    G --> I
    H --> J[Event silently skipped]
Loading

Reviews (2): Last reviewed commit: "[refactor] computed-posthog-events: use ..." | Re-trigger Greptile

Comment on lines +163 to +172
test('skips a registration with no linked user (no identity to attribute to)', async () => {
await testDb.insert(courseRegistrationTable, {
id: 'a1', courseId: 'c1', email: 'acc@x.com', acceptedAt: '2026-06-01T10:00:00.000Z',
});

const ph = mockPostHogBackend();
await forwardAllEventsToPostHog();

expect(ph.events.filter((e) => e.event === 'application_accepted')).toHaveLength(0);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing skip tests for other registration-based events

A "skips when no linked user" test was added for application_accepted, but the same behaviour change affects application_rejected and application_withdrawn (both now resolve distinctId via userId → keycloakIdentifier and produce null when the chain is broken). A facilitated certificate_issued row that carries a certificateId but has userId: null would also be silently dropped now, where previously it was sent with the email. Adding analogous coverage for these three cases would make the silent-skip contract explicit across all the newly-migrated event types.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libraries/computed-posthog-events/src/definitions.ts (1)

106-150: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Potential silent loss for rows scanned before keycloakIdentifier exists

The Keycloak-backed projections resolve distinctId at scan time and drop rows when the linked user has no identifier yet (or skip them entirely in identify_applicants). forwardEventTypeToPostHog rejects those events before they reach posthogEmittedEventsTable, so once the monotonic since cursor moves past the row’s timestamp, it will not be reconsidered after a later backfill. If that backfill can lag, this needs an explicit retry/backfill path or a guarantee that all referenced users are populated first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/computed-posthog-events/src/definitions.ts` around lines 106 - 150,
Update calculateDropoutEvents and the related Keycloak-backed projection flow so
events whose linked users lack a keycloakIdentifier are not permanently lost
when the since cursor advances. Add an explicit retry/backfill mechanism for
unresolved users, or enforce and validate that all referenced users are
populated before advancing the cursor; preserve these rows for later emission
once identifiers become available.
🧹 Nitpick comments (1)
libraries/computed-posthog-events/src/definitions.ts (1)

140-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated distinctId resolution pattern could be a shared helper.

The (x ? map.get(x) : undefined) ?? null expression is repeated across dropout, accept/reject/withdraw, certificate, exercise, resource, and project events. Extracting a small helper (e.g. resolveDistinctId(userId, map)) would remove the duplication and centralise the fallback semantics.

♻️ Proposed helper
+const resolveDistinctId = (
+  userId: string | null | undefined,
+  keycloakIdentifierByUserId: Map<string, string>,
+): string | null => (userId ? keycloakIdentifierByUserId.get(userId) : undefined) ?? null;

Then replace each occurrence, e.g.:

-      distinctId: (r.userId ? keycloakIdentifierByUserId.get(r.userId) : undefined) ?? null,
+      distinctId: resolveDistinctId(r.userId, keycloakIdentifierByUserId),

Also applies to: 235-235, 260-260, 285-285, 317-317, 332-332, 376-376, 419-419, 480-480

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/computed-posthog-events/src/definitions.ts` at line 140, Extract
the repeated `(userId ? map.get(userId) : undefined) ?? null` logic into a
shared `resolveDistinctId` helper in the definitions module, preserving its null
fallback semantics. Replace the corresponding `distinctId` expressions across
the dropout, accept/reject/withdraw, certificate, exercise, resource, and
project event definitions with calls to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@libraries/computed-posthog-events/src/definitions.ts`:
- Around line 106-150: Update calculateDropoutEvents and the related
Keycloak-backed projection flow so events whose linked users lack a
keycloakIdentifier are not permanently lost when the since cursor advances. Add
an explicit retry/backfill mechanism for unresolved users, or enforce and
validate that all referenced users are populated before advancing the cursor;
preserve these rows for later emission once identifiers become available.

---

Nitpick comments:
In `@libraries/computed-posthog-events/src/definitions.ts`:
- Line 140: Extract the repeated `(userId ? map.get(userId) : undefined) ??
null` logic into a shared `resolveDistinctId` helper in the definitions module,
preserving its null fallback semantics. Replace the corresponding `distinctId`
expressions across the dropout, accept/reject/withdraw, certificate, exercise,
resource, and project event definitions with calls to this helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 86efca6b-aecc-4142-8192-39f26115ba34

📥 Commits

Reviewing files that changed from the base of the PR and between 4f4ca03 and 6049e6d.

📒 Files selected for processing (2)
  • libraries/computed-posthog-events/src/definitions.test.ts
  • libraries/computed-posthog-events/src/definitions.ts

@Will-Howard
Will-Howard force-pushed the wh-2713-posthog-sub-distinct-ids branch from 47e9ae3 to 1cd5bed Compare July 19, 2026 13:04
@Will-Howard
Will-Howard temporarily deployed to wh-2713-posthog-sub-distinct-ids - bluedot-preview PR #2805 July 19, 2026 13:04 — with Render Destroyed
@Will-Howard
Will-Howard temporarily deployed to wh-2713-posthog-sub-distinct-ids - bluedot-storybook-preview PR #2805 July 19, 2026 13:04 — with Render Destroyed
@Will-Howard
Will-Howard marked this pull request as ready for review July 19, 2026 13:08
@Will-Howard
Will-Howard merged commit ba44a19 into master Jul 19, 2026
8 checks passed
@Will-Howard
Will-Howard deleted the wh-2713-posthog-sub-distinct-ids branch July 19, 2026 13:16
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