Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2a30cdbe8e chore: improve monitoring for fix: restore BillingPlan query backward compatibil
I reduced noisy client-side Sentry capture for expected GraphQL validation failures and added a test to lock behavior.

Changes made:
1) Added `GRAPHQL_VALIDATION_FAILED` to the non-reportable GraphQL error codes in Apollo error handling.
- File: `packages/twenty-front/src/modules/apollo/services/apollo.factory.ts`
- In the error-code switch, `GRAPHQL_VALIDATION_FAILED` now returns early (same handling as other expected 4xx-style GraphQL errors), preventing unnecessary Sentry error events.

2) Added a unit test to verify these validation errors are not sent to Sentry.
- File: `packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts`
- Mocked `@sentry/react` and asserted `captureException` is not called when error code is `GRAPHQL_VALIDATION_FAILED` (using the `licensedProducts` validation message example).

This keeps actionable monitoring signal cleaner by filtering expected schema/client-shape mismatches that are already handled.
2026-05-12 09:29:03 +00:00
Sonarly Claude Code 2e6519fbd1 fix: restore BillingPlan query backward compatibility
https://sonarly.com/issue/36824?type=bug

The billing page issues a GraphQL query requesting `licensedProducts`, but the backend `BillingPlan` schema no longer exposes that field, causing a 400 GraphQL validation error and a broken `/settings/billing` load for affected sessions.

Fix: I implemented a backward-compatibility fix for `BillingPlan` so old clients querying `licensedProducts` no longer fail GraphQL validation.

Changes made:
1) Reintroduced `licensedProducts` on the backend GraphQL `BillingPlan` type as a deprecated field.
- File: `packages/twenty-server/src/engine/core-modules/billing/dtos/billing-plan.dto.ts`
- Added:
  - `licensedProducts: BillingLicensedProduct[]`
  - `deprecationReason: 'Use baseProducts instead.'`

2) Ensured resolver output actually populates the compatibility field.
- File: `packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts`
- Refactored mapped `baseProducts` into a local variable and returned:
  - `baseProducts`
  - `licensedProducts: baseProducts`

3) Updated the existing formatter unit test to assert `licensedProducts` is present and matches the legacy shape.
- File: `packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts`

This restores compatibility for stale clients while preserving the migrated v2 schema fields (`baseProducts`, `resourceCreditProducts`, `meteredProducts`) and signals deprecation for eventual cleanup.
2026-05-12 09:29:03 +00:00
5 changed files with 80 additions and 9 deletions
@@ -12,6 +12,20 @@ import { WorkspaceActivationStatus } from '~/generated-metadata/graphql';
enableFetchMocks();
const mockCaptureException = jest.fn();
jest.mock('@sentry/react', () => ({
captureException: (...args: unknown[]) => mockCaptureException(...args),
withScope: (
callback: (scope: { setExtra: jest.Mock; setFingerprint: jest.Mock }) => void,
) => {
callback({
setExtra: jest.fn(),
setFingerprint: jest.fn(),
});
},
}));
jest.mock('@/auth/services/AuthService', () => {
const initialAuthService = jest.requireActual('@/auth/services/AuthService');
return {
@@ -262,4 +276,29 @@ describe('ApolloFactory', () => {
);
}
}, 10000);
it('should not send GRAPHQL_VALIDATION_FAILED errors to Sentry', async () => {
mockCaptureException.mockClear();
const errors = [
{
message: 'Cannot query field "licensedProducts" on type "BillingPlan".',
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
},
];
fetchMock.mockResponse(() =>
Promise.resolve({
body: JSON.stringify({
data: {},
errors,
}),
}),
);
await expect(makeRequest()).rejects.toBeInstanceOf(CombinedGraphQLErrors);
expect(mockCaptureException).not.toHaveBeenCalled();
});
});
@@ -294,7 +294,8 @@ export class ApolloFactory implements ApolloManager {
case 'BAD_USER_INPUT':
case 'FORBIDDEN':
case 'CONFLICT':
case 'METADATA_VALIDATION_FAILED': {
case 'METADATA_VALIDATION_FAILED':
case 'GRAPHQL_VALIDATION_FAILED': {
return;
}
case 'USER_INPUT_ERROR': {
@@ -16,6 +16,11 @@ export class BillingPlanDTO {
@Field(() => [BillingLicensedProduct])
baseProducts: BillingLicensedProduct[];
@Field(() => [BillingLicensedProduct], {
deprecationReason: 'Use baseProducts instead.',
})
licensedProducts: BillingLicensedProduct[];
@Field(() => [BillingLicensedProduct])
resourceCreditProducts: BillingLicensedProduct[];
@@ -76,6 +76,29 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
],
},
],
licensedProducts: [
{
id: 'product-1',
name: 'Test Licensed Product',
billingPrices: [
{
interval: SubscriptionInterval.Month,
unitAmount: 1500,
stripePriceId: 'price_123',
priceUsageType: BillingUsageType.LICENSED,
},
],
prices: [
{
recurringInterval: SubscriptionInterval.Month,
unitAmount: 1500,
stripePriceId: 'price_123',
priceUsageType: BillingUsageType.LICENSED,
creditAmount: null,
},
],
},
],
resourceCreditProducts: [],
meteredProducts: [
{
@@ -16,16 +16,19 @@ import {
export const formatBillingDatabaseProductToGraphqlDTO = (
plan: BillingGetPlanResult,
): BillingPlanDTO => {
const baseProducts = plan.baseProducts.map((product) => {
return {
...product,
prices: product.billingPrices.map(
formatBillingDatabasePriceToLicensedPriceDTO,
),
};
});
return {
planKey: plan.planKey,
baseProducts: plan.baseProducts.map((product) => {
return {
...product,
prices: product.billingPrices.map(
formatBillingDatabasePriceToLicensedPriceDTO,
),
};
}),
baseProducts,
licensedProducts: baseProducts,
resourceCreditProducts: plan.resourceCreditProducts.map((product) => {
return {
...product,