Skip to content

SentryFunctionsWorkerMiddleware does not propagate trace context for non-HTTP triggers (e.g. QueueTrigger) #5033

@max-skimmer

Description

@max-skimmer

Package

Sentry.Azure.Functions.Worker

.NET Flavor

.NET Core

.NET Version

10.0.103

OS

No response

OS Version

No response

Development Environment

Rider 2025.x (Windows)

Other Error Monitoring Solution

No

Other Error Monitoring Solution Name

No response

SDK Version

5.16.2

Self-Hosted Sentry Version

No response

Workload Versions

Workload version: 10.0.100-manifests.c992be6d
Installed Workload Id Manifest Version Installation Source

android 36.1.2/10.0.100 VS 18.3.11512.155, VS 17.14.37012.4
ios 26.2.10191/10.0.100 VS 18.3.11512.155, VS 17.14.37012.4
maccatalyst 26.2.10191/10.0.100 VS 18.3.11512.155, VS 17.14.37012.4
maui-windows 10.0.20/10.0.100 VS 18.3.11512.155, VS 17.14.37012.4

UseSentry or SentrySdk.Init call

// Program.cs

new HostBuilder()
      .ConfigureFunctionsWorkerDefaults((context, builder) =>
      {
          builder.UseSentry(context, options =>
          {
              options.Dsn = "...";
              options.TracesSampleRate = 1.0;
          });
      })
      .Build();

Steps to Reproduce

SentryFunctionsWorkerMiddleware.StartOrContinueTraceAsync only extracts sentry-trace and baggage headers from HttpRequestData. For non-HTTP triggers it passes null to ContinueTrace, generating a new trace every time:

      // Decompiled from SentryFunctionsWorkerMiddleware
      private async Task<TransactionContext> StartOrContinueTraceAsync(FunctionContext context)
      {
          string transactionName = context.FunctionDefinition.Name;
          HttpRequestData val = await context.GetHttpRequestDataAsync();
          if (val == null) // ← all non-HTTP triggers
          {
              return _hub.ContinueTrace(null, null, transactionName, "function");
          }
          // ... HTTP-only trace propagation
      }

Then Invoke forces this new transaction onto the scope:

_hub.ConfigureScope(scope => { scope.Transaction = transaction; });

This makes it impossible for user-defined middleware to continue a trace from a queue message, because SentryFunctionsWorkerMiddleware always overwrites scope.Transaction.

Steps:

  1. Azure Function A (timer trigger) sends a message to a Storage Queue, including sentry-trace and baggage headers in the message body
  2. Azure Function B (queue trigger) receives the message
  3. Custom middleware on Function B reads sentry-trace/baggage from the message, calls SentrySdk.ContinueTrace() and StartTransaction()
  4. SentryFunctionsWorkerMiddleware overwrites scope.Transaction with a new trace

Workaround: In custom middleware running after UseSentry(), override the scope:

SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);

This works but produces ghost transactions from SentryFunctionsWorkerMiddleware that cannot be suppressed (IsSampled has internal setter).

Suggested fix: StartOrContinueTraceAsync could accept trace context from non-HTTP sources via a pluggable extraction mechanism, or at minimum not overwrite scope.Transaction when it has already been set by user middleware.

Expected Result

Queue consumer function should be able to continue the trace from the producer. The sentry-trace and baggage values propagated through the queue message should be respected.

Actual Result

Every non-HTTP function invocation gets a new trace ID, breaking distributed tracing across queue-based workflows. User middleware cannot override this because SentryFunctionsWorkerMiddleware forces its own transaction onto the scope.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions