{"id":530398,"date":"2025-09-22T02:21:55","date_gmt":"2025-09-22T02:21:55","guid":{"rendered":"https:\/\/www.ninjaone.com\/?post_type=script_hub&#038;p=530398"},"modified":"2025-09-22T02:21:55","modified_gmt":"2025-09-22T02:21:55","slug":"collecter-les-avis-des-utilisateurs-avec-powershell","status":"publish","type":"script_hub","link":"https:\/\/www.ninjaone.com\/fr\/script-hub\/collecter-les-avis-des-utilisateurs-avec-powershell\/","title":{"rendered":"Comment collecter les avis des utilisateurs avec PowerShell"},"content":{"rendered":"<p>L&rsquo;exp\u00e9rience utilisateur est un param\u00e8tre essentiel dans le monde informatique actuel, en particulier pour les administrateurs informatiques et les <a href=\"https:\/\/www.ninjaone.com\/fr\/quest-ce-quun-msp\/\">fournisseurs de services g\u00e9r\u00e9s (MSP)<\/a> qui g\u00e8rent les environnements des utilisateurs finaux. Avec l&rsquo;importance croissante accord\u00e9e au <a href=\"https:\/\/www.ninjaone.com\/fr\/it-hub\/it-service-management\/qu-est-ce-que-la-surveillance-de-l-experience-numerique-digital-experience-monitoring-dem\/\">contr\u00f4le de l&rsquo;exp\u00e9rience num\u00e9rique (DEM &#8211; Digital Experience Monitoring)<\/a> et \u00e0 l&rsquo;exp\u00e9rience des appareils (DEX &#8211; Device Experience), la capacit\u00e9 de collecter, de stocker et d&rsquo;analyser efficacement le retour d&rsquo;information des utilisateurs est devenue une priorit\u00e9. L&rsquo;automatisation de ce processus permet d&rsquo;obtenir des informations coh\u00e9rentes et de r\u00e9duire les t\u00e2ches manuelles. Cet article explique comment <strong>collecter les avis des utilisateurs avec PowerShell<\/strong> et les stocker dans les champs personnalis\u00e9s de NinjaOne, transformant ainsi les avis bruts en informations structur\u00e9es et exploitables.<\/p>\n<h2>Contexte<\/h2>\n<p>Alors que les entreprises s&rsquo;orientent vers une assistance informatique proactive, l&rsquo;enqu\u00eate DEX est devenue un outil pr\u00e9cieux pour mesurer la fa\u00e7on dont les utilisateurs per\u00e7oivent les performances, la r\u00e9activit\u00e9 et la facilit\u00e9 d&rsquo;utilisation des appareils. Le script dont il est question ici est le deuxi\u00e8me d&rsquo;un flux de travail en trois parties. Le script 1 invite les utilisateurs \u00e0 donner leur avis et enregistre leurs r\u00e9ponses localement. Le script 2, qui fait l&rsquo;objet de cet article, lit ces fichiers journaux, traite les derniers avis et les enregistre dans les champs personnalis\u00e9s de NinjaOne : pour les avis r\u00e9cents et pour l&rsquo;historique des avis (facultatif). Pour les professionnels de l&rsquo;informatique qui utilisent NinjaOne, cette int\u00e9gration transparente garantit que les donn\u00e9es sur les sentiments des utilisateurs sont facilement accessibles dans la plateforme <a href=\"https:\/\/www.ninjaone.com\/fr\/blog\/definition-de-la-surveillance-et-gestion-a-distance\/\">RMM<\/a> pour l&rsquo;\u00e9tablissement de rapports ou l&rsquo;application de mesures correctives.<\/p>\n<h2>Le script<\/h2>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"powershell\">#Requires -Version 5.1\r\n\r\n&lt;#\r\n.SYNOPSIS\r\n    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'.\r\n.DESCRIPTION\r\n    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'.\r\nBy 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.\r\n    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. \r\n    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. \r\n    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. \r\n    Warranty Disclaimer: The script is provided \u201cas is\u201d and \u201cas available\u201d, 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. \r\n    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. \r\n    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. \r\n    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).\r\n.EXAMPLE\r\n    -MaxCommentsToKeep \"30\" -KeepFeedbackHistory\r\n\r\n    Gathering all user profiles.\r\n    Gathering each user's feedback log location.\r\n\r\n    Checking 'C:\\Users\\tuser1\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' for user feedback.\r\n    New feedback was found.\r\n\r\n    Checking 'C:\\Users\\tuser2\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' for user feedback.\r\n    New feedback was found.\r\n\r\n    Checking 'C:\\Users\\kpradlander\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' for user feedback.\r\n    New feedback was found.\r\n\r\n    Compiling the most recent feedback.\r\n    All the most recent feedback has already been collected.\r\n    Attempting to save the feedback to the custom field 'userFeedbackHistory'.\r\n    Successfully saved the feedback history to the custom field.\r\n\r\n    Updating the feedback log(s) to indicate that the feedback has been collected.\r\n    Updated the log file at 'C:\\Users\\tuser2\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log'.\r\n    Updated the log file at 'C:\\Users\\kpradlander\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log'.\r\n    Updated the log file at 'C:\\Users\\tuser1\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log'.\r\n\r\n    Cleaning up the log files so only the previous '30' are kept.\r\n    The log file at 'C:\\Users\\tuser1\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' already has less than or equal to '30' comments.\r\n    The log file at 'C:\\Users\\tuser2\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' already has less than or equal to '30' comments.\r\n    The log file at 'C:\\Users\\kpradlander\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' already has less than or equal to '30' comments.\r\n\r\n.PARAMETER KeepFeedbackHistory\r\n    This stores the user feedback history in a custom field titled 'User Feedback History'.\r\n\r\n.PARAMETER MaxCommentsToKeep\r\n    This controls the number of feedback comments to retain in the log file(s).\r\n\r\n.PARAMETER MultilineCustomFieldName\r\n    The name of a multiline custom field to store the latest user feedback.\r\n\r\n.PARAMETER WYSIWYGCustomFieldName\r\n    The name of a WYSIWYG custom field to store the total user feedback history.\r\n\r\n.NOTES\r\n    Minimum OS Architecture Supported: Windows 10, Windows Server 2016\r\n    Release Notes: Initial Release\r\n#&gt;\r\n\r\n[CmdletBinding()]\r\nparam (\r\n    [Parameter()]\r\n    [Switch]$KeepFeedbackHistory = [System.Convert]::ToBoolean($env:KeepFeedbackHistory),\r\n    [Parameter()]\r\n    [String]$MaxCommentsToKeep = \"30\",\r\n    [Parameter()]\r\n    [String]$MultilineCustomFieldName = \"latestUserFeedback\",\r\n    [Parameter()]\r\n    [String]$WYSIWYGCustomFieldName = \"userFeedbackHistory\"\r\n)\r\n\r\nbegin {\r\n    # If the script form variables are used, replace the command-line parameters with their values.\r\n    if ($env:maxCommentsToKeep) { $MaxCommentsToKeep = $env:maxCommentsToKeep }\r\n\r\n    # Check if the operating system build version is less than 10240 (Windows 10 or Windows Server 2016 minimum requirement)\r\n    if ([System.Environment]::OSVersion.Version.Build -lt 10240) {\r\n        Write-Host -Object \"`n[Warning] OS build '$([System.Environment]::OSVersion.Version.Build)' detected.\"\r\n        Write-Host -Object \"[Warning] The minimum OS version supported by this script is Windows 10 (10240) or Windows Server 2016 (14393).`n\"\r\n    }\r\n\r\n    # Validate that the maximum number of comments to keep is not null or empty\r\n    if ([String]::IsNullOrWhiteSpace($MaxCommentsToKeep)) {\r\n        Write-Host -Object \"[Error] You must specify a maximum number of comments to keep.\"\r\n        exit 1\r\n    }\r\n\r\n    # Ensure the value for 'MaxCommentsToKeep' contains only numeric characters\r\n    if ($MaxCommentsToKeep -match \"[^0-9]\") {\r\n        Write-Host -Object \"[Error] The maximum number of comments you specified '$MaxCommentsToKeep' is invalid. It contains invalid characters.\"\r\n        Write-Host -Object \"[Error] Please specify a positive whole number that is greater than 0 and less than $([int]::MaxValue).\"\r\n        exit 1\r\n    }\r\n\r\n    # Attempt to convert the 'MaxCommentsToKeep' value to a long integer\r\n    try {\r\n        [long]$CommentsToKeep = $MaxCommentsToKeep\r\n    } catch {\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        Write-Host -Object \"[Error] Unable to convert '$MaxCommentsToKeep' into an integer.\"\r\n        exit 1\r\n    }\r\n\r\n    # Validate that the converted value is within the acceptable range\r\n    if ($CommentsToKeep -le 0 -or $CommentsToKeep -ge [int]::MaxValue) {\r\n        Write-Host -Object \"[Error] The maximum number of comments you specified '$CommentsToKeep' is invalid.\"\r\n        Write-Host -Object \"[Error] Please specify a positive whole number that is greater than 0 and less than $([int]::MaxValue).\"\r\n        exit 1\r\n    }\r\n\r\n    function ConvertFrom-UrlSafeBase64 {\r\n        [CmdletBinding()]\r\n        param (\r\n            [Parameter(Mandatory)]\r\n            [String]$UrlSafeBase64\r\n        )\r\n\r\n        # Normalize base64 (replace URL-safe chars)\r\n        $Base64 = $UrlSafeBase64.Replace('-', '+').Replace('_', '\/')\r\n\r\n        # Add padding if needed\r\n        switch ($Base64.Length % 4) {\r\n            2 { $Base64 = \"$Base64==\" }\r\n            3 { $Base64 = \"$Base64=\" }\r\n            1 { throw \"Invalid Base64 string\" }\r\n        }\r\n\r\n        # Convert from base64 to text\r\n        $InputInBytes = [System.Convert]::FromBase64String($Base64)\r\n        [System.Text.Encoding]::UTF8.GetString($InputInBytes)\r\n    }\r\n\r\n    function Get-UserHive {\r\n        [CmdletBinding()]\r\n        param (\r\n            [Parameter()]\r\n            [ValidateSet('AzureAD', 'DomainAndLocal', 'All')]\r\n            [String]$Type = \"All\",\r\n            [Parameter()]\r\n            [String[]]$ExcludedUsers,\r\n            [Parameter()]\r\n            [switch]$IncludeDefault\r\n        )\r\n\r\n        # Define the SID patterns to match based on the selected user type\r\n        $Patterns = switch ($Type) {\r\n            \"AzureAD\" { \"S-1-12-1-(\\d+-?){4}$\" }\r\n            \"DomainAndLocal\" { \"S-1-5-21-(\\d+-?){4}$\" }\r\n            \"All\" { \"S-1-12-1-(\\d+-?){4}$\" ; \"S-1-5-21-(\\d+-?){4}$\" }\r\n        }\r\n\r\n        # Retrieve user profile information based on the defined patterns\r\n        $UserProfiles = Foreach ($Pattern in $Patterns) {\r\n            Get-ItemProperty \"Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\*\" |\r\n                Where-Object { $_.PSChildName -match $Pattern } |\r\n                Select-Object @{Name = \"SID\"; Expression = { $_.PSChildName } },\r\n                @{Name = \"Username\"; Expression = { \"$($_.ProfileImagePath | Split-Path -Leaf)\" } },\r\n                @{Name = \"Domain\"; Expression = { if ($_.PSChildName -match \"S-1-12-1-(\\d+-?){4}$\") { \"AzureAD\" }else { $Null } } },\r\n                @{Name = \"UserHive\"; Expression = { \"$($_.ProfileImagePath)\\NTuser.dat\" } },\r\n                @{Name = \"Path\"; Expression = { $_.ProfileImagePath } }\r\n        }\r\n\r\n        # If the IncludeDefault switch is set, add the Default profile to the results\r\n        switch ($IncludeDefault) {\r\n            $True {\r\n                $DefaultProfile = \"\" | Select-Object Username, SID, UserHive, Path\r\n                $DefaultProfile.Username = \"Default\"\r\n                $DefaultProfile.Domain = $env:COMPUTERNAME\r\n                $DefaultProfile.SID = \"DefaultProfile\"\r\n                $DefaultProfile.Userhive = \"$env:SystemDrive\\Users\\Default\\NTUSER.DAT\"\r\n                $DefaultProfile.Path = \"C:\\Users\\Default\"\r\n\r\n                # Exclude users specified in the ExcludedUsers list\r\n                $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username }\r\n            }\r\n        }\r\n\r\n        if ($PSVersionTable.PSVersion.Major -lt 3) {\r\n            $AllAccounts = Get-WmiObject -Class \"win32_UserAccount\"\r\n        } else {\r\n            $AllAccounts = Get-CimInstance -ClassName \"win32_UserAccount\"\r\n        }\r\n\r\n        $CompleteUserProfiles = $UserProfiles | ForEach-Object {\r\n            $SID = $_.SID\r\n            $Win32Object = $AllAccounts | Where-Object { $_.SID -like $SID }\r\n\r\n            if ($Win32Object) {\r\n                $Win32Object | Add-Member -NotePropertyName UserHive -NotePropertyValue $_.UserHive\r\n                $Win32Object | Add-Member -NotePropertyName Path -NotePropertyValue $_.Path\r\n                $Win32Object\r\n            } else {\r\n                [PSCustomObject]@{\r\n                    Name     = $_.Username\r\n                    Domain   = $_.Domain\r\n                    SID      = $_.SID\r\n                    UserHive = $_.UserHive\r\n                    Path     = $_.Path\r\n                }\r\n            }\r\n        }\r\n\r\n        # Return the list of user profiles, excluding any specified in the ExcludedUsers list\r\n        $CompleteUserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Name }\r\n    }\r\n\r\n    function Test-IsElevated {\r\n        [CmdletBinding()]\r\n        param ()\r\n\r\n        # Get the current Windows identity of the user running the script\r\n        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()\r\n\r\n        # Create a WindowsPrincipal object based on the current identity\r\n        $p = New-Object System.Security.Principal.WindowsPrincipal($id)\r\n\r\n        # Check if the current user is in the Administrator role\r\n        # The function returns $True if the user has administrative privileges, $False otherwise\r\n        # 544 is the value for the Built In Administrators role\r\n        # Reference: https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.security.principal.windowsbuiltinrole\r\n        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544')\r\n    }\r\n\r\n    if (!$ExitCode) {\r\n        $ExitCode = 0\r\n    }\r\n}\r\nprocess {\r\n    # Attempt to determine if the current session is running with Administrator privileges.\r\n    try {\r\n        $IsElevated = Test-IsElevated -ErrorAction Stop\r\n    } catch {\r\n        # Log an error if unable to determine elevation status\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        Write-Host -Object \"[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges.\"\r\n        exit 1\r\n    }\r\n\r\n    # Exit if the script is not running with Administrator privileges\r\n    if (!$IsElevated) {\r\n        Write-Host -Object \"[Error] Access Denied: The user '$env:Username' does not have administrator privileges, or the script is not running with elevated permissions.\"\r\n        exit 1\r\n    }\r\n\r\n    try {\r\n        # Retrieve all user profiles on the system\r\n        Write-Host -Object \"Gathering all user profiles.\"\r\n        $UserProfiles = Get-UserHive -Type \"All\" -ErrorAction Stop\r\n        $ProfileWasLoaded = New-Object System.Collections.Generic.List[object]\r\n    } catch {\r\n        # Log an error if unable to gather user profiles\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        Write-Host -Object \"[Error] Failed to gather all the user profiles.\"\r\n        exit 1\r\n    }\r\n\r\n    # Loop through each user profile to load their registry hive if not already loaded\r\n    ForEach ($UserProfile in $UserProfiles) {\r\n        If (!(Test-Path -Path Registry::HKEY_USERS\\$($UserProfile.SID) -ErrorAction SilentlyContinue)) {\r\n            Start-Process -FilePath \"cmd.exe\" -ArgumentList \"\/C reg.exe LOAD HKU\\$($UserProfile.SID) `\"$($UserProfile.UserHive)`\"\" -Wait -WindowStyle Hidden\r\n            $ProfileWasLoaded.Add($UserProfile)\r\n        }\r\n    }\r\n\r\n    # Locate feedback log files for each user profile\r\n    Write-Host -Object \"Gathering each user's feedback log location.\"\r\n    $FeedbackLogLocations = New-Object System.Collections.Generic.List[object]\r\n    $UserProfiles | ForEach-Object {\r\n        # Check for feedback log file in various locations\r\n        $AppDataLocation = Get-ItemProperty -Path \"Registry::HKEY_USERS\\$($_.SID)\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty \"Local AppData\" -ErrorAction SilentlyContinue\r\n\r\n        if ($AppDataLocation -and (Test-Path -Path \"$AppDataLocation\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\" -Type Leaf -ErrorAction SilentlyContinue)) {\r\n            $FeedbackLogLocations.Add(\r\n                [PSCustomObject]@{\r\n                    Name            = $_.Name\r\n                    Domain          = $_.Domain\r\n                    LogFileLocation = \"$AppDataLocation\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\"\r\n                }\r\n            )\r\n            return\r\n        }\r\n\r\n        if (Test-Path -Path \"$($_.Path)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\" -Type Leaf -ErrorAction SilentlyContinue) {\r\n            $FeedbackLogLocations.Add(\r\n                [PSCustomObject]@{\r\n                    Name            = $_.Name\r\n                    Domain          = $_.Domain\r\n                    LogFileLocation = \"$($_.Path)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\"\r\n                }\r\n            )\r\n            return\r\n        }\r\n\r\n        if (Test-Path -Path \"C:\\Users\\$($_.Name)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\" -Type Leaf -ErrorAction SilentlyContinue) {\r\n            $FeedbackLogLocations.Add(\r\n                [PSCustomObject]@{\r\n                    Name            = $_.Name\r\n                    Domain          = $_.Domain\r\n                    LogFileLocation = \"C:\\Users\\$($_.Name)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\"\r\n                }\r\n            )\r\n            return\r\n        }\r\n\r\n        # Log a message if no feedback log file is found for the user\r\n        Write-Host -Object \"No feedback log file found for $($_.Name) in the domain $($_.Domain).\"\r\n    }\r\n\r\n    # Unload user registry hives if they were loaded during this script execution\r\n    if ($ProfileWasLoaded.Count -gt 0) {\r\n        ForEach ($UserProfile in $ProfileWasLoaded) {\r\n            [gc]::Collect()\r\n            Start-Sleep 1\r\n            Start-Process -FilePath \"cmd.exe\" -ArgumentList \"\/C reg.exe UNLOAD HKU\\$($UserProfile.SID)\" -Wait -WindowStyle Hidden | Out-Null\r\n        }\r\n    }\r\n\r\n    # Log a warning if no feedback log files were found\r\n    if ($FeedbackLogLocations.Count -lt 1) {\r\n        Write-Host -Object \"[Warning] No feedback was found.\"\r\n    }\r\n\r\n    Write-Host -Object \"\"\r\n\r\n    # Initialize a list to store all feedback entries\r\n    $AllFeedback = New-Object System.Collections.Generic.List[object]\r\n    $FeedbackLogLocations | ForEach-Object {\r\n        $LogFileLocation = $_.LogFileLocation\r\n\r\n        # Check the feedback log file for user feedback\r\n        Write-Host -Object \"Checking '$LogFileLocation' for user feedback.\"\r\n\r\n        try {\r\n            # Read the first line of the log file to ensure it exists\r\n            Get-Content -Path $LogFileLocation -TotalCount 1 -ErrorAction Stop | Out-Null\r\n        } catch {\r\n            # Log an error if unable to read the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the feedback log file contents for the user '$($_.Name)'.\"\r\n            $ExitCode = 1\r\n            return\r\n        }\r\n\r\n        # Parse the feedback log file contents\r\n        $i = 0\r\n        Get-Content -Path $LogFileLocation -Tail 200000 | ForEach-Object {\r\n            $i++\r\n            $DataPoints = $_ -split '\\|', 6\r\n\r\n            # Warn if a line has fewer than 6 data points\r\n            if ($DataPoints.Count -lt 6) {\r\n                Write-Host -Object \"[Warning] Only $($DataPoints.Count) data points have been found for line '$i'.\"\r\n                return\r\n            }\r\n\r\n            # Parse the timestamp from the log entry\r\n            try {\r\n                $CurrentTimeStamp = [datetime]::ParseExact($DataPoints[0], 'yyyy-MM-dd HH:mm:ssK', $null)\r\n            } catch {\r\n                Write-Host -Object \"[Warning] $($_.Exception.Message)\"\r\n                Write-Host -Object \"[Warning] Failed to parse the date '$($DataPoints[0])' from the feedback log.\"\r\n            }\r\n\r\n            # Decode the feedback message from Base64\r\n            try {\r\n                $UserMessage = ConvertFrom-UrlSafeBase64 -UrlSafeBase64 $DataPoints[5] -ErrorAction Stop\r\n            } catch {\r\n                Write-Host -Object \"[Warning] $($_.Exception.Message)\"\r\n                Write-Host -Object \"[Warning] Failed to parse the following feedback message from the feedback log: $($DataPoints[5])\"\r\n            }\r\n\r\n            # Add the parsed feedback entry to the list\r\n            $AllFeedback.Add(\r\n                [PSCustomObject]@{\r\n                    TimeStamp               = $CurrentTimeStamp\r\n                    Username                = $DataPoints[1]\r\n                    Domain                  = $DataPoints[2]\r\n                    FeedbackCollected       = $DataPoints[3]\r\n                    SystemPerformanceHasRun = $DataPoints[4]\r\n                    Message                 = $UserMessage\r\n                    Base64                  = $DataPoints[5]\r\n                    LogLocation             = $LogFileLocation\r\n                }\r\n            )\r\n        }\r\n\r\n        # Filter feedback entries for the current log file\r\n        $UserFeedback = $AllFeedback | Where-Object { $_.LogLocation -eq $LogFileLocation }\r\n\r\n        # Log a warning if no feedback entries are found in the log file\r\n        if (!$UserFeedback) {\r\n            Write-Host -Object \"[Warning] The log file does not contain any feedback.\"\r\n            Write-Host -Object \"\"\r\n            return\r\n        }\r\n\r\n        # Check if there is new feedback in the log file\r\n        if (!($UserFeedback | Where-Object { $_.FeedbackCollected -eq $False })) {\r\n            Write-Host -Object \"There is no new feedback in this log file.\"\r\n        } else {\r\n            Write-Host -Object \"New feedback was found.\"\r\n        }\r\n\r\n        Write-Host -Object \"\"\r\n    }\r\n\r\n    # Check if there is no feedback collected\r\n    if ($AllFeedback.Count -eq 0) {\r\n        try {\r\n            # Retrieve the current value of the custom field for the latest feedback\r\n            Write-Host -Object \"Checking the current value of the custom field '$MultilineCustomFieldName'.\"\r\n            $CurrentCustomFieldValue = Get-NinjaProperty -Name $MultilineCustomFieldName -ErrorAction Stop\r\n        } catch {\r\n            # Log an error if unable to retrieve the custom field value\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the value from the custom field $MultilineCustomFieldName.\"\r\n            $ExitCode = 1\r\n        }\r\n\r\n        # Update the custom field if it is not already set to \"No feedback has been given.\"\r\n        if ($CurrentCustomFieldValue -ne \"No feedback has been given.\") {\r\n            try {\r\n                Write-Host -Object \"Attempting to set the custom field '$MultilineCustomFieldName'.\"\r\n                Set-NinjaProperty -Name $MultilineCustomFieldName -Value \"No feedback has been given.\" -ErrorAction Stop\r\n                Write-Host -Object \"Successfully saved the feedback to the custom field.\"\r\n            } catch {\r\n                # Log an error if unable to update the custom field\r\n                Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n                Write-Host -Object \"[Error] Failed to save to the custom field $MultilineCustomFieldName.\"\r\n                $ExitCode = 1\r\n            }\r\n        } else {\r\n            Write-Host -Object \"The custom field is already set to 'No feedback has been given.'.\"\r\n        }\r\n\r\n        # Handle feedback history if the KeepFeedbackHistory switch is enabled\r\n        if ($KeepFeedbackHistory) {\r\n            try {\r\n                # Retrieve the current value of the custom field for feedback history\r\n                Write-Host -Object \"`nChecking the current value of the custom field '$WYSIWYGCustomFieldName'.\"\r\n                $CurrentCustomFieldValue = Get-NinjaProperty -Name $WYSIWYGCustomFieldName -Type \"WYSIWYG\" -ErrorAction Stop\r\n            } catch {\r\n                # Log an error if unable to retrieve the custom field value\r\n                Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n                Write-Host -Object \"[Error] Failed to retrieve the value from the custom field $WYSIWYGCustomFieldName.\"\r\n                $ExitCode = 1\r\n            }\r\n\r\n            # Trim the current value if it is not null or empty\r\n            if (![string]::IsNullOrWhiteSpace($CurrentCustomFieldValue.Text)) {\r\n                $CurrentCustomFieldValue = $CurrentCustomFieldValue.Text.Trim()\r\n            }\r\n\r\n            # Update the custom field if it is not already set to \"No feedback has been given.\"\r\n            if ($CurrentCustomFieldValue -ne \"No feedback has been given.\") {\r\n                try {\r\n                    Write-Host -Object \"Attempting to set the custom field '$WYSIWYGCustomFieldName'.\"\r\n                    Set-NinjaProperty -Name $WYSIWYGCustomFieldName -Value \"No feedback has been given.\" -ErrorAction Stop\r\n                    Write-Host -Object \"Successfully saved the feedback history to the custom field.\"\r\n                } catch {\r\n                    # Log an error if unable to update the custom field\r\n                    Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n                    Write-Host -Object \"[Error] Failed to save to the custom field $WYSIWYGCustomFieldName.\"\r\n                    $ExitCode = 1\r\n                }\r\n            } else {\r\n                Write-Host -Object \"The custom field is already set to 'No feedback has been given.'.\"\r\n            }\r\n        }\r\n\r\n        # Exit the script with the current exit code\r\n        exit $ExitCode\r\n    }\r\n\r\n    # Compile the most recent feedback\r\n    Write-Host -Object \"Compiling the most recent feedback.\"\r\n    if ($AllFeedback.Count -gt 1) {\r\n        # Sort feedback by timestamp in descending order\r\n        $AllFeedback = [System.Collections.Generic.List[object]]::new(($AllFeedback | Sort-Object -Property \"Timestamp\" -Descending))\r\n    }\r\n\r\n    # Initialize lists for most recent feedback and collected feedback\r\n    $MostRecentFeedback = New-Object System.Collections.Generic.List[object]\r\n    $FeedbackCollected = New-Object System.Collections.Generic.List[object]\r\n\r\n    # Group feedback by log location and retrieve the most recent entry for each user\r\n    $FeedbackPerUser = $AllFeedback | Group-Object -Property \"LogLocation\"\r\n    $FeedbackPerUser | ForEach-Object {\r\n        $MostRecentFeedback.Add(\r\n            (\r\n                $_.Group | Sort-Object -Property TimeStamp | Select-Object -Last 1\r\n            )\r\n        )\r\n    }\r\n\r\n    # Check if all the most recent feedback has already been collected\r\n    if (!($MostRecentFeedback | Where-Object { $_.FeedbackCollected -eq $False })) {\r\n        Write-Host -Object \"All the most recent feedback has already been collected.`n\"\r\n    }\r\n\r\n    # Process new feedback if available\r\n    if ($MostRecentFeedback | Where-Object { $_.FeedbackCollected -eq $False }) {\r\n        $LatestUserFeedbackValue = New-Object System.Collections.Generic.List[String]\r\n        if ($MostRecentFeedback.Count -gt 1) {\r\n            # Sort most recent feedback by timestamp in descending order\r\n            $MostRecentFeedback = [System.Collections.Generic.List[object]]::new(($MostRecentFeedback | Sort-Object -Property \"TimeStamp\" -Descending))\r\n        }\r\n\r\n        # Format the feedback entries for display\r\n        $MostRecentFeedback | ForEach-Object {\r\n            $DateString = \"$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())\"\r\n            $LatestUserFeedbackValue.Add(\"$DateString | $($_.Username) | $($_.Domain)\")\r\n            $LatestUserFeedbackValue.Add([String]$_.Message)\r\n            $LatestUserFeedbackValue.Add(\"\")\r\n        }\r\n\r\n        try {\r\n            # Check if the character limit for the custom field is exceeded\r\n            $Characters = ($LatestUserFeedbackValue | Out-String) | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters\r\n            if ($Characters -ge 9500) {\r\n                Write-Host -Object \"[Warning] The character limit of 10,000 has been reached! Trimming the output until the character limit is satisfied.\"\r\n\r\n                $TrimStart = Get-Date\r\n                do {\r\n                    # Notify the user that data is being truncated\r\n                    $LatestUserFeedbackValue = New-Object System.Collections.Generic.List[String]\r\n                    $LatestUserFeedbackValue.Add(\"This info has been truncated to accommodate the 10,000 character limit.\")\r\n                    $LatestUserFeedbackValue.Add(\"\")\r\n\r\n                    # Calculate the batch size for trimming\r\n                    $ExceededAmount = $Characters - 9500\r\n                    if ($ExceededAmount -le 0) {\r\n                        $BatchSize = 1\r\n                    } else {\r\n                        $BatchSize = [math]::Ceiling($ExceededAmount \/ 500)\r\n                    }\r\n\r\n                    # Remove excess feedback entries\r\n                    for ($i = 0 ; $i -lt $BatchSize ; $i++) {\r\n                        $MostRecentFeedback.RemoveAt($MostRecentFeedback.Count - 1)\r\n                    }\r\n\r\n                    # Reformat the remaining feedback entries\r\n                    $MostRecentFeedback | ForEach-Object {\r\n                        $DateString = \"$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())\"\r\n                        $LatestUserFeedbackValue.Add(\"$DateString | $($_.Username) | $($_.Domain)\")\r\n                        $LatestUserFeedbackValue.Add([String]$_.Message)\r\n                        $LatestUserFeedbackValue.Add(\"\")\r\n                    }\r\n\r\n                    # Recalculate character count and continue trimming if necessary\r\n                    $Characters = ($LatestUserFeedbackValue | Out-String) | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters\r\n                    $ElapsedTime = (Get-Date) - $TrimStart\r\n                    if ($ElapsedTime.TotalMinutes -ge 5) {\r\n                        Write-Host -Object \"The current character count is '$Characters'.\"\r\n                        throw \"5 minute timeout reached. Unable to trim the output to comply with the character limit.\"\r\n                    }\r\n                } while ($Characters -ge 9500)\r\n            }\r\n\r\n            # Save the most recent feedback to the custom field\r\n            Write-Host -Object \"Attempting to set the custom field '$MultilineCustomFieldName'.\"\r\n            $LatestUserFeedbackValue | Set-NinjaProperty -Name $MultilineCustomFieldName -ErrorAction Stop\r\n            $MostRecentFeedback | ForEach-Object { $FeedbackCollected.Add($_) }\r\n            Write-Host -Object \"Successfully saved the feedback to the custom field.`n\"\r\n        } catch {\r\n            # Log an error if unable to save the feedback\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to save the most recent feedback to the custom field '$MultilineCustomFieldName'.\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    # Check if feedback history should be kept and if all feedback entries have already been collected\r\n    if ($KeepFeedbackHistory -and !($AllFeedback | Select-Object -First $CommentsToKeep | Where-Object { $_.FeedbackCollected -eq $False })) {\r\n        Write-Host -Object \"The past '$CommentsToKeep' feedback entries have already been collected.`nAttempting to verify if any update is necessary.\"\r\n\r\n        try {\r\n            # Retrieve the current value of the custom field for feedback history\r\n            Write-Host -Object \"Checking the current value of the custom field '$WYSIWYGCustomFieldName'.\"\r\n            $WysiwygValue = Get-NinjaProperty -Name $WYSIWYGCustomFieldName -ErrorAction Stop\r\n\r\n            # Extract the HTML content from the custom field value if it exists\r\n            if ($WysiwygValue) {\r\n                $WysiwygHTML = $WysiwygValue | ConvertFrom-Json -ErrorAction Stop | Select-Object -ExpandProperty HTML -ErrorAction SilentlyContinue\r\n            }\r\n        } catch {\r\n            # Log an error if unable to retrieve the custom field value\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the current value of the custom field '$WYSIWYGCustomFieldName'.\"\r\n            $ExitCode = 1\r\n        }\r\n\r\n        # Calculate the expected number of feedback entries based on the specified limit\r\n        $ExpectedNumberOfEntries = $AllFeedback | Select-Object -First $CommentsToKeep | Measure-Object -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Count -ErrorAction SilentlyContinue\r\n\r\n        # Check if the current HTML content matches the expected number of entries\r\n        if ($WysiwygHTML) {\r\n            # Count the number of entries in the current HTML content\r\n            $WysiwygEntries = $WysiwygHTML -split \"&lt;tr&gt;\" | Where-Object { $_ -match \"&lt;\/td&gt;\" } | Measure-Object -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Count -ErrorAction SilentlyContinue\r\n\r\n            # Determine if an update is necessary based on the mismatch in entry counts\r\n            if ($WysiwygEntries -ne $ExpectedNumberOfEntries) {\r\n                Write-Host -Object \"There are currently '$WysiwygEntries' entries when '$ExpectedNumberOfEntries' entries were expected. An update is necessary.\"\r\n                $NeedToUpdateWysiwyg = $True\r\n            }\r\n        } else {\r\n            # Log a message if no entries exist in the current HTML content\r\n            Write-Host -Object \"There are currently no entries when '$ExpectedNumberOfEntries' entries were expected. An update is necessary.\"\r\n            $NeedToUpdateWysiwyg = $True\r\n        }\r\n\r\n        # Log a message if no update is necessary\r\n        if (!$NeedToUpdateWysiwyg) {\r\n            Write-Host -Object \"The custom field '$WYSIWYGCustomFieldName' contains all '$ExpectedNumberOfEntries' expected entries. No update is necessary.\"\r\n        }\r\n\r\n        Write-Host -Object \"\"\r\n    }\r\n\r\n    # Check if feedback history should be kept and if there is new feedback to process\r\n    if ($KeepFeedbackHistory -and (($AllFeedback | Select-Object -First $CommentsToKeep | Where-Object { $_.FeedbackCollected -eq $False }) -or $NeedToUpdateWyswiyg)) {\r\n        # Initialize a list to store the feedback history in HTML format\r\n        $FeedbackHistoryValue = New-Object System.Collections.Generic.List[String]\r\n\r\n        # Begin the HTML structure\r\n        $FeedbackHistoryValue.Add(\"&lt;div&gt;\")\r\n\r\n        # Convert feedback data into an HTML table\r\n        $HTMLTable = $AllFeedback | Select-Object -First $CommentsToKeep | Select-Object @{ Name = 'Feedback Date'; Expression = {\r\n                \"$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())\"\r\n            }\r\n        }, Username, Domain, Message | ConvertTo-Html -Fragment\r\n\r\n        # Apply styling to the HTML table headers\r\n        $HTMLTable = $HTMLTable -replace '&lt;th&gt;', \"&lt;th&gt;&lt;b&gt;\" -replace '&lt;\\\/th&gt;', \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n        $HTMLTable = $HTMLTable -replace '&lt;th&gt;&lt;b&gt;Feedback Date', \"&lt;th style='width: 15em'&gt;&lt;b&gt;Feedback Date\"\r\n        $HTMLTable = $HTMLTable -replace '&lt;th&gt;&lt;b&gt;Username', \"&lt;th style='width: 15em'&gt;&lt;b&gt;Username\"\r\n        $HTMLTable = $HTMLTable -replace '&lt;th&gt;&lt;b&gt;Domain', \"&lt;th style='width: 15em'&gt;&lt;b&gt;Domain\"\r\n\r\n        # Wrap the table in a styled HTML card\r\n        $HTMLCard = \"&lt;div class='card flex-grow-1'&gt;\r\n    &lt;div class='card-title-box'&gt;\r\n        &lt;div class='card-title'&gt;&lt;i class='fa-solid fa-comment'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;User Feedback History&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div class='card-body' style='white-space: nowrap'&gt;\r\n        &lt;div&gt;\r\n            &lt;br&gt;\r\n            $HTMLTable\r\n        &lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n&lt;\/div&gt;\"\r\n\r\n        # Add the card to the feedback history\r\n        $FeedbackHistoryValue.Add($HTMLCard)\r\n        $FeedbackHistoryValue.Add(\"&lt;\/div&gt;\")\r\n\r\n        try {\r\n            # Check if the character limit for the custom field is exceeded\r\n            $Characters = ($FeedbackHistoryValue | Out-String).Length\r\n            if ($Characters -ge 190000) {\r\n                Write-Host -Object \"The current character count is '$Characters'.\"\r\n                Write-Host -Object \"[Warning] The character limit of 200,000 has been reached! Trimming the output until the character limit is satisfied.\"\r\n\r\n                $TrimStart = Get-Date\r\n                do {\r\n                    # Notify the user that data is being truncated\r\n                    $FeedbackHistoryValue = New-Object System.Collections.Generic.List[String]\r\n                    $FeedbackHistoryValue.Add(\"&lt;div&gt;\")\r\n                    $FeedbackHistoryValue.Add(\"&lt;h1&gt;This information has been truncated to fit within the character limit of 200,000.&lt;\/h1&gt;\")\r\n\r\n                    # Calculate the batch size for trimming\r\n                    $ExceededAmount = $Characters - 190000\r\n                    if ($ExceededAmount -le 0) {\r\n                        $BatchSize = 1\r\n                    } else {\r\n                        $BatchSize = [math]::Ceiling($ExceededAmount \/ 500)\r\n                    }\r\n\r\n                    # Remove excess feedback entries\r\n                    for ($i = 0 ; $i -lt $BatchSize ; $i++) {\r\n                        $AllFeedback.RemoveAt($AllFeedback.Count - 1)\r\n                    }\r\n\r\n                    # Rebuild the HTML table with the remaining feedback\r\n                    $HTMLTable = $AllFeedback | Select-Object -First $CommentsToKeep | Select-Object @{ Name = 'Feedback Date'; Expression = {\r\n                            \"$($_.TimeStamp.ToShortDateString()) $($_.TimeStamp.ToShortTimeString())\"\r\n                        }\r\n                    }, Username, Domain, Message | ConvertTo-Html -Fragment\r\n                    $HTMLTable = $HTMLTable -replace '&lt;th&gt;', \"&lt;th&gt;&lt;b&gt;\" -replace '&lt;\\\/th&gt;', \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n                    $HTMLTable = $HTMLTable -replace '&lt;th&gt;&lt;b&gt;Feedback Date', \"&lt;th style='width: 15em'&gt;&lt;b&gt;Feedback Date\"\r\n                    $HTMLTable = $HTMLTable -replace '&lt;th&gt;&lt;b&gt;Username', \"&lt;th style='width: 15em'&gt;&lt;b&gt;Username\"\r\n                    $HTMLTable = $HTMLTable -replace '&lt;th&gt;&lt;b&gt;Domain', \"&lt;th style='width: 15em'&gt;&lt;b&gt;Domain\"\r\n\r\n                    # Wrap the updated table in a styled HTML card\r\n                    $HTMLCard = \"&lt;div class='card flex-grow-1'&gt;\r\n    &lt;div class='card-title-box'&gt;\r\n        &lt;div class='card-title'&gt;&lt;i class='fa-solid fa-comment'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;User Feedback History&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div class='card-body' style='white-space: nowrap'&gt;\r\n        &lt;div&gt;\r\n            &lt;br&gt;\r\n            $HTMLTable\r\n        &lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n&lt;\/div&gt;\"\r\n\r\n                    # Add the updated card to the feedback history\r\n                    $FeedbackHistoryValue.Add($HTMLCard)\r\n                    $FeedbackHistoryValue.Add(\"&lt;\/div&gt;\")\r\n\r\n                    # Recalculate character count and continue trimming if necessary\r\n                    $Characters = ($FeedbackHistoryValue | Out-String).Length\r\n                    $ElapsedTime = (Get-Date) - $TrimStart\r\n                    if ($ElapsedTime.TotalMinutes -ge 5) {\r\n                        Write-Host -Object \"The current character count is '$Characters'.\"\r\n                        throw \"5 minute timeout reached. Unable to trim the output to comply with the character limit.\"\r\n                    }\r\n                } while ($Characters -ge 190000)\r\n            }\r\n\r\n            # Save the feedback history to the custom field\r\n            Write-Host -Object \"Attempting to save the feedback to the custom field '$WYSIWYGCustomFieldName'.\"\r\n            $FeedbackHistoryValue | Set-NinjaProperty -Name $WYSIWYGCustomFieldName -Type \"WYSIWYG\" -ErrorAction Stop\r\n            $AllFeedback | Select-Object -First $CommentsToKeep | Where-Object { $FeedbackCollected -notcontains $_ } | ForEach-Object { $FeedbackCollected.Add($_) }\r\n            Write-Host -Object \"Successfully saved the feedback history to the custom field.`n\"\r\n        } catch {\r\n            # Log an error if unable to save the feedback history\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to save the feedback history to the custom field '$WYSIWYGCustomFieldName'.`n\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    # Exit if no feedback was collected\r\n    if ($FeedbackCollected.Count -eq 0) {\r\n        exit $ExitCode\r\n    }\r\n\r\n    # Update feedback logs to mark feedback as collected\r\n    Write-Host -Object \"Updating the feedback log(s) to indicate that the feedback has been collected.\"\r\n    $LogsToUpdate = $FeedbackCollected | Group-Object -Property \"LogLocation\"\r\n    $LogsToUpdate | ForEach-Object {\r\n        try {\r\n            # Retrieve the log file content\r\n            $LogPath = $_.Name\r\n            $LogContent = Get-Content -Path $LogPath -ErrorAction Stop\r\n        } catch {\r\n            # Log an error if unable to retrieve the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the log file at '$($_.Name)'.\"\r\n            $ExitCode = 1\r\n            return\r\n        }\r\n\r\n        try {\r\n            # Create a map of updates for the log file\r\n            $UpdateMap = @{}\r\n            $_.Group | ForEach-Object {\r\n                $Key = \"$($_.TimeStamp.ToString('yyyy-MM-dd HH:mm:ssK'))|$($_.Username)|$($_.Domain)|$($_.FeedbackCollected)\"\r\n                $Message = $_.Base64\r\n                $UpdateMap[\"$Key;$Message\"] = \"$($_.TimeStamp.ToString('yyyy-MM-dd HH:mm:ssK'))|$($_.Username)|$($_.Domain)|$True\"\r\n            }\r\n\r\n            # Update the log file content with the collected feedback status\r\n            $LogContent = $LogContent | ForEach-Object {\r\n                $Line = $_\r\n                foreach ($UpdateKey in $UpdateMap.Keys) {\r\n                    $EscapedKey = [regex]::Escape(($UpdateKey -split ';', 2)[0])\r\n                    $EscapedMsg = [regex]::Escape(($UpdateKey -split ';', 2)[1])\r\n                    if ($Line -match $EscapedKey -and $Line -match $EscapedMsg) {\r\n                        $Line = $Line -replace $EscapedKey, $UpdateMap[$UpdateKey]\r\n                    }\r\n                }\r\n\r\n                $Line\r\n            }\r\n\r\n            # Save the updated log file content\r\n            Set-Content -Path $LogPath -Value $LogContent -ErrorAction Stop\r\n            Write-Host -Object \"Updated the log file at '$($_.Name)'.\"\r\n        } catch {\r\n            # Log an error if unable to update the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to update the log file at '$($_.Name)'.\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    # Clean up log files to retain only the specified number of comments\r\n    Write-Host -Object \"`nCleaning up the log files so only the previous '$CommentsToKeep' are kept.\"\r\n    $FeedbackLogLocations | ForEach-Object {\r\n        try {\r\n            # Retrieve the log file content\r\n            $LogPath = $_.LogFileLocation\r\n            $LogContent = Get-Content -Path $LogPath -ErrorAction Stop\r\n        } catch {\r\n            # Log an error if unable to retrieve the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the log file at '$($_.LogFileLocation)'.\"\r\n            $ExitCode = 1\r\n            return\r\n        }\r\n\r\n        try {\r\n            # Trim the log file content if it exceeds the specified number of comments\r\n            if ($LogContent.Count -le $CommentsToKeep) {\r\n                Write-Host -Object \"The log file at '$LogPath' already has less than or equal to '$CommentsToKeep' comments.\"\r\n            } else {\r\n                Set-Content -Path $LogPath -Value ($LogContent | Select-Object -Last $CommentsToKeep) -ErrorAction Stop\r\n                Write-Host -Object \"Updated the log file at '$LogPath'.\"\r\n            }\r\n        } catch {\r\n            # Log an error if unable to update the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to update the log file at '$($_.LogFileLocation)'.\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    exit $ExitCode\r\n}\r\nend {\r\n    \r\n    \r\n    \r\n}<\/pre>\n<p>&nbsp;<\/p>\n\n<h2>Description d\u00e9taill\u00e9e<\/h2>\n<p>Le script est complet et m\u00e9thodiquement con\u00e7u pour \u00eatre fiable et flexible. Voici comment :<\/p>\n<h3>1. Initialisation et validation<\/h3>\n<ul>\n<li>Valide la compatibilit\u00e9 de la version du syst\u00e8me d&rsquo;exploitation (Windows 10\/Server 2016 et plus r\u00e9cent).<\/li>\n<li>V\u00e9rifie les privil\u00e8ges administratifs.<\/li>\n<li>Valide et assainit les param\u00e8tres d&rsquo;entr\u00e9e tels que MaxCommentsToKeep, KeepFeedbackHistory et les noms de champs personnalis\u00e9s.<\/li>\n<\/ul>\n<h3>2. \u00c9num\u00e9ration des profils d&rsquo;utilisateurs<\/h3>\n<ul>\n<li>Utilise la fonction Get-UserHive pour \u00e9num\u00e9rer tous les profils d&rsquo;utilisateurs (domaine, local, AzureAD).<\/li>\n<li>Charge les ruches (hives) du registre lorsque cela est n\u00e9cessaire pour extraire les chemins d&rsquo;acc\u00e8s aux fichiers journaux appropri\u00e9s.<\/li>\n<\/ul>\n<h3>3. D\u00e9couverte du journal des avis<\/h3>\n<ul>\n<li>Recherche dans les chemins d&rsquo;acc\u00e8s standard AppData\\Local les fichiers journaux de retour d&rsquo;information nomm\u00e9s UserDeviceFeedback.log.<\/li>\n<\/ul>\n<h3>4. Extraction et analyse des avis<\/h3>\n<ul>\n<li>Lit les fichiers journaux ligne par ligne.<\/li>\n<li>Chaque ligne est divis\u00e9e en points de donn\u00e9es tels que l&rsquo;horodatage, le nom d&rsquo;utilisateur, le domaine et l&rsquo;avis encod\u00e9 en base64.<\/li>\n<li>Le message est d\u00e9cod\u00e9 et ajout\u00e9 \u00e0 une collection d&rsquo;entr\u00e9es de retour d&rsquo;information.<\/li>\n<\/ul>\n<h3>5. Mise \u00e0 jour des champs personnalis\u00e9s<\/h3>\n<ul>\n<li>Les avis les plus r\u00e9cents non collect\u00e9s, en fonction de l&rsquo;utilisateur, sont compil\u00e9s.<\/li>\n<li>Enregistre dans le Dernier avis de l&rsquo;utilisateur (champ multiligne) s&rsquo;il respecte la limite de 10 000 caract\u00e8res.<\/li>\n<li>Si cette option est activ\u00e9e, un tableau HTML format\u00e9 des derni\u00e8res entr\u00e9es\u00a0\u00a0est cr\u00e9\u00e9 et enregistr\u00e9 dans l&rsquo;historique des avis des utilisateurs (champ WYSIWYG), dans le respect d&rsquo;une limite de 200 000 caract\u00e8res.<\/li>\n<\/ul>\n<h3>6. Mises \u00e0 jour du fichier journal<\/h3>\n<ul>\n<li>Marque les entr\u00e9es d&rsquo;avis trait\u00e9es comme \u00e9tant \u00ab\u00a0collect\u00e9es\u00a0\u00bb.<\/li>\n<li>\u00c9limine les entr\u00e9es les plus anciennes au-del\u00e0 du maximum sp\u00e9cifi\u00e9 afin de maintenir la taille du journal.<\/li>\n<\/ul>\n<h3>7. Gestion des limites de caract\u00e8res<\/h3>\n<ul>\n<li>Il \u00e9limine automatiquement les anciens avis si les limites du champ sont d\u00e9pass\u00e9es, ce qui garantit que les donn\u00e9es ne sont pas tronqu\u00e9es au milieu de l&rsquo;enregistrement.<\/li>\n<\/ul>\n<p>Cette approche structur\u00e9e permet de ne recueillir que des informations nouvelles et pertinentes et d&rsquo;\u00e9viter les traitements redondants.<\/p>\n<h2>Cas d&rsquo;utilisation potentiels<\/h2>\n<p><strong>Sc\u00e9nario :<\/strong>\u00a0Un fournisseur de services g\u00e9r\u00e9s (MSP) de taille moyenne g\u00e8re 500 terminaux r\u00e9partis entre plusieurs clients. Apr\u00e8s avoir d\u00e9ploy\u00e9 une mise \u00e0 jour des performances, ils souhaitent \u00e9valuer son impact sur l&rsquo;exp\u00e9rience des utilisateurs.<\/p>\n<ul>\n<li><strong>\u00c9tape 1 :<\/strong>\u00a0D\u00e9ployer le script 1 sur tous les appareils pour inviter les utilisateurs \u00e0 donner leur avis.<\/li>\n<li><strong>\u00c9tape 2 :<\/strong>\u00a0Utiliser le script 2 pour consolider les r\u00e9ponses dans NinjaOne.<\/li>\n<li><strong>\u00c9tape 3 :<\/strong>\u00a0Examiner les r\u00e9ponses de mani\u00e8re centralis\u00e9e et faire remonter les appareil avec un avis est n\u00e9gatif.<\/li>\n<\/ul>\n<p>Cela permet de cr\u00e9er une boucle de r\u00e9troaction ferm\u00e9e sans intervention humaine, ce qui simplifie le d\u00e9pannage et am\u00e9liore la satisfaction des clients.<\/p>\n<h2>Comparaisons<\/h2>\n<p>Il existe d&rsquo;autres m\u00e9thodes pour recueillir les avis des utilisateurs dans Windows avec un script <a href=\"https:\/\/www.ninjaone.com\/it-hub\/endpoint-management\/what-is-powershell\/\">PowerShell<\/a> :<\/p>\n<ul>\n<li>\u00c9crire directement sur un fichier central partag\u00e9 (risque de probl\u00e8mes d&rsquo;autorisation).<\/li>\n<li>Utilisation des journaux d&rsquo;\u00e9v\u00e9nements (analyse complexe).<\/li>\n<li>Envoi des journaux par e-mail (non s\u00e9curis\u00e9 et non extensible).<\/li>\n<\/ul>\n<p>En comparaison, l&rsquo;int\u00e9gration de NinjaOne se fait par l&rsquo;interm\u00e9diaire de champs personnalis\u00e9s :<\/p>\n<ul>\n<li>Garantit un stockage des donn\u00e9es s\u00e9curis\u00e9 et li\u00e9 \u00e0 l&rsquo;appareil.<\/li>\n<li>Facilite l&rsquo;acc\u00e8s \u00e0 la console NinjaOne.<\/li>\n<li>Permet l&rsquo;automatisation et l&rsquo;\u00e9tablissement de rapports via les flux de travail NinjaOne.<\/li>\n<\/ul>\n<h2>Questions fr\u00e9quentes<\/h2>\n<h3>Question 1\u00a0: Que se passe-t-il si aucun avis n&rsquo;est trouv\u00e9 ?<\/h3>\n<p>Le script met \u00e0 jour le champ personnalis\u00e9 avec la mention \u00ab\u00a0Aucun avis n&rsquo;a \u00e9t\u00e9 donn\u00e9\u00a0\u00bb ( \u201cNo feedback has been given\u201d)<\/p>\n<h3>Question 2\u00a0: Que se passe-t-il lorsque les avis d\u00e9passent la limite de caract\u00e8res ?<\/h3>\n<p>Le script supprime automatiquement les entr\u00e9es les plus anciennes et notifie le contenu du champ.<\/p>\n<h3>Question 3\u00a0: Peut-on l&rsquo;utiliser sans le script 1 ?<\/h3>\n<p>Non, le script 2 d\u00e9pend des journaux de retour d&rsquo;information g\u00e9n\u00e9r\u00e9s par le script 1.<\/p>\n<h3>Question 4\u00a0: Prend-il en charge les utilisateurs AzureAD ?<\/h3>\n<p>Oui, il d\u00e9tecte les profils d&rsquo;utilisateurs AzureAD, locaux et de domaine.<\/p>\n<h3>Question 5 : Les balises HTML sont-elles n\u00e9cessaires dans le champ WYSIWYG ?<\/h3>\n<p>Oui, le script g\u00e9n\u00e8re automatiquement un tableau HTML complet avec une mise en forme pour une meilleure lisibilit\u00e9.<\/p>\n<h2>Implications<\/h2>\n<p>L&rsquo;automatisation de la collecte des avis a des implications consid\u00e9rables. En recueillant et en archivant continuellement les avis, les d\u00e9partements informatiques peuvent :<\/p>\n<ul>\n<li>D\u00e9tecter rapidement les r\u00e9gressions de performance.<\/li>\n<li>Identifier les probl\u00e8mes r\u00e9currents.<\/li>\n<li>Justifier les investissements dans les infrastructures \u00e0 l&rsquo;aide de donn\u00e9es r\u00e9elles sur les sentiments des utilisateurs.<\/li>\n<\/ul>\n<p>De plus, le stockage s\u00e9curis\u00e9 de ces informations dans NinjaOne renforce la conformit\u00e9 des donn\u00e9es et les normes de gouvernance.<\/p>\n<h2>Recommandations<\/h2>\n<ul>\n<li><strong>Ex\u00e9cutez le script 2 selon un calendrier<\/strong>\u00a0(par exemple, tous les jours) en utilisant les scripts planifi\u00e9s de NinjaOne.<\/li>\n<li>Pour une pleine fonctionnalit\u00e9, <strong>il faut toujours l&rsquo;associer au Script 1<\/strong>\u00a0.<\/li>\n<li><strong>D\u00e9finissez des valeurs r\u00e9alistes<\/strong>\u00a0pour MaxCommentsToKeep afin d&rsquo;\u00e9viter d&rsquo;inonder les journaux.<\/li>\n<li><strong>Activez\u00a0<\/strong><strong>KeepFeedbackHistory<\/strong>\u00a0si une analyse des tendances \u00e0 long terme est n\u00e9cessaire.<\/li>\n<li><strong>Surveillez la taille des champs<\/strong>\u00a0pour \u00e9viter que les donn\u00e9es ne soient tronqu\u00e9es en raison des limites de la plateforme.<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>Ce script illustre comment recueillir les avis des utilisateurs dans Windows avec un script PowerShell et l&rsquo;int\u00e9grer dans un \u00e9cosyst\u00e8me RMM tel que <a href=\"https:\/\/www.ninjaone.com\/fr\/\">NinjaOne<\/a>. Pour les professionnels de l&rsquo;informatique et les MSP qui s&rsquo;efforcent de fournir un support proactif et centr\u00e9 sur l&rsquo;utilisateur, cette automatisation r\u00e9duit les frais g\u00e9n\u00e9raux, am\u00e9liore la visibilit\u00e9 et permet de prendre des d\u00e9cisions fond\u00e9es sur des donn\u00e9es. En exploitant les capacit\u00e9s de script et les champs personnalis\u00e9s de NinjaOne, la v\u00e9rification des avis n&rsquo;est plus un \u00e9l\u00e9ment \u00e0 cocher, mais un pilier de la qualit\u00e9 du service informatique.<\/p>\n","protected":false},"author":35,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"_relevanssi_hide_post":"","_relevanssi_hide_content":"","_relevanssi_pin_for_all":"","_relevanssi_pin_keywords":"","_relevanssi_unpin_keywords":"","_relevanssi_related_keywords":"","_relevanssi_related_include_ids":"","_relevanssi_related_exclude_ids":"","_relevanssi_related_no_append":"","_relevanssi_related_not_related":"","_relevanssi_related_posts":"","_relevanssi_noindex_reason":"","_lmt_disableupdate":"no","_lmt_disable":""},"operating_system":[4212],"use_cases":[4289],"class_list":["post-530398","script_hub","type-script_hub","status-publish","hentry","script_hub_category-windows","use_cases-gestion-des-utilisateurs-et-des-acces"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/script_hub\/530398","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/script_hub"}],"about":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/types\/script_hub"}],"author":[{"embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/users\/35"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/comments?post=530398"}],"wp:attachment":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/media?parent=530398"}],"wp:term":[{"taxonomy":"script_hub_category","embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/operating_system?post=530398"},{"taxonomy":"use_cases","embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/use_cases?post=530398"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}