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 @@ -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

Expand Down
1 change: 1 addition & 0 deletions modules/Sentry/Sentry.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
25 changes: 16 additions & 9 deletions modules/Sentry/Sentry.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
55 changes: 55 additions & 0 deletions modules/Sentry/private/ScriptBlockEventProcessor.cs
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +29 to +32
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is contrary to the usual way event processors work right? In case user returns null, the event should be dropped AFAIR


var last = results[results.Count - 1];
if (last == null)
{
return null;
}

return (last.BaseObject as SentryEvent) ?? @event;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth warning if the user returns anything but SentryEvent. This line defaults to @event silently.

}
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;
}
}
}
37 changes: 37 additions & 0 deletions modules/Sentry/public/Add-SentryEventProcessor.ps1
Original file line number Diff line number Diff line change
@@ -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))
}
49 changes: 49 additions & 0 deletions tests/add-sentry-event-processor.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
BeforeAll {
. "$PSScriptRoot/utils.ps1"
}

Describe 'Add-SentryEventProcessor' {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Pester suite covers the happy paths well, but I'd add at least:

  • User scriptblock throws - event still captured, warning logged (asserts the silent-failure contract).
  • User returns a non-event value (the no-op case from chore: rename sentry.psd1 to Sentry.psd1 #2).
  • Closure capture across registrations - register two processors that close over different values of an outer variable to confirm GetNewClosure() is doing its job.

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*'
}
}
Loading