Comment collecter les avis des utilisateurs avec PowerShell

L’expérience utilisateur est un paramètre essentiel dans le monde informatique actuel, en particulier pour les administrateurs informatiques et les fournisseurs de services gérés (MSP) qui gèrent les environnements des utilisateurs finaux. Avec l’importance croissante accordée au contrôle de l’expérience numérique (DEM – Digital Experience Monitoring) et à l’expérience des appareils (DEX – Device Experience), la capacité de collecter, de stocker et d’analyser efficacement le retour d’information des utilisateurs est devenue une priorité. L’automatisation de ce processus permet d’obtenir des informations cohérentes et de réduire les tâches manuelles. Cet article explique comment collecter les avis des utilisateurs avec PowerShell et les stocker dans les champs personnalisés de NinjaOne, transformant ainsi les avis bruts en informations structurées et exploitables.

Contexte

Alors que les entreprises s’orientent vers une assistance informatique proactive, l’enquête DEX est devenue un outil précieux pour mesurer la façon dont les utilisateurs perçoivent les performances, la réactivité et la facilité d’utilisation des appareils. Le script dont il est question ici est le deuxième d’un flux de travail en trois parties. Le script 1 invite les utilisateurs à donner leur avis et enregistre leurs réponses localement. Le script 2, qui fait l’objet de cet article, lit ces fichiers journaux, traite les derniers avis et les enregistre dans les champs personnalisés de NinjaOne : pour les avis récents et pour l’historique des avis (facultatif). Pour les professionnels de l’informatique qui utilisent NinjaOne, cette intégration transparente garantit que les données sur les sentiments des utilisateurs sont facilement accessibles dans la plateforme RMM pour l’établissement de rapports ou l’application de mesures correctives.

Le script

#Requires -Version 5.1

<#
.SYNOPSIS
    Script 2 of 3 in the DEX Survey experience. Saves the latest feedback from DEX Survey - Script 1 to the multiline custom field 'Latest User Feedback' and optionally stores feedback history in the WYSIWYG field 'User Feedback History'.
.DESCRIPTION
    Script 2 of 3 in the DEX Survey experience. Saves the latest feedback from DEX Survey - Script 1 to the multiline custom field 'Latest User Feedback' and optionally stores feedback history in the WYSIWYG field 'User Feedback History'.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
    Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
.EXAMPLE
    -MaxCommentsToKeep "30" -KeepFeedbackHistory

    Gathering all user profiles.
    Gathering each user's feedback log location.

    Checking 'C:\Users\tuser1\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log' for user feedback.
    New feedback was found.

    Checking 'C:\Users\tuser2\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log' for user feedback.
    New feedback was found.

    Checking 'C:\Users\kpradlander\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log' for user feedback.
    New feedback was found.

    Compiling the most recent feedback.
    All the most recent feedback has already been collected.
    Attempting to save the feedback to the custom field 'userFeedbackHistory'.
    Successfully saved the feedback history to the custom field.

    Updating the feedback log(s) to indicate that the feedback has been collected.
    Updated the log file at 'C:\Users\tuser2\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log'.
    Updated the log file at 'C:\Users\kpradlander\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log'.
    Updated the log file at 'C:\Users\tuser1\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log'.

    Cleaning up the log files so only the previous '30' are kept.
    The log file at 'C:\Users\tuser1\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log' already has less than or equal to '30' comments.
    The log file at 'C:\Users\tuser2\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log' already has less than or equal to '30' comments.
    The log file at 'C:\Users\kpradlander\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log' already has less than or equal to '30' comments.

.PARAMETER KeepFeedbackHistory
    This stores the user feedback history in a custom field titled 'User Feedback History'.

.PARAMETER MaxCommentsToKeep
    This controls the number of feedback comments to retain in the log file(s).

.PARAMETER MultilineCustomFieldName
    The name of a multiline custom field to store the latest user feedback.

.PARAMETER WYSIWYGCustomFieldName
    The name of a WYSIWYG custom field to store the total user feedback history.

.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial Release
#>

[CmdletBinding()]
param (
    [Parameter()]
    [Switch]$KeepFeedbackHistory = [System.Convert]::ToBoolean($env:KeepFeedbackHistory),
    [Parameter()]
    [String]$MaxCommentsToKeep = "30",
    [Parameter()]
    [String]$MultilineCustomFieldName = "latestUserFeedback",
    [Parameter()]
    [String]$WYSIWYGCustomFieldName = "userFeedbackHistory"
)

