Skip to main content
The Supabase integration automatically instruments Supabase client instances to capture database queries, authentication operations, and errors.

Installation

1

Initialize Sentry

import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: 'your-dsn',
});
2

Instrument Your Supabase Client

import { createClient } from '@supabase/supabase-js';
import * as Sentry from '@sentry/node';

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_ANON_KEY
);

// Add Supabase integration
Sentry.addIntegration(
  Sentry.supabaseIntegration({ supabaseClient: supabase })
);

Basic Usage

Once instrumented, all Supabase operations are automatically tracked:

Database Queries

// Automatically tracked
const { data, error } = await supabase
  .from('users')
  .select('*')
  .eq('id', 123);

Authentication

// Automatically tracked
const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'password',
});

What Gets Captured

The integration tracks:

Database Operations

  • SELECT queries
  • INSERT operations
  • UPDATE operations
  • UPSERT operations
  • DELETE operations

Authentication Operations

  • Sign in (password, OAuth, OTP, SSO)
  • Sign up
  • Sign out
  • Reauthenticate
  • Verify OTP
  • Admin operations (create/update/delete users)

Captured Metadata

  • Operation type
  • Table name
  • Query filters
  • Database schema
  • Request body
  • HTTP status codes
  • Error details

Database Query Tracking

Database operations create spans with detailed information:
const { data, error } = await supabase
  .from('posts')
  .select('id, title, author')
  .eq('published', true)
  .gte('created_at', '2024-01-01')
  .order('created_at', { ascending: false })
  .limit(10);

// Creates span:
// "select(id, title, author) eq(published, true) gte(created_at, 2024-01-01) from(posts)"

Span Attributes

{
  'db.system': 'postgresql',
  'db.operation': 'select',
  'db.table': 'posts',
  'db.schema': 'public',
  'db.query': [
    'select(id, title, author)',
    'eq(published, true)',
    'gte(created_at, 2024-01-01)',
  ],
  'db.url': 'https://your-project.supabase.co',
  'db.sdk': 'supabase-js/2.x.x',
}

Practical Examples

CRUD Operations

import * as Sentry from '@sentry/node';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_ANON_KEY
);

Sentry.addIntegration(
  Sentry.supabaseIntegration({ supabaseClient: supabase })
);

// Create
const { data: newUser, error: createError } = await supabase
  .from('users')
  .insert({ name: 'John Doe', email: '[email protected]' })
  .select();
// Span: insert(...) from(users)

// Read
const { data: users, error: readError } = await supabase
  .from('users')
  .select('*')
  .eq('email', '[email protected]');
// Span: select(*) eq(email, [email protected]) from(users)

// Update
const { data: updated, error: updateError } = await supabase
  .from('users')
  .update({ name: 'Jane Doe' })
  .eq('id', 123);
// Span: update(...) eq(id, 123) from(users)

// Delete
const { data: deleted, error: deleteError } = await supabase
  .from('users')
  .delete()
  .eq('id', 123);
// Span: delete eq(id, 123) from(users)

Complex Queries

// Join with related data
const { data, error } = await supabase
  .from('posts')
  .select(`
    id,
    title,
    content,
    author:users(name, email)
  `)
  .eq('published', true);
// Tracked with full query details

// Full-text search
const { data, error } = await supabase
  .from('articles')
  .select('*')
  .textSearch('content', 'quantum computing');
// Span includes textSearch filter

// Range queries
const { data, error } = await supabase
  .from('events')
  .select('*')
  .gte('date', '2024-01-01')
  .lt('date', '2024-12-31');
// Span includes range filters

Authentication Flows

// Email/Password Sign Up
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password',
});
// Span: auth signUp

// Email/Password Sign In
const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password',
});
// Span: auth signInWithPassword

// OAuth Sign In
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
});
// Span: auth signInWithOAuth

// OTP Sign In
const { data, error } = await supabase.auth.signInWithOtp({
  email: '[email protected]',
});
// Span: auth signInWithOtp

