Skip to content

feat(ruby): add job extensions and shared instrumentation - #3129

Open
solnic wants to merge 1 commit into
masterfrom
feat/3117-public-backend-extension-registry-for-background-job-integrations
Open

solnic wants to merge 1 commit into
masterfrom
feat/3117-public-backend-extension-registry-for-background-job-integrations

Conversation

@solnic

@solnic solnic commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

This introduces a new public API for queue systems (ActiveJob, Sidekiq, GoodJob, SolidQueue etc.) to integrate with Sentry by simply defining an extension class.

Here's how it works:

module MyBackend
  class SentryExtension < Sentry::JobExtensions::Extension
    def initialize
      super(name: "my_backend")
    end

    def handles?(job)
      job.class.queue_adapter_name.to_s == "my_backend"
    end

    def scope_data(job:)
      { tags: { queue: job.queue_name } }
    end

    def messaging_data(job:)
      {
        system: "my_backend",
        message_id: job.job_id,
        destination: job.queue_name
      }
    end
  end
end

Sentry.register_job_extension(MyBackend::SentryExtension.new)

The registry selects the first registered extension that handles the job.

Hook defaults

A bare extension supplies these hook defaults:

extension = MyBackend::SentryExtension.new

extension.error_data(job: job, exception: error)        # => {}
extension.transaction_name(job: job)                   # => nil
extension.report_exception?(job: job, exception: error) # => nil
extension.inject_trace_headers(job: job, payload: payload, headers: headers) # => payload

{} adds no metadata, nil supplies no override, and returning the original payload leaves it unchanged. false from report_exception? explicitly suppresses capture.

Integration defaults

An integration can supply its own default extension without registering it globally:

registry = Sentry.job_extensions.with_default(default_extension)
extension = registry.for(job)
registry.call(extension, :transaction_name, job: job)

When no backend matches, the registry uses the default extension. Otherwise, both hook results are validated and combined through the default extension’s merge_hook. Its base implementation uses the backend result unless it is nil; false and {} remain explicit results.

Validation and fallbacks

Integrations invoke hooks through the registry, which validates their results:

class BrokenExtension < MyBackend::SentryExtension
  def messaging_data(job:)
    { system: "my_backend", message_id: 123 }
  end

  def scope_data(job:)
    raise "Unable to read backend metadata"
  end
end

extension = BrokenExtension.new

Sentry.job_extensions.call(extension, :messaging_data, job: job) # => {}
Sentry.job_extensions.call(extension, :scope_data, job: job)     # => {}

message_id must be a String, so the malformed result is rejected. Both malformed results and StandardError failures are logged and replaced with the hook’s fallback. Unknown metadata keys are dropped. Exceptions outside StandardError propagate.

Fallbacks are specific to each hook:

  • metadata falls back to {}
  • optional overrides fall back to nil
  • header injection falls back to the original payload
  • for header extraction, nil preserves trace handling, whereas a valid {} explicitly starts a new trace

Custom validation

Standard validators work automatically. A backend can optionally supply its own rules:

validators = Sentry::JobExtensions::Validators.new(
  messaging_fields:
    Sentry::JobExtensions::Validators::MESSAGING_DATA_VALIDATORS.merge(
      message_id: ->(value) { value.is_a?(String) && !value.empty? }
    )
)

extension = Sentry::JobExtensions::Extension.new(
  name: "custom_backend",
  validators: validators
)

Shared instrumentation

Adds shared producer and consumer helpers for spans, trace propagation, scope enrichment, retry decisions, and exception capture. publish_messaging_data defaults to messaging_data and can be overridden for enqueue-specific attributes.

A backend that already instruments enqueueing can return a delegated producer to avoid creating another span:

def producer(job:)
  @producer ||= Sentry::JobExtensions::Producer::Delegated.new
end

Consumer applies extension scope data, starts and finishes consumer transactions, and handles exception capture according to the backend’s reporting policy:

consumer = Sentry::JobExtensions::Consumer.new(
  integration: Sentry::Rails,
  origin: "auto.queue.active_job",
  registry: registry
)

consumer.apply_scope_data(scope, job)
consumer.capture_exception(job, error) if consumer.report_exception?(job, error)

The following PRs add core queue conformance and make ActiveJob and Sidekiq use these helpers.


Closes #3117
Closes #3123

@solnic
solnic added this pull request to stack #3135 October 13, 2026 13:17
@solnic
solnic marked this pull request as draft October 13, 2026 13:25
@solnic
solnic force-pushed the feat/3117-public-backend-extension-registry-for-background-job-integrations branch from 120f7a3 to 24621bb Compare October 14, 2026 09:17
@solnic
solnic marked this pull request as ready for review October 14, 2026 09:49
@solnic
solnic force-pushed the feat/3117-public-backend-extension-registry-for-background-job-integrations branch 3 times, most recently from 18edb4c to 605bebc Compare October 14, 2026 11:42
@solnic
solnic removed this pull request from stack #3135 October 14, 2026 11:44
@solnic solnic changed the title feat(ruby): add public backend-extension registry feat(ruby): add job extensions and shared instrumentation Oct 14, 2026
@solnic
solnic added this pull request to stack #3137 October 14, 2026 11:44
cursor[bot]

This comment was marked as resolved.

@solnic
solnic marked this pull request as draft October 15, 2026 08:25
@solnic
solnic force-pushed the feat/3117-public-backend-extension-registry-for-background-job-integrations branch 3 times, most recently from 2cddb72 to 3fa983c Compare October 15, 2026 11:00
@solnic
solnic marked this pull request as ready for review October 15, 2026 12:19
cursor[bot]

This comment was marked as resolved.

@solnic
solnic force-pushed the feat/3117-public-backend-extension-registry-for-background-job-integrations branch 2 times, most recently from 27970e7 to e3ade51 Compare October 15, 2026 12:51
cursor[bot]

This comment was marked as resolved.

@solnic
solnic force-pushed the feat/3117-public-backend-extension-registry-for-background-job-integrations branch from 5eaa1a5 to 77e02b1 Compare October 15, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shared queue instrumentation helpers Public backend-extension registry for background job integrations

1 participant