diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 69f3b953d64..961d05b36f9 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -130,6 +130,19 @@ export const injectIdentity = fp( async (server: FastifyZodProvider, opt: { shouldForwardWritesToPrimaryInstance?: boolean }) => { server.decorateRequest("auth", null); server.decorateRequest("shouldForwardWritesToPrimaryInstance", Boolean(opt.shouldForwardWritesToPrimaryInstance)); + + // Hoisted outside onRequest hook to avoid per-request function allocation on this hot path + const fireIdentifyForUser = (user: TUsers) => { + const distinctId = user.username ?? user.email ?? ""; + if (distinctId) { + void server.services.telemetry.identifyUser(distinctId, { + email: user.email ?? undefined, + username: user.username, + userId: user.id + }); + } + }; + server.addHook("onRequest", async (req) => { const appCfg = getConfig(); @@ -184,6 +197,7 @@ export const injectIdentity = fp( isMfaVerified: token.isMfaVerified, token }; + fireIdentifyForUser(user); break; } case AuthMode.MCP_JWT: { @@ -205,6 +219,7 @@ export const injectIdentity = fp( isMfaVerified: token.isMfaVerified, token }; + fireIdentifyForUser(user); break; } case AuthMode.IDENTITY_ACCESS_TOKEN: { diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index fe9274b7f3c..59b706f0947 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -643,14 +643,18 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { const adminDistinctId = user.user.username ?? user.user.email ?? ""; if (adminDistinctId) { - void server.services.telemetry.identifyUser(adminDistinctId, { - email: user.user.email ?? undefined, - username: user.user.username, - userId: user.user.id, - firstName: user.user.firstName ?? undefined, - lastName: user.user.lastName ?? undefined, - superAdmin: true - }); + void server.services.telemetry.identifyUser( + adminDistinctId, + { + email: user.user.email ?? undefined, + username: user.user.username, + userId: user.user.id, + firstName: user.user.firstName ?? undefined, + lastName: user.user.lastName ?? undefined, + superAdmin: true + }, + { skipDedup: true } + ); } void res.setCookie("jid", token.refresh, { @@ -804,14 +808,18 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { const bootstrapDistinctId = user.user.username ?? user.user.email ?? ""; if (bootstrapDistinctId) { - void server.services.telemetry.identifyUser(bootstrapDistinctId, { - email: user.user.email ?? undefined, - username: user.user.username, - userId: user.user.id, - firstName: user.user.firstName ?? undefined, - lastName: user.user.lastName ?? undefined, - superAdmin: true - }); + void server.services.telemetry.identifyUser( + bootstrapDistinctId, + { + email: user.user.email ?? undefined, + username: user.user.username, + userId: user.user.id, + firstName: user.user.firstName ?? undefined, + lastName: user.user.lastName ?? undefined, + superAdmin: true + }, + { skipDedup: true } + ); } return { diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index e9ffbad269b..bd5c92080d7 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -79,13 +79,17 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { const googleDistinctId = user.username ?? user.email ?? ""; if (googleDistinctId) { - void server.services.telemetry.identifyUser(googleDistinctId, { - email: user.email ?? undefined, - username: user.username, - userId: user.id, - firstName: user.firstName ?? undefined, - lastName: user.lastName ?? undefined - }); + void server.services.telemetry.identifyUser( + googleDistinctId, + { + email: user.email ?? undefined, + username: user.username, + userId: user.id, + firstName: user.firstName ?? undefined, + lastName: user.lastName ?? undefined + }, + { skipDedup: true } + ); } if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { @@ -162,13 +166,17 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { const githubDistinctId = user.username ?? user.email ?? ""; if (githubDistinctId) { - void server.services.telemetry.identifyUser(githubDistinctId, { - email: user.email ?? undefined, - username: user.username, - userId: user.id, - firstName: user.firstName ?? undefined, - lastName: user.lastName ?? undefined - }); + void server.services.telemetry.identifyUser( + githubDistinctId, + { + email: user.email ?? undefined, + username: user.username, + userId: user.id, + firstName: user.firstName ?? undefined, + lastName: user.lastName ?? undefined + }, + { skipDedup: true } + ); } if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { @@ -237,13 +245,17 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { const gitlabDistinctId = user.username ?? user.email ?? ""; if (gitlabDistinctId) { - void server.services.telemetry.identifyUser(gitlabDistinctId, { - email: user.email ?? undefined, - username: user.username, - userId: user.id, - firstName: user.firstName ?? undefined, - lastName: user.lastName ?? undefined - }); + void server.services.telemetry.identifyUser( + gitlabDistinctId, + { + email: user.email ?? undefined, + username: user.username, + userId: user.id, + firstName: user.firstName ?? undefined, + lastName: user.lastName ?? undefined + }, + { skipDedup: true } + ); } if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { @@ -593,11 +605,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { const tokenExchangeDistinctId = data.user.username ?? data.user.email ?? ""; if (tokenExchangeDistinctId) { - void server.services.telemetry.identifyUser(tokenExchangeDistinctId, { - email: data.user.email ?? undefined, - username: data.user.username, - userId: data.user.userId - }); + void server.services.telemetry.identifyUser( + tokenExchangeDistinctId, + { + email: data.user.email ?? undefined, + username: data.user.username, + userId: data.user.userId + }, + { skipDedup: true } + ); } if ([AuthMethod.GOOGLE, AuthMethod.GITHUB, AuthMethod.GITLAB].includes(data.decodedProviderToken.authMethod)) { diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index aea8883a697..05cc17352a3 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -157,11 +157,15 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { const login2DistinctId = data.user.username ?? data.user.email ?? ""; if (login2DistinctId) { - void server.services.telemetry.identifyUser(login2DistinctId, { - email: data.user.email ?? undefined, - username: data.user.username, - userId: data.user.userId - }); + void server.services.telemetry.identifyUser( + login2DistinctId, + { + email: data.user.email ?? undefined, + username: data.user.username, + userId: data.user.userId + }, + { skipDedup: true } + ); } void res.setCookie("jid", data.token.refresh, { @@ -232,11 +236,15 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { const loginDistinctId = user.username ?? user.email ?? ""; if (loginDistinctId) { - void server.services.telemetry.identifyUser(loginDistinctId, { - email: user.email ?? undefined, - username: user.username, - userId: user.userId - }); + void server.services.telemetry.identifyUser( + loginDistinctId, + { + email: user.email ?? undefined, + username: user.username, + userId: user.userId + }, + { skipDedup: true } + ); } void res.setCookie("jid", tokens.refreshToken, { diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 1793b5e2257..1b6ad4b4a18 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -160,16 +160,20 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { const signupDistinctId = user.username ?? user.email ?? ""; if (signupDistinctId) { - void server.services.telemetry.identifyUser(signupDistinctId, { - email: user.email ?? undefined, - username: user.username, - userId: user.id, - firstName: user.firstName ?? undefined, - lastName: user.lastName ?? undefined, - isEmailVerified: user.isEmailVerified ?? undefined, - isMfaEnabled: user.isMfaEnabled ?? undefined, - superAdmin: user.superAdmin ?? undefined - }); + void server.services.telemetry.identifyUser( + signupDistinctId, + { + email: user.email ?? undefined, + username: user.username, + userId: user.id, + firstName: user.firstName ?? undefined, + lastName: user.lastName ?? undefined, + isEmailVerified: user.isEmailVerified ?? undefined, + isMfaEnabled: user.isMfaEnabled ?? undefined, + superAdmin: user.superAdmin ?? undefined + }, + { skipDedup: true } + ); } void res.setCookie("jid", refreshToken, { @@ -236,16 +240,20 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { const inviteDistinctId = user.username ?? user.email ?? ""; if (inviteDistinctId) { - void server.services.telemetry.identifyUser(inviteDistinctId, { - email: user.email ?? undefined, - username: user.username, - userId: user.id, - firstName: user.firstName ?? undefined, - lastName: user.lastName ?? undefined, - isEmailVerified: user.isEmailVerified ?? undefined, - isMfaEnabled: user.isMfaEnabled ?? undefined, - superAdmin: user.superAdmin ?? undefined - }); + void server.services.telemetry.identifyUser( + inviteDistinctId, + { + email: user.email ?? undefined, + username: user.username, + userId: user.id, + firstName: user.firstName ?? undefined, + lastName: user.lastName ?? undefined, + isEmailVerified: user.isEmailVerified ?? undefined, + isMfaEnabled: user.isMfaEnabled ?? undefined, + superAdmin: user.superAdmin ?? undefined + }, + { skipDedup: true } + ); } void res.setCookie("jid", refreshToken, { diff --git a/backend/src/services/telemetry/telemetry-service.ts b/backend/src/services/telemetry/telemetry-service.ts index 8a681e3d43a..644e2d863e2 100644 --- a/backend/src/services/telemetry/telemetry-service.ts +++ b/backend/src/services/telemetry/telemetry-service.ts @@ -371,7 +371,13 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme } }; - const identifyUser = ( + const TELEMETRY_IDENTIFY_CACHE_KEY_PREFIX = "telemetry-identify"; + const TELEMETRY_IDENTIFY_CACHE_TTL = 600; // 10 minutes + + // In-memory fallback dedup set to limit blast radius during Redis outages + const inMemoryIdentifyDedup = new Set(); + + const identifyUser = async ( distinctId: string, properties: { email?: string; @@ -382,12 +388,35 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme isMfaEnabled?: boolean; isEmailVerified?: boolean; superAdmin?: boolean; - } + }, + { skipDedup }: { skipDedup?: boolean } = {} ) => { if (postHog && distinctId) { const instanceType = licenseService.getInstanceType(); if (instanceType === InstanceType.Cloud) { - postHog.identify({ distinctId, properties }); + if (!skipDedup) { + try { + const cacheKey = `${TELEMETRY_IDENTIFY_CACHE_KEY_PREFIX}:${distinctId}`; + // Atomic SET NX + EX: only the first caller within the TTL window proceeds + const wasSet = await keyStore.setItemWithExpiryNX(cacheKey, TELEMETRY_IDENTIFY_CACHE_TTL, "1"); + if (!wasSet) return; + } catch (error) { + logger.error(error, `Failed to check PostHog identify dedup cache for distinctId=${distinctId}`); + // In-memory fallback to limit blast radius during Redis outage + if (inMemoryIdentifyDedup.has(distinctId)) return; + inMemoryIdentifyDedup.add(distinctId); + const timer = setTimeout( + () => inMemoryIdentifyDedup.delete(distinctId), + TELEMETRY_IDENTIFY_CACHE_TTL * 1000 + ); + timer.unref(); + } + } + try { + postHog.identify({ distinctId, properties }); + } catch (err) { + logger.error(err, `Failed to call postHog.identify for distinctId=${distinctId}`); + } } } };