From 0419e56b670df8402a90318472465892e29f2071 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 11 Aug 2026 11:47:38 +0300 Subject: [PATCH 1/3] Add Sentry user feedback API helpers Add Get-SentryFeedback to query user feedback submissions via the issue.category:feedback issues endpoint, with optional resolution of the associated event ID from each feedback issue's latest event. Add a Get-SentryTestFeedback polling wrapper that locates a single feedback by a unique message token and saves it for assertion in integration tests. --- .../Public/Get-SentryFeedback.ps1 | 96 +++++++++++++++++++ utils/Integration.TestUtils.psm1 | 53 +++++++++- 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 sentry-api-client/Public/Get-SentryFeedback.ps1 diff --git a/sentry-api-client/Public/Get-SentryFeedback.ps1 b/sentry-api-client/Public/Get-SentryFeedback.ps1 new file mode 100644 index 0000000..116d2aa --- /dev/null +++ b/sentry-api-client/Public/Get-SentryFeedback.ps1 @@ -0,0 +1,96 @@ +function Get-SentryFeedback { + <# + .SYNOPSIS + Retrieves user feedback from Sentry. + + .DESCRIPTION + Fetches user feedback submissions, which are stored as issue-platform occurrences + with the 'feedback' category. Supports narrowing the results with a free-text query + (which matches the feedback message) and optionally enriches each result with the + associated event ID resolved from the feedback issue's latest event. + + .PARAMETER Query + Optional free-text query appended to 'issue.category:feedback'. Matches the feedback + message, so a unique token embedded in the message can be used to locate a single + submission. + + .PARAMETER StatsPeriod + Relative time period (e.g. '24h', '7d', '14d'). Default is '24h'. + + .PARAMETER Limit + Maximum number of feedback submissions to return. Default is 100. + + .PARAMETER Cursor + Pagination cursor for retrieving subsequent pages of results. + + .PARAMETER IncludeAssociatedEvent + If specified, resolves each feedback issue's latest event and attaches an + 'associatedEventId' property (from contexts.feedback.associated_event_id). + + .EXAMPLE + Get-SentryFeedback -Query 'integration-test-abc123' + + .EXAMPLE + Get-SentryFeedback -Query 'integration-test-abc123' -IncludeAssociatedEvent + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$Query, + + [Parameter(Mandatory = $false)] + [string]$StatsPeriod = '24h', + + [Parameter(Mandatory = $false)] + [int]$Limit = 100, + + [Parameter(Mandatory = $false)] + [string]$Cursor, + + [Parameter(Mandatory = $false)] + [switch]$IncludeAssociatedEvent + ) + + $QueryParts = @('issue.category:feedback') + if ($Query) { + $QueryParts += $Query + } + + $QueryParams = @{ + query = $QueryParts -join ' ' + statsPeriod = $StatsPeriod + limit = $Limit + } + + if ($Cursor) { + $QueryParams.cursor = $Cursor + } + + $QueryString = Build-QueryString -Parameters $QueryParams + $Uri = Get-SentryOrganizationUrl -Resource "issues/" -QueryString $QueryString + + try { + $Response = Invoke-SentryApiRequest -Uri $Uri -Method 'GET' + } + catch { + Write-Error "Failed to retrieve feedback - $_" + throw + } + + if ($IncludeAssociatedEvent) { + foreach ($item in $Response) { + $associatedEventId = $null + try { + $LatestUri = Get-SentryOrganizationUrl -Resource "issues/$($item.id)/events/latest/" + $LatestEvent = Invoke-SentryApiRequest -Uri $LatestUri -Method 'GET' + $associatedEventId = $LatestEvent.contexts.feedback.associated_event_id + } + catch { + Write-Debug "Failed to resolve latest event for feedback $($item.id) - $_" + } + $item | Add-Member -MemberType NoteProperty -Name 'associatedEventId' -Value $associatedEventId -Force + } + } + + return $Response +} diff --git a/utils/Integration.TestUtils.psm1 b/utils/Integration.TestUtils.psm1 index c4fa5f1..3a3c7fe 100644 --- a/utils/Integration.TestUtils.psm1 +++ b/utils/Integration.TestUtils.psm1 @@ -614,5 +614,56 @@ function Get-SentryTestReplayRecordingSegments { throw "Recording segments for replay $ReplayId not found in Sentry within $TimeoutSeconds seconds: $lastError" } +function Get-SentryTestFeedback { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$MessageContains, + + [Parameter()] + [int]$TimeoutSeconds = 300 + ) + + Write-Host "Fetching Sentry feedback matching: $MessageContains" -ForegroundColor Yellow + $progressActivity = "Waiting for Sentry feedback matching $MessageContains" + + $startTime = Get-Date + $endTime = $startTime.AddSeconds($TimeoutSeconds) + $lastError = $null + + try { + do { + $feedback = $null + $elapsedSeconds = [int]((Get-Date) - $startTime).TotalSeconds + $percentComplete = [math]::Min(100, ($elapsedSeconds / $TimeoutSeconds) * 100) + + Write-Progress -Activity $progressActivity -Status "Elapsed: $elapsedSeconds/$TimeoutSeconds seconds" -PercentComplete $percentComplete + + try { + $result = @(Get-SentryFeedback -Query $MessageContains -IncludeAssociatedEvent | + Where-Object { $_.metadata.message -like "*$MessageContains*" }) + $result.Count | Should -Be 1 + $feedback = $result[0] + } catch { + $lastError = $_.Exception.Message + Write-Debug "Feedback matching $MessageContains not found yet: $lastError" + } + + if ($feedback) { + Write-Host "Feedback $($feedback.id) fetched from Sentry" -ForegroundColor Green + $feedback | ConvertTo-Json -Depth 10 | Out-File -FilePath (Get-OutputFilePath "feedback-$($feedback.id).json") + return $feedback + } + + Start-Sleep -Milliseconds 500 + $currentTime = Get-Date + } while ($currentTime -lt $endTime) + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + throw "Feedback matching $MessageContains not found in Sentry within $TimeoutSeconds seconds: $lastError" +} + # Export module functions -Export-ModuleMember -Function Invoke-CMakeConfigure, Invoke-CMakeBuild, Set-OutputDir, Get-OutputFilePath, Get-EventIds, Get-SentryTestEvent, Get-SentryTestEventAttachments, Get-SentryTestLog, Get-SentryTestMetric, Get-SentryTestTransaction, Get-SentryTestReplay, Get-SentryTestReplayRecordingSegments, Get-PackageAumid +Export-ModuleMember -Function Invoke-CMakeConfigure, Invoke-CMakeBuild, Set-OutputDir, Get-OutputFilePath, Get-EventIds, Get-SentryTestEvent, Get-SentryTestEventAttachments, Get-SentryTestLog, Get-SentryTestMetric, Get-SentryTestTransaction, Get-SentryTestReplay, Get-SentryTestReplayRecordingSegments, Get-SentryTestFeedback, Get-PackageAumid From 3d3739ba6201af761d367291a4833eb6a973f140 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 11 Aug 2026 14:25:29 +0300 Subject: [PATCH 2/3] Harden Get-SentryFeedback for hashtable API responses Invoke-SentryApiRequest parses responses with ConvertFrom-Json -AsHashtable, so results are hashtables. Wrap the issues response in @() so a single feedback is still treated as a list, and attach associatedEventId as a real dictionary key rather than an ETS note property. No behavior change; makes the intent explicit and robust across PowerShell versions. --- sentry-api-client/Public/Get-SentryFeedback.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-api-client/Public/Get-SentryFeedback.ps1 b/sentry-api-client/Public/Get-SentryFeedback.ps1 index 116d2aa..bd1175f 100644 --- a/sentry-api-client/Public/Get-SentryFeedback.ps1 +++ b/sentry-api-client/Public/Get-SentryFeedback.ps1 @@ -70,7 +70,7 @@ function Get-SentryFeedback { $Uri = Get-SentryOrganizationUrl -Resource "issues/" -QueryString $QueryString try { - $Response = Invoke-SentryApiRequest -Uri $Uri -Method 'GET' + $Response = @(Invoke-SentryApiRequest -Uri $Uri -Method 'GET') } catch { Write-Error "Failed to retrieve feedback - $_" @@ -88,7 +88,7 @@ function Get-SentryFeedback { catch { Write-Debug "Failed to resolve latest event for feedback $($item.id) - $_" } - $item | Add-Member -MemberType NoteProperty -Name 'associatedEventId' -Value $associatedEventId -Force + $item['associatedEventId'] = $associatedEventId } } From 6a4f27c3ae5370b73beb7b6996b3d7cbf9cfa596 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 11 Aug 2026 14:46:14 +0300 Subject: [PATCH 3/3] Export Get-SentryFeedback from the module manifest The manifest lists FunctionsToExport explicitly, so Get-SentryFeedback must be added there to be available when importing via the .psd1. --- sentry-api-client/SentryApiClient.psd1 | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-api-client/SentryApiClient.psd1 b/sentry-api-client/SentryApiClient.psd1 index d4f0261..ffdbee2 100644 --- a/sentry-api-client/SentryApiClient.psd1 +++ b/sentry-api-client/SentryApiClient.psd1 @@ -36,6 +36,7 @@ 'Get-SentryEvent', 'Get-SentryEventAttachments', 'Get-SentryEventsByTag', + 'Get-SentryFeedback', 'Get-SentryLogs', 'Get-SentryLogsByAttribute', 'Get-SentryMetrics',