From 07566b8c54fad5ad00166ce77ea90473ce19a919 Mon Sep 17 00:00:00 2001 From: Steven Eubank Date: Wed, 30 Apr 2025 08:22:35 +0200 Subject: [PATCH 1/4] Update to always set db DB must be set to populate query/database insights --- packages/core/src/integrations/supabase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 63229ccbdcf4..b8205b667282 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -377,6 +377,7 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte return startSpan( { + op: 'db', name: description, attributes, }, From c47ee4abd48eeb57761796c6a6dd7fcc0efd202e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 30 Apr 2025 18:15:33 +0100 Subject: [PATCH 2/4] Use `db` as the span `op` --- .../supabase-nextjs/tests/performance.test.ts | 66 ++++++++++++++++--- packages/core/src/integrations/supabase.ts | 8 ++- 2 files changed, 61 insertions(+), 13 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 80eb1a166e9b..973fe2c16ec0 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 @@ -17,9 +17,14 @@ test('Sends server-side Supabase auth admin `createUser` span', async ({ page, b const transactionEvent = await httpTransactionPromise; expect(transactionEvent.spans).toContainEqual({ - data: expect.any(Object), + data: expect.objectContaining({ + "db.operation": "auth.admin.createUser", + "db.system": "postgresql", + "sentry.op": "db", + "sentry.origin": "auto.db.supabase" + }), description: 'createUser', - op: 'db.auth.admin.createUser', + op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -55,7 +60,17 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ description: 'from(todos)', - op: 'db.select', + op: 'db', + data: expect.objectContaining({ + "db.operation": "select", + "db.query": [ + "select(*)", + "filter(order, asc)" + ], + "db.system": "postgresql", + "sentry.op": "db", + "sentry.origin": "auto.db.supabase" + }), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -67,9 +82,18 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', ); expect(transactionEvent.spans).toContainEqual({ - data: expect.any(Object), + data: expect.objectContaining({ + "db.operation": "select", + "db.query": [ + "select(*)", + "filter(order, asc)" + ], + "db.system": "postgresql", + "sentry.op": "db", + "sentry.origin": "auto.db.supabase" + }), description: 'from(todos)', - op: 'db.insert', + op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -109,8 +133,17 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ + data: expect.objectContaining({ + "db.operation": "insert", + "db.query": [ + "select(*)", + ], + "db.system": "postgresql", + "sentry.op": "db", + "sentry.origin": "auto.db.supabase" + }), description: 'from(todos)', - op: 'db.select', + op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -122,9 +155,17 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', ); expect(transactionEvent.spans).toContainEqual({ - data: expect.any(Object), + data: expect.objectContaining({ + "db.operation": "select", + "db.query": [ + "select(*)", + ], + "db.system": "postgresql", + "sentry.op": "db", + "sentry.origin": "auto.db.supabase" + }), description: 'from(todos)', - op: 'db.insert', + op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -163,9 +204,14 @@ test('Sends server-side Supabase auth admin `listUsers` span', async ({ page, ba const transactionEvent = await httpTransactionPromise; expect(transactionEvent.spans).toContainEqual({ - data: expect.any(Object), + data: expect.objectContaining({ + "db.operation": "auth.admin.listUsers", + "db.system": "postgresql", + "sentry.op": "db", + "sentry.origin": "auto.db.supabase" + }), description: 'listUsers', - op: 'db.auth.admin.listUsers', + op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index b8205b667282..05599a1d3dc3 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -222,7 +222,9 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A name: operation.name, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + 'db.system': 'postgresql', + 'db.operation': `auth.${isAdmin ? 'admin.' : ''}${operation.name}`, }, }, span => { @@ -363,8 +365,9 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte 'db.url': typedThis.url.origin, 'db.sdk': typedThis.headers['X-Client-Info'], 'db.system': 'postgresql', + 'db.operation': operation, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', }; if (queryItems.length) { @@ -377,7 +380,6 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte return startSpan( { - op: 'db', name: description, attributes, }, From 8b518df3b10bb696a0b22a594b3dffdf9e74bd25 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 30 Apr 2025 19:21:55 +0100 Subject: [PATCH 3/4] Update span descriptions --- .../supabase-nextjs/tests/performance.test.ts | 89 ++++++++----------- packages/core/src/integrations/supabase.ts | 11 ++- 2 files changed, 47 insertions(+), 53 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 973fe2c16ec0..cfb66b372420 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 @@ -18,12 +18,12 @@ test('Sends server-side Supabase auth admin `createUser` span', async ({ page, b expect(transactionEvent.spans).toContainEqual({ data: expect.objectContaining({ - "db.operation": "auth.admin.createUser", - "db.system": "postgresql", - "sentry.op": "db", - "sentry.origin": "auto.db.supabase" + 'db.operation': 'auth.admin.createUser', + 'db.system': 'postgresql', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.supabase', }), - description: 'createUser', + description: 'auth (admin) createUser', op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -59,17 +59,14 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ - description: 'from(todos)', + description: 'select(*) filter(order, asc) from(todos)', op: 'db', data: expect.objectContaining({ - "db.operation": "select", - "db.query": [ - "select(*)", - "filter(order, asc)" - ], - "db.system": "postgresql", - "sentry.op": "db", - "sentry.origin": "auto.db.supabase" + 'db.operation': 'select', + 'db.query': ['select(*)', 'filter(order, asc)'], + 'db.system': 'postgresql', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.supabase', }), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -83,16 +80,13 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', expect(transactionEvent.spans).toContainEqual({ data: expect.objectContaining({ - "db.operation": "select", - "db.query": [ - "select(*)", - "filter(order, asc)" - ], - "db.system": "postgresql", - "sentry.op": "db", - "sentry.origin": "auto.db.supabase" + 'db.operation': 'select', + 'db.query': ['select(*)', 'filter(order, asc)'], + 'db.system': 'postgresql', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.supabase', }), - description: 'from(todos)', + description: 'select(*) filter(order, asc) from(todos)', op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -107,7 +101,7 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', timestamp: expect.any(Number), type: 'supabase', category: 'db.select', - message: 'from(todos)', + message: 'select(*) filter(order, asc) from(todos)', data: expect.any(Object), }); @@ -115,7 +109,7 @@ test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', timestamp: expect.any(Number), type: 'supabase', category: 'db.insert', - message: 'from(todos)', + message: 'insert(...) select(*) from(todos)', data: expect.any(Object), }); }); @@ -134,15 +128,13 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - "db.operation": "insert", - "db.query": [ - "select(*)", - ], - "db.system": "postgresql", - "sentry.op": "db", - "sentry.origin": "auto.db.supabase" + 'db.operation': 'insert', + 'db.query': ['select(*)'], + 'db.system': 'postgresql', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.supabase', }), - description: 'from(todos)', + description: 'insert(...) select(*) from(todos)', op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -156,15 +148,13 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', expect(transactionEvent.spans).toContainEqual({ data: expect.objectContaining({ - "db.operation": "select", - "db.query": [ - "select(*)", - ], - "db.system": "postgresql", - "sentry.op": "db", - "sentry.origin": "auto.db.supabase" + 'db.operation': 'select', + 'db.query': ['select(*)'], + 'db.system': 'postgresql', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.supabase', }), - description: 'from(todos)', + description: 'select(*) from(todos)', op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -179,7 +169,7 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', timestamp: expect.any(Number), type: 'supabase', category: 'db.select', - message: 'from(todos)', + message: 'select(*) from(todos)', data: expect.any(Object), }); @@ -187,7 +177,7 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', timestamp: expect.any(Number), type: 'supabase', category: 'db.insert', - message: 'from(todos)', + message: 'insert(...) select(*) from(todos)', data: expect.any(Object), }); }); @@ -195,8 +185,7 @@ test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', test('Sends server-side Supabase auth admin `listUsers` span', async ({ page, baseURL }) => { const httpTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /api/list-users' + transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /api/list-users' ); }); @@ -205,12 +194,12 @@ test('Sends server-side Supabase auth admin `listUsers` span', async ({ page, ba expect(transactionEvent.spans).toContainEqual({ data: expect.objectContaining({ - "db.operation": "auth.admin.listUsers", - "db.system": "postgresql", - "sentry.op": "db", - "sentry.origin": "auto.db.supabase" + 'db.operation': 'auth.admin.listUsers', + 'db.system': 'postgresql', + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.supabase', }), - description: 'listUsers', + description: 'auth (admin) listUsers', op: 'db', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 05599a1d3dc3..084d50356a83 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -219,7 +219,7 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A apply(target, thisArg, argumentsList) { return startSpan( { - name: operation.name, + name: `auth ${isAdmin ? '(admin) ' : ''}${operation.name}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', @@ -343,7 +343,6 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte const pathParts = typedThis.url.pathname.split('/'); const table = pathParts.length > 0 ? pathParts[pathParts.length - 1] : ''; - const description = `from(${table})`; const queryItems: string[] = []; for (const [key, value] of typedThis.url.searchParams.entries()) { @@ -351,7 +350,6 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte // so we need to use array instead of object to collect them. queryItems.push(translateFiltersIntoMethods(key, value)); } - const body: Record = Object.create(null); if (isPlainObject(typedThis.body)) { for (const [key, value] of Object.entries(typedThis.body)) { @@ -359,6 +357,13 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte } } + // Adding operation to the beginning of the description if it's not a `select` operation + // For example, it can be an `insert` or `update` operation but the query can be `select(...)` + // For `select` operations, we don't need repeat it in the description + const description = `${operation === 'select' ? '' : `${operation}${body ? '(...) ' : ''}`}${queryItems.join( + ' ', + )} from(${table})`; + const attributes: Record = { 'db.table': table, 'db.schema': typedThis.schema, From dedce8c9b06267f27764e3ce204e91d5747e83bc Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 1 May 2025 12:44:25 +0100 Subject: [PATCH 4/4] Update browser integration tests --- .../suites/integrations/supabase/auth/test.ts | 25 +++++++++++++------ .../supabase/db-operations/test.ts | 6 +++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index 6366fbac4f7e..c145e64bd1da 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -81,11 +81,12 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); - const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db.auth')); + const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db')); expect(supabaseSpans).toHaveLength(2); expect(supabaseSpans![0]).toMatchObject({ - description: 'signInWithPassword', + description: 'auth signInWithPassword', + op: 'db', parent_span_id: eventData.contexts?.trace?.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -93,13 +94,16 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest trace_id: eventData.contexts?.trace?.trace_id, status: 'ok', data: expect.objectContaining({ - 'sentry.op': 'db.auth.signInWithPassword', + 'sentry.op': 'db', 'sentry.origin': 'auto.db.supabase', + 'db.operation': 'auth.signInWithPassword', + 'db.system': 'postgresql', }), }); expect(supabaseSpans![1]).toMatchObject({ - description: 'signOut', + description: 'auth signOut', + op: 'db', parent_span_id: eventData.contexts?.trace?.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -107,8 +111,10 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest trace_id: eventData.contexts?.trace?.trace_id, status: 'ok', data: expect.objectContaining({ - 'sentry.op': 'db.auth.signOut', + 'sentry.op': 'db', 'sentry.origin': 'auto.db.supabase', + 'db.operation': 'auth.signOut', + 'db.system': 'postgresql', }), }); }); @@ -124,13 +130,14 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db.auth')); + const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db')); expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password'); expect(supabaseSpans).toHaveLength(2); expect(supabaseSpans![0]).toMatchObject({ - description: 'signInWithPassword', + description: 'auth signInWithPassword', + op: 'db', parent_span_id: transactionEvent.contexts?.trace?.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), @@ -138,8 +145,10 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes trace_id: transactionEvent.contexts?.trace?.trace_id, status: 'unknown_error', data: expect.objectContaining({ - 'sentry.op': 'db.auth.signInWithPassword', + 'sentry.op': 'db', 'sentry.origin': 'auto.db.supabase', + 'db.operation': 'auth.signInWithPassword', + 'db.system': 'postgresql', }), }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 2a10caa92c54..132a473e35f1 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -44,8 +44,10 @@ sentryTest('should capture Supabase database operation breadcrumbs', async ({ ge timestamp: expect.any(Number), type: 'supabase', category: 'db.insert', - message: 'from(todos)', - data: expect.any(Object), + message: 'insert(...) filter(columns, ) from(todos)', + data: expect.objectContaining({ + query: expect.arrayContaining(['filter(columns, )']), + }), }); });