Skip to content

Commit 6a92a7c

Browse files
committed
feat(payments-next): add metrics for each server action, better tagging
Because: - We want to see metrics for each server action This commit: - Adds a statsd counter for each server action Closes FXA-11593
1 parent c12c434 commit 6a92a7c

8 files changed

Lines changed: 66 additions & 7 deletions

File tree

libs/payments/cart/src/lib/cart.manager.in.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from './cart.factories';
2828
import { CartManager } from './cart.manager';
2929
import { ResultCart } from './cart.types';
30+
import { type StatsD } from '@fxa/shared/metrics/statsd';
3031

3132
// Fail action, which sometimes isn't here due to a weird issue defined here:
3233
// https://github.com/jestjs/jest/issues/11698#issuecomment-922351139
@@ -57,7 +58,9 @@ describe('CartManager', () => {
5758

5859
beforeAll(async () => {
5960
db = await testAccountDatabaseSetup(['accounts', 'carts']);
60-
cartManager = new CartManager(db);
61+
cartManager = new CartManager(db, {
62+
timing: jest.fn(),
63+
} as unknown as StatsD);
6164
await db
6265
.insertInto('carts')
6366
.values({

libs/payments/cart/src/lib/cart.manager.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
fetchCartsByUid,
2323
updateCart,
2424
} from './cart.repository';
25-
import {
25+
import type {
2626
FinishCart,
2727
FinishErrorCart,
2828
ResultCart,
@@ -35,6 +35,7 @@ import type {
3535
CartErrorReasonId,
3636
} from '@fxa/shared/db/mysql/account';
3737
import assert from 'assert';
38+
import { CaptureTimingWithStatsD, StatsDService, type StatsD } from '@fxa/shared/metrics/statsd';
3839
// For an action to be executed, the cart state needs to be in one of
3940
// valid states listed in the array of CartStates below
4041
const ACTIONS_VALID_STATE = {
@@ -57,7 +58,10 @@ const isAction = (action: string): action is keyof typeof ACTIONS_VALID_STATE =>
5758

5859
@Injectable()
5960
export class CartManager {
60-
constructor(@Inject(AccountDbProvider) private db: AccountDatabase) {}
61+
constructor(
62+
@Inject(AccountDbProvider) private db: AccountDatabase,
63+
@Inject(StatsDService) public statsd: StatsD
64+
) {}
6165

6266
/**
6367
* Ensure that the action being executed has a valid Cart state for
@@ -92,6 +96,7 @@ export class CartManager {
9296
return cart;
9397
}
9498

99+
@CaptureTimingWithStatsD()
95100
public async createCart(input: SetupCart): Promise<ResultCart> {
96101
const now = Date.now();
97102
try {
@@ -123,6 +128,7 @@ export class CartManager {
123128
}
124129
}
125130

131+
@CaptureTimingWithStatsD()
126132
public async createErrorCart(
127133
input: SetupCart,
128134
errorReasonId: CartErrorReasonId
@@ -158,6 +164,7 @@ export class CartManager {
158164
}
159165
}
160166

167+
@CaptureTimingWithStatsD()
161168
public async fetchCartById(id: string): Promise<ResultCart> {
162169
try {
163170
const cart = await fetchCartById(this.db, Buffer.from(id, 'hex'));
@@ -176,6 +183,7 @@ export class CartManager {
176183
}
177184
}
178185

186+
@CaptureTimingWithStatsD()
179187
public async fetchCartsByUid(uid: string): Promise<ResultCart[]> {
180188
const carts = await fetchCartsByUid(this.db, Buffer.from(uid, 'hex'));
181189

@@ -190,6 +198,7 @@ export class CartManager {
190198
});
191199
}
192200

201+
@CaptureTimingWithStatsD()
193202
public async updateFreshCart(
194203
cartId: string,
195204
version: number,
@@ -214,6 +223,7 @@ export class CartManager {
214223
}
215224
}
216225

226+
@CaptureTimingWithStatsD()
217227
public async finishCart(cartId: string, version: number, items: FinishCart) {
218228
const cart = await this.fetchAndValidateCartVersion(cartId, version);
219229

@@ -231,6 +241,7 @@ export class CartManager {
231241
}
232242
}
233243

244+
@CaptureTimingWithStatsD()
234245
public async finishErrorCart(cartId: string, items: FinishErrorCart) {
235246
const cart = await this.fetchCartById(cartId);
236247

@@ -248,6 +259,7 @@ export class CartManager {
248259
}
249260
}
250261

262+
@CaptureTimingWithStatsD()
251263
public async setNeedsInputCart(cartId: string) {
252264
const cart = await this.fetchCartById(cartId);
253265

@@ -263,6 +275,7 @@ export class CartManager {
263275
}
264276
}
265277

278+
@CaptureTimingWithStatsD()
266279
public async setProcessingCart(cartId: string) {
267280
const cart = await this.fetchCartById(cartId);
268281

@@ -278,6 +291,7 @@ export class CartManager {
278291
}
279292
}
280293

294+
@CaptureTimingWithStatsD()
281295
public async deleteCart(cart: ResultCart) {
282296
this.checkActionForValidCartState(cart, 'deleteCart');
283297

libs/payments/cart/src/lib/cart.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ export class CartService {
146146

147147
const errorReasonId = resolveErrorInstance(error);
148148

149+
this.statsd.increment('checkout_failure_unexpected', {
150+
errorReasonId,
151+
});
152+
149153
try {
150154
await this.cartManager.finishErrorCart(cartId, {
151155
errorReasonId,

libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { Injectable } from '@nestjs/common';
5+
import { Inject, Injectable } from '@nestjs/common';
66

77
import { GoogleManager } from '@fxa/google';
88
import {
@@ -75,6 +75,7 @@ import { ValidateLocationActionResult } from './validators/ValidateLocationActio
7575
import { ValidateLocationActionArgs } from './validators/ValidateLocationActionArgs';
7676
import { UpdateTaxAddressActionArgs } from './validators/UpdateTaxAddressActionArgs';
7777
import { UpdateTaxAddressActionResult } from './validators/UpdateTaxAddressActionResult';
78+
import { CaptureTimingWithStatsD, StatsDService, type StatsD } from '@fxa/shared/metrics/statsd';
7879

7980
/**
8081
* ANY AND ALL methods exposed via this service should be considered publicly accessible and callable with any arguments.
@@ -93,12 +94,14 @@ export class NextJSActionsService {
9394
private geodbManager: GeoDBManager,
9495
private currencyManager: CurrencyManager,
9596
private eligibilityService: EligibilityService,
96-
private productConfigurationManager: ProductConfigurationManager
97+
private productConfigurationManager: ProductConfigurationManager,
98+
@Inject(StatsDService) public statsd: StatsD
9799
) {}
98100

99101
@SanitizeExceptions()
100102
@NextIOValidator(GetCartActionArgs, GetCartActionResult)
101103
@WithTypeCachableAsyncLocalStorage()
104+
@CaptureTimingWithStatsD()
102105
async getCart(args: { cartId: string }) {
103106
const cart = await this.cartService.getCart(args.cartId);
104107

@@ -110,6 +113,7 @@ export class NextJSActionsService {
110113
})
111114
@NextIOValidator(GetCartActionArgs, GetSuccessCartActionResult)
112115
@WithTypeCachableAsyncLocalStorage()
116+
@CaptureTimingWithStatsD()
113117
async getSuccessCart(args: { cartId: string }): Promise<SuccessCartDTO> {
114118
const cart = await this.cartService.getCart(args.cartId);
115119

@@ -129,6 +133,7 @@ export class NextJSActionsService {
129133
})
130134
@NextIOValidator(UpdateCartActionArgs, UpdateCartActionResult)
131135
@WithTypeCachableAsyncLocalStorage()
136+
@CaptureTimingWithStatsD()
132137
async updateCart(args: {
133138
cartId: string;
134139
version: number;
@@ -151,6 +156,7 @@ export class NextJSActionsService {
151156
@SanitizeExceptions()
152157
@NextIOValidator(RestartCartActionArgs, RestartCartActionResult)
153158
@WithTypeCachableAsyncLocalStorage()
159+
@CaptureTimingWithStatsD()
154160
async restartCart(args: { cartId: string }) {
155161
const cart = await this.cartService.restartCart(args.cartId);
156162

@@ -162,6 +168,7 @@ export class NextJSActionsService {
162168
})
163169
@NextIOValidator(SetupCartActionArgs, SetupCartActionResult)
164170
@WithTypeCachableAsyncLocalStorage()
171+
@CaptureTimingWithStatsD()
165172
async setupCart(args: {
166173
interval: SubplatInterval;
167174
offeringConfigId: string;
@@ -180,6 +187,7 @@ export class NextJSActionsService {
180187
@SanitizeExceptions()
181188
@NextIOValidator(FinalizeCartWithErrorArgs, undefined)
182189
@WithTypeCachableAsyncLocalStorage()
190+
@CaptureTimingWithStatsD()
183191
async finalizeCartWithError(args: {
184192
cartId: string;
185193
errorReasonId: CartErrorReasonId;
@@ -193,13 +201,15 @@ export class NextJSActionsService {
193201
@SanitizeExceptions()
194202
@NextIOValidator(FinalizeProcessingCartActionArgs, undefined)
195203
@WithTypeCachableAsyncLocalStorage()
204+
@CaptureTimingWithStatsD()
196205
async finalizeProcessingCart(args: { cartId: string }) {
197206
await this.cartService.finalizeProcessingCart(args.cartId);
198207
}
199208

200209
@SanitizeExceptions()
201210
@NextIOValidator(GetPayPalCheckoutTokenArgs, GetPayPalCheckoutTokenResult)
202211
@WithTypeCachableAsyncLocalStorage()
212+
@CaptureTimingWithStatsD()
203213
async getPayPalCheckoutToken(args: { currencyCode: string }) {
204214
const token = await this.checkoutTokenManager.get(args.currencyCode);
205215

@@ -211,6 +221,7 @@ export class NextJSActionsService {
211221
@SanitizeExceptions()
212222
@NextIOValidator(GetTaxAddressArgs, GetTaxAddressResult)
213223
@WithTypeCachableAsyncLocalStorage()
224+
@CaptureTimingWithStatsD()
214225
async getTaxAddress(args: { ipAddress: string; uid?: string }) {
215226
const result = await this.taxService.getTaxAddress(
216227
args.ipAddress,
@@ -223,6 +234,7 @@ export class NextJSActionsService {
223234
@SanitizeExceptions()
224235
@NextIOValidator(CheckoutCartWithPaypalActionArgs, undefined)
225236
@WithTypeCachableAsyncLocalStorage()
237+
@CaptureTimingWithStatsD()
226238
async checkoutCartWithPaypal(args: {
227239
cartId: string;
228240
version: number;
@@ -240,6 +252,7 @@ export class NextJSActionsService {
240252
@SanitizeExceptions()
241253
@NextIOValidator(CheckoutCartWithStripeActionArgs, undefined)
242254
@WithTypeCachableAsyncLocalStorage()
255+
@CaptureTimingWithStatsD()
243256
async checkoutCartWithStripe(args: {
244257
cartId: string;
245258
version: number;
@@ -257,6 +270,7 @@ export class NextJSActionsService {
257270
@SanitizeExceptions({ allowlist: [ProductConfigError] })
258271
@NextIOValidator(FetchCMSDataActionArgs, FetchCMSDataActionResult)
259272
@WithTypeCachableAsyncLocalStorage()
273+
@CaptureTimingWithStatsD()
260274
async fetchCMSData(args: {
261275
offeringId: string;
262276
acceptLanguage?: string | null;
@@ -274,6 +288,7 @@ export class NextJSActionsService {
274288
@SanitizeExceptions()
275289
@NextIOValidator(RecordEmitterEventArgs, undefined)
276290
@WithTypeCachableAsyncLocalStorage()
291+
@CaptureTimingWithStatsD()
277292
async recordEmitterEvent(args: {
278293
eventName: string;
279294
requestArgs: CommonMetrics;
@@ -307,6 +322,7 @@ export class NextJSActionsService {
307322
@SanitizeExceptions()
308323
@NextIOValidator(GetNeedsInputActionArgs, getNeedsInputActionResult)
309324
@WithTypeCachableAsyncLocalStorage()
325+
@CaptureTimingWithStatsD()
310326
async getNeedsInput(args: { cartId: string }) {
311327
return await this.cartService.getNeedsInput(args.cartId);
312328
}
@@ -316,19 +332,22 @@ export class NextJSActionsService {
316332
})
317333
@NextIOValidator(SubmitNeedsInputActionArgs, undefined)
318334
@WithTypeCachableAsyncLocalStorage()
335+
@CaptureTimingWithStatsD()
319336
async submitNeedsInput(args: { cartId: string }) {
320337
await this.cartService.submitNeedsInput(args.cartId);
321338
}
322339

323340
@SanitizeExceptions()
324341
@NextIOValidator(undefined, GetMetricsFlowActionResult)
342+
@CaptureTimingWithStatsD()
325343
async getMetricsFlow() {
326344
return this.contentServerManager.getMetricsFlow();
327345
}
328346

329347
@SanitizeExceptions()
330348
@NextIOValidator(ValidatePostalCodeActionArgs, ValidatePostalCodeActionResult)
331349
@WithTypeCachableAsyncLocalStorage()
350+
@CaptureTimingWithStatsD()
332351
async validateAndFormatPostalCode(args: {
333352
postalCode: string;
334353
countryCode: string;
@@ -342,6 +361,7 @@ export class NextJSActionsService {
342361
@SanitizeExceptions()
343362
@NextIOValidator(DetermineCurrencyActionArgs, DetermineCurrencyActionResult)
344363
@WithTypeCachableAsyncLocalStorage()
364+
@CaptureTimingWithStatsD()
345365
async determineCurrency(args: { ip: string }) {
346366
const location = this.geodbManager.getTaxAddress(args.ip);
347367

@@ -361,6 +381,7 @@ export class NextJSActionsService {
361381
@SanitizeExceptions()
362382
@NextIOValidator(UpdateTaxAddressActionArgs, UpdateTaxAddressActionResult)
363383
@WithTypeCachableAsyncLocalStorage()
384+
@CaptureTimingWithStatsD()
364385
async updateTaxAddress(args: {
365386
cartId: string;
366387
version: number;
@@ -416,6 +437,7 @@ export class NextJSActionsService {
416437
@SanitizeExceptions()
417438
@NextIOValidator(ValidateLocationActionArgs, ValidateLocationActionResult)
418439
@WithTypeCachableAsyncLocalStorage()
440+
@CaptureTimingWithStatsD()
419441
async validateLocation(args: {
420442
offeringId: string;
421443
taxAddress?: TaxAddress;

libs/shared/error/src/lib/sanitizeExceptionsDecorator.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { Test, TestingModule } from '@nestjs/testing';
66
import { SanitizeExceptions } from './sanitizeExceptionsDecorator';
77
import { LOGGER_PROVIDER } from '@fxa/shared/log';
8+
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
89

910
// Mock Sentry
1011
jest.mock('@sentry/nextjs', () => ({
@@ -67,6 +68,7 @@ describe('SanitizeExceptions Decorator', () => {
6768
provide: LOGGER_PROVIDER,
6869
useValue: mockLogger,
6970
},
71+
MockStatsDProvider
7072
],
7173
}).compile();
7274

libs/shared/error/src/lib/sanitizeExceptionsDecorator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Inject } from '@nestjs/common';
77
import { ILogger } from '@fxa/shared/log';
88
import { LOGGER_PROVIDER } from '@fxa/shared/log';
99
import { GENERIC_ERROR_MESSAGE } from './error';
10+
import { StatsDService, type StatsD } from '@fxa/shared/metrics/statsd';
1011

1112
type Constructor<T> = new (...args: any[]) => T;
1213

@@ -20,13 +21,17 @@ export function SanitizeExceptions(
2021
{ allowlist }: { allowlist: Constructor<Error>[] } = { allowlist: [] }
2122
) {
2223
const injectLogger = Inject(LOGGER_PROVIDER);
24+
const injectStatsD = Inject(StatsDService);
2325

2426
return function (
2527
target: any,
2628
propertyKey: string,
2729
descriptor: PropertyDescriptor
2830
) {
2931
injectLogger(target, 'logger');
32+
if (!target.statsd) {
33+
injectStatsD(target, 'statsd');
34+
}
3035

3136
const originalMethod = descriptor.value;
3237
const isAsync = originalMethod.constructor.name === 'AsyncFunction';
@@ -43,6 +48,7 @@ export function SanitizeExceptions(
4348
methodName: propertyKey,
4449
allowlist,
4550
logger: this.logger,
51+
statsd: this.statsd,
4652
});
4753
}
4854
}
@@ -56,6 +62,7 @@ export function SanitizeExceptions(
5662
methodName: propertyKey,
5763
allowlist,
5864
logger: this.logger,
65+
statsd: this.statsd,
5966
});
6067
}
6168
};
@@ -70,6 +77,7 @@ function handleException(args: {
7077
methodName: string;
7178
allowlist: Constructor<Error>[];
7279
logger: ILogger;
80+
statsd: StatsD;
7381
}): Error {
7482
const { error, className, methodName, allowlist, logger } = args;
7583

@@ -98,5 +106,7 @@ function handleException(args: {
98106
},
99107
});
100108

109+
args.statsd.increment('unexpected_error_sanitized');
110+
101111
return new Error(GENERIC_ERROR_MESSAGE);
102112
}

0 commit comments

Comments
 (0)