Skip to content

feat: add PostHog identifyUser in auth hook with Redis dedup cache#5643

Merged
0xArshdeep merged 10 commits intomainfrom
devin/1773102788-posthog-identify-on-auth
Mar 17, 2026
Merged

feat: add PostHog identifyUser in auth hook with Redis dedup cache#5643
0xArshdeep merged 10 commits intomainfrom
devin/1773102788-posthog-identify-on-auth

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Mar 10, 2026

Context

PostHog identifyUser was previously only called during login, signup, and OAuth callback routes. Users accessing Infisical via CLI, SDK, or API would not get their PostHog person properties enriched if those one-time calls were missed or if the person record was created with a different distinctId.

This PR adds identifyUser calls in the inject-identity.ts authentication hook for JWT and MCP_JWT auth modes. This ensures that every authenticated request — regardless of client (CLI, SDK, API, UI) — enriches the PostHog person with basic properties (email, username, userId).

To avoid excessive PostHog API calls, a Redis-backed dedup cache (10-minute TTL) is used via an atomic SET NX EX operation (setItemWithExpiryNX). If a user was already identified within the last 10 minutes, the call is skipped. All existing login/signup/OAuth identifyUser call sites are updated to use skipDedup: true so they always fire (they carry richer properties like firstName, lastName, superAdmin).

