Skip to content

Commit cdd8b08

Browse files
authored
fix(supabase): Consider sendDefaultPii for supabase integration (#20490)
We did not consider `sendDefaultPii` for the supabase integration. However: > The Supabase integration captures the full request body of POST/PATCH/PUT/DELETE operations (database mutations) and attaches it as the 'db.body' span attribute (line 387). This body contains the actual data being inserted or updated in Supabase tables, which commonly includes PII such as user emails, names, addresses, and other sensitive fields. Unlike other integrations (e.g., the MCP server integration which checks sendDefaultPii), the Supabase integration performs no sendDefaultPii check and applies no filtering or redaction to the captured body. Additionally, query filter values from URL search parameters are captured at lines 351-355, which can also contain PII used in WHERE clauses. This PR fixes this.
1 parent 3c6078f commit cdd8b08

4 files changed

Lines changed: 272 additions & 91 deletions

File tree

dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Sentry.init({
99
dsn: 'https://public@dsn.ingest.sentry.io/1337',
1010
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
1111
tracesSampleRate: 1.0,
12+
sendDefaultPii: true,
1213
});
1314

1415
// Simulate database operations

dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,37 +57,16 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry',
5757

5858
const transactionEvent = await pageloadTransactionPromise;
5959

60-
expect(transactionEvent.spans).toContainEqual(
61-
expect.objectContaining({
62-
description: 'select(*) filter(order, asc) from(todos)',
63-
op: 'db',
64-
data: expect.objectContaining({
65-
'db.operation': 'select',
66-
'db.query': ['select(*)', 'filter(order, asc)'],
67-
'db.system': 'postgresql',
68-
'sentry.op': 'db',
69-
'sentry.origin': 'auto.db.supabase',
70-
}),
71-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
72-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
73-
start_timestamp: expect.any(Number),
74-
status: 'ok',
75-
timestamp: expect.any(Number),
76-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
77-
origin: 'auto.db.supabase',
78-
}),
79-
);
80-
81-
expect(transactionEvent.spans).toContainEqual({
60+
// Client uses default sendDefaultPii: false — URL filters and bodies are not attached to spans/breadcrumbs.
61+
const redactedSelectSpan = expect.objectContaining({
62+
description: '[redacted] from(todos)',
63+
op: 'db',
8264
data: expect.objectContaining({
8365
'db.operation': 'select',
84-
'db.query': ['select(*)', 'filter(order, asc)'],
8566
'db.system': 'postgresql',
8667
'sentry.op': 'db',
8768
'sentry.origin': 'auto.db.supabase',
8869
}),
89-
description: 'select(*) filter(order, asc) from(todos)',
90-
op: 'db',
9170
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
9271
span_id: expect.stringMatching(/[a-f0-9]{16}/),
9372
start_timestamp: expect.any(Number),
@@ -97,20 +76,26 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry',
9776
origin: 'auto.db.supabase',
9877
});
9978

79+
expect(transactionEvent.spans).toContainEqual(redactedSelectSpan);
80+
81+
const selectSpan = transactionEvent.spans?.find(
82+
(s: { description?: string }) => s.description === '[redacted] from(todos)',
83+
);
84+
expect(selectSpan).toBeDefined();
85+
expect(selectSpan!.data).not.toHaveProperty('db.query');
86+
10087
expect(transactionEvent.breadcrumbs).toContainEqual({
10188
timestamp: expect.any(Number),
10289
type: 'supabase',
10390
category: 'db.select',
104-
message: 'select(*) filter(order, asc) from(todos)',
105-
data: expect.any(Object),
91+
message: '[redacted] from(todos)',
10692
});
10793

10894
expect(transactionEvent.breadcrumbs).toContainEqual({
10995
timestamp: expect.any(Number),
11096
type: 'supabase',
11197
category: 'db.insert',
112-
message: 'insert(...) select(*) from(todos)',
113-
data: expect.any(Object),
98+
message: 'insert(...) [redacted] from(todos)',
11499
});
115100
});
116101

packages/core/src/integrations/supabase.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/* eslint-disable @typescript-eslint/no-explicit-any */
55
/* eslint-disable max-lines */
66
import { addBreadcrumb } from '../breadcrumbs';
7+
import { getClient } from '../currentScopes';
78
import { DEBUG_BUILD } from '../debug-build';
89
import { captureException } from '../exports';
910
import { defineIntegration } from '../integration';
@@ -148,6 +149,25 @@ function isInstrumented<T>(fn: T): boolean | undefined {
148149
}
149150
}
150151

