|
1 | | -// Ported from Kamil Ogórek's work on: |
| 1 | +// Based on Kamil Ogórek's work on: |
2 | 2 | // https://github.com/supabase-community/sentry-integration-js |
3 | 3 |
|
4 | | -// MIT License |
5 | | - |
6 | | -// Copyright (c) 2024 Supabase |
7 | | - |
8 | | -// Permission is hereby granted, free of charge, to any person obtaining a copy |
9 | | -// of this software and associated documentation files (the "Software"), to deal |
10 | | -// in the Software without restriction, including without limitation the rights |
11 | | -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
12 | | -// copies of the Software, and to permit persons to whom the Software is |
13 | | -// furnished to do so, subject to the following conditions: |
14 | | - |
15 | | -// The above copyright notice and this permission notice shall be included in all |
16 | | -// copies or substantial portions of the Software. |
17 | | - |
18 | | -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
19 | | -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
20 | | -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
21 | | -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
22 | | -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
23 | | -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
24 | | -// SOFTWARE. |
25 | | - |
26 | 4 | /* eslint-disable max-lines */ |
27 | 5 | import { logger, isPlainObject } from '../utils-hoist'; |
28 | 6 |
|
29 | 7 | import type { IntegrationFn } from '../types-hoist'; |
30 | | -import { setHttpStatus, startInactiveSpan } from '../tracing'; |
| 8 | +import { setHttpStatus, startInactiveSpan, startSpan } from '../tracing'; |
31 | 9 | import { addBreadcrumb } from '../breadcrumbs'; |
32 | 10 | import { defineIntegration } from '../integration'; |
33 | 11 | import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; |
34 | 12 | import { captureException } from '../exports'; |
| 13 | +import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '../tracing'; |
35 | 14 |
|
36 | | -export interface SupabaseClient { |
| 15 | +export interface SupabaseClientConstructor { |
37 | 16 | prototype: { |
38 | 17 | from: (table: string) => PostgrestQueryBuilder; |
39 | 18 | }; |
40 | 19 | } |
41 | 20 |
|
| 21 | +const AUTH_OPERATIONS_TO_INSTRUMENT = [ |
| 22 | + 'reauthenticate', |
| 23 | + 'signInAnonymously', |
| 24 | + 'signInWithOAuth', |
| 25 | + 'signInWithIdToken', |
| 26 | + 'signInWithOtp', |
| 27 | + 'signInWithPassword', |
| 28 | + 'signInWithSSO', |
| 29 | + 'signOut', |
| 30 | + 'signUp', |
| 31 | + 'verifyOtp', |
| 32 | +]; |
| 33 | + |
| 34 | +const AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT = [ |
| 35 | + 'createUser', |
| 36 | + 'deleteUser', |
| 37 | + 'listUsers', |
| 38 | + 'getUserById', |
| 39 | + 'updateUserById', |
| 40 | + 'inviteUserByEmail', |
| 41 | + 'signOut', |
| 42 | +]; |
| 43 | + |
| 44 | +type AuthOperationFn = (...args: unknown[]) => Promise<unknown>; |
| 45 | +type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; |
| 46 | +type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; |
| 47 | + |
| 48 | +export interface SupabaseClientInstance { |
| 49 | + auth: { |
| 50 | + admin: Record<AuthAdminOperationName, AuthOperationFn>; |
| 51 | + } & Record<AuthOperationName, AuthOperationFn>; |
| 52 | +} |
| 53 | + |
42 | 54 | export interface PostgrestQueryBuilder { |
43 | 55 | select: (...args: unknown[]) => PostgrestFilterBuilder; |
44 | 56 | insert: (...args: unknown[]) => PostgrestFilterBuilder; |
@@ -181,17 +193,65 @@ export function translateFiltersIntoMethods(key: string, query: string): string |
181 | 193 | return `${method}(${key}, ${value.join('.')})`; |
182 | 194 | } |
183 | 195 |
|
184 | | -function instrumentSupabaseClient(SupabaseClient: unknown): void { |
185 | | - if (instrumented.has(SupabaseClient)) { |
| 196 | +function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn { |
| 197 | + return new Proxy(operation, { |
| 198 | + apply(target, thisArg, argumentsList) { |
| 199 | + startSpan( |
| 200 | + { |
| 201 | + name: operation.name, |
| 202 | + attributes: { |
| 203 | + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', |
| 204 | + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, |
| 205 | + }, |
| 206 | + }, |
| 207 | + span => { |
| 208 | + return Reflect.apply(target, thisArg, argumentsList).then((res: unknown) => { |
| 209 | + debugger; |
| 210 | + if (res && typeof res === 'object' && 'error' in res && res.error) { |
| 211 | + span.setStatus({ code: SPAN_STATUS_ERROR }); |
| 212 | + } else { |
| 213 | + span.setStatus({ code: SPAN_STATUS_OK }); |
| 214 | + } |
| 215 | + |
| 216 | + span.end(); |
| 217 | + debugger; |
| 218 | + return res; |
| 219 | + }); |
| 220 | + }, |
| 221 | + ); |
| 222 | + }, |
| 223 | + }); |
| 224 | +} |
| 225 | + |
| 226 | +function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInstance): void { |
| 227 | + const auth = supabaseClientInstance.auth; |
| 228 | + |
| 229 | + if (!auth) { |
186 | 230 | return; |
187 | 231 | } |
188 | 232 |
|
189 | | - instrumented.set(SupabaseClient, { |
190 | | - from: (SupabaseClient as unknown as SupabaseClient).prototype.from, |
| 233 | + AUTH_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthOperationName) => { |
| 234 | + const authOperation = auth[operation]; |
| 235 | + if (typeof authOperation === 'function') { |
| 236 | + auth[operation] = instrumentAuthOperation(authOperation); |
| 237 | + } |
191 | 238 | }); |
192 | 239 |
|
193 | | - (SupabaseClient as unknown as SupabaseClient).prototype.from = new Proxy( |
194 | | - (SupabaseClient as unknown as SupabaseClient).prototype.from, |
| 240 | + AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthAdminOperationName) => { |
| 241 | + const authAdminOperation = auth.admin[operation]; |
| 242 | + if (typeof authAdminOperation === 'function') { |
| 243 | + auth.admin[operation] = instrumentAuthOperation(authAdminOperation); |
| 244 | + } |
| 245 | + }); |
| 246 | +} |
| 247 | + |
| 248 | +function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void { |
| 249 | + if (instrumented.has(SupabaseClient)) { |
| 250 | + return; |
| 251 | + } |
| 252 | + |
| 253 | + (SupabaseClient as unknown as SupabaseClientConstructor).prototype.from = new Proxy( |
| 254 | + (SupabaseClient as unknown as SupabaseClientConstructor).prototype.from, |
195 | 255 | { |
196 | 256 | apply(target, thisArg, argumentsList) { |
197 | 257 | const rv = Reflect.apply(target, thisArg, argumentsList); |
@@ -398,31 +458,15 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr |
398 | 458 | } |
399 | 459 | } |
400 | 460 |
|
401 | | -export const patchCreateClient = (moduleExports: { createClient?: (...args: unknown[]) => unknown }): void => { |
402 | | - const originalCreateClient = moduleExports.createClient; |
403 | | - if (!originalCreateClient) { |
404 | | - return; |
405 | | - } |
406 | | - |
407 | | - moduleExports.createClient = function wrappedCreateClient(...args: any[]) { |
408 | | - const client = originalCreateClient.apply(this, args); |
409 | | - |
410 | | - instrumentSupabaseClient(client); |
411 | | - |
412 | | - return client; |
413 | | - }; |
414 | | -}; |
415 | | - |
416 | | -const instrumentSupabase = (supabaseClient: unknown): void => { |
417 | | - if (!supabaseClient) { |
418 | | - throw new Error('SupabaseClient class constructor is required'); |
| 461 | +const instrumentSupabase = (supabaseClientInstance: unknown): void => { |
| 462 | + if (!supabaseClientInstance) { |
| 463 | + throw new Error('Supabase client instance is not defined.'); |
419 | 464 | } |
| 465 | + const SupabaseClientConstructor = |
| 466 | + supabaseClientInstance.constructor === Function ? supabaseClientInstance : supabaseClientInstance.constructor; |
420 | 467 |
|
421 | | - // We want to allow passing either `SupabaseClient` constructor |
422 | | - // or an instance returned from `createClient()`. |
423 | | - const SupabaseClient = supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor; |
424 | | - |
425 | | - instrumentSupabaseClient(SupabaseClient); |
| 468 | + instrumentSupabaseClientConstructor(SupabaseClientConstructor); |
| 469 | + instrumentSupabaseAuthClient(supabaseClientInstance as SupabaseClientInstance); |
426 | 470 | }; |
427 | 471 |
|
428 | 472 | const INTEGRATION_NAME = 'Supabase'; |
|
0 commit comments