diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj
index d844674d231f..6fdf7d4a30cf 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj
@@ -42,7 +42,9 @@ For more information on Az Predictor, please visit the following: https://aka.ms
+
+
-
+
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.psd1 b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.psd1
index 98ab334c1ebc..5528043a075f 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.psd1
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.psd1
@@ -29,16 +29,11 @@ CompanyName = 'Microsoft Corporation'
Copyright = 'Microsoft Corporation. All rights reserved.'
# Description of the functionality provided by this module
-Description = 'Microsoft Azure PowerShell - Module providing recommendations to PSReadLine v2.2.0 or above for cmdlets comprised in the Az module - This module is compatible with PowerShell 7.1 or above.
+Description = 'Microsoft Azure PowerShell - Module providing recommendations for cmdlets comprised in the Az module - This module is compatible with PowerShell 7.2 or above.
-The module needs to be imported manually via
-Import-Module Az.Tools.Predictor
-
-Enable plugins via
-Set-PSReadLineOption -PredictionSource HistoryAndPlugin
-
-Switch the output format of suggestions to list view via
-Set-PSReadLineOption -PredictionViewStyle ListView
+The suggestions must be activated:
+- Enable-AzPredictor: Activate the suggestions
+- Disable-AzPredictor: Disable the suggestions
For more information on Az Predictor, please visit the following: https://aka.ms/azpredictordocs'
@@ -50,6 +45,8 @@ PowerShellVersion = '7.1'
NestedModules = @("Microsoft.Azure.PowerShell.Tools.AzPredictor.dll")
+ScriptsToProcess = @("PromptSurvey.ps1")
+
CmdletsToExport = @("Enable-AzPredictor", "Disable-AzPredictor")
# Format files (.ps1xml) to be loaded when importing this module
@@ -60,7 +57,7 @@ PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
- Tags = 'Azure','PowerShell','Prediction'
+ Tags = 'Azure', 'PowerShell', 'Prediction', 'Recommendation', 'Az Predictor'
# A URL to the license for this module.
LicenseUri = 'https://aka.ms/azps-license'
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs
index 91e6dbc5cb87..72f70a6d85c1 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs
@@ -30,6 +30,7 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor
///
internal sealed class AzContext : IAzContext
{
+ private const string InternalUserSuffix = "@microsoft.com";
private static readonly Version DefaultVersion = new Version("0.0.0.0");
///
@@ -100,13 +101,33 @@ public Version ModuleVersion
}
}
+ ///
+ public bool IsInternal { get; internal set; }
+
+ ///
+ /// The survey session id appended to the survey.
+ ///
+ ///
+ /// We only collect this information in the preview and it'll be removed in GA. That's why it's not defined in the
+ /// interface IAzContext and it's internal.
+ ///
+ internal string SurveyId { get; set; }
+
///
public void UpdateContext()
{
AzVersion = GetAzVersion();
- UserId = GenerateSha256HashString(GetUserAccountId());
+ RawUserId = GetUserAccountId();
+ UserId = GenerateSha256HashString(RawUserId);
+
+ if (!IsInternal)
+ {
+ IsInternal = RawUserId.EndsWith(AzContext.InternalUserSuffix, StringComparison.OrdinalIgnoreCase);
+ }
}
+ internal string RawUserId { get; set; }
+
///
/// Gets the user account id if the user logs in, otherwise empty string.
///
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs
index 600c388a5603..706765240a94 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs
@@ -16,6 +16,7 @@
using Microsoft.Azure.PowerShell.Tools.AzPredictor.Utilities;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
@@ -225,7 +226,12 @@ public class PredictorInitializer : IModuleAssemblyInitializer
public void OnImport()
{
var settings = Settings.GetSettings();
- var azContext = new AzContext();
+ var azContext = new AzContext()
+ {
+ IsInternal = (settings.SetAsInternal == true) ? true : false,
+ SurveyId = settings.SurveyId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
+ };
+
azContext.UpdateContext();
var telemetryClient = new AzPredictorTelemetryClient(azContext);
var azPredictorService = new AzPredictorService(settings.ServiceUri, telemetryClient, azContext);
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs
index 657ea8369f3c..b6e4e38de58f 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs
@@ -51,6 +51,11 @@ internal interface IAzContext
///
public Version AzVersion { get; }
+ ///
+ /// Gets whether the user is an internal user.
+ ///
+ public bool IsInternal { get; }
+
///
/// Updates the Az context.
///
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/InterceptSurvey.ps1 b/tools/Az.Tools.Predictor/Az.Tools.Predictor/InterceptSurvey.ps1
new file mode 100644
index 000000000000..79b06848a9af
--- /dev/null
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/InterceptSurvey.ps1
@@ -0,0 +1,209 @@
+# ----------------------------------------------------------------------------------
+#
+# Copyright Microsoft Corporation
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.internal
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ----------------------------------------------------------------------------------
+
+# This file is a temporary approach to prompt the user for a survey.
+# It doesn't cover every case well or not tested well:
+# 1. Allow two or more modules to show the survey link.
+# 2. When the major version is changed.
+# 3. Not sure about the way to handle survey id or if it's needed in future.
+# 4. The file format is also subject to change in future.
+
+param (
+ [Parameter(Mandatory)]
+ [string] $moduleName,
+ [Parameter(Mandatory)]
+ [int] $majorVersion
+)
+
+if ([string]::IsNullOrWhiteSpace($moduleName)) {
+ return
+}
+
+if ($majorVersion -lt 0) {
+ return
+}
+
+if ($env:Azure_PS_Intercept_Survey -eq "false") {
+ return
+}
+
+$mutexName = "AzModulesInterceptSurvey"
+$mutexTiimeout = 1000
+$interceptDays = 30
+$interceptLoadTimes = 3
+$today = Get-Date
+$mutexTimeout = 500
+
+function ConvertTo-String {
+ param (
+ [Parameter(Mandatory)]
+ [DateTime] $date
+ )
+
+ return $date.ToString("yyyy-MM-dd")
+}
+
+function Init-InterceptFile {
+ $interceptContent = @{
+ "lastInterceptCheckDate"=ConvertTo-String($today);
+ "interceptTriggered"=$false;
+ "modules"=@(@{
+ "name"=$moduleName;
+ "majorVersion"=$majorVersion;
+ "activeDays"=1;
+ "lastActiveDate"=ConvertTo-String($today);
+ })
+ }
+
+ ConvertTo-Json -InputObject $interceptContent | Out-File -FilePath $interceptFilePath -Encoding utf8
+}
+
+# Update the intercept object and return $true if we need to show the survey.
+function Update-InterceptObject {
+ param (
+ $interceptObject
+ )
+
+ $thisModule = $null
+
+ foreach ($m in $interceptObject.modules) {
+ if ($m.name -eq $moduleName) {
+ $thisModule = $m
+ break
+ }
+ }
+
+ if ($thisModule -eq $null) {
+ # There is no information about this module. The file could be created by another module or in some other way.
+ # We need to add this module to the list.
+
+ $thisModule = @{
+ "name"=$moduleName;
+ "majorVersion"=$majorVersion;
+ "activeDays"=1;
+ "lastActiveDate"=ConvertTo-String($today);
+ }
+
+ $interceptObject.modules += $thisModule
+
+ return $false
+ }
+
+ $recordedMajorVersion = $thisModule.majorVersion
+ $thisModule.majorVersion = $majorVersion
+
+ if ($recordedMajorVersion -ne $majorVersion) {
+ $thisModule.activeDays = 1
+ $thisModule.lastActiveDate = ConvertTo-String($today)
+ $interceptObject.interceptTriggered = $false
+
+ return $false
+ }
+
+ $recordedLastActiveDate = Get-Date $thisModule.lastActiveDate
+ $recordedActiveDays = $thisModule.activeDays
+
+ $elapsedDays = ($today - $recordedLastActiveDate).Days
+
+ if ($elapsedDays -gt $interceptDays) {
+ $thisModule.activeDays = 1
+ $thisModule.lastActiveDate = ConvertTo-String($today)
+
+ return $false
+ }
+
+ $newActiveDays = $recordedActiveDays
+
+ if ($elapsedDays -ne 0) {
+ $newActiveDays++
+ }
+
+ if ($newActiveDays -ge $interceptLoadTimes) {
+ $thisModule.activeDays = 0
+ $thisModule.lastActiveDate = ConvertTo-String($today)
+ $interceptObject.interceptTriggered = $true
+ return $true
+ }
+
+ $thisModule.activeDays = $newActiveDays
+ $thisModule.lastActiveDate = ConvertTo-String($today)
+}
+
+$mutex = New-Object System.Threading.Mutex($false, $mutexName)
+
+$hasMutex = $mutex.WaitOne($mutexTimeout)
+
+if (-not $hasMutex) {
+ return
+}
+
+$shouldIntercept = $false
+
+try
+{
+ $interceptFilePath = Join-Path -Path (Join-Path -Path $env:USERPROFILE -ChildPath ".Azure") -ChildPath "InterceptSurvey.json"
+
+ if (-not (Test-Path $interceptFilePath)) {
+ New-Item -ItemType File -Force -Path $interceptFilePath
+ Init-InterceptFile
+ } else {
+ $interceptObject = $null
+ try {
+ $fileContent = Get-Content $interceptFilePath | Out-String
+ $interceptObject = ConvertFrom-Json $fileContent
+ } catch {
+ Init-InterceptFile
+ }
+
+ if (-not ($interceptObject -eq $null)) {
+ $shouldIntercept = Update-InterceptObject($interceptObject)
+
+ ConvertTo-Json -InputObject $interceptObject | Out-File $interceptFilePath -Encoding utf8
+ }
+ }
+} catch {
+}
+
+$mutex.ReleaseMutex()
+
+if ($shouldIntercept) {
+ $userId = (Get-AzContext).Account.Id
+ $surveyId = "000000"
+
+ if ($userId -ne $null)
+ {
+ $surveyId = Get-Random -Maximum 1000000 -SetSeed $userId.GetHashCode()
+ try {
+ $azPredictorSettingFilePath = Join-Path -Path (Join-Path -Path $env:USERPROFILE -ChildPath ".Azure") -ChildPath "AzPredictorSettings.json"
+ $setting = @{
+ "surveyId"=$surveyId;
+ }
+
+ if (Test-Path $azPredictorSettingFilePath) {
+ try {
+ $setting = Get-Content $azPredictorSettingFilePath | Out-String | ConvertFrom-Json
+ $setting | Add-Member -NotePropertyName "surveyId" -NotePropertyValue $surveyId -Force
+ } catch {
+ }
+ }
+
+ ConvertTo-Json -InputObject $setting | Out-File -FilePath $azPredictorSettingFilePath -Encoding utf8
+ } catch {
+ }
+ }
+
+ $escape = $([char]27)
+ Write-Host "`n$escape[7mHow was your experience using Az predictor? $escape[27m`n" -NoNewline; Write-Host "$escape[7mhttp://aka.ms/azpredictorisurvey?SessionId=$surveyId$escape[27m" -NoNewline
+ Write-Host "`n"
+}
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/PromptSurvey.ps1 b/tools/Az.Tools.Predictor/Az.Tools.Predictor/PromptSurvey.ps1
new file mode 100644
index 000000000000..05f6f1ce9245
--- /dev/null
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/PromptSurvey.ps1
@@ -0,0 +1,16 @@
+# ----------------------------------------------------------------------------------
+#
+# Copyright Microsoft Corporation
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.internal
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ----------------------------------------------------------------------------------
+
+$targetScript = (Join-Path -Path $PSScriptRoot -ChildPath "InterceptSurvey.ps1")
+& $targetScript "Az.Tools.Predictor" 0
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Settings.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Settings.cs
index 4dfabbe10a9b..2c3f9e8a8ead 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Settings.cs
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Settings.cs
@@ -31,16 +31,30 @@ sealed class Settings
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
+ ///
+ /// The maximum number of suggestions that have the same command name.
+ ///
+ public int? MaxAllowedCommandDuplicate { get; set; }
+
///
/// The service to get the prediction results back.
///
public string ServiceUri { get; set; }
+ ///
+ /// Set the user as an internal user.
+ ///
+ public bool? SetAsInternal { get; set; }
+
///
/// The number of suggestions to return to PSReadLine.
///
public int? SuggestionCount { get; set; }
- public int? MaxAllowedCommandDuplicate { get; set; }
+
+ ///
+ /// The survey id. It should be internal but make it public so that we can read/write to Json.
+ ///
+ public int? SurveyId { get; set; }
private static bool? _isContinueOnTimeout;
///
@@ -127,6 +141,14 @@ private void OverrideSettingsFromProfile()
{
this.MaxAllowedCommandDuplicate = profileSettings.MaxAllowedCommandDuplicate;
}
+
+ this.SetAsInternal = profileSettings.SetAsInternal;
+ this.SurveyId = profileSettings.SurveyId;
+
+ profileSettings.SurveyId = null;
+
+ fileContent = JsonSerializer.Serialize(profileSettings, new JsonSerializerOptions(Settings._jsonSerializerOptions) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
+ File.WriteAllText(profileSettingFilePath, fileContent, Encoding.UTF8);
}
catch
{
diff --git a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Telemetry/AzPredictorTelemetryClient.cs b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Telemetry/AzPredictorTelemetryClient.cs
index cbcf3bf490b6..92b1260ec76b 100644
--- a/tools/Az.Tools.Predictor/Az.Tools.Predictor/Telemetry/AzPredictorTelemetryClient.cs
+++ b/tools/Az.Tools.Predictor/Az.Tools.Predictor/Telemetry/AzPredictorTelemetryClient.cs
@@ -290,6 +290,8 @@ private IDictionary CreateProperties(ITelemetryData telemetryDat
{ "SessionId", telemetryData.SessionId },
{ "CorrelationId", telemetryData.CorrelationId },
{ "UserId", _azContext.UserId },
+ { "IsInternal", _azContext.IsInternal.ToString(CultureInfo.InvariantCulture) },
+ { "SurveyId", (_azContext as AzContext)?.SurveyId },
{ "HashMacAddress", _azContext.MacAddress },
{ "PowerShellVersion", _azContext.PowerShellVersion.ToString() },
{ "ModuleVersion", _azContext.ModuleVersion.ToString() },