@@ -3,10 +3,15 @@ import * as breadcrumbModule from '../../../src/breadcrumbs';
33import * as exportsModule from '../../../src/exports' ;
44import {
55 extractOperation ,
6+ getHeader ,
67 instrumentSupabaseClient ,
78 translateFiltersIntoMethods ,
89} from '../../../src/integrations/supabase' ;
9- import type { PostgRESTQueryBuilder , SupabaseClientInstance } from '../../../src/integrations/supabase' ;
10+ import type {
11+ PostgRESTHeaders ,
12+ PostgRESTQueryBuilder ,
13+ SupabaseClientInstance ,
14+ } from '../../../src/integrations/supabase' ;
1015import { resolveDataCollectionOptions } from '../../../src/utils/data-collection/resolveDataCollectionOptions' ;
1116
1217const tracingMocks = vi . hoisted ( ( ) => ( {
@@ -39,6 +44,8 @@ type CreateMockSupabaseClientOptions = {
3944 method ?: string ;
4045 url ?: URL | string ;
4146 body ?: unknown ;
47+ /** Defaults to the plain-object shape used by `postgrest-js` v1. Pass a `Headers` instance to emulate v2. */
48+ headers ?: PostgRESTHeaders ;
4249 /** When set, configures the mocked Sentry client's `dataCollection.databaseQueryData`. Omit to leave `getClient` to the test file `beforeEach`. */
4350 dataCollectionDatabaseQueryData ?: boolean ;
4451} ;
@@ -67,10 +74,11 @@ function createMockSupabaseClient(resolveWith: unknown, options?: CreateMockSupa
6774 : new URL ( options . url )
6875 : new URL ( DEFAULT_MOCK_SUPABASE_REST_URL ) ;
6976 const body = options ?. body ;
77+ const headers = options ?. headers ?? { 'X-Client-Info' : 'supabase-js/2.0.0' } ;
7078
7179 class MockPostgRESTFilterBuilder {
7280 method = method ;
73- headers : Record < string , string > = { 'X-Client-Info' : 'supabase-js/2.0.0' } ;
81+ headers : PostgRESTHeaders = headers ;
7482 url = requestUrl ;
7583 schema = 'public' ;
7684 body = body ;
@@ -116,6 +124,28 @@ describe('Supabase Integration', () => {
116124 currentScopesMocks . getClient . mockReturnValue ( undefined ) ;
117125 } ) ;
118126
127+ describe ( 'getHeader' , ( ) => {
128+ it ( 'reads a header off a plain object' , ( ) => {
129+ expect ( getHeader ( { 'X-Client-Info' : 'supabase-js/2.0.0' } , 'X-Client-Info' ) ) . toBe ( 'supabase-js/2.0.0' ) ;
130+ } ) ;
131+
132+ it ( 'reads a header off a Headers instance' , ( ) => {
133+ expect ( getHeader ( new Headers ( { 'X-Client-Info' : 'supabase-js/2.112.0' } ) , 'X-Client-Info' ) ) . toBe (
134+ 'supabase-js/2.112.0' ,
135+ ) ;
136+ } ) ;
137+
138+ it ( 'looks up plain object headers case-insensitively' , ( ) => {
139+ expect ( getHeader ( { prefer : 'resolution=merge-duplicates' } , 'Prefer' ) ) . toBe ( 'resolution=merge-duplicates' ) ;
140+ } ) ;
141+
142+ it ( 'returns undefined for unset headers' , ( ) => {
143+ expect ( getHeader ( { Prefer : 'count=exact' } , 'X-Client-Info' ) ) . toBeUndefined ( ) ;
144+ expect ( getHeader ( new Headers ( { Prefer : 'count=exact' } ) , 'X-Client-Info' ) ) . toBeUndefined ( ) ;
145+ expect ( getHeader ( undefined , 'X-Client-Info' ) ) . toBeUndefined ( ) ;
146+ } ) ;
147+ } ) ;
148+
119149 describe ( 'extractOperation' , ( ) => {
120150 it ( 'returns select for GET' , ( ) => {
121151 expect ( extractOperation ( 'GET' ) ) . toBe ( 'select' ) ;
@@ -129,6 +159,10 @@ describe('Supabase Integration', () => {
129159 expect ( extractOperation ( 'POST' , { Prefer : 'resolution=merge-duplicates' } ) ) . toBe ( 'upsert' ) ;
130160 } ) ;
131161
162+ it ( 'returns upsert for POST with resolution header on a Headers instance' , ( ) => {
163+ expect ( extractOperation ( 'POST' , new Headers ( { Prefer : 'resolution=merge-duplicates' } ) ) ) . toBe ( 'upsert' ) ;
164+ } ) ;
165+
132166 it ( 'returns update for PATCH' , ( ) => {
133167 expect ( extractOperation ( 'PATCH' ) ) . toBe ( 'update' ) ;
134168 } ) ;
@@ -433,4 +467,53 @@ describe('Supabase Integration', () => {
433467 expect ( spanOptions . attributes [ 'db.body' ] ) . toEqual ( [ { title : 'Test Todo' } ] ) ;
434468 } ) ;
435469 } ) ;
470+
471+ describe . each ( [
472+ [ 'plain object headers' , ( init : Record < string , string > ) : PostgRESTHeaders => init ] ,
473+ [ 'Headers instance' , ( init : Record < string , string > ) : PostgRESTHeaders => new Headers ( init ) ] ,
474+ ] ) ( '%s' , ( _name , createHeaders ) => {
475+ beforeEach ( ( ) => {
476+ vi . spyOn ( breadcrumbModule , 'addBreadcrumb' ) . mockImplementation ( ( ) => { } ) ;
477+ } ) ;
478+
479+ afterEach ( ( ) => {
480+ vi . restoreAllMocks ( ) ;
481+ } ) ;
482+
483+ it ( 'sets db.sdk from X-Client-Info' , async ( ) => {
484+ tracingMocks . startSpan . mockClear ( ) ;
485+ const client = createMockSupabaseClient (
486+ { status : 200 } ,
487+ { headers : createHeaders ( { 'X-Client-Info' : 'supabase-js/2.112.0' } ) } ,
488+ ) ;
489+ instrumentSupabaseClient ( client ) ;
490+
491+ await ( client as any ) . from ( 'todos' ) . select ( ) . then ( ) ;
492+
493+ const spanOptions = tracingMocks . startSpan . mock . calls [ 0 ] ! [ 0 ] as { attributes : Record < string , unknown > } ;
494+ expect ( spanOptions . attributes [ 'db.sdk' ] ) . toBe ( 'supabase-js/2.112.0' ) ;
495+ } ) ;
496+
497+ it ( 'detects upsert from the Prefer header' , async ( ) => {
498+ tracingMocks . startSpan . mockClear ( ) ;
499+ const client = createMockSupabaseClient (
500+ { status : 200 } ,
501+ {
502+ method : 'POST' ,
503+ body : { title : 'Test Todo' } ,
504+ headers : createHeaders ( { Prefer : 'resolution=merge-duplicates' } ) ,
505+ } ,
506+ ) ;
507+ instrumentSupabaseClient ( client ) ;
508+
509+ await ( client as any ) . from ( 'todos' ) . upsert ( { } ) . then ( ) ;
510+
511+ const spanOptions = tracingMocks . startSpan . mock . calls [ 0 ] ! [ 0 ] as {
512+ name : string ;
513+ attributes : Record < string , unknown > ;
514+ } ;
515+ expect ( spanOptions . name ) . toMatch ( / ^ u p s e r t \( \. \. \. \) / ) ;
516+ expect ( spanOptions . attributes [ 'db.operation' ] ) . toBe ( 'upsert' ) ;
517+ } ) ;
518+ } ) ;
436519} ) ;
0 commit comments