Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.uid2</groupId>
<artifactId>uid2-operator</artifactId>
<version>5.70.6</version>
<version>5.70.7-alpha-323-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()))
Expand Down
61 changes: 61 additions & 0 deletions src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading