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\)" } }