diff --git a/src/main/java/com/auth0/client/mgmt/ManagementApiBuilder.java b/src/main/java/com/auth0/client/mgmt/ManagementApiBuilder.java index 479fba5ab..042621417 100644 --- a/src/main/java/com/auth0/client/mgmt/ManagementApiBuilder.java +++ b/src/main/java/com/auth0/client/mgmt/ManagementApiBuilder.java @@ -4,6 +4,8 @@ package com.auth0.client.mgmt; import com.auth0.client.mgmt.core.*; +import com.auth0.net.Telemetry; +import com.auth0.utils.Asserts; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -27,6 +29,8 @@ public class ManagementApiBuilder { private String customDomain = null; + private Telemetry telemetry = null; + // Domain-based initialization fields private String domain = null; private String clientId = null; @@ -188,6 +192,24 @@ public ManagementApiBuilder addHeader(String name, String value) { return this; } + /** + * Configure the telemetry sent to Auth0 on every request. Use this when building an SDK on top of + * auth0-java so that requests are attributed to the wrapping library, with auth0-java reported as the + * underlying library in the telemetry environment. + * + *

If not set, telemetry defaults to identifying this library as {@code auth0-java}. + * + * @param name The name of the library sending the requests (e.g. the wrapping SDK's name) + * @param version The version of the library sending the requests + * @return This builder for method chaining + */ + public ManagementApiBuilder withTelemetry(String name, String version) { + Asserts.assertNotNull(name, "name"); + this.telemetry = + new Telemetry(name, version, Telemetry.class.getPackage().getImplementationVersion()); + return this; + } + protected ClientOptions buildClientOptions() { ClientOptions.Builder builder = ClientOptions.builder(); setEnvironment(builder); @@ -203,6 +225,9 @@ protected ClientOptions buildClientOptions() { builder.addHeader(CustomDomainInterceptor.HEADER_NAME, this.customDomain); builder.addInterceptor(new CustomDomainInterceptor()); } + if (this.telemetry != null) { + builder.telemetry(this.telemetry); + } setAdditional(builder); return builder.build(); } diff --git a/src/main/java/com/auth0/client/mgmt/core/ClientOptions.java b/src/main/java/com/auth0/client/mgmt/core/ClientOptions.java index 82886a4b4..a8d1357a3 100644 --- a/src/main/java/com/auth0/client/mgmt/core/ClientOptions.java +++ b/src/main/java/com/auth0/client/mgmt/core/ClientOptions.java @@ -27,26 +27,31 @@ public final class ClientOptions { private final int maxRetries; + private final Telemetry telemetry; + private ClientOptions( Environment environment, Map headers, Map> headerSuppliers, OkHttpClient httpClient, int timeout, - int maxRetries) { + int maxRetries, + Telemetry telemetry) { this.environment = environment; this.headers = new HashMap<>(); this.headers.putAll(headers); - Telemetry telemetry = - new Telemetry("auth0-java", Telemetry.class.getPackage().getImplementationVersion()); - if (telemetry.getValue() != null) { - this.headers.put("Auth0-Client", telemetry.getValue()); + Telemetry resolvedTelemetry = telemetry != null + ? telemetry + : new Telemetry("auth0-java", Telemetry.class.getPackage().getImplementationVersion()); + if (resolvedTelemetry.getValue() != null) { + this.headers.put("Auth0-Client", resolvedTelemetry.getValue()); } this.headerSuppliers = headerSuppliers; this.httpClient = httpClient; this.timeout = timeout; this.maxRetries = maxRetries; + this.telemetry = telemetry; } public Environment environment() { @@ -92,6 +97,32 @@ public int maxRetries() { return this.maxRetries; } + /** + * The telemetry explicitly configured on this instance, or {@code null} if it was left to + * default to {@code auth0-java}. Package-private so {@link Builder#from(ClientOptions)} can + * carry it over; not part of the public API. + */ + Telemetry telemetry() { + return this.telemetry; + } + + /** + * The static headers configured on this instance, including the resolved {@code Auth0-Client} + * header. Package-private so {@link Builder#from(ClientOptions)} can carry them over; not part + * of the public API. + */ + Map headers() { + return this.headers; + } + + /** + * The dynamic header suppliers configured on this instance. Package-private so + * {@link Builder#from(ClientOptions)} can carry them over; not part of the public API. + */ + Map> headerSuppliers() { + return this.headerSuppliers; + } + public static Builder builder() { return new Builder(); } @@ -113,6 +144,8 @@ public static class Builder { private LogConfig logging = null; + private Telemetry telemetry = null; + public Builder environment(Environment environment) { this.environment = environment; return this; @@ -173,6 +206,17 @@ public Builder addInterceptor(Interceptor interceptor) { return this; } + /** + * Internal plumbing. Sets the pre-built {@link Telemetry} used for the {@code Auth0-Client} + * header. Prefer {@link com.auth0.client.mgmt.ManagementApiBuilder#withTelemetry(String, String)}, + * which constructs the telemetry with the correct auth0-java version. Passing a hand-built + * {@link Telemetry} here bypasses that and is not a supported way to identify a wrapping SDK. + */ + public Builder telemetry(Telemetry telemetry) { + this.telemetry = telemetry; + return this; + } + public ClientOptions build() { OkHttpClient.Builder httpClientBuilder = this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder(); @@ -205,17 +249,32 @@ public ClientOptions build() { this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000); return new ClientOptions( - environment, headers, headerSuppliers, httpClient, this.timeout.get(), this.maxRetries); + environment, + headers, + headerSuppliers, + httpClient, + this.timeout.get(), + this.maxRetries, + this.telemetry); } /** - * Create a new Builder initialized with values from an existing ClientOptions + * Create a new Builder initialized with values from an existing ClientOptions. + * + *

Interceptors and logging are not copied explicitly: they are already baked into the + * {@link OkHttpClient} carried over here, and {@link #build()} preserves them via + * {@code newBuilder()}. Copying them would attach them a second time. The same applies to + * the retry interceptor, hence {@code maxRetries} is carried over for reporting only. */ public static Builder from(ClientOptions clientOptions) { Builder builder = new Builder(); builder.environment = clientOptions.environment(); builder.timeout = Optional.of(clientOptions.timeout(null)); builder.httpClient = clientOptions.httpClient(); + builder.maxRetries = clientOptions.maxRetries(); + builder.telemetry = clientOptions.telemetry(); + builder.headers.putAll(clientOptions.headers()); + builder.headerSuppliers.putAll(clientOptions.headerSuppliers()); return builder; } } diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java index d6bb8d0d1..7c86b457f 100644 --- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java +++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java @@ -3,7 +3,9 @@ import com.auth0.client.LoggingOptions; import com.auth0.client.ProxyOptions; import com.auth0.net.RateLimitInterceptor; +import com.auth0.net.Telemetry; import com.auth0.net.TelemetryInterceptor; +import com.auth0.utils.Asserts; import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -57,7 +59,7 @@ private DefaultHttpClient(Builder builder) { clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS); clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS); clientBuilder.addInterceptor(getLoggingInterceptor(builder.loggingOptions)); - clientBuilder.addInterceptor(getTelemetryInterceptor(builder.telemetryEnabled)); + clientBuilder.addInterceptor(getTelemetryInterceptor(builder.telemetryEnabled, builder.telemetry)); clientBuilder.addInterceptor(getRateLimitInterceptor(builder.maxRetries)); clientBuilder.dispatcher(getDispatcher(builder.maxRequests, builder.maxRequestsPerHost)); @@ -243,8 +245,11 @@ public okhttp3.Request authenticate(Route route, @NotNull Response response) { } } - private TelemetryInterceptor getTelemetryInterceptor(boolean telemetryEnabled) { + private TelemetryInterceptor getTelemetryInterceptor(boolean telemetryEnabled, Telemetry telemetry) { TelemetryInterceptor interceptor = new TelemetryInterceptor(); + if (telemetry != null) { + interceptor.setTelemetry(telemetry); + } interceptor.setEnabled(telemetryEnabled); return interceptor; } @@ -279,6 +284,7 @@ public static class Builder { private ProxyOptions proxyOptions; private LoggingOptions loggingOptions; private boolean telemetryEnabled = true; + private Telemetry telemetry; private int maxRetries = 3; private int maxRequests = 64; private int maxRequestsPerHost = 5; @@ -337,6 +343,24 @@ public Builder telemetryEnabled(boolean telemetryEnabled) { return this; } + /** + * Configure the telemetry sent to Auth0 on every request. Use this when building an SDK on top of + * auth0-java so that requests are attributed to the wrapping library, with auth0-java reported as the + * underlying library in the telemetry environment. + *

+ * If not set, telemetry defaults to identifying this library as {@code auth0-java}. + * + * @param name the name of the library sending the requests (e.g. the wrapping SDK's name). + * @param version the version of the library sending the requests. + * @return this builder instance. + */ + public Builder withTelemetry(String name, String version) { + Asserts.assertNotNull(name, "name"); + this.telemetry = + new Telemetry(name, version, Telemetry.class.getPackage().getImplementationVersion()); + return this; + } + /** * Sets the maximum number of consecutive retries for API requests that fail due to rate-limits being reached. * By default, rate-limited requests will be retried a maximum of three times. To disable retries on rate-limit diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java index d78699384..5cfdd37b7 100644 --- a/src/test/java/com/auth0/client/auth/AuthAPITest.java +++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java @@ -20,13 +20,16 @@ import com.auth0.net.BaseRequest; import com.auth0.net.Request; import com.auth0.net.SignUpRequest; +import com.auth0.net.Telemetry; import com.auth0.net.TokenRequest; import com.auth0.net.client.Auth0HttpClient; import com.auth0.net.client.Auth0HttpRequest; import com.auth0.net.client.Auth0HttpResponse; +import com.auth0.net.client.DefaultHttpClient; import com.auth0.net.client.HttpMethod; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.FileReader; import java.net.URLDecoder; @@ -261,6 +264,60 @@ public void shouldCreateUserInfoRequest() throws Exception { assertThat(identities.get(0), hasEntry("isSocial", false)); } + // Telemetry + + private JsonNode decodeTelemetryHeader(RecordedRequest request) throws Exception { + String header = request.getHeader("Auth0-Client"); + assertThat(header, is(notNullValue())); + return ObjectMapperProvider.getMapper().readTree(Base64.getUrlDecoder().decode(header)); + } + + @Test + public void shouldSendDefaultTelemetry() throws Exception { + Request request = api.userInfo("accessToken"); + server.jsonResponse(AUTH_USER_INFO, 200); + request.execute(); + + JsonNode telemetry = decodeTelemetryHeader(server.takeRequest()); + assertThat(telemetry.get("name").asText(), is("auth0-java")); + } + + @Test + public void shouldSendCustomTelemetryWhenConfigured() throws Exception { + AuthAPI customApi = AuthAPI.newBuilder(server.getBaseUrl(), CLIENT_ID, CLIENT_SECRET) + .withHttpClient(DefaultHttpClient.newBuilder() + .withTelemetry("my-wrapper-sdk", "1.2.3") + .build()) + .build(); + + Request request = customApi.userInfo("accessToken"); + server.jsonResponse(AUTH_USER_INFO, 200); + request.execute(); + + JsonNode telemetry = decodeTelemetryHeader(server.takeRequest()); + assertThat(telemetry.get("name").asText(), is("my-wrapper-sdk")); + assertThat(telemetry.get("version").asText(), is("1.2.3")); + } + + @Test + public void shouldNestAuth0JavaInTelemetryEnv() throws Exception { + String value = new Telemetry("my-wrapper-sdk", "1.2.3", "auth0-java-9.9.9").getValue(); + + JsonNode telemetry = + ObjectMapperProvider.getMapper().readTree(Base64.getUrlDecoder().decode(value)); + assertThat(telemetry.get("name").asText(), is("my-wrapper-sdk")); + assertThat(telemetry.get("version").asText(), is("1.2.3")); + assertThat(telemetry.get("env").get("auth0-java").asText(), is("auth0-java-9.9.9")); + } + + @Test + public void shouldThrowWhenTelemetryNameIsNull() { + verifyThrows( + IllegalArgumentException.class, + () -> DefaultHttpClient.newBuilder().withTelemetry(null, "1.2.3"), + "'name' cannot be null!"); + } + // Reset Password @Test diff --git a/src/test/java/com/auth0/client/mgmt/ManagementApiTelemetryTest.java b/src/test/java/com/auth0/client/mgmt/ManagementApiTelemetryTest.java new file mode 100644 index 000000000..27b7a0b40 --- /dev/null +++ b/src/test/java/com/auth0/client/mgmt/ManagementApiTelemetryTest.java @@ -0,0 +1,101 @@ +package com.auth0.client.mgmt; + +import static com.auth0.AssertsUtil.verifyThrows; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import com.auth0.client.mgmt.core.ClientOptions; +import com.auth0.json.ObjectMapperProvider; +import com.auth0.net.Telemetry; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.Base64; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ManagementApiTelemetryTest { + + private JsonNode decodeTelemetry(ManagementApi api) throws Exception { + Map headers = api.clientOptions.headers(null); + String value = headers.get("Auth0-Client"); + assertThat(value, is(notNullValue())); + byte[] json = Base64.getUrlDecoder().decode(value); + return ObjectMapperProvider.getMapper().readTree(json); + } + + @Test + public void shouldSendDefaultTelemetryWhenNotConfigured() throws Exception { + ManagementApi api = ManagementApi.builder() + .domain("my-tenant.auth0.com") + .token("test-token") + .build(); + + JsonNode telemetry = decodeTelemetry(api); + assertThat(telemetry.get("name").asText(), is("auth0-java")); + } + + @Test + public void shouldSendCustomTelemetryWhenConfigured() throws Exception { + ManagementApi api = ManagementApi.builder() + .domain("my-tenant.auth0.com") + .token("test-token") + .withTelemetry("my-wrapper-sdk", "1.2.3") + .build(); + + JsonNode telemetry = decodeTelemetry(api); + assertThat(telemetry.get("name").asText(), is("my-wrapper-sdk")); + assertThat(telemetry.get("version").asText(), is("1.2.3")); + } + + @Test + public void shouldNestAuth0JavaInTelemetryEnv() throws Exception { + ClientOptions options = ClientOptions.builder() + .environment(com.auth0.client.mgmt.core.Environment.DEFAULT) + .telemetry(new Telemetry("my-wrapper-sdk", "1.2.3", "auth0-java-9.9.9")) + .build(); + + String value = options.headers(null).get("Auth0-Client"); + assertThat(value, is(notNullValue())); + JsonNode telemetry = + ObjectMapperProvider.getMapper().readTree(Base64.getUrlDecoder().decode(value)); + assertThat(telemetry.get("name").asText(), is("my-wrapper-sdk")); + assertThat(telemetry.get("version").asText(), is("1.2.3")); + assertThat(telemetry.get("env").get("auth0-java").asText(), is("auth0-java-9.9.9")); + } + + @Test + public void shouldPreserveTelemetryAndHeadersWhenCopyingOptions() throws Exception { + ClientOptions original = ClientOptions.builder() + .environment(com.auth0.client.mgmt.core.Environment.DEFAULT) + .telemetry(new Telemetry("my-wrapper-sdk", "1.2.3")) + .addHeader("Authorization", "Bearer test-token") + .addHeader("X-Dynamic", () -> "dynamic-value") + .maxRetries(5) + .build(); + + ClientOptions copy = ClientOptions.Builder.from(original).build(); + + Map headers = copy.headers(null); + JsonNode telemetry = + ObjectMapperProvider.getMapper().readTree(Base64.getUrlDecoder().decode(headers.get("Auth0-Client"))); + assertThat(telemetry.get("name").asText(), is("my-wrapper-sdk")); + assertThat(telemetry.get("version").asText(), is("1.2.3")); + assertThat(headers.get("Authorization"), is("Bearer test-token")); + assertThat(headers.get("X-Dynamic"), is("dynamic-value")); + assertThat(copy.maxRetries(), is(5)); + } + + @Test + public void shouldReturnBuilderFromWithTelemetry() { + ManagementApiBuilder builder = ManagementApi.builder(); + ManagementApiBuilder result = builder.withTelemetry("my-wrapper-sdk", "1.2.3"); + assertThat(result, is(sameInstance(builder))); + } + + @Test + public void shouldThrowWhenTelemetryNameIsNull() { + verifyThrows( + IllegalArgumentException.class, + () -> ManagementApi.builder().withTelemetry(null, "1.2.3"), + "'name' cannot be null!"); + } +}