Skip to content

Commit 263eeee

Browse files
onurtemizkans1gr1dlforst
authored
feat: Add Supabase Integration (#15719)
Ref: #15436 Summary: - Ports https://github.com/supabase-community/sentry-integration-js into `@sentry/core` - Adds support for `auth` and `auth.admin` operations - Adds browser integration tests - Adds E2E tests running on NextJS --------- Co-authored-by: Sigrid Huemer <[email protected]> Co-authored-by: Luca Forstner <[email protected]>
1 parent 87e5f8b commit 263eeee

File tree

49 files changed

+2050
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2050
-1
lines changed

Diff for: dev-packages/browser-integration-tests/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@playwright/test": "~1.50.0",
4444
"@sentry-internal/rrweb": "2.34.0",
4545
"@sentry/browser": "9.13.0",
46+
"@supabase/supabase-js": "2.49.3",
4647
"axios": "1.8.2",
4748
"babel-loader": "^8.2.2",
4849
"fflate": "0.8.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
import { createClient } from '@supabase/supabase-js';
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key');
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
11+
tracesSampleRate: 1.0,
12+
});
13+
14+
// Simulate authentication operations
15+
async function performAuthenticationOperations() {
16+
await supabaseClient.auth.signInWithPassword({
17+
18+
password: 'test-password',
19+
});
20+
21+
await supabaseClient.auth.signOut();
22+
}
23+
24+
performAuthenticationOperations();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/core';
4+
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import {
7+
getFirstSentryEnvelopeRequest,
8+
getMultipleSentryEnvelopeRequests,
9+
shouldSkipTracingTest,
10+
} from '../../../../utils/helpers';
11+
12+
async function mockSupabaseAuthRoutesSuccess(page: Page) {
13+
await page.route('**/auth/v1/token?grant_type=password**', route => {
14+
return route.fulfill({
15+
status: 200,
16+
body: JSON.stringify({
17+
access_token: 'test-access-token',
18+
refresh_token: 'test-refresh-token',
19+
token_type: 'bearer',
20+
expires_in: 3600,
21+
}),
22+
headers: {
23+
'Content-Type': 'application/json',
24+
},
25+
});
26+
});
27+
28+
await page.route('**/auth/v1/logout**', route => {
29+
return route.fulfill({
30+
status: 200,
31+
body: JSON.stringify({
32+
message: 'Logged out',
33+
}),
34+
headers: {
35+
'Content-Type': 'application/json',
36+
},
37+
});
38+
});
39+
}
40+
41+
async function mockSupabaseAuthRoutesFailure(page: Page) {
42+
await page.route('**/auth/v1/token?grant_type=password**', route => {
43+
return route.fulfill({
44+
status: 400,
45+
body: JSON.stringify({
46+
error_description: 'Invalid email or password',
47+
error: 'invalid_grant',
48+
}),
49+
headers: {
50+
'Content-Type': 'application/json',
51+
},
52+
});
53+
});
54+
55+
await page.route('**/auth/v1/logout**', route => {
56+
return route.fulfill({
57+
status: 400,
58+
body: JSON.stringify({
59+
error_description: 'Invalid refresh token',
60+
error: 'invalid_grant',
61+
}),
62+
headers: {
63+
'Content-Type': 'application/json',
64+
},
65+
});
66+
});
67+
}
68+
69+
70+
const bundle = process.env.PW_BUNDLE || '';
71+
// We only want to run this in non-CDN bundle mode
72+
if (bundle.startsWith('bundle')) {
73+
sentryTest.skip();
74+
}
75+
76+
sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => {
77+
if (shouldSkipTracingTest()) {
78+
return;
79+
}
80+
81+
await mockSupabaseAuthRoutesSuccess(page);
82+
83+
const url = await getLocalTestUrl({ testDir: __dirname });
84+
85+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
86+
const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db.auth'));
87+
88+
expect(supabaseSpans).toHaveLength(2);
89+
expect(supabaseSpans![0]).toMatchObject({
90+
description: 'signInWithPassword',
91+
parent_span_id: eventData.contexts?.trace?.span_id,
92+
span_id: expect.any(String),
93+
start_timestamp: expect.any(Number),
94+
timestamp: expect.any(Number),
95+
trace_id: eventData.contexts?.trace?.trace_id,
96+
status: 'ok',
97+
data: expect.objectContaining({
98+
'sentry.op': 'db.auth.signInWithPassword',
99+
'sentry.origin': 'auto.db.supabase',
100+
}),
101+
});
102+
103+
expect(supabaseSpans![1]).toMatchObject({
104+
description: 'signOut',
105+
parent_span_id: eventData.contexts?.trace?.span_id,
106+
span_id: expect.any(String),
107+
start_timestamp: expect.any(Number),
108+
timestamp: expect.any(Number),
109+
trace_id: eventData.contexts?.trace?.trace_id,
110+
status: 'ok',
111+
data: expect.objectContaining({
112+
'sentry.op': 'db.auth.signOut',
113+
'sentry.origin': 'auto.db.supabase',
114+
}),
115+
});
116+
});
117+
118+
sentryTest('should capture Supabase authentication errors', async ({ getLocalTestUrl, page }) => {
119+
if (shouldSkipTracingTest()) {
120+
return;
121+
}
122+
123+
await mockSupabaseAuthRoutesFailure(page);
124+
125+
const url = await getLocalTestUrl({ testDir: __dirname });
126+
127+
const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests<Event>(page, 2, { url });
128+
129+
const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db.auth'));
130+
131+
expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password');
132+
133+
expect(supabaseSpans).toHaveLength(2);
134+
expect(supabaseSpans![0]).toMatchObject({
135+
description: 'signInWithPassword',
136+
parent_span_id: transactionEvent.contexts?.trace?.span_id,
137+
span_id: expect.any(String),
138+
start_timestamp: expect.any(Number),
139+
timestamp: expect.any(Number),
140+
trace_id: transactionEvent.contexts?.trace?.trace_id,
141+
status: 'unknown_error',
142+
data: expect.objectContaining({
143+
'sentry.op': 'db.auth.signInWithPassword',
144+
'sentry.origin': 'auto.db.supabase',
145+
}),
146+
});
147+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
import { createClient } from '@supabase/supabase-js';
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key');
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
11+
tracesSampleRate: 1.0,
12+
});
13+
14+
// Simulate database operations
15+
async function performDatabaseOperations() {
16+
try {
17+
await supabaseClient.from('todos').insert([{ title: 'Test Todo' }]);
18+
19+
await supabaseClient.from('todos').select('*');
20+
21+
// Trigger an error to capture the breadcrumbs
22+
throw new Error('Test Error');
23+
} catch (error) {
24+
Sentry.captureException(error);
25+
}
26+
}
27+
28+
performDatabaseOperations();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/core';
4+
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';
7+
8+
async function mockSupabaseRoute(page: Page) {
9+
await page.route('**/rest/v1/todos**', route => {
10+
return route.fulfill({
11+
status: 200,
12+
body: JSON.stringify({
13+
userNames: ['John', 'Jane'],
14+
}),
15+
headers: {
16+
'Content-Type': 'application/json',
17+
},
18+
});
19+
});
20+
}
21+
22+
23+
const bundle = process.env.PW_BUNDLE || '';
24+
// We only want to run this in non-CDN bundle mode
25+
if (bundle.startsWith('bundle')) {
26+
sentryTest.skip();
27+
}
28+
29+
30+
sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => {
31+
if (shouldSkipTracingTest()) {
32+
return;
33+
}
34+
35+
await mockSupabaseRoute(page);
36+
37+
const url = await getLocalTestUrl({ testDir: __dirname });
38+
39+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
40+
41+
expect(eventData.breadcrumbs).toBeDefined();
42+
expect(eventData.breadcrumbs).toContainEqual({
43+
timestamp: expect.any(Number),
44+
type: 'supabase',
45+
category: 'db.insert',
46+
message: 'from(todos)',
47+
data: expect.any(Object),
48+
});
49+
});
50+
51+
sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => {
52+
if (shouldSkipTracingTest()) {
53+
return;
54+
}
55+
56+
await mockSupabaseRoute(page);
57+
58+
const url = await getLocalTestUrl({ testDir: __dirname });
59+
60+
const events = await getMultipleSentryEnvelopeRequests<Event>(page, 2, { url });
61+
62+
expect(events).toHaveLength(2);
63+
64+
events.forEach(event => {
65+
expect(
66+
event.breadcrumbs?.some(breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.')),
67+
).toBe(true);
68+
});
69+
});
70+
71+
sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => {
72+
if (shouldSkipTracingTest()) {
73+
return;
74+
}
75+
76+
await mockSupabaseRoute(page);
77+
78+
const url = await getLocalTestUrl({ testDir: __dirname });
79+
80+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
81+
82+
const supabaseBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase');
83+
84+
expect(supabaseBreadcrumb).toBeDefined();
85+
expect(supabaseBreadcrumb?.data).toMatchObject({
86+
query: expect.arrayContaining([
87+
'filter(columns, )'
88+
]),
89+
});
90+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
38+
# Sentry Config File
39+
.env.sentry-build-plugin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873

0 commit comments

Comments
 (0)