diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index 8544d3018..7bcc593b0 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -756,7 +756,8 @@ public class SubscriptionController { try { final SubscriptionManager.ReceiptResult receiptCredential = subscriptionManager.createReceiptCredentials( subscriberCredentials, request.receiptCredentialRequest(), - r -> SubscriptionsUtil.receiptExpirationWithGracePeriod(subscriptionConfiguration, r)); + r -> SubscriptionsUtil.receiptExpirationWithGracePeriod(subscriptionConfiguration, r), + userAgent); final ReceiptCredentialResponse receiptCredentialResponse = receiptCredential.receiptCredentialResponse(); final CustomerAwareSubscriptionPaymentProcessor.ReceiptItem receipt = receiptCredential.receiptItem(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcService.java index 3cd11fd10..ea7aec1cd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcService.java @@ -441,7 +441,8 @@ public class SubscriptionsGrpcService extends SimpleSubscriptionsGrpc.Subscripti try { final SubscriptionManager.ReceiptResult result = subscriptionManager.createReceiptCredentials( subscriberCredentials, request.getReceiptCredentialRequest().toByteArray(), - r -> SubscriptionsUtil.receiptExpirationWithGracePeriod(subscriptionConfiguration, r)); + r -> SubscriptionsUtil.receiptExpirationWithGracePeriod(subscriptionConfiguration, r), + RequestAttributesUtil.getUserAgent().orElse(null)); Metrics.counter(RECEIPT_ISSUED_COUNTER_NAME, Tags.of( Tag.of(PROCESSOR_TAG_NAME, result.paymentProvider().toString()), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java index 23662d275..d1b510a85 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java @@ -4,6 +4,9 @@ */ package org.whispersystems.textsecuregcm.storage; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; import java.io.IOException; import java.io.UncheckedIOException; import java.time.Instant; @@ -15,17 +18,21 @@ import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreManager; import org.whispersystems.textsecuregcm.subscriptions.CustomerAwareSubscriptionPaymentProcessor; import org.whispersystems.textsecuregcm.subscriptions.GooglePlayBillingManager; import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider; import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; +import org.whispersystems.textsecuregcm.subscriptions.ReceiptLevel; import org.whispersystems.textsecuregcm.subscriptions.SubscriberIdCreationNotPermittedException; import org.whispersystems.textsecuregcm.subscriptions.SubscriptionForbiddenException; import org.whispersystems.textsecuregcm.subscriptions.SubscriptionInformation; @@ -53,6 +60,9 @@ import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; */ public class SubscriptionManager { + private static final String RECEIPT_ALREADY_REDEEMED_COUNTER_NAME = MetricsUtil.name(SubscriptionManager.class, + "receiptAlreadyRedeemed"); + private final Subscriptions subscriptions; private final EnumMap processors; private final ServerZkReceiptOperations zkReceiptOperations; @@ -177,6 +187,7 @@ public class SubscriptionManager { * @param expiration A function that takes a * {@link CustomerAwareSubscriptionPaymentProcessor.ReceiptItem} and returns the * expiration time of the receipt + * @param userAgent The requesting client's user agent * @return the requested ZK receipt credential * @throws SubscriptionForbiddenException if the subscriber credentials were incorrect * @throws SubscriptionNotFoundException if the subscriber did not exist or did not have a @@ -193,7 +204,8 @@ public class SubscriptionManager { public ReceiptResult createReceiptCredentials( final SubscriberCredentials subscriberCredentials, final byte[] receiptCredentialRequestBytes, - final Function expiration) + final Function expiration, + @Nullable final String userAgent) throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionInvalidArgumentsException, SubscriptionPaymentRequiredException, RateLimitExceededException, SubscriptionReceiptRequestedForOpenPaymentException, SubscriptionReceiptAlreadyRedeemedException { final Subscriptions.Record record = getSubscriber(subscriberCredentials); if (record.subscriptionId == null) { @@ -222,6 +234,12 @@ public class SubscriptionManager { } catch (final VerificationFailedException e) { throw new SubscriptionInvalidArgumentsException("receipt credential request failed verification", e); } catch (final WriteConflictException _) { + Metrics.counter(RECEIPT_ALREADY_REDEEMED_COUNTER_NAME, Tags.of( + Tag.of("receiptLevel", ReceiptLevel.lookupLevel(receipt.level()) + .map(ReceiptLevel::name) + .orElse("n/a")), + UserAgentTagUtil.getPlatformTag(userAgent))) + .increment(); throw new SubscriptionReceiptAlreadyRedeemedException(); } return new ReceiptResult(receiptCredentialResponse, receipt, processor); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcServiceTest.java index 7511a4101..ab2a48187 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SubscriptionsGrpcServiceTest.java @@ -576,7 +576,7 @@ public class SubscriptionsGrpcServiceTest extends final ReceiptCredentialResponse receiptCredentialResponse = mock(ReceiptCredentialResponse.class); final byte[] responseBytes = TestRandomUtil.nextBytes(16); when(receiptCredentialResponse.serialize()).thenReturn(responseBytes); - when(subscriptionManager.createReceiptCredentials(any(), any(), any())) + when(subscriptionManager.createReceiptCredentials(any(), any(), any(), any())) .thenReturn(new SubscriptionManager.ReceiptResult(receiptCredentialResponse, new SubscriptionPaymentProcessor.ReceiptItem("test-item-id", null, 5), PaymentProvider.STRIPE)); final GetReceiptCredentialsResponse response = unauthenticatedServiceStub().getReceiptCredentials( GetReceiptCredentialsRequest.newBuilder() @@ -602,7 +602,7 @@ public class SubscriptionsGrpcServiceTest extends @MethodSource void getReceiptCredentialsExceptions(final SubscriptionException exception, final GetReceiptCredentialsResponse.ResponseCase expectedCase) throws Exception { - doThrow(exception).when(subscriptionManager).createReceiptCredentials(any(), any(), any()); + doThrow(exception).when(subscriptionManager).createReceiptCredentials(any(), any(), any(), any()); final GetReceiptCredentialsResponse response = unauthenticatedServiceStub().getReceiptCredentials( GetReceiptCredentialsRequest.newBuilder() .setSubscriberId(SUBSCRIBER_ID) @@ -615,7 +615,7 @@ public class SubscriptionsGrpcServiceTest extends void getReceiptCredentialsChargeFailure() throws Exception { doThrow(new SubscriptionChargeFailurePaymentRequiredException(PaymentProvider.STRIPE, new ChargeFailure("card_declined", "Insufficient funds", null, null, null))) - .when(subscriptionManager).createReceiptCredentials(any(), any(), any()); + .when(subscriptionManager).createReceiptCredentials(any(), any(), any(), any()); final GetReceiptCredentialsResponse response = unauthenticatedServiceStub().getReceiptCredentials( GetReceiptCredentialsRequest.newBuilder() .setSubscriberId(SUBSCRIBER_ID)