Skip to content

Commit

Permalink
Fixes Azure#881. Opt in/out of telemetry in az configure (Azure#913)
Browse files Browse the repository at this point in the history
  • Loading branch information
BurtBiel authored Sep 17, 2016
1 parent c57095b commit da7a44d
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/azure-cli-core/azure/cli/core/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def getfloat(self, section, option, fallback=_UNSET):
return float(self.get(section, option, fallback))

def getboolean(self, section, option, fallback=_UNSET):
val = self.get(section, option, fallback)
val = str(self.get(section, option, fallback))
if val.lower() not in AzConfig._BOOLEAN_STATES: #pylint: disable=E1101
raise ValueError('Not a boolean: {}'.format(val))
return AzConfig._BOOLEAN_STATES[val.lower()] #pylint: disable=E1101
Expand Down
4 changes: 3 additions & 1 deletion src/azure-cli-core/azure/cli/core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import uuid
import argparse
import traceback
from azure.cli.core.parser import AzCliCommandParser
from azure.cli.core._output import CommandResultItem
import azure.cli.core.extensions
Expand Down Expand Up @@ -101,7 +102,8 @@ def execute(self, unexpanded_argv):
try:
_validate_arguments(expanded_arg)
except: # pylint: disable=bare-except
err = sys.exc_info()[1]
_, err, ex_traceback = sys.exc_info()
log_telemetry('Validation Error', trace=traceback.format_tb(ex_traceback))
getattr(expanded_arg, '_parser', self.parser).error(str(err))

# Consider - we are using any args that start with an underscore (_) as 'private'
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli-core/azure/cli/core/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def _get_subparser(self, path):
return parent_subparser

def error(self, message):
log_telemetry('parse error', message=message)
log_telemetry('parse error')
return super(AzCliCommandParser, self).error(message)

def format_help(self):
Expand Down
67 changes: 47 additions & 20 deletions src/azure-cli-core/azure/cli/core/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,22 @@

from applicationinsights import TelemetryClient
from applicationinsights.exceptions import enable
from azure.cli.core import __version__ as core_version
from azure.cli.core._profile import Profile
from azure.cli.core._util import CLIError
try:
from azure.cli.core import __version__ as core_version
except: #pylint: disable=bare-except
core_version = None
try:
from azure.cli.core._profile import Profile
except: #pylint: disable=bare-except
Profile = {}
try:
from azure.cli.core._util import CLIError
except: #pylint: disable=bare-except
CLIError = Exception
try:
from azure.cli.core._config import az_config
except: #pylint: disable=bare-except
az_config = {}

_DEBUG_TELEMETRY = 'AZURE_CLI_DEBUG_TELEMETRY'
client = {}
Expand All @@ -40,6 +53,7 @@ def init_telemetry():
client.context.application.id = 'Azure CLI'
client.context.application.ver = core_version
client.context.user.id = _get_user_machine_id()
client.context.device.type = 'az'

enable(instrumentation_key)
except Exception as ex: #pylint: disable=broad-except
Expand All @@ -48,18 +62,19 @@ def init_telemetry():
raise ex

def user_agrees_to_telemetry():
# TODO: agreement, needs to take Y/N from the command line
# and needs a "skip" param to not show (for scripts)
return True
return az_config.getboolean('core', 'collect_telemetry', fallback=True)

def log_telemetry(name, log_type='event', **kwargs):
"""
IMPORTANT: do not log events with quotes in the name, properties or measurements;
those events may fail to upload. Also, telemetry events must be verified in
the backend because successful upload does not guarentee success.
"""
if not user_agrees_to_telemetry():
return

try:
name = _remove_quotes(name)
name = _remove_cmd_chars(name)
_sanitize_inputs(kwargs)

source = 'az'
Expand All @@ -73,22 +88,24 @@ def log_telemetry(name, log_type='event', **kwargs):
raise ValueError('Type {} is not supported. Available types: {}'.format(log_type,
types))

profile = Profile()
props = {}
_safe_exec(props, 'time', lambda: str(datetime.datetime.now()))
_safe_exec(props, 'x-ms-client-request-id',
lambda: APPLICATION.session['headers']['x-ms-client-request-id'])
_safe_exec(props, 'command', lambda: APPLICATION.session.get('command', None))
_safe_exec(props, 'version', lambda: core_version)
_safe_exec(props, 'source', lambda: source)
_safe_exec(props, 'installation-id', profile.get_installation_id)
_safe_exec(props, 'python-version', lambda: _make_safe(str(platform.python_version())))
_safe_exec(props, 'installation-id', _get_installation_id)
_safe_exec(props, 'python-version', lambda: _remove_symbols(str(platform.python_version())))
_safe_exec(props, 'shell-type', _get_shell_type)
_safe_exec(props, 'locale', lambda: '{},{}'.format(locale.getdefaultlocale()[0],
locale.getdefaultlocale()[1]))
_safe_exec(props, 'user-machine-id', _get_user_machine_id)
_safe_exec(props, 'user-azure-id', _get_user_azure_id)
_safe_exec(props, 'azure-subscription-id', _get_azure_subscription_id)
_safe_exec(props, 'output-type', lambda: az_config.get('core', 'output',
fallback='unknown'))
_safe_exec(props, 'environment', _get_env_string)

