@@ -8,21 +8,113 @@ import {
88} from '../../../src/integrations/supabase' ;
99import type { PostgRESTQueryBuilder , SupabaseClientInstance } from '../../../src/integrations/supabase' ;
1010
11- // Mock tracing to avoid needing full SDK setup
12- vi . mock ( '../../../src/tracing' , ( ) => ( {
13- startSpan : ( _opts : any , cb : ( span : any ) => any ) => {
11+ const tracingMocks = vi . hoisted ( ( ) => ( {
12+ startSpan : vi . fn ( ( _opts : unknown , cb : ( span : unknown ) => unknown ) => {
1413 const mockSpan = {
1514 setStatus : vi . fn ( ) ,
1615 end : vi . fn ( ) ,
1716 } ;
1817 return cb ( mockSpan ) ;
19- } ,
18+ } ) ,
19+ } ) ) ;
20+
21+ const currentScopesMocks = vi . hoisted ( ( ) => ( {
22+ getClient : vi . fn ( ) ,
23+ } ) ) ;
24+
25+ // Mock tracing to avoid needing full SDK setup
26+ vi . mock ( '../../../src/tracing' , ( ) => ( {
27+ startSpan : tracingMocks . startSpan ,
2028 setHttpStatus : vi . fn ( ) ,
2129 SPAN_STATUS_OK : 1 ,
2230 SPAN_STATUS_ERROR : 2 ,
2331} ) ) ;
2432
33+ vi . mock ( '../../../src/currentScopes' , ( ) => ( {
34+ getClient : currentScopesMocks . getClient ,
35+ } ) ) ;
36+
37+ type CreateMockSupabaseClientOptions = {
38+ method ?: string ;
39+ url ?: URL | string ;
40+ body ?: unknown ;
41+ /** When set, configures the mocked Sentry client `sendDefaultPii`. Omit to leave `getClient` to the test file `beforeEach`. */
42+ sendDefaultPii ?: boolean ;
43+ } ;
44+
45+ const DEFAULT_MOCK_SUPABASE_REST_URL = 'https://example.supabase.co/rest/v1/todos' ;
46+
47+ /** Shared PATCH + query string + body shape for `sendDefaultPii` tests. */
48+ const MOCK_SUPABASE_PII_SCENARIO : Pick < CreateMockSupabaseClientOptions , 'method' | 'url' | 'body' > = {
49+ method : 'PATCH' ,
50+ url : 'https://example.supabase.co/rest/v1/users?email=eq.secret%40example.com&select=id' ,
51+ body : { full_name : 'Jane Doe' , phone : '555-0100' } ,
52+ } ;
53+
54+ function createMockSupabaseClient ( resolveWith : unknown , options ?: CreateMockSupabaseClientOptions ) : unknown {
55+ if ( options ?. sendDefaultPii !== undefined ) {
56+ currentScopesMocks . getClient . mockReturnValue ( {
57+ getOptions : ( ) => ( { sendDefaultPii : options . sendDefaultPii } ) ,
58+ } as any ) ;
59+ }
60+
61+ const method = options ?. method ?? 'GET' ;
62+ const requestUrl =
63+ options ?. url !== undefined
64+ ? options . url instanceof URL
65+ ? options . url
66+ : new URL ( options . url )
67+ : new URL ( DEFAULT_MOCK_SUPABASE_REST_URL ) ;
68+ const body = options ?. body ;
69+
70+ class MockPostgRESTFilterBuilder {
71+ method = method ;
72+ headers : Record < string , string > = { 'X-Client-Info' : 'supabase-js/2.0.0' } ;
73+ url = requestUrl ;
74+ schema = 'public' ;
75+ body = body ;
76+
77+ then ( onfulfilled ?: ( value : any ) => any , onrejected ?: ( reason : any ) => any ) : Promise < any > {
78+ return Promise . resolve ( resolveWith ) . then ( onfulfilled , onrejected ) ;
79+ }
80+ }
81+
82+ class MockPostgRESTQueryBuilder {
83+ select ( ) {
84+ return new MockPostgRESTFilterBuilder ( ) ;
85+ }
86+ insert ( ) {
87+ return new MockPostgRESTFilterBuilder ( ) ;
88+ }
89+ upsert ( ) {
90+ return new MockPostgRESTFilterBuilder ( ) ;
91+ }
92+ update ( ) {
93+ return new MockPostgRESTFilterBuilder ( ) ;
94+ }
95+ delete ( ) {
96+ return new MockPostgRESTFilterBuilder ( ) ;
97+ }
98+ }
99+
100+ class MockSupabaseClient {
101+ auth = {
102+ admin : { } as any ,
103+ } as SupabaseClientInstance [ 'auth' ] ;
104+
105+ from ( _table : string ) : PostgRESTQueryBuilder {
106+ return new MockPostgRESTQueryBuilder ( ) as unknown as PostgRESTQueryBuilder ;
107+ }
108+ }
109+
110+ return new MockSupabaseClient ( ) ;
111+ }
112+
25113describe ( 'Supabase Integration' , ( ) => {
114+ beforeEach ( ( ) => {
115+ currentScopesMocks . getClient . mockReturnValue ( undefined ) ;
116+ } ) ;
117+
26118 describe ( 'extractOperation' , ( ) => {
27119 it ( 'returns select for GET' , ( ) => {
28120 expect ( extractOperation ( 'GET' ) ) . toBe ( 'select' ) ;
@@ -72,52 +164,6 @@ describe('Supabase Integration', () => {
72164 vi . restoreAllMocks ( ) ;
73165 } ) ;
74166
75- function createMockSupabaseClient ( resolveWith : unknown ) : unknown {
76- // Create a PostgRESTFilterBuilder-like class
77- class MockPostgRESTFilterBuilder {
78- method = 'GET' ;
79- headers : Record < string , string > = { 'X-Client-Info' : 'supabase-js/2.0.0' } ;
80- url = new URL ( 'https://example.supabase.co/rest/v1/todos' ) ;
81- schema = 'public' ;
82- body = undefined ;
83-
84- then ( onfulfilled ?: ( value : any ) => any , onrejected ?: ( reason : any ) => any ) : Promise < any > {
85- return Promise . resolve ( resolveWith ) . then ( onfulfilled , onrejected ) ;
86- }
87- }
88-
89- class MockPostgRESTQueryBuilder {
90- select ( ) {
91- return new MockPostgRESTFilterBuilder ( ) ;
92- }
93- insert ( ) {
94- return new MockPostgRESTFilterBuilder ( ) ;
95- }
96- upsert ( ) {
97- return new MockPostgRESTFilterBuilder ( ) ;
98- }
99- update ( ) {
100- return new MockPostgRESTFilterBuilder ( ) ;
101- }
102- delete ( ) {
103- return new MockPostgRESTFilterBuilder ( ) ;
104- }
105- }
106-
107- // Create a mock SupabaseClient constructor
108- class MockSupabaseClient {
109- auth = {
110- admin : { } as any ,
111- } as SupabaseClientInstance [ 'auth' ] ;
112-
113- from ( _table : string ) : PostgRESTQueryBuilder {
114- return new MockPostgRESTQueryBuilder ( ) as unknown as PostgRESTQueryBuilder ;
115- }
116- }
117-
118- return new MockSupabaseClient ( ) ;
119- }
120-
121167 it ( 'handles undefined response without throwing' , async ( ) => {
122168 const client = createMockSupabaseClient ( undefined ) ;
123169 instrumentSupabaseClient ( client ) ;
@@ -176,4 +222,92 @@ describe('Supabase Integration', () => {
176222 expect ( captureExceptionSpy ) . toHaveBeenCalled ( ) ;
177223 } ) ;
178224 } ) ;
225+
226+ describe ( 'sendDefaultPii' , ( ) => {
227+ let captureExceptionSpy : ReturnType < typeof vi . spyOn > ;
228+ let addBreadcrumbSpy : ReturnType < typeof vi . spyOn > ;
229+
230+ beforeEach ( ( ) => {
231+ captureExceptionSpy = vi . spyOn ( exportsModule , 'captureException' ) . mockImplementation ( ( ) => '' ) ;
232+ addBreadcrumbSpy = vi . spyOn ( breadcrumbModule , 'addBreadcrumb' ) . mockImplementation ( ( ) => { } ) ;
233+ } ) ;
234+
235+ afterEach ( ( ) => {
236+ vi . restoreAllMocks ( ) ;
237+ } ) ;
238+
239+ it ( 'omits db.query, db.body, and breadcrumb query/body when sendDefaultPii is false' , async ( ) => {
240+ const client = createMockSupabaseClient (
241+ { status : 200 } ,
242+ { ...MOCK_SUPABASE_PII_SCENARIO , sendDefaultPii : false } ,
243+ ) ;
244+ instrumentSupabaseClient ( client ) ;
245+
246+ await ( client as any ) . from ( 'users' ) . update ( { } ) . then ( ) ;
247+
248+ const spanOptions = tracingMocks . startSpan . mock . calls [ 0 ] ! [ 0 ] as {
249+ name : string ;
250+ attributes : Record < string , unknown > ;
251+ } ;
252+ expect ( spanOptions . name ) . toContain ( '[redacted]' ) ;
253+ expect ( spanOptions . name ) . not . toContain ( 'secret' ) ;
254+ expect ( spanOptions . attributes [ 'db.query' ] ) . toBeUndefined ( ) ;
255+ expect ( spanOptions . attributes [ 'db.body' ] ) . toBeUndefined ( ) ;
256+
257+ const breadcrumb = addBreadcrumbSpy . mock . calls [ 0 ] ! [ 0 ] as { data ?: unknown } ;
258+ expect ( breadcrumb ) . not . toHaveProperty ( 'data' ) ;
259+ } ) ;
260+
261+ it ( 'includes db.query, db.body, and breadcrumb query/body when sendDefaultPii is true' , async ( ) => {
262+ const client = createMockSupabaseClient ( { status : 200 } , { ...MOCK_SUPABASE_PII_SCENARIO , sendDefaultPii : true } ) ;
263+ instrumentSupabaseClient ( client ) ;
264+
265+ await ( client as any ) . from ( 'users' ) . update ( { } ) . then ( ) ;
266+
267+ const spanOptions = tracingMocks . startSpan . mock . calls [ 0 ] ! [ 0 ] as {
268+ name : string ;
269+ attributes : Record < string , unknown > ;
270+ } ;
271+ expect ( spanOptions . name ) . toContain ( 'eq(email, secret@example.com)' ) ;
272+ expect ( spanOptions . attributes [ 'db.query' ] ) . toEqual (
273+ expect . arrayContaining ( [ expect . stringContaining ( 'secret@example.com' ) ] ) ,
274+ ) ;
275+ expect ( spanOptions . attributes [ 'db.body' ] ) . toEqual (
276+ expect . objectContaining ( { full_name : 'Jane Doe' , phone : '555-0100' } ) ,
277+ ) ;
278+
279+ expect ( addBreadcrumbSpy ) . toHaveBeenCalledWith (
280+ expect . objectContaining ( {
281+ data : expect . objectContaining ( {
282+ query : expect . any ( Array ) ,
283+ body : expect . objectContaining ( { full_name : 'Jane Doe' } ) ,
284+ } ) ,
285+ } ) ,
286+ ) ;
287+ } ) ;
288+
289+ it ( 'omits supabase error context query/body when sendDefaultPii is false' , async ( ) => {
290+ const client = createMockSupabaseClient (
291+ { status : 400 , error : { message : 'Bad request' , code : '400' } } ,
292+ { ...MOCK_SUPABASE_PII_SCENARIO , sendDefaultPii : false } ,
293+ ) ;
294+ instrumentSupabaseClient ( client ) ;
295+
296+ await ( client as any ) . from ( 'users' ) . update ( { } ) . then ( ) ;
297+
298+ expect ( captureExceptionSpy ) . toHaveBeenCalled ( ) ;
299+ const scopeCallback = captureExceptionSpy . mock . calls [ 0 ] ! [ 1 ] as ( scope : {
300+ addEventProcessor : ( fn : ( e : unknown ) => unknown ) => void ;
301+ setContext : ( key : string , ctx : Record < string , unknown > ) => void ;
302+ } ) => unknown ;
303+ const contexts : Record < string , Record < string , unknown > > = { } ;
304+ scopeCallback ( {
305+ addEventProcessor : ( ) => { } ,
306+ setContext ( key : string , ctx : Record < string , unknown > ) {
307+ contexts [ key ] = ctx ;
308+ } ,
309+ } as any ) ;
310+ expect ( contexts . supabase ) . toEqual ( { } ) ;
311+ } ) ;
312+ } ) ;
179313} ) ;
0 commit comments