Skip to main content
Custom integrations allow you to extend the Sentry SDK with your own functionality, such as capturing custom data, filtering events, or integrating with third-party services.

Integration Basics

An integration is a function that returns an integration object with specific methods:
import { defineIntegration } from '@sentry/core';

const myIntegration = defineIntegration(() => {
  return {
    name: 'MyIntegration',
    
    setupOnce() {
      // Called once when the integration is set up
    },
    
    setup(client) {
      // Called for each client
    },
    
    processEvent(event, hint, client) {
      // Process events before they're sent
      return event;
    },
  };
});

export { myIntegration };

Simple Integration Example

Add custom data to all events:
import { defineIntegration } from '@sentry/core';

const environmentInfoIntegration = defineIntegration(() => {
  return {
    name: 'EnvironmentInfo',
    
    processEvent(event) {
      // Add custom context to all events
      event.contexts = event.contexts || {};
      event.contexts.environment = {
        screen_resolution: `${window.screen.width}x${window.screen.height}`,
        viewport: `${window.innerWidth}x${window.innerHeight}`,
        color_depth: window.screen.colorDepth,
        pixel_ratio: window.devicePixelRatio,
      };
      
      return event;
    },
  };
});

export { environmentInfoIntegration };
Usage:
import * as Sentry from '@sentry/browser';
import { environmentInfoIntegration } from './integrations/environment-info';

Sentry.init({
  dsn: '__DSN__',
  integrations: [
    environmentInfoIntegration(),
  ],
});

Integration Lifecycle Methods

setupOnce()

Called once when the SDK initializes. Use for global setup:
const globalTrackerIntegration = defineIntegration(() => {
  return {
    name: 'GlobalTracker',
    
    setupOnce() {
      // Set up global event listeners
      window.addEventListener('online', () => {
        Sentry.addBreadcrumb({
          message: 'Network online',
          level: 'info',
        });
      });
      
      window.addEventListener('offline', () => {
        Sentry.addBreadcrumb({
          message: 'Network offline',
          level: 'warning',
        });
      });
    },
  };
});

setup(client)

Called for each client instance. Use for client-specific setup:
const clientConfigIntegration = defineIntegration(() => {
  return {
    name: 'ClientConfig',
    
    setup(client) {
      // Access client configuration
      const options = client.getOptions();
      console.log('Client environment:', options.environment);
      
      // Add event processor to this client
      client.on('beforeSendEvent', (event) => {
        console.log('Sending event:', event.event_id);
      });
    },
  };
});

processEvent(event, hint, client)

Process events before they’re sent:
const eventEnricherIntegration = defineIntegration(() => {
  return {
    name: 'EventEnricher',
    
    processEvent(event, hint, client) {
      // Add custom tags
      event.tags = {
        ...event.tags,
        custom_build: process.env.BUILD_ID,
      };
      
      // Access the original exception
      const error = hint.originalException;
      if (error instanceof TypeError) {
        event.tags.error_type = 'type_error';
      }
      
      // Modify event based on client options
      const options = client.getOptions();
      if (options.environment === 'development') {
        event.level = 'debug';
      }
      
      return event;
    },
  };
});

Advanced Integrations

Automatic Instrumentation

Capture breadcrumbs from custom events:
import { defineIntegration } from '@sentry/core';
import { addBreadcrumb } from '@sentry/browser';

const customEventsIntegration = defineIntegration(() => {
  let cleanup: (() => void) | undefined;
  
  return {
    name: 'CustomEvents',
    
    setupOnce() {
      const handler = (event: CustomEvent) => {
        addBreadcrumb({
          category: 'custom-event',
          message: event.type,
          level: 'info',
          data: event.detail,
        });
      };
      
      // Listen to custom events
      window.addEventListener('app:navigation', handler);
      window.addEventListener('app:action', handler);
      
      // Store cleanup function
      cleanup = () => {
        window.removeEventListener('app:navigation', handler);
        window.removeEventListener('app:action', handler);
      };
    },
    
    // Optional: cleanup on teardown
    teardown() {
      cleanup?.();
    },
  };
});

export { customEventsIntegration };

Third-Party Service Integration

