-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Changes from 1 commit
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
18cb6b8
feat(core): Add Supabase Integration
onurtemizkan a6c0d4d
Add missing package exports
onurtemizkan ab36c16
Remove debug logging
onurtemizkan 9f58b89
Bump next 14 version
onurtemizkan f37fa5a
Hard-code default development variables
onurtemizkan 68e7774
Add playwright to dev-dependencies.
onurtemizkan d384cc3
Move `supabase` into its own internal package.
onurtemizkan 4973f04
Add new package reference to Remix integration tests
onurtemizkan b5ff6db
Update deps
onurtemizkan 38962d1
Separate anon and service clients.
onurtemizkan dedf54e
Make `supabase` a public package
onurtemizkan 01bf0d6
Add `supabase` to verdaccio config
onurtemizkan 79dd5d7
Remove unused resolutions and dependencies
onurtemizkan 5f00c3c
Move `supabaseIntegration` to `@sentry/core`
onurtemizkan 390744c
Update import paths
onurtemizkan 5b249fb
Fix tests
onurtemizkan 832c4ef
Fix tests
onurtemizkan fa8199e
Fix formatting
onurtemizkan 5123016
Dedupe dependencies.
onurtemizkan 0e335bd
Skip tests on non-tracing bundles
onurtemizkan eeaa8a2
Remove test-debug mode
onurtemizkan adc5e10
Try reducing bundle size
onurtemizkan cad1352
Remove `supabaseIntegration` from non-Tracing bundles
onurtemizkan f0d66ce
Bring filter-mappings back.
onurtemizkan 5cb5f58
Export supabase from all tracing bundles
onurtemizkan 16e4a9d
Add vendor license
onurtemizkan 5f67b4a
Clean up
onurtemizkan 318f0a5
Add `auth` support
onurtemizkan 1acde43
Add `auth` error capturing
onurtemizkan e01ce65
Remove `signOut` from `admin` operations
onurtemizkan 7535386
Clean up
onurtemizkan 0532f8a
Address review comments
onurtemizkan 0ea2cdb
Update packages/core/src/integrations/supabase.ts
onurtemizkan af31dcf
Remove README.md
onurtemizkan ace4036
Mark SupabaseConstructor objects as instrumented and check for rewrap…
onurtemizkan 116fca5
Update packages/core/src/integrations/supabase.ts
onurtemizkan 68b04a4
Expose `instrumentSupabase`
onurtemizkan affaf36
Lint
onurtemizkan ba7c4e3
Add `db.system` attribute
onurtemizkan 2d8ca55
Update test `dsn`s and tunnel
onurtemizkan c6ef16f
Update auth `op`s
onurtemizkan d61aefe
Dedupe deps
onurtemizkan 0c3ff1d
Fix empty arguments on `auth`
onurtemizkan 556703c
Mark and check `auth` as instrumented
onurtemizkan 5776eb7
Rename to `instrumentSupabaseClient` with separate options
onurtemizkan f081a5d
Remove unnecessary option
1e87c93
Merge branch 'develop' into onur/supabase-integration
975d061
tests
9c2aa4d
woops
2c638eb
Merge branch 'develop' into onur/supabase-integration
83902e7
Fix test usage
onurtemizkan 3f5e172
Merge branch 'develop' into onur/supabase-integration
82043f0
Don't export from browser bundles yet
d4e3d09
Make types compatible with TS 3.x
onurtemizkan 0ae2ed4
Skip browser bundles on supabase integration tests
onurtemizkan 2f8b5e6
Skip bundle tests per file
onurtemizkan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add
auth
support
- Loading branch information
commit 318f0a56f92884c0e69f365eefe78c5ce8d019e0
There are no files selected for viewing
34 changes: 34 additions & 0 deletions
34
dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
import { createClient } from '@supabase/supabase-js'; | ||
window.Sentry = Sentry; | ||
|
||
const supabase = createClient( | ||
'https://test.supabase.co', | ||
'test-key' | ||
); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
integrations: [ | ||
Sentry.browserTracingIntegration(), | ||
Sentry.supabaseIntegration(supabase) | ||
], | ||
tracesSampleRate: 1.0, | ||
}); | ||
|
||
// Simulate authentication operations | ||
async function performAuthenticationOperations() { | ||
try { | ||
await supabase.auth.signInWithPassword({ | ||
email: '[email protected]', | ||
password: 'test-password', | ||
}); | ||
|
||
await supabase.auth.signOut(); | ||
} catch (error) { | ||
Sentry.captureException(error); | ||
} | ||
} | ||
|
||
performAuthenticationOperations(); |
81 changes: 81 additions & 0 deletions
81
dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
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 mockSupabaseAuthRoutes(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', | ||
}, | ||
}); | ||
}); | ||
} | ||
|
||
sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => { | ||
if (shouldSkipTracingTest()) { | ||
return; | ||
} | ||
|
||
await mockSupabaseAuthRoutes(page); | ||
|
||
const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
||
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url); | ||
const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db.supabase.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.supabase.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.supabase.auth.signOut', | ||
'sentry.origin': 'auto.db.supabase', | ||
}), | ||
}); | ||
}); |
2 changes: 1 addition & 1 deletion
2
dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,56 @@ | ||
// Ported from Kamil Ogórek's work on: | ||
// Based on Kamil Ogórek's work on: | ||
// https://github.com/supabase-community/sentry-integration-js | ||
|
||
// MIT License | ||
|
||
// Copyright (c) 2024 Supabase | ||
|
||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
|
||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
// SOFTWARE. | ||
|
||
/* eslint-disable max-lines */ | ||
import { logger, isPlainObject } from '../utils-hoist'; | ||
|
||
import type { IntegrationFn } from '../types-hoist'; | ||
import { setHttpStatus, startInactiveSpan } from '../tracing'; | ||
import { setHttpStatus, startInactiveSpan, startSpan } from '../tracing'; | ||
import { addBreadcrumb } from '../breadcrumbs'; | ||
import { defineIntegration } from '../integration'; | ||
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; | ||
import { captureException } from '../exports'; | ||
import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '../tracing'; | ||
|
||
export interface SupabaseClient { | ||
export interface SupabaseClientConstructor { | ||
prototype: { | ||
from: (table: string) => PostgrestQueryBuilder; | ||
}; | ||
} | ||
|
||
const AUTH_OPERATIONS_TO_INSTRUMENT = [ | ||
'reauthenticate', | ||
'signInAnonymously', | ||
'signInWithOAuth', | ||
'signInWithIdToken', | ||
'signInWithOtp', | ||
'signInWithPassword', | ||
'signInWithSSO', | ||
'signOut', | ||
'signUp', | ||
'verifyOtp', | ||
]; | ||
|
||
const AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT = [ | ||
'createUser', | ||
'deleteUser', | ||
'listUsers', | ||
'getUserById', | ||
'updateUserById', | ||
'inviteUserByEmail', | ||
'signOut', | ||
]; | ||
|
||
type AuthOperationFn = (...args: unknown[]) => Promise<unknown>; | ||
type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; | ||
type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; | ||
|
||
export interface SupabaseClientInstance { | ||
auth: { | ||
admin: Record<AuthAdminOperationName, AuthOperationFn>; | ||
} & Record<AuthOperationName, AuthOperationFn>; | ||
} | ||
|
||
export interface PostgrestQueryBuilder { | ||
select: (...args: unknown[]) => PostgrestFilterBuilder; | ||
insert: (...args: unknown[]) => PostgrestFilterBuilder; | ||
|
@@ -181,17 +193,65 @@ export function translateFiltersIntoMethods(key: string, query: string): string | |
return `${method}(${key}, ${value.join('.')})`; | ||
} | ||
|
||
function instrumentSupabaseClient(SupabaseClient: unknown): void { | ||
if (instrumented.has(SupabaseClient)) { | ||
function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn { | ||
return new Proxy(operation, { | ||
apply(target, thisArg, argumentsList) { | ||
startSpan( | ||
{ | ||
name: operation.name, | ||
attributes: { | ||
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', | ||
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, | ||
}, | ||
}, | ||
span => { | ||
return Reflect.apply(target, thisArg, argumentsList).then((res: unknown) => { | ||
debugger; | ||
if (res && typeof res === 'object' && 'error' in res && res.error) { | ||
span.setStatus({ code: SPAN_STATUS_ERROR }); | ||
} else { | ||
span.setStatus({ code: SPAN_STATUS_OK }); | ||
} | ||
|
||
span.end(); | ||
debugger; | ||
return res; | ||
}); | ||
}, | ||
); | ||
}, | ||
}); | ||
} | ||
|
||
function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInstance): void { | ||
const auth = supabaseClientInstance.auth; | ||
|
||
if (!auth) { | ||
return; | ||
} | ||
|
||
instrumented.set(SupabaseClient, { | ||
from: (SupabaseClient as unknown as SupabaseClient).prototype.from, | ||
AUTH_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthOperationName) => { | ||
const authOperation = auth[operation]; | ||
if (typeof authOperation === 'function') { | ||
auth[operation] = instrumentAuthOperation(authOperation); | ||
} | ||
}); | ||
|
||
(SupabaseClient as unknown as SupabaseClient).prototype.from = new Proxy( | ||
(SupabaseClient as unknown as SupabaseClient).prototype.from, | ||
AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthAdminOperationName) => { | ||
const authAdminOperation = auth.admin[operation]; | ||
if (typeof authAdminOperation === 'function') { | ||
auth.admin[operation] = instrumentAuthOperation(authAdminOperation); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this need the Maybe also add a test for the admin behavior. |
||
} | ||
}); | ||
} | ||
|
||
function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void { | ||
if (instrumented.has(SupabaseClient)) { | ||
return; | ||
} | ||
|
||
(SupabaseClient as unknown as SupabaseClientConstructor).prototype.from = new Proxy( | ||
(SupabaseClient as unknown as SupabaseClientConstructor).prototype.from, | ||
{ | ||
apply(target, thisArg, argumentsList) { | ||
const rv = Reflect.apply(target, thisArg, argumentsList); | ||
|
@@ -398,31 +458,15 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr | |
} | ||
} | ||
|
||
export const patchCreateClient = (moduleExports: { createClient?: (...args: unknown[]) => unknown }): void => { | ||
const originalCreateClient = moduleExports.createClient; | ||
if (!originalCreateClient) { | ||
return; | ||
} | ||
|
||
moduleExports.createClient = function wrappedCreateClient(...args: any[]) { | ||
const client = originalCreateClient.apply(this, args); | ||
|
||
instrumentSupabaseClient(client); | ||
|
||
return client; | ||
}; | ||
}; | ||
|
||
const instrumentSupabase = (supabaseClient: unknown): void => { | ||
if (!supabaseClient) { | ||
throw new Error('SupabaseClient class constructor is required'); | ||
const instrumentSupabase = (supabaseClientInstance: unknown): void => { | ||
if (!supabaseClientInstance) { | ||
throw new Error('Supabase client instance is not defined.'); | ||
} | ||
const SupabaseClientConstructor = | ||
supabaseClientInstance.constructor === Function ? supabaseClientInstance : supabaseClientInstance.constructor; | ||
|
||
// We want to allow passing either `SupabaseClient` constructor | ||
// or an instance returned from `createClient()`. | ||
const SupabaseClient = supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor; | ||
|
||
instrumentSupabaseClient(SupabaseClient); | ||
instrumentSupabaseClientConstructor(SupabaseClientConstructor); | ||
instrumentSupabaseAuthClient(supabaseClientInstance as SupabaseClientInstance); | ||
}; | ||
|
||
const INTEGRATION_NAME = 'Supabase'; | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to check for
instrumented.has()
here as well?