diff --git a/sentry-api-client/Public/Get-SentryFeedback.ps1 b/sentry-api-client/Public/Get-SentryFeedback.ps1 new file mode 100644 index 0000000..bd1175f --- /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['associatedEventId'] = $associatedEventId + } + } + + return $Response +} 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', 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