Skip to content

feat: Add Supabase Integration #15719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
18cb6b8
feat(core): Add Supabase Integration
onurtemizkan Mar 18, 2025
a6c0d4d
Add missing package exports
onurtemizkan Mar 18, 2025
ab36c16
Remove debug logging
onurtemizkan Mar 18, 2025
9f58b89
Bump next 14 version
onurtemizkan Mar 18, 2025
f37fa5a
Hard-code default development variables
onurtemizkan Mar 18, 2025
68e7774
Add playwright to dev-dependencies.
onurtemizkan Mar 18, 2025
d384cc3
Move `supabase` into its own internal package.
onurtemizkan Mar 18, 2025
4973f04
Add new package reference to Remix integration tests
onurtemizkan Mar 18, 2025
b5ff6db
Update deps
onurtemizkan Mar 19, 2025
38962d1
Separate anon and service clients.
onurtemizkan Mar 19, 2025
dedf54e
Make `supabase` a public package
onurtemizkan Mar 19, 2025
01bf0d6
Add `supabase` to verdaccio config
onurtemizkan Mar 19, 2025
79dd5d7
Remove unused resolutions and dependencies
onurtemizkan Mar 20, 2025
5f00c3c
Move `supabaseIntegration` to `@sentry/core`
onurtemizkan Mar 24, 2025
390744c
Update import paths
onurtemizkan Mar 25, 2025
5b249fb
Fix tests
onurtemizkan Mar 25, 2025
832c4ef
Fix tests
onurtemizkan Mar 25, 2025
fa8199e
Fix formatting
onurtemizkan Mar 25, 2025
5123016
Dedupe dependencies.
onurtemizkan Mar 25, 2025
0e335bd
Skip tests on non-tracing bundles
onurtemizkan Mar 26, 2025
eeaa8a2
Remove test-debug mode
onurtemizkan Mar 26, 2025
adc5e10
Try reducing bundle size
onurtemizkan Mar 26, 2025
cad1352
Remove `supabaseIntegration` from non-Tracing bundles
onurtemizkan Mar 26, 2025
f0d66ce
Bring filter-mappings back.
onurtemizkan Mar 26, 2025
5cb5f58
Export supabase from all tracing bundles
onurtemizkan Mar 26, 2025
16e4a9d
Add vendor license
onurtemizkan Mar 26, 2025
5f67b4a
Clean up
onurtemizkan Mar 27, 2025
318f0a5
Add `auth` support
onurtemizkan Mar 27, 2025
1acde43
Add `auth` error capturing
onurtemizkan Mar 27, 2025
e01ce65
Remove `signOut` from `admin` operations
onurtemizkan Mar 27, 2025
7535386
Clean up
onurtemizkan Mar 31, 2025
0532f8a
Address review comments
onurtemizkan Apr 3, 2025
0ea2cdb
Update packages/core/src/integrations/supabase.ts
onurtemizkan Apr 3, 2025
af31dcf
Remove README.md
onurtemizkan Apr 3, 2025
ace4036
Mark SupabaseConstructor objects as instrumented and check for rewrap…
onurtemizkan Apr 10, 2025
116fca5
Update packages/core/src/integrations/supabase.ts
onurtemizkan Apr 10, 2025
68b04a4
Expose `instrumentSupabase`
onurtemizkan Apr 10, 2025
affaf36
Lint
onurtemizkan Apr 10, 2025
ba7c4e3
Add `db.system` attribute
onurtemizkan Apr 11, 2025
2d8ca55
Update test `dsn`s and tunnel
onurtemizkan Apr 11, 2025
c6ef16f
Update auth `op`s
onurtemizkan Apr 11, 2025
d61aefe
Dedupe deps
onurtemizkan Apr 11, 2025
0c3ff1d
Fix empty arguments on `auth`
onurtemizkan Apr 11, 2025
556703c
Mark and check `auth` as instrumented
onurtemizkan Apr 14, 2025
5776eb7
Rename to `instrumentSupabaseClient` with separate options
onurtemizkan Apr 16, 2025
f081a5d
Remove unnecessary option
lforst Apr 16, 2025
1e87c93
Merge branch 'develop' into onur/supabase-integration
lforst Apr 16, 2025
975d061
tests
lforst Apr 16, 2025
9c2aa4d
woops
lforst Apr 16, 2025
2c638eb
Merge branch 'develop' into onur/supabase-integration
lforst Apr 16, 2025
83902e7
Fix test usage
onurtemizkan Apr 16, 2025
3f5e172
Merge branch 'develop' into onur/supabase-integration
lforst Apr 17, 2025
82043f0
Don't export from browser bundles yet
lforst Apr 17, 2025
d4e3d09
Make types compatible with TS 3.x
onurtemizkan Apr 17, 2025
0ae2ed4
Skip browser bundles on supabase integration tests
onurtemizkan Apr 17, 2025
2f8b5e6
Skip bundle tests per file
onurtemizkan Apr 17, 2025
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
1 change: 1 addition & 0 deletions dev-packages/browser-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@playwright/test": "~1.50.0",
"@sentry-internal/rrweb": "2.34.0",
"@sentry/browser": "9.13.0",
"@supabase/supabase-js": "2.49.3",
"axios": "1.8.2",
"babel-loader": "^8.2.2",
"fflate": "0.8.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Sentry from '@sentry/browser';

import { createClient } from '@supabase/supabase-js';
window.Sentry = Sentry;

