Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Features

- Honor `InAppInclude` / `InAppExclude` options when classifying PowerShell stack frames. Entries are matched against the PowerShell module name (the directory under a `$env:PSModulePath` entry) with prefix-then-dot semantics, mirroring how the .NET SDK matches namespaces. Regex variants (`AddInAppIncludeRegex` / `AddInAppExcludeRegex`) are also supported. The existing default — module frames are not in-app, script frames are — is preserved for modules not matched by any rule. ([#135](https://github.com/getsentry/sentry-powershell/pull/135))
- Add support for .NET 10 / PowerShell 7.6 ([#133](https://github.com/getsentry/sentry-powershell/pull/133))
- Add `Write-SentryLog` cmdlet, a native PowerShell API for sending structured logs (Sentry Logs) ([#131](https://github.com/getsentry/sentry-powershell/pull/131))
- Add `Add-SentryEventProcessor` cmdlet for registering a global event processor from a PowerShell script block ([#130](https://github.com/getsentry/sentry-powershell/pull/130))
Expand Down
52 changes: 51 additions & 1 deletion modules/Sentry/private/StackTraceProcessor.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class StackTraceProcessor : SentryEventProcessor {
hidden [Sentry.Extensibility.IDiagnosticLogger] $logger
hidden [string[]] $modulePaths
hidden [hashtable] $pwshModules = @{}
hidden [System.Collections.IEnumerable] $inAppInclude
hidden [System.Collections.IEnumerable] $inAppExclude

StackTraceProcessor([Sentry.SentryOptions] $options) {
$this.logger = $options.DiagnosticLogger
Expand All @@ -20,6 +22,54 @@ class StackTraceProcessor : SentryEventProcessor {
# Unix
$this.modulePaths = $env:PSModulePath -split ':'
}

# The SentryOptions.InAppInclude / InAppExclude lists are internal; read them via reflection.
# Entries are Sentry.StringOrRegex (string prefix or compiled regex) per the .NET SDK.
$flags = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance
$includeProp = [Sentry.SentryOptions].GetProperty('InAppInclude', $flags)
$excludeProp = [Sentry.SentryOptions].GetProperty('InAppExclude', $flags)
Comment thread
sentry[bot] marked this conversation as resolved.
if ($null -ne $includeProp) {
$this.inAppInclude = $includeProp.GetValue($options)
}
if ($null -ne $excludeProp) {
$this.inAppExclude = $excludeProp.GetValue($options)
}
}

hidden static [bool] MatchesAny([System.Collections.IEnumerable] $patterns, [string] $module) {
if ($null -eq $patterns -or [string]::IsNullOrEmpty($module)) {
return $false
}
foreach ($item in $patterns) {
# StringOrRegex has private _string / _regex fields, exactly one set.
$type = $item.GetType()
$flags = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance
$stringValue = $type.GetField('_string', $flags).GetValue($item)
$regexValue = $type.GetField('_regex', $flags).GetValue($item)
Comment thread
sentry[bot] marked this conversation as resolved.
if (-not [string]::IsNullOrEmpty($stringValue)) {
# Prefix match, matching .NET SDK namespace semantics ("Foo" matches "Foo" and "Foo.Bar").
if ($module -eq $stringValue -or $module.StartsWith("$stringValue.")) {
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
return $true
}
} elseif ($null -ne $regexValue -and $regexValue.IsMatch($module)) {
return $true
}
}
return $false
}

hidden [bool] ResolveInApp([Sentry.SentryStackFrame] $sentryFrame) {
$module = $sentryFrame.Module
# InAppExclude wins, then InAppInclude. Falls back to the PS default: user-script frames (no module)
# are in-app; module frames are not. This default differs from sentry-dotnet because PS module
# frames are almost always third-party.
if ([StackTraceProcessor]::MatchesAny($this.inAppExclude, $module)) {
return $false
}
if ([StackTraceProcessor]::MatchesAny($this.inAppInclude, $module)) {
return $true
}
return [string]::IsNullOrEmpty($module)
}

[Sentry.SentryEvent]DoProcess([Sentry.SentryEvent] $event_) {
Expand Down Expand Up @@ -137,7 +187,7 @@ class StackTraceProcessor : SentryEventProcessor {
foreach ($sentryFrame in $sentryFrames) {
# Update module info
$this.SetModule($sentryFrame)
$sentryFrame.InApp = [string]::IsNullOrEmpty($sentryFrame.Module)
$sentryFrame.InApp = $this.ResolveInApp($sentryFrame)
$this.SetContextLines($sentryFrame)
}

Expand Down
65 changes: 65 additions & 0 deletions tests/stacktrace-processor.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,69 @@ at <ScriptBlock>, : line 3' -split "[`r`n]+"
$frames[2].AbsolutePath | Should -Be 'C:\dev\sentry-powershell\tests\throwing.ps1'
$frames[2].LineNumber | Should -Be 17
}

Context 'ResolveInApp' {
BeforeAll {
function MakeFrame([string] $module) {
$f = [Sentry.SentryStackFrame]::new()
$f.Module = $module
$f
}
}

It 'Defaults user-script frames (no module) to in-app' {
$sut = [StackTraceProcessor]::new([Sentry.SentryOptions]::new())
$sut.ResolveInApp((MakeFrame $null)) | Should -BeTrue
$sut.ResolveInApp((MakeFrame '')) | Should -BeTrue
}

It 'Defaults module frames to not-in-app' {
$sut = [StackTraceProcessor]::new([Sentry.SentryOptions]::new())
$sut.ResolveInApp((MakeFrame 'Pester')) | Should -BeFalse
}

It 'Honors InAppInclude for module frames' {
$options = [Sentry.SentryOptions]::new()
$options.AddInAppInclude('MyApp')
$sut = [StackTraceProcessor]::new($options)
$sut.ResolveInApp((MakeFrame 'MyApp')) | Should -BeTrue
$sut.ResolveInApp((MakeFrame 'Pester')) | Should -BeFalse
}

It 'Honors InAppExclude for module frames' {
# Module frames already default to not-in-app, so an exclude can only be *observed* when it
# overrides an include. Include both modules, exclude one, and verify only the other stays in-app.
$options = [Sentry.SentryOptions]::new()
$options.AddInAppInclude('Included')
$options.AddInAppInclude('Excluded')
$options.AddInAppExclude('Excluded')
$sut = [StackTraceProcessor]::new($options)
$sut.ResolveInApp((MakeFrame 'Excluded')) | Should -BeFalse
$sut.ResolveInApp((MakeFrame 'Included')) | Should -BeTrue
}

It 'Matches prefix on dotted module names' {
$options = [Sentry.SentryOptions]::new()
$options.AddInAppInclude('MyApp')
$sut = [StackTraceProcessor]::new($options)
$sut.ResolveInApp((MakeFrame 'MyApp.Submodule')) | Should -BeTrue
$sut.ResolveInApp((MakeFrame 'MyAppOther')) | Should -BeFalse
}

It 'Honors regex include patterns' {
$options = [Sentry.SentryOptions]::new()
$options.AddInAppIncludeRegex('^My.*App$')
$sut = [StackTraceProcessor]::new($options)
$sut.ResolveInApp((MakeFrame 'MyAwesomeApp')) | Should -BeTrue
$sut.ResolveInApp((MakeFrame 'OtherApp')) | Should -BeFalse
}

It 'InAppExclude wins over InAppInclude' {
$options = [Sentry.SentryOptions]::new()
$options.AddInAppInclude('Foo')
$options.AddInAppExclude('Foo')
$sut = [StackTraceProcessor]::new($options)
$sut.ResolveInApp((MakeFrame 'Foo')) | Should -BeFalse
}
}
}
Loading