Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 1ad1685d53 Gmail "Precondition check failed" error not recognized as service-disabled
https://sonarly.com/issue/5372?type=bug

The `isServiceNotEnabledError()` method in v1.18.1 only matches Gmail API errors containing "service not enabled" but not the "Precondition check failed." variant, causing the Google account connection flow to fail entirely for users whose Google Workspace has Gmail disabled.

Fix: **Simplified `isServiceNotEnabledError` to match on `reason` field only, removing fragile message substring matching.**

The root cause was that the `isServiceNotEnabledError()` method relied on substring matching against Google API error messages (`"service not enabled"`, `"precondition check failed"`). This is fragile — Google can return different message variants for the same condition.

The previous fix (commit `e7f958c9cf`, not yet deployed) added a second substring check for `"precondition check failed"`. This change goes further by removing ALL message substring checks and relying solely on `firstError.reason === 'failedPrecondition'`, which is the structured/stable field in the Google API error response.

This handles:
- `"Mail service not enabled"` (original case)
- `"Precondition check failed."` (the Sentry error case)
- Any future `failedPrecondition` message variants

The existing test suite validates all three scenarios pass correctly.
2026-03-07 15:52:37 +00:00
Sonarly Claude Code bf6c1e2cc0 chore: additional changes for FK violation during calendar event association INS 2026-03-07 15:11:40 +00:00
Sonarly Claude Code 6ac2f09a53 chore: improve monitoring for FK violation during calendar event association INS
**Problem**: Commit `d4303469ff5` replaced all PostgreSQL error messages with the generic string `"Data validation error."` in `computeTwentyORMException`. This made it impossible to distinguish between different types of database errors (FK violations, check violations, not-null violations, etc.) in Sentry alerts and logs. The error code is the only diagnostic signal, but it was only stored in the `PostgresException.code` property — not visible in the error message that appears in Sentry.

**Fix**: Changed the generic message from `"Data validation error."` to `"Data validation error (code: ${errorCode})."` which includes the specific PostgreSQL error code (e.g., `23503` for FK violations, `23505` for unique violations). This preserves the sanitization intent (no SQL, table names, or column names are exposed) while providing enough diagnostic information to identify the specific database constraint that was violated. For example, the Sentry error will now show `"Unknown error ... : Data validation error (code: 23503)."` instead of the uninformative `"Data validation error."`.
2026-03-07 15:11:38 +00:00
Sonarly Claude Code 5843d1d4cd FK violation during calendar event association INSERT due to concurrent cleanup race
https://sonarly.com/issue/3806?type=bug

A race condition between concurrent calendar sync jobs causes a foreign key violation when inserting `calendarChannelEventAssociation` records. A concurrent hard-delete by the calendar event cleaner removes referenced `calendarEvent` rows between the find and insert within the same transaction, causing a PostgreSQL FK violation (23503) wrapped as a generic "Data validation error."

Fix: **Problem**: When a PostgreSQL foreign key violation (code `23503`) occurs during `calendarChannelEventAssociation` INSERT — caused by a race condition with the concurrent calendar event cleaner hard-deleting referenced events — the `PostgresException` with code `'23503'` doesn't match any recognized exception code in the calendar error handler's switch statement. It falls to the `default` case, which permanently marks the calendar channel as `FAILED_UNKNOWN`, stopping sync entirely.

**Fix**: Added `POSTGRESQL_ERROR_CODES.FOREIGN_KEY_VIOLATION` (`'23503'`) as a case in the `handleDriverException` switch statement, routing it to `handleTemporaryException`. This treats FK violations as transient errors (which they are — caused by a concurrent cleanup race), enabling retry with exponential backoff via the existing `throttleFailureCount` mechanism. On retry, the `find()` query will see the current database state and the race condition is unlikely to recur.

This follows the existing pattern where `TwentyORMExceptionCode.QUERY_READ_TIMEOUT` is also routed to `handleTemporaryException`.
2026-03-07 15:11:38 +00:00
Sonarly Claude Code 1b7e41f650 Empty filter object crashes query parser when workspace member state is not loaded
https://sonarly.com/issue/7575?type=bug

When `currentWorkspaceMember` is null/undefined during page initialization, the frontend sends `{ accountOwnerId: {} }` as a filter (because `JSON.stringify` drops the `undefined` `eq` value), which crashes the server-side query parser that assumes filter objects always contain at least one operator entry.

Fix: ## Changes

### Backend: Defensive guard at crash site (1 file)

**`graphql-query-filter-field.parser.ts`** — Added a guard before the destructuring on line 72 that checks `filterEntries.length !== 1` and throws a proper `GraphqlQueryRunnerException` instead of letting an unhandled `TypeError` crash the process. This is consistent with the validation pattern already established in the newer `validateAndTransformOperatorAndValue` utility.

