From 75bc15a3cbe07d4cc54e60e9e6d10255e338588e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 30 Mar 2026 09:20:04 +0200 Subject: [PATCH 1/3] fix(core): Guard nullish response in supabase PostgREST handler The `.then()` success handler in `instrumentPostgRESTFilterBuilder` accessed `res.error` without a null guard, causing a crash when `res` is undefined (observed in React Native). This adds a guard matching the pattern already used in `instrumentAuthOperation`. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/integrations/supabase.ts | 2 +- .../test/lib/integrations/supabase.test.ts | 184 ++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/lib/integrations/supabase.test.ts diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 1b6f24cc3136..6d4f542c8f62 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -403,7 +403,7 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte span.end(); } - if (res.error) { + if (res && res.error) { const err = new Error(res.error.message) as SupabaseError; if (res.error.code) { err.code = res.error.code; diff --git a/packages/core/test/lib/integrations/supabase.test.ts b/packages/core/test/lib/integrations/supabase.test.ts new file mode 100644 index 000000000000..0dc7dc288008 --- /dev/null +++ b/packages/core/test/lib/integrations/supabase.test.ts @@ -0,0 +1,184 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as breadcrumbModule from '../../../src/breadcrumbs'; +import * as exportsModule from '../../../src/exports'; +import { + DB_OPERATIONS_TO_INSTRUMENT, + extractOperation, + instrumentSupabaseClient, + translateFiltersIntoMethods, +} from '../../../src/integrations/supabase'; +import type { + PostgRESTFilterBuilder, + PostgRESTQueryBuilder, + SupabaseClientInstance, +} from '../../../src/integrations/supabase'; + +// Mock tracing to avoid needing full SDK setup +vi.mock('../../../src/tracing', () => ({ + startSpan: (_opts: any, cb: (span: any) => any) => { + const mockSpan = { + setStatus: vi.fn(), + end: vi.fn(), + }; + return cb(mockSpan); + }, + setHttpStatus: vi.fn(), + SPAN_STATUS_OK: 1, + SPAN_STATUS_ERROR: 2, +})); + +describe('Supabase Integration', () => { + describe('extractOperation', () => { + it('returns select for GET', () => { + expect(extractOperation('GET')).toBe('select'); + }); + + it('returns insert for POST without resolution header', () => { + expect(extractOperation('POST')).toBe('insert'); + }); + + it('returns upsert for POST with resolution header', () => { + expect(extractOperation('POST', { Prefer: 'resolution=merge-duplicates' })).toBe('upsert'); + }); + + it('returns update for PATCH', () => { + expect(extractOperation('PATCH')).toBe('update'); + }); + + it('returns delete for DELETE', () => { + expect(extractOperation('DELETE')).toBe('delete'); + }); + }); + + describe('translateFiltersIntoMethods', () => { + it('returns select(*) for wildcard', () => { + expect(translateFiltersIntoMethods('select', '*')).toBe('select(*)'); + }); + + it('returns select with columns', () => { + expect(translateFiltersIntoMethods('select', 'id,name')).toBe('select(id,name)'); + }); + + it('translates eq filter', () => { + expect(translateFiltersIntoMethods('id', 'eq.123')).toBe('eq(id, 123)'); + }); + }); + + describe('instrumentPostgRESTFilterBuilder - nullish response handling', () => { + let captureExceptionSpy: ReturnType; + let addBreadcrumbSpy: ReturnType; + + beforeEach(() => { + captureExceptionSpy = vi.spyOn(exportsModule, 'captureException').mockImplementation(() => ''); + addBreadcrumbSpy = vi.spyOn(breadcrumbModule, 'addBreadcrumb').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + function createMockSupabaseClient(resolveWith: unknown): unknown { + // Create a PostgRESTFilterBuilder-like class + class MockPostgRESTFilterBuilder { + method = 'GET'; + headers: Record = { 'X-Client-Info': 'supabase-js/2.0.0' }; + url = new URL('https://example.supabase.co/rest/v1/todos'); + schema = 'public'; + body = undefined; + + then(onfulfilled?: (value: any) => any, onrejected?: (reason: any) => any): Promise { + return Promise.resolve(resolveWith).then(onfulfilled, onrejected); + } + } + + class MockPostgRESTQueryBuilder { + select() { + return new MockPostgRESTFilterBuilder(); + } + insert() { + return new MockPostgRESTFilterBuilder(); + } + upsert() { + return new MockPostgRESTFilterBuilder(); + } + update() { + return new MockPostgRESTFilterBuilder(); + } + delete() { + return new MockPostgRESTFilterBuilder(); + } + } + + // Create a mock SupabaseClient constructor + class MockSupabaseClient { + auth = { + admin: {} as any, + } as SupabaseClientInstance['auth']; + + from(_table: string): PostgRESTQueryBuilder { + return new MockPostgRESTQueryBuilder() as unknown as PostgRESTQueryBuilder; + } + } + + return new MockSupabaseClient(); + } + + it('handles undefined response without throwing', async () => { + const client = createMockSupabaseClient(undefined); + instrumentSupabaseClient(client); + + const builder = (client as any).from('todos'); + const result = builder.select('*'); + + // This should not throw even though the response is undefined + const res = await result; + expect(res).toBeUndefined(); + }); + + it('handles null response without throwing', async () => { + const client = createMockSupabaseClient(null); + instrumentSupabaseClient(client); + + const builder = (client as any).from('todos'); + const result = builder.select('*'); + + const res = await result; + expect(res).toBeNull(); + }); + + it('still adds breadcrumb when response is undefined', async () => { + const client = createMockSupabaseClient(undefined); + instrumentSupabaseClient(client); + + const builder = (client as any).from('todos'); + await builder.select('*'); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'supabase', + category: 'db.select', + }), + ); + }); + + it('does not capture exception when response is undefined', async () => { + const client = createMockSupabaseClient(undefined); + instrumentSupabaseClient(client); + + const builder = (client as any).from('todos'); + await builder.select('*'); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + + it('still captures error when response has error', async () => { + const client = createMockSupabaseClient({ status: 400, error: { message: 'Bad request', code: '400' } }); + instrumentSupabaseClient(client); + + const builder = (client as any).from('todos'); + await builder.select('*'); + + expect(captureExceptionSpy).toHaveBeenCalled(); + }); + }); +}); From 4be0d44b810c9da9393576c7c1d4fc9ba84336c6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 30 Mar 2026 09:33:27 +0200 Subject: [PATCH 2/3] fix: Use optional chaining and remove unused imports Address lint failures: use `res?.error` instead of `res && res.error`, and remove unused imports in the test file. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/integrations/supabase.ts | 2 +- packages/core/test/lib/integrations/supabase.test.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 6d4f542c8f62..dac7530b46f0 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -403,7 +403,7 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte span.end(); } - if (res && res.error) { + if (res?.error) { const err = new Error(res.error.message) as SupabaseError; if (res.error.code) { err.code = res.error.code; diff --git a/packages/core/test/lib/integrations/supabase.test.ts b/packages/core/test/lib/integrations/supabase.test.ts index 0dc7dc288008..13d91def7f8a 100644 --- a/packages/core/test/lib/integrations/supabase.test.ts +++ b/packages/core/test/lib/integrations/supabase.test.ts @@ -2,13 +2,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as breadcrumbModule from '../../../src/breadcrumbs'; import * as exportsModule from '../../../src/exports'; import { - DB_OPERATIONS_TO_INSTRUMENT, extractOperation, instrumentSupabaseClient, translateFiltersIntoMethods, } from '../../../src/integrations/supabase'; import type { - PostgRESTFilterBuilder, PostgRESTQueryBuilder, SupabaseClientInstance, } from '../../../src/integrations/supabase'; From 9d9b5b8f33a48626d57c740d67c24568d8de6094 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 30 Mar 2026 09:41:52 +0200 Subject: [PATCH 3/3] chore: Fix formatting in supabase test file Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/test/lib/integrations/supabase.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/test/lib/integrations/supabase.test.ts b/packages/core/test/lib/integrations/supabase.test.ts index 13d91def7f8a..519dda4f06a0 100644 --- a/packages/core/test/lib/integrations/supabase.test.ts +++ b/packages/core/test/lib/integrations/supabase.test.ts @@ -6,10 +6,7 @@ import { instrumentSupabaseClient, translateFiltersIntoMethods, } from '../../../src/integrations/supabase'; -import type { - PostgRESTQueryBuilder, - SupabaseClientInstance, -} from '../../../src/integrations/supabase'; +import type { PostgRESTQueryBuilder, SupabaseClientInstance } from '../../../src/integrations/supabase'; // Mock tracing to avoid needing full SDK setup vi.mock('../../../src/tracing', () => ({