diff --git a/sentry-api-client/Public/Get-SentryMetrics.ps1 b/sentry-api-client/Public/Get-SentryMetrics.ps1 new file mode 100644 index 0000000..98c113b --- /dev/null +++ b/sentry-api-client/Public/Get-SentryMetrics.ps1 @@ -0,0 +1,87 @@ +function Get-SentryMetrics { + <# + .SYNOPSIS + Retrieves metrics from Sentry. + + .DESCRIPTION + Fetches Sentry metrics matching specified criteria. + Supports filtering by query, time range, and custom fields. + Uses the Sentry Discover API with the 'tracemetrics' dataset. + + .PARAMETER Query + Search query string using Sentry search syntax (e.g., 'metric.name:my.counter', 'metric.type:counter'). + + .PARAMETER StatsPeriod + Relative time period (e.g., '24h', '7d', '14d'). Default is '24h'. + + .PARAMETER Limit + Maximum number of metrics to return. Default is 100. + + .PARAMETER Cursor + Pagination cursor for retrieving subsequent pages of results. + + .PARAMETER Fields + Specific fields to return. Default includes: id, metric.name, metric.type, value, timestamp. + + .EXAMPLE + Get-SentryMetrics -Query 'metric.name:my.counter' + + .EXAMPLE + Get-SentryMetrics -Query 'metric.name:my.counter test_id:abc123' -StatsPeriod '7d' + #> + [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)] + [string[]]$Fields + ) + + # Default fields for metrics if not specified + if (-not $Fields -or $Fields.Count -eq 0) { + $Fields = @( + 'id', + 'metric.name', + 'metric.type', + 'value', + 'timestamp' + ) + } + + $QueryParams = @{ + dataset = 'tracemetrics' + statsPeriod = $StatsPeriod + per_page = $Limit + field = $Fields + } + + if ($Query) { + $QueryParams.query = $Query + } + + if ($Cursor) { + $QueryParams.cursor = $Cursor + } + + $QueryString = Build-QueryString -Parameters $QueryParams + $Uri = Get-SentryOrganizationUrl -Resource "events/" -QueryString $QueryString + + try { + $Response = Invoke-SentryApiRequest -Uri $Uri -Method 'GET' + return $Response + } + catch { + Write-Error "Failed to retrieve metrics - $_" + throw + } +} diff --git a/sentry-api-client/Public/Get-SentryMetricsByAttribute.ps1 b/sentry-api-client/Public/Get-SentryMetricsByAttribute.ps1 new file mode 100644 index 0000000..471c290 --- /dev/null +++ b/sentry-api-client/Public/Get-SentryMetricsByAttribute.ps1 @@ -0,0 +1,76 @@ +function Get-SentryMetricsByAttribute { + <# + .SYNOPSIS + Retrieves metrics filtered by metric name and a specific attribute. + + .DESCRIPTION + Fetches Sentry metrics that match a specific metric name and attribute + name/value pair. This is a convenience wrapper around Get-SentryMetrics + for common use cases like filtering by test_id for integration testing. + + .PARAMETER MetricName + The name of the metric to filter by (e.g., 'test.integration.counter'). + + .PARAMETER AttributeName + The name of the attribute to filter by (e.g., 'test_id'). + + .PARAMETER AttributeValue + The value of the attribute to match. + + .PARAMETER Limit + Maximum number of metrics to return. Default is 100. + + .PARAMETER StatsPeriod + Relative time period (e.g., '24h', '7d'). Default is '24h'. + + .PARAMETER Fields + Additional fields to include in the response. These are merged with default fields + (id, metric.name, metric.type, value, timestamp) and the filter attribute. + + .EXAMPLE + Get-SentryMetricsByAttribute -MetricName 'test.integration.counter' -AttributeName 'test_id' -AttributeValue 'abc-123' + + .EXAMPLE + Get-SentryMetricsByAttribute -MetricName 'my.counter' -AttributeName 'user_id' -AttributeValue '12345' -StatsPeriod '7d' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$MetricName, + + [Parameter(Mandatory = $true)] + [string]$AttributeName, + + [Parameter(Mandatory = $true)] + [string]$AttributeValue, + + [Parameter(Mandatory = $false)] + [int]$Limit = 100, + + [Parameter(Mandatory = $false)] + [string]$StatsPeriod = '24h', + + [Parameter(Mandatory = $false)] + [string[]]$Fields + ) + + $Query = "metric.name:$MetricName $AttributeName`:$AttributeValue" + + # Include default fields plus the attribute we're filtering by + $DefaultFields = @( + 'id', + 'metric.name', + 'metric.type', + 'value', + 'timestamp', + $AttributeName + ) + + if ($Fields) { + $AllFields = @($DefaultFields + $Fields) | Select-Object -Unique + } else { + $AllFields = $DefaultFields + } + + return Get-SentryMetrics -Query $Query -Limit $Limit -StatsPeriod $StatsPeriod -Fields $AllFields +} diff --git a/sentry-api-client/SentryApiClient.psd1 b/sentry-api-client/SentryApiClient.psd1 index d601b74..77236a2 100644 --- a/sentry-api-client/SentryApiClient.psd1 +++ b/sentry-api-client/SentryApiClient.psd1 @@ -37,6 +37,8 @@ 'Get-SentryEventsByTag', 'Get-SentryLogs', 'Get-SentryLogsByAttribute', + 'Get-SentryMetrics', + 'Get-SentryMetricsByAttribute', 'Invoke-SentryCLI' ) diff --git a/sentry-api-client/Tests/Fixtures/SentryMetricsResponses.json b/sentry-api-client/Tests/Fixtures/SentryMetricsResponses.json new file mode 100644 index 0000000..11c0daf --- /dev/null +++ b/sentry-api-client/Tests/Fixtures/SentryMetricsResponses.json @@ -0,0 +1,40 @@ +{ + "metrics_list": { + "data": [ + { + "id": "019bbce4569e7f5f81466757e8f84001", + "metric.name": "test.integration.counter", + "metric.type": "counter", + "value": 42.0, + "timestamp": "2025-01-14T14:32:45+00:00", + "test_id": "metrics-test-001" + }, + { + "id": "019bbce4569e7f5f81466757e8f84002", + "metric.name": "test.integration.counter", + "metric.type": "counter", + "value": 7.0, + "timestamp": "2025-01-14T14:32:46+00:00", + "test_id": "metrics-test-001" + } + ], + "meta": { + "fields": { + "id": "string", + "metric.name": "string", + "metric.type": "string", + "value": "number", + "timestamp": "date", + "test_id": "string" + }, + "units": {} + } + }, + "metrics_empty": { + "data": [], + "meta": { + "fields": {}, + "units": {} + } + } +} diff --git a/sentry-api-client/Tests/SentryApiClient.Metrics.Tests.ps1 b/sentry-api-client/Tests/SentryApiClient.Metrics.Tests.ps1 new file mode 100644 index 0000000..be52f4e --- /dev/null +++ b/sentry-api-client/Tests/SentryApiClient.Metrics.Tests.ps1 @@ -0,0 +1,138 @@ +BeforeAll { + $ModulePath = Join-Path $PSScriptRoot '..' 'SentryApiClient.psd1' + Import-Module $ModulePath -Force + + # Load test fixtures + $FixturesPath = Join-Path $PSScriptRoot 'Fixtures' 'SentryMetricsResponses.json' + $Script:MetricsFixtures = Get-Content $FixturesPath | ConvertFrom-Json -AsHashtable +} + +AfterAll { + Remove-Module SentryApiClient -Force +} + +Describe 'SentryApiClient Metrics Functions' { + Context 'Module Export' { + It 'Should export Get-SentryMetrics function' { + Get-Command Get-SentryMetrics -Module SentryApiClient | Should -Not -BeNullOrEmpty + } + + It 'Should export Get-SentryMetricsByAttribute function' { + Get-Command Get-SentryMetricsByAttribute -Module SentryApiClient | Should -Not -BeNullOrEmpty + } + } + + Context 'Get-SentryMetrics' { + BeforeAll { + Mock -ModuleName SentryApiClient Invoke-WebRequest { + return @{ Content = ($Script:MetricsFixtures.metrics_list | ConvertTo-Json -Depth 10) } + } + + Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project' + } + + It 'Should retrieve metrics from tracemetrics dataset' { + $result = Get-SentryMetrics -Query 'metric.name:test.integration.counter' + + $result | Should -Not -BeNullOrEmpty + $result.data | Should -HaveCount 2 + + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'dataset=tracemetrics' -and + $Uri -match 'organizations/test-org/events/' + } + } + + It 'Should include default fields when none specified' { + Get-SentryMetrics -Query 'metric.name:test.integration.counter' + + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'field=id' -and + $Uri -match 'field=metric\.name' -and + $Uri -match 'field=metric\.type' -and + $Uri -match 'field=value' -and + $Uri -match 'field=timestamp' + } + } + + It 'Should use custom fields when specified' { + Get-SentryMetrics -Query 'metric.name:test.counter' -Fields @('id', 'value', 'custom_field') + + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'field=id' -and + $Uri -match 'field=value' -and + $Uri -match 'field=custom_field' + } + } + + It 'Should pass stats period parameter' { + Get-SentryMetrics -Query 'metric.name:test.counter' -StatsPeriod '7d' + + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'statsPeriod=7d' + } + } + } + + Context 'Get-SentryMetricsByAttribute' { + BeforeAll { + Mock -ModuleName SentryApiClient Invoke-WebRequest { + return @{ Content = ($Script:MetricsFixtures.metrics_list | ConvertTo-Json -Depth 10) } + } + + Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project' + } + + It 'Should query by metric name and attribute' { + $result = Get-SentryMetricsByAttribute -MetricName 'test.integration.counter' -AttributeName 'test_id' -AttributeValue 'metrics-test-001' + + $result | Should -Not -BeNullOrEmpty + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'query=metric\.name%3Atest\.integration\.counter' -and + $Uri -match 'test_id%3Ametrics-test-001' + } + } + + It 'Should include the filter attribute in response fields' { + Get-SentryMetricsByAttribute -MetricName 'test.counter' -AttributeName 'test_id' -AttributeValue 'abc-123' + + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'field=test_id' + } + } + + It 'Should merge additional fields with defaults' { + Get-SentryMetricsByAttribute -MetricName 'test.counter' -AttributeName 'test_id' -AttributeValue 'abc-123' -Fields @('extra_field') + + Assert-MockCalled -ModuleName SentryApiClient Invoke-WebRequest -ParameterFilter { + $Uri -match 'field=extra_field' -and + $Uri -match 'field=metric\.name' -and + $Uri -match 'field=test_id' + } + } + } + + Context 'Error Handling' { + BeforeAll { + Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project' + } + + It 'Should handle API errors gracefully' { + Mock -ModuleName SentryApiClient Invoke-WebRequest { + throw [System.Net.WebException]::new('401 Unauthorized') + } + + { Get-SentryMetrics } | Should -Throw '*Sentry API request*failed*' + } + } + + Context 'Connection Validation' { + BeforeEach { + Disconnect-SentryApi + } + + It 'Should throw when organization is not configured' { + { Get-SentryMetrics } | Should -Throw '*Organization not configured*' + } + } +} diff --git a/utils/Integration.TestUtils.psm1 b/utils/Integration.TestUtils.psm1 index 363429a..cc31795 100644 --- a/utils/Integration.TestUtils.psm1 +++ b/utils/Integration.TestUtils.psm1 @@ -307,5 +307,87 @@ function Get-SentryTestLog { throw "Expected at least $ExpectedCount log(s) with $AttributeName=$AttributeValue but found $foundCount within $TimeoutSeconds seconds. Last error: $lastError" } +function Get-SentryTestMetric { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$MetricName, + + [Parameter(Mandatory = $true)] + [string]$AttributeName, + + [Parameter(Mandatory = $true)] + [string]$AttributeValue, + + [Parameter()] + [int]$ExpectedCount = 1, + + [Parameter()] + [int]$TimeoutSeconds = 120, + + [Parameter()] + [string]$StatsPeriod = '24h', + + [Parameter()] + [string[]]$Fields + ) + + Write-Host "Fetching Sentry metrics: $MetricName with $AttributeName=$AttributeValue" -ForegroundColor Yellow + $progressActivity = "Waiting for Sentry metrics $MetricName with $AttributeName=$AttributeValue" + + $startTime = Get-Date + $endTime = $startTime.AddSeconds($TimeoutSeconds) + $lastError = $null + $elapsedSeconds = 0 + + try { + do { + $metrics = @() + $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 { + $fetchParams = @{ + MetricName = $MetricName + AttributeName = $AttributeName + AttributeValue = $AttributeValue + StatsPeriod = $StatsPeriod + } + if ($Fields) { + $fetchParams['Fields'] = $Fields + } + $response = Get-SentryMetricsByAttribute @fetchParams + if ($response.data -and $response.data.Count -ge $ExpectedCount) { + $metrics = $response.data + } + } catch { + $lastError = $_.Exception.Message + Write-Debug "Metrics $MetricName with $AttributeName=$AttributeValue not found yet: $lastError" + } + + if ($metrics.Count -ge $ExpectedCount) { + Write-Host "Found $($metrics.Count) metric(s) from Sentry" -ForegroundColor Green + + # Save metrics to file for debugging + $metricsJson = $metrics | ConvertTo-Json -Depth 10 + $metricsJson | Out-File -FilePath (Get-OutputFilePath "metrics-$AttributeName-$AttributeValue.json") + + # Use comma operator to ensure array is preserved (prevents PowerShell unwrapping single item) + return , @($metrics) + } + + Start-Sleep -Milliseconds 500 + $currentTime = Get-Date + } while ($currentTime -lt $endTime) + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + $foundCount = if ($metrics) { $metrics.Count } else { 0 } + throw "Expected at least $ExpectedCount metric(s) $MetricName with $AttributeName=$AttributeValue but found $foundCount within $TimeoutSeconds seconds. Last error: $lastError" +} + # Export module functions -Export-ModuleMember -Function Invoke-CMakeConfigure, Invoke-CMakeBuild, Set-OutputDir, Get-OutputFilePath, Get-EventIds, Get-SentryTestEvent, Get-SentryTestLog, Get-PackageAumid +Export-ModuleMember -Function Invoke-CMakeConfigure, Invoke-CMakeBuild, Set-OutputDir, Get-OutputFilePath, Get-EventIds, Get-SentryTestEvent, Get-SentryTestLog, Get-SentryTestMetric, Get-PackageAumid