diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d514a..234290e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Send events to Sentry fully synchronously ([#59](https://github.com/SummitHosting/sentry-powershell/pull/59), [#62](https://github.com/SummitHosting/sentry-powershell/pull/62)) + ### Fixes - StackTrace parsing on Windows PowerShell 5.1 ([#50](https://github.com/getsentry/sentry-powershell/pull/50)) diff --git a/modules/Sentry/Sentry.psd1 b/modules/Sentry/Sentry.psd1 index 48525bc..180512a 100644 --- a/modules/Sentry/Sentry.psd1 +++ b/modules/Sentry/Sentry.psd1 @@ -30,6 +30,9 @@ # Script files (.ps1) that are run in the caller's environment prior to importing this module. ScriptsToProcess = @('assemblies-loader.ps1') + # Require System.Net.Http for use by SynchronousTransport + RequiredAssemblies = @('System.Net.Http') + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( 'Add-SentryBreadcrumb', diff --git a/modules/Sentry/private/SynchronousTransport.ps1 b/modules/Sentry/private/SynchronousTransport.ps1 new file mode 100644 index 0000000..ff8a0b2 --- /dev/null +++ b/modules/Sentry/private/SynchronousTransport.ps1 @@ -0,0 +1,69 @@ +# Take Sentry's SerializableHttpContent, convert it to a string, and send via PowerShell's Invoke-WebRequest, +# then translate the response back to a .NET HttpResponseMessage. +# There are limited options to perform synchronous operations in Windows PowerShell 5.1 on .NET 4.6, so this is a workaround. +class SynchronousTransport : Sentry.Http.HttpTransportBase, Sentry.Extensibility.ITransport +{ + hidden [Sentry.Extensibility.IDiagnosticLogger] $logger + hidden [System.Reflection.MethodInfo] $ProcessEnvelope + hidden [System.Reflection.MethodInfo] $CreateRequest + hidden [System.Reflection.MethodInfo] $SerializeToStream + + SynchronousTransport([Sentry.SentryOptions] $options) : base($options) + { + $this.logger = $options.DiagnosticLogger + + # These are internal methods, so we need to use reflection to access them. + $instanceMethod = [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public; + $this.ProcessEnvelope = [Sentry.Http.HttpTransportBase].GetMethod('ProcessEnvelope', $instanceMethod) + $this.CreateRequest = [Sentry.Http.HttpTransportBase].GetMethod('CreateRequest', $instanceMethod) + $EnvelopeHttpContentType = [Sentry.SentrySdk].Assembly.GetType('Sentry.Internal.Http.EnvelopeHttpContent') + $this.SerializeToStream = $EnvelopeHttpContentType.GetMethod('SerializeToStream', $instanceMethod) + } + + [System.Threading.Tasks.Task] SendEnvelopeAsync([Sentry.Protocol.Envelopes.Envelope] $envelope, [System.Threading.CancellationToken]$cancellationToken = [System.Threading.CancellationToken]::None) + { + $processedEnvelope = $this.ProcessEnvelope.Invoke($this, @($envelope)) + if ($processedEnvelope.Items.count -gt 0) + { + $request = $this.CreateRequest.Invoke($this, @($processedEnvelope)) + + $headers = @{} + foreach ($header in $request.Headers) + { + $Key = $header.Key + $Value = $header.Value.Trim() -join ', ' + $headers[$Key] = $Value + } + + $memoryStream = [System.IO.MemoryStream]::new() + $this.SerializeToStream.Invoke($request.Content, @($memoryStream, $null, $cancellationToken)) + $memoryStream.Position = 0 + + $reader = New-Object System.IO.StreamReader($memoryStream) + $content = $reader.ReadToEnd() + $reader.Close() + + $this.logger.Log([Sentry.SentryLevel]::Debug, 'Sending content synchronously, Content-Length: {0}', $null, $content.Length) + + $ProgressPreference = 'SilentlyContinue' + $psResponse = Invoke-WebRequest -Uri $request.RequestUri -Method $request.Method.Method -Headers $headers -Body $content -UseBasicParsing + + $response = [System.Net.Http.HttpResponseMessage]::new($psResponse.StatusCode) + $contentType = $psResponse.Headers['Content-Type'] + if ($null -eq $contentType) + { + $contentType = 'application/json' + } + $response.Content = [System.Net.Http.StringContent]::new($psResponse.Content, [System.Text.Encoding]::UTF8, $contentType) + + foreach ($header in $psResponse.Headers.GetEnumerator()) + { + $response.Headers.TryAddWithoutValidation($header.Key, $header.Value) + } + + $this.HandleResponse($response, $processedEnvelope) + } + + return [System.Threading.Tasks.Task]::CompletedTask + } +} diff --git a/modules/Sentry/public/Start-Sentry.ps1 b/modules/Sentry/public/Start-Sentry.ps1 index e272dec..91d7c68 100644 --- a/modules/Sentry/public/Start-Sentry.ps1 +++ b/modules/Sentry/public/Start-Sentry.ps1 @@ -1,6 +1,7 @@ . "$privateDir/DiagnosticLogger.ps1" . "$privateDir/ScopeIntegration.ps1" . "$privateDir/SynchronousWorker.ps1" +. "$privateDir/SynchronousTransport.ps1" . "$privateDir/EventUpdater.ps1" function Start-Sentry @@ -48,6 +49,18 @@ function Start-Sentry $logger = [DiagnosticLogger]::new($options.DiagnosticLevel) $options.DiagnosticLogger = $logger + if ($null -eq $options.Transport) + { + try + { + $options.Transport = [SynchronousTransport]::new($options) + } + catch + { + $logger.Log([Sentry.SentryLevel]::Warning, 'Failed to create a PowerShell-specific synchronous transport', $_.Exception, @()) + } + } + if ($null -eq $options.BackgroundWorker) { try diff --git a/samples/locate-city.ps1 b/samples/locate-city.ps1 index 4b9ed7c..250151c 100644 --- a/samples/locate-city.ps1 +++ b/samples/locate-city.ps1 @@ -65,7 +65,7 @@ try catch { $_ | Out-Sentry - "⚠️ Error in line $($_.InvocationInfo.ScriptLineNumber): $($Error[0])" + "⚠️ Error on line $($_.InvocationInfo.ScriptLineNumber): $($Error[0])" } finally {