From 6ca8b5d7642830e3861ec23c1f1d94033ed56a18 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 22 Sep 2025 20:14:34 +1200 Subject: [PATCH 01/23] Preparing for v6.0.0 and .NET 10 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141222b598..b89b6f7f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## Version 6.0.0 + ## 5.15.1 ### Fixes From 9e2f5f223c8651891ff9760abb9fd16163f3c820 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 3 Oct 2025 21:58:20 +1300 Subject: [PATCH 02/23] Add support for net10.0-rc.1 (#4461) Initial commit for #4456 - https://github.com/getsentry/sentry-dotnet/issues/4456 --- .github/actions/environment/action.yml | 17 +- .github/workflows/build.yml | 36 +- .github/workflows/device-tests-android.yml | 35 +- .github/workflows/device-tests-ios.yml | 1 + CHANGELOG.md | 34 + Directory.Build.props | 23 +- Sentry.sln | 75 + SentryNoMobile.slnf | 1 - .../BackgroundWorkerFlushBenchmarks.cs | 2 +- .../Sentry.Benchmarks.csproj | 2 +- global.json | 4 +- integration-test/aot.Tests.ps1 | 18 +- integration-test/cli.Tests.ps1 | 29 +- integration-test/common.ps1 | 82 +- integration-test/msbuild.Tests.ps1 | 101 + integration-test/nuget5.config | 11 + integration-test/runtime.Tests.ps1 | 4 +- modules/Ben.Demystifier | 2 +- modules/sentry-cocoa.properties | 2 +- modules/sentry-native | 2 +- .../Sentry.Samples.Android.csproj | 2 +- .../Sentry.Samples.AspNetCore.Basic.csproj | 2 +- ...ry.Samples.AspNetCore.Blazor.Server.csproj | 2 +- ...ntry.Samples.AspNetCore.Blazor.Wasm.csproj | 11 +- .../Sentry.Samples.AspNetCore.Grpc/Program.cs | 108 +- .../Sentry.Samples.AspNetCore.Grpc.csproj | 2 +- .../Sentry.Samples.AspNetCore.Grpc/Startup.cs | 32 - .../Sentry.Samples.AspNetCore.Mvc.csproj | 2 +- .../Sentry.Samples.AspNetCore.Serilog.csproj | 2 +- ...Samples.AspNetCore.WebAPI.Profiling.csproj | 2 +- ...Samples.Aws.Lambda.AspNetCoreServer.csproj | 2 +- ...ntry.Samples.Azure.Functions.Worker.csproj | 2 +- .../Sentry.Samples.Console.Basic.csproj | 4 +- .../Sentry.Samples.Console.Customized.csproj | 2 +- .../Sentry.Samples.Console.HeapDump.csproj | 4 +- .../Sentry.Samples.Console.Native.csproj | 2 +- .../Sentry.Samples.Console.Profiling.csproj | 2 +- .../Sentry.Samples.EntityFramework.csproj | 2 +- .../Sentry.Samples.GenericHost.csproj | 6 +- ...ntry.Samples.Google.Cloud.Functions.csproj | 2 +- .../Sentry.Samples.GraphQL.Client.Http.csproj | 2 +- .../Sentry.Samples.GraphQL.Server.csproj | 2 +- .../Sentry.Samples.Hangfire.csproj | 2 +- .../Sentry.Samples.Ios.csproj | 2 +- .../Sentry.Samples.Log4Net.csproj | 2 +- .../Sentry.Samples.ME.Logging.csproj | 4 +- .../Sentry.Samples.MacCatalyst/AppDelegate.cs | 2 +- .../Sentry.Samples.MacCatalyst.csproj | 4 +- .../Sentry.Samples.MacOS.csproj | 4 +- .../Sentry.Samples.NLog.csproj | 2 +- ...ry.Samples.OpenTelemetry.AspNetCore.csproj | 2 +- ...entry.Samples.OpenTelemetry.Console.csproj | 2 +- samples/Sentry.Samples.Serilog/Program.cs | 2 + .../Sentry.Samples.Serilog.csproj | 2 +- scripts/device-test.ps1 | 4 +- scripts/generate-solution-filters-config.yaml | 7 + scripts/install-libssl1.sh | 22 + src/Directory.Build.props | 2 +- .../Sentry.Android.AssemblyReader.csproj | 2 +- ...entry.AspNetCore.Blazor.WebAssembly.csproj | 11 +- .../Sentry.AspNetCore.Grpc.csproj | 2 +- .../Sentry.AspNetCore.csproj | 13 +- .../Sentry.Azure.Functions.Worker.csproj | 2 +- .../Sentry.Bindings.Android.csproj | 82 +- .../Transforms/Metadata.xml | 3 + src/Sentry.Bindings.Cocoa/ApiDefinitions.cs | 4 +- .../Sentry.Bindings.Cocoa.csproj | 6 +- .../SwiftApiDefinitions.cs | 3 - .../Sentry.Extensions.Logging.csproj | 8 +- .../Sentry.Google.Cloud.Functions.csproj | 9 +- src/Sentry.Hangfire/Sentry.Hangfire.csproj | 2 +- src/Sentry.Maui/Internal/MauiEventsBinder.cs | 16 +- src/Sentry.Maui/Sentry.Maui.csproj | 12 +- src/Sentry.NLog/Sentry.NLog.csproj | 2 +- .../Sentry.OpenTelemetry.csproj | 2 +- src/Sentry.Profiling/Sentry.Profiling.csproj | 2 +- src/Sentry.Serilog/LogLevelExtensions.cs | 14 + src/Sentry.Serilog/Sentry.Serilog.csproj | 2 +- src/Sentry.Serilog/SentrySink.Structured.cs | 126 ++ src/Sentry.Serilog/SentrySink.cs | 70 +- src/Sentry.Serilog/SentrySinkExtensions.cs | 27 +- src/Sentry/AttributeReader.cs | 23 +- 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/Platforms/Native/CFunctions.cs | 23 +- src/Sentry/Sentry.csproj | 39 +- src/Sentry/SentryClient.cs | 22 +- src/Sentry/SentryLog.cs | 1 + src/Sentry/SentryOptions.cs | 6 + src/Sentry/SentrySdk.cs | 6 + src/Sentry/buildTransitive/Sentry.targets | 29 +- test/AndroidTestApp/AndroidTestApp.csproj | 2 +- test/Directory.Build.props | 4 +- .../Sentry.Analyzers.Tests.csproj | 2 +- .../AndroidAssemblyReaderTests.cs | 4 +- ...piApprovalTests.Run.DotNet8_0.verified.txt | 20 - ...Sentry.Android.AssemblyReader.Tests.csproj | 6 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 50 + .../Sentry.AspNetCore.Grpc.Tests.csproj | 13 +- .../Sentry.AspNetCore.TestUtils.csproj | 11 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 83 + .../Sentry.AspNetCore.Tests.csproj | 2 +- ...ightIgnoresTransaction.DotNet.verified.txt | 1 - ...IgnoresTransaction.DotNet10_0.verified.txt | 14 + ...s.Versioning.DotNet.DotNet8_0.verified.txt | 87 - ...nTests.Versioning.DotNet10_0.verified.txt} | 14 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 15 + ...piApprovalTests.Run.DotNet9_0.verified.txt | 15 + ...Sentry.Azure.Functions.Worker.Tests.csproj | 2 +- .../LocalDbFixture.cs | 2 + ...y.DiagnosticSource.IntegrationTests.csproj | 11 +- ...Tests.LoggingAsync.DotNet10_0.verified.txt | 63 + ...sts.RecordsEfAsync.DotNet10_0.verified.txt | 131 ++ .../Sentry.DiagnosticSource.Tests.csproj | 10 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 39 + .../Sentry.EntityFramework.Tests.csproj | 2 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 66 + .../Sentry.Extensions.Logging.Tests.csproj | 8 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 10 + ...Sentry.Google.Cloud.Functions.Tests.csproj | 2 +- .../Sentry.Hangfire.Tests.csproj | 2 +- ...ApprovalTests.Run.DotNet10_0.verified.txt} | 0 ...piApprovalTests.Run.DotNet9_0.verified.txt | 14 - .../Sentry.Log4Net.Tests.csproj | 3 +- .../Sentry.Maui.Device.TestApp.csproj | 2 +- ...ApprovalTests.Run.DotNet10_0.verified.txt} | 13 +- .../MauiEventsBinderTests.Page.cs | 31 +- .../Sentry.Maui.Tests.csproj | 8 +- ...ryMauiAppBuilderExtensionsTests.Android.cs | 13 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 81 + ...rationTests.Simple.DotNet10_0.verified.txt | 109 + .../Sentry.NLog.Tests.csproj | 2 +- .../Sentry.OpenTelemetry.Tests.csproj | 10 +- .../SamplingTransactionProfilerTests.cs | 2 + .../Sentry.Profiling.Tests.csproj | 2 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 52 + ...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.Simple.DotNet10_0.verified.txt | 281 +++ ...rationTests.StructuredLogging.verified.txt | 70 + .../IntegrationTests.verify.cs | 40 + .../Sentry.Serilog.Tests.csproj | 2 +- .../SentrySerilogSinkExtensionsTests.cs | 6 +- .../SentrySinkTests.Structured.cs | 135 ++ test/Sentry.Serilog.Tests/SentrySinkTests.cs | 3 +- .../SerilogAspNetSentrySdkTestFixture.cs | 10 +- .../Sentry.SourceGenerators.Tests.csproj | 2 +- .../Sentry.Testing.CrashableApp.csproj | 2 +- .../InMemorySentryStructuredLogger.cs | 3 +- test/Sentry.Testing/RecordingTransport.cs | 2 + test/Sentry.Testing/Sentry.Testing.csproj | 8 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 1978 +++++++++++++++++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 2 + ...piApprovalTests.Run.DotNet9_0.verified.txt | 2 + .../ApiApprovalTests.Run.Net4_8.verified.txt | 2 + ...tionEndedAsCrashed.DotNet10_0.verified.txt | 190 ++ test/Sentry.Tests/HubTests.cs | 99 +- .../Internals/BackgroundWorkerTests.cs | 37 +- .../Internals/BackpressureMonitorTests.cs | 163 ++ .../Internals/Http/HttpTransportTests.cs | 51 +- .../Internals/ILSpy/SingleFileAppTests.cs | 10 +- ...yInfoTests.WriteTo.DotNet10_0.verified.txt | 19 + ...lize_Counter_statsd.DotNet8_0.verified.txt | 1 - test/Sentry.Tests/Sentry.Tests.csproj | 8 +- test/Sentry.Tests/SentryClientTests.cs | 68 +- ..._registered.DotNet10_0.DotNet.verified.txt | 38 + ...red.DotNet10_0.Windows.DotNet.verified.txt | 44 + .../SingleFileTestApp.csproj | 2 +- 179 files changed, 5318 insertions(+), 636 deletions(-) create mode 100644 integration-test/msbuild.Tests.ps1 create mode 100644 integration-test/nuget5.config delete mode 100644 samples/Sentry.Samples.AspNetCore.Grpc/Startup.cs create mode 100755 scripts/install-libssl1.sh create mode 100644 src/Sentry.Serilog/SentrySink.Structured.cs create mode 100644 src/Sentry/Internal/BackpressureMonitor.cs create mode 100644 src/Sentry/Internal/BackpressureMonitorExtensions.cs delete mode 100644 test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt create mode 100644 test/Sentry.AspNetCore.Grpc.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt delete mode 100644 test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet.verified.txt create mode 100644 test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet10_0.verified.txt delete mode 100644 test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet.DotNet8_0.verified.txt rename test/Sentry.AspNetCore.Tests/{WebIntegrationTests.Versioning.verified.txt => WebIntegrationTests.Versioning.DotNet10_0.verified.txt} (94%) create mode 100644 test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt create mode 100644 test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet10_0.verified.txt create mode 100644 test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt create mode 100644 test/Sentry.EntityFramework.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.Google.Cloud.Functions.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt rename test/Sentry.Log4Net.Tests/{ApiApprovalTests.Run.DotNet8_0.verified.txt => ApiApprovalTests.Run.DotNet10_0.verified.txt} (100%) delete mode 100644 test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt rename test/Sentry.Maui.Tests/{ApiApprovalTests.Run.DotNet8_0.verified.txt => ApiApprovalTests.Run.DotNet10_0.verified.txt} (74%) create mode 100644 test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt create mode 100644 test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt create mode 100644 test/Sentry.Serilog.Tests/IntegrationTests.StructuredLogging.verified.txt create mode 100644 test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs create mode 100644 test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt create mode 100644 test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt create mode 100644 test/Sentry.Tests/Internals/BackpressureMonitorTests.cs create mode 100644 test/Sentry.Tests/Internals/MemoryInfoTests.WriteTo.DotNet10_0.verified.txt delete mode 100644 test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt diff --git a/.github/actions/environment/action.yml b/.github/actions/environment/action.yml index 9e451a282e..a646c021f8 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 @@ -56,7 +62,7 @@ runs: uses: android-actions/setup-android@07976c6290703d34c16d382cb36445f98bb43b1f # v3.2.0 with: # Exclude "tools" because the emulator is not needed (nor available for Windows/Linux ARM64) - packages: platform-tools platforms;android-34 platforms;android-35 build-tools;36.0.0 + packages: platform-tools platforms;android-35 platforms;android-36 build-tools;36.0.0 log-accepted-android-sdk-licenses: false # Java 11 is needed by .NET Android @@ -90,6 +96,15 @@ runs: dotnet-version: | 8.0.x 9.0.304 + 10.0.100-rc.1.25451.107 + global-json-file: global.json + + # .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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bb59a1678..435e61fd07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,15 +64,11 @@ 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 - build: + build-sentry: needs: build-sentry-native name: .NET (${{ matrix.rid }}) runs-on: ${{ matrix.os }} @@ -254,7 +250,8 @@ jobs: uses: actions/checkout@v5 with: # We only check out what is absolutely necessary to reduce a chance of local files impacting - # integration tests, e.g. Directory.Build.props, nuget.config, ... + # integration tests (nuget.config etc.)... But we need the root Directory.Build.props calculate + # the package version sparse-checkout: | integration-test .github @@ -267,11 +264,23 @@ jobs: path: src - name: Integration test + if: ${{ (matrix.rid != 'linux-musl-x64') && (matrix.rid != 'linux-musl-arm64') }} + uses: getsentry/github-workflows/sentry-cli/integration-test/@a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1 + with: + path: integration-test + + # For the linux-musl runtimes we have to pin the ContainerBaseImage for preview or RC builds, since + # these don't conform to the normal naming conventions and don't get resolved automatically. We do + # by passing it as parameter to aot.Tests.ps1 via an environment variable + - name: Integration test (musl) + if: ${{ (matrix.rid == 'linux-musl-x64') || (matrix.rid == 'linux-musl-arm64') }} uses: getsentry/github-workflows/sentry-cli/integration-test/@v2 + env: + ContainerBaseImage: 'mcr.microsoft.com/dotnet/nightly/runtime-deps:10.0-preview-alpine3.22' with: path: integration-test - msbuild: + ms-build: needs: build-sentry-native name: MSBuild runs-on: windows-latest @@ -301,7 +310,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() }} @@ -316,7 +330,7 @@ jobs: # Unsupported Native AOT runtimes should have SentryNative auto-disabled # to avoid native library loading errors on startup. unsupported-aot: - needs: build + needs: build-sentry name: Unsupported AOT (${{ matrix.rid }}) runs-on: ${{ matrix.os }} @@ -348,7 +362,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: @@ -384,7 +398,7 @@ jobs: uses: ./.github/actions/buildnative - name: Publish Test app (macOS) - run: dotnet publish test/Sentry.TrimTest/Sentry.TrimTest.csproj -c Release -r osx-arm64 + run: dotnet publish test/Sentry.TrimTest/Sentry.TrimTest.csproj -c Release -r osx-arm64 -p:NO_MOBILE=true - name: Publish Test app (Android) run: dotnet publish test/Sentry.MauiTrimTest/Sentry.MauiTrimTest.csproj -c Release -f net9.0-android35.0 -r android-arm64 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/CHANGELOG.md b/CHANGELOG.md index b89b6f7f46..b32853db5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ ## Version 6.0.0 +### BREAKING CHANGES + +- This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) +## Unreleased + +### 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 + +- 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)) + +### 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)) + - [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) +- 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) + +### 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/Directory.Build.props b/Directory.Build.props index a74738d5cd..e32b83eed7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,8 @@ - 5.15.1 + 6.0.0 + prerelease 13 true true @@ -20,11 +21,27 @@ $(NoWarn);NU1902;NU1903 + + $(NoWarn);XCODE_26_0_PREVIEW + NU1902;NU1903 + + + net10.0 + net9.0 + net8.0 + net8.0;net9.0;net10.0 + net10.0-android36.0 + net9.0-android35.0 + net9.0-ios26 + net9.0-ios18.0 + net9.0-maccatalyst26 + net9.0-maccatalyst18.0 + net9.0-windows10.0.19041.0 - + dev @@ -86,7 +103,7 @@ - 2.54.0 + 2.55.0 $(MSBuildThisFileDirectory)tools\sentry-cli\$(SentryCLIVersion)\ diff --git a/Sentry.sln b/Sentry.sln index 3730377a46..e8b04afa38 100644 --- a/Sentry.sln +++ b/Sentry.sln @@ -209,6 +209,74 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.SourceGenerators.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Maui.CommunityToolkit.Mvvm.Tests", "test\Sentry.Maui.CommunityToolkit.Mvvm.Tests\Sentry.Maui.CommunityToolkit.Mvvm.Tests.csproj", "{ADC91A84-6054-42EC-8241-0D717E4C7194}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{A8A97D6A-02C0-4808-9D62-DFFAB324A323}" + ProjectSection(SolutionItems) = preProject + scripts\update-cli.ps1 = scripts\update-cli.ps1 + scripts\bump-version.sh = scripts\bump-version.sh + scripts\device-test.ps1 = scripts\device-test.ps1 + scripts\dirty-check.ps1 = scripts\dirty-check.ps1 + scripts\update-java.ps1 = scripts\update-java.ps1 + scripts\bump-version.ps1 = scripts\bump-version.ps1 + scripts\parse-xunit2-xml.ps1 = scripts\parse-xunit2-xml.ps1 + scripts\build-sentry-cocoa.sh = scripts\build-sentry-cocoa.sh + scripts\update-project-xml.ps1 = scripts\update-project-xml.ps1 + scripts\build-sentry-native.ps1 = scripts\build-sentry-native.ps1 + scripts\ios-simulator-utils.ps1 = scripts\ios-simulator-utils.ps1 + scripts\commit-formatted-code.sh = scripts\commit-formatted-code.sh + scripts\accept-verifier-changes.ps1 = scripts\accept-verifier-changes.ps1 + scripts\generate-cocoa-bindings.ps1 = scripts\generate-cocoa-bindings.ps1 + scripts\generate-solution-filters.ps1 = scripts\generate-solution-filters.ps1 + scripts\generate-solution-filters-config.yaml = scripts\generate-solution-filters-config.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EC6ADE8A-E557-4848-8F03-519039830B5F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{BFF081D8-7CC0-4069-99F5-5CA0D70B56AB}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\alpine.yml = .github\workflows\alpine.yml + .github\workflows\danger.yml = .github\workflows\danger.yml + .github\workflows\release.yml = .github\workflows\release.yml + .github\workflows\format-code.yml = .github\workflows\format-code.yml + .github\workflows\update-deps.yml = .github\workflows\update-deps.yml + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\vulnerabilities.yml = .github\workflows\vulnerabilities.yml + .github\workflows\device-tests-ios.yml = .github\workflows\device-tests-ios.yml + .github\workflows\device-tests-android.yml = .github\workflows\device-tests-android.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "actions", "actions", "{5D50D425-244F-4B79-B9F5-21D26DD52DC1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "environment", "environment", "{39216438-F347-427C-AB70-48DB1BA6E299}" + ProjectSection(SolutionItems) = preProject + .github\actions\environment\action.yml = .github\actions\environment\action.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "buildnative", "buildnative", "{A384A71C-A46F-49DB-B7FB-5DEEFC5E6CA3}" + ProjectSection(SolutionItems) = preProject + .github\actions\buildnative\action.yml = .github\actions\buildnative\action.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "freediskspace", "freediskspace", "{E34AA22F-B42E-4D4C-B96E-426AEBC2F367}" + ProjectSection(SolutionItems) = preProject + .github\actions\freediskspace\action.yml = .github\actions\freediskspace\action.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "install-zstd", "install-zstd", "{94A2DCA5-F298-41FB-913A-476668EF5786}" + ProjectSection(SolutionItems) = preProject + .github\actions\install-zstd\action.yml = .github\actions\install-zstd\action.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration-test", "integration-test", "{94CCDBEF-5867-4C24-A305-0C2AE738AF42}" + ProjectSection(SolutionItems) = preProject + integration-test\common.ps1 = integration-test\common.ps1 + integration-test\aot.Tests.ps1 = integration-test\aot.Tests.ps1 + integration-test\cli.Tests.ps1 = integration-test\cli.Tests.ps1 + integration-test\runtime.Tests.ps1 = integration-test\runtime.Tests.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net4-console", "net4-console", "{33793113-C7B5-434D-B5C1-6CA1A9587842}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1343,5 +1411,12 @@ Global {C3CDF61C-3E28-441C-A9CE-011C89D11719} = {230B9384-90FD-4551-A5DE-1A5C197F25B6} {3A76FF7D-2F32-4EA5-8999-2FFE3C7CB893} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} {ADC91A84-6054-42EC-8241-0D717E4C7194} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} + {BFF081D8-7CC0-4069-99F5-5CA0D70B56AB} = {EC6ADE8A-E557-4848-8F03-519039830B5F} + {5D50D425-244F-4B79-B9F5-21D26DD52DC1} = {EC6ADE8A-E557-4848-8F03-519039830B5F} + {39216438-F347-427C-AB70-48DB1BA6E299} = {5D50D425-244F-4B79-B9F5-21D26DD52DC1} + {A384A71C-A46F-49DB-B7FB-5DEEFC5E6CA3} = {5D50D425-244F-4B79-B9F5-21D26DD52DC1} + {E34AA22F-B42E-4D4C-B96E-426AEBC2F367} = {5D50D425-244F-4B79-B9F5-21D26DD52DC1} + {94A2DCA5-F298-41FB-913A-476668EF5786} = {5D50D425-244F-4B79-B9F5-21D26DD52DC1} + {33793113-C7B5-434D-B5C1-6CA1A9587842} = {94CCDBEF-5867-4C24-A305-0C2AE738AF42} EndGlobalSection EndGlobal diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf index 1592f77697..bfc8311ad7 100644 --- a/SentryNoMobile.slnf +++ b/SentryNoMobile.slnf @@ -24,7 +24,6 @@ "samples\\Sentry.Samples.GraphQL.Server\\Sentry.Samples.GraphQL.Server.csproj", "samples\\Sentry.Samples.Hangfire\\Sentry.Samples.Hangfire.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", - "samples\\Sentry.Samples.MacCatalyst\\Sentry.Samples.MacCatalyst.csproj", "samples\\Sentry.Samples.MacOS\\Sentry.Samples.MacOS.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", 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/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj index 48231469f2..5a28b1186f 100644 --- a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj +++ b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + $(PreviousTfm) false diff --git a/global.json b/global.json index 9cd89532c5..9920ba14bd 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.304", - "workloadVersion": "9.0.304", + "version": "10.0.100-rc.1.25451.107", + "workloadVersion": "10.0.100-rc.1.25458.2", "rollForward": "disable", "allowPrerelease": false } diff --git a/integration-test/aot.Tests.ps1 b/integration-test/aot.Tests.ps1 index 14849c332f..269e8d14ae 100644 --- a/integration-test/aot.Tests.ps1 +++ b/integration-test/aot.Tests.ps1 @@ -51,19 +51,21 @@ Console.WriteLine("Hello, Sentry!"); It 'Aot' { $rid = $env:RuntimeIdentifier - if ($rid) - { - dotnet publish -c Release -r $rid | Write-Host + $baseImage = $env:ContainerBaseImage + $publishArgs = @('-c', 'Release') + if ($rid) { + Write-Host "Environment RuntimeIdentifier: $rid" + $publishArgs += @('-r', $rid) } - else - { - dotnet publish -c Release | Write-Host + if ($baseImage) { + Write-Host "Using ContainerBaseImage: $baseImage" + $publishArgs += "-p:ContainerBaseImage=$baseImage" } + dotnet publish @publishArgs | Write-Host $LASTEXITCODE | Should -Be 0 $tfm = (Get-ChildItem -Path "bin/Release" -Directory | Select-Object -First 1).Name - if (-not $rid) - { + if (-not $rid) { $rid = (Get-ChildItem -Path "bin/Release/$tfm" -Directory | Select-Object -First 1).Name } & "bin/Release/$tfm/$rid/publish/hello-sentry" | Write-Host diff --git a/integration-test/cli.Tests.ps1 b/integration-test/cli.Tests.ps1 index 1a39cf541a..ff7b8a7eee 100644 --- a/integration-test/cli.Tests.ps1 +++ b/integration-test/cli.Tests.ps1 @@ -4,7 +4,7 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot/common.ps1 Describe 'Console apps () - normal build' -ForEach @( - @{ framework = "net8.0" } + foreach ($fw in $currentFrameworks) { @{ framework = $fw } } ) { BeforeAll { DotnetNew 'console' 'console-app' $framework @@ -40,7 +40,7 @@ Describe 'Console apps () - normal build' -ForEach @( } Describe 'Console apps () - native AOT publish' -ForEach @( - @{ framework = "net8.0" } + foreach ($fw in $currentFrameworks) { @{ framework = $fw } } ) { BeforeAll { DotnetNew 'console' 'console-app' $framework @@ -92,10 +92,6 @@ Describe 'Console apps () - native AOT publish' -ForEach @( $result = RunDotnetWithSentryCLI 'publish' 'console-app' $False $True $framework $result.ScriptOutput | Should -AnyElementMatch "Preparing upload to Sentry for project 'console-app'" $sourceBundle = 'console-app.src.zip' - if ($IsMacOS) - { - $sourceBundle = 'console-app.src.zip' - } $result.UploadedDebugFiles() | Sort-Object -Unique | Should -Be @($sourceBundle) } @@ -105,8 +101,8 @@ Describe 'Console apps () - native AOT publish' -ForEach @( } } -Describe 'MAUI' -ForEach @( - @{ framework = "net8.0" } +Describe 'MAUI ()' -ForEach @( + @{ framework = $previousFramework } ) -Skip:($env:NO_MOBILE -eq "true") { BeforeAll { RegisterLocalPackage 'Sentry.Android.AssemblyReader' @@ -119,8 +115,8 @@ Describe 'MAUI' -ForEach @( } $name = 'maui-app' - $androidTpv = '34.0' - $iosTpv = '17.0' + $androidTpv = GetAndroidTpv $framework + $iosTpv = GetIosTpv $framework DotnetNew 'maui' $name $framework @@ -148,6 +144,7 @@ Describe 'MAUI' -ForEach @( It "uploads symbols and sources for an Android build" { $result = RunDotnetWithSentryCLI 'build' 'maui-app' $True $True "$framework-android$androidTpv" + Write-Host "UploadedDebugFiles: $($result.UploadedDebugFiles() | Out-String)" $result.UploadedDebugFiles() | Sort-Object -Unique | Should -Be @( 'libsentry-android.so', 'libsentry.so', @@ -156,11 +153,12 @@ Describe 'MAUI' -ForEach @( 'maui-app.pdb' ) $result.ScriptOutput | Should -AnyElementMatch 'Uploaded a total of 1 new mapping files' - $result.ScriptOutput | Should -AnyElementMatch 'Found 25 debug information files \(1 with embedded sources\)' + $result.ScriptOutput | Should -AnyElementMatch "Found 23 debug information files \(1 with embedded sources\)" } It "uploads symbols and sources for an iOS build" -Skip:(!$IsMacOS) { $result = RunDotnetWithSentryCLI 'build' 'maui-app' $True $True "$framework-ios$iosTpv" + Write-Host "UploadedDebugFiles: $($result.UploadedDebugFiles() | Out-String)" $result.UploadedDebugFiles() | Sort-Object -Unique | Should -Be @( 'libmono-component-debugger.dylib', 'libmono-component-diagnostics_tracing.dylib', @@ -176,9 +174,14 @@ Describe 'MAUI' -ForEach @( 'libxamarin-dotnet.dylib', 'maui-app', 'maui-app.pdb', + 'Microsoft.iOS.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]*'; - $result.ScriptOutput | Should -AnyElementMatch "Found $nonZeroNumberRegex debug information files \($nonZeroNumberRegex with embedded sources\)" + $result.ScriptOutput | Should -AnyElementMatch "Found 77 debug information files \(8 with embedded sources\)" } } diff --git a/integration-test/common.ps1 b/integration-test/common.ps1 index b7215fc87e..312825360e 100644 --- a/integration-test/common.ps1 +++ b/integration-test/common.ps1 @@ -1,5 +1,9 @@ -# So that this works in VS Code testing integration. Otherwise the script is run within its directory. +$global:longTermFramework = 'net8.0' +$global:previousFramework = 'net9.0' +$global:latestFramework = 'net10.0' +$global:currentFrameworks = @($longTermFramework, $previousFramework, $latestFramework) +# So that this works in VS Code testing integration. Otherwise the script is run within its directory. # In CI, the module is loaded automatically if (!(Test-Path env:CI )) { @@ -58,9 +62,74 @@ BeforeAll { Push-Location $PSScriptRoot $env:SENTRY_LOG_LEVEL = 'debug'; + function GetAndroidTpv($framework) + { + switch ($framework) { + 'net9.0' { return '35.0' } # matches PreviousAndroidTfm (net9.0-android35.0) + 'net10.0' { return '36.0' } # matches LatestAndroidTfm (net10.0-android36.0) + default { throw "Unsupported framework '$framework' for Android target platform version." } + } + } + + function GetIosTpv($framework) + { + switch ($framework) { + 'net9.0' { return '18.0' } # matches PreviousIosTfm / PreviousMacCatalystTfm + 'net10.0' { return '26' } # aligns with ios26 / maccatalyst26 + default { throw "Unsupported framework '$framework' for iOS target platform version." } + } + } + function GetSentryPackageVersion() { - (Select-Xml -Path "$PSScriptRoot/../Directory.Build.props" -XPath "/Project/PropertyGroup/VersionPrefix").Node.InnerText + # Read version directly from Directory.Build.props + $propsFile = Join-Path $PSScriptRoot '..\Directory.Build.props' + + if (-not (Test-Path $propsFile)) { + throw "Directory.Build.props not found at $propsFile" + } + + # Parse the props file using PowerShell XML parsing + Write-Host "Parsing props file as XML..." + [xml]$propsXml = Get-Content $propsFile + + # Look for VersionPrefix and VersionSuffix in PropertyGroup elements + $versionPrefix = "" + $versionSuffix = "" + + foreach ($propGroup in $propsXml.Project.PropertyGroup) { + if ($propGroup.PSObject.Properties["VersionPrefix"]) { + $versionPrefix = $propGroup.VersionPrefix + Write-Host "Found VersionPrefix: '$versionPrefix'" + } + + # For VersionSuffix, we need to be careful about conditions + # Only use VersionSuffix if it's not in a conditional PropertyGroup + # or if it's explicitly set (not the 'dev' fallback for non-Release) + if ($propGroup.PSObject.Properties["VersionSuffix"]) { + $condition = $null + if ($propGroup.PSObject.Properties["Condition"]) { + $condition = $propGroup.Condition + Write-Host "Ignoring VersionSuffix: '$($propGroup.VersionSuffix)' with condition: '$condition'" + # Skip conditional VersionSuffix as we're building in Release mode + } + else { + # No condition - this is the explicit VersionSuffix we want + $versionSuffix = $propGroup.VersionSuffix + Write-Host "Found VersionSuffix: '$versionSuffix'" + } + } + } + + if (-not $versionPrefix) { + throw "Could not find VersionPrefix in $propsFile" + } + + # Combine prefix and suffix + $fullVersion = if ($versionSuffix) { "$versionPrefix-$versionSuffix" } else { $versionPrefix } + Write-Host "Full Version: '$fullVersion'" + + return $fullVersion } function RegisterLocalPackage([string] $name) @@ -142,7 +211,7 @@ BeforeAll { } elseif ($action -eq "publish") { - $result.ScriptOutput | Should -AnyElementMatch "$((Get-Item $project).Basename) -> .*$project/bin/Release/$TargetFramework/.*/publish" + $result.ScriptOutput | Should -AnyElementMatch "$((Get-Item $project).Basename) -> .*$project/bin/Release/$TargetFramework/.*publish.*" } $result.ScriptOutput | Should -Not -AnyElementMatch "Preparing upload to Sentry for project 'Sentry'" $result.HasErrors() | Should -BeFalse @@ -154,7 +223,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." @@ -197,16 +266,13 @@ BeforeAll { if ($type -eq 'console') { AddPackageReference $name 'Sentry' - if (!$IsMacOS -or $framework -eq 'net8.0') - { - @" + @" true "@ | Out-File $name/Directory.Build.props - } } } } 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/integration-test/runtime.Tests.ps1 b/integration-test/runtime.Tests.ps1 index 158890edc3..f6f98407c6 100644 --- a/integration-test/runtime.Tests.ps1 +++ b/integration-test/runtime.Tests.ps1 @@ -4,7 +4,7 @@ $ErrorActionPreference = 'Stop' . $PSScriptRoot/common.ps1 Describe 'Console app NativeAOT ()' -ForEach @( - @{ framework = 'net8.0' } + foreach ($fw in $currentFrameworks) { @{ framework = $fw } } ) { BeforeAll { $path = './console-app' @@ -188,7 +188,7 @@ Describe 'Console app regression (missing System.Reflection.Metadata)' { dotnet remove ./net4-console/console-app.csproj package Sentry } - It 'Ensure System.Reflection.Metadata is not missing' { + It 'Ensure System.Reflection.Metadata is not missing' -Skip:(!$IsWindows) { $path = './net4-console' Remove-Item -Recurse -Force -Path @("$path/bin", "$path/obj") -ErrorAction SilentlyContinue AddPackageReference $path 'Sentry' diff --git a/modules/Ben.Demystifier b/modules/Ben.Demystifier index 0a6b30f6ac..7c9eeea3ed 160000 --- a/modules/Ben.Demystifier +++ b/modules/Ben.Demystifier @@ -1 +1 @@ -Subproject commit 0a6b30f6ac6892ff45b9dcd5b07d710e5228256e +Subproject commit 7c9eeea3ed2416f2aa0491da640fa84337f2a7a2 diff --git a/modules/sentry-cocoa.properties b/modules/sentry-cocoa.properties index d4dde9863e..1b37e7bb3c 100644 --- a/modules/sentry-cocoa.properties +++ b/modules/sentry-cocoa.properties @@ -1,2 +1,2 @@ -version = 8.56.0 +version = 8.56.2 repo = https://github.com/getsentry/sentry-cocoa 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 diff --git a/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj b/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj index ace5748bae..a9eacd63be 100644 --- a/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj +++ b/samples/Sentry.Samples.Android/Sentry.Samples.Android.csproj @@ -1,6 +1,6 @@ - net8.0-android + net9.0-android35.0 21 Exe enable diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj b/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj index eb93b9fefb..6b83d73fc1 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj +++ b/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.AspNetCore.Blazor.Server/Sentry.Samples.AspNetCore.Blazor.Server.csproj b/samples/Sentry.Samples.AspNetCore.Blazor.Server/Sentry.Samples.AspNetCore.Blazor.Server.csproj index 04fa5f4380..e173fff774 100644 --- a/samples/Sentry.Samples.AspNetCore.Blazor.Server/Sentry.Samples.AspNetCore.Blazor.Server.csproj +++ b/samples/Sentry.Samples.AspNetCore.Blazor.Server/Sentry.Samples.AspNetCore.Blazor.Server.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/samples/Sentry.Samples.AspNetCore.Blazor.Wasm/Sentry.Samples.AspNetCore.Blazor.Wasm.csproj b/samples/Sentry.Samples.AspNetCore.Blazor.Wasm/Sentry.Samples.AspNetCore.Blazor.Wasm.csproj index c64bb41a0a..22d6f53b09 100644 --- a/samples/Sentry.Samples.AspNetCore.Blazor.Wasm/Sentry.Samples.AspNetCore.Blazor.Wasm.csproj +++ b/samples/Sentry.Samples.AspNetCore.Blazor.Wasm/Sentry.Samples.AspNetCore.Blazor.Wasm.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 true @@ -15,13 +15,8 @@ - - + + - - - - - diff --git a/samples/Sentry.Samples.AspNetCore.Grpc/Program.cs b/samples/Sentry.Samples.AspNetCore.Grpc/Program.cs index cdd26f9eb0..12d4313821 100644 --- a/samples/Sentry.Samples.AspNetCore.Grpc/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.Grpc/Program.cs @@ -1,69 +1,73 @@ using System.Net; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Server.Kestrel.Core; using Sentry.AspNetCore; using Sentry.AspNetCore.Grpc; +using Sentry.Samples.AspNetCore.Grpc; -namespace Sentry.Samples.AspNetCore.Grpc; +var builder = WebApplication.CreateBuilder(args); -public static class Program -{ - public static void Main(string[] args) +builder.WebHost + .UseShutdownTimeout(TimeSpan.FromSeconds(10)) + .ConfigureKestrel(options => { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseShutdownTimeout(TimeSpan.FromSeconds(10)) - .ConfigureKestrel(options => - { - // Setup a HTTP/2 endpoint without TLS due to macOS limitation. - // https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-5.0#unable-to-start-aspnet-core-grpc-app-on-macos - options.ListenLocalhost(5000, o => o.Protocols = - HttpProtocols.Http2); - }) - .UseStartup() - - // Example integration with advanced configuration scenarios: - .UseSentry(builder => - { - builder.AddGrpc(); - builder.AddSentryOptions(options => - { + // Setup a HTTP/2 endpoint without TLS due to macOS limitation. + // https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-5.0#unable-to-start-aspnet-core-grpc-app-on-macos + options.ListenLocalhost(5000, o => o.Protocols = HttpProtocols.Http2); + }) + .UseSentry(sentryBuilder => + { + sentryBuilder.AddGrpc(); + sentryBuilder.AddSentryOptions(options => + { #if !SENTRY_DSN_DEFINED_IN_ENV - // A DSN is required. You can set here in code, via the SENTRY_DSN environment variable or in your - // appsettings.json file. - // See https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/#configure - options.Dsn = SamplesShared.Dsn; + // A DSN is required. You can set here in code, via the SENTRY_DSN environment variable or in your + // appsettings.json file. + // See https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/#configure + options.Dsn = SamplesShared.Dsn; #endif - // The parameter 'options' here has values populated through the configuration system. - // That includes 'appsettings.json', environment variables and anything else - // defined on the ConfigurationBuilder. - // See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1&tabs=basicconfiguration - // Tracks the release which sent the event and enables more features: https://docs.sentry.io/learn/releases/ - // If not explicitly set here, the SDK attempts to read it from: AssemblyInformationalVersionAttribute and AssemblyVersion - // TeamCity: %build.vcs.number%, VSTS: BUILD_SOURCEVERSION, Travis-CI: TRAVIS_COMMIT, AppVeyor: APPVEYOR_REPO_COMMIT, CircleCI: CIRCLE_SHA1 - options.Release = - "e386dfd"; // Could also be any format, such as: 2.0, or however version of your app is + // The parameter 'options' here has values populated through the configuration system. + // That includes 'appsettings.json', environment variables and anything else + // defined on the ConfigurationBuilder. + // Tracks the release which sent the event and enables more features: https://docs.sentry.io/learn/releases/ + // If not explicitly set here, the SDK attempts to read it from: AssemblyInformationalVersionAttribute and AssemblyVersion + options.Release = "e386dfd"; + + // Enable performance monitoring + options.TracesSampleRate = 1.0; + + options.MaxBreadcrumbs = 200; - options.TracesSampleRate = 1.0; + // Set a proxy for outgoing HTTP connections + options.HttpProxy = null; // new WebProxy("https://localhost:3128"); - options.MaxBreadcrumbs = 200; + // Example: Disabling support to compressed responses: + options.DecompressionMethods = DecompressionMethods.None; - // Set a proxy for outgoing HTTP connections - options.HttpProxy = null; // new WebProxy("https://localhost:3128"); + options.MaxQueueItems = 100; + options.ShutdownTimeout = TimeSpan.FromSeconds(5); - // Example: Disabling support to compressed responses: - options.DecompressionMethods = DecompressionMethods.None; + // Configures the root scope + options.ConfigureScope(s => s.SetTag("Always sent", "this tag")); + }); + }); - options.MaxQueueItems = 100; - options.ShutdownTimeout = TimeSpan.FromSeconds(5); +// Services +builder.Services.AddGrpcReflection(); +builder.Services.AddGrpc(); - // Configures the root scope - options.ConfigureScope(s => s.SetTag("Always sent", "this tag")); - }); - }) - .Build(); +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +// Map gRPC endpoints +app.MapGrpcService(); +if (app.Environment.IsDevelopment()) +{ + app.MapGrpcReflectionService(); } + +app.Run(); diff --git a/samples/Sentry.Samples.AspNetCore.Grpc/Sentry.Samples.AspNetCore.Grpc.csproj b/samples/Sentry.Samples.AspNetCore.Grpc/Sentry.Samples.AspNetCore.Grpc.csproj index 89dc8c6a3e..dfc6c6520d 100644 --- a/samples/Sentry.Samples.AspNetCore.Grpc/Sentry.Samples.AspNetCore.Grpc.csproj +++ b/samples/Sentry.Samples.AspNetCore.Grpc/Sentry.Samples.AspNetCore.Grpc.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.AspNetCore.Grpc/Startup.cs b/samples/Sentry.Samples.AspNetCore.Grpc/Startup.cs deleted file mode 100644 index 5bc4580274..0000000000 --- a/samples/Sentry.Samples.AspNetCore.Grpc/Startup.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Sentry.Samples.AspNetCore.Grpc; - -public class Startup -{ - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpcReflection(); - services.AddGrpc(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - if (env.IsDevelopment()) - { - endpoints.MapGrpcReflectionService(); - } - }); - } -} diff --git a/samples/Sentry.Samples.AspNetCore.Mvc/Sentry.Samples.AspNetCore.Mvc.csproj b/samples/Sentry.Samples.AspNetCore.Mvc/Sentry.Samples.AspNetCore.Mvc.csproj index d3bbf0aa34..d6eb7609a7 100644 --- a/samples/Sentry.Samples.AspNetCore.Mvc/Sentry.Samples.AspNetCore.Mvc.csproj +++ b/samples/Sentry.Samples.AspNetCore.Mvc/Sentry.Samples.AspNetCore.Mvc.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable Samples.AspNetCore.Mvc diff --git a/samples/Sentry.Samples.AspNetCore.Serilog/Sentry.Samples.AspNetCore.Serilog.csproj b/samples/Sentry.Samples.AspNetCore.Serilog/Sentry.Samples.AspNetCore.Serilog.csproj index a56d6004e2..5c3cf4c562 100644 --- a/samples/Sentry.Samples.AspNetCore.Serilog/Sentry.Samples.AspNetCore.Serilog.csproj +++ b/samples/Sentry.Samples.AspNetCore.Serilog/Sentry.Samples.AspNetCore.Serilog.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.AspNetCore.WebAPI.Profiling/Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj b/samples/Sentry.Samples.AspNetCore.WebAPI.Profiling/Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj index 6ca934c5fc..68bf35efdd 100644 --- a/samples/Sentry.Samples.AspNetCore.WebAPI.Profiling/Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj +++ b/samples/Sentry.Samples.AspNetCore.WebAPI.Profiling/Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/samples/Sentry.Samples.Aws.Lambda.AspNetCoreServer/Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj b/samples/Sentry.Samples.Aws.Lambda.AspNetCoreServer/Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj index 8d2dd8b221..c9150b1b76 100644 --- a/samples/Sentry.Samples.Aws.Lambda.AspNetCoreServer/Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj +++ b/samples/Sentry.Samples.Aws.Lambda.AspNetCoreServer/Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 true Lambda diff --git a/samples/Sentry.Samples.Azure.Functions.Worker/Sentry.Samples.Azure.Functions.Worker.csproj b/samples/Sentry.Samples.Azure.Functions.Worker/Sentry.Samples.Azure.Functions.Worker.csproj index 3145f92db6..1ee89930e3 100644 --- a/samples/Sentry.Samples.Azure.Functions.Worker/Sentry.Samples.Azure.Functions.Worker.csproj +++ b/samples/Sentry.Samples.Azure.Functions.Worker/Sentry.Samples.Azure.Functions.Worker.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 v4 Exe diff --git a/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj b/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj index bf9efb93d8..44f961dfae 100644 --- a/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj +++ b/samples/Sentry.Samples.Console.Basic/Sentry.Samples.Console.Basic.csproj @@ -4,9 +4,9 @@ Exe enable enable - net9.0;net8.0;net462 + net9.0;net10.0;net462 - + true true diff --git a/samples/Sentry.Samples.Console.Customized/Sentry.Samples.Console.Customized.csproj b/samples/Sentry.Samples.Console.Customized/Sentry.Samples.Console.Customized.csproj index ec23ce7173..26a1861513 100644 --- a/samples/Sentry.Samples.Console.Customized/Sentry.Samples.Console.Customized.csproj +++ b/samples/Sentry.Samples.Console.Customized/Sentry.Samples.Console.Customized.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.Console.HeapDump/Sentry.Samples.Console.HeapDump.csproj b/samples/Sentry.Samples.Console.HeapDump/Sentry.Samples.Console.HeapDump.csproj index cfcc72e664..b88b507419 100644 --- a/samples/Sentry.Samples.Console.HeapDump/Sentry.Samples.Console.HeapDump.csproj +++ b/samples/Sentry.Samples.Console.HeapDump/Sentry.Samples.Console.HeapDump.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable true @@ -51,5 +51,5 @@ - + diff --git a/samples/Sentry.Samples.Console.Native/Sentry.Samples.Console.Native.csproj b/samples/Sentry.Samples.Console.Native/Sentry.Samples.Console.Native.csproj index e4fd5c44cc..190dd6ac5c 100644 --- a/samples/Sentry.Samples.Console.Native/Sentry.Samples.Console.Native.csproj +++ b/samples/Sentry.Samples.Console.Native/Sentry.Samples.Console.Native.csproj @@ -4,7 +4,7 @@ Exe enable enable - net8.0 + net10.0 true diff --git a/samples/Sentry.Samples.Console.Profiling/Sentry.Samples.Console.Profiling.csproj b/samples/Sentry.Samples.Console.Profiling/Sentry.Samples.Console.Profiling.csproj index 82e57fc84e..6f715d10c3 100644 --- a/samples/Sentry.Samples.Console.Profiling/Sentry.Samples.Console.Profiling.csproj +++ b/samples/Sentry.Samples.Console.Profiling/Sentry.Samples.Console.Profiling.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.EntityFramework/Sentry.Samples.EntityFramework.csproj b/samples/Sentry.Samples.EntityFramework/Sentry.Samples.EntityFramework.csproj index 2e27079e5d..0408fce517 100644 --- a/samples/Sentry.Samples.EntityFramework/Sentry.Samples.EntityFramework.csproj +++ b/samples/Sentry.Samples.EntityFramework/Sentry.Samples.EntityFramework.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.GenericHost/Sentry.Samples.GenericHost.csproj b/samples/Sentry.Samples.GenericHost/Sentry.Samples.GenericHost.csproj index 78283ed704..d2b5e11219 100644 --- a/samples/Sentry.Samples.GenericHost/Sentry.Samples.GenericHost.csproj +++ b/samples/Sentry.Samples.GenericHost/Sentry.Samples.GenericHost.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 @@ -20,8 +20,6 @@ - - - + diff --git a/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj b/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj index c212fcf871..4d6ec1bb59 100644 --- a/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj +++ b/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.GraphQL.Client.Http/Sentry.Samples.GraphQL.Client.Http.csproj b/samples/Sentry.Samples.GraphQL.Client.Http/Sentry.Samples.GraphQL.Client.Http.csproj index 183f1fef77..14fee315b4 100644 --- a/samples/Sentry.Samples.GraphQL.Client.Http/Sentry.Samples.GraphQL.Client.Http.csproj +++ b/samples/Sentry.Samples.GraphQL.Client.Http/Sentry.Samples.GraphQL.Client.Http.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/samples/Sentry.Samples.GraphQL.Server/Sentry.Samples.GraphQL.Server.csproj b/samples/Sentry.Samples.GraphQL.Server/Sentry.Samples.GraphQL.Server.csproj index 2078f97579..c7ed799d41 100644 --- a/samples/Sentry.Samples.GraphQL.Server/Sentry.Samples.GraphQL.Server.csproj +++ b/samples/Sentry.Samples.GraphQL.Server/Sentry.Samples.GraphQL.Server.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/samples/Sentry.Samples.Hangfire/Sentry.Samples.Hangfire.csproj b/samples/Sentry.Samples.Hangfire/Sentry.Samples.Hangfire.csproj index ac7347715a..804f68bb63 100644 --- a/samples/Sentry.Samples.Hangfire/Sentry.Samples.Hangfire.csproj +++ b/samples/Sentry.Samples.Hangfire/Sentry.Samples.Hangfire.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj index e1cd4e8127..55ff9e06b4 100644 --- a/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj +++ b/samples/Sentry.Samples.Ios/Sentry.Samples.Ios.csproj @@ -1,7 +1,7 @@ - net9.0-ios + net9.0-ios18.0 18 Exe enable diff --git a/samples/Sentry.Samples.Log4Net/Sentry.Samples.Log4Net.csproj b/samples/Sentry.Samples.Log4Net/Sentry.Samples.Log4Net.csproj index 43c4215f48..7f0c0b8178 100644 --- a/samples/Sentry.Samples.Log4Net/Sentry.Samples.Log4Net.csproj +++ b/samples/Sentry.Samples.Log4Net/Sentry.Samples.Log4Net.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 3.5.234 diff --git a/samples/Sentry.Samples.ME.Logging/Sentry.Samples.ME.Logging.csproj b/samples/Sentry.Samples.ME.Logging/Sentry.Samples.ME.Logging.csproj index 0d60cbe639..dfe4a8f252 100644 --- a/samples/Sentry.Samples.ME.Logging/Sentry.Samples.ME.Logging.csproj +++ b/samples/Sentry.Samples.ME.Logging/Sentry.Samples.ME.Logging.csproj @@ -2,11 +2,11 @@ Exe - net8.0 + net9.0 - + diff --git a/samples/Sentry.Samples.MacCatalyst/AppDelegate.cs b/samples/Sentry.Samples.MacCatalyst/AppDelegate.cs index 044ae0c169..1be71dc6d4 100644 --- a/samples/Sentry.Samples.MacCatalyst/AppDelegate.cs +++ b/samples/Sentry.Samples.MacCatalyst/AppDelegate.cs @@ -11,7 +11,7 @@ public override UIWindow? Window set; } - public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) + public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions) { // Init the Sentry SDK SentrySdk.Init(options => diff --git a/samples/Sentry.Samples.MacCatalyst/Sentry.Samples.MacCatalyst.csproj b/samples/Sentry.Samples.MacCatalyst/Sentry.Samples.MacCatalyst.csproj index deacfb2b37..0ba0946ebf 100644 --- a/samples/Sentry.Samples.MacCatalyst/Sentry.Samples.MacCatalyst.csproj +++ b/samples/Sentry.Samples.MacCatalyst/Sentry.Samples.MacCatalyst.csproj @@ -1,11 +1,11 @@ - net8.0-maccatalyst17.0 + net9.0-maccatalyst18.0 Exe enable true - 14.2 + 15.0 true diff --git a/samples/Sentry.Samples.MacOS/Sentry.Samples.MacOS.csproj b/samples/Sentry.Samples.MacOS/Sentry.Samples.MacOS.csproj index 6228d98e39..c2cced507d 100644 --- a/samples/Sentry.Samples.MacOS/Sentry.Samples.MacOS.csproj +++ b/samples/Sentry.Samples.MacOS/Sentry.Samples.MacOS.csproj @@ -1,11 +1,11 @@ - net8.0-macos + net9.0-macos Exe enable true - 10.15 + 12.0 true osx-arm64 diff --git a/samples/Sentry.Samples.NLog/Sentry.Samples.NLog.csproj b/samples/Sentry.Samples.NLog/Sentry.Samples.NLog.csproj index 9b7e809ed0..a25df53d21 100644 --- a/samples/Sentry.Samples.NLog/Sentry.Samples.NLog.csproj +++ b/samples/Sentry.Samples.NLog/Sentry.Samples.NLog.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj index 7159dfcb7e..a07956f015 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/samples/Sentry.Samples.OpenTelemetry.Console/Sentry.Samples.OpenTelemetry.Console.csproj b/samples/Sentry.Samples.OpenTelemetry.Console/Sentry.Samples.OpenTelemetry.Console.csproj index 8b5c0e9654..0aed012a4f 100644 --- a/samples/Sentry.Samples.OpenTelemetry.Console/Sentry.Samples.OpenTelemetry.Console.csproj +++ b/samples/Sentry.Samples.OpenTelemetry.Console/Sentry.Samples.OpenTelemetry.Console.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable 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/samples/Sentry.Samples.Serilog/Sentry.Samples.Serilog.csproj b/samples/Sentry.Samples.Serilog/Sentry.Samples.Serilog.csproj index f9df0dc20f..ab1add9334 100644 --- a/samples/Sentry.Samples.Serilog/Sentry.Samples.Serilog.csproj +++ b/samples/Sentry.Samples.Serilog/Sentry.Samples.Serilog.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 3.5.234 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 } diff --git a/scripts/generate-solution-filters-config.yaml b/scripts/generate-solution-filters-config.yaml index 45c95d8f4b..251aca7e7d 100644 --- a/scripts/generate-solution-filters-config.yaml +++ b/scripts/generate-solution-filters-config.yaml @@ -32,6 +32,11 @@ groupConfigs: trimTests: - "**/Sentry.TrimTest.csproj" - "**/Sentry.MauiTrimTest.csproj" + mobileOnly: + - "**/*Android*.csproj" + - "**/*Ios*.csproj" + - "**/*MacCatalyst*.csproj" + - "**/*Maui*.csproj" filterConfigs: @@ -61,6 +66,7 @@ filterConfigs: - "windowsOnly" - "artefacts" - "trimTests" + - "mobileOnly" patterns: - "**/*Android*.csproj" - "**/*DeviceTests*.csproj" @@ -240,6 +246,7 @@ filterConfigs: groups: - "artefacts" - "trimTests" + - "mobileOnly" patterns: - "**/*Bindings*.csproj" - "**/*Android*.csproj" 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 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a63860aea1..e3e1feb526 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -36,7 +36,7 @@ - + true diff --git a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj index 2c6dcde33e..fa94fec130 100644 --- a/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj +++ b/src/Sentry.Android.AssemblyReader/Sentry.Android.AssemblyReader.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0 + $(PreviousTfm);$(LatestTfm) .NET assembly reader for Android diff --git a/src/Sentry.AspNetCore.Blazor.WebAssembly/Sentry.AspNetCore.Blazor.WebAssembly.csproj b/src/Sentry.AspNetCore.Blazor.WebAssembly/Sentry.AspNetCore.Blazor.WebAssembly.csproj index 4ff5701fa0..61d6dd2229 100644 --- a/src/Sentry.AspNetCore.Blazor.WebAssembly/Sentry.AspNetCore.Blazor.WebAssembly.csproj +++ b/src/Sentry.AspNetCore.Blazor.WebAssembly/Sentry.AspNetCore.Blazor.WebAssembly.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) Sentry.AspNetCore.Blazor.WebAssembly @@ -10,11 +10,16 @@ + + + + - - + + + diff --git a/src/Sentry.AspNetCore.Grpc/Sentry.AspNetCore.Grpc.csproj b/src/Sentry.AspNetCore.Grpc/Sentry.AspNetCore.Grpc.csproj index cafddf4b7e..3ed88a9201 100644 --- a/src/Sentry.AspNetCore.Grpc/Sentry.AspNetCore.Grpc.csproj +++ b/src/Sentry.AspNetCore.Grpc/Sentry.AspNetCore.Grpc.csproj @@ -1,7 +1,7 @@ - net8.0 + $(CurrentTfms) $(PackageTags);AspNetCore;gRPC Official ASP.NET Core gRPC integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. diff --git a/src/Sentry.AspNetCore/Sentry.AspNetCore.csproj b/src/Sentry.AspNetCore/Sentry.AspNetCore.csproj index d75cabef9f..a44b2a9962 100644 --- a/src/Sentry.AspNetCore/Sentry.AspNetCore.csproj +++ b/src/Sentry.AspNetCore/Sentry.AspNetCore.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) $(PackageTags);AspNetCore;MVC Official ASP.NET Core integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. @@ -11,18 +11,15 @@ true + + $(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration + + - - - - - - - diff --git a/src/Sentry.Azure.Functions.Worker/Sentry.Azure.Functions.Worker.csproj b/src/Sentry.Azure.Functions.Worker/Sentry.Azure.Functions.Worker.csproj index 7d04ce1a0b..c3329b290f 100644 --- a/src/Sentry.Azure.Functions.Worker/Sentry.Azure.Functions.Worker.csproj +++ b/src/Sentry.Azure.Functions.Worker/Sentry.Azure.Functions.Worker.csproj @@ -1,7 +1,7 @@ - net8.0;netstandard2.0 + $(CurrentTfms);netstandard2.0 $(PackageTags);Azure;Functions;Worker Official Azure Functions Worker SDK integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj index 0180326bd2..e99fcd08fb 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 + $(LatestAndroidTfm);$(PreviousAndroidTfm) + 8.22.0 $(BaseIntermediateOutputPath)sdks\$(TargetFramework)\Sentry\Android\$(SentryAndroidSdkVersion)\ @@ -43,16 +43,18 @@ - - - - - - - + + + + + + + - - + + @@ -95,37 +86,6 @@ - - - - - - - - - - - - + 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 @@ + + + diff --git a/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs b/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs index f33b1c6815..2c076ddf1a 100644 --- a/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs +++ b/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs @@ -416,9 +416,9 @@ interface SentryEnvelopeItemHeader : SentrySerializable [Export ("initWithType:length:filenname:contentType:")] NativeHandle Constructor (string type, nuint length, string filename, string contentType); - // -(instancetype _Nonnull)initWithType:(NSString * _Nonnull)type length:(NSUInteger)length contentType:(NSString * _Nonnull)contentType itemCount:(NSNumber * _Nonnull)itemCount; + // -(instancetype _Nonnull)initWithType:(NSString * _Nonnull)type length:(NSUInteger)length contentType:(NSString * _Nullable)contentType itemCount:(NSNumber * _Nonnull)itemCount; [Export ("initWithType:length:contentType:itemCount:")] - NativeHandle Constructor (string type, nuint length, string contentType, NSNumber itemCount); + NativeHandle Constructor (string type, nuint length, [NullAllowed] string contentType, NSNumber itemCount); // @property (readonly, copy, nonatomic) NSString * _Nonnull type; [Export ("type")] diff --git a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj index 98e6fde2d2..10b3899977 100644 --- a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj +++ b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj @@ -1,9 +1,9 @@ - net8.0-ios17.0;net8.0-maccatalyst17.0 - net8.0-ios17.0 - net8.0-maccatalyst17.0 + $(PreviousIosTfm);$(PreviousMacCatalystTfm) + $(PreviousIosTfm) + $(PreviousMacCatalystTfm) true true .NET Bindings for the Sentry Cocoa SDK diff --git a/src/Sentry.Bindings.Cocoa/SwiftApiDefinitions.cs b/src/Sentry.Bindings.Cocoa/SwiftApiDefinitions.cs index 391360da15..1bf7b8ded4 100644 --- a/src/Sentry.Bindings.Cocoa/SwiftApiDefinitions.cs +++ b/src/Sentry.Bindings.Cocoa/SwiftApiDefinitions.cs @@ -329,9 +329,6 @@ interface SentryRRWebEvent : SentrySerializable [Export ("initWithType:timestamp:data:")] [DesignatedInitializer] NativeHandle Constructor (SentryRRWebEventType type, NSDate timestamp, [NullAllowed] NSDictionary data); - // -(NSDictionary * _Nonnull)serialize __attribute__((warn_unused_result(""))); - [Export ("serialize")] - new NSDictionary Serialize(); } // @interface SentrySDK : NSObject diff --git a/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj b/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj index 4d52cd091c..4201207da5 100644 --- a/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj +++ b/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0;netstandard2.0 + $(CurrentTfms);netstandard2.0 $(PackageTags);Logging;Microsoft.Extensions.Logging Official Microsoft.Extensions.Logging integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. @@ -32,6 +32,12 @@ + + + + + + diff --git a/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj b/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj index 6c455dc928..581bd88f3a 100644 --- a/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj +++ b/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj @@ -1,7 +1,7 @@ - net9.0;net8.0 + $(CurrentTfms) $(PackageTags);GCP;Google Cloud Functions Official Google Cloud Functions integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. @@ -16,11 +16,4 @@ - - - - - - - diff --git a/src/Sentry.Hangfire/Sentry.Hangfire.csproj b/src/Sentry.Hangfire/Sentry.Hangfire.csproj index 941dd8c94d..4e2f001c85 100644 --- a/src/Sentry.Hangfire/Sentry.Hangfire.csproj +++ b/src/Sentry.Hangfire/Sentry.Hangfire.csproj @@ -3,7 +3,7 @@ Official Hangfire integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. $(PackageTags);Hangfire - net9.0;net8.0;net462 + $(CurrentTfms);net462 enable false diff --git a/src/Sentry.Maui/Internal/MauiEventsBinder.cs b/src/Sentry.Maui/Internal/MauiEventsBinder.cs index 4af503036b..78e619bed9 100644 --- a/src/Sentry.Maui/Internal/MauiEventsBinder.cs +++ b/src/Sentry.Maui/Internal/MauiEventsBinder.cs @@ -298,9 +298,9 @@ internal void HandlePageEvents(Page page, bool bind = true) // https://github.com/dotnet/docs-maui/issues/583 page.NavigatedTo -= OnPageOnNavigatedTo; - // Layout changed event - // https://docs.microsoft.com/dotnet/api/xamarin.forms.ilayout.layoutchanged - page.LayoutChanged -= OnPageOnLayoutChanged; + // Size changed event + // https://learn.microsoft.com/dotnet/api/microsoft.maui.controls.visualelement.sizechanged + page.SizeChanged -= OnPageOnSizeChanged; if (bind) { @@ -313,9 +313,9 @@ internal void HandlePageEvents(Page page, bool bind = true) // https://github.com/dotnet/docs-maui/issues/583 page.NavigatedTo += OnPageOnNavigatedTo; - // Layout changed event - // https://docs.microsoft.com/dotnet/api/xamarin.forms.ilayout.layoutchanged - page.LayoutChanged += OnPageOnLayoutChanged; + // Size changed event + // https://learn.microsoft.com/dotnet/api/microsoft.maui.controls.visualelement.sizechanged + page.SizeChanged += OnPageOnSizeChanged; } } @@ -446,6 +446,6 @@ private void OnPageOnDisappearing(object? sender, EventArgs _) => private void OnPageOnNavigatedTo(object? sender, NavigatedToEventArgs e) => _hub.AddBreadcrumbForEvent(_options, sender, nameof(Page.NavigatedTo), NavigationType, NavigationCategory, data => data.AddElementInfo(_options, e.GetPreviousPage(), "PreviousPage")); - private void OnPageOnLayoutChanged(object? sender, EventArgs _) => - _hub.AddBreadcrumbForEvent(_options, sender, nameof(Page.LayoutChanged), SystemType, RenderingCategory); + private void OnPageOnSizeChanged(object? sender, EventArgs _) => + _hub.AddBreadcrumbForEvent(_options, sender, nameof(Page.SizeChanged), SystemType, RenderingCategory); } diff --git a/src/Sentry.Maui/Sentry.Maui.csproj b/src/Sentry.Maui/Sentry.Maui.csproj index 38c14e4788..cdaed0c240 100644 --- a/src/Sentry.Maui/Sentry.Maui.csproj +++ b/src/Sentry.Maui/Sentry.Maui.csproj @@ -3,14 +3,14 @@ Official MAUI integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. - net9.0;net8.0 - $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 - $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0 - $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0 - $(TargetFrameworks);net8.0-windows10.0.19041.0 + $(LatestTfm);$(PreviousTfm) + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) + $(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm) + $(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm) + $(TargetFrameworks);$(PreviousWindowsTfm) - net9.0;net8.0 + $(CurrentTfms) $(PackageTags);Profiling;Diagnostic Performance profiling support for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. 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/Sentry.Serilog.csproj b/src/Sentry.Serilog/Sentry.Serilog.csproj index eca849fcd6..60c27e77f7 100644 --- a/src/Sentry.Serilog/Sentry.Serilog.csproj +++ b/src/Sentry.Serilog/Sentry.Serilog.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0;netstandard2.1;netstandard2.0;net462 + $(CurrentTfms);netstandard2.1;netstandard2.0;net462 $(PackageTags);Logging;Serilog Official Serilog integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. true 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/AttributeReader.cs b/src/Sentry/AttributeReader.cs index c90de13c89..213ac99e13 100644 --- a/src/Sentry/AttributeReader.cs +++ b/src/Sentry/AttributeReader.cs @@ -2,6 +2,25 @@ namespace Sentry; internal static class AttributeReader { - public static string? TryGetProjectDirectory(Assembly assembly) => - assembly.GetCustomAttributes().FirstOrDefault(x => x.Key == "Sentry.ProjectDirectory")?.Value; + public static string? TryGetProjectDirectory(Assembly assembly) + { + // Use metadata (CustomAttributeData) to avoid hard-referencing AssemblyMetadataAttribute, + // which may be trimmed. This safely returns null if the attribute instances were removed. + // The constructor is: `AssemblyMetadataAttribute(string key, string? value);` + foreach (var cad in assembly.GetCustomAttributesData()) + { + var at = cad.AttributeType; + if (at is { Namespace: "System.Reflection", Name: "AssemblyMetadataAttribute" } + && cad.ConstructorArguments.Count == 2 + && cad.ConstructorArguments[0].ArgumentType == typeof(string) + && string.Equals(cad.ConstructorArguments[0].Value as string, "Sentry.ProjectDirectory", StringComparison.Ordinal) + && cad.ConstructorArguments[1].ArgumentType == typeof(string) + ) + { + return cad.ConstructorArguments[1].Value as string; + } + } + + return null; + } } 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/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")] diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index c560abff9b..850b3840d8 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -8,12 +8,12 @@ - net9.0;net8.0;netstandard2.1;netstandard2.0;net462 - $(TargetFrameworks);net9.0-android35.0;net8.0-android34.0 - $(TargetFrameworks);net9.0-ios18.0;net8.0-ios17.0 - $(TargetFrameworks);net9.0-maccatalyst18.0;net8.0-maccatalyst17.0 + $(CurrentTfms);netstandard2.1;netstandard2.0;net462 + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) + $(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm) + $(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm) - + @@ -25,8 +25,8 @@ true - - + + @@ -113,13 +113,13 @@ <_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) - - - - - - - + + + + + + + @@ -191,10 +191,11 @@ - + - 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/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/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/src/Sentry/buildTransitive/Sentry.targets b/src/Sentry/buildTransitive/Sentry.targets index 653cc294c0..c33ed0efbf 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()) @@ -217,7 +221,7 @@ $(SentryCLIUploadDirectory) - $(SentryCLIUploadItems) $(IntermediateOutputPath)linked/$(AssemblyName).pdb + $(SentryCLIUploadItems) $(IntermediateOutputPath)linked/*.pdb $(SentryCLIUploadItems) @(AndroidNativeSymbolFilesExceptDll -> '%(Identity)', ' ') @@ -267,9 +271,22 @@ - - + + + + + <_Parameter1>io.sentry.proguard-uuid + $(SentryProGuardUUID) + + + + + + diff --git a/test/AndroidTestApp/AndroidTestApp.csproj b/test/AndroidTestApp/AndroidTestApp.csproj index 3d8a1981e1..261fc05b24 100644 --- a/test/AndroidTestApp/AndroidTestApp.csproj +++ b/test/AndroidTestApp/AndroidTestApp.csproj @@ -1,6 +1,6 @@ - net8.0-android;net9.0-android + net10.0-android;net9.0-android false 21 Exe diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 20b6c7ac79..301d759ba4 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -11,6 +11,8 @@ $(NoWarn);SYSLIB0005;SYSLIB0012 $(NoWarn);IDE1006 + + $(NoWarn);ASPDEPR004;ASPDEPR008 false @@ -72,7 +74,7 @@ - + diff --git a/test/Sentry.Analyzers.Tests/Sentry.Analyzers.Tests.csproj b/test/Sentry.Analyzers.Tests/Sentry.Analyzers.Tests.csproj index 9750861210..c580d53e9d 100644 --- a/test/Sentry.Analyzers.Tests/Sentry.Analyzers.Tests.csproj +++ b/test/Sentry.Analyzers.Tests/Sentry.Analyzers.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + $(PreviousTfm) enable false diff --git a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs index 24344f2edd..f0d1a50c64 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs +++ b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs @@ -7,7 +7,9 @@ public class AndroidAssemblyReaderTests { private readonly ITestOutputHelper _output; -#if NET9_0 +#if NET10_0 + private static string TargetFramework => "net10.0"; +#elif NET9_0 private static string TargetFramework => "net9.0"; #elif NET8_0 private static string TargetFramework => "net8.0"; diff --git a/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt deleted file mode 100644 index 67bbca78b3..0000000000 --- a/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ /dev/null @@ -1,20 +0,0 @@ -namespace Sentry.Android.AssemblyReader -{ - public static class AndroidAssemblyReaderFactory - { - public static Sentry.Android.AssemblyReader.IAndroidAssemblyReader Open(string apkPath, System.Collections.Generic.IList supportedAbis, Sentry.Android.AssemblyReader.DebugLogger? logger = null) { } - } - public delegate void DebugLogger(Sentry.Android.AssemblyReader.DebugLoggerLevel level, string message, params object?[] args); - public enum DebugLoggerLevel : short - { - Debug = 0, - Info = 1, - Warning = 2, - Error = 3, - Fatal = 4, - } - public interface IAndroidAssemblyReader : System.IDisposable - { - System.Reflection.PortableExecutable.PEReader? TryReadAssembly(string name); - } -} \ No newline at end of file diff --git a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj index f30b490c91..8d50df6bec 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj +++ b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj @@ -1,9 +1,9 @@ - net9.0;net8.0 + net9.0 - $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) enable @@ -32,7 +32,7 @@ - + diff --git a/test/Sentry.AspNetCore.Grpc.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.AspNetCore.Grpc.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..60439aee0e --- /dev/null +++ b/test/Sentry.AspNetCore.Grpc.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,50 @@ +namespace Sentry.AspNetCore.Grpc +{ + public class DefaultProtobufRequestPayloadExtractor : Sentry.AspNetCore.Grpc.IProtobufRequestPayloadExtractor + { + public DefaultProtobufRequestPayloadExtractor() { } + public Google.Protobuf.IMessage ExtractPayload(Sentry.AspNetCore.Grpc.IProtobufRequest request) + where TRequest : class, Google.Protobuf.IMessage { } + } + public interface IProtobufRequestPayloadExtractor + { + Google.Protobuf.IMessage? ExtractPayload(Sentry.AspNetCore.Grpc.IProtobufRequest request) + where TRequest : class, Google.Protobuf.IMessage; + } + public interface IProtobufRequest + { + long? ContentLength { get; } + TRequest Request { get; } + } + public class ProtobufRequestExtractionDispatcher : Sentry.AspNetCore.Grpc.IProtobufRequestPayloadExtractor + { + public ProtobufRequestExtractionDispatcher(System.Collections.Generic.IEnumerable extractors, Sentry.SentryOptions options, System.Func sizeSwitch) { } + public Google.Protobuf.IMessage? ExtractPayload(Sentry.AspNetCore.Grpc.IProtobufRequest request) + where TRequest : class, Google.Protobuf.IMessage { } + } + public static class ScopeExtensions + { + public static void Populate(this Sentry.Scope scope, Grpc.Core.ServerCallContext context, TRequest? request, Sentry.AspNetCore.SentryAspNetCoreOptions options) + where TRequest : class { } + } + public static class SentryBuilderExtensions + { + public static Sentry.AspNetCore.ISentryBuilder AddGrpc(this Sentry.AspNetCore.ISentryBuilder builder) { } + } + public class SentryGrpcInterceptor : Grpc.Core.Interceptors.Interceptor + { + public SentryGrpcInterceptor(System.Func hubAccessor, Microsoft.Extensions.Options.IOptions options) { } + public override System.Threading.Tasks.Task ClientStreamingServerHandler(Grpc.Core.IAsyncStreamReader requestStream, Grpc.Core.ServerCallContext context, Grpc.Core.ClientStreamingServerMethod continuation) + where TRequest : class + where TResponse : class { } + public override System.Threading.Tasks.Task DuplexStreamingServerHandler(Grpc.Core.IAsyncStreamReader requestStream, Grpc.Core.IServerStreamWriter responseStream, Grpc.Core.ServerCallContext context, Grpc.Core.DuplexStreamingServerMethod continuation) + where TRequest : class + where TResponse : class { } + public override System.Threading.Tasks.Task ServerStreamingServerHandler(TRequest request, Grpc.Core.IServerStreamWriter responseStream, Grpc.Core.ServerCallContext context, Grpc.Core.ServerStreamingServerMethod continuation) + where TRequest : class + where TResponse : class { } + public override System.Threading.Tasks.Task UnaryServerHandler(TRequest request, Grpc.Core.ServerCallContext context, Grpc.Core.UnaryServerMethod continuation) + where TRequest : class + where TResponse : class { } + } +} \ No newline at end of file diff --git a/test/Sentry.AspNetCore.Grpc.Tests/Sentry.AspNetCore.Grpc.Tests.csproj b/test/Sentry.AspNetCore.Grpc.Tests/Sentry.AspNetCore.Grpc.Tests.csproj index 24227ac339..57dd94d734 100644 --- a/test/Sentry.AspNetCore.Grpc.Tests/Sentry.AspNetCore.Grpc.Tests.csproj +++ b/test/Sentry.AspNetCore.Grpc.Tests/Sentry.AspNetCore.Grpc.Tests.csproj @@ -1,13 +1,18 @@ - net9.0;net8.0 + $(CurrentTfms) - - - + + + + + + + + diff --git a/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj b/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj index c60ea32096..e93320ccdf 100644 --- a/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj +++ b/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj @@ -1,7 +1,7 @@ - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 false @@ -54,7 +54,14 @@ - + + + + + + + + diff --git a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..5f9eab70e9 --- /dev/null +++ b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,83 @@ +namespace Microsoft.AspNetCore.Builder +{ + public static class SentryTracingMiddlewareExtensions + { + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseSentryTracing(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder) { } + } +} +namespace Microsoft.AspNetCore.Hosting +{ + public static class SentryWebHostBuilderExtensions + { + public static void AddSentryTunneling(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, params string[] hostnames) { } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSentry(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) { } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSentry(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, System.Action? configureSentry) { } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSentry(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, System.Action? configureOptions) { } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSentry(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, System.Action? configureSentry) { } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSentry(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, System.Action? configureOptions) { } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseSentry(this Microsoft.AspNetCore.Hosting.IWebHostBuilder builder, string dsn) { } + public static void UseSentryTunneling(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/tunnel") { } + } +} +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static Sentry.AspNetCore.ISentryBuilder AddSentry(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { } + } +} +namespace Sentry.AspNetCore +{ + public interface ISentryBuilder + { + Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } + } + public static class SamplingExtensions + { + public static Microsoft.AspNetCore.Http.HttpContext? TryGetHttpContext(this Sentry.TransactionSamplingContext samplingContext) { } + public static string? TryGetHttpMethod(this Sentry.TransactionSamplingContext samplingContext) { } + public static string? TryGetHttpPath(this Sentry.TransactionSamplingContext samplingContext) { } + public static string? TryGetHttpRoute(this Sentry.TransactionSamplingContext samplingContext) { } + } + public static class ScopeExtensions + { + public static void Populate(this Sentry.Scope scope, System.Diagnostics.Activity activity) { } + public static void Populate(this Sentry.Scope scope, Microsoft.AspNetCore.Http.HttpContext context, Sentry.AspNetCore.SentryAspNetCoreOptions options) { } + } + [Microsoft.Extensions.Logging.ProviderAlias("Sentry")] + public class SentryAspNetCoreLoggerProvider : Sentry.Extensions.Logging.SentryLoggerProvider + { + public SentryAspNetCoreLoggerProvider(Microsoft.Extensions.Options.IOptions options, Sentry.IHub hub) { } + } + public class SentryAspNetCoreOptions : Sentry.Extensions.Logging.SentryLoggingOptions + { + public SentryAspNetCoreOptions() { } + public bool AdjustStandardEnvironmentNameCasing { get; set; } + public bool AutoRegisterTracing { get; set; } + public bool CaptureBlockingCalls { get; set; } + public bool FlushOnCompletedRequest { get; set; } + public bool IncludeActivityData { get; set; } + public Sentry.Extensibility.RequestSize MaxRequestBodySize { get; set; } + public Sentry.AspNetCore.TransactionNameProvider? TransactionNameProvider { get; set; } + } + public class SentryAspNetCoreOptionsSetup : Microsoft.Extensions.Options.IConfigureOptions + { + public SentryAspNetCoreOptionsSetup(Microsoft.Extensions.Logging.Configuration.ILoggerProviderConfiguration providerConfiguration) { } + public void Configure(Sentry.AspNetCore.SentryAspNetCoreOptions options) { } + } + public static class SentryBuilderExtensions + { + public static Sentry.AspNetCore.ISentryBuilder AddSentryOptions(this Sentry.AspNetCore.ISentryBuilder builder, System.Action? configureOptions) { } + } + public class SentryStartupFilter : Microsoft.AspNetCore.Hosting.IStartupFilter + { + public SentryStartupFilter() { } + public System.Action Configure(System.Action next) { } + } + public class SentryTunnelMiddleware : Microsoft.AspNetCore.Http.IMiddleware + { + public SentryTunnelMiddleware(string[] allowedHosts) { } + public System.Threading.Tasks.Task InvokeAsync(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next) { } + } + public delegate string? TransactionNameProvider(Microsoft.AspNetCore.Http.HttpContext context); +} \ No newline at end of file diff --git a/test/Sentry.AspNetCore.Tests/Sentry.AspNetCore.Tests.csproj b/test/Sentry.AspNetCore.Tests/Sentry.AspNetCore.Tests.csproj index 0761e91aa6..77c4d3735e 100644 --- a/test/Sentry.AspNetCore.Tests/Sentry.AspNetCore.Tests.csproj +++ b/test/Sentry.AspNetCore.Tests/Sentry.AspNetCore.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet.verified.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet.verified.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet10_0.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet10_0.verified.txt new file mode 100644 index 0000000000..679694a306 --- /dev/null +++ b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.PreFlightIgnoresTransaction.DotNet10_0.verified.txt @@ -0,0 +1,14 @@ +{ + result: { + Status: 204 No Content, + Headers: { + Access-Control-Allow-Headers: origin, + Access-Control-Allow-Methods: GET, + Access-Control-Allow-Origin: * + }, + Content: { + Headers: {} + } + }, + Payloads: [] +} \ No newline at end of file diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet.DotNet8_0.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet.DotNet8_0.verified.txt deleted file mode 100644 index 9abeb1309b..0000000000 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet.DotNet8_0.verified.txt +++ /dev/null @@ -1,87 +0,0 @@ -{ - result: Hello world, - Payloads: [ - { - Source: { - Name: GET v1.1/Target, - NameSource: Route, - Platform: csharp, - Operation: http.server, - Description: , - Status: Ok, - IsSampled: true, - SampleRate: 1.0, - Request: { - Method: GET, - QueryString: - }, - Contexts: { - trace: { - Operation: http.server, - Description: , - Status: Ok, - IsSampled: true - } - }, - User: {}, - Environment: production, - Breadcrumbs: [ - { - Message: Request starting HTTP/1.1 GET http://localhost/v1.1/Target - - -, - Data: { - eventId: 1 - }, - Category: Microsoft.AspNetCore.Hosting.Diagnostics - }, - { - Message: Executing endpoint 'Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests)', - Data: { - eventId: ExecutingEndpoint - }, - Category: Microsoft.AspNetCore.Routing.EndpointMiddleware - }, - { - Message: Route matched with {action = "Method", controller = "Version"}. Executing controller action with signature System.String Method() on controller Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController (Sentry.AspNetCore.Tests)., - Data: { - eventId: ControllerActionExecuting - }, - Category: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker - }, - { - Message: Executing ObjectResult, writing value of type 'System.String'., - Data: { - eventId: ObjectResultExecuting - }, - Category: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor - }, - { - Message: Executed action Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method, - Data: { - eventId: ActionExecuted - }, - Category: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker - }, - { - Message: Executed endpoint 'Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests)', - Data: { - eventId: ExecutedEndpoint - }, - Category: Microsoft.AspNetCore.Routing.EndpointMiddleware - } - ], - Extra: { - http.request.method: GET, - http.response.status_code: 200 - }, - Tags: { - ActionId: Guid_1, - ActionName: Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests), - route.action: Method, - route.controller: Version, - route.version: 1.1 - }, - IsFinished: true - } - } - ] -} \ No newline at end of file diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet10_0.verified.txt similarity index 94% rename from test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.verified.txt rename to test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet10_0.verified.txt index b132b2f346..019cc1fb2d 100644 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.verified.txt +++ b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet10_0.verified.txt @@ -3,6 +3,7 @@ Payloads: [ { Source: { + Origin: auto.http.aspnetcore, Name: GET /v1.1/Target, NameSource: Route, Platform: csharp, @@ -18,16 +19,19 @@ Contexts: { trace: { Operation: http.server, + Origin: auto.http.aspnetcore, Description: , Status: Ok, IsSampled: true } }, - User: {}, + User: { + Id: Guid_1 + }, Environment: production, Breadcrumbs: [ { - Message: Request starting HTTP/1.1 GET http://localhost/v1.1/Target - -, + Message: Request starting HTTP/1.1 GET http://localhost/v1.1/Target - - -, Data: { eventId: 1 }, @@ -69,12 +73,8 @@ Category: Microsoft.AspNetCore.Routing.EndpointMiddleware } ], - Extra: { - http.request.method: GET, - http.response.status_code: 200 - }, Tags: { - ActionId: Guid_1, + ActionId: Guid_2, ActionName: Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests), route.action: Method, route.controller: Version, diff --git a/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..f6ee752df8 --- /dev/null +++ b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,15 @@ +namespace Sentry.Azure.Functions.Worker +{ + public class SentryAzureFunctionsOptions : Sentry.Extensions.Logging.SentryLoggingOptions + { + public SentryAzureFunctionsOptions() { } + } + public static class SentryFunctionsWorkerApplicationBuilderExtensions + { + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, string dsn) { } + } +} \ No newline at end of file diff --git a/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt new file mode 100644 index 0000000000..f6ee752df8 --- /dev/null +++ b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -0,0 +1,15 @@ +namespace Sentry.Azure.Functions.Worker +{ + public class SentryAzureFunctionsOptions : Sentry.Extensions.Logging.SentryLoggingOptions + { + public SentryAzureFunctionsOptions() { } + } + public static class SentryFunctionsWorkerApplicationBuilderExtensions + { + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, string dsn) { } + } +} \ No newline at end of file diff --git a/test/Sentry.Azure.Functions.Worker.Tests/Sentry.Azure.Functions.Worker.Tests.csproj b/test/Sentry.Azure.Functions.Worker.Tests/Sentry.Azure.Functions.Worker.Tests.csproj index eee27b2b21..7e2f3e1603 100644 --- a/test/Sentry.Azure.Functions.Worker.Tests/Sentry.Azure.Functions.Worker.Tests.csproj +++ b/test/Sentry.Azure.Functions.Worker.Tests/Sentry.Azure.Functions.Worker.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/LocalDbFixture.cs b/test/Sentry.DiagnosticSource.IntegrationTests/LocalDbFixture.cs index 0e1a01e200..8afd8df752 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/LocalDbFixture.cs +++ b/test/Sentry.DiagnosticSource.IntegrationTests/LocalDbFixture.cs @@ -15,6 +15,8 @@ public sealed class LocalDbFixture : IDisposable "SqlListenerTests8"; #elif NET9_0 "SqlListenerTests9"; +#elif NET10_0 + "SqlListenerTests10"; #else #error Needs a version specific name to prevent the tests from tripping over one another when running in parallel #endif diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/Sentry.DiagnosticSource.IntegrationTests.csproj b/test/Sentry.DiagnosticSource.IntegrationTests/Sentry.DiagnosticSource.IntegrationTests.csproj index 504be0b959..62d67cadbb 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/Sentry.DiagnosticSource.IntegrationTests.csproj +++ b/test/Sentry.DiagnosticSource.IntegrationTests/Sentry.DiagnosticSource.IntegrationTests.csproj @@ -1,11 +1,18 @@  - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 + + + + + + + @@ -29,8 +36,6 @@ - - diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet10_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet10_0.verified.txt new file mode 100644 index 0000000000..348b185b6f --- /dev/null +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet10_0.verified.txt @@ -0,0 +1,63 @@ +[ + { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Ok, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Ok, + IsSampled: true + } + }, + User: { + Id: Guid_1 + }, + Spans: [ + { + IsFinished: true, + Operation: db.connection, + Description: SqlListenerTests.verify_LoggingAsync, + Status: Ok, + IsSampled: true, + Data: { + bytes_sent : 376, + db.connection_id: Guid_2, + db.name: SqlListenerTests.verify_LoggingAsync, + db.operation_id: Guid_3, + db.server: (LocalDb)\SqlListenerTests, + db.system: sql, + rows_sent: 0 + } + }, + { + IsFinished: true, + Operation: db.query, + Description: +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [TestEntities] ([Property]) +OUTPUT INSERTED.[Id] +VALUES (@p0); +, + Status: Ok, + IsSampled: true, + Data: { + db.connection_id: Guid_2, + db.name: SqlListenerTests.verify_LoggingAsync, + db.operation_id: Guid_4, + db.system: sql + } + } + ], + IsFinished: true + } + } +] \ No newline at end of file diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt new file mode 100644 index 0000000000..05803effbe --- /dev/null +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt @@ -0,0 +1,131 @@ +[ + { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: my exception + }, + Platform: csharp, + SentryExceptions: [ + { + Type: System.Exception, + Value: my exception, + Mechanism: { + Type: generic, + Synthetic: false, + IsExceptionGroup: false, + Data: { + HResult: 0x80131500 + } + } + } + ], + Level: error, + TransactionName: my transaction, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: Guid_1 + } + } + }, + { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Ok, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Ok, + IsSampled: true + } + }, + User: { + Id: Guid_1 + }, + Breadcrumbs: [ + { + Message: my exception, + Category: Exception, + Level: critical + } + ], + Spans: [ + { + IsFinished: true, + Operation: db.connection, + Description: SqlListenerTests.verify_RecordsEfAsync, + Status: Ok, + IsSampled: true, + Data: { + bytes_received: 225, + bytes_sent : 570, + db.connection_id: Guid_2, + db.name: SqlListenerTests.verify_RecordsEfAsync, + db.operation_id: Guid_3, + db.server: (LocalDb)\SqlListenerTests, + db.system: sql, + rows_sent: 1 + } + }, + { + IsFinished: true, + Operation: db.query, + Description: +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [TestEntities] ([Property]) +OUTPUT INSERTED.[Id] +VALUES (@p0); +, + Status: Ok, + IsSampled: true, + Data: { + db.connection_id: Guid_2, + db.name: SqlListenerTests.verify_RecordsEfAsync, + db.operation_id: Guid_4, + db.system: sql + } + }, + { + IsFinished: true, + Operation: db.query.compile, + Description: 'DbSet()', + Status: Ok, + IsSampled: true, + Data: { + db.system: mssql + } + }, + { + IsFinished: true, + Operation: db.query, + Description: +SELECT [t].[Id], [t].[Property] +FROM [TestEntities] AS [t], + Status: Ok, + IsSampled: true, + Data: { + db.connection_id: Guid_2, + db.name: SqlListenerTests.verify_RecordsEfAsync, + db.operation_id: Guid_5, + db.system: sql + } + } + ], + IsFinished: true + } + } +] \ No newline at end of file diff --git a/test/Sentry.DiagnosticSource.Tests/Sentry.DiagnosticSource.Tests.csproj b/test/Sentry.DiagnosticSource.Tests/Sentry.DiagnosticSource.Tests.csproj index cf4e670998..93ad42e99b 100644 --- a/test/Sentry.DiagnosticSource.Tests/Sentry.DiagnosticSource.Tests.csproj +++ b/test/Sentry.DiagnosticSource.Tests/Sentry.DiagnosticSource.Tests.csproj @@ -1,11 +1,17 @@  - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 + + + + + + @@ -16,8 +22,6 @@ - - diff --git a/test/Sentry.EntityFramework.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.EntityFramework.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..53cafe92cc --- /dev/null +++ b/test/Sentry.EntityFramework.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,39 @@ +namespace Sentry.EntityFramework.ErrorProcessors +{ + public class DbConcurrencyExceptionProcessor : Sentry.Extensibility.SentryEventExceptionProcessor + { + public DbConcurrencyExceptionProcessor() { } + protected override void ProcessException(System.Data.DBConcurrencyException exception, Sentry.SentryEvent sentryEvent) { } + } + public class DbEntityValidationExceptionProcessor : Sentry.Extensibility.SentryEventExceptionProcessor + { + public DbEntityValidationExceptionProcessor() { } + protected override void ProcessException(System.Data.Entity.Validation.DbEntityValidationException exception, Sentry.SentryEvent sentryEvent) { } + } +} +namespace Sentry.EntityFramework +{ + public interface IQueryLogger + { + void Log(string text, Sentry.BreadcrumbLevel level = -1); + } + public class SentryCommandInterceptor : System.Data.Entity.Infrastructure.Interception.IDbCommandInterceptor, System.Data.Entity.Infrastructure.Interception.IDbInterceptor + { + public SentryCommandInterceptor(Sentry.EntityFramework.IQueryLogger queryLogger) { } + public virtual void Log(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + public void NonQueryExecuted(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + public void NonQueryExecuting(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + public void ReaderExecuted(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + public void ReaderExecuting(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + public void ScalarExecuted(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + public void ScalarExecuting(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext) { } + } +} +namespace Sentry +{ + public static class SentryOptionsExtensions + { + public static Sentry.SentryOptions AddEntityFramework(this Sentry.SentryOptions sentryOptions) { } + public static void DisableDbInterceptionIntegration(this Sentry.SentryOptions options) { } + } +} \ No newline at end of file diff --git a/test/Sentry.EntityFramework.Tests/Sentry.EntityFramework.Tests.csproj b/test/Sentry.EntityFramework.Tests/Sentry.EntityFramework.Tests.csproj index d81324346b..3a50957452 100644 --- a/test/Sentry.EntityFramework.Tests/Sentry.EntityFramework.Tests.csproj +++ b/test/Sentry.EntityFramework.Tests/Sentry.EntityFramework.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 diff --git a/test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..9112ddfffa --- /dev/null +++ b/test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,66 @@ +namespace Microsoft.Extensions.Logging +{ + public static class LoggingBuilderExtensions + { + public static Microsoft.Extensions.Logging.ILoggingBuilder AddSentry(this Microsoft.Extensions.Logging.ILoggingBuilder builder) { } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddSentry(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action? optionsConfiguration) { } + public static Microsoft.Extensions.Logging.ILoggingBuilder AddSentry(this Microsoft.Extensions.Logging.ILoggingBuilder builder, string dsn) { } + } + public static class SentryLoggerFactoryExtensions + { + public static Microsoft.Extensions.Logging.ILoggerFactory AddSentry(this Microsoft.Extensions.Logging.ILoggerFactory factory, System.Action? optionsConfiguration = null) { } + } +} +namespace Sentry.Extensions.Logging +{ + public class DelegateLogEntryFilter : Sentry.Extensions.Logging.ILogEntryFilter + { + public DelegateLogEntryFilter(System.Func filter) { } + public bool Filter(string categoryName, Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, System.Exception? exception) { } + } + public interface ILogEntryFilter + { + bool Filter(string categoryName, Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, System.Exception? exception); + } + public class MelDiagnosticLogger : Sentry.Extensibility.IDiagnosticLogger + { + public MelDiagnosticLogger(Microsoft.Extensions.Logging.ILogger logger, Sentry.SentryLevel level) { } + public bool IsEnabled(Sentry.SentryLevel level) { } + public void Log(Sentry.SentryLevel logLevel, string message, System.Exception? exception = null, params object?[] args) { } + } + [Microsoft.Extensions.Logging.ProviderAlias("Sentry")] + public class SentryLoggerProvider : Microsoft.Extensions.Logging.ILoggerProvider, System.IDisposable + { + public SentryLoggerProvider(Microsoft.Extensions.Options.IOptions options, Sentry.IHub hub) { } + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) { } + public void Dispose() { } + } + public class SentryLoggingOptions : Sentry.SentryOptions + { + public SentryLoggingOptions() { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public Sentry.Extensions.Logging.SentryLoggingOptions.SentryLoggingExperimentalOptions ExperimentalLogging { get; set; } + public bool InitializeSdk { get; set; } + public Microsoft.Extensions.Logging.LogLevel MinimumBreadcrumbLevel { get; set; } + public Microsoft.Extensions.Logging.LogLevel MinimumEventLevel { get; set; } + public void ConfigureScope(System.Action action) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public sealed class SentryLoggingExperimentalOptions + { + public Microsoft.Extensions.Logging.LogLevel MinimumLogLevel { get; set; } + } + } + public static class SentryLoggingOptionsExtensions + { + public static void AddLogEntryFilter(this Sentry.Extensions.Logging.SentryLoggingOptions options, Sentry.Extensions.Logging.ILogEntryFilter filter) { } + public static void AddLogEntryFilter(this Sentry.Extensions.Logging.SentryLoggingOptions options, System.Func filter) { } + } +} +namespace Sentry.Extensions.Logging.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddSentry(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) + where TOptions : Sentry.Extensions.Logging.SentryLoggingOptions, new () { } + } +} \ No newline at end of file diff --git a/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj b/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj index 27b9c73763..bced932f74 100644 --- a/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj +++ b/test/Sentry.Extensions.Logging.Tests/Sentry.Extensions.Logging.Tests.csproj @@ -1,10 +1,10 @@  - net9.0;net8.0 - $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 - $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0 - $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0 + $(CurrentTfms) + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) + $(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm) + $(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm) $(TargetFrameworks);net48 diff --git a/test/Sentry.Google.Cloud.Functions.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Google.Cloud.Functions.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..acc2519014 --- /dev/null +++ b/test/Sentry.Google.Cloud.Functions.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,10 @@ +namespace Google.Cloud.Functions.Framework +{ + public class SentryStartup : Google.Cloud.Functions.Hosting.FunctionsStartup + { + public SentryStartup() { } + public override void Configure(Microsoft.AspNetCore.Hosting.WebHostBuilderContext context, Microsoft.AspNetCore.Builder.IApplicationBuilder app) { } + public override void ConfigureLogging(Microsoft.AspNetCore.Hosting.WebHostBuilderContext context, Microsoft.Extensions.Logging.ILoggingBuilder logging) { } + public override void ConfigureServices(Microsoft.AspNetCore.Hosting.WebHostBuilderContext context, Microsoft.Extensions.DependencyInjection.IServiceCollection services) { } + } +} \ No newline at end of file diff --git a/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj b/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj index 6eb35fea95..fe3c87694b 100644 --- a/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj +++ b/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj @@ -1,7 +1,7 @@ - net9.0;net8.0 + $(CurrentTfms) diff --git a/test/Sentry.Hangfire.Tests/Sentry.Hangfire.Tests.csproj b/test/Sentry.Hangfire.Tests/Sentry.Hangfire.Tests.csproj index 15a9d7f0c8..d8848e1668 100644 --- a/test/Sentry.Hangfire.Tests/Sentry.Hangfire.Tests.csproj +++ b/test/Sentry.Hangfire.Tests/Sentry.Hangfire.Tests.csproj @@ -3,7 +3,7 @@ - net9.0;net8.0 + $(CurrentTfms) enable diff --git a/test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt similarity index 100% rename from test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt rename to test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt diff --git a/test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt deleted file mode 100644 index 117659776a..0000000000 --- a/test/Sentry.Log4Net.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ /dev/null @@ -1,14 +0,0 @@ -[assembly: System.CLSCompliant(true)] -namespace Sentry.Log4Net -{ - public class SentryAppender : log4net.Appender.AppenderSkeleton - { - public SentryAppender() { } - public string? Dsn { get; set; } - public string? Environment { get; set; } - public log4net.Core.Level? MinimumEventLevel { get; set; } - public bool SendIdentity { get; set; } - protected override void Append(log4net.Core.LoggingEvent loggingEvent) { } - protected override void OnClose() { } - } -} \ No newline at end of file diff --git a/test/Sentry.Log4Net.Tests/Sentry.Log4Net.Tests.csproj b/test/Sentry.Log4Net.Tests/Sentry.Log4Net.Tests.csproj index 9939555343..afce541b0e 100644 --- a/test/Sentry.Log4Net.Tests/Sentry.Log4Net.Tests.csproj +++ b/test/Sentry.Log4Net.Tests/Sentry.Log4Net.Tests.csproj @@ -1,8 +1,7 @@  - - net8.0 + $(LatestTfm) $(TargetFrameworks);net48 diff --git a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj index 3ea49a432c..d4f16c1dd6 100644 --- a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj +++ b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj @@ -33,7 +33,7 @@ 13.0 21.0 + 'netX.0-ios' resolves the latest version of the iOS SDK otherwise. --> 18.0 true diff --git a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt similarity index 74% rename from test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt rename to test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 05a2136d62..244d13725f 100644 --- a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -1,4 +1,11 @@ -[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.sentry.io/maui", "Sentry.Maui")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Microsoft.Maui", AssemblyName="Microsoft.Maui")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Microsoft.Maui.Controls", AssemblyName="Microsoft.Maui.Controls")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Microsoft.Maui.Controls.Shapes", AssemblyName="Microsoft.Maui.Controls")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Microsoft.Maui.Controls.Xaml", AssemblyName="Microsoft.Maui.Controls.Xaml")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Microsoft.Maui.Graphics", AssemblyName="Microsoft.Maui.Graphics")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "http://schemas.microsoft.com/dotnet/2021/maui")] +[assembly: Microsoft.Maui.Controls.XmlnsDefinition("http://schemas.sentry.io/maui", "Sentry.Maui")] +[assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://schemas.microsoft.com/dotnet/maui/global", "global")] [assembly: Microsoft.Maui.Controls.XmlnsPrefix("http://schemas.sentry.io/maui", "sentry")] namespace Microsoft.Maui.Hosting { @@ -14,8 +21,8 @@ namespace Sentry.Maui public sealed class BreadcrumbEvent { public BreadcrumbEvent(object? sender, string eventName) { } - public BreadcrumbEvent(object? sender, string eventName, System.Collections.Generic.IEnumerable> extraData) { } - public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.ParamCollection] System.Collections.Generic.IEnumerable> extraData) { } + public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.ParamCollection] [System.Runtime.CompilerServices.TupleElementNames(new string[] { "key", "value"})] System.Collections.Generic.IEnumerable> extraData) { } [System.Obsolete("Use one of the other simpler constructors instead.")] diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.Page.cs b/test/Sentry.Maui.Tests/MauiEventsBinderTests.Page.cs index 222aced800..d361e5d340 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.Page.cs +++ b/test/Sentry.Maui.Tests/MauiEventsBinderTests.Page.cs @@ -66,10 +66,17 @@ public void Page_NavigatedTo_AddsBreadcrumb() { StyleId = "otherPage" }; +#if NET9_0 var navigatedToEventArgs = (NavigatedToEventArgs) typeof(NavigatedToEventArgs) - .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, new[] { typeof(Page) })! - .Invoke(new object[] { otherPage }); + .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, [typeof(Page)])! + .Invoke([otherPage]); +#elif NET10_0 + var navigatedToEventArgs = (NavigatedToEventArgs) + typeof(NavigatedToEventArgs) + .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, [typeof(Page), typeof(NavigationType)])! + .Invoke([otherPage, NavigationType.Replace]); +#endif // Act page.RaiseEvent(nameof(Page.NavigatedTo), navigatedToEventArgs); @@ -81,8 +88,11 @@ public void Page_NavigatedTo_AddsBreadcrumb() Assert.Equal(MauiEventsBinder.NavigationType, crumb.Type); Assert.Equal(MauiEventsBinder.NavigationCategory, crumb.Category); crumb.Data.Should().Contain($"{nameof(Page)}.Name", "page"); +#if !NET10_0 + // TODO: Work out why these are missing in .NET 10 crumb.Data.Should().Contain("PreviousPage", nameof(Page)); crumb.Data.Should().Contain("PreviousPage.Name", "otherPage"); +#endif } [Fact] @@ -99,10 +109,17 @@ public void Page_UnbindNavigatedTo_AddsBreadcrumb() { StyleId = "otherPage" }; +#if NET9_0 + var navigatedToEventArgs = (NavigatedToEventArgs) + typeof(NavigatedToEventArgs) + .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, [typeof(Page)])! + .Invoke([otherPage]); +#elif NET10_0 var navigatedToEventArgs = (NavigatedToEventArgs) typeof(NavigatedToEventArgs) - .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, new[] { typeof(Page) })! - .Invoke(new object[] { otherPage }); + .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, [typeof(Page), typeof(NavigationType)])! + .Invoke([otherPage, NavigationType.Replace]); +#endif page.RaiseEvent(nameof(Page.NavigatedTo), navigatedToEventArgs); Assert.Single(_fixture.Scope.Breadcrumbs); // Sanity check @@ -117,7 +134,7 @@ public void Page_UnbindNavigatedTo_AddsBreadcrumb() } [Fact] - public void Page_LayoutChanged_AddsBreadcrumb() + public void Page_SizeChanged_AddsBreadcrumb() { // Arrange var page = new Page @@ -127,11 +144,11 @@ public void Page_LayoutChanged_AddsBreadcrumb() _fixture.Binder.HandlePageEvents(page); // Act - page.RaiseEvent(nameof(Page.LayoutChanged), EventArgs.Empty); + page.RaiseEvent(nameof(Page.SizeChanged), EventArgs.Empty); // Assert var crumb = Assert.Single(_fixture.Scope.Breadcrumbs); - Assert.Equal($"{nameof(Page)}.{nameof(Page.LayoutChanged)}", crumb.Message); + Assert.Equal($"{nameof(Page)}.{nameof(Page.SizeChanged)}", crumb.Message); Assert.Equal(BreadcrumbLevel.Info, crumb.Level); Assert.Equal(MauiEventsBinder.SystemType, crumb.Type); Assert.Equal(MauiEventsBinder.RenderingCategory, crumb.Category); diff --git a/test/Sentry.Maui.Tests/Sentry.Maui.Tests.csproj b/test/Sentry.Maui.Tests/Sentry.Maui.Tests.csproj index ba3c3fbd09..45ba512c3f 100644 --- a/test/Sentry.Maui.Tests/Sentry.Maui.Tests.csproj +++ b/test/Sentry.Maui.Tests/Sentry.Maui.Tests.csproj @@ -1,10 +1,10 @@ - net9.0;net8.0 - $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 - $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0 - $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0 + $(LatestTfm);$(PreviousTfm) + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) + $(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm) + $(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm) true diff --git a/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.Android.cs b/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.Android.cs index 1b98060351..0db650f8a6 100644 --- a/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.Android.cs +++ b/test/Sentry.Maui.Tests/SentryMauiAppBuilderExtensionsTests.Android.cs @@ -1,7 +1,7 @@ +using Android.Runtime; using Microsoft.Maui.LifecycleEvents; using Sentry.Maui.Internal; using Sentry.Maui.Tests.Mocks; - namespace Sentry.Maui.Tests; public partial class SentryMauiAppBuilderExtensionsTests @@ -29,17 +29,22 @@ public void UseSentry_BindsToApplicationStartupEvent_Android() // Assert binder.Received(1).HandleApplicationEvents(application); } - private class MockAndroidApplication : global::Android.App.Application, IPlatformApplication { + // Required JNI activation constructor for Android runtime types + public MockAndroidApplication(IntPtr handle, JniHandleOwnership transfer) + : base(handle, transfer) + { + } + public MockAndroidApplication(IApplication application, IServiceProvider services) { Application = application; Services = services; } - public IApplication Application { get; } - public IServiceProvider Services { get; } + } + } diff --git a/test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..00a36bc53b --- /dev/null +++ b/test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,81 @@ +[assembly: System.CLSCompliant(true)] +namespace NLog +{ + public static class ConfigurationExtensions + { + public static NLog.Config.LoggingConfiguration AddSentry(this NLog.Config.LoggingConfiguration configuration, System.Action? optionsConfig = null) { } + public static NLog.Config.LoggingConfiguration AddSentry(this NLog.Config.LoggingConfiguration configuration, string? dsn, System.Action? optionsConfig = null) { } + public static NLog.Config.LoggingConfiguration AddSentry(this NLog.Config.LoggingConfiguration configuration, string? dsn, string targetName, System.Action? optionsConfig = null) { } + public static void AddTag(this Sentry.NLog.SentryNLogOptions options, string name, NLog.Layouts.Layout layout) { } + } +} +namespace Sentry.NLog +{ + [NLog.Config.NLogConfigurationItem] + public class SentryNLogOptions : Sentry.SentryOptions + { + public SentryNLogOptions() { } + [NLog.Config.NLogConfigurationIgnoreProperty] + public NLog.Layouts.Layout? BreadcrumbCategoryLayout { get; set; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public NLog.Layouts.Layout? BreadcrumbLayout { get; set; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public NLog.Layouts.Layout? DsnLayout { get; set; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public NLog.Layouts.Layout? EnvironmentLayout { get; set; } + public bool IgnoreEventsWithNoException { get; set; } + public bool IncludeEventDataOnBreadcrumbs { get; set; } + public bool IncludeEventPropertiesAsTags { get; set; } + public bool InitializeSdk { get; set; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public NLog.Layouts.Layout? Layout { get; set; } + public NLog.LogLevel? MinimumBreadcrumbLevel { get; set; } + public NLog.LogLevel? MinimumEventLevel { get; set; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public NLog.Layouts.Layout? ReleaseLayout { get; set; } + public int ShutdownTimeoutSeconds { get; set; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public System.Collections.Generic.IList Tags { get; } + [NLog.Config.NLogConfigurationIgnoreProperty] + public Sentry.NLog.SentryNLogUser? User { get; set; } + } + [NLog.Config.NLogConfigurationItem] + public class SentryNLogUser + { + public SentryNLogUser() { } + public NLog.Layouts.Layout? Email { get; set; } + public NLog.Layouts.Layout? Id { get; set; } + public NLog.Layouts.Layout? IpAddress { get; set; } + [NLog.Config.ArrayParameter(typeof(NLog.Targets.TargetPropertyWithContext?), "other")] + public System.Collections.Generic.IList? Other { get; } + public NLog.Layouts.Layout? Segment { get; set; } + public NLog.Layouts.Layout? Username { get; set; } + } + [NLog.Targets.Target("Sentry")] + public sealed class SentryTarget : NLog.Targets.TargetWithContext + { + public SentryTarget() { } + public SentryTarget(Sentry.NLog.SentryNLogOptions options) { } + public NLog.Layouts.Layout? BreadcrumbCategory { get; set; } + public NLog.Layouts.Layout? BreadcrumbLayout { get; set; } + public NLog.Layouts.Layout? Dsn { get; set; } + public NLog.Layouts.Layout? Environment { get; set; } + public int FlushTimeoutSeconds { get; set; } + public bool IgnoreEventsWithNoException { get; set; } + public bool IncludeEventDataOnBreadcrumbs { get; set; } + public bool IncludeEventPropertiesAsTags { get; set; } + public bool InitializeSdk { get; set; } + public string MinimumBreadcrumbLevel { get; set; } + public string MinimumEventLevel { get; set; } + public Sentry.NLog.SentryNLogOptions Options { get; } + public NLog.Layouts.Layout? Release { get; set; } + public int ShutdownTimeoutSeconds { get; set; } + [NLog.Config.ArrayParameter(typeof(NLog.Targets.TargetPropertyWithContext), "tag")] + public System.Collections.Generic.IList Tags { get; } + public Sentry.NLog.SentryNLogUser? User { get; set; } + protected override void CloseTarget() { } + protected override void FlushAsync(NLog.Common.AsyncContinuation asyncContinuation) { } + protected override void InitializeTarget() { } + protected override void Write(NLog.LogEventInfo logEvent) { } + } +} \ No newline at end of file diff --git a/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt b/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt new file mode 100644 index 0000000000..c1b263a0fb --- /dev/null +++ b/test/Sentry.NLog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt @@ -0,0 +1,109 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: test-release, + trace_id: Guid_2 + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message + }, + Message: { + Message: message = {arg}, + Formatted: message = "arg value" + }, + Logger: Sentry.NLog.Tests.IntegrationTests, + Platform: csharp, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + { + FileName: IntegrationTests.verify.cs, + Function: Task IntegrationTests.Simple(), + Module: null, + LineNumber: 52, + ColumnNumber: 17, + AbsolutePath: {ProjectDirectory}IntegrationTests.verify.cs, + ContextLine: null, + InApp: false, + Package: Sentry.NLog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED, + Platform: null, + ImageAddress: null, + SymbolAddress: null, + InstructionAddress: 2, + AddressMode: rel:0, + FunctionId: 1 + } + ] + }, + Mechanism: { + Type: generic, + Handled: true, + Synthetic: false, + IsExceptionGroup: false, + Data: { + HResult: 0x80131500 + } + } + } + ], + DebugImages: [ + { + Type: pe_dotnet, + ImageAddress: null, + ImageSize: null, + DebugId: ________-____-____-____-____________-________, + DebugChecksum: ______:________________________________________________________________, + DebugFile: .../Sentry.NLog.Tests.pdb, + CodeId: ______________, + CodeFile: .../Sentry.NLog.Tests.dll + } + ], + Level: error, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: myId, + Username: , + Email: , + IpAddress: , + Other: { + mood: joyous + } + }, + Environment: production, + Extra: { + arg: arg value + }, + Tags: { + logger: Sentry.NLog.Tests.IntegrationTests + } + } + } + } + ] + } +] \ No newline at end of file diff --git a/test/Sentry.NLog.Tests/Sentry.NLog.Tests.csproj b/test/Sentry.NLog.Tests/Sentry.NLog.Tests.csproj index bc003e6461..d7f32032ed 100644 --- a/test/Sentry.NLog.Tests/Sentry.NLog.Tests.csproj +++ b/test/Sentry.NLog.Tests/Sentry.NLog.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 diff --git a/test/Sentry.OpenTelemetry.Tests/Sentry.OpenTelemetry.Tests.csproj b/test/Sentry.OpenTelemetry.Tests/Sentry.OpenTelemetry.Tests.csproj index c5d402cc2e..5371e42135 100644 --- a/test/Sentry.OpenTelemetry.Tests/Sentry.OpenTelemetry.Tests.csproj +++ b/test/Sentry.OpenTelemetry.Tests/Sentry.OpenTelemetry.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 @@ -11,13 +11,13 @@ - - + + - - + + diff --git a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs index c4e5ae71c3..1832e88085 100644 --- a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs +++ b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs @@ -233,6 +233,8 @@ private static long MethodToBeLoaded(int n) [InlineData(false)] public void ProfilerIntegration_FullRoundtrip_Works(bool offlineCaching) { + Skip.If(TestEnvironment.IsGitHubActions, "Flaky in CI"); + var tcs = new TaskCompletionSource(); async Task VerifyAsync(HttpRequestMessage message) { diff --git a/test/Sentry.Profiling.Tests/Sentry.Profiling.Tests.csproj b/test/Sentry.Profiling.Tests/Sentry.Profiling.Tests.csproj index 932f6231e1..43d28fbab3 100644 --- a/test/Sentry.Profiling.Tests/Sentry.Profiling.Tests.csproj +++ b/test/Sentry.Profiling.Tests/Sentry.Profiling.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) enable diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..f204ed0701 --- /dev/null +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,52 @@ +[assembly: System.CLSCompliant(true)] +namespace Sentry.Serilog +{ + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } + public class SentrySerilogOptions : Sentry.SentryOptions + { + public SentrySerilogOptions() { } + public System.IFormatProvider? FormatProvider { get; set; } + public bool InitializeSdk { get; set; } + public Serilog.Events.LogEventLevel MinimumBreadcrumbLevel { get; set; } + public Serilog.Events.LogEventLevel MinimumEventLevel { get; set; } + public Serilog.Formatting.ITextFormatter? TextFormatter { get; set; } + } +} +namespace Serilog +{ + public static class SentrySinkExtensions + { + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action configureOptions) { } + public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { } + public static Serilog.LoggerConfiguration Sentry( + this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, + string dsn, + Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, + Serilog.Events.LogEventLevel? minimumEventLevel = default, + System.IFormatProvider? formatProvider = null, + Serilog.Formatting.ITextFormatter? textFormatter = null, + bool? sendDefaultPii = default, + bool? isEnvironmentUser = default, + string? serverName = null, + bool? attachStackTrace = default, + int? maxBreadcrumbs = default, + float? sampleRate = default, + string? release = null, + string? environment = null, + int? maxQueueItems = default, + System.TimeSpan? shutdownTimeout = default, + System.Net.DecompressionMethods? decompressionMethods = default, + System.IO.Compression.CompressionLevel? requestBodyCompressionLevel = default, + bool? requestBodyCompressionBuffered = default, + bool? debug = default, + Sentry.SentryLevel? diagnosticLevel = default, + Sentry.ReportAssembliesMode? reportAssembliesMode = default, + Sentry.DeduplicateMode? deduplicateMode = default, + System.Collections.Generic.Dictionary? defaultTags = null, + bool? experimentalEnableLogs = default) { } + } +} \ No newline at end of file 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.Simple.DotNet10_0.verified.txt b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt new file mode 100644 index 0000000000..e426868eab --- /dev/null +++ b/test/Sentry.Serilog.Tests/IntegrationTests.Simple.DotNet10_0.verified.txt @@ -0,0 +1,281 @@ +[ + { + Header: { + event_id: Guid_1, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: test-release, + trace_id: Guid_2 + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Debug message stored as breadcrumb., + Formatted: [42] Debug message stored as breadcrumb. + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: Guid_3, + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_4, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: test-release, + trace_id: Guid_2 + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Message with a different MyTaskId, + Formatted: [65] Message with a different MyTaskId + }, + Platform: csharp, + ServerName: TheMachineName, + Level: debug, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: Guid_3, + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 65 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_5, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: test-release, + trace_id: Guid_2 + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Message: { + Message: Some event that includes the previous breadcrumbs, + Formatted: [42] Some event that includes the previous breadcrumbs + }, + Platform: csharp, + ServerName: TheMachineName, + Level: error, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: Guid_3, + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + }, + { + Header: { + event_id: Guid_6, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: test-release, + trace_id: Guid_2 + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Exception: { + $type: Exception, + Type: Exception, + Message: Exception message, + Data: { + details: Do work always throws. + } + }, + Message: { + Message: Error: with exception, + Formatted: [42] Error: with exception + }, + Platform: csharp, + ServerName: TheMachineName, + SentryExceptions: [ + { + Type: System.Exception, + Value: Exception message, + Stacktrace: { + Frames: [ + { + FileName: IntegrationTests.verify.cs, + Function: Task IntegrationTests.Simple(), + Module: null, + LineNumber: 47, + ColumnNumber: 17, + AbsolutePath: {ProjectDirectory}IntegrationTests.verify.cs, + ContextLine: null, + InApp: false, + Package: Sentry.Serilog.Tests, Version=SCRUBBED, Culture=SCRUBBED, PublicKeyToken=SCRUBBED, + Platform: null, + ImageAddress: null, + SymbolAddress: null, + InstructionAddress: 2, + AddressMode: rel:0, + FunctionId: 1 + } + ] + }, + Mechanism: { + Type: generic, + Handled: true, + Synthetic: false, + IsExceptionGroup: false, + Data: { + details: Do work always throws., + HResult: 0x80131500 + } + } + } + ], + DebugImages: [ + { + Type: pe_dotnet, + ImageAddress: null, + ImageSize: null, + DebugId: ________-____-____-____-____________-________, + DebugChecksum: ______:________________________________________________________________, + DebugFile: .../Sentry.Serilog.Tests.pdb, + CodeId: ______________, + CodeFile: .../Sentry.Serilog.Tests.dll + } + ], + Level: fatal, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: Guid_3, + Username: TheUserName, + IpAddress: {{auto}} + }, + Environment: production, + Breadcrumbs: [ + { + Message: [42] Debug message stored as breadcrumb., + Level: debug + }, + { + Message: [65] Message with a different MyTaskId, + Level: debug + }, + { + Message: [42] Some event that includes the previous breadcrumbs, + Level: error + } + ], + Extra: { + inventory: { SmallPotion = 3, BigPotion = 0, CheeseWheels = 512 }, + MyTaskId: 42 + } + } + } + } + ] + } +] \ No newline at end of file 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/Sentry.Serilog.Tests.csproj b/test/Sentry.Serilog.Tests/Sentry.Serilog.Tests.csproj index 5bf555ed06..c4ed3357bb 100644 --- a/test/Sentry.Serilog.Tests/Sentry.Serilog.Tests.csproj +++ b/test/Sentry.Serilog.Tests/Sentry.Serilog.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) $(TargetFrameworks);net48 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.SourceGenerators.Tests/Sentry.SourceGenerators.Tests.csproj b/test/Sentry.SourceGenerators.Tests/Sentry.SourceGenerators.Tests.csproj index 820c77dd05..05ff9fa4ae 100644 --- a/test/Sentry.SourceGenerators.Tests/Sentry.SourceGenerators.Tests.csproj +++ b/test/Sentry.SourceGenerators.Tests/Sentry.SourceGenerators.Tests.csproj @@ -1,7 +1,7 @@  - net9.0;net8.0 + $(CurrentTfms) enable enable diff --git a/test/Sentry.Testing.CrashableApp/Sentry.Testing.CrashableApp.csproj b/test/Sentry.Testing.CrashableApp/Sentry.Testing.CrashableApp.csproj index 227a7e1f0d..da0c8e2588 100644 --- a/test/Sentry.Testing.CrashableApp/Sentry.Testing.CrashableApp.csproj +++ b/test/Sentry.Testing.CrashableApp/Sentry.Testing.CrashableApp.csproj @@ -2,7 +2,7 @@ Exe - net9.0;net8.0;net48 + $(CurrentTfms);net48 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.Testing/Sentry.Testing.csproj b/test/Sentry.Testing/Sentry.Testing.csproj index 8fbe7f01f1..ef007c56b8 100644 --- a/test/Sentry.Testing/Sentry.Testing.csproj +++ b/test/Sentry.Testing/Sentry.Testing.csproj @@ -1,10 +1,10 @@  - net9.0;net8.0;net48 - $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 - $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0 - $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0 + $(CurrentTfms);net48 + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) + $(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm) + $(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm) false diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..b83e9340d7 --- /dev/null +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,1978 @@ +[assembly: System.CLSCompliant(true)] +namespace Sentry +{ + public enum AttachmentType + { + Default = 0, + Minidump = 1, + AppleCrashReport = 2, + UnrealContext = 3, + UnrealLogs = 4, + ViewHierarchy = 5, + } + public class BaggageHeader + { + public override string ToString() { } + } + [System.Diagnostics.DebuggerDisplay("Message: {Message}, Type: {Type}")] + public sealed class Breadcrumb : Sentry.ISentryJsonSerializable + { + public Breadcrumb(string message, string type, System.Collections.Generic.IReadOnlyDictionary? data = null, string? category = null, Sentry.BreadcrumbLevel level = 0) { } + public string? Category { get; } + public System.Collections.Generic.IReadOnlyDictionary? Data { get; } + public Sentry.BreadcrumbLevel Level { get; } + public string? Message { get; } + public System.DateTimeOffset Timestamp { get; } + public string? Type { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Breadcrumb FromJson(System.Text.Json.JsonElement json) { } + } + public enum BreadcrumbLevel + { + [System.Runtime.Serialization.EnumMember(Value="debug")] + Debug = -1, + [System.Runtime.Serialization.EnumMember(Value="info")] + Info = 0, + [System.Runtime.Serialization.EnumMember(Value="warning")] + Warning = 1, + [System.Runtime.Serialization.EnumMember(Value="error")] + Error = 2, + [System.Runtime.Serialization.EnumMember(Value="critical")] + Critical = 3, + } + public class ByteAttachmentContent : Sentry.IAttachmentContent + { + public ByteAttachmentContent(byte[] bytes) { } + public System.IO.Stream GetStream() { } + } + public enum CheckInStatus + { + InProgress = 0, + Ok = 1, + Error = 2, + } + [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + + "l application.")] + public enum CrashType + { + Managed = 0, + ManagedBackgroundThread = 1, + Native = 2, + } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public class Debouncer + { + public static Sentry.Debouncer PerApplicationLifetime(int eventMaximum = 1, System.TimeSpan? cooldown = default) { } + public static Sentry.Debouncer PerDay(int eventMaximum = 1, System.TimeSpan? cooldown = default) { } + public static Sentry.Debouncer PerHour(int eventMaximum = 1, System.TimeSpan? cooldown = default) { } + public static Sentry.Debouncer PerMinute(int eventMaximum = 1, System.TimeSpan? cooldown = default) { } + } + [System.Flags] + public enum DeduplicateMode + { + SameEvent = 1, + SameExceptionInstance = 2, + InnerException = 4, + AggregateException = 8, + All = 2147483647, + } + public class DefaultSentryScopeStateProcessor : Sentry.ISentryScopeStateProcessor + { + public DefaultSentryScopeStateProcessor() { } + public void Apply(Sentry.Scope scope, object state) { } + } + [System.AttributeUsage(System.AttributeTargets.Assembly)] + public class DsnAttribute : System.Attribute + { + public DsnAttribute(string dsn) { } + public string Dsn { get; } + } + public static class EventLikeExtensions + { + public static void AddBreadcrumb(this Sentry.IEventLike eventLike, string message, string? category, string? type, System.ValueTuple? dataPair = default, Sentry.BreadcrumbLevel level = 0) { } + public static void AddBreadcrumb(this Sentry.IEventLike eventLike, string message, string? category = null, string? type = null, System.Collections.Generic.IReadOnlyDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } + public static void AddBreadcrumb(this Sentry.IEventLike eventLike, System.DateTimeOffset? timestamp, string message, string? category = null, string? type = null, System.Collections.Generic.IReadOnlyDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } + public static bool HasUser(this Sentry.IEventLike eventLike) { } + public static void SetFingerprint(this Sentry.IEventLike eventLike, System.Collections.Generic.IEnumerable fingerprint) { } + public static void SetFingerprint(this Sentry.IEventLike eventLike, params string[] fingerprint) { } + } + public class FileAttachmentContent : Sentry.IAttachmentContent + { + public FileAttachmentContent(string filePath) { } + public FileAttachmentContent(string filePath, bool readFileAsynchronously) { } + public System.IO.Stream GetStream() { } + } + public static class HasExtraExtensions + { + public static void SetExtras(this Sentry.IHasExtra hasExtra, System.Collections.Generic.IEnumerable> values) { } + } + public static class HasTagsExtensions + { + public static void SetTags(this Sentry.IHasTags hasTags, System.Collections.Generic.IEnumerable> tags) { } + } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public delegate bool HeapDumpTrigger(long usedMemory, long totalMemory); + public static class HintTypes + { + public const string HttpResponseMessage = "http-response-message"; + } + public readonly struct HttpStatusCodeRange : System.IEquatable + { + public HttpStatusCodeRange(int statusCode) { } + public HttpStatusCodeRange(int start, int end) { } + public int End { get; init; } + public int Start { get; init; } + public bool Contains(int statusCode) { } + public bool Contains(System.Net.HttpStatusCode statusCode) { } + public static Sentry.HttpStatusCodeRange op_Implicit(int statusCode) { } + public static Sentry.HttpStatusCodeRange op_Implicit(System.Net.HttpStatusCode statusCode) { } + public static Sentry.HttpStatusCodeRange op_Implicit([System.Runtime.CompilerServices.TupleElementNames(new string[] { + "Start", + "End"})] System.ValueTuple range) { } + public static Sentry.HttpStatusCodeRange op_Implicit([System.Runtime.CompilerServices.TupleElementNames(new string[] { + "start", + "end"})] System.ValueTuple range) { } + } + public static class HubExtensions + { + public static void AddBreadcrumb(this Sentry.IHub hub, Sentry.Breadcrumb breadcrumb, Sentry.SentryHint? hint = null) { } + public static void AddBreadcrumb(this Sentry.IHub hub, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } + public static void AddBreadcrumb(this Sentry.IHub hub, Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } + public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, System.Action configureScope) { } + public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } + public static void LockScope(this Sentry.IHub hub) { } + public static System.IDisposable PushAndLockScope(this Sentry.IHub hub) { } + public static Sentry.ISpan StartSpan(this Sentry.IHub hub, string operation, string description) { } + public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, Sentry.ITransactionContext context) { } + public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, string name, string operation) { } + public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, string name, string operation, Sentry.SentryTraceHeader traceHeader) { } + public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, string name, string operation, string? description) { } + public static void UnlockScope(this Sentry.IHub hub) { } + } + public interface IAttachmentContent + { + System.IO.Stream GetStream(); + } + public interface IEventLike : Sentry.IHasExtra, Sentry.IHasTags + { + System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } + Sentry.SentryContexts Contexts { get; set; } + string? Distribution { get; set; } + string? Environment { get; set; } + System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } + Sentry.SentryLevel? Level { get; set; } + string? Release { get; set; } + Sentry.SentryRequest Request { get; set; } + Sentry.SdkVersion Sdk { get; } + string? TransactionName { get; set; } + Sentry.SentryUser User { get; set; } + void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); + } + public interface IHasData + { + System.Collections.Generic.IReadOnlyDictionary Data { get; } + void SetData(string key, object? value); + } + public interface IHasExtra + { + System.Collections.Generic.IReadOnlyDictionary Extra { get; } + void SetExtra(string key, object? value); + } + public interface IHasTags + { + System.Collections.Generic.IReadOnlyDictionary Tags { get; } + void SetTag(string key, string value); + void UnsetTag(string key); + } + 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); + Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); + void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null); + Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); + Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); + void EndSession(Sentry.SessionEndStatus status = 0); + Sentry.BaggageHeader? GetBaggage(); + Sentry.ISpan? GetSpan(); + Sentry.SentryTraceHeader? GetTraceHeader(); + void PauseSession(); + void ResumeSession(); + void StartSession(); + Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary customSamplingContext); + } + public interface IScopeObserver + { + void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); + void SetExtra(string key, object? value); + void SetTag(string key, string value); + void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId); + void SetUser(Sentry.SentryUser? user); + void UnsetTag(string key); + } + public interface ISentryClient + { + bool IsEnabled { get; } + Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null); + bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); + Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); + void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); + void CaptureSession(Sentry.SessionUpdate sessionUpdate); + void CaptureTransaction(Sentry.SentryTransaction transaction); + void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); + [System.Obsolete("Use CaptureFeedback instead.")] + void CaptureUserFeedback(Sentry.UserFeedback userFeedback); + System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); + } + public interface ISentryJsonSerializable + { + void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); + } + public interface ISentryScopeManager + { + void BindClient(Sentry.ISentryClient client); + void ConfigureScope(System.Action configureScope); + void ConfigureScope(System.Action configureScope, TArg arg); + System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope); + System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg); + System.IDisposable PushScope(); + System.IDisposable PushScope(TState state); + void SetTag(string key, string value); + void UnsetTag(string key); + } + public interface ISentryScopeStateProcessor + { + void Apply(Sentry.Scope scope, object state); + } + public interface ISentrySession + { + string? DistinctId { get; } + string? Environment { get; } + int ErrorCount { get; } + Sentry.SentryId Id { get; } + string? IpAddress { get; } + string Release { get; } + System.DateTimeOffset StartTimestamp { get; } + string? UserAgent { get; } + } + public interface ISentryUserFactory + { + Sentry.SentryUser? Create(); + } + public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext + { + new string? Description { get; set; } + new string Operation { get; set; } + new Sentry.SpanStatus? Status { get; set; } + void Finish(); + void Finish(Sentry.SpanStatus status); + void Finish(System.Exception exception); + void Finish(System.Exception exception, Sentry.SpanStatus status); + Sentry.ISpan StartChild(string operation); + } + public interface ISpanData : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.Protocol.ITraceContext + { + System.DateTimeOffset? EndTimestamp { get; } + bool IsFinished { get; } + System.Collections.Generic.IReadOnlyDictionary Measurements { get; } + System.DateTimeOffset StartTimestamp { get; } + Sentry.SentryTraceHeader GetTraceHeader(); + void SetMeasurement(string name, Sentry.Protocol.Measurement measurement); + } + public interface ITransactionContext : Sentry.Protocol.ITraceContext + { + bool? IsParentSampled { get; } + string Name { get; } + Sentry.TransactionNameSource NameSource { get; } + } + public interface ITransactionData : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext + { + string? Platform { get; set; } + } + public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext + { + new bool? IsParentSampled { get; set; } + new string Name { get; set; } + System.Collections.Generic.IReadOnlyCollection Spans { get; } + Sentry.ISpan? GetLastActiveSpan(); + } + public enum InstructionAddressAdjustment + { + Auto = 0, + All = 1, + AllButFirst = 2, + None = 3, + } + public enum Instrumenter + { + Sentry = 0, + OpenTelemetry = 1, + } + public readonly struct MeasurementUnit : System.IEquatable + { + public static Sentry.MeasurementUnit None; + public bool Equals(Sentry.MeasurementUnit other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public static Sentry.MeasurementUnit Custom(string name) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Duration unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Fraction unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Information unit) { } + public static bool operator !=(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public static bool operator ==(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public enum Duration + { + Nanosecond = 0, + Microsecond = 1, + Millisecond = 2, + Second = 3, + Minute = 4, + Hour = 5, + Day = 6, + Week = 7, + } + public enum Fraction + { + Ratio = 0, + Percent = 1, + } + public enum Information + { + Bit = 0, + Byte = 1, + Kilobyte = 2, + Kibibyte = 3, + Megabyte = 4, + Mebibyte = 5, + Gigabyte = 6, + Gibibyte = 7, + Terabyte = 8, + Tebibyte = 9, + Petabyte = 10, + Pebibyte = 11, + Exabyte = 12, + Exbibyte = 13, + } + } + public enum ReportAssembliesMode + { + None = 0, + Version = 1, + InformationalVersion = 2, + } + public class Scope : Sentry.IEventLike, Sentry.IHasExtra, Sentry.IHasTags + { + public Scope(Sentry.SentryOptions? options) { } + public System.Collections.Generic.IReadOnlyCollection Attachments { get; } + public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } + public Sentry.SentryContexts Contexts { get; set; } + public string? Distribution { get; set; } + public string? Environment { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Extra { get; } + public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } + public Sentry.SentryLevel? Level { get; set; } + public string? Release { get; set; } + public Sentry.SentryRequest Request { get; set; } + public Sentry.SdkVersion Sdk { get; } + public Sentry.ISpan? Span { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Tags { get; } + public Sentry.ITransactionTracer? Transaction { get; set; } + public string? TransactionName { get; set; } + public Sentry.SentryUser User { get; set; } + public void AddAttachment(Sentry.SentryAttachment attachment) { } + public void AddAttachment(string filePath, Sentry.AttachmentType type = 0, string? contentType = null) { } + public void AddAttachment(byte[] data, string fileName, Sentry.AttachmentType type = 0, string? contentType = null) { } + public void AddAttachment(System.IO.Stream stream, string fileName, Sentry.AttachmentType type = 0, string? contentType = null) { } + public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } + public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.SentryHint hint) { } + public void AddEventProcessor(Sentry.Extensibility.ISentryEventProcessor processor) { } + public void AddEventProcessor(System.Func processor) { } + public void AddEventProcessors(System.Collections.Generic.IEnumerable processors) { } + public void AddExceptionProcessor(Sentry.Extensibility.ISentryEventExceptionProcessor processor) { } + public void AddExceptionProcessors(System.Collections.Generic.IEnumerable processors) { } + public void AddTransactionProcessor(Sentry.Extensibility.ISentryTransactionProcessor processor) { } + public void AddTransactionProcessor(System.Func processor) { } + public void AddTransactionProcessors(System.Collections.Generic.IEnumerable processors) { } + public void Apply(Sentry.IEventLike other) { } + public void Apply(Sentry.Scope other) { } + public void Apply(object state) { } + public void Clear() { } + public void ClearAttachments() { } + public void ClearBreadcrumbs() { } + public Sentry.Scope Clone() { } + public System.Collections.Generic.IEnumerable GetAllEventProcessors() { } + public System.Collections.Generic.IEnumerable GetAllExceptionProcessors() { } + public System.Collections.Generic.IEnumerable GetAllTransactionProcessors() { } + public void SetExtra(string key, object? value) { } + public void SetTag(string key, string value) { } + public void UnsetTag(string key) { } + } + public sealed class SdkVersion : Sentry.ISentryJsonSerializable + { + public SdkVersion() { } + public string? Name { get; set; } + public System.Collections.Generic.IEnumerable Packages { get; } + public string? Version { get; set; } + public void AddIntegration(string integration) { } + public void AddPackage(string name, string version) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } + } + [System.Diagnostics.DebuggerDisplay("{FileName}")] + public class SentryAttachment + { + public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType) { } + public Sentry.IAttachmentContent Content { get; } + public string? ContentType { get; } + public string FileName { get; } + public Sentry.AttachmentType Type { get; } + } + public class SentryCheckIn : Sentry.ISentryJsonSerializable + { + public SentryCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default) { } + public System.TimeSpan? Duration { get; set; } + public string? Environment { get; set; } + public Sentry.SentryId Id { get; } + public string MonitorSlug { get; } + public string? Release { get; set; } + public Sentry.CheckInStatus Status { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + } + public class SentryClient : Sentry.ISentryClient, System.IDisposable + { + public SentryClient(Sentry.SentryOptions options) { } + public bool IsEnabled { get; } + public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } + public void CaptureTransaction(Sentry.SentryTransaction transaction) { } + public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] + public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } + public void Dispose() { } + public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } + } + public static class SentryClientExtensions + { + public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } + public static void CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } + [System.Obsolete("Use CaptureFeedback instead.")] + public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } + public static void Flush(this Sentry.ISentryClient client) { } + public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } + public static System.Threading.Tasks.Task FlushAsync(this Sentry.ISentryClient client) { } + [System.Obsolete("This method is meant for external usage only")] + public static Sentry.SentryOptions? GetInternalSentryOptions(this Sentry.ISentryClient clientOrHub) { } + } + public static class SentryConstants + { + public const int DefaultMaxBreadcrumbs = 100; + public const string DisableSdkDsnValue = ""; + public const string Platform = "csharp"; + public const int ProtocolVersion = 7; + } + public sealed class SentryContexts : Sentry.ISentryJsonSerializable, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public SentryContexts() { } + public Sentry.Protocol.App App { get; } + public Sentry.Protocol.Browser Browser { get; } + public int Count { get; } + public Sentry.Protocol.Device Device { get; } + public Sentry.SentryFeedback? Feedback { get; set; } + public Sentry.Protocol.Gpu Gpu { get; } + public bool IsReadOnly { get; } + public object this[string key] { get; set; } + public System.Collections.Generic.ICollection Keys { get; } + public Sentry.Protocol.OperatingSystem OperatingSystem { get; } + public Sentry.Protocol.Response Response { get; } + public Sentry.Protocol.Runtime Runtime { get; } + public Sentry.Protocol.Trace Trace { get; } + public System.Collections.Generic.ICollection Values { get; } + public void Add(System.Collections.Generic.KeyValuePair item) { } + public void Add(string key, object value) { } + public void Clear() { } + public bool Contains(System.Collections.Generic.KeyValuePair item) { } + public bool ContainsKey(string key) { } + public void CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { } + public System.Collections.Generic.IEnumerator> GetEnumerator() { } + public bool Remove(System.Collections.Generic.KeyValuePair item) { } + public bool Remove(string key) { } + public bool TryGetValue(string key, out object value) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryContexts FromJson(System.Text.Json.JsonElement json) { } + } + [System.Diagnostics.DebuggerDisplay("{GetType().Name,nq}: {EventId,nq}")] + public sealed class SentryEvent : Sentry.IEventLike, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable + { + public SentryEvent() { } + public SentryEvent(System.Exception? exception) { } + public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } + public Sentry.SentryContexts Contexts { get; set; } + public System.Collections.Generic.List? DebugImages { get; set; } + public string? Distribution { get; set; } + public string? Environment { get; set; } + public Sentry.SentryId EventId { get; } + public System.Exception? Exception { get; } + public System.Collections.Generic.IReadOnlyDictionary Extra { get; } + public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } + public Sentry.SentryLevel? Level { get; set; } + public string? Logger { get; set; } + public Sentry.SentryMessage? Message { get; set; } + public System.Collections.Generic.IDictionary Modules { get; } + public string? Platform { get; set; } + public string? Release { get; set; } + public Sentry.SentryRequest Request { get; set; } + public Sentry.SdkVersion Sdk { get; } + public System.Collections.Generic.IEnumerable? SentryExceptions { get; set; } + public System.Collections.Generic.IEnumerable? SentryThreads { get; set; } + public string? ServerName { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Tags { get; } + public System.DateTimeOffset Timestamp { get; } + public string? TransactionName { get; set; } + public Sentry.SentryUser User { get; set; } + public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } + public void SetExtra(string key, object? value) { } + public void SetTag(string key, string value) { } + public void UnsetTag(string key) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryEvent FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class SentryFeedback : Sentry.ISentryJsonSerializable + { + public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default) { } + public Sentry.SentryId? AssociatedEventId { get; set; } + public string? ContactEmail { get; set; } + public string Message { get; set; } + public string? Name { get; set; } + public string? ReplayId { get; set; } + public string? Url { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryFeedback FromJson(System.Text.Json.JsonElement json) { } + } + public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler + { + public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } + protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } + } + public class SentryHint + { + public SentryHint() { } + public SentryHint(string key, object? value) { } + public System.Collections.Generic.ICollection Attachments { get; } + public System.Collections.Generic.IDictionary Items { get; } + public void AddAttachment(string filePath, Sentry.AttachmentType type = 0, string? contentType = null) { } + public void AddAttachment(byte[] data, string fileName, Sentry.AttachmentType type = 0, string? contentType = null) { } + public static Sentry.SentryHint WithAttachments(params Sentry.SentryAttachment[] attachments) { } + public static Sentry.SentryHint WithAttachments(System.Collections.Generic.IEnumerable attachments) { } + } + public class SentryHttpMessageHandler : Sentry.SentryMessageHandler + { + public SentryHttpMessageHandler() { } + public SentryHttpMessageHandler(Sentry.IHub hub) { } + public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } + public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } + } + public readonly struct SentryId : Sentry.ISentryJsonSerializable, System.IEquatable + { + public static readonly Sentry.SentryId Empty; + public SentryId(System.Guid guid) { } + public bool Equals(Sentry.SentryId other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryId Create() { } + public static Sentry.SentryId FromJson(System.Text.Json.JsonElement json) { } + public static Sentry.SentryId Parse(string value) { } + public static System.Guid op_Implicit(Sentry.SentryId sentryId) { } + public static Sentry.SentryId op_Implicit(System.Guid guid) { } + public static bool operator !=(Sentry.SentryId left, Sentry.SentryId right) { } + public static bool operator ==(Sentry.SentryId left, Sentry.SentryId right) { } + } + public enum SentryLevel : short + { + [System.Runtime.Serialization.EnumMember(Value="debug")] + Debug = 0, + [System.Runtime.Serialization.EnumMember(Value="info")] + Info = 1, + [System.Runtime.Serialization.EnumMember(Value="warning")] + Warning = 2, + [System.Runtime.Serialization.EnumMember(Value="error")] + Error = 3, + [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, + Debug = 5, + Info = 9, + Warning = 13, + Error = 17, + Fatal = 21, + } + public sealed class SentryMessage : Sentry.ISentryJsonSerializable + { + public SentryMessage() { } + public string? Formatted { get; set; } + public string? Message { get; set; } + public System.Collections.Generic.IEnumerable? Params { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryMessage FromJson(System.Text.Json.JsonElement json) { } + public static Sentry.SentryMessage op_Implicit(string? message) { } + } + public abstract class SentryMessageHandler : System.Net.Http.DelegatingHandler + { + protected SentryMessageHandler() { } + protected SentryMessageHandler(Sentry.IHub hub) { } + protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } + protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + protected abstract void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url); + protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); + protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } + protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } + } + public enum SentryMonitorInterval + { + Year = 0, + Month = 1, + Week = 2, + Day = 3, + Hour = 4, + Minute = 5, + } + public class SentryMonitorOptions : Sentry.ISentryJsonSerializable + { + public System.TimeSpan? CheckInMargin { get; set; } + public int? FailureIssueThreshold { get; set; } + public System.TimeSpan? MaxRuntime { get; set; } + public string? Owner { get; set; } + public int? RecoveryThreshold { get; set; } + public string? TimeZone { get; set; } + public void Interval(string crontab) { } + public void Interval(int interval, Sentry.SentryMonitorInterval unit) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + } + public class SentryOptions + { + public SentryOptions() { } + [System.CLSCompliant(false)] + public System.Func? AssemblyReader { get; set; } + public bool AttachStacktrace { get; set; } + public bool AutoSessionTracking { get; set; } + public System.TimeSpan AutoSessionTrackingInterval { get; set; } + public Sentry.Extensibility.IBackgroundWorker? BackgroundWorker { get; set; } + public string? CacheDirectoryPath { get; set; } + public bool CaptureFailedRequests { get; set; } + public System.Action? ConfigureClient { get; set; } + public System.Func? CrashedLastRun { get; set; } + public System.Func? CreateHttpMessageHandler { get; set; } + public bool Debug { get; set; } + public System.Net.DecompressionMethods DecompressionMethods { get; set; } + public Sentry.DeduplicateMode DeduplicateMode { get; set; } + public System.Collections.Generic.Dictionary DefaultTags { get; } + public Sentry.StartupTimeDetectionMode DetectStartupTime { get; set; } + public Sentry.SentryLevel DiagnosticLevel { get; set; } + public Sentry.Extensibility.IDiagnosticLogger? DiagnosticLogger { get; set; } + public bool DisableFileWrite { get; set; } + 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; } + [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; } + public System.TimeSpan FlushTimeout { get; set; } + public System.Net.IWebProxy? HttpProxy { get; set; } + public System.TimeSpan InitCacheFlushTimeout { get; set; } + public bool IsEnvironmentUser { get; set; } + public bool IsGlobalModeEnabled { get; set; } + public bool JsonPreserveReferences { get; set; } + public long MaxAttachmentSize { get; set; } + public int MaxBreadcrumbs { get; set; } + public int MaxCacheItems { get; set; } + public int MaxQueueItems { get; set; } + public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } + public double? ProfilesSampleRate { get; set; } + public string? Release { get; set; } + public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } + public bool RequestBodyCompressionBuffered { get; set; } + public System.IO.Compression.CompressionLevel RequestBodyCompressionLevel { get; set; } + public float? SampleRate { get; set; } + public Sentry.IScopeObserver? ScopeObserver { get; set; } + public bool SendClientReports { get; set; } + public bool SendDefaultPii { get; set; } + public Sentry.ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } + public string? ServerName { get; set; } + public System.TimeSpan ShutdownTimeout { get; set; } + public string SpotlightUrl { get; set; } + public Sentry.StackTraceMode StackTraceMode { get; set; } + public System.Collections.Generic.IList TagFilters { get; set; } + public System.Collections.Generic.IList TracePropagationTargets { get; set; } + public double? TracesSampleRate { get; set; } + public System.Func? TracesSampler { get; set; } + public Sentry.Extensibility.ITransport? Transport { get; set; } + public bool UseAsyncFileIO { get; set; } + public void AddEventProcessor(Sentry.Extensibility.ISentryEventProcessor processor) { } + public void AddEventProcessorProvider(System.Func> processorProvider) { } + public void AddEventProcessors(System.Collections.Generic.IEnumerable processors) { } + public void AddExceptionFilter(Sentry.Extensibility.IExceptionFilter exceptionFilter) { } + public void AddExceptionFilterForType() + where TException : System.Exception { } + public void AddExceptionProcessor(Sentry.Extensibility.ISentryEventExceptionProcessor processor) { } + public void AddExceptionProcessorProvider(System.Func> processorProvider) { } + public void AddExceptionProcessors(System.Collections.Generic.IEnumerable processors) { } + public void AddInAppExclude(string prefix) { } + public void AddInAppExclude(System.Text.RegularExpressions.Regex regex) { } + public void AddInAppExcludeRegex(string pattern) { } + public void AddInAppInclude(string prefix) { } + public void AddInAppInclude(System.Text.RegularExpressions.Regex regex) { } + public void AddInAppIncludeRegex(string pattern) { } + public void AddIntegration(Sentry.Integrations.ISdkIntegration integration) { } + public void AddJsonConverter(System.Text.Json.Serialization.JsonConverter converter) { } + public void AddJsonSerializerContext(System.Func contextBuilder) + where T : System.Text.Json.Serialization.JsonSerializerContext { } + public void AddTransactionProcessor(Sentry.Extensibility.ISentryTransactionProcessor processor) { } + public void AddTransactionProcessorProvider(System.Func> processorProvider) { } + public void AddTransactionProcessors(System.Collections.Generic.IEnumerable processors) { } + public void ApplyDefaultTags(Sentry.IHasTags hasTags) { } + public void DisableAppDomainProcessExitFlush() { } + public void DisableAppDomainUnhandledExceptionCapture() { } + public void DisableDiagnosticSourceIntegration() { } + public void DisableDuplicateEventDetection() { } + public void DisableSystemDiagnosticsMetricsIntegration() { } + public void DisableUnobservedTaskExceptionCapture() { } + public void DisableWinUiUnhandledExceptionIntegration() { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void EnableHeapDumps(Sentry.HeapDumpTrigger trigger, Sentry.Debouncer? debouncer = null, Sentry.SentryLevel level = 2) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void EnableHeapDumps(short memoryPercentageThreshold, Sentry.Debouncer? debouncer = null, Sentry.SentryLevel level = 2) { } + public System.Collections.Generic.IEnumerable GetAllEventProcessors() { } + public System.Collections.Generic.IEnumerable GetAllExceptionProcessors() { } + public System.Collections.Generic.IEnumerable GetAllTransactionProcessors() { } + public void RemoveEventProcessor() + where TProcessor : Sentry.Extensibility.ISentryEventProcessor { } + public void RemoveExceptionFilter() + where TFilter : Sentry.Extensibility.IExceptionFilter { } + public void RemoveIntegration() + where TIntegration : Sentry.Integrations.ISdkIntegration { } + public void RemoveTransactionProcessor() + where TProcessor : Sentry.Extensibility.ISentryTransactionProcessor { } + public void SetBeforeBreadcrumb(System.Func beforeBreadcrumb) { } + public void SetBeforeBreadcrumb(System.Func beforeBreadcrumb) { } + public void SetBeforeSend(System.Func beforeSend) { } + public void SetBeforeSend(System.Func beforeSend) { } + 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; } + public void SetBeforeSendLog(System.Func beforeSendLog) { } + } + } + public sealed class SentryPackage : Sentry.ISentryJsonSerializable + { + public SentryPackage(string name, string version) { } + public string Name { get; } + public string Version { get; } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryPackage FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class SentryRequest : Sentry.ISentryJsonSerializable + { + public SentryRequest() { } + public string? ApiTarget { get; set; } + public string? Cookies { get; set; } + public object? Data { get; set; } + public System.Collections.Generic.IDictionary Env { get; } + public System.Collections.Generic.IDictionary Headers { get; } + public string? Method { get; set; } + public System.Collections.Generic.IDictionary Other { get; } + public string? QueryString { get; set; } + public string? Url { get; set; } + public Sentry.SentryRequest Clone() { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryRequest FromJson(System.Text.Json.JsonElement json) { } + } + public static class SentrySdk + { + public static bool IsEnabled { get; } + public static Sentry.SentryId LastEventId { 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) { } + public static void BindClient(Sentry.ISentryClient client) { } + public static void BindException(System.Exception exception, Sentry.ISpan span) { } + public static Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } + public static bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } + public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } + public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(System.Exception exception) { } + public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } + public static void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } + public static void CaptureFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static Sentry.SentryId CaptureMessage(string message, Sentry.SentryLevel level = 1) { } + public static Sentry.SentryId CaptureMessage(string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } + public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } + public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } + public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] + public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } + [System.Obsolete("Use CaptureFeedback instead.")] + public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } + [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + + "l application.")] + public static void CauseCrash(Sentry.CrashType crashType) { } + public static void Close() { } + public static void ConfigureScope(System.Action configureScope) { } + public static void ConfigureScope(System.Action configureScope, TArg arg) { } + public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } + public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } + public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public static void EndSession(Sentry.SessionEndStatus status = 0) { } + public static void Flush() { } + public static void Flush(System.TimeSpan timeout) { } + public static System.Threading.Tasks.Task FlushAsync() { } + public static System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } + public static Sentry.BaggageHeader? GetBaggage() { } + public static Sentry.ISpan? GetSpan() { } + public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.ITransactionTracer? GetTransaction() { } + public static System.IDisposable Init() { } + public static System.IDisposable Init(Sentry.SentryOptions options) { } + public static System.IDisposable Init(System.Action? configureOptions) { } + public static System.IDisposable Init(string? dsn) { } + public static void PauseSession() { } + public static System.IDisposable PushScope() { } + public static System.IDisposable PushScope(TState state) { } + public static void ResumeSession() { } + public static void SetTag(string key, string value) { } + public static void StartSession() { } + public static Sentry.ISpan StartSpan(string operation, string description) { } + public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context) { } + public static Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary customSamplingContext) { } + public static Sentry.ITransactionTracer StartTransaction(string name, string operation) { } + 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 + { + public SentrySession(string? distinctId, string release, string? environment) { } + public string? DistinctId { get; } + public string? Environment { get; } + public int ErrorCount { get; } + public Sentry.SentryId Id { get; } + public string? IpAddress { get; } + public string Release { get; } + public System.DateTimeOffset StartTimestamp { get; } + public string? UserAgent { get; } + public void ReportError() { } + } + public class SentrySpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.Protocol.ITraceContext + { + public SentrySpan(Sentry.ISpan tracer) { } + public SentrySpan(Sentry.SpanId? parentSpanId, string operation) { } + public System.Collections.Generic.IReadOnlyDictionary Data { get; } + public string? Description { get; set; } + public System.DateTimeOffset? EndTimestamp { get; } + [System.Obsolete("Use Data")] + public System.Collections.Generic.IReadOnlyDictionary Extra { get; } + public bool IsFinished { get; } + public bool? IsSampled { get; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } + public string Operation { get; set; } + public string? Origin { get; } + public Sentry.SpanId? ParentSpanId { get; } + public Sentry.SpanId SpanId { get; } + public System.DateTimeOffset StartTimestamp { get; } + public Sentry.SpanStatus? Status { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Tags { get; } + public Sentry.SentryId TraceId { get; } + public Sentry.SentryTraceHeader GetTraceHeader() { } + public void SetData(string key, object? value) { } + [System.Obsolete("Use SetData")] + public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } + public void SetTag(string key, string value) { } + public void UnsetTag(string key) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentrySpan FromJson(System.Text.Json.JsonElement json) { } + } + [System.Diagnostics.DebuggerDisplay("{Function}")] + public sealed class SentryStackFrame : Sentry.ISentryJsonSerializable + { + public SentryStackFrame() { } + public string? AbsolutePath { get; set; } + public string? AddressMode { get; set; } + public int? ColumnNumber { get; set; } + public string? ContextLine { get; set; } + public string? FileName { get; set; } + public System.Collections.Generic.IList FramesOmitted { get; } + public string? Function { get; set; } + public long? FunctionId { get; set; } + public long? ImageAddress { get; set; } + public bool? InApp { get; set; } + public long? InstructionAddress { get; set; } + public int? LineNumber { get; set; } + public string? Module { get; set; } + public string? Package { get; set; } + public string? Platform { get; set; } + public System.Collections.Generic.IList PostContext { get; } + public System.Collections.Generic.IList PreContext { get; } + public long? SymbolAddress { get; set; } + public System.Collections.Generic.IDictionary Vars { get; } + public void ConfigureAppFrame(Sentry.SentryOptions options) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryStackFrame FromJson(System.Text.Json.JsonElement json) { } + } + public class SentryStackTrace : Sentry.ISentryJsonSerializable + { + public SentryStackTrace() { } + public Sentry.InstructionAddressAdjustment? AddressAdjustment { get; set; } + public System.Collections.Generic.IList Frames { get; set; } + 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 + { + public SentryThread() { } + public bool? Crashed { get; set; } + public bool? Current { get; set; } + public int? Id { get; set; } + public string? Name { get; set; } + public Sentry.SentryStackTrace? Stacktrace { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryThread FromJson(System.Text.Json.JsonElement json) { } + } + public class SentryTraceHeader + { + public SentryTraceHeader(Sentry.SentryId traceId, Sentry.SpanId spanSpanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + public static Sentry.SentryTraceHeader? Parse(string value) { } + } + public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext + { + public SentryTransaction(Sentry.ITransactionTracer tracer) { } + public SentryTransaction(string name, string operation) { } + public SentryTransaction(string name, string operation, Sentry.TransactionNameSource nameSource) { } + public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } + public Sentry.SentryContexts Contexts { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Data { get; } + public string? Description { get; set; } + public string? Distribution { get; set; } + public System.DateTimeOffset? EndTimestamp { get; } + public string? Environment { get; set; } + public Sentry.SentryId EventId { get; } + [System.Obsolete("Use Data")] + public System.Collections.Generic.IReadOnlyDictionary Extra { get; } + public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } + public bool IsFinished { get; } + public bool? IsParentSampled { get; set; } + public bool? IsSampled { get; } + public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } + public string Name { get; } + public Sentry.TransactionNameSource NameSource { get; } + public string Operation { get; } + public string? Origin { get; } + public Sentry.SpanId? ParentSpanId { get; } + public string? Platform { get; set; } + public string? Release { get; set; } + public Sentry.SentryRequest Request { get; set; } + public double? SampleRate { get; } + public Sentry.SdkVersion Sdk { get; } + public Sentry.SpanId SpanId { get; } + public System.Collections.Generic.IReadOnlyCollection Spans { get; } + public System.DateTimeOffset StartTimestamp { get; } + public Sentry.SpanStatus? Status { get; } + public System.Collections.Generic.IReadOnlyDictionary Tags { get; } + public Sentry.SentryId TraceId { get; } + public Sentry.SentryUser User { get; set; } + public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } + public Sentry.SentryTraceHeader GetTraceHeader() { } + public void SetData(string key, object? value) { } + [System.Obsolete("Use SetData")] + public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } + public void SetTag(string key, string value) { } + public void UnsetTag(string key) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class SentryUser : Sentry.ISentryJsonSerializable + { + public SentryUser() { } + public string? Email { get; set; } + public string? Id { get; set; } + public string? IpAddress { get; set; } + public System.Collections.Generic.IDictionary Other { get; set; } + public string? Username { get; set; } + public Sentry.SentryUser Clone() { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.SentryUser FromJson(System.Text.Json.JsonElement json) { } + } + public enum SessionEndStatus + { + Exited = 0, + Crashed = 1, + Abnormal = 2, + } + public class SessionUpdate : Sentry.ISentryJsonSerializable, Sentry.ISentrySession + { + public SessionUpdate(Sentry.SessionUpdate sessionUpdate, bool isInitial) { } + public SessionUpdate(Sentry.SessionUpdate sessionUpdate, bool isInitial, Sentry.SessionEndStatus? endStatus) { } + public SessionUpdate(Sentry.ISentrySession session, bool isInitial, System.DateTimeOffset timestamp, int sequenceNumber, Sentry.SessionEndStatus? endStatus) { } + public SessionUpdate(Sentry.SentryId id, string? distinctId, System.DateTimeOffset startTimestamp, string release, string? environment, string? ipAddress, string? userAgent, int errorCount, bool isInitial, System.DateTimeOffset timestamp, int sequenceNumber, Sentry.SessionEndStatus? endStatus) { } + public string? DistinctId { get; } + public System.TimeSpan Duration { get; } + public Sentry.SessionEndStatus? EndStatus { get; } + public string? Environment { get; } + public int ErrorCount { get; } + public Sentry.SentryId Id { get; } + public string? IpAddress { get; } + public bool IsInitial { get; } + public string Release { get; } + public int SequenceNumber { get; } + public System.DateTimeOffset StartTimestamp { get; } + public System.DateTimeOffset Timestamp { get; } + public string? UserAgent { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SessionUpdate FromJson(System.Text.Json.JsonElement json) { } + } + public class SpanContext : Sentry.Protocol.ITraceContext + { + public SpanContext(string operation, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, Sentry.SentryId? traceId = default, string? description = null, Sentry.SpanStatus? status = default, bool? isSampled = default) { } + public string? Description { get; } + public Sentry.Instrumenter Instrumenter { get; } + public bool? IsSampled { get; } + public string Operation { get; set; } + public string? Origin { get; } + public Sentry.SpanId? ParentSpanId { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SpanStatus? Status { get; } + public Sentry.SentryId TraceId { get; } + } + public static class SpanDataExtensions + { + public static void SetMeasurement(this Sentry.ISpanData spanData, string name, double value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ISpanData spanData, string name, int value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ISpanData spanData, string name, long value, Sentry.MeasurementUnit unit = default) { } + [System.CLSCompliant(false)] + public static void SetMeasurement(this Sentry.ISpanData spanData, string name, ulong value, Sentry.MeasurementUnit unit = default) { } + } + public static class SpanExtensions + { + public static Sentry.ITransactionTracer GetTransaction(this Sentry.ISpan span) { } + public static Sentry.ISpan StartChild(this Sentry.ISpan span, string operation, string? description) { } + } + public readonly struct SpanId : Sentry.ISentryJsonSerializable, System.IEquatable + { + public static readonly Sentry.SpanId Empty; + public SpanId(long value) { } + public SpanId(string value) { } + public bool Equals(Sentry.SpanId other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.SpanId Create() { } + public static Sentry.SpanId FromJson(System.Text.Json.JsonElement json) { } + public static Sentry.SpanId Parse(string value) { } + public static string op_Implicit(Sentry.SpanId id) { } + public static bool operator !=(Sentry.SpanId left, Sentry.SpanId right) { } + public static bool operator ==(Sentry.SpanId left, Sentry.SpanId right) { } + } + public enum SpanStatus + { + Ok = 0, + DeadlineExceeded = 1, + Unauthenticated = 2, + PermissionDenied = 3, + NotFound = 4, + ResourceExhausted = 5, + InvalidArgument = 6, + Unimplemented = 7, + Unavailable = 8, + InternalError = 9, + UnknownError = 10, + Cancelled = 11, + AlreadyExists = 12, + FailedPrecondition = 13, + Aborted = 14, + OutOfRange = 15, + DataLoss = 16, + } + public class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext + { + public SpanTracer(Sentry.IHub hub, Sentry.TransactionTracer transaction, Sentry.SpanId? parentSpanId, Sentry.SentryId traceId, string operation) { } + public System.Collections.Generic.IReadOnlyDictionary Data { get; } + public string? Description { get; set; } + public System.DateTimeOffset? EndTimestamp { get; } + public System.Collections.Generic.IReadOnlyDictionary Extra { get; } + public bool IsFinished { get; } + public bool? IsSampled { get; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } + public string Operation { get; set; } + public string? Origin { get; } + public Sentry.SpanId? ParentSpanId { get; } + public Sentry.SpanId SpanId { get; } + public System.DateTimeOffset StartTimestamp { get; } + public Sentry.SpanStatus? Status { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Tags { get; } + public Sentry.SentryId TraceId { get; } + public void Finish() { } + public void Finish(Sentry.SpanStatus status) { } + public void Finish(System.Exception exception) { } + public void Finish(System.Exception exception, Sentry.SpanStatus status) { } + public Sentry.SentryTraceHeader GetTraceHeader() { } + public void SetData(string key, object? value) { } + public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } + public void SetTag(string key, string value) { } + public Sentry.ISpan StartChild(string operation) { } + public void UnsetTag(string key) { } + } + public enum StackTraceMode + { + Original = 0, + Enhanced = 1, + } + public enum StartupTimeDetectionMode + { + None = 0, + Fast = 1, + Best = 2, + } + public class StreamAttachmentContent : Sentry.IAttachmentContent + { + public StreamAttachmentContent(System.IO.Stream stream) { } + public System.IO.Stream GetStream() { } + } + [System.ComponentModel.TypeConverter(typeof(Sentry.StringOrRegexTypeConverter))] + public class StringOrRegex + { + public StringOrRegex(string stringOrRegex) { } + public StringOrRegex(System.Text.RegularExpressions.Regex regex) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public static Sentry.StringOrRegex op_Implicit(string stringOrRegex) { } + public static Sentry.StringOrRegex op_Implicit(System.Text.RegularExpressions.Regex regex) { } + } + public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext + { + public TransactionContext(string name, string operation, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, Sentry.SentryId? traceId = default, string? description = "", Sentry.SpanStatus? status = default, bool? isSampled = default, bool? isParentSampled = default, Sentry.TransactionNameSource nameSource = 0) { } + public bool? IsParentSampled { get; } + public string Name { get; set; } + public Sentry.TransactionNameSource NameSource { get; set; } + } + public enum TransactionNameSource + { + Custom = 0, + Url = 1, + Route = 2, + View = 3, + Component = 4, + Task = 5, + } + public class TransactionSamplingContext + { + public TransactionSamplingContext(Sentry.ITransactionContext transactionContext, System.Collections.Generic.IReadOnlyDictionary customSamplingContext) { } + public System.Collections.Generic.IReadOnlyDictionary CustomSamplingContext { get; } + public Sentry.ITransactionContext TransactionContext { get; } + } + public class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext + { + public TransactionTracer(Sentry.IHub hub, Sentry.ITransactionContext context) { } + public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } + public Sentry.SentryContexts Contexts { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Data { get; } + public string? Description { get; set; } + public string? Distribution { get; set; } + public System.DateTimeOffset? EndTimestamp { get; } + public string? Environment { get; set; } + [System.Obsolete("Use Data")] + public System.Collections.Generic.IReadOnlyDictionary Extra { get; } + public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } + public bool IsFinished { get; } + public bool? IsParentSampled { get; set; } + public bool? IsSampled { get; } + public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } + public string Name { get; set; } + public Sentry.TransactionNameSource NameSource { get; set; } + public string Operation { get; set; } + public string? Origin { get; } + public Sentry.SpanId? ParentSpanId { get; } + public string? Platform { get; set; } + public string? Release { get; set; } + public Sentry.SentryRequest Request { get; set; } + public double? SampleRate { get; } + public Sentry.SdkVersion Sdk { get; } + public Sentry.SpanId SpanId { get; } + public System.Collections.Generic.IReadOnlyCollection Spans { get; } + public System.DateTimeOffset StartTimestamp { get; } + public Sentry.SpanStatus? Status { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Tags { get; } + public Sentry.SentryId TraceId { get; } + public Sentry.SentryUser User { get; set; } + public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } + public void Finish() { } + public void Finish(Sentry.SpanStatus status) { } + public void Finish(System.Exception exception) { } + public void Finish(System.Exception exception, Sentry.SpanStatus status) { } + public Sentry.ISpan? GetLastActiveSpan() { } + public Sentry.SentryTraceHeader GetTraceHeader() { } + public void SetData(string key, object? value) { } + [System.Obsolete("Use SetData")] + public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } + public void SetTag(string key, string value) { } + public Sentry.ISpan StartChild(string operation) { } + public void UnsetTag(string key) { } + } + [System.Obsolete("Use SentryFeedback instead.")] + public sealed class UserFeedback : Sentry.ISentryJsonSerializable + { + public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } + public string? Comments { get; } + public string? Email { get; } + public Sentry.SentryId EventId { get; } + public string? Name { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.UserFeedback FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class ViewHierarchy : Sentry.ISentryJsonSerializable + { + public ViewHierarchy(string renderingSystem) { } + public string RenderingSystem { get; set; } + public System.Collections.Generic.List Windows { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + } + public class ViewHierarchyAttachment : Sentry.SentryAttachment + { + public ViewHierarchyAttachment(Sentry.IAttachmentContent content) { } + } + public abstract class ViewHierarchyNode : Sentry.ISentryJsonSerializable + { + protected ViewHierarchyNode(string type) { } + public System.Collections.Generic.List Children { get; set; } + public string Type { get; set; } + protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + } +} +namespace Sentry.Ben.BlockingDetector +{ + public class SuppressBlockingDetection : System.IDisposable + { + public SuppressBlockingDetection() { } + public void Dispose() { } + } +} +namespace Sentry.CompilerServices +{ + public static class BuildProperties + { + public static void Initialize(System.Collections.Generic.Dictionary properties) { } + } +} +namespace Sentry.Extensibility +{ + public abstract class BaseRequestPayloadExtractor : Sentry.Extensibility.IRequestPayloadExtractor + { + protected BaseRequestPayloadExtractor() { } + protected abstract object? DoExtractPayLoad(Sentry.Extensibility.IHttpRequest request); + public object? ExtractPayload(Sentry.Extensibility.IHttpRequest request) { } + protected abstract bool IsSupported(Sentry.Extensibility.IHttpRequest request); + } + public class DefaultRequestPayloadExtractor : Sentry.Extensibility.BaseRequestPayloadExtractor + { + public DefaultRequestPayloadExtractor() { } + protected override object? DoExtractPayLoad(Sentry.Extensibility.IHttpRequest request) { } + protected override bool IsSupported(Sentry.Extensibility.IHttpRequest request) { } + } + public static class DiagnosticLoggerExtensions + { + public static void LogDebug(this Sentry.Extensibility.IDiagnosticLogger logger, string message) { } + public static void LogDebug(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg) { } + public static void LogDebug(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg, TArg2 arg2) { } + public static void LogDebug(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg, TArg2 arg2, TArg3 arg3) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, string message) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message, TArg arg) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message, TArg arg, TArg2 arg2) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message, TArg arg, TArg2 arg2, TArg3 arg3) { } + public static void LogError(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message, TArg arg, TArg2 arg2, TArg3 arg3, TArg4 arg4) { } + public static void LogFatal(this Sentry.Extensibility.IDiagnosticLogger logger, string message) { } + public static void LogFatal(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message) { } + public static void LogInfo(this Sentry.Extensibility.IDiagnosticLogger logger, string message) { } + public static void LogInfo(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg) { } + public static void LogInfo(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg, TArg2 arg2) { } + public static void LogInfo(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg, TArg2 arg2, TArg3 arg3) { } + public static void LogWarning(this Sentry.Extensibility.IDiagnosticLogger logger, string message) { } + public static void LogWarning(this Sentry.Extensibility.IDiagnosticLogger logger, System.Exception exception, string message) { } + public static void LogWarning(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg) { } + public static void LogWarning(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg, TArg2 arg2) { } + public static void LogWarning(this Sentry.Extensibility.IDiagnosticLogger logger, string message, TArg arg, TArg2 arg2, TArg3 arg3) { } + } + public class DisabledHub : Sentry.IHub, Sentry.ISentryClient, Sentry.ISentryScopeManager, System.IDisposable + { + 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) { } + public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } + public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } + public void CaptureTransaction(Sentry.SentryTransaction transaction) { } + public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] + public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } + public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } + public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public void Dispose() { } + public void EndSession(Sentry.SessionEndStatus status = 0) { } + public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } + public Sentry.BaggageHeader? GetBaggage() { } + public Sentry.ISpan? GetSpan() { } + public Sentry.SentryTraceHeader? GetTraceHeader() { } + public void PauseSession() { } + public System.IDisposable PushScope() { } + public System.IDisposable PushScope(TState state) { } + public void ResumeSession() { } + public void SetTag(string key, string value) { } + public void StartSession() { } + public Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary customSamplingContext) { } + public void UnsetTag(string key) { } + } + public class FormRequestPayloadExtractor : Sentry.Extensibility.BaseRequestPayloadExtractor + { + public FormRequestPayloadExtractor() { } + protected override object? DoExtractPayLoad(Sentry.Extensibility.IHttpRequest request) { } + protected override bool IsSupported(Sentry.Extensibility.IHttpRequest request) { } + } + public sealed class HubAdapter : Sentry.IHub, Sentry.ISentryClient, Sentry.ISentryScopeManager + { + 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) { } + public void BindClient(Sentry.ISentryClient client) { } + public void BindException(System.Exception exception, Sentry.ISpan span) { } + public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? monitorOptions = null) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope, Sentry.SentryHint? hint = null) { } + public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } + public Sentry.SentryId CaptureException(System.Exception exception) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } + public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } + public void CaptureTransaction(Sentry.SentryTransaction transaction) { } + public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] + public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } + public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } + public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public void EndSession(Sentry.SessionEndStatus status = 0) { } + public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } + public Sentry.BaggageHeader? GetBaggage() { } + public Sentry.ISpan? GetSpan() { } + public Sentry.SentryTraceHeader? GetTraceHeader() { } + public void PauseSession() { } + public System.IDisposable PushScope() { } + public System.IDisposable PushScope(TState state) { } + public void ResumeSession() { } + public void SetTag(string key, string value) { } + public void StartSession() { } + public Sentry.ITransactionTracer StartTransaction(Sentry.ITransactionContext context, System.Collections.Generic.IReadOnlyDictionary customSamplingContext) { } + public void UnsetTag(string key) { } + } + public interface IBackgroundWorker + { + int QueuedItems { get; } + bool EnqueueEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); + System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); + } + public interface IDiagnosticLogger + { + bool IsEnabled(Sentry.SentryLevel level); + void Log(Sentry.SentryLevel logLevel, string message, System.Exception? exception = null, params object?[] args); + } + public interface IExceptionFilter + { + bool Filter(System.Exception ex); + } + public interface IHttpRequest + { + System.IO.Stream? Body { get; } + long? ContentLength { get; } + string? ContentType { get; } + System.Collections.Generic.IEnumerable>>? Form { get; } + } + public interface INetworkStatusListener + { + bool Online { get; } + System.Threading.Tasks.Task WaitForNetworkOnlineAsync(System.Threading.CancellationToken cancellationToken); + } + public interface IRequestPayloadExtractor + { + object? ExtractPayload(Sentry.Extensibility.IHttpRequest request); + } + public interface ISentryEventExceptionProcessor + { + void Process(System.Exception exception, Sentry.SentryEvent sentryEvent); + } + public interface ISentryEventProcessor + { + Sentry.SentryEvent? Process(Sentry.SentryEvent @event); + } + public interface ISentryEventProcessorWithHint : Sentry.Extensibility.ISentryEventProcessor + { + Sentry.SentryEvent? Process(Sentry.SentryEvent @event, Sentry.SentryHint hint); + } + public interface ISentryStackTraceFactory + { + Sentry.SentryStackTrace? Create(System.Exception? exception = null); + } + public interface ISentryTransactionProcessor + { + Sentry.SentryTransaction? Process(Sentry.SentryTransaction transaction); + } + public interface ISentryTransactionProcessorWithHint : Sentry.Extensibility.ISentryTransactionProcessor + { + Sentry.SentryTransaction? Process(Sentry.SentryTransaction transaction, Sentry.SentryHint hint); + } + public interface ITransport + { + System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default); + } + public class RequestBodyExtractionDispatcher : Sentry.Extensibility.IRequestPayloadExtractor + { + public RequestBodyExtractionDispatcher(System.Collections.Generic.IEnumerable extractors, Sentry.SentryOptions options, System.Func sizeSwitch) { } + public object? ExtractPayload(Sentry.Extensibility.IHttpRequest request) { } + } + public enum RequestSize + { + None = 0, + Small = 1, + Medium = 2, + Always = 3, + } + public abstract class SentryEventExceptionProcessor : Sentry.Extensibility.ISentryEventExceptionProcessor + where TException : System.Exception + { + protected SentryEventExceptionProcessor() { } + public void Process(System.Exception? exception, Sentry.SentryEvent sentryEvent) { } + protected abstract void ProcessException(TException exception, Sentry.SentryEvent sentryEvent); + } + public sealed class SentryStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + { + public SentryStackTraceFactory(Sentry.SentryOptions options) { } + public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } + } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + { + public StringStackTraceFactory(Sentry.SentryOptions options) { } + public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } + } +} +namespace Sentry.Http +{ + public abstract class HttpTransportBase + { + protected HttpTransportBase(Sentry.SentryOptions options, System.Func? getEnvironmentVariable = null, Sentry.Infrastructure.ISystemClock? clock = null) { } + protected virtual System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope) { } + protected System.Threading.Tasks.Task HandleResponseAsync(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected System.IO.Stream ReadStreamFromHttpContent(System.Net.Http.HttpContent content) { } + } + public interface ISentryHttpClientFactory + { + System.Net.Http.HttpClient Create(Sentry.SentryOptions options); + } +} +namespace Sentry.Infrastructure +{ + public class ConsoleAndTraceDiagnosticLogger : Sentry.Infrastructure.DiagnosticLogger + { + public ConsoleAndTraceDiagnosticLogger(Sentry.SentryLevel minimalLevel) { } + protected override void LogMessage(string message) { } + } + public class ConsoleDiagnosticLogger : Sentry.Infrastructure.DiagnosticLogger + { + public ConsoleDiagnosticLogger(Sentry.SentryLevel minimalLevel) { } + protected override void LogMessage(string message) { } + } + public abstract class DiagnosticLogger : Sentry.Extensibility.IDiagnosticLogger + { + protected DiagnosticLogger(Sentry.SentryLevel minimalLevel) { } + public bool IsEnabled(Sentry.SentryLevel level) { } + public void Log(Sentry.SentryLevel logLevel, string message, System.Exception? exception = null, params object?[] args) { } + protected abstract void LogMessage(string message); + } + public class FileDiagnosticLogger : Sentry.Infrastructure.DiagnosticLogger + { + public FileDiagnosticLogger(string path, bool alsoWriteToConsole = false) { } + public FileDiagnosticLogger(string path, Sentry.SentryLevel minimalLevel, bool alsoWriteToConsole = false) { } + protected override void LogMessage(string message) { } + } + public interface ISystemClock + { + System.DateTimeOffset GetUtcNow(); + } + public sealed class SystemClock : Sentry.Infrastructure.ISystemClock + { + public static readonly Sentry.Infrastructure.SystemClock Clock; + public System.DateTimeOffset GetUtcNow() { } + } + public class TraceDiagnosticLogger : Sentry.Infrastructure.DiagnosticLogger + { + public TraceDiagnosticLogger(Sentry.SentryLevel minimalLevel) { } + protected override void LogMessage(string message) { } + } +} +namespace Sentry.Integrations +{ + public interface ISdkIntegration + { + void Register(Sentry.IHub hub, Sentry.SentryOptions options); + } +} +namespace Sentry.PlatformAbstractions +{ + public static class FrameworkInfo + { + public static System.Collections.Generic.IReadOnlyDictionary NetFxReleaseVersionMap { get; } + public static System.Collections.Generic.IEnumerable GetInstallations() { } + public static Sentry.PlatformAbstractions.FrameworkInstallation? GetLatest(int clr) { } + } + public class FrameworkInstallation + { + public FrameworkInstallation() { } + public Sentry.PlatformAbstractions.FrameworkProfile? Profile { get; set; } + public int? Release { get; set; } + public int? ServicePack { get; set; } + public string? ShortName { get; set; } + public System.Version? Version { get; set; } + public override string ToString() { } + } + public enum FrameworkProfile + { + Client = 0, + Full = 1, + } + public class SentryRuntime : System.IEquatable + { + public SentryRuntime(string? name = null, string? version = null, string? raw = null, string? identifier = null) { } + public string? Identifier { get; } + public string? Name { get; } + public string? Raw { get; } + public string? Version { get; } + public static Sentry.PlatformAbstractions.SentryRuntime Current { get; } + public bool Equals(Sentry.PlatformAbstractions.SentryRuntime? other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string? ToString() { } + } + public static class SentryRuntimeExtensions + { + public static bool IsMono(this Sentry.PlatformAbstractions.SentryRuntime runtime) { } + public static bool IsNetCore(this Sentry.PlatformAbstractions.SentryRuntime runtime) { } + public static bool IsNetFx(this Sentry.PlatformAbstractions.SentryRuntime runtime) { } + } +} +namespace Sentry.Protocol +{ + public sealed class App : Sentry.ISentryJsonSerializable + { + public const string Type = "app"; + public App() { } + public string? Build { get; set; } + public string? BuildType { get; set; } + public string? Hash { get; set; } + public string? Identifier { get; set; } + public bool? InForeground { get; set; } + public string? Name { get; set; } + public System.DateTimeOffset? StartTime { get; set; } + public string? Version { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.Protocol.App FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class Browser : Sentry.ISentryJsonSerializable + { + public const string Type = "browser"; + public Browser() { } + public string? Name { get; set; } + public string? Version { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.Protocol.Browser FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class DebugImage : Sentry.ISentryJsonSerializable + { + public DebugImage() { } + public string? CodeFile { get; set; } + public string? CodeId { get; set; } + public string? DebugChecksum { get; set; } + public string? DebugFile { get; set; } + public string? DebugId { get; set; } + public long? ImageAddress { get; set; } + public long? ImageSize { get; set; } + public string? Type { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.DebugImage FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class Device : Sentry.ISentryJsonSerializable + { + public const string Type = "device"; + public Device() { } + public string? Architecture { get; set; } + public float? BatteryLevel { get; set; } + public string? BatteryStatus { get; set; } + public System.DateTimeOffset? BootTime { get; set; } + public string? Brand { get; set; } + public string? CpuDescription { get; set; } + public string? DeviceType { get; set; } + public string? DeviceUniqueIdentifier { get; set; } + public long? ExternalFreeStorage { get; set; } + public long? ExternalStorageSize { get; set; } + public string? Family { get; set; } + public long? FreeMemory { get; set; } + public long? FreeStorage { get; set; } + public bool? IsCharging { get; set; } + public bool? IsOnline { get; set; } + public bool? LowMemory { get; set; } + public string? Manufacturer { get; set; } + public long? MemorySize { get; set; } + public string? Model { get; set; } + public string? ModelId { get; set; } + public string? Name { get; set; } + public Sentry.Protocol.DeviceOrientation? Orientation { get; set; } + public int? ProcessorCount { get; set; } + public float? ProcessorFrequency { get; set; } + public float? ScreenDensity { get; set; } + public int? ScreenDpi { get; set; } + public string? ScreenResolution { get; set; } + public bool? Simulator { get; set; } + public long? StorageSize { get; set; } + public bool? SupportsAccelerometer { get; set; } + public bool? SupportsAudio { get; set; } + public bool? SupportsGyroscope { get; set; } + public bool? SupportsLocationService { get; set; } + public bool? SupportsVibration { get; set; } + public System.TimeZoneInfo? Timezone { get; set; } + public long? UsableMemory { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.Protocol.Device FromJson(System.Text.Json.JsonElement json) { } + } + public enum DeviceOrientation + { + [System.Runtime.Serialization.EnumMember(Value="portrait")] + Portrait = 0, + [System.Runtime.Serialization.EnumMember(Value="landscape")] + Landscape = 1, + } + public sealed class Gpu : Sentry.ISentryJsonSerializable + { + public const string Type = "gpu"; + public Gpu() { } + public string? ApiType { get; set; } + public string? GraphicsShaderLevel { get; set; } + public int? Id { get; set; } + public int? MaxTextureSize { get; set; } + public int? MemorySize { get; set; } + public bool? MultiThreadedRendering { get; set; } + public string? Name { get; set; } + public string? NpotSupport { get; set; } + public bool? SupportsComputeShaders { get; set; } + public bool? SupportsDrawCallInstancing { get; set; } + public bool? SupportsGeometryShaders { get; set; } + public bool? SupportsRayTracing { get; set; } + public string? VendorId { get; set; } + public string? VendorName { get; set; } + public string? Version { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.Protocol.Gpu FromJson(System.Text.Json.JsonElement json) { } + } + public interface ITraceContext + { + string? Description { get; } + bool? IsSampled { get; } + string Operation { get; } + string? Origin { get; } + Sentry.SpanId? ParentSpanId { get; } + Sentry.SpanId SpanId { get; } + Sentry.SpanStatus? Status { get; } + Sentry.SentryId TraceId { get; } + } + public sealed class Measurement : Sentry.ISentryJsonSerializable + { + public Sentry.MeasurementUnit Unit { get; } + public object Value { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Measurement FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class Mechanism : Sentry.ISentryJsonSerializable + { + public static readonly string DescriptionKey; + public static readonly string HandledKey; + public static readonly string MechanismKey; + public Mechanism() { } + public System.Collections.Generic.IDictionary Data { get; } + public string? Description { get; set; } + public int? ExceptionId { get; set; } + public bool? Handled { get; set; } + public string? HelpLink { get; set; } + public bool IsExceptionGroup { get; set; } + public System.Collections.Generic.IDictionary Meta { get; } + public int? ParentId { get; set; } + public string? Source { get; set; } + public bool Synthetic { get; set; } + public string Type { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Mechanism FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class OperatingSystem : Sentry.ISentryJsonSerializable + { + public const string Type = "os"; + public OperatingSystem() { } + public string? Build { get; set; } + public string? KernelVersion { get; set; } + public string? Name { get; set; } + public string? RawDescription { get; set; } + public bool? Rooted { get; set; } + public string? Version { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class Response : Sentry.ISentryJsonSerializable + { + public const string Type = "response"; + public Response() { } + public long? BodySize { get; set; } + public string? Cookies { get; set; } + public object? Data { get; set; } + public System.Collections.Generic.IDictionary Headers { get; } + public short? StatusCode { get; set; } + public Sentry.Protocol.Response Clone() { } + public void UpdateFrom(Sentry.Protocol.Response source) { } + public void UpdateFrom(object source) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Response FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class Runtime : Sentry.ISentryJsonSerializable + { + public const string Type = "runtime"; + public Runtime() { } + public string? Build { get; set; } + public string? Identifier { get; set; } + public string? Name { get; set; } + public string? RawDescription { get; set; } + public string? Version { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } + public static Sentry.Protocol.Runtime FromJson(System.Text.Json.JsonElement json) { } + } + public sealed class SentryException : Sentry.ISentryJsonSerializable + { + public SentryException() { } + public Sentry.Protocol.Mechanism? Mechanism { get; set; } + public string? Module { get; set; } + public Sentry.SentryStackTrace? Stacktrace { get; set; } + public int ThreadId { get; set; } + public string? Type { get; set; } + public string? Value { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.SentryException FromJson(System.Text.Json.JsonElement json) { } + } + public class Trace : Sentry.ISentryJsonSerializable, Sentry.Protocol.ITraceContext + { + public const string Type = "trace"; + public Trace() { } + public System.Collections.Generic.IReadOnlyDictionary Data { get; } + public string? Description { get; set; } + public bool? IsSampled { get; } + public string Operation { get; set; } + public string? Origin { get; } + public Sentry.SpanId? ParentSpanId { get; set; } + public Sentry.SpanId SpanId { get; set; } + public Sentry.SpanStatus? Status { get; set; } + public Sentry.SentryId TraceId { get; set; } + public void SetData(string key, object? value) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Trace FromJson(System.Text.Json.JsonElement json) { } + } +} +namespace Sentry.Protocol.Envelopes +{ + public sealed class Envelope : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable + { + public Envelope(System.Collections.Generic.IReadOnlyDictionary header, System.Collections.Generic.IReadOnlyList items) { } + public System.Collections.Generic.IReadOnlyDictionary Header { get; } + public System.Collections.Generic.IReadOnlyList Items { get; } + public void Dispose() { } + public void Serialize(System.IO.Stream stream, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public System.Threading.Tasks.Task SerializeAsync(System.IO.Stream stream, Sentry.Extensibility.IDiagnosticLogger? logger, System.Threading.CancellationToken cancellationToken = default) { } + public Sentry.SentryId? TryGetEventId() { } + public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default) { } + public static Sentry.Protocol.Envelopes.Envelope FromCheckIn(Sentry.SentryCheckIn checkIn) { } + public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } + public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } + public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } + public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] + public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } + } + public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable + { + public EnvelopeItem(System.Collections.Generic.IReadOnlyDictionary header, Sentry.Protocol.Envelopes.ISerializable payload) { } + public System.Collections.Generic.IReadOnlyDictionary Header { get; } + public Sentry.Protocol.Envelopes.ISerializable Payload { get; } + public void Dispose() { } + public void Serialize(System.IO.Stream stream, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public System.Threading.Tasks.Task SerializeAsync(System.IO.Stream stream, Sentry.Extensibility.IDiagnosticLogger? logger, System.Threading.CancellationToken cancellationToken = default) { } + public string? TryGetFileName() { } + public long? TryGetLength() { } + public string? TryGetType() { } + public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromAttachment(Sentry.SentryAttachment attachment) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromCheckIn(Sentry.SentryCheckIn checkIn) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromEvent(Sentry.SentryEvent @event) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] + public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } + } + public interface ISerializable + { + void Serialize(System.IO.Stream stream, Sentry.Extensibility.IDiagnosticLogger? logger); + System.Threading.Tasks.Task SerializeAsync(System.IO.Stream stream, Sentry.Extensibility.IDiagnosticLogger? logger, System.Threading.CancellationToken cancellationToken = default); + } +} +namespace Sentry.Reflection +{ + public static class AssemblyExtensions + { + public static Sentry.SdkVersion GetNameAndVersion(this System.Reflection.Assembly asm) { } + } +} +public static class SentryExceptionExtensions +{ + public static void AddSentryContext(this System.Exception ex, string name, System.Collections.Generic.IReadOnlyDictionary data) { } + public static void AddSentryTag(this System.Exception ex, string name, string value) { } + public static void SetSentryMechanism(this System.Exception ex, string type, string? description = null, bool? handled = default) { } +} \ No newline at end of file diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 07c413e828..b83e9340d7 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 { @@ -716,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 07c413e828..b83e9340d7 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 { @@ -716,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 2de7c68513..6999d24318 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; } @@ -686,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.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt new file mode 100644 index 0000000000..c510d50d43 --- /dev/null +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt @@ -0,0 +1,190 @@ +[ + { + Header: { + sdk: { + name: sentry.dotnet + } + }, + Items: [ + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + IsInitial: true + } + } + } + ] + }, + { + Header: { + event_id: Guid_2, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: release, + sample_rand: {Scrubbed}, + sample_rate: 1, + sampled: true, + trace_id: Guid_3, + transaction: my transaction + } + }, + Items: [ + { + Header: { + type: event + }, + Payload: { + Source: { + Platform: csharp, + SentryExceptions: [ + { + Mechanism: { + Type: generic, + Handled: false, + Synthetic: false, + IsExceptionGroup: false + } + } + ], + SentryThreads: [ + { + Crashed: false, + Current: true + } + ], + DebugImages: [ + { + Type: pe_dotnet, + ImageAddress: null, + ImageSize: null, + DebugId: ________-____-____-____-____________-________, + DebugChecksum: ______:________________________________________________________________, + DebugFile: .../System.Private.CoreLib.pdb, + CodeId: ______________, + CodeFile: .../System.Private.CoreLib.dll + }, + { + Type: pe_dotnet, + ImageAddress: null, + ImageSize: null, + DebugId: ________-____-____-____-____________-________, + DebugChecksum: ______:________________________________________________________________, + DebugFile: .../Sentry.Tests.pdb, + CodeId: ______________, + CodeFile: .../Sentry.Tests.dll + }, + { + Type: pe_dotnet, + ImageAddress: null, + ImageSize: null, + DebugId: ________-____-____-____-____________-________, + DebugChecksum: ______:________________________________________________________________, + DebugFile: xunit.execution.dotnet.pdb, + CodeId: ______________, + CodeFile: .../xunit.execution.dotnet.dll + }, + { + Type: pe_dotnet, + ImageAddress: null, + ImageSize: null, + DebugId: ________-____-____-____-____________-________, + DebugChecksum: ______:________________________________________________________________, + DebugFile: xunit.core.pdb, + CodeId: ______________, + CodeFile: .../xunit.core.dll + } + ], + Level: error, + TransactionName: my transaction, + Request: {}, + Contexts: { + trace: { + Operation: + } + }, + User: { + Id: Guid_1 + }, + Environment: production + } + } + }, + { + Header: { + type: session + }, + Payload: { + Source: { + DistinctId: Guid_1, + Release: release, + Environment: production, + ErrorCount: 1, + IsInitial: false, + SequenceNumber: 1, + EndStatus: Crashed + } + } + } + ] + }, + { + Header: { + event_id: Guid_4, + sdk: { + name: sentry.dotnet + }, + trace: { + environment: production, + public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, + release: release, + sample_rand: {Scrubbed}, + sample_rate: 1, + sampled: true, + trace_id: Guid_3, + transaction: my transaction + } + }, + Items: [ + { + Header: { + type: transaction + }, + Payload: { + Source: { + Name: my transaction, + Platform: csharp, + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true, + SampleRate: 1.0, + Request: {}, + Contexts: { + trace: { + Operation: my operation, + Description: , + Status: Aborted, + IsSampled: true + } + }, + User: { + Id: Guid_1 + }, + Environment: production, + IsFinished: true + } + } + } + ] + } +] \ No newline at end of file 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..2b4ec36cc8 100644 --- a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs +++ b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs @@ -21,7 +21,7 @@ public async Task SendEnvelopeAsync_CancellationToken_PassedToClient() { // Arrange using var source = new CancellationTokenSource(); - source.Cancel(); + await source.CancelAsync(); var token = source.Token; var httpHandler = Substitute.For(); @@ -36,17 +36,20 @@ public async Task SendEnvelopeAsync_CancellationToken_PassedToClient() var envelope = Envelope.FromEvent( new SentryEvent(eventId: SentryResponses.ResponseId)); -#if NET5_0_OR_GREATER - await Assert.ThrowsAsync(() => httpTransport.SendEnvelopeAsync(envelope, token)); -#else // Act - await httpTransport.SendEnvelopeAsync(envelope, token); + try + { + await httpTransport.SendEnvelopeAsync(envelope, token); + } + catch (TaskCanceledException) + { + // Swallow this + } // Assert await httpHandler .Received(1) .VerifiableSendAsync(Arg.Any(), Arg.Is(c => c.IsCancellationRequested)); -#endif } [Fact] @@ -298,6 +301,7 @@ public async Task SendEnvelopeAsync_ItemRateLimit_DropsItem(string metricNamespa Debug = true }, new HttpClient(httpHandler), + null, clock: _fakeClock); // First request always goes through @@ -382,6 +386,7 @@ public async Task SendEnvelopeAsync_RateLimited_CountsDiscardedEventsCorrectly() var httpTransport = new HttpTransport( options, new HttpClient(httpHandler), + null, clock: _fakeClock ); @@ -846,4 +851,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/Internals/ILSpy/SingleFileAppTests.cs b/test/Sentry.Tests/Internals/ILSpy/SingleFileAppTests.cs index 784c580d59..e7a1c15d3e 100644 --- a/test/Sentry.Tests/Internals/ILSpy/SingleFileAppTests.cs +++ b/test/Sentry.Tests/Internals/ILSpy/SingleFileAppTests.cs @@ -30,16 +30,12 @@ static SingleFileAppTests() InValidBundleFile = Path.Combine(testRoot, invalidBundle); } -#if NET9_0 +#if NET10_0 + private static string TargetFramework => "net10.0"; +#elif NET9_0 private static string TargetFramework => "net9.0"; #elif NET8_0 private static string TargetFramework => "net8.0"; -#elif NET7_0 - private static string TargetFramework => "net7.0"; -#elif NET6_0 - private static string TargetFramework => "net6.0"; -#elif NET5_0 - private static string TargetFramework => "net5.0"; #else // Adding a new TFM to the project? Include it above #error "Target Framework not yet supported for single file apps" diff --git a/test/Sentry.Tests/Internals/MemoryInfoTests.WriteTo.DotNet10_0.verified.txt b/test/Sentry.Tests/Internals/MemoryInfoTests.WriteTo.DotNet10_0.verified.txt new file mode 100644 index 0000000000..1168853c1f --- /dev/null +++ b/test/Sentry.Tests/Internals/MemoryInfoTests.WriteTo.DotNet10_0.verified.txt @@ -0,0 +1,19 @@ +{ + total_allocated_bytes: 1, + fragmented_bytes: 2, + heap_size_bytes: 3, + high_memory_load_threshold_bytes: 4, + total_available_memory_bytes: 5, + memory_load_bytes: 6, + total_committed_bytes: 7, + promoted_bytes: 8, + pinned_objects_count: 9, + pause_time_percentage: 10, + index: 11, + finalization_pending_count: 12, + compacted: true, + concurrent: false, + pause_durations: [ + 1000 + ] +} \ No newline at end of file diff --git a/test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt b/test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt deleted file mode 100644 index 1f67076aaf..0000000000 --- a/test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt +++ /dev/null @@ -1 +0,0 @@ -my.counter@widgets:5|c|#tag1:SanitizeValue(value),tag2:SanitizeValue(value)|T1702270140 diff --git a/test/Sentry.Tests/Sentry.Tests.csproj b/test/Sentry.Tests/Sentry.Tests.csproj index 3f00bc5a9f..1a6e4f3ef8 100644 --- a/test/Sentry.Tests/Sentry.Tests.csproj +++ b/test/Sentry.Tests/Sentry.Tests.csproj @@ -1,10 +1,10 @@  - net9.0;net8.0 - $(TargetFrameworks);net8.0-android34.0;net9.0-android35.0 - $(TargetFrameworks);net8.0-ios17.0;net9.0-ios18.0 - $(TargetFrameworks);net8.0-maccatalyst17.0;net9.0-maccatalyst18.0 + $(CurrentTfms) + $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) + $(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm) + $(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm) $(TargetFrameworks);net48 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; diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt new file mode 100644 index 0000000000..dfbc55fc32 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt new file mode 100644 index 0000000000..10eaeaf749 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt @@ -0,0 +1,44 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + WinUIUnhandledExceptionIntegration + ] + } +] \ No newline at end of file diff --git a/test/SingleFileTestApp/SingleFileTestApp.csproj b/test/SingleFileTestApp/SingleFileTestApp.csproj index 87988ebd8a..93cc9d644a 100644 --- a/test/SingleFileTestApp/SingleFileTestApp.csproj +++ b/test/SingleFileTestApp/SingleFileTestApp.csproj @@ -3,7 +3,7 @@ false Exe - net8.0 + net9.0 11 true true From 876ec162482860676ff095e254187ebf7f6cd9bc Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Sat, 4 Oct 2025 10:14:24 +1300 Subject: [PATCH 03/23] Android AssemblyStore v3 (#4583) Initial commit for #4456 - https://github.com/getsentry/sentry-dotnet/issues/4456 --- CHANGELOG.md | 2 + .../Sentry.Samples.Maui.csproj | 2 +- .../AndroidAssemblyReaderFactory.cs | 22 +- .../V1/ATTRIBUTION.txt | 31 -- .../V1/AndroidAssemblyDirectoryReaderV1.cs | 53 -- .../V1/AndroidAssemblyStoreReaderV1.cs | 510 ------------------ ...2.cs => AndroidAssemblyDirectoryReader.cs} | 4 +- ...derV2.cs => AndroidAssemblyStoreReader.cs} | 8 +- .../V2/AssemblyStoreExplorer.cs | 21 +- .../V2/AssemblyStoreItem.cs | 6 +- .../V2/AssemblyStoreReader.cs | 18 +- ...erV2.Classes.cs => StoreReader.Classes.cs} | 17 +- .../V2/{StoreReaderV2.cs => StoreReader.cs} | 24 +- .../V2/StoreReaderV1.cs | 38 -- test/AndroidTestApp/AndroidTestApp.csproj | 2 + .../AndroidAssemblyReaderTests.cs | 23 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 20 + ...Sentry.Android.AssemblyReader.Tests.csproj | 16 +- 18 files changed, 92 insertions(+), 725 deletions(-) delete mode 100644 src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt delete mode 100644 src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs delete mode 100644 src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs rename src/Sentry.Android.AssemblyReader/V2/{AndroidAssemblyDirectoryReaderV2.cs => AndroidAssemblyDirectoryReader.cs} (98%) rename src/Sentry.Android.AssemblyReader/V2/{AndroidAssemblyStoreReaderV2.cs => AndroidAssemblyStoreReader.cs} (94%) rename src/Sentry.Android.AssemblyReader/V2/{StoreReaderV2.Classes.cs => StoreReader.Classes.cs} (80%) rename src/Sentry.Android.AssemblyReader/V2/{StoreReaderV2.cs => StoreReader.cs} (90%) delete mode 100644 src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs create mode 100644 test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index b32853db5b..94ba273798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### BREAKING CHANGES - This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) +- Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4583](https://github.com/getsentry/sentry-dotnet/pull/4583)) + ## Unreleased ### Features diff --git a/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj b/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj index 77c4e079ee..add0969475 100644 --- a/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj +++ b/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj @@ -6,7 +6,7 @@ On Mac, we'll also build for iOS and MacCatalyst. On Windows, we'll also build for Windows 10. --> - $(TargetFrameworks);net9.0-android35.0 + $(TargetFrameworks);net9.0-android $(TargetFrameworks);net9.0-windows10.0.19041.0;net9.0-ios18.0;net9.0-maccatalyst18.0 $(TargetFrameworks);net9.0-ios18.0;net9.0-maccatalyst18.0 Exe diff --git a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs index e111a46f71..2e4b8d03ab 100644 --- a/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs +++ b/src/Sentry.Android.AssemblyReader/AndroidAssemblyReaderFactory.cs @@ -1,4 +1,3 @@ -using Sentry.Android.AssemblyReader.V1; using Sentry.Android.AssemblyReader.V2; namespace Sentry.Android.AssemblyReader; @@ -19,28 +18,13 @@ public static IAndroidAssemblyReader Open(string apkPath, IList supporte { logger?.Invoke(DebugLoggerLevel.Debug, "Opening APK: {0}", apkPath); -#if NET9_0 - logger?.Invoke(DebugLoggerLevel.Debug, "Reading files using V2 APK layout."); - if (AndroidAssemblyStoreReaderV2.TryReadStore(apkPath, supportedAbis, logger, out var readerV2)) + if (AndroidAssemblyStoreReader.TryReadStore(apkPath, supportedAbis, logger, out var readerV2)) { - logger?.Invoke(DebugLoggerLevel.Debug, "APK uses AssemblyStore V2"); + logger?.Invoke(DebugLoggerLevel.Debug, "APK uses AssemblyStore"); return readerV2; } logger?.Invoke(DebugLoggerLevel.Debug, "APK doesn't use AssemblyStore"); - return new AndroidAssemblyDirectoryReaderV2(apkPath, supportedAbis, logger); -#else - logger?.Invoke(DebugLoggerLevel.Debug, "Reading files using V1 APK layout."); - - var zipArchive = ZipFile.OpenRead(apkPath); - if (zipArchive.GetEntry("assemblies/assemblies.manifest") is not null) - { - logger?.Invoke(DebugLoggerLevel.Debug, "APK uses AssemblyStore V1"); - return new AndroidAssemblyStoreReaderV1(zipArchive, supportedAbis, logger); - } - - logger?.Invoke(DebugLoggerLevel.Debug, "APK doesn't use AssemblyStore"); - return new AndroidAssemblyDirectoryReaderV1(zipArchive, supportedAbis, logger); -#endif + return new AndroidAssemblyDirectoryReader(apkPath, supportedAbis, logger); } } diff --git a/src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt b/src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt deleted file mode 100644 index 16c3f15ed7..0000000000 --- a/src/Sentry.Android.AssemblyReader/V1/ATTRIBUTION.txt +++ /dev/null @@ -1,31 +0,0 @@ -Parts of the code in this subdirectory have been adapted from -https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreExplorer.cs - -The original license is as follows: - - -Xamarin.Android SDK - -The MIT License (MIT) - -Copyright (c) .NET Foundation Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs deleted file mode 100644 index 7b5e1c1a7e..0000000000 --- a/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyDirectoryReaderV1.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Sentry.Android.AssemblyReader.V1; - -// The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file. -internal sealed class AndroidAssemblyDirectoryReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader -{ - public AndroidAssemblyDirectoryReaderV1(ZipArchive zip, IList supportedAbis, DebugLogger? logger) - : base(zip, supportedAbis, logger) { } - - public PEReader? TryReadAssembly(string name) - { - if (File.Exists(name)) - { - // The assembly is already extracted to the file system. Just read it. - var stream = File.OpenRead(name); - return new PEReader(stream); - } - - var zipEntry = FindAssembly(name); - if (zipEntry is null) - { - Logger?.Invoke(DebugLoggerLevel.Debug, "Couldn't find assembly {0} in the APK", name); - return null; - } - - Logger?.Invoke(DebugLoggerLevel.Debug, "Resolved assembly {0} in the APK at {1}", name, zipEntry.FullName); - - // We need a seekable stream for the PEReader (or even to check whether the DLL is compressed), so make a copy. - var memStream = zipEntry.Extract(); - return ArchiveUtils.CreatePEReader(name, memStream, Logger); - } - - private ZipArchiveEntry? FindAssembly(string name) - { - var zipEntry = ZipArchive.GetEntry($"assemblies/{name}"); - - if (zipEntry is null) - { - foreach (var abi in SupportedAbis) - { - if (abi.Length > 0) - { - zipEntry = ZipArchive.GetEntry($"assemblies/{abi}/{name}"); - if (zipEntry is not null) - { - break; - } - } - } - } - - return zipEntry; - } -} diff --git a/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs b/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs deleted file mode 100644 index 60c947b23e..0000000000 --- a/src/Sentry.Android.AssemblyReader/V1/AndroidAssemblyStoreReaderV1.cs +++ /dev/null @@ -1,510 +0,0 @@ -namespace Sentry.Android.AssemblyReader.V1; - -// See https://devblogs.microsoft.com/dotnet/performance-improvements-in-dotnet-maui/#single-file-assembly-stores -internal sealed class AndroidAssemblyStoreReaderV1 : AndroidAssemblyReader, IAndroidAssemblyReader -{ - private readonly AssemblyStoreExplorer _explorer; - - public AndroidAssemblyStoreReaderV1(ZipArchive zip, IList supportedAbis, DebugLogger? logger) - : base(zip, supportedAbis, logger) - { - _explorer = new(zip, supportedAbis, logger); - } - - public PEReader? TryReadAssembly(string name) - { - var assembly = TryFindAssembly(name); - if (assembly is null) - { - Logger?.Invoke(DebugLoggerLevel.Debug, "Couldn't find assembly {0} in the APK AssemblyStore", name); - return null; - } - - Logger?.Invoke(DebugLoggerLevel.Debug, "Resolved assembly {0} in the APK {1} AssemblyStore", name, assembly.Store.Arch); - - var stream = assembly.GetImage(); - if (stream is null) - { - Logger?.Invoke(DebugLoggerLevel.Debug, "Couldn't access assembly {0} image stream", name); - return null; - } - - return ArchiveUtils.CreatePEReader(name, stream, Logger); - } - - private AssemblyStoreAssembly? TryFindAssembly(string name) - { - if (_explorer.AssembliesByName.TryGetValue(name, out var assembly)) - { - return assembly; - } - - if (name.EndsWith(".dll", ignoreCase: true, CultureInfo.InvariantCulture) || - name.EndsWith(".exe", ignoreCase: true, CultureInfo.InvariantCulture)) - { - if (_explorer.AssembliesByName.TryGetValue(name.Substring(0, name.Length - 4), out assembly)) - { - return assembly; - } - } - - return null; - } - - // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreExplorer.cs - // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). - private class AssemblyStoreExplorer - { - private AssemblyStoreReader? _indexStore; - private readonly AssemblyStoreManifestReader _manifest; - private readonly DebugLogger? _logger; - - public IDictionary AssembliesByName { get; } = - new SortedDictionary(StringComparer.OrdinalIgnoreCase); - - public IDictionary AssembliesByHash32 { get; } = - new Dictionary(); - - public IDictionary AssembliesByHash64 { get; } = - new Dictionary(); - - public List Assemblies { get; } = new(); - - public IDictionary> Stores { get; } = - new SortedDictionary>(); - - public AssemblyStoreExplorer(ZipArchive zip, IList supportedAbis, DebugLogger? logger) - { - _logger = logger; - _manifest = new AssemblyStoreManifestReader(zip.GetEntry("assemblies/assemblies.manifest")!.Open()); - - TryAddStore(zip, null); - foreach (var abi in supportedAbis) - { - if (!string.IsNullOrEmpty(abi)) - { - TryAddStore(zip, abi); - } - } - - zip.Dispose(); - ProcessStores(); - } - - private void ProcessStores() - { - if (Stores.Count == 0 || _indexStore == null) - { - return; - } - - ProcessIndex(_indexStore.GlobalIndex32, "32", (he, assembly) => - { - assembly.Hash32 = (uint)he.Hash; - assembly.RuntimeIndex = he.MappingIndex; - - if (_manifest.EntriesByHash32.TryGetValue(assembly.Hash32, out var me)) - { - assembly.Name = me.Name; - } - - if (!AssembliesByHash32.ContainsKey(assembly.Hash32)) - { - AssembliesByHash32.Add(assembly.Hash32, assembly); - } - }); - - ProcessIndex(_indexStore.GlobalIndex64, "64", (he, assembly) => - { - assembly.Hash64 = he.Hash; - if (assembly.RuntimeIndex != he.MappingIndex) - { - _logger?.Invoke(DebugLoggerLevel.Debug, - $"assembly with hashes 0x{assembly.Hash32} and 0x{assembly.Hash64} has a different 32-bit runtime index ({assembly.RuntimeIndex}) than the 64-bit runtime index({he.MappingIndex})"); - } - - if (_manifest.EntriesByHash64.TryGetValue(assembly.Hash64, out var me)) - { - if (string.IsNullOrEmpty(assembly.Name)) - { - _logger?.Invoke(DebugLoggerLevel.Debug, - $"32-bit hash 0x{assembly.Hash32:x} did not match any assembly name in the manifest"); - assembly.Name = me.Name; - if (string.IsNullOrEmpty(assembly.Name)) - { - _logger?.Invoke(DebugLoggerLevel.Debug, - $"64-bit hash 0x{assembly.Hash64:x} did not match any assembly name in the manifest"); - } - } - else if (!string.Equals(assembly.Name, me.Name, StringComparison.Ordinal)) - { - _logger?.Invoke(DebugLoggerLevel.Debug, - $"32-bit hash 0x{assembly.Hash32:x} maps to assembly name '{assembly.Name}', however 64-bit hash 0x{assembly.Hash64:x} for the same entry matches assembly name '{me.Name}'"); - } - } - - if (!AssembliesByHash64.ContainsKey(assembly.Hash64)) - { - AssembliesByHash64.Add(assembly.Hash64, assembly); - } - }); - - foreach (var kvp in Stores) - { - var list = kvp.Value; - if (list.Count < 2) - { - continue; - } - - var template = list[0]; - for (var i = 1; i < list.Count; i++) - { - var other = list[i]; - if (!template.HasIdenticalContent(other)) - { - throw new Exception( - $"Store ID {template.StoreId} for architecture {other.Arch} is not identical to other stores with the same ID"); - } - } - } - - void ProcessIndex(List index, string bitness, - Action assemblyHandler) - { - foreach (var he in index) - { - if (!Stores.TryGetValue(he.StoreId, out var storeList)) - { - _logger?.Invoke(DebugLoggerLevel.Debug, $"store with id {he.StoreId} not part of the set"); - continue; - } - - foreach (var store in storeList) - { - if (he.LocalStoreIndex >= (uint)store.Assemblies.Count) - { - _logger?.Invoke(DebugLoggerLevel.Debug, - $"{bitness}-bit index entry with hash 0x{he.Hash:x} has invalid store {store.StoreId} index {he.LocalStoreIndex} (maximum allowed is {store.Assemblies.Count})"); - continue; - } - - var assembly = store.Assemblies[(int)he.LocalStoreIndex]; - assemblyHandler(he, assembly); - - if (!AssembliesByName.ContainsKey(assembly.Name)) - { - AssembliesByName.Add(assembly.Name, assembly); - } - } - } - } - } - - private void TryAddStore(ZipArchive archive, string? abi) - { - var infix = abi is null ? "" : $".{abi}"; - if (archive.GetEntry($"assemblies/assemblies{infix}.blob") is { } zipEntry) - { - var memStream = new MemoryStream((int)zipEntry.Length); - using (var zipStream = zipEntry.Open()) - { - zipStream.CopyTo(memStream); - memStream.Position = 0; - } - - AddStore(new AssemblyStoreReader(memStream, abi)); - } - } - - private void AddStore(AssemblyStoreReader reader) - { - if (reader.HasGlobalIndex) - { - _indexStore = reader; - } - - if (!Stores.TryGetValue(reader.StoreId, out var storeList)) - { - storeList = new List(); - Stores.Add(reader.StoreId, storeList); - } - - storeList.Add(reader); - - Assemblies.AddRange(reader.Assemblies); - } - } - - // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreManifestReader.cs - // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). - private class AssemblyStoreManifestReader - { - public List Entries { get; } = new(); - public Dictionary EntriesByHash32 { get; } = new(); - public Dictionary EntriesByHash64 { get; } = new(); - - public AssemblyStoreManifestReader(Stream manifest) - { - using var sr = new StreamReader(manifest, Encoding.UTF8, detectEncodingFromByteOrderMarks: false); - ReadManifest(sr); - } - - private void ReadManifest(StreamReader reader) - { - // First line is ignored, it contains headers - reader.ReadLine(); - - // Each subsequent line consists of fields separated with any number of spaces (for the pleasure of a human being reading the manifest) - while (!reader.EndOfStream) - { - var fields = reader.ReadLine()?.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - if (fields == null) - { - continue; - } - - var entry = new AssemblyStoreManifestEntry(fields); - Entries.Add(entry); - if (entry.Hash32 != 0) - { - EntriesByHash32.Add(entry.Hash32, entry); - } - - if (entry.Hash64 != 0) - { - EntriesByHash64.Add(entry.Hash64, entry); - } - } - } - } - - // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreManifestEntry.cs - // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). - private class AssemblyStoreManifestEntry - { - // Fields are: - // Hash 32 | Hash 64 | Store ID | Store idx | Name - private const int NumberOfFields = 5; - private const int Hash32FieldIndex = 0; - private const int Hash64FieldIndex = 1; - private const int StoreIdFieldIndex = 2; - private const int StoreIndexFieldIndex = 3; - private const int NameFieldIndex = 4; - - public uint Hash32 { get; } - public ulong Hash64 { get; } - public uint StoreId { get; } - public uint IndexInStore { get; } - public string Name { get; } - - public AssemblyStoreManifestEntry(string[] fields) - { - if (fields.Length != NumberOfFields) - { - throw new ArgumentOutOfRangeException(nameof(fields), "Invalid number of fields"); - } - - Hash32 = GetUInt32(fields[Hash32FieldIndex]); - Hash64 = GetUInt64(fields[Hash64FieldIndex]); - StoreId = GetUInt32(fields[StoreIdFieldIndex]); - IndexInStore = GetUInt32(fields[StoreIndexFieldIndex]); - Name = fields[NameFieldIndex].Trim(); - } - - private static uint GetUInt32(string value) - { - if (uint.TryParse(PrepHexValue(value), NumberStyles.HexNumber, null, out var hash)) - { - return hash; - } - - return 0; - } - - private static ulong GetUInt64(string value) - { - if (ulong.TryParse(PrepHexValue(value), NumberStyles.HexNumber, null, out var hash)) - { - return hash; - } - - return 0; - } - - private static string PrepHexValue(string value) - { - if (value.StartsWith("0x", StringComparison.Ordinal)) - { - return value.Substring(2); - } - - return value; - } - } - - // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreHashEntry.cs - // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). - private class AssemblyStoreHashEntry - { - public bool Is32Bit { get; } - public ulong Hash { get; } - public uint MappingIndex { get; } - public uint LocalStoreIndex { get; } - public uint StoreId { get; } - - internal AssemblyStoreHashEntry(BinaryReader reader, bool is32Bit) - { - Is32Bit = is32Bit; - Hash = reader.ReadUInt64(); - MappingIndex = reader.ReadUInt32(); - LocalStoreIndex = reader.ReadUInt32(); - StoreId = reader.ReadUInt32(); - } - } - - // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreReader.cs - // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). - private class AssemblyStoreReader - { - // These two constants must be identical to the native ones in src/monodroid/jni/xamarin-app.hh - private const uint ASSEMBLY_STORE_MAGIC = 0x41424158; // 'XABA', little-endian - private const uint ASSEMBLY_STORE_FORMAT_VERSION = 1; // The highest format version this reader understands - - private readonly MemoryStream _storeData; - - public uint Version { get; private set; } - public uint LocalEntryCount { get; private set; } - public uint GlobalEntryCount { get; private set; } - public uint StoreId { get; private set; } - public List Assemblies { get; } - public List GlobalIndex32 { get; } = new(); - public List GlobalIndex64 { get; } = new(); - public string Arch { get; } - - public bool HasGlobalIndex => StoreId == 0; - - public AssemblyStoreReader(MemoryStream store, string? arch = null) - { - Arch = arch ?? string.Empty; - _storeData = store; - using var reader = new BinaryReader(store, Encoding.UTF8, leaveOpen: true); - ReadHeader(reader); - - Assemblies = new List(); - ReadLocalEntries(reader, Assemblies); - if (HasGlobalIndex) - { - ReadGlobalIndex(reader, GlobalIndex32, GlobalIndex64); - } - } - - internal MemoryStream? GetAssemblyImageSlice(AssemblyStoreAssembly assembly) => - GetDataSlice(assembly.DataOffset, assembly.DataSize); - - internal MemoryStream? GetAssemblyDebugDataSlice(AssemblyStoreAssembly assembly) => - assembly.DebugDataOffset == 0 ? null : GetDataSlice(assembly.DebugDataOffset, assembly.DebugDataSize); - - internal MemoryStream? GetAssemblyConfigSlice(AssemblyStoreAssembly assembly) => - assembly.ConfigDataOffset == 0 ? null : GetDataSlice(assembly.ConfigDataOffset, assembly.ConfigDataSize); - - private MemoryStream? GetDataSlice(uint offset, uint size) => - size == 0 ? null : new ArchiveUtils.MemorySlice(_storeData, (int)offset, (int)size); - - public bool HasIdenticalContent(AssemblyStoreReader other) - { - return - other.Version == Version && - other.LocalEntryCount == LocalEntryCount && - other.GlobalEntryCount == GlobalEntryCount && - other.StoreId == StoreId && - other.Assemblies.Count == Assemblies.Count && - other.GlobalIndex32.Count == GlobalIndex32.Count && - other.GlobalIndex64.Count == GlobalIndex64.Count; - } - - private void ReadHeader(BinaryReader reader) - { - if (reader.ReadUInt32() != ASSEMBLY_STORE_MAGIC) - { - throw new InvalidOperationException("Invalid header magic number"); - } - - Version = reader.ReadUInt32(); - if (Version == 0) - { - throw new InvalidOperationException("Invalid version number: 0"); - } - - if (Version > ASSEMBLY_STORE_FORMAT_VERSION) - { - throw new InvalidOperationException( - $"Store format version {Version} is higher than the one understood by this reader, {ASSEMBLY_STORE_FORMAT_VERSION}"); - } - - LocalEntryCount = reader.ReadUInt32(); - GlobalEntryCount = reader.ReadUInt32(); - StoreId = reader.ReadUInt32(); - } - - private void ReadLocalEntries(BinaryReader reader, List assemblies) - { - for (uint i = 0; i < LocalEntryCount; i++) - { - assemblies.Add(new AssemblyStoreAssembly(reader, this)); - } - } - - private void ReadGlobalIndex(BinaryReader reader, List index32, - List index64) - { - ReadIndex(true, index32); - ReadIndex(false, index64); - - void ReadIndex(bool is32Bit, List index) - { - for (uint i = 0; i < GlobalEntryCount; i++) - { - index.Add(new AssemblyStoreHashEntry(reader, is32Bit)); - } - } - } - } - - // Adapted from https://github.com/xamarin/xamarin-android/blob/c92702619f5fabcff0ed88e09160baf9edd70f41/tools/assembly-store-reader/AssemblyStoreAssembly.cs - // With the original code licensed under MIT License (https://github.com/xamarin/xamarin-android/blob/2bd13c4a00ae78db34663a4b9c7a4c5bfb20c344/LICENSE). - private class AssemblyStoreAssembly - { - public uint DataOffset { get; } - public uint DataSize { get; } - public uint DebugDataOffset { get; } - public uint DebugDataSize { get; } - public uint ConfigDataOffset { get; } - public uint ConfigDataSize { get; } - - public uint Hash32 { get; set; } - public ulong Hash64 { get; set; } - public string Name { get; set; } = string.Empty; - public uint RuntimeIndex { get; set; } - - public AssemblyStoreReader Store { get; } - - internal AssemblyStoreAssembly(BinaryReader reader, AssemblyStoreReader store) - { - Store = store; - - DataOffset = reader.ReadUInt32(); - DataSize = reader.ReadUInt32(); - DebugDataOffset = reader.ReadUInt32(); - DebugDataSize = reader.ReadUInt32(); - ConfigDataOffset = reader.ReadUInt32(); - ConfigDataSize = reader.ReadUInt32(); - } - - public MemoryStream? GetImage() => Store.GetAssemblyImageSlice(this); - - public MemoryStream? GetDebugData() => Store.GetAssemblyDebugDataSlice(this); - - public MemoryStream? GetAssemblyConfig() => Store.GetAssemblyConfigSlice(this); - } -} diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReader.cs similarity index 98% rename from src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs rename to src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReader.cs index b250640d1f..ca2b96d886 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReaderV2.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyDirectoryReader.cs @@ -1,13 +1,13 @@ namespace Sentry.Android.AssemblyReader.V2; // The "Old" app type - where each DLL is placed in the 'assemblies' directory as an individual file. -internal sealed class AndroidAssemblyDirectoryReaderV2 : IAndroidAssemblyReader +internal sealed class AndroidAssemblyDirectoryReader : IAndroidAssemblyReader { private DebugLogger? Logger { get; } private HashSet SupportedArchitectures { get; } = new(); private readonly ArchiveAssemblyHelper _archiveAssemblyHelper; - public AndroidAssemblyDirectoryReaderV2(string apkPath, IList supportedAbis, DebugLogger? logger) + public AndroidAssemblyDirectoryReader(string apkPath, IList supportedAbis, DebugLogger? logger) { Logger = logger; foreach (var abi in supportedAbis) diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReader.cs similarity index 94% rename from src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs rename to src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReader.cs index 2840c7ec67..0e9c6586f4 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReader.cs @@ -1,17 +1,17 @@ namespace Sentry.Android.AssemblyReader.V2; -internal class AndroidAssemblyStoreReaderV2 : IAndroidAssemblyReader +internal class AndroidAssemblyStoreReader : IAndroidAssemblyReader { private readonly IList _explorers; private readonly DebugLogger? _logger; - private AndroidAssemblyStoreReaderV2(IList explorers, DebugLogger? logger) + private AndroidAssemblyStoreReader(IList explorers, DebugLogger? logger) { _explorers = explorers; _logger = logger; } - public static bool TryReadStore(string inputFile, IList supportedAbis, DebugLogger? logger, [NotNullWhen(true)] out AndroidAssemblyStoreReaderV2? reader) + public static bool TryReadStore(string inputFile, IList supportedAbis, DebugLogger? logger, [NotNullWhen(true)] out AndroidAssemblyStoreReader? reader) { List supportedExplorers = []; @@ -67,7 +67,7 @@ public static bool TryReadStore(string inputFile, IList supportedAbis, D return false; } - reader = new AndroidAssemblyStoreReaderV2(supportedExplorers, logger); + reader = new AndroidAssemblyStoreReader(supportedExplorers, logger); return true; } diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs index 0ab9c04e2e..5854d3c070 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs @@ -79,28 +79,19 @@ public static (IList? explorers, string? errorMessage) Op } private static (IList? explorers, string? errorMessage) OpenAab(FileInfo fi, DebugLogger? logger) - => OpenCommon(fi, [StoreReaderV2.AabPaths, StoreReader_V1.AabPaths], logger); + => OpenCommon(fi, StoreReader.AabPaths, logger); private static (IList? explorers, string? errorMessage) OpenAabBase(FileInfo fi, DebugLogger? logger) - => OpenCommon(fi, [StoreReaderV2.AabBasePaths, StoreReader_V1.AabBasePaths], logger); + => OpenCommon(fi, StoreReader.AabBasePaths, logger); private static (IList? explorers, string? errorMessage) OpenApk(FileInfo fi, DebugLogger? logger) - => OpenCommon(fi, [StoreReaderV2.ApkPaths, StoreReader_V1.ApkPaths], logger); + => OpenCommon(fi, StoreReader.ApkPaths, logger); - private static (IList? explorers, string? errorMessage) OpenCommon(FileInfo fi, List> pathLists, DebugLogger? logger) + private static (IList? explorers, string? errorMessage) OpenCommon(FileInfo fi, IList paths, DebugLogger? logger) { using var zip = ZipFile.OpenRead(fi.FullName); - - foreach (var paths in pathLists) - { - var (explorers, errorMessage, pathsFound) = TryLoad(fi, zip, paths, logger); - if (pathsFound) - { - return (explorers, errorMessage); - } - } - - return (null, "Unable to find any blob entries"); + var (explorers, errorMessage, pathsFound) = TryLoad(fi, zip, paths, logger); + return pathsFound ? (explorers, errorMessage) : (null, "Unable to find any blob entries"); } private static (IList? explorers, string? errorMessage, bool pathsFound) TryLoad(FileInfo fi, ZipArchive zip, IList paths, DebugLogger? logger) diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs index a132f51ead..2ecde5fd48 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreItem.cs @@ -1,5 +1,7 @@ /* * Adapted from https://github.com/dotnet/android/blob/86260ed36dfe1a90c8ed6a2bb1cd0607d637f403/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs + * Updated from https://github.com/dotnet/android/blob/64018e13e53cec7246e54866b520d3284de344e0/tools/assembly-store-reader-mk2/AssemblyStore/AssemblyStoreItem.cs + * - Adding support for AssemblyStore v3 format that shipped in .NET 10 (https://github.com/dotnet/android/pull/10249) * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) */ @@ -17,11 +19,13 @@ internal abstract class AssemblyStoreItem public uint ConfigOffset { get; protected set; } public uint ConfigSize { get; protected set; } public AndroidTargetArch TargetArch { get; protected set; } + public bool Ignore { get; } - protected AssemblyStoreItem(string name, bool is64Bit, List hashes) + protected AssemblyStoreItem(string name, bool is64Bit, List hashes, bool ignore) { Name = name; Hashes = hashes.AsReadOnly(); Is64Bit = is64Bit; + Ignore = ignore; } } diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs index 834cb2c36d..a3201754e1 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreReader.cs @@ -32,23 +32,7 @@ protected AssemblyStoreReader(Stream store, string path, DebugLogger? logger) public static AssemblyStoreReader? Create(Stream store, string path, DebugLogger? logger) { - var reader = MakeReaderReady(new StoreReader_V1(store, path, logger)); - if (reader != null) - { - return reader; - } - - reader = MakeReaderReady(new StoreReaderV2(store, path, logger)); - if (reader != null) - { - return reader; - } - - return null; - } - - private static AssemblyStoreReader? MakeReaderReady(AssemblyStoreReader reader) - { + var reader = new StoreReader(store, path, logger); if (!reader.IsSupported()) { return null; diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs similarity index 80% rename from src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs rename to src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs index 85aa91ba89..5ed713b6be 100644 --- a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.Classes.cs +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReader.Classes.cs @@ -1,11 +1,13 @@ /* * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs + * Updated from https://github.com/dotnet/android/blob/64018e13e53cec7246e54866b520d3284de344e0/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.Classes.cs + * - Adding support for AssemblyStore v3 format that shipped in .NET 10 (https://github.com/dotnet/android/pull/10249) * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) */ namespace Sentry.Android.AssemblyReader.V2; -internal partial class StoreReaderV2 +internal partial class StoreReader { private sealed class Header { @@ -33,11 +35,13 @@ private sealed class IndexEntry { public readonly ulong name_hash; public readonly uint descriptor_index; + public readonly bool ignore; - public IndexEntry(ulong name_hash, uint descriptor_index) + public IndexEntry(ulong name_hash, uint descriptor_index, bool ignore) { this.name_hash = name_hash; this.descriptor_index = descriptor_index; + this.ignore = ignore; } } @@ -57,8 +61,9 @@ private sealed class EntryDescriptor private sealed class StoreItemV2 : AssemblyStoreItem { - public StoreItemV2(AndroidTargetArch targetArch, string name, bool is64Bit, List indexEntries, EntryDescriptor descriptor) - : base(name, is64Bit, IndexToHashes(indexEntries)) + public StoreItemV2(AndroidTargetArch targetArch, string name, bool is64Bit, List indexEntries, + EntryDescriptor descriptor, bool ignore) + : base(name, is64Bit, IndexToHashes(indexEntries), ignore) { DataOffset = descriptor.data_offset; DataSize = descriptor.data_size; @@ -86,11 +91,13 @@ private sealed class TemporaryItem public readonly string Name; public readonly List IndexEntries = new List(); public readonly EntryDescriptor Descriptor; + public readonly bool Ignored; - public TemporaryItem(string name, EntryDescriptor descriptor) + public TemporaryItem(string name, EntryDescriptor descriptor, bool ignored) { Name = name; Descriptor = descriptor; + Ignored = ignored; } } } diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReader.cs similarity index 90% rename from src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs rename to src/Sentry.Android.AssemblyReader/V2/StoreReader.cs index 97bac95694..5220bf7dc7 100644 --- a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV2.cs +++ b/src/Sentry.Android.AssemblyReader/V2/StoreReader.cs @@ -1,15 +1,22 @@ /* * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs + * Updated from https://github.com/dotnet/android/blob/64018e13e53cec7246e54866b520d3284de344e0/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V2.cs + * - Adding support for AssemblyStore v3 format that shipped in .NET 10 (https://github.com/dotnet/android/pull/10249) * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) */ namespace Sentry.Android.AssemblyReader.V2; -internal partial class StoreReaderV2 : AssemblyStoreReader +internal partial class StoreReader : AssemblyStoreReader { // Bit 31 is set for 64-bit platforms, cleared for the 32-bit ones +#if NET9_0 private const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000002; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant private const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000002; +#else + private const uint ASSEMBLY_STORE_FORMAT_VERSION_64BIT = 0x80000003; // Must match the ASSEMBLY_STORE_FORMAT_VERSION native constant + private const uint ASSEMBLY_STORE_FORMAT_VERSION_32BIT = 0x00000003; +#endif private const uint ASSEMBLY_STORE_FORMAT_VERSION_MASK = 0xF0000000; private const uint ASSEMBLY_STORE_ABI_AARCH64 = 0x00010000; private const uint ASSEMBLY_STORE_ABI_ARM = 0x00020000; @@ -28,7 +35,7 @@ internal partial class StoreReaderV2 : AssemblyStoreReader private Header? header; private ulong elfOffset = 0; - static StoreReaderV2() + static StoreReader() { var paths = new List { GetArchPath (AndroidTargetArch.Arm64), @@ -69,7 +76,7 @@ string GetArchPath(AndroidTargetArch arch, string? root = null) } } - public StoreReaderV2(Stream store, string path, DebugLogger? logger) + public StoreReader(Stream store, string path, DebugLogger? logger) : base(store, path, logger) { supportedVersions = new HashSet { @@ -178,7 +185,12 @@ protected override void Prepare() } uint descriptor_index = reader.ReadUInt32(); - index.Add(new IndexEntry(name_hash, descriptor_index)); +#if NET10_0_OR_GREATER + bool ignore = reader.ReadByte () != 0; +#else + bool ignore = false; +#endif + index.Add(new IndexEntry(name_hash, descriptor_index, ignore)); } var descriptors = new List(); @@ -218,7 +230,7 @@ protected override void Prepare() { if (!tempItems.TryGetValue(ie.descriptor_index, out TemporaryItem? item)) { - item = new TemporaryItem(names[(int)ie.descriptor_index], descriptors[(int)ie.descriptor_index]); + item = new TemporaryItem(names[(int)ie.descriptor_index], descriptors[(int)ie.descriptor_index], ie.ignore); tempItems.Add(ie.descriptor_index, item); } item.IndexEntries.Add(ie); @@ -233,7 +245,7 @@ protected override void Prepare() foreach (var kvp in tempItems) { TemporaryItem ti = kvp.Value; - var item = new StoreItemV2(TargetArch, ti.Name, Is64Bit, ti.IndexEntries, ti.Descriptor); + var item = new StoreItemV2(TargetArch, ti.Name, Is64Bit, ti.IndexEntries, ti.Descriptor, ti.Ignored); storeItems.Add(item); } Assemblies = storeItems.AsReadOnly(); diff --git a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs b/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs deleted file mode 100644 index c1a3fe3c2d..0000000000 --- a/src/Sentry.Android.AssemblyReader/V2/StoreReaderV1.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Adapted from https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/tools/assembly-store-reader-mk2/AssemblyStore/StoreReader_V1.cs - * Original code licensed under the MIT License (https://github.com/dotnet/android/blob/5ebcb1dd1503648391e3c0548200495f634d90c6/LICENSE.TXT) - */ - -namespace Sentry.Android.AssemblyReader.V2; - -internal class StoreReader_V1 : AssemblyStoreReader -{ - public override string Description => "Assembly store v1"; - public override bool NeedsExtensionInName => false; - - public static IList ApkPaths { get; } - public static IList AabPaths { get; } - public static IList AabBasePaths { get; } - - static StoreReader_V1() - { - ApkPaths = new List().AsReadOnly(); - AabPaths = new List().AsReadOnly(); - AabBasePaths = new List().AsReadOnly(); - } - - public StoreReader_V1(Stream store, string path, DebugLogger? logger) - : base(store, path, logger) - { } - - protected override bool IsSupported() - { - return false; - } - - protected override void Prepare() - { - } - - protected override ulong GetStoreStartDataOffset() => 0; -} diff --git a/test/AndroidTestApp/AndroidTestApp.csproj b/test/AndroidTestApp/AndroidTestApp.csproj index 261fc05b24..cefce5290f 100644 --- a/test/AndroidTestApp/AndroidTestApp.csproj +++ b/test/AndroidTestApp/AndroidTestApp.csproj @@ -2,6 +2,7 @@ net10.0-android;net9.0-android false + false 21 Exe enable @@ -9,5 +10,6 @@ com.companyname.AndroidTestApp 1 1.0 + android-x64 diff --git a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs index f0d1a50c64..4675f4e02e 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs +++ b/test/Sentry.Android.AssemblyReader.Tests/AndroidAssemblyReaderTests.cs @@ -1,4 +1,3 @@ -using Sentry.Android.AssemblyReader.V1; using Sentry.Android.AssemblyReader.V2; namespace Sentry.Android.AssemblyReader.Tests; @@ -11,8 +10,6 @@ public class AndroidAssemblyReaderTests private static string TargetFramework => "net10.0"; #elif NET9_0 private static string TargetFramework => "net9.0"; -#elif NET8_0 - private static string TargetFramework => "net8.0"; #else // Adding a new TFM to the project? Include it above #error "Target Framework not yet supported for AndroidAssemblyReader" @@ -54,11 +51,11 @@ public void CreatesCorrectStoreReader() using var sut = GetSut(isAot: false, isAssemblyStore: true, isCompressed: true); switch (TargetFramework) { - case "net9.0": - Assert.IsType(sut); + case "net10.0": + Assert.IsType(sut); break; - case "net8.0": - Assert.IsType(sut); + case "net9.0": + Assert.IsType(sut); break; default: throw new NotSupportedException($"Unsupported target framework: {TargetFramework}"); @@ -74,11 +71,11 @@ public void CreatesCorrectArchiveReader() using var sut = GetSut(isAot: false, isAssemblyStore: false, isCompressed: true); switch (TargetFramework) { - case "net9.0": - Assert.IsType(sut); + case "net10.0": + Assert.IsType(sut); break; - case "net8.0": - Assert.IsType(sut); + case "net9.0": + Assert.IsType(sut); break; default: throw new NotSupportedException($"Unsupported target framework: {TargetFramework}"); @@ -95,11 +92,7 @@ public void ReturnsNullIfAssemblyDoesntExist(bool isAssemblyStore) } public static IEnumerable ReadsAssemblyPermutations => -#if NET8_0 - from isAot in new[] { false } -#else from isAot in new[] { true, false } -#endif from isStore in new[] { true, false } from isCompressed in new[] { true, false } from assemblyName in new[] { "Mono.Android.dll", "System.Private.CoreLib.dll" } diff --git a/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt new file mode 100644 index 0000000000..67bbca78b3 --- /dev/null +++ b/test/Sentry.Android.AssemblyReader.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -0,0 +1,20 @@ +namespace Sentry.Android.AssemblyReader +{ + public static class AndroidAssemblyReaderFactory + { + public static Sentry.Android.AssemblyReader.IAndroidAssemblyReader Open(string apkPath, System.Collections.Generic.IList supportedAbis, Sentry.Android.AssemblyReader.DebugLogger? logger = null) { } + } + public delegate void DebugLogger(Sentry.Android.AssemblyReader.DebugLoggerLevel level, string message, params object?[] args); + public enum DebugLoggerLevel : short + { + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + Fatal = 4, + } + public interface IAndroidAssemblyReader : System.IDisposable + { + System.Reflection.PortableExecutable.PEReader? TryReadAssembly(string name); + } +} \ No newline at end of file diff --git a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj index 8d50df6bec..a892724b49 100644 --- a/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj +++ b/test/Sentry.Android.AssemblyReader.Tests/Sentry.Android.AssemblyReader.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + $(LatestTfm);$(PreviousTfm) $(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm) enable @@ -20,7 +20,7 @@ - + @@ -42,11 +42,10 @@ <_TestAPK Include="2" Properties="_Aot=False;_Store=False;_Compressed=True" /> <_TestAPK Include="3" Properties="_Aot=False;_Store=True;_Compressed=False" /> <_TestAPK Include="4" Properties="_Aot=False;_Store=True;_Compressed=True" /> - - <_TestAPK Include="5" Condition="!$(TargetFramework.StartsWith('net8'))" Properties="_Aot=True;_Store=False;_Compressed=False" /> - <_TestAPK Include="6" Condition="!$(TargetFramework.StartsWith('net8'))" Properties="_Aot=True;_Store=False;_Compressed=True" /> - <_TestAPK Include="7" Condition="!$(TargetFramework.StartsWith('net8'))" Properties="_Aot=True;_Store=True;_Compressed=False" /> - <_TestAPK Include="8" Condition="!$(TargetFramework.StartsWith('net8'))" Properties="_Aot=True;_Store=True;_Compressed=True" /> + <_TestAPK Include="5" Properties="_Aot=True;_Store=False;_Compressed=False" /> + <_TestAPK Include="6" Properties="_Aot=True;_Store=False;_Compressed=True" /> + <_TestAPK Include="7" Properties="_Aot=True;_Store=True;_Compressed=False" /> + <_TestAPK Include="8" Properties="_Aot=True;_Store=True;_Compressed=True" /> @@ -57,7 +56,8 @@ ..\AndroidTestApp\bin\$(TargetFramework)\$(_ConfigString)\com.companyname.AndroidTestApp-Signed.apk TestAPKs\$(TargetFramework)-$(_ConfigString).apk - + + From 6ef9a94b3df3489ea8ca6e7ad4632b85dc21c6a1 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sun, 5 Oct 2025 22:35:43 +0000 Subject: [PATCH 04/23] release: 6.0.0-preview.1 --- CHANGELOG.md | 16 ++++++++-------- Directory.Build.props | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94ba273798..0e39c2c1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,6 @@ # Changelog -## Version 6.0.0 - -### BREAKING CHANGES - -- This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) -- Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4583](https://github.com/getsentry/sentry-dotnet/pull/4583)) - -## Unreleased +## 6.0.0-preview.1 ### Features @@ -38,6 +31,13 @@ - [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) +## Version 6.0.0 + +### BREAKING CHANGES + +- This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) +- Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4583](https://github.com/getsentry/sentry-dotnet/pull/4583)) + ## 5.15.1 ### Fixes diff --git a/Directory.Build.props b/Directory.Build.props index e32b83eed7..eac7530416 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0 + 6.0.0-preview.1 prerelease 13 true From 13d64cf6013d00c2a2fa07f9f68cb899d0b76de6 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 7 Oct 2025 09:30:48 +1300 Subject: [PATCH 05/23] Merge from main (#4596) --- .github/workflows/codeql-analysis.yml | 4 +- CHANGELOG.md | 27 ++++--- CONTRIBUTING.md | 23 ++++++ Directory.Build.props | 2 +- modules/sentry-native | 2 +- .../Sentry.Samples.Console.Basic/Program.cs | 8 +- scripts/build-sentry-cocoa.sh | 4 +- scripts/generate-cocoa-bindings.ps1 | 22 ++++-- .../Sentry.Bindings.Android.csproj | 2 +- .../Sentry.Bindings.Cocoa.csproj | 33 ++++++-- .../SentryStructuredLogger.cs | 6 +- src/Sentry.Serilog/SentrySink.Structured.cs | 24 +----- src/Sentry/Extensibility/DisabledHub.cs | 2 - src/Sentry/Extensibility/HubAdapter.cs | 4 +- src/Sentry/IHub.cs | 2 - .../Internal/DefaultSentryStructuredLogger.cs | 6 +- src/Sentry/Sentry.csproj | 14 ++-- src/Sentry/SentryLog.cs | 58 ++++++++------ 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 - .../SentryStructuredLoggerTests.cs | 19 +++-- .../SentrySinkTests.Structured.cs | 2 +- ...iApprovalTests.Run.DotNet10_0.verified.txt | 35 +-------- ...piApprovalTests.Run.DotNet8_0.verified.txt | 35 +-------- ...piApprovalTests.Run.DotNet9_0.verified.txt | 35 +-------- .../ApiApprovalTests.Run.Net4_8.verified.txt | 5 +- test/Sentry.Tests/SentryLogTests.cs | 76 +++++++++++++++++++ .../SentryStructuredLoggerTests.cs | 21 +++-- 31 files changed, 255 insertions(+), 265 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' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e39c2c1a3..020872b23c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## 6.0.0-preview.1 +### BREAKING CHANGES + +- This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) +- Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4583](https://github.com/getsentry/sentry-dotnet/pull/4583)) + +## 5.16.0 + ### 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)) @@ -9,10 +16,16 @@ ### 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)) +### 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)) @@ -27,17 +40,13 @@ ### Dependencies +- 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) - 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) -## Version 6.0.0 - -### BREAKING CHANGES - -- This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) -- Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4583](https://github.com/getsentry/sentry-dotnet/pull/4583)) - ## 5.15.1 ### Fixes @@ -80,8 +89,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) 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/Directory.Build.props b/Directory.Build.props index eac7530416..59e7cfa43b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -103,7 +103,7 @@ - 2.55.0 + 2.56.0 $(MSBuildThisFileDirectory)tools\sentry-cli\$(SentryCLIVersion)\ 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 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/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.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj index e99fcd08fb..2a7ced18b1 100644 --- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj +++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj @@ -1,7 +1,7 @@ $(LatestAndroidTfm);$(PreviousAndroidTfm) - 8.22.0 + 8.23.0 $(BaseIntermediateOutputPath)sdks\$(TargetFramework)\Sentry\Android\$(SentryAndroidSdkVersion)\ diff --git a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj index 10b3899977..26b4539776 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 @@ 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/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/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/Sentry.csproj b/src/Sentry/Sentry.csproj index 850b3840d8..2808038543 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -113,13 +113,13 @@ <_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) - - - - - - - + + + + + + + diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 7e58fec173..844d71a778 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -1,14 +1,17 @@ using Sentry.Extensibility; -using Sentry.Infrastructure; +using Sentry.Internal; 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 +30,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 +116,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 +142,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); @@ -225,7 +210,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); } @@ -257,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/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.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/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index b83e9340d7..406a853181 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_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.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 { diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index 3393137b85..c53c6711de 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() { @@ -382,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 f3e8729450a839f25cd30dde034d12c779c93b19 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 8 Oct 2025 13:07:16 +1300 Subject: [PATCH 06/23] Remove unnecessary files from SentryCocoaFramework before packing (#4602) --- CHANGELOG.md | 4 ++++ integration-test/cli.Tests.ps1 | 4 +++- .../Sentry.Bindings.Cocoa.csproj | 11 +++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 020872b23c..bf9d652035 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 ### BREAKING CHANGES 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\)" } } 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 33615f40e67fe024e60646bb2033e32fd7126cc3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 9 Oct 2025 10:10:09 +1300 Subject: [PATCH 07/23] Default IsEnvironmentUser to false on MAUI (#4606) * Default IsEnvironmentUser to false on MAUI Resolves #4114 - #4114 --- CHANGELOG.md | 3 +++ src/Sentry.Maui/SentryMauiOptions.cs | 1 + test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf9d652035..6a6cbf410b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### BREAKING CHANGES + +- SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) - Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) ## 6.0.0-preview.1 diff --git a/src/Sentry.Maui/SentryMauiOptions.cs b/src/Sentry.Maui/SentryMauiOptions.cs index c27109bebc..17038bf747 100644 --- a/src/Sentry.Maui/SentryMauiOptions.cs +++ b/src/Sentry.Maui/SentryMauiOptions.cs @@ -20,6 +20,7 @@ public SentryMauiOptions() // there for all MAUI targets, we'll set them again here. AutoSessionTracking = true; DetectStartupTime = StartupTimeDetectionMode.Fast; + IsEnvironmentUser = false; #if !PLATFORM_NEUTRAL CacheDirectoryPath = Microsoft.Maui.Storage.FileSystem.CacheDirectory; #endif diff --git a/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs b/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs index e1eb1dd564..1d2a9ae9ed 100644 --- a/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs +++ b/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs @@ -9,6 +9,13 @@ public class SentryMauiOptionsTests NetworkStatusListener = FakeReliableNetworkStatusListener.Instance }; + [Fact] + public void IsEnvironmentUser_Default() + { + var options = GetSut(); + Assert.False(options.IsEnvironmentUser); + } + [Fact] public void IncludeTextInBreadcrumbs_Default() { From a213de0eb5097c9a5480423272eae18e54819a00 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 10 Oct 2025 22:19:14 +1300 Subject: [PATCH 08/23] Fixed Sentry.Tests.pdb in verify test (#4614) fixed: Sentry.Tests.pdb module included randomly in debug files --- ...onTransactionEndedAsCrashed.DotNet10_0.verified.txt | 10 ---------- ...ionTransactionEndedAsCrashed.DotNet8_0.verified.txt | 10 ---------- ...ionTransactionEndedAsCrashed.DotNet9_0.verified.txt | 10 ---------- ...eptionTransactionEndedAsCrashed.Net4_8.verified.txt | 10 ---------- test/Sentry.Tests/HubTests.verify.cs | 3 ++- 5 files changed, 2 insertions(+), 41 deletions(-) diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt index c510d50d43..9ba1485316 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet10_0.verified.txt @@ -73,16 +73,6 @@ CodeId: ______________, CodeFile: .../System.Private.CoreLib.dll }, - { - Type: pe_dotnet, - ImageAddress: null, - ImageSize: null, - DebugId: ________-____-____-____-____________-________, - DebugChecksum: ______:________________________________________________________________, - DebugFile: .../Sentry.Tests.pdb, - CodeId: ______________, - CodeFile: .../Sentry.Tests.dll - }, { Type: pe_dotnet, ImageAddress: null, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt index c510d50d43..9ba1485316 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt @@ -73,16 +73,6 @@ CodeId: ______________, CodeFile: .../System.Private.CoreLib.dll }, - { - Type: pe_dotnet, - ImageAddress: null, - ImageSize: null, - DebugId: ________-____-____-____-____________-________, - DebugChecksum: ______:________________________________________________________________, - DebugFile: .../Sentry.Tests.pdb, - CodeId: ______________, - CodeFile: .../Sentry.Tests.dll - }, { Type: pe_dotnet, ImageAddress: null, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt index c510d50d43..9ba1485316 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt @@ -73,16 +73,6 @@ CodeId: ______________, CodeFile: .../System.Private.CoreLib.dll }, - { - Type: pe_dotnet, - ImageAddress: null, - ImageSize: null, - DebugId: ________-____-____-____-____________-________, - DebugChecksum: ______:________________________________________________________________, - DebugFile: .../Sentry.Tests.pdb, - CodeId: ______________, - CodeFile: .../Sentry.Tests.dll - }, { Type: pe_dotnet, ImageAddress: null, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt index f41bcaa626..df8ff1e044 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt @@ -73,16 +73,6 @@ CodeId: ______________, CodeFile: .../mscorlib.dll }, - { - Type: pe_dotnet, - ImageAddress: null, - ImageSize: null, - DebugId: ________-____-____-____-____________-________, - DebugChecksum: ______:________________________________________________________________, - DebugFile: .../Sentry.Tests.pdb, - CodeId: ______________, - CodeFile: .../Sentry.Tests.dll - }, { Type: pe_dotnet, ImageAddress: null, diff --git a/test/Sentry.Tests/HubTests.verify.cs b/test/Sentry.Tests/HubTests.verify.cs index 5d162b3612..067b4d3c7e 100644 --- a/test/Sentry.Tests/HubTests.verify.cs +++ b/test/Sentry.Tests/HubTests.verify.cs @@ -51,7 +51,8 @@ await Verify(worker.Envelopes) _.DebugFile.Contains("xunit.runner") || _.DebugFile.Contains("JetBrains.ReSharper.TestRunner") || _.DebugFile.Contains("Microsoft.TestPlatform") || - _.DebugFile.Contains("Microsoft.VisualStudio.TestPlatform.Common.pdb") + _.DebugFile.Contains("Microsoft.VisualStudio.TestPlatform.Common.pdb") || + _.DebugFile.Contains("Sentry.Tests.pdb") // This module is only sometimes included in local environments ) ); } From fc117ae893a73559acc70210814a6fb36022149f Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Sun, 12 Oct 2025 11:08:56 +0200 Subject: [PATCH 09/23] chore: Logging tweaks (#4622) --- src/Sentry/GlobalSessionManager.cs | 2 +- src/Sentry/Internal/BackgroundWorker.cs | 2 +- src/Sentry/Internal/DebugStackTrace.cs | 2 +- .../Internal/DuplicateEventDetectionEventProcessor.cs | 8 ++++---- src/Sentry/Internal/Http/CachingTransport.cs | 4 ++-- src/Sentry/Internal/Hub.cs | 2 +- src/Sentry/Internal/MainSentryEventProcessor.cs | 2 +- src/Sentry/Internal/SentryEventHelper.cs | 2 +- src/Sentry/SentryClient.cs | 4 ++-- src/Sentry/SentryPropagationContext.cs | 2 +- src/Sentry/TransactionTracer.cs | 8 ++++---- .../Sentry.Tests/Internals/DebugStackTraceTests.verify.cs | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Sentry/GlobalSessionManager.cs b/src/Sentry/GlobalSessionManager.cs index 35125817aa..29aa66b3e1 100644 --- a/src/Sentry/GlobalSessionManager.cs +++ b/src/Sentry/GlobalSessionManager.cs @@ -118,7 +118,7 @@ private void DeletePersistedSession() try { var contents = _options.FileSystem.ReadAllTextFromFile(filePath); - _options.LogDebug("Deleting persisted session file with contents: {0}", contents); + _options.LogDebug("Deleting persisted session file with contents: '{0}'", contents); } catch (Exception ex) { diff --git a/src/Sentry/Internal/BackgroundWorker.cs b/src/Sentry/Internal/BackgroundWorker.cs index 99c4ce2d78..d3040cacb0 100644 --- a/src/Sentry/Internal/BackgroundWorker.cs +++ b/src/Sentry/Internal/BackgroundWorker.cs @@ -177,7 +177,7 @@ private async Task DoWorkAsync() } finally { - _options.LogDebug("De-queueing event {0}", eventId); + _options.LogDebug("De-queueing event '{0}'", eventId); _queue.TryDequeue(out _); Interlocked.Decrement(ref _currentItems); OnFlushObjectReceived?.Invoke(envelope, EventArgs.Empty); diff --git a/src/Sentry/Internal/DebugStackTrace.cs b/src/Sentry/Internal/DebugStackTrace.cs index 853d925699..6ad3c94320 100644 --- a/src/Sentry/Internal/DebugStackTrace.cs +++ b/src/Sentry/Internal/DebugStackTrace.cs @@ -82,7 +82,7 @@ internal void MergeDebugImagesInto(SentryEvent @event) // Frame indexes may be changed as well as _debugImageIndexByModule becoming invalid. if (_debugImagesMerged) { - _options.LogWarning("Cannot call MergeDebugImagesInto multiple times. Event: {0}", @event.EventId); + _options.LogWarning("Cannot call MergeDebugImagesInto multiple times. Event: '{0}'", @event.EventId); return; } _debugImagesMerged = true; diff --git a/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs b/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs index b32d96c555..8e781a46a9 100644 --- a/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs +++ b/src/Sentry/Internal/DuplicateEventDetectionEventProcessor.cs @@ -15,7 +15,7 @@ internal class DuplicateEventDetectionEventProcessor : ISentryEventProcessor { if (_capturedObjects.TryGetValue(@event, out _)) { - _options.LogDebug("Same event instance detected and discarded. EventId: {0}", @event.EventId); + _options.LogDebug("Same event instance detected and discarded. EventId: '{0}'", @event.EventId); return null; } _capturedObjects.Add(@event, null); @@ -38,7 +38,7 @@ private bool IsDuplicate(Exception ex, SentryId eventId, bool debugLog) { if (debugLog) { - _options.LogDebug("Duplicate Exception: 'SameExceptionInstance'. Event {0} will be discarded.", eventId); + _options.LogDebug("Duplicate Exception: 'SameExceptionInstance'. Event '{0}' will be discarded.", eventId); } return true; } @@ -52,7 +52,7 @@ private bool IsDuplicate(Exception ex, SentryId eventId, bool debugLog) var result = aex.InnerExceptions.Any(e => IsDuplicate(e, eventId, false)); if (result) { - _options.LogDebug("Duplicate Exception: 'AggregateException'. Event {0} will be discarded.", eventId); + _options.LogDebug("Duplicate Exception: 'AggregateException'. Event '{0}' will be discarded.", eventId); } return result; @@ -63,7 +63,7 @@ private bool IsDuplicate(Exception ex, SentryId eventId, bool debugLog) { if (IsDuplicate(ex.InnerException, eventId, false)) { - _options.LogDebug("Duplicate Exception: 'SameExceptionInstance'. Event {0} will be discarded.", eventId); + _options.LogDebug("Duplicate Exception: 'SameExceptionInstance'. Event '{0}' will be discarded.", eventId); return true; } } diff --git a/src/Sentry/Internal/Http/CachingTransport.cs b/src/Sentry/Internal/Http/CachingTransport.cs index b5ed2cb34a..f2117f60b2 100644 --- a/src/Sentry/Internal/Http/CachingTransport.cs +++ b/src/Sentry/Internal/Http/CachingTransport.cs @@ -343,7 +343,7 @@ private async Task InnerProcessCacheAsync(string file, CancellationToken cancell try { - _options.LogDebug("Sending cached envelope: {0}", + _options.LogDebug("Sending cached envelope: '{0}'", envelope.TryGetEventId(_options.DiagnosticLogger)); await _innerTransport.SendEnvelopeAsync(envelope, cancellation).ConfigureAwait(false); @@ -351,7 +351,7 @@ private async Task InnerProcessCacheAsync(string file, CancellationToken cancell // OperationCancel should not log an error catch (OperationCanceledException ex) { - _options.LogDebug("Canceled sending cached envelope: {0}, retrying after a delay.", ex, file); + _options.LogDebug("Canceled sending cached envelope: '{0}', retrying after a delay.", ex, file); // Let the worker catch, log, wait a bit and retry. throw; } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 5170a64233..9034f0d666 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -505,7 +505,7 @@ private void AddBreadcrumbForException(SentryEvent evt, Scope scope) } catch (Exception e) { - _options.LogError(e, "Failure to store breadcrumb for exception event: {0}", evt.EventId); + _options.LogError(e, "Failure to store breadcrumb for exception event: '{0}'", evt.EventId); } } diff --git a/src/Sentry/Internal/MainSentryEventProcessor.cs b/src/Sentry/Internal/MainSentryEventProcessor.cs index d806a2cc2c..c8325a15a9 100644 --- a/src/Sentry/Internal/MainSentryEventProcessor.cs +++ b/src/Sentry/Internal/MainSentryEventProcessor.cs @@ -34,7 +34,7 @@ public MainSentryEventProcessor( public SentryEvent Process(SentryEvent @event) { - _options.LogDebug("Running main event processor on: Event {0}", @event.EventId); + _options.LogDebug("Running main event processor on: Event '{0}'", @event.EventId); if (TimeZoneInfo.Local is { } timeZoneInfo) { diff --git a/src/Sentry/Internal/SentryEventHelper.cs b/src/Sentry/Internal/SentryEventHelper.cs index 48a6c1a07f..eb3fbb7f9f 100644 --- a/src/Sentry/Internal/SentryEventHelper.cs +++ b/src/Sentry/Internal/SentryEventHelper.cs @@ -37,7 +37,7 @@ internal static class SentryEventHelper return @event; } - options.LogDebug("Calling the BeforeSend callback"); + options.LogDebug("Calling the BeforeSend callback."); try { @event = options.BeforeSendInternal?.Invoke(@event, hint); diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 80ede995dd..1467ee4364 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -79,7 +79,7 @@ public SentryId CaptureEvent(SentryEvent? @event, Scope? scope = null, SentryHin } catch (Exception e) { - _options.LogError(e, "An error occurred when capturing the event {0}.", @event.EventId); + _options.LogError(e, "An error occurred when capturing the event '{0}'.", @event.EventId); return SentryId.Empty; } } @@ -445,7 +445,7 @@ public bool CaptureEnvelope(Envelope envelope) } _options.LogWarning( - "The attempt to queue the event failed. Items in queue: {0}", + "The attempt to queue the event failed. Items in queue: '{0}'", Worker.QueuedItems); return false; diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index 24f9550638..6183262241 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -15,7 +15,7 @@ public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions op { if (_dynamicSamplingContext is null) { - options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context"); + options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context."); _dynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); } diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index 767ccf977c..72cf7fdeb7 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -361,10 +361,10 @@ public void Clear() /// public void Finish() { - _options?.LogDebug("Attempting to finish Transaction {0}.", SpanId); + _options?.LogDebug("Attempting to finish Transaction '{0}'.", SpanId); if (Interlocked.Exchange(ref _cancelIdleTimeout, 0) == 1) { - _options?.LogDebug("Disposing of idle timer for Transaction {0}.", SpanId); + _options?.LogDebug("Disposing of idle timer for Transaction '{0}'.", SpanId); _idleTimer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); _idleTimer?.Dispose(); } @@ -374,14 +374,14 @@ public void Finish() // Normally we wouldn't start transactions for Sentry requests but when instrumenting with OpenTelemetry // we are only able to determine whether it's a sentry request or not when closing a span... we leave these // to be garbage collected and we don't want idle timers triggering on them - _options?.LogDebug("Transaction {0} is a Sentry Request. Don't complete.", SpanId); + _options?.LogDebug("Transaction '{0}' is a Sentry Request. Don't complete.", SpanId); return; } TransactionProfiler?.Finish(); Status ??= SpanStatus.Ok; EndTimestamp ??= _stopwatch.CurrentDateTimeOffset; - _options?.LogDebug("Finished Transaction {0}.", SpanId); + _options?.LogDebug("Finished Transaction '{0}'.", SpanId); // Clear the transaction from the scope and regenerate the Propagation Context // We do this so new events don't have a trace context that is "older" than the transaction that just finished diff --git a/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs index 42f7e90e02..7aa46551aa 100644 --- a/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs +++ b/test/Sentry.Tests/Internals/DebugStackTraceTests.verify.cs @@ -134,7 +134,7 @@ public void MergeDebugImages_MultipleCalls_LogsWarning() logger.Received(1).Log( SentryLevel.Warning, - "Cannot call MergeDebugImagesInto multiple times. Event: {0}", + "Cannot call MergeDebugImagesInto multiple times. Event: '{0}'", null, Arg.Any() ); From 7b1b32eb8cfe087d40e53003d6ed519fac514fb6 Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Sun, 12 Oct 2025 11:14:02 +0200 Subject: [PATCH 10/23] fix: Removed redundant scope sync after transaction finish (#4623) --- CHANGELOG.md | 6 +++++- src/Sentry/Scope.cs | 6 +----- src/Sentry/TransactionTracer.cs | 6 +----- test/Sentry.Tests/ScopeTests.cs | 6 ++---- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6cbf410b..9daaca2244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ ### BREAKING CHANGES - SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) -- Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) +- Remove unnecessary files from SentryCocoaFramework before packing ([#4623](https://github.com/getsentry/sentry-dotnet/pull/4623)) + +### Fixes + +- The SDK avoids redundant scope sync after transaction finish ([#4479](https://github.com/getsentry/sentry-dotnet/pull/4479)) ## 6.0.0-preview.1 diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index d1fe138ded..04fab75ff1 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -818,11 +818,7 @@ internal void ResetTransaction(ITransactionTracer? expectedCurrentTransaction) if (ReferenceEquals(_transaction.Value, expectedCurrentTransaction)) { _transaction.Value = null; - if (Options.EnableScopeSync) - { - // We have to restore the trace on the native layers to be in sync with the current scope - Options.ScopeObserver?.SetTrace(PropagationContext.TraceId, PropagationContext.SpanId); - } + SetPropagationContext(new SentryPropagationContext()); } } finally diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index 72cf7fdeb7..bdd829ff81 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -385,11 +385,7 @@ public void Finish() // Clear the transaction from the scope and regenerate the Propagation Context // We do this so new events don't have a trace context that is "older" than the transaction that just finished - _hub.ConfigureScope(static (scope, transactionTracer) => - { - scope.ResetTransaction(transactionTracer); - scope.SetPropagationContext(new SentryPropagationContext()); - }, this); + _hub.ConfigureScope(static (scope, transactionTracer) => scope.ResetTransaction(transactionTracer), this); // Client decides whether to discard this transaction based on sampling _hub.CaptureTransaction(new SentryTransaction(this)); diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index 4b46d62c3f..0bb65dc8ec 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -356,9 +356,6 @@ public void ResetTransaction_MatchingTransaction_ObserverSetsTraceFromPropagatio }); var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); scope.Transaction = transaction; - - var expectedTraceId = scope.PropagationContext.TraceId; - var expectedSpanId = scope.PropagationContext.SpanId; var expectedCount = enableScopeSync ? 1 : 0; observer.ClearReceivedCalls(); @@ -367,7 +364,8 @@ public void ResetTransaction_MatchingTransaction_ObserverSetsTraceFromPropagatio scope.ResetTransaction(transaction); // Assert - observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId)); + observer.Received(expectedCount) + .SetTrace(Arg.Is(scope.PropagationContext.TraceId), Arg.Is(scope.PropagationContext.SpanId)); } [Theory] From 6377b49480b3d868bfc032974ba3303217e59e79 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 13 Oct 2025 20:43:44 +1300 Subject: [PATCH 11/23] Fix pull request references in CHANGELOG.md Updated references to pull requests in the CHANGELOG. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9daaca2244..719ce0d281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### BREAKING CHANGES - SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) -- Remove unnecessary files from SentryCocoaFramework before packing ([#4623](https://github.com/getsentry/sentry-dotnet/pull/4623)) +- Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) ### Fixes -- The SDK avoids redundant scope sync after transaction finish ([#4479](https://github.com/getsentry/sentry-dotnet/pull/4479)) +- The SDK avoids redundant scope sync after transaction finish ([#4623](https://github.com/getsentry/sentry-dotnet/pull/4623)) ## 6.0.0-preview.1 From ac5b47b43930045f49d1671901474a96f9392100 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 15 Oct 2025 10:26:13 +1300 Subject: [PATCH 12/23] ScopeExtensions.Populate is now internal (#4611) --- CHANGELOG.md | 1 + src/Sentry.AspNetCore/ScopeExtensions.cs | 6 +++--- .../ApiApprovalTests.Run.DotNet10_0.verified.txt | 5 ----- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 5 ----- .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 5 ----- 5 files changed, 4 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719ce0d281..ca19d120d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) - Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) +- ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611)) ### Fixes diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index 10ed791335..81ce09d421 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -13,7 +13,7 @@ namespace Sentry.AspNetCore; /// Scope Extensions /// [EditorBrowsable(EditorBrowsableState.Never)] -public static class ScopeExtensions +internal static class ScopeExtensions { /// /// Populates the scope with the HTTP data @@ -22,7 +22,7 @@ public static class ScopeExtensions /// NOTE: The scope is applied to the event BEFORE running the event processors/exception processors. /// The main Sentry SDK has processors which run right before any additional processors to the Event /// - public static void Populate(this Scope scope, HttpContext context, SentryAspNetCoreOptions options) + internal static void Populate(this Scope scope, HttpContext context, SentryAspNetCoreOptions options) { // Not to throw on code that ignores nullability warnings. if (scope.IsNull() || context.IsNull() || options.IsNull()) @@ -190,7 +190,7 @@ private static void SetBody(Scope scope, HttpContext context, SentryAspNetCoreOp /// /// The scope. /// The activity. - public static void Populate(this Scope scope, Activity activity) + internal static void Populate(this Scope scope, Activity activity) { // Not to throw on code that ignores nullability warnings. if (scope.IsNull() || activity.IsNull()) diff --git a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 5f9eab70e9..4ca83a4f8f 100644 --- a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -39,11 +39,6 @@ namespace Sentry.AspNetCore public static string? TryGetHttpPath(this Sentry.TransactionSamplingContext samplingContext) { } public static string? TryGetHttpRoute(this Sentry.TransactionSamplingContext samplingContext) { } } - public static class ScopeExtensions - { - public static void Populate(this Sentry.Scope scope, System.Diagnostics.Activity activity) { } - public static void Populate(this Sentry.Scope scope, Microsoft.AspNetCore.Http.HttpContext context, Sentry.AspNetCore.SentryAspNetCoreOptions options) { } - } [Microsoft.Extensions.Logging.ProviderAlias("Sentry")] public class SentryAspNetCoreLoggerProvider : Sentry.Extensions.Logging.SentryLoggerProvider { diff --git a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 5f9eab70e9..4ca83a4f8f 100644 --- a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -39,11 +39,6 @@ namespace Sentry.AspNetCore public static string? TryGetHttpPath(this Sentry.TransactionSamplingContext samplingContext) { } public static string? TryGetHttpRoute(this Sentry.TransactionSamplingContext samplingContext) { } } - public static class ScopeExtensions - { - public static void Populate(this Sentry.Scope scope, System.Diagnostics.Activity activity) { } - public static void Populate(this Sentry.Scope scope, Microsoft.AspNetCore.Http.HttpContext context, Sentry.AspNetCore.SentryAspNetCoreOptions options) { } - } [Microsoft.Extensions.Logging.ProviderAlias("Sentry")] public class SentryAspNetCoreLoggerProvider : Sentry.Extensions.Logging.SentryLoggerProvider { diff --git a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 5f9eab70e9..4ca83a4f8f 100644 --- a/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -39,11 +39,6 @@ namespace Sentry.AspNetCore public static string? TryGetHttpPath(this Sentry.TransactionSamplingContext samplingContext) { } public static string? TryGetHttpRoute(this Sentry.TransactionSamplingContext samplingContext) { } } - public static class ScopeExtensions - { - public static void Populate(this Sentry.Scope scope, System.Diagnostics.Activity activity) { } - public static void Populate(this Sentry.Scope scope, Microsoft.AspNetCore.Http.HttpContext context, Sentry.AspNetCore.SentryAspNetCoreOptions options) { } - } [Microsoft.Extensions.Logging.ProviderAlias("Sentry")] public class SentryAspNetCoreLoggerProvider : Sentry.Extensions.Logging.SentryLoggerProvider { From c32dfbbc0246141d944fd95f5c9409326936db22 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 15 Oct 2025 10:27:17 +1300 Subject: [PATCH 13/23] BreadcrumbLevel Critical renamed to Fatal (#4605) Resolves #4028: - #4028 --- CHANGELOG.md | 1 + .../Sentry.Benchmarks/BreadcrumbOverheadBenchmarks.cs | 2 +- samples/Sentry.Samples.Console.Customized/Program.cs | 2 +- src/Sentry.Extensions.Logging/LogLevelExtensions.cs | 2 +- src/Sentry.Log4Net/LevelMapping.cs | 2 +- src/Sentry.NLog/LogLevelExtensions.cs | 2 +- src/Sentry.Serilog/LogLevelExtensions.cs | 2 +- src/Sentry/Breadcrumb.cs | 5 +++-- src/Sentry/BreadcrumbLevel.cs | 6 +++--- src/Sentry/Internal/Hub.cs | 2 +- src/Sentry/Platforms/Android/Extensions/EnumExtensions.cs | 4 ++-- src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs | 4 ++-- .../SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt | 2 +- .../SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt | 2 +- .../SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt | 2 +- .../SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt | 2 +- .../SqlListenerTests.RecordsSqlAsync.verified.txt | 2 +- .../IntegrationTests.Simple.verified.txt | 2 +- .../LogLevelExtensionsTests.cs | 2 +- .../ApiApprovalTests.Run.DotNet10_0.verified.txt | 4 ++-- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 4 ++-- .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 4 ++-- test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt | 4 ++-- test/Sentry.Tests/Extensibility/HubAdapterTests.cs | 2 +- test/Sentry.Tests/HubExtensionsTests.cs | 2 +- test/Sentry.Tests/HubTests.cs | 4 ++-- test/Sentry.Tests/Protocol/BreadcrumbTests.cs | 2 +- test/Sentry.Tests/Protocol/ScopeTests.cs | 6 +++--- 28 files changed, 41 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca19d120d2..deab977457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### BREAKING CHANGES +- `BreadcrumbLevel.Critical` has been renamed to `BreadcrumbLevel.Fatal` for consistency with the other Sentry SDKs ([#4605](https://github.com/getsentry/sentry-dotnet/pull/4605)) - SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) - Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) - ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611)) diff --git a/benchmarks/Sentry.Benchmarks/BreadcrumbOverheadBenchmarks.cs b/benchmarks/Sentry.Benchmarks/BreadcrumbOverheadBenchmarks.cs index 9890d5223b..70a38aabe8 100644 --- a/benchmarks/Sentry.Benchmarks/BreadcrumbOverheadBenchmarks.cs +++ b/benchmarks/Sentry.Benchmarks/BreadcrumbOverheadBenchmarks.cs @@ -11,7 +11,7 @@ public class BreadcrumbOverheadBenchmarks { { Message, Type } }; - private const BreadcrumbLevel Level = BreadcrumbLevel.Critical; + private const BreadcrumbLevel Level = BreadcrumbLevel.Fatal; private IDisposable _sdk; diff --git a/samples/Sentry.Samples.Console.Customized/Program.cs b/samples/Sentry.Samples.Console.Customized/Program.cs index 0ccfad7b04..3fbc23f7a2 100644 --- a/samples/Sentry.Samples.Console.Customized/Program.cs +++ b/samples/Sentry.Samples.Console.Customized/Program.cs @@ -66,7 +66,7 @@ await SentrySdk.ConfigureScopeAsync(async scope => const string replaceBreadcrumb = "don't trust this breadcrumb"; if (hint.Items.TryGetValue(replaceBreadcrumb, out var replacementMessage)) { - return new Breadcrumb((string)replacementMessage, null, null, null, BreadcrumbLevel.Critical); + return new Breadcrumb((string)replacementMessage, null, null, null, BreadcrumbLevel.Fatal); } return crumb; diff --git a/src/Sentry.Extensions.Logging/LogLevelExtensions.cs b/src/Sentry.Extensions.Logging/LogLevelExtensions.cs index 9f1d2f85e2..9a6bbf5729 100644 --- a/src/Sentry.Extensions.Logging/LogLevelExtensions.cs +++ b/src/Sentry.Extensions.Logging/LogLevelExtensions.cs @@ -13,7 +13,7 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogLevel level) LogLevel.Information => BreadcrumbLevel.Info, LogLevel.Warning => BreadcrumbLevel.Warning, LogLevel.Error => BreadcrumbLevel.Error, - LogLevel.Critical => BreadcrumbLevel.Critical, + LogLevel.Critical => BreadcrumbLevel.Fatal, _ => (BreadcrumbLevel)level }; } diff --git a/src/Sentry.Log4Net/LevelMapping.cs b/src/Sentry.Log4Net/LevelMapping.cs index 28ced64ca5..e22dc24d7e 100644 --- a/src/Sentry.Log4Net/LevelMapping.cs +++ b/src/Sentry.Log4Net/LevelMapping.cs @@ -20,7 +20,7 @@ internal static class LevelMapping { return loggingLevel.Level switch { - var l when l == Level.Fatal || l == Level.Emergency => BreadcrumbLevel.Critical, + var l when l == Level.Fatal || l == Level.Emergency => BreadcrumbLevel.Fatal, var l when l == Level.Alert || l == Level.Critical || l == Level.Severe || l == Level.Error => BreadcrumbLevel.Error, var l when l == Level.Warn => BreadcrumbLevel.Warning, var l when l == Level.Notice || l == Level.Info => BreadcrumbLevel.Info, diff --git a/src/Sentry.NLog/LogLevelExtensions.cs b/src/Sentry.NLog/LogLevelExtensions.cs index eba3aedcbb..4050ff0c9e 100644 --- a/src/Sentry.NLog/LogLevelExtensions.cs +++ b/src/Sentry.NLog/LogLevelExtensions.cs @@ -22,7 +22,7 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogLevel level) { nameof(LogLevel.Debug) => BreadcrumbLevel.Debug, nameof(LogLevel.Error) => BreadcrumbLevel.Error, - nameof(LogLevel.Fatal) => BreadcrumbLevel.Critical, + nameof(LogLevel.Fatal) => BreadcrumbLevel.Fatal, nameof(LogLevel.Info) => BreadcrumbLevel.Info, nameof(LogLevel.Trace) => BreadcrumbLevel.Debug, nameof(LogLevel.Warn) => BreadcrumbLevel.Warning, diff --git a/src/Sentry.Serilog/LogLevelExtensions.cs b/src/Sentry.Serilog/LogLevelExtensions.cs index 07960179b2..502e6a22e2 100644 --- a/src/Sentry.Serilog/LogLevelExtensions.cs +++ b/src/Sentry.Serilog/LogLevelExtensions.cs @@ -38,7 +38,7 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogEventLevel level) LogEventLevel.Information => BreadcrumbLevel.Info, LogEventLevel.Warning => BreadcrumbLevel.Warning, LogEventLevel.Error => BreadcrumbLevel.Error, - LogEventLevel.Fatal => BreadcrumbLevel.Critical, + LogEventLevel.Fatal => BreadcrumbLevel.Fatal, _ => (BreadcrumbLevel)level }; } diff --git a/src/Sentry/Breadcrumb.cs b/src/Sentry/Breadcrumb.cs index b3b58606b0..f727d4ddb7 100644 --- a/src/Sentry/Breadcrumb.cs +++ b/src/Sentry/Breadcrumb.cs @@ -165,8 +165,9 @@ public static Breadcrumb FromJson(JsonElement json) "INFO" => BreadcrumbLevel.Info, "WARNING" => BreadcrumbLevel.Warning, "ERROR" => BreadcrumbLevel.Error, - "CRITICAL" => BreadcrumbLevel.Critical, - "FATAL" => BreadcrumbLevel.Critical, + // Renamed in v6 but kept here to avoid issues with cached envelopes. Can be safely removed in the future. + "CRITICAL" => BreadcrumbLevel.Fatal, + "FATAL" => BreadcrumbLevel.Fatal, _ => default }; return new Breadcrumb(timestamp, message, type, data!, category, level); diff --git a/src/Sentry/BreadcrumbLevel.cs b/src/Sentry/BreadcrumbLevel.cs index 14b1f11789..f4d90af4a0 100644 --- a/src/Sentry/BreadcrumbLevel.cs +++ b/src/Sentry/BreadcrumbLevel.cs @@ -33,8 +33,8 @@ public enum BreadcrumbLevel Error = 2, /// - /// Critical breadcrumb level. + /// Fatal breadcrumb level. /// - [EnumMember(Value = "critical")] - Critical = 3, + [EnumMember(Value = "fatal")] + Fatal = 3, } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 9034f0d666..2a3b934774 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -501,7 +501,7 @@ private void AddBreadcrumbForException(SentryEvent evt, Scope scope) {"exception_message", exceptionMessage} }; } - scope.AddBreadcrumb(breadcrumbMessage, "Exception", data: data, level: BreadcrumbLevel.Critical); + scope.AddBreadcrumb(breadcrumbMessage, "Exception", data: data, level: BreadcrumbLevel.Fatal); } catch (Exception e) { diff --git a/src/Sentry/Platforms/Android/Extensions/EnumExtensions.cs b/src/Sentry/Platforms/Android/Extensions/EnumExtensions.cs index f9352f831d..916f8c977d 100644 --- a/src/Sentry/Platforms/Android/Extensions/EnumExtensions.cs +++ b/src/Sentry/Platforms/Android/Extensions/EnumExtensions.cs @@ -31,7 +31,7 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this JavaSdk.SentryLevel level) "INFO" => BreadcrumbLevel.Info, "WARNING" => BreadcrumbLevel.Warning, "ERROR" => BreadcrumbLevel.Error, - "FATAL" => BreadcrumbLevel.Critical, + "FATAL" => BreadcrumbLevel.Fatal, _ => throw new ArgumentOutOfRangeException(nameof(level), level.Name(), message: default) }; @@ -42,7 +42,7 @@ public static JavaSdk.SentryLevel ToJavaSentryLevel(this BreadcrumbLevel level) BreadcrumbLevel.Info => JavaSdk.SentryLevel.Info, BreadcrumbLevel.Warning => JavaSdk.SentryLevel.Warning, BreadcrumbLevel.Error => JavaSdk.SentryLevel.Error, - BreadcrumbLevel.Critical => JavaSdk.SentryLevel.Fatal, + BreadcrumbLevel.Fatal => JavaSdk.SentryLevel.Fatal, _ => throw new ArgumentOutOfRangeException(nameof(level), level, message: default) }; diff --git a/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs b/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs index feb2a37e91..9a6c67ed3f 100644 --- a/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs +++ b/src/Sentry/Platforms/Cocoa/Extensions/EnumExtensions.cs @@ -22,7 +22,7 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this CocoaSdk.SentryLevel level) CocoaSdk.SentryLevel.Info => BreadcrumbLevel.Info, CocoaSdk.SentryLevel.Warning => BreadcrumbLevel.Warning, CocoaSdk.SentryLevel.Error => BreadcrumbLevel.Error, - CocoaSdk.SentryLevel.Fatal => BreadcrumbLevel.Critical, + CocoaSdk.SentryLevel.Fatal => BreadcrumbLevel.Fatal, _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) }; @@ -33,7 +33,7 @@ public static CocoaSdk.SentryLevel ToCocoaSentryLevel(this BreadcrumbLevel level BreadcrumbLevel.Info => CocoaSdk.SentryLevel.Info, BreadcrumbLevel.Warning => CocoaSdk.SentryLevel.Warning, BreadcrumbLevel.Error => CocoaSdk.SentryLevel.Error, - BreadcrumbLevel.Critical => CocoaSdk.SentryLevel.Fatal, + BreadcrumbLevel.Fatal => CocoaSdk.SentryLevel.Fatal, _ => throw new ArgumentOutOfRangeException(nameof(level), level, message: default) }; diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt index 05803effbe..290207f39e 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt @@ -59,7 +59,7 @@ { Message: my exception, Category: Exception, - Level: critical + Level: fatal } ], Spans: [ diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt index 05803effbe..290207f39e 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt @@ -59,7 +59,7 @@ { Message: my exception, Category: Exception, - Level: critical + Level: fatal } ], Spans: [ diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt index 05803effbe..290207f39e 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt @@ -59,7 +59,7 @@ { Message: my exception, Category: Exception, - Level: critical + Level: fatal } ], Spans: [ diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt index 75d6b3e321..51fc328f6a 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt @@ -59,7 +59,7 @@ { Message: my exception, Category: Exception, - Level: critical + Level: fatal } ], Spans: [ diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt index e317514b27..fbf3184173 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt @@ -59,7 +59,7 @@ { Message: my exception, Category: Exception, - Level: critical + Level: fatal } ], Spans: [ diff --git a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt index 826fb58e95..42af21ba1b 100644 --- a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt +++ b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt @@ -61,7 +61,7 @@ { Message: my exception, Category: Exception, - Level: critical + Level: fatal } ], Spans: [ diff --git a/test/Sentry.Extensions.Logging.Tests/LogLevelExtensionsTests.cs b/test/Sentry.Extensions.Logging.Tests/LogLevelExtensionsTests.cs index 629effa10b..c1d39af452 100644 --- a/test/Sentry.Extensions.Logging.Tests/LogLevelExtensionsTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/LogLevelExtensionsTests.cs @@ -16,7 +16,7 @@ public static IEnumerable BreadcrumbTestCases() yield return new object[] { (BreadcrumbLevel.Info, LogLevel.Information) }; yield return new object[] { (BreadcrumbLevel.Warning, LogLevel.Warning) }; yield return new object[] { (BreadcrumbLevel.Error, LogLevel.Error) }; - yield return new object[] { (BreadcrumbLevel.Critical, LogLevel.Critical) }; + yield return new object[] { (BreadcrumbLevel.Fatal, LogLevel.Critical) }; yield return new object[] { ((BreadcrumbLevel)6, LogLevel.None) }; yield return new object[] { ((BreadcrumbLevel)int.MaxValue, (LogLevel)int.MaxValue) }; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 406a853181..9a2f11bf6a 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -37,8 +37,8 @@ namespace Sentry Warning = 1, [System.Runtime.Serialization.EnumMember(Value="error")] Error = 2, - [System.Runtime.Serialization.EnumMember(Value="critical")] - Critical = 3, + [System.Runtime.Serialization.EnumMember(Value="fatal")] + Fatal = 3, } public class ByteAttachmentContent : Sentry.IAttachmentContent { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 406a853181..9a2f11bf6a 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -37,8 +37,8 @@ namespace Sentry Warning = 1, [System.Runtime.Serialization.EnumMember(Value="error")] Error = 2, - [System.Runtime.Serialization.EnumMember(Value="critical")] - Critical = 3, + [System.Runtime.Serialization.EnumMember(Value="fatal")] + Fatal = 3, } public class ByteAttachmentContent : Sentry.IAttachmentContent { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 406a853181..9a2f11bf6a 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -37,8 +37,8 @@ namespace Sentry Warning = 1, [System.Runtime.Serialization.EnumMember(Value="error")] Error = 2, - [System.Runtime.Serialization.EnumMember(Value="critical")] - Critical = 3, + [System.Runtime.Serialization.EnumMember(Value="fatal")] + Fatal = 3, } public class ByteAttachmentContent : Sentry.IAttachmentContent { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index e2a02fc89a..d508934005 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -37,8 +37,8 @@ namespace Sentry Warning = 1, [System.Runtime.Serialization.EnumMember(Value="error")] Error = 2, - [System.Runtime.Serialization.EnumMember(Value="critical")] - Critical = 3, + [System.Runtime.Serialization.EnumMember(Value="fatal")] + Fatal = 3, } public class ByteAttachmentContent : Sentry.IAttachmentContent { diff --git a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs index 0ddb6a89b2..26702163cf 100644 --- a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs +++ b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs @@ -171,7 +171,7 @@ private void TestAddBreadcrumbExtension( {"Key", "value"}, {"Key2", "value2"}, }; - const BreadcrumbLevel level = BreadcrumbLevel.Critical; + const BreadcrumbLevel level = BreadcrumbLevel.Fatal; var scope = new Scope(); Hub.SubstituteConfigureScope(scope); diff --git a/test/Sentry.Tests/HubExtensionsTests.cs b/test/Sentry.Tests/HubExtensionsTests.cs index c01657890c..280518b13d 100644 --- a/test/Sentry.Tests/HubExtensionsTests.cs +++ b/test/Sentry.Tests/HubExtensionsTests.cs @@ -86,7 +86,7 @@ public void AddBreadcrumb_AllFields_CreatesBreadcrumb() {"Key", null}, {"Key2", "value2"}, }; - const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Critical; + const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Fatal; Sut.AddBreadcrumb( clock, diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 342d38f84d..a7a5fcaa8c 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -309,7 +309,7 @@ public void CaptureEvent_Exception_LeavesBreadcrumb(bool withScopeCallback) using var assertionScope = new AssertionScope(); var breadcrumb = scope.Breadcrumbs.Last(); breadcrumb.Message.Should().Be(evt.Exception!.Message); - breadcrumb.Level.Should().Be(BreadcrumbLevel.Critical); + breadcrumb.Level.Should().Be(BreadcrumbLevel.Fatal); breadcrumb.Category.Should().Be("Exception"); } @@ -342,7 +342,7 @@ public void CaptureEvent_WithMessageAndException_StoresExceptionMessageAsData() { ["exception_message"] = evt.Exception!.Message }); - breadcrumb.Level.Should().Be(BreadcrumbLevel.Critical); + breadcrumb.Level.Should().Be(BreadcrumbLevel.Fatal); breadcrumb.Category.Should().Be("Exception"); } diff --git a/test/Sentry.Tests/Protocol/BreadcrumbTests.cs b/test/Sentry.Tests/Protocol/BreadcrumbTests.cs index a99bdfa971..191ebc21c6 100644 --- a/test/Sentry.Tests/Protocol/BreadcrumbTests.cs +++ b/test/Sentry.Tests/Protocol/BreadcrumbTests.cs @@ -108,6 +108,6 @@ public static IEnumerable TestCases() yield return new object[] { (new Breadcrumb(DateTimeOffset.MaxValue, type: "type"), """{"timestamp":"9999-12-31T23:59:59.999Z","type":"type"}""") }; yield return new object[] { (new Breadcrumb(DateTimeOffset.MaxValue, data: new Dictionary { { "key", "val" } }), """{"timestamp":"9999-12-31T23:59:59.999Z","data":{"key":"val"}}""") }; yield return new object[] { (new Breadcrumb(DateTimeOffset.MaxValue, category: "category"), """{"timestamp":"9999-12-31T23:59:59.999Z","category":"category"}""") }; - yield return new object[] { (new Breadcrumb(DateTimeOffset.MaxValue, level: BreadcrumbLevel.Critical), """{"timestamp":"9999-12-31T23:59:59.999Z","level":"critical"}""") }; + yield return new object[] { (new Breadcrumb(DateTimeOffset.MaxValue, level: BreadcrumbLevel.Fatal), """{"timestamp":"9999-12-31T23:59:59.999Z","level":"fatal"}""") }; } } diff --git a/test/Sentry.Tests/Protocol/ScopeTests.cs b/test/Sentry.Tests/Protocol/ScopeTests.cs index e3ac6a6cb0..4ac9bfae4d 100644 --- a/test/Sentry.Tests/Protocol/ScopeTests.cs +++ b/test/Sentry.Tests/Protocol/ScopeTests.cs @@ -392,7 +392,7 @@ public void AddBreadcrumb_ValueTuple_AllArgumentsMatch() const string expectedCategory = "original Category"; const string expectedType = "original Type"; var expectedData = (key: "key", value: "value"); - const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Critical; + const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Fatal; var sut = _fixture.GetSut(); sut.AddBreadcrumb( @@ -419,7 +419,7 @@ public void AddBreadcrumb_Dictionary_AllArgumentsMatch() const string expectedCategory = "original Category"; const string expectedType = "original Type"; var expectedData = new Dictionary { { "key", "val" } }; - const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Critical; + const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Fatal; var sut = _fixture.GetSut(); sut.AddBreadcrumb( @@ -446,7 +446,7 @@ public void AddBreadcrumb_ImmutableDictionary_AllArgumentsMatch() const string expectedCategory = "original Category"; const string expectedType = "original Type"; var expectedData = new Dictionary { { "key", "val" } }; - const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Critical; + const BreadcrumbLevel expectedLevel = BreadcrumbLevel.Fatal; var sut = _fixture.GetSut(); sut.AddBreadcrumb( From 4a3198aaaa1a2e3a7637271b7e1e737bf9c37c34 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 16 Oct 2025 11:41:27 +1300 Subject: [PATCH 14/23] Enable backpressure handling by default (#4615) --- CHANGELOG.md | 1 + src/Sentry/SentryOptions.cs | 5 ++++- ..._are_properly_registered.DotNet10_0.DotNet.verified.txt | 3 +++ ...perly_registered.DotNet10_0.Windows.DotNet.verified.txt | 3 +++ ...s_are_properly_registered.DotNet8_0.DotNet.verified.txt | 3 +++ ...operly_registered.DotNet8_0.Windows.DotNet.verified.txt | 3 +++ ...s_are_properly_registered.DotNet9_0.DotNet.verified.txt | 3 +++ ...operly_registered.DotNet9_0.Windows.DotNet.verified.txt | 3 +++ ...are_properly_registered.Net4_8.Windows.Net.verified.txt | 3 +++ test/Sentry.Tests/SentryOptionsTests.cs | 7 +++++++ 10 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deab977457..2a39889f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `BreadcrumbLevel.Critical` has been renamed to `BreadcrumbLevel.Fatal` for consistency with the other Sentry SDKs ([#4605](https://github.com/getsentry/sentry-dotnet/pull/4605)) - SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) - Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) +- Backpressure handling is now enabled by default, meaning that the SDK will monitor system health and reduce the sampling rate of events and transactions when the system is under load. When the system is determined to be healthy again, the sampling rates are returned to their original levels. ([#4615](https://github.com/getsentry/sentry-dotnet/pull/4615)) - ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611)) ### Fixes diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 4e2983cb91..9b8835a165 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -98,7 +98,10 @@ public bool IsGlobalModeEnabled /// 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; + /// + /// Defaults to true / enabled. + /// + public bool EnableBackpressureHandling { get; set; } = true; /// /// This holds a reference to the current transport, when one is active. diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt index dfbc55fc32..f477fddad0 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.DotNet.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt index 10eaeaf749..c1fa278003 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet10_0.Windows.DotNet.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt index dfbc55fc32..f477fddad0 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt index 10eaeaf749..c1fa278003 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt index dfbc55fc32..f477fddad0 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.DotNet.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt index 10eaeaf749..c1fa278003 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet9_0.Windows.DotNet.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt index be7336d126..e9aa248faa 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt @@ -5,6 +5,9 @@ https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 ] }, + { + Message: Starting BackpressureMonitor. + }, { Message: Registering integration: '{0}'., Args: [ diff --git a/test/Sentry.Tests/SentryOptionsTests.cs b/test/Sentry.Tests/SentryOptionsTests.cs index 7d53c1c9b5..df934ad69a 100644 --- a/test/Sentry.Tests/SentryOptionsTests.cs +++ b/test/Sentry.Tests/SentryOptionsTests.cs @@ -13,6 +13,13 @@ public void DecompressionMethods_ByDefault_AllBitsSet() Assert.Equal(~DecompressionMethods.None, sut.DecompressionMethods); } + [Fact] + public void EnableBackpressureHandling_Default_True() + { + var sut = new SentryOptions(); + sut.EnableBackpressureHandling.Should().BeTrue(); + } + [Fact] public void RequestBodyCompressionLevel_ByDefault_Optimal() { From 90a38233ff3dd727069b40b7d184d7f943b8e07e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 16 Oct 2025 22:12:37 +1300 Subject: [PATCH 15/23] Merge version 5.16.1 into version6 (#4634) Co-authored-by: J-P Nurmi --- .editorconfig | 3 + .github/actions/buildnative/action.yml | 4 +- .github/actions/environment/action.yml | 8 +- .github/actions/freediskspace/action.yml | 2 +- .github/workflows/alpine.yml | 10 +- .github/workflows/build.yml | 55 +-- .github/workflows/codeql-analysis.yml | 8 +- .github/workflows/danger.yml | 4 +- .github/workflows/device-tests-android.yml | 61 ++- .github/workflows/device-tests-ios.yml | 27 +- .github/workflows/format-code.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/update-deps.yml | 2 +- .github/workflows/vulnerabilities.yml | 2 +- CHANGELOG.md | 18 + Directory.Build.props | 2 +- Sentry.sln | 3 + integration-test/android.Tests.ps1 | 166 +++++++ integration-test/common.ps1 | 53 +-- integration-test/ios.Tests.ps1 | 126 +++++ integration-test/net9-maui/App.xaml | 14 + integration-test/net9-maui/App.xaml.cs | 14 + integration-test/net9-maui/AppShell.xaml | 14 + integration-test/net9-maui/AppShell.xaml.cs | 9 + integration-test/net9-maui/GlobalXmlns.cs | 2 + integration-test/net9-maui/MainPage.xaml | 29 ++ integration-test/net9-maui/MainPage.xaml.cs | 47 ++ integration-test/net9-maui/MauiProgram.cs | 33 ++ .../Platforms/Android/AndroidManifest.xml | 6 + .../Platforms/Android/MainActivity.cs | 23 + .../Platforms/Android/MainApplication.cs | 15 + .../Android/Resources/values/colors.xml | 6 + .../Resources/xml/network_security_config.xml | 4 + .../net9-maui/Platforms/iOS/AppDelegate.cs | 9 + .../net9-maui/Platforms/iOS/Info.plist | 32 ++ .../net9-maui/Platforms/iOS/Program.cs | 15 + .../iOS/Resources/PrivacyInfo.xcprivacy | 51 ++ .../net9-maui/Resources/AppIcon/appicon.svg | 4 + .../net9-maui/Resources/AppIcon/appiconfg.svg | 8 + .../Resources/Fonts/OpenSans-Regular.ttf | Bin 0 -> 96932 bytes .../Resources/Fonts/OpenSans-Semibold.ttf | Bin 0 -> 100820 bytes .../net9-maui/Resources/Images/dotnet_bot.png | Bin 0 -> 93437 bytes .../net9-maui/Resources/Raw/AboutAssets.txt | 15 + .../net9-maui/Resources/Splash/splash.svg | 8 + .../net9-maui/Resources/Styles/Colors.xaml | 45 ++ .../net9-maui/Resources/Styles/Styles.xaml | 444 ++++++++++++++++++ ...ntry.Maui.Device.IntegrationTestApp.csproj | 61 +++ integration-test/pester.ps1 | 56 +++ ...ulator-utils.ps1 => device-test-utils.ps1} | 39 +- scripts/device-test.ps1 | 13 +- .../Sentry.Bindings.Cocoa.csproj | 4 +- .../SentryDatabaseLogging.cs | 6 +- .../SentryStructuredLogger.cs | 1 + src/Sentry.Profiling/SampleProfilerSession.cs | 15 +- .../SamplingTransactionProfilerFactory.cs | 15 +- src/Sentry.Serilog/SentrySink.Structured.cs | 1 + .../DetectBlockingSynchronizationContext.cs | 4 +- src/Sentry/Internal/Hub.cs | 11 +- src/Sentry/Internal/InterlockedBoolean.cs | 54 +++ src/Sentry/Sentry.csproj | 14 +- src/Sentry/SentryLog.cs | 5 + src/Sentry/Threading/ScopedCountdownLock.cs | 15 +- src/Sentry/TransactionTracer.cs | 7 +- .../Sentry.AspNetCore.TestUtils.csproj | 4 +- ...AspNetCoreStructuredLoggerProviderTests.cs | 3 + .../SentryStructuredLoggerProviderTests.cs | 3 + .../SentryStructuredLoggerTests.cs | 1 + .../SentrySinkTests.Structured.cs | 2 + ...ionTests.cs => AggregateExceptionTests.cs} | 2 +- .../Internals/InterlockedBooleanTests.cs | 152 ++++++ test/Sentry.Tests/SentryLogTests.cs | 3 + 71 files changed, 1712 insertions(+), 186 deletions(-) create mode 100644 integration-test/android.Tests.ps1 create mode 100644 integration-test/ios.Tests.ps1 create mode 100644 integration-test/net9-maui/App.xaml create mode 100644 integration-test/net9-maui/App.xaml.cs create mode 100644 integration-test/net9-maui/AppShell.xaml create mode 100644 integration-test/net9-maui/AppShell.xaml.cs create mode 100644 integration-test/net9-maui/GlobalXmlns.cs create mode 100644 integration-test/net9-maui/MainPage.xaml create mode 100644 integration-test/net9-maui/MainPage.xaml.cs create mode 100644 integration-test/net9-maui/MauiProgram.cs create mode 100644 integration-test/net9-maui/Platforms/Android/AndroidManifest.xml create mode 100644 integration-test/net9-maui/Platforms/Android/MainActivity.cs create mode 100644 integration-test/net9-maui/Platforms/Android/MainApplication.cs create mode 100644 integration-test/net9-maui/Platforms/Android/Resources/values/colors.xml create mode 100644 integration-test/net9-maui/Platforms/Android/Resources/xml/network_security_config.xml create mode 100644 integration-test/net9-maui/Platforms/iOS/AppDelegate.cs create mode 100644 integration-test/net9-maui/Platforms/iOS/Info.plist create mode 100644 integration-test/net9-maui/Platforms/iOS/Program.cs create mode 100644 integration-test/net9-maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy create mode 100644 integration-test/net9-maui/Resources/AppIcon/appicon.svg create mode 100644 integration-test/net9-maui/Resources/AppIcon/appiconfg.svg create mode 100644 integration-test/net9-maui/Resources/Fonts/OpenSans-Regular.ttf create mode 100644 integration-test/net9-maui/Resources/Fonts/OpenSans-Semibold.ttf create mode 100644 integration-test/net9-maui/Resources/Images/dotnet_bot.png create mode 100644 integration-test/net9-maui/Resources/Raw/AboutAssets.txt create mode 100644 integration-test/net9-maui/Resources/Splash/splash.svg create mode 100644 integration-test/net9-maui/Resources/Styles/Colors.xaml create mode 100644 integration-test/net9-maui/Resources/Styles/Styles.xaml create mode 100644 integration-test/net9-maui/Sentry.Maui.Device.IntegrationTestApp.csproj create mode 100644 integration-test/pester.ps1 rename scripts/{ios-simulator-utils.ps1 => device-test-utils.ps1} (75%) create mode 100644 src/Sentry/Internal/InterlockedBoolean.cs rename test/Sentry.Tests/Internals/{AgggregateExceptionTests.cs => AggregateExceptionTests.cs} (97%) create mode 100644 test/Sentry.Tests/Internals/InterlockedBooleanTests.cs diff --git a/.editorconfig b/.editorconfig index a2a479130e..01deda3d42 100644 --- a/.editorconfig +++ b/.editorconfig @@ -40,6 +40,9 @@ indent_size = 2 # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true +# Keep using directives at the top of the file, outside of the namespace +csharp_using_directive_placement = outside_namespace + # Avoid "this." and "Me." if not necessary dotnet_style_qualification_for_field = false : warning dotnet_style_qualification_for_property = false : warning diff --git a/.github/actions/buildnative/action.yml b/.github/actions/buildnative/action.yml index b84f11a04e..14fc85092d 100644 --- a/.github/actions/buildnative/action.yml +++ b/.github/actions/buildnative/action.yml @@ -14,7 +14,7 @@ runs: echo "JAVA_HOME_11=$JAVA_HOME_11_X64" >> $GITHUB_ENV fi - - uses: actions/cache@v3 + - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 id: cache-c with: path: lib/sentrysupplemental/bin @@ -35,7 +35,7 @@ runs: shell: cmd run: lib\sentrysupplemental\build.cmd - - uses: actions/cache@v3 + - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 id: cache-android with: path: lib/sentry-android-supplemental/bin diff --git a/.github/actions/environment/action.yml b/.github/actions/environment/action.yml index a646c021f8..498a3f41d5 100644 --- a/.github/actions/environment/action.yml +++ b/.github/actions/environment/action.yml @@ -52,7 +52,7 @@ runs: # Java 17 is needed for Android SDK setup step - name: Install Java 17 if: ${{ !matrix.container }} - uses: actions/setup-java@v4 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: ${{ runner.os == 'Windows' && runner.arch == 'ARM64' && 'microsoft' || 'temurin' }} java-version: '17' @@ -68,7 +68,7 @@ runs: # Java 11 is needed by .NET Android - name: Install Java 11 if: ${{ !matrix.container }} - uses: actions/setup-java@v4 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: ${{ runner.os == 'Windows' && runner.arch == 'ARM64' && 'microsoft' || 'temurin' }} java-version: '11' @@ -91,13 +91,13 @@ runs: sudo chmod -R a+rw /usr/share/dotnet - name: Install .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: + global-json-file: global.json dotnet-version: | 8.0.x 9.0.304 10.0.100-rc.1.25451.107 - global-json-file: global.json # .NET 5.0 does not support ARM64 on macOS - name: Install .NET 5.0 SDK diff --git a/.github/actions/freediskspace/action.yml b/.github/actions/freediskspace/action.yml index 584430d227..f7fd4ce241 100644 --- a/.github/actions/freediskspace/action.yml +++ b/.github/actions/freediskspace/action.yml @@ -6,7 +6,7 @@ runs: - name: Free Disk Space if: runner.os == 'Linux' - uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: android: false dotnet: false diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml index 207f8e2c1f..71e6a6f722 100644 --- a/.github/workflows/alpine.yml +++ b/.github/workflows/alpine.yml @@ -23,18 +23,18 @@ jobs: packages: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: docker/login-action@v3 + - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: push: true platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 435e61fd07..66b3784b8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: curl -sSL https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/.github/alpine/setup-node.sh | sudo bash /dev/stdin - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - run: git submodule update --init modules/sentry-native @@ -57,8 +57,8 @@ jobs: - name: Install zstd on Windows ARM64 uses: ./.github/actions/install-zstd - - uses: actions/cache@v4 - id: cache + - id: cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-${{ matrix.rid }}-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -118,10 +118,10 @@ jobs: - name: Cancel Previous Runs if: github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # Tag: 0.12.1 + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive fetch-depth: 2 # default is 1 and codecov needs > 1 @@ -140,7 +140,7 @@ jobs: - name: Download sentry-native (linux-x64) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'linux-x64') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-linux-x64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -148,7 +148,7 @@ jobs: - name: Download sentry-native (linux-arm64) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'linux-arm64') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-linux-arm64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -156,7 +156,7 @@ jobs: - name: Download sentry-native (linux-musl-x64) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'linux-musl-x64') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-linux-musl-x64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -164,7 +164,7 @@ jobs: - name: Download sentry-native (linux-musl-arm64) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'linux-musl-arm64') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-linux-musl-arm64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -172,7 +172,7 @@ jobs: - name: Download sentry-native (macos) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'macos') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-macos-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -180,7 +180,7 @@ jobs: - name: Download sentry-native (win-x64) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'win-x64') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-win-x64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -189,7 +189,7 @@ jobs: - name: Download sentry-native (win-arm64) if: ${{ (env.CI_PUBLISHING_BUILD == 'true') || (matrix.rid == 'win-arm64') }} - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-win-arm64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -209,7 +209,7 @@ jobs: - name: Upload build logs if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.rid }}-build-logs path: | @@ -221,13 +221,13 @@ jobs: run: dotnet test ${{ matrix.slnf }} -c Release --no-build --nologo -l GitHubActions -l "trx;LogFilePrefix=testresults_${{ runner.os }}" --collect "XPlat Code Coverage" - name: Upload code coverage - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload build and test outputs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.rid }}-verify-test-results path: "**/*.received.*" @@ -237,7 +237,7 @@ jobs: - name: Archive NuGet Packages if: env.CI_PUBLISHING_BUILD == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ github.sha }} if-no-files-found: error @@ -247,18 +247,19 @@ jobs: - name: Sparse checkout if: env.CI_PUBLISHING_BUILD == 'true' - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # We only check out what is absolutely necessary to reduce a chance of local files impacting # integration tests (nuget.config etc.)... But we need the root Directory.Build.props calculate # the package version sparse-checkout: | integration-test + scripts .github - name: Fetch NuGet Packages if: env.CI_PUBLISHING_BUILD == 'true' - uses: actions/download-artifact@v5 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: ${{ github.sha }} path: src @@ -287,12 +288,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive - name: Download sentry-native (win-x64) - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-win-x64-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -306,7 +307,7 @@ jobs: uses: ./.github/actions/buildnative - name: Setup MSBuild - uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2 + uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 - name: Run MSBuild id: msbuild @@ -319,7 +320,7 @@ jobs: - name: Upload logs if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ runner.os }}-msbuild-logs path: | @@ -345,7 +346,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive @@ -356,7 +357,7 @@ jobs: uses: ./.github/actions/buildnative - name: Fetch NuGet Packages - uses: actions/download-artifact@v5 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: ${{ github.sha }} path: src @@ -375,7 +376,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive fetch-depth: 2 # default is 1 and codecov needs > 1 @@ -385,7 +386,7 @@ jobs: run: echo "CI_PUBLISHING_BUILD=true" >> $GITHUB_ENV - name: Download sentry-native (macos) - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: src/Sentry/Platforms/Native/sentry-native key: sentry-native-macos-${{ hashFiles('scripts/build-sentry-native.ps1') }}-${{ hashFiles('.git/modules/modules/sentry-native/HEAD') }} @@ -414,7 +415,7 @@ jobs: if: ${{ !startsWith(github.ref_name, 'release/') }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 79322f0dec..5c93f14d47 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Cancel Previous Runs if: github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # Tag: 0.12.1 + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive @@ -35,7 +35,7 @@ jobs: uses: ./.github/actions/environment - name: Initialize CodeQL - uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 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@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 with: category: '/language:csharp' diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 000b75ff3e..09d4bcb033 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -6,4 +6,6 @@ on: jobs: danger: - uses: getsentry/github-workflows/.github/workflows/danger.yml@v2 + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/danger@v3 diff --git a/.github/workflows/device-tests-android.yml b/.github/workflows/device-tests-android.yml index b234d39df2..4fe2a3bce5 100644 --- a/.github/workflows/device-tests-android.yml +++ b/.github/workflows/device-tests-android.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Cancel Previous Runs if: github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # Tag: 0.12.1 + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive @@ -42,7 +42,7 @@ jobs: - name: Upload Android Test App (net9.0) if: matrix.tfm == 'net9.0' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: device-test-android-net9.0 if-no-files-found: error @@ -80,25 +80,27 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + submodules: recursive - name: Download test app artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: device-test-android-${{ matrix.tfm }} path: bin - name: Setup Gradle - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # pin@v3 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 # Cached AVD setup per https://github.com/ReactiveCircus/android-emulator-runner/blob/main/README.md - name: Run Tests - id: first-run + id: first-test-run continue-on-error: true timeout-minutes: 40 - uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # Tag: v2.34.0 - with: + uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0 + with: &android-emulator-test api-level: ${{ matrix.api-level }} target: ${{ env.ANDROID_EMULATOR_TARGET }} force-avd-creation: false @@ -110,10 +112,33 @@ jobs: 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' + if: steps.first-test-run.outcome == 'failure' timeout-minutes: 40 - uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # Tag: v2.34.0 + uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0 + with: *android-emulator-test + + - name: Setup Environment + uses: ./.github/actions/environment + + - name: Select Java 17 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: + distribution: ${{ runner.os == 'Windows' && runner.arch == 'ARM64' && 'microsoft' || 'temurin' }} + java-version: '17' + + - name: Checkout github-workflows + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: getsentry/github-workflows + ref: a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1 + path: modules/github-workflows + + - name: Run Integration Tests + id: first-integration-test-run + continue-on-error: true + timeout-minutes: 40 + uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0 + with: &android-emulator-integration-test api-level: ${{ matrix.api-level }} target: ${{ env.ANDROID_EMULATOR_TARGET }} force-avd-creation: false @@ -122,11 +147,19 @@ jobs: 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 }} + script: pwsh integration-test/android.Tests.ps1 + + - name: Retry Integration Tests (if previous failed to run) + if: steps.first-integration-test-run.outcome == 'failure' + timeout-minutes: 40 + uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0 + with: *android-emulator-integration-test - name: Upload results if: success() || failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: device-test-android-${{ matrix.api-level }}-${{ matrix.tfm }}-results - path: test_output + path: | + test_output + integration-test/mobile-app/test_output diff --git a/.github/workflows/device-tests-ios.yml b/.github/workflows/device-tests-ios.yml index 0e118745cc..88b59676ae 100644 --- a/.github/workflows/device-tests-ios.yml +++ b/.github/workflows/device-tests-ios.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Cancel Previous Runs if: github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # Tag: 0.12.1 + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive @@ -35,17 +35,32 @@ jobs: run: pwsh ./scripts/device-test.ps1 ios -Build - name: Run Tests - id: first-run + id: first-test-run continue-on-error: true run: pwsh scripts/device-test.ps1 ios -Run - name: Retry Tests (if previous failed to run) - if: steps.first-run.outcome == 'failure' + if: steps.first-test-run.outcome == 'failure' run: pwsh scripts/device-test.ps1 ios -Run + - name: Run Integration Tests + id: first-integration-test-run + continue-on-error: true + uses: getsentry/github-workflows/sentry-cli/integration-test/@a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1 + with: + path: integration-test/ios.Tests.ps1 + + - name: Retry Integration Tests (if previous failed to run) + if: steps.first-integration-test-run.outcome == 'failure' + uses: getsentry/github-workflows/sentry-cli/integration-test/@a5e409bd5bad4c295201cdcfe862b17c50b29ab7 # v2.14.1 + with: + path: integration-test/ios.Tests.ps1 + - name: Upload results if: success() || failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: device-test-ios-results - path: test_output + path: | + test_output + integration-test/mobile-app/test_output diff --git a/.github/workflows/format-code.yml b/.github/workflows/format-code.yml index ede195cf6a..16b6f70e3d 100644 --- a/.github/workflows/format-code.yml +++ b/.github/workflows/format-code.yml @@ -15,7 +15,7 @@ jobs: runs-on: macos-15 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cc72a9ad8..0f94d89d60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,13 +25,13 @@ jobs: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} - name: Check out current commit (${{ github.sha }}) - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Prepare release ${{ github.event.inputs.version }} - uses: getsentry/action-prepare-release@v1 + uses: getsentry/action-prepare-release@3cea80dc3938c0baf5ec4ce752ecb311f8780cdc # v1.6.4 env: GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml index 626240a456..4363a73562 100644 --- a/.github/workflows/update-deps.yml +++ b/.github/workflows/update-deps.yml @@ -22,7 +22,7 @@ jobs: path: modules/sentry-native - name: CLI path: scripts/update-cli.ps1 - uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 + uses: getsentry/github-workflows/.github/workflows/updater.yml@1949ea01ec2da6139d1bcc306c372e6aea76fb72 # v2.13.1 with: name: ${{ matrix.name }} path: ${{ matrix.path }} diff --git a/.github/workflows/vulnerabilities.yml b/.github/workflows/vulnerabilities.yml index 8197efdd86..a8bdb21b6e 100644 --- a/.github/workflows/vulnerabilities.yml +++ b/.github/workflows/vulnerabilities.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: submodules: recursive diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a39889f63..e28ff1e0ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,24 @@ - This release adds support for .NET 10 and drops support for net8.0-android, net8.0-ios, net8.0-maccatalyst and net8.0-windows10.0.19041.0 ([#4461](https://github.com/getsentry/sentry-dotnet/pull/4461)) - Added support for v3 of the Android AssemblyStore format that is used in .NET 10 and dropped support for v1 that was used in .NET 8 ([#4583](https://github.com/getsentry/sentry-dotnet/pull/4583)) +## 5.16.1 + +### Fixes + +- Structured Logs now have a `sentry.origin` attribute to so it's clearer where these come from ([#4566](https://github.com/getsentry/sentry-dotnet/pull/4566)) + +### 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) +- Bump CLI from v2.56.0 to v2.56.1 ([#4625](https://github.com/getsentry/sentry-dotnet/pull/4625)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2561) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.56.0...2.56.1) + ## 5.16.0 ### Features diff --git a/Directory.Build.props b/Directory.Build.props index 59e7cfa43b..2be652ff9f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -103,7 +103,7 @@ - 2.56.0 + 2.56.1 $(MSBuildThisFileDirectory)tools\sentry-cli\$(SentryCLIVersion)\ diff --git a/Sentry.sln b/Sentry.sln index e8b04afa38..afa5819374 100644 --- a/Sentry.sln +++ b/Sentry.sln @@ -273,6 +273,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration-test", "integra integration-test\aot.Tests.ps1 = integration-test\aot.Tests.ps1 integration-test\cli.Tests.ps1 = integration-test\cli.Tests.ps1 integration-test\runtime.Tests.ps1 = integration-test\runtime.Tests.ps1 + integration-test\pester.ps1 = integration-test\pester.ps1 + integration-test\ios.Tests.ps1 = integration-test\ios.Tests.ps1 + integration-test\msbuild.Tests.ps1 = integration-test\msbuild.Tests.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net4-console", "net4-console", "{33793113-C7B5-434D-B5C1-6CA1A9587842}" diff --git a/integration-test/android.Tests.ps1 b/integration-test/android.Tests.ps1 new file mode 100644 index 0000000000..7ec0a80fbc --- /dev/null +++ b/integration-test/android.Tests.ps1 @@ -0,0 +1,166 @@ +# This file contains test cases for https://pester.dev/ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +. $PSScriptRoot/pester.ps1 +. $PSScriptRoot/../scripts/device-test-utils.ps1 + +BeforeDiscovery { + # Skip Android integration tests unless an emulator has been already started + # by Android Device Tests, or manually when testing locally. This avoids + # slowing down non-Device Test CI builds further. + Install-XHarness + $script:emulator = Get-AndroidEmulatorId +} + +Describe 'MAUI app' -ForEach @( + @{ tfm = "net9.0-android35.0" } +) -Skip:(-not $script:emulator) { + BeforeAll { + Remove-Item -Path "$PSScriptRoot/mobile-app" -Recurse -Force -ErrorAction SilentlyContinue + Copy-Item -Path "$PSScriptRoot/net9-maui" -Destination "$PSScriptRoot/mobile-app" -Recurse -Force + Push-Location $PSScriptRoot/mobile-app + + function InstallAndroidApp + { + param([string] $Dsn) + $dsn = $Dsn.Replace('http://', 'http://key@') + '/0' + + # replace {{SENTRY_DSN}} in MauiProgram.cs + (Get-Content MauiProgram.cs) ` + -replace '\{\{SENTRY_DSN\}\}', $dsn ` + | Set-Content MauiProgram.cs + + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + $rid = "android-$arch" + + Write-Host "::group::Build Sentry.Maui.Device.IntegrationTestApp.csproj" + dotnet build Sentry.Maui.Device.IntegrationTestApp.csproj ` + --configuration Release ` + --framework $tfm ` + --runtime $rid + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + + Write-Host "::group::Install bin/Release/$tfm/$rid/io.sentry.dotnet.maui.device.integrationtestapp-Signed.apk" + xharness android install -v ` + --app bin/Release/$tfm/$rid/io.sentry.dotnet.maui.device.integrationtestapp-Signed.apk ` + --package-name io.sentry.dotnet.maui.device.integrationtestapp ` + --output-directory=test_output + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + } + + function RunAndroidApp + { + param( + [string] $Dsn, + [string] $TestArg = 'None' + ) + + try + { + # Setup port forwarding for accessing sentry-server at 127.0.0.1:8000 from the emulator + $port = $Dsn.Split(':')[2].Split('/')[0] + xharness android adb -v -- reverse tcp:$port tcp:$port + + Write-Host "::group::Run Android app (TestArg=$TestArg)" + xharness android adb -v ` + -- shell am start -S -n io.sentry.dotnet.maui.device.integrationtestapp/.MainActivity ` + -e SENTRY_TEST_ARG $TestArg + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + + do + { + Write-Host "Waiting for app..." + Start-Sleep -Seconds 1 + + $procid = (& xharness android adb -- shell pidof "io.sentry.dotnet.maui.device.integrationtestapp") -replace '\s', '' + $activity = (& xharness android adb -- shell dumpsys activity activities) -match "io\.sentry\.dotnet\.maui\.device\.integrationtestapp" + + } while ($procid -and $activity) + } + finally + { + xharness android adb -v -- reverse --remove-all + } + } + + function UninstallAndroidApp + { + Write-Host "::group::Uninstall io.sentry.dotnet.maui.device.integrationtestapp" + xharness android uninstall -v ` + --package-name io.sentry.dotnet.maui.device.integrationtestapp + | ForEach-Object { Write-Host $_ } + $LASTEXITCODE | Should -Be 0 + Write-Host '::endgroup::' + } + + # Helper to dump server stderr if the test server reported errors + function Dump-ServerErrors { + param( + [Parameter(Mandatory)] + $Result + ) + if ($Result.HasErrors()) { + Write-Host '::group::sentry-server stderr' + $Result.ServerStdErr | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + } + } + } + + AfterAll { + Pop-Location + } + + AfterEach { + UninstallAndroidApp + } + + It 'Managed crash' { + $result = Invoke-SentryServer { + param([string]$url) + InstallAndroidApp -Dsn $url + RunAndroidApp -Dsn $url -TestArg "Managed" + RunAndroidApp -Dsn $url + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"System.ApplicationException`"" + $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"SIGABRT`"" + } + + It 'Java crash' { + $result = Invoke-SentryServer { + param([string]$url) + InstallAndroidApp -Dsn $url + RunAndroidApp -Dsn $url -TestArg "Java" + RunAndroidApp -Dsn $url + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"RuntimeException`"" + $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"System.\w+Exception`"" + } + + It 'Null reference exception' { + $result = Invoke-SentryServer { + param([string]$url) + InstallAndroidApp -Dsn $url + RunAndroidApp -Dsn $url -TestArg "NullReferenceException" + RunAndroidApp -Dsn $url + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"System.NullReferenceException`"" + # TODO: fix redundant RuntimeException (#3954) + { $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"SIGSEGV`"" } | Should -Throw + } +} diff --git a/integration-test/common.ps1 b/integration-test/common.ps1 index 312825360e..ba9c150aef 100644 --- a/integration-test/common.ps1 +++ b/integration-test/common.ps1 @@ -1,59 +1,10 @@ +. $PSScriptRoot/pester.ps1 + $global:longTermFramework = 'net8.0' $global:previousFramework = 'net9.0' $global:latestFramework = 'net10.0' $global:currentFrameworks = @($longTermFramework, $previousFramework, $latestFramework) -# So that this works in VS Code testing integration. Otherwise the script is run within its directory. -# In CI, the module is loaded automatically -if (!(Test-Path env:CI )) -{ - Import-Module $PSScriptRoot/../../github-workflows/sentry-cli/integration-test/action.psm1 -Force -} - -function ShouldAnyElementMatch ($ActualValue, [string]$ExpectedValue, [switch] $Negate, [string] $Because) -{ - <# - .SYNOPSIS - Asserts whether any item in the collection matches the expected value - .EXAMPLE - 'foo','bar','foobar' | Should -AnyElementMatch 'oob' - - This should pass because 'oob' is a substring of 'foobar'. - #> - - $filtered = $ActualValue | Where-Object { $_ -match $ExpectedValue } - [bool] $succeeded = @($filtered).Count -gt 0 - if ($Negate) { $succeeded = -not $succeeded } - - if (-not $succeeded) - { - if ($Negate) - { - $failureMessage = "Expected string '$ExpectedValue' to match no elements in collection @($($ActualValue -join ', '))$(if($Because) { " because $Because"})." - } - else - { - $failureMessage = "Expected string '$ExpectedValue' to match any element in collection @($($ActualValue -join ', '))$(if($Because) { " because $Because"})." - } - } - else - { - $failureMessage = $null - } - - return [pscustomobject]@{ - Succeeded = $succeeded - FailureMessage = $failureMessage - } -} - -BeforeDiscovery { - Add-ShouldOperator -Name AnyElementMatch ` - -InternalName 'ShouldAnyElementMatch' ` - -Test ${function:ShouldAnyElementMatch} ` - -SupportsArrayInput -} - AfterAll { Pop-Location } diff --git a/integration-test/ios.Tests.ps1 b/integration-test/ios.Tests.ps1 new file mode 100644 index 0000000000..b7dae6ba27 --- /dev/null +++ b/integration-test/ios.Tests.ps1 @@ -0,0 +1,126 @@ +# This file contains test cases for https://pester.dev/ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +. $PSScriptRoot/pester.ps1 +. $PSScriptRoot/../scripts/device-test-utils.ps1 + +BeforeDiscovery { + # Skip iOS integration tests unless a simulator has already been booted by + # iOS Device Tests, or manually when testing locally. This avoids slowing + # down the macOS build job further. + $script:simulator = Get-IosSimulatorUdid -PreferredStates @('Booted') +} + +Describe 'iOS app ()' -ForEach @( + @{ tfm = "net9.0-ios18.0" } +) -Skip:(-not $script:simulator) { + BeforeAll { + . $PSScriptRoot/../scripts/device-test-utils.ps1 + + Remove-Item -Path "$PSScriptRoot/mobile-app" -Recurse -Force -ErrorAction SilentlyContinue + Copy-Item -Path "$PSScriptRoot/net9-maui" -Destination "$PSScriptRoot/mobile-app" -Recurse -Force + Push-Location $PSScriptRoot/mobile-app + + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLower() + $rid = "iossimulator-$arch" + + Write-Host "::group::Build Sentry.Maui.Device.IntegrationTestApp.csproj" + dotnet build Sentry.Maui.Device.IntegrationTestApp.csproj ` + --configuration Release ` + --framework $tfm ` + --runtime $rid + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + + function InstallIosApp + { + Write-Host "::group::Install bin/Release/$tfm/$rid/Sentry.Maui.Device.IntegrationTestApp.app" + xcrun simctl install $simulator ` + bin/Release/$tfm/$rid/Sentry.Maui.Device.IntegrationTestApp.app + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + } + + function UninstallIosApp + { + Write-Host "::group::Uninstall io.sentry.dotnet.maui.device.integrationtestapp" + xcrun simctl uninstall $simulator ` + io.sentry.dotnet.maui.device.integrationtestapp + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + } + + function RunIosApp + { + param( + [string] $Dsn, + [string] $TestArg = 'None' + ) + $Dsn = $Dsn.Replace('http://', 'http://key@') + '/0' + Write-Host "::group::Run iOS app (TestArg=$TestArg)" + xcrun simctl spawn $simulator launchctl setenv SENTRY_DSN $Dsn + xcrun simctl spawn $simulator launchctl setenv SENTRY_TEST_ARG $TestArg + xcrun simctl launch ` + --console ` + --terminate-running-process ` + $simulator ` + io.sentry.dotnet.maui.device.integrationtestapp + | ForEach-Object { Write-Host $_ } + Write-Host '::endgroup::' + $LASTEXITCODE | Should -Be 0 + } + } + + AfterAll { + Pop-Location + } + + BeforeEach { + InstallIosApp + } + + AfterEach { + UninstallIosApp + } + + It 'captures managed crash' { + $result = Invoke-SentryServer { + param([string]$url) + RunIosApp -Dsn $url -TestArg "Managed" + RunIosApp -Dsn $url + } + + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"System.ApplicationException`"" + # TODO: fix redundant SIGABRT (#3954) + { $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"SIGABRT`"" } | Should -Throw + } + + It 'captures native crash' { + $result = Invoke-SentryServer { + param([string]$url) + RunIosApp -Dsn $url -TestArg "Native" + RunIosApp -Dsn $url + } + + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"EXC_[A-Z_]+`"" + $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"System.\w+Exception`"" + } + + It 'captures null reference exception' { + $result = Invoke-SentryServer { + param([string]$url) + RunIosApp -Dsn $url -TestArg "NullReferenceException" + RunIosApp -Dsn $url + } + + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"System.NullReferenceException`"" + # TODO: fix redundant EXC_BAD_ACCESS (#3954) + { $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"EXC_BAD_ACCESS`"" } | Should -Throw + } +} diff --git a/integration-test/net9-maui/App.xaml b/integration-test/net9-maui/App.xaml new file mode 100644 index 0000000000..183709a1c8 --- /dev/null +++ b/integration-test/net9-maui/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/integration-test/net9-maui/App.xaml.cs b/integration-test/net9-maui/App.xaml.cs new file mode 100644 index 0000000000..cd3a1668c9 --- /dev/null +++ b/integration-test/net9-maui/App.xaml.cs @@ -0,0 +1,14 @@ +namespace Sentry.Maui.Device.IntegrationTestApp; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } +} diff --git a/integration-test/net9-maui/AppShell.xaml b/integration-test/net9-maui/AppShell.xaml new file mode 100644 index 0000000000..9fc01ffd1c --- /dev/null +++ b/integration-test/net9-maui/AppShell.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/integration-test/net9-maui/AppShell.xaml.cs b/integration-test/net9-maui/AppShell.xaml.cs new file mode 100644 index 0000000000..285aaf2ce9 --- /dev/null +++ b/integration-test/net9-maui/AppShell.xaml.cs @@ -0,0 +1,9 @@ +namespace Sentry.Maui.Device.IntegrationTestApp; + +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + } +} diff --git a/integration-test/net9-maui/GlobalXmlns.cs b/integration-test/net9-maui/GlobalXmlns.cs new file mode 100644 index 0000000000..19ce62e506 --- /dev/null +++ b/integration-test/net9-maui/GlobalXmlns.cs @@ -0,0 +1,2 @@ +[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Sentry.Maui.Device.IntegrationTestApp")] +[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "Sentry.Maui.Device.IntegrationTestApp.Pages")] diff --git a/integration-test/net9-maui/MainPage.xaml b/integration-test/net9-maui/MainPage.xaml new file mode 100644 index 0000000000..c539acffa3 --- /dev/null +++ b/integration-test/net9-maui/MainPage.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/integration-test/net9-maui/MainPage.xaml.cs b/integration-test/net9-maui/MainPage.xaml.cs new file mode 100644 index 0000000000..857d370195 --- /dev/null +++ b/integration-test/net9-maui/MainPage.xaml.cs @@ -0,0 +1,47 @@ +#if ANDROID +using Android.OS; +#endif + +namespace Sentry.Maui.Device.IntegrationTestApp; + +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + } + + protected override void OnAppearing() + { + base.OnAppearing(); + + var testArg = System.Environment.GetEnvironmentVariable("SENTRY_TEST_ARG"); + +#pragma warning disable CS0618 + if (Enum.TryParse(testArg, ignoreCase: true, out var crashType)) + { + SentrySdk.CauseCrash(crashType); + } +#pragma warning restore CS0618 + + if (testArg?.Equals("NullReferenceException", StringComparison.OrdinalIgnoreCase) == true) + { + try + { + object? obj = null; + _ = obj!.ToString(); + } + catch (NullReferenceException ex) + { + SentrySdk.CaptureException(ex); + } + } + + SentrySdk.Close(); +#if ANDROID + Process.KillProcess(Process.MyPid()); +#elif IOS + System.Environment.Exit(0); +#endif + } +} diff --git a/integration-test/net9-maui/MauiProgram.cs b/integration-test/net9-maui/MauiProgram.cs new file mode 100644 index 0000000000..3d34a90ce8 --- /dev/null +++ b/integration-test/net9-maui/MauiProgram.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using Sentry.Maui; + +namespace Sentry.Maui.Device.IntegrationTestApp; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .UseSentry(options => + { +#if ANDROID + options.Dsn = "{{SENTRY_DSN}}"; +#endif + options.Debug = false; + options.DiagnosticLevel = SentryLevel.Error; + }) + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } +} diff --git a/integration-test/net9-maui/Platforms/Android/AndroidManifest.xml b/integration-test/net9-maui/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000000..e5af392e6a --- /dev/null +++ b/integration-test/net9-maui/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/integration-test/net9-maui/Platforms/Android/MainActivity.cs b/integration-test/net9-maui/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000000..c848964a0f --- /dev/null +++ b/integration-test/net9-maui/Platforms/Android/MainActivity.cs @@ -0,0 +1,23 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; + +namespace Sentry.Maui.Device.IntegrationTestApp; + +[Activity( + Name = "io.sentry.dotnet.maui.device.integrationtestapp.MainActivity", + Theme = "@style/Maui.SplashTheme", + MainLauncher = true, + LaunchMode = LaunchMode.SingleTop, + ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density +)] +public class MainActivity : MauiAppCompatActivity +{ + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + System.Environment.SetEnvironmentVariable("SENTRY_TEST_ARG", Intent?.GetStringExtra("SENTRY_TEST_ARG")); + } +} diff --git a/integration-test/net9-maui/Platforms/Android/MainApplication.cs b/integration-test/net9-maui/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000000..3361e98c0f --- /dev/null +++ b/integration-test/net9-maui/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace Sentry.Maui.Device.IntegrationTestApp; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/integration-test/net9-maui/Platforms/Android/Resources/values/colors.xml b/integration-test/net9-maui/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000000..c04d7492ab --- /dev/null +++ b/integration-test/net9-maui/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/integration-test/net9-maui/Platforms/Android/Resources/xml/network_security_config.xml b/integration-test/net9-maui/Platforms/Android/Resources/xml/network_security_config.xml new file mode 100644 index 0000000000..2439f15c2c --- /dev/null +++ b/integration-test/net9-maui/Platforms/Android/Resources/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + diff --git a/integration-test/net9-maui/Platforms/iOS/AppDelegate.cs b/integration-test/net9-maui/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000000..24f2626f1c --- /dev/null +++ b/integration-test/net9-maui/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Sentry.Maui.Device.IntegrationTestApp; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/integration-test/net9-maui/Platforms/iOS/Info.plist b/integration-test/net9-maui/Platforms/iOS/Info.plist new file mode 100644 index 0000000000..f2e00c68de --- /dev/null +++ b/integration-test/net9-maui/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/integration-test/net9-maui/Platforms/iOS/Program.cs b/integration-test/net9-maui/Platforms/iOS/Program.cs new file mode 100644 index 0000000000..72371dbe8f --- /dev/null +++ b/integration-test/net9-maui/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Sentry.Maui.Device.IntegrationTestApp; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/integration-test/net9-maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/integration-test/net9-maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..24ab3b4334 --- /dev/null +++ b/integration-test/net9-maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,51 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + + diff --git a/integration-test/net9-maui/Resources/AppIcon/appicon.svg b/integration-test/net9-maui/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000000..9d63b6513a --- /dev/null +++ b/integration-test/net9-maui/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/integration-test/net9-maui/Resources/AppIcon/appiconfg.svg b/integration-test/net9-maui/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000000..21dfb25f18 --- /dev/null +++ b/integration-test/net9-maui/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/integration-test/net9-maui/Resources/Fonts/OpenSans-Regular.ttf b/integration-test/net9-maui/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..29bfd35a2bfdd92b6e8b4ec2970f4d1eebf49357 GIT binary patch literal 96932 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

_T_kl)Xoan=RX{3lY#VQTZ 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 literal 0 HcmV?d00001 diff --git a/integration-test/net9-maui/Resources/Fonts/OpenSans-Semibold.ttf b/integration-test/net9-maui/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..54e7059cf36359cb5a3860085714a95306af0dea GIT binary patch literal 100820 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 DEZ%x^ literal 0 HcmV?d00001 diff --git a/integration-test/net9-maui/Resources/Images/dotnet_bot.png b/integration-test/net9-maui/Resources/Images/dotnet_bot.png new file mode 100644 index 0000000000000000000000000000000000000000..1d1b981ee1ab520412db1299de0df05882d23d3e GIT binary patch literal 93437 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 literal 0 HcmV?d00001 diff --git a/integration-test/net9-maui/Resources/Raw/AboutAssets.txt b/integration-test/net9-maui/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000000..6de1c15270 --- /dev/null +++ b/integration-test/net9-maui/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with your package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/integration-test/net9-maui/Resources/Splash/splash.svg b/integration-test/net9-maui/Resources/Splash/splash.svg new file mode 100644 index 0000000000..21dfb25f18 --- /dev/null +++ b/integration-test/net9-maui/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/integration-test/net9-maui/Resources/Styles/Colors.xaml b/integration-test/net9-maui/Resources/Styles/Colors.xaml new file mode 100644 index 0000000000..30307a5ddc --- /dev/null +++ b/integration-test/net9-maui/Resources/Styles/Colors.xaml @@ -0,0 +1,45 @@ + + + + + + + #512BD4 + #ac99ea + #242424 + #DFD8F7 + #9880e5 + #2B0B98 + + White + Black + #D600AA + #190649 + #1f1f1f + + #E1E1E1 + #C8C8C8 + #ACACAC + #919191 + #6E6E6E + #404040 + #212121 + #141414 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-test/net9-maui/Resources/Styles/Styles.xaml b/integration-test/net9-maui/Resources/Styles/Styles.xaml new file mode 100644 index 0000000000..fdb0cd763c --- /dev/null +++ b/integration-test/net9-maui/Resources/Styles/Styles.xaml @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-test/net9-maui/Sentry.Maui.Device.IntegrationTestApp.csproj b/integration-test/net9-maui/Sentry.Maui.Device.IntegrationTestApp.csproj new file mode 100644 index 0000000000..757aef448e --- /dev/null +++ b/integration-test/net9-maui/Sentry.Maui.Device.IntegrationTestApp.csproj @@ -0,0 +1,61 @@ + + + + $(TargetFrameworks);net9.0-android35.0 + $(TargetFrameworks);net9.0-ios18.0 + + Exe + Sentry.Maui.Device.IntegrationTestApp + true + true + enable + enable + true + + + false + false + + + Sentry.Maui.Device.IntegrationTestApp + + + io.sentry.dotnet.maui.device.integrationtestapp + + + 1.0 + 1 + + 15.0 + 21.0 + 18.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-test/pester.ps1 b/integration-test/pester.ps1 new file mode 100644 index 0000000000..2f051baaf4 --- /dev/null +++ b/integration-test/pester.ps1 @@ -0,0 +1,56 @@ +# This file contains extensions for https://pester.dev/ + +# So that this works in VS Code testing integration. Otherwise the script is run within its directory. +# In CI, the module is loaded automatically +if (Test-Path $PSScriptRoot/../modules/github-workflows) +{ + Import-Module $PSScriptRoot/../modules/github-workflows/sentry-cli/integration-test/action.psm1 -Force +} +elseif (!(Test-Path env:CI )) +{ + Import-Module $PSScriptRoot/../../github-workflows/sentry-cli/integration-test/action.psm1 -Force +} + +function ShouldAnyElementMatch ($ActualValue, [string]$ExpectedValue, [switch] $Negate, [string] $Because) +{ + <# + .SYNOPSIS + Asserts whether any item in the collection matches the expected value + .EXAMPLE + 'foo','bar','foobar' | Should -AnyElementMatch 'oob' + + This should pass because 'oob' is a substring of 'foobar'. + #> + + $filtered = $ActualValue | Where-Object { $_ -match $ExpectedValue } + [bool] $succeeded = @($filtered).Count -gt 0 + if ($Negate) { $succeeded = -not $succeeded } + + if (-not $succeeded) + { + if ($Negate) + { + $failureMessage = "Expected string '$ExpectedValue' to match no elements in collection @($($ActualValue -join ', '))$(if($Because) { " because $Because"})." + } + else + { + $failureMessage = "Expected string '$ExpectedValue' to match any element in collection @($($ActualValue -join ', '))$(if($Because) { " because $Because"})." + } + } + else + { + $failureMessage = $null + } + + return [pscustomobject]@{ + Succeeded = $succeeded + FailureMessage = $failureMessage + } +} + +BeforeDiscovery { + Add-ShouldOperator -Name AnyElementMatch ` + -InternalName 'ShouldAnyElementMatch' ` + -Test ${function:ShouldAnyElementMatch} ` + -SupportsArrayInput +} diff --git a/scripts/ios-simulator-utils.ps1 b/scripts/device-test-utils.ps1 similarity index 75% rename from scripts/ios-simulator-utils.ps1 rename to scripts/device-test-utils.ps1 index 51f1b72fbd..c3f8b771b2 100644 --- a/scripts/ios-simulator-utils.ps1 +++ b/scripts/device-test-utils.ps1 @@ -1,12 +1,41 @@ +function Install-XHarness +{ + if (!(Get-Command xharness -ErrorAction SilentlyContinue)) + { + $CI = Test-Path env:CI + 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 + } +} + +function Get-AndroidEmulatorId +{ + if ((Test-Path env:CI) -or (Test-Path env:ANDROID_SERIAL)) + { + return $env:ANDROID_SERIAL + } + try + { + return & xharness android adb -- devices | Select-String "device$" | ForEach-Object { ($_ -split "`t")[0] } | Select-Object -First 1 + } + catch + { + return $null + } +} + function Get-IosSimulatorUdid { [CmdletBinding()] param( [string]$IosVersion = '18.5', [string[]]$PreferredDeviceTypes = @( - 'com.apple.CoreSimulator.SimDeviceType.iPhone-XS', - 'com.apple.CoreSimulator.SimDeviceType.iPhone-16', - 'com.apple.CoreSimulator.SimDeviceType.iPhone-15' - ) + 'com.apple.CoreSimulator.SimDeviceType.iPhone-XS', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-16', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-15' + ), + [string[]]$PreferredStates = @('Shutdown','Booted') ) try { @@ -60,7 +89,7 @@ function Get-IosSimulatorUdid { return $null } - $usable = $runtimeDevices | Where-Object { $_.isAvailable -and $_.state -in @('Shutdown','Booted') } + $usable = $runtimeDevices | Where-Object { $_.isAvailable -and $_.state -in $PreferredStates } if (-not $usable) { Write-Verbose "No available devices in runtime $runtimeKey" return $null diff --git a/scripts/device-test.ps1 b/scripts/device-test.ps1 index f9d5c30868..b42a1ecc06 100644 --- a/scripts/device-test.ps1 +++ b/scripts/device-test.ps1 @@ -12,7 +12,7 @@ param( Set-StrictMode -Version latest $ErrorActionPreference = 'Stop' -. $PSScriptRoot/ios-simulator-utils.ps1 +. $PSScriptRoot/device-test-utils.ps1 if (!$Build -and !$Run) { @@ -64,7 +64,7 @@ try '--set-env', "CI=$envValue" ) - $udid = Get-IosSimulatorUdid -IosVersion '18.5' -Verbose + $udid = Get-IosSimulatorUdid -Verbose if ($udid) { $arguments += @('--device', $udid) } else { @@ -84,14 +84,7 @@ try if ($Run) { - if (!(Get-Command xharness -ErrorAction SilentlyContinue)) - { - 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 - } - + Install-XHarness Remove-Item -Recurse -Force test_output -ErrorAction SilentlyContinue try { diff --git a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj index 0881063924..8628626950 100644 --- a/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj +++ b/src/Sentry.Bindings.Cocoa/Sentry.Bindings.Cocoa.csproj @@ -12,7 +12,7 @@ $([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 + ../../modules/sentry-cocoa.properties;../../scripts/generate-cocoa-bindings.ps1;$(SentryCocoaFrameworkHeaders)**/*.h $(NoWarn);CS0108 @@ -20,7 +20,7 @@ $(SentryCocoaCache)Carthage\Build-$(TargetPlatformIdentifier)\Sentry.xcframework - ../../scripts/generate-cocoa-bindings.ps1;../../modules/sentry-cocoa/Carthage/.built-from-sha + ../../scripts/generate-cocoa-bindings.ps1;$(SentryCocoaCache)Carthage/.built-from-sha;$(SentryCocoaCache)Carthage/**/*.h diff --git a/src/Sentry.EntityFramework/SentryDatabaseLogging.cs b/src/Sentry.EntityFramework/SentryDatabaseLogging.cs index c61ff996d2..174af13873 100644 --- a/src/Sentry.EntityFramework/SentryDatabaseLogging.cs +++ b/src/Sentry.EntityFramework/SentryDatabaseLogging.cs @@ -1,3 +1,5 @@ +using Sentry.Internal; + namespace Sentry.EntityFramework; ///

@@ -5,14 +7,14 @@ namespace Sentry.EntityFramework; /// internal static class SentryDatabaseLogging { - private static int Init; + private static InterlockedBoolean Init; internal static SentryCommandInterceptor? UseBreadcrumbs( IQueryLogger? queryLogger = null, bool initOnce = true, IDiagnosticLogger? diagnosticLogger = null) { - if (initOnce && Interlocked.Exchange(ref Init, 1) != 0) + if (initOnce && Init.Exchange(true)) { diagnosticLogger?.LogWarning("{0}.{1} was already executed.", nameof(SentryDatabaseLogging), nameof(UseBreadcrumbs)); diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs index 23f549e0d0..43cb0f17a7 100644 --- a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs +++ b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs @@ -89,6 +89,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except }; log.SetDefaultAttributes(_options, _sdk); + log.SetOrigin("auto.log.extensions_logging"); if (_categoryName is not null) { diff --git a/src/Sentry.Profiling/SampleProfilerSession.cs b/src/Sentry.Profiling/SampleProfilerSession.cs index 6cd9a9242a..f3de9c2186 100644 --- a/src/Sentry.Profiling/SampleProfilerSession.cs +++ b/src/Sentry.Profiling/SampleProfilerSession.cs @@ -56,19 +56,12 @@ private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession sessio public TraceLog TraceLog => EventSource.TraceLog; - // default is false, set 1 for true. - private static int _throwOnNextStartupForTests = 0; + private static InterlockedBoolean _throwOnNextStartupForTests = false; internal static bool ThrowOnNextStartupForTests { - get { return Interlocked.CompareExchange(ref _throwOnNextStartupForTests, 1, 1) == 1; } - set - { - if (value) - Interlocked.CompareExchange(ref _throwOnNextStartupForTests, 1, 0); - else - Interlocked.CompareExchange(ref _throwOnNextStartupForTests, 0, 1); - } + get { return _throwOnNextStartupForTests; } + set { _throwOnNextStartupForTests.Exchange(value); } } public static SampleProfilerSession StartNew(IDiagnosticLogger? logger = null) @@ -77,7 +70,7 @@ public static SampleProfilerSession StartNew(IDiagnosticLogger? logger = null) { var client = new DiagnosticsClient(Environment.ProcessId); - if (Interlocked.CompareExchange(ref _throwOnNextStartupForTests, 0, 1) == 1) + if (_throwOnNextStartupForTests.CompareExchange(false, true) == true) { throw new Exception("Test exception"); } diff --git a/src/Sentry.Profiling/SamplingTransactionProfilerFactory.cs b/src/Sentry.Profiling/SamplingTransactionProfilerFactory.cs index 5f0d54453d..1d3d03c5f3 100644 --- a/src/Sentry.Profiling/SamplingTransactionProfilerFactory.cs +++ b/src/Sentry.Profiling/SamplingTransactionProfilerFactory.cs @@ -6,14 +6,11 @@ namespace Sentry.Profiling; internal class SamplingTransactionProfilerFactory : IDisposable, ITransactionProfilerFactory { // We only allow a single profile so let's keep track of the current status. - internal int _inProgress = FALSE; + internal InterlockedBoolean _inProgress = false; // Whether the session startup took longer than the given timeout. internal bool StartupTimedOut { get; } - private const int TRUE = 1; - private const int FALSE = 0; - // Stop profiling after the given number of milliseconds. private const int TIME_LIMIT_MS = 30_000; @@ -50,12 +47,12 @@ public SamplingTransactionProfilerFactory(SentryOptions options, TimeSpan startu public ITransactionProfiler? Start(ITransactionTracer _, CancellationToken cancellationToken) { // Start a profiler if one wasn't running yet. - if (!_errorLogged && Interlocked.Exchange(ref _inProgress, TRUE) == FALSE) + if (!_errorLogged && !_inProgress.Exchange(true)) { if (!_sessionTask.IsCompleted) { _options.LogWarning("Cannot start a sampling profiler, the session hasn't started yet."); - _inProgress = FALSE; + _inProgress = false; return null; } @@ -63,7 +60,7 @@ public SamplingTransactionProfilerFactory(SentryOptions options, TimeSpan startu { _options.LogWarning("Cannot start a sampling profiler because the session startup has failed. This is a permanent error and no future transactions will be sampled."); _errorLogged = true; - _inProgress = FALSE; + _inProgress = false; return null; } @@ -72,13 +69,13 @@ public SamplingTransactionProfilerFactory(SentryOptions options, TimeSpan startu { return new SamplingTransactionProfiler(_options, _sessionTask.Result, TIME_LIMIT_MS, cancellationToken) { - OnFinish = () => _inProgress = FALSE + OnFinish = () => _inProgress = false }; } catch (Exception e) { _options.LogError(e, "Failed to start a profiler session."); - _inProgress = FALSE; + _inProgress = false; } } return null; diff --git a/src/Sentry.Serilog/SentrySink.Structured.cs b/src/Sentry.Serilog/SentrySink.Structured.cs index 5b22ab7d97..41e3f95693 100644 --- a/src/Sentry.Serilog/SentrySink.Structured.cs +++ b/src/Sentry.Serilog/SentrySink.Structured.cs @@ -18,6 +18,7 @@ private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEve }; log.SetDefaultAttributes(options, Sdk); + log.SetOrigin("auto.log.serilog"); foreach (var attribute in attributes) { diff --git a/src/Sentry/Ben.BlockingDetector/DetectBlockingSynchronizationContext.cs b/src/Sentry/Ben.BlockingDetector/DetectBlockingSynchronizationContext.cs index 2e4aeb08ca..701465b5d3 100644 --- a/src/Sentry/Ben.BlockingDetector/DetectBlockingSynchronizationContext.cs +++ b/src/Sentry/Ben.BlockingDetector/DetectBlockingSynchronizationContext.cs @@ -10,8 +10,8 @@ internal sealed class DetectBlockingSynchronizationContext : SynchronizationCont internal int _isSuppressed; - internal void Suppress() => Interlocked.Exchange(ref _isSuppressed, _isSuppressed + 1); - internal void Restore() => Interlocked.Exchange(ref _isSuppressed, _isSuppressed - 1); + internal void Suppress() => Interlocked.Increment(ref _isSuppressed); + internal void Restore() => Interlocked.Decrement(ref _isSuppressed); public DetectBlockingSynchronizationContext(IBlockingMonitor monitor) { diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 2a3b934774..ef7e8bf9e3 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -24,15 +24,16 @@ internal class Hub : IHub, IDisposable private readonly MemoryMonitor? _memoryMonitor; #endif - private int _isPersistedSessionRecovered; + private InterlockedBoolean _isPersistedSessionRecovered; // Internal for testability internal ConditionalWeakTable ExceptionToSpanMap { get; } = new(); internal IInternalScopeManager ScopeManager { get; } - private int _isEnabled = 1; - public bool IsEnabled => _isEnabled == 1; + private InterlockedBoolean _isEnabled = true; + + public bool IsEnabled => _isEnabled; internal SentryOptions Options => _options; @@ -356,7 +357,7 @@ public TransactionContext ContinueTrace( public void StartSession() { // Attempt to recover persisted session left over from previous run - if (Interlocked.Exchange(ref _isPersistedSessionRecovered, 1) != 1) + if (_isPersistedSessionRecovered.Exchange(true) != true) { try { @@ -835,7 +836,7 @@ public void Dispose() { _options.LogInfo("Disposing the Hub."); - if (Interlocked.Exchange(ref _isEnabled, 0) != 1) + if (!_isEnabled.Exchange(false)) { return; } diff --git a/src/Sentry/Internal/InterlockedBoolean.cs b/src/Sentry/Internal/InterlockedBoolean.cs new file mode 100644 index 0000000000..61e7f0968d --- /dev/null +++ b/src/Sentry/Internal/InterlockedBoolean.cs @@ -0,0 +1,54 @@ +#if NET9_0_OR_GREATER +using TBool = System.Boolean; +#else +using TBool = System.Int32; +#endif + +namespace Sentry.Internal; + +internal struct InterlockedBoolean +{ + private volatile TBool _value; + + [Browsable(false)] + internal TBool ValueForTests => _value; + +#if NET9_0_OR_GREATER + private const TBool True = true; + private const TBool False = false; +#else + private const TBool True = 1; + private const TBool False = 0; +#endif + + public InterlockedBoolean() { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public InterlockedBoolean(bool value) { _value = value ? True : False; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(InterlockedBoolean @this) => (@this._value != False); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator InterlockedBoolean(bool @this) => new InterlockedBoolean(@this); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Exchange(bool newValue) + { + TBool localNewValue = newValue ? True : False; + + TBool localReturnValue = Interlocked.Exchange(ref _value, localNewValue); + + return (localReturnValue != False); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CompareExchange(bool value, bool comparand) + { + TBool localValue = value ? True : False; + TBool localComparand = comparand ? True : False; + + TBool localReturnValue = Interlocked.CompareExchange(ref _value, localValue, localComparand); + + return (localReturnValue != False); + } +} diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index 2808038543..dbbeb28b4f 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -113,13 +113,13 @@ <_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) - - - - - - - + + + + + + + diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 844d71a778..b3f7b78f5c 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -184,6 +184,11 @@ internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) } } + internal void SetOrigin(string origin) + { + SetAttribute("sentry.origin", origin); + } + internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); diff --git a/src/Sentry/Threading/ScopedCountdownLock.cs b/src/Sentry/Threading/ScopedCountdownLock.cs index f3d992f893..1cf38678fb 100644 --- a/src/Sentry/Threading/ScopedCountdownLock.cs +++ b/src/Sentry/Threading/ScopedCountdownLock.cs @@ -1,3 +1,5 @@ +using Sentry.Internal; + namespace Sentry.Threading; /// @@ -13,12 +15,13 @@ namespace Sentry.Threading; internal sealed class ScopedCountdownLock : IDisposable { private readonly CountdownEvent _event; - private volatile int _isEngaged; + + private InterlockedBoolean _isEngaged; internal ScopedCountdownLock() { _event = new CountdownEvent(1); - _isEngaged = 0; + _isEngaged = false; } /// @@ -31,13 +34,13 @@ internal ScopedCountdownLock() /// Gets the number of remaining required to exit in order to set/signal the event while a is active. /// When and while a is active, no more can be entered. /// - internal int Count => _isEngaged == 1 ? _event.CurrentCount : _event.CurrentCount - 1; + internal int Count => _isEngaged ? _event.CurrentCount : _event.CurrentCount - 1; /// /// Returns when a is active and the event can be set/signaled by reaching . /// Returns when the can only reach the initial count of when no is active any longer. /// - internal bool IsEngaged => _isEngaged == 1; + internal bool IsEngaged => _isEngaged; /// /// No will be entered when the has reached , or while the lock is engaged via an active . @@ -79,7 +82,7 @@ private void ExitCounterScope() /// internal LockScope TryEnterLockScope() { - if (Interlocked.CompareExchange(ref _isEngaged, 1, 0) == 0) + if (_isEngaged.CompareExchange(true, false) == false) { Debug.Assert(_event.CurrentCount >= 1); _ = _event.Signal(); // decrement the initial count of 1, so that the event can be set with the count reaching 0 when all entered 'CounterScope' instances have exited @@ -94,7 +97,7 @@ private void ExitLockScope() Debug.Assert(_event.IsSet); _event.Reset(); // reset the signaled event to the initial count of 1, so that new 'CounterScope' instances can be entered again - if (Interlocked.CompareExchange(ref _isEngaged, 0, 1) != 1) + if (_isEngaged.CompareExchange(false, true) != true) { Debug.Fail("The Lock should have not been disengaged without being engaged first."); } diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index bdd829ff81..d3f1de5a44 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -12,9 +12,10 @@ public class TransactionTracer : IBaseTracer, ITransactionTracer private readonly IHub _hub; private readonly SentryOptions? _options; private readonly Timer? _idleTimer; - private long _cancelIdleTimeout; private readonly SentryStopwatch _stopwatch = SentryStopwatch.StartNew(); + private InterlockedBoolean _cancelIdleTimeout; + private readonly Instrumenter _instrumenter = Instrumenter.Sentry; bool IBaseTracer.IsOtelInstrumenter => _instrumenter == Instrumenter.OpenTelemetry; @@ -247,7 +248,7 @@ internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idle // Set idle timer only if an idle timeout has been provided directly if (idleTimeout.HasValue) { - _cancelIdleTimeout = 1; // Timer will be cancelled once, atomically setting this back to 0 + _cancelIdleTimeout = true; // Timer will be cancelled once, atomically setting this back to false _idleTimer = new Timer(state => { if (state is not TransactionTracer transactionTracer) @@ -362,7 +363,7 @@ public void Clear() public void Finish() { _options?.LogDebug("Attempting to finish Transaction '{0}'.", SpanId); - if (Interlocked.Exchange(ref _cancelIdleTimeout, 0) == 1) + if (_cancelIdleTimeout.Exchange(false) == true) { _options?.LogDebug("Disposing of idle timer for Transaction '{0}'.", SpanId); _idleTimer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); diff --git a/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj b/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj index e93320ccdf..fba06619e4 100644 --- a/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj +++ b/test/Sentry.AspNetCore.TestUtils/Sentry.AspNetCore.TestUtils.csproj @@ -36,8 +36,8 @@ - - + + diff --git a/test/Sentry.AspNetCore.Tests/SentryAspNetCoreStructuredLoggerProviderTests.cs b/test/Sentry.AspNetCore.Tests/SentryAspNetCoreStructuredLoggerProviderTests.cs index 797aedf435..d19cdf5cc4 100644 --- a/test/Sentry.AspNetCore.Tests/SentryAspNetCoreStructuredLoggerProviderTests.cs +++ b/test/Sentry.AspNetCore.Tests/SentryAspNetCoreStructuredLoggerProviderTests.cs @@ -91,6 +91,9 @@ public void CreateLogger_DependencyInjection_CanLog() capturedLog.TryGetAttribute("sentry.sdk.version", out object? version).Should().BeTrue(); version.Should().Be(SentryMiddleware.NameAndVersion.Version); + + capturedLog.TryGetAttribute("sentry.origin", out object? origin).Should().BeTrue(); + origin.Should().Be("auto.log.extensions_logging"); } [Fact] diff --git a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerProviderTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerProviderTests.cs index cb48224c51..a8c52f13da 100644 --- a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerProviderTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerProviderTests.cs @@ -91,6 +91,9 @@ public void CreateLogger_DependencyInjection_CanLog() capturedLog.TryGetAttribute("sentry.sdk.version", out object? version).Should().BeTrue(); version.Should().Be(SentryLoggerProvider.NameAndVersion.Version); + + capturedLog.TryGetAttribute("sentry.origin", out object? origin).Should().BeTrue(); + origin.Should().Be("auto.log.extensions_logging"); } [Fact] diff --git a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs index f810fd9d14..dbede60ff9 100644 --- a/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/SentryStructuredLoggerTests.cs @@ -111,6 +111,7 @@ public void Log_LogLevel_CaptureLog(LogLevel logLevel, SentryLogLevel expectedLe log.ParentSpanId.Should().Be(parentSpanId); log.AssertAttribute("sentry.environment", "my-environment"); log.AssertAttribute("sentry.release", "my-release"); + log.AssertAttribute("sentry.origin", "auto.log.extensions_logging"); log.AssertAttribute("sentry.sdk.name", "SDK Name"); log.AssertAttribute("sentry.sdk.version", "SDK Version"); log.AssertAttribute("category.name", _fixture.CategoryName); diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs index 7b98e8181c..dd1f44928d 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.Structured.cs @@ -118,6 +118,8 @@ public void Emit_StructuredLogging_LogEvent(bool withActiveSpan) environment.Should().Be("test-environment"); log.TryGetAttribute("sentry.release", out object? release).Should().BeTrue(); release.Should().Be("test-release"); + log.TryGetAttribute("sentry.origin", out object? origin).Should().BeTrue(); + origin.Should().Be("auto.log.serilog"); 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(); diff --git a/test/Sentry.Tests/Internals/AgggregateExceptionTests.cs b/test/Sentry.Tests/Internals/AggregateExceptionTests.cs similarity index 97% rename from test/Sentry.Tests/Internals/AgggregateExceptionTests.cs rename to test/Sentry.Tests/Internals/AggregateExceptionTests.cs index 49f9468d9f..20b735aeae 100644 --- a/test/Sentry.Tests/Internals/AgggregateExceptionTests.cs +++ b/test/Sentry.Tests/Internals/AggregateExceptionTests.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests.Internals; -public class AgggregateExceptionTests +public class AggregateExceptionTests { private static readonly string DefaultAggregateExceptionMessage = new AggregateException().Message; diff --git a/test/Sentry.Tests/Internals/InterlockedBooleanTests.cs b/test/Sentry.Tests/Internals/InterlockedBooleanTests.cs new file mode 100644 index 0000000000..49c967a30b --- /dev/null +++ b/test/Sentry.Tests/Internals/InterlockedBooleanTests.cs @@ -0,0 +1,152 @@ +#if NET9_0_OR_GREATER +using TBool = System.Boolean; +#else +using TBool = System.Int32; +#endif + +namespace Sentry.Tests.Internals; + +public class InterlockedBooleanTests +{ +#if NET9_0_OR_GREATER + private const TBool True = true; + private const TBool False = false; +#else + private const TBool True = 1; + private const TBool False = 0; +#endif + + private TBool ToTBool(bool value) => value ? True : False; + private bool FromTBool(TBool value) => (value != False); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void InterlockedBoolean_Constructor_ConstructsExpected(bool value) + { + // Arrange + var expected = ToTBool(value); + + // Act + var actual = new InterlockedBoolean(value); + + // Assert + actual.ValueForTests.Should().Be(expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void InterlockedBoolean_ImplicitToBool_ReturnsExpected(bool value) + { + // Arrange + var sut = new InterlockedBoolean(value); + var expected = value; + + // Act + bool actual = sut; + + // Assert + actual.Should().Be(expected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void InterlockedBoolean_ImplicitFromBool_ReturnsExpected(bool value) + { + // Arrange + var expected = ToTBool(value); + + // Act + InterlockedBoolean actual = value; + + // Assert + actual.ValueForTests.Should().Be(expected); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void InterlockedBoolean_Exchange_ReturnsExpected(bool initialState, bool newValue) + { + // Arrange + var sut = new InterlockedBoolean(initialState); + var expected = initialState; + + // Act + var result = sut.Exchange(newValue); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void InterlockedBoolean_Exchange_SetsExpectedNewState(bool initialState, bool newValue) + { + // Arrange + var sut = new InterlockedBoolean(initialState); + + var expected = ToTBool(newValue); + + // Act + var _ = sut.Exchange(newValue); + + // Assert + sut.ValueForTests.Should().Be(expected); + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public void InterlockedBoolean_CompareExchange_ReturnsExpected(bool initialState, bool comparand, bool newValue) + { + // Arrange + var sut = new InterlockedBoolean(initialState); + var expected = initialState; + + // Act + var result = sut.CompareExchange(newValue, comparand); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public void InterlockedBoolean_CompareExchange_SetsExpectedNewState(bool initialState, bool comparand, bool newValue) + { + // Arrange + var sut = new InterlockedBoolean(initialState); + + var expected = ToTBool( + initialState == comparand + ? newValue + : initialState); + + // Act + sut.CompareExchange(newValue, comparand); + + // Assert + sut.ValueForTests.Should().Be(expected); + } +} diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index c53c6711de..505249b95e 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -53,6 +53,9 @@ public void Protocol_Default_VerifyAttributes() log.Parameters.Should().BeEquivalentTo(new KeyValuePair[] { new("param", "params"), }); log.ParentSpanId.Should().Be(ParentSpanId); + // should only show up in sdk integrations + log.TryGetAttribute("sentry.origin", out object origin).Should().BeFalse(); + log.TryGetAttribute("attribute", out object attribute).Should().BeTrue(); attribute.Should().Be("value"); log.TryGetAttribute("sentry.environment", out string environment).Should().BeTrue(); From 9d1b4fe1aed38757ffead83eb390b8daece8725b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 17 Oct 2025 09:56:42 +1300 Subject: [PATCH 16/23] Removed obsolete v6 APIs (#4619) --- CHANGELOG.md | 3 + src/Sentry.Maui/BreadcrumbEvent.cs | 16 ---- src/Sentry/Extensibility/DisabledHub.cs | 8 -- src/Sentry/Extensibility/HubAdapter.cs | 9 --- src/Sentry/ISentryClient.cs | 7 -- src/Sentry/Internal/Hub.cs | 28 ------- src/Sentry/Protocol/Envelopes/Envelope.cs | 17 ----- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 26 ------- src/Sentry/SentryClient.cs | 14 ---- src/Sentry/SentryClientExtensions.cs | 23 +----- src/Sentry/SentrySdk.cs | 21 ------ src/Sentry/UserFeedback.cs | 68 ----------------- ...iApprovalTests.Run.DotNet10_0.verified.txt | 4 - ...piApprovalTests.Run.DotNet9_0.verified.txt | 4 - .../Sentry.Maui.Tests/BreadcrumbEventTests.cs | 28 ------- ...iApprovalTests.Run.DotNet10_0.verified.txt | 31 +------- ...piApprovalTests.Run.DotNet8_0.verified.txt | 31 +------- ...piApprovalTests.Run.DotNet9_0.verified.txt | 31 +------- .../ApiApprovalTests.Run.Net4_8.verified.txt | 31 +------- test/Sentry.Tests/HubTests.cs | 73 ------------------- .../Protocol/Envelopes/EnvelopeTests.cs | 26 ------- .../Protocol/UserFeedbackTests.cs | 36 --------- .../SentryClientExtensionsTests.cs | 21 ------ test/Sentry.Tests/SentryClientTests.cs | 57 --------------- 24 files changed, 9 insertions(+), 604 deletions(-) delete mode 100644 src/Sentry/UserFeedback.cs delete mode 100644 test/Sentry.Maui.Tests/BreadcrumbEventTests.cs delete mode 100644 test/Sentry.Tests/Protocol/UserFeedbackTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e28ff1e0ab..c35e28676d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - `BreadcrumbLevel.Critical` has been renamed to `BreadcrumbLevel.Fatal` for consistency with the other Sentry SDKs ([#4605](https://github.com/getsentry/sentry-dotnet/pull/4605)) - SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606)) - Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602)) +- Removed obsolete APIs ([#4619](https://github.com/getsentry/sentry-dotnet/pull/4619)) + - Removed the unusual constructor from `Sentry.Maui.BreadcrumbEvent` that had been marked as obsolete. That constructor expected a `IEnumerable<(string Key, string Value)>[]` argument (i.e. an array of IEnumerable of tuples). If you were using this constructor, you should instead use the alternate constructor that expects just an IEnumerable of tuples: `IEnumerable<(string Key, string Value)>`. + - Removed `SentrySdk.CaptureUserFeedback` and all associated members. Use the newer `SentrySdk.CaptureFeedback` instead. - Backpressure handling is now enabled by default, meaning that the SDK will monitor system health and reduce the sampling rate of events and transactions when the system is under load. When the system is determined to be healthy again, the sampling rates are returned to their original levels. ([#4615](https://github.com/getsentry/sentry-dotnet/pull/4615)) - ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611)) diff --git a/src/Sentry.Maui/BreadcrumbEvent.cs b/src/Sentry.Maui/BreadcrumbEvent.cs index 48a9252901..3c3797d255 100644 --- a/src/Sentry.Maui/BreadcrumbEvent.cs +++ b/src/Sentry.Maui/BreadcrumbEvent.cs @@ -52,20 +52,4 @@ public BreadcrumbEvent( e => new KeyValuePair(e.key, e.value))) { } - - /// - /// This constructor remains for backward compatibility. - /// - /// - /// - /// - [Obsolete("Use one of the other simpler constructors instead.")] - public BreadcrumbEvent( - object? sender, - string eventName, - IEnumerable<(string Key, string Value)>[] extraData) : this(sender, eventName, extraData.SelectMany( - x => x.Select(pair => new KeyValuePair(pair.Key, pair.Value))) - ) - { - } } diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index e835a0edfc..30d2eefffa 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -242,14 +242,6 @@ public void Dispose() { } - /// - /// No-Op. - /// - [Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(UserFeedback userFeedback) - { - } - /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index e94a6c1914..25eb86800c 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -335,13 +335,4 @@ public SentryId CaptureCheckIn( [EditorBrowsable(EditorBrowsableState.Never)] public Task FlushAsync(TimeSpan timeout) => SentrySdk.FlushAsync(timeout); - - /// - /// Forwards the call to - /// - [DebuggerStepThrough] - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(UserFeedback sentryUserFeedback) - => SentrySdk.CaptureUserFeedback(sentryUserFeedback); } diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index 8229d57780..ae8f93dc0c 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -36,13 +36,6 @@ public interface ISentryClient /// An optional hint providing high level context for the source of the event public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null); - /// - /// Captures a user feedback. - /// - /// The user feedback to send to Sentry. - [Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(UserFeedback userFeedback); - /// /// Captures a transaction. /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index ef7e8bf9e3..2bc02c2185 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -662,34 +662,6 @@ internal void CaptureHeapDump(string dumpFile) } #endif - [Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(UserFeedback userFeedback) - { - if (!IsEnabled) - { - return; - } - - try - { - if (!string.IsNullOrWhiteSpace(userFeedback.Email) && !EmailValidator.IsValidEmail(userFeedback.Email)) - { - _options.LogWarning("Feedback email scrubbed due to invalid email format: '{0}'", userFeedback.Email); - userFeedback = new UserFeedback( - userFeedback.EventId, - userFeedback.Name, - null, // Scrubbed email - userFeedback.Comments); - } - - CurrentClient.CaptureUserFeedback(userFeedback); - } - catch (Exception e) - { - _options.LogError(e, "Failure to capture user feedback: {0}", userFeedback.EventId); - } - } - public void CaptureTransaction(SentryTransaction transaction) => CaptureTransaction(transaction, null, null); public void CaptureTransaction(SentryTransaction transaction, Scope? scope, SentryHint? hint) diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 45193ea097..e9b06cff00 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -330,23 +330,6 @@ public static Envelope FromFeedback( return new Envelope(eventId, header, items); } - /// - /// Creates an envelope that contains a single user feedback. - /// - [Obsolete("Use FromFeedback instead.")] - public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback) - { - var eventId = sentryUserFeedback.EventId; - var header = CreateHeader(eventId); - - var items = new[] - { - EnvelopeItem.FromUserFeedback(sentryUserFeedback) - }; - - return new Envelope(eventId, header, items); - } - /// /// Creates an envelope that contains a single transaction. /// diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 5409ebc0a6..976c9a1305 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -231,20 +231,6 @@ public static EnvelopeItem FromFeedback(SentryEvent @event) return new EnvelopeItem(header, new JsonSerializable(@event)); } - /// - /// Creates an from . - /// - [Obsolete("Use FromFeedback instead.")] - public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback) - { - var header = new Dictionary(1, StringComparer.Ordinal) - { - [TypeKey] = TypeValueUserReport - }; - - return new EnvelopeItem(header, new JsonSerializable(sentryUserFeedback)); - } - /// /// Creates an from . /// @@ -417,18 +403,6 @@ private static async Task DeserializePayloadAsync( return new JsonSerializable(sentryEvent); } - // User report - if (string.Equals(payloadType, TypeValueUserReport, StringComparison.OrdinalIgnoreCase)) - { -#pragma warning disable CS0618 // Type or member is obsolete - var bufferLength = (int)(payloadLength ?? stream.Length); - var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); - var userFeedback = Json.Parse(buffer, UserFeedback.FromJson); -#pragma warning restore CS0618 // Type or member is obsolete - - return new JsonSerializable(userFeedback); - } - // Transaction if (string.Equals(payloadType, TypeValueTransaction, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 1467ee4364..d42fe8c44f 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -119,20 +119,6 @@ public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, Sentry CaptureEnvelope(envelope); } - /// - [Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(UserFeedback userFeedback) - { - if (userFeedback.EventId.Equals(SentryId.Empty)) - { - // Ignore the user feedback if EventId is empty - _options.LogWarning("User feedback dropped due to empty id."); - return; - } - - CaptureEnvelope(Envelope.FromUserFeedback(userFeedback)); - } - /// public void CaptureTransaction(SentryTransaction transaction) => CaptureTransaction(transaction, null, null); diff --git a/src/Sentry/SentryClientExtensions.cs b/src/Sentry/SentryClientExtensions.cs index 73d394f205..fc0b120b8e 100644 --- a/src/Sentry/SentryClientExtensions.cs +++ b/src/Sentry/SentryClientExtensions.cs @@ -49,26 +49,6 @@ public static void CaptureFeedback(this ISentryClient client, string message, st => client.CaptureFeedback(new SentryFeedback(message, contactEmail, name, replayId, url, associatedEventId), scope, hint); - /// - /// Captures a user feedback. - /// - /// - /// The event Id. - /// The user email. - /// The user comments. - /// The optional username. - [Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(this ISentryClient client, SentryId eventId, string email, string comments, - string? name = null) - { - if (!client.IsEnabled) - { - return; - } - - client.CaptureUserFeedback(new UserFeedback(eventId, name, email, comments)); - } - /// /// Flushes the queue of captured events until the timeout set in /// is reached. @@ -125,7 +105,8 @@ public static Task FlushAsync(this ISentryClient client) /// /// /// - [Obsolete("This method is meant for external usage only")] + [Obsolete("WARNING: This method is meant for internal usage only")] + [EditorBrowsable(EditorBrowsableState.Never)] public static SentryOptions? GetInternalSentryOptions(this ISentryClient clientOrHub) => clientOrHub.GetSentryOptions(); } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index a8588370b6..982dbb6c3a 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -566,27 +566,6 @@ public static void CaptureFeedback(string message, string? contactEmail = null, => CurrentHub.CaptureFeedback(new SentryFeedback(message, contactEmail, name, replayId, url, associatedEventId), scope, hint); - /// - /// Captures a user feedback. - /// - /// The user feedback to send to Sentry. - [DebuggerStepThrough] - [Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(UserFeedback userFeedback) - => CurrentHub.CaptureUserFeedback(userFeedback); - - /// - /// Captures a user feedback. - /// - /// The event Id. - /// The user email. - /// The user comments. - /// The optional username. - [DebuggerStepThrough] - [Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null) - => CurrentHub.CaptureUserFeedback(new UserFeedback(eventId, name, email, comments)); - /// /// Captures a transaction. /// diff --git a/src/Sentry/UserFeedback.cs b/src/Sentry/UserFeedback.cs deleted file mode 100644 index 0094a6ade4..0000000000 --- a/src/Sentry/UserFeedback.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Sentry.Extensibility; -using Sentry.Internal.Extensions; - -namespace Sentry; - -/// -/// Sentry User Feedback. -/// -[Obsolete("Use SentryFeedback instead.")] -public sealed class UserFeedback : ISentryJsonSerializable -{ - /// - /// The eventId of the event to which the user feedback is associated. - /// - public SentryId EventId { get; } - - /// - /// The name of the user. - /// - public string? Name { get; } - - /// - /// The name of the user. - /// - public string? Email { get; } - - /// - /// Comments of the user about what happened. - /// - public string? Comments { get; } - - /// - /// Initializes an instance of . - /// - public UserFeedback(SentryId eventId, string? name, string? email, string? comments) - { - EventId = eventId; - Name = name; - Email = email; - Comments = comments; - } - - /// - public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) - { - writer.WriteStartObject(); - - writer.WriteSerializable("event_id", EventId, logger); - writer.WriteStringIfNotWhiteSpace("name", Name); - writer.WriteStringIfNotWhiteSpace("email", Email); - writer.WriteStringIfNotWhiteSpace("comments", Comments); - - writer.WriteEndObject(); - } - - /// - /// Parses from JSON. - /// - public static UserFeedback FromJson(JsonElement json) - { - var eventId = json.GetPropertyOrNull("event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var name = json.GetPropertyOrNull("name")?.GetString(); - var email = json.GetPropertyOrNull("email")?.GetString(); - var comments = json.GetPropertyOrNull("comments")?.GetString(); - - return new UserFeedback(eventId, name, email, comments); - } -} diff --git a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 244d13725f..db0645d89c 100644 --- a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -25,10 +25,6 @@ namespace Sentry.Maui public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.ParamCollection] [System.Runtime.CompilerServices.TupleElementNames(new string[] { "key", "value"})] System.Collections.Generic.IEnumerable> extraData) { } - [System.Obsolete("Use one of the other simpler constructors instead.")] - public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { - "Key", - "Value"})] System.Collections.Generic.IEnumerable>[] extraData) { } public string EventName { get; } public System.Collections.Generic.IEnumerable> ExtraData { get; } public object? Sender { get; } diff --git a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 12ae9dcc31..f2790100c2 100644 --- a/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Maui.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -18,10 +18,6 @@ namespace Sentry.Maui public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.ParamCollection] [System.Runtime.CompilerServices.TupleElementNames(new string[] { "key", "value"})] System.Collections.Generic.IEnumerable> extraData) { } - [System.Obsolete("Use one of the other simpler constructors instead.")] - public BreadcrumbEvent(object? sender, string eventName, [System.Runtime.CompilerServices.TupleElementNames(new string[] { - "Key", - "Value"})] System.Collections.Generic.IEnumerable>[] extraData) { } public string EventName { get; } public System.Collections.Generic.IEnumerable> ExtraData { get; } public object? Sender { get; } diff --git a/test/Sentry.Maui.Tests/BreadcrumbEventTests.cs b/test/Sentry.Maui.Tests/BreadcrumbEventTests.cs deleted file mode 100644 index c92938adde..0000000000 --- a/test/Sentry.Maui.Tests/BreadcrumbEventTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using FluentAssertions; -using Xunit; - -namespace Sentry.Maui.Tests; - -public class BreadcrumbEventTests -{ - [Fact] - public void BreadcrumbEvent_OldConstructor_EquivalentToNewConstructor() - { - // Arrange - var sender = new object(); - var eventName = "TestEvent"; - - // Act - IEnumerable<(string Key, string Value)>[] extraData = [[("key1", "value1")], [("key2", "value2")]]; -#pragma warning disable CS0618 // Type or member is obsolete - var oldEvent = new BreadcrumbEvent(sender, eventName, extraData); -#pragma warning restore CS0618 // Type or member is obsolete - var newEvent = new BreadcrumbEvent(sender, eventName, ("key1", "value1"), ("key2", "value2")); - - // Assert - oldEvent.Sender.Should().Be(newEvent.Sender); - oldEvent.EventName.Should().Be(newEvent.EventName); - oldEvent.ExtraData.Should().BeEquivalentTo(newEvent.ExtraData); - } -} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 9a2f11bf6a..b955fd5d7f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -222,8 +222,6 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); - [System.Obsolete("Use CaptureFeedback instead.")] - void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } public interface ISentryJsonSerializable @@ -452,8 +450,6 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } @@ -462,12 +458,10 @@ namespace Sentry public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static void CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } public static System.Threading.Tasks.Task FlushAsync(this Sentry.ISentryClient client) { } - [System.Obsolete("This method is meant for external usage only")] + [System.Obsolete("WARNING: This method is meant for internal usage only")] public static Sentry.SentryOptions? GetInternalSentryOptions(this Sentry.ISentryClient clientOrHub) { } } public static class SentryConstants @@ -852,10 +846,6 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] public static void CauseCrash(Sentry.CrashType crashType) { } @@ -1289,17 +1279,6 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } - [System.Obsolete("Use SentryFeedback instead.")] - public sealed class UserFeedback : Sentry.ISentryJsonSerializable - { - public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } - public string? Comments { get; } - public string? Email { get; } - public Sentry.SentryId EventId { get; } - public string? Name { get; } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.UserFeedback FromJson(System.Text.Json.JsonElement json) { } - } public sealed class ViewHierarchy : Sentry.ISentryJsonSerializable { public ViewHierarchy(string renderingSystem) { } @@ -1393,8 +1372,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1445,8 +1422,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1900,8 +1875,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable { @@ -1921,8 +1894,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 9a2f11bf6a..b955fd5d7f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -222,8 +222,6 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); - [System.Obsolete("Use CaptureFeedback instead.")] - void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } public interface ISentryJsonSerializable @@ -452,8 +450,6 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } @@ -462,12 +458,10 @@ namespace Sentry public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static void CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } public static System.Threading.Tasks.Task FlushAsync(this Sentry.ISentryClient client) { } - [System.Obsolete("This method is meant for external usage only")] + [System.Obsolete("WARNING: This method is meant for internal usage only")] public static Sentry.SentryOptions? GetInternalSentryOptions(this Sentry.ISentryClient clientOrHub) { } } public static class SentryConstants @@ -852,10 +846,6 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] public static void CauseCrash(Sentry.CrashType crashType) { } @@ -1289,17 +1279,6 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } - [System.Obsolete("Use SentryFeedback instead.")] - public sealed class UserFeedback : Sentry.ISentryJsonSerializable - { - public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } - public string? Comments { get; } - public string? Email { get; } - public Sentry.SentryId EventId { get; } - public string? Name { get; } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.UserFeedback FromJson(System.Text.Json.JsonElement json) { } - } public sealed class ViewHierarchy : Sentry.ISentryJsonSerializable { public ViewHierarchy(string renderingSystem) { } @@ -1393,8 +1372,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1445,8 +1422,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1900,8 +1875,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable { @@ -1921,8 +1894,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 9a2f11bf6a..b955fd5d7f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -222,8 +222,6 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); - [System.Obsolete("Use CaptureFeedback instead.")] - void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } public interface ISentryJsonSerializable @@ -452,8 +450,6 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } @@ -462,12 +458,10 @@ namespace Sentry public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static void CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } public static System.Threading.Tasks.Task FlushAsync(this Sentry.ISentryClient client) { } - [System.Obsolete("This method is meant for external usage only")] + [System.Obsolete("WARNING: This method is meant for internal usage only")] public static Sentry.SentryOptions? GetInternalSentryOptions(this Sentry.ISentryClient clientOrHub) { } } public static class SentryConstants @@ -852,10 +846,6 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] public static void CauseCrash(Sentry.CrashType crashType) { } @@ -1289,17 +1279,6 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } - [System.Obsolete("Use SentryFeedback instead.")] - public sealed class UserFeedback : Sentry.ISentryJsonSerializable - { - public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } - public string? Comments { get; } - public string? Email { get; } - public Sentry.SentryId EventId { get; } - public string? Name { get; } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.UserFeedback FromJson(System.Text.Json.JsonElement json) { } - } public sealed class ViewHierarchy : Sentry.ISentryJsonSerializable { public ViewHierarchy(string renderingSystem) { } @@ -1393,8 +1372,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1445,8 +1422,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1900,8 +1875,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable { @@ -1921,8 +1894,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index d508934005..4acd40d0ff 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -210,8 +210,6 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); - [System.Obsolete("Use CaptureFeedback instead.")] - void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } public interface ISentryJsonSerializable @@ -440,8 +438,6 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } @@ -450,12 +446,10 @@ namespace Sentry public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static void CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } public static System.Threading.Tasks.Task FlushAsync(this Sentry.ISentryClient client) { } - [System.Obsolete("This method is meant for external usage only")] + [System.Obsolete("WARNING: This method is meant for internal usage only")] public static Sentry.SentryOptions? GetInternalSentryOptions(this Sentry.ISentryClient clientOrHub) { } } public static class SentryConstants @@ -828,10 +822,6 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] public static void CauseCrash(Sentry.CrashType crashType) { } @@ -1265,17 +1255,6 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } - [System.Obsolete("Use SentryFeedback instead.")] - public sealed class UserFeedback : Sentry.ISentryJsonSerializable - { - public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } - public string? Comments { get; } - public string? Email { get; } - public Sentry.SentryId EventId { get; } - public string? Name { get; } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.UserFeedback FromJson(System.Text.Json.JsonElement json) { } - } public sealed class ViewHierarchy : Sentry.ISentryJsonSerializable { public ViewHierarchy(string renderingSystem) { } @@ -1369,8 +1348,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1421,8 +1398,6 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } - [System.Obsolete("Use CaptureFeedback instead.")] - public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1871,8 +1846,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable { @@ -1892,8 +1865,6 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } - [System.Obsolete("Use FromFeedback instead.")] - public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable { diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index a7a5fcaa8c..b1dc382b3e 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -2131,29 +2131,6 @@ public void CaptureAttachment_AttachmentNull_ReturnsFalse() _fixture.Client.DidNotReceive().CaptureEnvelope(Arg.Any()); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CaptureUserFeedback_HubEnabled(bool enabled) - { -#pragma warning disable CS0618 // Type or member is obsolete - // Arrange - var hub = _fixture.GetSut(); - if (!enabled) - { - hub.Dispose(); - } - - var feedback = new UserFeedback(SentryId.Create(), "foo", "bar@example.com", "baz"); - - // Act - hub.CaptureUserFeedback(feedback); - - // Assert - _fixture.Client.Received(enabled ? 1 : 0).CaptureUserFeedback(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - [Theory] [InlineData(true)] [InlineData(false)] @@ -2340,56 +2317,6 @@ public void CaptureFeedback_InvalidEmail_FeedbackDropped(string email) Arg.Any(), Arg.Any()); } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("test@example.com")] - [InlineData("user.name@domain.com")] - [InlineData("user+tag@example.com")] - public void CaptureUserFeedback_ValidEmail_FeedbackRegistered(string email) - { -#pragma warning disable CS0618 // Type or member is obsolete - // Arrange - var hub = _fixture.GetSut(); - var feedback = new UserFeedback(SentryId.Create(), "Test name", email, "Test comment"); - - // Act - hub.CaptureUserFeedback(feedback); - - // Assert - _fixture.Client.Received(1).CaptureUserFeedback(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Theory] - [InlineData("invalid-email")] - [InlineData("missing@domain")] - [InlineData("@missing-local.com")] - [InlineData("spaces in@email.com")] - public void CaptureUserFeedback_InvalidEmail_FeedbackDropped(string email) - { -#pragma warning disable CS0618 // Type or member is obsolete - // Arrange - _fixture.Options.Debug = true; - _fixture.Options.DiagnosticLogger = Substitute.For(); - _fixture.Options.DiagnosticLogger!.IsEnabled(Arg.Any()).Returns(true); - var hub = _fixture.GetSut(); - var feedback = new UserFeedback(SentryId.Create(), "Test name", email, "Test comment"); - - // Act - hub.CaptureUserFeedback(feedback); - - // Assert - _fixture.Options.DiagnosticLogger.Received(1).Log( - SentryLevel.Warning, - Arg.Is(s => s.Contains("invalid email format")), - null, - Arg.Any()); - _fixture.Client.Received(1).CaptureUserFeedback(Arg.Is(f => f.Email.IsNull())); -#pragma warning restore CS0618 // Type or member is obsolete - } - private class TestDisposableIntegration : ISdkIntegration, IDisposable { public int Registered { get; private set; } diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index 60f2fb98a4..8db1187afe 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -795,32 +795,6 @@ public async Task Roundtrip_WithEvent_WithSession_Success() .Which.Source.Should().BeEquivalentTo(sessionUpdate); } - [Fact] - public async Task Roundtrip_WithUserFeedback_Success() - { -#pragma warning disable CS0618 // Type or member is obsolete - // Arrange - var feedback = new UserFeedback( - SentryId.Create(), - "Someone Nice", - "foo@bar.com", - "Everything is great!"); - - using var envelope = Envelope.FromUserFeedback(feedback); - - using var stream = new MemoryStream(); - - // Act - await envelope.SerializeAsync(stream, _testOutputLogger); - stream.Seek(0, SeekOrigin.Begin); - - using var envelopeRoundtrip = await Envelope.DeserializeAsync(stream); - - // Assert - envelopeRoundtrip.Should().BeEquivalentTo(envelope); -#pragma warning restore CS0618 // Type or member is obsolete - } - [Fact] public async Task Roundtrip_WithFeedback_Success() { diff --git a/test/Sentry.Tests/Protocol/UserFeedbackTests.cs b/test/Sentry.Tests/Protocol/UserFeedbackTests.cs deleted file mode 100644 index 5358773067..0000000000 --- a/test/Sentry.Tests/Protocol/UserFeedbackTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Sentry.Tests.Protocol; - -public class UserFeedbackTests -{ - private readonly IDiagnosticLogger _testOutputLogger; - - public UserFeedbackTests(ITestOutputHelper output) - { - _testOutputLogger = new TestOutputDiagnosticLogger(output); - } - - [Fact] - public void Serialization_SentryUserFeedbacks_Success() - { -#pragma warning disable CS0618 // Type or member is obsolete - // Arrange - var eventId = new SentryId(Guid.Parse("acbe351c61494e7b807fd7e82a435ffc")); - var userFeedback = new UserFeedback(eventId, "myName", "myEmail@service.com", "my comment"); - using var stream = new MemoryStream(); - - // Act - var actual = userFeedback.ToJsonString(_testOutputLogger, indented: true); - - // Assert - Assert.Equal(""" - { - "event_id": "acbe351c61494e7b807fd7e82a435ffc", - "name": "myName", - "email": "myEmail@service.com", - "comments": "my comment" - } - """, - actual); -#pragma warning restore CS0618 // Type or member is obsolete - } -} diff --git a/test/Sentry.Tests/SentryClientExtensionsTests.cs b/test/Sentry.Tests/SentryClientExtensionsTests.cs index cb73d048ba..637e05078f 100644 --- a/test/Sentry.Tests/SentryClientExtensionsTests.cs +++ b/test/Sentry.Tests/SentryClientExtensionsTests.cs @@ -78,27 +78,6 @@ public void CaptureMessage_NullMessage_DoesNotCapturesEventWithMessage() Assert.Equal(default, id); } - [Fact] - public void CaptureUserFeedback_EnabledClient_CapturesUserFeedback() - { -#pragma warning disable CS0618 // Type or member is obsolete - _ = _sut.IsEnabled.Returns(true); - _sut.CaptureUserFeedback(Guid.Parse("1ec19311a7c048818de80b18dcc43eaa"), "email@email.com", "comments"); - _sut.Received(1).CaptureUserFeedback(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void CaptureUserFeedback_DisabledClient_DoesNotCaptureUserFeedback() - { -#pragma warning disable CS0618 // Type or member is obsolete - _ = _sut.IsEnabled.Returns(false); - _sut.CaptureUserFeedback(Guid.Parse("1ec19311a7c048818de80b18dcc43eea"), "email@email.com", "comments"); - - _sut.DidNotReceive().CaptureUserFeedback(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - [Fact] public async Task FlushAsync_NoTimeoutSpecified_UsesFlushTimeoutFromOptions() { diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index a2c3782638..2a2987b9ef 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -842,53 +842,6 @@ public void CaptureEvent_DisposedClient_DoesNotThrow() sut.CaptureEvent(@event); } - [Fact] - public void CaptureUserFeedback_EventIdEmpty_IgnoreUserFeedback() - { -#pragma warning disable CS0618 // Type or member is obsolete - //Arrange - var sut = _fixture.GetSut(); - - //Act - sut.CaptureUserFeedback( - new UserFeedback(SentryId.Empty, "name", "email", "comment")); - - //Assert - _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void CaptureUserFeedback_ValidUserFeedback_FeedbackRegistered() - { -#pragma warning disable CS0618 // Type or member is obsolete - //Arrange - var sut = _fixture.GetSut(); - - //Act - sut.CaptureUserFeedback( - new UserFeedback(SentryId.Parse("4eb98e5f861a41019f270a7a27e84f02"), "name", "email", "comment")); - - //Assert - _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void CaptureUserFeedback_EventIdEmpty_FeedbackIgnored() - { -#pragma warning disable CS0618 // Type or member is obsolete - //Arrange - var sut = _fixture.GetSut(); - - //Act - sut.CaptureUserFeedback(new UserFeedback(SentryId.Empty, "name", "email", "comment")); - - //Assert - _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); -#pragma warning restore CS0618 // Type or member is obsolete - } - [Fact] public void Dispose_should_only_flush() { @@ -989,16 +942,6 @@ public void CaptureFeedback_WithHint_HasHintAttachment() envelope.Items.Count(item => item.TryGetType() == "attachment") == 1)); } - [Fact] - public void CaptureUserFeedback_DisposedClient_DoesNotThrow() - { -#pragma warning disable CS0618 // Type or member is obsolete - var sut = _fixture.GetSut(); - sut.Dispose(); - sut.CaptureUserFeedback(new UserFeedback(SentryId.Empty, "name", "email", "comment")); -#pragma warning restore CS0618 // Type or member is obsolete - } - [Fact] public void CaptureTransaction_SampledOut_Dropped() { From 764534172084c1196403b74c53644d783f85db5c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 17 Oct 2025 09:58:04 +1300 Subject: [PATCH 17/23] Disable sentry-native for WASM applications (#4631) Resolves #4550 - #4550 --- CHANGELOG.md | 1 + .../Platforms/Native/buildTransitive/Sentry.Native.targets | 2 ++ src/Sentry/buildTransitive/Sentry.targets | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c35e28676d..9cb5f0c695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixes - The SDK avoids redundant scope sync after transaction finish ([#4623](https://github.com/getsentry/sentry-dotnet/pull/4623)) +- sentry-native is now automatically disabled for WASM applications ([#4631](https://github.com/getsentry/sentry-dotnet/pull/4631)) ## 6.0.0-preview.1 diff --git a/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets b/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets index 792acfc289..7cfa67275d 100644 --- a/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets +++ b/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets @@ -24,6 +24,8 @@ true false + + false true false diff --git a/src/Sentry/buildTransitive/Sentry.targets b/src/Sentry/buildTransitive/Sentry.targets index c33ed0efbf..8d24dc35b0 100644 --- a/src/Sentry/buildTransitive/Sentry.targets +++ b/src/Sentry/buildTransitive/Sentry.targets @@ -27,7 +27,7 @@ - + From e7d6b84aac9e1312ac2c4c70afd9d2952a1efda2 Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Fri, 17 Oct 2025 00:04:48 +0200 Subject: [PATCH 18/23] fix: `GlobalSessionManager` tests (#4649) --- .../Sentry.Tests/GlobalSessionManagerTests.cs | 51 +++++-------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/test/Sentry.Tests/GlobalSessionManagerTests.cs b/test/Sentry.Tests/GlobalSessionManagerTests.cs index a4e504cf92..1cfde77220 100644 --- a/test/Sentry.Tests/GlobalSessionManagerTests.cs +++ b/test/Sentry.Tests/GlobalSessionManagerTests.cs @@ -1,13 +1,9 @@ -using System.IO.Abstractions.TestingHelpers; - namespace Sentry.Tests; -public class GlobalSessionManagerTests : IDisposable +public class GlobalSessionManagerTests { - private class Fixture : IDisposable + private class Fixture { - public TempDirectory CacheDirectory; - public InMemoryDiagnosticLogger Logger { get; } public SentryOptions Options { get; } @@ -21,16 +17,14 @@ public Fixture(Action configureOptions = null) Clock.GetUtcNow().Returns(DateTimeOffset.Now); Logger = new InMemoryDiagnosticLogger(); - CacheDirectory = new TempDirectory(); Options = new SentryOptions { Dsn = ValidDsn, Release = "test", Debug = true, DiagnosticLogger = Logger, - CacheDirectoryPath = CacheDirectory.Path, - // This keeps all writing-to-file operations in memory instead of actually writing to disk - FileSystem = new FakeFileSystem() + CacheDirectoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), + FileSystem = new FakeFileSystem() // Keep all fileIO operations in memory }; configureOptions?.Invoke(Options); @@ -41,8 +35,6 @@ public GlobalSessionManager GetSut() => Options, Clock, PersistedSessionProvider); - - public void Dispose() => CacheDirectory.Dispose(); } private readonly Fixture _fixture = new(); @@ -104,22 +96,16 @@ public void StartSession_CacheDirectoryProvidedButFileWriteDisabled_Installation [SkippableFact] public void StartSession_CacheDirectoryNotProvided_InstallationIdFileCreated() { - Skip.If(TestEnvironment.IsGitHubActions, "Flaky in CI"); - // Arrange _fixture.Options.CacheDirectoryPath = null; - // Setting the test-cache directory to be properly disposed - _fixture.CacheDirectory = new TempDirectory(Path.Combine( + var filePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Sentry", - _fixture.Options.Dsn!.GetHashString())); - - var sut = _fixture.GetSut(); - - var filePath = Path.Combine(_fixture.CacheDirectory.Path, ".installation"); + _fixture.Options.Dsn!.GetHashString(), + ".installation"); // Act - sut.StartSession(); + _fixture.GetSut().StartSession(); // Assert Assert.True(_fixture.Options.FileSystem.FileExists(filePath)); @@ -131,18 +117,15 @@ public void StartSession_CacheDirectoryNotProvidedAndFileWriteDisabled_Installat // Arrange _fixture.Options.DisableFileWrite = true; _fixture.Options.CacheDirectoryPath = null; - // Setting the test-cache directory to be properly disposed - _fixture.CacheDirectory = new TempDirectory(Path.Combine( + + var filePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Sentry", - _fixture.Options.Dsn!.GetHashString())); - - var sut = _fixture.GetSut(); - - var filePath = Path.Combine(_fixture.CacheDirectory.Path, ".installation"); + _fixture.Options.Dsn!.GetHashString(), + ".installation"); // Act - sut.StartSession(); + _fixture.GetSut().StartSession(); // Assert Assert.False(_fixture.Options.FileSystem.FileExists(filePath)); @@ -468,9 +451,6 @@ public void TryRecoverPersistedSession_SessionStarted_CrashDelegateReturnsTrue_E _fixture.Options.CrashedLastRun = () => true; var sut = _fixture.GetSut(); - using var fixture = new Fixture(o => - o.CrashedLastRun = () => true); - sut.StartSession(); // Act @@ -558,9 +538,4 @@ private static SessionUpdate AnySessionUpdate() DateTimeOffset.Now, 1, null); - - public void Dispose() - { - _fixture.Dispose(); - } } From b9d4710ce9d75aa16ec8c68fed545287bb50178d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 21 Oct 2025 12:11:12 +1300 Subject: [PATCH 19/23] Add retry for ProfilerIntegration_FullRoundtrip_Works test (#4651) --- .../SamplingTransactionProfilerTests.cs | 174 +++++++++--------- 1 file changed, 90 insertions(+), 84 deletions(-) diff --git a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs index 1832e88085..139b834654 100644 --- a/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs +++ b/test/Sentry.Profiling.Tests/SamplingTransactionProfilerTests.cs @@ -235,106 +235,112 @@ public void ProfilerIntegration_FullRoundtrip_Works(bool offlineCaching) { Skip.If(TestEnvironment.IsGitHubActions, "Flaky in CI"); - var tcs = new TaskCompletionSource(); - async Task VerifyAsync(HttpRequestMessage message) + TestHelpers.RetryTest(maxAttempts: 2, ExecuteTest); + return; + + void ExecuteTest() { - var payload = await message.Content!.ReadAsStringAsync(); - // We're actually looking for type:profile but it must be sent in the same envelope as the transaction. - if (payload.Contains("\"type\":\"transaction\"")) + var tcs = new TaskCompletionSource(); + async Task VerifyAsync(HttpRequestMessage message) { - tcs.TrySetResult(payload); + var payload = await message.Content!.ReadAsStringAsync(); + // We're actually looking for type:profile but it must be sent in the same envelope as the transaction. + if (payload.Contains("\"type\":\"transaction\"")) + { + tcs.TrySetResult(payload); + } } - } - - var cts = new CancellationTokenSource(); - cts.Token.Register(() => tcs.TrySetCanceled()); - // envelope cache dir - using var cacheDirectory = offlineCaching ? new TempDirectory() : null; + var cts = new CancellationTokenSource(); + cts.Token.Register(() => tcs.TrySetCanceled()); - // profiler temp dir (doesn't support `FileSystem`) - var tempDir = new TempDirectory(); - - var options = new SentryOptions - { - Dsn = ValidDsn, - // To go through a round trip serialization of cached envelope - CacheDirectoryPath = cacheDirectory?.Path, - // So we don't need to deal with gzip'ed payload - RequestBodyCompressionLevel = CompressionLevel.NoCompression, - CreateHttpMessageHandler = () => new CallbackHttpClientHandler(VerifyAsync), - // Not to send some session envelope - AutoSessionTracking = false, - Debug = true, - DiagnosticLogger = _testOutputLogger, - TracesSampleRate = 1.0, - ProfilesSampleRate = 1.0, - // This keeps all writing-to-file operations in memory instead of actually writing to disk - FileSystem = new FakeFileSystem() - }; + // envelope cache dir + using var cacheDirectory = offlineCaching ? new TempDirectory() : null; - // Disable process exit flush to resolve "There is no currently active test." errors. - options.DisableAppDomainProcessExitFlush(); + // profiler temp dir (doesn't support `FileSystem`) + var tempDir = new TempDirectory(); - options.AddProfilingIntegration(TimeSpan.FromSeconds(10)); + var options = new SentryOptions + { + Dsn = ValidDsn, + // To go through a round trip serialization of cached envelope + CacheDirectoryPath = cacheDirectory?.Path, + // So we don't need to deal with gzip'ed payload + RequestBodyCompressionLevel = CompressionLevel.NoCompression, + CreateHttpMessageHandler = () => new CallbackHttpClientHandler(VerifyAsync), + // Not to send some session envelope + AutoSessionTracking = false, + Debug = true, + DiagnosticLogger = _testOutputLogger, + TracesSampleRate = 1.0, + ProfilesSampleRate = 1.0, + // This keeps all writing-to-file operations in memory instead of actually writing to disk + FileSystem = new FakeFileSystem() + }; + + // Disable process exit flush to resolve "There is no currently active test." errors. + options.DisableAppDomainProcessExitFlush(); + + options.AddProfilingIntegration(TimeSpan.FromSeconds(10)); - try - { - using var hub = new Hub(options); + try + { + using var hub = new Hub(options); - var factory = (options.TransactionProfilerFactory as SamplingTransactionProfilerFactory)!; - Skip.If(TestEnvironment.IsGitHubActions && factory.StartupTimedOut, "Session sometimes takes too long to start in CI."); - Assert.False(factory.StartupTimedOut); + var factory = (options.TransactionProfilerFactory as SamplingTransactionProfilerFactory)!; + Skip.If(TestEnvironment.IsGitHubActions && factory.StartupTimedOut, "Session sometimes takes too long to start in CI."); + Assert.False(factory.StartupTimedOut); - var clock = SentryStopwatch.StartNew(); - var tx = hub.StartTransaction("name", "op"); - RunForMs(RuntimeMs); - tx.Finish(); - var elapsedNanoseconds = (ulong)((clock.CurrentDateTimeOffset - clock.StartDateTimeOffset).TotalMilliseconds * 1_000_000); + var clock = SentryStopwatch.StartNew(); + var tx = hub.StartTransaction("name", "op"); + RunForMs(RuntimeMs); + tx.Finish(); + var elapsedNanoseconds = (ulong)((clock.CurrentDateTimeOffset - clock.StartDateTimeOffset).TotalMilliseconds * 1_000_000); - hub.FlushAsync().Wait(); + hub.FlushAsync().Wait(); - // Synchronizing in the tests to go through the caching and http transports - cts.CancelAfter(options.FlushTimeout + TimeSpan.FromSeconds(1)); - var ex = Record.Exception(tcs.Task.Wait); - Assert.Null(ex); - Assert.True(tcs.Task.IsCompleted); + // Synchronizing in the tests to go through the caching and http transports + cts.CancelAfter(options.FlushTimeout + TimeSpan.FromSeconds(1)); + var ex = Record.Exception(tcs.Task.Wait); + Assert.Null(ex); + Assert.True(tcs.Task.IsCompleted); - var envelopeLines = tcs.Task.Result.Split('\n'); - SkipIfFailsInCI(() => + var envelopeLines = tcs.Task.Result.Split('\n'); + SkipIfFailsInCI(() => + { + if (envelopeLines.Length != 6) + { + throw new ArgumentOutOfRangeException("envelopeLines", "Invalid number of envelope lines."); + } + }); + + // header rows before payloads + envelopeLines[1].Should().StartWith("{\"type\":\"transaction\""); + envelopeLines[3].Should().StartWith("{\"type\":\"profile\""); + + var transaction = Json.Parse(envelopeLines[2], SentryTransaction.FromJson); + + // TODO do we want to bother with JSON parsing just to do this? Doing at least simple checks for now... + // var profileInfo = Json.Parse(envelopeLines[4], ProfileInfo.FromJson); + // ValidateProfile(profileInfo.Profile, elapsedNanoseconds); + envelopeLines[4].Should().Contain("\"profile\":{"); + envelopeLines[4].Should().Contain($"\"id\":\"{transaction.EventId}\""); + envelopeLines[4].Length.Should().BeGreaterThan(10000); + + Directory.GetFiles(tempDir.Path).Should().BeEmpty("When profiling is done, the temp dir should be empty."); + } + finally { - if (envelopeLines.Length != 6) + // ensure the task is complete before leaving the test + tcs.TrySetResult(""); + tcs.Task.Wait(); + + if (options.Transport is CachingTransport cachingTransport) { - throw new ArgumentOutOfRangeException("envelopeLines", "Invalid number of envelope lines."); + // Disposing the caching transport will ensure its worker + // is shut down before we try to dispose and delete the temp folder + cachingTransport.Dispose(); } - }); - - // header rows before payloads - envelopeLines[1].Should().StartWith("{\"type\":\"transaction\""); - envelopeLines[3].Should().StartWith("{\"type\":\"profile\""); - - var transaction = Json.Parse(envelopeLines[2], SentryTransaction.FromJson); - - // TODO do we want to bother with JSON parsing just to do this? Doing at least simple checks for now... - // var profileInfo = Json.Parse(envelopeLines[4], ProfileInfo.FromJson); - // ValidateProfile(profileInfo.Profile, elapsedNanoseconds); - envelopeLines[4].Should().Contain("\"profile\":{"); - envelopeLines[4].Should().Contain($"\"id\":\"{transaction.EventId}\""); - envelopeLines[4].Length.Should().BeGreaterThan(10000); - - Directory.GetFiles(tempDir.Path).Should().BeEmpty("When profiling is done, the temp dir should be empty."); - } - finally - { - // ensure the task is complete before leaving the test - tcs.TrySetResult(""); - tcs.Task.Wait(); - - if (options.Transport is CachingTransport cachingTransport) - { - // Disposing the caching transport will ensure its worker - // is shut down before we try to dispose and delete the temp folder - cachingTransport.Dispose(); } } } From c693a7e06fa8b900566980c62d51a23c40aaaecf Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 22 Oct 2025 09:55:07 +1300 Subject: [PATCH 20/23] Bump to .NET 10.0.100-rc.2 (#4652) --- .editorconfig | 12 +++++++++++- .github/actions/environment/action.yml | 5 +++-- Directory.Build.props | 8 ++++++-- global.json | 4 ++-- test/Directory.Build.props | 2 ++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index 01deda3d42..422c04f5bc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -207,4 +207,14 @@ indent_size = unset indent_style = unset insert_final_newline = false tab_width = unset -trim_trailing_whitespace = false \ No newline at end of file +trim_trailing_whitespace = false + +# Disable AOT analyser for test files +[test/**/*.cs] +dotnet_diagnostic.IL2026.severity = none +dotnet_diagnostic.IL2070.severity = none +dotnet_diagnostic.IL2075.severity = none +dotnet_diagnostic.IL2090.severity = none + +# This appears to be broken and results in false positives (causing dotnet format to delete valid test scenarios) +dotnet_diagnostic.xUnit1025.severity = none diff --git a/.github/actions/environment/action.yml b/.github/actions/environment/action.yml index 498a3f41d5..315d1bc9f0 100644 --- a/.github/actions/environment/action.yml +++ b/.github/actions/environment/action.yml @@ -44,10 +44,11 @@ runs: uses: ./.github/actions/install-zstd # See https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#xcode + # Also https://github.com/dotnet/macios/issues/21762#issuecomment-3424033859 (don't reference symlinks) - name: Pin the Xcode Version if: runner.os == 'macOS' shell: bash - run: sudo xcode-select --switch /Applications/Xcode_16.4.app + run: sudo xcode-select --switch /Applications/Xcode_26.0.1.app # Java 17 is needed for Android SDK setup step - name: Install Java 17 @@ -97,7 +98,7 @@ runs: dotnet-version: | 8.0.x 9.0.304 - 10.0.100-rc.1.25451.107 + 10.0.100-rc.2.25502.107 # .NET 5.0 does not support ARM64 on macOS - name: Install .NET 5.0 SDK diff --git a/Directory.Build.props b/Directory.Build.props index 2be652ff9f..8011f4416d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,9 +34,9 @@ net8.0;net9.0;net10.0 net10.0-android36.0 net9.0-android35.0 - net9.0-ios26 + net10.0-ios26 net9.0-ios18.0 - net9.0-maccatalyst26 + net10.0-maccatalyst26 net9.0-maccatalyst18.0 net9.0-windows10.0.19041.0 @@ -114,6 +114,10 @@ true $(DefineConstants);MEMORY_DUMP_SUPPORTED + + + false + diff --git a/global.json b/global.json index 9920ba14bd..f260fed215 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "10.0.100-rc.1.25451.107", - "workloadVersion": "10.0.100-rc.1.25458.2", + "version": "10.0.100-rc.2.25502.107", + "workloadVersion": "10.0.100-rc.2.25513.4", "rollForward": "disable", "allowPrerelease": false } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 301d759ba4..0bd7aa9696 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -13,6 +13,8 @@ $(NoWarn);IDE1006 $(NoWarn);ASPDEPR004;ASPDEPR008 + + $(NoWarn);IL2026;IL2070;IL2075;IL2090 false From a267d32d641a66087e902f2aeeb23af9a4e32e40 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 09:10:02 +0200 Subject: [PATCH 21/23] test: add missing native crash integration test for Android (#4659) --- integration-test/android.Tests.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/integration-test/android.Tests.ps1 b/integration-test/android.Tests.ps1 index 7ec0a80fbc..30e7009378 100644 --- a/integration-test/android.Tests.ps1 +++ b/integration-test/android.Tests.ps1 @@ -149,6 +149,20 @@ Describe 'MAUI app' -ForEach @( $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"System.\w+Exception`"" } + It 'Native crash' { + $result = Invoke-SentryServer { + param([string]$url) + InstallAndroidApp -Dsn $url + RunAndroidApp -Dsn $url -TestArg "Native" + RunAndroidApp -Dsn $url + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"SIG[A-Z]+`"" # SIGILL (x86_64), SIGTRAP (arm64-v8a) + $result.Envelopes() | Should -Not -AnyElementMatch "`"type`":`"System.\w+Exception`"" + } + It 'Null reference exception' { $result = Invoke-SentryServer { param([string]$url) From aedce0494aa87376fb535f5709415229dc755fd4 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 22 Oct 2025 19:15:25 +0200 Subject: [PATCH 22/23] feat: Add support for w3c trace parent headers for outgoing requests (#4661) * feat: Add support for w3c trace parent headers for outgoing requests * Add changelog * Verify & bindable options * Add more tests * Update src/Sentry/SentryOptions.cs --------- Co-authored-by: bitsandfoxes Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 11 +++ src/Sentry/BindableSentryOptions.cs | 2 + src/Sentry/Extensibility/DisabledHub.cs | 5 ++ src/Sentry/Extensibility/HubAdapter.cs | 7 ++ src/Sentry/IHub.cs | 5 ++ src/Sentry/Internal/Hub.cs | 12 +++ src/Sentry/SentryMessageHandler.cs | 16 ++++ src/Sentry/SentryOptions.cs | 12 +++ src/Sentry/SentrySdk.cs | 7 ++ src/Sentry/W3CTraceparentHeader.cs | 39 +++++++++ ...iApprovalTests.Run.DotNet10_0.verified.txt | 13 +++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 13 +++ ...piApprovalTests.Run.DotNet9_0.verified.txt | 13 +++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 13 +++ test/Sentry.Tests/HubTests.cs | 41 ++++++++++ .../Protocol/W3CTraceparentHeaderTests.cs | 73 +++++++++++++++++ .../SentryHttpMessageHandlerTests.cs | 79 +++++++++++++++++++ 17 files changed, 361 insertions(+) create mode 100644 src/Sentry/W3CTraceparentHeader.cs create mode 100644 test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb5f0c695..9d103949ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ - Removed `SentrySdk.CaptureUserFeedback` and all associated members. Use the newer `SentrySdk.CaptureFeedback` instead. - Backpressure handling is now enabled by default, meaning that the SDK will monitor system health and reduce the sampling rate of events and transactions when the system is under load. When the system is determined to be healthy again, the sampling rates are returned to their original levels. ([#4615](https://github.com/getsentry/sentry-dotnet/pull/4615)) - ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611)) +- Add support for W3C traceparent header for outgoing requests ([#4661](https://github.com/getsentry/sentry-dotnet/pull/4661)) + This feature is disabled by default. When enabled, outgoing requests will include the W3C traceparent header. + ```csharp + SentrySdk.Init(options => + { + // ... + options.PropagateTraceparent = true; + }); + ``` + + See https://develop.sentry.dev/sdk/telemetry/traces/distributed-tracing/#w3c-trace-context-header for more details. ### Fixes diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs index f9077de9b4..c3314257f8 100644 --- a/src/Sentry/BindableSentryOptions.cs +++ b/src/Sentry/BindableSentryOptions.cs @@ -42,6 +42,7 @@ internal partial class BindableSentryOptions public bool? EnableTracing { get; set; } public double? TracesSampleRate { get; set; } public List? TracePropagationTargets { get; set; } + public bool? PropagateTraceparent { get; set; } public double? ProfilesSampleRate { get; set; } public StackTraceMode? StackTraceMode { get; set; } public long? MaxAttachmentSize { get; set; } @@ -98,6 +99,7 @@ public void ApplyTo(SentryOptions options) options.TracesSampleRate = TracesSampleRate ?? options.TracesSampleRate; options.ProfilesSampleRate = ProfilesSampleRate ?? options.ProfilesSampleRate; options.TracePropagationTargets = TracePropagationTargets?.Select(s => new StringOrRegex(s)).ToList() ?? options.TracePropagationTargets; + options.PropagateTraceparent = PropagateTraceparent ?? options.PropagateTraceparent; options.StackTraceMode = StackTraceMode ?? options.StackTraceMode; options.MaxAttachmentSize = MaxAttachmentSize ?? options.MaxAttachmentSize; options.DetectStartupTime = DetectStartupTime ?? options.DetectStartupTime; diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 30d2eefffa..9592b23023 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -99,6 +99,11 @@ public void BindException(Exception exception, ISpan span) /// public BaggageHeader? GetBaggage() => null; + /// + /// Returns null. + /// + public W3CTraceparentHeader? GetTraceparentHeader() => null; + /// /// Returns sampled out transaction context. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 25eb86800c..086a1ad351 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -140,6 +140,13 @@ public void BindException(Exception exception, ISpan span) => public BaggageHeader? GetBaggage() => SentrySdk.GetBaggage(); + /// + /// Forwards the call to . + /// + [DebuggerStepThrough] + public W3CTraceparentHeader? GetTraceparentHeader() + => SentrySdk.GetTraceparentHeader(); + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 8c3006c149..1579a7d659 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -59,6 +59,11 @@ public ITransactionTracer StartTransaction( /// public BaggageHeader? GetBaggage(); + /// + /// Gets the W3C Trace Context traceparent header that allows tracing across services + /// + public W3CTraceparentHeader? GetTraceparentHeader(); + /// /// Continues a trace based on HTTP header values provided as strings. /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 2bc02c2185..d4990363b9 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -314,6 +314,18 @@ public BaggageHeader GetBaggage() return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader(); } + public W3CTraceparentHeader? GetTraceparentHeader() + { + if (GetSpan()?.GetTraceHeader() is { } traceHeader) + { + return new W3CTraceparentHeader(traceHeader.TraceId, traceHeader.SpanId, traceHeader.IsSampled); + } + + // We fall back to the propagation context + var propagationContext = CurrentScope.PropagationContext; + return new W3CTraceparentHeader(propagationContext.TraceId, propagationContext.SpanId, null); + } + public TransactionContext ContinueTrace( string? traceHeader, string? baggageHeader, diff --git a/src/Sentry/SentryMessageHandler.cs b/src/Sentry/SentryMessageHandler.cs index 60c5b05ad5..9c8515e93e 100644 --- a/src/Sentry/SentryMessageHandler.cs +++ b/src/Sentry/SentryMessageHandler.cs @@ -137,6 +137,10 @@ private void PropagateTraceHeaders(HttpRequestMessage request, string url, ISpan { AddSentryTraceHeader(request, parentSpan); AddBaggageHeader(request); + if (_options?.PropagateTraceparent is true) + { + AddTraceparentHeader(request, parentSpan); + } } } @@ -181,4 +185,16 @@ private void AddBaggageHeader(HttpRequestMessage request) // Set the baggage header request.Headers.Add(BaggageHeader.HttpHeaderName, baggage.ToString()); } + + private void AddTraceparentHeader(HttpRequestMessage request, ISpan? parentSpan) + { + // Set W3C traceparent header if it hasn't already been set + if (!request.Headers.Contains(W3CTraceparentHeader.HttpHeaderName) && + // Use the span created by this integration as parent, instead of its own parent + (parentSpan?.GetTraceHeader() ?? _hub.GetTraceHeader()) is { } traceHeader) + { + var traceparentHeader = new W3CTraceparentHeader(traceHeader.TraceId, traceHeader.SpanId, traceHeader.IsSampled); + request.Headers.Add(W3CTraceparentHeader.HttpHeaderName, traceparentHeader.ToString()); + } + } } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 9b8835a165..1a043100e8 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -995,6 +995,18 @@ public IList TracePropagationTargets set => _tracePropagationTargets = value.WithConfigBinding(); } + /// + /// Whether to send W3C Trace Context traceparent headers in outgoing HTTP requests for distributed tracing. + /// When enabled, the SDK will send the traceparent header in addition to the sentry-trace header + /// for requests matching . + /// + /// + /// The default value is false. Set to true to enable W3C Trace Context propagation + /// for interoperability with services that support OpenTelemetry standards. + /// + /// + public bool PropagateTraceparent { get; set; } + internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; } private StackTraceMode? _stackTraceMode; diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 982dbb6c3a..3df33363f6 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -716,6 +716,13 @@ public static void BindException(Exception exception, ISpan span) public static BaggageHeader? GetBaggage() => CurrentHub.GetBaggage(); + /// + /// Gets the W3C Trace Context traceparent header that allows tracing across services + /// + [DebuggerStepThrough] + public static W3CTraceparentHeader? GetTraceparentHeader() + => CurrentHub.GetTraceparentHeader(); + /// /// Continues a trace based on HTTP header values provided as strings. /// diff --git a/src/Sentry/W3CTraceparentHeader.cs b/src/Sentry/W3CTraceparentHeader.cs new file mode 100644 index 0000000000..d31ab4a731 --- /dev/null +++ b/src/Sentry/W3CTraceparentHeader.cs @@ -0,0 +1,39 @@ +namespace Sentry; + +/// +/// W3C Trace Context traceparent header. +/// +public class W3CTraceparentHeader +{ + internal const string HttpHeaderName = "traceparent"; + + /// + /// Trace ID. + /// + public SentryId TraceId { get; } + + /// + /// Span ID. + /// + public SpanId SpanId { get; } + + /// + /// Whether the trace is sampled. + /// + public bool? IsSampled { get; } + + /// + /// Initializes an instance of . + /// + public W3CTraceparentHeader(SentryId traceId, SpanId spanId, bool? isSampled) + { + TraceId = traceId; + SpanId = spanId; + IsSampled = isSampled; + } + + /// + public override string ToString() => IsSampled is { } isSampled + ? $"00-{TraceId}-{SpanId}-{(isSampled ? "01" : "00")}" + : $"00-{TraceId}-{SpanId}-00"; +} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index b955fd5d7f..ffe89a225e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -198,6 +198,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -718,6 +719,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -864,6 +866,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1298,6 +1301,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b955fd5d7f..ffe89a225e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -198,6 +198,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -718,6 +719,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -864,6 +866,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1298,6 +1301,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b955fd5d7f..ffe89a225e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -198,6 +198,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -718,6 +719,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -864,6 +866,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1298,6 +1301,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 4acd40d0ff..809051ae57 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -186,6 +186,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -700,6 +701,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -840,6 +842,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1274,6 +1277,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1360,6 +1371,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1409,6 +1421,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index b1dc382b3e..c04fc23a94 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1363,6 +1363,47 @@ public void GetBaggage_NoSpanActive_ReturnsBaggageFromPropagationContext() Assert.Contains("sentry-trace_id=43365712692146d08ee11a729dfbcaca", baggage!.ToString()); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetTraceparentHeader_ReturnsHeaderForActiveSpan(bool isSampled) + { + // Arrange + _fixture.Options.TracesSampleRate = isSampled ? 1 : 0; + var hub = _fixture.GetSut(); + var transaction = hub.StartTransaction("foo", "bar"); + hub.ConfigureScope(scope => scope.Transaction = transaction); + + // Act + var header = hub.GetTraceparentHeader(); + + // Assert + header.Should().NotBeNull(); + header.SpanId.Should().Be(transaction.SpanId); + header.TraceId.Should().Be(transaction.TraceId); + header.IsSampled.Should().Be(transaction.IsSampled); + } + + [Fact] + public void GetTraceparentHeader_NoSpanActive_ReturnsHeaderFromPropagationContext() + { + // Arrange + var hub = _fixture.GetSut(); + var propagationContext = new SentryPropagationContext( + SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"), + SpanId.Parse("2000000000000000")); + hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); + + // Act + var header = hub.GetTraceparentHeader(); + + // Assert + header.Should().NotBeNull(); + header.SpanId.Should().Be(propagationContext.SpanId); + header.TraceId.Should().Be(propagationContext.TraceId); + header.IsSampled.Should().BeNull(); + } + [Fact] public void ContinueTrace_ReceivesHeaders_SetsPropagationContextAndReturnsTransactionContext() { diff --git a/test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs b/test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs new file mode 100644 index 0000000000..96a58d1b80 --- /dev/null +++ b/test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs @@ -0,0 +1,73 @@ +namespace Sentry.Tests.Protocol; + +public class W3CTraceparentHeaderTests +{ + [Fact] + public void ToString_WithSampledTrue_Works() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var header = new W3CTraceparentHeader(traceId, spanId, true); + + // Act + var result = header.ToString(); + + // Assert + result.Should().Be("00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-01"); + } + + [Fact] + public void ToString_WithSampledFalse_Works() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var header = new W3CTraceparentHeader(traceId, spanId, false); + + // Act + var result = header.ToString(); + + // Assert + result.Should().Be("00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00"); + } + + [Fact] + public void ToString_WithoutSampled_DefaultsToFalse() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var header = new W3CTraceparentHeader(traceId, spanId, null); + + // Act + var result = header.ToString(); + + // Assert + result.Should().Be("00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00"); + } + + [Fact] + public void Constructor_StoresProperties() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var isSampled = true; + + // Act + var header = new W3CTraceparentHeader(traceId, spanId, isSampled); + + // Assert + header.TraceId.Should().Be(traceId); + header.SpanId.Should().Be(spanId); + header.IsSampled.Should().Be(isSampled); + } + + [Fact] + public void HttpHeaderName_IsCorrect() + { + // Act & Assert + W3CTraceparentHeader.HttpHeaderName.Should().Be("traceparent"); + } +} diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index a15727e7b4..584fcd43d7 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -155,6 +155,85 @@ public async Task SendAsync_SentryTraceHeaderAlreadySet_NotOverwritten() string.Concat(h.Value) == "foobar"); } + [Fact] + public async Task SendAsync_W3C_TraceParent_NotSet_WhenPropagateTraceparentIsFalse() + { + // Arrange + var hub = Substitute.For(); + + hub.GetTraceHeader().ReturnsForAnyArgs( + SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")); + + var failedRequestHandler = Substitute.For(); + var options = new SentryOptions(); + using var innerHandler = new RecordingHttpMessageHandler(new FakeHttpMessageHandler()); + using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler); + using var client = new HttpClient(sentryHandler); + + // Act + await client.GetAsync("https://localhost/"); + + using var request = innerHandler.GetRequests().Single(); + + // Assert + request.Headers.Should().NotContain(h => h.Key == "traceparent"); + } + + [Fact] + public async Task SendAsync_W3C_TraceParent_Set_WhenPropagateTraceparentIsTrue() + { + // Arrange + var hub = Substitute.For(); + + hub.GetTraceHeader().ReturnsForAnyArgs( + SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")); + + var failedRequestHandler = Substitute.For(); + var options = new SentryOptions(); + options.PropagateTraceparent = true; + using var innerHandler = new RecordingHttpMessageHandler(new FakeHttpMessageHandler()); + using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler); + using var client = new HttpClient(sentryHandler); + + client.DefaultRequestHeaders.Add("sentry-trace", "foobar"); + + // Act + await client.GetAsync("https://localhost/"); + + using var request = innerHandler.GetRequests().Single(); + + // Assert + request.Headers.Should().Contain(h => h.Key == "traceparent" && h.Value.Single() == "00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00"); + } + + [Fact] + public async Task SendAsync_W3C_TraceParent_NotSet_WhenPropagateTraceparentAlreadySet() + { + // Arrange + var hub = Substitute.For(); + + hub.GetTraceHeader().ReturnsForAnyArgs( + SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")); + + var failedRequestHandler = Substitute.For(); + var options = new SentryOptions(); + options.PropagateTraceparent = true; + using var innerHandler = new RecordingHttpMessageHandler(new FakeHttpMessageHandler()); + using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler); + using var client = new HttpClient(sentryHandler); + + client.DefaultRequestHeaders.Add("sentry-trace", "foobar"); + client.DefaultRequestHeaders.Add("traceparent", "existing-value"); + + // Act + await client.GetAsync("https://localhost/"); + + using var request = innerHandler.GetRequests().Single(); + + // Assert + request.Headers.Should().Contain(h => h.Key == "traceparent" && h.Value.Single() == "existing-value"); + } + [Fact] public async Task SendAsync_TransactionOnScope_StartsNewSpan() { From 5c4f9e191478c55ed7f968b7015eef5b3d472de5 Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Wed, 22 Oct 2025 23:02:51 +0200 Subject: [PATCH 23/23] chore: Unseal `SentryExperimentalOptions` (#4665) --- src/Sentry/SentryOptions.cs | 2 +- test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt | 2 +- test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt | 2 +- test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt | 2 +- test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 1a043100e8..67e7248f11 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1893,7 +1893,7 @@ internal static List GetDefaultInAppExclude() => /// /// This and related experimental APIs may change in the future. /// - public sealed class SentryExperimentalOptions + public class SentryExperimentalOptions { internal SentryExperimentalOptions() { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index ffe89a225e..c2c782305e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -791,7 +791,7 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } - public sealed class SentryExperimentalOptions + public class SentryExperimentalOptions { public bool EnableLogs { get; set; } public void SetBeforeSendLog(System.Func beforeSendLog) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index ffe89a225e..c2c782305e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -791,7 +791,7 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } - public sealed class SentryExperimentalOptions + public class SentryExperimentalOptions { public bool EnableLogs { get; set; } public void SetBeforeSendLog(System.Func beforeSendLog) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index ffe89a225e..c2c782305e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -791,7 +791,7 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } - public sealed class SentryExperimentalOptions + public class SentryExperimentalOptions { public bool EnableLogs { get; set; } public void SetBeforeSendLog(System.Func beforeSendLog) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 809051ae57..9cd54f7fa0 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -767,7 +767,7 @@ namespace Sentry public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { } public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { } - public sealed class SentryExperimentalOptions + public class SentryExperimentalOptions { public bool EnableLogs { get; set; } public void SetBeforeSendLog(System.Func beforeSendLog) { }