Skip to content

feat: posthog distinct id migration#2062

Merged
AndreyHirsa merged 5 commits intomainfrom
feat/posthog-distinct-id-migration
Mar 31, 2026
Merged

feat: posthog distinct id migration#2062
AndreyHirsa merged 5 commits intomainfrom
feat/posthog-distinct-id-migration

Conversation

@AndreyHirsa
Copy link
Copy Markdown
Contributor

@AndreyHirsa AndreyHirsa commented Mar 31, 2026

Summary

Migrate PostHog distinct_id from email to database user ID

Changes

  • SDK: Don't cache apikey-{hash} fallback — a transient /users/me failure no longer poisons the identity cache for the entire process lifetime
  • SDK: distinct_id now uses payload.id from /users/me instead of payload.email
  • CLI: distinct_id now uses database user ID when authenticated
  • Both: Send $create_alias (email -> database user ID) on every event to merge existing email-based profiles
  • CLI: Old alias (hashed email -> raw email) replaced with new alias (email -> database user ID); crypto import removed
  • CLI: trackEvent signature changed from email: string | null to user: UserIdentity ({ email, id } | null); all callers updated
  • Localizer: checkAuth() return type extended with userId field

Testing

Existing tests updated to reflect new identity source:

  • "captures event with database user ID when whoami succeeds" — asserts distinct_id is database user ID
  • "falls back to API key hash when whoami returns no id" — asserts fallback when /users/me returns no id
  • PostHog mock updated with alias method to support new $create_alias call
  • All tests pass locally

Visuals

N/A - no UI changes, backend tracking only.

Checklist

  • Changeset added (if version bump needed)
  • Tests cover business logic (not just happy path)
  • No breaking changes (or documented below)

Summary by CodeRabbit

  • Chores
    • Switched analytics identity to use database user IDs (with email as an associated property) instead of raw emails across the CLI and SDK; updated event enrichment and aliasing behavior.
  • Tests
    • Updated observability tests and mocks to reflect the new identity, aliasing, and fallback behaviors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3eab318f-f127-4b8d-b3ad-dc86e50c7c5a

📥 Commits

Reviewing files that changed from the base of the PR and between f547cc9 and 9fc2c76.

📒 Files selected for processing (1)
  • packages/sdk/src/utils/observability.ts

📝 Walkthrough

Walkthrough

Migrates PostHog tracking from email-based identifiers to a structured UserIdentity ({ email, id } | null), updating identity resolution, aliasing, tracking APIs, CLI command integrations, localizer auth shapes, SDK observability logic, and related tests.

Changes

