@@ -114,33 +114,11 @@ class StackTraceProcessor : SentryEventProcessor
114
114
hidden [Sentry.SentryStackTrace ]GetStackTrace()
115
115
{
116
116
# 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.
118
118
$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 )
124
120
{
125
121
$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
- {
144
122
# Note: if InvocationInfo is present, use it to update:
145
123
# - the first frame (in case of `$_ | Out-Sentry` in a catch clause).
146
124
# - the second frame (in case of `write-error` and `$_ | Out-Sentry` in a trap).
@@ -167,10 +145,21 @@ class StackTraceProcessor : SentryEventProcessor
167
145
}
168
146
$sentryFrames.Add ($sentryFrame )
169
147
}
148
+
170
149
if ($null -ne $sentryFrameInitial )
171
150
{
172
151
$sentryFrames.Insert (0 , $sentryFrameInitial )
173
152
}
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
+ }
174
163
}
175
164
176
165
foreach ($sentryFrame in $sentryFrames )
@@ -213,7 +202,10 @@ class StackTraceProcessor : SentryEventProcessor
213
202
$regex = ' at (?<Function>[^,]*), (?<AbsolutePath>.*): line (?<LineNumber>\d*)'
214
203
if ($frame -match $regex )
215
204
{
216
- $sentryFrame.AbsolutePath = $Matches.AbsolutePath
205
+ if ($Matches.AbsolutePath -ne ' <No file>' )
206
+ {
207
+ $sentryFrame.AbsolutePath = $Matches.AbsolutePath
208
+ }
217
209
$sentryFrame.LineNumber = [int ]$Matches.LineNumber
218
210
$sentryFrame.Function = $Matches.Function
219
211
}
@@ -224,6 +216,54 @@ class StackTraceProcessor : SentryEventProcessor
224
216
return $sentryFrame
225
217
}
226
218
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
+
227
267
hidden SetScriptInfo([Sentry.SentryStackFrame ] $sentryFrame , [System.Management.Automation.CallStackFrame ] $frame )
228
268
{
229
269
if (! [string ]::IsNullOrEmpty($frame.ScriptName ))
@@ -268,13 +308,42 @@ class StackTraceProcessor : SentryEventProcessor
268
308
if ([string ]::IsNullOrEmpty($sentryFrame.AbsolutePath ) -and $frame.FunctionName -eq ' <ScriptBlock>' -and ! [string ]::IsNullOrEmpty($frame.Position ))
269
309
{
270
310
$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
+ }
271
324
}
272
325
else
273
326
{
274
327
$sentryFrame.Function = $frame.FunctionName
275
328
}
276
329
}
277
330
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
+
278
347
hidden SetContextLines([Sentry.SentryStackFrame ] $sentryFrame )
279
348
{
280
349
if ([string ]::IsNullOrEmpty($sentryFrame.AbsolutePath ) -or $sentryFrame.LineNumber -lt 1 )
@@ -287,26 +356,42 @@ class StackTraceProcessor : SentryEventProcessor
287
356
try
288
357
{
289
358
$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 )
305
360
}
306
361
catch
307
362
{
308
363
Write-Warning " Failed to read context lines for $ ( $sentryFrame.AbsolutePath ) : $_ "
309
364
}
310
365
}
311
366
}
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
+ }
312
397
}
0 commit comments