```typescript
// Before: crashes with TypeError when filterValue is {}
const [[operator, value]] = Object.entries(filterValue);

// After: explicit validation with descriptive error
const filterEntries = Object.entries(filterValue);
if (filterEntries.length !== 1) {
  throw new GraphqlQueryRunnerException(
    `Filter for field "${key}" must have exactly one operator, received ${filterEntries.length}`,
    GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
    { userFriendlyMessage: msg`Invalid filter value for field "${key}"` },
  );
}
const [[operator, value]] = filterEntries;
```

### Frontend: Prevent query with invalid filter (3 files)

**`SettingsAccounts.tsx`**, **`SettingsAccountsMessageChannelsContainer.tsx`**, **`SettingsAccountsCalendarChannelsContainer.tsx`** — Added `skip: !currentWorkspaceMember?.id` to each `useFindManyRecords` call. This prevents the GraphQL query from firing when `currentWorkspaceMember` is null/undefined (e.g., during initial page load), which would otherwise serialize `{ eq: undefined }` as `{}` after JSON stringification and send an empty filter object to the server.
2026-03-07 12:08:04 +00:00
Sonarly Claude Code 0df60256ba chore: improve monitoring for Race condition: debounced save fires while workflo
Applied the same `isReady` guard to `useUpdateWorkflowVersionTrigger`, which has the identical race condition pattern — it's called from debounced trigger save callbacks that can fire during transient workflow data loading states. Without this fix, the same "Failed to get updatable workflow version" error would generate noisy Sentry events from trigger editing flows as well.

The code fix itself is the primary monitoring improvement: by preventing the throw during expected transient states, it eliminates the noisy Sentry error events (currently tagged `handled: yes, mechanism: generic`) that provide no actionable signal. The remaining throw in `getUpdatableWorkflowVersion()` still fires for genuine configuration errors (e.g., missing `workflowVisualizerWorkflowId`), preserving alerting for real bugs.
2026-03-07 10:15:04 +00:00
Sonarly Claude Code de31a2822c Race condition: debounced save fires while workflow data is transiently undefined during refetch
https://sonarly.com/issue/5150?type=bug

The `useGetUpdatableWorkflowVersionOrThrow` hook throws "Failed to get updatable workflow version" when a debounced save callback fires during a transient window where `useWorkflowWithCurrentVersion` returns `undefined` — typically caused by an SSE-triggered workflow refetch that changes the version ID and causes a cache miss on the second sequential Apollo query.

Fix: Added an `isReady` boolean to the return value of `useGetUpdatableWorkflowVersionOrThrow` that callers can check synchronously before calling the async `getUpdatableWorkflowVersion()`. This flag is `true` only when both `workflowVisualizerWorkflowId` and `workflow` are defined (i.e., not in a transient loading/refetch state).

Updated `useUpdateStep` and `useUpdateWorkflowVersionTrigger` — the two hooks called from debounced save callbacks — to check `isReady` and return early when the workflow data is temporarily unavailable. This prevents the error from being thrown during the transient window when `useWorkflowWithCurrentVersion` returns `undefined` (e.g., after an SSE-triggered refetch changes the version ID and the second Apollo query has a cache miss).

The `isReady` flag is captured in the same React render cycle as the `workflow` value, so they're always consistent in the closure. When the debounced callback fires, `useDebouncedCallback`'s ref points to the latest callback which captures the latest `isReady` value. If `isReady` is `false`, the save is silently skipped. Once the data loads and the component re-renders with `isReady = true`, the next debounced save succeeds.
2026-03-07 10:15:03 +00:00
@@ -65,7 +65,7 @@ export class GoogleApisServiceAvailabilityService {
return false;
}
this.logger.error('Error checking messaging availability', error);
this.logger.warn('Error checking messaging availability', error);
throw error;
}
@@ -99,7 +99,7 @@ export class GoogleApisServiceAvailabilityService {
return false;
}
this.logger.error('Error checking Calendar availability', error);
this.logger.warn('Error checking Calendar availability', error);
throw error;
}
@@ -124,19 +124,6 @@ export class GoogleApisServiceAvailabilityService {
return false;
}
const isFailedPrecondition = firstError.reason === 'failedPrecondition';
const isServiceNotEnabled =
firstError.message?.toLowerCase()?.includes('service not enabled') ??
false;
const isPreconditionCheckFailed =
firstError.message
?.toLowerCase()
?.includes('precondition check failed') ?? false;
return (
isFailedPrecondition && (isServiceNotEnabled || isPreconditionCheckFailed)
);
return firstError.reason === 'failedPrecondition';
}
}