Private/TelemetryScope.ps1
|
<# .SYNOPSIS Internal helpers for Start-TcsTelemetry, Complete-TcsTelemetry and Invoke-TcsCommand. .DESCRIPTION Each command run gets a token. Only the outermost run of a module is reported: when an exported command calls another exported command of the same module, the inner run gets a token with IsOutermost = $false and sends nothing. A run counts as nested when a run of the same module that has not completed is still on the call stack. Checking the call stack (rather than keeping a counter) means a run that never completed, for example because a downstream Select-Object -First stopped the pipeline, cannot hide later runs. .NOTES Private helpers for the tcs.core module. #> # Runs that have started and not completed, per module: list of @{ Token; Invocation } $script:ActiveTelemetryRuns = @{} function New-TcsTelemetryToken { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Only creates an in-memory object.')] [CmdletBinding()] [OutputType([PSCustomObject])] param( [AllowNull()] [System.Management.Automation.InvocationInfo]$Invocation, [string]$CommandName, [string]$ModuleName, [string]$ModuleVersion ) $command = $null if ($Invocation) { $command = $Invocation.MyCommand } if ([string]::IsNullOrEmpty($CommandName)) { $CommandName = if ($command -and $command.Name) { [string]$command.Name } else { 'UnknownCommand' } } if ([string]::IsNullOrEmpty($ModuleName)) { $ModuleName = if ($command -and $command.Module) { [string]$command.Module.Name } else { 'UnknownModule' } } if ([string]::IsNullOrEmpty($ModuleVersion)) { $ModuleVersion = if ($command -and $command.Module -and $command.Module.Version) { [string]$command.Module.Version } else { 'Unknown' } } $runs = $script:ActiveTelemetryRuns[$ModuleName] if (-not $runs) { $runs = New-Object System.Collections.Generic.List[object] $script:ActiveTelemetryRuns[$ModuleName] = $runs } $isOutermost = $true if ($runs.Count -gt 0) { $frames = @(Get-PSCallStack) foreach ($run in $runs) { foreach ($frame in $frames) { if ($null -ne $frame.InvocationInfo -and [object]::ReferenceEquals($frame.InvocationInfo, $run.Invocation)) { $isOutermost = $false break } } if (-not $isOutermost) { break } } } $token = [PSCustomObject]@{ PSTypeName = 'Tcs.TelemetryToken' Id = [guid]::NewGuid().ToString() CommandName = $CommandName ModuleName = $ModuleName ModuleVersion = $ModuleVersion IsOutermost = $isOutermost Failed = $false Completed = $false Exception = $null } if ($isOutermost) { # Runs that never completed are kept (they cannot cause false nesting), but not forever while ($runs.Count -ge 100) { $runs.RemoveAt(0) } $runs.Add(@{ Token = $token; Invocation = $Invocation }) Invoke-TelemetryCollection -ModuleName $ModuleName -ModuleVersion $ModuleVersion -CommandName $CommandName -ExecutionID $token.Id -Stage Start -ClearTimer } return $token } function Set-TcsTelemetryFailure { <# .SYNOPSIS Marks a run as failed and remembers the first error, whose type is reported. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Only changes an in-memory object.')] [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [PSTypeName('Tcs.TelemetryToken')] [PSCustomObject]$Token, [AllowNull()] [object]$ErrorRecord ) $Token.Failed = $true if ($null -eq $ErrorRecord -or $null -ne $Token.Exception) { return } $exception = if ($ErrorRecord -is [System.Management.Automation.ErrorRecord]) { $ErrorRecord.Exception } else { $ErrorRecord } if ($exception -is [System.Exception]) { $Token.Exception = $exception } } function Complete-TcsTelemetryToken { [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [PSTypeName('Tcs.TelemetryToken')] [PSCustomObject]$Token ) if ($Token.Completed) { return } $Token.Completed = $true $runs = $script:ActiveTelemetryRuns[$Token.ModuleName] if ($runs) { for ($i = $runs.Count - 1; $i -ge 0; $i--) { if ([object]::ReferenceEquals($runs[$i].Token, $Token)) { $runs.RemoveAt($i) } } } if ($Token.IsOutermost) { Invoke-TelemetryCollection -ModuleName $Token.ModuleName -ModuleVersion $Token.ModuleVersion -CommandName $Token.CommandName -ExecutionID $Token.Id -Stage End -Failed ([bool]$Token.Failed) -Exception $Token.Exception } } function Invoke-TcsScriptBlock { <# .SYNOPSIS Runs a script block in the scope it was written in (dot-sourced), so its variables persist, as if it were written inline. #> [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock ) . $ScriptBlock } |