Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixes

- Warn instead of silently ignoring `InAppInclude` / `InAppExclude` when a sentry-dotnet bump moves the internal members they are read through ([#144](https://github.com/getsentry/sentry-powershell/pull/144))
- Remove the `SdkComposer` fallback transport in `SynchronousWorker`, which has thrown since the sentry-dotnet 5.1.1 bump. The SDK's own default worker and transport are used instead ([#143](https://github.com/getsentry/sentry-powershell/pull/143))
- Silence CS1701/CS1702 warnings emitted by `Add-Type` when importing the module on PowerShell hosts whose runtime `System.Runtime` version differs from the one `Sentry.dll` was compiled against ([#129](https://github.com/getsentry/sentry-powershell/pull/129))

Expand Down
50 changes: 33 additions & 17 deletions modules/Sentry/private/StackTraceProcessor.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
hidden [hashtable] $pwshModules = @{}
hidden [System.Collections.IEnumerable] $inAppInclude
hidden [System.Collections.IEnumerable] $inAppExclude
# A hashtable rather than typed FieldInfo properties, for the reason given in SynchronousTransport.
hidden [hashtable] $stringOrRegexFields = @{}

StackTraceProcessor([Sentry.SentryOptions] $options) {
$this.logger = $options.DiagnosticLogger
Expand All @@ -23,29 +25,43 @@
$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)
if ($null -ne $includeProp) {
$this.inAppInclude = $includeProp.GetValue($options)
# InAppInclude / InAppExclude and the StringOrRegex value fields are internal to sentry-dotnet.
try {
$this.inAppInclude = [StackTraceProcessor]::GetInternalMember([Sentry.SentryOptions], 'InAppInclude').GetValue($options)
$this.inAppExclude = [StackTraceProcessor]::GetInternalMember([Sentry.SentryOptions], 'InAppExclude').GetValue($options)
$this.stringOrRegexFields['_string'] = [StackTraceProcessor]::GetInternalMember([Sentry.StringOrRegex], '_string')
$this.stringOrRegexFields['_regex'] = [StackTraceProcessor]::GetInternalMember([Sentry.StringOrRegex], '_regex')
} catch {
Write-Warning "Ignoring InAppInclude / InAppExclude: $_"
if ($global:SentryPowershellRethrowErrors -eq $true) {
Comment thread
vaind marked this conversation as resolved.
Dismissed
throw
}
$this.inAppInclude = $null
$this.inAppExclude = $null
}
}

# Throws on a miss so an SDK bump that moves a member is reported instead of silently dropping the option.
hidden static [System.Reflection.MemberInfo] GetInternalMember([type] $type, [string] $name) {
$flags = [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public
$member = $type.GetProperty($name, $flags)
if ($null -eq $member) {
$member = $type.GetField($name, $flags)
}
if ($null -ne $excludeProp) {
$this.inAppExclude = $excludeProp.GetValue($options)
if ($null -eq $member) {
throw "Failed to find internal member '$name' on $type"
}
return $member
}

hidden static [bool] MatchesAny([System.Collections.IEnumerable] $patterns, [string] $module) {
hidden [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)
# Exactly one of the two is set.
$stringValue = $this.stringOrRegexFields['_string'].GetValue($item)
$regexValue = $this.stringOrRegexFields['_regex'].GetValue($item)
if (-not [string]::IsNullOrEmpty($stringValue)) {
# Prefix match, matching .NET SDK namespace semantics ("Foo" matches "Foo" and "Foo.Bar").
# Case-insensitive on both halves, consistent with sentry-dotnet and PS module name resolution.
Expand All @@ -65,10 +81,10 @@
# 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)) {
if ($this.MatchesAny($this.inAppExclude, $module)) {
return $false
}
if ([StackTraceProcessor]::MatchesAny($this.inAppInclude, $module)) {
if ($this.MatchesAny($this.inAppInclude, $module)) {
return $true
}
return [string]::IsNullOrEmpty($module)
Expand Down
38 changes: 38 additions & 0 deletions tests/sdk-internals.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
BeforeAll {
. "$PSScriptRoot/utils.ps1"
. "$PSScriptRoot/../modules/Sentry/private/SynchronousTransport.ps1"
. "$PSScriptRoot/../modules/Sentry/private/StackTraceProcessor.ps1"
$global:SentryPowershellRethrowErrors = $true
Comment thread
vaind marked this conversation as resolved.
Dismissed

$instanceFlags = [System.Reflection.BindingFlags]::Instance + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public
$staticFlags = [System.Reflection.BindingFlags]::Static + [System.Reflection.BindingFlags]::NonPublic + [System.Reflection.BindingFlags]::Public
Expand All @@ -19,6 +21,10 @@
}
}

AfterAll {
$global:SentryPowershellRethrowErrors = $false
Comment thread
vaind marked this conversation as resolved.
Dismissed
}

Describe 'Sentry SDK internals used by SynchronousTransport' {
It 'HttpTransportBase.ProcessEnvelope' {
$method = [Sentry.Http.HttpTransportBase].GetMethod('ProcessEnvelope', $instanceFlags)
Expand Down Expand Up @@ -54,6 +60,32 @@
}
}

Describe 'Sentry SDK internals used by StackTraceProcessor' {
It 'SentryOptions.InAppInclude' {
$property = [Sentry.SentryOptions].GetProperty('InAppInclude', $instanceFlags)
$property | Should -Not -BeNullOrEmpty
[System.Collections.Generic.IEnumerable[Sentry.StringOrRegex]].IsAssignableFrom($property.PropertyType) | Should -BeTrue
}

It 'SentryOptions.InAppExclude' {
$property = [Sentry.SentryOptions].GetProperty('InAppExclude', $instanceFlags)
$property | Should -Not -BeNullOrEmpty
[System.Collections.Generic.IEnumerable[Sentry.StringOrRegex]].IsAssignableFrom($property.PropertyType) | Should -BeTrue
}

It 'StringOrRegex._string' {
$field = [Sentry.StringOrRegex].GetField('_string', $instanceFlags)
$field | Should -Not -BeNullOrEmpty
$field.FieldType.FullName | Should -Be 'System.String'
}

It 'StringOrRegex._regex' {
$field = [Sentry.StringOrRegex].GetField('_regex', $instanceFlags)
$field | Should -Not -BeNullOrEmpty
$field.FieldType.FullName | Should -Be 'System.Text.RegularExpressions.Regex'
}
}

Describe 'SynchronousTransport' {
It 'resolves every internal member it needs' {
# The constructor does all of the above lookups and throws on any that fail.
Expand All @@ -62,3 +94,9 @@
{ [SynchronousTransport]::new($options) } | Should -Not -Throw
}
}

Describe 'StackTraceProcessor' {
It 'resolves every internal member it needs' {
{ [StackTraceProcessor]::new([Sentry.SentryOptions]::new()) } | Should -Not -Throw
}
}
9 changes: 2 additions & 7 deletions tests/stacktrace-processor.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,8 @@ at <ScriptBlock>, : line 3' -split "[`r`n]+"
$sut.ResolveInApp((MakeFrame 'Foo')) | Should -BeFalse
}

It 'Sentry.StringOrRegex still exposes the private fields we reflect on' {
# ResolveInApp reaches into the internal _string / _regex fields of Sentry.StringOrRegex.
# If a sentry-dotnet bump renames these, this fails so we catch it at upgrade time rather
# than silently no-op-ing InAppInclude/InAppExclude in production.
$flags = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance
[Sentry.StringOrRegex].GetField('_string', $flags) | Should -Not -BeNullOrEmpty
[Sentry.StringOrRegex].GetField('_regex', $flags) | Should -Not -BeNullOrEmpty
It 'Names the missing member when the SDK internals it reflects on have moved' {
{ [StackTraceProcessor]::GetInternalMember([Sentry.StringOrRegex], 'NoSuchMember') } | Should -Throw '*NoSuchMember*'
}
}
}
Loading