Cohort / File(s) Summary
Changeset Metadata
\.changeset/lazy-beds-lay.md
Adds changeset documenting patch bumps and the migration of PostHog tracking identity from email to database user ID.
Observability Core (CLI)
packages/cli/src/cli/utils/observability.ts
Introduces exported UserIdentity type; changes trackEvent to accept UserIdentity; switches distinct_id to DB user id ("database_id"); removes email hashing; updates aliasing to use DB id ↔ email.
Observability Core (SDK)
packages/sdk/src/utils/observability.ts, packages/sdk/src/utils/observability.spec.ts
Caches composite { identity, email? }; getDistinctId now prefers DB id and returns email separately; resolveIdentityAndCapture uses identity.distinct_id and sets $set.email; adds PostHog alias call when email present; tests updated to reflect DB id distinct_id and alias behavior.
CLI Commands
packages/cli/src/cli/cmd/i18n.ts, packages/cli/src/cli/cmd/run/index.ts, packages/cli/src/cli/cmd/status.ts
Replace nullable email with userIdentity: UserIdentity; call new determineUserIdentity; pass userIdentity to trackEvent; compute authenticated from !!userIdentity.
Identity Utilities
packages/cli/src/cli/cmd/run/_utils.ts
Renamed determineEmaildetermineUserIdentity; returns structured UserIdentity ({ email, id }
Localizer Types & Implementation
packages/cli/src/cli/localizer/_types.ts, packages/cli/src/cli/localizer/lingodotdev.ts
ILocalizer.checkAuth() return shape now may include userId?: string; lingodotdev localizer populates userId from /users/me response.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Command
    participant Localizer as Localizer.checkAuth()
    participant IdentityUtil as determineUserIdentity()
    participant Tracker as trackEvent()
    participant PostHog as PostHog

    CLI->>Localizer: request auth status
    Localizer-->>CLI: { authenticated, username?, userId? }
    CLI->>IdentityUtil: determineUserIdentity(ctx)
    IdentityUtil->>Localizer: read auth status
    IdentityUtil-->>CLI: UserIdentity { email, id } or null
    CLI->>Tracker: trackEvent(userIdentity, event, props)
    Tracker->>Tracker: determineDistinctId(userIdentity)
    Tracker->>PostHog: capture(distinct_id: user.id, properties: {..., $set.email?: user.email})
    alt email present
        Tracker->>PostHog: alias(distinct_id: user.id, alias: user.email)
    end
    PostHog-->>Tracker: ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ohmoses

Poem

🐰
I hopped from email to id so spry,
A tiny alias beneath the sky.
PostHog now finds the user true,
I left soft hops in every queue —
Hop, track, and ship with bunny dew! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: migrating PostHog's distinct_id mechanism from email-based to database user ID-based identity tracking.
Description check ✅ Passed The PR description follows the template structure with Summary, Changes, Testing, Visuals, and Checklist sections. All required sections are present and adequately filled with specific details about the migration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/posthog-distinct-id-migration

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/cli/src/cli/cmd/status.ts (1)

359-376: ⚠️ Potential issue | 🟡 Minor

Duplicate console.log for missing keys.

Lines 369-376 and 373-376 both output the "Missing:" message, resulting in duplicate output when missingKeys.length > 0.

Proposed fix — remove the duplicate
                 if (flags.verbose) {
                   if (missingKeys.length > 0) {
                     console.log(
                       `    ${chalk.red(`Missing:`)} ${missingKeys.length} keys, ~${wordsToTranslate} words`,
                     );
-                    console.log(
-                      `    ${chalk.red(`Missing:`)} ${
-                        missingKeys.length
-                      } keys, ~${wordsToTranslate} words`,
-                    );
                     console.log(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/cli/cmd/status.ts` around lines 359 - 376, The verbose
branch in status.ts prints the "Missing:" line twice; inside the flags.verbose
block remove the duplicate console.log that prints `Missing:
${missingKeys.length} keys, ~${wordsToTranslate} words` so only a single line is
logged when missingKeys.length > 0 (the remaining call should use the existing
variables missingKeys and wordsToTranslate and remain under the same if
(missingKeys.length > 0) check after bucketOra.succeed).
🧹 Nitpick comments (1)
packages/cli/src/cli/utils/observability.ts (1)

141-142: Inconsistent error handling between main and alias requests.

The main request logs errors in DEBUG mode (lines 113-117), but the alias request silently swallows errors. For consistency and to aid debugging migration issues, consider logging alias request errors in DEBUG mode as well.

🔧 Suggested fix for consistent error handling
         aliasReq.on("timeout", () => aliasReq.destroy());
-        aliasReq.on("error", () => {});
+        aliasReq.on("error", (error) => {
+          if (process.env.DEBUG === "true") {
+            console.error("[Tracking] Alias error ignored:", error.message);
+          }
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/cli/utils/observability.ts` around lines 141 - 142, The
alias request currently swallows errors (aliasReq.on("error", () => {})); change
this to mirror the main request's debug logging by capturing the error object
and logging it with the same logger used for the main request (e.g.,
processLogger.debug or logger.debug) when debug is enabled; update the
aliasReq.on("error", ...) handler to log a descriptive message including the
error (e.g., "Alias request error") rather than silently ignoring it so
aliasReq's error handling matches the main request's behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/cli/cmd/i18n.ts`:
- Around line 156-159: The code constructs a UserIdentity using auth.id without
validating it; update the auth handling in the block after
validateAuth(settings) (the auth variable used to populate userIdentity) to
explicitly check that auth.id is present and non-empty before assigning to
userIdentity (same safeguard as in status.ts). If auth.id is missing, handle it
deterministically — e.g., log a clear error / fail fast or attempt to resolve
the identifier (re-run authenticator.whoami() or prompt re-authentication) —
rather than creating a UserIdentity with id: undefined; ensure the check
references validateAuth and userIdentity so the fix is applied to this exact
assignment site.

In `@packages/cli/src/cli/cmd/status.ts`:
- Around line 77-80: The code constructs a UserIdentity from tryAuthenticate()
without verifying auth.id; update the block that calls tryAuthenticate so you
only set userIdentity = { email: auth.email, id: auth.id } when both auth.email
and auth.id are present and non-empty (match the safe pattern used in
run/_utils.ts like checking auth?.email and auth?.id), otherwise treat auth as
absent (leave userIdentity null or handle as unauthenticated) and adjust the ora
messaging accordingly; edit the code around tryAuthenticate, the auth variable,
and the userIdentity assignment to add this validation.

---

Outside diff comments:
In `@packages/cli/src/cli/cmd/status.ts`:
- Around line 359-376: The verbose branch in status.ts prints the "Missing:"
line twice; inside the flags.verbose block remove the duplicate console.log that
prints `Missing: ${missingKeys.length} keys, ~${wordsToTranslate} words` so only
a single line is logged when missingKeys.length > 0 (the remaining call should
use the existing variables missingKeys and wordsToTranslate and remain under the
same if (missingKeys.length > 0) check after bucketOra.succeed).

---

Nitpick comments:
In `@packages/cli/src/cli/utils/observability.ts`:
- Around line 141-142: The alias request currently swallows errors
(aliasReq.on("error", () => {})); change this to mirror the main request's debug
logging by capturing the error object and logging it with the same logger used
for the main request (e.g., processLogger.debug or logger.debug) when debug is
enabled; update the aliasReq.on("error", ...) handler to log a descriptive
message including the error (e.g., "Alias request error") rather than silently
ignoring it so aliasReq's error handling matches the main request's behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41bc0291-d17b-4f47-85ea-fd2a4995eb8a

📥 Commits

Reviewing files that changed from the base of the PR and between 336eeae and ba1b35a.

📒 Files selected for processing (10)
  • .changeset/lazy-beds-lay.md
  • packages/cli/src/cli/cmd/i18n.ts
  • packages/cli/src/cli/cmd/run/_utils.ts
  • packages/cli/src/cli/cmd/run/index.ts
  • packages/cli/src/cli/cmd/status.ts
  • packages/cli/src/cli/localizer/_types.ts
  • packages/cli/src/cli/localizer/lingodotdev.ts
  • packages/cli/src/cli/utils/observability.ts
  • packages/sdk/src/utils/observability.spec.ts
  • packages/sdk/src/utils/observability.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/sdk/src/utils/observability.ts`:
- Around line 57-77: Wrap the posthog calls in a try/finally so that
posthog.shutdown() is always executed: move the await posthog.shutdown() into a
finally block, keep await posthog.capture(...) and the conditional await
posthog.alias(...) inside the try (alias only if email), and after shutdown
rethrow any caught error (or let it propagate) so behavior and error semantics
remain unchanged; reference posthog.capture, posthog.alias, and posthog.shutdown
to locate the changes in observability.ts.
- Around line 60-63: The properties object in observability.ts currently
overwrites any incoming properties.$set by assigning $set: email ? { email } :
{}, which can drop caller-provided fields; instead, preserve and merge caller
$set with the email if present: compute $set as the spread merge of existing
properties.$set and the email ({ ...properties.$set, ...(email ? { email } : {})
}) so that provided fields are retained, and leave properties.$set untouched
when email is absent; keep the tracking_version: TRACKING_VERSION assignment
as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4e113a0-910e-4bef-8d0b-1e74a73170f2

📥 Commits

Reviewing files that changed from the base of the PR and between ba1b35a and f547cc9.

📒 Files selected for processing (1)
  • packages/sdk/src/utils/observability.ts

@AndreyHirsa AndreyHirsa merged commit 10e6364 into main Mar 31, 2026
9 checks passed
@AndreyHirsa AndreyHirsa deleted the feat/posthog-distinct-id-migration branch March 31, 2026 14:01
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