Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/integrations/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { captureException } from '../exports';
import { defineIntegration } from '../integration';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
import { setHttpStatus, SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '../tracing';
import { hasSpanStreamingEnabled } from '../tracing/spans/hasSpanStreamingEnabled';
import { startSpan } from '../tracing/trace';
import type { IntegrationFn } from '../types/integration';
import type { WebFetchHeaders } from '../types/webfetchapi';
Expand Down Expand Up @@ -433,6 +434,11 @@ function instrumentPostgRESTFilterBuilder(
const descriptionMiddle = [mutationPart.trimEnd(), queryPart].filter(Boolean).join(' ');
const description = descriptionMiddle ? `${descriptionMiddle} from(${table})` : `from(${table})`;

// With span streaming, span names have to be low cardinality — the description carries the
// filter list, which can hold user values — so `{db.operation.name} {db.collection.name}` is
// used instead. The filters are still reported via the `db.query` attribute.
const streamedName = client && hasSpanStreamingEnabled(client) ? `${operation} ${table}` : undefined;

const attributes: Record<string, any> = {
'db.table': table,
'db.schema': typedThis.schema,
Expand All @@ -454,7 +460,7 @@ function instrumentPostgRESTFilterBuilder(

return startSpan(
{
name: description,
name: streamedName ?? description,
attributes,
},
span => {
Expand Down
30 changes: 30 additions & 0 deletions packages/core/test/lib/integrations/supabase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type CreateMockSupabaseClientOptions = {
headers?: PostgRESTHeaders;
/** When set, configures the mocked Sentry client's `dataCollection.databaseQueryData`. Omit to leave `getClient` to the test file `beforeEach`. */
dataCollectionDatabaseQueryData?: boolean;
/** Defaults to `'static'`, so span names keep the full description. */
traceLifecycle?: 'static' | 'stream';
};

const DEFAULT_MOCK_SUPABASE_REST_URL = 'https://example.supabase.co/rest/v1/todos';
Expand All @@ -66,6 +68,7 @@ function createMockSupabaseClient(resolveWith: unknown, options?: CreateMockSupa
if (options?.dataCollectionDatabaseQueryData !== undefined) {
currentScopesMocks.getClient.mockReturnValue({
getDataCollectionOptions: () => ({ databaseQueryData: options.dataCollectionDatabaseQueryData }),
getOptions: () => ({ traceLifecycle: options.traceLifecycle ?? 'static' }),
} as any);
}

Expand Down Expand Up @@ -371,6 +374,7 @@ describe('Supabase Integration', () => {
const resolved = resolveDataCollectionOptions({});
currentScopesMocks.getClient.mockReturnValue({
getDataCollectionOptions: () => resolved,
getOptions: () => ({ traceLifecycle: 'static' }),
} as any);

const client = createMockSupabaseClient({ status: 200 }, { ...MOCK_SUPABASE_PII_SCENARIO });
Expand All @@ -391,10 +395,36 @@ describe('Supabase Integration', () => {
);
});

it('names the span from the conventions instead of the description with span streaming enabled', async () => {
const resolved = resolveDataCollectionOptions({ dataCollection: { databaseQueryData: true } });
currentScopesMocks.getClient.mockReturnValue({
getDataCollectionOptions: () => resolved,
getOptions: () => ({ traceLifecycle: 'stream' }),
} as any);

const client = createMockSupabaseClient({ status: 200 }, { ...MOCK_SUPABASE_PII_SCENARIO });
instrumentSupabaseClient(client);

await (client as any).from('users').update({}).then();

const spanOptions = tracingMocks.startSpan.mock.calls[0]![0] as {
name: string;
attributes: Record<string, unknown>;
};
// `{db.operation.name} {db.collection.name}` — the description, which carries the filters, is
// not used as the name.
expect(spanOptions.name).toBe('update users');
// the filters are still reported, just not as the name
expect(spanOptions.attributes['db.query']).toEqual(
expect.arrayContaining([expect.stringContaining('secret@example.com')]),
);
});

it('redacts data when dataCollection.databaseQueryData is false', async () => {
const resolved = resolveDataCollectionOptions({ dataCollection: { databaseQueryData: false } });
currentScopesMocks.getClient.mockReturnValue({
getDataCollectionOptions: () => resolved,
getOptions: () => ({ traceLifecycle: 'static' }),
} as any);

const client = createMockSupabaseClient({ status: 200 }, { ...MOCK_SUPABASE_PII_SCENARIO });
Expand Down
Loading