【问题标题】:Why does my production logs show an ActiveRecord::RecordNotUnique error but my local binding.pry shows me an ActiveModel::Errors?为什么我的生产日志显示 ActiveRecord::RecordNotUnique 错误,但我的本地 binding.pry 显示 ActiveModel::Errors?
【发布时间】:2019-12-04 22:29:21
【问题描述】:

我有一个注册表单,其中包含对模型和数据库的验证,以防止重复条目。

我在生产环境中使用 honeybadger 来记录错误。当用户尝试使用相同的凭据进行注册时,honeybadger 会报告ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry for ....。错误的其余部分包含 PII,我试图阻止它(我在一家金融公司工作,所以这是一个合规问题)。

我的解决方案是将Model.create 包装在一个救援块中,并在报告之前自定义蜜獾错误。我为它写了一些 rspecs,但一直失败。当我在块中包含 binding.pry 时,我可以看到副本创建错误,但错误是 ActiveModel::Errors 的实例。现在,我可以尝试挽救ActiveModel::Errors 错误,但我担心ActiveRecord::RecordNotUnique 错误仍会记录在生产环境中,这是我正在努力改变的。

我想不通:

1) 为什么生产和本地会显示不同类型的错误?

2) 为了从我们的生产日志记录中提供自定义错误消息(并隐藏 PII),我必须从哪类错误中解救出来。

我们将不胜感激任何和所有的帮助。谢谢!

作为整体解决方案我尝试过的一些事情是:

1) 使用准备好的语句。但是这不起作用,因为我使用的 ActiveRecord (4.2.11) 版本没有准备好的语句。

2) 使用 Honeybadger 的能力忽略以下错误: https://docs.honeybadger.io/lib/ruby/getting-started/ignoring-errors.html,但是,团队决定我们不想完全关闭错误。

# /app/models/prime_signup.rb
class PrimeSignup < ActiveRecord::Base
 validates_presence_of :first_name, :last_name, :email
 validates :email, uniqueness: true

  def person
    @person ||= Person.find_by(email: email)
  end

  def full_name
    "#{first_name} #{last_name}"
  end
end

# /db/schema.rb
create_table "prime_signups", force: :cascade do |t|
    t.string   "first_name",   limit: 255
    t.string   "last_name",    limit: 255
    t.string   "email",        limit: 255
    t.string   "phone_number", limit: 20
  end

  add_index "prime_signups", ["email"], name: "index_prime_signups_on_email", unique: true, using: :btree

# /app/controllers/api/v1/prime_signups_controller.rb

class API::V1::PrimeSignupsController < API::V1Controller
 // omitting skip_before_actions for brevity

  def create
    return render_forward_compatible_json_error(json_error, resource) unless resource.valid?
    service.perform
    render json: resource, serializer: API::V1::PrimeSignupSerializer, status: 201
  end

  private

  def resource_params
    params.require(:prime_signup).permit(:first_name, :last_name, :email, :phone_number,
                                         :utm_source, :utm_medium, :utm_campaign, :utm_term,
                                         :utm_content)
  end

  def resource # This is the method I'm trying to rescue the error from
    binding.pry 
    begin
      @resource ||= PrimeSignup.create(resource_params)
    rescue ActiveRecord::RecordNotUnique => e  # This is how I'm trying to customize the error
      Honeybadger.notify(
        error,
        error_message: 'Duplicate Entry',
      )
    end
  end

  def json_error
    JSONExceptions::InvalidFieldValues.new(detail: resource_errors)
  end

  def resource_errors
    resource.errors.messages.map {|field, message| "#{field} #{message.join}."}.join(" ")
  end

  def service
    ::Services::PrimeSignupCreation.new(resource)
  end

  def render_forward_compatible_json_error(error, resource)
    json_error_format = { errors: [error.to_json] }
    resource_key = resource.class.name.snakecase
    old_error_format = {resource_key => resource.errors.details}
    render json: json_error_format.merge(old_error_format), status: error.status
  end
