@@ -114,33 +114,11 @@ class StackTraceProcessor : SentryEventProcessor
114114 hidden [Sentry.SentryStackTrace ]GetStackTrace()
115115 {
116116 # We collect all frames and then reverse them to the order expected by Sentry (caller->callee).
117- # Do not try to make this code go backwards, because it relies on the InvocationInfo from the previous frame.
117+ # Do not try to make this code go backwards because it relies on the InvocationInfo from the previous frame.
118118 $sentryFrames = New-Object System.Collections.Generic.List[Sentry.SentryStackFrame ]
119- if ($null -ne $this.StackTraceFrames )
120- {
121- $sentryFrames.Capacity = $this.StackTraceFrames.Count + 1
122- }
123- elseif ($null -ne $this.StackTraceString )
119+ if ($null -ne $this.StackTraceString )
124120 {
125121 $sentryFrames.Capacity = $this.StackTraceString.Count + 1
126- }
127-
128- if ($null -ne $this.StackTraceFrames )
129- {
130- # Note: if InvocationInfo is present, use it to fill the first frame. This is the case for ErrroRecord handling
131- # and has the information about the actual script file and line that have thrown the exception.
132- if ($null -ne $this.InvocationInfo )
133- {
134- $sentryFrames.Add ($this.CreateFrame ($this.InvocationInfo ))
135- }
136-
137- foreach ($frame in $this.StackTraceFrames )
138- {
139- $sentryFrames.Add ($this.CreateFrame ($frame ))
140- }
141- }
142- elseif ($null -ne $this.StackTraceString )
143- {
144122 # Note: if InvocationInfo is present, use it to update:
145123 # - the first frame (in case of `$_ | Out-Sentry` in a catch clause).
146124 # - the second frame (in case of `write-error` and `$_ | Out-Sentry` in a trap).
@@ -167,10 +145,21 @@ class StackTraceProcessor : SentryEventProcessor
167145 }
168146 $sentryFrames.Add ($sentryFrame )
169147 }
148+
170149 if ($null -ne $sentryFrameInitial )
171150 {
172151 $sentryFrames.Insert (0 , $sentryFrameInitial )
173152 }
153+
154+ $this.EnhanceTailFrames ($sentryFrames )
155+ }
156+ elseif ($null -ne $this.StackTraceFrames )
157+ {
158+ $sentryFrames.Capacity = $this.StackTraceFrames.Count + 1
159+ foreach ($frame in $this.StackTraceFrames )
160+ {
161+ $sentryFrames.Add ($this.CreateFrame ($frame ))
162+ }
174163 }
175164
176165 foreach ($sentryFrame in $sentryFrames )
@@ -213,7 +202,10 @@ class StackTraceProcessor : SentryEventProcessor
213202 $regex = ' at (?<Function>[^,]*), (?<AbsolutePath>.*): line (?<LineNumber>\d*)'
214203 if ($frame -match $regex )
215204 {
216- $sentryFrame.AbsolutePath = $Matches.AbsolutePath
205+ if ($Matches.AbsolutePath -ne ' <No file>' )
206+ {
207+ $sentryFrame.AbsolutePath = $Matches.AbsolutePath
208+ }
217209 $sentryFrame.LineNumber = [int ]$Matches.LineNumber
218210 $sentryFrame.Function = $Matches.Function
219211 }
@@ -224,6 +216,54 @@ class StackTraceProcessor : SentryEventProcessor
224216 return $sentryFrame
225217 }
226218
219+ hidden EnhanceTailFrames([Sentry.SentryStackFrame []] $sentryFrames )
220+ {
221+ if ($null -eq $this.StackTraceFrames )
222+ {
223+ return
224+ }
225+
226+ # The last frame is usually how the PowerShell was invoked. We need to get this info from $this.StackTraceFrames
227+ # - for pwsh scriptname.ps1 it would be something like `. scriptname.ps1`
228+ # - for pwsh -c `& {..}` it would be the `& {..}` code block. And in this case, the next frame would also be
229+ # just a scriptblock without a filename so we need to get the source code from the StackTraceFrames too.
230+ $i = 0 ;
231+ for ($j = $sentryFrames.Count - 1 ; $j -ge 0 ; $j -- )
232+ {
233+ $sentryFrame = $sentryFrames [$j ]
234+ $frame = $this.StackTraceFrames | Select-Object - Last 1 - Skip $i
235+ $i ++
236+
237+ if ($null -eq $frame )
238+ {
239+ break
240+ }
241+
242+ if ($null -eq $sentryFrame.AbsolutePath -and $null -eq $frame.ScriptName )
243+ {
244+ if ($frame.ScriptLineNumber -gt 0 -and $frame.ScriptLineNumber -eq $sentryFrame.LineNumber )
245+ {
246+ $this.SetScriptInfo ($sentryFrame , $frame )
247+ $this.SetModule ($sentryFrame )
248+ $this.SetFunction ($sentryFrame , $frame )
249+ }
250+ $this.SetContextLines ($sentryFrame , $frame )
251+
252+ # Try to match following frames that are part of the same codeblock.
253+ while ($j -gt 0 )
254+ {
255+ $nextSentryFrame = $sentryFrames [$j - 1 ]
256+ if ($nextSentryFrame.AbsolutePath -ne $sentryFrame.AbsolutePath )
257+ {
258+ break
259+ }
260+ $this.SetContextLines ($nextSentryFrame , $frame )
261+ $j --
262+ }
263+ }
264+ }
265+ }
266+
227267 hidden SetScriptInfo([Sentry.SentryStackFrame ] $sentryFrame , [System.Management.Automation.CallStackFrame ] $frame )
228268 {
229269 if (! [string ]::IsNullOrEmpty($frame.ScriptName ))
@@ -268,13 +308,42 @@ class StackTraceProcessor : SentryEventProcessor
268308 if ([string ]::IsNullOrEmpty($sentryFrame.AbsolutePath ) -and $frame.FunctionName -eq ' <ScriptBlock>' -and ! [string ]::IsNullOrEmpty($frame.Position ))
269309 {
270310 $sentryFrame.Function = $frame.Position.Text
311+
312+ # $frame.Position.Text may be a multiline command (e.g. when executed with `pwsh -c '& { ... \n ... \n ... }`)
313+ # So we need to trim it to a single line.
314+ if ($sentryFrame.Function.Contains (" `n " ))
315+ {
316+ $lines = $sentryFrame.Function -split " [`r`n ]+"
317+ $sentryFrame.Function = $lines [0 ] + ' '
318+ if ($lines.Count -gt 2 )
319+ {
320+ $sentryFrame.Function += ' ...<multiline script content omitted>... '
321+ }
322+ $sentryFrame.Function += $lines [$lines.Count - 1 ]
323+ }
271324 }
272325 else
273326 {
274327 $sentryFrame.Function = $frame.FunctionName
275328 }
276329 }
277330
331+ hidden SetContextLines([Sentry.SentryStackFrame ] $sentryFrame , [System.Management.Automation.CallStackFrame ] $frame )
332+ {
333+ if ($sentryFrame.LineNumber -gt 0 )
334+ {
335+ try
336+ {
337+ $lines = $frame.InvocationInfo.MyCommand.ScriptBlock.ToString () -split " `n "
338+ $this.SetContextLines ($sentryFrame , $lines )
339+ }
340+ catch
341+ {
342+ Write-Warning " Failed to read context lines for frame with function '$ ( $sentryFrame.Function ) ': $_ "
343+ }
344+ }
345+ }
346+
278347 hidden SetContextLines([Sentry.SentryStackFrame ] $sentryFrame )
279348 {
280349 if ([string ]::IsNullOrEmpty($sentryFrame.AbsolutePath ) -or $sentryFrame.LineNumber -lt 1 )
@@ -287,26 +356,42 @@ class StackTraceProcessor : SentryEventProcessor
287356 try
288357 {
289358 $lines = Get-Content $sentryFrame.AbsolutePath - TotalCount ($sentryFrame.LineNumber + 5 )
290- if ($null -eq $sentryFrame.ContextLine )
291- {
292- $sentryFrame.ContextLine = $lines [$sentryFrame.LineNumber - 1 ]
293- }
294- $preContextCount = [math ]::Min(5 , $sentryFrame.LineNumber - 1 )
295- $postContextCount = [math ]::Min(5 , $lines.Count - $sentryFrame.LineNumber )
296- if ($sentryFrame.LineNumber -gt 6 )
297- {
298- $lines = $lines | Select-Object - Skip ($sentryFrame.LineNumber - 6 )
299- }
300- # Note: these are read-only in sentry-dotnet so we just update the underlying lists instead of replacing.
301- $sentryFrame.PreContext.Clear ()
302- $lines | Select-Object - First $preContextCount | ForEach-Object { $sentryFrame.PreContext.Add ($_ ) }
303- $sentryFrame.PostContext.Clear ()
304- $lines | Select-Object - Last $postContextCount | ForEach-Object { $sentryFrame.PostContext.Add ($_ ) }
359+ $this.SetContextLines ($sentryFrame , $lines )
305360 }
306361 catch
307362 {
308363 Write-Warning " Failed to read context lines for $ ( $sentryFrame.AbsolutePath ) : $_ "
309364 }
310365 }
311366 }
367+
368+ hidden SetContextLines([Sentry.SentryStackFrame ] $sentryFrame , [string []] $lines )
369+ {
370+ if ($lines.Count -lt $sentryFrame.LineNumber )
371+ {
372+ Write-Debug " Couldn't set frame context because the line number ($ ( $sentryFrame.LineNumber ) ) is lower than the available number of source code lines ($ ( $lines.Count ) )."
373+ return
374+ }
375+
376+ $numContextLines = 5
377+
378+ if ($null -eq $sentryFrame.ContextLine )
379+ {
380+ $sentryFrame.ContextLine = $lines [$sentryFrame.LineNumber - 1 ]
381+ }
382+
383+ $preContextCount = [math ]::Min($numContextLines , $sentryFrame.LineNumber - 1 )
384+ $postContextCount = [math ]::Min($numContextLines , $lines.Count - $sentryFrame.LineNumber )
385+
386+ if ($sentryFrame.LineNumber -gt $numContextLines + 1 )
387+ {
388+ $lines = $lines | Select-Object - Skip ($sentryFrame.LineNumber - $numContextLines - 1 )
389+ }
390+
391+ # Note: these are read-only in sentry-dotnet so we just update the underlying lists instead of replacing.
392+ $sentryFrame.PreContext.Clear ()
393+ $lines | Select-Object - First $preContextCount | ForEach-Object { $sentryFrame.PreContext.Add ($_ ) }
394+ $sentryFrame.PostContext.Clear ()
395+ $lines | Select-Object - First $postContextCount - Skip ($preContextCount + 1 ) | ForEach-Object { $sentryFrame.PostContext.Add ($_ ) }
396+ }
312397}
0 commit comments