From efc39573e4347e4753fcc1cb0fbcaad7785178ee Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Tue, 31 Mar 2026 12:41:42 -0400 Subject: [PATCH] Use `CaptchaMetrics` to measure captcha scores for sending messages and verification --- .../captcha/AssessmentResult.java | 3 +++ .../controllers/VerificationController.java | 6 +++++ .../limits/RateLimitChallengeManager.java | 22 +++++++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java b/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java index 4a3fd9334..de0a26d41 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java @@ -93,6 +93,9 @@ public class AssessmentResult { return this.actualScore; } + public int getNormalizedIntScore() { + return normalizedIntScore(this.actualScore); + } /** * Map a captcha score in [0.0, 1.0] to a low cardinality discrete space in [0, 100] suitable for use in metrics diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java index d1ad672ff..9109003e4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java @@ -77,6 +77,7 @@ import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; +import org.whispersystems.textsecuregcm.metrics.CaptchaMetrics; import org.whispersystems.textsecuregcm.metrics.DevicePlatformUtil; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.push.PushNotification; @@ -492,6 +493,11 @@ public class VerificationController { Tag.of(SCORE_TAG_NAME, assessmentResult.getScoreString()))) .increment(); + CaptchaMetrics.measureCaptchaOutcome(assessmentResult.getNormalizedIntScore(), + assessmentResult.isValid(captchaScoreThreshold), + Util.getRegion(registrationServiceSession.number()), + "verification"); + } catch (final IOException e) { logger.error("error assessing captcha during registration verification", e); throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE, e); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java index b7e41d59a..e66917e5f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java @@ -14,8 +14,10 @@ import java.io.IOException; import java.util.List; import java.util.Optional; import org.whispersystems.textsecuregcm.captcha.Action; +import org.whispersystems.textsecuregcm.captcha.AssessmentResult; import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.metrics.CaptchaMetrics; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.spam.ChallengeType; @@ -61,13 +63,18 @@ public class RateLimitChallengeManager { } } - public boolean answerCaptchaChallenge(final Account account, final String captcha, final String mostRecentProxyIp, - final String userAgent, final Optional scoreThreshold) - throws RateLimitExceededException, IOException { + public boolean answerCaptchaChallenge(final Account account, + final String captcha, + final String mostRecentProxyIp, + final String userAgent, + final Optional scoreThreshold) throws RateLimitExceededException, IOException { rateLimiters.getCaptchaChallengeAttemptLimiter().validate(account.getUuid()); - final boolean challengeSuccess = captchaChecker.verify(Optional.of(account.getUuid()), Action.CHALLENGE, captcha, mostRecentProxyIp, userAgent).isValid(scoreThreshold); + final AssessmentResult assessmentResult = + captchaChecker.verify(Optional.of(account.getUuid()), Action.CHALLENGE, captcha, mostRecentProxyIp, userAgent); + + final boolean challengeSuccess = assessmentResult.isValid(scoreThreshold); final Tags tags = Tags.of( Tag.of(SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())), @@ -77,6 +84,13 @@ public class RateLimitChallengeManager { Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, tags).increment(); + CaptchaMetrics.measureCaptchaOutcome(assessmentResult.getNormalizedIntScore(), + challengeSuccess, + Util.getRegion(account.getNumber()), + // Note: currently all challenges are for message-sending, but if we add more use cases, we'll need to make the + // accept a context from callers rather than hard-coding it here + "sendMessage"); + if (challengeSuccess) { rateLimiters.getCaptchaChallengeSuccessLimiter().validate(account.getUuid()); resetRateLimits(account, ChallengeType.CAPTCHA);