diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 09e4b7705740..6293bff213d7 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -66,6 +66,7 @@ describe('sentryMiddleware', () => { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: false, outputs: false }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }), diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 64865c123faf..64a75359bcbd 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -385,7 +385,8 @@ function instrumentPostgRESTFilterBuilder( } const client = getClient(); - const shouldSendData = _options.sendOperationData ?? client?.getDataCollectionOptions().userInfo === true; + const shouldSendData = + _options.sendOperationData ?? client?.getDataCollectionOptions().databaseQueryData === true; const bodyPayload = getMutationBodyPayloadForTelemetry(typedThis.body, body); // Adding operation to the beginning of the description if it's not a `select` operation @@ -563,7 +564,7 @@ interface SupabaseIntegrationOptions { * Whether to attach PostgREST query filters and mutation body payloads * to Sentry telemetry. * - * Falls back to `dataCollection.userInfo` when not set. + * Falls back to `dataCollection.databaseQueryData` when not set. * @default undefined */ sendOperationData?: boolean; diff --git a/packages/core/src/types/datacollection.ts b/packages/core/src/types/datacollection.ts index 995ea1159442..88af5b354ebd 100644 --- a/packages/core/src/types/datacollection.ts +++ b/packages/core/src/types/datacollection.ts @@ -78,6 +78,14 @@ export interface DataCollection { outputs?: boolean; }; + /** + * Include data associated with database queries. This controls collection of query parameters, inline literal values within query text, mutation/request bodies, and returned result data. + * + * Sanitized or parameterized DB statements (`db.query.text`) are **not** controlled by this property. Structural metadata such as the database system, query summary, operation name, or the table being acted upon is also **always** collected. + * @default true + */ + databaseQueryData?: boolean; + /** * Capture local variable values in stack frames. * @default true diff --git a/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts b/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts index 92f9e957a4c0..b1338bf6a777 100644 --- a/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts +++ b/packages/core/src/utils/data-collection/defaultPiiToCollectionOptions.ts @@ -18,6 +18,7 @@ export function defaultPiiToCollectionOptions(sendDefaultPii?: boolean): Resolve queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: true, outputs: true }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 7, // default should be 5, but ContextLines integration uses 7 } @@ -31,6 +32,9 @@ export function defaultPiiToCollectionOptions(sendDefaultPii?: boolean): Resolve // always attached regardless of `sendDefaultPii`; keep it on to preserve that behavior. graphQL: { document: true, variables: true }, genAI: { inputs: false, outputs: false }, + // Database query values were only sent with `sendDefaultPii: true` (e.g. Supabase gated on it), + // so map the legacy "off" state to `false`. + databaseQueryData: false, stackFrameVariables: true, frameContextLines: 7, // default should be 5, but ContextLines integration uses 7 }; diff --git a/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts index eb62bbcce126..26da8ac0c1f5 100644 --- a/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts +++ b/packages/core/src/utils/data-collection/resolveDataCollectionOptions.ts @@ -9,6 +9,7 @@ const DEFAULTS: ResolvedDataCollection = { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: true, outputs: true }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }; @@ -52,6 +53,7 @@ export function resolveDataCollectionOptions(options: { inputs: dc.genAI?.inputs ?? base.genAI.inputs, outputs: dc.genAI?.outputs ?? base.genAI.outputs, }, + databaseQueryData: dc.databaseQueryData ?? base.databaseQueryData, stackFrameVariables: dc.stackFrameVariables ?? base.stackFrameVariables, frameContextLines: dc.frameContextLines ?? base.frameContextLines, }; diff --git a/packages/core/test/lib/integrations/mcp-server/testUtils.ts b/packages/core/test/lib/integrations/mcp-server/testUtils.ts index 15334a47966e..d1b0c0aab237 100644 --- a/packages/core/test/lib/integrations/mcp-server/testUtils.ts +++ b/packages/core/test/lib/integrations/mcp-server/testUtils.ts @@ -19,6 +19,7 @@ export function createMockClient(userInfo = true, genAI?: { inputs: boolean; out queryParams: true, graphQL: { document: true, variables: true }, genAI: genAIOptions, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }), diff --git a/packages/core/test/lib/integrations/supabase.test.ts b/packages/core/test/lib/integrations/supabase.test.ts index a1837daf460a..361af18f19fc 100644 --- a/packages/core/test/lib/integrations/supabase.test.ts +++ b/packages/core/test/lib/integrations/supabase.test.ts @@ -39,8 +39,8 @@ type CreateMockSupabaseClientOptions = { method?: string; url?: URL | string; body?: unknown; - /** When set, configures the mocked Sentry client's `dataCollection.userInfo`. Omit to leave `getClient` to the test file `beforeEach`. */ - dataCollectionUserInfo?: boolean; + /** When set, configures the mocked Sentry client's `dataCollection.databaseQueryData`. Omit to leave `getClient` to the test file `beforeEach`. */ + dataCollectionDatabaseQueryData?: boolean; }; const DEFAULT_MOCK_SUPABASE_REST_URL = 'https://example.supabase.co/rest/v1/todos'; @@ -53,9 +53,9 @@ const MOCK_SUPABASE_PII_SCENARIO: Pick ({ userInfo: options.dataCollectionUserInfo }), + getDataCollectionOptions: () => ({ databaseQueryData: options.dataCollectionDatabaseQueryData }), } as any); } @@ -237,10 +237,10 @@ describe('Supabase Integration', () => { vi.restoreAllMocks(); }); - it('omits db.query, db.body, and breadcrumb query/body when dataCollection.userInfo is false', async () => { + it('omits db.query, db.body, and breadcrumb query/body when dataCollection.databaseQueryData is false', async () => { const client = createMockSupabaseClient( { status: 200 }, - { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionUserInfo: false }, + { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionDatabaseQueryData: false }, ); instrumentSupabaseClient(client); @@ -259,10 +259,10 @@ describe('Supabase Integration', () => { expect(breadcrumb).not.toHaveProperty('data'); }); - it('includes db.query, db.body, and breadcrumb query/body when dataCollection.userInfo is true', async () => { + it('includes db.query, db.body, and breadcrumb query/body when dataCollection.databaseQueryData is true', async () => { const client = createMockSupabaseClient( { status: 200 }, - { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionUserInfo: true }, + { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionDatabaseQueryData: true }, ); instrumentSupabaseClient(client); @@ -290,10 +290,10 @@ describe('Supabase Integration', () => { ); }); - it('includes data when sendOperationData option is set, regardless of dataCollection.userInfo', async () => { + it('includes data when sendOperationData option is set, regardless of dataCollection.databaseQueryData', async () => { const client = createMockSupabaseClient( { status: 200 }, - { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionUserInfo: false }, + { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionDatabaseQueryData: false }, ); instrumentSupabaseClient(client, { sendOperationData: true }); @@ -312,10 +312,10 @@ describe('Supabase Integration', () => { ); }); - it('sendOperationData: false takes precedence over dataCollection.userInfo: true', async () => { + it('sendOperationData: false takes precedence over dataCollection.databaseQueryData: true', async () => { const client = createMockSupabaseClient( { status: 200 }, - { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionUserInfo: true }, + { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionDatabaseQueryData: true }, ); instrumentSupabaseClient(client, { sendOperationData: false }); @@ -330,7 +330,7 @@ describe('Supabase Integration', () => { expect(spanOptions.attributes['db.body']).toBeUndefined(); }); - it('includes data when legacy sendDefaultPii: true is bridged to dataCollection.userInfo', async () => { + it('includes data when legacy sendDefaultPii: true is bridged to dataCollection.databaseQueryData', async () => { const resolved = resolveDataCollectionOptions({ sendDefaultPii: true }); currentScopesMocks.getClient.mockReturnValue({ getDataCollectionOptions: () => resolved, @@ -378,7 +378,7 @@ describe('Supabase Integration', () => { it('omits supabase error context query/body when data collection is off', async () => { const client = createMockSupabaseClient( { status: 400, error: { message: 'Bad request', code: '400' } }, - { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionUserInfo: false }, + { ...MOCK_SUPABASE_PII_SCENARIO, dataCollectionDatabaseQueryData: false }, ); instrumentSupabaseClient(client); @@ -417,7 +417,7 @@ describe('Supabase Integration', () => { method: 'POST', url: 'https://example.supabase.co/rest/v1/todos?columns=', body: [{ title: 'Test Todo' }], - dataCollectionUserInfo: true, + dataCollectionDatabaseQueryData: true, }, ); instrumentSupabaseClient(client); diff --git a/packages/core/test/lib/utils/data-collection/defaultPiiToCollectionOptions.test.ts b/packages/core/test/lib/utils/data-collection/defaultPiiToCollectionOptions.test.ts index ce4b8b519097..55449201d664 100644 --- a/packages/core/test/lib/utils/data-collection/defaultPiiToCollectionOptions.test.ts +++ b/packages/core/test/lib/utils/data-collection/defaultPiiToCollectionOptions.test.ts @@ -11,6 +11,7 @@ describe('defaultPiiToCollectionOptions', () => { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: true, outputs: true }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 7, }); @@ -28,6 +29,7 @@ describe('defaultPiiToCollectionOptions', () => { queryParams: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, graphQL: { document: true, variables: true }, genAI: { inputs: false, outputs: false }, + databaseQueryData: false, stackFrameVariables: true, frameContextLines: 7, }); @@ -45,6 +47,7 @@ describe('defaultPiiToCollectionOptions', () => { queryParams: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, graphQL: { document: true, variables: true }, genAI: { inputs: false, outputs: false }, + databaseQueryData: false, stackFrameVariables: true, frameContextLines: 7, }); @@ -62,6 +65,7 @@ describe('defaultPiiToCollectionOptions', () => { queryParams: { deny: ['forwarded', '-ip', 'remote-', 'via', '-user'] }, graphQL: { document: true, variables: true }, genAI: { inputs: false, outputs: false }, + databaseQueryData: false, stackFrameVariables: true, frameContextLines: 7, }); diff --git a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts index d00935d8b6b0..0deee7654d09 100644 --- a/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts +++ b/packages/core/test/lib/utils/data-collection/resolveDataCollectionOptions.test.ts @@ -10,6 +10,7 @@ describe('resolveDataCollectionOptions', () => { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: true, outputs: true }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }; @@ -24,6 +25,7 @@ describe('resolveDataCollectionOptions', () => { expect(result.genAI).toEqual({ inputs: false, outputs: false }); // GraphQL documents are redacted at collection time, so they stay on to preserve legacy behavior. expect(result.graphQL).toEqual({ document: true, variables: true }); + expect(result.databaseQueryData).toBe(false); expect(result.stackFrameVariables).toBe(true); expect(result.frameContextLines).toBe(7); }); @@ -50,6 +52,7 @@ describe('resolveDataCollectionOptions', () => { expect(result.queryParams).toBe(true); expect(result.graphQL).toEqual({ document: true, variables: true }); expect(result.genAI).toEqual({ inputs: true, outputs: true }); + expect(result.databaseQueryData).toBe(true); }); it('bridges sendDefaultPii: false to restrictive config', () => { @@ -59,6 +62,7 @@ describe('resolveDataCollectionOptions', () => { expect(result.httpBodies).toEqual([]); expect(result.genAI).toEqual({ inputs: false, outputs: false }); expect(result.graphQL).toEqual({ document: true, variables: true }); + expect(result.databaseQueryData).toBe(false); }); }); @@ -74,6 +78,7 @@ describe('resolveDataCollectionOptions', () => { // Remaining fields use spec defaults (not sendDefaultPii bridge) expect(result.httpBodies).toEqual(['incomingRequest', 'outgoingRequest', 'incomingResponse', 'outgoingResponse']); expect(result.genAI).toEqual({ inputs: true, outputs: true }); + expect(result.databaseQueryData).toBe(true); }); }); @@ -94,6 +99,7 @@ describe('resolveDataCollectionOptions', () => { expect(result.queryParams).toBe(true); expect(result.graphQL).toEqual({ document: true, variables: true }); expect(result.genAI).toEqual({ inputs: true, outputs: true }); + expect(result.databaseQueryData).toBe(true); expect(result.stackFrameVariables).toBe(true); expect(result.frameContextLines).toBe(5); }); @@ -150,13 +156,23 @@ describe('resolveDataCollectionOptions', () => { expect(result.queryParams).toBe(false); }); + + it('supports turning off database query data', () => { + const result = resolveDataCollectionOptions({ + dataCollection: { + databaseQueryData: false, + }, + }); + + expect(result.databaseQueryData).toBe(false); + }); }); describe('return type completeness', () => { it('always returns all fields', () => { const result = resolveDataCollectionOptions({}); - expect(Object.keys(result)).toHaveLength(9); + expect(Object.keys(result)).toHaveLength(10); expect(result).toHaveProperty('userInfo'); expect(result).toHaveProperty('cookies'); expect(result).toHaveProperty('httpHeaders'); @@ -170,6 +186,7 @@ describe('resolveDataCollectionOptions', () => { expect(result).toHaveProperty('genAI'); expect(result).toHaveProperty('genAI.inputs'); expect(result).toHaveProperty('genAI.outputs'); + expect(result).toHaveProperty('databaseQueryData'); expect(result).toHaveProperty('stackFrameVariables'); expect(result).toHaveProperty('frameContextLines'); }); diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts index 31acb909000b..cf0ffe3ac231 100644 --- a/packages/core/test/lib/utils/request.test.ts +++ b/packages/core/test/lib/utils/request.test.ts @@ -876,6 +876,7 @@ describe('request utils', () => { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: true, outputs: true }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }; diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts index e4bf8fea1559..c19bcc1ed099 100644 --- a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -49,6 +49,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: false, outputs: false }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }), diff --git a/packages/remix/test/server/errors.test.ts b/packages/remix/test/server/errors.test.ts index b1f6bf887e51..4f58e280c3ee 100644 --- a/packages/remix/test/server/errors.test.ts +++ b/packages/remix/test/server/errors.test.ts @@ -19,6 +19,7 @@ function createMockClient(httpBodies: string[] = []): Client { queryParams: true, graphQL: { document: true, variables: true }, genAI: { inputs: true, outputs: true }, + databaseQueryData: true, stackFrameVariables: true, frameContextLines: 5, }),