From 3dda6003440827d5bb98721860bac6067f56dd15 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 26 May 2026 14:09:31 +1200 Subject: [PATCH 01/10] Add Add-SentryEventProcessor cmdlet Public cmdlet that registers a global event processor backed by a PowerShell script block. The block receives the event via the automatic $_ variable (matching Edit-SentryScope), returns the event to send it, or $null to drop it. Add-SentryEventProcessor { $_.SetTag('host', $env:COMPUTERNAME); $_ } Add-SentryEventProcessor { if ($_.Message -match 'secret') { return $null } $_ } Internally the block is wrapped by a new C# ScriptBlockEventProcessor that implements ISentryEventProcessor directly. Doing the interface implementation in C# (rather than asking users to author a PowerShell class deriving from an internal base) keeps the public API a single scriptblock and avoids the `Process` keyword conflict in Windows PowerShell that would otherwise require a Process_ / DoProcess workaround. Sentry.psm1 factors the existing Add-Type call into a small helper so the second Add-Type for ScriptBlockEventProcessor.cs picks up the same /nowarn CompilerOptions treatment and can pass an extra System.Management.Automation reference. Co-Authored-By: Claude Opus 4.7 --- modules/Sentry/Sentry.psd1 | 1 + modules/Sentry/Sentry.psm1 | 25 ++++++--- .../private/ScriptBlockEventProcessor.cs | 55 +++++++++++++++++++ .../public/Add-SentryEventProcessor.ps1 | 37 +++++++++++++ tests/add-sentry-event-processor.tests.ps1 | 54 ++++++++++++++++++ 5 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 modules/Sentry/private/ScriptBlockEventProcessor.cs create mode 100644 modules/Sentry/public/Add-SentryEventProcessor.ps1 create mode 100644 tests/add-sentry-event-processor.tests.ps1 diff --git a/modules/Sentry/Sentry.psd1 b/modules/Sentry/Sentry.psd1 index 66b0a46..2963a13 100644 --- a/modules/Sentry/Sentry.psd1 +++ b/modules/Sentry/Sentry.psd1 @@ -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', diff --git a/modules/Sentry/Sentry.psm1 b/modules/Sentry/Sentry.psm1 index 11366ef..ab848b6 100644 --- a/modules/Sentry/Sentry.psm1 +++ b/modules/Sentry/Sentry.psm1 @@ -4,17 +4,24 @@ $moduleInfo = Import-PowerShellDataFile (Join-Path (Split-Path -Parent $MyInvoca . "$privateDir/Get-SentryAssembliesDirectory.ps1" $sentryDllPath = (Join-Path (Get-SentryAssembliesDirectory) 'Sentry.dll') +# ScriptBlockEventProcessor.cs uses System.Management.Automation.ScriptBlock. +$automationDllPath = [System.Management.Automation.PSObject].Assembly.Location -$addTypeParams = @{ - TypeDefinition = (Get-Content "$privateDir/SentryEventProcessor.cs" -Raw) - ReferencedAssemblies = $sentryDllPath - Debug = $false +function Add-SentryInlineType([string] $sourceFile, [string[]] $extraReferences) { + $addTypeParams = @{ + TypeDefinition = (Get-Content $sourceFile -Raw) + ReferencedAssemblies = @($sentryDllPath) + $extraReferences + Debug = $false + } + # -CompilerOptions is PS Core only; suppress CS1701/CS1702 (harmless binding-redirect noise) when available. + if ($PSEdition -eq 'Core') { + $addTypeParams['CompilerOptions'] = '/nowarn:CS1701;CS1702' + } + Add-Type @addTypeParams } -# -CompilerOptions is PS Core only; suppress CS1701/CS1702 (harmless binding-redirect noise) when available. -if ($PSEdition -eq 'Core') { - $addTypeParams['CompilerOptions'] = '/nowarn:CS1701;CS1702' -} -Add-Type @addTypeParams + +Add-SentryInlineType "$privateDir/SentryEventProcessor.cs" @() +Add-SentryInlineType "$privateDir/ScriptBlockEventProcessor.cs" @($automationDllPath) . "$privateDir/SentryEventProcessor.ps1" Get-ChildItem $publicDir -Filter '*.ps1' | ForEach-Object { diff --git a/modules/Sentry/private/ScriptBlockEventProcessor.cs b/modules/Sentry/private/ScriptBlockEventProcessor.cs new file mode 100644 index 0000000..fcd73d5 --- /dev/null +++ b/modules/Sentry/private/ScriptBlockEventProcessor.cs @@ -0,0 +1,55 @@ +using System; +using System.Management.Automation; +using Sentry; +using Sentry.Extensibility; + +// Wraps a PowerShell ScriptBlock as an ISentryEventProcessor so the public +// Add-SentryEventProcessor cmdlet can register user-supplied script blocks with +// the Sentry pipeline. Implementing the interface in C# (rather than asking users +// to author a PowerShell class deriving from an internal base) keeps the public +// API a single scriptblock and avoids the `Process` keyword conflict in Windows +// PowerShell that would otherwise require a Process_ / DoProcess workaround. +public sealed class ScriptBlockEventProcessor : ISentryEventProcessor +{ + private readonly ScriptBlock _scriptBlock; + private readonly IDiagnosticLogger _logger; + + public ScriptBlockEventProcessor(ScriptBlock scriptBlock, IDiagnosticLogger logger) + { + if (scriptBlock == null) throw new ArgumentNullException("scriptBlock"); + _scriptBlock = scriptBlock; + _logger = logger; + } + + public SentryEvent Process(SentryEvent @event) + { + try + { + var results = _scriptBlock.Invoke(@event); + if (results == null || results.Count == 0) + { + return @event; + } + + var last = results[results.Count - 1]; + if (last == null) + { + return null; + } + + return (last.BaseObject as SentryEvent) ?? @event; + } + catch (Exception ex) + { + if (_logger != null) + { + _logger.Log( + SentryLevel.Warning, + "Event processor scriptblock failed for event {0}: {1}", + ex, + new object[] { @event.EventId, ex.Message }); + } + return @event; + } + } +} diff --git a/modules/Sentry/public/Add-SentryEventProcessor.ps1 b/modules/Sentry/public/Add-SentryEventProcessor.ps1 new file mode 100644 index 0000000..8d4697c --- /dev/null +++ b/modules/Sentry/public/Add-SentryEventProcessor.ps1 @@ -0,0 +1,37 @@ +. "$privateDir/Get-CurrentOptions.ps1" + +<# +.SYNOPSIS + Registers a global event processor that runs on every event before it is sent to Sentry. +.DESCRIPTION + The script block receives the Sentry event via the automatic variable $_ (matching the + convention used by Edit-SentryScope). Return the event to send it, or $null to drop it. +.EXAMPLE + PS> Add-SentryEventProcessor { $_.SetTag('host', $env:COMPUTERNAME); $_ } +.EXAMPLE + PS> Add-SentryEventProcessor { + if ($_.Message -match 'secret') { return $null } + $_ + } +#> +function Add-SentryEventProcessor { + param( + [Parameter(Mandatory, Position = 0)] + [scriptblock] $ScriptBlock + ) + + $options = Get-CurrentOptions + if ($null -eq $options) { + throw 'Sentry is not initialized. Call Start-Sentry before adding an event processor.' + } + + # Wrap the user's script block in a pipeline so that $_ is bound to the event, + # matching Edit-SentryScope's convention. + $wrapped = { + param([Sentry.SentryEvent] $event_) + $event_ | ForEach-Object $ScriptBlock + }.GetNewClosure() + + $options.AddEventProcessor( + [ScriptBlockEventProcessor]::new($wrapped, $options.DiagnosticLogger)) +} diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 new file mode 100644 index 0000000..681eb3b --- /dev/null +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -0,0 +1,54 @@ +BeforeAll { + . "$PSScriptRoot/utils.ps1" + $global:SentryPowershellRethrowErrors = $true +} + +AfterAll { + $global:SentryPowershellRethrowErrors = $false +} + +Describe 'Add-SentryEventProcessor' { + BeforeEach { + $events = [System.Collections.Generic.List[Sentry.SentryEvent]]::new(); + $transport = [RecordingTransport]::new() + StartSentryForEventTests ([ref] $events) ([ref] $transport) + } + + AfterEach { + $events.Clear() + Stop-Sentry + } + + It 'Mutates events via $_' { + Add-SentryEventProcessor { $_.SetTag('custom', 'value'); $_ } + 'msg' | Out-Sentry + + $events[0].Tags['custom'] | Should -Be 'value' + } + + It 'Drops events when the script block returns $null' { + Add-SentryEventProcessor { + if ($_.Message.Message -match 'drop-me') { return $null } + $_ + } + 'drop-me please' | Out-Sentry + 'keep this one' | Out-Sentry + + $events.Count | Should -Be 1 + $events[0].Message.Message | Should -Be 'keep this one' + } + + It 'Chains multiple processors in registration order' { + Add-SentryEventProcessor { $_.SetTag('first', '1'); $_ } + Add-SentryEventProcessor { $_.SetTag('second', '2'); $_ } + 'msg' | Out-Sentry + + $events[0].Tags['first'] | Should -Be '1' + $events[0].Tags['second'] | Should -Be '2' + } + + It 'Throws when Sentry is not initialized' { + Stop-Sentry + { Add-SentryEventProcessor { $_ } } | Should -Throw '*Sentry is not initialized*' + } +} From f439577449b96077f16c8e77d0b89005b1181113 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 26 May 2026 14:15:16 +1200 Subject: [PATCH 02/10] Add changelog entry for Add-SentryEventProcessor Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90aaeff..d051a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Features - 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)) ## 0.4.0 From 257bf69b230161f593a1ab95f85df29917de7d8e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 26 May 2026 14:21:51 +1200 Subject: [PATCH 03/10] Drop SentryPowershellRethrowErrors from Add-SentryEventProcessor tests The flag exists so internal try/catch paths (currently only in StackTraceProcessor's source-file context-line reads and Start-Sentry's transport/worker init) rethrow during tests instead of swallowing errors. These tests register simple script blocks and capture events via 'msg' | Out-Sentry, so they don't exercise any of those paths and don't need the override. Silences the PSScriptAnalyzer global-variable warning on the new test file. Co-Authored-By: Claude Opus 4.7 --- tests/add-sentry-event-processor.tests.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 index 681eb3b..9d975f8 100644 --- a/tests/add-sentry-event-processor.tests.ps1 +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -1,10 +1,5 @@ BeforeAll { . "$PSScriptRoot/utils.ps1" - $global:SentryPowershellRethrowErrors = $true -} - -AfterAll { - $global:SentryPowershellRethrowErrors = $false } Describe 'Add-SentryEventProcessor' { From ac42d258928167018eea7efc43b9233ab2569bc2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 14:30:58 +1200 Subject: [PATCH 04/10] Warn when event processor scriptblock returns a non-SentryEvent Address review feedback: clarify the defensive null guard in the ScriptBlock.Invoke result handling, and log a warning (instead of silently keeping the original event) when the user's scriptblock returns something other than a SentryEvent. Co-Authored-By: Claude Opus 4.7 --- .../private/ScriptBlockEventProcessor.cs | 19 ++++++++++++++++++- tests/add-sentry-event-processor.tests.ps1 | 8 ++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/modules/Sentry/private/ScriptBlockEventProcessor.cs b/modules/Sentry/private/ScriptBlockEventProcessor.cs index fcd73d5..97d40e1 100644 --- a/modules/Sentry/private/ScriptBlockEventProcessor.cs +++ b/modules/Sentry/private/ScriptBlockEventProcessor.cs @@ -25,6 +25,9 @@ public SentryEvent Process(SentryEvent @event) { try { + // ScriptBlock.Invoke is not annotated and in practice always returns a + // Collection, but guard against null defensively and treat it + // the same as "no pipeline output" -> leave the event unchanged. var results = _scriptBlock.Invoke(@event); if (results == null || results.Count == 0) { @@ -34,10 +37,24 @@ public SentryEvent Process(SentryEvent @event) var last = results[results.Count - 1]; if (last == null) { + // User's script block explicitly returned $null -> drop the event. return null; } - return (last.BaseObject as SentryEvent) ?? @event; + if (last.BaseObject is SentryEvent processed) + { + return processed; + } + + if (_logger != null) + { + _logger.Log( + SentryLevel.Warning, + "Event processor scriptblock for event {0} returned {1} instead of a SentryEvent; keeping the original event.", + null, + new object[] { @event.EventId, last.BaseObject?.GetType().FullName ?? "null" }); + } + return @event; } catch (Exception ex) { diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 index 9d975f8..387e7bf 100644 --- a/tests/add-sentry-event-processor.tests.ps1 +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -33,6 +33,14 @@ Describe 'Add-SentryEventProcessor' { $events[0].Message.Message | Should -Be 'keep this one' } + It 'Keeps the original event when the script block returns a non-SentryEvent value' { + Add-SentryEventProcessor { 'not an event' } + 'msg' | Out-Sentry + + $events.Count | Should -Be 1 + $events[0].Message.Message | Should -Be 'msg' + } + It 'Chains multiple processors in registration order' { Add-SentryEventProcessor { $_.SetTag('first', '1'); $_ } Add-SentryEventProcessor { $_.SetTag('second', '2'); $_ } From d85e5f1c574f263df856547cacc30fee576e8dad Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 14:34:57 +1200 Subject: [PATCH 05/10] Avoid C# 7 pattern matching in ScriptBlockEventProcessor Windows PowerShell 5.1's Add-Type compiles via the legacy CSharpCodeProvider which only supports up to C# 5, so the `is SentryEvent processed` pattern in the previous commit failed with "Expected catch or finally". Use a classic `as` cast instead. Co-Authored-By: Claude Opus 4.7 --- modules/Sentry/private/ScriptBlockEventProcessor.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/Sentry/private/ScriptBlockEventProcessor.cs b/modules/Sentry/private/ScriptBlockEventProcessor.cs index 97d40e1..85117f1 100644 --- a/modules/Sentry/private/ScriptBlockEventProcessor.cs +++ b/modules/Sentry/private/ScriptBlockEventProcessor.cs @@ -41,18 +41,20 @@ public SentryEvent Process(SentryEvent @event) return null; } - if (last.BaseObject is SentryEvent processed) + var processed = last.BaseObject as SentryEvent; + if (processed != null) { return processed; } if (_logger != null) { + var returnedTypeName = last.BaseObject != null ? last.BaseObject.GetType().FullName : "null"; _logger.Log( SentryLevel.Warning, "Event processor scriptblock for event {0} returned {1} instead of a SentryEvent; keeping the original event.", null, - new object[] { @event.EventId, last.BaseObject?.GetType().FullName ?? "null" }); + new object[] { @event.EventId, returnedTypeName }); } return @event; } From 505330be08f36ef58813cb58bb9a2c7777f6cddd Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 15:03:30 +1200 Subject: [PATCH 06/10] Add tests for non-event and throwing scriptblock cases Cover two more contracts of Add-SentryEventProcessor: - When the scriptblock returns a non-SentryEvent value, the original event is kept (silent fallback). - When the scriptblock throws, Out-Sentry does not propagate the exception and the event is still captured. Co-Authored-By: Claude Opus 4.7 --- tests/add-sentry-event-processor.tests.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 index 387e7bf..e80b825 100644 --- a/tests/add-sentry-event-processor.tests.ps1 +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -50,6 +50,16 @@ Describe 'Add-SentryEventProcessor' { $events[0].Tags['second'] | Should -Be '2' } + It 'Still captures the event when the script block throws' { + # Silent-failure contract: a throwing processor must not break event capture + # or propagate the exception to the caller of Out-Sentry. + Add-SentryEventProcessor { throw 'boom' } + { 'msg' | Out-Sentry } | Should -Not -Throw + + $events.Count | Should -Be 1 + $events[0].Message.Message | Should -Be 'msg' + } + It 'Throws when Sentry is not initialized' { Stop-Sentry { Add-SentryEventProcessor { $_ } } | Should -Throw '*Sentry is not initialized*' From 84c82beccac08433f4d669ce69bd248735e2f489 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 15:16:38 +1200 Subject: [PATCH 07/10] Allow a custom DiagnosticLogger to be injected via Start-Sentry Start-Sentry was unconditionally overwriting $options.DiagnosticLogger after the EditOptions scriptblock ran, which made it impossible to wire the existing TestLogger helper through to production code. Only create the default logger when one isn't already set (matching the existing pattern for Transport and BackgroundWorker). This lets the new throw test assert that a failing scriptblock is logged via TestLogger (the silent-failure contract from PR #130 review). Co-Authored-By: Claude Opus 4.7 --- modules/Sentry/public/Start-Sentry.ps1 | 8 ++++++-- tests/add-sentry-event-processor.tests.ps1 | 21 +++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/Sentry/public/Start-Sentry.ps1 b/modules/Sentry/public/Start-Sentry.ps1 index 3b7476c..3b65e2f 100644 --- a/modules/Sentry/public/Start-Sentry.ps1 +++ b/modules/Sentry/public/Start-Sentry.ps1 @@ -38,11 +38,15 @@ function Start-Sentry { $options | ForEach-Object $EditOptions } - $logger = [DiagnosticLogger]::new($options.DiagnosticLevel) + # Respect a logger supplied via EditOptions (e.g. a TestLogger in tests); + # otherwise fall back to the project's default DiagnosticLogger. + if ($null -eq $options.DiagnosticLogger) { + $options.DiagnosticLogger = [DiagnosticLogger]::new($options.DiagnosticLevel) + } # Note: this is currently a no-op if options.debug == false; see https://github.com/getsentry/sentry-dotnet/issues/3212 # As a workaround, we set the logger as a global variable so that we can reach it in other scripts. - $options.DiagnosticLogger = $logger + $logger = $options.DiagnosticLogger $script:SentryPowerShellDiagnosticLogger = $logger if ($null -eq $options.Transport) { diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 index e80b825..cb78946 100644 --- a/tests/add-sentry-event-processor.tests.ps1 +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -50,14 +50,31 @@ Describe 'Add-SentryEventProcessor' { $events[0].Tags['second'] | Should -Be '2' } - It 'Still captures the event when the script block throws' { + It 'Still captures the event and logs a warning when the script block throws' { # Silent-failure contract: a throwing processor must not break event capture - # or propagate the exception to the caller of Out-Sentry. + # or propagate the exception, and the failure must be logged. + Stop-Sentry + $logger = [TestLogger]::new([Sentry.SentryLevel]::Debug) + Start-Sentry { + $_.Dsn = 'https://key@127.0.0.1/1' + # Debug=true is required for Sentry to retain a custom DiagnosticLogger; + # see https://github.com/getsentry/sentry-dotnet/issues/3212 + $_.Debug = $true + $_.DiagnosticLogger = $logger + $_.SetBeforeSend([System.Func[Sentry.SentryEvent, Sentry.SentryEvent]] { + param([Sentry.SentryEvent]$e) + $events.Add($e) + return $e + }) + $_.Transport = $transport + } + Add-SentryEventProcessor { throw 'boom' } { 'msg' | Out-Sentry } | Should -Not -Throw $events.Count | Should -Be 1 $events[0].Message.Message | Should -Be 'msg' + ($logger.entries | Where-Object { $_ -match 'Event processor scriptblock failed' }).Count | Should -BeGreaterThan 0 } It 'Throws when Sentry is not initialized' { From 5f9e209a32d4d62c0e59a199b16b1116303365b9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 16:47:51 +1200 Subject: [PATCH 08/10] Fix throw test for Windows PowerShell 5.1 Where-Object unwraps single-item or empty results to a scalar/null on Windows PowerShell 5.1, so `.Count` throws PropertyNotFoundException. Use a string-join + -Match assertion instead, which is robust across all supported PowerShell editions. Co-Authored-By: Claude Opus 4.7 --- tests/add-sentry-event-processor.tests.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 index cb78946..9fea2bb 100644 --- a/tests/add-sentry-event-processor.tests.ps1 +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -74,7 +74,9 @@ Describe 'Add-SentryEventProcessor' { $events.Count | Should -Be 1 $events[0].Message.Message | Should -Be 'msg' - ($logger.entries | Where-Object { $_ -match 'Event processor scriptblock failed' }).Count | Should -BeGreaterThan 0 + # Use string-join + -Match: in Windows PowerShell 5.1, Where-Object + # unwraps a single result to a scalar, breaking .Count. + ($logger.entries -join "`n") | Should -Match 'Event processor scriptblock failed' } It 'Throws when Sentry is not initialized' { From f9087882ce72dc0ef864fb8419c929872ed7cd6c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 17:06:41 +1200 Subject: [PATCH 09/10] Test that GetNewClosure binds each registration to its own scriptblock Asserts the contract that GetNewClosure() in Add-SentryEventProcessor actually delivers: each wrapper holds its own user-supplied $ScriptBlock reference, so two registrations don't collapse onto one. A comment notes that variables closed over from the caller's scope are still resolved at invoke time (standard PowerShell scriptblock semantics). Co-Authored-By: Claude Opus 4.7 --- tests/add-sentry-event-processor.tests.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/add-sentry-event-processor.tests.ps1 b/tests/add-sentry-event-processor.tests.ps1 index 9fea2bb..dc56c60 100644 --- a/tests/add-sentry-event-processor.tests.ps1 +++ b/tests/add-sentry-event-processor.tests.ps1 @@ -41,6 +41,20 @@ Describe 'Add-SentryEventProcessor' { $events[0].Message.Message | Should -Be 'msg' } + It 'Captures the closure independently for each registration' { + # Verifies that GetNewClosure() in Add-SentryEventProcessor binds each + # wrapper to its own user-supplied $ScriptBlock — i.e. registrations + # don't collapse to a single shared reference. (Note: variables that + # the user's scriptblock closes over from the caller's scope are still + # resolved at invoke time per normal PowerShell scoping rules.) + Add-SentryEventProcessor { $_.SetTag('which', 'first'); $_ } + Add-SentryEventProcessor { $_.SetTag('also', 'second'); $_ } + 'msg' | Out-Sentry + + $events[0].Tags['which'] | Should -Be 'first' + $events[0].Tags['also'] | Should -Be 'second' + } + It 'Chains multiple processors in registration order' { Add-SentryEventProcessor { $_.SetTag('first', '1'); $_ } Add-SentryEventProcessor { $_.SetTag('second', '2'); $_ } From 672cf014b6eea8033bebe2afcc753b78ba2905af Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 May 2026 19:23:25 +1200 Subject: [PATCH 10/10] Tweak comments --- modules/Sentry/private/ScriptBlockEventProcessor.cs | 5 ++--- modules/Sentry/public/Start-Sentry.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/Sentry/private/ScriptBlockEventProcessor.cs b/modules/Sentry/private/ScriptBlockEventProcessor.cs index 85117f1..4a567a5 100644 --- a/modules/Sentry/private/ScriptBlockEventProcessor.cs +++ b/modules/Sentry/private/ScriptBlockEventProcessor.cs @@ -25,12 +25,11 @@ public SentryEvent Process(SentryEvent @event) { try { - // ScriptBlock.Invoke is not annotated and in practice always returns a - // Collection, but guard against null defensively and treat it - // the same as "no pipeline output" -> leave the event unchanged. var results = _scriptBlock.Invoke(@event); if (results == null || results.Count == 0) { + // No pipeline output (empty block, bare `return`, or a defensive null from Invoke). + // Treat as no-opinion and keep the event; explicit `return $null` lands below. return @event; } diff --git a/modules/Sentry/public/Start-Sentry.ps1 b/modules/Sentry/public/Start-Sentry.ps1 index 3b65e2f..eaeb66e 100644 --- a/modules/Sentry/public/Start-Sentry.ps1 +++ b/modules/Sentry/public/Start-Sentry.ps1 @@ -38,8 +38,8 @@ function Start-Sentry { $options | ForEach-Object $EditOptions } - # Respect a logger supplied via EditOptions (e.g. a TestLogger in tests); - # otherwise fall back to the project's default DiagnosticLogger. + # Allow the logger to be injected/supplied via EditOptions (e.g. a TestLogger). + # Use default DiagnosticLogger as a fall back in normal circumstances. if ($null -eq $options.DiagnosticLogger) { $options.DiagnosticLogger = [DiagnosticLogger]::new($options.DiagnosticLevel) }