Skip to content

Commit 3dda600

Browse files
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 <noreply@anthropic.com>
1 parent 78b6a03 commit 3dda600

5 files changed

Lines changed: 163 additions & 9 deletions

File tree

modules/Sentry/Sentry.psd1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
# 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.
3434
FunctionsToExport = @(
3535
'Add-SentryBreadcrumb',
36+
'Add-SentryEventProcessor',
3637
'Edit-SentryScope',
3738
'Invoke-WithSentry',
3839
'Out-Sentry',

modules/Sentry/Sentry.psm1

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@ $moduleInfo = Import-PowerShellDataFile (Join-Path (Split-Path -Parent $MyInvoca
44

55
. "$privateDir/Get-SentryAssembliesDirectory.ps1"
66
$sentryDllPath = (Join-Path (Get-SentryAssembliesDirectory) 'Sentry.dll')
7+
# ScriptBlockEventProcessor.cs uses System.Management.Automation.ScriptBlock.
8+
$automationDllPath = [System.Management.Automation.PSObject].Assembly.Location
79

8-
$addTypeParams = @{
9-
TypeDefinition = (Get-Content "$privateDir/SentryEventProcessor.cs" -Raw)
10-
ReferencedAssemblies = $sentryDllPath
11-
Debug = $false
10+
function Add-SentryInlineType([string] $sourceFile, [string[]] $extraReferences) {
11+
$addTypeParams = @{
12+
TypeDefinition = (Get-Content $sourceFile -Raw)
13+
ReferencedAssemblies = @($sentryDllPath) + $extraReferences
14+
Debug = $false
15+
}
16+
# -CompilerOptions is PS Core only; suppress CS1701/CS1702 (harmless binding-redirect noise) when available.
17+
if ($PSEdition -eq 'Core') {
18+
$addTypeParams['CompilerOptions'] = '/nowarn:CS1701;CS1702'
19+
}
20+
Add-Type @addTypeParams
1221
}
13-
# -CompilerOptions is PS Core only; suppress CS1701/CS1702 (harmless binding-redirect noise) when available.
14-
if ($PSEdition -eq 'Core') {
15-
$addTypeParams['CompilerOptions'] = '/nowarn:CS1701;CS1702'
16-
}
17-
Add-Type @addTypeParams
22+
23+
Add-SentryInlineType "$privateDir/SentryEventProcessor.cs" @()
24+
Add-SentryInlineType "$privateDir/ScriptBlockEventProcessor.cs" @($automationDllPath)
1825
. "$privateDir/SentryEventProcessor.ps1"
1926

2027
Get-ChildItem $publicDir -Filter '*.ps1' | ForEach-Object {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Management.Automation;
3+
using Sentry;
4+
using Sentry.Extensibility;
5+
6+
// Wraps a PowerShell ScriptBlock as an ISentryEventProcessor so the public
7+
// Add-SentryEventProcessor cmdlet can register user-supplied script blocks with
8+
// the Sentry pipeline. Implementing the interface in C# (rather than asking users
9+
// to author a PowerShell class deriving from an internal base) keeps the public
10+
// API a single scriptblock and avoids the `Process` keyword conflict in Windows
11+
// PowerShell that would otherwise require a Process_ / DoProcess workaround.
12+
public sealed class ScriptBlockEventProcessor : ISentryEventProcessor
13+
{
14+
private readonly ScriptBlock _scriptBlock;
15+
private readonly IDiagnosticLogger _logger;
16+
17+
public ScriptBlockEventProcessor(ScriptBlock scriptBlock, IDiagnosticLogger logger)
18+
{
19+
if (scriptBlock == null) throw new ArgumentNullException("scriptBlock");
20+
_scriptBlock = scriptBlock;
21+
_logger = logger;
22+
}
23+
24+
public SentryEvent Process(SentryEvent @event)
25+
{
26+
try
27+
{
28+
var results = _scriptBlock.Invoke(@event);
29+
if (results == null || results.Count == 0)
30+
{
31+
return @event;
32+
}
33+
34+
var last = results[results.Count - 1];
35+
if (last == null)
36+
{
37+
return null;
38+
}
39+
40+
return (last.BaseObject as SentryEvent) ?? @event;
41+
}
42+
catch (Exception ex)
43+
{
44+
if (_logger != null)
45+
{
46+
_logger.Log(
47+
SentryLevel.Warning,
48+
"Event processor scriptblock failed for event {0}: {1}",
49+
ex,
50+
new object[] { @event.EventId, ex.Message });
51+
}
52+
return @event;
53+
}
54+
}
55+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
. "$privateDir/Get-CurrentOptions.ps1"
2+
3+
<#
4+
.SYNOPSIS
5+
Registers a global event processor that runs on every event before it is sent to Sentry.
6+
.DESCRIPTION
7+
The script block receives the Sentry event via the automatic variable $_ (matching the
8+
convention used by Edit-SentryScope). Return the event to send it, or $null to drop it.
9+
.EXAMPLE
10+
PS> Add-SentryEventProcessor { $_.SetTag('host', $env:COMPUTERNAME); $_ }
11+
.EXAMPLE
12+
PS> Add-SentryEventProcessor {
13+
if ($_.Message -match 'secret') { return $null }
14+
$_
15+
}
16+
#>
17+
function Add-SentryEventProcessor {
18+
param(
19+
[Parameter(Mandatory, Position = 0)]
20+
[scriptblock] $ScriptBlock
21+
)
22+
23+
$options = Get-CurrentOptions
24+
if ($null -eq $options) {
25+
throw 'Sentry is not initialized. Call Start-Sentry before adding an event processor.'
26+
}
27+
28+
# Wrap the user's script block in a pipeline so that $_ is bound to the event,
29+
# matching Edit-SentryScope's convention.
30+
$wrapped = {
31+
param([Sentry.SentryEvent] $event_)
32+
$event_ | ForEach-Object $ScriptBlock
33+
}.GetNewClosure()
34+
35+
$options.AddEventProcessor(
36+
[ScriptBlockEventProcessor]::new($wrapped, $options.DiagnosticLogger))
37+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
BeforeAll {
2+
. "$PSScriptRoot/utils.ps1"
3+
$global:SentryPowershellRethrowErrors = $true
4+
}
5+
6+
AfterAll {
7+
$global:SentryPowershellRethrowErrors = $false
8+
}
9+
10+
Describe 'Add-SentryEventProcessor' {
11+
BeforeEach {
12+
$events = [System.Collections.Generic.List[Sentry.SentryEvent]]::new();
13+
$transport = [RecordingTransport]::new()
14+
StartSentryForEventTests ([ref] $events) ([ref] $transport)
15+
}
16+
17+
AfterEach {
18+
$events.Clear()
19+
Stop-Sentry
20+
}
21+
22+
It 'Mutates events via $_' {
23+
Add-SentryEventProcessor { $_.SetTag('custom', 'value'); $_ }
24+
'msg' | Out-Sentry
25+
26+
$events[0].Tags['custom'] | Should -Be 'value'
27+
}
28+
29+
It 'Drops events when the script block returns $null' {
30+
Add-SentryEventProcessor {
31+
if ($_.Message.Message -match 'drop-me') { return $null }
32+
$_
33+
}
34+
'drop-me please' | Out-Sentry
35+
'keep this one' | Out-Sentry
36+
37+
$events.Count | Should -Be 1
38+
$events[0].Message.Message | Should -Be 'keep this one'
39+
}
40+
41+
It 'Chains multiple processors in registration order' {
42+
Add-SentryEventProcessor { $_.SetTag('first', '1'); $_ }
43+
Add-SentryEventProcessor { $_.SetTag('second', '2'); $_ }
44+
'msg' | Out-Sentry
45+
46+
$events[0].Tags['first'] | Should -Be '1'
47+
$events[0].Tags['second'] | Should -Be '2'
48+
}
49+
50+
It 'Throws when Sentry is not initialized' {
51+
Stop-Sentry
52+
{ Add-SentryEventProcessor { $_ } } | Should -Throw '*Sentry is not initialized*'
53+
}
54+
}

0 commit comments

Comments
 (0)