Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ Public API diffs are stored as Verify snapshot files. After any public API chang
## Changelog

- If the change is not user-facing, add `#skip-changelog` to the PR description
- Otherwise generated automatically from [Commit message conventions](https://develop.sentry.dev/engineering-practices/commit-messages/)
- Otherwise generated automatically from [Commit message conventions](https://develop.sentry.dev/engineering-practices/commit-messages/).
- Do **NOT** add changelogs manually.
Comment thread
jamescrosswell marked this conversation as resolved.

```sh
# Get PR number for current branch
Expand Down
2 changes: 1 addition & 1 deletion modules/sentry-cocoa
Submodule sentry-cocoa updated 62 files
+1 −1 .github/last-release-runid
+2 −2 .github/workflows/analyze-language-trends.yml
+2 −2 .github/workflows/assemble-xcframework-variant.yml
+3 −3 .github/workflows/auto-update-tools.yml
+3 −3 .github/workflows/benchmarking.yml
+2 −2 .github/workflows/build-xcframework-variant-slices.yml
+4 −4 .github/workflows/build.yml
+1 −1 .github/workflows/changelog-preview.yml
+2 −2 .github/workflows/changes-in-high-risk-code.yml
+2 −2 .github/workflows/integration-test.yml
+1 −1 .github/workflows/lint-cocoapods-specs.yml
+1 −1 .github/workflows/nightly-test.yml
+2 −2 .github/workflows/objc-conversion-analysis.yml
+1 −1 .github/workflows/ready-to-merge-workflow.yml
+1 −1 .github/workflows/release-upload-xcframework.yml
+19 −3 .github/workflows/release.yml
+1 −1 .github/workflows/size-analysis.yml
+2 −2 .github/workflows/test-3rd-party-integrations.yml
+1 −1 .github/workflows/test-cross-platform.yml
+11 −8 .github/workflows/test.yml
+2 −2 .github/workflows/testflight.yml
+4 −4 .github/workflows/ui-tests-common.yml
+2 −2 .github/workflows/ui-tests-critical.yml
+5 −5 .github/workflows/unit-test-common.yml
+1 −1 3rd-party-integrations/SentryCocoaLumberjack/Package.swift
+1 −1 3rd-party-integrations/SentryPulse/Package.swift
+1 −1 3rd-party-integrations/SentrySwiftLog/Package.swift
+1 −1 3rd-party-integrations/SentrySwiftyBeaver/Package.swift
+21 −1 CHANGELOG.md
+10 −10 Package.swift
+10 −10 Package@swift-6.1.swift
+123 −0 Package@swift-6.2.swift
+1 −1 Sentry.podspec
+2 −2 SentrySwiftUI.podspec
+7 −3 SentryTestUtils/Sources/TestNSNotificationCenterWrapper.swift
+6 −0 SentryTestUtils/Sources/TestSentryNSProcessInfoWrapper.swift
+1 −1 Sources/Configuration/SDK.xcconfig
+1 −1 Sources/Configuration/SentrySwiftUI.xcconfig
+1 −1 Sources/Configuration/Versioning.xcconfig
+1 −1 Sources/Sentry/SentryMeta.m
+11 −11 Sources/Sentry/SentryScope.m
+11 −0 Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_System.m
+77 −1 Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift
+4 −0 Sources/Swift/Helper/SentryExtraContextProvider.swift
+15 −0 Sources/Swift/Helper/SentryMobileProvisionParser.swift
+4 −1 Sources/Swift/Helper/SentryProcessInfo.swift
+48 −5 Sources/Swift/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbs.swift
+47 −0 Sources/Swift/Integrations/SentryCrash/SentryCrashIntegration.swift
+10 −0 Sources/Swift/Integrations/SentryHangTrackingIntegration.swift
+14 −6 Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift
+1 −1 Sources/Swift/Integrations/SessionReplay/SentrySessionReplayIntegration.swift
+2 −2 Sources/Swift/Integrations/WatchdogTerminations/SentryWatchdogTerminationScopeObserver.swift
+20 −0 Tests/SentryTests/Helper/SentryExtraContextProviderTests.swift
+100 −0 Tests/SentryTests/Helper/SentryMobileProvisionParserTests.swift
+3 −0 Tests/SentryTests/Integrations/ANR/SentryHangTrackingIntegrationTests.swift
+56 −0 Tests/SentryTests/Integrations/Breadcrumbs/SentrySystemEventBreadcrumbsTest.swift
+43 −0 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift
+1 −3 Tests/SentryTests/OptionsInSyncWithDocs/SentryOptionsDocumentationSyncTests.swift
+135 −1 Tests/SentryTests/SentryScopeSwiftTests.swift
+347 −0 Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift
+1 −0 Utils/VersionBump/main.swift
+1 −1 scripts/.clang-format-version
2 changes: 1 addition & 1 deletion modules/sentry-native
Submodule sentry-native updated 58 files
+30 −2 .github/workflows/ci.yml
+2 −2 .github/workflows/codeql.yml
+1 −1 .github/workflows/console-check.yml
+1 −1 .github/workflows/release.yml
+21 −0 CHANGELOG.md
+12 −1 examples/example.c
+1 −1 external/crashpad
+20 −2 include/sentry.h
+1 −1 ndk/gradle.properties
+6 −2 src/CMakeLists.txt
+6 −3 src/backends/native/minidump/sentry_minidump_format.h
+63 −1 src/backends/native/minidump/sentry_minidump_linux.c
+5 −1 src/backends/native/minidump/sentry_minidump_macos.c
+7 −1 src/backends/native/minidump/sentry_minidump_windows.c
+5 −0 src/backends/native/sentry_crash_context.h
+73 −21 src/backends/native/sentry_crash_daemon.c
+26 −4 src/backends/native/sentry_crash_ipc.c
+5 −0 src/backends/native/sentry_crash_ipc.h
+8 −6 src/backends/sentry_backend_breakpad.cpp
+47 −2 src/backends/sentry_backend_crashpad.cpp
+1 −26 src/backends/sentry_backend_inproc.c
+28 −5 src/backends/sentry_backend_native.c
+6 −10 src/modulefinder/sentry_modulefinder_linux.c
+5 −9 src/sentry_batcher.c
+0 −1 src/sentry_batcher.h
+28 −39 src/sentry_core.c
+6 −2 src/sentry_core.h
+40 −3 src/sentry_database.c
+17 −0 src/sentry_database.h
+25 −0 src/sentry_logger.h
+2 −2 src/sentry_logs.c
+2 −2 src/sentry_metrics.c
+12 −1 src/sentry_options.c
+0 −1 src/sentry_options.h
+4 −0 src/sentry_retry.c
+1 −1 src/sentry_session.c
+28 −0 src/sentry_utils.h
+1 −1 tests/__init__.py
+10 −0 tests/build_config.py
+2 −0 tests/conditions.py
+4 −5 tests/test_build_static.py
+17 −3 tests/test_dotnet_signals.py
+3 −0 tests/test_embedded_info.py
+87 −12 tests/test_integration_cache.py
+61 −5 tests/test_integration_crashpad.py
+16 −6 tests/test_integration_http.py
+5 −5 tests/test_integration_logger.py
+3 −3 tests/test_integration_logs.py
+3 −3 tests/test_integration_metrics.py
+48 −2 tests/test_integration_native.py
+10 −6 tests/test_integration_stdout.py
+78 −0 tests/unit/test_cache.c
+6 −0 tests/unit/test_client_report.c
+46 −0 tests/unit/test_retry.c
+4 −0 tests/unit/test_unwinder.c
+3 −0 tests/unit/tests.inc
+32 −1 vendor/libunwind/CMakeLists.txt
+2 −2 vendor/libunwind/VENDORING.md
2 changes: 2 additions & 0 deletions src/Sentry.AspNetCore/BindableSentryAspNetCoreOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal class BindableSentryAspNetCoreOptions : BindableSentryLoggingOptions
public bool? AdjustStandardEnvironmentNameCasing { get; set; }
public bool? AutoRegisterTracing { get; set; }
public bool? CaptureBlockingCalls { get; set; }
public bool? AlwaysCallTransactionNameProvider { get; set; }

public void ApplyTo(SentryAspNetCoreOptions options)
{
Expand All @@ -28,5 +29,6 @@ public void ApplyTo(SentryAspNetCoreOptions options)
options.AdjustStandardEnvironmentNameCasing = AdjustStandardEnvironmentNameCasing ?? options.AdjustStandardEnvironmentNameCasing;
options.AutoRegisterTracing = AutoRegisterTracing ?? options.AutoRegisterTracing;
options.CaptureBlockingCalls = CaptureBlockingCalls ?? options.CaptureBlockingCalls;
options.AlwaysCallTransactionNameProvider = AlwaysCallTransactionNameProvider ?? options.AlwaysCallTransactionNameProvider;
}
}
7 changes: 5 additions & 2 deletions src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ internal static class HttpContextExtensions
: null;
}

internal static string? TryGetCustomTransactionName(this HttpContext context) =>
context.Features.Get<TransactionNameProvider>()?.Invoke(context);
internal static string? TryGetCustomTransactionName(this HttpContext context)
{
var customName = context.Features.Get<TransactionNameProvider>()?.Invoke(context);
return !string.IsNullOrEmpty(customName) ? customName : null;
}

public static string? TryGetTransactionName(this HttpContext context)
{
Expand Down
13 changes: 13 additions & 0 deletions src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ public class SentryAspNetCoreOptions : SentryLoggingOptions
/// </remarks>
public TransactionNameProvider? TransactionNameProvider { get; set; }

/// <summary>
/// Controls whether <see cref="TransactionNameProvider"/> runs on every request, or only
/// when routing fails to resolve a template.
/// </summary>
/// <remarks>
/// When <c>false</c> (default), the provider is only invoked as a fallback for unresolved routes —
/// matching the legacy behaviour. When <c>true</c>, the provider is invoked for every request
/// after routing; a non-empty return value overrides the route-derived name. If the provider
/// returns <c>null</c> or <see cref="string.Empty"/>, the routed name is preserved (or the URL-path
/// fallback applies when no route was resolved).
/// </remarks>
public bool AlwaysCallTransactionNameProvider { get; set; }
Comment thread
Flash0ver marked this conversation as resolved.
Outdated
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated

/// <summary>
/// Controls whether the casing of the standard (Production, Development and Staging) environment name supplied by <see cref="Microsoft.AspNetCore.Hosting.IHostingEnvironment" />
/// is adjusted when setting the Sentry environment. Defaults to true.
Expand Down
8 changes: 5 additions & 3 deletions src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,15 @@ public async Task InvokeAsync(HttpContext context)
var status = SpanStatusConverter.FromHttpStatusCode(context.Response.StatusCode);

// If no Name was found for Transaction, then we don't have the route.
if (transaction.Name == string.Empty)
// Also, run this block if the caller has opted into always calling the TransactionNameProvider.
var customName = new Lazy<string?>(context.TryGetCustomTransactionName);
var forceCustomName = _options.AlwaysCallTransactionNameProvider && customName.Value is not null;
if (transaction.Name.Length == 0 || forceCustomName)
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
{
var method = context.Request.Method.ToUpperInvariant();

// If we've set a TransactionNameProvider, use that here
var customTransactionName = context.TryGetCustomTransactionName();
if (!string.IsNullOrEmpty(customTransactionName))
if (customName.Value is { } customTransactionName)
{
transaction.Name = $"{method} {customTransactionName}";
tracer.NameSource = TransactionNameSource.Custom;
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated
Comment on lines +182 to 192
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: When PreferTransactionNameProvider is true, the transaction name is unconditionally overwritten, ignoring any name manually set during the request pipeline.
Severity: MEDIUM

Suggested Fix

In the new code block (lines 182-192), add a guard to prevent overwriting the transaction name if it has already been manually modified. Before setting transaction.Name, check if transaction.Name == initialName, similar to the logic in the existing block at lines 158-172.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/Sentry.AspNetCore/SentryTracingMiddleware.cs#L182-L192

Potential issue: When `PreferTransactionNameProvider` is set to `true` and the
`TransactionNameProvider` returns a non-null value, the new code block unconditionally
overwrites `transaction.Name`. This bypasses an existing guard that is designed to
protect transaction names that are manually set during the request pipeline. The
existing logic correctly checks if `transaction.Name` has been modified from its initial
value and avoids overwriting it, but the new code at lines 182-192 omits this check.
This behavior is inconsistent with the documentation, which states the provider should
only override the route-derived name, not a manually set one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is intentional - the whole purpose of this PR is to address the gap highlighted in the original issue:

There is no SDK-level hook that runs unconditionally after routing resolves and allows the transaction name to be rewritten.

Anyone setting this option will have to check the existing transaction name (by reading the transaction from the scope) if they want to use that in the logic defined for returning a custom transaction name.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Sentry.AspNetCore
{
public SentryAspNetCoreOptions() { }
public bool AdjustStandardEnvironmentNameCasing { get; set; }
public bool AlwaysCallTransactionNameProvider { get; set; }
public bool AutoRegisterTracing { get; set; }
public bool CaptureBlockingCalls { get; set; }
public bool FlushOnCompletedRequest { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Sentry.AspNetCore
{
public SentryAspNetCoreOptions() { }
public bool AdjustStandardEnvironmentNameCasing { get; set; }
public bool AlwaysCallTransactionNameProvider { get; set; }
public bool AutoRegisterTracing { get; set; }
public bool CaptureBlockingCalls { get; set; }
public bool FlushOnCompletedRequest { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Sentry.AspNetCore
{
public SentryAspNetCoreOptions() { }
public bool AdjustStandardEnvironmentNameCasing { get; set; }
public bool AlwaysCallTransactionNameProvider { get; set; }
public bool AutoRegisterTracing { get; set; }
public bool CaptureBlockingCalls { get; set; }
public bool FlushOnCompletedRequest { get; set; }
Expand Down
130 changes: 130 additions & 0 deletions test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -752,4 +752,134 @@ public async Task Transaction_TransactionNameProviderSetUnset_TransactionNameSet
transaction.Name.Should().Be("GET /person/13.bmp");
transaction.NameSource.Should().Be(TransactionNameSource.Url);
}

[Fact]
public async Task Transaction_TransactionNameProviderSetForRoutedRequest_ProviderOverridesRouteName()
{
// Arrange
SentryTransaction transaction = null;

const string expectedName = "My custom name";

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<SentryTransaction>(), Arg.Any<Scope>(), Arg.Any<SentryHint>()))
.Do(callback => transaction = callback.Arg<SentryTransaction>());

var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1 }, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseDefaultServiceProvider(di => di.EnableValidation())
.UseSentry(aspNetOptions =>
{
aspNetOptions.TransactionNameProvider = _ => expectedName;
aspNetOptions.AlwaysCallTransactionNameProvider = true;
})
.ConfigureServices(services =>
{
services.AddRouting();

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();

app.UseEndpoints(routes => routes.Map("/person/{id}", _ => Task.CompletedTask));
}));

var client = server.CreateClient();

// Act
await client.GetStringAsync("/person/13");

// Assert
transaction.Should().NotBeNull();
transaction.Name.Should().Be($"GET {expectedName}");
transaction.NameSource.Should().Be(TransactionNameSource.Custom);
}

[Fact]
public async Task Transaction_TransactionNameProviderSetForRoutedRequest_DefaultsToRouteBehaviour()
{
// Arrange
SentryTransaction transaction = null;

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<SentryTransaction>(), Arg.Any<Scope>(), Arg.Any<SentryHint>()))
.Do(callback => transaction = callback.Arg<SentryTransaction>());

var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1 }, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseDefaultServiceProvider(di => di.EnableValidation())
.UseSentry(aspNetOptions => aspNetOptions.TransactionNameProvider = _ => "My custom name")
.ConfigureServices(services =>
{
services.AddRouting();

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();

app.UseEndpoints(routes => routes.Map("/person/{id}", _ => Task.CompletedTask));
}));

var client = server.CreateClient();

// Act
await client.GetStringAsync("/person/13");

// Assert
transaction.Should().NotBeNull();
transaction.Name.Should().Be("GET /person/{id}");
transaction.NameSource.Should().Be(TransactionNameSource.Route);
}

[Fact]
public async Task Transaction_AlwaysCallTransactionNameProviderWithNullReturn_PreservesRouteName()
{
// Arrange
SentryTransaction transaction = null;

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<SentryTransaction>(), Arg.Any<Scope>(), Arg.Any<SentryHint>()))
.Do(callback => transaction = callback.Arg<SentryTransaction>());

var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1 }, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseDefaultServiceProvider(di => di.EnableValidation())
.UseSentry(aspNetOptions =>
{
aspNetOptions.TransactionNameProvider = _ => null;
aspNetOptions.AlwaysCallTransactionNameProvider = true;
})
.ConfigureServices(services =>
{
services.AddRouting();

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();

app.UseEndpoints(routes => routes.Map("/person/{id}", _ => Task.CompletedTask));
}));

var client = server.CreateClient();

// Act
await client.GetStringAsync("/person/13");

// Assert
transaction.Should().NotBeNull();
transaction.Name.Should().Be("GET /person/{id}");
transaction.NameSource.Should().Be(TransactionNameSource.Route);
}
}