No release in over 3 years
ActiveSubscriber provides a clean interface for implementing the publisher-subscriber pattern in Rails applications using ActiveSupport Notifications, with automatic subscriber discovery and lifecycle callbacks.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 3.0

Runtime

>= 6.0
 Project Readme

ActiveSubscriber

ActiveSubscriber provides a clean interface for implementing the publisher-subscriber pattern in Rails applications using ActiveSupport Notifications, with automatic subscriber discovery and lifecycle callbacks.

Installation

Add this line to your application's Gemfile:

gem 'active_subscriber'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install active_subscriber

Quick Start

  1. Install ActiveSubscriber:
rails generate active_subscriber:install
  1. Create a subscriber:
rails generate active_subscriber:subscriber Analytics --events user_signed_in user_signed_up
  1. Include the Publisher module in your services:
class AnalyticsService
  include ActiveSubscriber::Publisher

  def self.track_analytics(event_name, properties = {}, context: analytics_context)
    publish_event(event_name, properties, context)
  end

  def self.track_analytics_with_timing(event_name, properties = {}, context = {}, &block)
    publish_event_with_timing(event_name, properties, context, &block)
  end
end
  1. Include the helper in your controllers:
class PostController < ApplicationController
  include ActiveSubscriber::Helpers::AnalyticsHelper

  def index
    @posts = Post.all
    track_analytics("posts.viewed", { count: @posts.count })
  end
end

Usage

Creating Subscribers

Subscribers inherit from ActiveSubscriber::Base and can subscribe to specific events or event patterns:

class LoggingSubscriber < ActiveSubscriber::Base
  subscribe_to :user_signed_in, :user_signed_up
  subscribe_to_pattern "analytics.*"

  # Lifecycle callbacks
  after_handle_event :increment_stats
  around_handle_event :with_timing
  before_handle_event :setup_context

  def handle_user_signed_in(data)
    Rails.logger.info "User signed in: #{data[:payload][:user_id]}"
  end

  def handle_user_signed_up(data)
    Rails.logger.info "New user signed up: #{data[:payload][:user_id]}"
  end

  def handle_all_events(event_name, data)
    Rails.logger.info "Analytics event: #{event_name}"
  end

  private

  def increment_stats
    # Called after handling any event
  end

  def with_timing
    start_time = Time.current
    yield
    duration = Time.current - start_time
    Rails.logger.debug "Event handled in #{duration * 1000}ms"
  end

  def setup_context
    # Called before handling any event
  end
end

Publishing Events

Using the Publisher Module

class UserService
  include ActiveSubscriber::Publisher

  def sign_in_user(user)
    # ... sign in logic ...

    publish_event("user_signed_in", { user_id: user.id }, { ip: request.ip })
  end

  def create_user(params)
    result = publish_event_with_timing("user_creation", { email: params[:email] }) do
      User.create!(params)
    end

    publish_event("user_signed_up", { user_id: result.id })
    result
  end
end

Using the Analytics Helper

class ApplicationController < ActionController::Base
  include ActiveSubscriber::Helpers::AnalyticsHelper

  def show
    @post = Post.find(params[:id])
    track_analytics("post.viewed", {
      post_id: @post.id,
      category: @post.category
    })
  end
end

Event Data Structure

Events published through ActiveSubscriber include:

{
  payload: { /* your custom data */ },
  context: { /* additional context */ },
  published_at: Time.current,
  publisher: "YourServiceClass"
}

When using the analytics helper, additional context is automatically added:

{
  controller: "posts",
  action: "show",
  user_id: current_user&.id,
  user_agent: request.user_agent,
  ip_address: request.remote_ip,
  # ... more request context
}

Configuration

Configure ActiveSubscriber in config/initializers/active_subscriber.rb:

ActiveSubscriber.configure do |config|
  # Enable or disable ActiveSubscriber (default: true)
  config.enabled = true

  # Namespace for Notification events
  config.namespace = "my_app"

  # Paths where subscribers are located (default: ["app/subscribers"])
  config.subscriber_paths = ["app/subscribers", "lib/subscribers"]

  # Filter events (optional)
  config.event_filter = ->(event_name) { !event_name.start_with?("internal.") }

  # Auto-load subscribers (default: true)
  config.auto_load = true
end

Lifecycle Callbacks

Subscribers support three types of lifecycle callbacks:

  • before_handle_event - Called before handling any event
  • after_handle_event - Called after handling any event
  • around_handle_event - Wraps the event handling (must yield)
class TimingSubscriber < ActiveSubscriber::Base
  subscribe_to_pattern ".*"

  around_handle_event :with_timing
  after_handle_event :log_completion

  def handle_all_events(event_name, data)
    # Handle the event
  end

  private

  def with_timing
    start_time = Time.current
    yield
    @duration = Time.current - start_time
  end

  def log_completion
    Rails.logger.info "Event completed in #{@duration * 1000}ms"
  end
end

Pattern Matching

Subscribe to events using patterns with wildcards:

class PatternSubscriber < ActiveSubscriber::Base
  subscribe_to_pattern "user.*"        # Matches user.created, user.updated, etc.
  subscribe_to_pattern "analytics.*"   # Matches analytics.page_view, analytics.click, etc.
  subscribe_to_pattern ".*"            # Matches all events

  def handle_all_events(event_name, data)
    case event_name
    when /^user\./
      handle_user_event(event_name, data)
    when /^analytics\./
      handle_analytics_event(event_name, data)
    end
  end
end

Error Handling

ActiveSubscriber includes built-in error handling. If a subscriber raises an exception, it will be logged but won't prevent other subscribers from processing the event.

Errors are logged to Rails.logger with the format:

ActiveSubscriber: Error handling event 'event_name': Error message

Thread Safety

ActiveSubscriber is designed to be thread-safe:

  • The subscriber registry uses a mutex for thread-safe registration
  • No shared mutable state in core components
  • Each subscriber instance handles events independently

Acknowledgements

This gem was inspired by a project at a previous role that needed a clean way to handle pub/sub notifications for third-party analytics. ActiveSubscriber is written entirely on personal time and resources, but the design is heavily informed by the needs of that work — along with my own.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/afomera/active_subscriber.