1# Copyright 2024 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Report usage to Google Analytics.""" 15 16import logging 17import os 18from pathlib import Path 19from typing import Any, Generator 20 21import requests # pylint: disable=unused-import 22 23import pw_config_loader.json_config_loader_mixin 24 25_LOG: logging.Logger = logging.getLogger(__name__) 26 27_PW_PROJECT_ROOT = Path(os.environ['PW_PROJECT_ROOT']) 28 29CONFIG_SECTION_TITLE = ('pw', 'pw_cli_analytics') 30DEFAULT_PROJECT_FILE = _PW_PROJECT_ROOT / 'pigweed.json' 31DEFAULT_PROJECT_USER_FILE = _PW_PROJECT_ROOT / '.pw_cli_analytics.user.json' 32DEFAULT_USER_FILE = Path(os.path.expanduser('~/.pw_cli_analytics.json')) 33ENVIRONMENT_VAR = 'PW_CLI_ANALYTICS_CONFIG_FILE' 34 35_DEFAULT_CONFIG = { 36 'api_secret': 'm7q0D-9ETtKrGqHAcQK2kQ', 37 'measurement_id': 'G-NY45VS0X1F', 38 'debug_url': 'https://www.google-analytics.com/debug/mp/collect', 39 'prod_url': 'https://www.google-analytics.com/mp/collect', 40 'report_command_line': False, 41 'report_project_name': False, 42 'report_remote_url': False, 43 'report_subcommand_name': 'limited', 44 'uuid': None, 45 'enabled': None, 46} 47 48 49class AnalyticsPrefs( 50 pw_config_loader.json_config_loader_mixin.JsonConfigLoaderMixin 51): 52 """Preferences for reporting analytics data.""" 53 54 def __init__( 55 self, 56 *, 57 project_file: Path | None = DEFAULT_PROJECT_FILE, 58 project_user_file: Path | None = DEFAULT_PROJECT_USER_FILE, 59 user_file: Path | None = DEFAULT_USER_FILE, 60 **kwargs, 61 ) -> None: 62 super().__init__(**kwargs) 63 64 self.config_init( 65 config_section_title=CONFIG_SECTION_TITLE, 66 project_file=project_file, 67 project_user_file=project_user_file, 68 user_file=user_file, 69 default_config=_DEFAULT_CONFIG, 70 environment_var=ENVIRONMENT_VAR, 71 skip_files_without_sections=True, 72 ) 73 74 def __iter__(self): 75 return iter(_DEFAULT_CONFIG.keys()) 76 77 def __getitem__(self, key): 78 return self._config[key] 79 80 def handle_overloaded_value( # pylint: disable=no-self-use 81 self, 82 key: str, 83 stage: pw_config_loader.json_config_loader_mixin.Stage, 84 original_value: Any, 85 overriding_value: Any, 86 ) -> Any: 87 """Overload this in subclasses to handle of overloaded values.""" 88 Stage = pw_config_loader.json_config_loader_mixin.Stage 89 90 # This is a user-specific value. Don't accept it from anywhere but the 91 # user file. 92 if key == 'uuid': 93 if stage == Stage.USER_FILE: 94 return overriding_value 95 return original_value 96 97 # If any level says that data collection should be disabled, disable it. 98 # The default value is None, not False, and the default in generated 99 # user files is True. But if the project says enabled is False we'll 100 # keep that, regardless of what any other files say. 101 if key == 'enabled': 102 if original_value is False: 103 return original_value 104 return overriding_value 105 106 # URLs can by changed by any config. 107 if key in ('debug_url', 'prod_url'): 108 return overriding_value 109 110 # What's left is the details of what to report. In general, these should 111 # only be set by the project file and the user/project file. 112 if stage in (Stage.PROJECT_FILE, Stage.USER_PROJECT_FILE): 113 return overriding_value 114 115 # Only honor user file settings about what to report when they disable 116 # things. Don't honor them when they enable things. 117 if stage == Stage.USER_FILE: 118 if overriding_value in (False, 'never'): 119 return overriding_value 120 if original_value == 'always' and overriding_value == 'limited': 121 return overriding_value 122 123 return original_value 124 125 def items(self) -> Generator[tuple[str, Any], None, None]: 126 """Yield all the key/value pairs in the config.""" 127 for key in _DEFAULT_CONFIG.keys(): 128 yield (key, self._config[key]) 129