Implement anonymous session mechanism to respect user privacy

This commit is contained in:
Ivan Kravets
2023-06-06 14:14:38 +03:00
parent 7f7bc76b20
commit 4ae24a619f
5 changed files with 60 additions and 13 deletions

View File

@ -69,6 +69,7 @@ SESSION_VARS = {
"command_ctx": None, "command_ctx": None,
"caller_id": None, "caller_id": None,
"custom_project_conf": None, "custom_project_conf": None,
"pause_telemetry": False,
} }

View File

@ -32,9 +32,7 @@ from platformio.system.prune import calculate_unnecessary_system_data
def on_platformio_start(ctx, caller): def on_platformio_start(ctx, caller):
app.set_session_var("command_ctx", ctx) app.set_session_var("command_ctx", ctx)
set_caller(caller) set_caller(caller)
telemetry.log_command(ctx) telemetry.on_platformio_start(ctx)
telemetry.resend_postponed_logs()
if PlatformioCLI.in_silence(): if PlatformioCLI.in_silence():
return return
after_upgrade(ctx) after_upgrade(ctx)

View File

@ -17,6 +17,7 @@ import json
import os import os
import re import re
import sys import sys
import time
from urllib.parse import quote from urllib.parse import quote
import click import click
@ -63,8 +64,15 @@ class PlatformRunMixin:
if not os.path.isfile(variables["build_script"]): if not os.path.isfile(variables["build_script"]):
raise BuildScriptNotFound(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) 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 assert "returncode" in result
return result return result

View File

@ -164,6 +164,7 @@ load_project_ide_data = load_build_metadata
def _load_build_metadata(project_dir, env_names, debug=False): def _load_build_metadata(project_dir, env_names, debug=False):
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from platformio import app
from platformio.run.cli import cli as cmd_run from platformio.run.cli import cli as cmd_run
args = ["--project-dir", project_dir, "--target", "__idedata"] 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"]) args.extend(["--target", "__debug"])
for name in env_names: for name in env_names:
args.extend(["-e", name]) args.extend(["-e", name])
app.set_session_var("pause_telemetry", True)
result = CliRunner().invoke(cmd_run, args) result = CliRunner().invoke(cmd_run, args)
app.set_session_var("pause_telemetry", False)
if result.exit_code != 0 and not isinstance( if result.exit_code != 0 and not isinstance(
result.exception, exception.ReturnErrorCode result.exception, exception.ReturnErrorCode
): ):

View File

@ -19,8 +19,8 @@ import platform as python_platform
import queue import queue
import re import re
import threading import threading
import time
from collections import deque from collections import deque
from time import sleep, time
from traceback import format_exc from traceback import format_exc
import requests import requests
@ -34,10 +34,13 @@ from platformio.proc import is_ci, is_container
KEEP_MAX_REPORTS = 100 KEEP_MAX_REPORTS = 100
SEND_MAX_EVENTS = 25 SEND_MAX_EVENTS = 25
SESSION_TIMEOUT_DURATION = 30 * 60 # secs
class MeasurementProtocol: class MeasurementProtocol:
def __init__(self): def __init__(self):
self.client_id = app.get_cid()
self.session_id = start_session()
self._user_properties = {} self._user_properties = {}
self._events = [] self._events = []
@ -57,12 +60,13 @@ class MeasurementProtocol:
self._user_properties[name] = {"value": value} self._user_properties[name] = {"value": value}
def add_event(self, name, params): 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) params["engagement_time_msec"] = params.get("engagement_time_msec", 1)
self._events.append({"name": name, "params": params}) self._events.append({"name": name, "params": params})
def to_payload(self): def to_payload(self):
return { return {
"client_id": app.get_cid(), "client_id": self.client_id,
"non_personalized_ads": True, "non_personalized_ads": True,
"user_properties": self._user_properties, "user_properties": self._user_properties,
"events": self._events, "events": self._events,
@ -81,7 +85,9 @@ class TelemetryLogger:
self._workers = [] self._workers = []
def log(self, payload): 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 return None
# if network is off-line # if network is off-line
@ -126,13 +132,11 @@ class TelemetryLogger:
try: try:
item = self._queue.get() item = self._queue.get()
_item = item.copy() _item = item.copy()
if "qt" not in _item:
_item["qt"] = time()
self._failedque.append(_item) self._failedque.append(_item)
if self._send(item): if self._send(item):
self._failedque.remove(_item) self._failedque.remove(_item)
self._queue.task_done() self._queue.task_done()
except: # pylint: disable=W0702 except: # pylint: disable=bare-except
pass pass
def _send(self, payload): def _send(self, payload):
@ -164,6 +168,35 @@ class TelemetryLogger:
return False 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): def log_event(name, params):
mp = MeasurementProtocol() mp = MeasurementProtocol()
mp.add_event(name, params) mp.add_event(name, params)
@ -255,10 +288,14 @@ def dump_project_env_params(config, env, platform):
return params 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) params = dump_project_env_params(project_config, project_env, platform)
if targets: if targets:
params["targets"] = ", ".join(targets) params["targets"] = ", ".join(targets)
if elapsed_time:
params["engagement_time_msec"] = int(elapsed_time * 1000)
log_event("pio_platform_run", params) log_event("pio_platform_run", params)
@ -302,7 +339,7 @@ def _finalize():
while elapsed < timeout: while elapsed < timeout:
if not TelemetryLogger().in_wait(): if not TelemetryLogger().in_wait():
break break
sleep(0.2) time.sleep(0.2)
elapsed += 200 elapsed += 200
postpone_logs(TelemetryLogger().get_unprocessed()) postpone_logs(TelemetryLogger().get_unprocessed())
except KeyboardInterrupt: except KeyboardInterrupt:
@ -338,7 +375,7 @@ def postpone_logs(payloads):
if not payloads: if not payloads:
return None return None
postponed_events = load_postponed_events() or [] postponed_events = load_postponed_events() or []
timestamp_micros = int(time() * 1000000) timestamp_micros = int(time.time() * 1000000)
for payload in payloads: for payload in payloads:
for event in payload.get("events", []): for event in payload.get("events", []):
event["timestamp_micros"] = timestamp_micros event["timestamp_micros"] = timestamp_micros