Skip to content

Commit ace4036

Browse files
committed
Mark SupabaseConstructor objects as instrumented and check for rewrapping
1 parent af31dcf commit ace4036

File tree

4 files changed

+81
-62
lines changed

4 files changed

+81
-62
lines changed

dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { NextApiRequest, NextApiResponse } from 'next';
22
import { getSupabaseClient } from '@/lib/initSupabaseAdmin';
3-
import * as Sentry from '@sentry/nextjs';
43

54
type Data = {
65
data: any;

dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { NextApiRequest, NextApiResponse } from 'next';
22
import { getSupabaseClient } from '@/lib/initSupabaseAdmin';
3-
import * as Sentry from '@sentry/nextjs';
43

54
type Data = {
65
data: any;
@@ -10,6 +9,7 @@ type Data = {
109
const supabaseClient = getSupabaseClient();
1110

1211
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
12+
console.debug("###############################3", supabaseClient.auth.admin.listUsers)
1313
const { data, error } = await supabaseClient.auth.admin.listUsers();
1414

1515
if (error) {

dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ sign_in_sign_ups = 30
141141
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
142142
token_verifications = 30
143143

144-
145144
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
146145
# [auth.captcha]
147146
# enabled = true
@@ -283,6 +282,8 @@ enabled = true
283282
policy = "oneshot"
284283
# Port to attach the Chrome inspector for debugging edge functions.
285284
inspector_port = 8083
285+
# The Deno major version to use.
286+
deno_version = 1
286287

287288
# [edge_runtime.secrets]
288289
# secret_key = "env(SECRET_VALUE)"

packages/core/src/integrations/supabase.ts

Lines changed: 78 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { defineIntegration } from '../integration';
1111
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
1212
import { captureException } from '../exports';
1313
import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '../tracing';
14+
import { DEBUG_BUILD } from '../debug-build';
1415

1516
const AUTH_OPERATIONS_TO_INSTRUMENT = [
1617
'reauthenticate',
@@ -64,25 +65,25 @@ export const FILTER_MAPPINGS = {
6465
not: 'not',
6566
};
6667

67-
export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete'];
68+
export const DB_OPERATIONS_TO_INSTRUMENT = ['select', 'insert', 'upsert', 'update', 'delete'];
6869

6970
type AuthOperationFn = (...args: unknown[]) => Promise<unknown>;
7071
type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number];
7172
type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number];
72-
type PostgrestQueryOperationName = (typeof AVAILABLE_OPERATIONS)[number];
73-
type PostgrestQueryOperationFn = (...args: unknown[]) => PostgrestFilterBuilder;
73+
type PostgRESTQueryOperationName = (typeof DB_OPERATIONS_TO_INSTRUMENT)[number];
74+
type PostgRESTQueryOperationFn = (...args: unknown[]) => PostgRESTFilterBuilder;
7475

7576
export interface SupabaseClientInstance {
7677
auth: {
7778
admin: Record<AuthAdminOperationName, AuthOperationFn>;
7879
} & Record<AuthOperationName, AuthOperationFn>;
7980
}
8081

81-
export interface PostgrestQueryBuilder {
82-
[key: PostgrestQueryOperationName]: PostgrestQueryOperationFn;
82+
export interface PostgRESTQueryBuilder {
83+
[key: PostgRESTQueryOperationName]: PostgRESTQueryOperationFn;
8384
}
8485

85-
export interface PostgrestFilterBuilder {
86+
export interface PostgRESTFilterBuilder {
8687
method: string;
8788
headers: Record<string, string>;
8889
url: URL;
@@ -116,18 +117,36 @@ export interface SupabaseBreadcrumb {
116117

117118
export interface SupabaseClientConstructor {
118119
prototype: {
119-
from: (table: string) => PostgrestQueryBuilder;
120+
from: (table: string) => PostgRESTQueryBuilder;
120121
};
121122
}
122123

123-
export interface PostgrestProtoThenable {
124+
export interface PostgRESTProtoThenable {
124125
then: <T>(
125126
onfulfilled?: ((value: T) => T | PromiseLike<T>) | null,
126127
onrejected?: ((reason: any) => T | PromiseLike<T>) | null,
127128
) => Promise<T>;
128129
}
129130

130-
const instrumented = new Map();
131+
type SentryInstrumented<T> = T & {
132+
__SENTRY_INSTRUMENTED__?: boolean;
133+
};
134+
135+
function markAsInstrumented<T>(fn: T): void {
136+
try {
137+
(fn as SentryInstrumented<T>).__SENTRY_INSTRUMENTED__ = true;
138+
} catch {
139+
// ignore errors here
140+
}
141+
}
142+
143+
function isInstrumented<T>(fn: T): boolean | undefined {
144+
try {
145+
return (fn as SentryInstrumented<T>).__SENTRY_INSTRUMENTED__;
146+
} catch {
147+
return false;
148+
}
149+
}
131150

132151
/**
133152
* Extracts the database operation type from the HTTP method and headers
@@ -198,14 +217,8 @@ export function translateFiltersIntoMethods(key: string, query: string): string
198217
}
199218

200219
function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn {
201-
if (instrumented.has(operation)) {
202-
return operation;
203-
}
204-
205220
return new Proxy(operation, {
206221
apply(target, thisArg, argumentsList) {
207-
instrumented.set(operation, true);
208-
209222
const span = startInactiveSpan({
210223
name: operation.name,
211224
attributes: {
@@ -255,23 +268,34 @@ function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInst
255268
return;
256269
}
257270

258-
AUTH_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthOperationName) => {
271+
272+
for (const operation of AUTH_OPERATIONS_TO_INSTRUMENT) {
259273
const authOperation = auth[operation];
260-
if (typeof authOperation === 'function') {
274+
275+
if (!authOperation) {
276+
continue;
277+
}
278+
279+
if ( typeof supabaseClientInstance.auth[operation] === 'function') {
261280
supabaseClientInstance.auth[operation] = instrumentAuthOperation(authOperation);
262281
}
263-
});
282+
}
264283

265-
AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthAdminOperationName) => {
266-
const authAdminOperation = auth.admin[operation];
267-
if (typeof authAdminOperation === 'function') {
268-
supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authAdminOperation, true);
284+
for (const operation of AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT) {
285+
const authOperation = auth.admin[operation];
286+
287+
if (!authOperation) {
288+
continue;
269289
}
270-
});
290+
291+
if (typeof supabaseClientInstance.auth.admin[operation] === 'function') {
292+
supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authOperation, true);
293+
}
294+
}
271295
}
272296

273297
function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void {
274-
if (instrumented.has(SupabaseClient)) {
298+
if (isInstrumented((SupabaseClient as unknown as SupabaseClientConstructor).prototype.from)) {
275299
return;
276300
}
277301

@@ -280,33 +304,29 @@ function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void {
280304
{
281305
apply(target, thisArg, argumentsList) {
282306
const rv = Reflect.apply(target, thisArg, argumentsList);
283-
const PostgrestQueryBuilder = (rv as PostgrestQueryBuilder).constructor;
307+
const PostgRESTQueryBuilder = (rv as PostgRESTQueryBuilder).constructor;
284308

285-
instrumentPostgrestQueryBuilder(PostgrestQueryBuilder as unknown as new () => PostgrestQueryBuilder);
309+
instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder as unknown as new () => PostgRESTQueryBuilder);
286310

287311
return rv;
288312
},
289313
},
290314
);
315+
316+
markAsInstrumented((SupabaseClient as unknown as SupabaseClientConstructor).prototype.from);
291317
}
292318

293-
// This is the only "instrumented" part of the SDK. The rest of instrumentation
294-
// methods are only used as a mean to get to the `PostgrestFilterBuilder` constructor itself.
295-
function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilterBuilder['constructor']): void {
296-
if (instrumented.has(PostgrestFilterBuilder)) {
319+
function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilterBuilder['constructor']): void {
320+
if (isInstrumented((PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then)) {
297321
return;
298322
}
299323

300-
instrumented.set(PostgrestFilterBuilder, {
301-
then: (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then,
302-
});
303-
304-
(PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then = new Proxy(
305-
(PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then,
324+
(PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then = new Proxy(
325+
(PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then,
306326
{
307327
apply(target, thisArg, argumentsList) {
308-
const operations = AVAILABLE_OPERATIONS;
309-
const typedThis = thisArg as PostgrestFilterBuilder;
328+
const operations = DB_OPERATIONS_TO_INSTRUMENT;
329+
const typedThis = thisArg as PostgRESTFilterBuilder;
310330
const operation = extractOperation(typedThis.method, typedThis.headers);
311331

312332
if (!operations.includes(operation)) {
@@ -427,44 +447,43 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte
427447
},
428448
},
429449
);
430-
}
431450

432-
function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => PostgrestQueryBuilder): void {
433-
if (instrumented.has(PostgrestQueryBuilder)) {
434-
return;
435-
}
451+
markAsInstrumented((PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then);
452+
}
436453

437-
// We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder`
454+
function instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder: new () => PostgRESTQueryBuilder): void {
455+
// We need to wrap _all_ operations despite them sharing the same `PostgRESTFilterBuilder`
438456
// constructor, as we don't know which method will be called first, and we don't want to miss any calls.
439-
for (const operation of AVAILABLE_OPERATIONS) {
440-
instrumented.set(PostgrestQueryBuilder, {
441-
[operation]: (PostgrestQueryBuilder.prototype as Record<string, unknown>)[
442-
operation as 'select' | 'insert' | 'upsert' | 'update' | 'delete'
443-
] as (...args: unknown[]) => PostgrestFilterBuilder,
444-
});
445-
446-
type PostgrestOperation = keyof Pick<PostgrestQueryBuilder, 'select' | 'insert' | 'upsert' | 'update' | 'delete'>;
447-
(PostgrestQueryBuilder.prototype as Record<string, any>)[operation as PostgrestOperation] = new Proxy(
448-
(PostgrestQueryBuilder.prototype as Record<string, any>)[operation as PostgrestOperation],
457+
for (const operation of DB_OPERATIONS_TO_INSTRUMENT) {
458+
if (isInstrumented((PostgRESTQueryBuilder.prototype as Record<string, any>)[operation])) {
459+
continue;
460+
}
461+
462+
type PostgRESTOperation = keyof Pick<PostgRESTQueryBuilder, 'select' | 'insert' | 'upsert' | 'update' | 'delete'>;
463+
(PostgRESTQueryBuilder.prototype as Record<string, any>)[operation as PostgRESTOperation] = new Proxy(
464+
(PostgRESTQueryBuilder.prototype as Record<string, any>)[operation as PostgRESTOperation],
449465
{
450466
apply(target, thisArg, argumentsList) {
451467
const rv = Reflect.apply(target, thisArg, argumentsList);
452-
const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor;
468+
const PostgRESTFilterBuilder = (rv as PostgRESTFilterBuilder).constructor;
453469

454-
logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`);
470+
DEBUG_BUILD && logger.log(`Instrumenting ${operation} operation's PostgRESTFilterBuilder`);
455471

456-
instrumentPostgrestFilterBuilder(PostgrestFilterBuilder);
472+
instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder);
457473

458474
return rv;
459475
},
460476
},
461477
);
478+
479+
markAsInstrumented((PostgRESTQueryBuilder.prototype as Record<string, any>)[operation]);
462480
}
463481
}
464482

465483
const instrumentSupabase = (supabaseClientInstance: unknown): void => {
466484
if (!supabaseClientInstance) {
467-
throw new Error('Supabase client instance is not available.');
485+
DEBUG_BUILD && logger.warn('Supabase integration was not installed because no Supabase client was provided.');
486+
return;
468487
}
469488
const SupabaseClientConstructor =
470489
supabaseClientInstance.constructor === Function ? supabaseClientInstance : supabaseClientInstance.constructor;
@@ -477,7 +496,7 @@ const INTEGRATION_NAME = 'Supabase';
477496

478497
const _supabaseIntegration = (supabaseClient => {
479498
// Instrumenting here instead of `setup` or `setupOnce` because we may need to instrument multiple clients.
480-
// So we don't want the instrumentation is skipped because the integration is already installed.
499+
// So we don't want the instrumentation skipped because the integration is already installed.
481500
instrumentSupabase(supabaseClient);
482501

483502
return {

0 commit comments

Comments
 (0)