Use CaptchaMetrics to measure captcha scores for sending messages and verification

This commit is contained in:
Jon Chambers
2026-03-31 12:41:42 -04:00
committed by Jon Chambers
parent 87e88dd3a1
commit efc39573e4
3 changed files with 27 additions and 4 deletions
@@ -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
@@ -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);
@@ -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<Float> scoreThreshold)
throws RateLimitExceededException, IOException {
public boolean answerCaptchaChallenge(final Account account,
final String captcha,
final String mostRecentProxyIp,
final String userAgent,
final Optional<Float> 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);