Integrate with analytics or logging services:
import { defineIntegration } from '@sentry/core';

const analyticsIntegration = defineIntegration((options = {}) => {
  return {
    name: 'Analytics',
    
    processEvent(event, hint) {
      // Send error to analytics
      if (window.analytics && event.exception) {
        window.analytics.track('Error Occurred', {
          message: event.message,
          level: event.level,
          event_id: event.event_id,
        });
      }
      
      return event;
    },
  };
});

export { analyticsIntegration };

Performance Monitoring Integration

Add custom performance tracking:
import { defineIntegration } from '@sentry/core';
import { startSpan } from '@sentry/browser';

const apiTrackerIntegration = defineIntegration(() => {
  return {
    name: 'ApiTracker',
    
    setupOnce() {
      // Intercept fetch calls
      const originalFetch = window.fetch;
      
      window.fetch = async function(...args) {
        const url = args[0] instanceof Request ? args[0].url : args[0];
        
        return startSpan(
          {
            name: `fetch ${url}`,
            op: 'http.client',
            attributes: {
              'http.url': url,
              'http.method': args[1]?.method || 'GET',
            },
          },
          async (span) => {
            try {
              const response = await originalFetch.apply(this, args);
              span?.setAttribute('http.status_code', response.status);
              return response;
            } catch (error) {
              span?.setAttribute('error', true);
              throw error;
            }
          }
        );
      };
    },
  };
});

export { apiTrackerIntegration };

Integration with Options

Accept configuration options:
import { defineIntegration } from '@sentry/core';

interface FeatureFlagOptions {
  flagProvider?: any;
  captureFlags?: boolean;
}

const featureFlagIntegration = defineIntegration((options: FeatureFlagOptions = {}) => {
  const {
    flagProvider,
    captureFlags = true,
  } = options;
  
  return {
    name: 'FeatureFlags',
    
    processEvent(event) {
      if (!captureFlags || !flagProvider) {
        return event;
      }
      
      // Add feature flags to event
      const flags = flagProvider.getAllFlags();
      event.contexts = event.contexts || {};
      event.contexts.feature_flags = flags;
      
      return event;
    },
  };
});

export { featureFlagIntegration };
Usage:
import { featureFlagIntegration } from './integrations/feature-flags';

Sentry.init({
  dsn: '__DSN__',
  integrations: [
    featureFlagIntegration({
      flagProvider: myFlagProvider,
      captureFlags: true,
    }),
  ],
});

Client Hooks

Listen to SDK events:
import { defineIntegration } from '@sentry/core';

const monitoringIntegration = defineIntegration(() => {
  return {
    name: 'Monitoring',
    
    setup(client) {
      // Hook into various events
      client.on('beforeSendEvent', (event, hint) => {
        console.log('About to send event:', event.event_id);
      });
      
      client.on('afterSendEvent', (event, sendResponse) => {
        console.log('Event sent:', event.event_id, sendResponse);
      });
      
      client.on('createSession', (session) => {
        console.log('Session created:', session.sid);
      });
      
      client.on('beforeEnvelope', (envelope) => {
        console.log('About to send envelope');
      });
    },
  };
});

export { monitoringIntegration };

Filtering Events

Drop events based on custom logic:
import { defineIntegration } from '@sentry/core';

const rateLimitIntegration = defineIntegration((maxEventsPerMinute = 10) => {
  const events: number[] = [];
  
  return {
    name: 'RateLimit',
    
    processEvent(event) {
      const now = Date.now();
      const oneMinuteAgo = now - 60000;
      
      // Remove old timestamps
      const recentEvents = events.filter(time => time > oneMinuteAgo);
      
      // Check rate limit
      if (recentEvents.length >= maxEventsPerMinute) {
        console.warn('Rate limit exceeded, dropping event');
        return null; // Drop the event
      }
      
      // Track this event
      events.push(now);
      
      return event;
    },
  };
});

export { rateLimitIntegration };

Testing Integrations

import { describe, it, expect, vi } from 'vitest';
import { myIntegration } from './my-integration';
import { getClient, init } from '@sentry/browser';

