Chapter 144 · Integration Ruby On Rails
Subchapter 144.5
references/EXAMPLE.mdMarkdown38 KBView on GitHub
Repository: https://github.com/PostHog/context-mill Path: basics/ruby-on-rails
This is a Ruby on Rails (opens in a new tab) example demonstrating PostHog integration with product analytics, error tracking (auto-instrumentation), feature flags, user identification, and ActiveJob instrumentation via the posthog-rails gem.
PostHog.captureposthog-railsPostHog.capture_exceptionPostHog.identifyPostHog.is_feature_enabledcurrent_userbundle installcp .env.example .env
# Edit .env and add your PostHog project tokenGet your PostHog project token from your PostHog project settings (opens in a new tab).
bin/rails db:create db:migrate db:seedbin/rails serverOpen http://localhost:3000 (opens in a new tab) with your browser. Login with admin@example.com / admin.
ruby-on-rails/
├── config/
│ ├── routes.rb # URL routing
│ └── initializers/
│ └── posthog.rb # PostHog + posthog-rails configuration
├── app/
│ ├── controllers/
│ │ ├── application_controller.rb # Base controller with current_user
│ │ ├── sessions_controller.rb # Login/logout with PostHog identify
│ │ ├── registrations_controller.rb # Signup with PostHog identify
│ │ ├── dashboard_controller.rb # Feature flags + ActiveJob demo
│ │ ├── burritos_controller.rb # Custom event tracking
│ │ ├── profiles_controller.rb # Page view tracking
│ │ └── errors_controller.rb # Error tracking demos
│ ├── jobs/
│ │ └── example_job.rb # ActiveJob auto-instrumentation demo
│ ├── models/
│ │ └── user.rb # posthog_distinct_id + posthog_properties
│ └── views/
│ ├── layouts/application.html.erb # Base layout with posthog-js snippet
│ ├── sessions/new.html.erb # Login page
│ ├── registrations/new.html.erb # Signup page
│ ├── dashboard/show.html.erb # Feature flags demo
│ ├── burritos/show.html.erb # Event tracking demo
│ └── profiles/show.html.erb # Error tracking demo
├── db/
│ ├── migrate/ # Database migrations
│ └── seeds.rb # Default admin user
├── .env.example # Environment variable template
├── Gemfile # Ruby dependencies
└── README.md # This file# Rails-specific auto-instrumentation
PostHog::Rails.configure do |config|
config.auto_capture_exceptions = true
config.report_rescued_exceptions = true
config.auto_instrument_active_job = true
config.capture_user_context = true
config.current_user_method = :current_user
config.user_id_method = :posthog_distinct_id
end
PostHog.init do |config|
config.api_key = ENV.fetch('POSTHOG_PROJECT_TOKEN', nil)
config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com')
endclass User < ApplicationRecord
has_secure_password
# Called by posthog-rails for automatic user association in error reports
def posthog_distinct_id
email
end
def posthog_properties
{ email: email, is_staff: is_staff, date_joined: created_at&.iso8601 }
end
end# Identify the user and capture login event
PostHog.identify(
distinct_id: user.posthog_distinct_id,
properties: user.posthog_properties
)
PostHog.capture(
distinct_id: user.posthog_distinct_id,
event: 'user_logged_in',
properties: { login_method: 'email' }
)# Check if a feature flag is enabled
@show_new_feature = PostHog.is_feature_enabled(
'new-dashboard-feature',
user.posthog_distinct_id,
person_properties: user.posthog_properties
)
# Get feature flag payload for configuration
@feature_config = PostHog.get_feature_flag_payload(
'new-dashboard-feature',
user.posthog_distinct_id
)With auto_capture_exceptions: true, unhandled exceptions in controllers are captured automatically. No code needed:
# This exception is automatically captured by posthog-rails
# with the current_user's posthog_distinct_id attached
def show
raise "Something went wrong" # Captured automatically!
endbegin
risky_operation
rescue => e
PostHog.capture_exception(e, current_user.posthog_distinct_id)
end# posthog-rails subscribes to Rails.error automatically
Rails.error.handle(context: { user_id: user.id }) do
risky_operation
end# config: auto_instrument_active_job = true
# Job failures are captured automatically.
# Use the posthog_distinct_id DSL to associate errors with a user.
class ExampleJob < ApplicationJob
posthog_distinct_id ->(distinct_id, *) { distinct_id }
def perform(distinct_id, should_fail: false)
raise "Job failed" # Captured automatically with user context
end
end
# In the controller, pass the distinct_id when enqueuing:
ExampleJob.perform_later(current_user.posthog_distinct_id, should_fail: true)This example includes the posthog-js snippet in the layout template to demonstrate how frontend and backend tracking work together.
distinct_id is used on both sides. Call posthog.identify(user.email) in posthog-js after login, matching the posthog_distinct_id used on the backendNote: Unlike the Django SDK, posthog-rails does not include a context middleware that reads X-POSTHOG-SESSION-ID or X-POSTHOG-DISTINCT-ID tracing headers. Frontend and backend events are correlated through the shared distinct_id.
# PostHog Configuration
POSTHOG_PROJECT_TOKEN=phc_your_project_token_here
POSTHOG_HOST=https://us.i.posthog.com
# Optional: Enable debug mode to see PostHog requests
# POSTHOG_DEBUG=true
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
private
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
helper_method :current_user
def require_login
unless current_user
redirect_to login_path
end
end
end
class BurritosController < ApplicationController
before_action :require_login
def show
@burrito_count = session[:burrito_count] || 0
end
def consider
count = (session[:burrito_count] || 0) + 1
session[:burrito_count] = count
user = current_user
# PostHog: Track custom event
PostHog.identify(
distinct_id: user.posthog_distinct_id,
properties: user.posthog_properties
)
PostHog.capture(
distinct_id: user.posthog_distinct_id,
event: 'burrito_considered',
properties: { total_considerations: count }
)
render json: { success: true, count: count }
end
end
class DashboardController < ApplicationController
before_action :require_login
def show
user = current_user
# PostHog: Track dashboard view
PostHog.capture(
distinct_id: user.posthog_distinct_id,
event: 'dashboard_viewed',
properties: { is_staff: user.is_staff }
)
# PostHog: Check feature flag
@show_new_feature = PostHog.is_feature_enabled(
'new-dashboard-feature',
user.posthog_distinct_id,
person_properties: user.posthog_properties
)
# PostHog: Get feature flag payload for configuration
@feature_config = PostHog.get_feature_flag_payload(
'new-dashboard-feature',
user.posthog_distinct_id
)
end
def enqueue_test_job
# Enqueue a job that will fail — posthog-rails captures the error automatically.
# The distinct_id is passed so the posthog_distinct_id DSL can associate the error with this user.
ExampleJob.perform_later(current_user.posthog_distinct_id, should_fail: true)
render json: {
success: true,
message: 'Job enqueued. The job will fail and posthog-rails will capture the error automatically.'
}
end
end
class ErrorsController < ApplicationController
before_action :require_login
def test
# Manual exception capture — catch the error and report it explicitly
begin
raise StandardError, 'Test exception from critical operation'
rescue StandardError => e
# PostHog: Manually capture the exception
PostHog.capture_exception(e, current_user.posthog_distinct_id)
PostHog.capture(
distinct_id: current_user.posthog_distinct_id,
event: 'error_triggered',
properties: {
error_type: e.class.name,
error_message: e.message
}
)
render json: {
success: false,
error: e.message,
message: 'Error has been captured by PostHog'
}, status: :internal_server_error
end
end
def test_rails_error
# Rails.error.handle — Rails 7+ error reporting integration.
# posthog-rails subscribes to Rails.error, so exceptions reported
# via Rails.error.handle are automatically captured in PostHog.
Rails.error.handle(context: { user_id: current_user.id }) do
raise StandardError, 'Test error via Rails.error.handle — captured automatically by posthog-rails'
end
render json: {
success: true,
message: 'Error was handled via Rails.error.handle and captured by posthog-rails'
}
end
end
class ProfilesController < ApplicationController
before_action :require_login
def show
# PostHog: Track profile view
PostHog.capture(
distinct_id: current_user.posthog_distinct_id,
event: 'profile_viewed'
)
end
end
class RegistrationsController < ApplicationController
def new
redirect_to dashboard_path if current_user
end
def create
user = User.new(
email: params[:email],
password: params[:password],
password_confirmation: params[:password_confirmation]
)
if user.save
session[:user_id] = user.id
# PostHog: Identify the new user and capture signup event
PostHog.identify(
distinct_id: user.posthog_distinct_id,
properties: user.posthog_properties
)
PostHog.capture(
distinct_id: user.posthog_distinct_id,
event: 'user_signed_up',
properties: { signup_method: 'form' }
)
redirect_to dashboard_path
else
flash[:error] = user.errors.full_messages.join(', ')
render :new, status: :unprocessable_entity
end
end
end
class SessionsController < ApplicationController
def new
redirect_to dashboard_path if current_user
end
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
# PostHog: Identify the user and capture login event
PostHog.identify(
distinct_id: user.posthog_distinct_id,
properties: user.posthog_properties
)
PostHog.capture(
distinct_id: user.posthog_distinct_id,
event: 'user_logged_in',
properties: { login_method: 'email' }
)
redirect_to dashboard_path
else
flash[:error] = 'Invalid email or password'
render :new, status: :unprocessable_entity
end
end
def destroy
if current_user
# PostHog: Track logout before session ends
PostHog.capture(
distinct_id: current_user.posthog_distinct_id,
event: 'user_logged_out'
)
end
session.delete(:user_id)
redirect_to login_path
end
end
class ApplicationJob < ActiveJob::Base
end
# Example ActiveJob demonstrating posthog-rails auto-instrumentation.
#
# When auto_instrument_active_job is enabled in the PostHog config,
# posthog-rails automatically captures exceptions from failed jobs.
# The job class name, queue, and arguments are included as properties
# on the error event.
#
# Use the posthog_distinct_id DSL to associate job errors with a user.
# The proc receives the same arguments as perform and should return
# the distinct_id string. Without this, job errors have no user context.
class ExampleJob < ApplicationJob
queue_as :default
# Extract distinct_id from the first argument so posthog-rails
# can associate the error with the user who triggered the job.
posthog_distinct_id ->(distinct_id, *) { distinct_id }
def perform(distinct_id, should_fail: false)
if should_fail
raise StandardError, 'Example job failure - this error is automatically captured by posthog-rails'
end
Rails.logger.info "ExampleJob completed successfully for #{distinct_id}"
end
end
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
# Called by posthog-rails for automatic user association in error reports.
# When auto_capture_exceptions and capture_user_context are enabled,
# posthog-rails calls this method on current_user to get the distinct_id.
def posthog_distinct_id
email
end
# Helper used by controllers when calling PostHog.identify to set person properties.
# These properties appear on the person profile in PostHog.
def posthog_properties
{
email: email,
is_staff: is_staff,
date_joined: created_at&.iso8601
}
end
end
<% content_for(:title) { 'Burrito - PostHog Rails example' } %>
<div class="card">
<h1>Burrito consideration tracker</h1>
<p>This page demonstrates custom event tracking with PostHog.</p>
</div>
<div class="card" style="text-align: center;">
<h2>Times considered</h2>
<div class="count" id="burrito-count"><%= @burrito_count %></div>
<button onclick="considerBurrito()" style="font-size: 18px; padding: 15px 30px;">
Consider a burrito
</button>
</div>
<div class="card">
<h3>How event tracking works</h3>
<p>Each time you click the button, a <code>burrito_considered</code> event is sent to PostHog:</p>
<pre style="background: #f3f4f6; padding: 15px; border-radius: 5px; overflow-x: auto; margin-top: 15px;"><code>PostHog.capture(
distinct_id: user.posthog_distinct_id,
event: 'burrito_considered',
properties: { total_considerations: count }
)</code></pre>
</div>
<% content_for :scripts do %>
<script>
async function considerBurrito() {
try {
const response = await fetch('/api/burrito/consider', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (data.success) {
document.getElementById('burrito-count').textContent = data.count;
}
} catch (error) {
console.error('Error:', error);
}
}
</script>
<% end %>
<% content_for(:title) { 'Dashboard - PostHog Rails example' } %>
<div class="card">
<h1>Dashboard</h1>
<p>Welcome back, <strong><%= current_user.email %></strong>!</p>
</div>
<div class="card">
<h2>Feature flags</h2>
<p>Feature flags allow you to control feature rollouts and run A/B tests.</p>
<% if @show_new_feature %>
<div class="feature-flag">
<h3>New feature enabled!</h3>
<p>
This section is only visible because the <code>new-dashboard-feature</code>
flag is enabled for your user.
</p>
<% if @feature_config %>
<p><strong>Feature config:</strong> <%= @feature_config %></p>
<% end %>
</div>
<% else %>
<div style="background: #f3f4f6; padding: 15px; border-radius: 8px; margin-top: 15px;">
<p>
The <code>new-dashboard-feature</code> flag is not enabled for your user.
Create this flag in your PostHog project to see it in action.
</p>
</div>
<% end %>
</div>
<div class="card">
<h2>ActiveJob instrumentation</h2>
<p>
Click below to enqueue a background job that will fail.
<code>posthog-rails</code> automatically captures the exception — no extra code needed.
</p>
<button onclick="enqueueTestJob()" style="margin-top: 10px;">Enqueue failing job</button>
<div id="job-result" style="margin-top: 15px; display: none;"></div>
</div>
<div class="card">
<h3>How feature flags work</h3>
<pre style="background: #f3f4f6; padding: 15px; border-radius: 5px; overflow-x: auto;"><code># Check if a feature flag is enabled
show_feature = PostHog.is_feature_enabled(
'new-dashboard-feature',
user.posthog_distinct_id,
person_properties: user.posthog_properties
)
# Get feature flag payload for configuration
config = PostHog.get_feature_flag_payload(
'new-dashboard-feature',
user.posthog_distinct_id
)</code></pre>
</div>
<% content_for :scripts do %>
<script>
async function enqueueTestJob() {
const resultDiv = document.getElementById('job-result');
try {
const response = await fetch('/api/test-job', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
});
const data = await response.json();
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="flash success">' + data.message + '</div>';
} catch (error) {
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="flash error">Request failed: ' + error + '</div>';
}
}
</script>
<% end %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= content_for?(:title) ? yield(:title) :
<% content_for(:title) { 'Profile - PostHog Rails example' } %>
<div class="card">
<h1>Profile</h1>
<p>This page demonstrates error tracking with PostHog and <code>posthog-rails</code>.</p>
</div>
<div class="card">
<h2
<% content_for(:title) { 'Sign Up - PostHog Rails example' } %>
<div class="card">
<h1>Sign Up</h1>
<p>Create an account to see PostHog analytics in action.</p>
<form action="<%= signup_path %>" method="post" style="margin-top: 20px;">
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<input type="password" name="password_confirmation" placeholder="Confirm Password" required>
<button type="submit">Sign Up</button>
</form>
<p style="margin-top: 15px; color: #666; font-size: 14px;">
Already have an account? <a href="<%= login_path %>">Login</a>
</p>
</div>
<% content_for(:title) { 'Login - PostHog Rails example' } %>
<div class="card">
<h1>PostHog Rails example</h1>
<p>Welcome! This example demonstrates PostHog integration with Ruby on Rails, including automatic error tracking via <code>posthog-rails</code>.</p>
</div>
<div class="card">
<h2>Login</h2>
<p>Login to see PostHog analytics in action.</p>
<form action="<%= login_path %>" method="post" style="margin-top: 20px;">
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
<p style="margin-top: 15px; color: #666; font-size: 14px;">
Don't have an account? <a href="<%= signup_path %>">Sign up</a><br>
Tip: Run <code>bin/rails db:seed</code> to create admin@example.com / admin
</p>
</div>
<div class="card">
<h3>What this example demonstrates</h3>
<ul style="padding-left: 20px;">
<li><strong>User identification</strong> — Users are identified with <code>PostHog.identify</code> on login</li>
<li><strong>Event tracking</strong> — Custom events captured with <code>PostHog.capture</code></li>
<li><strong>Feature flags</strong> — Conditional features with <code>PostHog.is_feature_enabled</code></li>
<li><strong>Error tracking (auto)</strong> — Unhandled exceptions captured automatically by <code>posthog-rails</code></li>
<li><strong>Error tracking (manual)</strong> — Handled errors captured with <code>PostHog.capture_exception</code></li>
<li><strong>ActiveJob instrumentation</strong> — Background job failures captured automatically</li>
<li><strong>Rails.error integration</strong> — Rails 7+ error reporting captured by <code>posthog-rails</code></li>
<li><strong>Frontend tracking</strong> — posthog-js captures pageviews and session replay</li>
</ul>
</div>
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
require_relative 'config/environment'
run Rails.application
require_relative 'boot'
require 'rails/all'
Bundler.require(*Rails.groups)
module PosthogExample
class Application < Rails::Application
config.load_defaults 7.1
# Use SQLite for all stores
config.active_job.queue_adapter = :async
end
end
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup'
require_relative 'application'
Rails.application.initialize!
require 'active_support/core_ext/integer/time'
Rails.application.configure do
config.enable_reloading = true
config.eager_load = false
config.consider_all_requests_local = true
config.server_timing = true
# Secret key for development (not used in production)
config.secret_key_base = 'dev-secret-key-for-posthog-example-only'
config.action_controller.perform_caching = false
config.cache_store = :memory_store
config.active_support.deprecation = :log
config.active_support.disallowed_deprecation = :raise
config.active_support.disallowed_deprecation_warnings = []
config.active_record.migration_error = :page_load
config.active_record.verbose_query_logs = true
end
# PostHog configuration with posthog-rails auto-instrumentation
#
# The posthog-rails gem provides:
# - Automatic exception capture for unhandled controller errors
# - ActiveJob instrumentation for background job failures
# - User context detection from current_user
# - Rails.error integration for rescued exceptions
PostHog.init do |config|
config.api_key = ENV.fetch('POSTHOG_PROJECT_TOKEN', nil)
config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com')
end
PostHog::Rails.configure do |config|
# Auto-capture unhandled exceptions in controllers
config.auto_capture_exceptions = true
# Also capture exceptions that Rails rescues (e.g. ActiveRecord::RecordNotFound)
config.report_rescued_exceptions = true
# Auto-instrument ActiveJob failures
config.auto_instrument_active_job = true
# Automatically associate errors with the current user
config.capture_user_context = true
config.current_user_method = :current_user
config.user_id_method = :posthog_distinct_id
end
Rails.application.routes.draw do
# Auth
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
get 'signup', to: 'registrations#new'
post 'signup', to: 'registrations#create'
# App
get 'dashboard', to: 'dashboard#show'
get 'burrito', to: 'burritos#show'
post 'api/burrito/consider', to: 'burritos#consider'
get 'profile', to: 'profiles#show'
# Error tracking demos
post 'api/test-error', to: 'errors#test'
post 'api/test-rails-error', to: 'errors#test_rails_error'
# Background job demo
post 'api/test-job', to: 'dashboard#enqueue_test_job'
root 'sessions#new'
end
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.boolean :is_staff, default: false
t.timestamps
end
add_index :users, :email, unique: true
end
end
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_01_01_000000) do
create_table "users", force: :cascade do |t|
t.string "email", null: false
t.string "password_digest", null: false
t.boolean "is_staff", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
end
end
# Create a default admin user for testing
User.find_or_create_by!(email: 'admin@example.com') do |user|
user.password = 'admin'
user.password_confirmation = 'admin'
user.is_staff = true
end
puts 'Seed data created: admin@example.com / admin'
source 'https://rubygems.org'
gem 'rails', '~> 7.1'
gem 'sqlite3', '~> 1.7'
gem 'puma', '~> 6.0'
gem 'bcrypt', '~> 3.1'
gem 'dotenv-rails', '~> 3.0'
# PostHog
gem 'posthog-ruby', '~> 3.0'
gem 'posthog-rails'
require_relative 'config/application'
Rails.application.load_tasks
This file