Skip to content

Commit

Permalink
[Feedback] az survey: New command for CLI survey (Azure#23460)
Browse files Browse the repository at this point in the history
* color test

* az survey

* morden terminal

* style

* remove unused enum

* frequency

* installationId

* style

* fix test

* unnecessary __init__

* pylint

* remove vt code

* Update EXPERIENCE_PERIOD_IN_DAYS

* add comments

* add `tty` check

* pylint

* remove old survey

* unused import

* read PSH InstallationId
  • Loading branch information
evelyn-ys authored Aug 31, 2022
1 parent b8d36d8 commit 8d8d717
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 38 deletions.
8 changes: 0 additions & 8 deletions src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,11 @@ def get_cli_version(self):

def show_version(self):
from azure.cli.core.util import get_az_version_string, show_updates
from azure.cli.core.commands.constants import SURVEY_PROMPT_STYLED, UX_SURVEY_PROMPT_STYLED
from azure.cli.core.style import print_styled_text

ver_string, updates_available_components = get_az_version_string()
print(ver_string)
show_updates(updates_available_components)

show_link = self.config.getboolean('output', 'show_survey_link', True)
if show_link:
print_styled_text()
print_styled_text(SURVEY_PROMPT_STYLED)
print_styled_text(UX_SURVEY_PROMPT_STYLED)

def exception_handler(self, ex): # pylint: disable=no-self-use
from azure.cli.core.util import handle_exception
return handle_exception(ex)
Expand Down
7 changes: 0 additions & 7 deletions src/azure-cli-core/azure/cli/core/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,6 @@ def show_help(self, cli_name, nouns, parser, is_group):
self._print_detailed_help(cli_name, help_file)
from azure.cli.core.util import show_updates_available
show_updates_available(new_line_after=True)
show_link = self.cli_ctx.config.getboolean('output', 'show_survey_link', True)
from azure.cli.core.commands.constants import (SURVEY_PROMPT_STYLED, UX_SURVEY_PROMPT_STYLED)
from azure.cli.core.style import print_styled_text
if show_link:
print_styled_text(SURVEY_PROMPT_STYLED)
if not nouns:
print_styled_text(UX_SURVEY_PROMPT_STYLED)

def get_examples(self, command, parser, is_group):
"""Get examples of a certain command from the help file.
Expand Down
15 changes: 13 additions & 2 deletions src/azure-cli-core/azure/cli/core/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,8 +675,19 @@ def get_sp_auth_info(self, subscription_id=None, name=None, password=None, cert_
def get_installation_id(self):
installation_id = self._storage.get(_INSTALLATION_ID)
if not installation_id:
import uuid
installation_id = str(uuid.uuid1())
try:
# We share the same installationId with Azure Powershell. So try to load installationId from PSH file
# Contact: DEV@Nanxiang Liu, PM@Damien Caro
shared_installation_id_file = os.path.join(self.cli_ctx.config.config_dir,
'AzureRmContextSettings.json')
with open(shared_installation_id_file, 'r', encoding='utf-8-sig') as f:
import json
content = json.load(f)
installation_id = content['Settings']['InstallationId']
except Exception as ex: # pylint: disable=broad-except
logger.debug('Failed to load installationId from AzureRmSurvey.json. %s', str(ex))
import uuid
installation_id = str(uuid.uuid1())
self._storage[_INSTALLATION_ID] = installation_id
return installation_id

Expand Down
13 changes: 0 additions & 13 deletions src/azure-cli-core/azure/cli/core/commands/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.style import Style
from knack.parser import ARGPARSE_SUPPORTED_KWARGS


Expand Down Expand Up @@ -31,15 +30,3 @@
DEFAULT_QUERY_TIME_RANGE = 3600000

BLOCKED_MODS = ['context', 'shell', 'documentdb', 'component']

SURVEY_PROMPT = 'Please let us know how we are doing: https://aka.ms/azureclihats'
SURVEY_PROMPT_STYLED = [
(Style.PRIMARY, 'Please let us know how we are doing: '),
(Style.HYPERLINK, 'https://aka.ms/azureclihats'),
]

UX_SURVEY_PROMPT = 'and let us know if you\'re interested in trying out our newest features: https://aka.ms/CLIUXstudy'
UX_SURVEY_PROMPT_STYLED = [
(Style.PRIMARY, 'and let us know if you\'re interested in trying out our newest features: '),
(Style.HYPERLINK, 'https://aka.ms/CLIUXstudy'),
]
101 changes: 101 additions & 0 deletions src/azure-cli-core/azure/cli/core/intercept_survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import sys
import json
from datetime import datetime, timedelta
from azure.cli.core import __version__ as core_version
from azure.cli.core._config import GLOBAL_CONFIG_DIR
from azure.cli.core._profile import Profile
from azure.cli.core.style import print_styled_text
from knack.log import get_logger

logger = get_logger(__name__)

SURVEY_NOTE_NAME = 'az_survey.json'
GLOBAL_SURVEY_NOTE_PATH = os.path.join(GLOBAL_CONFIG_DIR, SURVEY_NOTE_NAME)

EXPERIENCE_PERIOD_IN_DAYS = 3
PROMPT_INTERVAL_IN_DAYS = 180

# VT code for text formatting:
# https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#extended-colors
SURVEY_STYLE = '\x1b[0;38;2;255;255;255;48;2;0;120;212m' # Default & Foreground #FFFFFF & Background #0078D4

# VT code for text modification:
# https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#text-modification
NEW_LINE = '\x1b[1L'
ERASE_IN_LINE = '\x1b[0K'


_SURVEY_URL = "https://go.microsoft.com/fwlink/?linkid=2201856&ID={installation_id}&v={version}&d={day}"
_SURVEY_LEARN_MORE_URL = "https://go.microsoft.com/fwlink/?linkid=2203309"


def should_prompt(cli):
# should not prompt for automation
if not sys.stderr.isatty():
return False

# should not prompt if cx disables with config
if not cli.config.getboolean('core', 'survey_message', True):
return False

# should not prompt if cx is running survey command already
if sys.argv[1] == 'survey':
return False

if not os.path.isfile(GLOBAL_SURVEY_NOTE_PATH):
import uuid

# If the survey note file doesn't exist, then it should be the first time for cx to run CLI
# We should let cx try CLI for some days(EXPERIENCE_PERIOD_IN_DAYS) and then prompt the survey message
# We don't want to get the survey feedback on the same day, so we evenly distribute cx over 128 days
# using their installationId
installation_id = Profile(cli_ctx=cli).get_installation_id()
prompt_period = EXPERIENCE_PERIOD_IN_DAYS + (uuid.UUID(installation_id).int & 127)
next_prompt_time = datetime.utcnow() + timedelta(days=prompt_period)
survey_note = {
'last_prompt_time': '',
'next_prompt_time': next_prompt_time.strftime('%Y-%m-%dT%H:%M:%S')
}
with open(GLOBAL_SURVEY_NOTE_PATH, 'w') as f:
json.dump(survey_note, f)
return False

with open(GLOBAL_SURVEY_NOTE_PATH, 'r', encoding='utf-8-sig') as f:
survey_note = json.load(f)
next_prompt_time = datetime.strptime(survey_note['next_prompt_time'], '%Y-%m-%dT%H:%M:%S')
if datetime.utcnow() < next_prompt_time:
return False

return True


def prompt_survey_message(cli):
if not should_prompt(cli):
return

# prompt message
installation_id = Profile(cli_ctx=cli).get_installation_id()
survey_link = _SURVEY_URL.format(installation_id=installation_id, version=core_version, day=0)
print_styled_text((SURVEY_STYLE, NEW_LINE))
print_styled_text([
(SURVEY_STYLE, f"[Survey] Help us improve Azure CLI by sharing your experience. "
f"This survey should take about 5 minutes. Open {survey_link} or run 'az survey' to "
f"open in browser. Learn more at {_SURVEY_LEARN_MORE_URL}"),
(SURVEY_STYLE, ERASE_IN_LINE)
])
print_styled_text((SURVEY_STYLE, NEW_LINE))

# log prompt time
next_prompt_time = datetime.utcnow() + timedelta(days=PROMPT_INTERVAL_IN_DAYS)
survey_note = {
'last_prompt_time': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S'),
'next_prompt_time': next_prompt_time.strftime('%Y-%m-%dT%H:%M:%S')
}
with open(GLOBAL_SURVEY_NOTE_PATH, 'w') as f:
json.dump(survey_note, f)
8 changes: 6 additions & 2 deletions src/azure-cli-core/azure/cli/core/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,12 @@ def format_styled_text(styled_text, theme=None):
try:
escape_seq = theme[style]
except KeyError:
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid style. Only use pre-defined style in Style enum.")
if style.startswith('\x1b['):
escape_seq = style
else:
from azure.cli.core.azclierror import CLIInternalError
raise CLIInternalError("Invalid style. Please use pre-defined style in Style enum "
"or give a valid ANSI code.")
# Replace blue in powershell.exe
if is_legacy_powershell and escape_seq in POWERSHELL_COLOR_REPLACEMENT:
escape_seq = POWERSHELL_COLOR_REPLACEMENT[escape_seq]
Expand Down
2 changes: 2 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_format_styled_text(self):
(Style.ERROR, "Bright Red: Error message indicator\n"),
(Style.SUCCESS, "Bright Green: Success message indicator\n"),
(Style.WARNING, "Bright Yellow: Warning message indicator\n"),
('\x1b[4m', "Underline text\n"),
]
formatted = format_styled_text(styled_text)
excepted = """\x1b[0mWhite: Primary text color
Expand All @@ -41,6 +42,7 @@ def test_format_styled_text(self):
\x1b[91mBright Red: Error message indicator
\x1b[92mBright Green: Success message indicator
\x1b[93mBright Yellow: Warning message indicator
\x1b[4mUnderline text
\x1b[0m"""
self.assertEqual(formatted, excepted)

Expand Down
6 changes: 6 additions & 0 deletions src/azure-cli/azure/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from azure.cli.core import telemetry
from azure.cli.core import get_default_cli
from azure.cli.core.intercept_survey import prompt_survey_message
from knack.completion import ARGCOMPLETE_ENV_NAME
from knack.log import get_logger

Expand Down Expand Up @@ -118,6 +119,11 @@ def cli_main(cli, args):
logger.warning("Auto upgrade failed. %s", str(ex))
telemetry.set_exception(ex, fault_type='auto-upgrade-failed')

try:
prompt_survey_message(az_cli)
except Exception as ex: # pylint: disable=broad-except
logger.debug("Intercept survey prompt failed. %s", str(ex))

telemetry.set_init_time_elapsed("{:.6f}".format(init_finish_time - start_time))
telemetry.set_invoke_time_elapsed("{:.6f}".format(invoke_finish_time - init_finish_time))
telemetry.conclude()
4 changes: 4 additions & 0 deletions src/azure-cli/azure/cli/command_modules/feedback/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ def load_command_table(self, args):

with self.command_group('', custom_feedback) as g:
g.command('feedback', 'handle_feedback')
g.command('survey', 'handle_survey')

return self.command_table

def load_arguments(self, command):
with self.argument_context('feedback') as c:
c.ignore('_subscription') # hide global subscription param

with self.argument_context('survey') as c:
c.ignore('_subscription') # hide global subscription param


COMMAND_LOADER_CLS = FeedbackCommandsLoader
8 changes: 8 additions & 0 deletions src/azure-cli/azure/cli/command_modules/feedback/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@
web browser to open GitHub issue creation page with the body auto-generated and pre-filled.
You will have a chance to edit the issue body before submitting it.
"""

helps['survey'] = """
type: command
short-summary: Take Azure CLI survey.
long-summary: >-
Help us improve Azure CLI by sharing your experience. This survey should take about 3 minutes.
Learn more at https://go.microsoft.com/fwlink/?linkid=2203309.
"""
30 changes: 27 additions & 3 deletions src/azure-cli/azure/cli/command_modules/feedback/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from urllib.parse import urlencode

from azure.cli.core.azlogging import _UNKNOWN_COMMAND, _CMD_LOG_LINE_PREFIX
from azure.cli.core.commands.constants import SURVEY_PROMPT
from azure.cli.core.extension._resolve import resolve_project_url_from_index, NoExtensionCandidatesError
from azure.cli.core.intercept_survey import _SURVEY_URL
from azure.cli.core.util import get_az_version_string, open_page_in_browser, can_launch_browser, in_cloud_console
from knack.log import get_logger
from knack.prompting import prompt, NoTTYException
Expand Down Expand Up @@ -43,8 +43,7 @@
'\nWe appreciate your feedback!\n\n' \
'For more information on getting started, visit: {}\n' \
'If you have questions, visit our Stack Overflow page: {}\n'\
'{}\n'\
.format(_GET_STARTED_URL, _QUESTIONS_URL, SURVEY_PROMPT)
.format(_GET_STARTED_URL, _QUESTIONS_URL)

_MSG_CMD_ISSUE = "\nEnter the number of the command you would like to create an issue for. Enter q to quit: "

Expand Down Expand Up @@ -592,3 +591,28 @@ def handle_feedback(cmd):
raise CLIError('This command is interactive, however no tty is available.')
except (EOFError, KeyboardInterrupt):
print()


def handle_survey(cmd):
import json
from azure.cli.core import __version__ as core_version
from azure.cli.core._profile import Profile
from azure.cli.core.intercept_survey import GLOBAL_SURVEY_NOTE_PATH

use_duration = None
if os.path.isfile(GLOBAL_SURVEY_NOTE_PATH):
with open(GLOBAL_SURVEY_NOTE_PATH, 'r') as f:
survey_note = json.load(f)
if survey_note['last_prompt_time']:
last_prompt_time = datetime.datetime.strptime(survey_note['last_prompt_time'], '%Y-%m-%dT%H:%M:%S')
use_duration = datetime.datetime.utcnow() - last_prompt_time

url = _SURVEY_URL.format(installation_id=Profile(cli_ctx=cmd.cli_ctx).get_installation_id(),
version=core_version,
day=use_duration.days if use_duration else -1)
if can_launch_browser() and not in_cloud_console():
open_page_in_browser(url)
print("A new tab of {} has been launched in your browser, thanks for taking the survey!".format(url))
else:
print("There isn't an available browser to launch the survey. You can copy and paste the url"
" below in a browser to submit.\n\n{}\n\n".format(url))
4 changes: 1 addition & 3 deletions src/azure-cli/azure/cli/command_modules/find/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from azure.cli.core import telemetry as telemetry_core
from azure.cli.core import __version__ as core_version
from azure.cli.core.commands.constants import SURVEY_PROMPT
from packaging.version import parse
from knack.log import get_logger
logger = get_logger(__name__)
Expand Down Expand Up @@ -59,8 +58,7 @@ def process_query(cli_term):
print(style_message("More commands and examples are available in the latest version of the CLI. "
"Please update for the best experience.\n"))
from azure.cli.core.util import show_updates_available
show_updates_available(new_line_after=True)
print(SURVEY_PROMPT)
show_updates_available()


def get_generated_examples(cli_term):
Expand Down
5 changes: 5 additions & 0 deletions src/azure-cli/service_name.json
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,11 @@
"AzureServiceName": "Azure Storage",
"URL": "https://docs.microsoft.com/azure/storage/"
},
{
"Command": "az survey",
"AzureServiceName": "Azure CLI",
"URL": "https://go.microsoft.com/fwlink/?linkid=2203309"
},
{
"Command": "az synapse",
"AzureServiceName": "Azure Synapse Analytics",
Expand Down

0 comments on commit 8d8d717

Please sign in to comment.