Merge pull request #26198 from overleaf/jpa-recurly-metrics

[web] add metrics for recurly API usage

GitOrigin-RevId: 89840829f86ce1ff750d57f3445f279f4b151d6f
This commit is contained in:
Kristina
2025-06-09 08:05:24 +00:00
committed by Copybot
parent 7a449f4686
commit a9923fed4e
4 changed files with 83 additions and 2 deletions
@@ -22,6 +22,7 @@ const {
MissingBillingInfoError,
SubtotalLimitExceededError,
} = require('./Errors')
const RecurlyMetrics = require('./RecurlyMetrics')
/**
* @import { PaymentProviderSubscriptionChangeRequest } from './PaymentProviderEntities'
@@ -29,10 +30,28 @@ const {
* @import { PaymentMethod } from './types'
*/
class RecurlyClientWithErrorHandling extends recurly.Client {
/**
* @param {import('recurly/lib/recurly/Http').Response} response
* @return {Error | null}
* @private
*/
_errorFromResponse(response) {
RecurlyMetrics.recordMetrics(
response.status,
response.rateLimit,
response.rateLimitRemaining,
response.rateLimitReset.getTime()
)
// @ts-ignore
return super._errorFromResponse(response)
}
}
const recurlySettings = Settings.apis.recurly
const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined
const client = new recurly.Client(recurlyApiKey)
const client = new RecurlyClientWithErrorHandling(recurlyApiKey)
/**
* Get account for a given user
@@ -0,0 +1,38 @@
const Metrics = require('@overleaf/metrics')
/**
* @param {number} status
* @param {number} rateLimit
* @param {number} rateLimitRemaining
* @param {number} rateLimitReset
*/
function recordMetrics(status, rateLimit, rateLimitRemaining, rateLimitReset) {
Metrics.inc('recurly_request', 1, { status })
const metrics = { rateLimit, rateLimitRemaining, rateLimitReset }
for (const [method, v] of Object.entries(metrics)) {
if (Number.isNaN(v)) continue
Metrics.gauge('recurly_request_rate_limiting', v, 1, { method })
}
}
/**
* @param {Response} response
*/
function recordMetricsFromResponse(response) {
const rateLimit = parseInt(
response.headers.get('X-RateLimit-Limit') || '',
10
)
const rateLimitRemaining = parseInt(
response.headers.get('X-RateLimit-Remaining') || '',
10
)
const rateLimitReset =
parseInt(response.headers.get('X-RateLimit-Reset') || '', 10) * 1000
recordMetrics(response.status, rateLimit, rateLimitRemaining, rateLimitReset)
}
module.exports = {
recordMetrics,
recordMetricsFromResponse,
}
@@ -9,6 +9,7 @@ const logger = require('@overleaf/logger')
const Errors = require('../Errors/Errors')
const SubscriptionErrors = require('./Errors')
const { callbackify } = require('@overleaf/promise-utils')
const RecurlyMetrics = require('./RecurlyMetrics')
/**
* Updates the email address of a Recurly account
@@ -417,9 +418,15 @@ const promises = {
}
try {
return await fetchStringWithResponse(fetchUrl, fetchOptions)
const { body, response } = await fetchStringWithResponse(
fetchUrl,
fetchOptions
)
RecurlyMetrics.recordMetricsFromResponse(response)
return { body, response }
} catch (error) {
if (error instanceof RequestFailedError) {
RecurlyMetrics.recordMetricsFromResponse(error.response)
if (error.response.status === 404 && expect404) {
return { response: error.response, body: null }
} else if (error.response.status === 422 && expect422) {
@@ -7,6 +7,9 @@ class MockRecurlyApi extends AbstractMockApi {
this.mockSubscriptions = []
this.redemptions = {}
this.coupons = {}
this.rateLimitResetSeconds = Math.ceil(
(Date.now() + 24 * 60 * 60 * 1000) / 1000
)
}
addMockSubscription(recurlySubscription) {
@@ -25,7 +28,21 @@ class MockRecurlyApi extends AbstractMockApi {
)
}
getRateLimitHeaders() {
return {
'X-RateLimit-Limit': 1000,
'X-RateLimit-Remaining': 999,
'X-RateLimit-Reset': this.rateLimitResetSeconds,
}
}
applyRoutes() {
this.app.use((req, res, next) => {
for (const [name, v] of Object.entries(this.getRateLimitHeaders())) {
res.setHeader(name, v)
}
next()
})
this.app.get('/subscriptions/:id', (req, res) => {
const subscription = this.getMockSubscriptionById(req.params.id)
if (!subscription) {