From 18cb6b8c2c29cf98bff9b373e584020100cc57c9 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 09:59:07 +0000 Subject: [PATCH 01/53] feat(core): Add Supabase Integration --- .../supabase-nextjs/.eslintrc.json | 3 + .../supabase-nextjs/.gitignore | 39 ++ .../test-applications/supabase-nextjs/.npmrc | 2 + .../supabase-nextjs/README.md | 0 .../supabase-nextjs/components/TodoList.tsx | 125 +++++ .../supabase-nextjs/instrumentation.ts | 13 + .../supabase-nextjs/lib/initSupabase.ts | 13 + .../supabase-nextjs/lib/schema.ts | 49 ++ .../supabase-nextjs/next.config.js | 51 +++ .../supabase-nextjs/package.json | 39 ++ .../supabase-nextjs/pages/_app.tsx | 11 + .../supabase-nextjs/pages/_document.tsx | 13 + .../supabase-nextjs/pages/_error.jsx | 17 + .../pages/api/add-todo-entry-edge.ts | 62 +++ .../pages/api/add-todo-entry.ts | 46 ++ .../pages/api/create-test-user.ts | 32 ++ .../supabase-nextjs/pages/index.tsx | 39 ++ .../supabase-nextjs/playwright.config.mjs | 8 + .../supabase-nextjs/sentry.client.config.ts | 34 ++ .../supabase-nextjs/sentry.edge.config.ts | 19 + .../supabase-nextjs/sentry.server.config.ts | 19 + .../supabase-nextjs/start-event-proxy.mjs | 6 + .../supabase-nextjs/supabase/.gitignore | 13 + .../supabase-nextjs/supabase/config.toml | 307 +++++++++++++ .../migrations/20230712094349_init.sql | 16 + .../tests/client.performance.test.ts | 70 +++ .../tests/server.performance.test.ts | 57 +++ .../supabase-nextjs/tsconfig.json | 24 + packages/browser/src/exports.ts | 1 + packages/browser/src/index.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/integrations/supabase.ts | 433 ++++++++++++++++++ packages/node/src/index.ts | 1 + 33 files changed, 1564 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/README.md create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/components/TodoList.tsx create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/schema.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_document.tsx create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_error.jsx create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/migrations/20230712094349_init.sql create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/tsconfig.json create mode 100644 packages/core/src/integrations/supabase.ts diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json new file mode 100644 index 000000000000..bffb357a7122 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.gitignore b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.gitignore new file mode 100644 index 000000000000..e7e8ec25eed1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/README.md b/dev-packages/e2e-tests/test-applications/supabase-nextjs/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/components/TodoList.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/components/TodoList.tsx new file mode 100644 index 000000000000..6fe5b810e05b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/components/TodoList.tsx @@ -0,0 +1,125 @@ +import { Database } from '@/lib/schema'; +import { Session, useSupabaseClient } from '@supabase/auth-helpers-react'; +import { useEffect, useState } from 'react'; + +type Todos = Database['public']['Tables']['todos']['Row']; + +export default function TodoList({ session }: { session: Session }) { + const supabase = useSupabaseClient(); + const [todos, setTodos] = useState([]); + const [newTaskText, setNewTaskText] = useState(''); + const [errorText, setErrorText] = useState(''); + + const user = session.user; + + useEffect(() => { + const fetchTodos = async () => { + const { data: todos, error } = await supabase.from('todos').select('*').order('id', { ascending: true }); + + if (error) console.log('error', error); + else setTodos(todos); + }; + + fetchTodos(); + }, [supabase]); + + const addTodo = async (taskText: string) => { + let task = taskText.trim(); + if (task.length) { + const { data: todo, error } = await supabase.from('todos').insert({ task, user_id: user.id }).select().single(); + + if (error) { + setErrorText(error.message); + } else { + setTodos([...todos, todo]); + setNewTaskText(''); + } + } + }; + + const deleteTodo = async (id: number) => { + try { + await supabase.from('todos').delete().eq('id', id).throwOnError(); + setTodos(todos.filter(x => x.id != id)); + } catch (error) { + console.log('error', error); + } + }; + + return ( +
+

Todo List.

+
{ + e.preventDefault(); + addTodo(newTaskText); + }} + > + { + setErrorText(''); + setNewTaskText(e.target.value); + }} + /> + +
+ {!!errorText && } +
    + {todos.map(todo => ( + deleteTodo(todo.id)} /> + ))} +