begin {
    # If the script form variables are used, replace the command-line parameters with their values.
    if ($env:maxCommentsToKeep) { $MaxCommentsToKeep = $env:maxCommentsToKeep }

    # Check if the operating system build version is less than 10240 (Windows 10 or Windows Server 2016 minimum requirement)
    if ([System.Environment]::OSVersion.Version.Build -lt 10240) {
        Write-Host -Object "`n[Warning] OS build '$([System.Environment]::OSVersion.Version.Build)' detected."
        Write-Host -Object "[Warning] The minimum OS version supported by this script is Windows 10 (10240) or Windows Server 2016 (14393).`n"
    }

    # Validate that the maximum number of comments to keep is not null or empty
    if ([String]::IsNullOrWhiteSpace($MaxCommentsToKeep)) {
        Write-Host -Object "[Error] You must specify a maximum number of comments to keep."
        exit 1
    }

    # Ensure the value for 'MaxCommentsToKeep' contains only numeric characters
    if ($MaxCommentsToKeep -match "[^0-9]") {
        Write-Host -Object "[Error] The maximum number of comments you specified '$MaxCommentsToKeep' is invalid. It contains invalid characters."
        Write-Host -Object "[Error] Please specify a positive whole number that is greater than 0 and less than $([int]::MaxValue)."
        exit 1
    }

    # Attempt to convert the 'MaxCommentsToKeep' value to a long integer
    try {
        [long]$CommentsToKeep = $MaxCommentsToKeep
    } catch {
        Write-Host -Object "[Error] $($_.Exception.Message)"
        Write-Host -Object "[Error] Unable to convert '$MaxCommentsToKeep' into an integer."
        exit 1
    }

    # Validate that the converted value is within the acceptable range
    if ($CommentsToKeep -le 0 -or $CommentsToKeep -ge [int]::MaxValue) {
        Write-Host -Object "[Error] The maximum number of comments you specified '$CommentsToKeep' is invalid."
        Write-Host -Object "[Error] Please specify a positive whole number that is greater than 0 and less than $([int]::MaxValue)."
        exit 1
    }

    function ConvertFrom-UrlSafeBase64 {
        [CmdletBinding()]
        param (
            [Parameter(Mandatory)]
            [String]$UrlSafeBase64
        )

        # Normalize base64 (replace URL-safe chars)
        $Base64 = $UrlSafeBase64.Replace('-', '+').Replace('_', '/')

        # Add padding if needed
        switch ($Base64.Length % 4) {
            2 { $Base64 = "$Base64==" }
            3 { $Base64 = "$Base64=" }
            1 { throw "Invalid Base64 string" }
        }

        # Convert from base64 to text
        $InputInBytes = [System.Convert]::FromBase64String($Base64)
        [System.Text.Encoding]::UTF8.GetString($InputInBytes)
    }

    function Get-UserHive {
        [CmdletBinding()]
        param (
            [Parameter()]
            [ValidateSet('AzureAD', 'DomainAndLocal', 'All')]
            [String]$Type = "All",
            [Parameter()]
            [String[]]$ExcludedUsers,
            [Parameter()]
            [switch]$IncludeDefault
        )

        # Define the SID patterns to match based on the selected user type
        $Patterns = switch ($Type) {
            "AzureAD" { "S-1-12-1-(\d+-?){4}$" }
            "DomainAndLocal" { "S-1-5-21-(\d+-?){4}$" }
            "All" { "S-1-12-1-(\d+-?){4}$" ; "S-1-5-21-(\d+-?){4}$" }
        }

        # Retrieve user profile information based on the defined patterns
        $UserProfiles = Foreach ($Pattern in $Patterns) {
            Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" |
                Where-Object { $_.PSChildName -match $Pattern } |
                Select-Object @{Name = "SID"; Expression = { $_.PSChildName } },
                @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf)" } },
                @{Name = "Domain"; Expression = { if ($_.PSChildName -match "S-1-12-1-(\d+-?){4}$") { "AzureAD" }else { $Null } } },
                @{Name = "UserHive"; Expression = { "$($_.ProfileImagePath)\NTuser.dat" } },
                @{Name = "Path"; Expression = { $_.ProfileImagePath } }
        }

        # If the IncludeDefault switch is set, add the Default profile to the results
        switch ($IncludeDefault) {
            $True {
                $DefaultProfile = "" | Select-Object Username, SID, UserHive, Path
                $DefaultProfile.Username = "Default"
                $DefaultProfile.Domain = $env:COMPUTERNAME
                $DefaultProfile.SID = "DefaultProfile"
                $DefaultProfile.Userhive = "$env:SystemDrive\Users\Default\NTUSER.DAT"
                $DefaultProfile.Path = "C:\Users\Default"

                # Exclude users specified in the ExcludedUsers list
                $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username }
            }
        }

        if ($PSVersionTable.PSVersion.Major -lt 3) {
            $AllAccounts = Get-WmiObject -Class "win32_UserAccount"
        } else {
            $AllAccounts = Get-CimInstance -ClassName "win32_UserAccount"
        }

        $CompleteUserProfiles = $UserProfiles | ForEach-Object {
            $SID = $_.SID
            $Win32Object = $AllAccounts | Where-Object { $_.SID -like $SID }

            if ($Win32Object) {
                $Win32Object | Add-Member -NotePropertyName UserHive -NotePropertyValue $_.UserHive
                $Win32Object | Add-Member -NotePropertyName Path -NotePropertyValue $_.Path
                $Win32Object
            } else {
                [PSCustomObject]@{
                    Name     = $_.Username
                    Domain   = $_.Domain
                    SID      = $_.SID
                    UserHive = $_.UserHive
                    Path     = $_.Path
                }
            }
        }

        # Return the list of user profiles, excluding any specified in the ExcludedUsers list
        $CompleteUserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Name }
    }

    function Test-IsElevated {
        [CmdletBinding()]
        param ()

        # Get the current Windows identity of the user running the script
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()

        # Create a WindowsPrincipal object based on the current identity
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)

        # Check if the current user is in the Administrator role
        # The function returns $True if the user has administrative privileges, $False otherwise
        # 544 is the value for the Built In Administrators role
        # Reference: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsbuiltinrole
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544')
    }

    if (!$ExitCode) {
        $ExitCode = 0
    }
}
process {
    # Attempt to determine if the current session is running with Administrator privileges.
    try {
        $IsElevated = Test-IsElevated -ErrorAction Stop
    } catch {
        # Log an error if unable to determine elevation status
        Write-Host -Object "[Error] $($_.Exception.Message)"
        Write-Host -Object "[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges."
        exit 1
    }

    # Exit if the script is not running with Administrator privileges
    if (!$IsElevated) {
        Write-Host -Object "[Error] Access Denied: The user '$env:Username' does not have administrator privileges, or the script is not running with elevated permissions."
        exit 1
    }

    try {
        # Retrieve all user profiles on the system
        Write-Host -Object "Gathering all user profiles."
        $UserProfiles = Get-UserHive -Type "All" -ErrorAction Stop
        $ProfileWasLoaded = New-Object System.Collections.Generic.List[object]
    } catch {
        # Log an error if unable to gather user profiles
        Write-Host -Object "[Error] $($_.Exception.Message)"
        Write-Host -Object "[Error] Failed to gather all the user profiles."
        exit 1
    }

    # Loop through each user profile to load their registry hive if not already loaded
    ForEach ($UserProfile in $UserProfiles) {
        If (!(Test-Path -Path Registry::HKEY_USERS\$($UserProfile.SID) -ErrorAction SilentlyContinue)) {
            Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe LOAD HKU\$($UserProfile.SID) `"$($UserProfile.UserHive)`"" -Wait -WindowStyle Hidden
            $ProfileWasLoaded.Add($UserProfile)
        }
    }

    # Locate feedback log files for each user profile
    Write-Host -Object "Gathering each user's feedback log location."
    $FeedbackLogLocations = New-Object System.Collections.Generic.List[object]
    $UserProfiles | ForEach-Object {
        # Check for feedback log file in various locations
        $AppDataLocation = Get-ItemProperty -Path "Registry::HKEY_USERS\$($_.SID)\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "Local AppData" -ErrorAction SilentlyContinue

        if ($AppDataLocation -and (Test-Path -Path "$AppDataLocation\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log" -Type Leaf -ErrorAction SilentlyContinue)) {
            $FeedbackLogLocations.Add(
                [PSCustomObject]@{
                    Name            = $_.Name
                    Domain          = $_.Domain
                    LogFileLocation = "$AppDataLocation\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log"
                }
            )
            return
        }

        if (Test-Path -Path "$($_.Path)\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log" -Type Leaf -ErrorAction SilentlyContinue) {
            $FeedbackLogLocations.Add(
                [PSCustomObject]@{
                    Name            = $_.Name
                    Domain          = $_.Domain
                    LogFileLocation = "$($_.Path)\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log"
                }
            )
            return
        }

        if (Test-Path -Path "C:\Users\$($_.Name)\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log" -Type Leaf -ErrorAction SilentlyContinue) {
            $FeedbackLogLocations.Add(
                [PSCustomObject]@{
                    Name            = $_.Name
                    Domain          = $_.Domain
                    LogFileLocation = "C:\Users\$($_.Name)\AppData\Local\NinjaOne-DEX-Survey-Script\UserDeviceFeedback.log"
                }
            )
            return
        }

        # Log a message if no feedback log file is found for the user
        Write-Host -Object "No feedback log file found for $($_.Name) in the domain $($_.Domain)."
    }

    # Unload user registry hives if they were loaded during this script execution
    if ($ProfileWasLoaded.Count -gt 0) {
        ForEach ($UserProfile in $ProfileWasLoaded) {
            [gc]::Collect()
            Start-Sleep 1
            Start-Process -FilePath "cmd.exe" -ArgumentList "/C reg.exe UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden | Out-Null
        }
    }

    # Log a warning if no feedback log files were found
    if ($FeedbackLogLocations.Count -lt 1) {
        Write-Host -Object "[Warning] No feedback was found."
    }

    Write-Host -Object ""

    # Initialize a list to store all feedback entries
    $AllFeedback = New-Object System.Collections.Generic.List[object]
    $FeedbackLogLocations | ForEach-Object {
        $LogFileLocation = $_.LogFileLocation

        # Check the feedback log file for user feedback
        Write-Host -Object "Checking '$LogFileLocation' for user feedback."

        try {
            # Read the first line of the log file to ensure it exists
            Get-Content -Path $LogFileLocation -TotalCount 1 -ErrorAction Stop | Out-Null
        } catch {
            # Log an error if unable to read the log file
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to retrieve the feedback log file contents for the user '$($_.Name)'."
            $ExitCode = 1
            return
        }

        # Parse the feedback log file contents
        $i = 0
        Get-Content -Path $LogFileLocation -Tail 200000 | ForEach-Object {
            $i++
            $DataPoints = $_ -split '\|', 6

            # Warn if a line has fewer than 6 data points
            if ($DataPoints.Count -lt 6) {
                Write-Host -Object "[Warning] Only $($DataPoints.Count) data points have been found for line '$i'."
                return
            }

            # Parse the timestamp from the log entry
            try {
                $CurrentTimeStamp = [datetime]::ParseExact($DataPoints[0], 'yyyy-MM-dd HH:mm:ssK', $null)
            } catch {
                Write-Host -Object "[Warning] $($_.Exception.Message)"
                Write-Host -Object "[Warning] Failed to parse the date '$($DataPoints[0])' from the feedback log."
            }

            # Decode the feedback message from Base64
            try {
                $UserMessage = ConvertFrom-UrlSafeBase64 -UrlSafeBase64 $DataPoints[5] -ErrorAction Stop
            } catch {
                Write-Host -Object "[Warning] $($_.Exception.Message)"
                Write-Host -Object "[Warning] Failed to parse the following feedback message from the feedback log: $($DataPoints[5])"
            }

            # Add the parsed feedback entry to the list
            $AllFeedback.Add(
                [PSCustomObject]@{
                    TimeStamp               = $CurrentTimeStamp
                    Username                = $DataPoints[1]
                    Domain                  = $DataPoints[2]
                    FeedbackCollected       = $DataPoints[3]
                    SystemPerformanceHasRun = $DataPoints[4]
                    Message                 = $UserMessage
                    Base64                  = $DataPoints[5]
                    LogLocation             = $LogFileLocation
                }
            )
        }

        # Filter feedback entries for the current log file
        $UserFeedback = $AllFeedback | Where-Object { $_.LogLocation -eq $LogFileLocation }

        # Log a warning if no feedback entries are found in the log file
        if (!$UserFeedback) {
            Write-Host -Object "[Warning] The log file does not contain any feedback."
            Write-Host -Object ""
            return
        }

        # Check if there is new feedback in the log file
        if (!($UserFeedback | Where-Object { $_.FeedbackCollected -eq $False })) {
            Write-Host -Object "There is no new feedback in this log file."
        } else {
            Write-Host -Object "New feedback was found."
        }

        Write-Host -Object ""
    }

    # Check if there is no feedback collected
    if ($AllFeedback.Count -eq 0) {
        try {
            # Retrieve the current value of the custom field for the latest feedback
            Write-Host -Object "Checking the current value of the custom field '$MultilineCustomFieldName'."
            $CurrentCustomFieldValue = Get-NinjaProperty -Name $MultilineCustomFieldName -ErrorAction Stop
        } catch {
            # Log an error if unable to retrieve the custom field value
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to retrieve the value from the custom field $MultilineCustomFieldName."
            $ExitCode = 1
        }

        # Update the custom field if it is not already set to "No feedback has been given."
        if ($CurrentCustomFieldValue -ne "No feedback has been given.") {
            try {
                Write-Host -Object "Attempting to set the custom field '$MultilineCustomFieldName'."
                Set-NinjaProperty -Name $MultilineCustomFieldName -Value "No feedback has been given." -ErrorAction Stop
                Write-Host -Object "Successfully saved the feedback to the custom field."
            } catch {
                # Log an error if unable to update the custom field
                Write-Host -Object "[Error] $($_.Exception.Message)"
                Write-Host -Object "[Error] Failed to save to the custom field $MultilineCustomFieldName."
                $ExitCode = 1
            }
        } else {
            Write-Host -Object "The custom field is already set to 'No feedback has been given.'."
        }

        # Handle feedback history if the KeepFeedbackHistory switch is enabled
        if ($KeepFeedbackHistory) {
            try {
                # Retrieve the current value of the custom field for feedback history
                Write-Host -Object "`nChecking the current value of the custom field '$WYSIWYGCustomFieldName'."
                $CurrentCustomFieldValue = Get-NinjaProperty -Name $WYSIWYGCustomFieldName -Type "WYSIWYG" -ErrorAction Stop
            } catch {
                # Log an error if unable to retrieve the custom field value
                Write-Host -Object "[Error] $($_.Exception.Message)"
                Write-Host -Object "[Error] Failed to retrieve the value from the custom field $WYSIWYGCustomFieldName."
                $ExitCode = 1
            }

            # Trim the current value if it is not null or empty
            if (![string]::IsNullOrWhiteSpace($CurrentCustomFieldValue.Text)) {
                $CurrentCustomFieldValue = $CurrentCustomFieldValue.Text.Trim()
            }

            # Update the custom field if it is not already set to "No feedback has been given."
            if ($CurrentCustomFieldValue -ne "No feedback has been given.") {
                try {
                    Write-Host -Object "Attempting to set the custom field '$WYSIWYGCustomFieldName'."
                    Set-NinjaProperty -Name $WYSIWYGCustomFieldName -Value "No feedback has been given." -ErrorAction Stop
                    Write-Host -Object "Successfully saved the feedback history to the custom field."
                } catch {
                    # Log an error if unable to update the custom field
                    Write-Host -Object "[Error] $($_.Exception.Message)"
                    Write-Host -Object "[Error] Failed to save to the custom field $WYSIWYGCustomFieldName."
                    $ExitCode = 1
                }
            } else {
                Write-Host -Object "The custom field is already set to 'No feedback has been given.'."
            }
        }

        # Exit the script with the current exit code
        exit $ExitCode
    }

    # Compile the most recent feedback
    Write-Host -Object "Compiling the most recent feedback."
    if ($AllFeedback.Count -gt 1) {
        # Sort feedback by timestamp in descending order
        $AllFeedback = [System.Collections.Generic.List[object]]::new(($AllFeedback | Sort-Object -Property "Timestamp" -Descending))
    }

    # Initialize lists for most recent feedback and collected feedback
    $MostRecentFeedback = New-Object System.Collections.Generic.List[object]
    $FeedbackCollected = New-Object System.Collections.Generic.List[object]

    # Group feedback by log location and retrieve the most recent entry for each user
    $FeedbackPerUser = $AllFeedback | Group-Object -Property "LogLocation"
    $FeedbackPerUser | ForEach-Object {
        $MostRecentFeedback.Add(
            (
                $_.Group | Sort-Object -Property TimeStamp | Select-Object -Last 1
            )
        )
    }

    # Check if all the most recent feedback has already been collected
    if (!($MostRecentFeedback | Where-Object { $_.FeedbackCollected -eq $False })) {
        Write-Host -Object "All the most recent feedback has already been collected.`n"
    }

    # Process new feedback if available
    if ($MostRecentFeedback | Where-Object { $_.FeedbackCollected -eq $False }) {
        $LatestUserFeedbackValue = New-Object System.Collections.Generic.List[String]
        if ($MostRecentFeedback.Count -gt 1) {
            # Sort most recent feedback by timestamp in descending order
            $MostRecentFeedback = [System.Collections.Generic.List[object]]::new(($MostRecentFeedback | Sort-Object -Property "TimeStamp" -Descending))
        }

        # Format the feedback entries for display
        $MostRecentFeedback | ForEach-Object {
            $DateString = "$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())"
            $LatestUserFeedbackValue.Add("$DateString | $($_.Username) | $($_.Domain)")
            $LatestUserFeedbackValue.Add([String]$_.Message)
            $LatestUserFeedbackValue.Add("")
        }

        try {
            # Check if the character limit for the custom field is exceeded
            $Characters = ($LatestUserFeedbackValue | Out-String) | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters
            if ($Characters -ge 9500) {
                Write-Host -Object "[Warning] The character limit of 10,000 has been reached! Trimming the output until the character limit is satisfied."

                $TrimStart = Get-Date
                do {
                    # Notify the user that data is being truncated
                    $LatestUserFeedbackValue = New-Object System.Collections.Generic.List[String]
                    $LatestUserFeedbackValue.Add("This info has been truncated to accommodate the 10,000 character limit.")
                    $LatestUserFeedbackValue.Add("")

                    # Calculate the batch size for trimming
                    $ExceededAmount = $Characters - 9500
                    if ($ExceededAmount -le 0) {
                        $BatchSize = 1
                    } else {
                        $BatchSize = [math]::Ceiling($ExceededAmount / 500)
                    }

                    # Remove excess feedback entries
                    for ($i = 0 ; $i -lt $BatchSize ; $i++) {
                        $MostRecentFeedback.RemoveAt($MostRecentFeedback.Count - 1)
                    }

                    # Reformat the remaining feedback entries
                    $MostRecentFeedback | ForEach-Object {
                        $DateString = "$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())"
                        $LatestUserFeedbackValue.Add("$DateString | $($_.Username) | $($_.Domain)")
                        $LatestUserFeedbackValue.Add([String]$_.Message)
                        $LatestUserFeedbackValue.Add("")
                    }

                    # Recalculate character count and continue trimming if necessary
                    $Characters = ($LatestUserFeedbackValue | Out-String) | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters
                    $ElapsedTime = (Get-Date) - $TrimStart
                    if ($ElapsedTime.TotalMinutes -ge 5) {
                        Write-Host -Object "The current character count is '$Characters'."
                        throw "5 minute timeout reached. Unable to trim the output to comply with the character limit."
                    }
                } while ($Characters -ge 9500)
            }

            # Save the most recent feedback to the custom field
            Write-Host -Object "Attempting to set the custom field '$MultilineCustomFieldName'."
            $LatestUserFeedbackValue | Set-NinjaProperty -Name $MultilineCustomFieldName -ErrorAction Stop
            $MostRecentFeedback | ForEach-Object { $FeedbackCollected.Add($_) }
            Write-Host -Object "Successfully saved the feedback to the custom field.`n"
        } catch {
            # Log an error if unable to save the feedback
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to save the most recent feedback to the custom field '$MultilineCustomFieldName'."
            $ExitCode = 1
        }
    }

    # Check if feedback history should be kept and if all feedback entries have already been collected
    if ($KeepFeedbackHistory -and !($AllFeedback | Select-Object -First $CommentsToKeep | Where-Object { $_.FeedbackCollected -eq $False })) {
        Write-Host -Object "The past '$CommentsToKeep' feedback entries have already been collected.`nAttempting to verify if any update is necessary."

        try {
            # Retrieve the current value of the custom field for feedback history
            Write-Host -Object "Checking the current value of the custom field '$WYSIWYGCustomFieldName'."
            $WysiwygValue = Get-NinjaProperty -Name $WYSIWYGCustomFieldName -ErrorAction Stop

            # Extract the HTML content from the custom field value if it exists
            if ($WysiwygValue) {
                $WysiwygHTML = $WysiwygValue | ConvertFrom-Json -ErrorAction Stop | Select-Object -ExpandProperty HTML -ErrorAction SilentlyContinue
            }
        } catch {
            # Log an error if unable to retrieve the custom field value
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to retrieve the current value of the custom field '$WYSIWYGCustomFieldName'."
            $ExitCode = 1
        }

        # Calculate the expected number of feedback entries based on the specified limit
        $ExpectedNumberOfEntries = $AllFeedback | Select-Object -First $CommentsToKeep | Measure-Object -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Count -ErrorAction SilentlyContinue

        # Check if the current HTML content matches the expected number of entries
        if ($WysiwygHTML) {
            # Count the number of entries in the current HTML content
            $WysiwygEntries = $WysiwygHTML -split "<tr>" | Where-Object { $_ -match "</td>" } | Measure-Object -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Count -ErrorAction SilentlyContinue

            # Determine if an update is necessary based on the mismatch in entry counts
            if ($WysiwygEntries -ne $ExpectedNumberOfEntries) {
                Write-Host -Object "There are currently '$WysiwygEntries' entries when '$ExpectedNumberOfEntries' entries were expected. An update is necessary."
                $NeedToUpdateWysiwyg = $True
            }
        } else {
            # Log a message if no entries exist in the current HTML content
            Write-Host -Object "There are currently no entries when '$ExpectedNumberOfEntries' entries were expected. An update is necessary."
            $NeedToUpdateWysiwyg = $True
        }

        # Log a message if no update is necessary
        if (!$NeedToUpdateWysiwyg) {
            Write-Host -Object "The custom field '$WYSIWYGCustomFieldName' contains all '$ExpectedNumberOfEntries' expected entries. No update is necessary."
        }

        Write-Host -Object ""
    }

    # Check if feedback history should be kept and if there is new feedback to process
    if ($KeepFeedbackHistory -and (($AllFeedback | Select-Object -First $CommentsToKeep | Where-Object { $_.FeedbackCollected -eq $False }) -or $NeedToUpdateWyswiyg)) {
        # Initialize a list to store the feedback history in HTML format
        $FeedbackHistoryValue = New-Object System.Collections.Generic.List[String]

        # Begin the HTML structure
        $FeedbackHistoryValue.Add("<div>")

        # Convert feedback data into an HTML table
        $HTMLTable = $AllFeedback | Select-Object -First $CommentsToKeep | Select-Object @{ Name = 'Feedback Date'; Expression = {
                "$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())"
            }
        }, Username, Domain, Message | ConvertTo-Html -Fragment

        # Apply styling to the HTML table headers
        $HTMLTable = $HTMLTable -replace '<th>', "<th><b>" -replace '<\/th>', "</b></th>"
        $HTMLTable = $HTMLTable -replace '<th><b>Feedback Date', "<th style='width: 15em'><b>Feedback Date"
        $HTMLTable = $HTMLTable -replace '<th><b>Username', "<th style='width: 15em'><b>Username"
        $HTMLTable = $HTMLTable -replace '<th><b>Domain', "<th style='width: 15em'><b>Domain"

        # Wrap the table in a styled HTML card
        $HTMLCard = "<div class='card flex-grow-1'>
    <div class='card-title-box'>
        <div class='card-title'><i class='fa-solid fa-comment'></i>&nbsp;&nbsp;User Feedback History</div>
    </div>
    <div class='card-body' style='white-space: nowrap'>
        <div>
            <br>
            $HTMLTable
        </div>
    </div>
</div>"

        # Add the card to the feedback history
        $FeedbackHistoryValue.Add($HTMLCard)
        $FeedbackHistoryValue.Add("</div>")

        try {
            # Check if the character limit for the custom field is exceeded
            $Characters = ($FeedbackHistoryValue | Out-String).Length
            if ($Characters -ge 190000) {
                Write-Host -Object "The current character count is '$Characters'."
                Write-Host -Object "[Warning] The character limit of 200,000 has been reached! Trimming the output until the character limit is satisfied."

                $TrimStart = Get-Date
                do {
                    # Notify the user that data is being truncated
                    $FeedbackHistoryValue = New-Object System.Collections.Generic.List[String]
                    $FeedbackHistoryValue.Add("<div>")
                    $FeedbackHistoryValue.Add("<h1>This information has been truncated to fit within the character limit of 200,000.</h1>")

                    # Calculate the batch size for trimming
                    $ExceededAmount = $Characters - 190000
                    if ($ExceededAmount -le 0) {
                        $BatchSize = 1
                    } else {
                        $BatchSize = [math]::Ceiling($ExceededAmount / 500)
                    }

                    # Remove excess feedback entries
                    for ($i = 0 ; $i -lt $BatchSize ; $i++) {
                        $AllFeedback.RemoveAt($AllFeedback.Count - 1)
                    }

                    # Rebuild the HTML table with the remaining feedback
                    $HTMLTable = $AllFeedback | Select-Object -First $CommentsToKeep | Select-Object @{ Name = 'Feedback Date'; Expression = {
                            "$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())"
                        }
                    }, Username, Domain, Message | ConvertTo-Html -Fragment
                    $HTMLTable = $HTMLTable -replace '<th>', "<th><b>" -replace '<\/th>', "</b></th>"
                    $HTMLTable = $HTMLTable -replace '<th><b>Feedback Date', "<th style='width: 15em'><b>Feedback Date"
                    $HTMLTable = $HTMLTable -replace '<th><b>Username', "<th style='width: 15em'><b>Username"
                    $HTMLTable = $HTMLTable -replace '<th><b>Domain', "<th style='width: 15em'><b>Domain"

                    # Wrap the updated table in a styled HTML card
                    $HTMLCard = "<div class='card flex-grow-1'>
    <div class='card-title-box'>
        <div class='card-title'><i class='fa-solid fa-comment'></i>&nbsp;&nbsp;User Feedback History</div>
    </div>
    <div class='card-body' style='white-space: nowrap'>
        <div>
            <br>
            $HTMLTable
        </div>
    </div>
</div>"

                    # Add the updated card to the feedback history
                    $FeedbackHistoryValue.Add($HTMLCard)
                    $FeedbackHistoryValue.Add("</div>")

                    # Recalculate character count and continue trimming if necessary
                    $Characters = ($FeedbackHistoryValue | Out-String).Length
                    $ElapsedTime = (Get-Date) - $TrimStart
                    if ($ElapsedTime.TotalMinutes -ge 5) {
                        Write-Host -Object "The current character count is '$Characters'."
                        throw "5 minute timeout reached. Unable to trim the output to comply with the character limit."
                    }
                } while ($Characters -ge 190000)
            }

            # Save the feedback history to the custom field
            Write-Host -Object "Attempting to save the feedback to the custom field '$WYSIWYGCustomFieldName'."
            $FeedbackHistoryValue | Set-NinjaProperty -Name $WYSIWYGCustomFieldName -Type "WYSIWYG" -ErrorAction Stop
            $AllFeedback | Select-Object -First $CommentsToKeep | Where-Object { $FeedbackCollected -notcontains $_ } | ForEach-Object { $FeedbackCollected.Add($_) }
            Write-Host -Object "Successfully saved the feedback history to the custom field.`n"
        } catch {
            # Log an error if unable to save the feedback history
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to save the feedback history to the custom field '$WYSIWYGCustomFieldName'.`n"
            $ExitCode = 1
        }
    }

    # Exit if no feedback was collected
    if ($FeedbackCollected.Count -eq 0) {
        exit $ExitCode
    }

    # Update feedback logs to mark feedback as collected
    Write-Host -Object "Updating the feedback log(s) to indicate that the feedback has been collected."
    $LogsToUpdate = $FeedbackCollected | Group-Object -Property "LogLocation"
    $LogsToUpdate | ForEach-Object {
        try {
            # Retrieve the log file content
            $LogPath = $_.Name
            $LogContent = Get-Content -Path $LogPath -ErrorAction Stop
        } catch {
            # Log an error if unable to retrieve the log file
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to retrieve the log file at '$($_.Name)'."
            $ExitCode = 1
            return
        }

        try {
            # Create a map of updates for the log file
            $UpdateMap = @{}
            $_.Group | ForEach-Object {
                $Key = "$($_.TimeStamp.ToString('yyyy-MM-dd HH:mm:ssK'))|$($_.Username)|$($_.Domain)|$($_.FeedbackCollected)"
                $Message = $_.Base64
                $UpdateMap["$Key;$Message"] = "$($_.TimeStamp.ToString('yyyy-MM-dd HH:mm:ssK'))|$($_.Username)|$($_.Domain)|$True"
            }

            # Update the log file content with the collected feedback status
            $LogContent = $LogContent | ForEach-Object {
                $Line = $_
                foreach ($UpdateKey in $UpdateMap.Keys) {
                    $EscapedKey = [regex]::Escape(($UpdateKey -split ';', 2)[0])
                    $EscapedMsg = [regex]::Escape(($UpdateKey -split ';', 2)[1])
                    if ($Line -match $EscapedKey -and $Line -match $EscapedMsg) {
                        $Line = $Line -replace $EscapedKey, $UpdateMap[$UpdateKey]
                    }
                }

                $Line
            }

            # Save the updated log file content
            Set-Content -Path $LogPath -Value $LogContent -ErrorAction Stop
            Write-Host -Object "Updated the log file at '$($_.Name)'."
        } catch {
            # Log an error if unable to update the log file
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to update the log file at '$($_.Name)'."
            $ExitCode = 1
        }
    }

    # Clean up log files to retain only the specified number of comments
    Write-Host -Object "`nCleaning up the log files so only the previous '$CommentsToKeep' are kept."
    $FeedbackLogLocations | ForEach-Object {
        try {
            # Retrieve the log file content
            $LogPath = $_.LogFileLocation
            $LogContent = Get-Content -Path $LogPath -ErrorAction Stop
        } catch {
            # Log an error if unable to retrieve the log file
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to retrieve the log file at '$($_.LogFileLocation)'."
            $ExitCode = 1
            return
        }

        try {
            # Trim the log file content if it exceeds the specified number of comments
            if ($LogContent.Count -le $CommentsToKeep) {
                Write-Host -Object "The log file at '$LogPath' already has less than or equal to '$CommentsToKeep' comments."
            } else {
                Set-Content -Path $LogPath -Value ($LogContent | Select-Object -Last $CommentsToKeep) -ErrorAction Stop
                Write-Host -Object "Updated the log file at '$LogPath'."
            }
        } catch {
            # Log an error if unable to update the log file
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] Failed to update the log file at '$($_.LogFileLocation)'."
            $ExitCode = 1
        }
    }

    exit $ExitCode
}
end {
    
    
    
}

 

Description détaillée

Le script est complet et méthodiquement conçu pour être fiable et flexible. Voici comment :

1. Initialisation et validation

  • Valide la compatibilité de la version du système d’exploitation (Windows 10/Server 2016 et plus récent).
  • Vérifie les privilèges administratifs.
  • Valide et assainit les paramètres d’entrée tels que MaxCommentsToKeep, KeepFeedbackHistory et les noms de champs personnalisés.

2. Énumération des profils d’utilisateurs

  • Utilise la fonction Get-UserHive pour énumérer tous les profils d’utilisateurs (domaine, local, AzureAD).
  • Charge les ruches (hives) du registre lorsque cela est nécessaire pour extraire les chemins d’accès aux fichiers journaux appropriés.

3. Découverte du journal des avis

  • Recherche dans les chemins d’accès standard AppData\Local les fichiers journaux de retour d’information nommés UserDeviceFeedback.log.

4. Extraction et analyse des avis

  • Lit les fichiers journaux ligne par ligne.
  • Chaque ligne est divisée en points de données tels que l’horodatage, le nom d’utilisateur, le domaine et l’avis encodé en base64.
  • Le message est décodé et ajouté à une collection d’entrées de retour d’information.

5. Mise à jour des champs personnalisés

  • Les avis les plus récents non collectés, en fonction de l’utilisateur, sont compilés.
  • Enregistre dans le Dernier avis de l’utilisateur (champ multiligne) s’il respecte la limite de 10 000 caractères.
  • Si cette option est activée, un tableau HTML formaté des dernières entrées  est créé et enregistré dans l’historique des avis des utilisateurs (champ WYSIWYG), dans le respect d’une limite de 200 000 caractères.

6. Mises à jour du fichier journal

  • Marque les entrées d’avis traitées comme étant « collectées ».
  • Élimine les entrées les plus anciennes au-delà du maximum spécifié afin de maintenir la taille du journal.

7. Gestion des limites de caractères

  • Il élimine automatiquement les anciens avis si les limites du champ sont dépassées, ce qui garantit que les données ne sont pas tronquées au milieu de l’enregistrement.

Cette approche structurée permet de ne recueillir que des informations nouvelles et pertinentes et d’éviter les traitements redondants.

Cas d’utilisation potentiels

Scénario : Un fournisseur de services gérés (MSP) de taille moyenne gère 500 terminaux répartis entre plusieurs clients. Après avoir déployé une mise à jour des performances, ils souhaitent évaluer son impact sur l’expérience des utilisateurs.

  • Étape 1 : Déployer le script 1 sur tous les appareils pour inviter les utilisateurs à donner leur avis.
  • Étape 2 : Utiliser le script 2 pour consolider les réponses dans NinjaOne.
  • Étape 3 : Examiner les réponses de manière centralisée et faire remonter les appareil avec un avis est négatif.

Cela permet de créer une boucle de rétroaction fermée sans intervention humaine, ce qui simplifie le dépannage et améliore la satisfaction des clients.

Comparaisons

Il existe d’autres méthodes pour recueillir les avis des utilisateurs dans Windows avec un script PowerShell :

  • Écrire directement sur un fichier central partagé (risque de problèmes d’autorisation).
  • Utilisation des journaux d’événements (analyse complexe).
  • Envoi des journaux par e-mail (non sécurisé et non extensible).

En comparaison, l’intégration de NinjaOne se fait par l’intermédiaire de champs personnalisés :

  • Garantit un stockage des données sécurisé et lié à l’appareil.
  • Facilite l’accès à la console NinjaOne.
  • Permet l’automatisation et l’établissement de rapports via les flux de travail NinjaOne.

Questions fréquentes

Question 1 : Que se passe-t-il si aucun avis n’est trouvé ?

Le script met à jour le champ personnalisé avec la mention « Aucun avis n’a été donné » ( “No feedback has been given”)

Question 2 : Que se passe-t-il lorsque les avis dépassent la limite de caractères ?

Le script supprime automatiquement les entrées les plus anciennes et notifie le contenu du champ.

Question 3 : Peut-on l’utiliser sans le script 1 ?

Non, le script 2 dépend des journaux de retour d’information générés par le script 1.

Question 4 : Prend-il en charge les utilisateurs AzureAD ?

Oui, il détecte les profils d’utilisateurs AzureAD, locaux et de domaine.

Question 5 : Les balises HTML sont-elles nécessaires dans le champ WYSIWYG ?

Oui, le script génère automatiquement un tableau HTML complet avec une mise en forme pour une meilleure lisibilité.

Implications

L’automatisation de la collecte des avis a des implications considérables. En recueillant et en archivant continuellement les avis, les départements informatiques peuvent :

  • Détecter rapidement les régressions de performance.
  • Identifier les problèmes récurrents.
  • Justifier les investissements dans les infrastructures à l’aide de données réelles sur les sentiments des utilisateurs.

De plus, le stockage sécurisé de ces informations dans NinjaOne renforce la conformité des données et les normes de gouvernance.

Recommandations

  • Exécutez le script 2 selon un calendrier (par exemple, tous les jours) en utilisant les scripts planifiés de NinjaOne.
  • Pour une pleine fonctionnalité, il faut toujours l’associer au Script 1 .
  • Définissez des valeurs réalistes pour MaxCommentsToKeep afin d’éviter d’inonder les journaux.
  • Activez KeepFeedbackHistory si une analyse des tendances à long terme est nécessaire.
  • Surveillez la taille des champs pour éviter que les données ne soient tronquées en raison des limites de la plateforme.

Conclusion

Ce script illustre comment recueillir les avis des utilisateurs dans Windows avec un script PowerShell et l’intégrer dans un écosystème RMM tel que NinjaOne. Pour les professionnels de l’informatique et les MSP qui s’efforcent de fournir un support proactif et centré sur l’utilisateur, cette automatisation réduit les frais généraux, améliore la visibilité et permet de prendre des décisions fondées sur des données. En exploitant les capacités de script et les champs personnalisés de NinjaOne, la vérification des avis n’est plus un élément à cocher, mais un pilier de la qualité du service informatique.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Catégories :

Vous pourriez aussi aimer