From 8e46bc91545c80797a45858a42aedb45059afc9d Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Wed, 7 Aug 2024 20:37:15 +0800 Subject: [PATCH 1/2] feat: report subscription usage updates --- packages/core/package.json | 2 +- packages/core/src/libraries/quota.ts | 57 ++++++++++++++++--- .../core/src/middleware/koa-quota-guard.ts | 16 ++++++ .../src/routes/applications/application.ts | 9 +++ packages/core/src/routes/hook.ts | 15 ++++- .../src/routes/organization-role/index.ts | 16 +++++- .../src/routes/organization-scope/index.ts | 16 +++++- .../core/src/routes/organization/index.ts | 17 ++++-- packages/core/src/routes/resource.ts | 15 ++++- .../src/routes/sign-in-experience/index.ts | 4 +- .../core/src/routes/sso-connector/index.ts | 15 ++++- packages/core/src/test-utils/quota.ts | 1 + packages/core/src/utils/subscription/index.ts | 34 ++++++++++- packages/core/src/utils/subscription/types.ts | 18 ++++++ pnpm-lock.yaml | 15 ++++- 15 files changed, 223 insertions(+), 27 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index a7bc58c4a67..63a644f52e6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -95,7 +95,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@logto/cloud": "0.2.5-3b703da", + "@logto/cloud": "0.2.5-50ff8fe", "@silverhand/eslint-config": "6.0.1", "@silverhand/ts-config": "6.0.0", "@types/adm-zip": "^0.5.5", diff --git a/packages/core/src/libraries/quota.ts b/packages/core/src/libraries/quota.ts index d4da96ab427..c3f0823d670 100644 --- a/packages/core/src/libraries/quota.ts +++ b/packages/core/src/libraries/quota.ts @@ -7,8 +7,10 @@ import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; import { getTenantSubscriptionPlan, - getTenantSubscriptionQuotaAndUsage, + getTenantSubscriptionData, getTenantSubscriptionScopeUsage, + reportSubscriptionUpdates, + isReportSubscriptionUpdatesUsageKey, } from '#src/utils/subscription/index.js'; import { type SubscriptionQuota, type FeatureQuota } from '#src/utils/subscription/types.js'; @@ -144,7 +146,7 @@ export const createQuotaLibrary = ( }; const guardTenantUsageByKey = async (key: keyof SubscriptionQuota) => { - const { isCloud, isIntegrationTest } = EnvSet.values; + const { isCloud, isIntegrationTest, isDevFeaturesEnabled } = EnvSet.values; // Cloud only feature, skip in non-cloud environments if (!isCloud) { @@ -156,9 +158,20 @@ export const createQuotaLibrary = ( return; } - const { quota: fullQuota, usage: fullUsage } = await getTenantSubscriptionQuotaAndUsage( - cloudConnection - ); + const { + planId, + quota: fullQuota, + usage: fullUsage, + } = await getTenantSubscriptionData(cloudConnection); + + // Do not block Pro plan from adding add-on resources. + if ( + isDevFeaturesEnabled && + planId === ReservedPlanId.Pro && + isReportSubscriptionUpdatesUsageKey(key) + ) { + return; + } // Type `SubscriptionQuota` and type `SubscriptionUsage` are sharing keys, this design helps us to compare the usage with the quota limit in a easier way. const { [key]: limit } = fullQuota; @@ -227,7 +240,7 @@ export const createQuotaLibrary = ( }, scopeUsages, ] = await Promise.all([ - getTenantSubscriptionQuotaAndUsage(cloudConnection), + getTenantSubscriptionData(cloudConnection), getTenantSubscriptionScopeUsage(cloudConnection, entityName), ]); const usage = scopeUsages[entityId] ?? 0; @@ -262,5 +275,35 @@ export const createQuotaLibrary = ( ); }; - return { guardKey, guardTenantUsageByKey, guardEntityScopesUsage }; + const reportSubscriptionUpdatesUsage = async (key: keyof SubscriptionQuota) => { + const { isCloud, isIntegrationTest, isDevFeaturesEnabled } = EnvSet.values; + + // Cloud only feature, skip in non-cloud environments + if (!isCloud) { + return; + } + + // Disable in integration tests + if (isIntegrationTest) { + return; + } + + const { planId } = await getTenantSubscriptionData(cloudConnection); + + // Do not block Pro plan from adding add-on resources. + if ( + isDevFeaturesEnabled && + planId === ReservedPlanId.Pro && + isReportSubscriptionUpdatesUsageKey(key) + ) { + await reportSubscriptionUpdates(cloudConnection, key); + } + }; + + return { + guardKey, + guardTenantUsageByKey, + guardEntityScopesUsage, + reportSubscriptionUpdatesUsage, + }; }; diff --git a/packages/core/src/middleware/koa-quota-guard.ts b/packages/core/src/middleware/koa-quota-guard.ts index e17f875e799..8daba2154f2 100644 --- a/packages/core/src/middleware/koa-quota-guard.ts +++ b/packages/core/src/middleware/koa-quota-guard.ts @@ -1,3 +1,4 @@ +import { type Nullable } from '@silverhand/essentials'; import type { MiddlewareType } from 'koa'; import { type QuotaLibrary } from '#src/libraries/quota.js'; @@ -45,3 +46,18 @@ export function newKoaQuotaGuard({ return next(); }; } + +export function koaReportSubscriptionUpdates({ + key, + quota, + methods = ['POST', 'PUT', 'DELETE'], +}: NewUsageGuardConfig): MiddlewareType> { + return async (ctx, next) => { + await next(); + + // eslint-disable-next-line no-restricted-syntax + if (methods.includes(ctx.method.toUpperCase() as Method)) { + await quota.reportSubscriptionUpdatesUsage(key); + } + }; +} diff --git a/packages/core/src/routes/applications/application.ts b/packages/core/src/routes/applications/application.ts index f0adf3adcb3..bb78e839771 100644 --- a/packages/core/src/routes/applications/application.ts +++ b/packages/core/src/routes/applications/application.ts @@ -213,6 +213,11 @@ export default function applicationRoutes( } ctx.body = application; + + if (rest.type === ApplicationType.MachineToMachine) { + await quota.reportSubscriptionUpdatesUsage('machineToMachineLimit'); + } + return next(); } ); @@ -356,6 +361,10 @@ export default function applicationRoutes( await queries.applications.deleteApplicationById(id); ctx.status = 204; + if (type === ApplicationType.MachineToMachine) { + await quota.reportSubscriptionUpdatesUsage('machineToMachineLimit'); + } + return next(); } ); diff --git a/packages/core/src/routes/hook.ts b/packages/core/src/routes/hook.ts index e3dc0920b83..a6298d42c2f 100644 --- a/packages/core/src/routes/hook.ts +++ b/packages/core/src/routes/hook.ts @@ -18,7 +18,10 @@ import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; -import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; +import koaQuotaGuard, { + koaReportSubscriptionUpdates, + newKoaQuotaGuard, +} from '#src/middleware/koa-quota-guard.js'; import { type AllowedKeyPrefix } from '#src/queries/log.js'; import assertThat from '#src/utils/assert-that.js'; @@ -169,6 +172,11 @@ export default function hookRoutes( response: Hooks.guard, status: [201, 400], }), + koaReportSubscriptionUpdates({ + key: 'hooksLimit', + quota, + methods: ['POST'], + }), async (ctx, next) => { const { event, events, enabled, ...rest } = ctx.guard.body; assertThat(events ?? event, new RequestError({ code: 'hook.missing_events', status: 400 })); @@ -257,6 +265,11 @@ export default function hookRoutes( router.delete( '/hooks/:id', koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }), + koaReportSubscriptionUpdates({ + key: 'hooksLimit', + quota, + methods: ['DELETE'], + }), async (ctx, next) => { const { id } = ctx.guard.params; await deleteHookById(id); diff --git a/packages/core/src/routes/organization-role/index.ts b/packages/core/src/routes/organization-role/index.ts index 84fef012ce7..00636750dfc 100644 --- a/packages/core/src/routes/organization-role/index.ts +++ b/packages/core/src/routes/organization-role/index.ts @@ -6,13 +6,17 @@ import { type OrganizationRoleKeys, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; +import { condArray } from '@silverhand/essentials'; import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; import { buildManagementApiContext } from '#src/libraries/hook/utils.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; -import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; +import koaQuotaGuard, { + koaReportSubscriptionUpdates, + newKoaQuotaGuard, +} from '#src/middleware/koa-quota-guard.js'; import { organizationRoleSearchKeys } from '#src/queries/organization/index.js'; import SchemaRouter from '#src/utils/SchemaRouter.js'; import { parseSearchOptions } from '#src/utils/search.js'; @@ -45,11 +49,17 @@ export default function organizationRoleRoutes( unknown, ManagementApiRouterContext >(OrganizationRoles, roles, { - middlewares: [ + middlewares: condArray( EnvSet.values.isDevFeaturesEnabled ? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }) : koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }), - ], + EnvSet.values.isDevFeaturesEnabled && + koaReportSubscriptionUpdates({ + key: 'organizationsEnabled', + quota, + methods: ['POST', 'PUT', 'DELETE'], + }) + ), disabled: { get: true, post: true }, errorHandler, searchFields: ['name'], diff --git a/packages/core/src/routes/organization-scope/index.ts b/packages/core/src/routes/organization-scope/index.ts index 5d40fc6c55f..1c1f321fc18 100644 --- a/packages/core/src/routes/organization-scope/index.ts +++ b/packages/core/src/routes/organization-scope/index.ts @@ -1,7 +1,11 @@ import { OrganizationScopes } from '@logto/schemas'; +import { condArray } from '@silverhand/essentials'; import { EnvSet } from '#src/env-set/index.js'; -import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; +import koaQuotaGuard, { + newKoaQuotaGuard, + koaReportSubscriptionUpdates, +} from '#src/middleware/koa-quota-guard.js'; import SchemaRouter from '#src/utils/SchemaRouter.js'; import { errorHandler } from '../organization/utils.js'; @@ -19,11 +23,17 @@ export default function organizationScopeRoutes( ]: RouterInitArgs ) { const router = new SchemaRouter(OrganizationScopes, scopes, { - middlewares: [ + middlewares: condArray( EnvSet.values.isDevFeaturesEnabled ? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }) : koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }), - ], + EnvSet.values.isDevFeaturesEnabled && + koaReportSubscriptionUpdates({ + key: 'organizationsEnabled', + quota, + methods: ['POST', 'PUT', 'DELETE'], + }) + ), errorHandler, searchFields: ['name'], }); diff --git a/packages/core/src/routes/organization/index.ts b/packages/core/src/routes/organization/index.ts index b548485a720..045f3c2d337 100644 --- a/packages/core/src/routes/organization/index.ts +++ b/packages/core/src/routes/organization/index.ts @@ -1,11 +1,14 @@ import { type OrganizationWithFeatured, Organizations, featuredUserGuard } from '@logto/schemas'; -import { yes } from '@silverhand/essentials'; +import { condArray, yes } from '@silverhand/essentials'; import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; -import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; +import koaQuotaGuard, { + newKoaQuotaGuard, + koaReportSubscriptionUpdates, +} from '#src/middleware/koa-quota-guard.js'; import SchemaRouter from '#src/utils/SchemaRouter.js'; import { parseSearchOptions } from '#src/utils/search.js'; @@ -31,11 +34,17 @@ export default function organizationRoutes( ] = args; const router = new SchemaRouter(Organizations, organizations, { - middlewares: [ + middlewares: condArray( EnvSet.values.isDevFeaturesEnabled ? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }) : koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }), - ], + EnvSet.values.isDevFeaturesEnabled && + koaReportSubscriptionUpdates({ + key: 'organizationsEnabled', + quota, + methods: ['POST', 'PUT', 'DELETE'], + }) + ), errorHandler, searchFields: ['name'], disabled: { get: true }, diff --git a/packages/core/src/routes/resource.ts b/packages/core/src/routes/resource.ts index 1553cb5c7a2..486a703d88b 100644 --- a/packages/core/src/routes/resource.ts +++ b/packages/core/src/routes/resource.ts @@ -7,7 +7,10 @@ import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; -import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; +import koaQuotaGuard, { + newKoaQuotaGuard, + koaReportSubscriptionUpdates, +} from '#src/middleware/koa-quota-guard.js'; import assertThat from '#src/utils/assert-that.js'; import { attachScopesToResources } from '#src/utils/resource.js'; @@ -87,6 +90,11 @@ export default function resourceRoutes( response: Resources.guard.extend({ scopes: Scopes.guard.array().optional() }), status: [201, 422], }), + koaReportSubscriptionUpdates({ + key: 'resourcesLimit', + quota, + methods: ['POST'], + }), async (ctx, next) => { const { body } = ctx.guard; const { indicator } = body; @@ -184,6 +192,11 @@ export default function resourceRoutes( router.delete( '/resources/:id', koaGuard({ params: object({ id: string().min(1) }), status: [204, 400, 404] }), + koaReportSubscriptionUpdates({ + key: 'resourcesLimit', + quota, + methods: ['DELETE'], + }), async (ctx, next) => { const { id } = ctx.guard.params; diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts index b698da24a19..f537e7b42e1 100644 --- a/packages/core/src/routes/sign-in-experience/index.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -19,7 +19,7 @@ export default function signInExperiencesRoutes( const { deleteConnectorById } = queries.connectors; const { signInExperiences: { validateLanguageInfo }, - quota: { guardKey, guardTenantUsageByKey }, + quota: { guardKey, guardTenantUsageByKey, reportSubscriptionUpdatesUsage }, } = libraries; const { getLogtoConnectors } = connectors; @@ -122,6 +122,8 @@ export default function signInExperiencesRoutes( : rest ); + await reportSubscriptionUpdatesUsage('mfaEnabled'); + return next(); } ); diff --git a/packages/core/src/routes/sso-connector/index.ts b/packages/core/src/routes/sso-connector/index.ts index cf611fbc65a..b12e5372bdc 100644 --- a/packages/core/src/routes/sso-connector/index.ts +++ b/packages/core/src/routes/sso-connector/index.ts @@ -11,7 +11,10 @@ import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; -import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js'; +import koaQuotaGuard, { + koaReportSubscriptionUpdates, + newKoaQuotaGuard, +} from '#src/middleware/koa-quota-guard.js'; import { ssoConnectorCreateGuard, ssoConnectorPatchGuard } from '#src/routes/sso-connector/type.js'; import { ssoConnectorFactories } from '#src/sso/index.js'; import { isSupportedSsoConnector, isSupportedSsoProvider } from '#src/sso/utils.js'; @@ -77,6 +80,11 @@ export default function singleSignOnConnectorsRoutes { const { body } = ctx.guard; const { providerName, connectorName, config, domains, ...rest } = body; @@ -202,6 +210,11 @@ export default function singleSignOnConnectorsRoutes { const { id } = ctx.guard.params; diff --git a/packages/core/src/test-utils/quota.ts b/packages/core/src/test-utils/quota.ts index d995f17b9ef..81386bbfbc5 100644 --- a/packages/core/src/test-utils/quota.ts +++ b/packages/core/src/test-utils/quota.ts @@ -7,5 +7,6 @@ export const createMockQuotaLibrary = (): QuotaLibrary => { guardKey: jest.fn(), guardTenantUsageByKey: jest.fn(), guardEntityScopesUsage: jest.fn(), + reportSubscriptionUpdatesUsage: jest.fn(), }; }; diff --git a/packages/core/src/utils/subscription/index.ts b/packages/core/src/utils/subscription/index.ts index c6e701b6239..6f5e97e81ac 100644 --- a/packages/core/src/utils/subscription/index.ts +++ b/packages/core/src/utils/subscription/index.ts @@ -1,3 +1,5 @@ +import { trySafe } from '@silverhand/essentials'; + import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'; import assertThat from '../assert-that.js'; @@ -7,6 +9,8 @@ import { type SubscriptionUsage, type SubscriptionPlan, type Subscription, + type ReportSubscriptionUpdatesUsageKey, + allReportSubscriptionUpdatesUsageKeys, } from './types.js'; export const getTenantSubscription = async ( @@ -33,19 +37,21 @@ export const getTenantSubscriptionPlan = async ( return plan; }; -export const getTenantSubscriptionQuotaAndUsage = async ( +export const getTenantSubscriptionData = async ( cloudConnection: CloudConnectionLibrary ): Promise<{ + planId: string; quota: SubscriptionQuota; usage: SubscriptionUsage; }> => { const client = await cloudConnection.getClient(); - const [quota, usage] = await Promise.all([ + const [{ planId }, quota, usage] = await Promise.all([ + client.get('/api/tenants/my/subscription'), client.get('/api/tenants/my/subscription/quota'), client.get('/api/tenants/my/subscription/usage'), ]); - return { quota, usage }; + return { planId, quota, usage }; }; export const getTenantSubscriptionScopeUsage = async ( @@ -60,3 +66,25 @@ export const getTenantSubscriptionScopeUsage = async ( return scopeUsages; }; + +export const reportSubscriptionUpdates = async ( + cloudConnection: CloudConnectionLibrary, + usageKey: ReportSubscriptionUpdatesUsageKey +): Promise => { + const client = await cloudConnection.getClient(); + // We only report to the Cloud to notify the resource usage updates, and do not care the response. We will see error logs on the Cloud side if there is any issue. + await trySafe( + client.post('/api/tenants/my/subscription/item-updates', { + body: { + usageKey, + }, + }) + ); +}; + +export const isReportSubscriptionUpdatesUsageKey = ( + value: string +): value is ReportSubscriptionUpdatesUsageKey => { + // eslint-disable-next-line no-restricted-syntax + return allReportSubscriptionUpdatesUsageKeys.includes(value as ReportSubscriptionUpdatesUsageKey); +}; diff --git a/packages/core/src/utils/subscription/types.ts b/packages/core/src/utils/subscription/types.ts index e1ae1b6d15a..74f9f138b60 100644 --- a/packages/core/src/utils/subscription/types.ts +++ b/packages/core/src/utils/subscription/types.ts @@ -3,9 +3,12 @@ import { type RouterRoutes } from '@withtyped/client'; import { type z, type ZodType } from 'zod'; type GetRoutes = RouterRoutes['get']; +type PostRoutes = RouterRoutes['post']; type RouteResponseType = z.infer>; +type RouteRequestBodyType = + z.infer>; export type SubscriptionPlan = RouteResponseType[number]; @@ -37,3 +40,18 @@ export type SubscriptionQuota = Omit< export type SubscriptionUsage = RouteResponseType< GetRoutes['/api/tenants/:tenantId/subscription/usage'] >; + +export type ReportSubscriptionUpdatesUsageKey = RouteRequestBodyType< + PostRoutes['/api/tenants/my/subscription/item-updates'] +>['usageKey']; + +// Have to manually define this variable since we can only get the literal union from the @logto/cloud/routes module. +export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([ + 'tokenLimit', + 'machineToMachineLimit', + 'resourcesLimit', + 'mfaEnabled', + 'organizationsEnabled', + 'tenantMembersLimit', + 'enterpriseSsoLimit', +]) satisfies readonly ReportSubscriptionUpdatesUsageKey[]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f34ad8f3afd..3c9cc2ec0b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2890,8 +2890,8 @@ importers: version: 3.23.8 devDependencies: '@logto/cloud': - specifier: 0.2.5-3b703da - version: 0.2.5-3b703da(zod@3.23.8) + specifier: 0.2.5-50ff8fe + version: 0.2.5-50ff8fe(zod@3.23.8) '@silverhand/eslint-config': specifier: 6.0.1 version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3) @@ -5022,6 +5022,10 @@ packages: resolution: {integrity: sha512-VCevQnxP5910s/cDYAxoJRim9iH1yN/La0HAlOP6FhVGtZofYwTTfT9AQXC+dZScgydpcFWo4k/6MYOFRtZCLg==} engines: {node: ^20.9.0} + '@logto/cloud@0.2.5-50ff8fe': + resolution: {integrity: sha512-EMIGnx3swILEcSvYsAlPg9E1srtPcZxHxVH+D/dTrg8ctHbRAJkFbeuQFhwHGvs1dfgULd9MKtaAkL2qckExMw==} + engines: {node: ^20.9.0} + '@logto/cloud@0.2.5-923c26f': resolution: {integrity: sha512-NAK9/T7HxEfE2djO6VTekMziOXH6NtbAzwumZcZo0bqIUDGiKlUvted/KY6iqpCdfFOF4aIyKp+pvlQIjj1T6Q==} engines: {node: ^20.9.0} @@ -14660,6 +14664,13 @@ snapshots: transitivePeerDependencies: - zod + '@logto/cloud@0.2.5-50ff8fe(zod@3.23.8)': + dependencies: + '@silverhand/essentials': 2.9.1 + '@withtyped/server': 0.13.6(zod@3.23.8) + transitivePeerDependencies: + - zod + '@logto/cloud@0.2.5-923c26f(zod@3.23.8)': dependencies: '@silverhand/essentials': 2.9.1 From 3f13b777fec16fecfc24ccbfa331022461fb59b2 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Thu, 8 Aug 2024 11:09:50 +0800 Subject: [PATCH 2/2] refactor: refactor code according to CR --- packages/core/src/libraries/quota.ts | 22 ++++++++----------- packages/core/src/utils/subscription/index.ts | 6 ++++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core/src/libraries/quota.ts b/packages/core/src/libraries/quota.ts index c3f0823d670..f3338877aa5 100644 --- a/packages/core/src/libraries/quota.ts +++ b/packages/core/src/libraries/quota.ts @@ -23,6 +23,11 @@ const notNumber = (): never => { throw new Error('Only support usage query for numeric quota'); }; +const shouldReportSubscriptionUpdates = (planId: string, key: keyof SubscriptionQuota): boolean => + EnvSet.values.isDevFeaturesEnabled && + planId === ReservedPlanId.Pro && + isReportSubscriptionUpdatesUsageKey(key); + export const createQuotaLibrary = ( queries: Queries, cloudConnection: CloudConnectionLibrary, @@ -146,7 +151,7 @@ export const createQuotaLibrary = ( }; const guardTenantUsageByKey = async (key: keyof SubscriptionQuota) => { - const { isCloud, isIntegrationTest, isDevFeaturesEnabled } = EnvSet.values; + const { isCloud, isIntegrationTest } = EnvSet.values; // Cloud only feature, skip in non-cloud environments if (!isCloud) { @@ -165,11 +170,7 @@ export const createQuotaLibrary = ( } = await getTenantSubscriptionData(cloudConnection); // Do not block Pro plan from adding add-on resources. - if ( - isDevFeaturesEnabled && - planId === ReservedPlanId.Pro && - isReportSubscriptionUpdatesUsageKey(key) - ) { + if (shouldReportSubscriptionUpdates(planId, key)) { return; } @@ -276,7 +277,7 @@ export const createQuotaLibrary = ( }; const reportSubscriptionUpdatesUsage = async (key: keyof SubscriptionQuota) => { - const { isCloud, isIntegrationTest, isDevFeaturesEnabled } = EnvSet.values; + const { isCloud, isIntegrationTest } = EnvSet.values; // Cloud only feature, skip in non-cloud environments if (!isCloud) { @@ -290,12 +291,7 @@ export const createQuotaLibrary = ( const { planId } = await getTenantSubscriptionData(cloudConnection); - // Do not block Pro plan from adding add-on resources. - if ( - isDevFeaturesEnabled && - planId === ReservedPlanId.Pro && - isReportSubscriptionUpdatesUsageKey(key) - ) { + if (shouldReportSubscriptionUpdates(planId, key)) { await reportSubscriptionUpdates(cloudConnection, key); } }; diff --git a/packages/core/src/utils/subscription/index.ts b/packages/core/src/utils/subscription/index.ts index 6f5e97e81ac..303cb8a95dd 100644 --- a/packages/core/src/utils/subscription/index.ts +++ b/packages/core/src/utils/subscription/index.ts @@ -69,8 +69,12 @@ export const getTenantSubscriptionScopeUsage = async ( export const reportSubscriptionUpdates = async ( cloudConnection: CloudConnectionLibrary, - usageKey: ReportSubscriptionUpdatesUsageKey + usageKey: keyof SubscriptionQuota ): Promise => { + if (!isReportSubscriptionUpdatesUsageKey(usageKey)) { + return; + } + const client = await cloudConnection.getClient(); // We only report to the Cloud to notify the resource usage updates, and do not care the response. We will see error logs on the Cloud side if there is any issue. await trySafe(