├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── rails70.gemfile ├── rails71.gemfile └── rails72.gemfile ├── lib ├── ownership.rb └── ownership │ ├── controller_methods.rb │ ├── global_methods.rb │ ├── honeybadger.rb │ ├── job_methods.rb │ ├── marginalia.rb │ ├── rollbar.rb │ └── version.rb ├── ownership.gemspec └── test ├── active_record_test.rb ├── controller_test.rb ├── honeybadger_test.rb ├── internal ├── app │ ├── controllers │ │ ├── home_controller.rb │ │ └── users_controller.rb │ ├── jobs │ │ └── test_job.rb │ └── models │ │ └── user.rb ├── config │ ├── database.yml │ └── routes.rb └── db │ └── schema.rb ├── job_test.rb ├── marginalia_test.rb ├── ownership_test.rb ├── rollbar_test.rb └── test_helper.rb /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | include: 10 | - ruby: 3.4 11 | gemfile: Gemfile 12 | - ruby: 3.3 13 | gemfile: gemfiles/rails72.gemfile 14 | - ruby: 3.2 15 | gemfile: gemfiles/rails71.gemfile 16 | - ruby: 3.1 17 | gemfile: gemfiles/rails70.gemfile 18 | env: 19 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - run: bundle exec rake test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.lock 10 | *.log 11 | *.sqlite* 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 (2024-10-01) 2 | 3 | - Added support for Rails 8 4 | - Dropped support for Ruby < 3.1 and Rails < 7 5 | 6 | ## 0.3.0 (2023-07-02) 7 | 8 | - Dropped support for Ruby < 3 and Rails < 6.1 9 | 10 | ## 0.2.0 (2022-04-26) 11 | 12 | - Fixed issue with nested `owner` blocks 13 | - Dropped support for Ruby < 2.6 14 | 15 | ## 0.1.2 (2022-03-11) 16 | 17 | - Added Active Record query log tags integration 18 | 19 | ## 0.1.1 (2019-10-27) 20 | 21 | - Added Honeybadger integration 22 | - Made `owner` method private to behave like `Kernel` methods 23 | - Fixed conflict with Pry 24 | 25 | ## 0.1.0 (2017-11-05) 26 | 27 | - First release 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "combustion" 8 | gem "rails", "~> 8.0.0" 9 | gem "marginalia", require: false 10 | gem "honeybadger", require: false 11 | gem "rollbar", require: false 12 | gem "pry" 13 | gem "sqlite3", platform: :ruby 14 | gem "sqlite3-ffi", platform: :jruby 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2023 Andrew Kane 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ownership 2 | 3 | Code ownership for Rails 4 | 5 | Check out [Scaling the Monolith](https://ankane.org/scaling-the-monolith) for other tips 6 | 7 | :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) 8 | 9 | [![Build Status](https://github.com/ankane/ownership/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/ownership/actions) 10 | 11 | ## Installation 12 | 13 | Add this line to your application’s Gemfile: 14 | 15 | ```ruby 16 | gem "ownership" 17 | ``` 18 | 19 | ## Getting Started 20 | 21 | Ownership provides the ability to specify owners for different parts of the codebase. **We highly recommend owners are teams rather than individuals.** You can then use this information however you’d like, like routing errors to the correct team. 22 | 23 | ## Specifying Ownership 24 | 25 | ### Controllers 26 | 27 | ```ruby 28 | class OrdersController < ApplicationController 29 | owner :logistics 30 | end 31 | ``` 32 | 33 | You can use any options that `before_action` supports. 34 | 35 | ```ruby 36 | class OrdersController < ApplicationController 37 | owner :logistics, only: [:index] 38 | owner :customers, except: [:index] 39 | end 40 | ``` 41 | 42 | ### Jobs 43 | 44 | ```ruby 45 | class SomeJob < ApplicationJob 46 | owner :logistics 47 | end 48 | ``` 49 | 50 | ### Anywhere 51 | 52 | ```ruby 53 | owner :logistics do 54 | # code 55 | end 56 | ``` 57 | 58 | ### Default 59 | 60 | You can set a default owner with: 61 | 62 | ```ruby 63 | Ownership.default_owner = :logistics 64 | ``` 65 | 66 | ## Integrations 67 | 68 | There are a few built-in integrations with other gems. 69 | 70 | - [Active Record](#active-record) 71 | - [AppSignal](#appsignal) 72 | - [Honeybadger](#honeybadger) 73 | - [Marginalia](#marginalia) 74 | - [Rollbar](#rollbar) 75 | 76 | You can also add [custom integrations](#custom-integrations). 77 | 78 | ### Active Record 79 | 80 | Active Record 7+ has the option to add comments to queries. 81 | 82 | ```sql 83 | SELECT ... 84 | /*application:MyApp,controller:posts,action:index,owner:logistics*/ 85 | ``` 86 | 87 | Add to `config/application.rb`: 88 | 89 | ```ruby 90 | config.active_record.query_log_tags_enabled = true 91 | config.active_record.query_log_tags << :owner 92 | ``` 93 | 94 | ### AppSignal 95 | 96 | The [AppSignal gem integrates with Ownership](https://docs.appsignal.com/ruby/integrations/ownership.html) automatically. Error and performance samples in AppSignal will be tagged with the specified owner. 97 | 98 | You can set AppSignal's [`ownership_set_namespace` configuration option](https://docs.appsignal.com/ruby/configuration/options.html#option-ownership_set_namespace) to `true` in order to use the specified owner as an AppSignal namespace, which allows you to easily list performance actions and error incidents for each namespace. 99 | 100 | ### Honeybadger 101 | 102 | [Honeybadger](https://github.com/honeybadger-io/honeybadger-ruby) tracks exceptions. This integration makes it easy to send exceptions to different projects based on the owner. We recommend having a project for each team. 103 | 104 | ```ruby 105 | Ownership::Honeybadger.api_keys = { 106 | logistics: "token1", 107 | customers: "token2" 108 | } 109 | ``` 110 | 111 | Also works with a proc 112 | 113 | ```ruby 114 | Ownership::Honeybadger.api_keys = ->(owner) { ENV["#{owner.to_s.upcase}_HONEYBADGER_API_KEY"] } 115 | ``` 116 | 117 | ### Marginalia 118 | 119 | [Marginalia](https://github.com/basecamp/marginalia) adds comments to Active Record queries. If installed, the owner is added. 120 | 121 | ```sql 122 | SELECT ... 123 | /*application:MyApp,controller:posts,action:index,owner:logistics*/ 124 | ``` 125 | 126 | This can be useful when looking at the most time-consuming queries on your database. 127 | 128 | ### Rollbar 129 | 130 | [Rollbar](https://github.com/rollbar/rollbar-gem) tracks exceptions. This integration makes it easy to send exceptions to different projects based on the owner. We recommend having a project for each team. 131 | 132 | ```ruby 133 | Ownership::Rollbar.access_token = { 134 | logistics: "token1", 135 | customers: "token2" 136 | } 137 | ``` 138 | 139 | Also works with a proc 140 | 141 | ```ruby 142 | Ownership::Rollbar.access_token = ->(owner) { ENV["#{owner.to_s.upcase}_ROLLBAR_ACCESS_TOKEN"] } 143 | ``` 144 | 145 | For version 3.1+ of the `rollbar` gem, add to `config/initializers/rollbar.rb`: 146 | 147 | ```ruby 148 | config.use_payload_access_token = true 149 | ``` 150 | 151 | ## Custom Integrations 152 | 153 | You can define a custom block of code to run with: 154 | 155 | ```ruby 156 | Ownership.around_change = proc do |owner, block| 157 | puts "New owner: #{owner}" 158 | block.call 159 | puts "Done" 160 | end 161 | ``` 162 | 163 | Please don’t hesitate to [submit a pull request](https://github.com/ankane/ownership/pulls) if you create an integration that others can use. 164 | 165 | Exceptions that bubble up from an `owner` block have the owner, which your exception reporting library can use. 166 | 167 | ```ruby 168 | begin 169 | owner :logistics do 170 | raise "error" 171 | end 172 | rescue => e 173 | puts e.owner # :logistics 174 | end 175 | ``` 176 | 177 | ## Other Useful Tools 178 | 179 | - [GitHub Code Owners](https://github.com/blog/2392-introducing-code-owners) for code reviews 180 | 181 | ## Thanks 182 | 183 | Thanks to [Nick Elser](https://github.com/nickelser) for creating this pattern. 184 | 185 | ## History 186 | 187 | View the [changelog](https://github.com/ankane/ownership/blob/master/CHANGELOG.md). 188 | 189 | ## Contributing 190 | 191 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 192 | 193 | - [Report bugs](https://github.com/ankane/ownership/issues) 194 | - Fix bugs and [submit pull requests](https://github.com/ankane/ownership/pulls) 195 | - Write, clarify, or fix documentation 196 | - Suggest or add new features 197 | 198 | To get started with development and testing: 199 | 200 | ```sh 201 | git clone https://github.com/ankane/ownership.git 202 | cd ownership 203 | bundle install 204 | bundle exec rake test 205 | ``` 206 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | t.warning = false # for marginalia 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /gemfiles/rails70.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "combustion" 8 | gem "rails", "~> 7.0.0" 9 | gem "marginalia", require: false 10 | gem "honeybadger", require: false 11 | gem "rollbar", require: false 12 | gem "pry" 13 | gem "sqlite3", "< 2" 14 | -------------------------------------------------------------------------------- /gemfiles/rails71.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "combustion" 8 | gem "rails", "~> 7.1.0" 9 | gem "marginalia", require: false 10 | gem "honeybadger", require: false 11 | gem "rollbar", require: false 12 | gem "pry" 13 | gem "sqlite3", "< 2" 14 | -------------------------------------------------------------------------------- /gemfiles/rails72.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "combustion" 8 | gem "rails", "~> 7.2.0" 9 | gem "marginalia", require: false 10 | gem "honeybadger", require: false 11 | gem "rollbar", require: false 12 | gem "pry" 13 | gem "sqlite3" 14 | -------------------------------------------------------------------------------- /lib/ownership.rb: -------------------------------------------------------------------------------- 1 | # modules 2 | require_relative "ownership/global_methods" 3 | require_relative "ownership/version" 4 | 5 | # integrations 6 | require_relative "ownership/honeybadger" 7 | require_relative "ownership/rollbar" 8 | 9 | module Ownership 10 | class << self 11 | attr_accessor :around_change, :default_owner 12 | 13 | def owner 14 | Thread.current[:ownership_owner] || default_owner 15 | end 16 | end 17 | end 18 | 19 | Object.include Ownership::GlobalMethods 20 | 21 | if defined?(ActiveSupport) 22 | ActiveSupport.on_load(:action_controller) do 23 | require_relative "ownership/controller_methods" 24 | include Ownership::ControllerMethods 25 | end 26 | 27 | ActiveSupport.on_load(:active_record) do 28 | if ActiveRecord::VERSION::MAJOR >= 7 29 | # taggings is frozen in Active Record 8 30 | if !ActiveRecord::QueryLogs.taggings[:owner] 31 | ActiveRecord::QueryLogs.taggings = ActiveRecord::QueryLogs.taggings.merge({owner: -> { Ownership.owner }}) 32 | end 33 | end 34 | 35 | require_relative "ownership/marginalia" if defined?(Marginalia) 36 | end 37 | 38 | ActiveSupport.on_load(:active_job) do 39 | require_relative "ownership/job_methods" 40 | include Ownership::JobMethods 41 | end 42 | else 43 | require_relative "ownership/marginalia" if defined?(Marginalia) 44 | end 45 | 46 | class Exception 47 | attr_accessor :owner 48 | end 49 | -------------------------------------------------------------------------------- /lib/ownership/controller_methods.rb: -------------------------------------------------------------------------------- 1 | require "active_support/concern" 2 | 3 | module Ownership 4 | module ControllerMethods 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | def owner(owner, options = {}) 9 | around_action options do |_, block| 10 | owner(owner) { block.call } 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ownership/global_methods.rb: -------------------------------------------------------------------------------- 1 | module Ownership 2 | module GlobalMethods 3 | private 4 | 5 | def owner(*args, &block) 6 | return super if is_a?(Method) # hack for pry 7 | 8 | owner = args[0] 9 | # same error message as Ruby 10 | raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1 11 | raise ArgumentError, "Missing block" unless block_given? 12 | 13 | previous_value = Thread.current[:ownership_owner] 14 | begin 15 | Thread.current[:ownership_owner] = owner 16 | 17 | # callbacks 18 | if Ownership.around_change 19 | Ownership.around_change.call(owner, block) 20 | else 21 | block.call 22 | end 23 | rescue Exception => e 24 | e.owner ||= owner 25 | raise 26 | ensure 27 | Thread.current[:ownership_owner] = previous_value 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/ownership/honeybadger.rb: -------------------------------------------------------------------------------- 1 | module Ownership 2 | module Honeybadger 3 | class << self 4 | attr_reader :api_keys 5 | 6 | def api_keys=(api_keys) 7 | @api_keys = api_keys 8 | @configuration ||= configure 9 | api_keys 10 | end 11 | 12 | private 13 | 14 | def add_owner_as_tag(notice, current_owner) 15 | return unless current_owner 16 | 17 | notice.tags << current_owner.to_s 18 | end 19 | 20 | def configure 21 | ::Honeybadger.configure do |config| 22 | config.before_notify do |notice| 23 | current_owner = notice.exception.owner if notice.exception.is_a?(Exception) 24 | current_owner ||= Ownership.owner 25 | 26 | add_owner_as_tag(notice, current_owner) 27 | use_owner_api_key(notice, current_owner) 28 | end 29 | end 30 | end 31 | 32 | def owner_api_key(current_owner) 33 | api_keys.respond_to?(:call) ? api_keys.call(current_owner) : api_keys[current_owner] 34 | end 35 | 36 | def use_owner_api_key(notice, current_owner) 37 | return unless current_owner 38 | 39 | if (api_key = owner_api_key(current_owner)) 40 | notice.api_key = api_key 41 | else 42 | warn "[ownership] Missing Honeybadger API key for owner: #{current_owner}" 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/ownership/job_methods.rb: -------------------------------------------------------------------------------- 1 | require "active_support/concern" 2 | 3 | module Ownership 4 | module JobMethods 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | def owner(*args) 9 | around_perform do |_, block| 10 | owner(*args) { block.call } 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ownership/marginalia.rb: -------------------------------------------------------------------------------- 1 | module Marginalia 2 | module Comment 3 | def self.owner 4 | Ownership.owner 5 | end 6 | end 7 | end 8 | 9 | Marginalia::Comment.components << :owner 10 | -------------------------------------------------------------------------------- /lib/ownership/rollbar.rb: -------------------------------------------------------------------------------- 1 | module Ownership 2 | module Rollbar 3 | class << self 4 | attr_reader :access_token 5 | 6 | def access_token=(access_token) 7 | @access_token = access_token 8 | @configure ||= configure # just once 9 | access_token 10 | end 11 | 12 | private 13 | 14 | def owner_access_token(owner) 15 | access_token.respond_to?(:call) ? access_token.call(owner) : access_token[owner] 16 | end 17 | 18 | def configure 19 | ::Rollbar.configure do |config| 20 | config.before_process << proc do |options| 21 | options[:scope][:ownership_owner] = Ownership.owner if Ownership.owner 22 | end 23 | 24 | config.transform << proc do |options| 25 | # clean up payload 26 | options[:payload]["data"].delete(:ownership_owner) 27 | 28 | owner = options[:exception].owner if options[:exception].respond_to?(:owner) 29 | unless owner 30 | owner = options[:scope][:ownership_owner] if options[:scope].is_a?(Hash) 31 | owner ||= Ownership.default_owner 32 | end 33 | 34 | if owner 35 | access_token = owner_access_token(owner) 36 | if access_token 37 | options[:payload]["access_token"] = access_token 38 | else 39 | warn "[ownership] Missing Rollbar access token for owner: #{owner}" 40 | end 41 | end 42 | end 43 | end 44 | true 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ownership/version.rb: -------------------------------------------------------------------------------- 1 | module Ownership 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /ownership.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/ownership/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "ownership" 5 | spec.version = Ownership::VERSION 6 | spec.summary = "Code ownership for Rails" 7 | spec.homepage = "https://github.com/ankane/ownership" 8 | spec.license = "MIT" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{lib}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 3.1" 17 | end 18 | -------------------------------------------------------------------------------- /test/active_record_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | $io = StringIO.new 4 | ActiveRecord::Base.logger = ActiveSupport::Logger.new($io) 5 | 6 | class ActiveRecordTest < Minitest::Test 7 | def setup 8 | ActiveRecord::QueryLogs.tags = [:owner] 9 | super 10 | $io.truncate(0) 11 | end 12 | 13 | def teardown 14 | ActiveRecord::QueryLogs.tags = [] 15 | end 16 | 17 | def test_owner 18 | owner(:logistics) do 19 | User.last 20 | end 21 | if ActiveRecord::VERSION::STRING.to_f >= 7.1 22 | assert_match "/*owner='logistics'*/", logs 23 | else 24 | assert_match "/*owner:logistics*/", logs 25 | end 26 | end 27 | 28 | def test_no_owner 29 | User.last 30 | refute_match "owner", logs 31 | end 32 | 33 | def logs 34 | $io.rewind 35 | $io.read 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/controller_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class ControllerTest < ActionDispatch::IntegrationTest 4 | def test_controller 5 | get root_url 6 | assert_equal :logistics, $current_owner 7 | end 8 | 9 | def test_only 10 | get users_url 11 | assert_equal :logistics, $current_owner 12 | end 13 | 14 | def test_except 15 | get user_url(1) 16 | assert_equal :customers, $current_owner 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/honeybadger_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | require "honeybadger/ruby" 4 | Honeybadger.init!(framework: :ruby, env: "test", "logging.path": Tempfile.new.path) 5 | 6 | Honeybadger.configure do |config| 7 | config.api_key = "default-key" 8 | config.backend = "test" 9 | config.logger = Logger.new(IO::NULL) 10 | end 11 | 12 | Ownership::Honeybadger.api_keys = { 13 | logistics: "logistics-key", 14 | sales: "sales-key", 15 | support: "support-key" 16 | } 17 | 18 | class HoneybadgerTest < Minitest::Test 19 | def setup 20 | super 21 | Honeybadger.config.backend.notifications.clear 22 | Honeybadger.context.clear! 23 | end 24 | 25 | def test_tagging 26 | Honeybadger.context tags: 'critical, badgers' 27 | 28 | owner :logistics do 29 | Honeybadger.notify("boom for logistics", sync: true) 30 | end 31 | 32 | assert_equal %w[critical badgers logistics], notices.last.tags 33 | 34 | Honeybadger.context.clear! 35 | 36 | owner :sales do 37 | Honeybadger.notify("boom for sales", sync: true) 38 | end 39 | 40 | assert_equal %w[sales], notices.last.tags 41 | end 42 | 43 | def test_uses_default_key_without_ownership_block 44 | Honeybadger.notify("boom for default", sync: true) 45 | 46 | assert_equal "default-key", notices.last.api_key 47 | end 48 | 49 | def test_uses_owner_key_within_ownership_block 50 | owner :logistics do 51 | Honeybadger.notify("boom for logistics", sync: true) 52 | end 53 | 54 | assert_equal "logistics-key", notices.last.api_key 55 | end 56 | 57 | def test_uses_default_key_and_warns_with_unknown_owner 58 | assert_output(nil, /Missing Honeybadger API key for owner: unknown/) do 59 | owner :unknown do 60 | Honeybadger.notify("boom for default", sync: true) 61 | end 62 | end 63 | 64 | assert_equal "default-key", notices.last.api_key 65 | end 66 | 67 | def test_async_works_properly 68 | owner :logistics do 69 | Honeybadger.notify("boom for logistics") 70 | Honeybadger.flush 71 | end 72 | 73 | assert_equal "logistics-key", notices.last.api_key 74 | end 75 | 76 | def test_prefer_exception_owner_over_thread_local_ownership 77 | owner :logistics do 78 | ex = StandardError.new("boom for sales") 79 | ex.owner = :sales 80 | 81 | Honeybadger.notify(ex, sync: true) 82 | end 83 | 84 | assert_equal "sales-key", notices.last.api_key 85 | end 86 | 87 | private 88 | 89 | def notices 90 | Honeybadger.config.backend.notifications[:notices] 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/internal/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ActionController::Base 2 | owner :logistics 3 | 4 | def index 5 | $current_owner = Ownership.owner 6 | head :ok 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/internal/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ActionController::Base 2 | owner :logistics, only: [:index] 3 | owner :customers, except: [:index] 4 | 5 | def index 6 | $current_owner = Ownership.owner 7 | head :ok 8 | end 9 | 10 | def show 11 | $current_owner = Ownership.owner 12 | head :ok 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/internal/app/jobs/test_job.rb: -------------------------------------------------------------------------------- 1 | class TestJob < ActiveJob::Base 2 | owner :logistics 3 | 4 | def perform 5 | $current_owner = Ownership.owner 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/internal/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/internal/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: db/combustion_test.sqlite 4 | -------------------------------------------------------------------------------- /test/internal/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root "home#index" 3 | resources :users, only: [:index, :show] 4 | end 5 | -------------------------------------------------------------------------------- /test/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table :users do |t| 3 | t.string :name 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/job_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class JobTest < Minitest::Test 4 | def test_job 5 | TestJob.perform_now 6 | assert_equal :logistics, $current_owner 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/marginalia_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class MarginaliaTest < Minitest::Test 4 | # no great way to test SQL comment unfortunately 5 | # ActiveSupport::Notifications are sent before the comment is added 6 | def test_marginalia 7 | assert_includes Marginalia::Comment.components, :owner 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/ownership_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class OwnershipTest < Minitest::Test 4 | def test_around 5 | owner :logistics do 6 | $around_calls << "middle" 7 | end 8 | assert_equal ["start", "middle", "finish"], $around_calls 9 | end 10 | 11 | def test_exception 12 | error = assert_raises do 13 | owner :logistics do 14 | raise "boom" 15 | end 16 | end 17 | assert_equal :logistics, error.owner 18 | end 19 | 20 | def test_nested_exception 21 | error = assert_raises do 22 | owner :logistics do 23 | owner :sales do 24 | raise "boom" 25 | end 26 | end 27 | end 28 | assert_equal :sales, error.owner 29 | end 30 | 31 | def test_default_owner 32 | assert_nil Ownership.owner 33 | Ownership.default_owner = :logistics 34 | assert_equal :logistics, Ownership.owner 35 | ensure 36 | Ownership.default_owner = nil 37 | end 38 | 39 | def test_respond_to? 40 | refute nil.respond_to?(:owner) 41 | end 42 | 43 | def test_method_owner 44 | assert_equal Kernel, method(:puts).owner 45 | end 46 | 47 | def test_pry 48 | assert_equal Kernel, Pry::Method.new(method(:puts)).wrapped_owner.wrapped 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/rollbar_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | require "rollbar" 4 | 5 | Rollbar.configure do |config| 6 | config.logger = Logger.new(nil) 7 | config.access_token = "footoken" 8 | config.transmit = false 9 | config.disable_monkey_patch = true 10 | config.use_payload_access_token = true 11 | end 12 | 13 | Ownership::Rollbar.access_token = { 14 | logistics: "logistics-token", 15 | sales: "sales-token", 16 | support: "support-token" 17 | } 18 | 19 | Rollbar.configure do |config| 20 | config.transform << proc do |options| 21 | $errors << options 22 | end 23 | end 24 | 25 | class RollbarTest < Minitest::Test 26 | def setup 27 | super 28 | $errors = [] 29 | end 30 | 31 | def test_error 32 | begin 33 | owner :logistics do 34 | raise "Error" 35 | end 36 | rescue => e 37 | Rollbar.error(e) 38 | end 39 | 40 | assert_equal 1, $errors.size 41 | assert_equal "logistics-token", $errors.last[:payload]["access_token"] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "logger" # for Rails 7.0 3 | require "combustion" 4 | Bundler.require(:default) 5 | require "minitest/autorun" 6 | require "minitest/pride" 7 | 8 | logger = ActiveSupport::Logger.new(ENV["VERBOSE"] ? STDOUT : nil) 9 | 10 | Combustion.path = "test/internal" 11 | Combustion.initialize! :active_record, :action_controller, :active_job do 12 | config.load_defaults Rails::VERSION::STRING.to_f 13 | 14 | config.action_controller.logger = logger 15 | config.active_record.logger = logger 16 | config.active_job.logger = logger 17 | 18 | config.active_record.query_log_tags_enabled = true 19 | 20 | require "marginalia" 21 | end 22 | 23 | class Minitest::Test 24 | def setup 25 | $current_owner = nil 26 | $around_calls = [] 27 | end 28 | end 29 | 30 | Ownership.around_change = proc do |owner, block| 31 | $around_calls << "start" 32 | block.call 33 | $around_calls << "finish" 34 | end 35 | 36 | # https://github.com/rails/rails/issues/54595 37 | if RUBY_ENGINE == "jruby" && Rails::VERSION::MAJOR >= 8 38 | Rails.application.reload_routes_unless_loaded 39 | end 40 | --------------------------------------------------------------------------------