From 2c304f23562e7335e814a6e5fb40dd56283ad404 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Wed, 28 Oct 2026 19:03:21 +0800 Subject: [PATCH 1/4] fix(core): Only create Supabase db spans when there is a parent span Pass onlyIfParent to the Supabase auth and PostgREST startSpanManual calls so a query made without an active span no longer becomes its own root span. --- CHANGELOG.md | 4 +++ packages/core/src/integrations/supabase.ts | 2 ++ .../test/lib/integrations/supabase.test.ts | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bd3d02ec53..29448d2d5b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,10 @@ Work in this release was contributed by @faizansaiyed123, @itisrmk, @kunal649, @ - A request to `/` gets the `route` source and the name `GET /`. - Spans also get `user_agent.original` and `http.request.body.size`. +### Other Changes + +- fix(core): Only create Supabase db spans when there is a parent span ([#25806](https://github.com/getsentry/sentry-javascript/issues/25806)) + ## 11.9.0 ### Important Changes diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 187659aa6345..16b6f95588b7 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -295,6 +295,7 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A return startSpanManual( { name, + onlyIfParent: true, attributes: { [SENTRY_ORIGIN]: 'auto.db.supabase', [SENTRY_OP]: DB, @@ -479,6 +480,7 @@ function instrumentPostgRESTFilterBuilder( return startSpanManual( { name, + onlyIfParent: true, attributes, }, span => { diff --git a/packages/core/test/lib/integrations/supabase.test.ts b/packages/core/test/lib/integrations/supabase.test.ts index cae6111f7e4d..c1c344b9927e 100644 --- a/packages/core/test/lib/integrations/supabase.test.ts +++ b/packages/core/test/lib/integrations/supabase.test.ts @@ -645,4 +645,38 @@ describe('Supabase Integration', () => { expect(spanOptions.attributes['db.operation.name']).toBe('upsert'); }); }); + + describe('onlyIfParent', () => { + beforeEach(() => { + vi.spyOn(exportsModule, 'captureException').mockImplementation(() => ''); + vi.spyOn(breadcrumbModule, 'addBreadcrumb').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('starts the PostgREST span with onlyIfParent so it is dropped without an active parent', async () => { + tracingMocks.startSpanManual.mockClear(); + const client = createMockSupabaseClient({ status: 200, data: [] }); + instrumentSupabaseClient(client); + + await (client as any).from('todos').select('*').then(); + + const spanOptions = tracingMocks.startSpanManual.mock.calls[0]![0] as { onlyIfParent?: boolean }; + expect(spanOptions.onlyIfParent).toBe(true); + }); + + it('starts the auth span with onlyIfParent so it is dropped without an active parent', async () => { + tracingMocks.startSpanManual.mockClear(); + const client = createMockSupabaseClient(undefined) as any; + client.auth.signInWithPassword = vi.fn(() => Promise.resolve({ data: { user: {} }, error: null })); + instrumentSupabaseClient(client); + + await client.auth.signInWithPassword({ email: 'a@b.c', password: 'pw' }); + + const spanOptions = tracingMocks.startSpanManual.mock.calls[0]![0] as { onlyIfParent?: boolean }; + expect(spanOptions.onlyIfParent).toBe(true); + }); + }); }); From b808ec0eb53b6e168bb94ffcf6a30f1acc1c41b8 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Wed, 28 Oct 2026 19:59:03 +0800 Subject: [PATCH 2/4] test(core): Use toHaveBeenCalledWith for Supabase onlyIfParent assertions --- packages/core/test/lib/integrations/supabase.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/test/lib/integrations/supabase.test.ts b/packages/core/test/lib/integrations/supabase.test.ts index c1c344b9927e..dcb0233abdb0 100644 --- a/packages/core/test/lib/integrations/supabase.test.ts +++ b/packages/core/test/lib/integrations/supabase.test.ts @@ -663,8 +663,10 @@ describe('Supabase Integration', () => { await (client as any).from('todos').select('*').then(); - const spanOptions = tracingMocks.startSpanManual.mock.calls[0]![0] as { onlyIfParent?: boolean }; - expect(spanOptions.onlyIfParent).toBe(true); + expect(tracingMocks.startSpanManual).toHaveBeenCalledWith( + expect.objectContaining({ onlyIfParent: true }), + expect.any(Function), + ); }); it('starts the auth span with onlyIfParent so it is dropped without an active parent', async () => { @@ -675,8 +677,10 @@ describe('Supabase Integration', () => { await client.auth.signInWithPassword({ email: 'a@b.c', password: 'pw' }); - const spanOptions = tracingMocks.startSpanManual.mock.calls[0]![0] as { onlyIfParent?: boolean }; - expect(spanOptions.onlyIfParent).toBe(true); + expect(tracingMocks.startSpanManual).toHaveBeenCalledWith( + expect.objectContaining({ onlyIfParent: true }), + expect.any(Function), + ); }); }); }); From 37eaa968b3c1b48323cf7a388fccf8b06fb0000d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 28 Oct 2026 13:21:20 +0100 Subject: [PATCH 3/4] Update supabase-nextjs e2e test for parent-only spans --- .../supabase-nextjs/tests/performance.test.ts | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts index 79c9861008f7..6b28f1ee0e9f 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts @@ -1,5 +1,10 @@ import { expect, test } from '@playwright/test'; -import { collectStreamedSpansUntilSegment, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { + collectStreamedSpansUntilSegment, + getSpanOp, + waitForEnvelopeItem, + waitForStreamedSpan, +} from '@sentry-internal/test-utils'; type StreamedSpan = Awaited>; @@ -65,7 +70,21 @@ test('Sends client-side Supabase db-operation spans to Sentry', async ({ page }) ); }); - const insertSpanPromise = waitForStreamedSpan('supabase-nextjs', span => span.name === 'insert todos'); + let insertSpanSent = false; + waitForStreamedSpan('supabase-nextjs', span => { + if (span.name === 'insert todos') { + insertSpanSent = true; + } + return false; + }).catch(() => {}); + + const clientReportPromise = waitForEnvelopeItem('supabase-nextjs', ([header, payload]) => { + if (header.type !== 'client_report') { + return false; + } + const { discarded_events } = payload as { discarded_events: Array<{ reason: string; category: string }> }; + return discarded_events.some(e => e.reason === 'no_parent_span' && e.category === 'span'); + }); await page.goto('/'); @@ -78,15 +97,21 @@ test('Sends client-side Supabase db-operation spans to Sentry', async ({ page }) // Wait for login to complete await page.waitForSelector('button:has-text("Add")'); + const [pageloadSpan, selectSpan] = await Promise.all([pageloadSpanPromise, selectSpanPromise]); + // Add a new todo entry - await page.locator('input[id=new-task-text]').fill('test'); + const task = `test-${Date.now()}`; + await page.locator('input[id=new-task-text]').fill(task); await page.locator('button[id=add-task]').click(); + await page.getByText(task).waitFor(); + + // Hiding the page flushes the pending client report. + await page.evaluate(() => { + Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' }); + document.dispatchEvent(new Event('visibilitychange')); + }); - const [pageloadSpan, selectSpan, insertSpan] = await Promise.all([ - pageloadSpanPromise, - selectSpanPromise, - insertSpanPromise, - ]); + await clientReportPromise; // Client-side database query data is collected by default. expectDbSpan(selectSpan, 'select todos', { @@ -94,13 +119,10 @@ test('Sends client-side Supabase db-operation spans to Sentry', async ({ page }) 'db.query': { value: ['select(*)', 'filter(order, asc)'], type: 'array' }, }); expect(selectSpan.trace_id).toBe(pageloadSpan.trace_id); + expect(selectSpan.parent_span_id).toBe(pageloadSpan.span_id); - // The insert is triggered long after the pageload span has ended, so it is streamed on its own - // rather than as a child of the pageload span. - expectDbSpan(insertSpan, 'insert todos', { - 'db.operation.name': { value: 'insert', type: 'string' }, - 'db.query': { value: ['select(*)'], type: 'array' }, - }); + // The insert runs after the pageload span has ended, so it has no parent and no span is sent. + expect(insertSpanSent).toBe(false); }); test('Sends server-side Supabase db-operation spans to Sentry', async ({ baseURL }) => { From e5e938ce87ec349710da19e7501b85c876ee543d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 28 Oct 2026 13:55:39 +0100 Subject: [PATCH 4/4] Remove CHANGELOG entry --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29448d2d5b32..a9bd3d02ec53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,10 +58,6 @@ Work in this release was contributed by @faizansaiyed123, @itisrmk, @kunal649, @ - A request to `/` gets the `route` source and the name `GET /`. - Spans also get `user_agent.original` and `http.request.body.size`. -### Other Changes - -- fix(core): Only create Supabase db spans when there is a parent span ([#25806](https://github.com/getsentry/sentry-javascript/issues/25806)) - ## 11.9.0 ### Important Changes