├── Gemfile ├── app ├── views │ ├── airbrake │ │ ├── index.html.erb │ │ └── _issue_description.en.erb │ └── airbrake_server_project_settings │ │ └── _show.html.erb ├── controllers │ ├── airbrake_server_project_settings_controller.rb │ └── airbrake_controller.rb └── models │ └── airbrake_server_project_settings.rb ├── config ├── routes.rb └── locales │ └── en.yml ├── test ├── test_helper.rb ├── unit │ └── airbrake_server_project_settings_test.rb └── integration │ └── airbrake_server_test.rb ├── lib └── airbrake_server │ └── hooks.rb ├── init.rb ├── db └── migrate │ └── 001_create_airbrake_server_project_settings.rb └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | gem "hpricot" -------------------------------------------------------------------------------- /app/views/airbrake/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= @issue.id %> 3 | <%= issue_url(@issue) %> 4 | <%= @issue.id %> 5 | 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | map.connect '/notifier_api/v2/notices/', :controller => 'airbrake', :action => 'index', :conditions => { :method => :post } 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the normal Rails helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') 3 | 4 | # Ensure that we are using the temporary fixture path 5 | Engines::Testing.set_fixture_path 6 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | airbrake: 3 | heading: Airbrake Server 4 | label: Airbrake Server 5 | fixed_version: Default target version 6 | category: Default issue category 7 | assign_to: Assign new tickets to user 8 | author: Author for new tickets 9 | priority: Priority for new tickets 10 | tracker: Tracker for new tickets 11 | reopen_strategy: Don't reopen tickets belonging to environment 12 | -------------------------------------------------------------------------------- /lib/airbrake_server/hooks.rb: -------------------------------------------------------------------------------- 1 | module AirbrakeServer 2 | module Hooks 3 | class ProjectSettingsTabHook < Redmine::Hook::ViewListener 4 | def helper_projects_settings_tabs(tabs) 5 | tabs[:tabs] << {:name => 'airbrake_server', :controller => 'airbrake_server_project_settings', :action => :show, :partial => 'airbrake_server_project_settings/show', :label => 'airbrake.heading'} 6 | tabs 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | require_dependency 'airbrake_server/hooks' 3 | 4 | Redmine::Plugin.register :redmine_airbrake_server do 5 | name 'Redmine Airbrake Server plugin' 6 | author 'Marcus Ilgner' 7 | description 'Allows Redmine to receive error notifications Airbrake-style' 8 | version '0.2' 9 | url 'https://github.com/milgner/redmine_airbrake_server' 10 | author_url 'http://marcusilgner.com' 11 | 12 | requires_redmine_plugin :project_settings_hook_plugin, :version_or_higher => '0.0.1' 13 | end 14 | 15 | config.gem 'hpricot' 16 | -------------------------------------------------------------------------------- /app/controllers/airbrake_server_project_settings_controller.rb: -------------------------------------------------------------------------------- 1 | class AirbrakeServerProjectSettingsController < ApplicationController 2 | unloadable 3 | 4 | before_filter :load_project_and_settings 5 | 6 | def update 7 | @settings.update_attributes(params[:settings]) 8 | redirect_to :controller => 'projects', :action => "settings", :id => @project, :tab => 'airbrake_server' 9 | end 10 | 11 | private 12 | 13 | def load_project_and_settings 14 | @project = Project.find(params[:project_id]) 15 | @settings = AirbrakeServerProjectSettings.find_or_create(@project) 16 | end 17 | end -------------------------------------------------------------------------------- /db/migrate/001_create_airbrake_server_project_settings.rb: -------------------------------------------------------------------------------- 1 | class CreateAirbrakeServerProjectSettings < ActiveRecord::Migration 2 | def self.up 3 | create_table :airbrake_server_project_settings do |t| 4 | t.column :project_id, :integer 5 | t.column :fixed_version_id, :integer 6 | t.column :category_id, :integer 7 | t.column :assign_to_id, :integer 8 | t.column :author_id, :integer 9 | t.column :tracker_id, :integer 10 | t.column :reopen_strategy, :string 11 | t.column :priority_id, :integer 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table :airbrake_server_project_settings 17 | end 18 | end -------------------------------------------------------------------------------- /test/unit/airbrake_server_project_settings_test.rb: -------------------------------------------------------------------------------- 1 | class ActivityTest < ActiveSupport::TestCase 2 | fixtures :projects, :issue_categories, :versions, :users, 3 | :trackers, :projects_trackers, :issue_statuses, :enumerations 4 | 5 | def test_find_or_create 6 | p = Project.find(1) 7 | s = AirbrakeServerProjectSettings.find_or_create(p) 8 | assert_equal p, s.project 9 | assert_equal User.anonymous, s.author 10 | assert_equal p.versions.last, s.fixed_version 11 | assert_not_nil s.category 12 | assert_equal p.issue_categories.first, s.category 13 | assert_equal p.trackers.first, s.tracker 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /app/models/airbrake_server_project_settings.rb: -------------------------------------------------------------------------------- 1 | class AirbrakeServerProjectSettings < ActiveRecord::Base 2 | belongs_to :project 3 | 4 | belongs_to :tracker 5 | belongs_to :category, :class_name => 'IssueCategory' 6 | belongs_to :fixed_version, :class_name => 'Version' 7 | 8 | belongs_to :author, :class_name => 'User' 9 | belongs_to :assign_to, :class_name => 'User' 10 | belongs_to :priority, :class_name => 'IssuePriority' 11 | 12 | def self.find_or_create(project) 13 | settings = find_by_project_id(project.id) 14 | unless settings 15 | settings = AirbrakeServerProjectSettings.new 16 | settings.project = project 17 | settings.author = User.anonymous 18 | settings.fixed_version = project.versions.last 19 | settings.category = project.issue_categories.first 20 | settings.tracker = project.trackers.first 21 | settings.save 22 | end 23 | settings 24 | end 25 | end -------------------------------------------------------------------------------- /app/views/airbrake/_issue_description.en.erb: -------------------------------------------------------------------------------- 1 | h3. <%= @notice['error']['message'] %> 2 | 3 | <% if @notice['error']['backtrace']['line'].is_a?(Array) %> 4 | h4. Backtrace: 5 | 6 | p((. <% @notice['error']['backtrace']['line'].each do |element| 7 | if (element['file'].start_with?('[PROJECT_ROOT]') && @settings[:project].repository && @settings[:project].repository.entry(element['file'][14..-1])) # don't create source link if the file isn't in the repository 8 | %>source:"<%= element['file'][14..-1] %>#L<%= element['number'] %>" in ??<%= element['method'] %>??<% 9 | else 10 | %>@<%=element['file']%>:<%=element['number']%>@ in ??<%=element['method']%>??<% 11 | end %> 12 | <% end %> 13 | <% end %> 14 | <% unless @notice['request'].nil? %> 15 | h4. Request: 16 | 17 | p((. URL: <%= @notice['request']['url'] %> 18 | Component: <%= @notice['request']['component']%> 19 | Action: <%= @notice['request']['action'] %> 20 | 21 | <% def explode_vars(vars) 22 | result = "|_.Key|_.Value|\n" 23 | vars.each_pair do |key, value| 24 | result << "|#{key}|#{value}|\n" 25 | end 26 | result 27 | end 28 | 29 | unless (@notice['request']['params'].nil?) %> 30 | h4. Parameters: 31 | 32 | <%= explode_vars(@notice['request']['params']) %> 33 | <% end %> 34 | 35 | <% # remove the 'true ||' bit if you want to save headers 36 | unless true || (@notice['request']['cgi_data'].nil?) %> 37 | h4. Headers: 38 | 39 | <%= explode_vars(@notice['request']['cgi_data']) %> 40 | <% end %> 41 | <% unless (@notice['request']['session'].nil?) %> 42 | h4. Session: 43 | 44 | <%= explode_vars(@notice['request']['session']) %> 45 | <% end %> 46 | 47 | <% end %> 48 | -------------------------------------------------------------------------------- /app/views/airbrake_server_project_settings/_show.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | @settings = AirbrakeServerProjectSettings.find_or_create(@project) 3 | %> 4 | <% form_for :settings, :url => { :controller => 'airbrake_server_project_settings', :action => 'update' } do |f| -%> 5 | <%= hidden_field_tag 'project_id', @project.id %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
<%= f.label 'author_id', l('airbrake.author') %><%= f.select 'author_id', @project.members.collect {|u| [ u.name, u.id ] }, {:include_blank => false} %>
<%= f.label 'assign_to_id', l('airbrake.assign_to') %><%= f.select 'assign_to_id', @project.members.collect{|u| [ u.name, u.id ]}, {:include_blank => true} %>
<%= f.label 'priority_id', l('airbrake.priority') %><%= f.select 'priority_id', IssuePriority.all.collect{|ip| [ip.name, ip.id]} %>
<%= f.label 'tracker_id', l('airbrake.tracker') %><%= f.select 'tracker_id', @project.trackers.collect{|t| [t.name, t.id]} %>
<%= f.label 'reopen_strategy', l('airbrake.reopen_strategy') %><%= f.text_field 'reopen_strategy', {:size => 20} %>
<%= f.label 'fixed_version_id', l('airbrake.fixed_version')%><%= f.select 'fixed_version_id', @project.versions.collect{|v| [v.name, v.id]} %>
<%= f.label 'category_id', l('airbrake.category')%><%= f.select 'category_id', @project.issue_categories.all.collect{|ic| [ic.name, ic.id]} %>
36 |
<%= f.submit t('button_save') %>
37 | 38 | <% end -%> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airbrake Server for Redmine 2 | 3 | An implementation of the [Airbrake](http://airbrakeapp.com) (formerly known as Hoptoad) protocol v2 for Redmine which automatically creates issues from notices submitted from your applications notifier. 4 | Inspired by and loosely based on the [Hoptoad server by Jan Schulz-Hofen](https://github.com/yeah/redmine_hoptoad_server) 5 | 6 | 7 | ## Installation 8 | 9 | Install the hpricot gem, then install the plugin into Redmine [as usual](http://www.redmine.org/projects/redmine/wiki/Plugins). 10 | Now go to _Administration -> Settings -> Incoming emails_ and, if neccessary, check _Enable WS for incoming emails_ and generate an API key. This is the key you will need in the next step to configure your notifier. 11 | 12 | 13 | ## Client-Configuration 14 | 15 | For a Rails application, just setup the [Airbrake notifier](https://github.com/airbrake/airbrake) as usual, then modify `config/initializers/airbrake.rb` according to your needs using this template: 16 | 17 | HoptoadNotifier.configure do |config| 18 | config.api_key = {:project => 'redmine_project_identifier', # the identifier you specified for your project in Redmine 19 | :tracker => 'Bug', # the name of your desired tracker 20 | :api_key => 'redmine_api_key', # the key you generated in the previous step 21 | :category => 'Development', # the name of a ticket category, optional 22 | :fixed_version_id => 25, # the default version, optional 23 | :assigned_to => 'admin', # the login of a user the ticket should get assigned to by default, optional 24 | :login => 'airbrake', # the login who should be displayed as author of the tickets. Defaults to anonymous. 25 | :reopen_strategy => 'production', # will only reopen if the error occurs on the specified environment. Defaults to all, optional. 26 | :priority => 5 # the default priority (use id instead of name, optional.) 27 | }.to_yaml 28 | config.host = 'my_redmine_host.com' # the hostname your Redmine runs at 29 | config.port = 443 # the port your Redmine runs at 30 | config.secure = true # sends data to your server using SSL, optional 31 | end 32 | 33 | Note: as of version 0.2, it is possible to specify defaults for all settings except :project and :api_key on the server using the web interface. Settings supplied by the client will take precedence however. 34 | 35 | That's it. You may run `rake airbrake:test` to generate a test issue. 36 | If it doesn't work, check your logs and configuration, then [submit an issue on Github](https://github.com/milgner/redmine_airbrake_server/issues) 37 | 38 | 39 | ## License 40 | 41 | This plugin is licensed under the [Apache license 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 42 | 43 | 44 | ## Author 45 | 46 | Written by Marcus Ilgner (mail@marcusilgner.com) 47 | 48 | [![Flattr this](http://api.flattr.com/button/flattr-badge-large.png)](http://flattr.com/thing/307397/Airbrake-server-plugin-for-the-Redmine-issue-tracker) 49 | -------------------------------------------------------------------------------- /test/integration/airbrake_server_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class AirbrakeServerTest < ActionController::IntegrationTest 4 | NOTIFIER_URL = '/notifier_api/v2/notices/' 5 | 6 | fixtures :projects, :versions, :users, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations 7 | 8 | @@xml_notice_data = < 10 | Airbrake Notifier 11 | 1.2.4 12 | http://airbrakeapp.com 13 | 14 | 15 | RuntimeError 16 | RuntimeError: I've made a huge mistake 17 | 18 | 19 | 20 | 21 | 22 | 23 | http://example.com 24 | 25 | 26 | 27 | example.org 28 | Mozilla 29 | 30 | 31 | 32 | /testapp 33 | production 34 | 1.0.0 35 | 36 | 37 | EOF 38 | 39 | @@xml_notice_data_full = " 40 | 41 | --- \n:project: ecookbook\n:priority: 5\n:assigned_to: jsmith\n:fixed_version_id: 3\n:tracker: Bug\n:login: jsmith\n:category: Development\n:api_key: \"1234567890\"\n:reopen_strategy: production\n" + @@xml_notice_data 42 | 43 | @@xml_notice_data_slim = " 44 | 45 | --- \n:api_key: \"1234567890\"\n:project: ecookbook\n" + @@xml_notice_data 46 | 47 | def setup 48 | Setting['mail_handler_api_key'] = "1234567890" 49 | end 50 | 51 | def test_routing 52 | assert_routing( 53 | {:method => :post, :path => '/notifier_api/v2/notices'}, 54 | :controller => 'airbrake', :action => 'index' 55 | ) 56 | end 57 | 58 | def test_create_new_issues 59 | i = post_and_find_issue(@@xml_notice_data_full) 60 | # 8b6a2e77 = Digest::MD5.hexdigest("RuntimeError: I've made a huge mistake")[0..7] 61 | assert_equal '[Airbrake] 8b6a2e77 RuntimeError in /testapp/app/models/user.rb:53', i.subject 62 | assert i.description.include?("RuntimeError: I've made a huge mistake") 63 | assert i.description.include?("/testapp/app/controllers/users_controller.rb") 64 | assert_equal "production", i.custom_value_for(IssueCustomField.find_by_name('Environment')).value 65 | assert_equal "1.0.0", i.custom_value_for(IssueCustomField.find_by_name('Version')).value 66 | assert_equal 'jsmith', i.author.login 67 | assert_equal 3, i.fixed_version_id 68 | end 69 | 70 | def test_with_local_settings 71 | AirbrakeServerProjectSettings.create(:project_id => 1, 72 | :category_id => 1, 73 | :fixed_version_id => 3, 74 | :tracker_id => 1, 75 | :author_id => 2, 76 | :assign_to_id => 3, 77 | :priority_id => 5, 78 | :reopen_strategy => 'production') 79 | s = AirbrakeServerProjectSettings.find_by_project_id(1) 80 | assert_equal 1, s.category_id 81 | i = post_and_find_issue(@@xml_notice_data_slim) 82 | assert_equal(1, i.category_id) 83 | assert_equal(3, i.fixed_version_id) 84 | assert_equal(2, i.author_id) 85 | assert_equal(5, i.priority_id) 86 | assert_equal(3, i.assigned_to_id) 87 | end 88 | 89 | def test_increase_occurrences_for_existing_issues 90 | i = post_and_find_issue(@@xml_notice_data_full) 91 | assert_equal "1", i.custom_value_for(IssueCustomField.find_by_name('# Occurrences').id).value 92 | i2 = post_and_find_issue(@@xml_notice_data_full) 93 | assert_equal i, i2 94 | assert_equal "2", i2.custom_value_for(IssueCustomField.find_by_name('# Occurrences').id).value 95 | end 96 | 97 | def test_reopen_journal 98 | i = post_and_find_issue(@@xml_notice_data_full) 99 | i.status = IssueStatus.find(:first, :conditions => {:is_closed => true}, :order => 'position ASC') 100 | i.save 101 | assert_equal 0, i.journals.size 102 | i2 = post_and_find_issue(@@xml_notice_data_full) 103 | assert !i2.status.is_closed? 104 | assert_equal 1, i2.journals.size 105 | end 106 | 107 | def test_settings_page 108 | log_user("admin", "admin") 109 | get(url_for(:controller => 'projects', :action => "settings", :id => 'ecookbook', :tab => 'airbrake_server')) 110 | assert_response :success 111 | end 112 | 113 | private 114 | 115 | #def filtered_backtrace 116 | # if exception = @response.template.instance_variable_get(:@exception) 117 | # filter_backtrace(exception.backtrace).join("\n") 118 | # end 119 | #end 120 | 121 | def post_and_find_issue(issue_data) 122 | post(NOTIFIER_URL, issue_data, {"Content-type" => "text/xml"}) 123 | assert_response :success #, filtered_backtrace 124 | Issue.find :last 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /app/controllers/airbrake_controller.rb: -------------------------------------------------------------------------------- 1 | require 'hpricot' 2 | 3 | class AirbrakeController < ApplicationController 4 | skip_before_filter :check_if_login_required 5 | before_filter :find_or_create_custom_fields 6 | 7 | unloadable 8 | 9 | def index 10 | redirect_to root_path unless request.post? && !params[:notice].nil? 11 | @notice = params[:notice] 12 | if (@notice['version'] != "2.0") 13 | logger.warn("Expected Airbrake notice version 2.0 but got #{@notice['version']}. You should consider filing an enhancement request or updating the plugin.") 14 | end 15 | 16 | restore_var_elements(request.body) 17 | 18 | redmine_params = YAML.load(@notice['api_key']) 19 | raise ArgumentError.new("Invalid API key #{Setting.mail_handler_api_key} != #{redmine_params[:api_key]}") unless Setting.mail_handler_api_key == redmine_params[:api_key] 20 | 21 | read_settings(redmine_params) 22 | 23 | subject = build_subject 24 | @issue = Issue.find_by_subject_and_project_id_and_tracker_id(subject, @settings[:project].id, @settings[:tracker].id) 25 | 26 | if @issue.nil? 27 | create_new_issue 28 | else 29 | update_existing_issue 30 | end 31 | 32 | render :layout => false 33 | end 34 | 35 | private 36 | 37 | # The automagic XML parsing by Rails ignores the text elements 38 | # This method replaces the garbled elements with new hashes 39 | def restore_var_elements(original_xml) 40 | return if @notice['request'].nil? 41 | 42 | doc = Hpricot::XML(request.body) 43 | 44 | unless @notice['request']['params'].nil? 45 | request_params = convert_var_elements(doc/'/notice/request/params/var') 46 | request_params.delete('action') # already known 47 | request_params.delete('controller') # already known 48 | @notice['request']['params'] = request_params 49 | end 50 | 51 | unless @notice['request']['cgi_data'].nil? 52 | cgi_data = convert_var_elements(doc/'notice/request/cgi-data/var') 53 | @notice['request']['cgi_data'] = cgi_data 54 | end 55 | 56 | unless @notice['request']['session'].nil? 57 | session_vars = convert_var_elements(doc/'/notice/request/session/var') 58 | @notice['request']['session'] = session_vars 59 | end 60 | end 61 | 62 | def convert_var_elements(elements) 63 | result = {} 64 | elements.each do |elem| 65 | result[elem.attributes['key']] = elem.inner_text 66 | end 67 | result 68 | end 69 | 70 | def read_settings(params) 71 | project = Project.find_by_identifier(params[:project]) or raise ArgumentError.new("invalid project #{params[:project]}") 72 | @settings = {} 73 | @settings[:project] = project 74 | @settings[:tracker] = project.trackers.find_by_name(params[:tracker]) if params.has_key?(:tracker) 75 | # these are optional 76 | [:reopen_strategy, :fixed_version_id].each do |key| 77 | @settings[key] = params[key] if params.has_key?(key) 78 | end 79 | @settings[:priority] = IssuePriority.find_by_id(params[:priority]) if params.has_key?(:priority) 80 | @settings[:author] = User.find_by_login(params[:login]) if params.has_key?(:login) 81 | @settings[:category] = IssueCategory.find_by_name(params[:category]) if params.has_key?(:category) 82 | @settings[:assign_to] = User.find_by_login(params[:assigned_to]) if params.has_key?(:assigned_to) 83 | 84 | read_local_settings 85 | check_custom_field_assignments 86 | end 87 | 88 | def read_local_settings 89 | local_settings = AirbrakeServerProjectSettings.find_by_project_id(@settings[:project].id) 90 | return if local_settings.nil? 91 | [:author, :priority, :reopen_strategy, :tracker, :category, :assign_to, :fixed_version_id].each do |key| 92 | @settings[key] = local_settings.send(key.to_s) unless @settings.has_key?(key) 93 | end 94 | end 95 | 96 | def create_new_issue 97 | @issue = Issue.new 98 | @issue.author = @settings[:author] 99 | @issue.subject = build_subject 100 | @issue.tracker = @settings[:tracker] 101 | @issue.project = @settings[:project] 102 | @issue.category = @settings[:category] 103 | @issue.fixed_version_id = @settings[:fixed_version_id] 104 | @issue.assigned_to = @settings[:assign_to] 105 | @issue.priority = @settings[:priority] unless @settings[:priority].nil? 106 | @issue.description = render_to_string(:partial => 'issue_description') 107 | @issue.status = issue_status_open 108 | @issue.custom_values.build(:custom_field => @occurrences_field, :value => '1') 109 | @issue.custom_values.build(:custom_field => @environment_field, :value => @notice['server_environment']['environment_name']) 110 | @issue.custom_values.build(:custom_field => @version_field, :value => @notice['server_environment']['app_version']) unless @notice['server_environment']['app_version'].nil? 111 | @issue.save! 112 | end 113 | 114 | def check_custom_field_assignments 115 | [@occurrences_field, @environment_field, @version_field].each do |field| 116 | @settings[:project].issue_custom_fields << field unless @settings[:project].issue_custom_fields.include?(field) 117 | @settings[:tracker].custom_fields << field unless @settings[:tracker].custom_fields.include?(field) 118 | end 119 | end 120 | 121 | def update_existing_issue 122 | environment_name = @notice['server_environment']['environment_name'] 123 | if (['always', environment_name].include?(@settings[:reopen_strategy])) 124 | @issue.status = issue_status_open if @issue.status.is_closed? 125 | @issue.init_journal(@settings[:author], render_to_string(:partial => 'issue_description')) 126 | end 127 | number_occurrences = @issue.custom_value_for(@occurrences_field.id).value 128 | @issue.custom_field_values = { @occurrences_field.id => (number_occurrences.to_i+1).to_s } 129 | @issue.save! 130 | end 131 | 132 | def issue_status_open 133 | IssueStatus.find(:first, :conditions => {:is_default => true}, :order => 'position ASC') 134 | end 135 | 136 | def build_subject 137 | error_class = @notice['error']['message'] 138 | # if there's only one line, it gets parsed into a hash instead of an array 139 | if @notice['error']['backtrace']['line'].is_a? Hash 140 | file = @notice['error']['backtrace']['line']['file'] 141 | line = @notice['error']['backtrace']['line']['number'] 142 | else 143 | file = @notice['error']['backtrace']['line'].first()['file'] 144 | line = @notice['error']['backtrace']['line'].first()['number'] 145 | end 146 | "[Airbrake] #{build_message_hash} #{error_class} in #{file}:#{line}"[0..254] 147 | end 148 | 149 | def build_message_hash 150 | Digest::MD5.hexdigest(@notice['error']['message'])[0..7] 151 | end 152 | 153 | def find_or_create_custom_fields 154 | @occurrences_field = IssueCustomField.find_or_initialize_by_name('# Occurrences') 155 | if @occurrences_field.new_record? 156 | @occurrences_field.attributes = {:field_format => 'int', :default_value => '1', :is_filter => true} 157 | @occurrences_field.save(false) 158 | end 159 | 160 | @environment_field = IssueCustomField.find_or_initialize_by_name('Environment') 161 | if @environment_field.new_record? 162 | @environment_field.attributes = {:field_format => 'string', :default_value => 'production', :is_filter => true} 163 | @environment_field.save(false) 164 | end 165 | 166 | @version_field = IssueCustomField.find_or_initialize_by_name('Version') 167 | if @version_field.new_record? 168 | @version_field.attributes = {:field_format => 'string', :default_value => '', :is_filter => true} 169 | @version_field.save(false) 170 | end 171 | end 172 | end 173 | --------------------------------------------------------------------------------