const supabaseClient = createClient('https://test.supabase.co', 'test-key');

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
tracesSampleRate: 1.0,
});

// Simulate authentication operations
async function performAuthenticationOperations() {
await supabaseClient.auth.signInWithPassword({
email: '[email protected]',
password: 'test-password',
});

await supabaseClient.auth.signOut();
}

performAuthenticationOperations();
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';

import { sentryTest } from '../../../../utils/fixtures';
import {
getFirstSentryEnvelopeRequest,
getMultipleSentryEnvelopeRequests,
shouldSkipTracingTest,
} from '../../../../utils/helpers';

async function mockSupabaseAuthRoutesSuccess(page: Page) {
await page.route('**/auth/v1/token?grant_type=password**', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
token_type: 'bearer',
expires_in: 3600,
}),
headers: {
'Content-Type': 'application/json',
},
});
});

await page.route('**/auth/v1/logout**', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
message: 'Logged out',
}),
headers: {
'Content-Type': 'application/json',
},
});
});
}

async function mockSupabaseAuthRoutesFailure(page: Page) {
await page.route('**/auth/v1/token?grant_type=password**', route => {
return route.fulfill({
status: 400,
body: JSON.stringify({
error_description: 'Invalid email or password',
error: 'invalid_grant',
}),
headers: {
'Content-Type': 'application/json',
},
});
});

await page.route('**/auth/v1/logout**', route => {
return route.fulfill({
status: 400,
body: JSON.stringify({
error_description: 'Invalid refresh token',
error: 'invalid_grant',
}),
headers: {
'Content-Type': 'application/json',
},
});
});
}


const bundle = process.env.PW_BUNDLE || '';
// We only want to run this in non-CDN bundle mode
if (bundle.startsWith('bundle')) {
sentryTest.skip();
}

sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

await mockSupabaseAuthRoutesSuccess(page);

const url = await getLocalTestUrl({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db.auth'));

expect(supabaseSpans).toHaveLength(2);
expect(supabaseSpans![0]).toMatchObject({
description: 'signInWithPassword',
parent_span_id: eventData.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: eventData.contexts?.trace?.trace_id,
status: 'ok',
data: expect.objectContaining({
'sentry.op': 'db.auth.signInWithPassword',
'sentry.origin': 'auto.db.supabase',
}),
});

expect(supabaseSpans![1]).toMatchObject({
description: 'signOut',
parent_span_id: eventData.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: eventData.contexts?.trace?.trace_id,
status: 'ok',
data: expect.objectContaining({
'sentry.op': 'db.auth.signOut',
'sentry.origin': 'auto.db.supabase',
}),
});
});

sentryTest('should capture Supabase authentication errors', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

await mockSupabaseAuthRoutesFailure(page);

const url = await getLocalTestUrl({ testDir: __dirname });

const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests<Event>(page, 2, { url });

const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db.auth'));

expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password');

expect(supabaseSpans).toHaveLength(2);
expect(supabaseSpans![0]).toMatchObject({
description: 'signInWithPassword',
parent_span_id: transactionEvent.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: transactionEvent.contexts?.trace?.trace_id,
status: 'unknown_error',
data: expect.objectContaining({
'sentry.op': 'db.auth.signInWithPassword',
'sentry.origin': 'auto.db.supabase',
}),
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as Sentry from '@sentry/browser';

import { createClient } from '@supabase/supabase-js';
window.Sentry = Sentry;

const supabaseClient = createClient('https://test.supabase.co', 'test-key');

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
tracesSampleRate: 1.0,
});

// Simulate database operations
async function performDatabaseOperations() {
try {
await supabaseClient.from('todos').insert([{ title: 'Test Todo' }]);

await supabaseClient.from('todos').select('*');

// Trigger an error to capture the breadcrumbs
throw new Error('Test Error');
} catch (error) {
Sentry.captureException(error);
}
}

performDatabaseOperations();
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';

import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';

async function mockSupabaseRoute(page: Page) {
await page.route('**/rest/v1/todos**', route => {
return route.fulfill({
status: 200,
body: JSON.stringify({
userNames: ['John', 'Jane'],
}),
headers: {
'Content-Type': 'application/json',
},
});
});
}


const bundle = process.env.PW_BUNDLE || '';
// We only want to run this in non-CDN bundle mode
if (bundle.startsWith('bundle')) {
sentryTest.skip();
}


sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

await mockSupabaseRoute(page);

const url = await getLocalTestUrl({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.breadcrumbs).toBeDefined();
expect(eventData.breadcrumbs).toContainEqual({
timestamp: expect.any(Number),
type: 'supabase',
category: 'db.insert',
message: 'from(todos)',
data: expect.any(Object),
});
});

sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

await mockSupabaseRoute(page);

const url = await getLocalTestUrl({ testDir: __dirname });

const events = await getMultipleSentryEnvelopeRequests<Event>(page, 2, { url });

expect(events).toHaveLength(2);

events.forEach(event => {
expect(
event.breadcrumbs?.some(breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.')),
).toBe(true);
});
});

sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
return;
}

await mockSupabaseRoute(page);

const url = await getLocalTestUrl({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

const supabaseBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase');

expect(supabaseBreadcrumb).toBeDefined();
expect(supabaseBreadcrumb?.data).toMatchObject({
query: expect.arrayContaining([
'filter(columns, )'
]),
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Sentry Config File
.env.sentry-build-plugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Loading
Loading