+
+ ); +} + +const Todo = ({ todo, onDelete }: { todo: Todos; onDelete: () => void }) => { + const supabase = useSupabaseClient(); + const [isCompleted, setIsCompleted] = useState(todo.is_complete); + + const toggle = async () => { + try { + const { data } = await supabase + .from('todos') + .update({ is_complete: !isCompleted }) + .eq('id', todo.id) + .throwOnError() + .select() + .single(); + + if (data) setIsCompleted(data.is_complete); + } catch (error) { + console.log('error', error); + } + }; + + return ( +
  • +
    +
    +
    {todo.task}
    +
    +
    + toggle()} + type="checkbox" + checked={isCompleted ? true : false} + /> +
    + +
    +
  • + ); +}; + +const Alert = ({ text }: { text: string }) =>
    {text}
    ; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/instrumentation.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/instrumentation.ts new file mode 100644 index 000000000000..964f937c439a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts new file mode 100644 index 000000000000..9ea30eb9f089 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts @@ -0,0 +1,13 @@ +import { createClient } from '@supabase/supabase-js'; +import * as Sentry from '@sentry/nextjs'; + +export const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '', +); + +Sentry.addIntegration( + Sentry.supabaseIntegration({ + supabaseClient: supabase, + }), +); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/schema.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/schema.ts new file mode 100644 index 000000000000..ec8b8f854b2a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/schema.ts @@ -0,0 +1,49 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[] + +export interface Database { + public: { + Tables: { + todos: { + Row: { + id: number + inserted_at: string + is_complete: boolean | null + task: string | null + user_id: string + } + Insert: { + id?: number + inserted_at?: string + is_complete?: boolean | null + task?: string | null + user_id: string + } + Update: { + id?: number + inserted_at?: string + is_complete?: boolean | null + task?: string | null + user_id?: string + } + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js b/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js new file mode 100644 index 000000000000..003a6cb03964 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/next.config.js @@ -0,0 +1,51 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig + + +// Injected content via Sentry wizard below + +const { withSentryConfig } = require("@sentry/nextjs"); + +module.exports = withSentryConfig( + module.exports, + { + // For all available options, see: + // https://www.npmjs.com/package/@sentry/webpack-plugin#options + + org: "sentry-sdks", + project: "sentry-javascript-nextjs", + + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Automatically annotate React components to show their full name in breadcrumbs and session replay + reactComponentAnnotation: { + enabled: true, + }, + + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: "/monitoring", + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: false, + + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, + } +); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json new file mode 100644 index 000000000000..692ec2200695 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -0,0 +1,39 @@ +{ + "name": "supabase-nextjs-e2e-test-app", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "concurrently \"next dev\"", + "build": "next build", + "start": "next start", + "clean": "npx rimraf node_modules pnpm-lock.yaml .next", + "start-local-supabase": "supabase init --force --workdir . && supabase start -o env", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm start-local-supabase && pnpm build", + "test:assert": "pnpm test:prod" + }, + "dependencies": { + "@next/font": "14.1.3", + "@sentry/nextjs": "latest || *", + "@supabase/auth-helpers-react": "^0.3.1", + "@supabase/auth-ui-react": "^0.2.8", + "@supabase/supabase-js": "^2.8.0", + "@types/node": "18.14.0", + "@types/react": "18.0.28", + "@types/react-dom": "18.0.11", + "concurrently": "^7.6.0", + "next": "14.1.3", + "react": "18.2.0", + "react-dom": "18.2.0", + "supabase": "^2.19.7", + "typescript": "4.9.5" + }, + "devDependencies": { + "@sentry-internal/test-utils": "link:../../../test-utils", + "eslint": "8.34.0", + "eslint-config-next": "14.1.3" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx new file mode 100644 index 000000000000..a36700d408b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx @@ -0,0 +1,11 @@ +import { supabase } from '@/lib/initSupabase' +import { SessionContextProvider } from '@supabase/auth-helpers-react' +import type { AppProps } from 'next/app' + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ) +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_document.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_document.tsx new file mode 100644 index 000000000000..54e8bf3e2a29 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
    + + + + ) +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_error.jsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_error.jsx new file mode 100644 index 000000000000..46a61d690c38 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_error.jsx @@ -0,0 +1,17 @@ +import * as Sentry from "@sentry/nextjs"; +import Error from "next/error"; + +const CustomErrorComponent = (props) => { + return ; +}; + +CustomErrorComponent.getInitialProps = async (contextData) => { + // In case this is running in a serverless function, await this in order to give Sentry + // time to send the error before the lambda exits + await Sentry.captureUnderscoreErrorException(contextData); + + // This will contain the status code of the response + return Error.getInitialProps(contextData); +}; + +export default CustomErrorComponent; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts new file mode 100644 index 000000000000..decca208ddb3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts @@ -0,0 +1,62 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import * as Sentry from '@sentry/nextjs'; +import { supabase } from '@/lib/initSupabase'; + +Sentry.addIntegration(Sentry.supabaseIntegration({ + supabaseClient: supabase, +})); + +export const config = { + runtime: 'edge', +}; + +type Data = { + data: any; + error: any; +}; + +async function login() { + const { data, error } = await supabase.auth.signInWithPassword({ + email: 'test@sentry.test', + password: 'sentry.test', + }); + + if (error) { + console.log('error', error); + } + + return data; +} + +async function addTodoEntry(userId?: string) { + const { error } = await supabase.from('todos').insert({ task: 'test', user_id: userId }).select().single(); + + if (error) { + console.log('error', error); + } +} + +export default async function handler() { + const { user } = await login(); + + await addTodoEntry(user?.id); + + const { data, error } = await supabase.from('todos').select('*'); + + if (error) { + console.log('error', error); + } + + return new Response( + JSON.stringify({ + data, + error, + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ); +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts new file mode 100644 index 000000000000..2230868d9de9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts @@ -0,0 +1,46 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next'; +import { supabase } from '@/lib/initSupabase'; + +type Data = { + data: any; + error: any; +}; + +async function login() { + const { data, error } = await supabase.auth.signInWithPassword({ + email: 'test@sentry.test', + password: 'sentry.test', + }); + + if (error) { + console.log('error', error); + } + + return data; +} + +async function addTodoEntry(userId?: string) { + const { error } = await supabase.from('todos').insert({ task: 'test', user_id: userId }).select().single(); + + if (error) { + console.log('error', error); + } +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { user } = await login(); + + await addTodoEntry(user?.id); + + const { data, error } = await supabase.from('todos').select('*'); + + if (error) { + console.log('error', error); + } + + res.status(200).json({ + data, + error, + }); +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts new file mode 100644 index 000000000000..0d1bd9395b6f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts @@ -0,0 +1,32 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next'; +import { supabase } from '@/lib/initSupabase'; + +type Data = { + data: any; + error: any; +}; + +async function deleteExistingUsers() { + const { data: { users }, error } = await supabase.auth.admin.listUsers() + + for (const user of users) { + const { error } = await supabase.auth.admin.deleteUser(user.id, true); + if (error) console.log('error', error); + } +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + await deleteExistingUsers(); + + const { data, error } = await supabase.auth.admin.createUser({ + email: 'test@sentry.test', + password: 'sentry.test', + email_confirm: true, + }); + + res.status(200).json({ + data, + error, + }); +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/index.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/index.tsx new file mode 100644 index 000000000000..e3b04bb22534 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/index.tsx @@ -0,0 +1,39 @@ +import Head from 'next/head'; +import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'; +import { Auth } from '@supabase/auth-ui-react'; +import TodoList from '@/components/TodoList'; + +export default function Home() { + const session = useSession(); + const supabase = useSupabaseClient(); + + return ( + <> + + Create Next App + + + +
    + {!session ? ( +
    + Login + +
    + ) : ( +
    + + +
    + )} +
    + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs new file mode 100644 index 000000000000..3d3ab7d8df02 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts new file mode 100644 index 000000000000..076e6504cb48 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts @@ -0,0 +1,34 @@ +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + + // Add optional integrations for additional features + integrations: [ + Sentry.replayIntegration(), + ], + tunnel: 'http://localhost:3031/', // proxy server + + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Define how likely Replay events are sampled. + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts new file mode 100644 index 000000000000..c05ce9a5fbc5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts @@ -0,0 +1,19 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + sendDefaultPii: true, + tracesSampleRate: 1, + tunnel: 'http://localhost:3031/', // proxy server + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, + + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts new file mode 100644 index 000000000000..175a063db952 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts @@ -0,0 +1,19 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1, + sendDefaultPii: true, + tunnel: 'http://localhost:3031/', // proxy server + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, + + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/supabase-nextjs/start-event-proxy.mjs new file mode 100644 index 000000000000..2f41cb42d4ee --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'supabase-nextjs', +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/.gitignore b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/.gitignore new file mode 100644 index 000000000000..a735017e0d2a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/.gitignore @@ -0,0 +1,13 @@ +# Supabase +.branches +.temp +.env + +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml new file mode 100644 index 000000000000..35dcff35bec4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml @@ -0,0 +1,307 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "supabase-nextjs" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 15 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 + + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +[edge_runtime] +enabled = true +# Configure one of the supported request policies: `oneshot`, `per_worker`. +# Use `oneshot` for hot reload, or `per_worker` for load testing. +policy = "oneshot" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/migrations/20230712094349_init.sql b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/migrations/20230712094349_init.sql new file mode 100644 index 000000000000..1b1a98ace2e4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/migrations/20230712094349_init.sql @@ -0,0 +1,16 @@ +create table todos ( + id bigint generated by default as identity primary key, + user_id uuid references auth.users not null, + task text check (char_length(task) > 3), + is_complete boolean default false, + inserted_at timestamp with time zone default timezone('utc'::text, now()) not null +); +alter table todos enable row level security; +create policy "Individuals can create todos." on todos for + insert with check (auth.uid() = user_id); +create policy "Individuals can view their own todos. " on todos for + select using (auth.uid() = user_id); +create policy "Individuals can update their own todos." on todos for + update using (auth.uid() = user_id); +create policy "Individuals can delete their own todos." on todos for + delete using (auth.uid() = user_id); \ No newline at end of file diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts new file mode 100644 index 000000000000..529412e54b2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', async ({ page, baseURL }) => { + // Create test user + await fetch(`${baseURL}/api/create-test-user`); + + const pageloadTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + // Fill in login credentials + await page.locator('input[name=email]').fill('test@sentry.test'); + await page.locator('input[name=password]').fill('sentry.test'); + await page.locator('button[type=submit]').click(); + + // Wait for login to complete + await page.waitForSelector('button:has-text("Add")'); + + // Add a new todo entry + await page.locator('input[id=new-task-text]').fill('test'); + await page.locator('button[id=add-task]').click(); + + const transactionEvent = await pageloadTransactionPromise; + + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'from(todos)', + op: 'db.select', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }), + ); + + expect(transactionEvent.spans).toContainEqual({ + data: expect.any(Object), + description: 'from(todos)', + op: 'db.insert', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.select', + message: 'from(todos)', + data: expect.any(Object), + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.insert', + message: 'from(todos)', + data: expect.any(Object), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts new file mode 100644 index 000000000000..887d87e6ce8f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', async ({ page, baseURL }) => { + // Create test user + await fetch(`${baseURL}/api/create-test-user`); + + const httpTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /api/add-todo-entry'; + }); + + await fetch(`${baseURL}/api/add-todo-entry`); + const transactionEvent = await httpTransactionPromise; + + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'from(todos)', + op: 'db.select', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }), + ); + + expect(transactionEvent.spans).toContainEqual({ + data: expect.any(Object), + description: 'from(todos)', + op: 'db.insert', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.select', + message: 'from(todos)', + data: expect.any(Object), + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.insert', + message: 'from(todos)', + data: expect.any(Object), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tsconfig.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tsconfig.json new file mode 100644 index 000000000000..f4ab65fd2ebf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 8745e34106f4..a66d7d9a52b7 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -61,6 +61,7 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + supabaseIntegration, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 275144cd280c..34dd7afb96ed 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -60,6 +60,7 @@ export { makeMultiplexedTransport, moduleMetadataIntegration, zodErrorsIntegration, + supabaseIntegration, thirdPartyErrorFilterIntegration, } from '@sentry/core'; export type { Span } from '@sentry/core'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 71a8b03acacb..f4e4e9e4a001 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -107,6 +107,7 @@ export { dedupeIntegration } from './integrations/dedupe'; export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; export { zodErrorsIntegration } from './integrations/zoderrors'; +export { supabaseIntegration } from './integrations/supabase'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts new file mode 100644 index 000000000000..847de37d399b --- /dev/null +++ b/packages/core/src/integrations/supabase.ts @@ -0,0 +1,433 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +import { addBreadcrumb } from '../breadcrumbs'; +import { captureException } from '../exports'; +import { defineIntegration } from '../integration'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes'; +import { startInactiveSpan, setHttpStatus } from '../tracing'; +import type { Span, IntegrationFn } from '../types-hoist'; +import { logger, isPlainObject } from '../utils-hoist'; + +/* eslint-disable max-lines */ +export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; + +export const FILTER_MAPPINGS = { + eq: 'eq', + neq: 'neq', + gt: 'gt', + gte: 'gte', + lt: 'lt', + lte: 'lte', + like: 'like', + 'like(all)': 'likeAllOf', + 'like(any)': 'likeAnyOf', + ilike: 'ilike', + 'ilike(all)': 'ilikeAllOf', + 'ilike(any)': 'ilikeAnyOf', + is: 'is', + in: 'in', + cs: 'contains', + cd: 'containedBy', + sr: 'rangeGt', + nxl: 'rangeGte', + sl: 'rangeLt', + nxr: 'rangeLte', + adj: 'rangeAdjacent', + ov: 'overlaps', + fts: '', + plfts: 'plain', + phfts: 'phrase', + wfts: 'websearch', + not: 'not', +}; +/** + * Translates Supabase filter parameters into readable method names for tracing + * @param key - The filter key from the URL search parameters + * @param query - The filter value from the URL search parameters + * @returns A string representation of the filter as a method call + */ +export function translateFiltersIntoMethods(key: string, query: string): string { + if (query === '' || query === '*') { + return 'select(*)'; + } + + if (key === 'select') { + return `select(${query})`; + } + + if (key === 'or' || key.endsWith('.or')) { + return `${key}${query}`; + } + + const [filter, ...value] = query.split('.'); + + let method; + // Handle optional `configPart` of the filter + if (filter?.startsWith('fts')) { + method = 'textSearch'; + } else if (filter?.startsWith('plfts')) { + method = 'textSearch[plain]'; + } else if (filter?.startsWith('phfts')) { + method = 'textSearch[phrase]'; + } else if (filter?.startsWith('wfts')) { + method = 'textSearch[websearch]'; + } else { + method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || 'filter'; + } + + return `${method}(${key}, ${value.join('.')})`; +} + +interface SupabaseClient { + prototype: { + from: (table: string) => PostgrestQueryBuilder; + }; +} + +interface PostgrestQueryBuilder { + select: (...args: unknown[]) => PostgrestFilterBuilder; + insert: (...args: unknown[]) => PostgrestFilterBuilder; + upsert: (...args: unknown[]) => PostgrestFilterBuilder; + update: (...args: unknown[]) => PostgrestFilterBuilder; + delete: (...args: unknown[]) => PostgrestFilterBuilder; +} + +interface PostgrestFilterBuilder { + method: string; + headers: Record; + url: URL; + schema: string; + body: any; +} + +interface SupabaseResponse { + status?: number; + error?: { + message: string; + code?: string; + details?: unknown; + }; +} + +interface SupabaseError extends Error { + code?: string; + details?: unknown; +} + +interface SupabaseBreadcrumb { + type: string; + category: string; + message: string; + data?: { + query?: string[]; + body?: Record; + }; +} + +const instrumented = new Map(); + +/** + * Extracts the database operation type from the HTTP method and headers + * @param method - The HTTP method of the request + * @param headers - The request headers + * @returns The database operation type ('select', 'insert', 'upsert', 'update', or 'delete') + */ +export function extractOperation(method: string, headers: Record = {}): string { + switch (method) { + case 'GET': { + return 'select'; + } + case 'POST': { + if (headers['Prefer']?.includes('resolution=')) { + return 'upsert'; + } else { + return 'insert'; + } + } + case 'PATCH': { + return 'update'; + } + case 'DELETE': { + return 'delete'; + } + default: { + return ''; + } + } +} + +function instrumentSupabaseClient(SupabaseClient: unknown): void { + if (instrumented.has(SupabaseClient)) { + return; + } + + instrumented.set(SupabaseClient, { + from: (SupabaseClient as unknown as SupabaseClient).prototype.from, + }); + + (SupabaseClient as unknown as SupabaseClient).prototype.from = new Proxy( + (SupabaseClient as unknown as SupabaseClient).prototype.from, + { + apply(target, thisArg, argumentsList) { + const rv = Reflect.apply(target, thisArg, argumentsList); + const PostgrestQueryBuilder = (rv as PostgrestQueryBuilder).constructor; + + instrumentPostgrestQueryBuilder(PostgrestQueryBuilder as unknown as new () => PostgrestQueryBuilder); + + return rv; + }, + }, + ); +} + +// This is the only "instrumented" part of the SDK. The rest of instrumentation +// methods are only used as a mean to get to the `PostgrestFilterBuilder` constructor itself. +function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilterBuilder['constructor']): void { + if (instrumented.has(PostgrestFilterBuilder)) { + return; + } + + instrumented.set(PostgrestFilterBuilder, { + then: ( + PostgrestFilterBuilder.prototype as unknown as { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; + } + ).then, + }); + + ( + PostgrestFilterBuilder.prototype as unknown as { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; + } + ).then = new Proxy( + ( + PostgrestFilterBuilder.prototype as unknown as { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; + } + ).then, + { + apply(target, thisArg, argumentsList) { + const operations = AVAILABLE_OPERATIONS; + const typedThis = thisArg as PostgrestFilterBuilder; + const operation = extractOperation(typedThis.method, typedThis.headers); + + if (!operations.includes(operation)) { + return Reflect.apply(target, thisArg, argumentsList); + } + + if (!typedThis?.url?.pathname || typeof typedThis.url.pathname !== 'string') { + return Reflect.apply(target, thisArg, argumentsList); + } + + const pathParts = typedThis.url.pathname.split('/'); + const table = pathParts.length > 0 ? pathParts[pathParts.length - 1] : ''; + const description = `from(${table})`; + + const query: string[] = []; + for (const [key, value] of typedThis.url.searchParams.entries()) { + // It's possible to have multiple entries for the same key, eg. `id=eq.7&id=eq.3`, + // so we need to use array instead of object to collect them. + query.push(translateFiltersIntoMethods(key, value)); + } + + const body: Record = {}; + if (isPlainObject(typedThis.body)) { + for (const [key, value] of Object.entries(typedThis.body)) { + body[key] = value; + } + } + + // TODO / Should? + const shouldCreateSpan = true; + + let span: Span | undefined; + + if (shouldCreateSpan) { + const attributes: Record = { + 'db.table': table, + 'db.schema': typedThis.schema, + 'db.url': typedThis.url.origin, + 'db.sdk': typedThis.headers['X-Client-Info'], + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, + }; + + if (query.length) { + attributes['db.query'] = query; + } + + if (Object.keys(body).length) { + attributes['db.body'] = body; + } + + span = startInactiveSpan({ + name: description, + attributes, + }); + } + + return (Reflect.apply(target, thisArg, []) as Promise) + .then( + (res: SupabaseResponse) => { + if (span) { + if (res && typeof res === 'object' && 'status' in res) { + setHttpStatus(span, res.status || 500); + } + span.end(); + } + + if (res.error) { + const err = new Error(res.error.message) as SupabaseError; + if (res.error.code) { + err.code = res.error.code; + } + if (res.error.details) { + err.details = res.error.details; + } + + const supabaseContext: Record = {}; + if (query.length) { + supabaseContext.query = query; + } + if (Object.keys(body).length) { + supabaseContext.body = body; + } + + captureException(err, { + contexts: { + supabase: supabaseContext, + }, + }); + } + + // Todo / Should? + const shouldCreateBreadcrumb = true; + + if (shouldCreateBreadcrumb) { + const breadcrumb: SupabaseBreadcrumb = { + type: 'supabase', + category: `db.${operation}`, + message: description, + }; + + const data: Record = {}; + + if (query.length) { + data.query = query; + } + + if (Object.keys(body).length) { + data.body = body; + } + + if (Object.keys(data).length) { + breadcrumb.data = data; + } + + addBreadcrumb(breadcrumb); + } + + return res; + }, + (err: Error) => { + if (span) { + setHttpStatus(span, 500); + span.end(); + } + throw err; + }, + ) + .then(...argumentsList); + }, + }, + ); +} + +function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => PostgrestQueryBuilder): void { + if (instrumented.has(PostgrestQueryBuilder)) { + return; + } + + // We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder` + // constructor, as we don't know which method will be called first, an we don't want to miss any calls. + for (const operation of AVAILABLE_OPERATIONS) { + logger.log(`Instrumenting ${operation} operation`); + + instrumented.set(PostgrestQueryBuilder, { + [operation]: (PostgrestQueryBuilder.prototype as Record)[ + operation as 'select' | 'insert' | 'upsert' | 'update' | 'delete' + ] as (...args: unknown[]) => PostgrestFilterBuilder, + }); + + type PostgrestOperation = keyof Pick; + (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation] = new Proxy( + (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation], + { + apply(target, thisArg, argumentsList) { + const rv = Reflect.apply(target, thisArg, argumentsList); + const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor; + + logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`); + + instrumentPostgrestFilterBuilder(PostgrestFilterBuilder); + + return rv; + }, + }, + ); + } +} + +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'); + } + + // We want to allow passing either `SupabaseClient` constructor + // or an instance returned from `createClient()`. + const SupabaseClient = supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor; + + instrumentSupabaseClient(SupabaseClient); +}; + +const INTEGRATION_NAME = 'Supabase'; + +const _supabaseIntegration = (({ supabaseClient } = {}) => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentSupabase(supabaseClient); + }, + }; +}) satisfies IntegrationFn; + +export const supabaseIntegration = defineIntegration((options: { supabaseClient: unknown }) => { + return { + ..._supabaseIntegration(options), + name: INTEGRATION_NAME, + }; +}) satisfies IntegrationFn; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 8467f3e3727d..2afa93126218 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -128,6 +128,7 @@ export { trpcMiddleware, updateSpanName, zodErrorsIntegration, + supabaseIntegration, profiler, consoleLoggingIntegration, consoleIntegration, From a6c0d4d53c1fc19840f15ea3498f5d6d92892c85 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 10:36:03 +0000 Subject: [PATCH 02/53] Add missing package exports --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index d89503eb9dfb..58521a4d11d0 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -117,6 +117,7 @@ export { startSession, startSpan, startSpanManual, + supabaseIntegration, tediousIntegration, trpcMiddleware, updateSpanName, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 59465831a734..1299eafd982b 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -109,6 +109,7 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, + supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index a1c26d5a2819..689509a7d202 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -130,6 +130,7 @@ export { updateSpanName, zodErrorsIntegration, profiler, + supabaseIntegration, amqplibIntegration, vercelAIIntegration, logger, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 54ae30fb5c8c..d1a3cc906f4e 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -108,6 +108,7 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, + supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, From ab36c162b347afab944a382d1446cfd5b8f47dbf Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 10:38:50 +0000 Subject: [PATCH 03/53] Remove debug logging --- .../test-applications/supabase-nextjs/sentry.client.config.ts | 3 --- .../test-applications/supabase-nextjs/sentry.edge.config.ts | 2 -- .../test-applications/supabase-nextjs/sentry.server.config.ts | 2 -- 3 files changed, 7 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts index 076e6504cb48..acd2f0768675 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts @@ -28,7 +28,4 @@ Sentry.init({ // We expect the app to send a lot of events in a short time bufferSize: 1000, }, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: true, }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts index c05ce9a5fbc5..59ad9eb6befe 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts @@ -14,6 +14,4 @@ Sentry.init({ // We expect the app to send a lot of events in a short time bufferSize: 1000, }, - - debug: true, }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts index 175a063db952..aa1d2031926f 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts @@ -14,6 +14,4 @@ Sentry.init({ // We expect the app to send a lot of events in a short time bufferSize: 1000, }, - - debug: true, }); From 9f58b895e8d96d32a35ad8c95e49f23eb001298a Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 10:45:32 +0000 Subject: [PATCH 04/53] Bump next 14 version --- .../test-applications/supabase-nextjs/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 692ec2200695..d8a2e7546545 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -13,7 +13,7 @@ "test:assert": "pnpm test:prod" }, "dependencies": { - "@next/font": "14.1.3", + "@next/font": "14.2.15", "@sentry/nextjs": "latest || *", "@supabase/auth-helpers-react": "^0.3.1", "@supabase/auth-ui-react": "^0.2.8", @@ -22,7 +22,7 @@ "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "concurrently": "^7.6.0", - "next": "14.1.3", + "next": "14.2.15", "react": "18.2.0", "react-dom": "18.2.0", "supabase": "^2.19.7", @@ -31,7 +31,7 @@ "devDependencies": { "@sentry-internal/test-utils": "link:../../../test-utils", "eslint": "8.34.0", - "eslint-config-next": "14.1.3" + "eslint-config-next": "14.2.15" }, "volta": { "extends": "../../package.json" From f37fa5a5284cb2d2e005489d448e90bd7da751fa Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 12:53:26 +0000 Subject: [PATCH 05/53] Hard-code default development variables --- .../supabase-nextjs/lib/initSupabase.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts index 9ea30eb9f089..27c5707f0d54 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts @@ -1,9 +1,16 @@ import { createClient } from '@supabase/supabase-js'; import * as Sentry from '@sentry/nextjs'; +// These are the default development keys for a local Supabase instance +const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; +const NEXT_PUBLIC_SUPABASE_ANON_KEY = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4cGxvcmV0ZXN0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2'; +const SUPABASE_SERVICE_ROLE_KEY = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4cGxvcmV0ZXN0Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsIml'; + export const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', - process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '', + NEXT_PUBLIC_SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY ?? NEXT_PUBLIC_SUPABASE_ANON_KEY, ); Sentry.addIntegration( From 68e7774b4a9f1671f9bf3765ef15495533dddcf2 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 13:06:43 +0000 Subject: [PATCH 06/53] Add playwright to dev-dependencies. --- .../e2e-tests/test-applications/supabase-nextjs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index d8a2e7546545..8e0e1bd4c009 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -29,6 +29,7 @@ "typescript": "4.9.5" }, "devDependencies": { + "@playwright/test": "~1.50.0", "@sentry-internal/test-utils": "link:../../../test-utils", "eslint": "8.34.0", "eslint-config-next": "14.2.15" From d384cc3235224f328889ecd06de865c2233b945f Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 14:27:37 +0000 Subject: [PATCH 07/53] Move `supabase` into its own internal package. --- .craft.yml | 1 - package.json | 1 + packages/browser/src/exports.ts | 2 +- packages/browser/src/index.ts | 3 +- packages/core/src/index.ts | 1 - packages/core/src/integrations/supabase.ts | 433 --------------------- packages/node/src/index.ts | 2 +- 7 files changed, 5 insertions(+), 438 deletions(-) delete mode 100644 packages/core/src/integrations/supabase.ts diff --git a/.craft.yml b/.craft.yml index deb38bf0c40d..f1f6df1f2a7b 100644 --- a/.craft.yml +++ b/.craft.yml @@ -32,7 +32,6 @@ targets: - name: npm id: '@sentry-internal/replay-canvas' includeNames: /^sentry-internal-replay-canvas-\d.*\.tgz$/ - ## 2. Browser & Node SDKs - name: npm id: '@sentry/browser' diff --git a/package.json b/package.json index a3fb0123a5ae..779ff8513e62 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "packages/replay-worker", "packages/solid", "packages/solidstart", + "packages/supabase", "packages/svelte", "packages/sveltekit", "packages/tanstackstart", diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index a66d7d9a52b7..534ef7197691 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -60,8 +60,8 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, - updateSpanName, supabaseIntegration, + updateSpanName, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 34dd7afb96ed..c35371377cb6 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -59,10 +59,11 @@ export { setHttpStatus, makeMultiplexedTransport, moduleMetadataIntegration, - zodErrorsIntegration, supabaseIntegration, + zodErrorsIntegration, thirdPartyErrorFilterIntegration, } from '@sentry/core'; + export type { Span } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f4e4e9e4a001..71a8b03acacb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -107,7 +107,6 @@ export { dedupeIntegration } from './integrations/dedupe'; export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; export { zodErrorsIntegration } from './integrations/zoderrors'; -export { supabaseIntegration } from './integrations/supabase'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts deleted file mode 100644 index 847de37d399b..000000000000 --- a/packages/core/src/integrations/supabase.ts +++ /dev/null @@ -1,433 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ - -import { addBreadcrumb } from '../breadcrumbs'; -import { captureException } from '../exports'; -import { defineIntegration } from '../integration'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes'; -import { startInactiveSpan, setHttpStatus } from '../tracing'; -import type { Span, IntegrationFn } from '../types-hoist'; -import { logger, isPlainObject } from '../utils-hoist'; - -/* eslint-disable max-lines */ -export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; - -export const FILTER_MAPPINGS = { - eq: 'eq', - neq: 'neq', - gt: 'gt', - gte: 'gte', - lt: 'lt', - lte: 'lte', - like: 'like', - 'like(all)': 'likeAllOf', - 'like(any)': 'likeAnyOf', - ilike: 'ilike', - 'ilike(all)': 'ilikeAllOf', - 'ilike(any)': 'ilikeAnyOf', - is: 'is', - in: 'in', - cs: 'contains', - cd: 'containedBy', - sr: 'rangeGt', - nxl: 'rangeGte', - sl: 'rangeLt', - nxr: 'rangeLte', - adj: 'rangeAdjacent', - ov: 'overlaps', - fts: '', - plfts: 'plain', - phfts: 'phrase', - wfts: 'websearch', - not: 'not', -}; -/** - * Translates Supabase filter parameters into readable method names for tracing - * @param key - The filter key from the URL search parameters - * @param query - The filter value from the URL search parameters - * @returns A string representation of the filter as a method call - */ -export function translateFiltersIntoMethods(key: string, query: string): string { - if (query === '' || query === '*') { - return 'select(*)'; - } - - if (key === 'select') { - return `select(${query})`; - } - - if (key === 'or' || key.endsWith('.or')) { - return `${key}${query}`; - } - - const [filter, ...value] = query.split('.'); - - let method; - // Handle optional `configPart` of the filter - if (filter?.startsWith('fts')) { - method = 'textSearch'; - } else if (filter?.startsWith('plfts')) { - method = 'textSearch[plain]'; - } else if (filter?.startsWith('phfts')) { - method = 'textSearch[phrase]'; - } else if (filter?.startsWith('wfts')) { - method = 'textSearch[websearch]'; - } else { - method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || 'filter'; - } - - return `${method}(${key}, ${value.join('.')})`; -} - -interface SupabaseClient { - prototype: { - from: (table: string) => PostgrestQueryBuilder; - }; -} - -interface PostgrestQueryBuilder { - select: (...args: unknown[]) => PostgrestFilterBuilder; - insert: (...args: unknown[]) => PostgrestFilterBuilder; - upsert: (...args: unknown[]) => PostgrestFilterBuilder; - update: (...args: unknown[]) => PostgrestFilterBuilder; - delete: (...args: unknown[]) => PostgrestFilterBuilder; -} - -interface PostgrestFilterBuilder { - method: string; - headers: Record; - url: URL; - schema: string; - body: any; -} - -interface SupabaseResponse { - status?: number; - error?: { - message: string; - code?: string; - details?: unknown; - }; -} - -interface SupabaseError extends Error { - code?: string; - details?: unknown; -} - -interface SupabaseBreadcrumb { - type: string; - category: string; - message: string; - data?: { - query?: string[]; - body?: Record; - }; -} - -const instrumented = new Map(); - -/** - * Extracts the database operation type from the HTTP method and headers - * @param method - The HTTP method of the request - * @param headers - The request headers - * @returns The database operation type ('select', 'insert', 'upsert', 'update', or 'delete') - */ -export function extractOperation(method: string, headers: Record = {}): string { - switch (method) { - case 'GET': { - return 'select'; - } - case 'POST': { - if (headers['Prefer']?.includes('resolution=')) { - return 'upsert'; - } else { - return 'insert'; - } - } - case 'PATCH': { - return 'update'; - } - case 'DELETE': { - return 'delete'; - } - default: { - return ''; - } - } -} - -function instrumentSupabaseClient(SupabaseClient: unknown): void { - if (instrumented.has(SupabaseClient)) { - return; - } - - instrumented.set(SupabaseClient, { - from: (SupabaseClient as unknown as SupabaseClient).prototype.from, - }); - - (SupabaseClient as unknown as SupabaseClient).prototype.from = new Proxy( - (SupabaseClient as unknown as SupabaseClient).prototype.from, - { - apply(target, thisArg, argumentsList) { - const rv = Reflect.apply(target, thisArg, argumentsList); - const PostgrestQueryBuilder = (rv as PostgrestQueryBuilder).constructor; - - instrumentPostgrestQueryBuilder(PostgrestQueryBuilder as unknown as new () => PostgrestQueryBuilder); - - return rv; - }, - }, - ); -} - -// This is the only "instrumented" part of the SDK. The rest of instrumentation -// methods are only used as a mean to get to the `PostgrestFilterBuilder` constructor itself. -function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilterBuilder['constructor']): void { - if (instrumented.has(PostgrestFilterBuilder)) { - return; - } - - instrumented.set(PostgrestFilterBuilder, { - then: ( - PostgrestFilterBuilder.prototype as unknown as { - then: ( - onfulfilled?: ((value: T) => T | PromiseLike) | null, - onrejected?: ((reason: any) => T | PromiseLike) | null, - ) => Promise; - } - ).then, - }); - - ( - PostgrestFilterBuilder.prototype as unknown as { - then: ( - onfulfilled?: ((value: T) => T | PromiseLike) | null, - onrejected?: ((reason: any) => T | PromiseLike) | null, - ) => Promise; - } - ).then = new Proxy( - ( - PostgrestFilterBuilder.prototype as unknown as { - then: ( - onfulfilled?: ((value: T) => T | PromiseLike) | null, - onrejected?: ((reason: any) => T | PromiseLike) | null, - ) => Promise; - } - ).then, - { - apply(target, thisArg, argumentsList) { - const operations = AVAILABLE_OPERATIONS; - const typedThis = thisArg as PostgrestFilterBuilder; - const operation = extractOperation(typedThis.method, typedThis.headers); - - if (!operations.includes(operation)) { - return Reflect.apply(target, thisArg, argumentsList); - } - - if (!typedThis?.url?.pathname || typeof typedThis.url.pathname !== 'string') { - return Reflect.apply(target, thisArg, argumentsList); - } - - const pathParts = typedThis.url.pathname.split('/'); - const table = pathParts.length > 0 ? pathParts[pathParts.length - 1] : ''; - const description = `from(${table})`; - - const query: string[] = []; - for (const [key, value] of typedThis.url.searchParams.entries()) { - // It's possible to have multiple entries for the same key, eg. `id=eq.7&id=eq.3`, - // so we need to use array instead of object to collect them. - query.push(translateFiltersIntoMethods(key, value)); - } - - const body: Record = {}; - if (isPlainObject(typedThis.body)) { - for (const [key, value] of Object.entries(typedThis.body)) { - body[key] = value; - } - } - - // TODO / Should? - const shouldCreateSpan = true; - - let span: Span | undefined; - - if (shouldCreateSpan) { - const attributes: Record = { - 'db.table': table, - 'db.schema': typedThis.schema, - 'db.url': typedThis.url.origin, - 'db.sdk': typedThis.headers['X-Client-Info'], - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, - }; - - if (query.length) { - attributes['db.query'] = query; - } - - if (Object.keys(body).length) { - attributes['db.body'] = body; - } - - span = startInactiveSpan({ - name: description, - attributes, - }); - } - - return (Reflect.apply(target, thisArg, []) as Promise) - .then( - (res: SupabaseResponse) => { - if (span) { - if (res && typeof res === 'object' && 'status' in res) { - setHttpStatus(span, res.status || 500); - } - span.end(); - } - - if (res.error) { - const err = new Error(res.error.message) as SupabaseError; - if (res.error.code) { - err.code = res.error.code; - } - if (res.error.details) { - err.details = res.error.details; - } - - const supabaseContext: Record = {}; - if (query.length) { - supabaseContext.query = query; - } - if (Object.keys(body).length) { - supabaseContext.body = body; - } - - captureException(err, { - contexts: { - supabase: supabaseContext, - }, - }); - } - - // Todo / Should? - const shouldCreateBreadcrumb = true; - - if (shouldCreateBreadcrumb) { - const breadcrumb: SupabaseBreadcrumb = { - type: 'supabase', - category: `db.${operation}`, - message: description, - }; - - const data: Record = {}; - - if (query.length) { - data.query = query; - } - - if (Object.keys(body).length) { - data.body = body; - } - - if (Object.keys(data).length) { - breadcrumb.data = data; - } - - addBreadcrumb(breadcrumb); - } - - return res; - }, - (err: Error) => { - if (span) { - setHttpStatus(span, 500); - span.end(); - } - throw err; - }, - ) - .then(...argumentsList); - }, - }, - ); -} - -function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => PostgrestQueryBuilder): void { - if (instrumented.has(PostgrestQueryBuilder)) { - return; - } - - // We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder` - // constructor, as we don't know which method will be called first, an we don't want to miss any calls. - for (const operation of AVAILABLE_OPERATIONS) { - logger.log(`Instrumenting ${operation} operation`); - - instrumented.set(PostgrestQueryBuilder, { - [operation]: (PostgrestQueryBuilder.prototype as Record)[ - operation as 'select' | 'insert' | 'upsert' | 'update' | 'delete' - ] as (...args: unknown[]) => PostgrestFilterBuilder, - }); - - type PostgrestOperation = keyof Pick; - (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation] = new Proxy( - (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation], - { - apply(target, thisArg, argumentsList) { - const rv = Reflect.apply(target, thisArg, argumentsList); - const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor; - - logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`); - - instrumentPostgrestFilterBuilder(PostgrestFilterBuilder); - - return rv; - }, - }, - ); - } -} - -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'); - } - - // We want to allow passing either `SupabaseClient` constructor - // or an instance returned from `createClient()`. - const SupabaseClient = supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor; - - instrumentSupabaseClient(SupabaseClient); -}; - -const INTEGRATION_NAME = 'Supabase'; - -const _supabaseIntegration = (({ supabaseClient } = {}) => { - return { - name: INTEGRATION_NAME, - setupOnce() { - instrumentSupabase(supabaseClient); - }, - }; -}) satisfies IntegrationFn; - -export const supabaseIntegration = defineIntegration((options: { supabaseClient: unknown }) => { - return { - ..._supabaseIntegration(options), - name: INTEGRATION_NAME, - }; -}) satisfies IntegrationFn; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 2afa93126218..2e162636462b 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -127,8 +127,8 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, + supabseIntegration, zodErrorsIntegration, - supabaseIntegration, profiler, consoleLoggingIntegration, consoleIntegration, From 4973f04d6ce96c71adf7443cadd13d16760f9f1e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 18 Mar 2025 14:42:12 +0000 Subject: [PATCH 08/53] Add new package reference to Remix integration tests --- packages/remix/test/integration/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 7f4172b0b42f..80c258afd09e 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -32,6 +32,7 @@ "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", "@sentry-internal/replay-canvas": "file:../../../replay-canvas", + "@sentry-internal/supabase": "file:../../../supabase", "@sentry-internal/feedback": "file:../../../feedback", "@sentry-internal/browser-integration-tests": "file:../../../../dev-packages/browser-integration-tests", "@vanilla-extract/css": "1.13.0", From b5ff6db9be9c19ddc5d4b0208277d6d33d99bc04 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 19 Mar 2025 10:01:15 +0000 Subject: [PATCH 09/53] Update deps --- .../supabase-nextjs/package.json | 10 +-- .../pages/api/add-todo-entry-edge.ts | 62 ------------------- 2 files changed, 5 insertions(+), 67 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 8e0e1bd4c009..99164b21c15c 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -15,17 +15,17 @@ "dependencies": { "@next/font": "14.2.15", "@sentry/nextjs": "latest || *", - "@supabase/auth-helpers-react": "^0.3.1", - "@supabase/auth-ui-react": "^0.2.8", - "@supabase/supabase-js": "^2.8.0", + "@supabase/auth-helpers-react": "0.5.0", + "@supabase/auth-ui-react": "0.4.7", + "@supabase/supabase-js": "2.49.1", "@types/node": "18.14.0", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", - "concurrently": "^7.6.0", + "concurrently": "7.6.0", "next": "14.2.15", "react": "18.2.0", "react-dom": "18.2.0", - "supabase": "^2.19.7", + "supabase": "2.19.7", "typescript": "4.9.5" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts deleted file mode 100644 index decca208ddb3..000000000000 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry-edge.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import * as Sentry from '@sentry/nextjs'; -import { supabase } from '@/lib/initSupabase'; - -Sentry.addIntegration(Sentry.supabaseIntegration({ - supabaseClient: supabase, -})); - -export const config = { - runtime: 'edge', -}; - -type Data = { - data: any; - error: any; -}; - -async function login() { - const { data, error } = await supabase.auth.signInWithPassword({ - email: 'test@sentry.test', - password: 'sentry.test', - }); - - if (error) { - console.log('error', error); - } - - return data; -} - -async function addTodoEntry(userId?: string) { - const { error } = await supabase.from('todos').insert({ task: 'test', user_id: userId }).select().single(); - - if (error) { - console.log('error', error); - } -} - -export default async function handler() { - const { user } = await login(); - - await addTodoEntry(user?.id); - - const { data, error } = await supabase.from('todos').select('*'); - - if (error) { - console.log('error', error); - } - - return new Response( - JSON.stringify({ - data, - error, - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ); -} From 38962d18c44addf7dce09d9066f17fdbbd08fb17 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 19 Mar 2025 10:38:20 +0000 Subject: [PATCH 10/53] Separate anon and service clients. --- .../supabase-nextjs/lib/initSupabaseAdmin.ts | 21 +++++++++++++++++++ .../{initSupabase.ts => initSupabaseAnon.ts} | 7 +------ .../supabase-nextjs/pages/_app.tsx | 2 +- .../pages/api/add-todo-entry.ts | 2 +- .../pages/api/create-test-user.ts | 2 +- 5 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts rename dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/{initSupabase.ts => initSupabaseAnon.ts} (62%) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts new file mode 100644 index 000000000000..051f856975bc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -0,0 +1,21 @@ +import { createClient } from '@supabase/supabase-js'; +import * as Sentry from '@sentry/nextjs'; + +// These are the default development keys for a local Supabase instance +const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; +const SUPABASE_SERVICE_ROLE_KEY = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'; + +export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + }, +}); + +Sentry.addIntegration( + Sentry.supabaseIntegration({ + supabaseClient: supabase, + }), +); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts similarity index 62% rename from dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts rename to dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 27c5707f0d54..0b92c9ae2e78 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabase.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -5,13 +5,8 @@ import * as Sentry from '@sentry/nextjs'; const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; const NEXT_PUBLIC_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4cGxvcmV0ZXN0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2'; -const SUPABASE_SERVICE_ROLE_KEY = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4cGxvcmV0ZXN0Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsIml'; -export const supabase = createClient( - NEXT_PUBLIC_SUPABASE_URL, - SUPABASE_SERVICE_ROLE_KEY ?? NEXT_PUBLIC_SUPABASE_ANON_KEY, -); +export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); Sentry.addIntegration( Sentry.supabaseIntegration({ diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx index a36700d408b7..50aba0f261b7 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx @@ -1,4 +1,4 @@ -import { supabase } from '@/lib/initSupabase' +import { supabase } from '@/lib/initSupabaseAnon' import { SessionContextProvider } from '@supabase/auth-helpers-react' import type { AppProps } from 'next/app' diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts index 2230868d9de9..af5c96eb8321 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabase } from '@/lib/initSupabase'; +import { supabase } from '@/lib/initSupabaseAdmin'; type Data = { data: any; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts index 0d1bd9395b6f..f4d2143a37b2 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabase } from '@/lib/initSupabase'; +import { supabase } from '@/lib/initSupabaseAdmin'; type Data = { data: any; From dedf54ec86177306f84d704fc2361928e00f54f1 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 19 Mar 2025 13:39:49 +0000 Subject: [PATCH 11/53] Make `supabase` a public package --- .../test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts | 3 ++- .../test-applications/supabase-nextjs/lib/initSupabaseAnon.ts | 3 ++- .../e2e-tests/test-applications/supabase-nextjs/package.json | 1 + packages/astro/src/index.server.ts | 1 - packages/aws-serverless/src/index.ts | 1 - packages/browser/src/index.ts | 1 - packages/bun/src/index.ts | 1 - packages/google-cloud-serverless/src/index.ts | 1 - packages/remix/test/integration/package.json | 2 +- 9 files changed, 6 insertions(+), 8 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index 051f856975bc..f28371fce180 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -1,5 +1,6 @@ import { createClient } from '@supabase/supabase-js'; import * as Sentry from '@sentry/nextjs'; +import { supabaseIntegration } from '@sentry/supabase'; // These are the default development keys for a local Supabase instance const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; @@ -15,7 +16,7 @@ export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ }); Sentry.addIntegration( - Sentry.supabaseIntegration({ + supabaseIntegration({ supabaseClient: supabase, }), ); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 0b92c9ae2e78..471f9b0d815f 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -1,5 +1,6 @@ import { createClient } from '@supabase/supabase-js'; import * as Sentry from '@sentry/nextjs'; +import { supabaseIntegration } from '@sentry/supabase'; // These are the default development keys for a local Supabase instance const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; @@ -9,7 +10,7 @@ const NEXT_PUBLIC_SUPABASE_ANON_KEY = export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); Sentry.addIntegration( - Sentry.supabaseIntegration({ + supabaseIntegration({ supabaseClient: supabase, }), ); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 99164b21c15c..eee53031fb74 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -15,6 +15,7 @@ "dependencies": { "@next/font": "14.2.15", "@sentry/nextjs": "latest || *", + "@sentry/supabase": "latest || *", "@supabase/auth-helpers-react": "0.5.0", "@supabase/auth-ui-react": "0.4.7", "@supabase/supabase-js": "2.49.1", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 58521a4d11d0..d89503eb9dfb 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -117,7 +117,6 @@ export { startSession, startSpan, startSpanManual, - supabaseIntegration, tediousIntegration, trpcMiddleware, updateSpanName, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 1299eafd982b..59465831a734 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -109,7 +109,6 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, - supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index c35371377cb6..1a5c090a55ec 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -63,7 +63,6 @@ export { zodErrorsIntegration, thirdPartyErrorFilterIntegration, } from '@sentry/core'; - export type { Span } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 689509a7d202..a1c26d5a2819 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -130,7 +130,6 @@ export { updateSpanName, zodErrorsIntegration, profiler, - supabaseIntegration, amqplibIntegration, vercelAIIntegration, logger, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index d1a3cc906f4e..54ae30fb5c8c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -108,7 +108,6 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, - supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 80c258afd09e..07b6982a8f32 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -29,10 +29,10 @@ "@sentry/node": "file:../../../node", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", + "@sentry/supabase": "file:../../../supabase", "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", "@sentry-internal/replay-canvas": "file:../../../replay-canvas", - "@sentry-internal/supabase": "file:../../../supabase", "@sentry-internal/feedback": "file:../../../feedback", "@sentry-internal/browser-integration-tests": "file:../../../../dev-packages/browser-integration-tests", "@vanilla-extract/css": "1.13.0", From 01bf0d607f2ddbffc8fa702df6348d422cb86283 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 19 Mar 2025 14:00:48 +0000 Subject: [PATCH 12/53] Add `supabase` to verdaccio config --- dev-packages/e2e-tests/verdaccio-config/config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 8535c5898175..52a9d51ac383 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -158,6 +158,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/supabase': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/svelte': access: $all publish: $all From 79dd5d7636214b6d223a9e12cdd42393e2073ee7 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 20 Mar 2025 12:46:04 +0000 Subject: [PATCH 13/53] Remove unused resolutions and dependencies --- packages/remix/test/integration/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 07b6982a8f32..7f4172b0b42f 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -29,7 +29,6 @@ "@sentry/node": "file:../../../node", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", - "@sentry/supabase": "file:../../../supabase", "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", "@sentry-internal/replay-canvas": "file:../../../replay-canvas", From 5f00c3ccacd78b7d5e21c1350409b685be0b0622 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 24 Mar 2025 12:46:10 +0000 Subject: [PATCH 14/53] Move `supabaseIntegration` to `@sentry/core` --- .craft.yml | 1 + .../supabase/db-operations/init.js | 41 ++++++++++++++ .../supabase/db-operations/test.ts | 53 +++++++++++++++++++ .../supabase-nextjs/.eslintrc.json | 3 -- .../supabase-nextjs/lib/initSupabaseAdmin.ts | 3 +- .../supabase-nextjs/lib/initSupabaseAnon.ts | 3 +- .../supabase-nextjs/package.json | 9 ++-- .../supabase-nextjs/playwright.config.mjs | 2 +- .../e2e-tests/verdaccio-config/config.yaml | 6 --- package.json | 1 - packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/cloudflare/src/index.ts | 1 + packages/core/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 2 +- packages/remix/src/cloudflare/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/sveltekit/src/worker/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 24 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json diff --git a/.craft.yml b/.craft.yml index f1f6df1f2a7b..deb38bf0c40d 100644 --- a/.craft.yml +++ b/.craft.yml @@ -32,6 +32,7 @@ targets: - name: npm id: '@sentry-internal/replay-canvas' includeNames: /^sentry-internal-replay-canvas-\d.*\.tgz$/ + ## 2. Browser & Node SDKs - name: npm id: '@sentry/browser' diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js new file mode 100644 index 000000000000..ed56ce0ec989 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -0,0 +1,41 @@ +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://public@dsn.ingest.sentry.io/1337', + integrations: [ + new Sentry.BrowserTracing({ + tracingOrigins: ['localhost', 'my.supabase.co'], + }), + Sentry.supabaseIntegration(supabase) + ], + tracesSampleRate: 1.0, +}); + +// Simulate database operations +async function performDatabaseOperations() { + try { + await supabase + .from('todos') + .insert([{ title: 'Test Todo' }]); + + await supabase + .from('todos') + .select('*'); + + // Trigger an error to capture the breadcrumbs + throw new Error('Test Error'); + } catch (error) { + Sentry.captureException(error); + } +} + +performDatabaseOperations(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts new file mode 100644 index 000000000000..f6f38347d711 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -0,0 +1,53 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +describe('Supabase Integration', () => { + sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.breadcrumbs).toBeDefined(); + expect(eventData.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.insert', + message: 'from(todos)', + data: expect.any(Object), + }); + }); + + sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + expect(events).toHaveLength(2); + + events.forEach(event => { + expect( + event.breadcrumbs?.some( + breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.'), + ), + ).toBe(true); + }); + }); + + sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const supaBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); + + expect(supaBreadcrumb).toBeDefined(); + expect(supaBreadcrumb?.data).toMatchObject({ + table: expect.any(String), + operation: expect.any(String), + timestamp: expect.any(Number), + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json deleted file mode 100644 index bffb357a7122..000000000000 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index f28371fce180..051f856975bc 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -1,6 +1,5 @@ import { createClient } from '@supabase/supabase-js'; import * as Sentry from '@sentry/nextjs'; -import { supabaseIntegration } from '@sentry/supabase'; // These are the default development keys for a local Supabase instance const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; @@ -16,7 +15,7 @@ export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ }); Sentry.addIntegration( - supabaseIntegration({ + Sentry.supabaseIntegration({ supabaseClient: supabase, }), ); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 471f9b0d815f..0b92c9ae2e78 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -1,6 +1,5 @@ import { createClient } from '@supabase/supabase-js'; import * as Sentry from '@sentry/nextjs'; -import { supabaseIntegration } from '@sentry/supabase'; // These are the default development keys for a local Supabase instance const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; @@ -10,7 +9,7 @@ const NEXT_PUBLIC_SUPABASE_ANON_KEY = export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); Sentry.addIntegration( - supabaseIntegration({ + Sentry.supabaseIntegration({ supabaseClient: supabase, }), ); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index eee53031fb74..4af4f244b364 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -8,14 +8,13 @@ "start": "next start", "clean": "npx rimraf node_modules pnpm-lock.yaml .next", "start-local-supabase": "supabase init --force --workdir . && supabase start -o env", - "test:prod": "TEST_ENV=production playwright test", + "test:prod": "TEST_ENV=production playwright test --ui", "test:build": "pnpm install && pnpm start-local-supabase && pnpm build", "test:assert": "pnpm test:prod" }, "dependencies": { - "@next/font": "14.2.15", + "@next/font": "14.2.25", "@sentry/nextjs": "latest || *", - "@sentry/supabase": "latest || *", "@supabase/auth-helpers-react": "0.5.0", "@supabase/auth-ui-react": "0.4.7", "@supabase/supabase-js": "2.49.1", @@ -23,7 +22,7 @@ "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "concurrently": "7.6.0", - "next": "14.2.15", + "next": "14.2.25", "react": "18.2.0", "react-dom": "18.2.0", "supabase": "2.19.7", @@ -33,7 +32,7 @@ "@playwright/test": "~1.50.0", "@sentry-internal/test-utils": "link:../../../test-utils", "eslint": "8.34.0", - "eslint-config-next": "14.2.15" + "eslint-config-next": "14.2.25" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs index 3d3ab7d8df02..a35fe82a4001 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/playwright.config.mjs @@ -1,7 +1,7 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: `pnpm dev`, port: 3030, }); diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 52a9d51ac383..8535c5898175 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -158,12 +158,6 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! - '@sentry/supabase': - access: $all - publish: $all - unpublish: $all - # proxy: npmjs # Don't proxy for E2E tests! - '@sentry/svelte': access: $all publish: $all diff --git a/package.json b/package.json index 779ff8513e62..a3fb0123a5ae 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "packages/replay-worker", "packages/solid", "packages/solidstart", - "packages/supabase", "packages/svelte", "packages/sveltekit", "packages/tanstackstart", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index d89503eb9dfb..0de7ab9b897a 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -125,6 +125,7 @@ export { withIsolationScope, withMonitor, withScope, + supabaseIntegration, zodErrorsIntegration, profiler, logger, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 59465831a734..1299eafd982b 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -109,6 +109,7 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, + supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index a1c26d5a2819..05433871d541 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -128,6 +128,7 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, + supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index faad474cc801..1129020e27bf 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -75,6 +75,7 @@ export { rewriteFramesIntegration, captureConsoleIntegration, moduleMetadataIntegration, + supabaseIntegration, zodErrorsIntegration, consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 71a8b03acacb..f9fe2b47d370 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -106,6 +106,7 @@ export { captureConsoleIntegration } from './integrations/captureconsole'; export { dedupeIntegration } from './integrations/dedupe'; export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; +export { supabaseIntegration } from './integrations/supabase'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index a906197b40c2..b25d810cd3c8 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -71,6 +71,7 @@ export { dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, + supabaseIntegration, zodErrorsIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 54ae30fb5c8c..d1a3cc906f4e 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -108,6 +108,7 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, + supabaseIntegration, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 2e162636462b..8a98ff37c95b 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -127,7 +127,7 @@ export { spanToBaggageHeader, trpcMiddleware, updateSpanName, - supabseIntegration, + supabaseIntegration, zodErrorsIntegration, profiler, consoleLoggingIntegration, diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 5d15be8edee7..8093088a0171 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -95,6 +95,7 @@ export { rewriteFramesIntegration, captureConsoleIntegration, moduleMetadataIntegration, + supabaseIntegration, zodErrorsIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 6c5319349294..92e6f95e1786 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -111,6 +111,7 @@ export { withIsolationScope, withMonitor, withScope, + supabaseIntegration, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index da00b43a4fde..001f87be0f90 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -114,6 +114,7 @@ export { withIsolationScope, withMonitor, withScope, + supabaseIntegration, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index f50420fd2937..6a92e063b02e 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -116,6 +116,7 @@ export { withIsolationScope, withMonitor, withScope, + supabaseIntegration, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 8e0e549440ca..ed5263ac4897 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -78,6 +78,7 @@ export { withIsolationScope, withMonitor, withScope, + supabaseIntegration, zodErrorsIntegration, } from '@sentry/cloudflare'; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 64ae281481d1..0d4e3f800604 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -75,6 +75,7 @@ export { rewriteFramesIntegration, captureConsoleIntegration, moduleMetadataIntegration, + supabaseIntegration, zodErrorsIntegration, consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, From 390744cd297b00daf7089601b29c90277ed71876 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 25 Mar 2025 12:16:12 +0000 Subject: [PATCH 15/53] Update import paths --- packages/core/src/integrations/supabase.ts | 433 +++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 packages/core/src/integrations/supabase.ts diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts new file mode 100644 index 000000000000..0c6deceb31e2 --- /dev/null +++ b/packages/core/src/integrations/supabase.ts @@ -0,0 +1,433 @@ +// eslint-disable max-lines +import { logger, isPlainObject } from '../utils-hoist'; + +import type { Span, IntegrationFn } from '../types-hoist'; +import { setHttpStatus, startInactiveSpan } 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'; + +export interface SupabaseClient { + prototype: { + from: (table: string) => PostgrestQueryBuilder; + }; +} + +export interface PostgrestQueryBuilder { + select: (...args: unknown[]) => PostgrestFilterBuilder; + insert: (...args: unknown[]) => PostgrestFilterBuilder; + upsert: (...args: unknown[]) => PostgrestFilterBuilder; + update: (...args: unknown[]) => PostgrestFilterBuilder; + delete: (...args: unknown[]) => PostgrestFilterBuilder; +} + +export interface PostgrestFilterBuilder { + method: string; + headers: Record; + url: URL; + schema: string; + body: any; +} + +export interface SupabaseResponse { + status?: number; + error?: { + message: string; + code?: string; + details?: unknown; + }; +} + +export interface SupabaseError extends Error { + code?: string; + details?: unknown; +} + +export interface SupabaseBreadcrumb { + type: string; + category: string; + message: string; + data?: { + query?: string[]; + body?: Record; + }; +} + +export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; + +export const FILTER_MAPPINGS = { + eq: 'eq', + neq: 'neq', + gt: 'gt', + gte: 'gte', + lt: 'lt', + lte: 'lte', + like: 'like', + 'like(all)': 'likeAllOf', + 'like(any)': 'likeAnyOf', + ilike: 'ilike', + 'ilike(all)': 'ilikeAllOf', + 'ilike(any)': 'ilikeAnyOf', + is: 'is', + in: 'in', + cs: 'contains', + cd: 'containedBy', + sr: 'rangeGt', + nxl: 'rangeGte', + sl: 'rangeLt', + nxr: 'rangeLte', + adj: 'rangeAdjacent', + ov: 'overlaps', + fts: '', + plfts: 'plain', + phfts: 'phrase', + wfts: 'websearch', + not: 'not', +}; + +const instrumented = new Map(); + +/** + * Extracts the database operation type from the HTTP method and headers + * @param method - The HTTP method of the request + * @param headers - The request headers + * @returns The database operation type ('select', 'insert', 'upsert', 'update', or 'delete') + */ +export function extractOperation(method: string, headers: Record = {}): string { + switch (method) { + case 'GET': { + return 'select'; + } + case 'POST': { + if (headers['Prefer']?.includes('resolution=')) { + return 'upsert'; + } else { + return 'insert'; + } + } + case 'PATCH': { + return 'update'; + } + case 'DELETE': { + return 'delete'; + } + default: { + return ''; + } + } +} + +/** + * Translates Supabase filter parameters into readable method names for tracing + * @param key - The filter key from the URL search parameters + * @param query - The filter value from the URL search parameters + * @returns A string representation of the filter as a method call + */ +export function translateFiltersIntoMethods(key: string, query: string): string { + if (query === '' || query === '*') { + return 'select(*)'; + } + + if (key === 'select') { + return `select(${query})`; + } + + if (key === 'or' || key.endsWith('.or')) { + return `${key}${query}`; + } + + const [filter, ...value] = query.split('.'); + + let method; + // Handle optional `configPart` of the filter + if (filter?.startsWith('fts')) { + method = 'textSearch'; + } else if (filter?.startsWith('plfts')) { + method = 'textSearch[plain]'; + } else if (filter?.startsWith('phfts')) { + method = 'textSearch[phrase]'; + } else if (filter?.startsWith('wfts')) { + method = 'textSearch[websearch]'; + } else { + method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || 'filter'; + } + + return `${method}(${key}, ${value.join('.')})`; +} + +function instrumentSupabaseClient(SupabaseClient: unknown): void { + if (instrumented.has(SupabaseClient)) { + return; + } + + instrumented.set(SupabaseClient, { + from: (SupabaseClient as unknown as SupabaseClient).prototype.from, + }); + + (SupabaseClient as unknown as SupabaseClient).prototype.from = new Proxy( + (SupabaseClient as unknown as SupabaseClient).prototype.from, + { + apply(target, thisArg, argumentsList) { + const rv = Reflect.apply(target, thisArg, argumentsList); + const PostgrestQueryBuilder = (rv as PostgrestQueryBuilder).constructor; + + instrumentPostgrestQueryBuilder(PostgrestQueryBuilder as unknown as new () => PostgrestQueryBuilder); + + return rv; + }, + }, + ); +} + +// This is the only "instrumented" part of the SDK. The rest of instrumentation +// methods are only used as a mean to get to the `PostgrestFilterBuilder` constructor itself. +function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilterBuilder['constructor']): void { + if (instrumented.has(PostgrestFilterBuilder)) { + return; + } + + instrumented.set(PostgrestFilterBuilder, { + then: ( + PostgrestFilterBuilder.prototype as unknown as { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; + } + ).then, + }); + + ( + PostgrestFilterBuilder.prototype as unknown as { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; + } + ).then = new Proxy( + ( + PostgrestFilterBuilder.prototype as unknown as { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; + } + ).then, + { + apply(target, thisArg, argumentsList) { + const operations = AVAILABLE_OPERATIONS; + const typedThis = thisArg as PostgrestFilterBuilder; + const operation = extractOperation(typedThis.method, typedThis.headers); + + if (!operations.includes(operation)) { + return Reflect.apply(target, thisArg, argumentsList); + } + + if (!typedThis?.url?.pathname || typeof typedThis.url.pathname !== 'string') { + return Reflect.apply(target, thisArg, argumentsList); + } + + const pathParts = typedThis.url.pathname.split('/'); + const table = pathParts.length > 0 ? pathParts[pathParts.length - 1] : ''; + const description = `from(${table})`; + + const query: string[] = []; + for (const [key, value] of typedThis.url.searchParams.entries()) { + // It's possible to have multiple entries for the same key, eg. `id=eq.7&id=eq.3`, + // so we need to use array instead of object to collect them. + query.push(translateFiltersIntoMethods(key, value)); + } + + const body: Record = {}; + if (isPlainObject(typedThis.body)) { + for (const [key, value] of Object.entries(typedThis.body)) { + body[key] = value; + } + } + + // TODO / Should? + const shouldCreateSpan = true; + + let span: Span | undefined; + + if (shouldCreateSpan) { + const attributes: Record = { + 'db.table': table, + 'db.schema': typedThis.schema, + 'db.url': typedThis.url.origin, + 'db.sdk': typedThis.headers['X-Client-Info'], + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, + }; + + if (query.length) { + attributes['db.query'] = query; + } + + if (Object.keys(body).length) { + attributes['db.body'] = body; + } + + span = startInactiveSpan({ + name: description, + attributes, + }); + } + + return (Reflect.apply(target, thisArg, []) as Promise) + .then( + (res: SupabaseResponse) => { + if (span) { + if (res && typeof res === 'object' && 'status' in res) { + setHttpStatus(span, res.status || 500); + } + span.end(); + } + + if (res.error) { + const err = new Error(res.error.message) as SupabaseError; + if (res.error.code) { + err.code = res.error.code; + } + if (res.error.details) { + err.details = res.error.details; + } + + const supabaseContext: Record = {}; + if (query.length) { + supabaseContext.query = query; + } + if (Object.keys(body).length) { + supabaseContext.body = body; + } + + captureException(err, { + contexts: { + supabase: supabaseContext, + }, + }); + } + + // Todo / Should? + const shouldCreateBreadcrumb = true; + + if (shouldCreateBreadcrumb) { + const breadcrumb: SupabaseBreadcrumb = { + type: 'supabase', + category: `db.${operation}`, + message: description, + }; + + const data: Record = {}; + + if (query.length) { + data.query = query; + } + + if (Object.keys(body).length) { + data.body = body; + } + + if (Object.keys(data).length) { + breadcrumb.data = data; + } + + addBreadcrumb(breadcrumb); + } + + return res; + }, + (err: Error) => { + if (span) { + setHttpStatus(span, 500); + span.end(); + } + throw err; + }, + ) + .then(...argumentsList); + }, + }, + ); +} + +function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => PostgrestQueryBuilder): void { + if (instrumented.has(PostgrestQueryBuilder)) { + return; + } + + // We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder` + // constructor, as we don't know which method will be called first, an we don't want to miss any calls. + for (const operation of AVAILABLE_OPERATIONS) { + logger.log(`Instrumenting ${operation} operation`); + + instrumented.set(PostgrestQueryBuilder, { + [operation]: (PostgrestQueryBuilder.prototype as Record)[ + operation as 'select' | 'insert' | 'upsert' | 'update' | 'delete' + ] as (...args: unknown[]) => PostgrestFilterBuilder, + }); + + type PostgrestOperation = keyof Pick; + (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation] = new Proxy( + (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation], + { + apply(target, thisArg, argumentsList) { + const rv = Reflect.apply(target, thisArg, argumentsList); + const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor; + + logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`); + + instrumentPostgrestFilterBuilder(PostgrestFilterBuilder); + + return rv; + }, + }, + ); + } +} + +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'); + } + + // We want to allow passing either `SupabaseClient` constructor + // or an instance returned from `createClient()`. + const SupabaseClient = supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor; + + instrumentSupabaseClient(SupabaseClient); +}; + +const INTEGRATION_NAME = 'Supabase'; + +const _supabaseIntegration = (({ supabaseClient } = {}) => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentSupabase(supabaseClient); + }, + }; +}) satisfies IntegrationFn; + +export const supabaseIntegration = defineIntegration((options: { supabaseClient: unknown }) => { + return { + ..._supabaseIntegration(options), + name: INTEGRATION_NAME, + }; +}) satisfies IntegrationFn; From 5b249fb697b8684a15b88f3d2d0d57eb715c202d Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 25 Mar 2025 13:29:03 +0000 Subject: [PATCH 16/53] Fix tests --- .../supabase/db-operations/test.ts | 66 +++++++++---------- .../supabase-nextjs/package.json | 2 +- packages/core/src/integrations/supabase.ts | 2 +- 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index f6f38347d711..2312ea778a40 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -4,50 +4,46 @@ import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; -describe('Supabase Integration', () => { - sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.breadcrumbs).toBeDefined(); - expect(eventData.breadcrumbs).toContainEqual({ - timestamp: expect.any(Number), - type: 'supabase', - category: 'db.insert', - message: 'from(todos)', - data: expect.any(Object), - }); +sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.breadcrumbs).toBeDefined(); + expect(eventData.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.insert', + message: 'from(todos)', + data: expect.any(Object), }); +}); - sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); +sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); - const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - expect(events).toHaveLength(2); + expect(events).toHaveLength(2); - events.forEach(event => { - expect( - event.breadcrumbs?.some( - breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.'), - ), - ).toBe(true); - }); + events.forEach(event => { + expect( + event.breadcrumbs?.some(breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.')), + ).toBe(true); }); +}); - sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); +sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); - const supaBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); + const supaBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); - expect(supaBreadcrumb).toBeDefined(); - expect(supaBreadcrumb?.data).toMatchObject({ - table: expect.any(String), - operation: expect.any(String), - timestamp: expect.any(Number), - }); + expect(supaBreadcrumb).toBeDefined(); + expect(supaBreadcrumb?.data).toMatchObject({ + table: expect.any(String), + operation: expect.any(String), + timestamp: expect.any(Number), }); }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 4af4f244b364..10053a1278dc 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -13,7 +13,7 @@ "test:assert": "pnpm test:prod" }, "dependencies": { - "@next/font": "14.2.25", + "@next/font": "14.2.15", "@sentry/nextjs": "latest || *", "@supabase/auth-helpers-react": "0.5.0", "@supabase/auth-ui-react": "0.4.7", diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 0c6deceb31e2..b0f3510bec27 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -1,4 +1,4 @@ -// eslint-disable max-lines +/* eslint-disable max-lines */ import { logger, isPlainObject } from '../utils-hoist'; import type { Span, IntegrationFn } from '../types-hoist'; From 832c4efefc39892536bfcf0f569728ed62e9543c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 25 Mar 2025 18:08:19 +0000 Subject: [PATCH 17/53] Fix tests --- .../browser-integration-tests/package.json | 1 + .../supabase/db-operations/init.js | 7 +- .../supabase/db-operations/test.ts | 34 +++++++-- .../supabase-nextjs/lib/initSupabaseAdmin.ts | 6 +- .../supabase-nextjs/lib/initSupabaseAnon.ts | 8 +- .../supabase-nextjs/pages/_app.tsx | 4 +- .../pages/api/add-todo-entry.ts | 8 +- .../pages/api/create-test-user.ts | 8 +- packages/core/src/integrations/supabase.ts | 2 +- yarn.lock | 74 +++++++++++++++++++ 10 files changed, 119 insertions(+), 33 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index f388c9d07a02..8b4f011e2f56 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -43,6 +43,7 @@ "@playwright/test": "~1.50.0", "@sentry-internal/rrweb": "2.34.0", "@sentry/browser": "9.12.0", + "@supabase/supabase-js": "2.49.3", "axios": "1.8.2", "babel-loader": "^8.2.2", "fflate": "0.8.2", diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js index ed56ce0ec989..08023f1a38a0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/browser'; -import { createClient } from '@supabase/supabase-js'; - +import { createClient } from '@supabase/supabase-js'; window.Sentry = Sentry; const supabase = createClient( @@ -12,9 +11,7 @@ const supabase = createClient( Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ - new Sentry.BrowserTracing({ - tracingOrigins: ['localhost', 'my.supabase.co'], - }), + Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabase) ], tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 2312ea778a40..68651e47f9f5 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -1,10 +1,26 @@ -import { expect } from '@playwright/test'; +import { Page, expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; +async function mockSupabaseRoute(page: Page) { + await page.route('**/rest/v1/todos**', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); +} + sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { + await mockSupabaseRoute(page); + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); @@ -20,6 +36,8 @@ sentryTest('should capture Supabase database operation breadcrumbs', async ({ ge }); sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { + await mockSupabaseRoute(page); + const url = await getLocalTestUrl({ testDir: __dirname }); const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); @@ -34,16 +52,18 @@ sentryTest('should capture multiple Supabase operations in sequence', async ({ g }); sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { + await mockSupabaseRoute(page); + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); - const supaBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); + const supabaseBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); - expect(supaBreadcrumb).toBeDefined(); - expect(supaBreadcrumb?.data).toMatchObject({ - table: expect.any(String), - operation: expect.any(String), - timestamp: expect.any(Number), + expect(supabaseBreadcrumb).toBeDefined(); + expect(supabaseBreadcrumb?.data).toMatchObject({ + query: expect.arrayContaining([ + "filter(columns, )" + ]), }); }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index 051f856975bc..b5381132cf32 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -6,7 +6,7 @@ const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'; -export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { +export const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false, autoRefreshToken: false, @@ -15,7 +15,5 @@ export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ }); Sentry.addIntegration( - Sentry.supabaseIntegration({ - supabaseClient: supabase, - }), + Sentry.supabaseIntegration(supabaseClient), ); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 0b92c9ae2e78..3e4f1b1cf7ac 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -6,10 +6,6 @@ const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; const NEXT_PUBLIC_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4cGxvcmV0ZXN0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2'; -export const supabase = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); +export const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); -Sentry.addIntegration( - Sentry.supabaseIntegration({ - supabaseClient: supabase, - }), -); +Sentry.addIntegration(Sentry.supabaseIntegration(supabaseClient)); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx index 50aba0f261b7..3405946d0a8d 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx @@ -1,10 +1,10 @@ -import { supabase } from '@/lib/initSupabaseAnon' +import { supabaseClient } from '@/lib/initSupabaseAnon' import { SessionContextProvider } from '@supabase/auth-helpers-react' import type { AppProps } from 'next/app' export default function App({ Component, pageProps }: AppProps) { return ( - + ) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts index af5c96eb8321..6d49348cf886 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabase } from '@/lib/initSupabaseAdmin'; +import { supabaseClient } from '@/lib/initSupabaseAdmin'; type Data = { data: any; @@ -8,7 +8,7 @@ type Data = { }; async function login() { - const { data, error } = await supabase.auth.signInWithPassword({ + const { data, error } = await supabaseClient.auth.signInWithPassword({ email: 'test@sentry.test', password: 'sentry.test', }); @@ -21,7 +21,7 @@ async function login() { } async function addTodoEntry(userId?: string) { - const { error } = await supabase.from('todos').insert({ task: 'test', user_id: userId }).select().single(); + const { error } = await supabaseClient.from('todos').insert({ task: 'test', user_id: userId }).select().single(); if (error) { console.log('error', error); @@ -33,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await addTodoEntry(user?.id); - const { data, error } = await supabase.from('todos').select('*'); + const { data, error } = await supabaseClient.from('todos').select('*'); if (error) { console.log('error', error); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts index f4d2143a37b2..996d3d0ffdb6 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabase } from '@/lib/initSupabaseAdmin'; +import { supabaseClient } from '@/lib/initSupabaseAdmin'; type Data = { data: any; @@ -8,10 +8,10 @@ type Data = { }; async function deleteExistingUsers() { - const { data: { users }, error } = await supabase.auth.admin.listUsers() + const { data: { users }, error } = await supabaseClient.auth.admin.listUsers() for (const user of users) { - const { error } = await supabase.auth.admin.deleteUser(user.id, true); + const { error } = await supabaseClient.auth.admin.deleteUser(user.id, true); if (error) console.log('error', error); } } @@ -19,7 +19,7 @@ async function deleteExistingUsers() { export default async function handler(req: NextApiRequest, res: NextApiResponse) { await deleteExistingUsers(); - const { data, error } = await supabase.auth.admin.createUser({ + const { data, error } = await supabaseClient.auth.admin.createUser({ email: 'test@sentry.test', password: 'sentry.test', email_confirm: true, diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index b0f3510bec27..f884b9d86827 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -416,7 +416,7 @@ const instrumentSupabase = (supabaseClient: unknown): void => { const INTEGRATION_NAME = 'Supabase'; -const _supabaseIntegration = (({ supabaseClient } = {}) => { +const _supabaseIntegration = ((supabaseClient) => { return { name: INTEGRATION_NAME, setupOnce() { diff --git a/yarn.lock b/yarn.lock index 9160308494b2..009367c064d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -89,6 +89,11 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== +"@adobe/css-tools@^4.4.0": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" + integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== + "@ai-sdk/provider-utils@2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.0.2.tgz#ea9d510be442b38bd40ae50dbf5b64ffc396952b" @@ -7216,6 +7221,63 @@ dependencies: "@testing-library/dom" "^9.3.1" +"@supabase/auth-js@2.69.1": + version "2.69.1" + resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.69.1.tgz#fcf310d24dfab823ffbf22191e6ceaef933360d8" + integrity sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/functions-js@2.4.4": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.4.tgz#45fcd94d546bdfa66d01f93a796ca0304ec154b8" + integrity sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14": + version "2.6.15" + resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c" + integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ== + dependencies: + whatwg-url "^5.0.0" + +"@supabase/postgrest-js@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz#cb721860fefd9ec2818bbafc56de4314c0ebca81" + integrity sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/realtime-js@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.11.2.tgz#7f7399c326be717eadc9d5e259f9e2690fbf83dd" + integrity sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@types/phoenix" "^1.5.4" + "@types/ws" "^8.5.10" + ws "^8.18.0" + +"@supabase/storage-js@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.7.1.tgz#761482f237deec98a59e5af1ace18c7a5e0a69af" + integrity sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/supabase-js@2.49.3": + version "2.49.3" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.49.3.tgz#789b01074b9e62ea6e41657ad65b3c06610ea3c5" + integrity sha512-42imTuAm9VEQGlXT0O6zrSwNnsIblU1eieqrAWj8HSmFaYkxepk/IuUVw1M5hKelk0ZYlqDKNwRErI1rF1EL4w== + dependencies: + "@supabase/auth-js" "2.69.1" + "@supabase/functions-js" "2.4.4" + "@supabase/node-fetch" "2.6.15" + "@supabase/postgrest-js" "1.19.2" + "@supabase/realtime-js" "2.11.2" + "@supabase/storage-js" "2.7.1" + "@sveltejs/kit@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-2.0.2.tgz#bd02523fe570ddaf89148bffb1eb2233c458054b" @@ -8047,6 +8109,11 @@ pg-protocol "*" pg-types "^2.2.0" +"@types/phoenix@^1.5.4": + version "1.6.6" + resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.6.tgz#3c1ab53fd5a23634b8e37ea72ccacbf07fbc7816" + integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A== + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -8263,6 +8330,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.5.10": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.0.tgz#8a2ec491d6f0685ceaab9a9b7ff44146236993b5" + integrity sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^5.48.0": version "5.48.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" From fa8199e2119473d010449e471dece1871b811d44 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 25 Mar 2025 18:24:27 +0000 Subject: [PATCH 18/53] Fix formatting --- .../suites/integrations/supabase/db-operations/test.ts | 5 +++-- packages/core/src/integrations/supabase.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 68651e47f9f5..cd1cd8c979ab 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -1,4 +1,5 @@ -import { Page, expect } from '@playwright/test'; +import type { Page} from '@playwright/test'; +import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -63,7 +64,7 @@ sentryTest('should include correct data payload in Supabase breadcrumbs', async expect(supabaseBreadcrumb).toBeDefined(); expect(supabaseBreadcrumb?.data).toMatchObject({ query: expect.arrayContaining([ - "filter(columns, )" + 'filter(columns, )' ]), }); }); diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index f884b9d86827..3bab57f82220 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -416,7 +416,7 @@ const instrumentSupabase = (supabaseClient: unknown): void => { const INTEGRATION_NAME = 'Supabase'; -const _supabaseIntegration = ((supabaseClient) => { +const _supabaseIntegration = (supabaseClient => { return { name: INTEGRATION_NAME, setupOnce() { From 51230166f94478a9e87155c81646abb65f86da46 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 25 Mar 2025 19:24:12 +0000 Subject: [PATCH 19/53] Dedupe dependencies. --- yarn.lock | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 009367c064d7..96049acb3a91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -89,11 +89,6 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== -"@adobe/css-tools@^4.4.0": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" - integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== - "@ai-sdk/provider-utils@2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.0.2.tgz#ea9d510be442b38bd40ae50dbf5b64ffc396952b" @@ -8323,14 +8318,14 @@ dependencies: "@types/webidl-conversions" "*" -"@types/ws@*", "@types/ws@^8.5.1": +"@types/ws@*": version "8.18.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" -"@types/ws@^8.5.10": +"@types/ws@^8.5.1", "@types/ws@^8.5.10": version "8.18.0" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.0.tgz#8a2ec491d6f0685ceaab9a9b7ff44146236993b5" integrity sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw== From 0e335bd8441aec3b540e0ee2e6090567ac639c0c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 13:10:47 +0000 Subject: [PATCH 20/53] Skip tests on non-tracing bundles --- .../integrations/supabase/db-operations/test.ts | 14 +++++++++++++- packages/core/src/integrations/supabase.ts | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index cd1cd8c979ab..55a208512940 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; async function mockSupabaseRoute(page: Page) { await page.route('**/rest/v1/todos**', route => { @@ -20,6 +20,10 @@ async function mockSupabaseRoute(page: Page) { } sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -37,6 +41,10 @@ sentryTest('should capture Supabase database operation breadcrumbs', async ({ ge }); sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -53,6 +61,10 @@ sentryTest('should capture multiple Supabase operations in sequence', async ({ g }); sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 3bab57f82220..3c1e266fff87 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -425,9 +425,9 @@ const _supabaseIntegration = (supabaseClient => { }; }) satisfies IntegrationFn; -export const supabaseIntegration = defineIntegration((options: { supabaseClient: unknown }) => { +export const supabaseIntegration = defineIntegration((supabaseClient: unknown) => { return { - ..._supabaseIntegration(options), + ..._supabaseIntegration(supabaseClient), name: INTEGRATION_NAME, }; }) satisfies IntegrationFn; From eeaa8a2eb379ae551e4c5aecb5edc06336f4719f Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 13:56:14 +0000 Subject: [PATCH 21/53] Remove test-debug mode --- .../e2e-tests/test-applications/supabase-nextjs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 10053a1278dc..639bb6e64698 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -8,7 +8,7 @@ "start": "next start", "clean": "npx rimraf node_modules pnpm-lock.yaml .next", "start-local-supabase": "supabase init --force --workdir . && supabase start -o env", - "test:prod": "TEST_ENV=production playwright test --ui", + "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm start-local-supabase && pnpm build", "test:assert": "pnpm test:prod" }, From adc5e1058f18336aaf285b6932a9c0ba3db8d2a1 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 14:17:10 +0000 Subject: [PATCH 22/53] Try reducing bundle size --- packages/core/src/integrations/supabase.ts | 23 ++-------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 3c1e266fff87..a4723b87e0c2 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { logger, isPlainObject } from '../utils-hoist'; +import { isPlainObject } from '../utils-hoist'; import type { Span, IntegrationFn } from '../types-hoist'; import { setHttpStatus, startInactiveSpan } from '../tracing'; @@ -57,20 +57,10 @@ export interface SupabaseBreadcrumb { export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; export const FILTER_MAPPINGS = { - eq: 'eq', - neq: 'neq', - gt: 'gt', - gte: 'gte', - lt: 'lt', - lte: 'lte', - like: 'like', 'like(all)': 'likeAllOf', 'like(any)': 'likeAnyOf', - ilike: 'ilike', 'ilike(all)': 'ilikeAllOf', 'ilike(any)': 'ilikeAnyOf', - is: 'is', - in: 'in', cs: 'contains', cd: 'containedBy', sr: 'rangeGt', @@ -79,11 +69,6 @@ export const FILTER_MAPPINGS = { nxr: 'rangeLte', adj: 'rangeAdjacent', ov: 'overlaps', - fts: '', - plfts: 'plain', - phfts: 'phrase', - wfts: 'websearch', - not: 'not', }; const instrumented = new Map(); @@ -150,7 +135,7 @@ export function translateFiltersIntoMethods(key: string, query: string): string } else if (filter?.startsWith('wfts')) { method = 'textSearch[websearch]'; } else { - method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || 'filter'; + method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || filter || 'filter'; } return `${method}(${key}, ${value.join('.')})`; @@ -360,8 +345,6 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr // We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder` // constructor, as we don't know which method will be called first, an we don't want to miss any calls. for (const operation of AVAILABLE_OPERATIONS) { - logger.log(`Instrumenting ${operation} operation`); - instrumented.set(PostgrestQueryBuilder, { [operation]: (PostgrestQueryBuilder.prototype as Record)[ operation as 'select' | 'insert' | 'upsert' | 'update' | 'delete' @@ -376,8 +359,6 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr const rv = Reflect.apply(target, thisArg, argumentsList); const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor; - logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`); - instrumentPostgrestFilterBuilder(PostgrestFilterBuilder); return rv; From cad1352493c39df2acd1366b7e25aa647b6db7e1 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 14:38:32 +0000 Subject: [PATCH 23/53] Remove `supabaseIntegration` from non-Tracing bundles --- packages/browser/src/exports.ts | 1 - packages/browser/src/index.bundle.tracing.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 534ef7197691..8745e34106f4 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -60,7 +60,6 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, - supabaseIntegration, updateSpanName, } from '@sentry/core'; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index d540ff0bd6f9..06d533b51e0e 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -15,6 +15,7 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, + supabaseIntegration, } from '@sentry/core'; export { From f0d66ce8e25dbd197a5b188cf2a56499868f2d14 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 15:19:41 +0000 Subject: [PATCH 24/53] Bring filter-mappings back. --- packages/core/src/integrations/supabase.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index a4723b87e0c2..c393ead9985f 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { isPlainObject } from '../utils-hoist'; +import { logger, isPlainObject } from '../utils-hoist'; import type { Span, IntegrationFn } from '../types-hoist'; import { setHttpStatus, startInactiveSpan } from '../tracing'; @@ -57,10 +57,20 @@ export interface SupabaseBreadcrumb { export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; export const FILTER_MAPPINGS = { + eq: 'eq', + neq: 'neq', + gt: 'gt', + gte: 'gte', + lt: 'lt', + lte: 'lte', + like: 'like', 'like(all)': 'likeAllOf', 'like(any)': 'likeAnyOf', + ilike: 'ilike', 'ilike(all)': 'ilikeAllOf', 'ilike(any)': 'ilikeAnyOf', + is: 'is', + in: 'in', cs: 'contains', cd: 'containedBy', sr: 'rangeGt', @@ -69,6 +79,11 @@ export const FILTER_MAPPINGS = { nxr: 'rangeLte', adj: 'rangeAdjacent', ov: 'overlaps', + fts: '', + plfts: 'plain', + phfts: 'phrase', + wfts: 'websearch', + not: 'not', }; const instrumented = new Map(); @@ -135,7 +150,7 @@ export function translateFiltersIntoMethods(key: string, query: string): string } else if (filter?.startsWith('wfts')) { method = 'textSearch[websearch]'; } else { - method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || filter || 'filter'; + method = (filter && FILTER_MAPPINGS[filter as keyof typeof FILTER_MAPPINGS]) || 'filter'; } return `${method}(${key}, ${value.join('.')})`; @@ -359,6 +374,8 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr const rv = Reflect.apply(target, thisArg, argumentsList); const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor; + logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`); + instrumentPostgrestFilterBuilder(PostgrestFilterBuilder); return rv; From 5cb5f586a6df000beacc5b11524b066573007af5 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 15:40:22 +0000 Subject: [PATCH 25/53] Export supabase from all tracing bundles --- packages/browser/src/index.bundle.tracing.replay.feedback.ts | 1 + packages/browser/src/index.bundle.tracing.replay.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index a16f07bafaf2..fab33b2726f2 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -15,6 +15,7 @@ export { getSpanDescendants, setMeasurement, captureFeedback, + supabaseIntegration, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 37f0da34ae25..9cadf4cb42f7 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -14,6 +14,7 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, + supabaseIntegration, } from '@sentry/core'; export { From 16e4a9dc8627ce6bc21605e877d6a24b8345d971 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 26 Mar 2025 21:26:04 +0000 Subject: [PATCH 26/53] Add vendor license --- packages/core/src/integrations/supabase.ts | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index c393ead9985f..589911da2769 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -1,3 +1,28 @@ +// Ported from 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'; From 5f67b4acf39ecc362de355f6ee1e42a62064beb8 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 27 Mar 2025 10:54:46 +0000 Subject: [PATCH 27/53] Clean up --- packages/core/src/integrations/supabase.ts | 84 ++++++++++------------ 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 589911da2769..7a26feba600c 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -26,7 +26,7 @@ /* eslint-disable max-lines */ import { logger, isPlainObject } from '../utils-hoist'; -import type { Span, IntegrationFn } from '../types-hoist'; +import type { IntegrationFn } from '../types-hoist'; import { setHttpStatus, startInactiveSpan } from '../tracing'; import { addBreadcrumb } from '../breadcrumbs'; import { defineIntegration } from '../integration'; @@ -271,35 +271,28 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte } } - // TODO / Should? - const shouldCreateSpan = true; - - let span: Span | undefined; - - if (shouldCreateSpan) { - const attributes: Record = { - 'db.table': table, - 'db.schema': typedThis.schema, - 'db.url': typedThis.url.origin, - 'db.sdk': typedThis.headers['X-Client-Info'], - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, - }; - - if (query.length) { - attributes['db.query'] = query; - } - - if (Object.keys(body).length) { - attributes['db.body'] = body; - } + const attributes: Record = { + 'db.table': table, + 'db.schema': typedThis.schema, + 'db.url': typedThis.url.origin, + 'db.sdk': typedThis.headers['X-Client-Info'], + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, + }; + + if (query.length) { + attributes['db.query'] = query; + } - span = startInactiveSpan({ - name: description, - attributes, - }); + if (Object.keys(body).length) { + attributes['db.body'] = body; } + const span = startInactiveSpan({ + name: description, + attributes, + }); + return (Reflect.apply(target, thisArg, []) as Promise) .then( (res: SupabaseResponse) => { @@ -334,33 +327,28 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte }); } - // Todo / Should? - const shouldCreateBreadcrumb = true; - - if (shouldCreateBreadcrumb) { - const breadcrumb: SupabaseBreadcrumb = { - type: 'supabase', - category: `db.${operation}`, - message: description, - }; + const breadcrumb: SupabaseBreadcrumb = { + type: 'supabase', + category: `db.${operation}`, + message: description, + }; - const data: Record = {}; + const data: Record = {}; - if (query.length) { - data.query = query; - } - - if (Object.keys(body).length) { - data.body = body; - } + if (query.length) { + data.query = query; + } - if (Object.keys(data).length) { - breadcrumb.data = data; - } + if (Object.keys(body).length) { + data.body = body; + } - addBreadcrumb(breadcrumb); + if (Object.keys(data).length) { + breadcrumb.data = data; } + addBreadcrumb(breadcrumb); + return res; }, (err: Error) => { From 318f0a56f92884c0e69f365eefe78c5ce8d019e0 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 27 Mar 2025 16:52:09 +0000 Subject: [PATCH 28/53] Add `auth` support --- .../suites/integrations/supabase/auth/init.js | 34 ++++ .../suites/integrations/supabase/auth/test.ts | 81 ++++++++++ .../supabase/db-operations/test.ts | 2 +- packages/core/src/integrations/supabase.ts | 152 +++++++++++------- 4 files changed, 214 insertions(+), 55 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js new file mode 100644 index 000000000000..7ade1ab16b2c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -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://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.supabaseIntegration(supabase) + ], + tracesSampleRate: 1.0, +}); + +// Simulate authentication operations +async function performAuthenticationOperations() { + try { + await supabase.auth.signInWithPassword({ + email: 'test@example.com', + password: 'test-password', + }); + + await supabase.auth.signOut(); + } catch (error) { + Sentry.captureException(error); + } +} + +performAuthenticationOperations(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts new file mode 100644 index 000000000000..00ea343b55eb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -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(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', + }), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 55a208512940..76abb4881aec 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -1,4 +1,4 @@ -import type { Page} from '@playwright/test'; +import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 7a26feba600c..c54118314c8d 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -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; +type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; +type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; + +export interface SupabaseClientInstance { + auth: { + admin: Record; + } & Record; +} + 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); + } + }); +} + +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'; From 1acde43c1600794c428f7aeb397b835ac2d5f433 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 27 Mar 2025 17:44:35 +0000 Subject: [PATCH 29/53] Add `auth` error capturing --- .../suites/integrations/supabase/auth/init.js | 24 +++---- .../suites/integrations/supabase/auth/test.ts | 63 ++++++++++++++++++- packages/core/src/integrations/supabase.ts | 45 ++++++++----- 3 files changed, 99 insertions(+), 33 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js index 7ade1ab16b2c..a88c4cec54f3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -3,32 +3,22 @@ import * as Sentry from '@sentry/browser'; import { createClient } from '@supabase/supabase-js'; window.Sentry = Sentry; -const supabase = createClient( - 'https://test.supabase.co', - 'test-key' -); +const supabase = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.supabaseIntegration(supabase) - ], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabase)], tracesSampleRate: 1.0, }); // Simulate authentication operations async function performAuthenticationOperations() { - try { - await supabase.auth.signInWithPassword({ - email: 'test@example.com', - password: 'test-password', - }); + await supabase.auth.signInWithPassword({ + email: 'test@example.com', + password: 'test-password', + }); - await supabase.auth.signOut(); - } catch (error) { - Sentry.captureException(error); - } + await supabase.auth.signOut(); } performAuthenticationOperations(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index 00ea343b55eb..494039b64136 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -9,7 +9,7 @@ import { shouldSkipTracingTest, } from '../../../../utils/helpers'; -async function mockSupabaseAuthRoutes(page: Page) { +async function mockSupabaseAuthRoutesSuccess(page: Page) { await page.route('**/auth/v1/token?grant_type=password**', route => { return route.fulfill({ status: 200, @@ -38,12 +38,40 @@ async function mockSupabaseAuthRoutes(page: Page) { }); } +async function mockSupabaseAuthRoutesFailure(page: Page) { + await page.route('**/auth/v1/token?grant_type=password**', route => { + return route.fulfill({ + status: 400, + body: JSON.stringify({ + error_description: 'Invalid email or password', + error: 'invalid_grant', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + await page.route('**/auth/v1/logout**', route => { + return route.fulfill({ + status: 400, + body: JSON.stringify({ + error_description: 'Invalid refresh token', + error: 'invalid_grant', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); +} + sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { return; } - await mockSupabaseAuthRoutes(page); + await mockSupabaseAuthRoutesSuccess(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -79,3 +107,34 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest }), }); }); + +sentryTest('should capture Supabase authentication errors', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + + await mockSupabaseAuthRoutesFailure(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db.supabase.auth')); + + expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password'); + + expect(supabaseSpans).toHaveLength(2); + expect(supabaseSpans![0]).toMatchObject({ + description: 'signInWithPassword', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + status: 'unknown_error', + data: expect.objectContaining({ + 'sentry.op': 'db.supabase.auth.signInWithPassword', + 'sentry.origin': 'auto.db.supabase', + }), + }); +}); diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index c54118314c8d..536dc5977f0d 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -196,7 +196,7 @@ export function translateFiltersIntoMethods(key: string, query: string): string function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn { return new Proxy(operation, { apply(target, thisArg, argumentsList) { - startSpan( + return startSpan( { name: operation.name, attributes: { @@ -204,19 +204,36 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A [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) { + async span => { + return Reflect.apply(target, thisArg, argumentsList) + .then((res: unknown) => { + if (res && typeof res === 'object' && 'error' in res && res.error) { + span.setStatus({ code: SPAN_STATUS_ERROR }); + + captureException(res.error, { + mechanism: { + handled: false, + }, + }); + } else { + span.setStatus({ code: SPAN_STATUS_OK }); + } + + span.end(); + return res; + }) + .catch((err: unknown) => { span.setStatus({ code: SPAN_STATUS_ERROR }); - } else { - span.setStatus({ code: SPAN_STATUS_OK }); - } - - span.end(); - debugger; - return res; - }); + span.end(); + + captureException(err, { + mechanism: { + handled: false, + }, + }); + + throw err; + }); }, ); }, @@ -460,7 +477,7 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr const instrumentSupabase = (supabaseClientInstance: unknown): void => { if (!supabaseClientInstance) { - throw new Error('Supabase client instance is not defined.'); + throw new Error('Supabase client instance is not available.'); } const SupabaseClientConstructor = supabaseClientInstance.constructor === Function ? supabaseClientInstance : supabaseClientInstance.constructor; From e01ce65579ace775d646ebe9e4635f9ff744d3db Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 27 Mar 2025 18:25:06 +0000 Subject: [PATCH 30/53] Remove `signOut` from `admin` operations --- packages/core/src/integrations/supabase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 536dc5977f0d..6062b7b0aacf 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -38,7 +38,6 @@ const AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT = [ 'getUserById', 'updateUserById', 'inviteUserByEmail', - 'signOut', ]; type AuthOperationFn = (...args: unknown[]) => Promise; From 75353868a578fafe993536f05c77ac962738d131 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 31 Mar 2025 11:43:44 +0100 Subject: [PATCH 31/53] Clean up --- packages/core/src/integrations/supabase.ts | 114 +++++++++------------ 1 file changed, 49 insertions(+), 65 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 6062b7b0aacf..85ca8a50b6ed 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -12,12 +12,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from ' import { captureException } from '../exports'; import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '../tracing'; -export interface SupabaseClientConstructor { - prototype: { - from: (table: string) => PostgrestQueryBuilder; - }; -} - const AUTH_OPERATIONS_TO_INSTRUMENT = [ 'reauthenticate', 'signInAnonymously', @@ -40,9 +34,43 @@ const AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT = [ 'inviteUserByEmail', ]; +export const FILTER_MAPPINGS = { + eq: 'eq', + neq: 'neq', + gt: 'gt', + gte: 'gte', + lt: 'lt', + lte: 'lte', + like: 'like', + 'like(all)': 'likeAllOf', + 'like(any)': 'likeAnyOf', + ilike: 'ilike', + 'ilike(all)': 'ilikeAllOf', + 'ilike(any)': 'ilikeAnyOf', + is: 'is', + in: 'in', + cs: 'contains', + cd: 'containedBy', + sr: 'rangeGt', + nxl: 'rangeGte', + sl: 'rangeLt', + nxr: 'rangeLte', + adj: 'rangeAdjacent', + ov: 'overlaps', + fts: '', + plfts: 'plain', + phfts: 'phrase', + wfts: 'websearch', + not: 'not', +}; + +export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; + type AuthOperationFn = (...args: unknown[]) => Promise; type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; +type PostgrestQueryOperationName = (typeof AVAILABLE_OPERATIONS)[number]; +type PostgrestQueryOperationFn = (...args: unknown[]) => PostgrestFilterBuilder; export interface SupabaseClientInstance { auth: { @@ -51,11 +79,7 @@ export interface SupabaseClientInstance { } export interface PostgrestQueryBuilder { - select: (...args: unknown[]) => PostgrestFilterBuilder; - insert: (...args: unknown[]) => PostgrestFilterBuilder; - upsert: (...args: unknown[]) => PostgrestFilterBuilder; - update: (...args: unknown[]) => PostgrestFilterBuilder; - delete: (...args: unknown[]) => PostgrestFilterBuilder; + [key: PostgrestQueryOperationName]: PostgrestQueryOperationFn; } export interface PostgrestFilterBuilder { @@ -90,37 +114,18 @@ export interface SupabaseBreadcrumb { }; } -export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; +export interface SupabaseClientConstructor { + prototype: { + from: (table: string) => PostgrestQueryBuilder; + }; +} -export const FILTER_MAPPINGS = { - eq: 'eq', - neq: 'neq', - gt: 'gt', - gte: 'gte', - lt: 'lt', - lte: 'lte', - like: 'like', - 'like(all)': 'likeAllOf', - 'like(any)': 'likeAnyOf', - ilike: 'ilike', - 'ilike(all)': 'ilikeAllOf', - 'ilike(any)': 'ilikeAnyOf', - is: 'is', - in: 'in', - cs: 'contains', - cd: 'containedBy', - sr: 'rangeGt', - nxl: 'rangeGte', - sl: 'rangeLt', - nxr: 'rangeLte', - adj: 'rangeAdjacent', - ov: 'overlaps', - fts: '', - plfts: 'plain', - phfts: 'phrase', - wfts: 'websearch', - not: 'not', -}; +export interface PostgrestProtoThenable { + then: ( + onfulfilled?: ((value: T) => T | PromiseLike) | null, + onrejected?: ((reason: any) => T | PromiseLike) | null, + ) => Promise; +} const instrumented = new Map(); @@ -289,32 +294,11 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte } instrumented.set(PostgrestFilterBuilder, { - then: ( - PostgrestFilterBuilder.prototype as unknown as { - then: ( - onfulfilled?: ((value: T) => T | PromiseLike) | null, - onrejected?: ((reason: any) => T | PromiseLike) | null, - ) => Promise; - } - ).then, + then: (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then, }); - ( - PostgrestFilterBuilder.prototype as unknown as { - then: ( - onfulfilled?: ((value: T) => T | PromiseLike) | null, - onrejected?: ((reason: any) => T | PromiseLike) | null, - ) => Promise; - } - ).then = new Proxy( - ( - PostgrestFilterBuilder.prototype as unknown as { - then: ( - onfulfilled?: ((value: T) => T | PromiseLike) | null, - onrejected?: ((reason: any) => T | PromiseLike) | null, - ) => Promise; - } - ).then, + (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then = new Proxy( + (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then, { apply(target, thisArg, argumentsList) { const operations = AVAILABLE_OPERATIONS; From 0532f8ac55f554ed2f51e69dfbdc5cbe6aa177b6 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 3 Apr 2025 14:17:15 +0100 Subject: [PATCH 32/53] Address review comments --- .../supabase-nextjs/lib/initSupabaseAdmin.ts | 22 ++- .../supabase-nextjs/lib/initSupabaseAnon.ts | 7 +- .../supabase-nextjs/package.json | 2 +- .../supabase-nextjs/pages/_app.tsx | 4 +- .../pages/api/add-todo-entry.ts | 5 +- .../pages/api/create-test-user.ts | 22 +-- .../supabase-nextjs/pages/api/list-users.ts | 23 +++ .../supabase-nextjs/sentry.server.config.ts | 2 +- .../supabase-nextjs/supabase/seed.sql | 2 + .../tests/client.performance.test.ts | 70 ------- .../supabase-nextjs/tests/performance.test.ts | 177 ++++++++++++++++++ .../tests/server.performance.test.ts | 57 ------ packages/core/src/integrations/supabase.ts | 105 ++++++----- 13 files changed, 292 insertions(+), 206 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/seed.sql delete mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index b5381132cf32..5abca2cdad97 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -6,14 +6,16 @@ const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'; -export const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { - auth: { - persistSession: false, - autoRefreshToken: false, - detectSessionInUrl: false, - }, -}); +export const getSupabaseClient = () => { + const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + }, + }); -Sentry.addIntegration( - Sentry.supabaseIntegration(supabaseClient), -); + Sentry.addIntegration(Sentry.supabaseIntegration(supabaseClient)); + + return supabaseClient; +}; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 3e4f1b1cf7ac..203205c23489 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -6,6 +6,9 @@ const NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321'; const NEXT_PUBLIC_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4cGxvcmV0ZXN0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2'; -export const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); +export const getSupabaseClient = () => { + const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); + Sentry.addIntegration(Sentry.supabaseIntegration(supabaseClient)); -Sentry.addIntegration(Sentry.supabaseIntegration(supabaseClient)); + return supabaseClient; +}; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index 639bb6e64698..a46519e9c75d 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "clean": "npx rimraf node_modules pnpm-lock.yaml .next", - "start-local-supabase": "supabase init --force --workdir . && supabase start -o env", + "start-local-supabase": "supabase init --force --workdir . && supabase start -o env && supabase db reset", "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm start-local-supabase && pnpm build", "test:assert": "pnpm test:prod" diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx index 3405946d0a8d..b3d470023b6e 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/_app.tsx @@ -1,7 +1,9 @@ -import { supabaseClient } from '@/lib/initSupabaseAnon' +import { getSupabaseClient } from '@/lib/initSupabaseAnon' import { SessionContextProvider } from '@supabase/auth-helpers-react' import type { AppProps } from 'next/app' +const supabaseClient = getSupabaseClient() + export default function App({ Component, pageProps }: AppProps) { return ( diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts index 6d49348cf886..e75cac13fc4c 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/add-todo-entry.ts @@ -1,12 +1,13 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabaseClient } from '@/lib/initSupabaseAdmin'; +import { getSupabaseClient } from '@/lib/initSupabaseAdmin'; type Data = { data: any; error: any; }; +const supabaseClient = getSupabaseClient(); + async function login() { const { data, error } = await supabaseClient.auth.signInWithPassword({ email: 'test@sentry.test', diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts index 996d3d0ffdb6..732930ce4369 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts @@ -1,30 +1,28 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabaseClient } from '@/lib/initSupabaseAdmin'; +import { getSupabaseClient } from '@/lib/initSupabaseAdmin'; +import * as Sentry from '@sentry/nextjs'; type Data = { data: any; error: any; }; -async function deleteExistingUsers() { - const { data: { users }, error } = await supabaseClient.auth.admin.listUsers() - - for (const user of users) { - const { error } = await supabaseClient.auth.admin.deleteUser(user.id, true); - if (error) console.log('error', error); - } -} +const supabaseClient = getSupabaseClient(); export default async function handler(req: NextApiRequest, res: NextApiResponse) { - await deleteExistingUsers(); - + // Note for test usage + // This only works once in tests as it will error if the user already exists + // So this should be called only once before all tests to create the user const { data, error } = await supabaseClient.auth.admin.createUser({ email: 'test@sentry.test', password: 'sentry.test', email_confirm: true, }); + if (error) { + console.warn('ERROR', error); + } + res.status(200).json({ data, error, diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts new file mode 100644 index 000000000000..ccf859aeb9bb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts @@ -0,0 +1,23 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getSupabaseClient } from '@/lib/initSupabaseAdmin'; +import * as Sentry from '@sentry/nextjs'; + +type Data = { + data: any; + error: any; +}; + +const supabaseClient = getSupabaseClient(); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { data, error } = await supabaseClient.auth.admin.listUsers(); + + if (error) { + console.warn('ERROR', error); + } + + res.status(200).json({ + data, + error, + }); +} diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts index aa1d2031926f..a9966e3a71a5 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts @@ -2,7 +2,7 @@ // The config you add here will be used whenever the server handles a request. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from "@sentry/nextjs"; +import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/seed.sql b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/seed.sql new file mode 100644 index 000000000000..57b5c4d07e05 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/seed.sql @@ -0,0 +1,2 @@ +TRUNCATE auth.users CASCADE; +TRUNCATE auth.identities CASCADE; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts deleted file mode 100644 index 529412e54b2f..000000000000 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/client.performance.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', async ({ page, baseURL }) => { - // Create test user - await fetch(`${baseURL}/api/create-test-user`); - - const pageloadTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { - return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; - }); - - await page.goto('/'); - - // Fill in login credentials - await page.locator('input[name=email]').fill('test@sentry.test'); - await page.locator('input[name=password]').fill('sentry.test'); - await page.locator('button[type=submit]').click(); - - // Wait for login to complete - await page.waitForSelector('button:has-text("Add")'); - - // Add a new todo entry - await page.locator('input[id=new-task-text]').fill('test'); - await page.locator('button[id=add-task]').click(); - - const transactionEvent = await pageloadTransactionPromise; - - expect(transactionEvent.spans).toContainEqual( - expect.objectContaining({ - description: 'from(todos)', - op: 'db.select', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.db.supabase', - }), - ); - - expect(transactionEvent.spans).toContainEqual({ - data: expect.any(Object), - description: 'from(todos)', - op: 'db.insert', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.db.supabase', - }); - - expect(transactionEvent.breadcrumbs).toContainEqual({ - timestamp: expect.any(Number), - type: 'supabase', - category: 'db.select', - message: 'from(todos)', - data: expect.any(Object), - }); - - expect(transactionEvent.breadcrumbs).toContainEqual({ - timestamp: expect.any(Number), - type: 'supabase', - category: 'db.insert', - message: 'from(todos)', - data: expect.any(Object), - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts new file mode 100644 index 000000000000..4a6782d7fe5b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts @@ -0,0 +1,177 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// This test should be run in serial mode to ensure that the test user is created before the other tests +test.describe.configure({ mode: 'serial' }); + +// This should be the first test as it will be needed for the other tests +test('Sends server-side Supabase auth admin `createUser` span', async ({ page, baseURL }) => { + const httpTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /api/create-test-user' + ); + }); + + await fetch(`${baseURL}/api/create-test-user`); + const transactionEvent = await httpTransactionPromise; + + expect(transactionEvent.spans).toContainEqual({ + data: expect.any(Object), + description: 'createUser', + op: 'db.supabase.auth.admin.createUser', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }); +}); + +test('Sends client-side Supabase db-operation spans and breadcrumbs to Sentry', async ({ page, baseURL }) => { + const pageloadTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + // Fill in login credentials + // The email and password should be the same as the ones used in the `create-test-user` endpoint + await page.locator('input[name=email]').fill('test@sentry.test'); + await page.locator('input[name=password]').fill('sentry.test'); + await page.locator('button[type=submit]').click(); + + // Wait for login to complete + await page.waitForSelector('button:has-text("Add")'); + + // Add a new todo entry + await page.locator('input[id=new-task-text]').fill('test'); + await page.locator('button[id=add-task]').click(); + + const transactionEvent = await pageloadTransactionPromise; + + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'from(todos)', + op: 'db.select', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }), + ); + + expect(transactionEvent.spans).toContainEqual({ + data: expect.any(Object), + description: 'from(todos)', + op: 'db.insert', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.select', + message: 'from(todos)', + data: expect.any(Object), + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.insert', + message: 'from(todos)', + data: expect.any(Object), + }); +}); + +test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', async ({ page, baseURL }) => { + const httpTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /api/add-todo-entry' + ); + }); + + await fetch(`${baseURL}/api/add-todo-entry`); + const transactionEvent = await httpTransactionPromise; + + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'from(todos)', + op: 'db.select', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }), + ); + + expect(transactionEvent.spans).toContainEqual({ + data: expect.any(Object), + description: 'from(todos)', + op: 'db.insert', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.select', + message: 'from(todos)', + data: expect.any(Object), + }); + + expect(transactionEvent.breadcrumbs).toContainEqual({ + timestamp: expect.any(Number), + type: 'supabase', + category: 'db.insert', + message: 'from(todos)', + data: expect.any(Object), + }); +}); + +test('Sends server-side Supabase auth admin `listUsers` span', async ({ page, baseURL }) => { + const httpTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /api/list-users' + ); + }); + + await fetch(`${baseURL}/api/list-users`); + const transactionEvent = await httpTransactionPromise; + + expect(transactionEvent.spans).toContainEqual({ + data: expect.any(Object), + description: 'listUsers', + op: 'db.supabase.auth.admin.listUsers', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.db.supabase', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts deleted file mode 100644 index 887d87e6ce8f..000000000000 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/server.performance.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends server-side Supabase db-operation spans and breadcrumbs to Sentry', async ({ page, baseURL }) => { - // Create test user - await fetch(`${baseURL}/api/create-test-user`); - - const httpTransactionPromise = waitForTransaction('supabase-nextjs', transactionEvent => { - return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /api/add-todo-entry'; - }); - - await fetch(`${baseURL}/api/add-todo-entry`); - const transactionEvent = await httpTransactionPromise; - - expect(transactionEvent.spans).toContainEqual( - expect.objectContaining({ - description: 'from(todos)', - op: 'db.select', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.db.supabase', - }), - ); - - expect(transactionEvent.spans).toContainEqual({ - data: expect.any(Object), - description: 'from(todos)', - op: 'db.insert', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.db.supabase', - }); - - expect(transactionEvent.breadcrumbs).toContainEqual({ - timestamp: expect.any(Number), - type: 'supabase', - category: 'db.select', - message: 'from(todos)', - data: expect.any(Object), - }); - - expect(transactionEvent.breadcrumbs).toContainEqual({ - timestamp: expect.any(Number), - type: 'supabase', - category: 'db.insert', - message: 'from(todos)', - data: expect.any(Object), - }); -}); diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 85ca8a50b6ed..49380703e809 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -5,7 +5,7 @@ import { logger, isPlainObject } from '../utils-hoist'; import type { IntegrationFn } from '../types-hoist'; -import { setHttpStatus, startInactiveSpan, startSpan } from '../tracing'; +import { setHttpStatus, startInactiveSpan } from '../tracing'; import { addBreadcrumb } from '../breadcrumbs'; import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; @@ -198,48 +198,52 @@ export function translateFiltersIntoMethods(key: string, query: string): string } function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn { + if (instrumented.has(operation)) { + return operation; + } + return new Proxy(operation, { apply(target, thisArg, argumentsList) { - return startSpan( - { - name: operation.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, - }, - }, - async span => { - return Reflect.apply(target, thisArg, argumentsList) - .then((res: unknown) => { - if (res && typeof res === 'object' && 'error' in res && res.error) { - span.setStatus({ code: SPAN_STATUS_ERROR }); - - captureException(res.error, { - mechanism: { - handled: false, - }, - }); - } else { - span.setStatus({ code: SPAN_STATUS_OK }); - } + instrumented.set(operation, true); - span.end(); - return res; - }) - .catch((err: unknown) => { - span.setStatus({ code: SPAN_STATUS_ERROR }); - span.end(); + const span = startInactiveSpan({ + name: operation.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, + }, + }); - captureException(err, { - mechanism: { - handled: false, - }, - }); + return Reflect.apply(target, thisArg, argumentsList) + .then((res: unknown) => { + if (res && typeof res === 'object' && 'error' in res && res.error) { + span.setStatus({ code: SPAN_STATUS_ERROR }); - throw err; + captureException(res.error, { + mechanism: { + handled: false, + }, }); - }, - ); + } else { + span.setStatus({ code: SPAN_STATUS_OK }); + } + + span.end(); + return res; + }) + .catch((err: unknown) => { + span.setStatus({ code: SPAN_STATUS_ERROR }); + span.end(); + + captureException(err, { + mechanism: { + handled: false, + }, + }); + + throw err; + }) + .then(...argumentsList); }, }); } @@ -254,14 +258,14 @@ function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInst AUTH_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthOperationName) => { const authOperation = auth[operation]; if (typeof authOperation === 'function') { - auth[operation] = instrumentAuthOperation(authOperation); + supabaseClientInstance.auth[operation] = instrumentAuthOperation(authOperation); } }); AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthAdminOperationName) => { const authAdminOperation = auth.admin[operation]; if (typeof authAdminOperation === 'function') { - auth.admin[operation] = instrumentAuthOperation(authAdminOperation); + supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authAdminOperation, true); } }); } @@ -317,11 +321,11 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte const table = pathParts.length > 0 ? pathParts[pathParts.length - 1] : ''; const description = `from(${table})`; - const query: string[] = []; + const queryItems: string[] = []; for (const [key, value] of typedThis.url.searchParams.entries()) { // It's possible to have multiple entries for the same key, eg. `id=eq.7&id=eq.3`, // so we need to use array instead of object to collect them. - query.push(translateFiltersIntoMethods(key, value)); + queryItems.push(translateFiltersIntoMethods(key, value)); } const body: Record = {}; @@ -340,8 +344,8 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, }; - if (query.length) { - attributes['db.query'] = query; + if (queryItems.length) { + attributes['db.query'] = queryItems; } if (Object.keys(body).length) { @@ -373,8 +377,8 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte } const supabaseContext: Record = {}; - if (query.length) { - supabaseContext.query = query; + if (queryItems.length) { + supabaseContext.query = queryItems; } if (Object.keys(body).length) { supabaseContext.body = body; @@ -395,8 +399,8 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte const data: Record = {}; - if (query.length) { - data.query = query; + if (queryItems.length) { + data.query = queryItems; } if (Object.keys(body).length) { @@ -472,11 +476,12 @@ const instrumentSupabase = (supabaseClientInstance: unknown): void => { const INTEGRATION_NAME = 'Supabase'; const _supabaseIntegration = (supabaseClient => { + // Instrumenting here instead of `setup` or `setupOnce` because we may need to instrument multiple clients. + // So we don't want the instrumentation is skipped because the integration is already installed. + instrumentSupabase(supabaseClient); + return { name: INTEGRATION_NAME, - setupOnce() { - instrumentSupabase(supabaseClient); - }, }; }) satisfies IntegrationFn; From 0ea2cdb921e348eca7a25dcbcc4a11e8150c6094 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 3 Apr 2025 14:22:30 +0100 Subject: [PATCH 33/53] Update packages/core/src/integrations/supabase.ts Co-authored-by: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> --- packages/core/src/integrations/supabase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 49380703e809..349ff8ece19a 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -435,7 +435,7 @@ function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => Postgr } // We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder` - // constructor, as we don't know which method will be called first, an we don't want to miss any calls. + // constructor, as we don't know which method will be called first, and we don't want to miss any calls. for (const operation of AVAILABLE_OPERATIONS) { instrumented.set(PostgrestQueryBuilder, { [operation]: (PostgrestQueryBuilder.prototype as Record)[ From af31dcfaa4b7b73920ed2a1d130ab6a28a2edf95 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 3 Apr 2025 14:23:33 +0100 Subject: [PATCH 34/53] Remove README.md --- .../e2e-tests/test-applications/supabase-nextjs/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/supabase-nextjs/README.md diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/README.md b/dev-packages/e2e-tests/test-applications/supabase-nextjs/README.md deleted file mode 100644 index e69de29bb2d1..000000000000 From ace4036abe8570d40e22534fe581c12513e5d2b0 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 10 Apr 2025 13:01:18 +0100 Subject: [PATCH 35/53] Mark SupabaseConstructor objects as instrumented and check for rewrapping --- .../pages/api/create-test-user.ts | 1 - .../supabase-nextjs/pages/api/list-users.ts | 2 +- .../supabase-nextjs/supabase/config.toml | 3 +- packages/core/src/integrations/supabase.ts | 137 ++++++++++-------- 4 files changed, 81 insertions(+), 62 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts index 732930ce4369..57b0c210afa8 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/create-test-user.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { getSupabaseClient } from '@/lib/initSupabaseAdmin'; -import * as Sentry from '@sentry/nextjs'; type Data = { data: any; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts index ccf859aeb9bb..9d6e14f2e921 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { getSupabaseClient } from '@/lib/initSupabaseAdmin'; -import * as Sentry from '@sentry/nextjs'; type Data = { data: any; @@ -10,6 +9,7 @@ type Data = { const supabaseClient = getSupabaseClient(); export default async function handler(req: NextApiRequest, res: NextApiResponse) { + console.debug("###############################3", supabaseClient.auth.admin.listUsers) const { data, error } = await supabaseClient.auth.admin.listUsers(); if (error) { diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml index 35dcff35bec4..09a2fc658b27 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml @@ -141,7 +141,6 @@ sign_in_sign_ups = 30 # Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. token_verifications = 30 - # Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. # [auth.captcha] # enabled = true @@ -283,6 +282,8 @@ enabled = true policy = "oneshot" # Port to attach the Chrome inspector for debugging edge functions. inspector_port = 8083 +# The Deno major version to use. +deno_version = 1 # [edge_runtime.secrets] # secret_key = "env(SECRET_VALUE)" diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 349ff8ece19a..64208a8f15f1 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -11,6 +11,7 @@ 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'; +import { DEBUG_BUILD } from '../debug-build'; const AUTH_OPERATIONS_TO_INSTRUMENT = [ 'reauthenticate', @@ -64,13 +65,13 @@ export const FILTER_MAPPINGS = { not: 'not', }; -export const AVAILABLE_OPERATIONS = ['select', 'insert', 'upsert', 'update', 'delete']; +export const DB_OPERATIONS_TO_INSTRUMENT = ['select', 'insert', 'upsert', 'update', 'delete']; type AuthOperationFn = (...args: unknown[]) => Promise; type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; -type PostgrestQueryOperationName = (typeof AVAILABLE_OPERATIONS)[number]; -type PostgrestQueryOperationFn = (...args: unknown[]) => PostgrestFilterBuilder; +type PostgRESTQueryOperationName = (typeof DB_OPERATIONS_TO_INSTRUMENT)[number]; +type PostgRESTQueryOperationFn = (...args: unknown[]) => PostgRESTFilterBuilder; export interface SupabaseClientInstance { auth: { @@ -78,11 +79,11 @@ export interface SupabaseClientInstance { } & Record; } -export interface PostgrestQueryBuilder { - [key: PostgrestQueryOperationName]: PostgrestQueryOperationFn; +export interface PostgRESTQueryBuilder { + [key: PostgRESTQueryOperationName]: PostgRESTQueryOperationFn; } -export interface PostgrestFilterBuilder { +export interface PostgRESTFilterBuilder { method: string; headers: Record; url: URL; @@ -116,18 +117,36 @@ export interface SupabaseBreadcrumb { export interface SupabaseClientConstructor { prototype: { - from: (table: string) => PostgrestQueryBuilder; + from: (table: string) => PostgRESTQueryBuilder; }; } -export interface PostgrestProtoThenable { +export interface PostgRESTProtoThenable { then: ( onfulfilled?: ((value: T) => T | PromiseLike) | null, onrejected?: ((reason: any) => T | PromiseLike) | null, ) => Promise; } -const instrumented = new Map(); +type SentryInstrumented = T & { + __SENTRY_INSTRUMENTED__?: boolean; +}; + +function markAsInstrumented(fn: T): void { + try { + (fn as SentryInstrumented).__SENTRY_INSTRUMENTED__ = true; + } catch { + // ignore errors here + } +} + +function isInstrumented(fn: T): boolean | undefined { + try { + return (fn as SentryInstrumented).__SENTRY_INSTRUMENTED__; + } catch { + return false; + } +} /** * Extracts the database operation type from the HTTP method and headers @@ -198,14 +217,8 @@ export function translateFiltersIntoMethods(key: string, query: string): string } function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn { - if (instrumented.has(operation)) { - return operation; - } - return new Proxy(operation, { apply(target, thisArg, argumentsList) { - instrumented.set(operation, true); - const span = startInactiveSpan({ name: operation.name, attributes: { @@ -255,23 +268,34 @@ function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInst return; } - AUTH_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthOperationName) => { + + for (const operation of AUTH_OPERATIONS_TO_INSTRUMENT) { const authOperation = auth[operation]; - if (typeof authOperation === 'function') { + + if (!authOperation) { + continue; + } + + if ( typeof supabaseClientInstance.auth[operation] === 'function') { supabaseClientInstance.auth[operation] = instrumentAuthOperation(authOperation); } - }); + } - AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT.forEach((operation: AuthAdminOperationName) => { - const authAdminOperation = auth.admin[operation]; - if (typeof authAdminOperation === 'function') { - supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authAdminOperation, true); + for (const operation of AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT) { + const authOperation = auth.admin[operation]; + + if (!authOperation) { + continue; } - }); + + if (typeof supabaseClientInstance.auth.admin[operation] === 'function') { + supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authOperation, true); + } + } } function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void { - if (instrumented.has(SupabaseClient)) { + if (isInstrumented((SupabaseClient as unknown as SupabaseClientConstructor).prototype.from)) { return; } @@ -280,33 +304,29 @@ function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void { { apply(target, thisArg, argumentsList) { const rv = Reflect.apply(target, thisArg, argumentsList); - const PostgrestQueryBuilder = (rv as PostgrestQueryBuilder).constructor; + const PostgRESTQueryBuilder = (rv as PostgRESTQueryBuilder).constructor; - instrumentPostgrestQueryBuilder(PostgrestQueryBuilder as unknown as new () => PostgrestQueryBuilder); + instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder as unknown as new () => PostgRESTQueryBuilder); return rv; }, }, ); + + markAsInstrumented((SupabaseClient as unknown as SupabaseClientConstructor).prototype.from); } -// This is the only "instrumented" part of the SDK. The rest of instrumentation -// methods are only used as a mean to get to the `PostgrestFilterBuilder` constructor itself. -function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilterBuilder['constructor']): void { - if (instrumented.has(PostgrestFilterBuilder)) { +function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilterBuilder['constructor']): void { + if (isInstrumented((PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then)) { return; } - instrumented.set(PostgrestFilterBuilder, { - then: (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then, - }); - - (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then = new Proxy( - (PostgrestFilterBuilder.prototype as unknown as PostgrestProtoThenable).then, + (PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then = new Proxy( + (PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then, { apply(target, thisArg, argumentsList) { - const operations = AVAILABLE_OPERATIONS; - const typedThis = thisArg as PostgrestFilterBuilder; + const operations = DB_OPERATIONS_TO_INSTRUMENT; + const typedThis = thisArg as PostgRESTFilterBuilder; const operation = extractOperation(typedThis.method, typedThis.headers); if (!operations.includes(operation)) { @@ -427,44 +447,43 @@ function instrumentPostgrestFilterBuilder(PostgrestFilterBuilder: PostgrestFilte }, }, ); -} -function instrumentPostgrestQueryBuilder(PostgrestQueryBuilder: new () => PostgrestQueryBuilder): void { - if (instrumented.has(PostgrestQueryBuilder)) { - return; - } + markAsInstrumented((PostgRESTFilterBuilder.prototype as unknown as PostgRESTProtoThenable).then); +} - // We need to wrap _all_ operations despite them sharing the same `PostgrestFilterBuilder` +function instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder: new () => PostgRESTQueryBuilder): void { + // We need to wrap _all_ operations despite them sharing the same `PostgRESTFilterBuilder` // constructor, as we don't know which method will be called first, and we don't want to miss any calls. - for (const operation of AVAILABLE_OPERATIONS) { - instrumented.set(PostgrestQueryBuilder, { - [operation]: (PostgrestQueryBuilder.prototype as Record)[ - operation as 'select' | 'insert' | 'upsert' | 'update' | 'delete' - ] as (...args: unknown[]) => PostgrestFilterBuilder, - }); - - type PostgrestOperation = keyof Pick; - (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation] = new Proxy( - (PostgrestQueryBuilder.prototype as Record)[operation as PostgrestOperation], + for (const operation of DB_OPERATIONS_TO_INSTRUMENT) { + if (isInstrumented((PostgRESTQueryBuilder.prototype as Record)[operation])) { + continue; + } + + type PostgRESTOperation = keyof Pick; + (PostgRESTQueryBuilder.prototype as Record)[operation as PostgRESTOperation] = new Proxy( + (PostgRESTQueryBuilder.prototype as Record)[operation as PostgRESTOperation], { apply(target, thisArg, argumentsList) { const rv = Reflect.apply(target, thisArg, argumentsList); - const PostgrestFilterBuilder = (rv as PostgrestFilterBuilder).constructor; + const PostgRESTFilterBuilder = (rv as PostgRESTFilterBuilder).constructor; - logger.log(`Instrumenting ${operation} operation's PostgrestFilterBuilder`); + DEBUG_BUILD && logger.log(`Instrumenting ${operation} operation's PostgRESTFilterBuilder`); - instrumentPostgrestFilterBuilder(PostgrestFilterBuilder); + instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder); return rv; }, }, ); + + markAsInstrumented((PostgRESTQueryBuilder.prototype as Record)[operation]); } } const instrumentSupabase = (supabaseClientInstance: unknown): void => { if (!supabaseClientInstance) { - throw new Error('Supabase client instance is not available.'); + DEBUG_BUILD && logger.warn('Supabase integration was not installed because no Supabase client was provided.'); + return; } const SupabaseClientConstructor = supabaseClientInstance.constructor === Function ? supabaseClientInstance : supabaseClientInstance.constructor; @@ -477,7 +496,7 @@ const INTEGRATION_NAME = 'Supabase'; const _supabaseIntegration = (supabaseClient => { // Instrumenting here instead of `setup` or `setupOnce` because we may need to instrument multiple clients. - // So we don't want the instrumentation is skipped because the integration is already installed. + // So we don't want the instrumentation skipped because the integration is already installed. instrumentSupabase(supabaseClient); return { From 116fca537f46ae8e9fbe8c7c2ccbcbd7880d3247 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 10 Apr 2025 13:01:56 +0100 Subject: [PATCH 36/53] Update packages/core/src/integrations/supabase.ts Co-authored-by: Luca Forstner --- packages/core/src/integrations/supabase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 64208a8f15f1..0dc72cfad7d6 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -348,7 +348,7 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte queryItems.push(translateFiltersIntoMethods(key, value)); } - const body: Record = {}; + const body: Record = Object.create(null); if (isPlainObject(typedThis.body)) { for (const [key, value] of Object.entries(typedThis.body)) { body[key] = value; From 68b04a44ad7e68f732350b11b42db1e026da653d Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 10 Apr 2025 18:54:19 +0100 Subject: [PATCH 37/53] Expose `instrumentSupabase` --- .../suites/integrations/supabase/auth/init.js | 10 +- .../supabase/db-operations/init.js | 18 +- .../supabase-nextjs/lib/initSupabaseAdmin.ts | 2 +- .../supabase-nextjs/lib/initSupabaseAnon.ts | 3 +- .../supabase-nextjs/pages/api/list-users.ts | 1 - .../supabase-nextjs/sentry.client.config.ts | 4 +- .../supabase-nextjs/sentry.edge.config.ts | 4 +- .../supabase-nextjs/sentry.server.config.ts | 4 +- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + .../index.bundle.tracing.replay.feedback.ts | 1 + .../src/index.bundle.tracing.replay.ts | 1 + packages/browser/src/index.bundle.tracing.ts | 1 + packages/browser/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/cloudflare/src/index.ts | 1 + packages/core/src/index.ts | 2 +- packages/core/src/integrations/supabase.ts | 251 +++++++++--------- packages/deno/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/cloudflare/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/sveltekit/src/worker/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 27 files changed, 163 insertions(+), 153 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js index a88c4cec54f3..0281337cf996 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -3,22 +3,24 @@ import * as Sentry from '@sentry/browser'; import { createClient } from '@supabase/supabase-js'; window.Sentry = Sentry; -const supabase = createClient('https://test.supabase.co', 'test-key'); +const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabase)], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ + supabaseClient + })], tracesSampleRate: 1.0, }); // Simulate authentication operations async function performAuthenticationOperations() { - await supabase.auth.signInWithPassword({ + await supabaseClient.auth.signInWithPassword({ email: 'test@example.com', password: 'test-password', }); - await supabase.auth.signOut(); + await supabaseClient.auth.signOut(); } performAuthenticationOperations(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js index 08023f1a38a0..fbb60cd104c3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -3,30 +3,20 @@ import * as Sentry from '@sentry/browser'; import { createClient } from '@supabase/supabase-js'; window.Sentry = Sentry; -const supabase = createClient( - 'https://test.supabase.co', - 'test-key' -); +const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.supabaseIntegration(supabase) - ], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, }); // Simulate database operations async function performDatabaseOperations() { try { - await supabase - .from('todos') - .insert([{ title: 'Test Todo' }]); + await supabaseClient.from('todos').insert([{ title: 'Test Todo' }]); - await supabase - .from('todos') - .select('*'); + await supabaseClient.from('todos').select('*'); // Trigger an error to capture the breadcrumbs throw new Error('Test Error'); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index 5abca2cdad97..fbb38c3e0012 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -15,7 +15,7 @@ export const getSupabaseClient = () => { }, }); - Sentry.addIntegration(Sentry.supabaseIntegration(supabaseClient)); + Sentry.instrumentSupabase({ supabaseClient }); return supabaseClient; }; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 203205c23489..2c539ca64d0a 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -8,7 +8,8 @@ const NEXT_PUBLIC_SUPABASE_ANON_KEY = export const getSupabaseClient = () => { const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); - Sentry.addIntegration(Sentry.supabaseIntegration(supabaseClient)); + + Sentry.instrumentSupabase({ supabaseClient }); return supabaseClient; }; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts index 9d6e14f2e921..fdbfbc8328a1 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/pages/api/list-users.ts @@ -9,7 +9,6 @@ type Data = { const supabaseClient = getSupabaseClient(); export default async function handler(req: NextApiRequest, res: NextApiResponse) { - console.debug("###############################3", supabaseClient.auth.admin.listUsers) const { data, error } = await supabaseClient.auth.admin.listUsers(); if (error) { diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts index acd2f0768675..693c7efcd087 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts @@ -5,14 +5,14 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', + dsn: 'https://f183f54e7d7d4f018dddaaade1f8481a@o447951.ingest.us.sentry.io/5659328', environment: 'qa', // dynamic sampling bias to keep transactions // Add optional integrations for additional features integrations: [ Sentry.replayIntegration(), ], - tunnel: 'http://localhost:3031/', // proxy server + // tunnel: 'http://localhost:3031/', // proxy server // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. tracesSampleRate: 1, diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts index 59ad9eb6befe..4c7b189a13d4 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts @@ -5,11 +5,11 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', + dsn: 'https://f183f54e7d7d4f018dddaaade1f8481a@o447951.ingest.us.sentry.io/5659328', environment: 'qa', // dynamic sampling bias to keep transactions sendDefaultPii: true, tracesSampleRate: 1, - tunnel: 'http://localhost:3031/', // proxy server + // tunnel: 'http://localhost:3031/', // proxy server transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts index a9966e3a71a5..1fd9b69fc5d3 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts @@ -5,11 +5,11 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', + dsn: 'https://f183f54e7d7d4f018dddaaade1f8481a@o447951.ingest.us.sentry.io/5659328', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1, sendDefaultPii: true, - tunnel: 'http://localhost:3031/', // proxy server + // tunnel: 'http://localhost:3031/', // proxy server transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 0de7ab9b897a..b9b702ad7de3 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -126,6 +126,7 @@ export { withMonitor, withScope, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, profiler, logger, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 1299eafd982b..8c8af61e5cf3 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -110,6 +110,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index fab33b2726f2..06e5f4396958 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -16,6 +16,7 @@ export { setMeasurement, captureFeedback, supabaseIntegration, + instrumentSupabase } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 9cadf4cb42f7..c425f094bf71 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -15,6 +15,7 @@ export { getSpanDescendants, setMeasurement, supabaseIntegration, + instrumentSupabase } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index 06d533b51e0e..48ad645b439d 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -16,6 +16,7 @@ export { getSpanDescendants, setMeasurement, supabaseIntegration, + instrumentSupabase } from '@sentry/core'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 1a5c090a55ec..c8b2f209a35b 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -60,6 +60,7 @@ export { makeMultiplexedTransport, moduleMetadataIntegration, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, thirdPartyErrorFilterIntegration, } from '@sentry/core'; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 05433871d541..e7ab960231be 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -129,6 +129,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 1129020e27bf..8618ff0d6371 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -76,6 +76,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f9fe2b47d370..544dff026a09 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -106,7 +106,7 @@ export { captureConsoleIntegration } from './integrations/captureconsole'; export { dedupeIntegration } from './integrations/dedupe'; export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; -export { supabaseIntegration } from './integrations/supabase'; +export { supabaseIntegration, instrumentSupabase } from './integrations/supabase'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 0dc72cfad7d6..15b4e225662a 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -5,7 +5,7 @@ import { logger, isPlainObject } from '../utils-hoist'; import type { IntegrationFn } from '../types-hoist'; -import { setHttpStatus, startInactiveSpan } from '../tracing'; +import { setHttpStatus, startSpan } from '../tracing'; import { addBreadcrumb } from '../breadcrumbs'; import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; @@ -219,44 +219,47 @@ export function translateFiltersIntoMethods(key: string, query: string): string function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): AuthOperationFn { return new Proxy(operation, { apply(target, thisArg, argumentsList) { - const span = startInactiveSpan({ - name: operation.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, + return startSpan( + { + name: operation.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, + }, }, - }); - - return Reflect.apply(target, thisArg, argumentsList) - .then((res: unknown) => { - if (res && typeof res === 'object' && 'error' in res && res.error) { - span.setStatus({ code: SPAN_STATUS_ERROR }); - - captureException(res.error, { - mechanism: { - handled: false, - }, - }); - } else { - span.setStatus({ code: SPAN_STATUS_OK }); - } + span => { + return Reflect.apply(target, thisArg, argumentsList) + .then((res: unknown) => { + if (res && typeof res === 'object' && 'error' in res && res.error) { + span.setStatus({ code: SPAN_STATUS_ERROR }); + + captureException(res.error, { + mechanism: { + handled: false, + }, + }); + } else { + span.setStatus({ code: SPAN_STATUS_OK }); + } - span.end(); - return res; - }) - .catch((err: unknown) => { - span.setStatus({ code: SPAN_STATUS_ERROR }); - span.end(); - - captureException(err, { - mechanism: { - handled: false, - }, - }); - - throw err; - }) - .then(...argumentsList); + span.end(); + return res; + }) + .catch((err: unknown) => { + span.setStatus({ code: SPAN_STATUS_ERROR }); + span.end(); + + captureException(err, { + mechanism: { + handled: false, + }, + }); + + throw err; + }) + .then(...argumentsList); + }, + ); }, }); } @@ -268,7 +271,6 @@ function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInst return; } - for (const operation of AUTH_OPERATIONS_TO_INSTRUMENT) { const authOperation = auth[operation]; @@ -276,7 +278,7 @@ function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInst continue; } - if ( typeof supabaseClientInstance.auth[operation] === 'function') { + if (typeof supabaseClientInstance.auth[operation] === 'function') { supabaseClientInstance.auth[operation] = instrumentAuthOperation(authOperation); } } @@ -372,78 +374,81 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte attributes['db.body'] = body; } - const span = startInactiveSpan({ - name: description, - attributes, - }); - - return (Reflect.apply(target, thisArg, []) as Promise) - .then( - (res: SupabaseResponse) => { - if (span) { - if (res && typeof res === 'object' && 'status' in res) { - setHttpStatus(span, res.status || 500); - } - span.end(); - } - - if (res.error) { - const err = new Error(res.error.message) as SupabaseError; - if (res.error.code) { - err.code = res.error.code; - } - if (res.error.details) { - err.details = res.error.details; - } - - const supabaseContext: Record = {}; - if (queryItems.length) { - supabaseContext.query = queryItems; - } - if (Object.keys(body).length) { - supabaseContext.body = body; - } - - captureException(err, { - contexts: { - supabase: supabaseContext, - }, - }); - } - - const breadcrumb: SupabaseBreadcrumb = { - type: 'supabase', - category: `db.${operation}`, - message: description, - }; - - const data: Record = {}; - - if (queryItems.length) { - data.query = queryItems; - } - - if (Object.keys(body).length) { - data.body = body; - } - - if (Object.keys(data).length) { - breadcrumb.data = data; - } - - addBreadcrumb(breadcrumb); - - return res; - }, - (err: Error) => { - if (span) { - setHttpStatus(span, 500); - span.end(); - } - throw err; - }, - ) - .then(...argumentsList); + return startSpan( + { + name: description, + attributes, + }, + span => { + return (Reflect.apply(target, thisArg, []) as Promise) + .then( + (res: SupabaseResponse) => { + if (span) { + if (res && typeof res === 'object' && 'status' in res) { + setHttpStatus(span, res.status || 500); + } + span.end(); + } + + if (res.error) { + const err = new Error(res.error.message) as SupabaseError; + if (res.error.code) { + err.code = res.error.code; + } + if (res.error.details) { + err.details = res.error.details; + } + + const supabaseContext: Record = {}; + if (queryItems.length) { + supabaseContext.query = queryItems; + } + if (Object.keys(body).length) { + supabaseContext.body = body; + } + + captureException(err, { + contexts: { + supabase: supabaseContext, + }, + }); + } + + const breadcrumb: SupabaseBreadcrumb = { + type: 'supabase', + category: `db.${operation}`, + message: description, + }; + + const data: Record = {}; + + if (queryItems.length) { + data.query = queryItems; + } + + if (Object.keys(body).length) { + data.body = body; + } + + if (Object.keys(data).length) { + breadcrumb.data = data; + } + + addBreadcrumb(breadcrumb); + + return res; + }, + (err: Error) => { + if (span) { + setHttpStatus(span, 500); + span.end(); + } + throw err; + }, + ) + .then(...argumentsList); + }, + ); }, }, ); @@ -480,33 +485,29 @@ function instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder: new () => PostgR } } -const instrumentSupabase = (supabaseClientInstance: unknown): void => { - if (!supabaseClientInstance) { +export const instrumentSupabase = (options: { supabaseClient: unknown }): void => { + if (!options?.supabaseClient) { DEBUG_BUILD && logger.warn('Supabase integration was not installed because no Supabase client was provided.'); return; } const SupabaseClientConstructor = - supabaseClientInstance.constructor === Function ? supabaseClientInstance : supabaseClientInstance.constructor; + options.supabaseClient.constructor === Function ? options.supabaseClient : options.supabaseClient.constructor; instrumentSupabaseClientConstructor(SupabaseClientConstructor); - instrumentSupabaseAuthClient(supabaseClientInstance as SupabaseClientInstance); + instrumentSupabaseAuthClient(options.supabaseClient as SupabaseClientInstance); }; const INTEGRATION_NAME = 'Supabase'; -const _supabaseIntegration = (supabaseClient => { - // Instrumenting here instead of `setup` or `setupOnce` because we may need to instrument multiple clients. - // So we don't want the instrumentation skipped because the integration is already installed. - instrumentSupabase(supabaseClient); - +const _supabaseIntegration = ((options: { supabaseClient: unknown }) => { return { + setupOnce() { + instrumentSupabase(options); + }, name: INTEGRATION_NAME, }; }) satisfies IntegrationFn; -export const supabaseIntegration = defineIntegration((supabaseClient: unknown) => { - return { - ..._supabaseIntegration(supabaseClient), - name: INTEGRATION_NAME, - }; +export const supabaseIntegration = defineIntegration((options: { supabaseClient: unknown }) => { + return _supabaseIntegration(options); }) satisfies IntegrationFn; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index b25d810cd3c8..635927eb908e 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -72,6 +72,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index d1a3cc906f4e..c6e5ef3bc88c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -109,6 +109,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 8a98ff37c95b..9c75da55ea20 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -128,6 +128,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, profiler, consoleLoggingIntegration, diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 8093088a0171..5b3aca87b715 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -96,6 +96,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 92e6f95e1786..cfbbab7c6290 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -112,6 +112,7 @@ export { withMonitor, withScope, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 001f87be0f90..af9c6e3ebdb5 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -115,6 +115,7 @@ export { withMonitor, withScope, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 6a92e063b02e..55fe1dc9c6a0 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -117,6 +117,7 @@ export { withMonitor, withScope, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index ed5263ac4897..aa47ca654eee 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -79,6 +79,7 @@ export { withMonitor, withScope, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, } from '@sentry/cloudflare'; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 0d4e3f800604..4575345ae131 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -76,6 +76,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, supabaseIntegration, + instrumentSupabase, zodErrorsIntegration, consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, From affaf36f3bb5fbee23f037fa3b9f4d88ed484b25 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 10 Apr 2025 23:39:56 +0100 Subject: [PATCH 38/53] Lint --- packages/browser/src/index.bundle.tracing.replay.feedback.ts | 2 +- packages/browser/src/index.bundle.tracing.replay.ts | 2 +- packages/browser/src/index.bundle.tracing.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 06e5f4396958..94f8c719afad 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -16,7 +16,7 @@ export { setMeasurement, captureFeedback, supabaseIntegration, - instrumentSupabase + instrumentSupabase, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index c425f094bf71..2a2306fc91e8 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -15,7 +15,7 @@ export { getSpanDescendants, setMeasurement, supabaseIntegration, - instrumentSupabase + instrumentSupabase, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index 48ad645b439d..2fcc46424435 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -16,7 +16,7 @@ export { getSpanDescendants, setMeasurement, supabaseIntegration, - instrumentSupabase + instrumentSupabase, } from '@sentry/core'; export { From ba7c4e36997360e787ffc75f39be604db70e437e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 11 Apr 2025 10:59:01 +0100 Subject: [PATCH 39/53] Add `db.system` attribute --- packages/core/src/integrations/supabase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 15b4e225662a..54a8fb8adadc 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -362,6 +362,7 @@ function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder: PostgRESTFilte 'db.schema': typedThis.schema, 'db.url': typedThis.url.origin, 'db.sdk': typedThis.headers['X-Client-Info'], + 'db.system': 'postgresql', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.${operation}`, }; From 2d8ca55b8dda1409869f059c4235c26aca56bf8b Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 11 Apr 2025 10:59:21 +0100 Subject: [PATCH 40/53] Update test `dsn`s and tunnel --- .../test-applications/supabase-nextjs/sentry.client.config.ts | 4 ++-- .../test-applications/supabase-nextjs/sentry.edge.config.ts | 4 ++-- .../test-applications/supabase-nextjs/sentry.server.config.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts index 693c7efcd087..acd2f0768675 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.client.config.ts @@ -5,14 +5,14 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: 'https://f183f54e7d7d4f018dddaaade1f8481a@o447951.ingest.us.sentry.io/5659328', + dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions // Add optional integrations for additional features integrations: [ Sentry.replayIntegration(), ], - // tunnel: 'http://localhost:3031/', // proxy server + tunnel: 'http://localhost:3031/', // proxy server // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. tracesSampleRate: 1, diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts index 4c7b189a13d4..59ad9eb6befe 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.edge.config.ts @@ -5,11 +5,11 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: 'https://f183f54e7d7d4f018dddaaade1f8481a@o447951.ingest.us.sentry.io/5659328', + dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions sendDefaultPii: true, tracesSampleRate: 1, - // tunnel: 'http://localhost:3031/', // proxy server + tunnel: 'http://localhost:3031/', // proxy server transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts index 1fd9b69fc5d3..a9966e3a71a5 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts @@ -5,11 +5,11 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ - dsn: 'https://f183f54e7d7d4f018dddaaade1f8481a@o447951.ingest.us.sentry.io/5659328', + dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1, sendDefaultPii: true, - // tunnel: 'http://localhost:3031/', // proxy server + tunnel: 'http://localhost:3031/', // proxy server transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, From c6ef16f8655129ce7c8dc90811b9aaf917f78a81 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 11 Apr 2025 18:41:40 +0100 Subject: [PATCH 41/53] Update auth `op`s --- .../suites/integrations/supabase/auth/test.ts | 10 +++++----- .../supabase-nextjs/tests/performance.test.ts | 4 ++-- packages/core/src/integrations/supabase.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index 494039b64136..eb2bbcf50e13 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -76,7 +76,7 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); - const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db.supabase.auth')); + const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db.auth')); expect(supabaseSpans).toHaveLength(2); expect(supabaseSpans![0]).toMatchObject({ @@ -88,7 +88,7 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest trace_id: eventData.contexts?.trace?.trace_id, status: 'ok', data: expect.objectContaining({ - 'sentry.op': 'db.supabase.auth.signInWithPassword', + 'sentry.op': 'db.auth.signInWithPassword', 'sentry.origin': 'auto.db.supabase', }), }); @@ -102,7 +102,7 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest trace_id: eventData.contexts?.trace?.trace_id, status: 'ok', data: expect.objectContaining({ - 'sentry.op': 'db.supabase.auth.signOut', + 'sentry.op': 'db.auth.signOut', 'sentry.origin': 'auto.db.supabase', }), }); @@ -119,7 +119,7 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db.supabase.auth')); + const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db.auth')); expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password'); @@ -133,7 +133,7 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes trace_id: transactionEvent.contexts?.trace?.trace_id, status: 'unknown_error', data: expect.objectContaining({ - 'sentry.op': 'db.supabase.auth.signInWithPassword', + 'sentry.op': 'db.auth.signInWithPassword', 'sentry.origin': 'auto.db.supabase', }), }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts index 4a6782d7fe5b..80eb1a166e9b 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/tests/performance.test.ts @@ -19,7 +19,7 @@ test('Sends server-side Supabase auth admin `createUser` span', async ({ page, b expect(transactionEvent.spans).toContainEqual({ data: expect.any(Object), description: 'createUser', - op: 'db.supabase.auth.admin.createUser', + op: 'db.auth.admin.createUser', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -165,7 +165,7 @@ test('Sends server-side Supabase auth admin `listUsers` span', async ({ page, ba expect(transactionEvent.spans).toContainEqual({ data: expect.any(Object), description: 'listUsers', - op: 'db.supabase.auth.admin.listUsers', + op: 'db.auth.admin.listUsers', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 54a8fb8adadc..f24de219bc0e 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -224,11 +224,11 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A name: operation.name, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.supabase.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `db.auth.${isAdmin ? 'admin.' : ''}${operation.name}`, }, }, span => { - return Reflect.apply(target, thisArg, argumentsList) + return Reflect.apply(target, thisArg, []) .then((res: unknown) => { if (res && typeof res === 'object' && 'error' in res && res.error) { span.setStatus({ code: SPAN_STATUS_ERROR }); From d61aefe77e16e115b3f42f8b65d59b6e157d086a Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 11 Apr 2025 19:36:17 +0100 Subject: [PATCH 42/53] Dedupe deps --- yarn.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 96049acb3a91..3eb115a9370a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8318,20 +8318,13 @@ dependencies: "@types/webidl-conversions" "*" -"@types/ws@*": +"@types/ws@*", "@types/ws@^8.5.1", "@types/ws@^8.5.10": version "8.18.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" -"@types/ws@^8.5.1", "@types/ws@^8.5.10": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.0.tgz#8a2ec491d6f0685ceaab9a9b7ff44146236993b5" - integrity sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@^5.48.0": version "5.48.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" From 0c3ff1d39ddb595719b9205df67eb9db913dd559 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 11 Apr 2025 21:41:53 +0100 Subject: [PATCH 43/53] Fix empty arguments on `auth` --- packages/core/src/integrations/supabase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index f24de219bc0e..b6bba5f0a86c 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -228,7 +228,7 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A }, }, span => { - return Reflect.apply(target, thisArg, []) + return Reflect.apply(target, thisArg, argumentsList) .then((res: unknown) => { if (res && typeof res === 'object' && 'error' in res && res.error) { span.setStatus({ code: SPAN_STATUS_ERROR }); From 556703c769d9ed0f830dcc7b92ec6e4b904b8b39 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 14 Apr 2025 16:18:25 +0100 Subject: [PATCH 44/53] Mark and check `auth` as instrumented --- .../test-applications/supabase-nextjs/supabase/config.toml | 3 +-- packages/core/src/integrations/supabase.ts | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml index 09a2fc658b27..35dcff35bec4 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/supabase/config.toml @@ -141,6 +141,7 @@ sign_in_sign_ups = 30 # Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. token_verifications = 30 + # Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. # [auth.captcha] # enabled = true @@ -282,8 +283,6 @@ enabled = true policy = "oneshot" # Port to attach the Chrome inspector for debugging edge functions. inspector_port = 8083 -# The Deno major version to use. -deno_version = 1 # [edge_runtime.secrets] # secret_key = "env(SECRET_VALUE)" diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index b6bba5f0a86c..d12fb6694373 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -267,7 +267,7 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInstance): void { const auth = supabaseClientInstance.auth; - if (!auth) { + if (!auth || isInstrumented(supabaseClientInstance.auth)) { return; } @@ -294,6 +294,8 @@ function instrumentSupabaseAuthClient(supabaseClientInstance: SupabaseClientInst supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authOperation, true); } } + + markAsInstrumented(supabaseClientInstance.auth); } function instrumentSupabaseClientConstructor(SupabaseClient: unknown): void { From 5776eb7e1c8f3d838b7ee71bed0c7e6d2aabf564 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 16 Apr 2025 09:02:50 +0000 Subject: [PATCH 45/53] Rename to `instrumentSupabaseClient` with separate options --- .../suites/integrations/supabase/auth/init.js | 4 +-- .../supabase/db-operations/init.js | 2 +- .../supabase-nextjs/lib/initSupabaseAdmin.ts | 2 +- .../supabase-nextjs/lib/initSupabaseAnon.ts | 2 +- packages/astro/src/index.server.ts | 2 +- packages/aws-serverless/src/index.ts | 2 +- .../index.bundle.tracing.replay.feedback.ts | 2 +- .../src/index.bundle.tracing.replay.ts | 2 +- packages/browser/src/index.bundle.tracing.ts | 2 +- packages/browser/src/index.ts | 2 +- packages/bun/src/index.ts | 2 +- packages/cloudflare/src/index.ts | 2 +- packages/core/src/index.ts | 2 +- packages/core/src/integrations/supabase.ts | 29 +++++++++++++------ packages/deno/src/index.ts | 2 +- packages/google-cloud-serverless/src/index.ts | 2 +- packages/node/src/index.ts | 2 +- packages/remix/src/cloudflare/index.ts | 2 +- packages/remix/src/server/index.ts | 2 +- packages/solidstart/src/server/index.ts | 2 +- packages/sveltekit/src/server/index.ts | 2 +- packages/sveltekit/src/worker/index.ts | 2 +- packages/vercel-edge/src/index.ts | 2 +- 23 files changed, 42 insertions(+), 33 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js index 0281337cf996..2198c282dcfc 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -7,9 +7,7 @@ const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ - supabaseClient - })], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabaseClient)], tracesSampleRate: 1.0, }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js index fbb60cd104c3..fea3819e3e06 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -7,7 +7,7 @@ const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabaseClient)], tracesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index fbb38c3e0012..d48b315cdd08 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -15,7 +15,7 @@ export const getSupabaseClient = () => { }, }); - Sentry.instrumentSupabase({ supabaseClient }); + Sentry.instrumentSupabaseClient(supabaseClient); return supabaseClient; }; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 2c539ca64d0a..4e8ab6acc2b7 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -9,7 +9,7 @@ const NEXT_PUBLIC_SUPABASE_ANON_KEY = export const getSupabaseClient = () => { const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); - Sentry.instrumentSupabase({ supabaseClient }); + Sentry.instrumentSupabaseClient(supabaseClient); return supabaseClient; }; diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index b9b702ad7de3..ad06d131c9d6 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -126,7 +126,7 @@ export { withMonitor, withScope, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, profiler, logger, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 8c8af61e5cf3..ee8c588ac8b4 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -110,7 +110,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 94f8c719afad..21b538799521 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -16,7 +16,7 @@ export { setMeasurement, captureFeedback, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 2a2306fc91e8..118284c83569 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -15,7 +15,7 @@ export { getSpanDescendants, setMeasurement, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index 2fcc46424435..e17d633510aa 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -16,7 +16,7 @@ export { getSpanDescendants, setMeasurement, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index c8b2f209a35b..dd079cfc0241 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -60,7 +60,7 @@ export { makeMultiplexedTransport, moduleMetadataIntegration, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, thirdPartyErrorFilterIntegration, } from '@sentry/core'; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index e7ab960231be..afbb9dac0035 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -129,7 +129,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 8618ff0d6371..551929eb405a 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -76,7 +76,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 544dff026a09..b5dac82ffa54 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -106,7 +106,7 @@ export { captureConsoleIntegration } from './integrations/captureconsole'; export { dedupeIntegration } from './integrations/dedupe'; export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; -export { supabaseIntegration, instrumentSupabase } from './integrations/supabase'; +export { supabaseIntegration, instrumentSupabaseClient } from './integrations/supabase'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index d12fb6694373..55ab3cf39453 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -67,6 +67,10 @@ export const FILTER_MAPPINGS = { export const DB_OPERATIONS_TO_INSTRUMENT = ['select', 'insert', 'upsert', 'update', 'delete']; +// We may need options in the future, for now it's empty +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface SupabaseInstrumentationOptions {} + type AuthOperationFn = (...args: unknown[]) => Promise; type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; @@ -488,29 +492,36 @@ function instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder: new () => PostgR } } -export const instrumentSupabase = (options: { supabaseClient: unknown }): void => { - if (!options?.supabaseClient) { +export const instrumentSupabaseClient = ( + supabaseClient: unknown, + // In future, we may need options. For now it's unused. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options: SupabaseInstrumentationOptions = {}, +): void => { + if (!supabaseClient) { DEBUG_BUILD && logger.warn('Supabase integration was not installed because no Supabase client was provided.'); return; } const SupabaseClientConstructor = - options.supabaseClient.constructor === Function ? options.supabaseClient : options.supabaseClient.constructor; + supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor; instrumentSupabaseClientConstructor(SupabaseClientConstructor); - instrumentSupabaseAuthClient(options.supabaseClient as SupabaseClientInstance); + instrumentSupabaseAuthClient(supabaseClient as SupabaseClientInstance); }; const INTEGRATION_NAME = 'Supabase'; -const _supabaseIntegration = ((options: { supabaseClient: unknown }) => { +const _supabaseIntegration = ((supabaseClient: unknown, options: SupabaseInstrumentationOptions) => { return { setupOnce() { - instrumentSupabase(options); + instrumentSupabaseClient(supabaseClient, options); }, name: INTEGRATION_NAME, }; }) satisfies IntegrationFn; -export const supabaseIntegration = defineIntegration((options: { supabaseClient: unknown }) => { - return _supabaseIntegration(options); -}) satisfies IntegrationFn; +export const supabaseIntegration = defineIntegration( + (supabaseClient: unknown, options: SupabaseInstrumentationOptions) => { + return _supabaseIntegration(supabaseClient, options); + }, +) satisfies IntegrationFn; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 635927eb908e..dde05fca3ea4 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -72,7 +72,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index c6e5ef3bc88c..d205762c23f6 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -109,7 +109,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, profiler, amqplibIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9c75da55ea20..3579f667fbb8 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -128,7 +128,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, profiler, consoleLoggingIntegration, diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 5b3aca87b715..958376f802a3 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -96,7 +96,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index cfbbab7c6290..d2bef3f0b4f4 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -112,7 +112,7 @@ export { withMonitor, withScope, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index af9c6e3ebdb5..617ad950eab7 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -115,7 +115,7 @@ export { withMonitor, withScope, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 55fe1dc9c6a0..52e4e182300c 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -117,7 +117,7 @@ export { withMonitor, withScope, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, logger, consoleLoggingIntegration, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index aa47ca654eee..9fc8429e5864 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -79,7 +79,7 @@ export { withMonitor, withScope, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, } from '@sentry/cloudflare'; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 4575345ae131..98a83d042928 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -76,7 +76,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, supabaseIntegration, - instrumentSupabase, + instrumentSupabaseClient, zodErrorsIntegration, consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, From f081a5ddbbdd5f60d573fa7958c4905071b6ff8c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 16 Apr 2025 13:48:03 +0200 Subject: [PATCH 46/53] Remove unnecessary option --- packages/core/src/integrations/supabase.ts | 23 ++++++---------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 55ab3cf39453..872c90012d15 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -67,10 +67,6 @@ export const FILTER_MAPPINGS = { export const DB_OPERATIONS_TO_INSTRUMENT = ['select', 'insert', 'upsert', 'update', 'delete']; -// We may need options in the future, for now it's empty -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface SupabaseInstrumentationOptions {} - type AuthOperationFn = (...args: unknown[]) => Promise; type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; @@ -492,12 +488,7 @@ function instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder: new () => PostgR } } -export const instrumentSupabaseClient = ( - supabaseClient: unknown, - // In future, we may need options. For now it's unused. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options: SupabaseInstrumentationOptions = {}, -): void => { +export const instrumentSupabaseClient = (supabaseClient: unknown): void => { if (!supabaseClient) { DEBUG_BUILD && logger.warn('Supabase integration was not installed because no Supabase client was provided.'); return; @@ -511,17 +502,15 @@ export const instrumentSupabaseClient = ( const INTEGRATION_NAME = 'Supabase'; -const _supabaseIntegration = ((supabaseClient: unknown, options: SupabaseInstrumentationOptions) => { +const _supabaseIntegration = ((supabaseClient: unknown) => { return { setupOnce() { - instrumentSupabaseClient(supabaseClient, options); + instrumentSupabaseClient(supabaseClient); }, name: INTEGRATION_NAME, }; }) satisfies IntegrationFn; -export const supabaseIntegration = defineIntegration( - (supabaseClient: unknown, options: SupabaseInstrumentationOptions) => { - return _supabaseIntegration(supabaseClient, options); - }, -) satisfies IntegrationFn; +export const supabaseIntegration = defineIntegration((options: { supabaseClient: any }) => { + return _supabaseIntegration(options.supabaseClient); +}) satisfies IntegrationFn; From 975d061bb214c1788574d3239c1466ea8dad9d14 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 16 Apr 2025 14:15:45 +0200 Subject: [PATCH 47/53] tests --- .../suites/integrations/supabase/auth/init.js | 2 +- .../suites/integrations/supabase/db-operations/init.js | 2 +- .../test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts | 2 +- .../test-applications/supabase-nextjs/lib/initSupabaseAnon.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js index 2198c282dcfc..0d76a283878e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -7,7 +7,7 @@ const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabaseClient)], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js index fea3819e3e06..fbb60cd104c3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -7,7 +7,7 @@ const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration(supabaseClient)], + integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index d48b315cdd08..4c43f15676f9 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -15,7 +15,7 @@ export const getSupabaseClient = () => { }, }); - Sentry.instrumentSupabaseClient(supabaseClient); + Sentry.instrumentSupabaseClient({ supabaseClient }); return supabaseClient; }; diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 4e8ab6acc2b7..5396c616303f 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -9,7 +9,7 @@ const NEXT_PUBLIC_SUPABASE_ANON_KEY = export const getSupabaseClient = () => { const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); - Sentry.instrumentSupabaseClient(supabaseClient); + Sentry.instrumentSupabaseClient({ supabaseClient }); return supabaseClient; }; From 9c2aa4d5d5ee6c6da196af2ce4fa8119a75b494e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 16 Apr 2025 15:59:53 +0200 Subject: [PATCH 48/53] woops --- .../test-applications/supabase-nextjs/lib/initSupabaseAnon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts index 5396c616303f..4e8ab6acc2b7 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAnon.ts @@ -9,7 +9,7 @@ const NEXT_PUBLIC_SUPABASE_ANON_KEY = export const getSupabaseClient = () => { const supabaseClient = createClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY); - Sentry.instrumentSupabaseClient({ supabaseClient }); + Sentry.instrumentSupabaseClient(supabaseClient); return supabaseClient; }; From 83902e78f7be6b0e616e7fe3a4c77e27cf93d286 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Wed, 16 Apr 2025 22:05:02 +0100 Subject: [PATCH 49/53] Fix test usage --- .../test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts index 4c43f15676f9..d48b315cdd08 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/lib/initSupabaseAdmin.ts @@ -15,7 +15,7 @@ export const getSupabaseClient = () => { }, }); - Sentry.instrumentSupabaseClient({ supabaseClient }); + Sentry.instrumentSupabaseClient(supabaseClient); return supabaseClient; }; From 82043f061ac734b90a1788701e75b6985469949e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 17 Apr 2025 10:56:31 +0200 Subject: [PATCH 50/53] Don't export from browser bundles yet --- packages/browser/src/index.bundle.tracing.replay.feedback.ts | 2 -- packages/browser/src/index.bundle.tracing.replay.ts | 2 -- packages/browser/src/index.bundle.tracing.ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 21b538799521..a16f07bafaf2 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -15,8 +15,6 @@ export { getSpanDescendants, setMeasurement, captureFeedback, - supabaseIntegration, - instrumentSupabaseClient, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 118284c83569..37f0da34ae25 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -14,8 +14,6 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, - supabaseIntegration, - instrumentSupabaseClient, } from '@sentry/core'; export { diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index e17d633510aa..d540ff0bd6f9 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -15,8 +15,6 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, - supabaseIntegration, - instrumentSupabaseClient, } from '@sentry/core'; export { From d4e3d09a568ecefc174705671fafd3c4c90eb4f7 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 17 Apr 2025 10:06:05 +0100 Subject: [PATCH 51/53] Make types compatible with TS 3.x --- packages/core/src/integrations/supabase.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 872c90012d15..f6541da51aa2 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -70,7 +70,6 @@ export const DB_OPERATIONS_TO_INSTRUMENT = ['select', 'insert', 'upsert', 'updat type AuthOperationFn = (...args: unknown[]) => Promise; type AuthOperationName = (typeof AUTH_OPERATIONS_TO_INSTRUMENT)[number]; type AuthAdminOperationName = (typeof AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT)[number]; -type PostgRESTQueryOperationName = (typeof DB_OPERATIONS_TO_INSTRUMENT)[number]; type PostgRESTQueryOperationFn = (...args: unknown[]) => PostgRESTFilterBuilder; export interface SupabaseClientInstance { @@ -80,7 +79,7 @@ export interface SupabaseClientInstance { } export interface PostgRESTQueryBuilder { - [key: PostgRESTQueryOperationName]: PostgRESTQueryOperationFn; + [key: string]: PostgRESTQueryOperationFn; } export interface PostgRESTFilterBuilder { From 0ae2ed4935e3907a34e541a4af00dee695efb777 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 17 Apr 2025 10:55:06 +0100 Subject: [PATCH 52/53] Skip browser bundles on supabase integration tests --- .../suites/integrations/supabase/auth/test.ts | 12 ++++++++++++ .../supabase/db-operations/test.ts | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index eb2bbcf50e13..a75f32c8a012 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -71,6 +71,12 @@ sentryTest('should capture Supabase authentication spans', async ({ getLocalTest return; } + const bundle = process.env.PW_BUNDLE || ''; + // We only want to run this in non-CDN bundle mode + if (bundle.startsWith('bundle')) { + sentryTest.skip(); + } + await mockSupabaseAuthRoutesSuccess(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -113,6 +119,12 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes return; } + const bundle = process.env.PW_BUNDLE || ''; + // We only want to run this in non-CDN bundle mode + if (bundle.startsWith('bundle')) { + sentryTest.skip(); + } + await mockSupabaseAuthRoutesFailure(page); const url = await getLocalTestUrl({ testDir: __dirname }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 76abb4881aec..56daf8914d55 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -24,6 +24,12 @@ sentryTest('should capture Supabase database operation breadcrumbs', async ({ ge return; } + const bundle = process.env.PW_BUNDLE || ''; + // We only want to run this in non-CDN bundle mode + if (bundle.startsWith('bundle')) { + sentryTest.skip(); + } + await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -45,6 +51,12 @@ sentryTest('should capture multiple Supabase operations in sequence', async ({ g return; } + const bundle = process.env.PW_BUNDLE || ''; + // We only want to run this in non-CDN bundle mode + if (bundle.startsWith('bundle')) { + sentryTest.skip(); + } + await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -65,6 +77,12 @@ sentryTest('should include correct data payload in Supabase breadcrumbs', async return; } + const bundle = process.env.PW_BUNDLE || ''; + // We only want to run this in non-CDN bundle mode + if (bundle.startsWith('bundle')) { + sentryTest.skip(); + } + await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); From 2f8b5e6ceb26b1d0ed1ebe81446cd38f020c1dc2 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 17 Apr 2025 10:57:59 +0100 Subject: [PATCH 53/53] Skip bundle tests per file --- .../suites/integrations/supabase/auth/test.ts | 19 +++++--------- .../supabase/db-operations/test.ts | 26 ++++++------------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index a75f32c8a012..31277f4afe3c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -66,17 +66,18 @@ async function mockSupabaseAuthRoutesFailure(page: Page) { }); } + +const bundle = process.env.PW_BUNDLE || ''; +// We only want to run this in non-CDN bundle mode +if (bundle.startsWith('bundle')) { + sentryTest.skip(); +} + sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { return; } - const bundle = process.env.PW_BUNDLE || ''; - // We only want to run this in non-CDN bundle mode - if (bundle.startsWith('bundle')) { - sentryTest.skip(); - } - await mockSupabaseAuthRoutesSuccess(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -119,12 +120,6 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes return; } - const bundle = process.env.PW_BUNDLE || ''; - // We only want to run this in non-CDN bundle mode - if (bundle.startsWith('bundle')) { - sentryTest.skip(); - } - await mockSupabaseAuthRoutesFailure(page); const url = await getLocalTestUrl({ testDir: __dirname }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 56daf8914d55..cb9fe0430228 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -19,17 +19,19 @@ async function mockSupabaseRoute(page: Page) { }); } + +const bundle = process.env.PW_BUNDLE || ''; +// We only want to run this in non-CDN bundle mode +if (bundle.startsWith('bundle')) { + sentryTest.skip(); +} + + sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { return; } - const bundle = process.env.PW_BUNDLE || ''; - // We only want to run this in non-CDN bundle mode - if (bundle.startsWith('bundle')) { - sentryTest.skip(); - } - await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -51,12 +53,6 @@ sentryTest('should capture multiple Supabase operations in sequence', async ({ g return; } - const bundle = process.env.PW_BUNDLE || ''; - // We only want to run this in non-CDN bundle mode - if (bundle.startsWith('bundle')) { - sentryTest.skip(); - } - await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -77,12 +73,6 @@ sentryTest('should include correct data payload in Supabase breadcrumbs', async return; } - const bundle = process.env.PW_BUNDLE || ''; - // We only want to run this in non-CDN bundle mode - if (bundle.startsWith('bundle')) { - sentryTest.skip(); - } - await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname });