From 2dab97d13e7d70ff771b9e3239341ac70c2fdc2b Mon Sep 17 00:00:00 2001 From: Anastasiia Pnevskaia Date: Mon, 13 Jan 2025 14:54:38 +0100 Subject: [PATCH 1/6] Add usage count info. --- src/backend/backend.py | 6 +++ src/backend/backend_ga4.py | 5 ++ src/main.py | 26 +++++++++- src/utils/stats_processor.py | 81 +++++++++++++++++++++++++++++++ src/utils/stats_processor_test.py | 65 +++++++++++++++++++++++++ 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/utils/stats_processor.py create mode 100644 src/utils/stats_processor_test.py diff --git a/src/backend/backend.py b/src/backend/backend.py index ec6088f..d60113b 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -100,3 +100,9 @@ def cid_file_initialized(self): :return: True if client ID file is initialized, otherwise False. """ + + @abc.abstractmethod + def set_stats(self, data: dict): + """ + Pass additional statistic, which will be added to telemetry messages + """ \ No newline at end of file diff --git a/src/backend/backend_ga4.py b/src/backend/backend_ga4.py index e7bf474..6c138a9 100644 --- a/src/backend/backend_ga4.py +++ b/src/backend/backend_ga4.py @@ -40,6 +40,7 @@ def __init__(self, tid: str = None, app_name: str = None, app_version: str = Non 'app_name': self.app_name, 'app_version': self.app_version, } + self.stats = {} def send(self, message: dict): if message is None: @@ -90,6 +91,7 @@ def build_event_message(self, event_category: str, event_action: str, event_labe "event_count": event_value, "session_id": self.session_id, **default_args, + **self.stats } } ] @@ -124,6 +126,9 @@ def remove_cid_file(self): remove_cid_file(self.cid_filename) remove_cid_file(self.old_cid_filename) + def set_stats(self, data: dict): + self.stats = data + def is_valid_cid(cid: str): try: diff --git a/src/main.py b/src/main.py index c52e6cb..36d2fea 100644 --- a/src/main.py +++ b/src/main.py @@ -7,8 +7,9 @@ from enum import Enum from .backend.backend import BackendRegistry -from .utils.opt_in_checker import OptInChecker, ConsentCheckResult, DialogResult from .utils.sender import TelemetrySender +from .utils.opt_in_checker import OptInChecker, ConsentCheckResult, DialogResult +from .utils.stats_processor import StatsProcessor class OptInStatus(Enum): @@ -76,6 +77,9 @@ def init(self, app_name: str = None, app_version: str = None, tid: str = None, if self.consent and not self.backend.cid_file_initialized(): self.backend.generate_new_cid_file() + if self.consent: + self.backend.set_stats(self.get_stats()) + if not enable_opt_in_dialog and self.consent: # Try to create directory for client ID if it does not exist if not opt_in_checker.create_or_check_consent_dir(): @@ -264,6 +268,8 @@ def _update_opt_in_status(tid: str, new_opt_in_status: bool): if prev_status != OptInStatus.DECLINED: telemetry.send_opt_in_event(OptInStatus.DECLINED, prev_status, force_send=True) telemetry.backend.remove_cid_file() + from .utils.stats_processor import StatsProcessor + StatsProcessor().remove_stats_file() print("You have successfully opted out to send the telemetry data.") def send_opt_in_event(self, new_state: OptInStatus, prev_state: OptInStatus = OptInStatus.UNDEFINED, @@ -283,6 +289,24 @@ def send_opt_in_event(self, new_state: OptInStatus, prev_state: OptInStatus = Op label = "{{prev_state:{}, new_state: {}}}".format(prev_state.value, new_state.value) self.send_event("opt_in", new_state.value, label, force_send=force_send) + def get_stats(self): + stats = StatsProcessor() + file_exists, data = stats.get_stats() + if not file_exists: + created = stats.create_new_stats_file() + data = {} + if not created: + return None + if "usage_count" in data: + usage_count = data["usage_count"] + if usage_count < sys.maxsize: + usage_count += 1 + else: + usage_count = 1 + data["usage_count"] = usage_count + stats.update_stats(data) + return data + @staticmethod def opt_in(tid: str): """ diff --git a/src/utils/stats_processor.py b/src/utils/stats_processor.py new file mode 100644 index 0000000..a5e86ee --- /dev/null +++ b/src/utils/stats_processor.py @@ -0,0 +1,81 @@ +# Copyright (C) 2018-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from .opt_in_checker import OptInChecker +import logging as log +import os +import json + + +class StatsProcessor: + def __init__(self): + self.opt_in_checker = OptInChecker() + + def stats_file(self): + """ + Returns the statistics file path. + """ + return os.path.join(self.opt_in_checker.consent_file_base_dir(), self.opt_in_checker.consent_file_subdirectory(), "stats") + + def create_new_stats_file(self): + """ + Creates a new statistics file. + :return: True if the file is created successfully, otherwise False + """ + if not self.opt_in_checker.create_or_check_consent_dir(): + return False + try: + open(self.stats_file(), 'w').close() + except Exception: + return False + return True + + def update_stats(self, stats: dict): + """ + Updates the statistics in the statistics file. + :param stats: the dictionary with statistics. + :return: False if the statistics file is not writable, otherwise True + """ + if self.opt_in_checker.consent_file_base_dir() is None or self.opt_in_checker.consent_file_subdirectory() is None: + return False + if not os.path.exists(self.stats_file()): + if not self.create_new_stats_file(): + return False + if not os.access(self.stats_file(), os.W_OK): + log.warning("Failed to usage statistics. " + "Please allow write access to the following file: {}".format(self.stats_file())) + return False + try: + str_data = json.dumps(stats, indent=4) + with open(self.stats_file(), 'w') as file: + file.write(str_data) + except Exception: + return False + return True + + def get_stats(self): + """ + Gets information from statistics file. + :return: the tuple, where the first element is True if the file is read successfully, otherwise False + and the second element is the content of the statistics file. + """ + if not os.access(self.stats_file(), os.R_OK): + return False, {} + try: + with open(self.stats_file(), 'r') as file: + data = json.load(file) + except Exception: + return False, {} + return True, data + + def remove_stats_file(self): + """ + Removes statistics file. + :return: None + """ + stats_file = self.stats_file() + if os.path.exists(stats_file): + if not os.access(stats_file, os.W_OK): + log.warning("Failed to remove statistics file {}.".format(stats_file)) + return + os.remove(stats_file) diff --git a/src/utils/stats_processor_test.py b/src/utils/stats_processor_test.py new file mode 100644 index 0000000..22bdd2f --- /dev/null +++ b/src/utils/stats_processor_test.py @@ -0,0 +1,65 @@ +# Copyright (C) 2018-2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import os +import unittest +from tempfile import TemporaryDirectory +from unittest.mock import MagicMock, patch + +from .stats_processor import StatsProcessor +from .opt_in_checker import OptInChecker + + +class StatsProcessorTest(unittest.TestCase): + test_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_stats') + test_subdir = 'test_stats_subdir' + stats_processor = StatsProcessor() + + def init_stats_processor(self, test_directory): + self.stats_processor.consent_file_base_dir = MagicMock(return_value=test_directory) + self.stats_processor.consent_file_subdirectory = MagicMock(return_value=self.test_subdir) + + def test_stats_usage(self): + with TemporaryDirectory(prefix=self.test_directory) as test_dir: + with patch.object(OptInChecker, 'consent_file_base_dir', return_value=test_dir): + with patch.object(OptInChecker, 'consent_file_subdirectory', return_value=self.test_subdir): + if not os.path.exists(test_dir): + os.mkdir(test_dir) + stats_filename = os.path.join(test_dir, self.test_subdir, 'stats') + test_data1 = {"value1": 12, "value2": 7, "value3": 8} + + # Test first creation of statistics file + self.stats_processor.update_stats(test_data1) + assert os.path.exists(stats_filename) + with open(stats_filename, 'r') as file: + assert file.readlines() == ['{\n', ' "value1": 12,\n', ' "value2": 7,\n', ' "value3": 8\n', '}'] + + status, res = self.stats_processor.get_stats() + assert status + assert res == test_data1 + + # Test updating of statistics file + test_data2 = {"value1": 15, "a": "abs"} + self.stats_processor.update_stats(test_data2) + assert os.path.exists(stats_filename) + with open(stats_filename, 'r') as file: + assert file.readlines() == ['{\n', ' "value1": 15,\n', ' "a": "abs"\n', '}'] + + status, res = self.stats_processor.get_stats() + assert status + assert res == test_data2 + + # Test removing of statistics file + self.stats_processor.remove_stats_file() + assert not os.path.exists(stats_filename) + + status, res = self.stats_processor.get_stats() + assert not status + assert res == {} + + # Test attempt to read incorrect statistics file + with open(stats_filename, 'w') as file: + file.write("{ abc") + status, res = self.stats_processor.get_stats() + assert not status + assert res == {} From 32cbd3d462b6b28a0130f5be237ba34bfcac4a94 Mon Sep 17 00:00:00 2001 From: Anastasiia Pnevskaia Date: Mon, 13 Jan 2025 14:59:44 +0100 Subject: [PATCH 2/6] Minor correction. --- src/utils/stats_processor.py | 2 +- src/utils/stats_processor_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/stats_processor.py b/src/utils/stats_processor.py index a5e86ee..42e33e4 100644 --- a/src/utils/stats_processor.py +++ b/src/utils/stats_processor.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2024 Intel Corporation +# Copyright (C) 2018-2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 from .opt_in_checker import OptInChecker diff --git a/src/utils/stats_processor_test.py b/src/utils/stats_processor_test.py index 22bdd2f..e321d0e 100644 --- a/src/utils/stats_processor_test.py +++ b/src/utils/stats_processor_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2024 Intel Corporation +# Copyright (C) 2018-2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 import os From 7f4b61627590c9284740a463b94f2809ba485e0e Mon Sep 17 00:00:00 2001 From: Anastasiia Pnevskaia Date: Mon, 13 Jan 2025 15:08:36 +0100 Subject: [PATCH 3/6] Tests fixed. --- src/backend/backend_ga.py | 3 +++ src/main_test.py | 3 ++- src/utils/stats_processor_test.py | 26 +++++++++++++------------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/backend/backend_ga.py b/src/backend/backend_ga.py index 7171c09..9bbec94 100644 --- a/src/backend/backend_ga.py +++ b/src/backend/backend_ga.py @@ -115,6 +115,9 @@ def generate_new_cid_file(self): def cid_file_initialized(self): return self.cid is not None + def set_stats(self, data: dict): + pass + def is_valid_cid(cid: str): try: diff --git a/src/main_test.py b/src/main_test.py index 29d79de..f599f87 100644 --- a/src/main_test.py +++ b/src/main_test.py @@ -72,7 +72,8 @@ def make_message(self, client_id, app_name, app_version, category, action, label 'event_count': value, 'session_id': session_id, 'app_name': app_name, - 'app_version': app_version} + 'app_version': app_version, + 'usage_count': 1} } ]} diff --git a/src/utils/stats_processor_test.py b/src/utils/stats_processor_test.py index e321d0e..5b8aaf3 100644 --- a/src/utils/stats_processor_test.py +++ b/src/utils/stats_processor_test.py @@ -30,36 +30,36 @@ def test_stats_usage(self): # Test first creation of statistics file self.stats_processor.update_stats(test_data1) - assert os.path.exists(stats_filename) + self.assertTrue(os.path.exists(stats_filename)) with open(stats_filename, 'r') as file: - assert file.readlines() == ['{\n', ' "value1": 12,\n', ' "value2": 7,\n', ' "value3": 8\n', '}'] + self.assertTrue(file.readlines() == ['{\n', ' "value1": 12,\n', ' "value2": 7,\n', ' "value3": 8\n', '}']) status, res = self.stats_processor.get_stats() - assert status - assert res == test_data1 + self.assertTrue(status) + self.assertTrue(res == test_data1) # Test updating of statistics file test_data2 = {"value1": 15, "a": "abs"} self.stats_processor.update_stats(test_data2) - assert os.path.exists(stats_filename) + self.assertTrue(os.path.exists(stats_filename)) with open(stats_filename, 'r') as file: - assert file.readlines() == ['{\n', ' "value1": 15,\n', ' "a": "abs"\n', '}'] + self.assertTrue(file.readlines() == ['{\n', ' "value1": 15,\n', ' "a": "abs"\n', '}']) status, res = self.stats_processor.get_stats() - assert status - assert res == test_data2 + self.assertTrue(status) + self.assertTrue(res == test_data2) # Test removing of statistics file self.stats_processor.remove_stats_file() - assert not os.path.exists(stats_filename) + self.assertFalse(os.path.exists(stats_filename)) status, res = self.stats_processor.get_stats() - assert not status - assert res == {} + self.assertFalse(status) + self.assertTrue(res == {}) # Test attempt to read incorrect statistics file with open(stats_filename, 'w') as file: file.write("{ abc") status, res = self.stats_processor.get_stats() - assert not status - assert res == {} + self.assertFalse(status) + self.assertTrue(res == {}) From bea8fec4ec915f649d7a83e7d9493f02f0cae23d Mon Sep 17 00:00:00 2001 From: Anastasiia Pnevskaia Date: Wed, 15 Jan 2025 17:26:26 +0100 Subject: [PATCH 4/6] Update src/backend/backend.py Co-authored-by: Roman Kazantsev --- src/backend/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/backend.py b/src/backend/backend.py index d60113b..8ace8b7 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -104,5 +104,5 @@ def cid_file_initialized(self): @abc.abstractmethod def set_stats(self, data: dict): """ - Pass additional statistic, which will be added to telemetry messages + Pass additional statistics, which will be added to telemetry messages """ \ No newline at end of file From fc43cb6903e5a599567c2e1e3c5e348291e7bd0e Mon Sep 17 00:00:00 2001 From: Anastasiia Pnevskaia Date: Thu, 16 Jan 2025 11:50:48 +0100 Subject: [PATCH 5/6] Added comment. --- src/utils/stats_processor.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/utils/stats_processor.py b/src/utils/stats_processor.py index 42e33e4..395cea9 100644 --- a/src/utils/stats_processor.py +++ b/src/utils/stats_processor.py @@ -8,6 +8,23 @@ class StatsProcessor: + """ + The class is used for storing of additional statistics, that need be stored on the local system. + For example, number of usages of OpenVino. + The class is used by main Telemetry class, for example: + + stats_processor = StatsProcessor() + created = stats_processor.create_new_stats_file() + + # store statistics + updated = stats_processor.update_stats({"usage_count": 1}) + + # read statistics + read_status, stats = stats_processor.get_stats() + + # remove statistics file + stats_processor.remove_stats_file() + """ def __init__(self): self.opt_in_checker = OptInChecker() From 4ccd5e0f454dda6b0501b47246c428a907d69370 Mon Sep 17 00:00:00 2001 From: Anastasiia Pnevskaia Date: Thu, 16 Jan 2025 12:56:44 +0100 Subject: [PATCH 6/6] Minor correction. --- src/utils/stats_processor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/stats_processor.py b/src/utils/stats_processor.py index 395cea9..89af156 100644 --- a/src/utils/stats_processor.py +++ b/src/utils/stats_processor.py @@ -59,8 +59,6 @@ def update_stats(self, stats: dict): if not self.create_new_stats_file(): return False if not os.access(self.stats_file(), os.W_OK): - log.warning("Failed to usage statistics. " - "Please allow write access to the following file: {}".format(self.stats_file())) return False try: str_data = json.dumps(stats, indent=4) @@ -93,6 +91,5 @@ def remove_stats_file(self): stats_file = self.stats_file() if os.path.exists(stats_file): if not os.access(stats_file, os.W_OK): - log.warning("Failed to remove statistics file {}.".format(stats_file)) return os.remove(stats_file)