describe('MyIntegration', () => {
  beforeEach(() => {
    init({
      dsn: 'https://test@sentry.io/123',
      integrations: [myIntegration()],
    });
  });
  
  it('should add custom data to events', () => {
    const client = getClient();
    const integration = client?.getIntegrationByName('MyIntegration');
    
    expect(integration).toBeDefined();
    
    // Test processEvent
    const event = { message: 'test' };
    const processed = integration?.processEvent(event, {}, client!);
    
    expect(processed?.contexts?.custom).toBeDefined();
  });
});

Best Practices

Always use defineIntegration for proper typing and structure:
import { defineIntegration } from '@sentry/core';

const myIntegration = defineIntegration(() => {
  return { name: 'MyIntegration', /* ... */ };
});
Don’t let integration errors crash the app:
processEvent(event) {
  try {
    // Your logic
    return event;
  } catch (error) {
    console.error('Integration error:', error);
    return event; // Return original event
  }
}
Clean up resources when the integration is removed:
setupOnce() {
  window.addEventListener('event', handler);
},

teardown() {
  window.removeEventListener('event', handler);
}
Provide clear documentation on:
  • What the integration does
  • Available options
  • Performance impact
  • Browser/Node compatibility
Each integration should have a single, clear purpose. Don’t create “god integrations” that do too much.

Complete Example

import { defineIntegration } from '@sentry/core';
import { addBreadcrumb } from '@sentry/browser';

interface UserActivityOptions {
  trackClicks?: boolean;
  trackScrolls?: boolean;
  trackPageVisibility?: boolean;
}

const userActivityIntegration = defineIntegration((options: UserActivityOptions = {}) => {
  const {
    trackClicks = true,
    trackScrolls = true,
    trackPageVisibility = true,
  } = options;
  
  const handlers: Array<() => void> = [];
  
  return {
    name: 'UserActivity',
    
    setupOnce() {
      // Track clicks
      if (trackClicks) {
        const clickHandler = (event: MouseEvent) => {
          const target = event.target as HTMLElement;
          addBreadcrumb({
            category: 'ui.click',
            message: `Clicked: ${target.tagName}`,
            data: {
              tag: target.tagName,
              id: target.id,
              className: target.className,
            },
          });
        };
        document.addEventListener('click', clickHandler);
        handlers.push(() => document.removeEventListener('click', clickHandler));
      }
      
      // Track scrolls
      if (trackScrolls) {
        let lastScroll = 0;
        const scrollHandler = () => {
          const now = Date.now();
          if (now - lastScroll > 1000) { // Throttle
            addBreadcrumb({
              category: 'ui.scroll',
              message: 'Page scrolled',
              data: {
                scrollY: window.scrollY,
                scrollX: window.scrollX,
              },
            });
            lastScroll = now;
          }
        };
        window.addEventListener('scroll', scrollHandler, { passive: true });
        handlers.push(() => window.removeEventListener('scroll', scrollHandler));
      }
      
      // Track page visibility
      if (trackPageVisibility) {
        const visibilityHandler = () => {
          addBreadcrumb({
            category: 'ui.visibility',
            message: `Page ${document.hidden ? 'hidden' : 'visible'}`,
            level: document.hidden ? 'info' : 'debug',
          });
        };
        document.addEventListener('visibilitychange', visibilityHandler);
        handlers.push(() => document.removeEventListener('visibilitychange', visibilityHandler));
      }
    },
    
    processEvent(event, hint, client) {
      // Add user activity summary
      const options = client.getOptions();
      event.tags = {
        ...event.tags,
        has_user_activity: 'true',
        environment: options.environment,
      };
      
      return event;
    },
    
    teardown() {
      // Cleanup all handlers
      handlers.forEach(cleanup => cleanup());
      handlers.length = 0;
    },
  };
});

export { userActivityIntegration };
Usage:
import * as Sentry from '@sentry/browser';
import { userActivityIntegration } from './integrations/user-activity';

Sentry.init({
  dsn: '__DSN__',
  integrations: [
    userActivityIntegration({
      trackClicks: true,
      trackScrolls: true,
      trackPageVisibility: true,
    }),
  ],
});

Next Steps

Custom Transports

Build custom transport layers

OpenTelemetry

Integrate with OpenTelemetry