From e29bb0ec0e299f5714340ac7e0297fc7951228d7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 22 May 2026 14:39:50 +1200 Subject: [PATCH 1/3] Add Add-SentryEventProcessor cmdlet Public cmdlet that registers a global event processor backed by a PowerShell script block. The block receives the event via the automatic $_ variable (matching Edit-SentryScope), returns the event to send it, or $null to drop it. Internally the block is wrapped by a new C# ScriptBlockEventProcessor that implements ISentryEventProcessor directly. Implementing the interface in C# (rather than via a PowerShell class deriving from it) sidesteps PowerShell's parse-time base-type resolution, which is the root cause of the noisy 'Unable to find type [SentryEventProcessor]' errors on module import. Subsequent commits migrate the internal EventUpdater and StackTraceProcessor onto this same mechanism so the PowerShell class files can be removed entirely. Co-Authored-By: Claude Opus 4.7 --- modules/Sentry/Sentry.psd1 | 1 + modules/Sentry/Sentry.psm1 | 1 + .../private/ScriptBlockEventProcessor.cs | 54 +++++++++++++++++++ .../public/Add-SentryEventProcessor.ps1 | 37 +++++++++++++ tests/add-sentry-event-processor.tests.ps1 | 54 +++++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 modules/Sentry/private/ScriptBlockEventProcessor.cs create mode 100644 modules/Sentry/public/Add-SentryEventProcessor.ps1 create mode 100644 tests/add-sentry-event-processor.tests.ps1 diff --git a/modules/Sentry/Sentry.psd1 b/modules/Sentry/Sentry.psd1 index 2e9abe4..cb96b38 100644 --- a/modules/Sentry/Sentry.psd1 +++ b/modules/Sentry/Sentry.psd1 @@ -33,6 +33,7 @@ # 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', + 'Add-SentryEventProcessor', 'Edit-SentryScope', 'Invoke-WithSentry', 'Out-Sentry', diff --git a/modules/Sentry/Sentry.psm1 b/modules/Sentry/Sentry.psm1 index 595b59d..c72451e 100644 --- a/modules/Sentry/Sentry.psm1 +++ b/modules/Sentry/Sentry.psm1 @@ -6,6 +6,7 @@ $moduleInfo = Import-PowerShellDataFile (Join-Path (Split-Path -Parent $MyInvoca $sentryDllPath = (Join-Path (Get-SentryAssembliesDirectory) 'Sentry.dll') Add-Type -TypeDefinition (Get-Content "$privateDir/SentryEventProcessor.cs" -Raw) -ReferencedAssemblies $sentryDllPath -Debug:$false +Add-Type -TypeDefinition (Get-Content "$privateDir/ScriptBlockEventProcessor.cs" -Raw) -ReferencedAssemblies $sentryDllPath -Debug:$false . "$privateDir/SentryEventProcessor.ps1" Get-ChildItem $publicDir -Filter '*.ps1' | ForEach-Object { diff --git a/modules/Sentry/private/ScriptBlockEventProcessor.cs b/modules/Sentry/private/ScriptBlockEventProcessor.cs new file mode 100644 index 0000000..a44edee --- /dev/null +++ b/modules/Sentry/private/ScriptBlockEventProcessor.cs @@ -0,0 +1,54 @@ +using System; +using System.Management.Automation; +using Sentry; +using Sentry.Extensibility; + +// Wraps a PowerShell ScriptBlock as an ISentryEventProcessor. The wrapped block receives the +// event via `$_` (and as the first positional arg) and is expected to return the event to keep +// it, or $null to drop it. Implementing ISentryEventProcessor directly here lets the module +// avoid PowerShell `class : Base` definitions, which fail to resolve their base type at +// parse time (the base type only exists after the Sentry assembly is loaded at runtime). +public sealed class ScriptBlockEventProcessor : ISentryEventProcessor +{ + private readonly ScriptBlock _scriptBlock; + private readonly IDiagnosticLogger _logger; + + public ScriptBlockEventProcessor(ScriptBlock scriptBlock, IDiagnosticLogger logger) + { + if (scriptBlock == null) throw new ArgumentNullException("scriptBlock"); + _scriptBlock = scriptBlock; + _logger = logger; + } + + public SentryEvent Process(SentryEvent @event) + { + try + { + var results = _scriptBlock.Invoke(@event); + if (results == null || results.Count == 0) + { + return @event; + } + + var last = results[results.Count - 1]; + if (last == null) + { + return null; + } + + return (last.BaseObject as SentryEvent) ?? @event; + } + catch (Exception ex) + { + if (_logger != null) + { + _logger.Log( + SentryLevel.Warning, + "Event processor scriptblock failed for event {0}: {1}", + ex, + new object[] { @event.EventId, ex.Message }); + } + return @event; + } + } +} diff --git a/modules/Sentry/public/Add-SentryEventProcessor.ps1 b/modules/Sentry/public/Add-SentryEventProcessor.ps1 new file mode 100644 index 0000000..8d4697c --- /dev/null +++ b/modules/Sentry/public/Add-SentryEventProcessor.ps1 @@ -0,0 +1,37 @@ +. "$privateDir/Get-CurrentOptions.ps1" + +<# +.SYNOPSIS + Registers a global event processor that runs on every event before it is sent to Sentry. +.DESCRIPTION + The script block receives the Sentry event via the automatic variable $_ (matching the + convention used by Edit-SentryScope). Return the event to send it, or $null to drop it. +.EXAMPLE + PS> Add-SentryEventProcessor { $_.SetTag('host', $env:COMPUTERNAME); $_ } +.EXAMPLE + PS> Add-SentryEventProcessor { + if ($_.Message -match 'secret') { return $null } + $_ + } +#> +function Add-SentryEventProcessor { + param( + [Parameter(Mandatory, Position = 0)] + [scriptblock] $ScriptBlock + ) + + $options = Get-CurrentOptions + if ($null -eq $options) { + throw 'Sentry is not initialized. Call Start-Sentry before adding an event processor.' + } + + # Wrap the user's script block in a pipeline so that $_ is bound to the event, + # matching Edit-SentryScope's convention. + $wrapped = { + param([Sentry.SentryEvent] $event_) + $event_ | ForEach-Object $ScriptBlock + }.GetNewClosure() + + $options.AddEventProcessor( + [ScriptBlockEventProcessor]::new($wrapped, $options.DiagnosticLogger)) +} diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 new file mode 100644 index 0000000..681eb3b --- /dev/null +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -0,0 +1,54 @@ +BeforeAll { + . "$PSScriptRoot/utils.ps1" + $global:SentryPowershellRethrowErrors = $true +} + +AfterAll { + $global:SentryPowershellRethrowErrors = $false +} + +Describe 'Add-SentryEventProcessor' { + BeforeEach { + $events = [System.Collections.Generic.List[Sentry.SentryEvent]]::new(); + $transport = [RecordingTransport]::new() + StartSentryForEventTests ([ref] $events) ([ref] $transport) + } + + AfterEach { + $events.Clear() + Stop-Sentry + } + + It 'Mutates events via $_' { + Add-SentryEventProcessor { $_.SetTag('custom', 'value'); $_ } + 'msg' | Out-Sentry + + $events[0].Tags['custom'] | Should -Be 'value' + } + + It 'Drops events when the script block returns $null' { + Add-SentryEventProcessor { + if ($_.Message.Message -match 'drop-me') { return $null } + $_ + } + 'drop-me please' | Out-Sentry + 'keep this one' | Out-Sentry + + $events.Count | Should -Be 1 + $events[0].Message.Message | Should -Be 'keep this one' + } + + It 'Chains multiple processors in registration order' { + Add-SentryEventProcessor { $_.SetTag('first', '1'); $_ } + Add-SentryEventProcessor { $_.SetTag('second', '2'); $_ } + 'msg' | Out-Sentry + + $events[0].Tags['first'] | Should -Be '1' + $events[0].Tags['second'] | Should -Be '2' + } + + It 'Throws when Sentry is not initialized' { + Stop-Sentry + { Add-SentryEventProcessor { $_ } } | Should -Throw '*Sentry is not initialized*' + } +} From c1b6aea15de4bd93b5d7b3488dbe871e978e90fb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 22 May 2026 14:40:39 +1200 Subject: [PATCH 2/3] Migrate EventUpdater to ScriptBlockEventProcessor Replace the PowerShell class-based EventUpdater with an equivalent script block registered through ScriptBlockEventProcessor. Behavior is unchanged: the event Platform is set to 'powershell' and a bogus pwsh-versioned Release set by the .NET SDK is cleared. This eliminates one of the three PowerShell class-based event processors. StackTraceProcessor follows in the next commit; the parse-time errors won't disappear until that one is migrated too. Co-Authored-By: Claude Opus 4.7 --- modules/Sentry/private/EventUpdater.ps1 | 13 ------------- modules/Sentry/public/Start-Sentry.ps1 | 20 ++++++++++++++++++-- 2 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 modules/Sentry/private/EventUpdater.ps1 diff --git a/modules/Sentry/private/EventUpdater.ps1 b/modules/Sentry/private/EventUpdater.ps1 deleted file mode 100644 index 0c99fe7..0000000 --- a/modules/Sentry/private/EventUpdater.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -class EventUpdater : SentryEventProcessor { - [Sentry.SentryEvent]DoProcess([Sentry.SentryEvent] $event_) { - $event_.Platform = 'powershell' - - # Clear useless release set by the .NET SDK (referring to the PowerShell assembly version) - # "pwsh@7.4.1 SHA: 6a98b28414948626f1b29a5e8b062e73b7ff165a+6a98b28414948626f1b29a5e8b062e73b7ff165a" - if ($event_.Release -match "pwsh@$($global:PSVersionTable.PSVersion) .*") { - $event_.Release = $null - } - - return $event_ - } -} diff --git a/modules/Sentry/public/Start-Sentry.ps1 b/modules/Sentry/public/Start-Sentry.ps1 index 3b7476c..cda8024 100644 --- a/modules/Sentry/public/Start-Sentry.ps1 +++ b/modules/Sentry/public/Start-Sentry.ps1 @@ -2,7 +2,22 @@ . "$privateDir/ScopeIntegration.ps1" . "$privateDir/SynchronousWorker.ps1" . "$privateDir/SynchronousTransport.ps1" -. "$privateDir/EventUpdater.ps1" + +# Built-in event processor that fixes up platform/release on every event. +# Implemented as a script block wrapped in ScriptBlockEventProcessor rather than a +# PowerShell class so the module avoids parse-time base-type resolution errors. +$script:EventUpdaterScript = { + param([Sentry.SentryEvent] $event_) + $event_.Platform = 'powershell' + + # Clear the useless release set by the .NET SDK (referring to the PowerShell assembly version): + # "pwsh@7.4.1 SHA: 6a98b28414948626f1b29a5e8b062e73b7ff165a+6a98b28414948626f1b29a5e8b062e73b7ff165a" + if ($event_.Release -match "pwsh@$($global:PSVersionTable.PSVersion) .*") { + $event_.Release = $null + } + + $event_ +} function Start-Sentry { [CmdletBinding(DefaultParameterSetName = 'Simple')] @@ -21,7 +36,8 @@ function Start-Sentry { $options.ReportAssembliesMode = [Sentry.ReportAssembliesMode]::None $options.IsGlobalModeEnabled = $true $options.AddIntegration([ScopeIntegration]::new()) - $options.AddEventProcessor([EventUpdater]::new()) + $options.AddEventProcessor( + [ScriptBlockEventProcessor]::new($script:EventUpdaterScript, $null)) if ($DebugPreference -eq 'SilentlyContinue') { $Options.Debug = $false From dcc6d7eb30322a4ac568c6e8b19bce51aee366a8 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 22 May 2026 14:47:27 +1200 Subject: [PATCH 3/3] Migrate StackTraceProcessor to ScriptBlockEventProcessor Replace the PowerShell class-based StackTraceProcessor with a factory function New-StackTraceEventProcessor that captures per-event state via closure and returns a ScriptBlockEventProcessor. Behavior is intended to match the previous class exactly; all helper methods are defined inline inside the returned script block. Out-Sentry is updated to gather state into local variables and call the factory immediately before CaptureEvent, replacing the previous mutate-the-processor-instance pattern. With this commit the module no longer defines any PowerShell `class : SentryEventProcessor` subclasses, so the C# base SentryEventProcessor_ (which existed only to host the Process_ / DoProcess workaround) is also removed. The 'Unable to find type [SentryEventProcessor]' parse errors on module import should be gone. Co-Authored-By: Claude Opus 4.7 --- modules/Sentry/Sentry.psm1 | 5 +- .../private/New-StackTraceEventProcessor.ps1 | 319 +++++++++++++++++ .../Sentry/private/SentryEventProcessor.cs | 13 - .../Sentry/private/SentryEventProcessor.ps1 | 18 - .../Sentry/private/StackTraceProcessor.ps1 | 333 ------------------ modules/Sentry/public/Out-Sentry.ps1 | 38 +- tests/stacktrace-processor.tests.ps1 | 14 +- 7 files changed, 354 insertions(+), 386 deletions(-) create mode 100644 modules/Sentry/private/New-StackTraceEventProcessor.ps1 delete mode 100644 modules/Sentry/private/SentryEventProcessor.cs delete mode 100644 modules/Sentry/private/SentryEventProcessor.ps1 delete mode 100644 modules/Sentry/private/StackTraceProcessor.ps1 diff --git a/modules/Sentry/Sentry.psm1 b/modules/Sentry/Sentry.psm1 index c72451e..9993383 100644 --- a/modules/Sentry/Sentry.psm1 +++ b/modules/Sentry/Sentry.psm1 @@ -5,9 +5,8 @@ $moduleInfo = Import-PowerShellDataFile (Join-Path (Split-Path -Parent $MyInvoca . "$privateDir/Get-SentryAssembliesDirectory.ps1" $sentryDllPath = (Join-Path (Get-SentryAssembliesDirectory) 'Sentry.dll') -Add-Type -TypeDefinition (Get-Content "$privateDir/SentryEventProcessor.cs" -Raw) -ReferencedAssemblies $sentryDllPath -Debug:$false -Add-Type -TypeDefinition (Get-Content "$privateDir/ScriptBlockEventProcessor.cs" -Raw) -ReferencedAssemblies $sentryDllPath -Debug:$false -. "$privateDir/SentryEventProcessor.ps1" +$automationDllPath = [System.Management.Automation.PSObject].Assembly.Location +Add-Type -TypeDefinition (Get-Content "$privateDir/ScriptBlockEventProcessor.cs" -Raw) -ReferencedAssemblies @($sentryDllPath, $automationDllPath) -Debug:$false Get-ChildItem $publicDir -Filter '*.ps1' | ForEach-Object { . $_.FullName diff --git a/modules/Sentry/private/New-StackTraceEventProcessor.ps1 b/modules/Sentry/private/New-StackTraceEventProcessor.ps1 new file mode 100644 index 0000000..f155e4d --- /dev/null +++ b/modules/Sentry/private/New-StackTraceEventProcessor.ps1 @@ -0,0 +1,319 @@ +# Factory for the built-in stack-trace event processor. Replaces the previous +# PowerShell class-based StackTraceProcessor so the module no longer relies on +# `class : Base` syntax (whose base-type resolution happens at parse time and +# fails for classes whose base is defined at runtime by assemblies-loader.ps1). +# +# Behavior is intended to match the previous class exactly; the per-event state +# that used to live on instance fields is now captured by closure inside the +# returned ScriptBlockEventProcessor. +function New-StackTraceEventProcessor { + param( + [Parameter(Mandatory)][Sentry.SentryOptions] $Options, + [Sentry.Protocol.SentryException] $SentryException, + [System.Management.Automation.InvocationInfo] $InvocationInfo, + [System.Management.Automation.CallStackFrame[]] $StackTraceFrames, + [string[]] $StackTraceString + ) + + $logger = $Options.DiagnosticLogger + if ($null -eq $logger) { + $logger = Get-Variable -Scope script -Name SentryPowerShellDiagnosticLogger -ValueOnly -ErrorAction SilentlyContinue + } + + if ($env:PSModulePath.Contains(';')) { + $modulePaths = $env:PSModulePath -split ';' + } else { + $modulePaths = $env:PSModulePath -split ':' + } + + $state = @{ + SentryException = $SentryException + InvocationInfo = $InvocationInfo + StackTraceFrames = $StackTraceFrames + StackTraceString = $StackTraceString + Logger = $logger + ModulePaths = $modulePaths + PwshModules = @{} + } + + $processorScript = { + param([Sentry.SentryEvent] $event_) + + $s = $state + + function SetModule([Sentry.SentryStackFrame] $sentryFrame) { + if ([string]::IsNullOrEmpty($sentryFrame.AbsolutePath)) { return } + $prefix = $s.ModulePaths | Where-Object { $sentryFrame.AbsolutePath.StartsWith($_) } + if (-not $prefix) { return } + $relativePath = $sentryFrame.AbsolutePath.Substring($prefix.Length + 1) + $parts = $relativePath -split '[\\/]' + $sentryFrame.Module = $parts | Select-Object -First 1 + if ($parts.Length -ge 2) { + if (-not $s.PwshModules.ContainsKey($parts[0])) { + $s.PwshModules[$parts[0]] = $parts[1] + } elseif ($s.PwshModules[$parts[0]] -ne $parts[1]) { + $s.PwshModules[$parts[0]] = $s.PwshModules[$parts[0]] + ", $($parts[1])" + } + } + } + + function SetScriptInfo([Sentry.SentryStackFrame] $sentryFrame, [System.Management.Automation.CallStackFrame] $frame) { + if (![string]::IsNullOrEmpty($frame.ScriptName)) { + $sentryFrame.AbsolutePath = $frame.ScriptName + $sentryFrame.LineNumber = $frame.ScriptLineNumber + } elseif (![string]::IsNullOrEmpty($frame.Position) -and ![string]::IsNullOrEmpty($frame.Position.File)) { + $sentryFrame.AbsolutePath = $frame.Position.File + $sentryFrame.LineNumber = $frame.Position.StartLineNumber + $sentryFrame.ColumnNumber = $frame.Position.StartColumnNumber + } + } + + function SetFunction([Sentry.SentryStackFrame] $sentryFrame, [System.Management.Automation.CallStackFrame] $frame) { + if ([string]::IsNullOrEmpty($sentryFrame.AbsolutePath) -and $frame.FunctionName -eq '' -and ![string]::IsNullOrEmpty($frame.Position)) { + $sentryFrame.Function = $frame.Position.Text + if ($sentryFrame.Function.Contains("`n")) { + $lines = $sentryFrame.Function -split "[`r`n]+" + $sentryFrame.Function = $lines[0] + ' ' + if ($lines.Count -gt 2) { + $sentryFrame.Function += ' ...... ' + } + $sentryFrame.Function += $lines[$lines.Count - 1] + } + } else { + $sentryFrame.Function = $frame.FunctionName + } + } + + function SetContextLinesFromArray([Sentry.SentryStackFrame] $sentryFrame, [string[]] $lines) { + if ($lines.Count -lt $sentryFrame.LineNumber) { + if ($null -ne $s.Logger) { + $s.Logger.Log( + [Sentry.SentryLevel]::Debug, + "Couldn't set frame context because the line number ($($sentryFrame.LineNumber)) " + + "is lower than the available number of source code lines ($($lines.Count)).", + $null, @()) + } + return + } + + $numContextLines = 5 + + if ($null -eq $sentryFrame.ContextLine) { + $sentryFrame.ContextLine = $lines[$sentryFrame.LineNumber - 1] + } + + $preContextCount = [math]::Min($numContextLines, $sentryFrame.LineNumber - 1) + $postContextCount = [math]::Min($numContextLines, $lines.Count - $sentryFrame.LineNumber) + + if ($sentryFrame.LineNumber -gt $numContextLines + 1) { + $lines = $lines | Select-Object -Skip ($sentryFrame.LineNumber - $numContextLines - 1) + } + + $sentryFrame.PreContext.Clear() + $lines | Select-Object -First $preContextCount | ForEach-Object { $sentryFrame.PreContext.Add($_) } + $sentryFrame.PostContext.Clear() + $lines | Select-Object -First $postContextCount -Skip ($preContextCount + 1) | ForEach-Object { $sentryFrame.PostContext.Add($_) } + } + + function SetContextLinesFromFile([Sentry.SentryStackFrame] $sentryFrame) { + if ([string]::IsNullOrEmpty($sentryFrame.AbsolutePath) -or $sentryFrame.LineNumber -lt 1) { return } + if ((Test-Path $sentryFrame.AbsolutePath -IsValid) -and (Test-Path $sentryFrame.AbsolutePath -PathType Leaf)) { + try { + $lines = Get-Content $sentryFrame.AbsolutePath -TotalCount ($sentryFrame.LineNumber + 5) + SetContextLinesFromArray $sentryFrame $lines + } catch { + Write-Warning "Failed to read context lines for $($sentryFrame.AbsolutePath): $_" + if ($global:SentryPowershellRethrowErrors -eq $true) { throw } + } + } + } + + function SetContextLinesFromFrame([Sentry.SentryStackFrame] $sentryFrame, [System.Management.Automation.CallStackFrame] $frame) { + if ($sentryFrame.LineNumber -gt 0) { + try { + $lines = $frame.InvocationInfo.MyCommand.ScriptBlock.ToString() -split "`n" + SetContextLinesFromArray $sentryFrame $lines + } catch { + Write-Warning "Failed to read context lines for frame with function '$($sentryFrame.Function)': $_" + if ($global:SentryPowershellRethrowErrors -eq $true) { throw } + } + } + } + + function CreateFrameFromInvocation([System.Management.Automation.InvocationInfo] $info) { + $sentryFrame = [Sentry.SentryStackFrame]::new() + $sentryFrame.AbsolutePath = $info.ScriptName + $sentryFrame.LineNumber = $info.ScriptLineNumber + $sentryFrame.ColumnNumber = $info.OffsetInLine + $sentryFrame.ContextLine = $info.Line.TrimEnd() + return $sentryFrame + } + + function CreateFrameFromCallStack([System.Management.Automation.CallStackFrame] $frame) { + $sentryFrame = [Sentry.SentryStackFrame]::new() + SetScriptInfo $sentryFrame $frame + SetModule $sentryFrame + SetFunction $sentryFrame $frame + return $sentryFrame + } + + function CreateFrameFromString([string] $frame) { + $sentryFrame = [Sentry.SentryStackFrame]::new() + # at funcB, C:\dev\sentry-powershell\tests\capture.tests.ps1: line 363 + $regex = 'at (?[^,]*), (?.*): line (?\d*)' + if ($frame -match $regex) { + if ($Matches.AbsolutePath -ne '') { + $sentryFrame.AbsolutePath = $Matches.AbsolutePath + } + $sentryFrame.LineNumber = [int]$Matches.LineNumber + $sentryFrame.Function = $Matches.Function + } else { + Write-Warning "Failed to parse stack frame: $frame" + } + return $sentryFrame + } + + function EnhanceTailFrames([Sentry.SentryStackFrame[]] $sentryFrames) { + if ($null -eq $s.StackTraceFrames) { return } + + $i = 0 + for ($j = $sentryFrames.Count - 1; $j -ge 0; $j--) { + $sentryFrame = $sentryFrames[$j] + $frame = $s.StackTraceFrames | Select-Object -Last 1 -Skip $i + $i++ + + if ($null -eq $frame) { break } + + if ($null -eq $sentryFrame.AbsolutePath -and $null -eq $frame.ScriptName) { + if ($frame.ScriptLineNumber -gt 0 -and $frame.ScriptLineNumber -eq $sentryFrame.LineNumber) { + SetScriptInfo $sentryFrame $frame + SetModule $sentryFrame + SetFunction $sentryFrame $frame + } + SetContextLinesFromFrame $sentryFrame $frame + + while ($j -gt 0) { + $nextSentryFrame = $sentryFrames[$j - 1] + if ($nextSentryFrame.AbsolutePath -ne $sentryFrame.AbsolutePath) { break } + SetContextLinesFromFrame $nextSentryFrame $frame + $j-- + } + } + } + } + + function GetStackTrace { + # Collect all frames and then reverse them to the order expected by Sentry (caller->callee). + # Do not try to make this code go backwards because it relies on the InvocationInfo from the previous frame. + $sentryFrames = New-Object System.Collections.Generic.List[Sentry.SentryStackFrame] + if ($null -ne $s.StackTraceString) { + $sentryFrames.Capacity = $s.StackTraceString.Count + 1 + if ($null -ne $s.InvocationInfo) { + $sentryFrameInitial = CreateFrameFromInvocation $s.InvocationInfo + } else { + $sentryFrameInitial = $null + } + + foreach ($frame in $s.StackTraceString) { + $sentryFrame = CreateFrameFromString $frame + if ($null -ne $sentryFrameInitial -and $sentryFrames.Count -lt 2) { + if ($sentryFrameInitial.AbsolutePath -eq $sentryFrame.AbsolutePath -and $sentryFrameInitial.LineNumber -eq $sentryFrame.LineNumber) { + $sentryFrame.ContextLine = $sentryFrameInitial.ContextLine + $sentryFrame.ColumnNumber = $sentryFrameInitial.ColumnNumber + $sentryFrameInitial = $null + } + } + $sentryFrames.Add($sentryFrame) + } + + if ($null -ne $sentryFrameInitial) { + $sentryFrames.Insert(0, $sentryFrameInitial) + } + + EnhanceTailFrames $sentryFrames + } elseif ($null -ne $s.StackTraceFrames) { + $sentryFrames.Capacity = $s.StackTraceFrames.Count + 1 + foreach ($frame in $s.StackTraceFrames) { + $sentryFrames.Add((CreateFrameFromCallStack $frame)) + } + } + + foreach ($sentryFrame in $sentryFrames) { + SetModule $sentryFrame + $sentryFrame.InApp = [string]::IsNullOrEmpty($sentryFrame.Module) + SetContextLinesFromFile $sentryFrame + } + + $sentryFrames.Reverse() + $stacktrace_ = [Sentry.SentryStackTrace]::new() + $stacktrace_.Frames = $sentryFrames + return $stacktrace_ + } + + function PrependThread([Sentry.SentryEvent] $evt, [Sentry.SentryStackTrace] $sentryStackTrace) { + $newThreads = New-Object System.Collections.Generic.List[Sentry.SentryThread] + $thread = New-Object Sentry.SentryThread + $thread.Id = 0 + $thread.Name = 'PowerShell Script' + $thread.Crashed = $true + $thread.Current = $true + $thread.Stacktrace = $sentryStackTrace + $newThreads.Add($thread) + if ($null -ne $evt.SentryThreads) { + foreach ($t in $evt.SentryThreads) { + $t.Crashed = $false + $t.Current = $false + $newThreads.Add($t) + } + } + $evt.SentryThreads = $newThreads + } + + # ---- main entry ---- + + if ($null -ne $s.SentryException) { + $s.SentryException.Stacktrace = GetStackTrace + if ($s.SentryException.Stacktrace.Frames.Count -gt 0) { + $topFrame = $s.SentryException.Stacktrace.Frames | Select-Object -Last 1 + $s.SentryException.Module = $topFrame.Module + } + + $newExceptions = New-Object System.Collections.Generic.List[Sentry.Protocol.SentryException] + if ($null -ne $event_.SentryExceptions) { + foreach ($e in $event_.SentryExceptions) { + if ($null -eq $e.Mechanism) { + $e.Mechanism = [Sentry.Protocol.Mechanism]::new() + } + $e.Mechanism.Synthetic = $true + $newExceptions.Add($e) + } + } + $newExceptions.Add($s.SentryException) + $event_.SentryExceptions = $newExceptions + PrependThread $event_ $s.SentryException.Stacktrace + } elseif ($null -ne $event_.Message) { + PrependThread $event_ (GetStackTrace) + } + + # Add modules present in PowerShell + foreach ($module in $s.PwshModules.GetEnumerator()) { + $event_.Modules[$module.Name] = $module.Value + } + + # Add .NET modules. Note: we don't let sentry-dotnet do it because it would just add all the loaded assemblies, + # regardless of their presence in a stacktrace. So we set the option ReportAssembliesMode=None in [Start-Sentry]. + foreach ($thread in $event_.SentryThreads) { + foreach ($frame in $thread.Stacktrace.Frames) { + # .NET SDK sets the assembly info to frame.Package, for example: + # "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" + if ($frame.Package -match '^(?[^,]+), Version=(?[^,]+), ') { + $event_.Modules[$Matches.Assembly] = $Matches.Version + } + } + } + + return $event_ + }.GetNewClosure() + + return [ScriptBlockEventProcessor]::new($processorScript, $logger) +} diff --git a/modules/Sentry/private/SentryEventProcessor.cs b/modules/Sentry/private/SentryEventProcessor.cs deleted file mode 100644 index de25a2f..0000000 --- a/modules/Sentry/private/SentryEventProcessor.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This is an abstract class for any PowerShell event processors. It gets around an issue with Windows PowerShell -// failing to compile scripts that have a method name `Process`, which is a reserved word. -// https://stackoverflow.com/questions/78001695/windows-powershell-implement-c-sharp-interface-with-reserved-words-as-method-n/78001981 -// This way, we can keep the PowerShell implementation of the event processor, with access to System.Management.Automation, etc. -public abstract class SentryEventProcessor_ : Sentry.Extensibility.ISentryEventProcessor -{ - public Sentry.SentryEvent Process(Sentry.SentryEvent event_) - { - return Process_(event_); - } - - protected abstract Sentry.SentryEvent Process_(Sentry.SentryEvent event_); -} diff --git a/modules/Sentry/private/SentryEventProcessor.ps1 b/modules/Sentry/private/SentryEventProcessor.ps1 deleted file mode 100644 index 0c7b31d..0000000 --- a/modules/Sentry/private/SentryEventProcessor.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -class SentryEventProcessor : SentryEventProcessor_ { - [Sentry.SentryEvent]DoProcess([Sentry.SentryEvent] $event_) { - throw [NotImplementedException]::new('You must override SentryEventProcessor::DoProcess()') - } - - [Sentry.SentryEvent]Process_([Sentry.SentryEvent] $event_) { - try { - return $this.DoProcess($event_) - } catch { - $ErrorRecord = $_ - "$($this.GetType()) failed to process event $($event_.EventId):" | Write-Warning - $ErrorRecord | Format-List * -Force | Out-String | Write-Warning - $ErrorRecord.InvocationInfo | Format-List * | Out-String | Write-Warning - $ErrorRecord.Exception | Format-List * -Force | Out-String | Write-Warning - return $event_ - } - } -} diff --git a/modules/Sentry/private/StackTraceProcessor.ps1 b/modules/Sentry/private/StackTraceProcessor.ps1 deleted file mode 100644 index a89e6e7..0000000 --- a/modules/Sentry/private/StackTraceProcessor.ps1 +++ /dev/null @@ -1,333 +0,0 @@ -class StackTraceProcessor : SentryEventProcessor { - [Sentry.Protocol.SentryException]$SentryException - [System.Management.Automation.InvocationInfo]$InvocationInfo - [System.Management.Automation.CallStackFrame[]]$StackTraceFrames - [string[]]$StackTraceString - hidden [Sentry.Extensibility.IDiagnosticLogger] $logger - hidden [string[]] $modulePaths - hidden [hashtable] $pwshModules = @{} - - StackTraceProcessor([Sentry.SentryOptions] $options) { - $this.logger = $options.DiagnosticLogger - if ($null -eq $this.logger) { - $this.logger = Get-Variable -Scope script -Name SentryPowerShellDiagnosticLogger -ValueOnly -ErrorAction SilentlyContinue - } - - if ($env:PSModulePath.Contains(';')) { - # Windows - $this.modulePaths = $env:PSModulePath -split ';' - } else { - # Unix - $this.modulePaths = $env:PSModulePath -split ':' - } - } - - [Sentry.SentryEvent]DoProcess([Sentry.SentryEvent] $event_) { - if ($null -ne $this.SentryException) { - $this.ProcessException($event_) - } elseif ($null -ne $event_.Message) { - $this.ProcessMessage($event_) - } - - # Add modules present in PowerShell - foreach ($module in $this.pwshModules.GetEnumerator()) { - $event_.Modules[$module.Name] = $module.Value - } - - # Add .NET modules. Note: we don't let sentry-dotnet do it because it would just add all the loaded assemblies, - # regardless of their presence in a stacktrace. So we set the option ReportAssembliesMode=None in [Start-Sentry]. - foreach ($thread in $event_.SentryThreads) { - foreach ($frame in $thread.Stacktrace.Frames) { - # .NET SDK sets the assembly info to frame.Package, for example: - # "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" - if ($frame.Package -match '^(?[^,]+), Version=(?[^,]+), ') { - $event_.Modules[$Matches.Assembly] = $Matches.Version - } - } - } - - return $event_ - } - - hidden ProcessMessage([Sentry.SentryEvent] $event_) { - $this.PrependThread($event_, $this.GetStackTrace()) - } - - hidden ProcessException([Sentry.SentryEvent] $event_) { - $this.SentryException.Stacktrace = $this.GetStackTrace() - if ($this.SentryException.Stacktrace.Frames.Count -gt 0) { - $topFrame = $this.SentryException.Stacktrace.Frames | Select-Object -Last 1 - $this.SentryException.Module = $topFrame.Module - } - - # Add the c# exception to the front of the exception list, followed by whatever is already there. - $newExceptions = New-Object System.Collections.Generic.List[Sentry.Protocol.SentryException] - if ($null -ne $event_.SentryExceptions) { - foreach ($e in $event_.SentryExceptions) { - if ($null -eq $e.Mechanism) { - $e.Mechanism = [Sentry.Protocol.Mechanism]::new() - } - $e.Mechanism.Synthetic = $true - $newExceptions.Add($e) - } - } - $newExceptions.Add($this.SentryException) - $event_.SentryExceptions = $newExceptions - $this.PrependThread($event_, $this.SentryException.Stacktrace) - } - - hidden PrependThread([Sentry.SentryEvent] $event_, [Sentry.SentryStackTrace] $sentryStackTrace) { - $newThreads = New-Object System.Collections.Generic.List[Sentry.SentryThread] - $thread = New-Object Sentry.SentryThread - $thread.Id = 0 - $thread.Name = 'PowerShell Script' - $thread.Crashed = $true - $thread.Current = $true - $thread.Stacktrace = $sentryStackTrace - $newThreads.Add($thread) - if ($null -ne $event_.SentryThreads) { - foreach ($t in $event_.SentryThreads) { - $t.Crashed = $false - $t.Current = $false - $newThreads.Add($t) - } - } - $event_.SentryThreads = $newThreads - } - - hidden [Sentry.SentryStackTrace]GetStackTrace() { - # We collect all frames and then reverse them to the order expected by Sentry (caller->callee). - # Do not try to make this code go backwards because it relies on the InvocationInfo from the previous frame. - $sentryFrames = New-Object System.Collections.Generic.List[Sentry.SentryStackFrame] - if ($null -ne $this.StackTraceString) { - $sentryFrames.Capacity = $this.StackTraceString.Count + 1 - # Note: if InvocationInfo is present, use it to update: - # - the first frame (in case of `$_ | Out-Sentry` in a catch clause). - # - the second frame (in case of `write-error` and `$_ | Out-Sentry` in a trap). - if ($null -ne $this.InvocationInfo) { - $sentryFrameInitial = $this.CreateFrame($this.InvocationInfo) - } else { - $sentryFrameInitial = $null - } - - foreach ($frame in $this.StackTraceString) { - $sentryFrame = $this.CreateFrame($frame) - if ($null -ne $sentryFrameInitial -and $sentryFrames.Count -lt 2) { - if ($sentryFrameInitial.AbsolutePath -eq $sentryFrame.AbsolutePath -and $sentryFrameInitial.LineNumber -eq $sentryFrame.LineNumber) { - $sentryFrame.ContextLine = $sentryFrameInitial.ContextLine - $sentryFrame.ColumnNumber = $sentryFrameInitial.ColumnNumber - $sentryFrameInitial = $null - } - } - $sentryFrames.Add($sentryFrame) - } - - if ($null -ne $sentryFrameInitial) { - $sentryFrames.Insert(0, $sentryFrameInitial) - } - - $this.EnhanceTailFrames($sentryFrames) - } elseif ($null -ne $this.StackTraceFrames) { - $sentryFrames.Capacity = $this.StackTraceFrames.Count + 1 - foreach ($frame in $this.StackTraceFrames) { - $sentryFrames.Add($this.CreateFrame($frame)) - } - } - - foreach ($sentryFrame in $sentryFrames) { - # Update module info - $this.SetModule($sentryFrame) - $sentryFrame.InApp = [string]::IsNullOrEmpty($sentryFrame.Module) - $this.SetContextLines($sentryFrame) - } - - $sentryFrames.Reverse() - $stacktrace_ = [Sentry.SentryStackTrace]::new() - $stacktrace_.Frames = $sentryFrames - return $stacktrace_ - } - - hidden [Sentry.SentryStackFrame] CreateFrame([System.Management.Automation.InvocationInfo] $info) { - $sentryFrame = [Sentry.SentryStackFrame]::new() - $sentryFrame.AbsolutePath = $info.ScriptName - $sentryFrame.LineNumber = $info.ScriptLineNumber - $sentryFrame.ColumnNumber = $info.OffsetInLine - $sentryFrame.ContextLine = $info.Line.TrimEnd() - return $sentryFrame - } - - hidden [Sentry.SentryStackFrame] CreateFrame([System.Management.Automation.CallStackFrame] $frame) { - $sentryFrame = [Sentry.SentryStackFrame]::new() - $this.SetScriptInfo($sentryFrame, $frame) - $this.SetModule($sentryFrame) - $this.SetFunction($sentryFrame, $frame) - return $sentryFrame - } - - hidden [Sentry.SentryStackFrame] CreateFrame([string] $frame) { - $sentryFrame = [Sentry.SentryStackFrame]::new() - # at funcB, C:\dev\sentry-powershell\tests\capture.tests.ps1: line 363 - $regex = 'at (?[^,]*), (?.*): line (?\d*)' - if ($frame -match $regex) { - if ($Matches.AbsolutePath -ne '') { - $sentryFrame.AbsolutePath = $Matches.AbsolutePath - } - $sentryFrame.LineNumber = [int]$Matches.LineNumber - $sentryFrame.Function = $Matches.Function - } else { - Write-Warning "Failed to parse stack frame: $frame" - } - return $sentryFrame - } - - hidden EnhanceTailFrames([Sentry.SentryStackFrame[]] $sentryFrames) { - if ($null -eq $this.StackTraceFrames) { - return - } - - # The last frame is usually how the PowerShell was invoked. We need to get this info from $this.StackTraceFrames - # - for pwsh scriptname.ps1 it would be something like `. scriptname.ps1` - # - for pwsh -c `& {..}` it would be the `& {..}` code block. And in this case, the next frame would also be - # just a scriptblock without a filename so we need to get the source code from the StackTraceFrames too. - $i = 0; - for ($j = $sentryFrames.Count - 1; $j -ge 0; $j--) { - $sentryFrame = $sentryFrames[$j] - $frame = $this.StackTraceFrames | Select-Object -Last 1 -Skip $i - $i++ - - if ($null -eq $frame) { - break - } - - if ($null -eq $sentryFrame.AbsolutePath -and $null -eq $frame.ScriptName) { - if ($frame.ScriptLineNumber -gt 0 -and $frame.ScriptLineNumber -eq $sentryFrame.LineNumber) { - $this.SetScriptInfo($sentryFrame, $frame) - $this.SetModule($sentryFrame) - $this.SetFunction($sentryFrame, $frame) - } - $this.SetContextLines($sentryFrame, $frame) - - # Try to match following frames that are part of the same codeblock. - while ($j -gt 0) { - $nextSentryFrame = $sentryFrames[$j - 1] - if ($nextSentryFrame.AbsolutePath -ne $sentryFrame.AbsolutePath) { - break - } - $this.SetContextLines($nextSentryFrame, $frame) - $j-- - } - } - } - } - - hidden SetScriptInfo([Sentry.SentryStackFrame] $sentryFrame, [System.Management.Automation.CallStackFrame] $frame) { - if (![string]::IsNullOrEmpty($frame.ScriptName)) { - $sentryFrame.AbsolutePath = $frame.ScriptName - $sentryFrame.LineNumber = $frame.ScriptLineNumber - } elseif (![string]::IsNullOrEmpty($frame.Position) -and ![string]::IsNullOrEmpty($frame.Position.File)) { - $sentryFrame.AbsolutePath = $frame.Position.File - $sentryFrame.LineNumber = $frame.Position.StartLineNumber - $sentryFrame.ColumnNumber = $frame.Position.StartColumnNumber - } - } - - hidden SetModule([Sentry.SentryStackFrame] $sentryFrame) { - if (![string]::IsNullOrEmpty($sentryFrame.AbsolutePath)) { - if ($prefix = $this.modulePaths | Where-Object { $sentryFrame.AbsolutePath.StartsWith($_) }) { - $relativePath = $sentryFrame.AbsolutePath.Substring($prefix.Length + 1) - $parts = $relativePath -split '[\\/]' - $sentryFrame.Module = $parts | Select-Object -First 1 - if ($parts.Length -ge 2) { - if (-not $this.pwshModules.ContainsKey($parts[0])) { - $this.pwshModules[$parts[0]] = $parts[1] - } elseif ($this.pwshModules[$parts[0]] -ne $parts[1]) { - $this.pwshModules[$parts[0]] = $this.pwshModules[$parts[0]] + ", $($parts[1])" - } - } - } - } - } - - hidden SetFunction([Sentry.SentryStackFrame] $sentryFrame, [System.Management.Automation.CallStackFrame] $frame) { - if ([string]::IsNullOrEmpty($sentryFrame.AbsolutePath) -and $frame.FunctionName -eq '' -and ![string]::IsNullOrEmpty($frame.Position)) { - $sentryFrame.Function = $frame.Position.Text - - # $frame.Position.Text may be a multiline command (e.g. when executed with `pwsh -c '& { ... \n ... \n ... }`) - # So we need to trim it to a single line. - if ($sentryFrame.Function.Contains("`n")) { - $lines = $sentryFrame.Function -split "[`r`n]+" - $sentryFrame.Function = $lines[0] + ' ' - if ($lines.Count -gt 2) { - $sentryFrame.Function += ' ...... ' - } - $sentryFrame.Function += $lines[$lines.Count - 1] - } - } else { - $sentryFrame.Function = $frame.FunctionName - } - } - - hidden SetContextLines([Sentry.SentryStackFrame] $sentryFrame, [System.Management.Automation.CallStackFrame] $frame) { - if ($sentryFrame.LineNumber -gt 0) { - try { - $lines = $frame.InvocationInfo.MyCommand.ScriptBlock.ToString() -split "`n" - $this.SetContextLines($sentryFrame, $lines) - } catch { - Write-Warning "Failed to read context lines for frame with function '$($sentryFrame.Function)': $_" - if ($global:SentryPowershellRethrowErrors -eq $true) { - throw - } - } - } - } - - hidden SetContextLines([Sentry.SentryStackFrame] $sentryFrame) { - if ([string]::IsNullOrEmpty($sentryFrame.AbsolutePath) -or $sentryFrame.LineNumber -lt 1) { - return - } - - if ((Test-Path $sentryFrame.AbsolutePath -IsValid) -and (Test-Path $sentryFrame.AbsolutePath -PathType Leaf)) { - try { - $lines = Get-Content $sentryFrame.AbsolutePath -TotalCount ($sentryFrame.LineNumber + 5) - $this.SetContextLines($sentryFrame, $lines) - } catch { - Write-Warning "Failed to read context lines for $($sentryFrame.AbsolutePath): $_" - if ($global:SentryPowershellRethrowErrors -eq $true) { - throw - } - } - } - } - - hidden SetContextLines([Sentry.SentryStackFrame] $sentryFrame, [string[]] $lines) { - if ($lines.Count -lt $sentryFrame.LineNumber) { - if ($null -ne $this.logger) { - $this.logger.Log( - [Sentry.SentryLevel]::Debug, - "Couldn't set frame context because the line number ($($sentryFrame.LineNumber)) " + - "is lower than the available number of source code lines ($($lines.Count))." - ) - } - return - } - - $numContextLines = 5 - - if ($null -eq $sentryFrame.ContextLine) { - $sentryFrame.ContextLine = $lines[$sentryFrame.LineNumber - 1] - } - - $preContextCount = [math]::Min($numContextLines, $sentryFrame.LineNumber - 1) - $postContextCount = [math]::Min($numContextLines, $lines.Count - $sentryFrame.LineNumber) - - if ($sentryFrame.LineNumber -gt $numContextLines + 1) { - $lines = $lines | Select-Object -Skip ($sentryFrame.LineNumber - $numContextLines - 1) - } - - # Note: these are read-only in sentry-dotnet so we just update the underlying lists instead of replacing. - $sentryFrame.PreContext.Clear() - $lines | Select-Object -First $preContextCount | ForEach-Object { $sentryFrame.PreContext.Add($_) } - $sentryFrame.PostContext.Clear() - $lines | Select-Object -First $postContextCount -Skip ($preContextCount + 1) | ForEach-Object { $sentryFrame.PostContext.Add($_) } - } -} diff --git a/modules/Sentry/public/Out-Sentry.ps1 b/modules/Sentry/public/Out-Sentry.ps1 index 4c6167f..b53a4c0 100644 --- a/modules/Sentry/public/Out-Sentry.ps1 +++ b/modules/Sentry/public/Out-Sentry.ps1 @@ -1,4 +1,4 @@ -. "$privateDir/StackTraceProcessor.ps1" +. "$privateDir/New-StackTraceEventProcessor.ps1" . "$privateDir/Get-CurrentOptions.ps1" function Out-Sentry { @@ -37,37 +37,40 @@ function Out-Sentry { $options = Get-CurrentOptions [Sentry.SentryEvent]$event_ = $null - $processor = [StackTraceProcessor]::new($options) + $sentryException = $null + $stackTraceString = $null + $invocationInfo = $null + $stackTraceFrames = $null if ($ErrorRecord -ne $null) { $event_ = [Sentry.SentryEvent]::new($ErrorRecord.Exception) - $processor.SentryException = [Sentry.Protocol.SentryException]::new() + $sentryException = [Sentry.Protocol.SentryException]::new() if ($($ErrorRecord.CategoryInfo.Activity) -eq 'Write-Error') { # FullyQualifiedErrorId would be "Microsoft.PowerShell.Commands.WriteErrorException,funcB" - $processor.SentryException.Type = 'Write-Error' + $sentryException.Type = 'Write-Error' } else { - $processor.SentryException.Type = $ErrorRecord.FullyQualifiedErrorId + $sentryException.Type = $ErrorRecord.FullyQualifiedErrorId } if (($details = $ErrorRecord.ErrorDetails) -and $null -ne $details.Message) { - $processor.SentryException.Value = $details.Message + $sentryException.Value = $details.Message } else { - $processor.SentryException.Value = $ErrorRecord.Exception.Message + $sentryException.Value = $ErrorRecord.Exception.Message } if ($options.AttachStackTrace) { # Note: we use ScriptStackTrace even though we need to parse it, becaause it contains actual stack trace # to the throw, not just the trace to the call to this function. - $processor.StackTraceString = @($ErrorRecord.ScriptStackTrace -split "[`r`n]+") - $processor.InvocationInfo = $ErrorRecord.InvocationInfo + $stackTraceString = @($ErrorRecord.ScriptStackTrace -split "[`r`n]+") + $invocationInfo = $ErrorRecord.InvocationInfo } } elseif ($Exception -ne $null) { $event_ = [Sentry.SentryEvent]::new($Exception) - $processor.SentryException = [Sentry.Protocol.SentryException]::new() - $processor.SentryException.Type = $Exception.GetType().FullName - $processor.SentryException.Value = $Exception.Message + $sentryException = [Sentry.Protocol.SentryException]::new() + $sentryException.Type = $Exception.GetType().FullName + $sentryException.Value = $Exception.Message } elseif ($Message -ne $null) { $event_ = [Sentry.SentryEvent]::new() $event_.Message = $Message @@ -82,10 +85,17 @@ function Out-Sentry { return } - if ($options.AttachStackTrace -and $null -eq $processor.StackTraceFrames) { - $processor.StackTraceFrames = Get-PSCallStack | Select-Object -Skip 1 + if ($options.AttachStackTrace -and $null -eq $stackTraceFrames) { + $stackTraceFrames = Get-PSCallStack | Select-Object -Skip 1 } + $processor = New-StackTraceEventProcessor ` + -Options $options ` + -SentryException $sentryException ` + -InvocationInfo $invocationInfo ` + -StackTraceFrames $stackTraceFrames ` + -StackTraceString $stackTraceString + return [Sentry.SentrySdk]::CaptureEvent($event_, [System.Action[Sentry.Scope]] { param([Sentry.Scope]$scope) $scope.AddEventProcessor($processor) diff --git a/tests/stacktrace-processor.tests.ps1 b/tests/stacktrace-processor.tests.ps1 index ee95c07..3633956 100644 --- a/tests/stacktrace-processor.tests.ps1 +++ b/tests/stacktrace-processor.tests.ps1 @@ -1,5 +1,5 @@ BeforeAll { - . "$PSScriptRoot/../modules/Sentry/private/StackTraceProcessor.ps1" + . "$PSScriptRoot/../modules/Sentry/private/New-StackTraceEventProcessor.ps1" $global:SentryPowershellRethrowErrors = $true } @@ -7,17 +7,21 @@ AfterAll { $global:SentryPowershellRethrowErrors = $false } -Describe 'StackTraceProcessor' { +Describe 'New-StackTraceEventProcessor' { It 'Parses stack trace properly' { $event_ = [Sentry.SentryEvent]::new() $event_.Message = 'Test' $event_.Level = [Sentry.SentryLevel]::Info - $sut = [StackTraceProcessor]::new([Sentry.SentryOptions]::new()) - $sut.StackTraceString = 'at funcB, C:\dev\sentry-powershell\tests\throwing.ps1: line 17 + $stackTraceString = 'at funcB, C:\dev\sentry-powershell\tests\throwing.ps1: line 17 at , : line 1 at , : line 3' -split "[`r`n]+" - $sut.process($event_) + + $sut = New-StackTraceEventProcessor ` + -Options ([Sentry.SentryOptions]::new()) ` + -StackTraceString $stackTraceString + + $sut.Process($event_) $frames = $event_.SentryThreads[0].Stacktrace.Frames $frames[0].Function | Should -Be ''