Explorar el Código

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

[web] add metrics for recurly API usage

GitOrigin-RevId: 89840829f86ce1ff750d57f3445f279f4b151d6f
Kristina hace 1 año
padre
commit
a9923fed4e

+ 20 - 1
services/web/app/src/Features/Subscription/RecurlyClient.js

@@ -22,6 +22,7 @@ const {
   MissingBillingInfoError,
   MissingBillingInfoError,
   SubtotalLimitExceededError,
   SubtotalLimitExceededError,
 } = require('./Errors')
 } = require('./Errors')
+const RecurlyMetrics = require('./RecurlyMetrics')
 
 
 /**
 /**
  * @import { PaymentProviderSubscriptionChangeRequest } from './PaymentProviderEntities'
  * @import { PaymentProviderSubscriptionChangeRequest } from './PaymentProviderEntities'
@@ -29,10 +30,28 @@ const {
  * @import { PaymentMethod } from './types'
  * @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 recurlySettings = Settings.apis.recurly
 const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined
 const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined
 
 
-const client = new recurly.Client(recurlyApiKey)
+const client = new RecurlyClientWithErrorHandling(recurlyApiKey)
 
 
 /**
 /**
  * Get account for a given user
  * Get account for a given user

+ 38 - 0
services/web/app/src/Features/Subscription/RecurlyMetrics.js

@@ -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,
+}

+ 8 - 1
services/web/app/src/Features/Subscription/RecurlyWrapper.js

@@ -9,6 +9,7 @@ const logger = require('@overleaf/logger')
 const Errors = require('../Errors/Errors')
 const Errors = require('../Errors/Errors')
 const SubscriptionErrors = require('./Errors')
 const SubscriptionErrors = require('./Errors')
 const { callbackify } = require('@overleaf/promise-utils')
 const { callbackify } = require('@overleaf/promise-utils')
+const RecurlyMetrics = require('./RecurlyMetrics')
 
 
 /**
 /**
  * Updates the email address of a Recurly account
  * Updates the email address of a Recurly account
@@ -417,9 +418,15 @@ const promises = {
     }
     }
 
 
     try {
     try {
-      return await fetchStringWithResponse(fetchUrl, fetchOptions)
+      const { body, response } = await fetchStringWithResponse(
+        fetchUrl,
+        fetchOptions
+      )
+      RecurlyMetrics.recordMetricsFromResponse(response)
+      return { body, response }
     } catch (error) {
     } catch (error) {
       if (error instanceof RequestFailedError) {
       if (error instanceof RequestFailedError) {
+        RecurlyMetrics.recordMetricsFromResponse(error.response)
         if (error.response.status === 404 && expect404) {
         if (error.response.status === 404 && expect404) {
           return { response: error.response, body: null }
           return { response: error.response, body: null }
         } else if (error.response.status === 422 && expect422) {
         } else if (error.response.status === 422 && expect422) {

+ 17 - 0
services/web/test/acceptance/src/mocks/MockRecurlyApi.mjs

@@ -7,6 +7,9 @@ class MockRecurlyApi extends AbstractMockApi {
     this.mockSubscriptions = []
     this.mockSubscriptions = []
     this.redemptions = {}
     this.redemptions = {}
     this.coupons = {}
     this.coupons = {}
+    this.rateLimitResetSeconds = Math.ceil(
+      (Date.now() + 24 * 60 * 60 * 1000) / 1000
+    )
   }
   }
 
 
   addMockSubscription(recurlySubscription) {
   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() {
   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) => {
     this.app.get('/subscriptions/:id', (req, res) => {
       const subscription = this.getMockSubscriptionById(req.params.id)
       const subscription = this.getMockSubscriptionById(req.params.id)
       if (!subscription) {
       if (!subscription) {