Skip to content

Commit 2e1fff1

Browse files
feat: Associate replays with errors and traces on Android (#4133)
Part of #2136 Associate Errors and Traces with the active Session Replay (if one exists) on Android.
1 parent 162f0d4 commit 2e1fff1

File tree

14 files changed

+411
-114
lines changed

14 files changed

+411
-114
lines changed

CHANGELOG.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- New source generator allows Sentry to see true build variables like PublishAot and PublishTrimmed to properly adapt checks in the Sentry SDK ([#4101](https://github.com/getsentry/sentry-dotnet/pull/4101))
8+
- Auto breadcrumbs now include all .NET MAUI gesture recognizer events ([#4124](https://github.com/getsentry/sentry-dotnet/pull/4124))
9+
- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133))
10+
511
### Fixes
612

713
- Redact Authorization headers before sending events to Sentry ([#4164](https://github.com/getsentry/sentry-dotnet/pull/4164))
814
- Remove Strong Naming from Sentry.Hangfire ([#4099](https://github.com/getsentry/sentry-dotnet/pull/4099))
915
- Increase `RequestSize.Small` threshold from 1 kB to 4 kB to match other SDKs ([#4177](https://github.com/getsentry/sentry-dotnet/pull/4177))
1016

11-
### Features
12-
13-
- New source generator allows Sentry to see true build variables like PublishAot and PublishTrimmed to properly adapt checks in the Sentry SDK ([#4101](https://github.com/getsentry/sentry-dotnet/pull/4101))
14-
- Auto breadcrumbs now include all .NET MAUI gesture recognizer events ([#4124](https://github.com/getsentry/sentry-dotnet/pull/4124))
15-
1617
### Dependencies
1718

1819
- Bump CLI from v2.43.1 to v2.45.0 ([#4169](https://github.com/getsentry/sentry-dotnet/pull/4169), [#4179](https://github.com/getsentry/sentry-dotnet/pull/4179))
@@ -23,7 +24,7 @@
2324

2425
### Features
2526

26-
- When setting a transaction on the scope, the SDK will attempt to sync the transaction's trace context with the SDK on the native layer. Finishing a transaction will now also start a new trace ([#4153](https://github.com/getsentry/sentry-dotnet/pull/4153))
27+
- When setting a transaction on the scope, the SDK will attempt to sync the transaction's trace context with the SDK on the native layer. Finishing a transaction will now also start a new trace ([#4153](https://github.com/getsentry/sentry-dotnet/pull/4153))
2728
- Added `CaptureFeedback` overload with `configureScope` parameter ([#4073](https://github.com/getsentry/sentry-dotnet/pull/4073))
2829
- Custom SessionReplay masks in MAUI Android apps ([#4121](https://github.com/getsentry/sentry-dotnet/pull/4121))
2930

@@ -45,7 +46,7 @@
4546
### Features
4647

4748
- Option to disable the SentryNative integration ([#4107](https://github.com/getsentry/sentry-dotnet/pull/4107), [#4134](https://github.com/getsentry/sentry-dotnet/pull/4134))
48-
- To disable it, add this msbuild property: `<SentryNative>false</SentryNative>`
49+
- To disable it, add this msbuild property: `<SentryNative>false</SentryNative>`
4950
- Reintroduced experimental support for Session Replay on Android ([#4097](https://github.com/getsentry/sentry-dotnet/pull/4097))
5051
- If an incoming HTTP request has the `traceparent` header, it is now parsed and interpreted like the `sentry-trace` header. Outgoing requests now contain the `traceparent` header to facilitate integration with servesr that only support the [W3C Trace Context](https://www.w3.org/TR/trace-context/). ([#4084](https://github.com/getsentry/sentry-dotnet/pull/4084))
5152

src/Sentry.AspNet/HttpContextExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Sentry.Extensibility;
2+
using Sentry.Internal;
23
using Sentry.Protocol;
34

45
namespace Sentry.AspNet;
@@ -125,7 +126,7 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon
125126
["__HttpContext"] = httpContext,
126127
};
127128

128-
// Set the Dynamic Sampling Context from the baggage header, if it exists.
129+
// Set the Dynamic Sampling Context from the baggage header, if it exists
129130
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext();
130131

131132
if (traceHeader is not null && baggageHeader is null)

src/Sentry.AspNetCore/SentryTracingMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.Options;
33
using Sentry.AspNetCore.Extensions;
44
using Sentry.Extensibility;
5+
using Sentry.Internal;
56
using Sentry.Internal.OpenTelemetry;
67

78
namespace Sentry.AspNetCore;
@@ -64,7 +65,6 @@ public SentryTracingMiddleware(
6465
? traceHeaderObject as SentryTraceHeader : null;
6566
var baggageHeader = context.Items.TryGetValue(SentryMiddleware.BaggageHeaderItemKey, out var baggageHeaderObject)
6667
? baggageHeaderObject as BaggageHeader : null;
67-
6868
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext();
6969

7070
if (traceHeader is not null && baggageHeader is null)

src/Sentry.OpenTelemetry/SentrySpanProcessor.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class SentrySpanProcessor : BaseProcessor<Activity>
1313
{
1414
private readonly IHub _hub;
1515
internal readonly IEnumerable<IOpenTelemetryEnricher> _enrichers;
16+
private readonly IReplaySession _replaySession;
1617
internal const string OpenTelemetryOrigin = "auto.otel";
1718

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

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

60-
_enrichers = enrichers ?? Enumerable.Empty<IOpenTelemetryEnricher>();
61+
_enrichers = enrichers ?? [];
62+
_replaySession = replaySession ?? ReplaySession.Instance;
6163
_options = hub.GetSentryOptions();
6264

6365
if (_options is null)
@@ -158,7 +160,7 @@ private void CreateRootSpan(Activity data)
158160
};
159161

160162
var baggageHeader = data.Baggage.AsBaggageHeader();
161-
var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext();
163+
var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(_replaySession);
162164
var transaction = (TransactionTracer)_hub.StartTransaction(
163165
transactionContext, new Dictionary<string, object?>(), dynamicSamplingContext
164166
);

src/Sentry/DynamicSamplingContext.cs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ internal class DynamicSamplingContext
2020
/// </summary>
2121
public static readonly DynamicSamplingContext Empty = new(new Dictionary<string, string>().AsReadOnly());
2222

23-
private DynamicSamplingContext(
24-
SentryId traceId,
23+
private DynamicSamplingContext(SentryId traceId,
2524
string publicKey,
2625
bool? sampled,
2726
double? sampleRate = null,
2827
double? sampleRand = null,
2928
string? release = null,
3029
string? environment = null,
31-
string? transactionName = null)
30+
string? transactionName = null,
31+
IReplaySession? replaySession = null)
3232
{
3333
// Validate and set required values
3434
if (traceId == SentryId.Empty)
@@ -51,7 +51,7 @@ private DynamicSamplingContext(
5151
throw new ArgumentOutOfRangeException(nameof(sampleRand), "Arg invalid if < 0.0 or >= 1.0");
5252
}
5353

54-
var items = new Dictionary<string, string>(capacity: 8)
54+
var items = new Dictionary<string, string>(capacity: 9)
5555
{
5656
["trace_id"] = traceId.ToString(),
5757
["public_key"] = publicKey,
@@ -88,12 +88,29 @@ private DynamicSamplingContext(
8888
items.Add("transaction", transactionName);
8989
}
9090

91+
if (replaySession?.ActiveReplayId is { } replayId && replayId != SentryId.Empty)
92+
{
93+
items.Add("replay_id", replayId.ToString());
94+
}
95+
9196
Items = items;
9297
}
9398

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

96-
public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage)
101+
public DynamicSamplingContext WithReplayId(IReplaySession? replaySession)
102+
{
103+
if (replaySession?.ActiveReplayId is not { } replayId || replayId == SentryId.Empty)
104+
{
105+
return this;
106+
}
107+
108+
var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
109+
items["replay_id"] = replayId.ToString();
110+
return new DynamicSamplingContext(items);
111+
}
112+
113+
public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession? replaySession)
97114
{
98115
var items = baggage.GetSentryMembers();
99116

@@ -144,10 +161,19 @@ private DynamicSamplingContext(
144161
}
145162
items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture));
146163
}
164+
165+
if (replaySession?.ActiveReplayId is { } replayId)
166+
{
167+
// Any upstream replay_id will be propagated only if the current process hasn't started it's own replay session.
168+
// Otherwise we have to overwrite this as it's the only way to communicate the replayId to Sentry Relay.
169+
// In Mobile apps this should never be a problem.
170+
items["replay_id"] = replayId.ToString();
171+
}
172+
147173
return new DynamicSamplingContext(items);
148174
}
149175

150-
public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options)
176+
public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession)
151177
{
152178
// These should already be set on the transaction.
153179
var publicKey = options.ParsedDsn.PublicKey;
@@ -161,18 +187,18 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
161187
var release = options.SettingLocator.GetRelease();
162188
var environment = options.SettingLocator.GetEnvironment();
163189

164-
return new DynamicSamplingContext(
165-
traceId,
190+
return new DynamicSamplingContext(traceId,
166191
publicKey,
167192
sampled,
168193
sampleRate,
169194
sampleRand,
170195
release,
171196
environment,
172-
transactionName);
197+
transactionName,
198+
replaySession);
173199
}
174200

175-
public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options)
201+
public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
176202
{
177203
var traceId = propagationContext.TraceId;
178204
var publicKey = options.ParsedDsn.PublicKey;
@@ -184,18 +210,20 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat
184210
publicKey,
185211
null,
186212
release: release,
187-
environment: environment);
213+
environment: environment,
214+
replaySession: replaySession
215+
);
188216
}
189217
}
190218

191219
internal static class DynamicSamplingContextExtensions
192220
{
193-
public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage)
194-
=> DynamicSamplingContext.CreateFromBaggageHeader(baggage);
221+
public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession? replaySession = null)
222+
=> DynamicSamplingContext.CreateFromBaggageHeader(baggage, replaySession);
195223

196-
public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options)
197-
=> DynamicSamplingContext.CreateFromTransaction(transaction, options);
224+
public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession)
225+
=> DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession);
198226

199-
public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options)
200-
=> DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options);
227+
public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
228+
=> DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession);
201229
}

src/Sentry/Internal/Hub.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal class Hub : IHub, IDisposable
1313
private readonly ISessionManager _sessionManager;
1414
private readonly SentryOptions _options;
1515
private readonly RandomValuesFactory _randomValuesFactory;
16+
private readonly IReplaySession _replaySession;
1617

1718
#if MEMORY_DUMP_SUPPORTED
1819
private readonly MemoryMonitor? _memoryMonitor;
@@ -39,7 +40,8 @@ internal Hub(
3940
ISessionManager? sessionManager = null,
4041
ISystemClock? clock = null,
4142
IInternalScopeManager? scopeManager = null,
42-
RandomValuesFactory? randomValuesFactory = null)
43+
RandomValuesFactory? randomValuesFactory = null,
44+
IReplaySession? replaySession = null)
4345
{
4446
if (string.IsNullOrWhiteSpace(options.Dsn))
4547
{
@@ -55,7 +57,7 @@ internal Hub(
5557
_sessionManager = sessionManager ?? new GlobalSessionManager(options);
5658
_clock = clock ?? SystemClock.Clock;
5759
client ??= new SentryClient(options, randomValuesFactory: _randomValuesFactory, sessionManager: _sessionManager);
58-
60+
_replaySession = replaySession ?? ReplaySession.Instance;
5961
ScopeManager = scopeManager ?? new SentryScopeManager(options, client);
6062

6163
if (!options.IsGlobalModeEnabled)
@@ -178,10 +180,10 @@ _options.TransactionProfilerFactory is { } profilerFactory &&
178180
}
179181
}
180182

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

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

226228
var propagationContext = CurrentScope.PropagationContext;
227-
return propagationContext.GetOrCreateDynamicSamplingContext(_options).ToBaggageHeader();
229+
return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader();
228230
}
229231

230232
public TransactionContext ContinueTrace(
@@ -254,7 +256,7 @@ public TransactionContext ContinueTrace(
254256
string? name = null,
255257
string? operation = null)
256258
{
257-
var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader);
259+
var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession);
258260
ConfigureScope(scope => scope.SetPropagationContext(propagationContext));
259261

260262
return new TransactionContext(
@@ -382,7 +384,7 @@ private void ApplyTraceContextToEvent(SentryEvent evt, SentryPropagationContext
382384
evt.Contexts.Trace.TraceId = propagationContext.TraceId;
383385
evt.Contexts.Trace.SpanId = propagationContext.SpanId;
384386
evt.Contexts.Trace.ParentSpanId = propagationContext.ParentSpanId;
385-
evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options);
387+
evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession);
386388
}
387389

388390
public bool CaptureEnvelope(Envelope envelope) => CurrentClient.CaptureEnvelope(envelope);
@@ -473,10 +475,7 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope)
473475
var span = GetLinkedSpan(evt) ?? scope.Span;
474476
if (span is not null)
475477
{
476-
if (span.IsSampled is not false)
477-
{
478-
ApplyTraceContextToEvent(evt, span);
479-
}
478+
ApplyTraceContextToEvent(evt, span);
480479
}
481480
else
482481
{

src/Sentry/Internal/ReplaySession.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#if __ANDROID__
2+
using Sentry.Android.Extensions;
3+
#endif
4+
5+
namespace Sentry.Internal;
6+
7+
internal interface IReplaySession
8+
{
9+
public SentryId? ActiveReplayId { get; }
10+
}
11+
12+
internal class ReplaySession : IReplaySession
13+
{
14+
public static readonly IReplaySession Instance = new ReplaySession();
15+
16+
private ReplaySession()
17+
{
18+
}
19+
20+
public SentryId? ActiveReplayId
21+
{
22+
get
23+
{
24+
#if __ANDROID__
25+
// Check to see if a Replay ID is available
26+
var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId();
27+
return (replayId is { } id && id != SentryId.Empty) ? id : null;
28+
#else
29+
return null;
30+
#endif
31+
}
32+
}
33+
}

src/Sentry/SentryPropagationContext.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Sentry.Extensibility;
2+
using Sentry.Internal;
23

34
namespace Sentry;
45

@@ -10,12 +11,12 @@ internal class SentryPropagationContext
1011

1112
internal DynamicSamplingContext? _dynamicSamplingContext;
1213

13-
public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options)
14+
public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession)
1415
{
1516
if (_dynamicSamplingContext is null)
1617
{
1718
options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context");
18-
_dynamicSamplingContext = this.CreateDynamicSamplingContext(options);
19+
_dynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession);
1920
}
2021

2122
return _dynamicSamplingContext;
@@ -47,7 +48,7 @@ public SentryPropagationContext(SentryPropagationContext? other)
4748
_dynamicSamplingContext = other?._dynamicSamplingContext;
4849
}
4950

50-
public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader)
51+
public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader, IReplaySession replaySession)
5152
{
5253
logger?.LogDebug("Creating a propagation context from headers.");
5354

@@ -57,7 +58,7 @@ public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logg
5758
return new SentryPropagationContext();
5859
}
5960

60-
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext();
61+
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(replaySession);
6162
return new SentryPropagationContext(traceHeader.TraceId, traceHeader.SpanId, dynamicSamplingContext);
6263
}
6364
}

0 commit comments

Comments
 (0)