Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3ef1422
Associate replays with errors and traces on Android
jamescrosswell Apr 22, 2025
9872376
Format code
getsentry-bot Apr 23, 2025
b8d9452
Set replay_id on DSC rather than directly on the event Context
jamescrosswell Apr 23, 2025
ae6157e
Update CHANGELOG.md
jamescrosswell Apr 23, 2025
6f848a3
Verify NetFX
getsentry-bot Apr 27, 2025
05c674c
Merge branch 'main' into replay-link-events
jamescrosswell Apr 28, 2025
cf534b9
Update CHANGELOG.md
jamescrosswell Apr 28, 2025
ff8019b
DSC tests
jamescrosswell Apr 28, 2025
0989eca
Hub Tests
jamescrosswell Apr 28, 2025
b43f1bf
Update AndroidEventProcessor.cs
jamescrosswell Apr 28, 2025
f6a8242
Protocol tests
jamescrosswell Apr 28, 2025
6386234
Fix verify tests
jamescrosswell Apr 28, 2025
41ba57d
Update DynamicSamplingContextTests.cs
jamescrosswell Apr 28, 2025
75997c6
Windows verify tests
getsentry-bot Apr 28, 2025
32ba50b
Format code
getsentry-bot Apr 28, 2025
efb200d
Merge branch 'main' into replay-link-events
jamescrosswell Apr 28, 2025
6d770cb
Ensure any replay_id in the PropagationContext is used when creating …
jamescrosswell Apr 29, 2025
37b40c5
Prefer our session replay id over the propagated ones
jamescrosswell May 1, 2025
356d39d
Converted static ReplayHelper to a Singleton (for cleaner testing)
jamescrosswell May 2, 2025
66e3f80
Add replayId scenarios to SentryPropagationContextTests
jamescrosswell May 2, 2025
edc0b9c
Format code
getsentry-bot May 2, 2025
81c4f50
Merge branch 'main' into replay-link-events
getsentry-bot May 2, 2025
b50ae47
Merge branch 'main' into replay-link-events
getsentry-bot May 2, 2025
7b95524
Windows verify tests
getsentry-bot May 2, 2025
330bd66
Merge branch 'replay-link-events' of https://github.com/getsentry/sen…
getsentry-bot May 2, 2025
39e56fd
Update ReplaySession.cs
jamescrosswell May 2, 2025
4f2f39c
Ensure trace or propagation context always gets applied to event
jamescrosswell May 4, 2025
4c2fc76
Merge branch 'main' into replay-link-events
jamescrosswell May 4, 2025
8ef8d66
Update CHANGELOG.md
jamescrosswell May 4, 2025
eee7a20
Merge branch 'main' into replay-link-events
jamescrosswell May 8, 2025
4ce0049
Removed ReplaySession.DisabledInstance
jamescrosswell May 8, 2025
c3ab0b4
Review feedback
jamescrosswell May 9, 2025
cf96b60
Update CHANGELOG.md
jamescrosswell May 9, 2025
b989d5c
Merge branch 'main' into replay-link-events
jamescrosswell May 10, 2025
54e5536
Update src/Sentry/DynamicSamplingContext.cs
jamescrosswell May 12, 2025
0b21161
Merge branch 'main' into replay-link-events
jamescrosswell May 12, 2025
369b529
Merge branch 'main' into replay-link-events
vaind May 13, 2025
438c4e6
chore: fixup changelog merge changes
vaind May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133))

## 5.7.0-beta.0

### Features
Expand Down
6 changes: 4 additions & 2 deletions src/Sentry.AspNet/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Protocol;

namespace Sentry.AspNet;
Expand Down Expand Up @@ -125,8 +126,9 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon
["__HttpContext"] = httpContext,
};

// Set the Dynamic Sampling Context from the baggage header, if it exists.
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext();
// Set the Dynamic Sampling Context from the baggage header, if it exists
// Note: We don't record Session Replays in ASP.NET
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(ReplaySession.DisabledInstance);

