Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/Sentry/Sentry.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions modules/Sentry/Sentry.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +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
. "$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
Expand Down
13 changes: 0 additions & 13 deletions modules/Sentry/private/EventUpdater.ps1

This file was deleted.

319 changes: 319 additions & 0 deletions modules/Sentry/private/New-StackTraceEventProcessor.ps1
Original file line number Diff line number Diff line change
@@ -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 {

Check warning

Code scanning / PSScriptAnalyzer

Function 'New-StackTraceEventProcessor' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'. Warning

Function 'New-StackTraceEventProcessor' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'.
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 '<ScriptBlock>' -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 += ' ...<multiline script content omitted>... '
}
$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 }

Check warning

Code scanning / PSScriptAnalyzer

Found global variable 'global:SentryPowershellRethrowErrors'. Warning

Found global variable 'global:SentryPowershellRethrowErrors'.
}
}
}

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 }

Check warning

Code scanning / PSScriptAnalyzer

Found global variable 'global:SentryPowershellRethrowErrors'. Warning

Found global variable 'global:SentryPowershellRethrowErrors'.
}
}
}

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 (?<Function>[^,]*), (?<AbsolutePath>.*): line (?<LineNumber>\d*)'
if ($frame -match $regex) {
if ($Matches.AbsolutePath -ne '<No file>') {
$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 '^(?<Assembly>[^,]+), Version=(?<Version>[^,]+), ') {
$event_.Modules[$Matches.Assembly] = $Matches.Version
}
}
}

return $event_
}.GetNewClosure()

return [ScriptBlockEventProcessor]::new($processorScript, $logger)
}
Loading
Loading