// Sign Out
const { error } = await supabase.auth.signOut();
// Span: auth signOut

Admin Operations

// Create user (admin)
const { data, error } = await supabase.auth.admin.createUser({
  email: '[email protected]',
  password: 'password',
  email_confirm: true,
});
// Span: auth (admin) createUser

// Update user (admin)
const { data, error } = await supabase.auth.admin.updateUserById(
  'user-uuid',
  { email: '[email protected]' }
);
// Span: auth (admin) updateUserById

// Delete user (admin)
const { data, error } = await supabase.auth.admin.deleteUser('user-uuid');
// Span: auth (admin) deleteUser

RLS (Row Level Security)

// Query with user context (RLS enforced)
const { data, error } = await supabase
  .from('private_notes')
  .select('*');
// RLS policies automatically enforced
// Errors captured if RLS denies access

Error Tracking

Errors are automatically captured with context:
const { data, error } = await supabase
  .from('users')
  .select('*')
  .eq('email', '[email protected]');

if (error) {
  // Error automatically captured in Sentry with:
  // - Query details
  // - Error message
  // - Status code
  // - Database context
}

Captured Error Context

{
  error: {
    message: 'relation "users" does not exist',
    code: '42P01',
    details: '...',
  },
  context: {
    supabase: {
      query: ['select(*)'],
      body: {},
    },
  },
}
Database operations are added as breadcrumbs:
{
  type: 'supabase',
  category: 'db.select',
  message: 'select(*) eq(published, true) from(posts)',
  data: {
    query: ['select(*)', 'eq(published, true)'],
  },
}

Performance Monitoring

Track database performance:
Transaction: POST /api/posts
├─ select(*) from(posts)
│  └─ Duration: 45ms
├─ insert(...) from(posts)
│  └─ Duration: 23ms
└─ Total: 150ms

Filter Mappings

Supabase query filters are translated to readable method names:
Supabase FilterSpan Name
eqeq(column, value)
neqneq(column, value)
gtgt(column, value)
gtegte(column, value)
ltlt(column, value)
ltelte(column, value)
likelike(column, pattern)
ilikeilike(column, pattern)
inin(column, values)
containscontains(column, value)
textSearchtextSearch(column, query)

Source Code

The Supabase integration is implemented in: packages/core/src/integrations/supabase.ts:525

Best Practices

The integration automatically filters sensitive authentication data.

1. Monitor Query Performance

// Add custom spans for complex operations
await Sentry.startSpan(
  { name: 'Fetch User Dashboard', op: 'db.query' },
  async () => {
    const { data: user } = await supabase
      .from('users')
      .select('*')
      .eq('id', userId)
      .single();
    
    const { data: posts } = await supabase
      .from('posts')
      .select('*')
      .eq('author_id', userId);
    
    return { user, posts };
  }
);

2. Handle Errors Gracefully

const { data, error } = await supabase
  .from('users')
  .select('*');

if (error) {
  Sentry.captureException(error, {
    contexts: {
      query: {
        table: 'users',
        operation: 'select',
      },
    },
  });
}

3. Use Transactions

try {
  // Multiple operations in a transaction
  const { data: user } = await supabase
    .from('users')
    .insert({ name: 'John' })
    .select()
    .single();
  
  const { data: profile } = await supabase
    .from('profiles')
    .insert({ user_id: user.id })
    .select();
  
  // Both operations tracked separately
} catch (error) {
  Sentry.captureException(error);
}

Troubleshooting

Integration Not Working

Ensure you’ve added the integration after creating the client:
const supabase = createClient(url, key);
Sentry.addIntegration(
  Sentry.supabaseIntegration({ supabaseClient: supabase })
);

Queries Not Tracked

Verify tracing is enabled:
Sentry.init({
  dsn: 'your-dsn',
  tracesSampleRate: 1.0,
});

Build docs developers (and LLMs) love