Private/Analytics.ps1

<#
.SYNOPSIS
    Analytics and trend tracking functions for TSG searches.
#>


function Get-AnalyticsPath {
    <#
    .SYNOPSIS
        Get the path to the analytics data file.
    #>

    $cachePath = Get-CacheRoot
    return Join-Path $cachePath "analytics.json"
}

function Add-SearchAnalytics {
    <#
    .SYNOPSIS
        Record a search query for analytics tracking.
    .PARAMETER ErrorText
        The error text that was searched for.
    .PARAMETER ResultCount
        Number of results returned.
    .PARAMETER TopMatch
        Title of the top matching TSG (if any).
    .PARAMETER TopConfidence
        Confidence score of the top match.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ErrorText,
        
        [Parameter(Mandatory)]
        [int]$ResultCount,
        
        [Parameter()]
        [string]$TopMatch,
        
        [Parameter()]
        [int]$TopConfidence
    )

    try {
        $analyticsPath = Get-AnalyticsPath
        
        # Load existing analytics or create new
        $analytics = @{
            Searches = @()
        }
        
        if (Test-Path $analyticsPath) {
            $existingData = Get-Content $analyticsPath -Raw -ErrorAction SilentlyContinue
            if ($existingData) {
                $analytics = $existingData | ConvertFrom-Json -AsHashtable
            }
        }
        
        # Create search record
        $searchRecord = @{
            Timestamp     = (Get-Date).ToString('o')
            ErrorText     = $ErrorText.Substring(0, [Math]::Min(200, $ErrorText.Length))  # Truncate long errors
            ResultCount   = $ResultCount
            TopMatch      = $TopMatch
            TopConfidence = $TopConfidence
        }
        
        # Add to searches array
        $analytics.Searches += $searchRecord
        
        # Keep only last 1000 searches to prevent file from growing too large
        if ($analytics.Searches.Count -gt 1000) {
            $analytics.Searches = $analytics.Searches | Select-Object -Last 1000
        }
        
        # Save analytics
        $analytics | ConvertTo-Json -Depth 10 | Set-Content -Path $analyticsPath -Force
    } catch {
        # Silently fail - analytics shouldn't break the main tool
        Write-Verbose "Failed to record analytics: $_"
    }
}

function Get-SearchAnalytics {
    <#
    .SYNOPSIS
        Retrieve search analytics data.
    .PARAMETER DaysBack
        Number of days to look back. Default is 30.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [int]$DaysBack = 30
    )

    $analyticsPath = Get-AnalyticsPath
    
    if (-not (Test-Path $analyticsPath)) {
        Write-Warning "No analytics data found. Start using Get-AzLocalTSGFix to collect data."
        return $null
    }
    
    $analytics = Get-Content $analyticsPath -Raw | ConvertFrom-Json
    
    # Filter by date range
    $cutoffDate = (Get-Date).AddDays(-$DaysBack)
    $recentSearches = @($analytics.Searches | Where-Object {
            try {
                [DateTime]::Parse($_.Timestamp) -gt $cutoffDate
            } catch {
                $false
            }
        })
    
    if ($recentSearches.Count -eq 0) {
        Write-Warning "No searches found in the last $DaysBack days."
        return $null
    }
    
    # Calculate statistics
    $totalSearches = $recentSearches.Count
    $noResultsCount = @($recentSearches | Where-Object { $_.ResultCount -eq 0 }).Count
    $lowConfidenceCount = @($recentSearches | Where-Object { $_.ResultCount -gt 0 -and $_.TopConfidence -lt 20 }).Count
    
    # Top errors (by frequency)
    $errorFrequency = $recentSearches | Group-Object ErrorText | 
    Sort-Object Count -Descending | 
    Select-Object -First 10 | 
    ForEach-Object {
        [PSCustomObject]@{
            ErrorText  = $_.Name
            Count      = $_.Count
            Percentage = [math]::Round(($_.Count / $totalSearches * 100), 1)
        }
    }
    
    # Top matched TSGs
    $tsgFrequency = $recentSearches | 
    Where-Object { $_.TopMatch } | 
    Group-Object TopMatch | 
    Sort-Object Count -Descending | 
    Select-Object -First 10 | 
    ForEach-Object {
        [PSCustomObject]@{
            TSG           = $_.Name
            MatchCount    = $_.Count
            AvgConfidence = [math]::Round(($_.Group | Measure-Object -Property TopConfidence -Average).Average, 1)
        }
    }
    
    # Searches with no/poor results (potential gaps)
    $gaps = $recentSearches | 
    Where-Object { $_.ResultCount -eq 0 -or ($_.ResultCount -gt 0 -and $_.TopConfidence -lt 15) } |
    Group-Object ErrorText |
    Sort-Object Count -Descending |
    Select-Object -First 10 |
    ForEach-Object {
        [PSCustomObject]@{
            ErrorText      = $_.Name
            SearchCount    = $_.Count
            BestConfidence = ($_.Group | Measure-Object -Property TopConfidence -Maximum).Maximum
        }
    }
    
    # Return analytics summary
    return [PSCustomObject]@{
        Period              = "$DaysBack days"
        TotalSearches       = $totalSearches
        NoResultsCount      = $noResultsCount
        LowConfidenceCount  = $lowConfidenceCount
        NoResultsPercentage = [math]::Round(($noResultsCount / $totalSearches * 100), 1)
        TopErrors           = $errorFrequency
        TopMatchedTSGs      = $tsgFrequency
        PotentialGaps       = $gaps
    }
}