From 447a22bb55f114dd1ddf6935cd78977582f076fa Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Mon, 2 Mar 2026 16:35:41 -0600 Subject: [PATCH] feat: add Sentry error tracking for user validation failures Add Sentry.captureMessage calls to track "User not found" errors that were previously invisible because they were returned as objects rather than thrown as exceptions. This adds error-level tracking to: - IDP-only validation failures (disableLoginWithEmail && disableLoginWithPhone) - Email-disabled validation failures - Phone-disabled validation failures - Multiple users found scenarios - searchUsers exhausted all queries Each capture includes detailed context (preferredLoginName, email, organizationId, login settings) to help diagnose intermittent authentication failures. Co-Authored-By: Claude Opus 4.5 --- apps/login/src/lib/server/loginname.ts | 55 ++++++++++++++++++++++++++ apps/login/src/lib/zitadel.ts | 45 +++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index c0c726820..a54468963 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -1,5 +1,6 @@ "use server"; +import * as Sentry from "@sentry/nextjs"; import { create } from "@zitadel/client"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; @@ -219,6 +220,22 @@ export async function sendLoginname(command: SendLoginnameCommand) { userLoginSettings?.disableLoginWithPhone ) { if (user.preferredLoginName !== concatLoginname) { + Sentry.captureMessage("User not found: IDP-only validation failed", { + level: "error", + tags: { + validation_type: "idp_only", + user_id: user.userId, + }, + extra: { + preferredLoginName: user.preferredLoginName, + concatLoginname, + commandLoginName: command.loginName, + email: humanUser?.email?.email, + organizationId: command.organization, + disableLoginWithEmail: userLoginSettings?.disableLoginWithEmail, + disableLoginWithPhone: userLoginSettings?.disableLoginWithPhone, + }, + }); return { error: "User not found in the system!" }; } } else if (userLoginSettings?.disableLoginWithEmail) { @@ -226,6 +243,25 @@ export async function sendLoginname(command: SendLoginnameCommand) { user.preferredLoginName !== concatLoginname || humanUser?.phone?.phone !== command.loginName ) { + Sentry.captureMessage( + "User not found: email-disabled validation failed", + { + level: "error", + tags: { + validation_type: "email_disabled", + user_id: user.userId, + }, + extra: { + preferredLoginName: user.preferredLoginName, + concatLoginname, + commandLoginName: command.loginName, + phone: humanUser?.phone?.phone, + organizationId: command.organization, + disableLoginWithEmail: userLoginSettings?.disableLoginWithEmail, + disableLoginWithPhone: userLoginSettings?.disableLoginWithPhone, + }, + }, + ); return { error: "User not found in the system!" }; } } else if (userLoginSettings?.disableLoginWithPhone) { @@ -233,6 +269,25 @@ export async function sendLoginname(command: SendLoginnameCommand) { user.preferredLoginName !== concatLoginname || humanUser?.email?.email !== command.loginName ) { + Sentry.captureMessage( + "User not found: phone-disabled validation failed", + { + level: "error", + tags: { + validation_type: "phone_disabled", + user_id: user.userId, + }, + extra: { + preferredLoginName: user.preferredLoginName, + concatLoginname, + commandLoginName: command.loginName, + email: humanUser?.email?.email, + organizationId: command.organization, + disableLoginWithEmail: userLoginSettings?.disableLoginWithEmail, + disableLoginWithPhone: userLoginSettings?.disableLoginWithPhone, + }, + }, + ); return { error: "User not found in the system!" }; } } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 727495cd2..45a0409c2 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -1,3 +1,4 @@ +import * as Sentry from "@sentry/nextjs"; import { Client, create, Duration } from "@zitadel/client"; import { createServerTransport as libCreateServerTransport } from "@zitadel/client/node"; import { makeReqCtx } from "@zitadel/client/v2"; @@ -856,6 +857,18 @@ export async function searchUsers({ } if (loginNameResult.result.length > 1) { + Sentry.captureMessage("Multiple users found: loginName search", { + level: "error", + tags: { + search_type: "multiple_users_loginname", + }, + extra: { + searchValue, + organizationId, + suffix, + resultCount: loginNameResult.result.length, + }, + }); return { error: "Multiple users found" }; } @@ -951,6 +964,21 @@ export async function searchUsers({ } if (emailOrPhoneResult.result.length > 1) { + Sentry.captureMessage("Multiple users found: email/phone search", { + level: "error", + tags: { + search_type: "multiple_users_email_phone", + }, + extra: { + searchValue, + organizationId, + suffix, + userId, + resultCount: emailOrPhoneResult.result.length, + disableLoginWithEmail: loginSettings?.disableLoginWithEmail, + disableLoginWithPhone: loginSettings?.disableLoginWithPhone, + }, + }); return { error: "Multiple users found." }; } @@ -958,6 +986,23 @@ export async function searchUsers({ return emailOrPhoneResult; } + Sentry.captureMessage("User not found: searchUsers exhausted all queries", { + level: "error", + tags: { + search_type: "user_search_failed", + }, + extra: { + searchValue, + organizationId, + suffix, + userId, + disableLoginWithEmail: loginSettings?.disableLoginWithEmail, + disableLoginWithPhone: loginSettings?.disableLoginWithPhone, + loginNameResultCount: 0, + emailOrPhoneResultCount: emailOrPhoneResult.result.length, + }, + }); + return { error: "User not found in the system" }; }