Skip to content

Commit

Permalink
Add prompt for usage and survey (#13965)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
kceiw and dcaro authored Feb 19, 2021
1 parent e38e1a3 commit 319b8e6
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ For more information on Az Predictor, please visit the following: https://aka.ms

<ItemGroup>
<None Include="Az.Tools.Predictor.psd1" CopyToOutputDirectory="PreserveNewest" />
<None Include="InterceptSurvey.ps1" CopyToOutputDirectory="PreserveNewest" />
<None Include="PromptSurvey.ps1" CopyToOutputDirectory="PreserveNewest" />
<None Include="AzPredictorSettings.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="command_param_to_resource_map.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="command_param_to_resource_map.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Expand All @@ -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'
Expand Down
23 changes: 22 additions & 1 deletion tools/Az.Tools.Predictor/Az.Tools.Predictor/AzContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace Microsoft.Azure.PowerShell.Tools.AzPredictor
/// </summary>
internal sealed class AzContext : IAzContext
{
private const string InternalUserSuffix = "@microsoft.com";
private static readonly Version DefaultVersion = new Version("0.0.0.0");

/// <inheritdoc/>
Expand Down Expand Up @@ -100,13 +101,33 @@ public Version ModuleVersion
}
}

/// <inheritdoc/>
public bool IsInternal { get; internal set; }

/// <summary>
/// The survey session id appended to the survey.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal string SurveyId { get; set; }

/// <inheritdoc/>
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; }

/// <summary>
/// Gets the user account id if the user logs in, otherwise empty string.
/// </summary>
Expand Down
8 changes: 7 additions & 1 deletion tools/Az.Tools.Predictor/Az.Tools.Predictor/AzPredictor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions tools/Az.Tools.Predictor/Az.Tools.Predictor/IAzContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ internal interface IAzContext
/// </summary>
public Version AzVersion { get; }

/// <summary>
/// Gets whether the user is an internal user.
/// </summary>
public bool IsInternal { get; }

/// <summary>
/// Updates the Az context.
/// </summary>
Expand Down
209 changes: 209 additions & 0 deletions tools/Az.Tools.Predictor/Az.Tools.Predictor/InterceptSurvey.ps1
Original file line number Diff line number Diff line change
@@ -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"
}
16 changes: 16 additions & 0 deletions tools/Az.Tools.Predictor/Az.Tools.Predictor/PromptSurvey.ps1
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 319b8e6

Please sign in to comment.