Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report usage of StripeClient #1698

Closed
wants to merge 11 commits into from
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ tasks.withType(JavaCompile) {
// com.stripe.param.AccountUpdateParams.Individual.Address] within this file.)
// We should fix this by having autogen use the fully-qualified class to eliminate ambiguity.
check("SameNameButDifferent", net.ltgt.gradle.errorprone.CheckSeverity.OFF)

// InlineMe (https://errorprone.info/docs/inlineme) seems neat, but in order to add these annotations
// we would be imposing another dependency on `errorprone` to our users, not worth it.
check("InlineMeSuggester", net.ltgt.gradle.errorprone.CheckSeverity.OFF)
anniel-stripe marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
24 changes: 21 additions & 3 deletions src/main/java/com/stripe/StripeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.stripe.net.*;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.util.Arrays;
import lombok.Getter;

/**
Expand All @@ -18,18 +19,35 @@ public class StripeClient {
* instead if you require more complex configuration.
*/
public StripeClient(String apiKey) {
this.responseGetter =
new LiveStripeResponseGetter(builder().setApiKey(apiKey).buildOptions(), null);
this(new LiveStripeResponseGetter(builder().setApiKey(apiKey).buildOptions(), null));
}

/**
* Constructs a StripeClient with a custom StripeResponseGetter.
*
* <p>Use this for testing, or advanced use cases where you need to make fundamental changes to
* how the StripeClient makes requests.
*
* <p>responseGetter should not be re-used across StripeClient and
* ApiResource.setStripeResponseGetter
*/
public StripeClient(StripeResponseGetter responseGetter) {
this.responseGetter = responseGetter;

if (this.responseGetter instanceof LiveStripeResponseGetter) {
// Setting usage via mutation instead of making a copy is necessary if we want to support
// users to create their own subclasses of LiveStripeResponseGetter to use here.
//
// Unfortunately, mutation could technically cause some non-obvious behavior in the case
// of re-use; e.g.
//
// var lsrg = new LiveStripeResponseGetter();
// ApiResource.setStripeResponseGetter(lsrg);
// new StripeClient(lsrg);
//
// would cause e.g. Customer.create(...) to erroneously report "stripe_client" in usage
((LiveStripeResponseGetter) this.responseGetter).setUsage(Arrays.asList("stripe_client"));
richardm-stripe marked this conversation as resolved.
Show resolved Hide resolved
}
}

protected StripeResponseGetter getResponseGetter() {
Expand Down Expand Up @@ -287,7 +305,7 @@ public com.stripe.service.WebhookEndpointService webhookEndpoints() {

// The end of the section generated from our OpenAPI spec
static class ClientStripeResponseGetterOptions extends StripeResponseGetterOptions {
// When adding setting here keep them in sync with settings in RequestOptions and
// When adding settings here, keep them in sync with settings in RequestOptions and
// in the RequestOptions.merge method
@Getter(onMethod_ = {@Override})
private final String apiKey;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/stripe/net/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ private <T extends AbstractStripeResponse<?>> T sendWithTelemetry(

stopwatch.stop();

requestTelemetry.maybeEnqueueMetrics(response, stopwatch.getElapsed());
requestTelemetry.maybeEnqueueMetrics(response, stopwatch.getElapsed(), request.usage());

return response;
}
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/com/stripe/net/LiveStripeResponseGetter.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.Setter;

public class LiveStripeResponseGetter implements StripeResponseGetter {
private final HttpClient httpClient;
private final StripeResponseGetterOptions options;

@Setter private List<String> usage;

/**
* Initializes a new instance of the {@link LiveStripeResponseGetter} class with default
* parameters.
Expand All @@ -41,6 +46,7 @@ public LiveStripeResponseGetter(HttpClient httpClient) {
public LiveStripeResponseGetter(StripeResponseGetterOptions options, HttpClient httpClient) {
this.options = options != null ? options : GlobalStripeResponseGetterOptions.INSTANCE;
this.httpClient = (httpClient != null) ? httpClient : buildDefaultHttpClient();
this.usage = Arrays.asList();
}

@Override
Expand All @@ -56,7 +62,8 @@ public <T extends StripeObjectInterface> T request(
throws StripeException {
String fullUrl = fullUrl(baseAddress, options, path);
StripeRequest request =
new StripeRequest(method, fullUrl, params, RequestOptions.merge(this.options, options));
new StripeRequest(
method, fullUrl, params, RequestOptions.merge(this.options, options), this.usage);
StripeResponse response = httpClient.requestWithRetries(request);

int responseCode = response.code();
Expand Down Expand Up @@ -96,7 +103,8 @@ public InputStream requestStream(
String fullUrl = fullUrl(baseAddress, options, path);

StripeRequest request =
new StripeRequest(method, fullUrl, params, RequestOptions.merge(this.options, options));
new StripeRequest(
method, fullUrl, params, RequestOptions.merge(this.options, options), this.usage);
StripeResponseStream responseStream = httpClient.requestStreamWithRetries(request);

int responseCode = responseStream.code();
Expand Down
24 changes: 22 additions & 2 deletions src/main/java/com/stripe/net/RequestTelemetry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.google.gson.annotations.SerializedName;
import com.stripe.Stripe;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.Data;
Expand Down Expand Up @@ -45,14 +47,25 @@ public Optional<String> getHeaderValue(HttpHeaders headers) {
return Optional.of(gson.toJson(payload));
}

// TODO (major) remove this overload
/**
* @deprecated use {@link #maybeEnqueueMetrics(AbstractStripeResponse, Duration, List)} instead.
*/
@Deprecated
public void maybeEnqueueMetrics(AbstractStripeResponse<?> response, Duration duration) {
maybeEnqueueMetrics(response, duration, new ArrayList<String>());
}

/**
* If telemetry is enabled and the queue is not full, then enqueue a new metrics item; otherwise,
* do nothing.
*
* @param response the Stripe response
* @param duration the request duration
* @param usage a list of tracked features used by the corresponding request
*/
public void maybeEnqueueMetrics(AbstractStripeResponse<?> response, Duration duration) {
public void maybeEnqueueMetrics(
AbstractStripeResponse<?> response, Duration duration, List<String> usage) {
if (!Stripe.enableTelemetry) {
return;
}
Expand All @@ -65,7 +78,11 @@ public void maybeEnqueueMetrics(AbstractStripeResponse<?> response, Duration dur
return;
}

RequestMetrics metrics = new RequestMetrics(response.requestId(), duration.toMillis());
if (usage.size() == 0) {
usage = null;
}

RequestMetrics metrics = new RequestMetrics(response.requestId(), duration.toMillis(), usage);
prevRequestMetrics.add(metrics);
}

Expand All @@ -82,5 +99,8 @@ private static class RequestMetrics {

@SerializedName("request_duration_ms")
private final long requestDurationMs;

@SerializedName("usage")
private final List<String> usage;
}
}
21 changes: 19 additions & 2 deletions src/main/java/com/stripe/net/StripeRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.stripe.util.StringUtils;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -51,20 +52,25 @@ public class StripeRequest {
/** The special modifiers of the request. */
RequestOptions options;

/** List of tracked behaviors associated with this request. */
List<String> usage;

/**
* Initializes a new instance of the {@link StripeRequest} class.
*
* @param method the HTTP method
* @param url the URL of the request
* @param params the parameters of the request
* @param options the special modifiers of the request
* @param usage list of tracked behaviors associated with this request
* @throws StripeException if the request cannot be initialized for any reason
*/
public StripeRequest(
ApiResource.RequestMethod method,
String url,
Map<String, Object> params,
RequestOptions options)
RequestOptions options,
List<String> usage)
throws StripeException {
try {
this.params = (params != null) ? Collections.unmodifiableMap(params) : null;
Expand All @@ -73,6 +79,7 @@ public StripeRequest(
this.url = buildURL(method, url, params);
this.content = buildContent(method, params);
this.headers = buildHeaders(method, this.options);
this.usage = usage;
} catch (IOException e) {
throw new ApiConnectionException(
String.format(
Expand All @@ -85,6 +92,15 @@ public StripeRequest(
}
}

public StripeRequest(
ApiResource.RequestMethod method,
String url,
Map<String, Object> params,
RequestOptions options)
throws StripeException {
this(method, url, params, options, new ArrayList<String>());
}

/**
* Returns a new {@link StripeRequest} instance with an additional header.
*
Expand All @@ -99,7 +115,8 @@ public StripeRequest withAdditionalHeader(String name, String value) {
this.content,
this.headers.withAdditionalHeader(name, value),
this.params,
this.options);
this.options,
this.usage);
}

private static URL buildURL(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

/** Controls how the request is sent by {@link StripeResponseGetter} */
public abstract class StripeResponseGetterOptions {

// When adding setting here keep them in sync with settings in RequestOptions and
// When adding settings here keep them in sync with settings in RequestOptions and
// in the RequestOptions.merge method
public abstract String getApiKey();

Expand Down
3 changes: 3 additions & 0 deletions src/test/java/com/stripe/BaseStripeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public void setUpStripeMockUsage() {
httpClientSpy = Mockito.spy(new HttpURLConnectionClient());
networkSpy = Mockito.spy(new LiveStripeResponseGetter(null, httpClientSpy));
mockClient = new StripeClient(networkSpy);
// The StripeClient constructor calls .setUsage on networkSpy, let's reset that so
// in the test cases we can just verify the calls we care about.
Mockito.reset(networkSpy);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we change our test infra to have 2 spies / separate LiveStripeResponseGetters for APIResource and StripeClient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My worry with that, is that we would then need to make copies of all the other helper functions here that use networkSpy and it might be a bad experience when you're writing a test to have to choose between them and select the right one, and I hesitated to make the experience worse in 95% of cases for an edge case that only affects usage-related tests.

I was on the fence though, let me know what you prefer.

ApiResource.setStripeResponseGetter(networkSpy);
OAuth.setGlobalResponseGetter(networkSpy);
}
Expand Down
14 changes: 14 additions & 0 deletions src/test/java/com/stripe/StripeClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import com.stripe.model.terminal.Reader;
import com.stripe.net.*;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand Down Expand Up @@ -43,4 +45,16 @@ public void clientOptionsDefaults() {
assertEquals(Stripe.UPLOAD_API_BASE, options.getFilesBase());
assertEquals(0, options.getMaxNetworkRetries());
}

@Test
public void setsUsageOnResponseGetter() throws Exception {
StripeResponseGetter responseGetter = new LiveStripeResponseGetter();
new StripeClient(responseGetter);
Field field = responseGetter.getClass().getDeclaredField("usage");
field.setAccessible(true);
Object usage = field.get(responseGetter);
assertTrue(usage instanceof List);
assertEquals(1, ((List<?>) usage).size());
assertEquals("stripe_client", ((List<?>) usage).get(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.stripe.BaseStripeTest;
import com.stripe.Stripe;
import com.stripe.exception.ApiException;
import com.stripe.exception.StripeException;
import com.stripe.model.Customer;
import com.stripe.model.Subscription;
import com.stripe.net.ApiResource;
import com.stripe.net.HttpClient;
Expand All @@ -17,8 +21,12 @@
import com.stripe.net.StripeRequest;
import com.stripe.net.StripeResponse;
import com.stripe.net.StripeResponseGetter;
import com.stripe.param.CustomerCreateParams;
import com.stripe.param.CustomerUpdateParams;
import java.util.Collections;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

Expand All @@ -43,4 +51,44 @@ public void testInvalidJson() throws StripeException {
assertNotNull(exception.getCause());
assertThat(exception.getCause(), CoreMatchers.instanceOf(JsonSyntaxException.class));
}

private Boolean originalTelemetry;

@BeforeEach
public void setUp() {
this.originalTelemetry = Stripe.enableTelemetry;
Stripe.enableTelemetry = true;
}

@AfterEach
public void tearDown() {
Stripe.enableTelemetry = originalTelemetry;
}

@Test
public void testGlobalClientTelemetryTest() throws StripeException {
StripeResponseGetter responseGetterSpy =
Mockito.spy(new LiveStripeResponseGetter(null, httpClientSpy));
// We need to explicitly override the ApiResource.responseGetter = networkSpy that's set
// in BaseStripeTest. Unfortunately, BaseStripeTest shares the response getter spy across
// StripeClient and the Global Client, and so the global client will erroneously report stripe
// client usage.
ApiResource.setStripeResponseGetter(responseGetterSpy);
Customer cus = Customer.create(CustomerCreateParams.builder().build());
Mockito.reset(httpClientSpy);
cus.update(CustomerUpdateParams.builder().setDescription("hello").build());
Mockito.verify(httpClientSpy)
.request(
Mockito.argThat(
stripeRequest -> {
JsonObject metrics =
new Gson()
.fromJson(
stripeRequest.headers().firstValue("X-Stripe-Client-Telemetry").get(),
JsonObject.class)
.get("last_request_metrics")
.getAsJsonObject();
return !metrics.has("usage") && metrics.has("request_duration_ms");
}));
}
}
Loading
Loading