end

require 'rails_helper'

describe 'API::V1::PrimeSignups', type: :request do
  describe 'POST /api/v1/prime_signups' do
    context 'duplicate entry' do
      it 'raises a custom honeybadger error' do

        prime_signup_params = {
          prime_signup: {
            first_name: "Walter",
            last_name: "White",
            email: "walter@white.com",
            phone_number: '123456789'
          },
          authenticity_token: 'authenticated',
          format: :json
        }

        expect(Honeybadger).to receive(:notify)
        VCR.use_cassette('/api/v1/prime_signups') do
          post '/api/v1/prime_signups', prime_signup_params.to_json, { "CONTENT_TYPE"=>"application/json" }
          post '/api/v1/prime_signups', prime_signup_params.to_json, { "CONTENT_TYPE"=>"application/json" }
        end
        expect(response.status).to eq(422)

        # VCR.use_cassette('/api/v1/prime_signups') do
        #   post '/api/v1/prime_signups', prime_signup_params.to_json, { "CONTENT_TYPE"=>"application/json" }
        # end
      end
    end
  end
end

我希望从中救出的错误是 ActiveRecord::RecordNotUnique 类型或记录的生产错误是 ActiveModel::Errors 类型。基本上期望两者之间的一致性。

此外,我们将不胜感激任何有关规格和更好格式的指导。我不擅长。

【问题讨论】:

    标签: ruby-on-rails ruby activerecord rspec activemodel


    【解决方案1】:

    您在生产中看到ActiveRecord::RecordNotUnique 而在本地看到ActiveModel::Errors 的原因可能是由于生产中的竞争条件您没有在本地复制。

    也就是说,在生产中可能发生的情况是唯一性验证正在通过,因为正在尝试同时创建具有重复信息的两条记录,并且每条运行中的记录都是有效的,因为它们都找不到现有记录在具有相同电子邮件地址的数据库中。其中一个创建成功,然后第二个创建失败,因为第一个创建在第二个完成唯一性查询之后但在插入之前被数据库持久化。

    在您的测试环境中,两次尝试创建是按顺序进行的,而不是同时进行的,因此第二次尝试不会通过唯一性检查。

    要测试控制器的生产行为,您需要存根 PrimeSignup.create 并让它引发 ActiveRecord::RecordNotUnique 错误。

    虽然你没有问这个,但我也会给你一个替代方法的建议,因为我恰好是 Honeybadger 的联合创始人之一...... :) 你可以将 email 参数添加到您的 Honeybadger 配置中的 list of filtered parameters,然后该 PII 将不会与其余错误信息一起报告。

    【讨论】:

    • 嘿本杰明。感谢您的回复以及您和您的团队创建的很棒的公司。我们喜欢你的产品。比赛条件完全有道理。对于您的最后一部分,在我们的配置中,我们禁用了所有参数。所以听起来 ActiveRecord::RecordNotUnique 消息可能由于某种原因超出了该配置(可能正在报告 mysql 字符串?)
    • 哦,对了...对不起——我正在考虑过滤参数,但您需要从与ActiveRecord::RecordNotUnique 错误相关的错误消息中过滤查询。以下是我们在 Rails 应用程序中处理该问题的方法:gist.github.com/stympy/39aa691f90b97fb4e4912ecbeb8ddbe0 在我们的例子中,我们从 Postgres 查询中删除私钥数据,因此您需要稍微调整一下,但这是一般的想法。
    【解决方案2】:

    这样,Rails 验证在验证失败时不应允许保存,并且您根本不必处理救援异常。

    def resource 
      @resource ||= PrimeSignup.find_or_initialize_by(resource_params)
        if @resource.save
          Honeybadger.notify(
            @resource.errors.full_messages,
            error_message:  @resource.errors.full_messages.join(', ')
          )
        end
      end
    end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-23
      • 2020-11-11
      • 1970-01-01
      • 1970-01-01
      • 2013-12-17
      • 1970-01-01
      相关资源
      最近更新 更多