From e2e4e6dafc5971b324c785054e887e5f75d5bab2 Mon Sep 17 00:00:00 2001 From: Drago Crnjac Date: Mon, 27 Oct 2025 16:56:32 +0100 Subject: [PATCH 01/31] Add posthog-rails gem for automatic Rails exception tracking This commit introduces the posthog-rails gem which provides automatic exception tracking for Rails applications, including: - 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 Core library improvements: - Added comprehensive logging throughout exception capture flow - Added ExceptionCapture module for standardized exception parsing - Fixed field handling in Transport to strip internal-only fields (type, library, library_version, messageId) that are not part of PostHog's RawEvent struct - Fixed UUID field handling to avoid sending null values - Added capture_exception method to Client for explicit exception tracking The posthog-rails gem includes: - Railtie for automatic initialization and middleware insertion - Multiple middleware layers for capturing different exception types - ErrorSubscriber for Rails 7.0+ error reporter integration - ActiveJob extensions for job failure tracking - Comprehensive configuration options --- CHANGELOG.md | 10 + README.md | 4 + lib/posthog/logging.rb | 4 +- posthog-rails/IMPLEMENTATION.md | 384 ++++++++++++++++++ posthog-rails/README.md | 340 ++++++++++++++++ posthog-rails/examples/posthog.rb | 153 +++++++ posthog-rails/lib/posthog-rails.rb | 7 + posthog-rails/lib/posthog/rails.rb | 14 + posthog-rails/lib/posthog/rails/active_job.rb | 85 ++++ .../lib/posthog/rails/capture_exceptions.rb | 134 ++++++ .../lib/posthog/rails/configuration.rb | 64 +++ .../lib/posthog/rails/error_subscriber.rb | 34 ++ .../lib/posthog/rails/parameter_filter.rb | 51 +++ posthog-rails/lib/posthog/rails/railtie.rb | 215 ++++++++++ .../rails/rescued_exception_interceptor.rb | 30 ++ posthog-rails/posthog-rails.gemspec | 23 ++ 16 files changed, 1550 insertions(+), 2 deletions(-) create mode 100644 posthog-rails/IMPLEMENTATION.md create mode 100644 posthog-rails/README.md create mode 100644 posthog-rails/examples/posthog.rb create mode 100644 posthog-rails/lib/posthog-rails.rb create mode 100644 posthog-rails/lib/posthog/rails.rb create mode 100644 posthog-rails/lib/posthog/rails/active_job.rb create mode 100644 posthog-rails/lib/posthog/rails/capture_exceptions.rb create mode 100644 posthog-rails/lib/posthog/rails/configuration.rb create mode 100644 posthog-rails/lib/posthog/rails/error_subscriber.rb create mode 100644 posthog-rails/lib/posthog/rails/parameter_filter.rb create mode 100644 posthog-rails/lib/posthog/rails/railtie.rb create mode 100644 posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb create mode 100644 posthog-rails/posthog-rails.gemspec 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..24e18df --- /dev/null +++ b/posthog-rails/examples/posthog.rb @@ -0,0 +1,153 @@ +# 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['POSTHOG_API_KEY'] + + # ============================================================================ + # 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['POSTHOG_PERSONAL_API_KEY'] + + # 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 + if Rails.env.test? + config.test_mode = true + end + + # 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..6528f71 --- /dev/null +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -0,0 +1,85 @@ +# 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 => exception + # Capture the exception with job context + capture_job_exception(exception) + 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) + if arguments.present? + properties['$job_arguments'] = sanitize_job_arguments(arguments) + end + + 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..caed042 --- /dev/null +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -0,0 +1,134 @@ +# 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) + + if exception && should_capture?(exception) + capture_exception(exception, env) + end + + response + rescue StandardError => exception + # Capture unhandled exceptions + capture_exception(exception, env) if should_capture?(exception) + raise + end + + private + + def collect_exception(env) + # Rails stores exceptions in these env keys + exception = env['action_dispatch.exception'] || + env['rack.exception'] || + env['posthog.rescued_exception'] + + 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(params) + + # 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..374b604 --- /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?([: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..6f97d47 --- /dev/null +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -0,0 +1,215 @@ +# 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 = {}, &block) + @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 + + def is_feature_enabled(*args, **kwargs) + 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! + unless initialized? + raise 'PostHog is not initialized. Call PostHog.init in an initializer.' + end + 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 + + private + + 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..e254792 --- /dev/null +++ b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb @@ -0,0 +1,30 @@ +# 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 => exception + # Store the exception so CaptureExceptions middleware can report it + if should_intercept? + env['posthog.rescued_exception'] = exception + end + raise exception + 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 From 472aaa7ddbdea274db9bf0e0853c8e16c54f4ac3 Mon Sep 17 00:00:00 2001 From: Drago Crnjac Date: Fri, 7 Nov 2025 14:08:03 +0100 Subject: [PATCH 02/31] fix rubocop errors --- .rubocop.yml | 4 ++++ posthog-rails/examples/posthog.rb | 8 +++----- posthog-rails/lib/posthog/rails/active_job.rb | 8 +++----- .../lib/posthog/rails/capture_exceptions.rb | 15 ++++++--------- .../lib/posthog/rails/error_subscriber.rb | 2 +- posthog-rails/lib/posthog/rails/railtie.rb | 19 +++++++++---------- .../rails/rescued_exception_interceptor.rb | 8 +++----- 7 files changed, 29 insertions(+), 35 deletions(-) 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/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 24e18df..3d9a8bb 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -10,7 +10,7 @@ # Your PostHog project API key (required) # Get this from: PostHog Project Settings > API Keys - config.api_key = ENV['POSTHOG_API_KEY'] + config.api_key = ENV.fetch('POSTHOG_API_KEY', nil) # ============================================================================ # CORE POSTHOG CONFIGURATION @@ -21,7 +21,7 @@ # Personal API key (optional, but required for local feature flag evaluation) # Get this from: PostHog Settings > Personal API Keys - config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY'] + 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 @@ -82,9 +82,7 @@ # ============================================================================ # Disable in test environment - if Rails.env.test? - config.test_mode = true - end + config.test_mode = true if Rails.env.test? # Optional: Disable in development # if Rails.env.development? diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb index 6528f71..4549738 100644 --- a/posthog-rails/lib/posthog/rails/active_job.rb +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -10,9 +10,9 @@ module ActiveJobExtensions def perform_now super - rescue StandardError => exception + rescue StandardError => e # Capture the exception with job context - capture_job_exception(exception) + capture_job_exception(e) raise end @@ -34,9 +34,7 @@ def capture_job_exception(exception) } # Add serialized job arguments (be careful with sensitive data) - if arguments.present? - properties['$job_arguments'] = sanitize_job_arguments(arguments) - end + properties['$job_arguments'] = sanitize_job_arguments(arguments) if arguments.present? PostHog.capture_exception(exception, distinct_id, properties) rescue StandardError => e diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index caed042..6350322 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -18,14 +18,12 @@ def call(env) # Check if there was an exception that Rails handled exception = collect_exception(env) - if exception && should_capture?(exception) - capture_exception(exception, env) - end + capture_exception(exception, env) if exception && should_capture?(exception) response - rescue StandardError => exception + rescue StandardError => e # Capture unhandled exceptions - capture_exception(exception, env) if should_capture?(exception) + capture_exception(e, env) if should_capture?(e) raise end @@ -33,11 +31,9 @@ def call(env) def collect_exception(env) # Rails stores exceptions in these env keys - exception = env['action_dispatch.exception'] || + env['action_dispatch.exception'] || env['rack.exception'] || env['posthog.rescued_exception'] - - exception end def should_capture?(exception) @@ -80,6 +76,7 @@ def extract_user_id(user) 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 @@ -116,7 +113,7 @@ def build_properties(request, env) 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(params) + filtered = super # Also truncate long values filtered.transform_values do |value| diff --git a/posthog-rails/lib/posthog/rails/error_subscriber.rb b/posthog-rails/lib/posthog/rails/error_subscriber.rb index 374b604..202ec9b 100644 --- a/posthog-rails/lib/posthog/rails/error_subscriber.rb +++ b/posthog-rails/lib/posthog/rails/error_subscriber.rb @@ -20,7 +20,7 @@ def report(error, handled:, severity:, context:, source: nil) # Add context information if context.present? context.each do |key, value| - properties["$context_#{key}"] = value unless key.in?([:user_id, :distinct_id]) + properties["$context_#{key}"] = value unless key.in?(%i[user_id distinct_id]) end end diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 6f97d47..4bdd318 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -4,13 +4,13 @@ module PostHog module Rails class Railtie < ::Rails::Railtie # Add PostHog module methods for accessing Rails-specific client - initializer 'posthog.set_configs' do |app| + 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 = {}, &block) + def init(options = {}) @rails_config ||= PostHog::Rails::Configuration.new # If block given, yield to configuration @@ -50,7 +50,9 @@ def group_identify(*args, **kwargs) client.group_identify(*args, **kwargs) end - def is_feature_enabled(*args, **kwargs) + # 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 @@ -72,9 +74,9 @@ def initialized? private def ensure_initialized! - unless initialized? - raise 'PostHog is not initialized. Call PostHog.init in an initializer.' - end + return if initialized? + + raise 'PostHog is not initialized. Call PostHog.init in an initializer.' end end end @@ -104,7 +106,7 @@ def ensure_initialized! end # After initialization, set up remaining integrations - config.after_initialize do |app| + config.after_initialize do |_app| next unless PostHog.initialized? # Register with Rails error reporter (Rails 7.0+) @@ -118,14 +120,11 @@ def ensure_initialized! end end - private - 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")}") diff --git a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb index e254792..8543ff7 100644 --- a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb +++ b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb @@ -12,12 +12,10 @@ def initialize(app) def call(env) @app.call(env) - rescue StandardError => exception + rescue StandardError => e # Store the exception so CaptureExceptions middleware can report it - if should_intercept? - env['posthog.rescued_exception'] = exception - end - raise exception + env['posthog.rescued_exception'] = e if should_intercept? + raise e end private From 1ccd588b5bebe8b23d2048d1ef131d8c391cbaa1 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 08:40:33 -0500 Subject: [PATCH 03/31] correct default URL Co-authored-by: Rafael Audibert <32079912+rafaeelaudibert@users.noreply.github.com> --- posthog-rails/examples/posthog.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 3d9a8bb..8e4efa9 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -17,7 +17,7 @@ # ============================================================================ # 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') + config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com') # Personal API key (optional, but required for local feature flag evaluation) # Get this from: PostHog Settings > Personal API Keys From 805b79e934956b54c4232647611a37cc8ab761b6 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 08:41:09 -0500 Subject: [PATCH 04/31] test mode example config which includes development Co-authored-by: Rafael Audibert <32079912+rafaeelaudibert@users.noreply.github.com> --- posthog-rails/examples/posthog.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 8e4efa9..61e077c 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -85,9 +85,7 @@ config.test_mode = true if Rails.env.test? # Optional: Disable in development - # if Rails.env.development? - # config.test_mode = true - # end + # config.test_mode = true if Rails.env.test? || Rails.env.development? end # ============================================================================ From 78e48525402ebf27468be5859b8f20a8378353b2 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 08:43:25 -0500 Subject: [PATCH 05/31] Add URLs to API key documentation in example config Added direct links to PostHog settings pages: - Project API key: https://app.posthog.com/settings/project-details#variables - Personal API key: https://app.posthog.com/settings/user-api-keys --- posthog-rails/examples/posthog.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 61e077c..98b549d 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -10,6 +10,7 @@ # Your PostHog project API key (required) # Get this from: PostHog Project Settings > API Keys + # https://app.posthog.com/settings/project-details#variables config.api_key = ENV.fetch('POSTHOG_API_KEY', nil) # ============================================================================ @@ -21,6 +22,7 @@ # Personal API key (optional, but required for local feature flag evaluation) # Get this from: PostHog Settings > Personal API Keys + # https://app.posthog.com/settings/user-api-keys config.personal_api_key = ENV.fetch('POSTHOG_PERSONAL_API_KEY', nil) # Maximum number of events to queue before dropping (default: 10000) From aca80a518b985af26c8f29190eb04aa8480eec7d Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 08:44:40 -0500 Subject: [PATCH 06/31] Comment out on_error callback in example config Keep it as an example users can uncomment if needed, consistent with other optional callbacks like before_send. --- posthog-rails/examples/posthog.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 98b549d..eac6088 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -35,9 +35,9 @@ 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}") - } + # 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 From b41b3f28af0cde80a57d8b2013135756104ae317 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 08:48:18 -0500 Subject: [PATCH 07/31] Add docs explaining benefits of current_user_method config --- posthog-rails/examples/posthog.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index eac6088..96ff283 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -69,7 +69,9 @@ 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 + # Change this if your app uses a different method name (e.g., :authenticated_user) + # When configured, exceptions will include user context (distinct_id, email, name), + # making it easier to identify affected users and debug user-specific issues. config.current_user_method = :current_user # Additional exception classes to exclude from reporting From 5226fc41139fcace74ce6ad84bcfe01c2c235114 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:00:25 -0500 Subject: [PATCH 08/31] refactor: move rails_config to PostHog::Rails.config namespace Move Rails-specific configuration from PostHog.rails_config to PostHog::Rails.config for cleaner namespacing. --- posthog-rails/IMPLEMENTATION.md | 2 +- posthog-rails/lib/posthog/rails.rb | 8 ++++++++ posthog-rails/lib/posthog/rails/active_job.rb | 2 +- posthog-rails/lib/posthog/rails/capture_exceptions.rb | 6 +++--- posthog-rails/lib/posthog/rails/error_subscriber.rb | 4 ++-- posthog-rails/lib/posthog/rails/railtie.rb | 8 +++----- .../lib/posthog/rails/rescued_exception_interceptor.rb | 2 +- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/posthog-rails/IMPLEMENTATION.md b/posthog-rails/IMPLEMENTATION.md index c4dd6c3..96162ce 100644 --- a/posthog-rails/IMPLEMENTATION.md +++ b/posthog-rails/IMPLEMENTATION.md @@ -60,7 +60,7 @@ The core Rails integration hook that: - Extends `PostHog` module with class methods - Adds `PostHog.init` configuration block - Adds delegation methods (`capture`, `capture_exception`, etc.) -- Stores singleton `client` and `rails_config` +- Stores singleton `client` (Rails config accessed via `PostHog::Rails.config`) #### Middleware Registration Inserts two middleware in the Rails stack: diff --git a/posthog-rails/lib/posthog/rails.rb b/posthog-rails/lib/posthog/rails.rb index 3502acb..9315dde 100644 --- a/posthog-rails/lib/posthog/rails.rb +++ b/posthog-rails/lib/posthog/rails.rb @@ -10,5 +10,13 @@ module PostHog module Rails VERSION = PostHog::VERSION + + class << self + def config + @config ||= Configuration.new + end + + attr_writer :config + end end end diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb index 4549738..8e0a04c 100644 --- a/posthog-rails/lib/posthog/rails/active_job.rb +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -19,7 +19,7 @@ def perform_now private def capture_job_exception(exception) - return unless PostHog.rails_config&.auto_instrument_active_job + return unless PostHog::Rails.config&.auto_instrument_active_job # Build distinct_id from job arguments if possible distinct_id = extract_distinct_id_from_job diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index 6350322..9cf3406 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -37,8 +37,8 @@ def collect_exception(env) end def should_capture?(exception) - return false unless PostHog.rails_config&.auto_capture_exceptions - return false unless PostHog.rails_config&.should_capture_exception?(exception) + return false unless PostHog::Rails.config&.auto_capture_exceptions + return false unless PostHog::Rails.config&.should_capture_exception?(exception) true end @@ -58,7 +58,7 @@ 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 + method_name = PostHog::Rails.config&.current_user_method || :current_user if controller.respond_to?(method_name, true) user = controller.send(method_name) diff --git a/posthog-rails/lib/posthog/rails/error_subscriber.rb b/posthog-rails/lib/posthog/rails/error_subscriber.rb index 202ec9b..174a2a4 100644 --- a/posthog-rails/lib/posthog/rails/error_subscriber.rb +++ b/posthog-rails/lib/posthog/rails/error_subscriber.rb @@ -6,8 +6,8 @@ module Rails # 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) + 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] diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 4bdd318..2d4a337 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -7,15 +7,13 @@ class Railtie < ::Rails::Railtie initializer 'posthog.set_configs' do |_app| PostHog.class_eval do class << self - attr_accessor :rails_config, :client + attr_accessor :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) + config = PostHog::Rails::InitConfig.new(PostHog::Rails.config, options) yield config options = config.to_client_options end @@ -121,7 +119,7 @@ def ensure_initialized! end def self.register_error_subscriber - return unless PostHog.rails_config&.auto_capture_exceptions + return unless PostHog::Rails.config&.auto_capture_exceptions subscriber = PostHog::Rails::ErrorSubscriber.new ::Rails.error.subscribe(subscriber) diff --git a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb index 8543ff7..3658a69 100644 --- a/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb +++ b/posthog-rails/lib/posthog/rails/rescued_exception_interceptor.rb @@ -21,7 +21,7 @@ def call(env) private def should_intercept? - PostHog.rails_config&.report_rescued_exceptions + PostHog::Rails.config&.report_rescued_exceptions end end end From 5b75d223b9e35dc6e613001b41c19959c5f0a6e1 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:09:17 -0500 Subject: [PATCH 09/31] feat(rails): Add user_id_method config and pk support for ID extraction Add configurable `user_id_method` option and support for `posthog_distinct_id`, `distinct_id`, and `pk` methods when extracting user IDs for exception tracking. --- posthog-rails/README.md | 40 ++++++++++++++++++- .../lib/posthog/rails/capture_exceptions.rb | 10 +++++ .../lib/posthog/rails/configuration.rb | 5 +++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index 2eb5e20..e8893db 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -49,6 +49,7 @@ PostHog.init do |config| 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 + config.user_id_method = nil # Method to get ID from user (auto-detect) # Add additional exceptions to ignore config.excluded_exceptions = ['MyCustomError'] @@ -180,6 +181,7 @@ Rails.error.record(exception, context: { user_id: current_user.id }) | `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 | +| `user_id_method` | Symbol | `nil` | Method to extract ID from user object (auto-detect if nil) | | `excluded_exceptions` | Array | `[]` | Additional exceptions to ignore | ### Understanding Exception Tracking Options @@ -244,6 +246,39 @@ If your user method has a different name, configure it: config.current_user_method = :logged_in_user ``` +### User ID Extraction + +By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order: + +1. `posthog_distinct_id` - Define this on your User model for full control +2. `distinct_id` - Common analytics convention +3. `id` - Standard ActiveRecord primary key +4. `pk` - Primary key alias +5. `uuid` - For UUID-based primary keys + +**Option 1: Configure a specific method** + +```ruby +# config/initializers/posthog.rb +config.user_id_method = :email # or :external_id, :customer_id, etc. +``` + +**Option 2: Define a method on your User model** + +```ruby +class User < ApplicationRecord + def posthog_distinct_id + # Custom logic for your distinct ID + "user_#{id}" # or external_id, or any unique identifier + end +end +``` + +This approach is useful when you want to: +- Use a different identifier than the database ID (e.g., `external_id`) +- Prefix IDs to distinguish user types +- Use composite identifiers + ## Sensitive Data Filtering PostHog Rails automatically filters sensitive parameters: @@ -320,8 +355,9 @@ PostHog Rails uses the following components: ### 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 +2. Check that the user object responds to one of: `posthog_distinct_id`, `distinct_id`, `id`, `pk`, or `uuid` +3. If using a custom identifier, set `config.user_id_method = :your_method` +4. Enable logging to see what's being captured ### Feature flags not working diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index 9cf3406..bcf94e0 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -71,9 +71,19 @@ def extract_distinct_id(env, request) end def extract_user_id(user) + # Use configured method if specified + method_name = PostHog::Rails.config&.user_id_method + return user.send(method_name) if method_name && user.respond_to?(method_name) + + # Try explicit PostHog method (allows users to customize without config) + return user.posthog_distinct_id if user.respond_to?(:posthog_distinct_id) + return user.distinct_id if user.respond_to?(:distinct_id) + # Try common ID methods return user.id if user.respond_to?(:id) return user['id'] if user.respond_to?(:[]) && user['id'] + return user.pk if user.respond_to?(:pk) + return user['pk'] if user.respond_to?(:[]) && user['pk'] return user.uuid if user.respond_to?(:uuid) return user['uuid'] if user.respond_to?(:[]) && user['uuid'] diff --git a/posthog-rails/lib/posthog/rails/configuration.rb b/posthog-rails/lib/posthog/rails/configuration.rb index f36781d..5352861 100644 --- a/posthog-rails/lib/posthog/rails/configuration.rb +++ b/posthog-rails/lib/posthog/rails/configuration.rb @@ -21,6 +21,10 @@ class Configuration # Method name to call on controller to get user ID (default: :current_user) attr_accessor :current_user_method + # Method name to call on user object to get distinct_id (default: auto-detect) + # When nil, tries: posthog_distinct_id, distinct_id, id, pk, uuid in order + attr_accessor :user_id_method + def initialize @auto_capture_exceptions = true @report_rescued_exceptions = true @@ -28,6 +32,7 @@ def initialize @excluded_exceptions = [] @capture_user_context = true @current_user_method = :current_user + @user_id_method = nil end # Default exceptions that Rails apps typically don't want to track From e177372d556fec8e76cb54f13f56f72aabe27c98 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:11:52 -0500 Subject: [PATCH 10/31] rescue on individual entries Co-authored-by: Rafael Audibert <32079912+rafaeelaudibert@users.noreply.github.com> --- posthog-rails/lib/posthog/rails/capture_exceptions.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index bcf94e0..e78a11e 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -132,6 +132,8 @@ def filter_sensitive_params(params) else value end + rescue + value end rescue StandardError {} # Return empty hash if filtering fails From ea81dd46b7fd3c3db5ab5883e4d53f0f703c62b9 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:19:55 -0500 Subject: [PATCH 11/31] Separate Rails config from PostHog.init Move Rails-specific options (auto_capture_exceptions, etc.) to PostHog::Rails.configure, keeping core SDK options in PostHog.init. This follows the common Rails gem pattern of namespaced configuration. --- posthog-rails/README.md | 41 +++++++----- posthog-rails/examples/posthog.rb | 72 +++++++++++++--------- posthog-rails/lib/posthog/rails.rb | 4 ++ posthog-rails/lib/posthog/rails/railtie.rb | 32 +--------- 4 files changed, 74 insertions(+), 75 deletions(-) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index e8893db..55b1459 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -33,17 +33,8 @@ bundle install 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 +# Rails-specific configuration +PostHog::Rails.configure do |config| 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 @@ -53,6 +44,18 @@ PostHog.init do |config| # Add additional exceptions to ignore config.excluded_exceptions = ['MyCustomError'] +end + +# Core PostHog client initialization +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'] # Error callback config.on_error = proc { |status, msg| @@ -61,6 +64,12 @@ PostHog.init do |config| end ``` +You can also configure Rails options directly: + +```ruby +PostHog::Rails.config.auto_capture_exceptions = true +``` + ### Environment Variables The recommended approach is to use environment variables: @@ -174,6 +183,8 @@ Rails.error.record(exception, context: { user_id: current_user.id }) ### Rails-Specific Options +Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: + | Option | Type | Default | Description | |--------|------|---------|-------------| | `auto_capture_exceptions` | Boolean | `true` | Automatically capture exceptions | @@ -225,7 +236,7 @@ The following exceptions are not reported by default (common 4xx errors): - `ActiveRecord::RecordNotFound` - `ActiveRecord::RecordNotUnique` -You can add more with `config.excluded_exceptions = ['MyException']`. +You can add more with `PostHog::Rails.config.excluded_exceptions = ['MyException']`. ## User Context @@ -243,7 +254,7 @@ end If your user method has a different name, configure it: ```ruby -config.current_user_method = :logged_in_user +PostHog::Rails.config.current_user_method = :logged_in_user ``` ### User ID Extraction @@ -260,7 +271,7 @@ By default, PostHog Rails auto-detects the user's distinct ID by trying these me ```ruby # config/initializers/posthog.rb -config.user_id_method = :email # or :external_id, :customer_id, etc. +PostHog::Rails.config.user_id_method = :email # or :external_id, :customer_id, etc. ``` **Option 2: Define a method on your User model** @@ -356,7 +367,7 @@ PostHog Rails uses the following components: 1. Verify `current_user_method` matches your controller method 2. Check that the user object responds to one of: `posthog_distinct_id`, `distinct_id`, `id`, `pk`, or `uuid` -3. If using a custom identifier, set `config.user_id_method = :your_method` +3. If using a custom identifier, set `PostHog::Rails.config.user_id_method = :your_method` 4. Enable logging to see what's being captured ### Feature flags not working diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 96ff283..e2fb9ac 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -3,6 +3,47 @@ # PostHog Rails Initializer # Place this file in config/initializers/posthog.rb +# ============================================================================ +# RAILS-SPECIFIC CONFIGURATION +# ============================================================================ +# Configure Rails-specific options via PostHog::Rails.configure +# These settings control how PostHog integrates with Rails features. + +PostHog::Rails.configure do |config| + # 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 (e.g., :authenticated_user) + # When configured, exceptions will include user context (distinct_id, email, name), + # making it easier to identify affected users and debug user-specific issues. + 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' + ] +end + +# You can also configure Rails options directly: +# PostHog::Rails.config.auto_capture_exceptions = true + +# ============================================================================ +# CORE POSTHOG CONFIGURATION +# ============================================================================ +# Initialize the PostHog client with core SDK options. + PostHog.init do |config| # ============================================================================ # REQUIRED CONFIGURATION @@ -14,7 +55,7 @@ config.api_key = ENV.fetch('POSTHOG_API_KEY', nil) # ============================================================================ - # CORE POSTHOG CONFIGURATION + # OPTIONAL CONFIGURATION # ============================================================================ # For PostHog Cloud, use: https://us.i.posthog.com or https://eu.i.posthog.com @@ -52,35 +93,6 @@ # 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 (e.g., :authenticated_user) - # When configured, exceptions will include user context (distinct_id, email, name), - # making it easier to identify affected users and debug user-specific issues. - 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 # ============================================================================ diff --git a/posthog-rails/lib/posthog/rails.rb b/posthog-rails/lib/posthog/rails.rb index 9315dde..305444f 100644 --- a/posthog-rails/lib/posthog/rails.rb +++ b/posthog-rails/lib/posthog/rails.rb @@ -17,6 +17,10 @@ def config end attr_writer :config + + def configure + yield config if block_given? + end end end end diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 2d4a337..d67d813 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -13,7 +13,7 @@ class << self def init(options = {}) # If block given, yield to configuration if block_given? - config = PostHog::Rails::InitConfig.new(PostHog::Rails.config, options) + config = PostHog::Rails::InitConfig.new(options) yield config options = config.to_client_options end @@ -135,10 +135,7 @@ def self.rails_version_above_7? # Configuration wrapper for the init block class InitConfig - attr_reader :rails_config - - def initialize(rails_config, base_options = {}) - @rails_config = rails_config + def initialize(base_options = {}) @base_options = base_options end @@ -179,31 +176,6 @@ 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 From e127f14dedf2588aa354c197f5562dcbac6efbbe Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:20:38 -0500 Subject: [PATCH 12/31] remove LLM-y file --- posthog-rails/IMPLEMENTATION.md | 384 -------------------------------- 1 file changed, 384 deletions(-) delete mode 100644 posthog-rails/IMPLEMENTATION.md diff --git a/posthog-rails/IMPLEMENTATION.md b/posthog-rails/IMPLEMENTATION.md deleted file mode 100644 index 96162ce..0000000 --- a/posthog-rails/IMPLEMENTATION.md +++ /dev/null @@ -1,384 +0,0 @@ -# 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` (Rails config accessed via `PostHog::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. From 58fe569b7ee3e159b8517ed016c4441e114b2ce4 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:22:32 -0500 Subject: [PATCH 13/31] docs: clarify only ActiveJob is supported, other runners coming soon --- posthog-rails/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index 55b1459..2276536 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -138,6 +138,8 @@ class EmailJob < ApplicationJob end ``` +> **Note:** Currently only ActiveJob is supported. Support for other job runners (Sidekiq, Resque, Good Job, etc.) is planned for future releases. Contributions are welcome! + ### Feature Flags Use feature flags in your Rails app: From 0e6aa55de9816efa3926cc1efc45ce294d8a291f Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:29:03 -0500 Subject: [PATCH 14/31] Add Rails install generator for posthog:install command - Create generator at lib/generators/posthog/install_generator.rb that copies the initializer template to config/initializers/posthog.rb - Remove USAGE EXAMPLES section from examples/posthog.rb to keep it as a clean, focused initializer template for customers - Document the generator in README with usage instructions Users can now run `rails generate posthog:install` to automatically set up PostHog in their Rails application. --- posthog-rails/README.md | 12 +++++- posthog-rails/examples/posthog.rb | 39 ------------------- .../generators/posthog/install_generator.rb | 31 +++++++++++++++ 3 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 posthog-rails/lib/generators/posthog/install_generator.rb diff --git a/posthog-rails/README.md b/posthog-rails/README.md index 2276536..300ad1b 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -28,9 +28,19 @@ bundle install **Note:** `posthog-rails` depends on `posthog-ruby`, but it's recommended to explicitly include both gems in your Gemfile for clarity. +### Generate the Initializer + +Run the install generator to create the PostHog initializer: + +```bash +rails generate posthog:install +``` + +This will create `config/initializers/posthog.rb` with sensible defaults and documentation. + ## Configuration -Create an initializer at `config/initializers/posthog.rb`: +The generated initializer at `config/initializers/posthog.rb` includes all available options: ```ruby # Rails-specific configuration diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index e2fb9ac..7685cc4 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -124,42 +124,3 @@ # - 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/generators/posthog/install_generator.rb b/posthog-rails/lib/generators/posthog/install_generator.rb new file mode 100644 index 0000000..f13b10e --- /dev/null +++ b/posthog-rails/lib/generators/posthog/install_generator.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails/generators' + +module Posthog + module Generators + class InstallGenerator < ::Rails::Generators::Base + desc 'Creates a PostHog initializer file at config/initializers/posthog.rb' + + source_root File.expand_path('../../..', __dir__) + + def copy_initializer + copy_file 'examples/posthog.rb', 'config/initializers/posthog.rb' + end + + def show_readme + say '' + say 'PostHog Rails has been installed!', :green + say '' + say 'Next steps:', :yellow + say ' 1. Edit config/initializers/posthog.rb with your PostHog API key' + say ' 2. Set environment variables:' + say ' - POSTHOG_API_KEY (required)' + say ' - POSTHOG_PERSONAL_API_KEY (optional, for feature flags)' + say '' + say 'For more information, see: https://posthog.com/docs/libraries/ruby' + say '' + end + end + end +end From 8c40bd0581bef85a48c8ead93b6d1f042d1a08a2 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:31:53 -0500 Subject: [PATCH 15/31] Abort loading posthog-rails if Rails is not defined --- posthog-rails/lib/posthog-rails.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/posthog-rails/lib/posthog-rails.rb b/posthog-rails/lib/posthog-rails.rb index d29d3d6..f2160c9 100644 --- a/posthog-rails/lib/posthog-rails.rb +++ b/posthog-rails/lib/posthog-rails.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true +unless defined?(Rails) + raise LoadError, 'posthog-rails requires Rails. Use the posthog-ruby gem directly for non-Rails applications.' +end + # Load core PostHog Ruby SDK require 'posthog' # Load Rails integration -require 'posthog/rails' if defined?(Rails) +require 'posthog/rails' From 3b0d9ca162c744844e2cda8344974f527bedb08f Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:36:23 -0500 Subject: [PATCH 16/31] Add posthog_distinct_id DSL for ActiveJob distinct_id extraction --- posthog-rails/README.md | 46 +++++++++++++++++++ posthog-rails/lib/posthog/rails/active_job.rb | 31 +++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index 300ad1b..f2e53df 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -148,6 +148,52 @@ class EmailJob < ApplicationJob end ``` +#### Associating Jobs with Users + +By default, PostHog tries to extract a `distinct_id` from job arguments by looking for a `user_id` key in hash arguments: + +```ruby +class ProcessOrderJob < ApplicationJob + def perform(order_id, options = {}) + # PostHog will automatically use options[:user_id] or options['user_id'] + # as the distinct_id if present + end +end + +# Call with user context +ProcessOrderJob.perform_later(order.id, user_id: current_user.id) +``` + +#### Custom Distinct ID Extraction + +For more control, use the `posthog_distinct_id` class method to define exactly how to extract the user's distinct ID from your job arguments: + +```ruby +class SendWelcomeEmailJob < ApplicationJob + posthog_distinct_id ->(user, options) { user.id } + + def perform(user, options = {}) + UserMailer.welcome(user).deliver_now + end +end +``` + +You can also use a block: + +```ruby +class ProcessOrderJob < ApplicationJob + posthog_distinct_id do |order, notify_user_id| + notify_user_id + end + + def perform(order, notify_user_id) + # Process the order... + end +end +``` + +The proc/block receives the same arguments as `perform`, so you can extract the distinct ID however makes sense for your job. + > **Note:** Currently only ActiveJob is supported. Support for other job runners (Sidekiq, Resque, Good Job, etc.) is planned for future releases. Contributions are welcome! ### Feature Flags diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb index 8e0a04c..49e5a8a 100644 --- a/posthog-rails/lib/posthog/rails/active_job.rb +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -8,6 +8,28 @@ module Rails module ActiveJobExtensions include ParameterFilter + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + # DSL for defining how to extract distinct_id from job arguments + # Example: + # class MyJob < ApplicationJob + # posthog_distinct_id ->(user, arg1, arg2) { user.id } + # def perform(user, arg1, arg2) + # # ... + # end + # end + def posthog_distinct_id(proc = nil, &block) + @posthog_distinct_id_proc = proc || block + end + + def posthog_distinct_id_proc + @posthog_distinct_id_proc + end + end + def perform_now super rescue StandardError => e @@ -43,11 +65,12 @@ def capture_job_exception(exception) end def extract_distinct_id_from_job - # Try to find a user ID in job arguments + # First, check if the job class defines a custom extractor + return self.class.posthog_distinct_id_proc.call(*arguments) if self.class.posthog_distinct_id_proc + + # Fallback: look for explicit user_id in hash arguments only arguments.each do |arg| - if arg.respond_to?(:id) - return arg.id - elsif arg.is_a?(Hash) && arg['user_id'] + if arg.is_a?(Hash) && arg['user_id'] return arg['user_id'] elsif arg.is_a?(Hash) && arg[:user_id] return arg[:user_id] From ddb82423f9b4c6377f592d30b4d95b095ab2da76 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:42:43 -0500 Subject: [PATCH 17/31] docs: clarify user_id/distinct_id detection in Rails error reporter context --- posthog-rails/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index f2e53df..c9db119 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -225,6 +225,8 @@ end Rails.error.record(exception, context: { user_id: current_user.id }) ``` +PostHog will automatically extract the user's distinct ID from either `user_id` or `distinct_id` in the context hash (checking `user_id` first). Any other context keys are included as properties on the exception event. + ## Configuration Options ### Core PostHog Options From e50ad5200d347cd10780b56911289e172cd9f7af Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:47:00 -0500 Subject: [PATCH 18/31] refactor(rails): use metaprogramming for client method delegation Replace repetitive method definitions with define_method loop and add method_missing fallback for future client methods. --- posthog-rails/lib/posthog/rails/railtie.rb | 71 ++++++++++------------ 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index d67d813..9962226 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -9,6 +9,18 @@ class Railtie < ::Rails::Railtie class << self attr_accessor :client + # Methods explicitly delegated to the client + DELEGATED_METHODS = %i[ + capture + capture_exception + identify + alias + group_identify + is_feature_enabled + get_feature_flag + get_all_flags + ].freeze + # Initialize PostHog client with a block configuration def init(options = {}) # If block given, yield to configuration @@ -22,52 +34,33 @@ def init(options = {}) @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) + # Define delegated methods using metaprogramming + DELEGATED_METHODS.each do |method_name| + define_method(method_name) do |*args, **kwargs, &block| + ensure_initialized! + client.public_send(method_name, *args, **kwargs, &block) + end end - def get_feature_flag(*args, **kwargs) - ensure_initialized! - client.get_feature_flag(*args, **kwargs) + def initialized? + !@client.nil? end - def get_all_flags(*args, **kwargs) - ensure_initialized! - client.get_all_flags(*args, **kwargs) + # Fallback for any client methods not explicitly defined + # rubocop:disable Lint/RedundantSafeNavigation + def method_missing(method_name, ...) + if client&.respond_to?(method_name) + ensure_initialized! + client.public_send(method_name, ...) + else + super + end end - def initialized? - !@client.nil? + def respond_to_missing?(method_name, include_private = false) + client&.respond_to?(method_name) || super end + # rubocop:enable Lint/RedundantSafeNavigation private From 611ee39cd8957c56f780f8375d6f5f6bb8e06585 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 09:56:07 -0500 Subject: [PATCH 19/31] Fix session ID extraction in exception capture middleware - Use request.session.id instead of session_options.dig(:id) - session_options returns Session::Options, not a Hash --- posthog-rails/lib/posthog/rails/capture_exceptions.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index e78a11e..3b348dd 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -67,7 +67,7 @@ def extract_distinct_id(env, request) end # Fallback to session ID or nil - request.session_options&.dig(:id) + request.session&.id end def extract_user_id(user) @@ -132,7 +132,7 @@ def filter_sensitive_params(params) else value end - rescue + rescue StandardError value end rescue StandardError From 2331f35b3bec06d54bf7d553b3964337f890d057 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 12:02:55 -0500 Subject: [PATCH 20/31] fix(rails): safely serialize exception context to prevent stack overflow Add safe_serialize to handle circular references and complex objects in exception context. Tracks visited objects, limits depth/size, and converts non-serializable objects to safe string representations. Prevents SystemStackError when Rails error reporter context contains ActiveRecord models or other objects with circular references. --- .../lib/posthog/rails/capture_exceptions.rb | 36 +++------ .../lib/posthog/rails/error_subscriber.rb | 12 ++- .../lib/posthog/rails/parameter_filter.rb | 76 +++++++++++++++++++ 3 files changed, 94 insertions(+), 30 deletions(-) diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index 3b348dd..b5586aa 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -93,51 +93,33 @@ def extract_user_id(user) def build_properties(request, env) properties = { '$exception_source' => 'rails', - '$request_url' => request.url, - '$request_method' => request.method, - '$request_path' => request.path + '$request_url' => safe_serialize(request.url), + '$request_method' => safe_serialize(request.method), + '$request_path' => safe_serialize(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 + properties['$controller'] = safe_serialize(controller.controller_name) + properties['$action'] = safe_serialize(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? + # Safe serialize to handle any complex objects in params + properties['$request_params'] = safe_serialize(filtered_params) unless filtered_params.empty? end # Add user agent - properties['$user_agent'] = request.user_agent if request.user_agent + properties['$user_agent'] = safe_serialize(request.user_agent) if request.user_agent # Add referrer - properties['$referrer'] = request.referrer if request.referrer + properties['$referrer'] = safe_serialize(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 - rescue StandardError - value - end - rescue StandardError - {} # Return empty hash if filtering fails - end end end end diff --git a/posthog-rails/lib/posthog/rails/error_subscriber.rb b/posthog-rails/lib/posthog/rails/error_subscriber.rb index 174a2a4..9001e4d 100644 --- a/posthog-rails/lib/posthog/rails/error_subscriber.rb +++ b/posthog-rails/lib/posthog/rails/error_subscriber.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true +require 'posthog/rails/parameter_filter' + module PostHog module Rails # Rails 7.0+ error reporter integration # This integrates with Rails.error.handle and Rails.error.record class ErrorSubscriber + include ParameterFilter + 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) @@ -14,13 +18,15 @@ def report(error, handled:, severity:, context:, source: nil) properties = { '$exception_source' => source || 'rails_error_reporter', '$exception_handled' => handled, - '$exception_severity' => severity + '$exception_severity' => severity.to_s } - # Add context information + # Add context information (safely serialized to avoid circular references) if context.present? context.each do |key, value| - properties["$context_#{key}"] = value unless key.in?(%i[user_id distinct_id]) + next if key.in?(%i[user_id distinct_id]) + + properties["$context_#{key}"] = safe_serialize(value) end end diff --git a/posthog-rails/lib/posthog/rails/parameter_filter.rb b/posthog-rails/lib/posthog/rails/parameter_filter.rb index b46fc91..1fcac40 100644 --- a/posthog-rails/lib/posthog/rails/parameter_filter.rb +++ b/posthog-rails/lib/posthog/rails/parameter_filter.rb @@ -20,6 +20,8 @@ module Rails # end module ParameterFilter EMPTY_HASH = {}.freeze + MAX_STRING_LENGTH = 10_000 + MAX_DEPTH = 10 if ::Rails.version.to_f >= 6.0 def self.backend @@ -46,6 +48,80 @@ def filter_sensitive_params(params) parameter_filter.filter(params) end + + # Safely serialize a value to a JSON-compatible format. + # + # Handles circular references and complex objects by converting them to + # simple primitives or string representations. This prevents SystemStackError + # when serializing objects with circular references (like ActiveRecord models). + # + # @param value [Object] The value to serialize + # @param seen [Set] Set of object_ids already visited (for cycle detection) + # @param depth [Integer] Current recursion depth + # @return [Object] A JSON-safe value (String, Numeric, Boolean, nil, Array, or Hash) + def safe_serialize(value, seen = Set.new, depth = 0) + return '[max depth exceeded]' if depth > MAX_DEPTH + + case value + when nil, true, false, Integer, Float + value + when String + truncate_string(value) + when Symbol + value.to_s + when Time, DateTime + value.iso8601(3) + when Date + value.iso8601 + when Array + serialize_array(value, seen, depth) + when Hash + serialize_hash(value, seen, depth) + else + serialize_object(value, seen) + end + rescue StandardError => e + "[serialization error: #{e.class}]" + end + + private + + def truncate_string(str) + return str if str.length <= MAX_STRING_LENGTH + + "#{str[0...MAX_STRING_LENGTH]}... (truncated)" + end + + def serialize_array(array, seen, depth) + return '[circular reference]' if seen.include?(array.object_id) + + seen = seen.dup.add(array.object_id) + array.first(100).map { |item| safe_serialize(item, seen, depth + 1) } + end + + def serialize_hash(hash, seen, depth) + return '[circular reference]' if seen.include?(hash.object_id) + + seen = seen.dup.add(hash.object_id) + result = {} + hash.first(100).each do |key, val| + result[key.to_s] = safe_serialize(val, seen, depth + 1) + end + result + end + + def serialize_object(obj, seen) + return '[circular reference]' if seen.include?(obj.object_id) + + # For ActiveRecord and similar objects, use id if available + return "#{obj.class.name}##{obj.id}" if obj.respond_to?(:id) && obj.respond_to?(:class) + + # Try to_s as fallback, but limit length + str = obj.to_s + truncate_string(str) + rescue StandardError + "[#{obj.class.name}]" + end end end end From 35d73eea05405b0298b59c6a5b6e5045daee353f Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sat, 17 Jan 2026 12:12:01 -0500 Subject: [PATCH 21/31] Fix duplicate exception capture for web requests When auto_capture_exceptions was enabled, exceptions in web requests were being captured twice: 1. By CaptureExceptions middleware (after ShowExceptions) 2. By ErrorSubscriber (via Rails.error.subscribe in Rails 7.0+) The ErrorSubscriber fires from within DebugExceptions before the CaptureExceptions middleware catches the exception, causing both to report the same error to PostHog. Fix by tracking web request context via thread-local flag: - CaptureExceptions sets flag at request start, clears in ensure block - ErrorSubscriber checks flag and skips if in web request context This ensures web requests are captured only by the middleware (which has richer context: URL, params, controller, action), while ErrorSubscriber still handles non-web contexts (ActiveJob, manual Rails.error.record). --- posthog-rails/lib/posthog/rails.rb | 20 +++++++++++++++++++ .../lib/posthog/rails/capture_exceptions.rb | 6 ++++++ .../lib/posthog/rails/error_subscriber.rb | 3 +++ 3 files changed, 29 insertions(+) diff --git a/posthog-rails/lib/posthog/rails.rb b/posthog-rails/lib/posthog/rails.rb index 305444f..450b3e1 100644 --- a/posthog-rails/lib/posthog/rails.rb +++ b/posthog-rails/lib/posthog/rails.rb @@ -11,6 +11,9 @@ module PostHog module Rails VERSION = PostHog::VERSION + # Thread-local key for tracking web request context + IN_WEB_REQUEST_KEY = :posthog_in_web_request + class << self def config @config ||= Configuration.new @@ -21,6 +24,23 @@ def config def configure yield config if block_given? end + + # Mark that we're in a web request context + # CaptureExceptions middleware will handle exception capture + def enter_web_request + Thread.current[IN_WEB_REQUEST_KEY] = true + end + + # Clear web request context (called at end of request) + def exit_web_request + Thread.current[IN_WEB_REQUEST_KEY] = false + end + + # Check if we're currently in a web request context + # Used by ErrorSubscriber to avoid duplicate captures + def in_web_request? + Thread.current[IN_WEB_REQUEST_KEY] == true + end end end end diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index b5586aa..0b3eae3 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -13,6 +13,10 @@ def initialize(app) end def call(env) + # Signal that we're in a web request context + # ErrorSubscriber will skip capture for web requests to avoid duplicates + PostHog::Rails.enter_web_request + response = @app.call(env) # Check if there was an exception that Rails handled @@ -25,6 +29,8 @@ def call(env) # Capture unhandled exceptions capture_exception(e, env) if should_capture?(e) raise + ensure + PostHog::Rails.exit_web_request end private diff --git a/posthog-rails/lib/posthog/rails/error_subscriber.rb b/posthog-rails/lib/posthog/rails/error_subscriber.rb index 9001e4d..0d163c7 100644 --- a/posthog-rails/lib/posthog/rails/error_subscriber.rb +++ b/posthog-rails/lib/posthog/rails/error_subscriber.rb @@ -12,6 +12,9 @@ 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) + # Skip if in a web request - CaptureExceptions middleware will handle it + # with richer context (URL, params, controller, etc.) + return if PostHog::Rails.in_web_request? distinct_id = context[:user_id] || context[:distinct_id] From ea6a8039965ff6f4ef573b7e9da836ede7f1bf06 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Tue, 27 Jan 2026 15:38:46 -0500 Subject: [PATCH 22/31] feat(rails): make automatic error tracking opt-in by default Change auto_capture_exceptions, report_rescued_exceptions, and auto_instrument_active_job to default to false, requiring explicit opt-in for automatic error tracking features. --- posthog-rails/README.md | 34 +++++++++---------- posthog-rails/examples/posthog.rb | 9 +++-- .../lib/posthog/rails/configuration.rb | 6 ++-- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index c9db119..d54582e 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -45,9 +45,9 @@ The generated initializer at `config/initializers/posthog.rb` includes all avail ```ruby # Rails-specific configuration PostHog::Rails.configure do |config| - 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.auto_capture_exceptions = true # Enable automatic exception capture (default: false) + config.report_rescued_exceptions = true # Report exceptions Rails rescues (default: false) + config.auto_instrument_active_job = true # Instrument background jobs (default: false) config.capture_user_context = true # Include user info in exceptions config.current_user_method = :current_user # Method to get current user config.user_id_method = nil # Method to get ID from user (auto-detect) @@ -94,7 +94,7 @@ POSTHOG_PERSONAL_API_KEY=your_personal_api_key # Optional, for feature flags ### Automatic Exception Tracking -Once configured, exceptions are automatically captured: +When `auto_capture_exceptions` is enabled, exceptions are automatically captured: ```ruby class PostsController < ApplicationController @@ -136,7 +136,7 @@ PostHog.capture_exception( ### Background Jobs -ActiveJob exceptions are automatically captured: +When `auto_instrument_active_job` is enabled, ActiveJob exceptions are automatically captured: ```ruby class EmailJob < ApplicationJob @@ -247,9 +247,9 @@ Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: | 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 | +| `auto_capture_exceptions` | Boolean | `false` | Automatically capture exceptions | +| `report_rescued_exceptions` | Boolean | `false` | Report exceptions Rails rescues | +| `auto_instrument_active_job` | Boolean | `false` | Instrument ActiveJob | | `capture_user_context` | Boolean | `true` | Include user info | | `current_user_method` | Symbol | `:current_user` | Controller method for user | | `user_id_method` | Symbol | `nil` | Method to extract ID from user object (auto-detect if nil) | @@ -257,15 +257,15 @@ Configure these via `PostHog::Rails.configure` or `PostHog::Rails.config`: ### Understanding Exception Tracking Options -**`auto_capture_exceptions`** - Master switch for all automatic error tracking +**`auto_capture_exceptions`** - Master switch for all automatic error tracking (default: `false`) - 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 +- When `false` (default): No automatic error tracking (you must manually call `PostHog.capture_exception`) +- **Use case:** Enable to get automatic error tracking -**`report_rescued_exceptions`** - Control exceptions that Rails handles gracefully +**`report_rescued_exceptions`** - Control exceptions that Rails handles gracefully (default: `false`) - 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 +- When `false` (default): Only capture truly unhandled exceptions that crash your app +- **Use case:** Enable along with `auto_capture_exceptions` for complete error visibility **Example:** @@ -278,11 +278,11 @@ end | Configuration | Result | |---------------|--------| -| `auto_capture_exceptions = true`
`report_rescued_exceptions = true` | ✅ Exception captured (default behavior) | +| `auto_capture_exceptions = true`
`report_rescued_exceptions = true` | ✅ Exception captured | | `auto_capture_exceptions = true`
`report_rescued_exceptions = false` | ❌ Not captured (Rails rescued it) | -| `auto_capture_exceptions = false` | ❌ Not captured (automatic tracking disabled) | +| `auto_capture_exceptions = false` | ❌ Not captured (automatic tracking disabled, this is the default) | -**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. +**Recommendation:** Enable both options to get complete visibility into all errors. Set `report_rescued_exceptions = false` if you only want to track critical crashes. ## Excluded Exceptions by Default diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index 7685cc4..a39b861 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -10,13 +10,16 @@ # These settings control how PostHog integrates with Rails features. PostHog::Rails.configure do |config| - # Automatically capture exceptions (default: true) + # Automatically capture exceptions (default: false) + # Set to true to enable automatic exception tracking config.auto_capture_exceptions = true - # Report exceptions that Rails rescues (e.g., with rescue_from) (default: true) + # Report exceptions that Rails rescues (e.g., with rescue_from) (default: false) + # Set to true to capture rescued exceptions config.report_rescued_exceptions = true - # Automatically instrument ActiveJob background jobs (default: true) + # Automatically instrument ActiveJob background jobs (default: false) + # Set to true to enable automatic ActiveJob exception tracking config.auto_instrument_active_job = true # Capture user context with exceptions (default: true) diff --git a/posthog-rails/lib/posthog/rails/configuration.rb b/posthog-rails/lib/posthog/rails/configuration.rb index 5352861..f64d3bf 100644 --- a/posthog-rails/lib/posthog/rails/configuration.rb +++ b/posthog-rails/lib/posthog/rails/configuration.rb @@ -26,9 +26,9 @@ class Configuration attr_accessor :user_id_method def initialize - @auto_capture_exceptions = true - @report_rescued_exceptions = true - @auto_instrument_active_job = true + @auto_capture_exceptions = false + @report_rescued_exceptions = false + @auto_instrument_active_job = false @excluded_exceptions = [] @capture_user_context = true @current_user_method = :current_user From 96f9fdb545bbfb3507c19fb652ee250cd24dd7d3 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Mon, 2 Feb 2026 21:01:35 -0500 Subject: [PATCH 23/31] ensuring the example config we copy over maintains the new defaults --- posthog-rails/examples/posthog.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index a39b861..3b6fc81 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -12,31 +12,31 @@ PostHog::Rails.configure do |config| # Automatically capture exceptions (default: false) # Set to true to enable automatic exception tracking - config.auto_capture_exceptions = true + # config.auto_capture_exceptions = true # Report exceptions that Rails rescues (e.g., with rescue_from) (default: false) # Set to true to capture rescued exceptions - config.report_rescued_exceptions = true + # config.report_rescued_exceptions = true # Automatically instrument ActiveJob background jobs (default: false) # Set to true to enable automatic ActiveJob exception tracking - config.auto_instrument_active_job = true + # config.auto_instrument_active_job = true # Capture user context with exceptions (default: true) - config.capture_user_context = 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 (e.g., :authenticated_user) # When configured, exceptions will include user context (distinct_id, email, name), # making it easier to identify affected users and debug user-specific issues. - config.current_user_method = :current_user + # 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' - ] + # config.excluded_exceptions = [ + # # 'MyCustom404Error', + # # 'MyCustomValidationError' + # ] end # You can also configure Rails options directly: From b8522891233c03b0c4f3fca4547afab9df64ace6 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:13:36 -0300 Subject: [PATCH 24/31] fix: Fix posthog_distinct_id This must be prepended to make sure it works as expected --- posthog-rails/lib/posthog/rails/active_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb index 49e5a8a..de71c16 100644 --- a/posthog-rails/lib/posthog/rails/active_job.rb +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -8,7 +8,7 @@ module Rails module ActiveJobExtensions include ParameterFilter - def self.included(base) + def self.prepended(base) base.extend(ClassMethods) end From 09c2f0505da34aab3a7d5d4c31e5bfc9d07e551c Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:14:07 -0300 Subject: [PATCH 25/31] refactor: Small fixes Tiny bugfixes found by Claude --- posthog-rails/lib/posthog/rails/active_job.rb | 2 +- .../lib/posthog/rails/capture_exceptions.rb | 6 +-- .../lib/posthog/rails/parameter_filter.rb | 1 + posthog-rails/lib/posthog/rails/railtie.rb | 41 +++++++++++-------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/posthog-rails/lib/posthog/rails/active_job.rb b/posthog-rails/lib/posthog/rails/active_job.rb index de71c16..c4b5b41 100644 --- a/posthog-rails/lib/posthog/rails/active_job.rb +++ b/posthog-rails/lib/posthog/rails/active_job.rb @@ -92,7 +92,7 @@ def sanitize_job_arguments(args) when Hash # Use Rails' filter_parameters to filter sensitive data filter_sensitive_params(arg) - when ActiveRecord::Base + when defined?(ActiveRecord::Base) && ActiveRecord::Base { class: arg.class.name, id: arg.id } else arg.class.name diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index 0b3eae3..c17534e 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -61,8 +61,8 @@ def capture_exception(exception, env) end def extract_distinct_id(env, request) - # Try to get user from controller - if env['action_controller.instance'] + # Try to get user from controller if capture_user_context is enabled + if PostHog::Rails.config&.capture_user_context && env['action_controller.instance'] controller = env['action_controller.instance'] method_name = PostHog::Rails.config&.current_user_method || :current_user @@ -73,7 +73,7 @@ def extract_distinct_id(env, request) end # Fallback to session ID or nil - request.session&.id + request.session&.id&.to_s end def extract_user_id(user) diff --git a/posthog-rails/lib/posthog/rails/parameter_filter.rb b/posthog-rails/lib/posthog/rails/parameter_filter.rb index 1fcac40..6ae197f 100644 --- a/posthog-rails/lib/posthog/rails/parameter_filter.rb +++ b/posthog-rails/lib/posthog/rails/parameter_filter.rb @@ -42,6 +42,7 @@ def self.backend # @return [Hash] Filtered parameters with sensitive data masked def filter_sensitive_params(params) return EMPTY_HASH unless params.is_a?(Hash) + return params unless ::Rails.application filter_parameters = ::Rails.application.config.filter_parameters parameter_filter = ParameterFilter.backend.new(filter_parameters) diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 9962226..292731d 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -76,38 +76,47 @@ def ensure_initialized! # 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, + insert_middleware_after( + app, ActionDispatch::DebugExceptions, PostHog::Rails::RescuedExceptionInterceptor ) # Insert after ShowExceptions to capture all exceptions - app.config.middleware.insert_after( - ActionDispatch::ShowExceptions, + insert_middleware_after( + app, 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| + # Hook into ActiveJob only if enabled + if PostHog::Rails.config&.auto_instrument_active_job + ActiveSupport.on_load(:active_job) do + prepend PostHog::Rails::ActiveJobExtensions + end + end + 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? + # Ensure PostHog shuts down gracefully (register only once) + config.after_initialize do + next if @posthog_at_exit_registered + + @posthog_at_exit_registered = true + at_exit { PostHog.client&.shutdown if PostHog.initialized? } + end + + def self.insert_middleware_after(app, target, middleware) + if app.config.middleware.include?(target) + app.config.middleware.insert_after(target, middleware) + else + # Fallback: append to stack if target middleware is missing (e.g., API-only apps) + app.config.middleware.use(middleware) end end From 9d4caf8140db74032486a65195f51d3fa8dafaa6 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:14:20 -0300 Subject: [PATCH 26/31] fix: Update and point default to us.i.posthog.com --- posthog-rails/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/posthog-rails/README.md b/posthog-rails/README.md index d54582e..78c1292 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -61,8 +61,8 @@ 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: Your PostHog instance URL + config.host = 'https://us.i.posthog.com' # or https://eu.i.posthog.com # Optional: Personal API key for feature flags config.personal_api_key = ENV['POSTHOG_PERSONAL_API_KEY'] @@ -234,7 +234,7 @@ PostHog will automatically extract the user's distinct ID from either `user_id` | Option | Type | Default | Description | |--------|------|---------|-------------| | `api_key` | String | **required** | Your PostHog project API key | -| `host` | String | `https://app.posthog.com` | PostHog instance URL | +| `host` | String | `https://us.i.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) | @@ -291,7 +291,13 @@ The following exceptions are not reported by default (common 4xx errors): - `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` @@ -361,7 +367,7 @@ PostHog Rails automatically filters sensitive parameters: - `api_key` - `authenticity_token` -Long parameter values are also truncated to 1000 characters. +Long parameter values are also truncated to 10,000 characters. ## Testing From 8c635fb97e2b70a992cc7d85e3f086e0a662ad7b Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:23:58 -0300 Subject: [PATCH 27/31] chore: Bump to v3.5.0 --- CHANGELOG.md | 2 +- lib/posthog/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c226d3f..ae083de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 3.5.0 - 2026-02-05 1. feat: Add posthog-rails gem for automatic Rails exception tracking - Automatic capture of unhandled exceptions via Rails middleware diff --git a/lib/posthog/version.rb b/lib/posthog/version.rb index c75d9eb..5a4b6c8 100644 --- a/lib/posthog/version.rb +++ b/lib/posthog/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module PostHog - VERSION = '3.4.0' + VERSION = '3.5.0' end From 09ef2d40ddaf4f315ebca966a2fac2d5173aa792 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:24:07 -0300 Subject: [PATCH 28/31] docs: Update release process --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 05633bb..430ff63 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,22 @@ Specifically, the [Ruby integration](https://posthog.com/docs/integrations/ruby- ## How to release -1. Get access to RubyGems from @dmarticus, @daibhin or @mariusandra -2. Install [`gh`](https://cli.github.com/) and authenticate with `gh auth login` -3. Update `lib/posthog/version.rb` with the new version & add to `CHANGELOG.md` making sure to add the current date. Commit the changes: +Both `posthog-ruby` and `posthog-rails` are released together with the same version number. -```shell -VERSION=1.2.3 # Replace with the new version here -git commit -am "Version $VERSION" -git tag -a $VERSION -m "Version $VERSION" -git push && git push --tags -gh release create $VERSION --generate-notes --fail-on-no-commits -``` +1. Create a PR that: + - Updates `lib/posthog/version.rb` with the new version + - Updates `CHANGELOG.md` with the changes and current date -4. [Trigger](https://github.com/PostHog/posthog-ruby/actions) "Publish Release" GitHub Action +2. Add the `release` label to the PR -5. Authenticate with your RubyGems account and approve the publish! +3. Merge the PR to `main` + +4. The release workflow will: + - Notify the Client Libraries team in Slack + - Wait for approval via the GitHub `Release` environment + - Publish both gems to RubyGems (via trusted publishing) + - Create and push a git tag + +5. Approve the release in GitHub when prompted + +The workflow handles publishing both `posthog-ruby` and `posthog-rails` in the correct order (since `posthog-rails` depends on `posthog-ruby`). From 6e8f9467ce46a3592b28317b23ef995a336573dd Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:26:00 -0300 Subject: [PATCH 29/31] refactor: Simplify rubocop rules --- .rubocop.yml | 7 ++++--- .rubocop_todo.yml | 12 ------------ 2 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml index 4f4f0f9..19e7e00 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,7 +9,7 @@ Style/Documentation: Naming/FileName: Exclude: - - 'posthog-rails/lib/posthog-rails.rb' + - "posthog-rails/lib/posthog-rails.rb" # Modern Ruby 3.0+ specific cops Style/HashTransformKeys: @@ -79,7 +79,8 @@ Metrics/MethodLength: Metrics/ParameterLists: Enabled: false -# Allow longer modules in spec files since they contain many test cases +# Ideally need to bring this down Metrics/ModuleLength: + Max: 4055 Exclude: - - 'spec/**/*_spec.rb' + - "spec/**/*_spec.rb" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 9d27730..0000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,12 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2025-05-20 14:44:39 UTC using RuboCop version 1.75.6. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 5 -# Configuration parameters: CountComments, CountAsOne. -Metrics/ModuleLength: - Max: 4055 From 9d3f8fb0ede360ef56ea984122dc895848048ed7 Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:48:30 -0300 Subject: [PATCH 30/31] chore: Improve release flow to use new approvals workflow --- .github/workflows/release.yml | 157 ++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cb1dea..3fa5d78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,91 @@ -name: Publish Release +name: "Release" on: - workflow_dispatch: + pull_request: + types: [closed] + branches: [main] + +permissions: + contents: read + +# Concurrency control: only one release process can run at a time +# This prevents race conditions if multiple PRs with 'release' label merge simultaneously +concurrency: + group: release + cancel-in-progress: false jobs: - publish: + check-release-label: + name: Check for release label runs-on: ubuntu-latest + # Run when PR with 'release' label is merged to main + if: | + github.event.pull_request.merged == true + && contains(github.event.pull_request.labels.*.name, 'release') + outputs: + should-release: ${{ steps.check.outputs.should-release }} + version: ${{ steps.check.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Check version is new + id: check + run: | + # Extract version from source + version=$(grep "VERSION = '" lib/posthog/version.rb | grep -o "'[0-9]\+\.[0-9]\+\.[0-9]\+'" | tr -d "'") + echo "version=$version" >> "$GITHUB_OUTPUT" + + # Get currently published version from RubyGems + published=$(gem info posthog-ruby --remote 2>/dev/null | grep -o 'posthog-ruby ([0-9.]*' | grep -o '[0-9].*' || echo "none") + + echo "Local version: $version" + echo "Published version: $published" + + if [ "$version" = "$published" ]; then + echo "Version $version is already published, skipping release" + echo "should-release=false" >> "$GITHUB_OUTPUT" + else + echo "Ready to release version $version" + echo "should-release=true" >> "$GITHUB_OUTPUT" + fi + + notify-approval-needed: + name: Notify Slack - Approval Needed + needs: check-release-label + if: needs.check-release-label.outputs.should-release == 'true' + uses: posthog/.github/.github/workflows/notify-approval-needed.yml@main + with: + slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} + slack_user_group_id: ${{ vars.GROUP_CLIENT_LIBRARIES_SLACK_GROUP_ID }} + secrets: + slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} + posthog_project_api_key: ${{ secrets.POSTHOG_PROJECT_API_KEY }} + + release: + name: Release and publish + needs: [check-release-label, notify-approval-needed] + runs-on: ubuntu-latest + if: always() && needs.check-release-label.outputs.should-release == 'true' + environment: "Release" # This will require an approval from a maintainer, they are notified in Slack above permissions: - contents: read + contents: write id-token: write steps: - - name: Checkout - uses: actions/checkout@v5 + - name: Notify Slack - Approved + if: needs.notify-approval-needed.outputs.slack_ts != '' + uses: posthog/.github/.github/actions/slack-thread-reply@main + with: + slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} + slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} + thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} + message: "✅ Release approved! Publishing v${{ needs.check-release-label.outputs.version }}..." + emoji_reaction: "white_check_mark" + + - name: Checkout repository + uses: actions/checkout@v4 with: ref: main fetch-depth: 0 @@ -22,4 +96,73 @@ jobs: bundler-cache: true ruby-version: ruby - - uses: rubygems/release-gem@v1 + - name: Configure trusted publishing credentials + uses: rubygems/configure-rubygems-credentials@v1.0.0 + + # Build and publish posthog-ruby first (posthog-rails depends on it) + - name: Build posthog-ruby + run: gem build posthog-ruby.gemspec + + - name: Publish posthog-ruby + run: gem push posthog-ruby-*.gem + + - name: Wait for posthog-ruby to be available + run: gem exec rubygems-await posthog-ruby-*.gem + + # Build and publish posthog-rails + - name: Build posthog-rails + run: gem build posthog-rails/posthog-rails.gemspec + + - name: Publish posthog-rails + run: gem push posthog-rails-*.gem + + - name: Wait for posthog-rails to be available + run: gem exec rubygems-await posthog-rails-*.gem + + # Create and push git tag + - name: Create git tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v${{ needs.check-release-label.outputs.version }}" -m "Release v${{ needs.check-release-label.outputs.version }}" + git push origin "v${{ needs.check-release-label.outputs.version }}" + + # Notify in case of a failure + - name: Send failure event to PostHog + if: ${{ failure() }} + uses: PostHog/posthog-github-action@v0.1 + with: + posthog-token: "${{ secrets.POSTHOG_PROJECT_API_KEY }}" + event: "posthog-ruby-github-release-workflow-failure" + properties: >- + { + "commitSha": "${{ github.sha }}", + "jobStatus": "${{ job.status }}", + "ref": "${{ github.ref }}", + "version": "v${{ needs.check-release-label.outputs.version }}" + } + + - name: Notify Slack - Failed + if: ${{ failure() && needs.notify-approval-needed.outputs.slack_ts != '' }} + uses: posthog/.github/.github/actions/slack-thread-reply@main + with: + slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} + slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} + thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} + message: "❌ Failed to release `posthog-ruby@v${{ needs.check-release-label.outputs.version }}`! " + emoji_reaction: "x" + + notify-released: + name: Notify Slack - Released + needs: [check-release-label, notify-approval-needed, release] + runs-on: ubuntu-latest + if: always() && needs.release.result == 'success' && needs.notify-approval-needed.outputs.slack_ts != '' + steps: + - name: Notify Slack - Released + uses: posthog/.github/.github/actions/slack-thread-reply@main + with: + slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }} + slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }} + thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }} + message: "🚀 posthog-ruby and posthog-rails v${{ needs.check-release-label.outputs.version }} released successfully!" + emoji_reaction: "rocket" From 059b44e0f462d42f6c4b0b664c3f19670526168b Mon Sep 17 00:00:00 2001 From: Rafa Audibert Date: Thu, 5 Feb 2026 19:52:21 -0300 Subject: [PATCH 31/31] chore: Remove inherit call --- .rubocop.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 19e7e00..2b96a3c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,3 @@ -inherit_from: .rubocop_todo.yml - AllCops: NewCops: enable SuggestExtensions: false