From 076a04d716bf71e4edd2d939e8717c9de6481e24 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 25 Mar 2026 11:15:31 +1100 Subject: [PATCH 1/4] add uid2_operator_raw_email_dot_total and record in all paths with raw emails --- .../operator/vertx/UIDOperatorVerticle.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 2481b1019..a89fe382e 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; } + recordRawEmailDotMetric(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)) { + recordRawEmailDotMetric(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()) { + recordRawEmailDotMetric(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()) { + recordRawEmailDotMetric(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) { + recordRawEmailDotMetric(emailInput, rc.request().path()); + } + } + final JsonObject response = processIdentityMapV3Response(rc, normalizedInput); ResponseUtil.SuccessV2(rc, response); } catch (ClassCastException | JsonProcessingException processingException) { @@ -1507,6 +1523,24 @@ public TokenVersion getRefreshTokenVersion(String s) { return null; } + private void recordRawEmailDotMetric(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 hasDot = atIndex > 0 && provided.lastIndexOf('.', atIndex) >= 0; + boolean isGmail = input.getNormalized().endsWith("@gmail.com"); + Counter.builder("uid2_operator_raw_email_dot_total") + .description("Count of valid raw emails processed, by presence of dot before @ and gmail domain") + .tag("path", path) + .tag("has_dot", String.valueOf(hasDot)) + .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())) From 80ec15daa0c0212b47e1f5f9629062c9f945cd99 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 25 Mar 2026 11:16:22 +1100 Subject: [PATCH 2/4] add test for metric --- .../operator/UIDOperatorVerticleTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 7159adc9d..650a0c188 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -5253,6 +5253,51 @@ void asyncBatchRequestEnabledLogsCorrectMessage(Vertx vertx, VertxTestContext te testContext.completeNow(); } + @Test + void rawEmailDotMetricRecorded(Vertx vertx, VertxTestContext testContext) { + fakeAuth(201, Role.GENERATOR, Role.MAPPER); + setupSalts(); + setupKeys(); + + // /v2/token/generate + String emailHash = TokenUtils.getIdentityHashString("hash@example.com"); + sendTokenGenerate(vertx, new JsonObject().put("email", "john.doe@example.com"), 200, json -> {}); // dot, 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_hash", emailHash), 200, json -> { // hash - should not record + + 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", "false").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 still 1 — hash request did not increment it + assertEquals(1, Metrics.globalRegistry + .get("uid2_operator_raw_email_dot_total") + .tag("path", "/v2/token/generate").tag("has_dot", "false").tag("is_gmail", "false") + .counter().count()); + + // /v2/identity/map — batch with two dot 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 From c4e3f7d05696983585ef4c39923f2c859ede59f2 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 25 Mar 2026 16:07:42 +1100 Subject: [PATCH 3/4] add more metrics --- .../operator/vertx/UIDOperatorVerticle.java | 44 +++++++++++++++---- .../operator/UIDOperatorVerticleTest.java | 42 ++++++++++++------ 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index a89fe382e..051d50aff 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -910,7 +910,7 @@ private void handleTokenValidateV2(RoutingContext rc) { recordTokenValidateStats(participantSiteId, "invalid_input"); return; } - recordRawEmailDotMetric(input, rc.request().path()); + recordRawEmailMetrics(input, rc.request().path()); final Instant now = Instant.now(); final String token = req.getString("token"); @@ -953,7 +953,7 @@ private void handleTokenGenerateV2(RoutingContext rc) { final InputUtil.InputVal input = this.getTokenInputV2(req); if (isTokenInputValid(input, rc)) { - recordRawEmailDotMetric(input, rc.request().path()); + recordRawEmailMetrics(input, rc.request().path()); final String apiContact = getApiContact(rc); switch (validateUserConsent(req, apiContact)) { @@ -1010,7 +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()) { - recordRawEmailDotMetric(input, rc.request().path()); + recordRawEmailMetrics(input, rc.request().path()); final Instant now = Instant.now(); Promise promise = Promise.promise(); @@ -1237,7 +1237,7 @@ private void handleIdentityMapV2(RoutingContext rc) { if (v2Input.diiType().equals("email")) { for (InputUtil.InputVal input : v2Input.inputList()) { - recordRawEmailDotMetric(input, rc.request().path()); + recordRawEmailMetrics(input, rc.request().path()); } } @@ -1307,7 +1307,7 @@ private void handleIdentityMapV3(RoutingContext rc) { InputUtil.InputVal[] emailInputs = normalizedInput.get("email"); if (emailInputs != null) { for (InputUtil.InputVal emailInput : emailInputs) { - recordRawEmailDotMetric(emailInput, rc.request().path()); + recordRawEmailMetrics(emailInput, rc.request().path()); } } @@ -1523,22 +1523,50 @@ public TokenVersion getRefreshTokenVersion(String s) { return null; } - private void recordRawEmailDotMetric(InputUtil.InputVal input, String path) { + 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 hasDot = atIndex > 0 && provided.lastIndexOf('.', atIndex) >= 0; 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 @ and gmail domain") + .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) { diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 650a0c188..c7d906a99 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -5254,37 +5254,53 @@ void asyncBatchRequestEnabledLogsCorrectMessage(Vertx vertx, VertxTestContext te } @Test - void rawEmailDotMetricRecorded(Vertx vertx, VertxTestContext testContext) { + void rawEmailMetricsRecorded(Vertx vertx, VertxTestContext testContext) { fakeAuth(201, Role.GENERATOR, Role.MAPPER); setupSalts(); setupKeys(); - // /v2/token/generate + // /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, 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_hash", emailHash), 200, json -> { // hash - should not record - + 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_dot_total") - .tag("path", "/v2/token/generate").tag("has_dot", "true").tag("is_gmail", "true") + .get("uid2_operator_raw_email_plus_total") + .tag("path", "/v2/token/generate").tag("has_plus", "true").tag("is_gmail", "false") .counter().count()); - // has_dot=false, is_gmail=false count is still 1 — hash request did not increment it assertEquals(1, Metrics.globalRegistry - .get("uid2_operator_raw_email_dot_total") - .tag("path", "/v2/token/generate").tag("has_dot", "false").tag("is_gmail", "false") + .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 dot emails to verify counter reaches 2 + // /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")); From f88110e41400a74d569519267d52e632c8d5bc0c Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Wed, 25 Mar 2026 05:13:54 +0000 Subject: [PATCH 4/4] [CI Pipeline] Released Snapshot version: 5.70.7-alpha-323-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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