diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7e1ad..a59798e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add SynchronousTransport implementation ([#59](https://github.com/SummitHosting/sentry-powershell/pull/59)) + ### 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..4bc1415 --- /dev/null +++ b/modules/Sentry/private/SynchronousTransport.ps1 @@ -0,0 +1,62 @@ +class SynchronousTransport : Sentry.Http.HttpTransportBase, Sentry.Extensibility.ITransport +{ + hidden [System.Net.Http.HttpClient] $httpClient + hidden [Sentry.SentryOptions] $options + + SynchronousTransport([Sentry.SentryOptions] $options) + : base($options) + { + $this.options = $options + $this.httpClient = [System.Net.Http.HttpClient]::new() + } + + [System.Threading.Tasks.Task] SendEnvelopeAsync([Sentry.Protocol.Envelopes.Envelope] $envelope, [System.Threading.CancellationToken]$cancellationToken = [System.Threading.CancellationToken]::None) + { + # 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. + $assembly = [Sentry.SentrySdk].Assembly + $type = $assembly.GetType('Sentry.Http.HttpTransportBase') + $ProcessEnvelope = $type.GetMethod('ProcessEnvelope', [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public) + $CreateRequest = $type.GetMethod('CreateRequest', [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public) + $HandleResponse = $type.GetMethod('HandleResponse', [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public) + + $processedEnvelope = $ProcessEnvelope.Invoke($this, @($envelope)) + if ($processedEnvelope.Items.count -gt 0) + { + $request = $CreateRequest.Invoke($this, @($processedEnvelope)) + + $headers = @{} + foreach ($header in $request.Headers) { + $Key = $header.Key + $Value = $header.Value.Trim() -join ", " + $headers[$Key] = $Value + } + + $EnvelopeHttpContentType = $assembly.GetType('Sentry.Internal.Http.EnvelopeHttpContent') + $SerializeToStream = $EnvelopeHttpContentType.GetMethod('SerializeToStream', [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public) + + $memoryStream = [System.IO.MemoryStream]::new() + $SerializeToStream.Invoke($request.Content, @($memoryStream, $null, $cancellationToken)) + $memoryStream.Position = 0 + + $reader = New-Object System.IO.StreamReader($memoryStream) + $content = $reader.ReadToEnd() + $reader.Close() + + $this.options.DiagnosticLogger.Log([Sentry.SentryLevel]::Debug, "Sending content synchronously, Content-Length: {0}", $null, $content.Length) + + $psResponse = Invoke-WebRequest -Uri $request.RequestUri -Method "POST" -Headers $headers -Body $content -UseBasicParsing + + $response = [System.Net.Http.HttpResponseMessage]::new($psResponse.StatusCode) + $response.Content = [System.Net.Http.StringContent]::new($psResponse.Content, [System.Text.Encoding]::UTF8, "application/json") + + foreach ($header in $psResponse.Headers.GetEnumerator()) { + $response.Headers.TryAddWithoutValidation($header.Key, $header.Value) + } + + $HandleResponse.Invoke($this, @($response, $processedEnvelope)) + } + + return [System.Threading.Tasks.Task]::CompletedTask + } +} \ No newline at end of file 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