Skip to content

Commit 319b8e6

Browse files
kceiwdcaro
andauthored
Add prompt for usage and survey (#13965)
* Add prompt for usage and survey. * Condition the survey and correlate survey and telemetry. - Check if the user has used the module in the past 30 days and whether the user uses at least 3 times. If so, prompt the survey. - Create a survey id based on user id and use it to associate the survey and the telemetry. - Add a field in the telemetry to check if the telemetry data is from an internal user. * Wrap the intercept script in the script to process. * Update the link and message color. * Update the survey prompt UI. * Updated module description * Revert the version and add copyright. * Incorporate feedback. * Incorporate feedback. * Improve the comment. * Use a new flag to ignore null value in json serialization. * Remove the prompt about psreadlineoption. - It's replaced with cmdlets. Co-authored-by: Damien Caro <[email protected]>
1 parent e38e1a3 commit 319b8e6

File tree

9 files changed

+294
-14
lines changed

9 files changed

+294
-14
lines changed

tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ For more information on Az Predictor, please visit the following: https://aka.ms
4242

4343
<ItemGroup>
4444
<None Include="Az.Tools.Predictor.psd1" CopyToOutputDirectory="PreserveNewest" />
45+
<None Include="InterceptSurvey.ps1" CopyToOutputDirectory="PreserveNewest" />
46+
<None Include="PromptSurvey.ps1" CopyToOutputDirectory="PreserveNewest" />
4547
<None Include="AzPredictorSettings.json" CopyToOutputDirectory="PreserveNewest" />
46-
<None Include="command_param_to_resource_map.json" CopyToOutputDirectory="PreserveNewest" />
48+
<None Include="command_param_to_resource_map.json" CopyToOutputDirectory="PreserveNewest" />
4749
</ItemGroup>
4850
</Project>

tools/Az.Tools.Predictor/Az.Tools.Predictor/Az.Tools.Predictor.psd1

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,11 @@ CompanyName = 'Microsoft Corporation'
2929
Copyright = 'Microsoft Corporation. All rights reserved.'
3030

3131
# Description of the functionality provided by this module
32-
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.
32+
Description = 'Microsoft Azure PowerShell - Module providing recommendations for cmdlets comprised in the Az module - This module is compatible with PowerShell 7.2 or above.
3333
34-
The module needs to be imported manually via
35-
Import-Module Az.Tools.Predictor
36-
37-
Enable plugins via
38-
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
39-
40-
Switch the output format of suggestions to list view via
41-
Set-PSReadLineOption -PredictionViewStyle ListView
34+
The suggestions must be activated:
35+
- Enable-AzPredictor: Activate the suggestions
36+
- Disable-AzPredictor: Disable the suggestions
4237
4338
For more information on Az Predictor, please visit the following: https://aka.ms/azpredictordocs'
4439

@@ -50,6 +45,8 @@ PowerShellVersion = '7.1'
5045

5146
NestedModules = @("Microsoft.Azure.PowerShell.Tools.AzPredictor.dll")
5247

48+
ScriptsToProcess = @("PromptSurvey.ps1")
49+
5350
CmdletsToExport = @("Enable-AzPredictor", "Disable-AzPredictor")
5451

5552
# Format files (.ps1xml) to be loaded when importing this module
@@ -60,7 +57,7 @@ PrivateData = @{
6057
PSData = @{
6158

6259
# Tags applied to this module. These help with module discovery in online galleries.
63-
Tags = 'Azure','PowerShell','Prediction'
60+
Tags = 'Azure', 'PowerShell', 'Prediction', 'Recommendation', 'Az Predictor'
6461

6562
# A URL to the license for this module.
6663
LicenseUri = 'https://aka.ms/azps-license'

tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor
3030
/// </summary>
3131
internal sealed class AzContext : IAzContext
3232
{
33+
private const string InternalUserSuffix = "@microsoft.com";
3334
private static readonly Version DefaultVersion = new Version("0.0.0.0");
3435

3536
/// <inheritdoc/>
@@ -100,13 +101,33 @@ public Version ModuleVersion
100101
}
101102
}
102103

104+
/// <inheritdoc/>
105+
public bool IsInternal { get; internal set; }
106+
107+
/// <summary>
108+
/// The survey session id appended to the survey.
109+
/// </summary>
110+
/// <remarks>
111+
/// We only collect this information in the preview and it'll be removed in GA. That's why it's not defined in the
112+
/// interface IAzContext and it's internal.
113+
/// </remarks>
114+
internal string SurveyId { get; set; }
115+
103116
/// <inheritdoc/>
104117
public void UpdateContext()
105118
{
106119
AzVersion = GetAzVersion();
107-
UserId = GenerateSha256HashString(GetUserAccountId());
120+
RawUserId = GetUserAccountId();
121+
UserId = GenerateSha256HashString(RawUserId);
122+
123+
if (!IsInternal)
124+
{
125+
IsInternal = RawUserId.EndsWith(AzContext.InternalUserSuffix, StringComparison.OrdinalIgnoreCase);
126+
}
108127
}
109128

129+
internal string RawUserId { get; set; }
130+
110131
/// <summary>
111132
/// Gets the user account id if the user logs in, otherwise empty string.
112133
/// </summary>

tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.Azure.PowerShell.Tools.AzPredictor.Utilities;
1717
using System;
1818
using System.Collections.Generic;
19+
using System.Globalization;
1920
using System.Linq;
2021
using System.Management.Automation;
2122
using System.Management.Automation.Language;
@@ -225,7 +226,12 @@ public class PredictorInitializer : IModuleAssemblyInitializer
225226
public void OnImport()
226227
{
227228
var settings = Settings.GetSettings();
228-
var azContext = new AzContext();
229+
var azContext = new AzContext()
230+
{
231+
IsInternal = (settings.SetAsInternal == true) ? true : false,
232+
SurveyId = settings.SurveyId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
233+
};
234+
229235
azContext.UpdateContext();
230236
var telemetryClient = new AzPredictorTelemetryClient(azContext);
231237
var azPredictorService = new AzPredictorService(settings.ServiceUri, telemetryClient, azContext);

tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ internal interface IAzContext
5151
/// </summary>
5252
public Version AzVersion { get; }
5353

54+
/// <summary>
55+
/// Gets whether the user is an internal user.
56+
/// </summary>
57+
public bool IsInternal { get; }
58+
5459
/// <summary>
5560
/// Updates the Az context.
5661
/// </summary>
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# ----------------------------------------------------------------------------------
2+
#
3+
# Copyright Microsoft Corporation
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.internal
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
# ----------------------------------------------------------------------------------
14+
15+
# This file is a temporary approach to prompt the user for a survey.
16+
# It doesn't cover every case well or not tested well:
17+
# 1. Allow two or more modules to show the survey link.
18+
# 2. When the major version is changed.
19+
# 3. Not sure about the way to handle survey id or if it's needed in future.
20+
# 4. The file format is also subject to change in future.
21+
22+
param (
23+
[Parameter(Mandatory)]
24+
[string] $moduleName,
25+
[Parameter(Mandatory)]
26+
[int] $majorVersion
27+
)
28+
29+
if ([string]::IsNullOrWhiteSpace($moduleName)) {
30+
return
31+
}
32+
33+
if ($majorVersion -lt 0) {
34+
return
35+
}
36+
37+
if ($env:Azure_PS_Intercept_Survey -eq "false") {
38+
return
39+
}
40+
41+
$mutexName = "AzModulesInterceptSurvey"
42+
$mutexTiimeout = 1000
43+
$interceptDays = 30
44+
$interceptLoadTimes = 3
45+
$today = Get-Date
46+
$mutexTimeout = 500
47+
48+
function ConvertTo-String {
49+
param (
50+
[Parameter(Mandatory)]
51+
[DateTime] $date
52+
)
53+
54+
return $date.ToString("yyyy-MM-dd")
55+
}
56+
57+
function Init-InterceptFile {
58+
$interceptContent = @{
59+
"lastInterceptCheckDate"=ConvertTo-String($today);
60+
"interceptTriggered"=$false;
61+
"modules"=@(@{
62+
"name"=$moduleName;
63+
"majorVersion"=$majorVersion;
64+
"activeDays"=1;
65+
"lastActiveDate"=ConvertTo-String($today);
66+
})
67+
}
68+
69+
ConvertTo-Json -InputObject $interceptContent | Out-File -FilePath $interceptFilePath -Encoding utf8
70+
}
71+
72+
# Update the intercept object and return $true if we need to show the survey.
73+
function Update-InterceptObject {
74+
param (
75+
$interceptObject
76+
)
77+
78+
$thisModule = $null
79+
80+
foreach ($m in $interceptObject.modules) {
81+
if ($m.name -eq $moduleName) {
82+
$thisModule = $m
83+
break
84+
}
85+
}
86+
87+
if ($thisModule -eq $null) {
88+
# There is no information about this module. The file could be created by another module or in some other way.
89+
# We need to add this module to the list.
90+
91+
$thisModule = @{
92+
"name"=$moduleName;
93+
"majorVersion"=$majorVersion;
94+
"activeDays"=1;
95+
"lastActiveDate"=ConvertTo-String($today);
96+
}
97+
98+
$interceptObject.modules += $thisModule
99+
100+
return $false
101+
}
102+
103+
$recordedMajorVersion = $thisModule.majorVersion
104+
$thisModule.majorVersion = $majorVersion
105+
106+
if ($recordedMajorVersion -ne $majorVersion) {
107+
$thisModule.activeDays = 1
108+
$thisModule.lastActiveDate = ConvertTo-String($today)
109+
$interceptObject.interceptTriggered = $false
110+
111+
return $false
112+
}
113+
114+
$recordedLastActiveDate = Get-Date $thisModule.lastActiveDate
115+
$recordedActiveDays = $thisModule.activeDays
116+
117+
$elapsedDays = ($today - $recordedLastActiveDate).Days
118+
119+
if ($elapsedDays -gt $interceptDays) {
120+
$thisModule.activeDays = 1
121+
$thisModule.lastActiveDate = ConvertTo-String($today)
122+
123+
return $false
124+
}
125+
126+
$newActiveDays = $recordedActiveDays
127+
128+
if ($elapsedDays -ne 0) {
129+
$newActiveDays++
130+
}
131+
132+
if ($newActiveDays -ge $interceptLoadTimes) {
133+
$thisModule.activeDays = 0
134+
$thisModule.lastActiveDate = ConvertTo-String($today)
135+
$interceptObject.interceptTriggered = $true
136+
return $true
137+
}
138+
139+
$thisModule.activeDays = $newActiveDays
140+
$thisModule.lastActiveDate = ConvertTo-String($today)
141+
}
142+
143+
$mutex = New-Object System.Threading.Mutex($false, $mutexName)
144+
145+
$hasMutex = $mutex.WaitOne($mutexTimeout)
146+
147+
if (-not $hasMutex) {
148+
return
149+
}
150+
151+
$shouldIntercept = $false
152+
153+
try
154+
{
155+
$interceptFilePath = Join-Path -Path (Join-Path -Path $env:USERPROFILE -ChildPath ".Azure") -ChildPath "InterceptSurvey.json"
156+
157+
if (-not (Test-Path $interceptFilePath)) {
158+
New-Item -ItemType File -Force -Path $interceptFilePath
159+
Init-InterceptFile
160+
} else {
161+
$interceptObject = $null
162+
try {
163+
$fileContent = Get-Content $interceptFilePath | Out-String
164+
$interceptObject = ConvertFrom-Json $fileContent
165+
} catch {
166+
Init-InterceptFile
167+
}
168+
169+
if (-not ($interceptObject -eq $null)) {
170+
$shouldIntercept = Update-InterceptObject($interceptObject)
171+
172+
ConvertTo-Json -InputObject $interceptObject | Out-File $interceptFilePath -Encoding utf8
173+
}
174+
}
175+
} catch {
176+
}
177+
178+
$mutex.ReleaseMutex()
179+
180+
if ($shouldIntercept) {
181+
$userId = (Get-AzContext).Account.Id
182+
$surveyId = "000000"
183+
184+
if ($userId -ne $null)
185+
{
186+
$surveyId = Get-Random -Maximum 1000000 -SetSeed $userId.GetHashCode()
187+
try {
188+
$azPredictorSettingFilePath = Join-Path -Path (Join-Path -Path $env:USERPROFILE -ChildPath ".Azure") -ChildPath "AzPredictorSettings.json"
189+
$setting = @{
190+
"surveyId"=$surveyId;
191+
}
192+
193+
if (Test-Path $azPredictorSettingFilePath) {
194+
try {
195+
$setting = Get-Content $azPredictorSettingFilePath | Out-String | ConvertFrom-Json
196+
$setting | Add-Member -NotePropertyName "surveyId" -NotePropertyValue $surveyId -Force
197+
} catch {
198+
}
199+
}
200+
201+
ConvertTo-Json -InputObject $setting | Out-File -FilePath $azPredictorSettingFilePath -Encoding utf8
202+
} catch {
203+
}
204+
}
205+
206+
$escape = $([char]27)
207+
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
208+
Write-Host "`n"
209+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ----------------------------------------------------------------------------------
2+
#
3+
# Copyright Microsoft Corporation
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.internal
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
# ----------------------------------------------------------------------------------
14+
15+
$targetScript = (Join-Path -Path $PSScriptRoot -ChildPath "InterceptSurvey.ps1")
16+
& $targetScript "Az.Tools.Predictor" 0

0 commit comments

Comments
 (0)