if (traceHeader is not null && baggageHeader is null)
{
Expand Down
4 changes: 3 additions & 1 deletion src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Options;
using Sentry.AspNetCore.Extensions;
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Internal.OpenTelemetry;

namespace Sentry.AspNetCore;
Expand Down Expand Up @@ -65,7 +66,8 @@ public SentryTracingMiddleware(
var baggageHeader = context.Items.TryGetValue(SentryMiddleware.BaggageHeaderItemKey, out var baggageHeaderObject)
? baggageHeaderObject as BaggageHeader : null;

var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext();
// Note: We don't record Session Replays in ASP.NET core
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(ReplaySession.DisabledInstance);

if (traceHeader is not null && baggageHeader is null)
{
Expand Down
8 changes: 5 additions & 3 deletions src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class SentrySpanProcessor : BaseProcessor<Activity>
{
private readonly IHub _hub;
internal readonly IEnumerable<IOpenTelemetryEnricher> _enrichers;
private readonly IReplaySession _replaySession;
internal const string OpenTelemetryOrigin = "auto.otel";

// ReSharper disable once MemberCanBePrivate.Global - Used by tests
Expand All @@ -38,7 +39,7 @@ public SentrySpanProcessor(IHub hub) : this(hub, null)
{
}

internal SentrySpanProcessor(IHub hub, IEnumerable<IOpenTelemetryEnricher>? enrichers)
internal SentrySpanProcessor(IHub hub, IEnumerable<IOpenTelemetryEnricher>? enrichers, IReplaySession? replaySession = null)
{
_hub = hub;
_realHub = new Lazy<Hub?>(() =>
Expand All @@ -57,7 +58,8 @@ internal SentrySpanProcessor(IHub hub, IEnumerable<IOpenTelemetryEnricher>? enri
"You should use the TracerProviderBuilderExtensions to configure Sentry with OpenTelemetry");
}

_enrichers = enrichers ?? Enumerable.Empty<IOpenTelemetryEnricher>();
_enrichers = enrichers ?? [];
_replaySession = replaySession ?? ReplaySession.Instance;
_options = hub.GetSentryOptions();

if (_options is null)
Expand Down Expand Up @@ -158,7 +160,7 @@ private void CreateRootSpan(Activity data)
};

var baggageHeader = data.Baggage.AsBaggageHeader();
var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext();
var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(_replaySession);
var transaction = (TransactionTracer)_hub.StartTransaction(
transactionContext, new Dictionary<string, object?>(), dynamicSamplingContext
);
Expand Down
51 changes: 40 additions & 11 deletions src/Sentry/DynamicSamplingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ internal class DynamicSamplingContext
public static readonly DynamicSamplingContext Empty = new(new Dictionary<string, string>().AsReadOnly());

private DynamicSamplingContext(
IReplaySession replaySession,
SentryId traceId,
string publicKey,
bool? sampled,
double? sampleRate = null,
double? sampleRand = null,
string? release = null,
string? environment = null,
string? transactionName = null)
string? transactionName = null
)
{
// Validate and set required values
if (traceId == SentryId.Empty)
Expand All @@ -51,7 +53,7 @@ private DynamicSamplingContext(
throw new ArgumentOutOfRangeException(nameof(sampleRand), "Arg invalid if < 0.0 or >= 1.0");
}

var items = new Dictionary<string, string>(capacity: 8)
var items = new Dictionary<string, string>(capacity: 9)
{
["trace_id"] = traceId.ToString(),
["public_key"] = publicKey,
Expand Down Expand Up @@ -88,12 +90,29 @@ private DynamicSamplingContext(
items.Add("transaction", transactionName);
}

if (replaySession.ActiveReplayId is { } replayId && replayId != SentryId.Empty)
{
items.Add("replay_id", replayId.ToString());
}

Items = items;
}

public BaggageHeader ToBaggageHeader() => BaggageHeader.Create(Items, useSentryPrefix: true);

public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage)
public DynamicSamplingContext WithReplayId(IReplaySession replaySession)
{
if (replaySession.ActiveReplayId is not { } replayId || replayId == SentryId.Empty)
{
return this;
}

var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
items["replay_id"] = replayId.ToString();
return new DynamicSamplingContext(items);
}

public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession replaySession)
{
var items = baggage.GetSentryMembers();

Expand Down Expand Up @@ -144,10 +163,18 @@ private DynamicSamplingContext(
}
items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture));
}

if (replaySession.ActiveReplayId is { } replayId)
{
// Overwrite any existing value - the DSC is simply used as a transport mechanism so that SDKs can
// communicate the replayId to Sentry Relay (SDKs don't need to propagate the replayId to each other).
items["replay_id"] = replayId.ToString();
}

return new DynamicSamplingContext(items);
}

public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options)
public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options, IReplaySession replaySession)
{
// These should already be set on the transaction.
var publicKey = options.ParsedDsn.PublicKey;
Expand All @@ -162,6 +189,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
var environment = options.SettingLocator.GetEnvironment();

return new DynamicSamplingContext(
replaySession,
traceId,
publicKey,
sampled,
Expand All @@ -172,14 +200,15 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
transactionName);
}

public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options)
public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession replaySession)
{
var traceId = propagationContext.TraceId;
var publicKey = options.ParsedDsn.PublicKey;
var release = options.SettingLocator.GetRelease();
var environment = options.SettingLocator.GetEnvironment();

return new DynamicSamplingContext(
replaySession,
traceId,
publicKey,
null,
Expand All @@ -190,12 +219,12 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat

internal static class DynamicSamplingContextExtensions
{
public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage)
=> DynamicSamplingContext.CreateFromBaggageHeader(baggage);
public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession replaySession)
=> DynamicSamplingContext.CreateFromBaggageHeader(baggage, replaySession);

public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options)
=> DynamicSamplingContext.CreateFromTransaction(transaction, options);
public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession replaySession)
=> DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession);

