Skip to content

Commit c9b9b42

Browse files
committed
Skip all RPC calls in PostgREST instrumentation to prevent duplicate spans
1 parent e6eca29 commit c9b9b42

File tree

3 files changed

+87
-13
lines changed
  • dev-packages/browser-integration-tests/suites/integrations/supabase/generic-rpc
  • packages/core/src/integrations/supabase

3 files changed

+87
-13
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { createClient } from '@supabase/supabase-js';
3+
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key');
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
11+
tracesSampleRate: 1.0,
12+
});
13+
14+
// Simulate generic RPC call
15+
async function callGenericRpc() {
16+
try {
17+
await supabaseClient.rpc('my_custom_function', { param1: 'value1' });
18+
} catch (error) {
19+
Sentry.captureException(error);
20+
}
21+
}
22+
23+
callGenericRpc();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/core';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
6+
7+
async function mockSupabaseRoute(page: Page) {
8+
await page.route('**/rpc/my_custom_function', route => {
9+
return route.fulfill({
10+
status: 200,
11+
body: JSON.stringify({ result: 'success' }),
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
});
16+
});
17+
}
18+
19+
const bundle = process.env.PW_BUNDLE || '';
20+
// We only want to run this in non-CDN bundle mode
21+
if (bundle.startsWith('bundle')) {
22+
sentryTest.skip();
23+
}
24+
25+
sentryTest(
26+
'should capture exactly one db span for generic RPC calls (no double instrumentation)',
27+
async ({ getLocalTestUrl, page }) => {
28+
if (shouldSkipTracingTest()) {
29+
return;
30+
}
31+
32+
await mockSupabaseRoute(page);
33+
34+
const url = await getLocalTestUrl({ testDir: __dirname });
35+
36+
const event = await getFirstSentryEnvelopeRequest<Event>(page, url);
37+
const dbSpans = event.spans?.filter(({ op }) => op === 'db');
38+
39+
// Should have exactly one db span (not doubled by PostgREST instrumentation)
40+
expect(dbSpans).toHaveLength(1);
41+
42+
expect(dbSpans![0]).toMatchObject({
43+
description: 'rpc(my_custom_function)',
44+
parent_span_id: event.contexts?.trace?.span_id,
45+
span_id: expect.any(String),
46+
start_timestamp: expect.any(Number),
47+
timestamp: expect.any(Number),
48+
trace_id: event.contexts?.trace?.trace_id,
49+
data: expect.objectContaining({
50+
'sentry.op': 'db',
51+
'sentry.origin': 'auto.db.supabase',
52+
'db.system': 'postgresql',
53+
'db.operation': 'insert',
54+
'db.table': 'my_custom_function',
55+
'db.params': { param1: 'value1' },
56+
}),
57+
});
58+
},
59+
);

packages/core/src/integrations/supabase/postgrest.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { setHttpStatus, startInactiveSpan, withActiveSpan } from '../../tracing'
55
import type { SpanAttributes } from '../../types-hoist/span';
66
import { debug } from '../../utils/debug-logger';
77
import { isPlainObject } from '../../utils/is';
8-
import { DB_OPERATIONS_TO_INSTRUMENT, QUEUE_RPC_OPERATIONS } from './constants';
8+
import { DB_OPERATIONS_TO_INSTRUMENT } from './constants';
99
import { captureSupabaseError } from './errors';
1010
import type {
1111
PostgRESTFilterBuilder,
@@ -17,13 +17,7 @@ import type {
1717
SupabaseError,
1818
SupabaseResponse,
1919
} from './types';
20-
import {
21-
_isInstrumented,
22-
_markAsInstrumented,
23-
_normalizeRpcFunctionName,
24-
extractOperation,
25-
translateFiltersIntoMethods,
26-
} from './utils';
20+
import { _isInstrumented, _markAsInstrumented, extractOperation, translateFiltersIntoMethods } from './utils';
2721

2822
/**
2923
* Instruments PostgREST filter builder to trace database operations.
@@ -67,11 +61,9 @@ function _createInstrumentedPostgRESTThen(
6761

6862
const pathParts = typedThis.url.pathname.split('/');
6963
const rpcIndex = pathParts.indexOf('rpc');
70-
const rpcFunctionName = rpcIndex !== -1 && pathParts.length > rpcIndex + 1 ? pathParts[rpcIndex + 1] : undefined;
71-
72-
// Normalize RPC function name to handle schema-qualified names (e.g., 'pgmq.send' → 'send')
73-
if (rpcFunctionName && QUEUE_RPC_OPERATIONS.has(_normalizeRpcFunctionName(rpcFunctionName))) {
74-
// Queue RPC calls are instrumented in the dedicated queue instrumentation.
64+
// Skip all RPC calls - they are fully instrumented in rpc.ts
65+
// (both queue operations and generic RPC functions)
66+
if (rpcIndex !== -1) {
7567
return Reflect.apply(target, thisArg, argumentsList);
7668
}
7769

0 commit comments

Comments
 (0)