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