Note: This is the first of two PRs. A separate PR in the CLI repo (Infisical/cli#146) addresses the anonymous distinctId issue where the CLI generates random IDs instead of using the user's email/username.

Updates since last revision

Addressed all code review feedback across four rounds of Greptile review and Devin Review:

  1. TOCTOU race fix: Replaced non-atomic getItem + setItemWithExpiry with a single atomic setItemWithExpiryNX (Redis SET key value EX ttl NX). Only the first caller within the TTL window proceeds; all others get null back and skip the identify call.
  2. Redis outage blast radius: Added an in-memory Set<string> fallback in the catch block. If Redis is unreachable, the in-memory set absorbs the burst (entries auto-expire via setTimeout matching the cache TTL) instead of flooding PostHog with identify calls on every request.
  3. Code duplication: Extracted a fireIdentifyForUser(user) helper in inject-identity.ts, called in both JWT and MCP_JWT cases.
  4. Keystore additions: Added setItemWithExpiryNX to TKeyStoreFactory type, implementation, mock (e2e-test/mocks/keystore.ts), and in-memory store (keystore/memory.ts).
  5. Hot-path optimization: Hoisted fireIdentifyForUser to plugin scope (outside onRequest hook, inside injectIdentity) so the function is created once per plugin registration instead of per-request.
  6. Defensive try/catch on postHog.identify(): Wrapped the postHog.identify() call in a try/catch to prevent unhandled promise rejections from propagating when call sites use void.
  7. Error log context: Included distinctId in error log messages for both the dedup cache and postHog.identify() failures, improving production debuggability.
  8. Graceful shutdown fix: Added .unref() to the fallback setTimeout in identifyUser, consistent with the existing identifyIdentity pattern. Without .unref(), active timers during Redis outages would keep the Node.js event loop alive and prevent graceful shutdown for up to 600 seconds.

Steps to verify the change

  1. Deploy to a Cloud instance
  2. Log in via CLI and make an authenticated API request (e.g. infisical secrets)
  3. Check PostHog — the person record for that user should have email, username, and userId properties set
  4. Make additional requests within 10 minutes — verify no duplicate $identify events are sent (dedup cache working)
  5. Verify login/signup/OAuth flows still fire identifyUser on every occurrence (skipDedup)

Important review notes

  • identifyUser is now async but all call sites use void (fire-and-forget) — this is intentional for telemetry
  • The auth hook calls pass only email, username, userId — the richer property set (firstName, lastName, superAdmin, etc.) is still set by login/signup routes via skipDedup: true
  • The in-memory fallback Set is per-process (not shared across instances). This is intentional — it's a last-resort rate limiter, not a primary dedup mechanism
  • setTimeout entries accumulate one per distinct user during a Redis outage; review whether this is acceptable under expected concurrent user counts
  • distinctId is derived from user.username ?? user.email. In the unlikely edge case where these values collide across accounts, dedup could suppress identification for up to 10 minutes (telemetry-accuracy impact only, not a security concern)

Human review checklist

  • Verify setItemWithExpiryNX return type ("OK" | null) is handled correctly — null means key already existed (skip identify)
  • Check that setTimeout accumulation in inMemoryIdentifyDedup won't cause issues during extended Redis outages with many distinct users
  • Confirm identifyUser becoming async doesn't break anything (all call sites use void so should be safe)
  • Verify the fireIdentifyForUser helper correctly extracts the shared logic without altering behavior
  • Confirm the try/catch around postHog.identify() is appropriate and won't mask legitimate errors
  • Review that including distinctId (email/username) in error logs doesn't introduce PII logging concerns in your environment
  • Verify that timer.unref() is called on both identifyUser and identifyIdentity fallback timers for graceful shutdown consistency

Type

  • Fix
  • Feature
  • Improvement
  • Breaking
  • Docs
  • Chore

Checklist


Link to Devin Session: https://app.devin.ai/sessions/6cf49960d59a4f7b974a7fe4861d6206
Requested by: @0xArshdeep


Open with Devin

- Add identifyUser calls in inject-identity.ts for JWT and MCP_JWT auth modes
- Implement Redis-backed dedup cache (10-min TTL) to prevent excessive PostHog API calls
- Add skipDedup option to identifyUser for login/signup routes that should always fire
- Update all existing identifyUser call sites to use skipDedup: true

Co-Authored-By: arsh <[email protected]>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@maidul98
Copy link
Copy Markdown
Collaborator

maidul98 commented Mar 10, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR enriches PostHog person profiles for users who authenticate via CLI, SDK, or API by adding identifyUser calls to the inject-identity.ts onRequest hook for JWT and MCP_JWT auth modes. A Redis-backed atomic SET NX EX dedup cache (10-minute TTL) prevents excessive PostHog calls, with an in-memory Set fallback for Redis outage resilience. All existing login/signup/OAuth call sites are updated to pass { skipDedup: true } so they always fire with their richer property sets.

  • The core implementation is well-structured: the fireIdentifyForUser helper is correctly hoisted to plugin scope, the atomic NX operation is race-free, and the in-memory fallback with .unref() is consistent with the existing identifyIdentity pattern.
  • Two minor style inconsistencies in telemetry-service.ts: the new error log messages use for distinctId=… instead of the project's bracket convention [username=…], and the cache key prefix/TTL are defined as local constants rather than being registered in the shared KeyStorePrefixes/KeyStoreTtls objects the way identifyIdentity does.
  • The identifyUser function becoming async is safe since all call sites already use void (fire-and-forget).

Confidence Score: 4/5

  • Safe to merge; only telemetry enrichment is affected and all changes are fire-and-forget with defensive error handling.
  • The functional logic is sound — atomic Redis NX dedup, in-memory fallback with TTL cleanup, try/catch on both the cache check and PostHog call, and skipDedup: true on all existing identify sites. The two flagged items are both style-level issues (log message bracket format and centralising cache constants), neither of which affects correctness or security.
  • backend/src/services/telemetry/telemetry-service.ts — minor log format and constant-centralisation style issues.

Important Files Changed

Filename Overview
backend/src/services/telemetry/telemetry-service.ts Adds async identifyUser with atomic Redis NX dedup cache and in-memory fallback. Logic is correct; two minor style issues: log messages use for distinctId=… format instead of the project's [username=…] bracket convention, and cache constants are defined locally instead of in the shared KeyStorePrefixes/KeyStoreTtls objects like the adjacent identifyIdentity does.
backend/src/server/plugins/auth/inject-identity.ts Adds fireIdentifyForUser helper hoisted to plugin scope (avoiding per-request allocation), called in both JWT and MCP_JWT cases. Clean, no logic issues.
backend/src/server/routes/v1/admin-router.ts Mechanical change: adds { skipDedup: true } to two identifyUser call sites so admin login/bootstrap always fire identify events regardless of the new dedup cache. No issues.
backend/src/server/routes/v1/sso-router.ts Mechanical change: adds { skipDedup: true } to three OAuth callback identifyUser calls (Google, GitHub, GitLab). No issues.
backend/src/server/routes/v3/login-router.ts Mechanical change: adds { skipDedup: true } to two login identifyUser calls. No issues.
backend/src/server/routes/v3/signup-router.ts Mechanical change: adds { skipDedup: true } to two signup identifyUser calls. No issues.

Last reviewed commit: 69219e1

greptile-apps[bot]

This comment was marked as resolved.

…k, extract helper

- Replace getItem+setItemWithExpiry with atomic setItemWithExpiryNX (TOCTOU fix)
- Add in-memory dedup Set fallback to limit blast radius during Redis outages
- Extract fireIdentifyForUser helper to deduplicate JWT/MCP_JWT identify logic
- Add setItemWithExpiryNX to keystore interface, memory store, and mock

Co-Authored-By: arsh <[email protected]>
@0xArshdeep
Copy link
Copy Markdown
Contributor

@greptileai

greptile-apps[bot]

This comment was marked as resolved.

Addresses Greptile review: moves helper to plugin scope so it's allocated
once per plugin registration instead of once per authenticated request.

Co-Authored-By: arsh <[email protected]>
@0xArshdeep
Copy link
Copy Markdown
Contributor

@greptileai

greptile-apps[bot]

This comment was marked as resolved.

@0xArshdeep
Copy link
Copy Markdown
Contributor

@greptileai

greptile-apps[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@0xArshdeep
Copy link
Copy Markdown
Contributor

@greptileai

@0xArshdeep 0xArshdeep merged commit b9ade39 into main Mar 17, 2026
12 of 15 checks passed
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.

3 participants