if kwargs:
props.update(**kwargs)
Expand All @@ -111,21 +128,29 @@ def _safe_exec(props, key, fn):
if _debugging():
raise ex

def _get_installation_id():
profile = Profile()
return profile.get_installation_id()

def _get_env_string():
return _remove_cmd_chars(_remove_symbols(str([v for v in os.environ
if v.startswith('AZURE_CLI')])))

def _get_user_machine_id():
return hash(platform.node() + getpass.getuser())

def _get_user_azure_id():
try:
profile = Profile()
return hash(profile.get_current_account_user())
except CLIError:
except CLIError: #pylint: disable=broad-except
pass

def _get_azure_subscription_id():
try:
profile = Profile()
return profile.get_login_credentials()[1]
except CLIError:
except CLIError: #pylint: disable=broad-except
pass

def _get_shell_type():
Expand All @@ -138,29 +163,31 @@ def _get_shell_type():
elif 'WINDIR' in os.environ:
return 'cmd'
else:
return _make_safe(os.environ.get('SHELL'))
return _remove_cmd_chars(_remove_symbols(os.environ.get('SHELL')))

def _sanitize_inputs(d):
for key, value in d.items():
if isinstance(value, str):
d[key] = _remove_quotes(value)
d[key] = _remove_cmd_chars(value)
elif isinstance(value, list):
d[key] = [_remove_quotes(v) for v in value]
d[key] = [_remove_cmd_chars(v) for v in value]
if next((v for v in value if isinstance(v, list) or isinstance(v, dict)),
None) is not None:
raise ValueError('List object too complex, will fail server-side')
elif isinstance(value, dict):
d[key] = {key:_remove_quotes(v) for key, v in value.items()}
d[key] = {key:_remove_cmd_chars(v) for key, v in value.items()}
if next((v for v in value.values() if isinstance(v, list) or isinstance(v, dict)),
None) is not None:
raise ValueError('Dict object too complex, will fail server-side')
else:
d[key] = _remove_cmd_chars(str(value))

def _remove_quotes(s):
def _remove_cmd_chars(s):
if isinstance(s, str):
return s.replace("'", '_').replace('"', '_')
return s.replace("'", '_').replace('"', '_').replace('\r\n', ' ').replace('\n', ' ')
return s

def _make_safe(s):
def _remove_symbols(s):
for c in '$%^&|':
s = s.replace(c, '_')
return s
Expand All @@ -179,7 +206,7 @@ def telemetry_flush():

subprocess.Popen([sys.executable,
os.path.realpath(__file__),
_make_safe(json.dumps(telemetry_records))])
_remove_symbols(json.dumps(telemetry_records))])
except Exception as ex: #pylint: disable=broad-except
# Never fail the command because of telemetry, unless debugging
if _debugging():
Expand Down
4 changes: 4 additions & 0 deletions src/azure-cli/azure/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

import os
import sys
import traceback

from azure.cli.core.application import APPLICATION, Configuration
import azure.cli.core._logging as _logging
from azure.cli.core._session import ACCOUNT, CONFIG, SESSION
from azure.cli.core._output import OutputProducer
from azure.cli.core._util import show_version_info_exit, handle_exception
from azure.cli.core.telemetry import log_telemetry

logger = _logging.get_az_logger(__name__)

Expand Down Expand Up @@ -38,6 +40,8 @@ def main(args, file=sys.stdout): #pylint: disable=redefined-builtin
formatter = OutputProducer.get_formatter(APPLICATION.configuration.output_format)
OutputProducer(formatter=formatter, file=file).out(cmd_result)
except Exception as ex: # pylint: disable=broad-except
_, _, ex_traceback = sys.exc_info()
log_telemetry('Error', trace=traceback.format_tb(ex_traceback))
error_code = handle_exception(ex)
return error_code

Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@
MSG_PROMPT_GLOBAL_OUTPUT,
MSG_PROMPT_WHICH_ENV,
MSG_PROMPT_WHICH_CLOUD,
MSG_PROMPT_LOGIN)
MSG_PROMPT_LOGIN,
MSG_PROMPT_TELEMETRY)
from azure.cli.command_modules.configure._utils import (prompt_y_n,
prompt_choice_list,
get_default_from_config)
import azure.cli.command_modules.configure._help # pylint: disable=unused-import
from azure.cli.core.telemetry import log_telemetry

