Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code e42df0fa87 fix: detect schema mismatch in SSE subscription and prompt user to refresh
https://sonarly.com/issue/16638?type=bug

Users with stale browser-cached frontend JavaScript from pre-v1.19.1 send deprecated GraphQL queries (using `CoreView`, `getCoreViews`, `metadataEventsWithQueryIds`) to the v1.19.2 server, which rejects them. The SSE subscription captures these GraphQL validation errors and reports them to Sentry as raw objects.

Fix: **Fix: Detect stale frontend via SSE GraphQL schema validation errors and trigger page reload**

When a user's browser runs cached frontend JS from before a breaking schema change (e.g., the `CoreView` → `View` rename in v1.19.1), the SSE subscription sends GraphQL queries with field/type names that no longer exist on the server. The server responds with validation errors like "Cannot query field 'getCoreViews'" and "Unknown type 'CoreView'".

Previously, these errors were either:
1. Passed as raw objects to `captureException()` (producing unhelpful "Object captured as exception with keys: locations, message" Sentry entries)
2. Captured as `Error` instances but without triggering recovery

**Changes:**

1. **New utility `isGraphQLSchemaValidationError.ts`**: Detects GraphQL validation errors that indicate a schema mismatch — specifically messages starting with "Cannot query field" or "Unknown type". These are the standard GraphQL spec error messages for schema validation failures.

2. **`useTriggerEventStreamCreation.ts`**: Added schema validation error detection in both the `next` callback and the `message` handler. When detected, `window.location.reload()` is called to force-load the latest frontend bundle, matching the existing pattern used in `AppErrorBoundary` for stale Vite chunks.

3. **Monitoring improvement (same file)**: In the `message` handler's `default` case, raw GraphQL error objects are now wrapped in `Error` instances before being passed to `captureException()`. This produces readable Sentry titles like "SSE subscription error: ..." instead of the opaque "Object captured as exception with keys: locations, message".
2026-03-19 18:26:07 +00:00
2 changed files with 45 additions and 2 deletions
@@ -9,10 +9,11 @@ import { shouldDestroyEventStreamState } from '@/sse-db-event/states/shouldDestr
import { sseClientState } from '@/sse-db-event/states/sseClientState';
import { sseEventStreamIdState } from '@/sse-db-event/states/sseEventStreamIdState';
import { sseEventStreamReadyState } from '@/sse-db-event/states/sseEventStreamReadyState';
import { isGraphQLSchemaValidationError } from '@/sse-db-event/utils/isGraphQLSchemaValidationError';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { captureException } from '@sentry/react';
import { isNonEmptyString } from '@sniptt/guards';
import { print, type ExecutionResult } from 'graphql';
import { print, type ExecutionResult, type GraphQLError } from 'graphql';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -80,6 +81,17 @@ export const useTriggerEventStreamCreation = () => {
}>,
) => {
if (isDefined(value?.errors) && Array.isArray(value.errors)) {
const hasSchemaValidationError = value.errors.some(
(error: GraphQLError) =>
isGraphQLSchemaValidationError(error),
);
if (hasSchemaValidationError) {
window.location.reload();
return;
}
captureException(
new Error(`SSE subscription error: ${value.errors[0]?.message}`),
);
@@ -128,6 +140,17 @@ export const useTriggerEventStreamCreation = () => {
try {
if (event === 'next') {
if (isDefined(result?.errors)) {
const hasSchemaValidationError = result.errors.some(
(error) =>
isGraphQLSchemaValidationError(error as GraphQLError),
);
if (hasSchemaValidationError) {
window.location.reload();
return;
}
const subCode = result.errors[0]?.extensions?.subCode;
switch (subCode) {
@@ -137,7 +160,11 @@ export const useTriggerEventStreamCreation = () => {
}
default: {
for (const error of result.errors) {
captureException(error);
captureException(
new Error(
`SSE subscription error: ${error.message ?? 'Unknown error'}`,
),
);
}
}
}
@@ -0,0 +1,16 @@
import { type GraphQLError } from 'graphql';
// GraphQL validation errors like "Cannot query field" or "Unknown type"
// in SSE subscription responses indicate a stale frontend with an outdated
// schema. These occur when the server schema changes (e.g. type renames)
// and the user's browser is still running cached JS from before the upgrade.
export const isGraphQLSchemaValidationError = (
error: GraphQLError,
): boolean => {
const message = error.message ?? '';
return (
message.startsWith('Cannot query field') ||
message.startsWith('Unknown type')
);
};