public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options)
=> DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options);
public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession replaySession)
=> DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession);
}
25 changes: 12 additions & 13 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal class Hub : IHub, IDisposable
private readonly ISessionManager _sessionManager;
private readonly SentryOptions _options;
private readonly RandomValuesFactory _randomValuesFactory;
private readonly IReplaySession _replaySession;

#if MEMORY_DUMP_SUPPORTED
private readonly MemoryMonitor? _memoryMonitor;
Expand All @@ -39,7 +40,8 @@ internal Hub(
ISessionManager? sessionManager = null,
ISystemClock? clock = null,
IInternalScopeManager? scopeManager = null,
RandomValuesFactory? randomValuesFactory = null)
RandomValuesFactory? randomValuesFactory = null,
IReplaySession? replaySession = null)
{
if (string.IsNullOrWhiteSpace(options.Dsn))
{
Expand All @@ -55,7 +57,7 @@ internal Hub(
_sessionManager = sessionManager ?? new GlobalSessionManager(options);
_clock = clock ?? SystemClock.Clock;
client ??= new SentryClient(options, randomValuesFactory: _randomValuesFactory, sessionManager: _sessionManager);

_replaySession = replaySession ?? ReplaySession.Instance;
ScopeManager = scopeManager ?? new SentryScopeManager(options, client);

if (!options.IsGlobalModeEnabled)
Expand Down Expand Up @@ -178,10 +180,10 @@ _options.TransactionProfilerFactory is { } profilerFactory &&
}
}

// Use the provided DSC, or create one based on this transaction.
// Use the provided DSC (adding the active replayId if necessary), or create one based on this transaction.
// DSC creation must be done AFTER the sampling decision has been made.
transaction.DynamicSamplingContext =
dynamicSamplingContext ?? transaction.CreateDynamicSamplingContext(_options);
transaction.DynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession)
?? transaction.CreateDynamicSamplingContext(_options, _replaySession);

// A sampled out transaction still appears fully functional to the user
// but will be dropped by the client and won't reach Sentry's servers.
Expand Down Expand Up @@ -224,7 +226,7 @@ public BaggageHeader GetBaggage()
}

var propagationContext = CurrentScope.PropagationContext;
return propagationContext.GetOrCreateDynamicSamplingContext(_options).ToBaggageHeader();
return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader();
}

public TransactionContext ContinueTrace(
Expand Down Expand Up @@ -254,7 +256,7 @@ public TransactionContext ContinueTrace(
string? name = null,
string? operation = null)
{
var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader);
var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession);
ConfigureScope(scope => scope.SetPropagationContext(propagationContext));

return new TransactionContext(
Expand Down Expand Up @@ -382,7 +384,7 @@ private void ApplyTraceContextToEvent(SentryEvent evt, SentryPropagationContext
evt.Contexts.Trace.TraceId = propagationContext.TraceId;
evt.Contexts.Trace.SpanId = propagationContext.SpanId;
evt.Contexts.Trace.ParentSpanId = propagationContext.ParentSpanId;
evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options);
evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession);
}

public bool CaptureEnvelope(Envelope envelope) => CurrentClient.CaptureEnvelope(envelope);
Expand Down Expand Up @@ -471,12 +473,9 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope)
{
// We get the span linked to the event or fall back to the current span
var span = GetLinkedSpan(evt) ?? scope.Span;
if (span is not null)
if (span is not null && span.IsSampled is not false)
{
if (span.IsSampled is not false)
{
ApplyTraceContextToEvent(evt, span);
}
ApplyTraceContextToEvent(evt, span);
}
else
{
Expand Down
40 changes: 40 additions & 0 deletions src/Sentry/Internal/ReplaySession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#if __ANDROID__
using Sentry.Android.Extensions;
#endif

namespace Sentry.Internal;

internal interface IReplaySession
{
public SentryId? ActiveReplayId { get; }
}

internal class ReplaySession : IReplaySession
{
public static readonly IReplaySession Instance = new ReplaySession();

internal static readonly IReplaySession DisabledInstance = new DisabledReplaySession();

private ReplaySession()
{
}

public SentryId? ActiveReplayId
{
get
{
#if __ANDROID__
// Check to see if a Replay ID is available
var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId();
return (replayId is { } id && id != SentryId.Empty) ? id : null;
#else
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the idea to add iOS here later? potentially Web for example if we were brave enough?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was planning for at least iOS...

#endif
}
}

private class DisabledReplaySession : IReplaySession
{
public SentryId? ActiveReplayId => null;
}
}
Loading
Loading