logger = _logging.get_az_logger(__name__)

answers = {}

def _print_cur_configuration(file_config):
print(MSG_HEADING_CURRENT_CONFIG_INFO)
for section in file_config.sections():
Expand Down Expand Up @@ -67,6 +71,8 @@ def _config_env_public_azure(_):
login_successful = False
while not login_successful:
method_index = prompt_choice_list(MSG_PROMPT_LOGIN, LOGIN_METHOD_LIST)
answers['login_index'] = method_index
answers['login_options'] = str(LOGIN_METHOD_LIST)
profile = Profile()
interactive = False
username = None
Expand Down Expand Up @@ -112,6 +118,8 @@ def _create_or_update_env(env_name=None):
'cloud',
'name',
CLOUD_LIST))
answers['cloud_prompt'] = selected_cloud_index
answers['cloud_options'] = str(CLOUD_LIST)
if CLOUD_LIST[selected_cloud_index]['name'] != 'public-azure':
# TODO support other clouds
print('Support for other clouds is coming soon.\n')
Expand Down Expand Up @@ -145,17 +153,23 @@ def _handle_global_configuration():
# print current config and prompt to allow global config modification
_print_cur_configuration(file_config)
should_modify_global_config = prompt_y_n(MSG_PROMPT_MANAGE_GLOBAL, default='n')
answers['modify_global_prompt'] = should_modify_global_config
if not config_exists or should_modify_global_config:
# no config exists yet so configure global config or user wants to modify global config
output_index = prompt_choice_list(MSG_PROMPT_GLOBAL_OUTPUT, OUTPUT_LIST,
default=get_default_from_config(global_config, \
'core', 'output', OUTPUT_LIST))
answers['output_type_prompt'] = output_index
answers['output_type_options'] = str(OUTPUT_LIST)
allow_telemetry = prompt_y_n(MSG_PROMPT_TELEMETRY, default='y')
answers['telemetry_prompt'] = allow_telemetry
# save the global config
try:
global_config.add_section('core')
except configparser.DuplicateSectionError:
pass
global_config.set('core', 'output', OUTPUT_LIST[output_index]['name'])
global_config.set('core', 'collect_telemetry', 'yes' if allow_telemetry else 'no')
if not os.path.isdir(GLOBAL_CONFIG_DIR):
os.makedirs(GLOBAL_CONFIG_DIR)
with open(GLOBAL_CONFIG_PATH, 'w') as configfile:
Expand All @@ -165,10 +179,12 @@ def _handle_env_configuration():
envs = _get_envs()
if envs:
should_configure_envs = prompt_y_n(MSG_PROMPT_MANAGE_ENVS, default='n')
answers['configure_envs_prompt'] = should_configure_envs
if not should_configure_envs:
return
env_to_configure_index = prompt_choice_list(MSG_PROMPT_WHICH_ENV, envs + \
['Create new environment (not yet supported)'])
answers['env_to_configure_prompt'] = env_to_configure_index
if env_to_configure_index == len(envs):
# The last choice was picked by the user which corresponds to 'create new environment'
_create_or_update_env()
Expand All @@ -185,6 +201,7 @@ def handle_configure():
_handle_global_configuration()
_handle_env_configuration()
print(MSG_CLOSING)
log_telemetry('configure', **answers)
except (EOFError, KeyboardInterrupt):
print()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,17 @@
MSG_PROMPT_WHICH_ENV = '\nWhich environment would you like to configure?'
MSG_PROMPT_WHICH_CLOUD = '\nWhich cloud are you targeting?'
MSG_PROMPT_LOGIN = '\nHow would you like to log in to access your subscriptions?'
MSG_PROMPT_TELEMETRY = '\nMicrosoft Azure CLI would like to collect data about how users use CLI' \
' cmdlets and some problems they encounter. ' \
'Microsoft uses this information to improve our CLI. Participation is voluntary and when you' \
' choose to participate your device ' \
'automatically sends information to Microsoft about how you use Azure CLI. ' \
'\n\nIf you choose to participate, you can stop at any time later by using Azure CLI' \
' as follows: ' \
'\n1. Update az configuration. ' \
'\nTo disable data collection, execute: "az configure" and choose "n" when prompted.' \
'\n\nIf you choose to not participate, you can enable at any time later by using Azure' \
' CLI as follows: ' \
'\n1. Update az configuration. ' \
'\nTo enable data collection, execute: "az configure" and choose "y" when prompted.' \
'\n\nSelect y to enable data collection'

0 comments on commit da7a44d

Please sign in to comment.