From 45814177297201138ec2e03f923381126eaf33ba Mon Sep 17 00:00:00 2001
From: Alex Sohn <44201357+alexsohn1126@users.noreply.github.com>
Date: Mon, 22 Sep 2025 12:03:56 -0400
Subject: [PATCH 01/28] fix: Generate and inject uuid to apk and upload
proguard with that uuid (#4532)
Automatically generate a UUID to associate an APK and its ProGuard mapping file that will be uploaded to Sentry.
Fixes #3872
---
.github/workflows/build.yml | 4 ++--
CHANGELOG.md | 6 +++++
src/Sentry/buildTransitive/Sentry.targets | 27 ++++++++++++++++++-----
3 files changed, 30 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1bb59a1678..f54403af15 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -267,7 +267,7 @@ jobs:
path: src
- name: Integration test
- uses: getsentry/github-workflows/sentry-cli/integration-test/@v2
+ uses: getsentry/github-workflows/sentry-cli/integration-test/@a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1
with:
path: integration-test
@@ -348,7 +348,7 @@ jobs:
path: src
- name: Test AOT
- uses: getsentry/github-workflows/sentry-cli/integration-test/@v2
+ uses: getsentry/github-workflows/sentry-cli/integration-test/@a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1
env:
RuntimeIdentifier: ${{ matrix.rid }}
with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 141222b598..baadd4e5e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## Unreleased
+
+### Fixes
+
+- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
+
## 5.15.1
### Fixes
diff --git a/src/Sentry/buildTransitive/Sentry.targets b/src/Sentry/buildTransitive/Sentry.targets
index 653cc294c0..76b4feb88e 100644
--- a/src/Sentry/buildTransitive/Sentry.targets
+++ b/src/Sentry/buildTransitive/Sentry.targets
@@ -18,6 +18,7 @@
Sentry.Attributes$(MSBuildProjectExtension.Replace('proj', ''))
+ $([System.Guid]::NewGuid())
true
@@ -125,11 +126,14 @@
$(SentrySetCommitReleaseOptions) --org $(SentryOrg)
$(SentrySetCommitReleaseOptions) --project $(SentryProject)
+ <_SentryCLIProGuardOptions Condition="'$(SentryProGuardUUID)' != ''">$(_SentryCLIProGuardOptions) --uuid "$(SentryProGuardUUID)"
+ <_SentryCLIProGuardOptions Condition="'$(_SentryCLIProGuardOptions.Trim())' != ''">$(_SentryCLIProGuardOptions.Trim())
+
$(SentryCLIUploadOptions) --org $(SentryOrg)
$(SentryCLIUploadOptions) --project $(SentryProject)
$(SentryCLIBaseCommand) debug-files upload
$(SentryCLIDebugFilesUploadCommand) $(SentryCLIUploadOptions.Trim())
- $(SentryCLIBaseCommand) upload-proguard
+ $(SentryCLIBaseCommand) upload-proguard $(_SentryCLIProGuardOptions)
$(SentryCLIProGuardMappingUploadCommand) $(SentryCLIUploadOptions.Trim())
@@ -267,9 +271,22 @@
-
-
+
+
+
+
+ <_Parameter1>io.sentry.proguard-uuid
+ $(SentryProGuardUUID)
+
+
+
+
+
+
From 934ee4e2868f19873a31b54e0af6047aacf6ad5d Mon Sep 17 00:00:00 2001
From: J-P Nurmi
Date: Tue, 23 Sep 2025 08:41:35 +0200
Subject: [PATCH 02/28] fix: upload linked PDBs for iOS (#4527)
Co-authored-by: James Crosswell
---
CHANGELOG.md | 1 +
integration-test/cli.Tests.ps1 | 7 +++++++
src/Sentry/buildTransitive/Sentry.targets | 2 +-
3 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index baadd4e5e7..e5a83ba3cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Fixes
+- Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527))
- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
## 5.15.1
diff --git a/integration-test/cli.Tests.ps1 b/integration-test/cli.Tests.ps1
index 1a39cf541a..e3e91c0d12 100644
--- a/integration-test/cli.Tests.ps1
+++ b/integration-test/cli.Tests.ps1
@@ -176,6 +176,13 @@ Describe 'MAUI' -ForEach @(
'libxamarin-dotnet.dylib',
'maui-app',
'maui-app.pdb',
+ 'Microsoft.iOS.pdb',
+ 'Microsoft.Maui.Controls.Compatibility.pdb',
+ 'Microsoft.Maui.Controls.pdb',
+ 'Microsoft.Maui.Controls.Xaml.pdb',
+ 'Microsoft.Maui.Essentials.pdb',
+ 'Microsoft.Maui.Graphics.pdb',
+ 'Microsoft.Maui.pdb',
'Sentry'
)
$nonZeroNumberRegex = '[1-9][0-9]*';
diff --git a/src/Sentry/buildTransitive/Sentry.targets b/src/Sentry/buildTransitive/Sentry.targets
index 76b4feb88e..c33ed0efbf 100644
--- a/src/Sentry/buildTransitive/Sentry.targets
+++ b/src/Sentry/buildTransitive/Sentry.targets
@@ -221,7 +221,7 @@
$(SentryCLIUploadDirectory)
- $(SentryCLIUploadItems) $(IntermediateOutputPath)linked/$(AssemblyName).pdb
+ $(SentryCLIUploadItems) $(IntermediateOutputPath)linked/*.pdb
$(SentryCLIUploadItems) @(AndroidNativeSymbolFilesExceptDll -> '%(Identity)', ' ')
From c1ae41be566800b3546b8f170b1cd6ebc2cdc110 Mon Sep 17 00:00:00 2001
From: Alex Sohn <44201357+alexsohn1126@users.noreply.github.com>
Date: Tue, 23 Sep 2025 12:18:03 -0400
Subject: [PATCH 03/28] fix: Stop warnings from showing in Blazor WASM projects
(#4519)
Fix WASM0001 warnings when building Blazor WebAssembly projects that came out from our native bindings.
The fix was to change the way we represent `sentry_value_t` in C# side, instead of using `FieldOffset`, we use a backing field along with getters and setters for the variable as a `double`, or `ulong`.
Fixes #3369
---
CHANGELOG.md | 1 +
src/Sentry/Platforms/Native/CFunctions.cs | 23 +++++++++++++++++------
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5a83ba3cb..e7248f0c8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527))
- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
+- Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519))
## 5.15.1
diff --git a/src/Sentry/Platforms/Native/CFunctions.cs b/src/Sentry/Platforms/Native/CFunctions.cs
index 582112fc4c..5faa149720 100644
--- a/src/Sentry/Platforms/Native/CFunctions.cs
+++ b/src/Sentry/Platforms/Native/CFunctions.cs
@@ -322,14 +322,25 @@ private static Dictionary LoadDebugImagesOnce(IDiagnosticLogge
[DllImport("sentry-native")]
internal static extern void sentry_value_decref(sentry_value_t value);
- // native union sentry_value_u/t
- [StructLayout(LayoutKind.Explicit)]
+ // Mirrors the native `sentry_value_t` union (uint64_t or double).
+ // Implemented with a single ulong backing field and BitConverter
+ // to reinterpret values, since explicit unions cause issues with
+ // Blazor WASM interop generators.
internal struct sentry_value_t
{
- [FieldOffset(0)]
- internal ulong _bits;
- [FieldOffset(0)]
- internal double _double;
+ private ulong _bits;
+
+ internal ulong Bits
+ {
+ readonly get => _bits;
+ set => _bits = value;
+ }
+
+ internal double Double
+ {
+ readonly get => BitConverter.UInt64BitsToDouble(_bits);
+ set => _bits = BitConverter.DoubleToUInt64Bits(value);
+ }
}
[DllImport("sentry-native")]
From e7cfea6d5da564a4f3a27537c4fabec7a1157260 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 24 Sep 2025 20:58:16 +1200
Subject: [PATCH 04/28] chore: update scripts/update-cli.ps1 to 2.55.0 (#4556)
Co-authored-by: GitHub
---
CHANGELOG.md | 6 ++++++
Directory.Build.props | 2 +-
src/Sentry/Sentry.csproj | 14 +++++++-------
3 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7248f0c8a..76e057ca7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@
- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
- Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519))
+### Dependencies
+
+- Bump CLI from v2.54.0 to v2.55.0 ([#4556](https://github.com/getsentry/sentry-dotnet/pull/4556))
+ - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2550)
+ - [diff](https://github.com/getsentry/sentry-cli/compare/2.54.0...2.55.0)
+
## 5.15.1
### Fixes
diff --git a/Directory.Build.props b/Directory.Build.props
index a74738d5cd..1f88fcac2b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -86,7 +86,7 @@
- 2.54.0
+ 2.55.0
$(MSBuildThisFileDirectory)tools\sentry-cli\$(SentryCLIVersion)\
diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj
index c560abff9b..72fcabcb4e 100644
--- a/src/Sentry/Sentry.csproj
+++ b/src/Sentry/Sentry.csproj
@@ -113,13 +113,13 @@
<_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
-
-
-
-
-
-
-
+
+
+
+
+
+
+
From 6ea6bf465fc722a69f91c93b7a7ee4677ce28d73 Mon Sep 17 00:00:00 2001
From: J-P Nurmi
Date: Wed, 24 Sep 2025 12:59:09 +0200
Subject: [PATCH 05/28] ci: retry flaky android device tests (#4553)
---
.github/workflows/device-tests-android.yml | 35 ++++++++++++++++++----
.github/workflows/device-tests-ios.yml | 1 +
scripts/device-test.ps1 | 4 +--
3 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/device-tests-android.yml b/.github/workflows/device-tests-android.yml
index b97936f3e2..b234d39df2 100644
--- a/.github/workflows/device-tests-android.yml
+++ b/.github/workflows/device-tests-android.yml
@@ -8,6 +8,7 @@ on:
pull_request:
paths-ignore:
- "**.md"
+ workflow_dispatch:
jobs:
build:
@@ -64,6 +65,12 @@ jobs:
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: 1
+ # We don't need the Google APIs, but the default images are not available for 32+
+ ANDROID_EMULATOR_TARGET: google_apis
+ ANDROID_EMULATOR_RAM_SIZE: 2048M
+ ANDROID_EMULATOR_ARCH: x86_64
+ ANDROID_EMULATOR_DISK_SIZE: 4096M
+ ANDROID_EMULATOR_OPTIONS: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
steps:
# See https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
- name: Enable KVM group perms
@@ -87,17 +94,33 @@ jobs:
# Cached AVD setup per https://github.com/ReactiveCircus/android-emulator-runner/blob/main/README.md
- name: Run Tests
+ id: first-run
+ continue-on-error: true
timeout-minutes: 40
uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # Tag: v2.34.0
with:
api-level: ${{ matrix.api-level }}
- # We don't need the Google APIs, but the default images are not available for 32+
- target: google_apis
+ target: ${{ env.ANDROID_EMULATOR_TARGET }}
force-avd-creation: false
- ram-size: 2048M
- arch: x86_64
- disk-size: 4096M
- emulator-options: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ ram-size: ${{ env.ANDROID_EMULATOR_RAM_SIZE }}
+ arch: ${{ env.ANDROID_EMULATOR_ARCH }}
+ disk-size: ${{ env.ANDROID_EMULATOR_DISK_SIZE }}
+ emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }}
+ disable-animations: false
+ script: pwsh scripts/device-test.ps1 android -Run -Tfm ${{ matrix.tfm }}
+
+ - name: Retry Tests (if previous failed to run)
+ if: steps.first-run.outcome == 'failure'
+ timeout-minutes: 40
+ uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # Tag: v2.34.0
+ with:
+ api-level: ${{ matrix.api-level }}
+ target: ${{ env.ANDROID_EMULATOR_TARGET }}
+ force-avd-creation: false
+ ram-size: ${{ env.ANDROID_EMULATOR_RAM_SIZE }}
+ arch: ${{ env.ANDROID_EMULATOR_ARCH }}
+ disk-size: ${{ env.ANDROID_EMULATOR_DISK_SIZE }}
+ emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }}
disable-animations: false
script: pwsh scripts/device-test.ps1 android -Run -Tfm ${{ matrix.tfm }}
diff --git a/.github/workflows/device-tests-ios.yml b/.github/workflows/device-tests-ios.yml
index c56226bdb6..0e118745cc 100644
--- a/.github/workflows/device-tests-ios.yml
+++ b/.github/workflows/device-tests-ios.yml
@@ -8,6 +8,7 @@ on:
pull_request:
paths-ignore:
- "**.md"
+ workflow_dispatch:
jobs:
ios-tests:
diff --git a/scripts/device-test.ps1 b/scripts/device-test.ps1
index 7822b81582..f9d5c30868 100644
--- a/scripts/device-test.ps1
+++ b/scripts/device-test.ps1
@@ -86,8 +86,8 @@ try
{
if (!(Get-Command xharness -ErrorAction SilentlyContinue))
{
- Push-Location ($CI ? $env:RUNNER_TEMP : $IsWindows ? $env:TMP : $IsMacos ? $env:TMPDIR : '/temp')
- dotnet tool install Microsoft.DotNet.XHarness.CLI --global --version '10.0.0-prerelease.25412.1' `
+ Push-Location ($CI ? $env:RUNNER_TEMP : $IsWindows ? $env:TMP : $IsMacos ? $env:TMPDIR : '/tmp')
+ dotnet tool install Microsoft.DotNet.XHarness.CLI --global --version '10.0.0-prerelease.25466.1' `
--add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json
Pop-Location
}
From 78199ab95dba05543aeef3cf90ea5705691c0df4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 25 Sep 2025 10:02:30 +1200
Subject: [PATCH 06/28] chore: update modules/sentry-native to 0.11.1 (#4557)
Co-authored-by: GitHub
Co-authored-by: James Crosswell
---
CHANGELOG.md | 3 +++
modules/sentry-native | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76e057ca7a..157a753e1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,9 @@
### Dependencies
+- Bump Native SDK from v0.11.0 to v0.11.1 ([#4557](https://github.com/getsentry/sentry-dotnet/pull/4557))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0111)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.11.0...0.11.1)
- Bump CLI from v2.54.0 to v2.55.0 ([#4556](https://github.com/getsentry/sentry-dotnet/pull/4556))
- [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2550)
- [diff](https://github.com/getsentry/sentry-cli/compare/2.54.0...2.55.0)
diff --git a/modules/sentry-native b/modules/sentry-native
index 3bd091313a..075b3bfee1 160000
--- a/modules/sentry-native
+++ b/modules/sentry-native
@@ -1 +1 @@
-Subproject commit 3bd091313ae97be90be62696a2babe591a988eb8
+Subproject commit 075b3bfee1dbb85fa10d50df631286196943a3e0
From 839feff15e2bc40c6d1de5f8c1492ae6724d12e4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 25 Sep 2025 10:03:39 +1200
Subject: [PATCH 07/28] chore: update modules/sentry-cocoa.properties to 8.56.1
(#4555)
Co-authored-by: GitHub
Co-authored-by: James Crosswell
---
CHANGELOG.md | 3 +++
modules/sentry-cocoa.properties | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 157a753e1a..da9fbb3577 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,9 @@
### Dependencies
+- Bump Cocoa SDK from v8.56.0 to v8.56.1 ([#4555](https://github.com/getsentry/sentry-dotnet/pull/4555))
+ - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8561)
+ - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.56.0...8.56.1)
- Bump Native SDK from v0.11.0 to v0.11.1 ([#4557](https://github.com/getsentry/sentry-dotnet/pull/4557))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0111)
- [diff](https://github.com/getsentry/sentry-native/compare/0.11.0...0.11.1)
diff --git a/modules/sentry-cocoa.properties b/modules/sentry-cocoa.properties
index d4dde9863e..8d3c842313 100644
--- a/modules/sentry-cocoa.properties
+++ b/modules/sentry-cocoa.properties
@@ -1,2 +1,2 @@
-version = 8.56.0
+version = 8.56.1
repo = https://github.com/getsentry/sentry-cocoa
From 2dc1551e2101c5df1ffa68cab47099db3bb6a81f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 25 Sep 2025 10:46:39 +1200
Subject: [PATCH 08/28] chore(deps): update Java SDK to v8.22.0 (#4552)
* chore: update scripts/update-java.ps1 to 8.22.0
* Metadata.xml: remove UpdateStatus to avoid CS0108 for conflicting _members fields in nested subclasses
Sentry.Bindings.Android net8.0-android34.0 failed with 3 error(s) (6.9s)
/home/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Android/obj/Release/net8.0-android34.0/generated/src/Sentry.JavaSdk.UpdateStatus.cs(24,35): error CS0108: 'UpdateStatus.NewRelease._members' hides inherited member 'UpdateStatus._members'. Use the new keyword if hiding was intended.
/home/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Android/obj/Release/net8.0-android34.0/generated/src/Sentry.JavaSdk.UpdateStatus.cs(90,35): error CS0108: 'UpdateStatus.UpdateError._members' hides inherited member 'UpdateStatus._members'. Use the new keyword if hiding was intended.
/home/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Android/obj/Release/net8.0-android34.0/generated/src/Sentry.JavaSdk.UpdateStatus.cs(157,35): error CS0108: 'UpdateStatus.UpToDate._members' hides inherited member 'UpdateStatus._members'. Use the new keyword if hiding was intended.
Sentry.Bindings.Android net9.0-android35.0 failed with 3 error(s) (7.8s)
/home/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Android/obj/Release/net9.0-android35.0/generated/src/Sentry.JavaSdk.UpdateStatus.cs(24,35): error CS0108: 'UpdateStatus.NewRelease._members' hides inherited member 'UpdateStatus._members'. Use the new keyword if hiding was intended.
/home/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Android/obj/Release/net9.0-android35.0/generated/src/Sentry.JavaSdk.UpdateStatus.cs(90,35): error CS0108: 'UpdateStatus.UpdateError._members' hides inherited member 'UpdateStatus._members'. Use the new keyword if hiding was intended.
/home/jpnurmi/Projects/sentry/sentry-dotnet/src/Sentry.Bindings.Android/obj/Release/net9.0-android35.0/generated/src/Sentry.JavaSdk.UpdateStatus.cs(157,35): error CS0108: 'UpdateStatus.UpToDate._members' hides inherited member 'UpdateStatus._members'. Use the new keyword if hiding was intended
---------
Co-authored-by: GitHub
Co-authored-by: J-P Nurmi
---
CHANGELOG.md | 6 ++++++
src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj | 2 +-
src/Sentry.Bindings.Android/Transforms/Metadata.xml | 3 +++
3 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da9fbb3577..8df0f5694f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,12 @@
- [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2550)
- [diff](https://github.com/getsentry/sentry-cli/compare/2.54.0...2.55.0)
+### Dependencies
+
+- Bump Java SDK from v8.21.1 to v8.22.0 ([#4552](https://github.com/getsentry/sentry-dotnet/pull/4552))
+ - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8220)
+ - [diff](https://github.com/getsentry/sentry-java/compare/8.21.1...8.22.0)
+
## 5.15.1
### Fixes
diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj
index 0180326bd2..dab642dc24 100644
--- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj
+++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj
@@ -1,7 +1,7 @@
net8.0-android34.0;net9.0-android35.0
- 8.21.1
+ 8.22.0
$(BaseIntermediateOutputPath)sdks\$(TargetFramework)\Sentry\Android\$(SentryAndroidSdkVersion)\
diff --git a/src/Sentry.Bindings.Android/Transforms/Metadata.xml b/src/Sentry.Bindings.Android/Transforms/Metadata.xml
index fb6b6558da..35f9c0be20 100644
--- a/src/Sentry.Bindings.Android/Transforms/Metadata.xml
+++ b/src/Sentry.Bindings.Android/Transforms/Metadata.xml
@@ -156,4 +156,7 @@
+
+
+
From bc5c060f9ab0c371dc12ce596a9f5e0a023dcf2e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20P=C3=B6lz?=
<38893694+Flash0ver@users.noreply.github.com>
Date: Thu, 25 Sep 2025 11:36:59 +0200
Subject: [PATCH 09/28] feat: add `Serilog` integration (#4462)
Co-authored-by: Sentry Github Bot
Co-authored-by: James Crosswell
---
CHANGELOG.md | 4 +
samples/Sentry.Samples.Serilog/Program.cs | 2 +
src/Sentry.Serilog/LogLevelExtensions.cs | 14 ++
src/Sentry.Serilog/SentrySink.Structured.cs | 126 ++++++++++++++++
src/Sentry.Serilog/SentrySink.cs | 70 ++++++---
src/Sentry.Serilog/SentrySinkExtensions.cs | 27 ++--
src/Sentry/SentryLog.cs | 1 +
...piApprovalTests.Run.DotNet8_0.verified.txt | 3 +-
...piApprovalTests.Run.DotNet9_0.verified.txt | 3 +-
.../ApiApprovalTests.Run.Net4_8.verified.txt | 3 +-
.../AspNetCoreIntegrationTests.cs | 49 +++++++
...rationTests.StructuredLogging.verified.txt | 70 +++++++++
.../IntegrationTests.verify.cs | 40 ++++++
.../SentrySerilogSinkExtensionsTests.cs | 6 +-
.../SentrySinkTests.Structured.cs | 135 ++++++++++++++++++
test/Sentry.Serilog.Tests/SentrySinkTests.cs | 3 +-
.../SerilogAspNetSentrySdkTestFixture.cs | 10 +-
.../InMemorySentryStructuredLogger.cs | 3 +-
test/Sentry.Testing/RecordingTransport.cs | 2 +
...piApprovalTests.Run.DotNet8_0.verified.txt | 1 +
...piApprovalTests.Run.DotNet9_0.verified.txt | 1 +
.../ApiApprovalTests.Run.Net4_8.verified.txt | 1 +
22 files changed, 535 insertions(+), 39 deletions(-)
create mode 100644 src/Sentry.Serilog/SentrySink.Structured.cs
create mode 100644 test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt
create mode 100644 test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8df0f5694f..7cdf7c28d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Features
+
+- Add (experimental) _Structured Logs_ integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462))
+
### Fixes
- Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527))
diff --git a/samples/Sentry.Samples.Serilog/Program.cs b/samples/Sentry.Samples.Serilog/Program.cs
index 59ed09548d..c1822a3714 100644
--- a/samples/Sentry.Samples.Serilog/Program.cs
+++ b/samples/Sentry.Samples.Serilog/Program.cs
@@ -25,6 +25,8 @@ private static void Main()
// Error and higher is sent as event (default is Error)
options.MinimumEventLevel = LogEventLevel.Error;
options.AttachStacktrace = true;
+ // send structured logs to Sentry
+ options.Experimental.EnableLogs = true;
// send PII like the username of the user logged in to the device
options.SendDefaultPii = true;
// Optional Serilog text formatter used to format LogEvent to string. If TextFormatter is set, FormatProvider is ignored.
diff --git a/src/Sentry.Serilog/LogLevelExtensions.cs b/src/Sentry.Serilog/LogLevelExtensions.cs
index 03a16ea216..07960179b2 100644
--- a/src/Sentry.Serilog/LogLevelExtensions.cs
+++ b/src/Sentry.Serilog/LogLevelExtensions.cs
@@ -42,4 +42,18 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogEventLevel level)
_ => (BreadcrumbLevel)level
};
}
+
+ public static SentryLogLevel ToSentryLogLevel(this LogEventLevel level)
+ {
+ return level switch
+ {
+ LogEventLevel.Verbose => SentryLogLevel.Trace,
+ LogEventLevel.Debug => SentryLogLevel.Debug,
+ LogEventLevel.Information => SentryLogLevel.Info,
+ LogEventLevel.Warning => SentryLogLevel.Warning,
+ LogEventLevel.Error => SentryLogLevel.Error,
+ LogEventLevel.Fatal => SentryLogLevel.Fatal,
+ _ => (SentryLogLevel)level,
+ };
+ }
}
diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs
new file mode 100644
index 0000000000..6584afb934
--- /dev/null
+++ b/src/Sentry.Serilog/SentrySink.Structured.cs
@@ -0,0 +1,126 @@
+using Sentry.Internal.Extensions;
+using Serilog.Parsing;
+
+namespace Sentry.Serilog;
+
+internal sealed partial class SentrySink
+{
+ private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEvent logEvent, string formatted, string? template)
+ {
+ GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
+ GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes);
+
+ SentryLog log = new(logEvent.Timestamp, traceId, logEvent.Level.ToSentryLogLevel(), formatted)
+ {
+ Template = template,
+ Parameters = parameters,
+ ParentSpanId = spanId,
+ };
+
+ log.SetDefaultAttributes(options, Sdk);
+
+ foreach (var attribute in attributes)
+ {
+ log.SetAttribute(attribute.Key, attribute.Value);
+ }
+
+ hub.Logger.CaptureLog(log);
+ }
+
+ private static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId)
+ {
+ var span = hub.GetSpan();
+ if (span is not null)
+ {
+ traceId = span.TraceId;
+ spanId = span.SpanId;
+ return;
+ }
+
+ var scope = hub.GetScope();
+ if (scope is not null)
+ {
+ traceId = scope.PropagationContext.TraceId;
+ spanId = scope.PropagationContext.SpanId;
+ return;
+ }
+
+ traceId = SentryId.Empty;
+ spanId = null;
+ }
+
+ private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray> parameters, out List> attributes)
+ {
+ var propertyNames = new HashSet();
+ foreach (var token in logEvent.MessageTemplate.Tokens)
+ {
+ if (token is PropertyToken property)
+ {
+ propertyNames.Add(property.PropertyName);
+ }
+ }
+
+ var @params = ImmutableArray.CreateBuilder>();
+ attributes = new List>();
+
+ foreach (var property in logEvent.Properties)
+ {
+ if (propertyNames.Contains(property.Key))
+ {
+ foreach (var parameter in GetLogEventProperties(property))
+ {
+ @params.Add(parameter);
+ }
+ }
+ else
+ {
+ foreach (var attribute in GetLogEventProperties(property))
+ {
+ attributes.Add(new KeyValuePair($"property.{attribute.Key}", attribute.Value));
+ }
+ }
+ }
+
+ parameters = @params.DrainToImmutable();
+ return;
+
+ static IEnumerable> GetLogEventProperties(KeyValuePair property)
+ {
+ if (property.Value is ScalarValue scalarValue)
+ {
+ if (scalarValue.Value is not null)
+ {
+ yield return new KeyValuePair(property.Key, scalarValue.Value);
+ }
+ }
+ else if (property.Value is SequenceValue sequenceValue)
+ {
+ if (sequenceValue.Elements.Count != 0)
+ {
+ yield return new KeyValuePair(property.Key, sequenceValue.ToString());
+ }
+ }
+ else if (property.Value is DictionaryValue dictionaryValue)
+ {
+ if (dictionaryValue.Elements.Count != 0)
+ {
+ yield return new KeyValuePair(property.Key, dictionaryValue.ToString());
+ }
+ }
+ else if (property.Value is StructureValue structureValue)
+ {
+ foreach (var prop in structureValue.Properties)
+ {
+ if (LogEventProperty.IsValidName(prop.Name))
+ {
+ yield return new KeyValuePair($"{property.Key}.{prop.Name}", prop.Value.ToString());
+ }
+ }
+ }
+ else if (!property.Value.IsNull())
+ {
+ yield return new KeyValuePair(property.Key, property.Value);
+ }
+ }
+ }
+}
diff --git a/src/Sentry.Serilog/SentrySink.cs b/src/Sentry.Serilog/SentrySink.cs
index b2a6671c67..369d52a673 100644
--- a/src/Sentry.Serilog/SentrySink.cs
+++ b/src/Sentry.Serilog/SentrySink.cs
@@ -5,7 +5,7 @@ namespace Sentry.Serilog;
///
///
///
-internal sealed class SentrySink : ILogEventSink, IDisposable
+internal sealed partial class SentrySink : ILogEventSink, IDisposable
{
private readonly IDisposable? _sdkDisposable;
private readonly SentrySerilogOptions _options;
@@ -13,6 +13,12 @@ internal sealed class SentrySink : ILogEventSink, IDisposable
internal static readonly SdkVersion NameAndVersion
= typeof(SentrySink).Assembly.GetNameAndVersion();
+ private static readonly SdkVersion Sdk = new()
+ {
+ Name = SdkName,
+ Version = NameAndVersion.Version,
+ };
+
///
/// Serilog SDK name.
///
@@ -50,6 +56,11 @@ internal SentrySink(
public void Emit(LogEvent logEvent)
{
+ if (!IsEnabled(logEvent))
+ {
+ return;
+ }
+
if (isReentrant.Value)
{
_options.DiagnosticLogger?.LogError($"Reentrant log event detected. Logging when inside the scope of another log event can cause a StackOverflowException. LogEventInfo.Message: {logEvent.MessageTemplate.Text}");
@@ -67,6 +78,15 @@ public void Emit(LogEvent logEvent)
}
}
+ private bool IsEnabled(LogEvent logEvent)
+ {
+ var options = _hubAccessor().GetSentryOptions();
+
+ return logEvent.Level >= _options.MinimumEventLevel
+ || logEvent.Level >= _options.MinimumBreadcrumbLevel
+ || options?.Experimental.EnableLogs is true;
+ }
+
private void InnerEmit(LogEvent logEvent)
{
if (logEvent.TryGetSourceContext(out var context))
@@ -77,8 +97,7 @@ private void InnerEmit(LogEvent logEvent)
}
}
- var hub = _hubAccessor();
- if (hub is null || !hub.IsEnabled)
+ if (_hubAccessor() is not { IsEnabled: true } hub)
{
return;
}
@@ -122,30 +141,37 @@ private void InnerEmit(LogEvent logEvent)
}
}
- if (logEvent.Level < _options.MinimumBreadcrumbLevel)
+ if (logEvent.Level >= _options.MinimumBreadcrumbLevel)
{
- return;
+ Dictionary? data = null;
+ if (exception != null && !string.IsNullOrWhiteSpace(formatted))
+ {
+ // Exception.Message won't be used as Breadcrumb message
+ // Avoid losing it by adding as data:
+ data = new Dictionary
+ {
+ { "exception_message", exception.Message }
+ };
+ }
+
+ hub.AddBreadcrumb(
+ _clock,
+ string.IsNullOrWhiteSpace(formatted)
+ ? exception?.Message ?? ""
+ : formatted,
+ context,
+ data: data,
+ level: logEvent.Level.ToBreadcrumbLevel());
}
- Dictionary? data = null;
- if (exception != null && !string.IsNullOrWhiteSpace(formatted))
+ // Read the options from the Hub, rather than the Sink's Serilog-Options, because 'EnableLogs' is declared in the base 'SentryOptions', rather than the derived 'SentrySerilogOptions'.
+ // In cases where Sentry's Serilog-Sink is added without a DSN (i.e., without initializing the SDK) and the SDK is initialized differently (e.g., through ASP.NET Core),
+ // then the 'EnableLogs' option of this Sink's Serilog-Options is default, but the Hub's Sentry-Options have the actual user-defined value configured.
+ var options = hub.GetSentryOptions();
+ if (options?.Experimental.EnableLogs is true)
{
- // Exception.Message won't be used as Breadcrumb message
- // Avoid losing it by adding as data:
- data = new Dictionary
- {
- {"exception_message", exception.Message}
- };
+ CaptureStructuredLog(hub, options, logEvent, formatted, template);
}
-
- hub.AddBreadcrumb(
- _clock,
- string.IsNullOrWhiteSpace(formatted)
- ? exception?.Message ?? ""
- : formatted,
- context,
- data: data,
- level: logEvent.Level.ToBreadcrumbLevel());
}
private static bool IsSentryContext(string context) =>
diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs
index 924cec9d84..e300ae1697 100644
--- a/src/Sentry.Serilog/SentrySinkExtensions.cs
+++ b/src/Sentry.Serilog/SentrySinkExtensions.cs
@@ -13,8 +13,8 @@ public static class SentrySinkExtensions
///
/// The logger configuration .
/// The Sentry DSN (required).
- /// Minimum log level to send an event.
/// Minimum log level to record a breadcrumb.
+ /// Minimum log level to send an event.
/// The Serilog format provider.
/// The Serilog text formatter.
/// Whether to include default Personal Identifiable information.
@@ -35,6 +35,7 @@ public static class SentrySinkExtensions
/// What mode to use for reporting referenced assemblies in each event sent to sentry. Defaults to
/// What modes to use for event automatic de-duplication.
/// Default tags to add to all events.
+ /// Whether to send structured logs.
///
/// This sample shows how each item may be set from within a configuration file:
///
@@ -50,7 +51,7 @@ public static class SentrySinkExtensions
/// "dsn": "https://MY-DSN@sentry.io",
/// "minimumBreadcrumbLevel": "Verbose",
/// "minimumEventLevel": "Error",
- /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"///
+ /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}",
/// "sendDefaultPii": false,
/// "isEnvironmentUser": false,
/// "serverName": "MyServerName",
@@ -71,7 +72,8 @@ public static class SentrySinkExtensions
/// "defaultTags": {
/// "key-1", "value-1",
/// "key-2", "value-2"
- /// }
+ /// },
+ /// "experimentalEnableLogs": true
/// }
/// }
/// ]
@@ -103,7 +105,8 @@ public static LoggerConfiguration Sentry(
SentryLevel? diagnosticLevel = null,
ReportAssembliesMode? reportAssembliesMode = null,
DeduplicateMode? deduplicateMode = null,
- Dictionary? defaultTags = null)
+ Dictionary? defaultTags = null,
+ bool? experimentalEnableLogs = null)
{
return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o,
dsn,
@@ -128,7 +131,8 @@ public static LoggerConfiguration Sentry(
diagnosticLevel,
reportAssembliesMode,
deduplicateMode,
- defaultTags));
+ defaultTags,
+ experimentalEnableLogs));
}
///
@@ -157,7 +161,7 @@ public static LoggerConfiguration Sentry(
/// "Args": {
/// "minimumEventLevel": "Error",
/// "minimumBreadcrumbLevel": "Verbose",
- /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"///
+ /// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"
/// }
/// }
/// ]
@@ -205,7 +209,8 @@ internal static void ConfigureSentrySerilogOptions(
SentryLevel? diagnosticLevel = null,
ReportAssembliesMode? reportAssembliesMode = null,
DeduplicateMode? deduplicateMode = null,
- Dictionary? defaultTags = null)
+ Dictionary? defaultTags = null,
+ bool? experimentalEnableLogs = null)
{
if (dsn is not null)
{
@@ -317,6 +322,11 @@ internal static void ConfigureSentrySerilogOptions(
sentrySerilogOptions.DeduplicateMode = deduplicateMode.Value;
}
+ if (experimentalEnableLogs.HasValue)
+ {
+ sentrySerilogOptions.Experimental.EnableLogs = experimentalEnableLogs.Value;
+ }
+
// Serilog-specific items
sentrySerilogOptions.InitializeSdk = dsn is not null; // Inferred from the Sentry overload that is used
if (defaultTags?.Count > 0)
@@ -354,7 +364,6 @@ public static LoggerConfiguration Sentry(
sdkDisposable = SentrySdk.Init(options);
}
- var minimumOverall = (LogEventLevel)Math.Min((int)options.MinimumBreadcrumbLevel, (int)options.MinimumEventLevel);
- return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), minimumOverall);
+ return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable));
}
}
diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs
index b506b9da6c..7e58fec173 100644
--- a/src/Sentry/SentryLog.cs
+++ b/src/Sentry/SentryLog.cs
@@ -9,6 +9,7 @@ namespace Sentry;
/// This API is experimental and it may change in the future.
///
[Experimental(DiagnosticId.ExperimentalFeature)]
+[DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")]
public sealed class SentryLog
{
private readonly Dictionary _attributes;
diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index 1455bbc51b..f204ed0701 100644
--- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -46,6 +46,7 @@ namespace Serilog
Sentry.SentryLevel? diagnosticLevel = default,
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
- System.Collections.Generic.Dictionary? defaultTags = null) { }
+ System.Collections.Generic.Dictionary? defaultTags = null,
+ bool? experimentalEnableLogs = default) { }
}
}
\ No newline at end of file
diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 1455bbc51b..f204ed0701 100644
--- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -46,6 +46,7 @@ namespace Serilog
Sentry.SentryLevel? diagnosticLevel = default,
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
- System.Collections.Generic.Dictionary? defaultTags = null) { }
+ System.Collections.Generic.Dictionary? defaultTags = null,
+ bool? experimentalEnableLogs = default) { }
}
}
\ No newline at end of file
diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index 1455bbc51b..f204ed0701 100644
--- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -46,6 +46,7 @@ namespace Serilog
Sentry.SentryLevel? diagnosticLevel = default,
Sentry.ReportAssembliesMode? reportAssembliesMode = default,
Sentry.DeduplicateMode? deduplicateMode = default,
- System.Collections.Generic.Dictionary? defaultTags = null) { }
+ System.Collections.Generic.Dictionary? defaultTags = null,
+ bool? experimentalEnableLogs = default) { }
}
}
\ No newline at end of file
diff --git a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs
index 8088548272..760b5b84ff 100644
--- a/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs
+++ b/test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs
@@ -1,4 +1,6 @@
#if NET6_0_OR_GREATER
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Sentry.AspNetCore.TestUtils;
namespace Sentry.Serilog.Tests;
@@ -22,5 +24,52 @@ public async Task UnhandledException_MarkedAsUnhandled()
Assert.Contains(Events, e => e.Logger == "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware");
Assert.Collection(Events, @event => Assert.Collection(@event.SentryExceptions, x => Assert.False(x.Mechanism?.Handled)));
}
+
+ [Fact]
+ public async Task StructuredLogging_Disabled()
+ {
+ Assert.False(ExperimentalEnableLogs);
+
+ var handler = new RequestHandler
+ {
+ Path = "/log",
+ Handler = context =>
+ {
+ context.RequestServices.GetRequiredService>().LogInformation("Hello, World!");
+ return Task.CompletedTask;
+ }
+ };
+
+ Handlers = new[] { handler };
+ Build();
+ await HttpClient.GetAsync(handler.Path);
+ await ServiceProvider.GetRequiredService().FlushAsync();
+
+ Assert.Empty(Logs);
+ }
+
+ [Fact]
+ public async Task StructuredLogging_Enabled()
+ {
+ ExperimentalEnableLogs = true;
+
+ var handler = new RequestHandler
+ {
+ Path = "/log",
+ Handler = context =>
+ {
+ context.RequestServices.GetRequiredService>().LogInformation("Hello, World!");
+ return Task.CompletedTask;
+ }
+ };
+
+ Handlers = new[] { handler };
+ Build();
+ await HttpClient.GetAsync(handler.Path);
+ await ServiceProvider.GetRequiredService().FlushAsync();
+
+ Assert.NotEmpty(Logs);
+ Assert.Contains(Logs, log => log.Level == SentryLogLevel.Info && log.Message == "Hello, World!");
+ }
}
#endif
diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt
new file mode 100644
index 0000000000..2eb81f0805
--- /dev/null
+++ b/test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt
@@ -0,0 +1,70 @@
+{
+ envelopes: [
+ {
+ Header: {
+ sdk: {
+ name: sentry.dotnet
+ }
+ },
+ Items: [
+ {
+ Header: {
+ content_type: application/vnd.sentry.items.log+json,
+ item_count: 4,
+ type: log
+ },
+ Payload: {
+ Source: {
+ Length: 4
+ }
+ }
+ }
+ ]
+ }
+ ],
+ logs: [
+ [
+ {
+ Level: Debug,
+ Message: Debug message with a Scalar property: 42,
+ Template: Debug message with a Scalar property: {Scalar},
+ Parameters: [
+ {
+ Scalar: 42
+ }
+ ]
+ },
+ {
+ Level: Info,
+ Message: Information message with a Sequence property: [41, 42, 43],
+ Template: Information message with a Sequence property: {Sequence},
+ Parameters: [
+ {
+ Sequence: [41, 42, 43]
+ }
+ ]
+ },
+ {
+ Level: Warning,
+ Message: Warning message with a Dictionary property: [("key": "value")],
+ Template: Warning message with a Dictionary property: {Dictionary},
+ Parameters: [
+ {
+ Dictionary: [("key": "value")]
+ }
+ ]
+ },
+ {
+ Level: Error,
+ Message: Error message with a Structure property: [42, "42"],
+ Template: Error message with a Structure property: {Structure},
+ Parameters: [
+ {
+ Structure: [42, "42"]
+ }
+ ]
+ }
+ ]
+ ],
+ diagnostics: []
+}
\ No newline at end of file
diff --git a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs
index 10d7b538bc..aab8e7dd17 100644
--- a/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs
+++ b/test/Sentry.Serilog.Tests/IntegrationTests.verify.cs
@@ -100,5 +100,45 @@ public Task LoggingInsideTheContextOfLogging()
})
.IgnoreStandardSentryMembers();
}
+
+ [Fact]
+ public Task StructuredLogging()
+ {
+ var transport = new RecordingTransport();
+
+ var configuration = new LoggerConfiguration();
+ configuration.MinimumLevel.Debug();
+ var diagnosticLogger = new InMemoryDiagnosticLogger();
+ configuration.WriteTo.Sentry(
+ _ =>
+ {
+ _.MinimumEventLevel = (LogEventLevel)int.MaxValue;
+ _.Experimental.EnableLogs = true;
+ _.Transport = transport;
+ _.DiagnosticLogger = diagnosticLogger;
+ _.Dsn = ValidDsn;
+ _.Debug = true;
+ _.Environment = "test-environment";
+ _.Release = "test-release";
+ });
+
+ Log.Logger = configuration.CreateLogger();
+
+ Log.Debug("Debug message with a Scalar property: {Scalar}", 42);
+ Log.Information("Information message with a Sequence property: {Sequence}", new object[] { new int[] { 41, 42, 43 } });
+ Log.Warning("Warning message with a Dictionary property: {Dictionary}", new Dictionary { { "key", "value" } });
+ Log.Error("Error message with a Structure property: {Structure}", (Number: 42, Text: "42"));
+
+ Log.CloseAndFlush();
+
+ var envelopes = transport.Envelopes;
+ var logs = transport.Payloads.OfType()
+ .Select(payload => payload.Source)
+ .OfType()
+ .Select(log => log.Items.ToArray());
+ var diagnostics = diagnosticLogger.Entries.Where(_ => _.Level >= SentryLevel.Warning);
+ return Verify(new { envelopes, logs, diagnostics })
+ .IgnoreStandardSentryMembers();
+ }
}
#endif
diff --git a/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs b/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs
index 57f5fcf9a5..c0cda5c45a 100644
--- a/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs
+++ b/test/Sentry.Serilog.Tests/SentrySerilogSinkExtensionsTests.cs
@@ -28,6 +28,7 @@ private class Fixture
public bool InitializeSdk { get; } = false;
public LogEventLevel MinimumEventLevel { get; } = LogEventLevel.Verbose;
public LogEventLevel MinimumBreadcrumbLevel { get; } = LogEventLevel.Fatal;
+ public bool ExperimentalEnableLogs { get; } = true;
public static SentrySerilogOptions GetSut() => new();
}
@@ -97,7 +98,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan
_fixture.SampleRate, _fixture.Release, _fixture.Environment, _fixture.MaxQueueItems,
_fixture.ShutdownTimeout, _fixture.DecompressionMethods, _fixture.RequestBodyCompressionLevel,
_fixture.RequestBodyCompressionBuffered, _fixture.Debug, _fixture.DiagnosticLevel,
- _fixture.ReportAssembliesMode, _fixture.DeduplicateMode);
+ _fixture.ReportAssembliesMode, _fixture.DeduplicateMode, null, _fixture.ExperimentalEnableLogs);
// Compare individual properties
Assert.Equal(_fixture.SendDefaultPii, sut.SendDefaultPii);
@@ -108,7 +109,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan
Assert.Equal(_fixture.SampleRate, sut.SampleRate);
Assert.Equal(_fixture.Release, sut.Release);
Assert.Equal(_fixture.Environment, sut.Environment);
- Assert.Equal(_fixture.Dsn, sut.Dsn!);
+ Assert.Equal(_fixture.Dsn, sut.Dsn);
Assert.Equal(_fixture.MaxQueueItems, sut.MaxQueueItems);
Assert.Equal(_fixture.ShutdownTimeout, sut.ShutdownTimeout);
Assert.Equal(_fixture.DecompressionMethods, sut.DecompressionMethods);
@@ -118,6 +119,7 @@ public void ConfigureSentrySerilogOptions_WithAllParameters_MakesAppropriateChan
Assert.Equal(_fixture.DiagnosticLevel, sut.DiagnosticLevel);
Assert.Equal(_fixture.ReportAssembliesMode, sut.ReportAssembliesMode);
Assert.Equal(_fixture.DeduplicateMode, sut.DeduplicateMode);
+ Assert.Equal(_fixture.ExperimentalEnableLogs, sut.Experimental.EnableLogs);
Assert.True(sut.InitializeSdk);
Assert.Equal(_fixture.MinimumEventLevel, sut.MinimumEventLevel);
Assert.Equal(_fixture.MinimumBreadcrumbLevel, sut.MinimumBreadcrumbLevel);
diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs
new file mode 100644
index 0000000000..b7cb36b76f
--- /dev/null
+++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs
@@ -0,0 +1,135 @@
+#nullable enable
+
+namespace Sentry.Serilog.Tests;
+
+public partial class SentrySinkTests
+{
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Emit_StructuredLogging_IsEnabled(bool isEnabled)
+ {
+ InMemorySentryStructuredLogger capturer = new();
+ _fixture.Hub.Logger.Returns(capturer);
+ _fixture.Options.Experimental.EnableLogs = isEnabled;
+
+ var sut = _fixture.GetSut();
+ var logger = new LoggerConfiguration().WriteTo.Sink(sut).MinimumLevel.Verbose().CreateLogger();
+
+ logger.Write(LogEventLevel.Information, "Message");
+
+ capturer.Logs.Should().HaveCount(isEnabled ? 1 : 0);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Emit_StructuredLogging_UseHubOptionsOverSinkOptions(bool isEnabled)
+ {
+ InMemorySentryStructuredLogger capturer = new();
+ _fixture.Hub.Logger.Returns(capturer);
+ _fixture.Options.Experimental.EnableLogs = true;
+
+ if (!isEnabled)
+ {
+ SentryClientExtensions.SentryOptionsForTestingOnly = null;
+ }
+
+ var sut = _fixture.GetSut();
+ var logger = new LoggerConfiguration().WriteTo.Sink(sut).MinimumLevel.Verbose().CreateLogger();
+
+ logger.Write(LogEventLevel.Information, "Message");
+
+ capturer.Logs.Should().HaveCount(isEnabled ? 1 : 0);
+ }
+
+ [Theory]
+ [InlineData(LogEventLevel.Verbose, SentryLogLevel.Trace)]
+ [InlineData(LogEventLevel.Debug, SentryLogLevel.Debug)]
+ [InlineData(LogEventLevel.Information, SentryLogLevel.Info)]
+ [InlineData(LogEventLevel.Warning, SentryLogLevel.Warning)]
+ [InlineData(LogEventLevel.Error, SentryLogLevel.Error)]
+ [InlineData(LogEventLevel.Fatal, SentryLogLevel.Fatal)]
+ public void Emit_StructuredLogging_LogLevel(LogEventLevel level, SentryLogLevel expected)
+ {
+ InMemorySentryStructuredLogger capturer = new();
+ _fixture.Hub.Logger.Returns(capturer);
+ _fixture.Options.Experimental.EnableLogs = true;
+
+ var sut = _fixture.GetSut();
+ var logger = new LoggerConfiguration().WriteTo.Sink(sut).MinimumLevel.Verbose().CreateLogger();
+
+ logger.Write(level, "Message");
+
+ capturer.Logs.Should().ContainSingle().Which.Level.Should().Be(expected);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Emit_StructuredLogging_LogEvent(bool withActiveSpan)
+ {
+ InMemorySentryStructuredLogger capturer = new();
+ _fixture.Hub.Logger.Returns(capturer);
+ _fixture.Options.Experimental.EnableLogs = true;
+ _fixture.Options.Environment = "test-environment";
+ _fixture.Options.Release = "test-release";
+
+ if (withActiveSpan)
+ {
+ var span = Substitute.For();
+ span.TraceId.Returns(SentryId.Create());
+ span.SpanId.Returns(SpanId.Create());
+ _fixture.Hub.GetSpan().Returns(span);
+ }
+ else
+ {
+ _fixture.Hub.GetSpan().Returns((ISpan?)null);
+ }
+
+ var sut = _fixture.GetSut();
+ var logger = new LoggerConfiguration()
+ .WriteTo.Sink(sut)
+ .MinimumLevel.Verbose()
+ .Enrich.WithProperty("Scalar-Property", 42)
+ .Enrich.WithProperty("Sequence-Property", new[] { 41, 42, 43 })
+ .Enrich.WithProperty("Dictionary-Property", new Dictionary { { "key", "value" } })
+ .Enrich.WithProperty("Structure-Property", (Number: 42, Text: "42"))
+ .CreateLogger();
+
+ logger.Write(LogEventLevel.Information,
+ "Message with Scalar property {Scalar}, Sequence property: {Sequence}, Dictionary property: {Dictionary}, and Structure property: {Structure}.",
+ 42, new[] { 41, 42, 43 }, new Dictionary { { "key", "value" } }, (Number: 42, Text: "42"));
+
+ var log = capturer.Logs.Should().ContainSingle().Which;
+ log.Timestamp.Should().BeOnOrBefore(DateTimeOffset.Now);
+ log.TraceId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.TraceId : _fixture.Scope.PropagationContext.TraceId);
+ log.Level.Should().Be(SentryLogLevel.Info);
+ log.Message.Should().Be("""Message with Scalar property 42, Sequence property: [41, 42, 43], Dictionary property: [("key": "value")], and Structure property: [42, "42"].""");
+ log.Template.Should().Be("Message with Scalar property {Scalar}, Sequence property: {Sequence}, Dictionary property: {Dictionary}, and Structure property: {Structure}.");
+ log.Parameters.Should().HaveCount(4);
+ log.Parameters[0].Should().BeEquivalentTo(new KeyValuePair("Scalar", 42));
+ log.Parameters[1].Should().BeEquivalentTo(new KeyValuePair("Sequence", "[41, 42, 43]"));
+ log.Parameters[2].Should().BeEquivalentTo(new KeyValuePair("Dictionary", """[("key": "value")]"""));
+ log.Parameters[3].Should().BeEquivalentTo(new KeyValuePair("Structure", """[42, "42"]"""));
+ log.ParentSpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : _fixture.Scope.PropagationContext.SpanId);
+
+ log.TryGetAttribute("sentry.environment", out object? environment).Should().BeTrue();
+ environment.Should().Be("test-environment");
+ log.TryGetAttribute("sentry.release", out object? release).Should().BeTrue();
+ release.Should().Be("test-release");
+ log.TryGetAttribute("sentry.sdk.name", out object? sdkName).Should().BeTrue();
+ sdkName.Should().Be(SentrySink.SdkName);
+ log.TryGetAttribute("sentry.sdk.version", out object? sdkVersion).Should().BeTrue();
+ sdkVersion.Should().Be(SentrySink.NameAndVersion.Version);
+
+ log.TryGetAttribute("property.Scalar-Property", out object? scalar).Should().BeTrue();
+ scalar.Should().Be(42);
+ log.TryGetAttribute("property.Sequence-Property", out object? sequence).Should().BeTrue();
+ sequence.Should().Be("[41, 42, 43]");
+ log.TryGetAttribute("property.Dictionary-Property", out object? dictionary).Should().BeTrue();
+ dictionary.Should().Be("""[("key": "value")]""");
+ log.TryGetAttribute("property.Structure-Property", out object? structure).Should().BeTrue();
+ structure.Should().Be("""[42, "42"]""");
+ }
+}
diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.cs
index ce1011aa2b..0ed6e94139 100644
--- a/test/Sentry.Serilog.Tests/SentrySinkTests.cs
+++ b/test/Sentry.Serilog.Tests/SentrySinkTests.cs
@@ -1,6 +1,6 @@
namespace Sentry.Serilog.Tests;
-public class SentrySinkTests
+public partial class SentrySinkTests
{
private class Fixture
{
@@ -15,6 +15,7 @@ public Fixture()
Hub.IsEnabled.Returns(true);
HubAccessor = () => Hub;
Hub.SubstituteConfigureScope(Scope);
+ SentryClientExtensions.SentryOptionsForTestingOnly = Options;
}
public SentrySink GetSut()
diff --git a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs
index 4510240ceb..b7b1a6d764 100644
--- a/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs
+++ b/test/Sentry.Serilog.Tests/SerilogAspNetSentrySdkTestFixture.cs
@@ -6,13 +6,21 @@ namespace Sentry.Serilog.Tests;
public class SerilogAspNetSentrySdkTestFixture : AspNetSentrySdkTestFixture
{
protected List Events;
+ protected List Logs;
+
+ protected bool ExperimentalEnableLogs { get; set; }
protected override void ConfigureBuilder(WebHostBuilder builder)
{
Events = new List();
+ Logs = new List();
+
Configure = options =>
{
options.SetBeforeSend((@event, _) => { Events.Add(@event); return @event; });
+
+ options.Experimental.EnableLogs = ExperimentalEnableLogs;
+ options.Experimental.SetBeforeSendLog(log => { Logs.Add(log); return log; });
};
ConfigureApp = app =>
@@ -27,7 +35,7 @@ protected override void ConfigureBuilder(WebHostBuilder builder)
builder.ConfigureLogging(loggingBuilder =>
{
var logger = new LoggerConfiguration()
- .WriteTo.Sentry(ValidDsn)
+ .WriteTo.Sentry(ValidDsn, experimentalEnableLogs: ExperimentalEnableLogs)
.CreateLogger();
loggingBuilder.AddSerilog(logger);
});
diff --git a/test/Sentry.Testing/InMemorySentryStructuredLogger.cs b/test/Sentry.Testing/InMemorySentryStructuredLogger.cs
index 440b83cdc7..0dfde97564 100644
--- a/test/Sentry.Testing/InMemorySentryStructuredLogger.cs
+++ b/test/Sentry.Testing/InMemorySentryStructuredLogger.cs
@@ -5,6 +5,7 @@ namespace Sentry.Testing;
public sealed class InMemorySentryStructuredLogger : SentryStructuredLogger
{
public List Entries { get; } = new();
+ public List Logs { get; } = new();
///
private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action? configureLog)
@@ -15,7 +16,7 @@ private protected override void CaptureLog(SentryLogLevel level, string template
///
protected internal override void CaptureLog(SentryLog log)
{
- throw new NotSupportedException();
+ Logs.Add(log);
}
///
diff --git a/test/Sentry.Testing/RecordingTransport.cs b/test/Sentry.Testing/RecordingTransport.cs
index 386be50b9b..ba0566a88c 100644
--- a/test/Sentry.Testing/RecordingTransport.cs
+++ b/test/Sentry.Testing/RecordingTransport.cs
@@ -1,5 +1,7 @@
using ISerializable = Sentry.Protocol.Envelopes.ISerializable;
+namespace Sentry.Testing;
+
public class RecordingTransport : ITransport
{
private List _envelopes = new();
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index 07c413e828..444cbfe027 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -613,6 +613,7 @@ namespace Sentry
Fatal = 4,
}
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
+ [System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")]
[System.Runtime.CompilerServices.RequiredMember]
public sealed class SentryLog
{
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 07c413e828..444cbfe027 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -613,6 +613,7 @@ namespace Sentry
Fatal = 4,
}
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
+ [System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")]
[System.Runtime.CompilerServices.RequiredMember]
public sealed class SentryLog
{
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index 2de7c68513..cd961b2d1d 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -599,6 +599,7 @@ namespace Sentry
[System.Runtime.Serialization.EnumMember(Value="fatal")]
Fatal = 4,
}
+ [System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")]
public sealed class SentryLog
{
public Sentry.SentryLogLevel Level { get; init; }
From 27a5afa3260af417ae6628cf31fa9df2990adc85 Mon Sep 17 00:00:00 2001
From: James Crosswell
Date: Sun, 28 Sep 2025 16:20:47 +1300
Subject: [PATCH 10/28] Backpressure (#4452)
---
CHANGELOG.md | 1 +
.../BackgroundWorkerFlushBenchmarks.cs | 2 +-
src/Sentry/BindableSentryOptions.cs | 2 +
src/Sentry/Http/HttpTransportBase.cs | 23 ++-
src/Sentry/Internal/BackgroundWorker.cs | 4 +
src/Sentry/Internal/BackpressureMonitor.cs | 166 ++++++++++++++++++
.../Internal/BackpressureMonitorExtensions.cs | 6 +
src/Sentry/Internal/Http/HttpTransport.cs | 4 +-
src/Sentry/Internal/Http/LazyHttpTransport.cs | 4 +-
src/Sentry/Internal/Hub.cs | 38 +++-
src/Sentry/Internal/SampleRandHelper.cs | 11 ++
src/Sentry/Internal/SdkComposer.cs | 8 +-
src/Sentry/Internal/UnsampledTransaction.cs | 7 +-
src/Sentry/SentryClient.cs | 22 ++-
src/Sentry/SentryOptions.cs | 6 +
src/Sentry/SentrySdk.cs | 6 +
...piApprovalTests.Run.DotNet8_0.verified.txt | 1 +
...piApprovalTests.Run.DotNet9_0.verified.txt | 1 +
.../ApiApprovalTests.Run.Net4_8.verified.txt | 1 +
test/Sentry.Tests/HubTests.cs | 99 ++++++++++-
.../Internals/BackgroundWorkerTests.cs | 37 +++-
.../Internals/BackpressureMonitorTests.cs | 163 +++++++++++++++++
.../Internals/Http/HttpTransportTests.cs | 36 ++++
test/Sentry.Tests/SentryClientTests.cs | 68 +++++--
24 files changed, 671 insertions(+), 45 deletions(-)
create mode 100644 src/Sentry/Internal/BackpressureMonitor.cs
create mode 100644 src/Sentry/Internal/BackpressureMonitorExtensions.cs
create mode 100644 test/Sentry.Tests/Internals/BackpressureMonitorTests.cs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cdf7c28d0..482bb3827b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Features
+- Added `EnableBackpressureHandling` option for Automatic backpressure handling. When enabled this automatically reduces the sample rate when the SDK detects events being dropped. ([#4452](https://github.com/getsentry/sentry-dotnet/pull/4452))
- Add (experimental) _Structured Logs_ integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462))
### Fixes
diff --git a/benchmarks/Sentry.Benchmarks/BackgroundWorkerFlushBenchmarks.cs b/benchmarks/Sentry.Benchmarks/BackgroundWorkerFlushBenchmarks.cs
index 3f31663e09..7d165930a8 100644
--- a/benchmarks/Sentry.Benchmarks/BackgroundWorkerFlushBenchmarks.cs
+++ b/benchmarks/Sentry.Benchmarks/BackgroundWorkerFlushBenchmarks.cs
@@ -20,7 +20,7 @@ public Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationT
[IterationSetup]
public void IterationSetup()
{
- _backgroundWorker = new BackgroundWorker(new FakeTransport(), new SentryOptions { MaxQueueItems = 1000 });
+ _backgroundWorker = new BackgroundWorker(new FakeTransport(), new SentryOptions { MaxQueueItems = 1000 }, null);
_event = new SentryEvent();
_envelope = Envelope.FromEvent(_event);
diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs
index 9ca3847e1e..f9077de9b4 100644
--- a/src/Sentry/BindableSentryOptions.cs
+++ b/src/Sentry/BindableSentryOptions.cs
@@ -9,6 +9,7 @@ internal partial class BindableSentryOptions
{
public bool? IsGlobalModeEnabled { get; set; }
public bool? EnableScopeSync { get; set; }
+ public bool? EnableBackpressureHandling { get; set; }
public List? TagFilters { get; set; }
public bool? SendDefaultPii { get; set; }
public bool? IsEnvironmentUser { get; set; }
@@ -64,6 +65,7 @@ public void ApplyTo(SentryOptions options)
{
options.IsGlobalModeEnabled = IsGlobalModeEnabled ?? options.IsGlobalModeEnabled;
options.EnableScopeSync = EnableScopeSync ?? options.EnableScopeSync;
+ options.EnableBackpressureHandling = EnableBackpressureHandling ?? options.EnableBackpressureHandling;
options.TagFilters = TagFilters?.Select(s => new StringOrRegex(s)).ToList() ?? options.TagFilters;
options.SendDefaultPii = SendDefaultPii ?? options.SendDefaultPii;
options.IsEnvironmentUser = IsEnvironmentUser ?? options.IsEnvironmentUser;
diff --git a/src/Sentry/Http/HttpTransportBase.cs b/src/Sentry/Http/HttpTransportBase.cs
index 846e889923..681818e824 100644
--- a/src/Sentry/Http/HttpTransportBase.cs
+++ b/src/Sentry/Http/HttpTransportBase.cs
@@ -16,6 +16,7 @@ public abstract class HttpTransportBase
internal const string DefaultErrorMessage = "No message";
private readonly SentryOptions _options;
+ private readonly BackpressureMonitor? _backpressureMonitor;
private readonly ISystemClock _clock;
private readonly Func _getEnvironmentVariable;
@@ -24,7 +25,7 @@ public abstract class HttpTransportBase
// Using string instead of SentryId here so that we can use Interlocked.Exchange(...).
private string? _lastDiscardedSessionInitId;
- private string _typeName;
+ private readonly string _typeName;
///
/// Constructor for this class.
@@ -33,8 +34,8 @@ public abstract class HttpTransportBase
/// An optional method used to read environment variables.
/// An optional system clock - used for testing.
protected HttpTransportBase(SentryOptions options,
- Func? getEnvironmentVariable = default,
- ISystemClock? clock = default)
+ Func? getEnvironmentVariable = null,
+ ISystemClock? clock = null)
{
_options = options;
_clock = clock ?? SystemClock.Clock;
@@ -42,6 +43,21 @@ protected HttpTransportBase(SentryOptions options,
_typeName = GetType().Name;
}
+ ///
+ /// Constructor for this class.
+ ///
+ /// The Sentry options.
+ /// The Sentry options.
+ /// An optional method used to read environment variables.
+ /// An optional system clock - used for testing.
+ internal HttpTransportBase(SentryOptions options, BackpressureMonitor? backpressureMonitor,
+ Func? getEnvironmentVariable = null,
+ ISystemClock? clock = null)
+ : this(options, getEnvironmentVariable, clock)
+ {
+ _backpressureMonitor = backpressureMonitor;
+ }
+
// Keep track of rate limits and their expiry dates.
// Internal for testing.
internal ConcurrentDictionary CategoryLimitResets { get; } = new();
@@ -256,6 +272,7 @@ private void ExtractRateLimits(HttpHeaders responseHeaders)
}
var now = _clock.GetUtcNow();
+ _backpressureMonitor?.RecordRateLimitHit(now);
// Join to a string to handle both single-header and multi-header cases
var rateLimitsEncoded = string.Join(",", rateLimitHeaderValues);
diff --git a/src/Sentry/Internal/BackgroundWorker.cs b/src/Sentry/Internal/BackgroundWorker.cs
index d747922d5a..99c4ce2d78 100644
--- a/src/Sentry/Internal/BackgroundWorker.cs
+++ b/src/Sentry/Internal/BackgroundWorker.cs
@@ -9,6 +9,7 @@ internal class BackgroundWorker : IBackgroundWorker, IDisposable
{
private readonly ITransport _transport;
private readonly SentryOptions _options;
+ private readonly BackpressureMonitor? _backpressureMonitor;
private readonly ConcurrentQueueLite _queue;
private readonly int _maxItems;
private readonly CancellationTokenSource _shutdownSource;
@@ -26,11 +27,13 @@ internal class BackgroundWorker : IBackgroundWorker, IDisposable
public BackgroundWorker(
ITransport transport,
SentryOptions options,
+ BackpressureMonitor? backpressureMonitor,
CancellationTokenSource? shutdownSource = null,
ConcurrentQueueLite? queue = null)
{
_transport = transport;
_options = options;
+ _backpressureMonitor = backpressureMonitor;
_queue = queue ?? new ConcurrentQueueLite();
_maxItems = options.MaxQueueItems;
_shutdownSource = shutdownSource ?? new CancellationTokenSource();
@@ -66,6 +69,7 @@ public bool EnqueueEnvelope(Envelope envelope, bool process)
var eventId = envelope.TryGetEventId(_options.DiagnosticLogger);
if (Interlocked.Increment(ref _currentItems) > _maxItems)
{
+ _backpressureMonitor?.RecordQueueOverflow();
Interlocked.Decrement(ref _currentItems);
_options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.QueueOverflow, envelope);
_options.LogInfo("Discarding envelope {0} because the queue is full.", eventId);
diff --git a/src/Sentry/Internal/BackpressureMonitor.cs b/src/Sentry/Internal/BackpressureMonitor.cs
new file mode 100644
index 0000000000..c45b38264d
--- /dev/null
+++ b/src/Sentry/Internal/BackpressureMonitor.cs
@@ -0,0 +1,166 @@
+using Sentry.Extensibility;
+using Sentry.Infrastructure;
+
+namespace Sentry.Internal;
+
+///
+///
+/// Monitors system health and calculates a DownsampleFactor that can be applied to events and transactions when the
+/// system is under load.
+///
+///
+/// The health checks used by the monitor are:
+///
+///
+/// - if any events have been dropped due to queue being full in the last 2 seconds
+/// - if any new rate limits have been applied since the last check
+///
+/// This check is performed every 10 seconds. With each negative health check we halve tracesSampleRate up to 10 times, meaning the original tracesSampleRate is multiplied by 1, 1/2, 1/4, ... up to 1/1024 (~ 0.001%). Any positive health check resets to the original tracesSampleRate set in SentryOptions.
+///
+/// Backpressure Management
+internal class BackpressureMonitor : IDisposable
+{
+ internal const int MaxDownsamples = 10;
+ private const int CheckIntervalInSeconds = 10;
+ private const int RecentThresholdInSeconds = 2;
+
+ private readonly IDiagnosticLogger? _logger;
+ private readonly ISystemClock _clock;
+ private long _lastQueueOverflow = DateTimeOffset.MinValue.Ticks;
+ private long _lastRateLimitEvent = DateTimeOffset.MinValue.Ticks;
+ private volatile int _downsampleLevel = 0;
+
+ private static readonly long RecencyThresholdTicks = TimeSpan.FromSeconds(RecentThresholdInSeconds).Ticks;
+ private static readonly long CheckIntervalTicks = TimeSpan.FromSeconds(CheckIntervalInSeconds).Ticks;
+
+ private readonly CancellationTokenSource _cts = new();
+
+ private readonly Task _workerTask;
+ internal int DownsampleLevel => _downsampleLevel;
+ internal long LastQueueOverflowTicks => Interlocked.Read(ref _lastQueueOverflow);
+ internal long LastRateLimitEventTicks => Interlocked.Read(ref _lastRateLimitEvent);
+
+ public BackpressureMonitor(IDiagnosticLogger? logger, ISystemClock? clock = null, bool enablePeriodicHealthCheck = true)
+ {
+ _logger = logger;
+ _clock = clock ?? SystemClock.Clock;
+
+ if (enablePeriodicHealthCheck)
+ {
+ _logger?.LogDebug("Starting BackpressureMonitor.");
+ _workerTask = Task.Run(() => DoWorkAsync(_cts.Token));
+ }
+ else
+ {
+ _workerTask = Task.CompletedTask;
+ }
+ }
+
+ ///
+ /// For testing purposes only. Sets the downsample level directly.
+ ///
+ internal void SetDownsampleLevel(int level)
+ {
+ Interlocked.Exchange(ref _downsampleLevel, level);
+ }
+
+ internal void IncrementDownsampleLevel()
+ {
+ var oldValue = _downsampleLevel;
+ if (oldValue < MaxDownsamples)
+ {
+ var newValue = oldValue + 1;
+ if (Interlocked.CompareExchange(ref _downsampleLevel, newValue, oldValue) == oldValue)
+ {
+ _logger?.LogDebug("System is under pressure, increasing downsample level to {0}.", newValue);
+ }
+ }
+ }
+
+ ///
+ /// A multiplier that can be applied to the SampleRate or TracesSampleRate to reduce the amount of data sent to
+ /// Sentry when the system is under pressure.
+ ///
+ public double DownsampleFactor
+ {
+ get
+ {
+ var level = _downsampleLevel;
+ return 1d / (1 << level); // 1 / (2^level) = 1, 1/2, 1/4, 1/8, ...
+ }
+ }
+
+ public void RecordRateLimitHit(DateTimeOffset when) => Interlocked.Exchange(ref _lastRateLimitEvent, when.Ticks);
+
+ public void RecordQueueOverflow() => Interlocked.Exchange(ref _lastQueueOverflow, _clock.GetUtcNow().Ticks);
+
+ private async Task DoWorkAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ DoHealthCheck();
+
+ await Task.Delay(TimeSpan.FromSeconds(CheckIntervalInSeconds), cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Task was cancelled, exit gracefully
+ }
+ }
+
+ internal void DoHealthCheck()
+ {
+ if (IsHealthy)
+ {
+ var previous = Interlocked.Exchange(ref _downsampleLevel, 0);
+ if (previous > 0)
+ {
+ _logger?.LogDebug("System is healthy, resetting downsample level.");
+ }
+ }
+ else
+ {
+ IncrementDownsampleLevel();
+ }
+ }
+
+ ///
+ /// Checks for any recent queue overflows or any rate limit events since the last check.
+ ///
+ ///
+ internal bool IsHealthy
+ {
+ get
+ {
+ var nowTicks = _clock.GetUtcNow().Ticks;
+ var recentOverflowCutoff = nowTicks - RecencyThresholdTicks;
+ var rateLimitCutoff = nowTicks - CheckIntervalTicks;
+ return LastQueueOverflowTicks < recentOverflowCutoff && LastRateLimitEventTicks < rateLimitCutoff;
+ }
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ _cts.Cancel();
+ _workerTask.Wait();
+ }
+ catch (AggregateException ex) when (ex.InnerException is OperationCanceledException)
+ {
+ // Ignore cancellation
+ }
+ catch (Exception ex)
+ {
+ // Log rather than throw
+ _logger?.LogWarning(ex, "Error in BackpressureMonitor.Dispose");
+ }
+ finally
+ {
+ _cts.Dispose();
+ }
+ }
+}
diff --git a/src/Sentry/Internal/BackpressureMonitorExtensions.cs b/src/Sentry/Internal/BackpressureMonitorExtensions.cs
new file mode 100644
index 0000000000..cddbe22023
--- /dev/null
+++ b/src/Sentry/Internal/BackpressureMonitorExtensions.cs
@@ -0,0 +1,6 @@
+namespace Sentry.Internal;
+
+internal static class BackpressureMonitorExtensions
+{
+ internal static double GetDownsampleFactor(this BackpressureMonitor? monitor) => monitor?.DownsampleFactor ?? 1.0;
+}
diff --git a/src/Sentry/Internal/Http/HttpTransport.cs b/src/Sentry/Internal/Http/HttpTransport.cs
index 7f8b7ce91d..a245dc4e14 100644
--- a/src/Sentry/Internal/Http/HttpTransport.cs
+++ b/src/Sentry/Internal/Http/HttpTransport.cs
@@ -15,10 +15,10 @@ public HttpTransport(SentryOptions options, HttpClient httpClient)
_httpClient = httpClient;
}
- internal HttpTransport(SentryOptions options, HttpClient httpClient,
+ internal HttpTransport(SentryOptions options, HttpClient httpClient, BackpressureMonitor? backpressureMonitor,
Func? getEnvironmentVariable = default,
ISystemClock? clock = default)
- : base(options, getEnvironmentVariable, clock)
+ : base(options, backpressureMonitor, getEnvironmentVariable, clock)
{
_httpClient = httpClient;
}
diff --git a/src/Sentry/Internal/Http/LazyHttpTransport.cs b/src/Sentry/Internal/Http/LazyHttpTransport.cs
index 51f6b71be7..f2475c9471 100644
--- a/src/Sentry/Internal/Http/LazyHttpTransport.cs
+++ b/src/Sentry/Internal/Http/LazyHttpTransport.cs
@@ -7,9 +7,9 @@ internal class LazyHttpTransport : ITransport
{
private readonly Lazy _httpTransport;
- public LazyHttpTransport(SentryOptions options)
+ public LazyHttpTransport(SentryOptions options, BackpressureMonitor? backpressureMonitor)
{
- _httpTransport = new Lazy(() => new HttpTransport(options, options.GetHttpClient()));
+ _httpTransport = new Lazy(() => new HttpTransport(options, options.GetHttpClient(), backpressureMonitor));
}
public Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default)
diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs
index ec92169cd3..5170a64233 100644
--- a/src/Sentry/Internal/Hub.cs
+++ b/src/Sentry/Internal/Hub.cs
@@ -14,9 +14,11 @@ internal class Hub : IHub, IDisposable
private readonly ISystemClock _clock;
private readonly ISessionManager _sessionManager;
private readonly SentryOptions _options;
+ private readonly ISampleRandHelper _sampleRandHelper;
private readonly RandomValuesFactory _randomValuesFactory;
private readonly IReplaySession _replaySession;
private readonly List _integrationsToCleanup = new();
+ private readonly BackpressureMonitor? _backpressureMonitor;
#if MEMORY_DUMP_SUPPORTED
private readonly MemoryMonitor? _memoryMonitor;
@@ -44,7 +46,9 @@ internal Hub(
ISystemClock? clock = null,
IInternalScopeManager? scopeManager = null,
RandomValuesFactory? randomValuesFactory = null,
- IReplaySession? replaySession = null)
+ IReplaySession? replaySession = null,
+ ISampleRandHelper? sampleRandHelper = null,
+ BackpressureMonitor? backpressureMonitor = null)
{
if (string.IsNullOrWhiteSpace(options.Dsn))
{
@@ -59,8 +63,13 @@ internal Hub(
_randomValuesFactory = randomValuesFactory ?? new SynchronizedRandomValuesFactory();
_sessionManager = sessionManager ?? new GlobalSessionManager(options);
_clock = clock ?? SystemClock.Clock;
- client ??= new SentryClient(options, randomValuesFactory: _randomValuesFactory, sessionManager: _sessionManager);
+ if (_options.EnableBackpressureHandling)
+ {
+ _backpressureMonitor = backpressureMonitor ?? new BackpressureMonitor(_options.DiagnosticLogger, clock);
+ }
+ client ??= new SentryClient(options, randomValuesFactory: _randomValuesFactory, sessionManager: _sessionManager, backpressureMonitor: _backpressureMonitor);
_replaySession = replaySession ?? ReplaySession.Instance;
+ _sampleRandHelper = sampleRandHelper ?? new SampleRandHelperAdapter();
ScopeManager = scopeManager ?? new SentryScopeManager(options, client);
if (!options.IsGlobalModeEnabled)
@@ -175,9 +184,10 @@ internal ITransactionTracer StartTransaction(
bool? isSampled = null;
double? sampleRate = null;
+ DiscardReason? discardReason = null;
var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscSampleRand) ?? false
? double.Parse(dscSampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
- : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString());
+ : _sampleRandHelper.GenerateSampleRand(context.TraceId.ToString());
// TracesSampler runs regardless of whether a decision has already been made, as it can be used to override it.
if (_options.TracesSampler is { } tracesSampler)
@@ -189,8 +199,14 @@ internal ITransactionTracer StartTransaction(
if (tracesSampler(samplingContext) is { } samplerSampleRate)
{
// The TracesSampler trumps all other sampling decisions (even the trace header)
- sampleRate = samplerSampleRate;
- isSampled = SampleRandHelper.IsSampled(sampleRand, samplerSampleRate);
+ sampleRate = samplerSampleRate * _backpressureMonitor.GetDownsampleFactor();
+ isSampled = SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
+ if (isSampled is false)
+ {
+ // If sampling out is only a result of the downsampling then we specify the reason as backpressure
+ // management... otherwise the event would have been sampled out anyway, so it's just regular sampling.
+ discardReason = sampleRand < samplerSampleRate ? DiscardReason.Backpressure : DiscardReason.SampleRate;
+ }
// Ensure the actual sampleRate is set on the provided DSC (if any) when the TracesSampler reached a sampling decision
dynamicSamplingContext?.SetSampleRate(samplerSampleRate);
@@ -201,8 +217,15 @@ internal ITransactionTracer StartTransaction(
// finally fallback to Random sampling if the decision has been made by no other means
if (isSampled == null)
{
- sampleRate = _options.TracesSampleRate ?? 0.0;
+ var optionsSampleRate = _options.TracesSampleRate ?? 0.0;
+ sampleRate = optionsSampleRate * _backpressureMonitor.GetDownsampleFactor();
isSampled = context.IsSampled ?? SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
+ if (isSampled is false)
+ {
+ // If sampling out is only a result of the downsampling then we specify the reason as backpressure
+ // management... otherwise the event would have been sampled out anyway, so it's just regular sampling.
+ discardReason = sampleRand < optionsSampleRate ? DiscardReason.Backpressure : DiscardReason.SampleRate;
+ }
if (context.IsSampled is null && _options.TracesSampleRate is not null)
{
@@ -220,6 +243,7 @@ internal ITransactionTracer StartTransaction(
{
SampleRate = sampleRate,
SampleRand = sampleRand,
+ DiscardReason = discardReason,
DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
};
// If no DSC was provided, create one based on this transaction.
@@ -845,6 +869,8 @@ public void Dispose()
}
//Don't dispose of ScopeManager since we want dangling transactions to still be able to access tags.
+ _backpressureMonitor?.Dispose();
+
#if __IOS__
// TODO
#elif ANDROID
diff --git a/src/Sentry/Internal/SampleRandHelper.cs b/src/Sentry/Internal/SampleRandHelper.cs
index 5e420c70f8..4a8d1c028f 100644
--- a/src/Sentry/Internal/SampleRandHelper.cs
+++ b/src/Sentry/Internal/SampleRandHelper.cs
@@ -11,5 +11,16 @@ internal static double GenerateSampleRand(string traceId)
<= 0 => false,
_ => sampleRand < rate
};
+}
+[DebuggerStepThrough]
+internal class SampleRandHelperAdapter : ISampleRandHelper
+{
+ [DebuggerStepThrough]
+ public double GenerateSampleRand(string traceId) => SampleRandHelper.GenerateSampleRand(traceId);
+}
+
+internal interface ISampleRandHelper
+{
+ public double GenerateSampleRand(string traceId);
}
diff --git a/src/Sentry/Internal/SdkComposer.cs b/src/Sentry/Internal/SdkComposer.cs
index dab9a08986..d7263f8765 100644
--- a/src/Sentry/Internal/SdkComposer.cs
+++ b/src/Sentry/Internal/SdkComposer.cs
@@ -8,14 +8,16 @@ namespace Sentry.Internal;
internal class SdkComposer
{
private readonly SentryOptions _options;
+ private readonly BackpressureMonitor? _backpressureMonitor;
- public SdkComposer(SentryOptions options)
+ public SdkComposer(SentryOptions options, BackpressureMonitor? backpressureMonitor)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
if (options.Dsn is null)
{
throw new ArgumentException("No DSN defined in the SentryOptions");
}
+ _backpressureMonitor = backpressureMonitor;
}
private ITransport CreateTransport()
@@ -23,7 +25,7 @@ private ITransport CreateTransport()
_options.LogDebug("Creating transport.");
// Start from either the transport given on options, or create a new HTTP transport.
- var transport = _options.Transport ?? new LazyHttpTransport(_options);
+ var transport = _options.Transport ?? new LazyHttpTransport(_options, _backpressureMonitor);
// When a cache directory path is given, wrap the transport in a caching transport.
if (!string.IsNullOrWhiteSpace(_options.CacheDirectoryPath))
@@ -87,6 +89,6 @@ public IBackgroundWorker CreateBackgroundWorker()
var transport = CreateTransport();
- return new BackgroundWorker(transport, _options);
+ return new BackgroundWorker(transport, _options, _backpressureMonitor);
}
}
diff --git a/src/Sentry/Internal/UnsampledTransaction.cs b/src/Sentry/Internal/UnsampledTransaction.cs
index a14698f7f3..8f55b7d808 100644
--- a/src/Sentry/Internal/UnsampledTransaction.cs
+++ b/src/Sentry/Internal/UnsampledTransaction.cs
@@ -42,6 +42,8 @@ public UnsampledTransaction(IHub hub, ITransactionContext context)
public double? SampleRand { get; set; }
+ public DiscardReason? DiscardReason { get; set; }
+
public override string Name
{
get => _context.Name;
@@ -72,8 +74,9 @@ public override void Finish()
// Record the discarded events
var spanCount = Spans.Count + 1; // 1 for each span + 1 for the transaction itself
- _options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Transaction);
- _options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Span, spanCount);
+ var discardReason = DiscardReason ?? Internal.DiscardReason.SampleRate;
+ _options?.ClientReportRecorder.RecordDiscardedEvent(discardReason, DataCategory.Transaction);
+ _options?.ClientReportRecorder.RecordDiscardedEvent(discardReason, DataCategory.Span, spanCount);
_options?.LogDebug("Finished unsampled transaction");
}
diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs
index 5e7ae5cfd1..80ede995dd 100644
--- a/src/Sentry/SentryClient.cs
+++ b/src/Sentry/SentryClient.cs
@@ -16,6 +16,7 @@ namespace Sentry;
public class SentryClient : ISentryClient, IDisposable
{
private readonly SentryOptions _options;
+ private readonly BackpressureMonitor? _backpressureMonitor;
private readonly ISessionManager _sessionManager;
private readonly RandomValuesFactory _randomValuesFactory;
private readonly Enricher _enricher;
@@ -41,9 +42,11 @@ internal SentryClient(
SentryOptions options,
IBackgroundWorker? worker = null,
RandomValuesFactory? randomValuesFactory = null,
- ISessionManager? sessionManager = null)
+ ISessionManager? sessionManager = null,
+ BackpressureMonitor? backpressureMonitor = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
+ _backpressureMonitor = backpressureMonitor;
_randomValuesFactory = randomValuesFactory ?? new SynchronizedRandomValuesFactory();
_sessionManager = sessionManager ?? new GlobalSessionManager(options);
_enricher = new Enricher(options);
@@ -52,7 +55,7 @@ internal SentryClient(
if (worker == null)
{
- var composer = new SdkComposer(options);
+ var composer = new SdkComposer(options, backpressureMonitor);
Worker = composer.CreateBackgroundWorker();
}
else
@@ -307,7 +310,6 @@ public SentryId CaptureCheckIn(
/// A task to await for the flush operation.
public Task FlushAsync(TimeSpan timeout) => Worker.FlushAsync(timeout);
- // TODO: this method needs to be refactored, it's really hard to analyze nullability
private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope)
{
var filteredExceptions = ApplyExceptionFilters(@event.Exception);
@@ -375,16 +377,22 @@ private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope)
if (_options.SampleRate != null)
{
- if (!_randomValuesFactory.NextBool(_options.SampleRate.Value))
+ var sampleRate = _options.SampleRate.Value;
+ var downsampledRate = sampleRate * _backpressureMonitor.GetDownsampleFactor();
+ var sampleRand = _randomValuesFactory.NextDouble();
+ if (sampleRand >= downsampledRate)
{
- _options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Error);
- _options.LogDebug("Event sampled.");
+ // If sampling out is only a result of the downsampling then we specify the reason as backpressure
+ // management... otherwise the event would have been sampled out anyway, so it's just regular sampling.
+ var reason = sampleRand < sampleRate ? DiscardReason.Backpressure : DiscardReason.SampleRate;
+ _options.ClientReportRecorder.RecordDiscardedEvent(reason, DataCategory.Error);
+ _options.LogDebug("Event sampled out.");
return SentryId.Empty;
}
}
else
{
- _options.LogDebug("Event not sampled.");
+ _options.LogDebug("Event sampled in.");
}
if (!_options.SendDefaultPii)
diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs
index df4e2937e3..ace651ec46 100644
--- a/src/Sentry/SentryOptions.cs
+++ b/src/Sentry/SentryOptions.cs
@@ -94,6 +94,12 @@ public bool IsGlobalModeEnabled
///
public bool EnableScopeSync { get; set; }
+ ///
+ /// Enables or disables automatic backpressure handling. When enabled, the SDK will monitor system health and
+ /// reduce the sampling rate of events and transactions when the system is under load.
+ ///
+ public bool EnableBackpressureHandling { get; set; } = false;
+
///
/// This holds a reference to the current transport, when one is active.
/// If set manually before initialization, the provided transport will be used instead of the default transport.
diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs
index eab7b9838f..b4bb1fbeea 100644
--- a/src/Sentry/SentrySdk.cs
+++ b/src/Sentry/SentrySdk.cs
@@ -316,7 +316,13 @@ public static class Experimental
public static IDisposable PushScope() => CurrentHub.PushScope();
///
+ ///
/// Binds the client to the current scope.
+ ///
+ ///
+ /// This might be used to bind a client with a different DSN or configuration (e.g. so that a particular thread or
+ /// part of the application sends events to a different Sentry project).
+ ///
///
/// The client.
[DebuggerStepThrough]
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index 444cbfe027..b83e9340d7 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -717,6 +717,7 @@ namespace Sentry
public bool DisableSentryHttpMessageHandler { get; set; }
public string? Distribution { get; set; }
public string? Dsn { get; set; }
+ public bool EnableBackpressureHandling { get; set; }
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 444cbfe027..b83e9340d7 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -717,6 +717,7 @@ namespace Sentry
public bool DisableSentryHttpMessageHandler { get; set; }
public string? Distribution { get; set; }
public string? Dsn { get; set; }
+ public bool EnableBackpressureHandling { get; set; }
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index cd961b2d1d..6999d24318 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -687,6 +687,7 @@ namespace Sentry
public bool DisableSentryHttpMessageHandler { get; set; }
public string? Distribution { get; set; }
public string? Dsn { get; set; }
+ public bool EnableBackpressureHandling { get; set; }
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs
index 2df8394504..342d38f84d 100644
--- a/test/Sentry.Tests/HubTests.cs
+++ b/test/Sentry.Tests/HubTests.cs
@@ -5,11 +5,11 @@
namespace Sentry.Tests;
-public partial class HubTests
+public partial class HubTests : IDisposable
{
private readonly ITestOutputHelper _output;
- private class Fixture
+ private class Fixture : IDisposable
{
public SentryOptions Options { get; }
public ISentryClient Client { get; set; }
@@ -17,6 +17,8 @@ private class Fixture
public IInternalScopeManager ScopeManager { get; set; }
public ISystemClock Clock { get; set; }
public IReplaySession ReplaySession { get; }
+ public ISampleRandHelper SampleRandHelper { get; set; }
+ public BackpressureMonitor BackpressureMonitor { get; set; }
public Fixture()
{
@@ -26,13 +28,22 @@ public Fixture()
TracesSampleRate = 1.0,
AutoSessionTracking = false
};
-
Client = Substitute.For();
-
ReplaySession = Substitute.For();
}
- public Hub GetSut() => new(Options, Client, SessionManager, Clock, ScopeManager, replaySession: ReplaySession);
+ public void Dispose()
+ {
+ BackpressureMonitor?.Dispose();
+ }
+
+ public Hub GetSut() => new(Options, Client, SessionManager, Clock, ScopeManager, replaySession: ReplaySession,
+ sampleRandHelper: SampleRandHelper, backpressureMonitor: BackpressureMonitor);
+ }
+
+ public void Dispose()
+ {
+ _fixture.Dispose();
}
private readonly Fixture _fixture = new();
@@ -714,6 +725,84 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_UsesSampleRate
transactionTracer.DynamicSamplingContext.Should().BeSameAs(dsc);
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void StartTransaction_Backpressure_Downsamples(bool usesTracesSampler)
+ {
+ // Arrange
+ var transactionContext = new TransactionContext("name", "operation");
+
+ var clock = new MockClock(DateTimeOffset.UtcNow);
+ _fixture.Options.EnableBackpressureHandling = true;
+ _fixture.BackpressureMonitor = new BackpressureMonitor(null, clock, enablePeriodicHealthCheck: false);
+ _fixture.BackpressureMonitor.SetDownsampleLevel(1);
+ var sampleRate = 0.5f;
+ var expectedDownsampledRate = sampleRate * _fixture.BackpressureMonitor.DownsampleFactor;
+ if (usesTracesSampler)
+ {
+ _fixture.Options.TracesSampler = _ => sampleRate;
+ }
+ else
+ {
+ _fixture.Options.TracesSampleRate = sampleRate;
+ }
+
+ var hub = _fixture.GetSut();
+
+ // Act
+ var transaction = hub.StartTransaction(transactionContext, new Dictionary());
+
+ switch (transaction)
+ {
+ // Assert
+ case TransactionTracer tracer:
+ tracer.SampleRate.Should().Be(expectedDownsampledRate);
+ break;
+ case UnsampledTransaction unsampledTransaction:
+ unsampledTransaction.SampleRate.Should().Be(expectedDownsampledRate);
+ break;
+ default:
+ throw new Exception("Unexpected transaction type.");
+ }
+ }
+
+ [Theory]
+ [InlineData(true, 0.4f, "backpressure")]
+ [InlineData(true, 0.6f, "sample_rate")]
+ [InlineData(false, 0.4f, "backpressure")]
+ [InlineData(false, 0.6f, "sample_rate")]
+ public void StartTransaction_Backpressure_SetsDiscardReason(bool usesTracesSampler, double sampleRand, string discardReason)
+ {
+ // Arrange
+ var transactionContext = new TransactionContext("name", "operation");
+
+ var clock = new MockClock(DateTimeOffset.UtcNow);
+ _fixture.SampleRandHelper = Substitute.For();
+ _fixture.SampleRandHelper.GenerateSampleRand(Arg.Any()).Returns(sampleRand);
+ _fixture.Options.EnableBackpressureHandling = true;
+ _fixture.BackpressureMonitor = new BackpressureMonitor(null, clock, enablePeriodicHealthCheck: false);
+ _fixture.BackpressureMonitor.SetDownsampleLevel(1);
+ var sampleRate = 0.5f;
+ if (usesTracesSampler)
+ {
+ _fixture.Options.TracesSampler = _ => sampleRate;
+ }
+ else
+ {
+ _fixture.Options.TracesSampleRate = sampleRate;
+ }
+
+ var hub = _fixture.GetSut();
+
+ // Act
+ var transaction = hub.StartTransaction(transactionContext, new Dictionary());
+ transaction.Should().BeOfType();
+ var unsampledTransaction = (UnsampledTransaction)transaction;
+ var expectedReason = new DiscardReason(discardReason);
+ unsampledTransaction.DiscardReason.Should().Be(expectedReason);
+ }
+
// overwrite the 'sample_rate' of the Dynamic Sampling Context (DSC) when a sampling decisions is made in the downstream SDK
// 1. overwrite when 'TracesSampler' reaches a sampling decision
// 2. keep when a sampling decision has been made upstream (via 'TransactionContext.IsSampled')
diff --git a/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs b/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs
index d506a179e6..87fbed204e 100644
--- a/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs
+++ b/test/Sentry.Tests/Internals/BackgroundWorkerTests.cs
@@ -4,7 +4,7 @@
namespace Sentry.Tests.Internals;
-public class BackgroundWorkerTests
+public class BackgroundWorkerTests : IDisposable
{
private readonly Fixture _fixture;
@@ -13,7 +13,12 @@ public BackgroundWorkerTests(ITestOutputHelper outputHelper)
_fixture = new Fixture(outputHelper);
}
- private class Fixture
+ public void Dispose()
+ {
+ _fixture.Dispose();
+ }
+
+ private class Fixture : IDisposable
{
public IClientReportRecorder ClientReportRecorder { get; private set; } = Substitute.For();
public ITransport Transport { get; set; } = Substitute.For();
@@ -23,6 +28,7 @@ private class Fixture
public SentryOptions SentryOptions { get; set; } = new();
private readonly TimeSpan _defaultShutdownTimeout;
+ public BackpressureMonitor BackpressureMonitor { get; set; }
public Fixture(ITestOutputHelper outputHelper)
{
@@ -39,7 +45,6 @@ public Fixture(ITestOutputHelper outputHelper)
var token = callInfo.Arg();
return token.IsCancellationRequested ? Task.FromCanceled(token) : Task.CompletedTask;
});
-
SentryOptions.Dsn = ValidDsn;
SentryOptions.Debug = true;
SentryOptions.DiagnosticLogger = Logger;
@@ -54,6 +59,7 @@ public BackgroundWorker GetSut()
=> new(
Transport,
SentryOptions,
+ BackpressureMonitor,
CancellationTokenSource,
Queue);
@@ -68,6 +74,11 @@ public IClientReportRecorder UseRealClientReportRecorder()
SentryOptions.ClientReportRecorder = ClientReportRecorder;
return ClientReportRecorder;
}
+
+ public void Dispose()
+ {
+ BackpressureMonitor?.Dispose();
+ }
}
[Fact]
@@ -244,6 +255,26 @@ public void CaptureEvent_LimitReached_RecordsDiscardedEvent()
.RecordDiscardedEvent(DiscardReason.QueueOverflow, DataCategory.Error);
}
+ [Fact]
+ public void CaptureEvent_LimitReached_CallsBackpressureMonitor()
+ {
+ // Arrange
+ var clock = new MockClock(DateTimeOffset.UtcNow);
+ _fixture.BackpressureMonitor = new BackpressureMonitor(null, clock, false);
+ var envelope = Envelope.FromEvent(new SentryEvent());
+ _fixture.SentryOptions.MaxQueueItems = 1;
+
+ using var sut = _fixture.GetSut();
+ sut.EnqueueEnvelope(envelope, process: false);
+
+ // Act
+ sut.EnqueueEnvelope(envelope);
+
+ // Assert
+ _fixture.BackpressureMonitor.LastQueueOverflowTicks.Should().Be(clock.GetUtcNow().Ticks);
+ _fixture.BackpressureMonitor.IsHealthy.Should().BeFalse();
+ }
+
[Fact]
public void CaptureEvent_DisposedWorker_ThrowsObjectDisposedException()
{
diff --git a/test/Sentry.Tests/Internals/BackpressureMonitorTests.cs b/test/Sentry.Tests/Internals/BackpressureMonitorTests.cs
new file mode 100644
index 0000000000..9cb10a4c3d
--- /dev/null
+++ b/test/Sentry.Tests/Internals/BackpressureMonitorTests.cs
@@ -0,0 +1,163 @@
+namespace Sentry.Tests.Internals;
+
+public class BackpressureMonitorTests
+{
+ private class Fixture
+ {
+ private IDiagnosticLogger Logger { get; } = Substitute.For();
+ public ISystemClock Clock { get; } = Substitute.For();
+ public DateTimeOffset Now { get; set; } = DateTimeOffset.UtcNow;
+
+ public BackpressureMonitor GetSut() => new(Logger, Clock, enablePeriodicHealthCheck: false);
+ }
+
+ private readonly Fixture _fixture = new();
+
+ [Fact]
+ public void DownsampleFactor_Initial_IsOne()
+ {
+ // Arrange
+ using var monitor = _fixture.GetSut();
+
+ // Act
+ var factor = monitor.DownsampleFactor;
+
+ // Assert
+ factor.Should().Be(1.0);
+ }
+
+ [Theory]
+ [InlineData(0, 1.0)]
+ [InlineData(1, 0.5)]
+ [InlineData(2, 0.25)]
+ [InlineData(10, 1.0 / 1024)]
+ public void DownsampleFactor_CalculatesCorrectly(int level, double expected)
+ {
+ // Arrange
+ using var monitor = _fixture.GetSut();
+ monitor.SetDownsampleLevel(level);
+
+ // Act
+ var factor = monitor.DownsampleFactor;
+
+ // Assert
+ factor.Should().BeApproximately(expected, 1e-8);
+ }
+
+ [Fact]
+ public void RecordRateLimitHit_UpdatesState()
+ {
+ // Arrange
+ using var monitor = _fixture.GetSut();
+ var when = _fixture.Now.Subtract(TimeSpan.FromSeconds(1));
+
+ // Act
+ monitor.RecordRateLimitHit(when);
+
+ // Assert
+ monitor.LastRateLimitEventTicks.Should().Be(when.Ticks);
+ }
+
+ [Fact]
+ public void RecordQueueOverflow_UpdatesState()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+
+ // Act
+ monitor.RecordQueueOverflow();
+
+ // Assert
+ monitor.LastQueueOverflowTicks.Should().Be(_fixture.Now.Ticks);
+ }
+
+ [Fact]
+ public void IsHealthy_True_WhenNoRecentEvents()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+
+ // Act & Assert
+ monitor.IsHealthy.Should().BeTrue();
+ }
+
+ [Fact]
+ public void IsHealthy_False_WhenRecentQueueOverflow()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+
+ // Act
+ monitor.RecordQueueOverflow();
+
+ // Assert
+ monitor.IsHealthy.Should().BeFalse();
+ }
+
+ [Fact]
+ public void IsHealthy_False_WhenRecentRateLimit()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+
+ // Act
+ monitor.RecordRateLimitHit(_fixture.Now);
+
+ // Assert
+ monitor.IsHealthy.Should().BeFalse();
+ }
+
+ [Fact]
+ public void DoHealthCheck_Unhealthy_DownsampleLevelIncreases()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+ monitor.RecordQueueOverflow();
+
+ // Act
+ monitor.DoHealthCheck();
+
+ // Assert
+ monitor.DownsampleLevel.Should().Be(1);
+ }
+
+ [Fact]
+ public void DoHealthCheck_Unhealthy_MaximumDownsampleLevelRespected()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+ monitor.RecordQueueOverflow();
+
+ // Act
+ var overmax = BackpressureMonitor.MaxDownsamples + 1;
+ for (var i = 0; i <= overmax; i++)
+ {
+ monitor.DoHealthCheck();
+ }
+
+ // Assert
+ monitor.DownsampleLevel.Should().Be(BackpressureMonitor.MaxDownsamples);
+ }
+
+ [Fact]
+ public void DoHealthCheck_Healthy_DownsampleLevelResets()
+ {
+ // Arrange
+ _fixture.Clock.GetUtcNow().Returns(_fixture.Now);
+ using var monitor = _fixture.GetSut();
+ monitor.SetDownsampleLevel(2);
+
+ // Act
+ monitor.DoHealthCheck();
+
+ // Assert
+ monitor.IsHealthy.Should().BeTrue();
+ monitor.DownsampleLevel.Should().Be(0);
+ }
+}
diff --git a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs
index 2c92200cd9..548a39f2d0 100644
--- a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs
+++ b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs
@@ -298,6 +298,7 @@ public async Task SendEnvelopeAsync_ItemRateLimit_DropsItem(string metricNamespa
Debug = true
},
new HttpClient(httpHandler),
+ null,
clock: _fakeClock);
// First request always goes through
@@ -382,6 +383,7 @@ public async Task SendEnvelopeAsync_RateLimited_CountsDiscardedEventsCorrectly()
var httpTransport = new HttpTransport(
options,
new HttpClient(httpHandler),
+ null,
clock: _fakeClock
);
@@ -846,4 +848,38 @@ public void ProcessEnvelope_SendClientReportsEnabled_ShouldReportTransactionsAnd
var expectedDiscardedSpanCount = transaction.Spans.Count + 1;
options.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.RateLimitBackoff, DataCategory.Span, expectedDiscardedSpanCount);
}
+
+ [Fact]
+ public async Task SendEnvelopeAsync_RateLimited_CallsBackpressureMonitor()
+ {
+ // Arrange
+ using var httpHandler = new RecordingHttpMessageHandler(
+ new FakeHttpMessageHandler(
+ () => SentryResponses.GetRateLimitResponse("1234:event, 897:transaction")
+ ));
+
+ using var backpressureMonitor = new BackpressureMonitor(null, _fakeClock, false);
+ var options = new SentryOptions
+ {
+ Dsn = ValidDsn,
+ DiagnosticLogger = _testOutputLogger,
+ SendClientReports = false,
+ ClientReportRecorder = Substitute.For(),
+ Debug = true
+ };
+
+ var httpTransport = new HttpTransport(
+ options,
+ new HttpClient(httpHandler),
+ backpressureMonitor,
+ clock: _fakeClock
+ );
+
+ // Act
+ await httpTransport.SendEnvelopeAsync(Envelope.FromEvent(new SentryEvent()));
+
+ // Assert
+ backpressureMonitor.LastRateLimitEventTicks.Should().Be(_fakeClock.GetUtcNow().Ticks);
+ backpressureMonitor.IsHealthy.Should().BeFalse();
+ }
}
diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs
index 0ebb3eb2af..a2c3782638 100644
--- a/test/Sentry.Tests/SentryClientTests.cs
+++ b/test/Sentry.Tests/SentryClientTests.cs
@@ -4,9 +4,9 @@
namespace Sentry.Tests;
-public partial class SentryClientTests
+public partial class SentryClientTests : IDisposable
{
- private class Fixture
+ private class Fixture : IDisposable
{
public SentryOptions SentryOptions { get; set; } = new()
{
@@ -17,7 +17,9 @@ private class Fixture
public IBackgroundWorker BackgroundWorker { get; set; } = Substitute.For();
public IClientReportRecorder ClientReportRecorder { get; } = Substitute.For();
+ public RandomValuesFactory RandomValuesFactory { get; set; } = null;
public ISessionManager SessionManager { get; set; } = Substitute.For();
+ public BackpressureMonitor BackpressureMonitor { get; set; }
public Fixture()
{
@@ -27,9 +29,19 @@ public Fixture()
public SentryClient GetSut()
{
- var randomValuesFactory = new IsolatedRandomValuesFactory();
- return new SentryClient(SentryOptions, BackgroundWorker, randomValuesFactory, SessionManager);
+ var randomValuesFactory = RandomValuesFactory ?? new IsolatedRandomValuesFactory();
+ return new SentryClient(SentryOptions, BackgroundWorker, randomValuesFactory, SessionManager, BackpressureMonitor);
}
+
+ public void Dispose()
+ {
+ BackpressureMonitor?.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ _fixture.Dispose();
}
private readonly Fixture _fixture = new();
@@ -590,6 +602,29 @@ public void CaptureEvent_SampleDrop_RecordsDiscard()
.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Error);
}
+ [Theory]
+ [InlineData(0.6f, "sample_rate")] // Sample rand is greater than the sample rate
+ [InlineData(0.4f, "backpressure")] // Sample is dropped due to downsampling
+ public void CaptureEvent_SampleDrop_RecordsCorrectDiscardReason(double sampleRand, string discardReason)
+ {
+ // Arrange
+ _fixture.RandomValuesFactory = Substitute.For();
+ _fixture.RandomValuesFactory.NextDouble().Returns(sampleRand);
+ _fixture.SentryOptions.SampleRate = 0.5f;
+ var logger = Substitute.For();
+ _fixture.BackpressureMonitor = new BackpressureMonitor(logger, null, false);
+ _fixture.BackpressureMonitor.SetDownsampleLevel(1);
+ var sut = _fixture.GetSut();
+
+ // Act
+ var @event = new SentryEvent();
+ _ = sut.CaptureEvent(@event);
+
+ // Assert
+ var expectedReason = new DiscardReason(discardReason);
+ _fixture.ClientReportRecorder.Received(1).RecordDiscardedEvent(expectedReason, DataCategory.Error);
+ }
+
[Fact]
public void CaptureEvent_SamplingHighest_SendsEvent()
{
@@ -624,17 +659,28 @@ public void CaptureEvent_SamplingNull_DropsEvent()
}
[Theory]
- [InlineData(0.25f)]
- [InlineData(0.50f)]
- [InlineData(0.75f)]
- public void CaptureEvent_WithSampleRate_AppropriateDistribution(float sampleRate)
+ [InlineData(0.25f, 0)]
+ [InlineData(0.50f, 0)]
+ [InlineData(0.75f, 0)]
+ [InlineData(0.25f, 1)]
+ [InlineData(0.50f, 1)]
+ [InlineData(0.75f, 1)]
+ [InlineData(0.25f, 3)]
+ [InlineData(0.50f, 3)]
+ [InlineData(0.75f, 3)]
+ public void CaptureEvent_WithSampleRate_AppropriateDistribution(float sampleRate, int downsampleLevel)
{
// Arrange
+ var now = DateTimeOffset.UtcNow;
+ var clock = new MockClock(now);
+ _fixture.BackpressureMonitor = new BackpressureMonitor(null, clock, enablePeriodicHealthCheck: false);
+ _fixture.BackpressureMonitor.SetDownsampleLevel(downsampleLevel);
+ _fixture.SentryOptions.SampleRate = sampleRate;
+
const int numEvents = 1000;
const double allowedRelativeDeviation = 0.15;
const uint allowedDeviation = (uint)(allowedRelativeDeviation * numEvents);
- var expectedSampled = (int)(sampleRate * numEvents);
- _fixture.SentryOptions.SampleRate = sampleRate;
+ var expectedSampled = (int)(numEvents * sampleRate * _fixture.BackpressureMonitor.DownsampleFactor);
// This test expects an approximate uniform distribution of random numbers, so we'll retry a few times.
TestHelpers.RetryTest(maxAttempts: 3, _output, () =>
@@ -695,7 +741,7 @@ public void CaptureEvent_Processing_Order()
var logger = Substitute.For();
logger.IsEnabled(Arg.Any()).Returns(true);
- logger.When(x => x.Log(Arg.Any(), Arg.Is("Event not sampled.")))
+ logger.When(x => x.Log(Arg.Any(), Arg.Is("Event sampled in.")))
.Do(_ => processingOrder.Add("SampleRate"));
_fixture.SentryOptions.DiagnosticLogger = logger;
_fixture.SentryOptions.Debug = true;
From a7cac114e9b8ecce0cbc43325252a6bce406defa Mon Sep 17 00:00:00 2001
From: J-P Nurmi
Date: Sun, 28 Sep 2025 06:03:31 +0200
Subject: [PATCH 11/28] ci: remove unnecessary "Remove unused applications" for
build-sentry-native (#4570)
---
.github/workflows/build.yml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f54403af15..8dce487c30 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -64,10 +64,6 @@ jobs:
key: sentry-native-${{ matrix.rid }}-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }}
enableCrossOsArchive: true
- - name: Remove unused applications
- if: ${{ !matrix.container }}
- uses: ./.github/actions/freediskspace
-
- run: scripts/build-sentry-native.ps1
if: steps.cache.outputs.cache-hit != 'true'
shell: pwsh
From 4ca9324f13a6560478b97e3d64e1041c67e1eb6d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sun, 28 Sep 2025 17:08:15 +1300
Subject: [PATCH 12/28] chore: update modules/sentry-cocoa.properties to 8.56.2
(#4572)
Co-authored-by: GitHub
---
CHANGELOG.md | 6 +++---
modules/sentry-cocoa.properties | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 482bb3827b..55cb587afd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,9 +15,9 @@
### Dependencies
-- Bump Cocoa SDK from v8.56.0 to v8.56.1 ([#4555](https://github.com/getsentry/sentry-dotnet/pull/4555))
- - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8561)
- - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.56.0...8.56.1)
+- Bump Cocoa SDK from v8.56.0 to v8.56.2 ([#4555](https://github.com/getsentry/sentry-dotnet/pull/4555), [#4572](https://github.com/getsentry/sentry-dotnet/pull/4572))
+ - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8562)
+ - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.56.0...8.56.2)
- Bump Native SDK from v0.11.0 to v0.11.1 ([#4557](https://github.com/getsentry/sentry-dotnet/pull/4557))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0111)
- [diff](https://github.com/getsentry/sentry-native/compare/0.11.0...0.11.1)
diff --git a/modules/sentry-cocoa.properties b/modules/sentry-cocoa.properties
index 8d3c842313..1b37e7bb3c 100644
--- a/modules/sentry-cocoa.properties
+++ b/modules/sentry-cocoa.properties
@@ -1,2 +1,2 @@
-version = 8.56.1
+version = 8.56.2
repo = https://github.com/getsentry/sentry-cocoa
From fc55d48911df3b181767f9c3093c9fd556edc356 Mon Sep 17 00:00:00 2001
From: J-P Nurmi
Date: Sun, 28 Sep 2025 06:11:41 +0200
Subject: [PATCH 13/28] ci: use global.json for actions/setup-dotnet (#4571)
---
.github/actions/environment/action.yml | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/.github/actions/environment/action.yml b/.github/actions/environment/action.yml
index 9e451a282e..a139546f38 100644
--- a/.github/actions/environment/action.yml
+++ b/.github/actions/environment/action.yml
@@ -87,9 +87,8 @@ runs:
- name: Install .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: |
- 8.0.x
- 9.0.304
+ global-json-file: global.json
+ dotnet-version: 8.0.x
- name: Install .NET Workloads
shell: bash
From e9f75ac013089e3191a8a3925fb012237cda2939 Mon Sep 17 00:00:00 2001
From: J-P Nurmi
Date: Tue, 30 Sep 2025 16:00:22 +0200
Subject: [PATCH 14/28] test(ci): .NET 5.0 with MSBuild 16 (#4569)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Stefan Pölz <38893694+Flash0ver@users.noreply.github.com>
---
.github/actions/environment/action.yml | 13 ++++
.github/workflows/build.yml | 7 +-
integration-test/common.ps1 | 2 +-
integration-test/msbuild.Tests.ps1 | 101 +++++++++++++++++++++++++
integration-test/nuget5.config | 11 +++
scripts/install-libssl1.sh | 22 ++++++
6 files changed, 154 insertions(+), 2 deletions(-)
create mode 100644 integration-test/msbuild.Tests.ps1
create mode 100644 integration-test/nuget5.config
create mode 100755 scripts/install-libssl1.sh
diff --git a/.github/actions/environment/action.yml b/.github/actions/environment/action.yml
index a139546f38..9561573ed9 100644
--- a/.github/actions/environment/action.yml
+++ b/.github/actions/environment/action.yml
@@ -24,6 +24,12 @@ runs:
shell: bash
run: sudo chmod 666 /var/run/docker.sock
+ # Install old deprecated libssl1 for .NET 5.0 on Linux
+ - name: Install libssl1 for NET 5.0 on Linux
+ if: ${{ runner.os == 'Linux' }}
+ shell: bash
+ run: sudo ./scripts/install-libssl1.sh
+
- name: Install Linux ARM 32-bit dependencies
if: ${{ matrix.rid == 'linux-arm' }}
shell: bash
@@ -90,6 +96,13 @@ runs:
global-json-file: global.json
dotnet-version: 8.0.x
+ # .NET 5.0 does not support ARM64 on macOS
+ - name: Install .NET 5.0 SDK
+ if: ${{ runner.os != 'macOS' || runner.arch != 'ARM64' }}
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
+ with:
+ dotnet-version: 5.0.x
+
- name: Install .NET Workloads
shell: bash
run: |
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8dce487c30..7f9755f98e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -297,7 +297,12 @@ jobs:
- name: Run MSBuild
id: msbuild
- run: msbuild Sentry-CI-Build-Windows.slnf -t:Restore,Build -p:Configuration=Release --nologo -v:minimal -flp:logfile=msbuild.log -p:CopyLocalLockFileAssemblies=true -bl:msbuild.binlog
+ run: msbuild Sentry-CI-Build-Windows.slnf -t:Restore,Build,Pack -p:Configuration=Release --nologo -v:minimal -flp:logfile=msbuild.log -p:CopyLocalLockFileAssemblies=true -bl:msbuild.binlog
+
+ - name: Test MSBuild
+ uses: getsentry/github-workflows/sentry-cli/integration-test/@a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1
+ with:
+ path: integration-test/msbuild.Tests.ps1
- name: Upload logs
if: ${{ always() }}
diff --git a/integration-test/common.ps1 b/integration-test/common.ps1
index b7215fc87e..51594c38e7 100644
--- a/integration-test/common.ps1
+++ b/integration-test/common.ps1
@@ -154,7 +154,7 @@ BeforeAll {
Push-Location $projectPath
try
{
- dotnet restore | ForEach-Object { Write-Host $_ }
+ dotnet restore /p:CheckEolTargetFramework=false | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0)
{
throw "Failed to restore the test app project."
diff --git a/integration-test/msbuild.Tests.ps1 b/integration-test/msbuild.Tests.ps1
new file mode 100644
index 0000000000..95288d7213
--- /dev/null
+++ b/integration-test/msbuild.Tests.ps1
@@ -0,0 +1,101 @@
+# This file contains test cases for https://pester.dev/
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+. $PSScriptRoot/common.ps1
+
+$IsARM64 = "Arm64".Equals([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString())
+
+# NOTE: These .NET versions are used to build a test app that consumes the Sentry
+# .NET SDK, and are not tied to the .NET version used to build the SDK itself.
+Describe 'MSBuild app' {
+ BeforeDiscovery {
+ $frameworks = @()
+
+ # .NET 5.0 does not support ARM64 on macOS
+ if (-not $IsMacOS -or -not $IsARM64)
+ {
+ $frameworks += @{
+ framework = 'net5.0'
+ sdk = '5.0.400'
+ # NuGet 5 does not support packageSourceMapping
+ config = "$PSScriptRoot\nuget5.config"
+ }
+ }
+
+ $frameworks += @(
+ @{ framework = 'net8.0'; sdk = '8.0.400' },
+ @{ framework = 'net9.0'; sdk = '9.0.300' }
+ )
+ }
+
+ Context '()' -ForEach $frameworks {
+ BeforeEach {
+ Write-Host "::group::Create msbuild-app"
+ dotnet new console --no-restore --output msbuild-app --framework $framework | ForEach-Object { Write-Host $_ }
+ $LASTEXITCODE | Should -Be 0
+ AddPackageReference msbuild-app Sentry
+ Push-Location msbuild-app
+ @'
+using System.Runtime.InteropServices;
+using Sentry;
+
+SentrySdk.Init(options =>
+{
+ options.Dsn = args[0];
+ options.Debug = true;
+});
+
+SentrySdk.CaptureMessage($"Hello from MSBuild app");
+'@ | Out-File Program.cs
+ Write-Host "::endgroup::"
+
+ Write-Host "::group::Setup .NET SDK"
+ if (Test-Path variable:sdk)
+ {
+ # Pin to a specific SDK version to use MSBuild from that version
+ @"
+{
+ "sdk": {
+ "version": "$sdk",
+ "rollForward": "latestFeature"
+ }
+}
+"@ | Out-File global.json
+ }
+ Write-Host "Using .NET SDK: $(dotnet --version)"
+ Write-Host "Using MSBuild version: $(dotnet msbuild -version)"
+ Write-Host "::endgroup::"
+ }
+
+ AfterEach {
+ Pop-Location
+ Remove-Item msbuild-app -Recurse -Force -ErrorAction SilentlyContinue
+ }
+
+ It 'builds without warnings and is able to capture a message' {
+ Write-Host "::group::Restore packages"
+ if (!(Test-Path variable:config))
+ {
+ $config = "$PSScriptRoot/nuget.config"
+ }
+ dotnet restore msbuild-app.csproj --configfile $config -p:CheckEolTargetFramework=false | ForEach-Object { Write-Host $_ }
+ $LASTEXITCODE | Should -Be 0
+ Write-Host "::endgroup::"
+
+ # TODO: pass -p:TreatWarningsAsErrors=true after #4554 is fixed
+ dotnet msbuild msbuild-app.csproj -t:Build -p:Configuration=Release -p:TreatWarningsAsErrors=false | ForEach-Object { Write-Host $_ }
+ $LASTEXITCODE | Should -Be 0
+
+ Write-Host "::group::Run msbuild-app"
+ $result = Invoke-SentryServer {
+ param([string]$url)
+ $dsn = $url.Replace('http://', 'http://key@') + '/0'
+ dotnet msbuild msbuild-app.csproj -t:Run -p:Configuration=Release -p:RunArguments=$dsn | ForEach-Object { Write-Host $_ }
+ $LASTEXITCODE | Should -Be 0
+ }
+ $result.HasErrors() | Should -BeFalse
+ $result.Envelopes() | Should -AnyElementMatch "`"message`":`"Hello from MSBuild app`""
+ Write-Host "::endgroup::"
+ }
+ }
+}
diff --git a/integration-test/nuget5.config b/integration-test/nuget5.config
new file mode 100644
index 0000000000..822e3e5985
--- /dev/null
+++ b/integration-test/nuget5.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/scripts/install-libssl1.sh b/scripts/install-libssl1.sh
new file mode 100755
index 0000000000..7097e1cd6b
--- /dev/null
+++ b/scripts/install-libssl1.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -euo pipefail
+
+# Install old deprecated libssl 1.x for .NET 5.0 on Linux to avoid:
+# Error: 'No usable version of libssl was found'
+
+if apk --version >/dev/null 2>&1; then
+ # Alpine Linux: openssl1.1-compat from the community repo
+ apk add --repository=https://dl-cdn.alpinelinux.org/alpine/v3.18/community openssl1.1-compat
+elif dpkg --version >/dev/null 2>&1; then
+ # Ubuntu: libssl1 from focal-security
+ # https://github.com/actions/runner-images/blob/d43555be6577f2ac4e4f78bf683c520687891e1b/images/ubuntu/scripts/build/install-sqlpackage.sh#L11-L21
+ if [ "$(dpkg --print-architecture)" = "arm64" ]; then
+ echo "deb http://ports.ubuntu.com/ubuntu-ports focal-security main" | tee /etc/apt/sources.list.d/focal-security.list
+ else
+ echo "deb http://security.ubuntu.com/ubuntu focal-security main" | tee /etc/apt/sources.list.d/focal-security.list
+ fi
+ apt-get update
+ apt-get install -y --no-install-recommends libssl1.1
+ rm /etc/apt/sources.list.d/focal-security.list
+ apt-get update
+fi
From de8dc9a71d691e5347be98a877b1e777e0440030 Mon Sep 17 00:00:00 2001
From: James Crosswell
Date: Wed, 1 Oct 2025 22:23:21 +1300
Subject: [PATCH 15/28] Ensure template is not sent for Structured logs with no
parameters (#4544)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Sentry Github Bot
Co-authored-by: Stefan Pölz <38893694+Flash0ver@users.noreply.github.com>
---
CHANGELOG.md | 1 +
src/Sentry/SentryLog.cs | 4 +++-
test/Sentry.Tests/SentryLogTests.cs | 24 ++++++++++++++++++++++++
3 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55cb587afd..61aaaee6c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
### Fixes
+- Templates are no longer sent with Structured Logs that have no parameters ([#4544](https://github.com/getsentry/sentry-dotnet/pull/4544))
- Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527))
- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
- Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519))
diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs
index 7e58fec173..6db62d0ba8 100644
--- a/src/Sentry/SentryLog.cs
+++ b/src/Sentry/SentryLog.cs
@@ -225,7 +225,9 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
writer.WritePropertyName("attributes");
writer.WriteStartObject();
- if (Template is not null)
+ // the SDK MUST NOT attach a sentry.message.template attribute if there are no parameters
+ // https://develop.sentry.dev/sdk/telemetry/logs/#default-attributes
+ if (Template is not null && !Parameters.IsDefaultOrEmpty)
{
SentryAttributeSerializer.WriteStringAttribute(writer, "sentry.message.template", Template);
}
diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs
index 3393137b85..105e196d56 100644
--- a/test/Sentry.Tests/SentryLogTests.cs
+++ b/test/Sentry.Tests/SentryLogTests.cs
@@ -67,6 +67,30 @@ public void Protocol_Default_VerifyAttributes()
notFound.Should().BeNull();
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void WriteTo_NoParameters_NoTemplate(bool hasParameters)
+ {
+ // Arrange
+ ImmutableArray> parameters = hasParameters
+ ? [new KeyValuePair("param", "params")]
+ : [];
+ var log = new SentryLog(Timestamp, TraceId, SentryLogLevel.Debug, "message")
+ {
+ Template = "template",
+ Parameters = parameters,
+ ParentSpanId = ParentSpanId,
+ };
+
+ // Act
+ var document = log.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output);
+ var attributes = document.RootElement.GetProperty("attributes");
+
+ // Assert
+ attributes.TryGetProperty("sentry.message.template", out _).Should().Be(hasParameters);
+ }
+
[Fact]
public void WriteTo_Envelope_MinimalSerializedSentryLog()
{
From 0c6ca6a11dd043ab338f67b9fcd5d62942a7d805 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20P=C3=B6lz?=
<38893694+Flash0ver@users.noreply.github.com>
Date: Wed, 1 Oct 2025 11:29:58 +0200
Subject: [PATCH 16/28] ref(logs): remove ExperimentalAttribute and
Experimental type from SentrySdk (#4567)
---
CHANGELOG.md | 4 +++
.../Sentry.Samples.Console.Basic/Program.cs | 8 ++---
src/Sentry/Extensibility/DisabledHub.cs | 2 --
src/Sentry/Extensibility/HubAdapter.cs | 4 +--
src/Sentry/IHub.cs | 2 --
src/Sentry/SentryLog.cs | 28 ++++-----------
src/Sentry/SentryLogLevel.cs | 3 --
src/Sentry/SentryOptions.cs | 4 ---
src/Sentry/SentrySdk.cs | 14 ++------
src/Sentry/SentryStructuredLogger.Format.cs | 26 --------------
src/Sentry/SentryStructuredLogger.cs | 2 --
...piApprovalTests.Run.DotNet8_0.verified.txt | 35 +------------------
...piApprovalTests.Run.DotNet9_0.verified.txt | 35 +------------------
.../ApiApprovalTests.Run.Net4_8.verified.txt | 5 +--
14 files changed, 20 insertions(+), 152 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61aaaee6c6..c7ae408c4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@
- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
- Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519))
+### API Changes
+
+- Remove `ExperimentalAttribute` from all _Structured Logs_ APIs, and remove `Experimental` property from `SentrySdk`, but keep `Experimental` property on `SentryOptions` ([#4567](https://github.com/getsentry/sentry-dotnet/pull/4567))
+
### Dependencies
- Bump Cocoa SDK from v8.56.0 to v8.56.2 ([#4555](https://github.com/getsentry/sentry-dotnet/pull/4555), [#4572](https://github.com/getsentry/sentry-dotnet/pull/4572))
diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs
index 6b1815bf93..dfc7723796 100644
--- a/samples/Sentry.Samples.Console.Basic/Program.cs
+++ b/samples/Sentry.Samples.Console.Basic/Program.cs
@@ -37,7 +37,7 @@
// This option tells Sentry to capture 100% of traces. You still need to start transactions and spans.
options.TracesSampleRate = 1.0;
- // This option enables Sentry Logs created via SentrySdk.Experimental.Logger.
+ // This option enables Sentry Logs created via SentrySdk.Logger.
options.Experimental.EnableLogs = true;
options.Experimental.SetBeforeSendLog(static log =>
{
@@ -73,7 +73,7 @@ async Task FirstFunction()
var httpClient = new HttpClient(messageHandler, true);
var html = await httpClient.GetStringAsync("https://example.com/");
WriteLine(html);
- SentrySdk.Experimental.Logger.LogInfo("HTTP Request completed.");
+ SentrySdk.Logger.LogInfo("HTTP Request completed.");
}
async Task SecondFunction()
@@ -94,7 +94,7 @@ async Task SecondFunction()
SentrySdk.CaptureException(exception);
span.Finish(exception);
- SentrySdk.Experimental.Logger.LogError(static log => log.SetAttribute("method", nameof(SecondFunction)),
+ SentrySdk.Logger.LogError(static log => log.SetAttribute("method", nameof(SecondFunction)),
"Error with message: {0}", exception.Message);
}
@@ -109,7 +109,7 @@ async Task ThirdFunction()
// Simulate doing some work
await Task.Delay(100);
- SentrySdk.Experimental.Logger.LogFatal(static log => log.SetAttribute("suppress", true),
+ SentrySdk.Logger.LogFatal(static log => log.SetAttribute("suppress", true),
"Crash imminent!");
// This is an example of an unhandled exception. It will be captured automatically.
diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs
index ad6165a50a..e835a0edfc 100644
--- a/src/Sentry/Extensibility/DisabledHub.cs
+++ b/src/Sentry/Extensibility/DisabledHub.cs
@@ -257,8 +257,6 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
///
/// Disabled Logger.
- /// This API is experimental and it may change in the future.
///
- [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger => DisabledSentryStructuredLogger.Instance;
}
diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs
index 132997cb5f..e94a6c1914 100644
--- a/src/Sentry/Extensibility/HubAdapter.cs
+++ b/src/Sentry/Extensibility/HubAdapter.cs
@@ -34,10 +34,8 @@ private HubAdapter() { }
///
/// Forwards the call to .
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
- public SentryStructuredLogger Logger { [DebuggerStepThrough] get => SentrySdk.Experimental.Logger; }
+ public SentryStructuredLogger Logger { [DebuggerStepThrough] get => SentrySdk.Logger; }
///
/// Forwards the call to .
diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs
index 7232aea817..8c3006c149 100644
--- a/src/Sentry/IHub.cs
+++ b/src/Sentry/IHub.cs
@@ -19,7 +19,6 @@ public interface IHub : ISentryClient, ISentryScopeManager
///
/// Creates and sends logs to Sentry.
- /// This API is experimental and it may change in the future.
///
///
/// Available options:
@@ -28,7 +27,6 @@ public interface IHub : ISentryClient, ISentryScopeManager
///
///
///
- [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger { get; }
///
diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs
index 6db62d0ba8..32fe2716e1 100644
--- a/src/Sentry/SentryLog.cs
+++ b/src/Sentry/SentryLog.cs
@@ -1,14 +1,16 @@
using Sentry.Extensibility;
-using Sentry.Infrastructure;
using Sentry.Protocol;
namespace Sentry;
///
-/// Represents the Sentry Log protocol.
-/// This API is experimental and it may change in the future.
+/// Represents a Sentry Structured Log.
///
-[Experimental(DiagnosticId.ExperimentalFeature)]
+///
+/// Sentry Docs: .
+/// Sentry Developer Documentation: .
+/// Sentry .NET SDK Docs: .
+///
[DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")]
public sealed class SentryLog
{
@@ -27,59 +29,44 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le
///
/// The timestamp of the log.
- /// This API is experimental and it may change in the future.
///
///
/// Sent as seconds since the Unix epoch.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public required DateTimeOffset Timestamp { get; init; }
///
/// The trace id of the log.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public required SentryId TraceId { get; init; }
///
/// The severity level of the log.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public required SentryLogLevel Level { get; init; }
///
/// The formatted log message.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public required string Message { get; init; }
///
/// The parameterized template string.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public string? Template { get; init; }
///
/// The parameters to the template string.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public ImmutableArray> Parameters { get; init; }
///
/// The span id of the span that was active when the log was collected.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public SpanId? ParentSpanId { get; init; }
///
/// Gets the attribute value associated with the specified key.
- /// This API is experimental and it may change in the future.
///
///
/// Returns if the contains an attribute with the specified key and it's value is not .
@@ -128,7 +115,6 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le
///
///
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public bool TryGetAttribute(string key, [NotNullWhen(true)] out object? value)
{
if (_attributes.TryGetValue(key, out var attribute) && attribute.Value is not null)
@@ -155,9 +141,7 @@ internal bool TryGetAttribute(string key, [NotNullWhen(true)] out string? value)
///
/// Set a key-value pair of data attached to the log.
- /// This API is experimental and it may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void SetAttribute(string key, object value)
{
_attributes[key] = new SentryAttribute(value);
diff --git a/src/Sentry/SentryLogLevel.cs b/src/Sentry/SentryLogLevel.cs
index 9ccde83f0d..f76a617571 100644
--- a/src/Sentry/SentryLogLevel.cs
+++ b/src/Sentry/SentryLogLevel.cs
@@ -1,11 +1,9 @@
using Sentry.Extensibility;
-using Sentry.Infrastructure;
namespace Sentry;
///
/// The severity of the structured log.
-/// This API is experimental and it may change in the future.
///
///
/// The named constants use the value of the lowest severity number per severity level:
@@ -41,7 +39,6 @@ namespace Sentry;
///
///
///
-[Experimental(DiagnosticId.ExperimentalFeature)]
public enum SentryLogLevel
{
///
diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs
index ace651ec46..4e2983cb91 100644
--- a/src/Sentry/SentryOptions.cs
+++ b/src/Sentry/SentryOptions.cs
@@ -1870,7 +1870,6 @@ internal static List GetDefaultInAppExclude() =>
///
/// This and related experimental APIs may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public SentryExperimentalOptions Experimental { get; set; } = new();
///
@@ -1879,7 +1878,6 @@ internal static List GetDefaultInAppExclude() =>
///
/// This and related experimental APIs may change in the future.
///
- [Experimental(DiagnosticId.ExperimentalFeature)]
public sealed class SentryExperimentalOptions
{
internal SentryExperimentalOptions()
@@ -1889,7 +1887,6 @@ internal SentryExperimentalOptions()
///
/// When set to , logs are sent to Sentry.
/// Defaults to .
- /// This API is experimental and it may change in the future.
///
///
public bool EnableLogs { get; set; } = false;
@@ -1901,7 +1898,6 @@ internal SentryExperimentalOptions()
///
/// Sets a callback function to be invoked before sending the log to Sentry.
/// When the delegate throws an during invocation, the log will not be captured.
- /// This API is experimental and it may change in the future.
///
///
/// It can be used to modify the log object before being sent to Sentry.
diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs
index b4bb1fbeea..a8588370b6 100644
--- a/src/Sentry/SentrySdk.cs
+++ b/src/Sentry/SentrySdk.cs
@@ -284,18 +284,8 @@ public void Dispose()
///
public static bool IsEnabled { [DebuggerStepThrough] get => CurrentHub.IsEnabled; }
- ///
- /// Experimental Sentry SDK features.
- ///
- ///
- /// This and related experimental APIs may change in the future.
- ///
- [Experimental(DiagnosticId.ExperimentalFeature)]
- public static class Experimental
- {
- ///
- public static SentryStructuredLogger Logger { [DebuggerStepThrough] get => CurrentHub.Logger; }
- }
+ ///
+ public static SentryStructuredLogger Logger { [DebuggerStepThrough] get => CurrentHub.Logger; }
///
/// Creates a new scope that will terminate when disposed.
diff --git a/src/Sentry/SentryStructuredLogger.Format.cs b/src/Sentry/SentryStructuredLogger.Format.cs
index 4575b5e0d9..cd7d8a4631 100644
--- a/src/Sentry/SentryStructuredLogger.Format.cs
+++ b/src/Sentry/SentryStructuredLogger.Format.cs
@@ -1,16 +1,12 @@
-using Sentry.Infrastructure;
-
namespace Sentry;
public abstract partial class SentryStructuredLogger
{
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogTrace(string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Trace, template, parameters, null);
@@ -18,12 +14,10 @@ public void LogTrace(string template, params object[] parameters)
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured.
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogTrace(Action configureLog, string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Trace, template, parameters, configureLog);
@@ -31,11 +25,9 @@ public void LogTrace(Action configureLog, string template, params obj
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogDebug(string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Debug, template, parameters, null);
@@ -43,12 +35,10 @@ public void LogDebug(string template, params object[] parameters)
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured.
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogDebug(Action configureLog, string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Debug, template, parameters, configureLog);
@@ -56,11 +46,9 @@ public void LogDebug(Action configureLog, string template, params obj
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogInfo(string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Info, template, parameters, null);
@@ -68,12 +56,10 @@ public void LogInfo(string template, params object[] parameters)
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured.
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogInfo(Action configureLog, string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Info, template, parameters, configureLog);
@@ -81,11 +67,9 @@ public void LogInfo(Action configureLog, string template, params obje
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogWarning(string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Warning, template, parameters, null);
@@ -93,12 +77,10 @@ public void LogWarning(string template, params object[] parameters)
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured.
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogWarning(Action configureLog, string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Warning, template, parameters, configureLog);
@@ -106,11 +88,9 @@ public void LogWarning(Action configureLog, string template, params o
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogError(string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Error, template, parameters, null);
@@ -118,12 +98,10 @@ public void LogError(string template, params object[] parameters)
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured.
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogError(Action configureLog, string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Error, template, parameters, configureLog);
@@ -131,11 +109,9 @@ public void LogError(Action configureLog, string template, params obj
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogFatal(string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Fatal, template, parameters, null);
@@ -143,12 +119,10 @@ public void LogFatal(string template, params object[] parameters)
///
/// Creates and sends a structured log to Sentry, with severity , when enabled and sampled.
- /// This API is experimental and it may change in the future.
///
/// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured.
/// A formattable . When incompatible with the , the log will not be captured. See System.String.Format.
/// The arguments to the . See System.String.Format.
- [Experimental(DiagnosticId.ExperimentalFeature)]
public void LogFatal(Action configureLog, string template, params object[] parameters)
{
CaptureLog(SentryLogLevel.Fatal, template, parameters, configureLog);
diff --git a/src/Sentry/SentryStructuredLogger.cs b/src/Sentry/SentryStructuredLogger.cs
index 8a0dd9da1b..9d81bd2820 100644
--- a/src/Sentry/SentryStructuredLogger.cs
+++ b/src/Sentry/SentryStructuredLogger.cs
@@ -5,9 +5,7 @@ namespace Sentry;
///
/// Creates and sends logs to Sentry.
-/// This API is experimental and it may change in the future.
///
-[Experimental(DiagnosticId.ExperimentalFeature)]
public abstract partial class SentryStructuredLogger
{
internal static SentryStructuredLogger Create(IHub hub, SentryOptions options, ISystemClock clock)
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index b83e9340d7..406a853181 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -187,7 +187,6 @@ namespace Sentry
public interface IHub : Sentry.ISentryClient, Sentry.ISentryScopeManager
{
Sentry.SentryId LastEventId { get; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
Sentry.SentryStructuredLogger Logger { get; }
void BindException(System.Exception exception, Sentry.ISpan span);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope);
@@ -612,35 +611,24 @@ namespace Sentry
[System.Runtime.Serialization.EnumMember(Value="fatal")]
Fatal = 4,
}
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")]
[System.Runtime.CompilerServices.RequiredMember]
public sealed class SentryLog
{
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public Sentry.SentryLogLevel Level { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public string Message { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public System.Collections.Immutable.ImmutableArray> Parameters { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SpanId? ParentSpanId { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public string? Template { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public System.DateTimeOffset Timestamp { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public Sentry.SentryId TraceId { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void SetAttribute(string key, object value) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? value) { }
}
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public enum SentryLogLevel
{
Trace = 1,
@@ -721,7 +709,6 @@ namespace Sentry
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SentryOptions.SentryExperimentalOptions Experimental { get; set; }
public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; }
public System.Collections.Generic.IList FailedRequestTargets { get; set; }
@@ -808,7 +795,6 @@ namespace Sentry
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public sealed class SentryExperimentalOptions
{
public bool EnableLogs { get; set; }
@@ -845,6 +831,7 @@ namespace Sentry
{
public static bool IsEnabled { get; }
public static Sentry.SentryId LastEventId { get; }
+ public static Sentry.SentryStructuredLogger Logger { get; }
public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.SentryHint? hint = null) { }
public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
@@ -905,11 +892,6 @@ namespace Sentry
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { }
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { }
public static void UnsetTag(string key) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
- public static class Experimental
- {
- public static Sentry.SentryStructuredLogger Logger { get; }
- }
}
public class SentrySession : Sentry.ISentrySession
{
@@ -989,34 +971,21 @@ namespace Sentry
public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { }
public static Sentry.SentryStackTrace FromJson(System.Text.Json.JsonElement json) { }
}
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public abstract class SentryStructuredLogger
{
protected abstract void CaptureLog(Sentry.SentryLog log);
protected abstract void Flush();
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogDebug(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogDebug(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogError(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogError(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogFatal(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogFatal(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogInfo(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogInfo(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogTrace(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogTrace(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogWarning(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogWarning(System.Action configureLog, string template, params object[] parameters) { }
}
public sealed class SentryThread : Sentry.ISentryJsonSerializable
@@ -1411,7 +1380,6 @@ namespace Sentry.Extensibility
public static readonly Sentry.Extensibility.DisabledHub Instance;
public bool IsEnabled { get; }
public Sentry.SentryId LastEventId { get; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SentryStructuredLogger Logger { get; }
public void BindClient(Sentry.ISentryClient client) { }
public void BindException(System.Exception exception, Sentry.ISpan span) { }
@@ -1459,7 +1427,6 @@ namespace Sentry.Extensibility
public static readonly Sentry.Extensibility.HubAdapter Instance;
public bool IsEnabled { get; }
public Sentry.SentryId LastEventId { get; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SentryStructuredLogger Logger { get; }
public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index b83e9340d7..406a853181 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -187,7 +187,6 @@ namespace Sentry
public interface IHub : Sentry.ISentryClient, Sentry.ISentryScopeManager
{
Sentry.SentryId LastEventId { get; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
Sentry.SentryStructuredLogger Logger { get; }
void BindException(System.Exception exception, Sentry.ISpan span);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope);
@@ -612,35 +611,24 @@ namespace Sentry
[System.Runtime.Serialization.EnumMember(Value="fatal")]
Fatal = 4,
}
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Diagnostics.DebuggerDisplay("SentryLog \\{ Level = {Level}, Message = \'{Message}\' \\}")]
[System.Runtime.CompilerServices.RequiredMember]
public sealed class SentryLog
{
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public Sentry.SentryLogLevel Level { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public string Message { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public System.Collections.Immutable.ImmutableArray> Parameters { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SpanId? ParentSpanId { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public string? Template { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public System.DateTimeOffset Timestamp { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
[System.Runtime.CompilerServices.RequiredMember]
public Sentry.SentryId TraceId { get; init; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void SetAttribute(string key, object value) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? value) { }
}
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public enum SentryLogLevel
{
Trace = 1,
@@ -721,7 +709,6 @@ namespace Sentry
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SentryOptions.SentryExperimentalOptions Experimental { get; set; }
public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; }
public System.Collections.Generic.IList FailedRequestTargets { get; set; }
@@ -808,7 +795,6 @@ namespace Sentry
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public sealed class SentryExperimentalOptions
{
public bool EnableLogs { get; set; }
@@ -845,6 +831,7 @@ namespace Sentry
{
public static bool IsEnabled { get; }
public static Sentry.SentryId LastEventId { get; }
+ public static Sentry.SentryStructuredLogger Logger { get; }
public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.SentryHint? hint = null) { }
public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
@@ -905,11 +892,6 @@ namespace Sentry
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { }
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { }
public static void UnsetTag(string key) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
- public static class Experimental
- {
- public static Sentry.SentryStructuredLogger Logger { get; }
- }
}
public class SentrySession : Sentry.ISentrySession
{
@@ -989,34 +971,21 @@ namespace Sentry
public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { }
public static Sentry.SentryStackTrace FromJson(System.Text.Json.JsonElement json) { }
}
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public abstract class SentryStructuredLogger
{
protected abstract void CaptureLog(Sentry.SentryLog log);
protected abstract void Flush();
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogDebug(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogDebug(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogError(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogError(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogFatal(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogFatal(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogInfo(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogInfo(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogTrace(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogTrace(System.Action configureLog, string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogWarning(string template, params object[] parameters) { }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public void LogWarning(System.Action configureLog, string template, params object[] parameters) { }
}
public sealed class SentryThread : Sentry.ISentryJsonSerializable
@@ -1411,7 +1380,6 @@ namespace Sentry.Extensibility
public static readonly Sentry.Extensibility.DisabledHub Instance;
public bool IsEnabled { get; }
public Sentry.SentryId LastEventId { get; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SentryStructuredLogger Logger { get; }
public void BindClient(Sentry.ISentryClient client) { }
public void BindException(System.Exception exception, Sentry.ISpan span) { }
@@ -1459,7 +1427,6 @@ namespace Sentry.Extensibility
public static readonly Sentry.Extensibility.HubAdapter Instance;
public bool IsEnabled { get; }
public Sentry.SentryId LastEventId { get; }
- [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public Sentry.SentryStructuredLogger Logger { get; }
public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index 6999d24318..e2a02fc89a 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -807,6 +807,7 @@ namespace Sentry
{
public static bool IsEnabled { get; }
public static Sentry.SentryId LastEventId { get; }
+ public static Sentry.SentryStructuredLogger Logger { get; }
public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.SentryHint? hint = null) { }
public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { }
@@ -867,10 +868,6 @@ namespace Sentry
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { }
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { }
public static void UnsetTag(string key) { }
- public static class Experimental
- {
- public static Sentry.SentryStructuredLogger Logger { get; }
- }
}
public class SentrySession : Sentry.ISentrySession
{
From 16a1cacc6184c3b78e476264f9c6a32b5be56fc2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 1 Oct 2025 11:41:30 +0200
Subject: [PATCH 17/28] chore: update scripts/update-cli.ps1 to 2.56.0 (#4577)
Co-authored-by: GitHub
---
CHANGELOG.md | 6 +++---
Directory.Build.props | 2 +-
src/Sentry/Sentry.csproj | 14 +++++++-------
3 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7ae408c4e..23b0026ba5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,9 +26,9 @@
- Bump Native SDK from v0.11.0 to v0.11.1 ([#4557](https://github.com/getsentry/sentry-dotnet/pull/4557))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0111)
- [diff](https://github.com/getsentry/sentry-native/compare/0.11.0...0.11.1)
-- Bump CLI from v2.54.0 to v2.55.0 ([#4556](https://github.com/getsentry/sentry-dotnet/pull/4556))
- - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2550)
- - [diff](https://github.com/getsentry/sentry-cli/compare/2.54.0...2.55.0)
+- Bump CLI from v2.54.0 to v2.56.0 ([#4556](https://github.com/getsentry/sentry-dotnet/pull/4556), [#4577](https://github.com/getsentry/sentry-dotnet/pull/4577))
+ - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2560)
+ - [diff](https://github.com/getsentry/sentry-cli/compare/2.54.0...2.56.0)
### Dependencies
diff --git a/Directory.Build.props b/Directory.Build.props
index 1f88fcac2b..e949cc93d5 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -86,7 +86,7 @@
- 2.55.0
+ 2.56.0
$(MSBuildThisFileDirectory)tools\sentry-cli\$(SentryCLIVersion)\
diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj
index 72fcabcb4e..0384a1cebd 100644
--- a/src/Sentry/Sentry.csproj
+++ b/src/Sentry/Sentry.csproj
@@ -113,13 +113,13 @@
<_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
-
-
-
-
-
-
-
+
+
+
+
+
+
+
From c4fe48f7d50ab2d6927fda8fcb3ea75acc19561f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20P=C3=B6lz?=
<38893694+Flash0ver@users.noreply.github.com>
Date: Wed, 1 Oct 2025 13:50:18 +0200
Subject: [PATCH 18/28] docs: fix CHANGELOG format (#4580)
---
CHANGELOG.md | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23b0026ba5..ddcbeba2b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,9 +29,6 @@
- Bump CLI from v2.54.0 to v2.56.0 ([#4556](https://github.com/getsentry/sentry-dotnet/pull/4556), [#4577](https://github.com/getsentry/sentry-dotnet/pull/4577))
- [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2560)
- [diff](https://github.com/getsentry/sentry-cli/compare/2.54.0...2.56.0)
-
-### Dependencies
-
- Bump Java SDK from v8.21.1 to v8.22.0 ([#4552](https://github.com/getsentry/sentry-dotnet/pull/4552))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8220)
- [diff](https://github.com/getsentry/sentry-java/compare/8.21.1...8.22.0)
@@ -78,8 +75,8 @@
### Dependencies
- Bump sentry-cocoa from 8.39.0 to 8.55.1 ([#4442](https://github.com/getsentry/sentry-dotnet/pull/4442), [#4483](https://github.com/getsentry/sentry-dotnet/pull/4483), [#4485](https://github.com/getsentry/sentry-dotnet/pull/4485))
- - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8551)
- - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.39.0...8.55.1)
+ - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8551)
+ - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.39.0...8.55.1)
- Bump Native SDK from v0.9.1 to v0.10.1 ([#4436](https://github.com/getsentry/sentry-dotnet/pull/4436), [#4492](https://github.com/getsentry/sentry-dotnet/pull/4492))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0101)
- [diff](https://github.com/getsentry/sentry-native/compare/0.9.1...0.10.1)
From 20e6136c15fdd9cff3291c25f49c2bb21d744824 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20P=C3=B6lz?=
<38893694+Flash0ver@users.noreply.github.com>
Date: Wed, 1 Oct 2025 20:33:18 +0200
Subject: [PATCH 19/28] fix(logs): Structured Logs do not send ParentSpanId
when no Span was active (#4565)
---
CHANGELOG.md | 1 +
.../SentryStructuredLogger.cs | 6 +--
src/Sentry.Serilog/SentrySink.Structured.cs | 24 +--------
.../Internal/DefaultSentryStructuredLogger.cs | 6 +--
src/Sentry/SentryLog.cs | 26 ++++++++++
.../SentryStructuredLoggerTests.cs | 19 ++++---
.../SentrySinkTests.Structured.cs | 2 +-
test/Sentry.Tests/SentryLogTests.cs | 52 +++++++++++++++++++
.../SentryStructuredLoggerTests.cs | 21 +++++---
9 files changed, 112 insertions(+), 45 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddcbeba2b2..4097dd177f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
### Fixes
- Templates are no longer sent with Structured Logs that have no parameters ([#4544](https://github.com/getsentry/sentry-dotnet/pull/4544))
+- Parent-Span-IDs are no longer sent with Structured Logs when recorded without an active Span ([#4565](https://github.com/getsentry/sentry-dotnet/pull/4565))
- Upload linked PDBs to fix non-IL-stripped symbolication for iOS ([#4527](https://github.com/getsentry/sentry-dotnet/pull/4527))
- In MAUI Android apps, generate and inject UUID to APK and upload ProGuard mapping to Sentry with the UUID ([#4532](https://github.com/getsentry/sentry-dotnet/pull/4532))
- Fixed WASM0001 warning when building Blazor WebAssembly projects ([#4519](https://github.com/getsentry/sentry-dotnet/pull/4519))
diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs
index 36e68454a6..23f549e0d0 100644
--- a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs
+++ b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs
@@ -42,7 +42,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
}
var timestamp = _clock.GetUtcNow();
- var traceHeader = _hub.GetTraceHeader() ?? SentryTraceHeader.Empty;
+ SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId);
var level = logLevel.ToSentryLogLevel();
Debug.Assert(level != default);
@@ -81,11 +81,11 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
}
}
- SentryLog log = new(timestamp, traceHeader.TraceId, level, message)
+ SentryLog log = new(timestamp, traceId, level, message)
{
Template = template,
Parameters = parameters.DrainToImmutable(),
- ParentSpanId = traceHeader.SpanId,
+ ParentSpanId = spanId,
};
log.SetDefaultAttributes(_options, _sdk);
diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs
index 6584afb934..5b22ab7d97 100644
--- a/src/Sentry.Serilog/SentrySink.Structured.cs
+++ b/src/Sentry.Serilog/SentrySink.Structured.cs
@@ -7,7 +7,7 @@ internal sealed partial class SentrySink
{
private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEvent logEvent, string formatted, string? template)
{
- GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
+ SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes);
SentryLog log = new(logEvent.Timestamp, traceId, logEvent.Level.ToSentryLogLevel(), formatted)
@@ -27,28 +27,6 @@ private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEve
hub.Logger.CaptureLog(log);
}
- private static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId)
- {
- var span = hub.GetSpan();
- if (span is not null)
- {
- traceId = span.TraceId;
- spanId = span.SpanId;
- return;
- }
-
- var scope = hub.GetScope();
- if (scope is not null)
- {
- traceId = scope.PropagationContext.TraceId;
- spanId = scope.PropagationContext.SpanId;
- return;
- }
-
- traceId = SentryId.Empty;
- spanId = null;
- }
-
private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray> parameters, out List> attributes)
{
var propertyNames = new HashSet();
diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
index 42d61705f1..1f13191ed2 100644
--- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
+++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
@@ -27,7 +27,7 @@ internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemC
private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action? configureLog)
{
var timestamp = _clock.GetUtcNow();
- var traceHeader = _hub.GetTraceHeader() ?? SentryTraceHeader.Empty;
+ SentryLog.GetTraceIdAndSpanId(_hub, out var traceId, out var spanId);
string message;
try
@@ -51,11 +51,11 @@ private protected override void CaptureLog(SentryLogLevel level, string template
@params = builder.DrainToImmutable();
}
- SentryLog log = new(timestamp, traceHeader.TraceId, level, message)
+ SentryLog log = new(timestamp, traceId, level, message)
{
Template = template,
Parameters = @params,
- ParentSpanId = traceHeader.SpanId,
+ ParentSpanId = spanId,
};
try
diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs
index 32fe2716e1..844d71a778 100644
--- a/src/Sentry/SentryLog.cs
+++ b/src/Sentry/SentryLog.cs
@@ -1,4 +1,5 @@
using Sentry.Extensibility;
+using Sentry.Internal;
using Sentry.Protocol;
namespace Sentry;
@@ -243,4 +244,29 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
writer.WriteEndObject();
}
+
+ internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId)
+ {
+ var activeSpan = hub.GetSpan();
+ if (activeSpan is not null)
+ {
+ traceId = activeSpan.TraceId;
+ spanId = activeSpan.SpanId;
+ return;
+ }
+
+ // set "sentry.trace.parent_span_id" to the ID of the Span that was active when the Log was collected
+ // do not set "sentry.trace.parent_span_id" if there was no active Span
+ spanId = null;
+
+ var scope = hub.GetScope();
+ if (scope is not null)
+ {
+ traceId = scope.PropagationContext.TraceId;
+ return;
+ }
+
+ Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable.");
+ traceId = SentryId.Empty;
+ }
}
diff --git a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs
index dcf7e0a4c5..f810fd9d14 100644
--- a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs
+++ b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs
@@ -51,10 +51,12 @@ public Fixture()
public void EnableLogs(bool isEnabled) => Options.Value.Experimental.EnableLogs = isEnabled;
public void SetMinimumLogLevel(LogLevel logLevel) => Options.Value.ExperimentalLogging.MinimumLogLevel = logLevel;
- public void WithTraceHeader(SentryId traceId, SpanId parentSpanId)
+ public void WithActiveSpan(SentryId traceId, SpanId parentSpanId)
{
- var traceHeader = new SentryTraceHeader(traceId, parentSpanId, null);
- Hub.GetTraceHeader().Returns(traceHeader);
+ var span = Substitute.For();
+ span.TraceId.Returns(traceId);
+ span.SpanId.Returns(parentSpanId);
+ Hub.GetSpan().Returns(span);
}
public SentryStructuredLogger GetSut()
@@ -83,7 +85,7 @@ public void Log_LogLevel_CaptureLog(LogLevel logLevel, SentryLogLevel expectedLe
{
var traceId = SentryId.Create();
var parentSpanId = SpanId.Create();
- _fixture.WithTraceHeader(traceId, parentSpanId);
+ _fixture.WithActiveSpan(traceId, parentSpanId);
var logger = _fixture.GetSut();
EventId eventId = new(123, "EventName");
@@ -127,15 +129,18 @@ public void Log_LogLevelNone_DoesNotCaptureLog()
}
[Fact]
- public void Log_WithoutTraceHeader_CaptureLog()
+ public void Log_WithoutActiveSpan_CaptureLog()
{
+ var scope = new Scope(_fixture.Options.Value);
+ _fixture.Hub.GetSpan().Returns((ISpan?)null);
+ _fixture.Hub.SubstituteConfigureScope(scope);
var logger = _fixture.GetSut();
logger.Log(LogLevel.Information, new EventId(123, "EventName"), new InvalidOperationException("message"), "Message with {Argument}.", "argument");
var log = _fixture.CapturedLogs.Dequeue();
- log.TraceId.Should().Be(SentryTraceHeader.Empty.TraceId);
- log.ParentSpanId.Should().Be(SentryTraceHeader.Empty.SpanId);
+ log.TraceId.Should().Be(scope.PropagationContext.TraceId);
+ log.ParentSpanId.Should().BeNull();
}
[Fact]
diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs
index b7cb36b76f..7b98e8181c 100644
--- a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs
+++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs
@@ -112,7 +112,7 @@ public void Emit_StructuredLogging_LogEvent(bool withActiveSpan)
log.Parameters[1].Should().BeEquivalentTo(new KeyValuePair("Sequence", "[41, 42, 43]"));
log.Parameters[2].Should().BeEquivalentTo(new KeyValuePair("Dictionary", """[("key": "value")]"""));
log.Parameters[3].Should().BeEquivalentTo(new KeyValuePair("Structure", """[42, "42"]"""));
- log.ParentSpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : _fixture.Scope.PropagationContext.SpanId);
+ log.ParentSpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : null);
log.TryGetAttribute("sentry.environment", out object? environment).Should().BeTrue();
environment.Should().Be("test-environment");
diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs
index 105e196d56..c53c6711de 100644
--- a/test/Sentry.Tests/SentryLogTests.cs
+++ b/test/Sentry.Tests/SentryLogTests.cs
@@ -406,6 +406,58 @@ public void WriteTo_Attributes_AsJson()
entry => entry.Message.Should().Match("*null*is not supported*ignored*")
);
}
+
+ [Fact]
+ public void GetTraceIdAndSpanId_WithActiveSpan_HasBothTraceIdAndSpanId()
+ {
+ // Arrange
+ var span = Substitute.For();
+ span.TraceId.Returns(SentryId.Create());
+ span.SpanId.Returns(SpanId.Create());
+
+ var hub = Substitute.For();
+ hub.GetSpan().Returns(span);
+
+ // Act
+ SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
+
+ // Assert
+ traceId.Should().Be(span.TraceId);
+ spanId.Should().Be(span.SpanId);
+ }
+
+ [Fact]
+ public void GetTraceIdAndSpanId_WithoutActiveSpan_HasOnlyTraceIdButNoSpanId()
+ {
+ // Arrange
+ var hub = Substitute.For();
+ hub.GetSpan().Returns((ISpan)null);
+
+ var scope = new Scope();
+ hub.SubstituteConfigureScope(scope);
+
+ // Act
+ SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
+
+ // Assert
+ traceId.Should().Be(scope.PropagationContext.TraceId);
+ spanId.Should().BeNull();
+ }
+
+ [Fact]
+ public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable()
+ {
+ // Arrange
+ var hub = Substitute.For();
+ hub.GetSpan().Returns((ISpan)null);
+
+ // Act
+ SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
+
+ // Assert
+ traceId.Should().Be(SentryId.Empty);
+ spanId.Should().BeNull();
+ }
}
file static class AssertExtensions
diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs
index b0a2e6e3a5..bee10461ff 100644
--- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs
+++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs
@@ -26,8 +26,10 @@ public Fixture()
Hub.IsEnabled.Returns(true);
- var traceHeader = new SentryTraceHeader(TraceId, ParentSpanId.Value, null);
- Hub.GetTraceHeader().Returns(traceHeader);
+ var span = Substitute.For();
+ span.TraceId.Returns(TraceId);
+ span.SpanId.Returns(ParentSpanId.Value);
+ Hub.GetSpan().Returns(span);
ExpectedAttributes = new Dictionary(1)
{
@@ -46,11 +48,14 @@ public Fixture()
public Dictionary ExpectedAttributes { get; }
- public void WithoutTraceHeader()
+ public void WithoutActiveSpan()
{
- Hub.GetTraceHeader().Returns((SentryTraceHeader?)null);
- TraceId = SentryId.Empty;
- ParentSpanId = SpanId.Empty;
+ Hub.GetSpan().Returns((ISpan?)null);
+
+ var scope = new Scope();
+ Hub.SubstituteConfigureScope(scope);
+ TraceId = scope.PropagationContext.TraceId;
+ ParentSpanId = null;
}
public SentryStructuredLogger GetSut() => SentryStructuredLogger.Create(Hub, Options, Clock, BatchSize, BatchTimeout);
@@ -93,9 +98,9 @@ public void Create_Disabled_CachedDisabledInstance()
}
[Fact]
- public void Log_WithoutTraceHeader_CapturesEnvelope()
+ public void Log_WithoutActiveSpan_CapturesEnvelope()
{
- _fixture.WithoutTraceHeader();
+ _fixture.WithoutActiveSpan();
_fixture.Options.Experimental.EnableLogs = true;
var logger = _fixture.GetSut();
From 0a552fcb1b6de8dfa9145cd682dc6a23a97a1309 Mon Sep 17 00:00:00 2001
From: getsentry-bot
Date: Wed, 1 Oct 2025 19:05:05 +0000
Subject: [PATCH 20/28] release: 5.16.0
---
CHANGELOG.md | 2 +-
Directory.Build.props | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4097dd177f..e679fb62d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## Unreleased
+## 5.16.0
### Features
diff --git a/Directory.Build.props b/Directory.Build.props
index e949cc93d5..c6bbd4cc89 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 5.15.1
+ 5.16.0
13
true
true
From 44b7cdfbbb08c0c992cec274e4f39abd613811f8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 2 Oct 2025 16:18:23 +1300
Subject: [PATCH 21/28] build(deps): bump github/codeql-action from 3.30.3 to
3.30.5 (#4573)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* build(deps): bump github/codeql-action from 3.30.3 to 3.30.5
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.3 to 3.30.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/192325c86100d080feab897ff886c34abd4c83a3...3599b3baa15b485a2e49ef411a7a4bb2452e7f93)
---
updated-dependencies:
- dependency-name: github/codeql-action
dependency-version: 3.30.5
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
* Update .github/workflows/codeql-analysis.yml
Co-authored-by: Stefan Pölz <38893694+Flash0ver@users.noreply.github.com>
* Update .github/workflows/codeql-analysis.yml
Co-authored-by: Stefan Pölz <38893694+Flash0ver@users.noreply.github.com>
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Crosswell
Co-authored-by: Stefan Pölz <38893694+Flash0ver@users.noreply.github.com>
---
.github/workflows/codeql-analysis.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 225ac38054..79322f0dec 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -35,7 +35,7 @@ jobs:
uses: ./.github/actions/environment
- name: Initialize CodeQL
- uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # pin@v2
+ uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
languages: csharp
@@ -49,6 +49,6 @@ jobs:
run: dotnet build Sentry-CI-CodeQL.slnf --no-restore --nologo
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # pin@v2
+ uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
category: '/language:csharp'
From c8da336fefa40e21f35b055e4af9d2d81e6281e5 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 2 Oct 2025 21:50:59 +1300
Subject: [PATCH 22/28] chore: update scripts/update-java.ps1 to 8.23.0 (#4586)
Co-authored-by: GitHub
---
CHANGELOG.md | 8 ++++++++
.../Sentry.Bindings.Android.csproj | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e679fb62d2..fbe102adb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## Unreleased
+
+### Dependencies
+
+- Bump Java SDK from v8.22.0 to v8.23.0 ([#4586](https://github.com/getsentry/sentry-dotnet/pull/4586))
+ - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8230)
+ - [diff](https://github.com/getsentry/sentry-java/compare/8.22.0...8.23.0)
+
## 5.16.0
### Features
diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj
index dab642dc24..5db9b21244 100644
--- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj
+++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj
@@ -1,7 +1,7 @@
net8.0-android34.0;net9.0-android35.0
- 8.22.0
+ 8.23.0
$(BaseIntermediateOutputPath)sdks\$(TargetFramework)\Sentry\Android\$(SentryAndroidSdkVersion)\
From 93a6689c8fcb436bbe6f6c7f10e4ca21a7c73a5a Mon Sep 17 00:00:00 2001
From: J-P Nurmi
Date: Thu, 2 Oct 2025 21:49:31 +0200
Subject: [PATCH 23/28] build: allow local `modules/sentry-cocoa` clone for
development (#4551)
---
CONTRIBUTING.md | 23 +++++++++++++
scripts/build-sentry-cocoa.sh | 4 ++-
scripts/generate-cocoa-bindings.ps1 | 22 ++++++++++---
.../Sentry.Bindings.Cocoa.csproj | 33 +++++++++++++++----
4 files changed, 70 insertions(+), 12 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 78d0b1ab7a..cf6582a782 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -171,3 +171,26 @@ Once changes to Ben.Demystifier have been merged into the main branch then, the
should be updated from the main branch and the `modules/make-internal.sh` script run again (if necessary). This repo
should reference the most recent commit on the `internal` branch of Ben.Demystifier then (functionally identical to the
main branch - the only difference being the changes to member visibility).
+
+## Local Sentry Cocoa SDK checkout
+
+By default, `Sentry.Bindings.Cocoa` downloads a pre-built Sentry Cocoa SDK from
+GitHub Releases. The version is specified in `modules/sentry-cocoa.properties`.
+
+If you want to build an unreleased Sentry Cocoa SDK version from source instead,
+replace the pre-built SDK with [getsentry/sentry-cocoa](https://github.com/getsentry/sentry-cocoa/)
+by cloning it into the `modules/sentry-cocoa` directory:
+
+```sh
+$ rm -rf modules/sentry-cocoa
+$ gh repo clone getsentry/sentry-cocoa modules/sentry-cocoa
+$ dotnet build ... # uses modules/sentry-cocoa as is
+```
+
+To switch back to the pre-built SDK, delete the `modules/sentry-cocoa` directory
+and let the next build download the pre-built SDK again:
+
+```sh
+$ rm -rf modules/sentry-cocoa
+$ dotnet build ... # downloads pre-built Cocoa SDK into modules/sentry-cocoa
+```
diff --git a/scripts/build-sentry-cocoa.sh b/scripts/build-sentry-cocoa.sh
index 95247e281f..55dbdbde6e 100755
--- a/scripts/build-sentry-cocoa.sh
+++ b/scripts/build-sentry-cocoa.sh
@@ -25,6 +25,7 @@ xcodebuild archive -project Sentry.xcodeproj \
-archivePath ./Carthage/output-ios.xcarchive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
+./scripts/remove-architectures.sh ./Carthage/output-ios.xcarchive arm64e
xcodebuild archive -project Sentry.xcodeproj \
-scheme Sentry \
-configuration Release \
@@ -47,6 +48,7 @@ xcodebuild archive -project Sentry.xcodeproj \
-archivePath ./Carthage/output-maccatalyst.xcarchive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
+./scripts/remove-architectures.sh ./Carthage/output-maccatalyst.xcarchive arm64e
xcodebuild -create-xcframework \
-framework ./Carthage/output-maccatalyst.xcarchive/Products/Library/Frameworks/Sentry.framework \
-output ./Carthage/Build-maccatalyst/Sentry.xcframework
@@ -60,7 +62,7 @@ find Carthage/Build-ios/Sentry.xcframework/ios-arm64 -name '*.h' -exec cp {} Car
find Carthage/Build* \( -name Headers -o -name PrivateHeaders -o -name Modules \) -exec rm -rf {} +
rm -rf Carthage/output-*
-cp ../../.git/modules/modules/sentry-cocoa/HEAD Carthage/.built-from-sha
+cp .git/HEAD Carthage/.built-from-sha
echo ""
popd >/dev/null
diff --git a/scripts/generate-cocoa-bindings.ps1 b/scripts/generate-cocoa-bindings.ps1
index eb1295808e..f91b32f4c7 100644
--- a/scripts/generate-cocoa-bindings.ps1
+++ b/scripts/generate-cocoa-bindings.ps1
@@ -4,7 +4,19 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$RootPath = (Get-Item $PSScriptRoot).Parent.FullName
-$CocoaSdkPath = "$RootPath/modules/sentry-cocoa/Sentry.framework"
+$CocoaSdkPath = "$RootPath/modules/sentry-cocoa"
+if (Test-Path "$CocoaSdkPath/.git")
+{
+ # Cocoa SDK cloned to modules/sentry-cocoa for local development
+ $HeadersPath = "$CocoaSdkPath/Carthage/Headers"
+ $PrivateHeadersPath = "$CocoaSdkPath/Carthage/Headers"
+}
+else
+{
+ # Cocoa SDK downloaded from GitHub releases and extracted into modules/sentry-cocoa
+ $HeadersPath = "$CocoaSdkPath/Sentry.framework/Headers"
+ $PrivateHeadersPath = "$CocoaSdkPath/Sentry.framework/PrivateHeaders"
+}
$BindingsPath = "$RootPath/src/Sentry.Bindings.Cocoa"
$BackupPath = "$BindingsPath/obj/_unpatched"
@@ -101,7 +113,7 @@ Write-Output "iPhoneSdkVersion: $iPhoneSdkVersion"
# ...instead of:
# `#import "SomeHeader.h"`
# This causes sharpie to fail resolve those headers
-$filesToPatch = Get-ChildItem -Path "$CocoaSdkPath/Headers" -Filter *.h -Recurse | Select-Object -ExpandProperty FullName
+$filesToPatch = Get-ChildItem -Path "$HeadersPath" -Filter *.h -Recurse | Select-Object -ExpandProperty FullName
foreach ($file in $filesToPatch)
{
if (Test-Path $file)
@@ -116,7 +128,7 @@ foreach ($file in $filesToPatch)
Write-Host "File not found: $file"
}
}
-$privateHeaderFile = "$CocoaSdkPath/PrivateHeaders/PrivatesHeader.h"
+$privateHeaderFile = "$PrivateHeadersPath/PrivatesHeader.h"
if (Test-Path $privateHeaderFile)
{
$content = Get-Content -Path $privateHeaderFile -Raw
@@ -134,8 +146,8 @@ else
Write-Output 'Generating bindings with Objective Sharpie.'
sharpie bind -sdk $iPhoneSdkVersion `
-scope "$CocoaSdkPath" `
- "$CocoaSdkPath/Headers/Sentry.h" `
- "$CocoaSdkPath/PrivateHeaders/PrivateSentrySDKOnly.h" `
+ "$HeadersPath/Sentry.h" `
+ "$PrivateHeadersPath/PrivateSentrySDKOnly.h" `
-o $BindingsPath `
-c -Wno-objc-property-no-attribute
diff --git a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj
index 98e6fde2d2..860b0f2db4 100644
--- a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj
+++ b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj
@@ -12,10 +12,17 @@
$([System.IO.File]::ReadAllText("$(MSBuildThisFileDirectory)../../modules/sentry-cocoa.properties"))
$([System.Text.RegularExpressions.Regex]::Match($(SentryCocoaProperties), 'version\s*=\s*([^\s]+)').Groups[1].Value)
$(SentryCocoaCache)Sentry-$(SentryCocoaVersion).xcframework
+ ../../modules/sentry-cocoa.properties;../../scripts/generate-cocoa-bindings.ps1
$(NoWarn);CS0108
+
+
+ $(SentryCocoaCache)Carthage\Build-$(TargetPlatformIdentifier)\Sentry.xcframework
+ ../../scripts/generate-cocoa-bindings.ps1;../../modules/sentry-cocoa/Carthage/.built-from-sha
+
+
@@ -52,8 +59,8 @@
-
+
@@ -84,14 +91,28 @@
SkipUnchangedFiles="true" />
+
+
+
+
+
+
+
+
+
+
+ Condition="$([MSBuild]::IsOSPlatform('OSX'))">
@@ -102,8 +123,8 @@
From ad849d4aa01a4282f4691d74568b5dfe16cb774e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 3 Oct 2025 21:51:43 +1300
Subject: [PATCH 24/28] chore: update modules/sentry-native to 0.11.2 (#4590)
Co-authored-by: GitHub
---
CHANGELOG.md | 3 +++
modules/sentry-native | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbe102adb8..bcd5653686 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@
- Bump Java SDK from v8.22.0 to v8.23.0 ([#4586](https://github.com/getsentry/sentry-dotnet/pull/4586))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8230)
- [diff](https://github.com/getsentry/sentry-java/compare/8.22.0...8.23.0)
+- Bump Native SDK from v0.11.1 to v0.11.2 ([#4590](https://github.com/getsentry/sentry-dotnet/pull/4590))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0112)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.11.1...0.11.2)
## 5.16.0
diff --git a/modules/sentry-native b/modules/sentry-native
index 075b3bfee1..027459265a 160000
--- a/modules/sentry-native
+++ b/modules/sentry-native
@@ -1 +1 @@
-Subproject commit 075b3bfee1dbb85fa10d50df631286196943a3e0
+Subproject commit 027459265ab94de340a5f59b767248652640d1e6
From 6872adfcf339283c92d45914f514f26b0ecc0db8 Mon Sep 17 00:00:00 2001
From: James Crosswell
Date: Tue, 7 Oct 2025 10:31:44 +1300
Subject: [PATCH 25/28] Remove unnecessary files from SentryCocoaFramework
before packing
Workaround for #4292:
- https://github.com/getsentry/sentry-dotnet/issues/4292#issuecomment-3083566046
Replaces #4533 (targets version6 branch instead of main so that we can get adequate feedback from users before releasing this).
---
.../Sentry.Bindings.Cocoa.csproj | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj
index 26b4539776..0881063924 100644
--- a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj
+++ b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj
@@ -102,7 +102,7 @@
@@ -122,7 +122,7 @@
-
@@ -134,6 +134,13 @@
+
+
+
+
+
+
From d91cf534aec31a9d37dbf3ae6f188183ab9b480f Mon Sep 17 00:00:00 2001
From: James Crosswell
Date: Tue, 7 Oct 2025 10:35:42 +1300
Subject: [PATCH 26/28] Update CHANGELOG.md
---
CHANGELOG.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d867e8fb1..db59f33749 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## Unreleased
+
+- Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602))
+
## 6.0.0-preview.1
## Unreleased
From 76da5844f2e4a580294e7ed44187417335b8b387 Mon Sep 17 00:00:00 2001
From: James Crosswell
Date: Tue, 7 Oct 2025 10:40:57 +1300
Subject: [PATCH 27/28] Update CHANGELOG.md
---
CHANGELOG.md | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db59f33749..bf9d652035 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,18 +5,6 @@
- Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602))
## 6.0.0-preview.1
-## Unreleased
-
-### Dependencies
-
-- Bump Java SDK from v8.22.0 to v8.23.0 ([#4586](https://github.com/getsentry/sentry-dotnet/pull/4586))
- - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8230)
- - [diff](https://github.com/getsentry/sentry-java/compare/8.22.0...8.23.0)
-- Bump Native SDK from v0.11.1 to v0.11.2 ([#4590](https://github.com/getsentry/sentry-dotnet/pull/4590))
- - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0112)
- - [diff](https://github.com/getsentry/sentry-native/compare/0.11.1...0.11.2)
-
-## 5.16.0
### BREAKING CHANGES
From af2af6ea1eb2e5442a1acaff362ea91e794899a8 Mon Sep 17 00:00:00 2001
From: James Crosswell
Date: Wed, 8 Oct 2025 11:00:47 +1300
Subject: [PATCH 28/28] Update cli.Tests.ps1
---
integration-test/cli.Tests.ps1 | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/integration-test/cli.Tests.ps1 b/integration-test/cli.Tests.ps1
index ff7b8a7eee..1217bb7376 100644
--- a/integration-test/cli.Tests.ps1
+++ b/integration-test/cli.Tests.ps1
@@ -182,6 +182,8 @@ Describe 'MAUI ()' -ForEach @(
'Microsoft.Maui.pdb',
'Sentry'
)
- $result.ScriptOutput | Should -AnyElementMatch "Found 77 debug information files \(8 with embedded sources\)"
+ # The specific number of debug information files seems to change with different SDK - so we just check for non-zero
+ $nonZeroNumberRegex = '[1-9][0-9]*';
+ $result.ScriptOutput | Should -AnyElementMatch "Found $nonZeroNumberRegex debug information files \($nonZeroNumberRegex with embedded sources\)"
}
}