Private/Telemetry.ps1

<#
.SYNOPSIS
    Internal helpers for Invoke-TelemetryCollection.

.NOTES
    Private helpers for the tcs.core module.
#>


# Stopwatches for in-flight commands, keyed by ExecutionID
$script:TelemetryTimers = @{}
$script:TelemetryHttpClient = $null
$script:TelemetryInstallationId = $null

function Test-TelemetryOptOut {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    return ([string]$env:TCS_TELEMETRY_OPTOUT -in @('1', 'true', 'yes'))
}

function Get-TelemetryInstallationId {
    <#
    .SYNOPSIS
        Returns a random, anonymous ID for this user profile, created on first use.
        It is not derived from any hardware, user or machine identifier.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param()

    if ($script:TelemetryInstallationId) {
        return $script:TelemetryInstallationId
    }

    $idPath = Join-Path -Path (Join-Path -Path (Get-ModuleConfigRoot) -ChildPath 'tcs.core') -ChildPath 'Telemetry.json'
    $id = $null
    try {
        if (Test-Path -LiteralPath $idPath) {
            $id = [string](Read-JsonFileAsHashtable -Path $idPath)['InstallationId']
        }
        if ([string]::IsNullOrWhiteSpace($id)) {
            $id = [guid]::NewGuid().ToString()
            Write-JsonFile -Path $idPath -Data @{ InstallationId = $id }
        }
    }
    catch {
        # Fall back to a per-session ID if the profile is not writable
        if ([string]::IsNullOrWhiteSpace($id)) {
            $id = [guid]::NewGuid().ToString()
        }
        Write-Verbose "Using a per-session telemetry ID: $($_.Exception.Message)"
    }

    $script:TelemetryInstallationId = $id
    return $id
}

function Get-TelemetryPlatform {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    if (Test-IsWindowsPlatform) { return 'Windows' }
    if ($IsMacOS) { return 'macOS' }
    if ($IsLinux) { return 'Linux' }
    return 'Unknown'
}

function Send-TelemetryPayload {
    <#
    .SYNOPSIS
        Posts a JSON payload without waiting for the response (fire and forget).
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)]
        [string]$Uri,

        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$Body
    )

    if (-not $script:TelemetryHttpClient) {
        if (-not ('System.Net.Http.HttpClient' -as [type])) {
            Add-Type -AssemblyName System.Net.Http
        }
        if ($PSVersionTable.PSEdition -eq 'Desktop') {
            # .NET Framework may not enable TLS 1.2 by default; add it without removing anything
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
        }
        $client = New-Object System.Net.Http.HttpClient
        $client.Timeout = [TimeSpan]::FromSeconds(5)
        $script:TelemetryHttpClient = $client
    }

    $request = New-Object System.Net.Http.HttpRequestMessage -ArgumentList ([System.Net.Http.HttpMethod]::Post), $Uri
    if (-not [string]::IsNullOrEmpty($ApiKey)) {
        $null = $request.Headers.TryAddWithoutValidation('X-API-Key', $ApiKey)
    }
    $request.Content = New-Object System.Net.Http.StringContent -ArgumentList $Body, ([System.Text.Encoding]::UTF8), 'application/json'

    # The request runs on the thread pool; the caller never waits and failures are ignored
    $null = $script:TelemetryHttpClient.SendAsync($request)
}