152+
/**
153+
* Plain-object bodies are copied into `plainBody`; array inserts (and other non-plain shapes) stay only on `rawBody`.
154+
* Returns a payload suitable for span attributes / breadcrumbs when the client has `sendDefaultPii` enabled.
155+
*/
156+
function getMutationBodyPayloadForTelemetry(rawBody: unknown, plainBody: Record<string, unknown>): unknown | undefined {
157+
if (Object.keys(plainBody).length > 0) {
158+
return plainBody;
159+
}
160+
if (Array.isArray(rawBody) && rawBody.length > 0) {
161+
return rawBody;
162+
}
163+
return undefined;
164+
}
165+
166+
/** True when the PostgREST builder carries a mutation body (for `insert(...)`, etc. in span descriptions). */
167+
function hasMutationBodyForDescription(rawBody: unknown, plainBody: Record<string, unknown>): boolean {
168+
return getMutationBodyPayloadForTelemetry(rawBody, plainBody) !== undefined;
169+
}
170+
151171
/**
152172
* Extracts the database operation type from the HTTP method and headers
153173
* @param method - The HTTP method of the request
@@ -361,12 +381,19 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte
361381
}
362382
}
363383

384+
const sendDefaultPii = Boolean(getClient()?.getOptions().sendDefaultPii);
385+
const bodyPayload = getMutationBodyPayloadForTelemetry(typedThis.body, body);
386+
364387
// Adding operation to the beginning of the description if it's not a `select` operation
365388
// For example, it can be an `insert` or `update` operation but the query can be `select(...)`
366389
// For `select` operations, we don't need repeat it in the description
367-
const description = `${operation === 'select' ? '' : `${operation}${body ? '(...) ' : ''}`}${queryItems.join(
368-
' ',
369-
)} from(${table})`;
390+
const mutationPart =
391+
operation === 'select'
392+
? ''
393+
: `${operation}${hasMutationBodyForDescription(typedThis.body, body) ? '(...) ' : ''}`;
394+
const queryPart = sendDefaultPii ? queryItems.join(' ') : queryItems.length > 0 ? '[redacted]' : '';
395+
const descriptionMiddle = [mutationPart.trimEnd(), queryPart].filter(Boolean).join(' ');
396+
const description = descriptionMiddle ? `${descriptionMiddle} from(${table})` : `from(${table})`;
370397

371398
const attributes: Record<string, any> = {
372399
'db.table': table,
@@ -379,12 +406,12 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte
379406
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
380407
};
381408

382-
if (queryItems.length) {
409+
if (queryItems.length && sendDefaultPii) {
383410
attributes['db.query'] = queryItems;
384411
}
385412

386-
if (Object.keys(body).length) {
387-
attributes['db.body'] = body;
413+
if (bodyPayload !== undefined && sendDefaultPii) {
414+
attributes['db.body'] = bodyPayload;
388415
}
389416

390417
return startSpan(
@@ -413,11 +440,11 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte
413440
}
414441

415442
const supabaseContext: Record<string, any> = {};
416-
if (queryItems.length) {
443+
if (queryItems.length && sendDefaultPii) {
417444
supabaseContext.query = queryItems;
418445
}
419-
if (Object.keys(body).length) {
420-
supabaseContext.body = body;
446+
if (bodyPayload !== undefined && sendDefaultPii) {
447+
supabaseContext.body = bodyPayload;
421448
}
422449

423450
captureException(err, scope => {
@@ -444,12 +471,12 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte
444471

445472
const data: Record<string, unknown> = {};
446473

447-
if (queryItems.length) {
474+
if (queryItems.length && sendDefaultPii) {
448475
data.query = queryItems;
449476
}
450477

451-
if (Object.keys(body).length) {
452-
data.body = body;
478+
if (bodyPayload !== undefined && sendDefaultPii) {
479+
data.body = bodyPayload;
453480
}
454481

455482
if (Object.keys(data).length) {

0 commit comments

Comments
 (0)