diff --git a/pom.xml b/pom.xml index 4799af7a8..a2b7ea849 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.70.6 + 5.70.7-alpha-323-SNAPSHOT UTF-8 diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 2481b1019..051d50aff 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -910,6 +910,7 @@ private void handleTokenValidateV2(RoutingContext rc) { recordTokenValidateStats(participantSiteId, "invalid_input"); return; } + recordRawEmailMetrics(input, rc.request().path()); final Instant now = Instant.now(); final String token = req.getString("token"); @@ -952,6 +953,7 @@ private void handleTokenGenerateV2(RoutingContext rc) { final InputUtil.InputVal input = this.getTokenInputV2(req); if (isTokenInputValid(input, rc)) { + recordRawEmailMetrics(input, rc.request().path()); final String apiContact = getApiContact(rc); switch (validateUserConsent(req, apiContact)) { @@ -1008,6 +1010,7 @@ private Future handleLogoutAsyncV2(RoutingContext rc) { final InputUtil.InputVal input = getTokenInputV2(req); final String uidTraceId = rc.request().getHeader(Audit.UID_TRACE_ID_HEADER); if (input != null && input.isValid()) { + recordRawEmailMetrics(input, rc.request().path()); final Instant now = Instant.now(); Promise promise = Promise.promise(); @@ -1232,6 +1235,12 @@ private void handleIdentityMapV2(RoutingContext rc) { if (!validateServiceLink(rc)) { return; } + if (v2Input.diiType().equals("email")) { + for (InputUtil.InputVal input : v2Input.inputList()) { + recordRawEmailMetrics(input, rc.request().path()); + } + } + final JsonObject resp = processIdentityMapV2Response(rc, v2Input); ResponseUtil.SuccessV2(rc, resp); } catch (Exception e) { @@ -1295,6 +1304,13 @@ private void handleIdentityMapV3(RoutingContext rc) { if (!validateServiceLink(rc)) { return; } + InputUtil.InputVal[] emailInputs = normalizedInput.get("email"); + if (emailInputs != null) { + for (InputUtil.InputVal emailInput : emailInputs) { + recordRawEmailMetrics(emailInput, rc.request().path()); + } + } + final JsonObject response = processIdentityMapV3Response(rc, normalizedInput); ResponseUtil.SuccessV2(rc, response); } catch (ClassCastException | JsonProcessingException processingException) { @@ -1507,6 +1523,52 @@ public TokenVersion getRefreshTokenVersion(String s) { return null; } + private void recordRawEmailMetrics(InputUtil.InputVal input, String path) { + if (!input.isValid() || input.getInputType() != InputUtil.IdentityInputType.Raw + || input.getIdentityType() != IdentityType.Email) { + return; + } + String provided = input.getProvided(); + int atIndex = provided.indexOf('@'); + boolean isGmail = input.getNormalized().endsWith("@gmail.com"); + + boolean hasDot = atIndex > 0 && provided.lastIndexOf('.', atIndex) >= 0; + Counter.builder("uid2_operator_raw_email_dot_total") + .description("Count of valid raw emails processed, by presence of dot before @") + .tag("path", path) + .tag("has_dot", String.valueOf(hasDot)) + .tag("is_gmail", String.valueOf(isGmail)) + .register(Metrics.globalRegistry) + .increment(); + + int plusIndex = provided.indexOf('+'); + boolean hasPlus = plusIndex >= 0 && plusIndex < atIndex; + Counter.builder("uid2_operator_raw_email_plus_total") + .description("Count of valid raw emails processed, by presence of + before @") + .tag("path", path) + .tag("has_plus", String.valueOf(hasPlus)) + .tag("is_gmail", String.valueOf(isGmail)) + .register(Metrics.globalRegistry) + .increment(); + + boolean hasTrailingDot = provided.charAt(provided.length() - 1) == '.'; + Counter.builder("uid2_operator_raw_email_trailing_dot_total") + .description("Count of valid raw emails processed, by presence of trailing dot at end of address") + .tag("path", path) + .tag("has_trailing_dot", String.valueOf(hasTrailingDot)) + .register(Metrics.globalRegistry) + .increment(); + + boolean hasWhitespace = provided.strip().chars().anyMatch(Character::isWhitespace); + Counter.builder("uid2_operator_raw_email_whitespace_total") + .description("Count of valid raw emails processed, by presence of internal whitespace and gmail domain") + .tag("path", path) + .tag("has_whitespace", String.valueOf(hasWhitespace)) + .tag("is_gmail", String.valueOf(isGmail)) + .register(Metrics.globalRegistry) + .increment(); + } + private void recordRefreshTokenVersionCount(String siteId, TokenVersion tokenVersion) { Counter.builder("uid2_refresh_token_received_count_total") .description(String.format("Counter for the amount of refresh token %s received", tokenVersion.toString().toLowerCase())) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 7159adc9d..c7d906a99 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -5253,6 +5253,67 @@ void asyncBatchRequestEnabledLogsCorrectMessage(Vertx vertx, VertxTestContext te testContext.completeNow(); } + @Test + void rawEmailMetricsRecorded(Vertx vertx, VertxTestContext testContext) { + fakeAuth(201, Role.GENERATOR, Role.MAPPER); + setupSalts(); + setupKeys(); + + // /v2/token/generate — one email per characteristic being tested + String emailHash = TokenUtils.getIdentityHashString("hash@example.com"); + sendTokenGenerate(vertx, new JsonObject().put("email", "john.doe@example.com"), 200, json -> {}); // dot, no plus, non-gmail + sendTokenGenerate(vertx, new JsonObject().put("email", "johndoe@example.com"), 200, json -> {}); // no dot, non-gmail + sendTokenGenerate(vertx, new JsonObject().put("email", "john.doe@gmail.com"), 200, json -> {}); // dot, gmail + sendTokenGenerate(vertx, new JsonObject().put("email", "john+tag@example.com"), 200, json -> {}); // plus, non-gmail + sendTokenGenerate(vertx, new JsonObject().put("email", "john+tag@gmail.com"), 200, json -> {}); // plus, gmail + sendTokenGenerate(vertx, new JsonObject().put("email", "john@example.com."), 200, json -> {}); // trailing dot + sendTokenGenerate(vertx, new JsonObject().put("email_hash", emailHash), 200, json -> { // hash - should not record + + // dot metric + assertEquals(1, Metrics.globalRegistry + .get("uid2_operator_raw_email_dot_total") + .tag("path", "/v2/token/generate").tag("has_dot", "true").tag("is_gmail", "false") + .counter().count()); + assertEquals(1, Metrics.globalRegistry + .get("uid2_operator_raw_email_dot_total") + .tag("path", "/v2/token/generate").tag("has_dot", "true").tag("is_gmail", "true") + .counter().count()); + // has_dot=false, is_gmail=false count is 3 (johndoe, john+tag@example.com, john@example.com.) — hash did not increment + assertEquals(3, Metrics.globalRegistry + .get("uid2_operator_raw_email_dot_total") + .tag("path", "/v2/token/generate").tag("has_dot", "false").tag("is_gmail", "false") + .counter().count()); + + // plus metric + assertEquals(1, Metrics.globalRegistry + .get("uid2_operator_raw_email_plus_total") + .tag("path", "/v2/token/generate").tag("has_plus", "true").tag("is_gmail", "false") + .counter().count()); + assertEquals(1, Metrics.globalRegistry + .get("uid2_operator_raw_email_plus_total") + .tag("path", "/v2/token/generate").tag("has_plus", "true").tag("is_gmail", "true") + .counter().count()); + + // trailing dot metric + assertEquals(1, Metrics.globalRegistry + .get("uid2_operator_raw_email_trailing_dot_total") + .tag("path", "/v2/token/generate").tag("has_trailing_dot", "true") + .counter().count()); + + // /v2/identity/map — batch with two emails to verify counter reaches 2 + JsonObject mapReq = new JsonObject().put("email", new JsonArray() + .add("a.b@example.com") + .add("c.d@example.com")); + send(vertx, "v2/identity/map", mapReq, 200, mapJson -> { + assertEquals(2, Metrics.globalRegistry + .get("uid2_operator_raw_email_dot_total") + .tag("path", "/v2/identity/map").tag("has_dot", "true").tag("is_gmail", "false") + .counter().count()); + testContext.completeNow(); + }); + }); + } + @Test void asyncBatchRequestDisabledLogsCorrectMessage(Vertx vertx, VertxTestContext testContext) { // Verify that when enable_async_batch_request is false, the correct log message is emitted