-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathusage_reporting.py
More file actions
135 lines (112 loc) · 5.48 KB
/
Copy pathusage_reporting.py
File metadata and controls
135 lines (112 loc) · 5.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import atexit
import json
import os
from trace_client import TraceClient, environment_opts_out
# @author Daniel McCoy Stephenson
# @since September 11th, 2026
APPLICATION = "patchwork"
SETTINGS_FILE = "settings.json"
SETTINGS_SECTION = "usage_reporting"
DEFAULT_ENDPOINT = "https://trace.danielstephenson.dev"
# The program key patchwork ships with. Keys identify a program rather than
# guard anything (trace's ADR 0001), so it lives here and in settings.json in
# the open. PATCHWORK_USAGE_REPORTING_KEY, when set, overrides both.
DEFAULT_KEY = "oAMBqZC_yjTIqlB86_O7G4ZZ3gWuGBCBLJLFbUuFaoU"
KEY_ENV_VAR = "PATCHWORK_USAGE_REPORTING_KEY"
VERSION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.txt")
# Sent as the version when version.txt cannot be read: the client requires one, and a missing
# file must never stop patchwork from starting.
UNKNOWN_VERSION = "unknown"
DETAILS_URL = "https://github.com/Stephenson-Software/trace#usage-reporting"
FIRST_RUN_NOTICE = (
"Usage reporting is on: patchwork sends its name and version at startup and an "
"environment-created event to https://trace.danielstephenson.dev - "
"nothing about you, your machine or the environments. "
'Turn it off with "usage_reporting": {"enabled": false} in settings.json, or for every '
"trace-reporting program with the environment variable TRACE_USAGE_REPORTING=off. "
"Details: " + DETAILS_URL
)
# Shown on the first run instead when TRACE_USAGE_REPORTING=off or DO_NOT_TRACK=1 is already
# set: settings.json still gets its block, but saying reporting is on would mislead.
FIRST_RUN_NOTICE_OFF_BY_ENVIRONMENT = "Usage reporting is off (environment). Details: " + DETAILS_URL
def firstRunNotice():
"""The line the first run prints: FIRST_RUN_NOTICE, unless the environment has opted out."""
if environment_opts_out():
return FIRST_RUN_NOTICE_OFF_BY_ENVIRONMENT
return FIRST_RUN_NOTICE
def defaultSettings():
"""The usage_reporting block written to settings.json on first run."""
return {"enabled": True, "endpoint": DEFAULT_ENDPOINT, "key": DEFAULT_KEY}
def readVersion(versionFile=VERSION_FILE):
"""The program's own version, as recorded in version.txt, or None if it cannot be read."""
try:
with open(versionFile, "r") as f:
version = f.read().strip()
return version or None
except OSError:
return None
def loadSettings(settingsFile=SETTINGS_FILE, log=print):
"""
Read the usage_reporting block from the settings file, writing the default
block (and printing the one-time notice) when the file does not have one yet.
Returns:
dict or None: The usage_reporting settings, or None if the settings file
exists but cannot be read, in which case nothing should be reported.
"""
settings = {}
if os.path.exists(settingsFile):
try:
with open(settingsFile, "r") as f:
settings = json.load(f)
if not isinstance(settings, dict):
raise ValueError("settings file is not a JSON object")
except (OSError, ValueError) as e:
log(f"Could not read {settingsFile} ({e}); usage reporting is off until it is fixed.")
return None
section = settings.get(SETTINGS_SECTION)
if isinstance(section, dict):
return section
settings[SETTINGS_SECTION] = defaultSettings()
log(firstRunNotice())
try:
with open(settingsFile, "w") as f:
json.dump(settings, f, indent=2)
except OSError as e:
log(f"Could not write {settingsFile} ({e}); the notice above will be shown again next time.")
return settings[SETTINGS_SECTION]
def buildClient(section, log=print):
"""
A TraceClient for the given usage_reporting settings; disabled when they are None or opted out.
Every event it sends carries the version from version.txt (or UNKNOWN_VERSION) as ``version``.
The client is built through TraceClient whenever the settings could be read, because the
client checks the TRACE_USAGE_REPORTING and DO_NOT_TRACK environment variables before the
settings' own ``enabled`` and records why it is off in ``disabled_reason``.
"""
if section is None:
return TraceClient.disabled()
enabled = section.get("enabled", True)
endpoint = section.get("endpoint") or DEFAULT_ENDPOINT
# PATCHWORK_USAGE_REPORTING_KEY first, then the settings block, then the shipped key: a
# settings.json written before the key shipped has no "key" entry and still reports.
key = os.environ.get(KEY_ENV_VAR, "").strip() or str(section.get("key") or "").strip() or DEFAULT_KEY
try:
return TraceClient(endpoint, APPLICATION, readVersion() or UNKNOWN_VERSION, key=key,
enabled=bool(enabled))
except ValueError as e:
log(f"Could not configure usage reporting ({e}); usage reporting is off.")
return TraceClient.disabled()
def startUsageReporting(settingsFile=SETTINGS_FILE, log=print):
"""
Read the settings, build the client and report the startup event.
Never raises; the client is closed automatically when the interpreter exits.
Returns:
TraceClient: The client, so that further events could be reported.
"""
try:
client = buildClient(loadSettings(settingsFile, log), log)
except Exception as e:
log(f"Could not start usage reporting ({e}); usage reporting is off.")
return TraceClient.disabled()
client.report("startup")
atexit.register(client.close)
return client