diff --git a/.rubocop.yml b/.rubocop.yml
index 66a8949..4f4f0f9 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -7,6 +7,10 @@ AllCops:
Style/Documentation:
Enabled: false
+Naming/FileName:
+ Exclude:
+ - 'posthog-rails/lib/posthog-rails.rb'
+
# Modern Ruby 3.0+ specific cops
Style/HashTransformKeys:
Enabled: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18fc048..79876cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,13 @@
+## Unreleased
+
+1. feat: Add posthog-rails gem for automatic Rails exception tracking
+ - Automatic capture of unhandled exceptions via Rails middleware
+ - Automatic capture of rescued exceptions (configurable)
+ - Automatic instrumentation of ActiveJob failures
+ - Integration with Rails 7.0+ error reporter
+ - Configurable exception exclusion list
+ - User context capture from controllers
+
## 3.3.3 - 2025-10-22
1. fix: fallback to API for multi-condition flags with static cohorts ([#80](https://github.com/PostHog/posthog-ruby/pull/80))
diff --git a/README.md b/README.md
index 4f4380b..7b0941b 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,10 @@ Specifically, the [Ruby integration](https://posthog.com/docs/integrations/ruby-
>
> All 2.x versions of the PostHog Ruby library are compatible with Ruby 2.0 and above if you need Ruby 2.0 support.
+## Rails Integration
+
+**Using Rails?** Check out [posthog-rails](posthog-rails/README.md) for automatic exception tracking, ActiveJob instrumentation, and Rails-specific features.
+
## Developing Locally
1. Install `asdf` to manage your Ruby version: `brew install asdf`
diff --git a/lib/posthog/logging.rb b/lib/posthog/logging.rb
index 8c9a0b0..acbfdca 100644
--- a/lib/posthog/logging.rb
+++ b/lib/posthog/logging.rb
@@ -41,8 +41,8 @@ def logger
return @logger if @logger
base_logger =
- if defined?(Rails)
- Rails.logger
+ if defined?(::Rails)
+ ::Rails.logger
else
logger = Logger.new $stdout
logger.progname = 'PostHog'
diff --git a/posthog-rails/IMPLEMENTATION.md b/posthog-rails/IMPLEMENTATION.md
new file mode 100644
index 0000000..c4dd6c3
--- /dev/null
+++ b/posthog-rails/IMPLEMENTATION.md
@@ -0,0 +1,384 @@
+# PostHog Rails Implementation Summary
+
+This document provides an overview of the posthog-rails gem implementation, following the Sentry Rails integration pattern.
+
+## Architecture Overview
+
+PostHog Rails is a separate gem that provides Rails-specific integrations for the core `posthog-ruby` SDK. It follows a monorepo pattern where both gems live in the same repository but are published separately.
+
+## Directory Structure
+
+```
+posthog-rails/
+├── lib/
+│ ├── posthog-rails.rb # Main entry point
+│ └── posthog/
+│ └── rails/
+│ ├── rails.rb # Module definition & requires
+│ ├── railtie.rb # Rails integration hook
+│ ├── configuration.rb # Rails-specific config
+│ ├── capture_exceptions.rb # Exception capture middleware
+│ ├── rescued_exception_interceptor.rb # Rescued exception middleware
+│ ├── active_job.rb # ActiveJob instrumentation
+│ └── error_subscriber.rb # Rails 7.0+ error reporter
+├── examples/
+│ └── posthog.rb # Example initializer
+├── posthog-rails.gemspec # Gem specification
+├── README.md # User documentation
+└── IMPLEMENTATION.md # This file
+```
+
+## Component Descriptions
+
+### 1. Gemspec (`posthog-rails.gemspec`)
+
+Defines the gem and its dependencies:
+- Depends on `posthog-ruby` (core SDK)
+- Depends on `railties >= 5.2.0` (minimal Rails dependency)
+- Version is synced with posthog-ruby
+
+### 2. Main Entry Point (`lib/posthog-rails.rb`)
+
+Simple entry point that:
+- Requires the core `posthog-ruby` gem
+- Requires `posthog/rails` if Rails is defined
+
+### 3. Rails Module (`lib/posthog/rails.rb`)
+
+Loads all Rails-specific components in the correct order:
+1. Configuration
+2. Middleware components
+3. ActiveJob integration
+4. Error subscriber
+5. Railtie (must be last)
+
+### 4. Railtie (`lib/posthog/rails/railtie.rb`)
+
+The core Rails integration hook that:
+
+#### Adds Module Methods
+- Extends `PostHog` module with class methods
+- Adds `PostHog.init` configuration block
+- Adds delegation methods (`capture`, `capture_exception`, etc.)
+- Stores singleton `client` and `rails_config`
+
+#### Middleware Registration
+Inserts two middleware in the Rails stack:
+```ruby
+ActionDispatch::DebugExceptions
+ ↓
+PostHog::Rails::RescuedExceptionInterceptor # Catches exceptions early
+ ↓
+Application Code
+ ↓
+ActionDispatch::ShowExceptions
+ ↓
+PostHog::Rails::CaptureExceptions # Reports to PostHog
+```
+
+#### ActiveJob Hook
+Uses `ActiveSupport.on_load(:active_job)` to prepend exception handling module before ActiveJob loads.
+
+#### After Initialize
+- Configures Rails environment (logger, etc.)
+- Registers Rails 7.0+ error subscriber
+- Sets up graceful shutdown
+
+### 5. Configuration (`lib/posthog/rails/configuration.rb`)
+
+Rails-specific configuration options:
+- `auto_capture_exceptions` - Enable/disable automatic capture
+- `report_rescued_exceptions` - Report exceptions Rails rescues
+- `auto_instrument_active_job` - Enable/disable job instrumentation
+- `excluded_exceptions` - Additional exceptions to ignore
+- `capture_user_context` - Include user info
+- `current_user_method` - Controller method name for user
+
+Also includes:
+- Default excluded exceptions list (404s, parameter errors, etc.)
+- `should_capture_exception?` method for filtering
+
+### 6. CaptureExceptions Middleware (`lib/posthog/rails/capture_exceptions.rb`)
+
+Main exception capture middleware that:
+1. Wraps application call in exception handler
+2. Checks for exceptions in `env` from Rails or other middleware
+3. Filters exceptions based on configuration
+4. Extracts user context from controller
+5. Builds request properties (URL, method, params, etc.)
+6. Filters sensitive parameters
+7. Calls `PostHog.capture_exception`
+
+**User Context Extraction:**
+- Gets controller from `env['action_controller.instance']`
+- Calls configured user method (default: `current_user`)
+- Extracts ID from user object
+- Falls back to session ID if no user
+
+**Request Context:**
+- Request URL, method, path
+- Controller and action names
+- Filtered request parameters
+- User agent and referrer
+
+### 7. RescuedExceptionInterceptor Middleware (`lib/posthog/rails/rescued_exception_interceptor.rb`)
+
+Lightweight middleware that:
+- Catches exceptions before Rails rescues them
+- Stores in `env['posthog.rescued_exception']`
+- Re-raises the exception (doesn't suppress it)
+- Only runs if `report_rescued_exceptions` is enabled
+
+This ensures we capture exceptions that Rails handles with `rescue_from` or similar.
+
+### 8. ActiveJob Integration (`lib/posthog/rails/active_job.rb`)
+
+Module prepended to `ActiveJob::Base`:
+- Wraps `perform_now` method
+- Catches exceptions during job execution
+- Extracts job context (class, ID, queue, priority)
+- Tries to extract user ID from job arguments
+- Sanitizes job arguments (filters sensitive data)
+- Calls `PostHog.capture_exception`
+
+**Argument Sanitization:**
+- Keeps primitives (string, integer, boolean, nil)
+- Filters sensitive hash keys
+- Converts ActiveRecord objects to `{class, id}`
+- Replaces complex objects with class name
+
+### 9. Error Subscriber (`lib/posthog/rails/error_subscriber.rb`)
+
+Rails 7.0+ integration:
+- Subscribes to `Rails.error` reporter
+- Receives errors from `Rails.error.handle` and `Rails.error.record`
+- Captures error with context
+- Includes handled/unhandled status and severity
+
+## Exception Flow
+
+### HTTP Request Exceptions
+
+```
+1. User makes request
+ ↓
+2. RescuedExceptionInterceptor catches and stores exception
+ ↓
+3. Exception bubbles up through Rails
+ ↓
+4. Rails may rescue it (rescue_from, etc.)
+ ↓
+5. Rails stores in env['action_dispatch.exception']
+ ↓
+6. CaptureExceptions middleware checks env for exception
+ ↓
+7. Extracts user and request context
+ ↓
+8. Filters based on configuration
+ ↓
+9. Calls PostHog.capture_exception
+ ↓
+10. Response returned to user
+```
+
+### ActiveJob Exceptions
+
+```
+1. Job.perform_later called
+ ↓
+2. ActiveJob enqueues job
+ ↓
+3. Worker picks up job
+ ↓
+4. Calls perform_now (our wrapped version)
+ ↓
+5. Exception raised in perform
+ ↓
+6. Our module catches it
+ ↓
+7. Extracts job context
+ ↓
+8. Calls PostHog.capture_exception
+ ↓
+9. Re-raises exception for normal job error handling
+```
+
+## User Experience
+
+### Installation
+```bash
+# Gemfile
+gem 'posthog-rails'
+
+bundle install
+```
+
+### Configuration
+```ruby
+# config/initializers/posthog.rb
+PostHog.init do |config|
+ config.api_key = ENV['POSTHOG_API_KEY']
+ config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY']
+
+ # Rails options
+ config.auto_capture_exceptions = true
+ config.current_user_method = :current_user
+end
+```
+
+### Usage
+```ruby
+# Automatic - just works!
+class PostsController < ApplicationController
+ def show
+ @post = Post.find(params[:id])
+ # Exceptions automatically captured
+ end
+end
+
+# Manual tracking
+PostHog.capture(
+ distinct_id: current_user.id,
+ event: 'post_viewed'
+)
+```
+
+## Key Design Decisions
+
+### 1. Separate Gem
+Following Sentry's pattern, posthog-rails is a separate gem. Benefits:
+- Non-Rails users don't get Rails bloat
+- Clear separation of concerns
+- Independent versioning possible
+- Rails-specific features don't affect core
+
+### 2. Middleware-Based Capture
+Using middleware instead of monkey-patching:
+- More reliable
+- Works with any exception handling strategy
+- Respects Rails conventions
+- Easy to understand and debug
+
+### 3. Two Middleware
+Why two middleware instead of one?
+- `RescuedExceptionInterceptor` runs early to catch exceptions before rescue
+- `CaptureExceptions` runs late to report after Rails processing
+- This ensures we catch both rescued and unrescued exceptions
+
+### 4. Module Prepend for ActiveJob
+Using `prepend` instead of `alias_method_chain`:
+- Cleaner Ruby pattern
+- Respects method resolution order
+- Works with other gems that extend ActiveJob
+- More maintainable
+
+### 5. Railtie for Integration
+Railtie is the Rails-native way to integrate:
+- Automatic discovery (no manual setup)
+- Access to Rails lifecycle hooks
+- Proper initialization order
+- Follows Rails conventions
+
+### 6. InitConfig Wrapper
+The `InitConfig` class wraps both core and Rails options:
+- Single configuration block
+- Type-safe option setting
+- Clear separation of concerns
+- Easy to extend
+
+### 7. Sensitive Data Filtering
+Built-in filtering for security:
+- Common sensitive parameter names
+- Parameter value truncation
+- Safe serialization fallbacks
+- Fails gracefully if filtering errors
+
+## Testing Strategy
+
+To test this gem, you would:
+
+1. **Unit tests** for each component
+ - Configuration options
+ - Exception filtering
+ - User extraction
+ - Parameter sanitization
+
+2. **Integration tests** with Rails
+ - Middleware insertion
+ - Exception capture flow
+ - ActiveJob instrumentation
+ - Rails 7.0+ error reporter
+
+3. **Test Rails app**
+ - Dummy Rails app in spec/
+ - Test actual exception capture
+ - Verify user context
+ - Test feature flags
+
+## Comparison with Sentry
+
+| Feature | PostHog Rails | Sentry Rails |
+|---------|---------------|--------------|
+| Separate gem | ✅ | ✅ |
+| Middleware-based | ✅ | ✅ |
+| ActiveJob | ✅ | ✅ |
+| Railtie | ✅ | ✅ |
+| Rails 7 errors | ✅ | ✅ |
+| Performance tracing | ❌ | ✅ |
+| Breadcrumbs | ❌ | ✅ |
+| ActionCable | ❌ | ✅ |
+
+## Future Enhancements
+
+Possible additions:
+1. **Performance tracing** - Track request/query times
+2. **Breadcrumbs** - Capture logs leading up to errors
+3. **ActionCable** - WebSocket exception tracking
+4. **Background workers** - Sidekiq, Resque integrations
+5. **Tests** - Full test suite
+6. **Rails generators** - `rails generate posthog:install`
+7. **Controller helpers** - `posthog_identify`, `posthog_capture` helpers
+
+## File Sizes
+
+Approximate lines of code:
+- `railtie.rb`: ~200 lines
+- `capture_exceptions.rb`: ~130 lines
+- `configuration.rb`: ~60 lines
+- `active_job.rb`: ~80 lines
+- `error_subscriber.rb`: ~30 lines
+- `rescued_exception_interceptor.rb`: ~25 lines
+
+Total: ~525 lines of implementation code
+
+## Dependencies
+
+Runtime:
+- `posthog-ruby` (core SDK)
+- `railties >= 5.2.0`
+
+Development (inherited from posthog-ruby):
+- `rspec`
+- `rubocop`
+
+## Compatibility
+
+- **Ruby**: 3.0+
+- **Rails**: 5.2+
+- **Tested on**: Rails 5.2, 6.0, 6.1, 7.0, 7.1 (planned)
+
+## Deployment
+
+To release:
+```bash
+cd posthog-rails
+gem build posthog-rails.gemspec
+gem push posthog-rails-3.3.3.gem
+```
+
+Users install with:
+```ruby
+gem 'posthog-rails'
+```
+
+This automatically brings in `posthog-ruby` as a dependency.
diff --git a/posthog-rails/README.md b/posthog-rails/README.md
new file mode 100644
index 0000000..2eb5e20
--- /dev/null
+++ b/posthog-rails/README.md
@@ -0,0 +1,340 @@
+# PostHog Rails
+
+Official PostHog integration for Ruby on Rails applications. Automatically track exceptions, instrument background jobs, and capture user analytics.
+
+## Features
+
+- 🚨 **Automatic exception tracking** - Captures unhandled and rescued exceptions
+- 🔄 **ActiveJob instrumentation** - Tracks background job exceptions
+- 👤 **User context** - Automatically associates exceptions with the current user
+- 🎯 **Smart filtering** - Excludes common Rails exceptions (404s, etc.) by default
+- 📊 **Rails 7.0+ error reporter** - Integrates with Rails' built-in error reporting
+- ⚙️ **Highly configurable** - Customize what gets tracked
+
+## Installation
+
+Add to your Gemfile:
+
+```ruby
+gem 'posthog-ruby'
+gem 'posthog-rails'
+```
+
+Then run:
+
+```bash
+bundle install
+```
+
+**Note:** `posthog-rails` depends on `posthog-ruby`, but it's recommended to explicitly include both gems in your Gemfile for clarity.
+
+## Configuration
+
+Create an initializer at `config/initializers/posthog.rb`:
+
+```ruby
+PostHog.init do |config|
+ # Required: Your PostHog API key
+ config.api_key = ENV['POSTHOG_API_KEY']
+
+ # Optional: Your PostHog instance URL (defaults to https://app.posthog.com)
+ config.host = 'https://app.posthog.com'
+
+ # Optional: Personal API key for feature flags
+ config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY']
+
+ # Rails-specific configuration
+ config.auto_capture_exceptions = true # Capture exceptions automatically
+ config.report_rescued_exceptions = true # Report exceptions Rails rescues
+ config.auto_instrument_active_job = true # Instrument background jobs
+ config.capture_user_context = true # Include user info in exceptions
+ config.current_user_method = :current_user # Method to get current user
+
+ # Add additional exceptions to ignore
+ config.excluded_exceptions = ['MyCustomError']
+
+ # Error callback
+ config.on_error = proc { |status, msg|
+ Rails.logger.error("PostHog error: #{msg}")
+ }
+end
+```
+
+### Environment Variables
+
+The recommended approach is to use environment variables:
+
+```bash
+# .env
+POSTHOG_API_KEY=your_project_api_key
+POSTHOG_PERSONAL_API_KEY=your_personal_api_key # Optional, for feature flags
+```
+
+## Usage
+
+### Automatic Exception Tracking
+
+Once configured, exceptions are automatically captured:
+
+```ruby
+class PostsController < ApplicationController
+ def show
+ @post = Post.find(params[:id])
+ # Any exception here is automatically captured
+ end
+end
+```
+
+### Manual Event Tracking
+
+Track custom events anywhere in your Rails app:
+
+```ruby
+# Track an event
+PostHog.capture(
+ distinct_id: current_user.id,
+ event: 'post_created',
+ properties: { title: @post.title }
+)
+
+# Identify a user
+PostHog.identify(
+ distinct_id: current_user.id,
+ properties: {
+ email: current_user.email,
+ plan: current_user.plan
+ }
+)
+
+# Track an exception manually
+PostHog.capture_exception(
+ exception,
+ current_user.id,
+ { custom_property: 'value' }
+)
+```
+
+### Background Jobs
+
+ActiveJob exceptions are automatically captured:
+
+```ruby
+class EmailJob < ApplicationJob
+ def perform(user_id)
+ user = User.find(user_id)
+ UserMailer.welcome(user).deliver_now
+ # Exceptions are automatically captured with job context
+ end
+end
+```
+
+### Feature Flags
+
+Use feature flags in your Rails app:
+
+```ruby
+class PostsController < ApplicationController
+ def show
+ if PostHog.is_feature_enabled('new-post-design', current_user.id)
+ render 'posts/show_new'
+ else
+ render 'posts/show'
+ end
+ end
+end
+```
+
+### Rails 7.0+ Error Reporter
+
+PostHog integrates with Rails' built-in error reporting:
+
+```ruby
+# These errors are automatically sent to PostHog
+Rails.error.handle do
+ # Code that might raise an error
+end
+
+Rails.error.record(exception, context: { user_id: current_user.id })
+```
+
+## Configuration Options
+
+### Core PostHog Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `api_key` | String | **required** | Your PostHog project API key |
+| `host` | String | `https://app.posthog.com` | PostHog instance URL |
+| `personal_api_key` | String | `nil` | For feature flag evaluation |
+| `max_queue_size` | Integer | `10000` | Max events to queue |
+| `test_mode` | Boolean | `false` | Don't send events (for testing) |
+| `on_error` | Proc | `nil` | Error callback |
+| `feature_flags_polling_interval` | Integer | `30` | Seconds between flag polls |
+
+### Rails-Specific Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `auto_capture_exceptions` | Boolean | `true` | Automatically capture exceptions |
+| `report_rescued_exceptions` | Boolean | `true` | Report exceptions Rails rescues |
+| `auto_instrument_active_job` | Boolean | `true` | Instrument ActiveJob |
+| `capture_user_context` | Boolean | `true` | Include user info |
+| `current_user_method` | Symbol | `:current_user` | Controller method for user |
+| `excluded_exceptions` | Array | `[]` | Additional exceptions to ignore |
+
+### Understanding Exception Tracking Options
+
+**`auto_capture_exceptions`** - Master switch for all automatic error tracking
+- When `true`: All exceptions are automatically captured and sent to PostHog
+- When `false`: No automatic error tracking (you must manually call `PostHog.capture_exception`)
+- **Use case:** Turn off automatic error tracking completely
+
+**`report_rescued_exceptions`** - Control exceptions that Rails handles gracefully
+- When `true`: Capture exceptions that Rails rescues and shows error pages for (404s, 500s, etc.)
+- When `false`: Only capture truly unhandled exceptions that crash your app
+- **Use case:** Reduce noise by ignoring errors Rails already handles
+
+**Example:**
+
+```ruby
+# Scenario: User visits /posts/999999 (post doesn't exist)
+def show
+ @post = Post.find(params[:id]) # Raises ActiveRecord::RecordNotFound
+end
+```
+
+| Configuration | Result |
+|---------------|--------|
+| `auto_capture_exceptions = true`
`report_rescued_exceptions = true` | ✅ Exception captured (default behavior) |
+| `auto_capture_exceptions = true`
`report_rescued_exceptions = false` | ❌ Not captured (Rails rescued it) |
+| `auto_capture_exceptions = false` | ❌ Not captured (automatic tracking disabled) |
+
+**Recommendation:** Keep both `true` (default) to get complete visibility into all errors. Set `report_rescued_exceptions = false` only if you want to track just critical crashes.
+
+## Excluded Exceptions by Default
+
+The following exceptions are not reported by default (common 4xx errors):
+
+- `AbstractController::ActionNotFound`
+- `ActionController::BadRequest`
+- `ActionController::InvalidAuthenticityToken`
+- `ActionController::RoutingError`
+- `ActionDispatch::Http::Parameters::ParseError`
+- `ActiveRecord::RecordNotFound`
+- `ActiveRecord::RecordNotUnique`
+
+You can add more with `config.excluded_exceptions = ['MyException']`.
+
+## User Context
+
+PostHog Rails automatically captures user information from your controllers:
+
+```ruby
+class ApplicationController < ActionController::Base
+ # PostHog will automatically call this method
+ def current_user
+ @current_user ||= User.find_by(id: session[:user_id])
+ end
+end
+```
+
+If your user method has a different name, configure it:
+
+```ruby
+config.current_user_method = :logged_in_user
+```
+
+## Sensitive Data Filtering
+
+PostHog Rails automatically filters sensitive parameters:
+
+- `password`
+- `password_confirmation`
+- `token`
+- `secret`
+- `api_key`
+- `authenticity_token`
+
+Long parameter values are also truncated to 1000 characters.
+
+## Testing
+
+In your test environment, you can disable PostHog or use test mode:
+
+```ruby
+# config/environments/test.rb
+PostHog.init do |config|
+ config.test_mode = true # Events are queued but not sent
+end
+```
+
+Or in your tests:
+
+```ruby
+# spec/rails_helper.rb
+RSpec.configure do |config|
+ config.before(:each) do
+ allow(PostHog).to receive(:capture)
+ end
+end
+```
+
+## Development
+
+To run tests:
+
+```bash
+cd posthog-rails
+bundle install
+bundle exec rspec
+```
+
+## Architecture
+
+PostHog Rails uses the following components:
+
+- **Railtie** - Hooks into Rails initialization
+- **Middleware** - Two middleware components capture exceptions:
+ - `RescuedExceptionInterceptor` - Catches rescued exceptions
+ - `CaptureExceptions` - Reports all exceptions to PostHog
+- **ActiveJob** - Prepends exception handling to `perform_now`
+- **Error Subscriber** - Integrates with Rails 7.0+ error reporter
+
+## Troubleshooting
+
+### Exceptions not being captured
+
+1. Verify PostHog is initialized:
+ ```ruby
+ Rails.console
+ > PostHog.initialized?
+ => true
+ ```
+
+2. Check your excluded exceptions list
+3. Verify middleware is installed:
+ ```ruby
+ Rails.application.middleware
+ ```
+
+### User context not working
+
+1. Verify `current_user_method` matches your controller method
+2. Check that the method returns an object with an `id` attribute
+3. Enable logging to see what's being captured
+
+### Feature flags not working
+
+Ensure you've set `personal_api_key`:
+
+```ruby
+config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY']
+```
+
+## Contributing
+
+See the main [PostHog Ruby](../README.md) repository for contribution guidelines.
+
+## License
+
+MIT License. See [LICENSE](../LICENSE) for details.
diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb
new file mode 100644
index 0000000..3d9a8bb
--- /dev/null
+++ b/posthog-rails/examples/posthog.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+# PostHog Rails Initializer
+# Place this file in config/initializers/posthog.rb
+
+PostHog.init do |config|
+ # ============================================================================
+ # REQUIRED CONFIGURATION
+ # ============================================================================
+
+ # Your PostHog project API key (required)
+ # Get this from: PostHog Project Settings > API Keys
+ config.api_key = ENV.fetch('POSTHOG_API_KEY', nil)
+
+ # ============================================================================
+ # CORE POSTHOG CONFIGURATION
+ # ============================================================================
+
+ # For PostHog Cloud, use: https://us.i.posthog.com or https://eu.i.posthog.com
+ config.host = ENV.fetch('POSTHOG_HOST', 'https://app.posthog.com')
+
+ # Personal API key (optional, but required for local feature flag evaluation)
+ # Get this from: PostHog Settings > Personal API Keys
+ config.personal_api_key = ENV.fetch('POSTHOG_PERSONAL_API_KEY', nil)
+
+ # Maximum number of events to queue before dropping (default: 10000)
+ config.max_queue_size = 10_000
+
+ # Feature flags polling interval in seconds (default: 30)
+ config.feature_flags_polling_interval = 30
+
+ # Feature flag request timeout in seconds (default: 3)
+ config.feature_flag_request_timeout_seconds = 3
+
+ # Error callback - called when PostHog encounters an error
+ config.on_error = proc { |status, message|
+ Rails.logger.error("[PostHog] Error #{status}: #{message}")
+ }
+
+ # Before send callback - modify or filter events before sending
+ # Return nil to prevent the event from being sent
+ # config.before_send = proc { |event|
+ # # Filter out test users
+ # return nil if event[:properties]&.dig('$user_email')&.end_with?('@test.com')
+ #
+ # # Add custom properties to all events
+ # event[:properties] ||= {}
+ # event[:properties]['environment'] = Rails.env
+ #
+ # event
+ # }
+
+ # ============================================================================
+ # RAILS-SPECIFIC CONFIGURATION
+ # ============================================================================
+
+ # Automatically capture exceptions (default: true)
+ config.auto_capture_exceptions = true
+
+ # Report exceptions that Rails rescues (e.g., with rescue_from) (default: true)
+ config.report_rescued_exceptions = true
+
+ # Automatically instrument ActiveJob background jobs (default: true)
+ config.auto_instrument_active_job = true
+
+ # Capture user context with exceptions (default: true)
+ config.capture_user_context = true
+
+ # Controller method name to get current user (default: :current_user)
+ # Change this if your app uses a different method name
+ config.current_user_method = :current_user
+
+ # Additional exception classes to exclude from reporting
+ # These are added to the default excluded exceptions
+ config.excluded_exceptions = [
+ # 'MyCustom404Error',
+ # 'MyCustomValidationError'
+ ]
+
+ # ============================================================================
+ # ENVIRONMENT-SPECIFIC CONFIGURATION
+ # ============================================================================
+
+ # Disable in test environment
+ config.test_mode = true if Rails.env.test?
+
+ # Optional: Disable in development
+ # if Rails.env.development?
+ # config.test_mode = true
+ # end
+end
+
+# ============================================================================
+# DEFAULT EXCLUDED EXCEPTIONS
+# ============================================================================
+# The following exceptions are excluded by default:
+#
+# - AbstractController::ActionNotFound
+# - ActionController::BadRequest
+# - ActionController::InvalidAuthenticityToken
+# - ActionController::InvalidCrossOriginRequest
+# - ActionController::MethodNotAllowed
+# - ActionController::NotImplemented
+# - ActionController::ParameterMissing
+# - ActionController::RoutingError
+# - ActionController::UnknownFormat
+# - ActionController::UnknownHttpMethod
+# - ActionDispatch::Http::Parameters::ParseError
+# - ActiveRecord::RecordNotFound
+# - ActiveRecord::RecordNotUnique
+#
+# These can be re-enabled by removing them from the exclusion list if needed.
+
+# ============================================================================
+# USAGE EXAMPLES
+# ============================================================================
+
+# Track custom events:
+# PostHog.capture(
+# distinct_id: current_user.id,
+# event: 'user_signed_up',
+# properties: {
+# plan: 'pro',
+# source: 'organic'
+# }
+# )
+
+# Identify users:
+# PostHog.identify(
+# distinct_id: current_user.id,
+# properties: {
+# email: current_user.email,
+# name: current_user.name,
+# plan: current_user.plan
+# }
+# )
+
+# Check feature flags:
+# if PostHog.is_feature_enabled('new-checkout-flow', current_user.id)
+# render 'checkout/new'
+# else
+# render 'checkout/old'
+# end
+
+# Capture exceptions manually:
+# begin
+# dangerous_operation
+# rescue => e
+# PostHog.capture_exception(e, current_user.id, { context: 'manual' })
+# raise
+# end
diff --git a/posthog-rails/lib/posthog-rails.rb b/posthog-rails/lib/posthog-rails.rb
new file mode 100644
index 0000000..d29d3d6
--- /dev/null
+++ b/posthog-rails/lib/posthog-rails.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Load core PostHog Ruby SDK
+require 'posthog'
+
+# Load Rails integration
+require 'posthog/rails' if defined?(Rails)
diff --git a/posthog-rails/lib/posthog/rails.rb b/posthog-rails/lib/posthog/rails.rb
new file mode 100644
index 0000000..3502acb
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'posthog/rails/configuration'
+require 'posthog/rails/capture_exceptions'
+require 'posthog/rails/rescued_exception_interceptor'
+require 'posthog/rails/active_job'
+require 'posthog/rails/error_subscriber'
+require 'posthog/rails/railtie'
+
+module PostHog
+ module Rails
+ VERSION = PostHog::VERSION
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb
new file mode 100644
index 0000000..4549738
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/active_job.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'posthog/rails/parameter_filter'
+
+module PostHog
+ module Rails
+ # ActiveJob integration to capture exceptions from background jobs
+ module ActiveJobExtensions
+ include ParameterFilter
+
+ def perform_now
+ super
+ rescue StandardError => e
+ # Capture the exception with job context
+ capture_job_exception(e)
+ raise
+ end
+
+ private
+
+ def capture_job_exception(exception)
+ return unless PostHog.rails_config&.auto_instrument_active_job
+
+ # Build distinct_id from job arguments if possible
+ distinct_id = extract_distinct_id_from_job
+
+ properties = {
+ '$exception_source' => 'active_job',
+ '$job_class' => self.class.name,
+ '$job_id' => job_id,
+ '$queue_name' => queue_name,
+ '$job_priority' => priority,
+ '$job_executions' => executions
+ }
+
+ # Add serialized job arguments (be careful with sensitive data)
+ properties['$job_arguments'] = sanitize_job_arguments(arguments) if arguments.present?
+
+ PostHog.capture_exception(exception, distinct_id, properties)
+ rescue StandardError => e
+ # Don't let PostHog errors break job processing
+ PostHog::Logging.logger.error("Failed to capture job exception: #{e.message}")
+ end
+
+ def extract_distinct_id_from_job
+ # Try to find a user ID in job arguments
+ arguments.each do |arg|
+ if arg.respond_to?(:id)
+ return arg.id
+ elsif arg.is_a?(Hash) && arg['user_id']
+ return arg['user_id']
+ elsif arg.is_a?(Hash) && arg[:user_id]
+ return arg[:user_id]
+ end
+ end
+
+ nil # No user context found
+ end
+
+ def sanitize_job_arguments(args)
+ # Convert arguments to a safe format
+ args.map do |arg|
+ case arg
+ when String
+ # Truncate long strings to prevent huge payloads
+ arg.length > 100 ? "[FILTERED: #{arg.length} chars]" : arg
+ when Integer, Float, TrueClass, FalseClass, NilClass
+ arg
+ when Hash
+ # Use Rails' filter_parameters to filter sensitive data
+ filter_sensitive_params(arg)
+ when ActiveRecord::Base
+ { class: arg.class.name, id: arg.id }
+ else
+ arg.class.name
+ end
+ end
+ rescue StandardError
+ ['']
+ end
+ end
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb
new file mode 100644
index 0000000..6350322
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'posthog/rails/parameter_filter'
+
+module PostHog
+ module Rails
+ # Middleware that captures exceptions and sends them to PostHog
+ class CaptureExceptions
+ include ParameterFilter
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ response = @app.call(env)
+
+ # Check if there was an exception that Rails handled
+ exception = collect_exception(env)
+
+ capture_exception(exception, env) if exception && should_capture?(exception)
+
+ response
+ rescue StandardError => e
+ # Capture unhandled exceptions
+ capture_exception(e, env) if should_capture?(e)
+ raise
+ end
+
+ private
+
+ def collect_exception(env)
+ # Rails stores exceptions in these env keys
+ env['action_dispatch.exception'] ||
+ env['rack.exception'] ||
+ env['posthog.rescued_exception']
+ end
+
+ def should_capture?(exception)
+ return false unless PostHog.rails_config&.auto_capture_exceptions
+ return false unless PostHog.rails_config&.should_capture_exception?(exception)
+
+ true
+ end
+
+ def capture_exception(exception, env)
+ request = ActionDispatch::Request.new(env)
+ distinct_id = extract_distinct_id(env, request)
+ additional_properties = build_properties(request, env)
+
+ PostHog.capture_exception(exception, distinct_id, additional_properties)
+ rescue StandardError => e
+ PostHog::Logging.logger.error("Failed to capture exception: #{e.message}")
+ PostHog::Logging.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
+ end
+
+ def extract_distinct_id(env, request)
+ # Try to get user from controller
+ if env['action_controller.instance']
+ controller = env['action_controller.instance']
+ method_name = PostHog.rails_config&.current_user_method || :current_user
+
+ if controller.respond_to?(method_name, true)
+ user = controller.send(method_name)
+ return extract_user_id(user) if user
+ end
+ end
+
+ # Fallback to session ID or nil
+ request.session_options&.dig(:id)
+ end
+
+ def extract_user_id(user)
+ # Try common ID methods
+ return user.id if user.respond_to?(:id)
+ return user['id'] if user.respond_to?(:[]) && user['id']
+ return user.uuid if user.respond_to?(:uuid)
+ return user['uuid'] if user.respond_to?(:[]) && user['uuid']
+
+ user.to_s
+ end
+
+ def build_properties(request, env)
+ properties = {
+ '$exception_source' => 'rails',
+ '$request_url' => request.url,
+ '$request_method' => request.method,
+ '$request_path' => request.path
+ }
+
+ # Add controller and action if available
+ if env['action_controller.instance']
+ controller = env['action_controller.instance']
+ properties['$controller'] = controller.controller_name
+ properties['$action'] = controller.action_name
+ end
+
+ # Add request parameters (be careful with sensitive data)
+ if request.params.present?
+ filtered_params = filter_sensitive_params(request.params)
+ properties['$request_params'] = filtered_params unless filtered_params.empty?
+ end
+
+ # Add user agent
+ properties['$user_agent'] = request.user_agent if request.user_agent
+
+ # Add referrer
+ properties['$referrer'] = request.referrer if request.referrer
+
+ properties
+ end
+
+ def filter_sensitive_params(params)
+ # Use Rails' configured filter_parameters to filter sensitive data
+ # This respects the app's config.filter_parameters setting
+ filtered = super
+
+ # Also truncate long values
+ filtered.transform_values do |value|
+ if value.is_a?(String) && value.length > 1000
+ "#{value[0..1000]}... (truncated)"
+ else
+ value
+ end
+ end
+ rescue StandardError
+ {} # Return empty hash if filtering fails
+ end
+ end
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/configuration.rb b/posthog-rails/lib/posthog/rails/configuration.rb
new file mode 100644
index 0000000..f36781d
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/configuration.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module PostHog
+ module Rails
+ class Configuration
+ # Whether to automatically capture exceptions from Rails
+ attr_accessor :auto_capture_exceptions
+
+ # Whether to capture exceptions that Rails rescues (e.g., with rescue_from)
+ attr_accessor :report_rescued_exceptions
+
+ # Whether to automatically instrument ActiveJob
+ attr_accessor :auto_instrument_active_job
+
+ # List of exception classes to ignore (in addition to default)
+ attr_accessor :excluded_exceptions
+
+ # Whether to capture the current user context in exceptions
+ attr_accessor :capture_user_context
+
+ # Method name to call on controller to get user ID (default: :current_user)
+ attr_accessor :current_user_method
+
+ def initialize
+ @auto_capture_exceptions = true
+ @report_rescued_exceptions = true
+ @auto_instrument_active_job = true
+ @excluded_exceptions = []
+ @capture_user_context = true
+ @current_user_method = :current_user
+ end
+
+ # Default exceptions that Rails apps typically don't want to track
+ def default_excluded_exceptions
+ [
+ 'AbstractController::ActionNotFound',
+ 'ActionController::BadRequest',
+ 'ActionController::InvalidAuthenticityToken',
+ 'ActionController::InvalidCrossOriginRequest',
+ 'ActionController::MethodNotAllowed',
+ 'ActionController::NotImplemented',
+ 'ActionController::ParameterMissing',
+ 'ActionController::RoutingError',
+ 'ActionController::UnknownFormat',
+ 'ActionController::UnknownHttpMethod',
+ 'ActionDispatch::Http::Parameters::ParseError',
+ 'ActiveRecord::RecordNotFound',
+ 'ActiveRecord::RecordNotUnique'
+ ]
+ end
+
+ def should_capture_exception?(exception)
+ exception_name = exception.class.name
+ !all_excluded_exceptions.include?(exception_name)
+ end
+
+ private
+
+ def all_excluded_exceptions
+ default_excluded_exceptions + excluded_exceptions
+ end
+ end
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/error_subscriber.rb b/posthog-rails/lib/posthog/rails/error_subscriber.rb
new file mode 100644
index 0000000..202ec9b
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/error_subscriber.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module PostHog
+ module Rails
+ # Rails 7.0+ error reporter integration
+ # This integrates with Rails.error.handle and Rails.error.record
+ class ErrorSubscriber
+ def report(error, handled:, severity:, context:, source: nil)
+ return unless PostHog.rails_config&.auto_capture_exceptions
+ return unless PostHog.rails_config&.should_capture_exception?(error)
+
+ distinct_id = context[:user_id] || context[:distinct_id]
+
+ properties = {
+ '$exception_source' => source || 'rails_error_reporter',
+ '$exception_handled' => handled,
+ '$exception_severity' => severity
+ }
+
+ # Add context information
+ if context.present?
+ context.each do |key, value|
+ properties["$context_#{key}"] = value unless key.in?(%i[user_id distinct_id])
+ end
+ end
+
+ PostHog.capture_exception(error, distinct_id, properties)
+ rescue StandardError => e
+ PostHog::Logging.logger.error("Failed to report error via subscriber: #{e.message}")
+ PostHog::Logging.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
+ end
+ end
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/parameter_filter.rb b/posthog-rails/lib/posthog/rails/parameter_filter.rb
new file mode 100644
index 0000000..b46fc91
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/parameter_filter.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module PostHog
+ module Rails
+ # Shared utility module for filtering sensitive parameters
+ #
+ # This module provides consistent parameter filtering across all PostHog Rails
+ # components, leveraging Rails' built-in parameter filtering when available.
+ # It automatically detects the correct Rails parameter filtering API based on
+ # the Rails version.
+ #
+ # @example Usage in a class
+ # class MyClass
+ # include PostHog::Rails::ParameterFilter
+ #
+ # def my_method(params)
+ # filtered = filter_sensitive_params(params)
+ # PostHog.capture(event: 'something', properties: filtered)
+ # end
+ # end
+ module ParameterFilter
+ EMPTY_HASH = {}.freeze
+
+ if ::Rails.version.to_f >= 6.0
+ def self.backend
+ ActiveSupport::ParameterFilter
+ end
+ else
+ def self.backend
+ ActionDispatch::Http::ParameterFilter
+ end
+ end
+
+ # Filter sensitive parameters from a hash, respecting Rails configuration.
+ #
+ # Uses Rails' configured filter_parameters (e.g., :password, :token, :api_key)
+ # to automatically filter sensitive data that the Rails app has configured.
+ #
+ # @param params [Hash] The parameters to filter
+ # @return [Hash] Filtered parameters with sensitive data masked
+ def filter_sensitive_params(params)
+ return EMPTY_HASH unless params.is_a?(Hash)
+
+ filter_parameters = ::Rails.application.config.filter_parameters
+ parameter_filter = ParameterFilter.backend.new(filter_parameters)
+
+ parameter_filter.filter(params)
+ end
+ end
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb
new file mode 100644
index 0000000..4bdd318
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/railtie.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+module PostHog
+ module Rails
+ class Railtie < ::Rails::Railtie
+ # Add PostHog module methods for accessing Rails-specific client
+ initializer 'posthog.set_configs' do |_app|
+ PostHog.class_eval do
+ class << self
+ attr_accessor :rails_config, :client
+
+ # Initialize PostHog client with a block configuration
+ def init(options = {})
+ @rails_config ||= PostHog::Rails::Configuration.new
+
+ # If block given, yield to configuration
+ if block_given?
+ config = PostHog::Rails::InitConfig.new(@rails_config, options)
+ yield config
+ options = config.to_client_options
+ end
+
+ # Create the PostHog client
+ @client = PostHog::Client.new(options)
+ end
+
+ # Delegate common methods to the singleton client
+ def capture(*args, **kwargs)
+ ensure_initialized!
+ client.capture(*args, **kwargs)
+ end
+
+ def capture_exception(*args, **kwargs)
+ ensure_initialized!
+ client.capture_exception(*args, **kwargs)
+ end
+
+ def identify(*args, **kwargs)
+ ensure_initialized!
+ client.identify(*args, **kwargs)
+ end
+
+ def alias(*args, **kwargs)
+ ensure_initialized!
+ client.alias(*args, **kwargs)
+ end
+
+ def group_identify(*args, **kwargs)
+ ensure_initialized!
+ client.group_identify(*args, **kwargs)
+ end
+
+ # NOTE: Method name matches the underlying PostHog Ruby Client for consistency.
+ # TODO: Rename to feature_flag_enabled? when the client method is updated.
+ def is_feature_enabled(*args, **kwargs) # rubocop:disable Naming/PredicateName
+ ensure_initialized!
+ client.is_feature_enabled(*args, **kwargs)
+ end
+
+ def get_feature_flag(*args, **kwargs)
+ ensure_initialized!
+ client.get_feature_flag(*args, **kwargs)
+ end
+
+ def get_all_flags(*args, **kwargs)
+ ensure_initialized!
+ client.get_all_flags(*args, **kwargs)
+ end
+
+ def initialized?
+ !@client.nil?
+ end
+
+ private
+
+ def ensure_initialized!
+ return if initialized?
+
+ raise 'PostHog is not initialized. Call PostHog.init in an initializer.'
+ end
+ end
+ end
+ end
+
+ # Insert middleware for exception capturing
+ initializer 'posthog.insert_middlewares' do |app|
+ # Insert after DebugExceptions to catch rescued exceptions
+ app.config.middleware.insert_after(
+ ActionDispatch::DebugExceptions,
+ PostHog::Rails::RescuedExceptionInterceptor
+ )
+
+ # Insert after ShowExceptions to capture all exceptions
+ app.config.middleware.insert_after(
+ ActionDispatch::ShowExceptions,
+ PostHog::Rails::CaptureExceptions
+ )
+ end
+
+ # Hook into ActiveJob before classes are loaded
+ initializer 'posthog.active_job', before: :eager_load! do
+ ActiveSupport.on_load(:active_job) do
+ # Prepend our module to ActiveJob::Base to wrap perform_now
+ prepend PostHog::Rails::ActiveJobExtensions
+ end
+ end
+
+ # After initialization, set up remaining integrations
+ config.after_initialize do |_app|
+ next unless PostHog.initialized?
+
+ # Register with Rails error reporter (Rails 7.0+)
+ register_error_subscriber if rails_version_above_7?
+ end
+
+ # Ensure PostHog shuts down gracefully
+ config.to_prepare do
+ at_exit do
+ PostHog.client&.shutdown if PostHog.initialized?
+ end
+ end
+
+ def self.register_error_subscriber
+ return unless PostHog.rails_config&.auto_capture_exceptions
+
+ subscriber = PostHog::Rails::ErrorSubscriber.new
+ ::Rails.error.subscribe(subscriber)
+ rescue StandardError => e
+ PostHog::Logging.logger.warn("Failed to register error subscriber: #{e.message}")
+ PostHog::Logging.logger.warn("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
+ end
+
+ def self.rails_version_above_7?
+ ::Rails.version.to_f >= 7.0
+ end
+ end
+
+ # Configuration wrapper for the init block
+ class InitConfig
+ attr_reader :rails_config
+
+ def initialize(rails_config, base_options = {})
+ @rails_config = rails_config
+ @base_options = base_options
+ end
+
+ # Core PostHog options
+ def api_key=(value)
+ @base_options[:api_key] = value
+ end
+
+ def personal_api_key=(value)
+ @base_options[:personal_api_key] = value
+ end
+
+ def host=(value)
+ @base_options[:host] = value
+ end
+
+ def max_queue_size=(value)
+ @base_options[:max_queue_size] = value
+ end
+
+ def test_mode=(value)
+ @base_options[:test_mode] = value
+ end
+
+ def on_error=(value)
+ @base_options[:on_error] = value
+ end
+
+ def feature_flags_polling_interval=(value)
+ @base_options[:feature_flags_polling_interval] = value
+ end
+
+ def feature_flag_request_timeout_seconds=(value)
+ @base_options[:feature_flag_request_timeout_seconds] = value
+ end
+
+ def before_send=(value)
+ @base_options[:before_send] = value
+ end
+
+ # Rails-specific options
+ def auto_capture_exceptions=(value)
+ @rails_config.auto_capture_exceptions = value
+ end
+
+ def report_rescued_exceptions=(value)
+ @rails_config.report_rescued_exceptions = value
+ end
+
+ def auto_instrument_active_job=(value)
+ @rails_config.auto_instrument_active_job = value
+ end
+
+ def excluded_exceptions=(value)
+ @rails_config.excluded_exceptions = value
+ end
+
+ def capture_user_context=(value)
+ @rails_config.capture_user_context = value
+ end
+
+ def current_user_method=(value)
+ @rails_config.current_user_method = value
+ end
+
+ def to_client_options
+ @base_options
+ end
+ end
+ end
+end
diff --git a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb
new file mode 100644
index 0000000..8543ff7
--- /dev/null
+++ b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module PostHog
+ module Rails
+ # Middleware that intercepts exceptions that are rescued by Rails
+ # This middleware runs before ShowExceptions and captures the exception
+ # so we can report it even if Rails rescues it
+ class RescuedExceptionInterceptor
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ rescue StandardError => e
+ # Store the exception so CaptureExceptions middleware can report it
+ env['posthog.rescued_exception'] = e if should_intercept?
+ raise e
+ end
+
+ private
+
+ def should_intercept?
+ PostHog.rails_config&.report_rescued_exceptions
+ end
+ end
+ end
+end
diff --git a/posthog-rails/posthog-rails.gemspec b/posthog-rails/posthog-rails.gemspec
new file mode 100644
index 0000000..95fe731
--- /dev/null
+++ b/posthog-rails/posthog-rails.gemspec
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require File.expand_path('../lib/posthog/version', __dir__)
+
+Gem::Specification.new do |spec|
+ spec.name = 'posthog-rails'
+ spec.version = PostHog::VERSION
+ spec.files = Dir.glob('lib/**/*')
+ spec.require_paths = ['lib']
+ spec.summary = 'PostHog integration for Rails'
+ spec.description = 'Automatic exception tracking and instrumentation for Ruby on Rails applications using PostHog'
+ spec.authors = ['PostHog']
+ spec.email = 'hey@posthog.com'
+ spec.homepage = 'https://github.com/PostHog/posthog-ruby'
+ spec.license = 'MIT'
+ spec.required_ruby_version = '>= 3.0'
+ spec.metadata['rubygems_mfa_required'] = 'true'
+
+ # Rails dependency - support Rails 5.2+
+ spec.add_dependency 'railties', '>= 5.2.0'
+ # Core PostHog SDK
+ spec.add_dependency 'posthog-ruby', "~> #{PostHog::VERSION.split('.')[0..1].join('.')}"
+end