lib/sentry/rspec.rb



# frozen_string_literal: true

RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts|
  match do |sentry_events|
    @expected_exception = expected_exception(**opts)
    @context = context(**opts)
    @tags = tags(**opts)

    @expected_event = expected_event(event_message)
    @matched_event = find_matched_event(event_message, sentry_events)

    return false unless @matched_event

    [verify_context(), verify_tags()].all?
  end

  chain :with_context do |context|
    @context = context
  end

  chain :with_tags do |tags|
    @tags = tags
  end

  failure_message do |sentry_events|
    info = ["Failed to find event matching:\n"]
    info << "  message: #{@expected_event.message.inspect}"
    info << "  exception: #{@expected_exception.inspect}"
    info << "  context: #{@context.inspect}"
    info << "  tags: #{@tags.inspect}"
    info << "\n"
    info << "Captured events:\n"
    info << dump_events(sentry_events)
    info.join("\n")
  end

  def expected_event(event_message)
    if @expected_exception
      Sentry.get_current_client.event_from_exception(@expected_exception)
    else
      Sentry.get_current_client.event_from_message(event_message)
    end
  end

  def expected_exception(**opts)
    opts[:exception].new(opts[:message]) if opts[:exception]
  end

  def context(**opts)
    opts.fetch(:context, @context || {})
  end

  def tags(**opts)
    opts.fetch(:tags, @tags || {})
  end

  def find_matched_event(event_message, sentry_events)
    @matched_event ||= sentry_events
      .find { |event|
        if @expected_exception
          # Is it OK that we only compare the first exception?
          event_exception = event.exception.values.first
          expected_event_exception = @expected_event.exception.values.first

          event_exception.type == expected_event_exception.type && event_exception.value == expected_event_exception.value
        else
          event.message == @expected_event.message
        end
      }
  end

  def dump_events(sentry_events)
    sentry_events.map(&Kernel.method(:Hash)).map do |hash|
      hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) }
    end.map do |hash|
      JSON.pretty_generate(hash)
    end.join("\n\n")
  end

  def verify_context
    return true if @context.empty?

    @matched_event.contexts.any? { |key, value| value == @context[key] }
  end

  def verify_tags
    return true if @tags.empty?

    @tags.all? { |key, value| @matched_event.tags.include?(key) && @matched_event.tags[key] == value }
  end
end