diff --git a/CHANGELOG.md b/CHANGELOG.md index c531a49..8db234b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Send events synchronously so they're not lost when the script exits ([#39](https://github.com/getsentry/sentry-powershell/pull/39)) + ## 0.0.2 ### Various fixes & improvements diff --git a/modules/Sentry/private/New-HttpTransport.ps1 b/modules/Sentry/private/New-HttpTransport.ps1 new file mode 100644 index 0000000..98022ce --- /dev/null +++ b/modules/Sentry/private/New-HttpTransport.ps1 @@ -0,0 +1,17 @@ +# Wrapper to expose Sentry.Internal.SdkComposer::CreateHttpTransport() +function New-HttpTransport +{ + [OutputType([Sentry.Extensibility.ITransport])] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [Sentry.SentryOptions] $options + ) + + $assembly = [Sentry.SentrySdk].Assembly + $type = $assembly.GetType('Sentry.Internal.SdkComposer') + $composer = [Activator]::CreateInstance($type, @($options)) + + $method = $type.GetMethod('CreateHttpTransport', [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public) + return $method.Invoke($composer, @()) +} diff --git a/modules/Sentry/private/SynchronousWorker.ps1 b/modules/Sentry/private/SynchronousWorker.ps1 new file mode 100644 index 0000000..2938cf3 --- /dev/null +++ b/modules/Sentry/private/SynchronousWorker.ps1 @@ -0,0 +1,42 @@ +. "$privateDir/New-HttpTransport.ps1" + +class SynchronousWorker : Sentry.Extensibility.IBackgroundWorker +{ + hidden [Sentry.Extensibility.ITransport] $transport + hidden [Sentry.SentryOptions] $options + hidden $unfinishedTasks = [System.Collections.Generic.List[System.Threading.Tasks.Task]]::new() + + SynchronousWorker([Sentry.SentryOptions] $options) + { + $this.options = $options + + # Start from either the transport given on options, or create a new HTTP transport. + $this.transport = $options.Transport; + if ($null -eq $this.transport) + { + $this.transport = New-HttpTransport($options) + } + } + + [bool] EnqueueEnvelope([Sentry.Protocol.Envelopes.Envelope] $envelope) + { + $task = $this.transport.SendEnvelopeAsync($envelope, [System.Threading.CancellationToken]::None) + if (-not $task.Wait($this.options.FlushTimeout)) + { + $this.unfinishedTasks.Add($task) + } + return $true + } + + [System.Threading.Tasks.Task] FlushAsync([System.TimeSpan] $timeout) + { + [System.Threading.Tasks.Task]::WhenAll($this.unfinishedTasks).Wait($timeout) + $this.unfinishedTasks.Clear() + return [System.Threading.Tasks.Task]::CompletedTask + } + + [int] get_QueuedItems() + { + return $this.unfinishedTasks.Count + } +} diff --git a/modules/Sentry/public/Start-Sentry.ps1 b/modules/Sentry/public/Start-Sentry.ps1 index b2b3609..6bb3149 100644 --- a/modules/Sentry/public/Start-Sentry.ps1 +++ b/modules/Sentry/public/Start-Sentry.ps1 @@ -1,5 +1,6 @@ . "$privateDir/DiagnosticLogger.ps1" . "$privateDir/ScopeIntegration.ps1" +. "$privateDir/SynchronousWorker.ps1" . "$privateDir/EventUpdater.ps1" function Start-Sentry @@ -16,6 +17,8 @@ function Start-Sentry begin { $options = [Sentry.SentryOptions]::new() + $options.FlushTimeout = [System.TimeSpan]::FromSeconds(10) + $options.ShutDownTimeout = $options.FlushTimeout $options.ReportAssembliesMode = [Sentry.ReportAssembliesMode]::None $options.IsGlobalModeEnabled = $true [Sentry.sentryOptionsExtensions]::AddIntegration($options, [ScopeIntegration]::new()) @@ -42,7 +45,20 @@ function Start-Sentry $options | ForEach-Object $EditOptions } - $options.DiagnosticLogger = [DiagnosticLogger]::new($options.DiagnosticLevel) + $logger = [DiagnosticLogger]::new($options.DiagnosticLevel) + $options.DiagnosticLogger = $logger + + if ($null -eq $options.BackgroundWorker) + { + try + { + $options.BackgroundWorker = [SynchronousWorker]::new($options) + } + catch + { + $logger.Log([Sentry.SentryLevel]::Warning, 'Failed to create a PowerShell-specific synchronous worker', $_.Exception, @()) + } + } # Workaround for https://github.com/getsentry/sentry-dotnet/issues/3141 [Sentry.SentryOptionsExtensions]::DisableAppDomainProcessExitFlush($options) @@ -52,4 +68,3 @@ function Start-Sentry [Sentry.SentrySdk]::init($options) | Out-Null } } - diff --git a/tests/userfeedback.tests.ps1 b/tests/out-sentry.tests.ps1 similarity index 82% rename from tests/userfeedback.tests.ps1 rename to tests/out-sentry.tests.ps1 index 6ce073a..3d061c4 100644 --- a/tests/userfeedback.tests.ps1 +++ b/tests/out-sentry.tests.ps1 @@ -2,7 +2,7 @@ BeforeAll { . "$PSScriptRoot/utils.ps1" } -Describe 'UserFeedback' { +Describe 'Out-Sentry' { BeforeEach { $events = [System.Collections.Generic.List[Sentry.SentryEvent]]::new(); $transport = [RecordingTransport]::new() @@ -15,13 +15,13 @@ Describe 'UserFeedback' { Stop-Sentry } - It 'Out-Sentry returns an event ID for messages' { + It 'returns an event ID for messages' { $eventId = 'msg' | Out-Sentry $eventId | Should -BeOfType [Sentry.SentryId] $eventId.ToString().Length | Should -Be 32 } - It 'Out-Sentry returns an event ID for an error record' { + It 'returns an event ID for an error record' { try { throw 'error' @@ -34,15 +34,13 @@ Describe 'UserFeedback' { $eventId.ToString().Length | Should -Be 32 } - It 'Feedback gets captured' { + It 'captures feedback' { $eventId = 'msg' | Out-Sentry $eventId | Should -BeOfType [Sentry.SentryId] - [Sentry.SentrySdk]::Flush() $transport.Envelopes.Count | Should -Be 1 [Sentry.SentrySdk]::CaptureUserFeedback($eventId, 'email@example.com', 'comments', 'name') - [Sentry.SentrySdk]::Flush() $transport.Envelopes.Count | Should -Be 2 $envelopeItem = $transport.Envelopes.ToArray()[1].Items[0] $envelopeItem.Header['type'] | Should -Be 'user_report' @@ -52,4 +50,9 @@ Describe 'UserFeedback' { $envelopeItem.Payload.Source.Comments | Should -Be 'comments' } + It 'sends synchronously' { + $eventId = 'msg' | Out-Sentry + $eventId | Should -Not -Be $null + $transport.Envelopes.Count | Should -Be 1 + } } diff --git a/tests/scope.tests.ps1 b/tests/scope.tests.ps1 index 76234a0..f4b797a 100644 --- a/tests/scope.tests.ps1 +++ b/tests/scope.tests.ps1 @@ -19,7 +19,6 @@ Describe 'Edit-SentryScope' { [Sentry.ScopeExtensions]::AddAttachment($_, $PSCommandPath) } 'message' | Out-Sentry - [Sentry.SentrySdk]::Flush() $transport.Envelopes.Count | Should -Be 1 [Sentry.Protocol.Envelopes.Envelope]$envelope = $transport.Envelopes.ToArray()[0] $envelope.Items.Count | Should -Be 2 @@ -32,7 +31,6 @@ Describe 'Edit-SentryScope' { [byte[]] $data = 1, 2, 3, 4, 5 [Sentry.ScopeExtensions]::AddAttachment($_, $data, 'filename.bin') } - [Sentry.SentrySdk]::Flush() $transport.Envelopes.Count | Should -Be 1 [Sentry.Protocol.Envelopes.Envelope]$envelope = $transport.Envelopes.ToArray()[0] $envelope.Items.Count | Should -Be 2 diff --git a/tests/utils.ps1 b/tests/utils.ps1 index a52bfa8..96afc6e 100644 --- a/tests/utils.ps1 +++ b/tests/utils.ps1 @@ -1,11 +1,11 @@ class RecordingTransport:Sentry.Extensibility.ITransport { - $envelopes = [System.Collections.Concurrent.ConcurrentQueue[Sentry.Protocol.Envelopes.Envelope]]::new(); + $envelopes = [System.Collections.Concurrent.ConcurrentQueue[Sentry.Protocol.Envelopes.Envelope]]::new() [System.Threading.Tasks.Task]SendEnvelopeAsync([Sentry.Protocol.Envelopes.Envelope] $envelope, [System.Threading.CancellationToken] $cancellationToken) { - $this.envelopes.Enqueue($envelope); - return [System.Threading.Tasks.Task]::CompletedTask; + $this.envelopes.Enqueue($envelope) + return [System.Threading.Tasks.Task]::CompletedTask } [void] Clear() @@ -18,9 +18,9 @@ class TestLogger:Sentry.Infrastructure.DiagnosticLogger { TestLogger([Sentry.SentryLevel]$level) : base($level) {} - $entries = [System.Collections.Concurrent.ConcurrentQueue[string]]::new(); + $entries = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() - [void]LogMessage([string] $message) { $this.entries.Enqueue($message); } + [void]LogMessage([string] $message) { $this.entries.Enqueue($message) } } class TestIntegration : Sentry.Integrations.ISdkIntegration @@ -45,7 +45,7 @@ function StartSentryForEventTests([ref] $events, [ref] $transport) param([Sentry.SentryEvent]$e) $events.Add($e) return $e - }); + }) # If events are not sent, there's a client report sent at the end and it blocks the process for the default flush # timeout because it cannot connect to the server. Let's just replace the transport too.