From 4ae24a619ff071503846b1e95d790bb1d6599268 Mon Sep 17 00:00:00 2001 From: Ivan Kravets Date: Tue, 6 Jun 2023 14:14:38 +0300 Subject: [PATCH] Implement anonymous session mechanism to respect user privacy --- platformio/app.py | 1 + platformio/maintenance.py | 4 +-- platformio/platform/_run.py | 10 ++++++- platformio/project/helpers.py | 3 ++ platformio/telemetry.py | 55 +++++++++++++++++++++++++++++------ 5 files changed, 60 insertions(+), 13 deletions(-) diff --git a/platformio/app.py b/platformio/app.py index c01e13c3..d94cdfef 100644 --- a/platformio/app.py +++ b/platformio/app.py @@ -69,6 +69,7 @@ SESSION_VARS = { "command_ctx": None, "caller_id": None, "custom_project_conf": None, + "pause_telemetry": False, } diff --git a/platformio/maintenance.py b/platformio/maintenance.py index c7a3d3b7..f0f7a67d 100644 --- a/platformio/maintenance.py +++ b/platformio/maintenance.py @@ -32,9 +32,7 @@ from platformio.system.prune import calculate_unnecessary_system_data def on_platformio_start(ctx, caller): app.set_session_var("command_ctx", ctx) set_caller(caller) - telemetry.log_command(ctx) - telemetry.resend_postponed_logs() - + telemetry.on_platformio_start(ctx) if PlatformioCLI.in_silence(): return after_upgrade(ctx) diff --git a/platformio/platform/_run.py b/platformio/platform/_run.py index 6a29d955..f16a46a3 100644 --- a/platformio/platform/_run.py +++ b/platformio/platform/_run.py @@ -17,6 +17,7 @@ import json import os import re import sys +import time from urllib.parse import quote import click @@ -63,8 +64,15 @@ class PlatformRunMixin: if not os.path.isfile(variables["build_script"]): raise BuildScriptNotFound(variables["build_script"]) - telemetry.log_platform_run(self, self.config, variables["pioenv"], targets) + started_at = time.time() result = self._run_scons(variables, targets, jobs) + telemetry.log_platform_run( + self, + self.config, + variables["pioenv"], + targets, + elapsed_time=time.time() - started_at, + ) assert "returncode" in result return result diff --git a/platformio/project/helpers.py b/platformio/project/helpers.py index aec12dd9..546cd670 100644 --- a/platformio/project/helpers.py +++ b/platformio/project/helpers.py @@ -164,6 +164,7 @@ load_project_ide_data = load_build_metadata def _load_build_metadata(project_dir, env_names, debug=False): # pylint: disable=import-outside-toplevel + from platformio import app from platformio.run.cli import cli as cmd_run args = ["--project-dir", project_dir, "--target", "__idedata"] @@ -171,7 +172,9 @@ def _load_build_metadata(project_dir, env_names, debug=False): args.extend(["--target", "__debug"]) for name in env_names: args.extend(["-e", name]) + app.set_session_var("pause_telemetry", True) result = CliRunner().invoke(cmd_run, args) + app.set_session_var("pause_telemetry", False) if result.exit_code != 0 and not isinstance( result.exception, exception.ReturnErrorCode ): diff --git a/platformio/telemetry.py b/platformio/telemetry.py index d74f099d..06e400ce 100644 --- a/platformio/telemetry.py +++ b/platformio/telemetry.py @@ -19,8 +19,8 @@ import platform as python_platform import queue import re import threading +import time from collections import deque -from time import sleep, time from traceback import format_exc import requests @@ -34,10 +34,13 @@ from platformio.proc import is_ci, is_container KEEP_MAX_REPORTS = 100 SEND_MAX_EVENTS = 25 +SESSION_TIMEOUT_DURATION = 30 * 60 # secs class MeasurementProtocol: def __init__(self): + self.client_id = app.get_cid() + self.session_id = start_session() self._user_properties = {} self._events = [] @@ -57,12 +60,13 @@ class MeasurementProtocol: self._user_properties[name] = {"value": value} def add_event(self, name, params): + params["session_id"] = params.get("session_id", self.session_id) params["engagement_time_msec"] = params.get("engagement_time_msec", 1) self._events.append({"name": name, "params": params}) def to_payload(self): return { - "client_id": app.get_cid(), + "client_id": self.client_id, "non_personalized_ads": True, "user_properties": self._user_properties, "events": self._events, @@ -81,7 +85,9 @@ class TelemetryLogger: self._workers = [] def log(self, payload): - if not app.get_setting("enable_telemetry"): + if not app.get_setting("enable_telemetry") or app.get_session_var( + "pause_telemetry" + ): return None # if network is off-line @@ -126,13 +132,11 @@ class TelemetryLogger: try: item = self._queue.get() _item = item.copy() - if "qt" not in _item: - _item["qt"] = time() self._failedque.append(_item) if self._send(item): self._failedque.remove(_item) self._queue.task_done() - except: # pylint: disable=W0702 + except: # pylint: disable=bare-except pass def _send(self, payload): @@ -164,6 +168,35 @@ class TelemetryLogger: return False +@util.memoized("1m") +def start_session(): + with app.State( + app.resolve_state_path("cache_dir", "session.json"), lock=True + ) as state: + state.modified = True + start_at = state.get("start_at") + last_seen_at = state.get("last_seen_at") + + if ( + not start_at + or not last_seen_at + or last_seen_at < (time.time() - SESSION_TIMEOUT_DURATION) + ): + start_at = last_seen_at = int(time.time()) + state["start_at"] = state["last_seen_at"] = start_at + else: + state["last_seen_at"] = int(time.time()) + + session_hash = hashlib.sha1(hashlib_encode_data(app.get_cid())) + session_hash.update(hashlib_encode_data(start_at)) + return session_hash.hexdigest() + + +def on_platformio_start(cmd_ctx): + log_command(cmd_ctx) + resend_postponed_logs() + + def log_event(name, params): mp = MeasurementProtocol() mp.add_event(name, params) @@ -255,10 +288,14 @@ def dump_project_env_params(config, env, platform): return params -def log_platform_run(platform, project_config, project_env, targets=None): +def log_platform_run( + platform, project_config, project_env, targets=None, elapsed_time=None +): params = dump_project_env_params(project_config, project_env, platform) if targets: params["targets"] = ", ".join(targets) + if elapsed_time: + params["engagement_time_msec"] = int(elapsed_time * 1000) log_event("pio_platform_run", params) @@ -302,7 +339,7 @@ def _finalize(): while elapsed < timeout: if not TelemetryLogger().in_wait(): break - sleep(0.2) + time.sleep(0.2) elapsed += 200 postpone_logs(TelemetryLogger().get_unprocessed()) except KeyboardInterrupt: @@ -338,7 +375,7 @@ def postpone_logs(payloads): if not payloads: return None postponed_events = load_postponed_events() or [] - timestamp_micros = int(time() * 1000000) + timestamp_micros = int(time.time() * 1000000) for payload in payloads: for event in payload.get("events", []): event["timestamp_micros"] = timestamp_micros