From 8b464825b58ccf8b59b8ebd39961fb64e668bbd3 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Jul 2026 15:08:43 +0300 Subject: [PATCH 1/3] Add replay recording segments API helper Adds Get-SentryReplayRecordingSegments to fetch the rrweb recording data of a session replay, plus a polling Get-SentryTestReplayRecordingSegments test util, so integration tests can assert on recording contents (e.g. embedded breadcrumbs). --- .../Get-SentryReplayRecordingSegments.ps1 | 31 +++++++++++ sentry-api-client/SentryApiClient.psd1 | 1 + utils/Integration.TestUtils.psm1 | 51 ++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 diff --git a/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 b/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 new file mode 100644 index 0000000..3635f0e --- /dev/null +++ b/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 @@ -0,0 +1,31 @@ +function Get-SentryReplayRecordingSegments { + <# + .SYNOPSIS + Retrieves the recording segments of a session replay from Sentry. + + .DESCRIPTION + Fetches the rrweb recording data of a Sentry session replay by its ID. + The response is a JSON array with one entry per segment; each entry is the + segment's list of rrweb events (video metadata, breadcrumbs, etc.). + Automatically removes hyphens from GUID-formatted replay IDs. + + .PARAMETER ReplayId + The unique identifier of the replay. Can be provided with or without hyphens. + + .EXAMPLE + Get-SentryReplayRecordingSegments -ReplayId "7acc9c0d4a2e0a85187fe9b75e6b05ac" + # Retrieves the rrweb events of all recording segments of the given replay + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ReplayId + ) + + # Remove hyphens from GUID-formatted replay IDs + $ReplayId = $ReplayId -replace '-', '' + + $Uri = Get-SentryProjectUrl -Resource "replays/$ReplayId/recording-segments/" -QueryString "download=true" + + return Invoke-SentryApiRequest -Uri $Uri -Method 'GET' +} diff --git a/sentry-api-client/SentryApiClient.psd1 b/sentry-api-client/SentryApiClient.psd1 index a3aa9a7..d4f0261 100644 --- a/sentry-api-client/SentryApiClient.psd1 +++ b/sentry-api-client/SentryApiClient.psd1 @@ -41,6 +41,7 @@ 'Get-SentryMetrics', 'Get-SentryMetricsByAttribute', 'Get-SentryReplay', + 'Get-SentryReplayRecordingSegments', 'Get-SentrySpans', 'Invoke-SentryCLI' ) diff --git a/utils/Integration.TestUtils.psm1 b/utils/Integration.TestUtils.psm1 index 588b790..5b4d796 100644 --- a/utils/Integration.TestUtils.psm1 +++ b/utils/Integration.TestUtils.psm1 @@ -563,5 +563,54 @@ function Get-SentryTestReplay { throw "Replay $ReplayId not found in Sentry within $TimeoutSeconds seconds: $lastError" } +function Get-SentryTestReplayRecordingSegments { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ReplayId, + + [Parameter()] + [int]$TimeoutSeconds = 300 + ) + + Write-Host "Fetching Sentry replay recording segments for replay: $ReplayId" -ForegroundColor Yellow + $progressActivity = "Waiting for Sentry replay recording segments of $ReplayId" + + $startTime = Get-Date + $endTime = $startTime.AddSeconds($TimeoutSeconds) + $lastError = $null + $elapsedSeconds = 0 + + try { + do { + $segments = $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 { + $segments = Get-SentryReplayRecordingSegments -ReplayId $ReplayId + } catch { + $lastError = $_.Exception.Message + Write-Debug "Recording segments for replay $ReplayId not found yet: $lastError" + } + + if ($segments -and @($segments).Count -gt 0) { + Write-Host "Recording segments for replay $ReplayId fetched from Sentry" -ForegroundColor Green + $segments | ConvertTo-Json -Depth 10 | Out-File -FilePath (Get-OutputFilePath "replay-recording-$ReplayId.json") + return $segments + } + + Start-Sleep -Milliseconds 500 + $currentTime = Get-Date + } while ($currentTime -lt $endTime) + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + throw "Recording segments for replay $ReplayId 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-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-PackageAumid From 6dac008e53292dc2df043ed4bcfa4c6f3c1fa075 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Jul 2026 16:06:48 +0300 Subject: [PATCH 2/3] Preserve segment array structure in replay recording helpers Pipeline enumeration collapses a single-segment response into its inner rrweb event list, so callers received a flat event list instead of one entry per segment. Re-wrap that case and return with the comma operator, matching the other test util helpers. --- .../Public/Get-SentryReplayRecordingSegments.ps1 | 14 +++++++++++++- utils/Integration.TestUtils.psm1 | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 b/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 index 3635f0e..df9f537 100644 --- a/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 +++ b/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 @@ -27,5 +27,17 @@ function Get-SentryReplayRecordingSegments { $Uri = Get-SentryProjectUrl -Resource "replays/$ReplayId/recording-segments/" -QueryString "download=true" - return Invoke-SentryApiRequest -Uri $Uri -Method 'GET' + $Response = Invoke-SentryApiRequest -Uri $Uri -Method 'GET' + + # The endpoint returns an array with one entry per segment. Pipeline + # enumeration collapses a single-segment response into its inner rrweb + # event list on the way here, so detect that case (elements are event + # dictionaries rather than segment lists) and re-wrap it. The comma + # operator prevents the same unwrapping on return. + $Segments = @($Response) + if ($Segments.Count -gt 0 -and $Segments[0] -is [System.Collections.IDictionary]) { + $Segments = , $Segments + } + + return , $Segments } diff --git a/utils/Integration.TestUtils.psm1 b/utils/Integration.TestUtils.psm1 index 5b4d796..6a1d8f7 100644 --- a/utils/Integration.TestUtils.psm1 +++ b/utils/Integration.TestUtils.psm1 @@ -599,7 +599,8 @@ function Get-SentryTestReplayRecordingSegments { if ($segments -and @($segments).Count -gt 0) { Write-Host "Recording segments for replay $ReplayId fetched from Sentry" -ForegroundColor Green $segments | ConvertTo-Json -Depth 10 | Out-File -FilePath (Get-OutputFilePath "replay-recording-$ReplayId.json") - return $segments + # Use comma operator to ensure array is preserved (prevents PowerShell unwrapping single item) + return , @($segments) } Start-Sleep -Milliseconds 500 From 89d9bb53a3a9cb1b9229de24fa2a2ddf3f65cd88 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Thu, 16 Jul 2026 16:27:30 +0300 Subject: [PATCH 3/3] Handle empty recording responses and preserve nesting in saved artifact Wrap the API call in @() so an empty segments response yields an empty array instead of a one-element array containing null, letting the test util keep polling. Serialize the saved artifact via -InputObject so a single-segment recording keeps its array-of-segments nesting. --- .../Public/Get-SentryReplayRecordingSegments.ps1 | 15 +++++++-------- utils/Integration.TestUtils.psm1 | 3 ++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 b/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 index df9f537..4033557 100644 --- a/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 +++ b/sentry-api-client/Public/Get-SentryReplayRecordingSegments.ps1 @@ -27,14 +27,13 @@ function Get-SentryReplayRecordingSegments { $Uri = Get-SentryProjectUrl -Resource "replays/$ReplayId/recording-segments/" -QueryString "download=true" - $Response = Invoke-SentryApiRequest -Uri $Uri -Method 'GET' - - # The endpoint returns an array with one entry per segment. Pipeline - # enumeration collapses a single-segment response into its inner rrweb - # event list on the way here, so detect that case (elements are event - # dictionaries rather than segment lists) and re-wrap it. The comma - # operator prevents the same unwrapping on return. - $Segments = @($Response) + # The endpoint returns an array with one entry per segment. Wrapping the + # call in `@(...)` collects an empty response into an empty array rather + # than `$null`. Pipeline enumeration collapses a single-segment response + # into its inner rrweb event list on the way here, so detect that case + # (elements are event dictionaries rather than segment lists) and re-wrap + # it. The comma operator prevents the same unwrapping on return. + $Segments = @(Invoke-SentryApiRequest -Uri $Uri -Method 'GET') if ($Segments.Count -gt 0 -and $Segments[0] -is [System.Collections.IDictionary]) { $Segments = , $Segments } diff --git a/utils/Integration.TestUtils.psm1 b/utils/Integration.TestUtils.psm1 index 6a1d8f7..c4fa5f1 100644 --- a/utils/Integration.TestUtils.psm1 +++ b/utils/Integration.TestUtils.psm1 @@ -598,7 +598,8 @@ function Get-SentryTestReplayRecordingSegments { if ($segments -and @($segments).Count -gt 0) { Write-Host "Recording segments for replay $ReplayId fetched from Sentry" -ForegroundColor Green - $segments | ConvertTo-Json -Depth 10 | Out-File -FilePath (Get-OutputFilePath "replay-recording-$ReplayId.json") + # Pass as -InputObject so a single-segment array is not enumerated away + ConvertTo-Json -InputObject $segments -Depth 10 | Out-File -FilePath (Get-OutputFilePath "replay-recording-$ReplayId.json") # Use comma operator to ensure array is preserved (prevents PowerShell unwrapping single item) return , @($segments) }