golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +1 -1
- golf/auth/__init__.py +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +209 -87
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +81 -53
- golf/commands/run.py +30 -15
- golf/core/__init__.py +1 -1
- golf/core/builder.py +493 -362
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +216 -95
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +455 -310
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
- golf_mcp-0.1.13.dist-info/RECORD +55 -0
- golf_mcp-0.1.11.dist-info/RECORD +0 -55
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
golf/core/telemetry.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Telemetry module for anonymous usage tracking with PostHog."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import hashlib
|
|
5
|
-
import platform
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional, Dict, Any
|
|
8
4
|
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
9
7
|
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
import posthog
|
|
12
12
|
from rich.console import Console
|
|
@@ -23,11 +23,22 @@ POSTHOG_API_KEY = os.environ.get("GOLF_POSTHOG_API_KEY", DEFAULT_POSTHOG_API_KEY
|
|
|
23
23
|
POSTHOG_HOST = "https://us.i.posthog.com"
|
|
24
24
|
|
|
25
25
|
# Telemetry state
|
|
26
|
-
_telemetry_enabled:
|
|
27
|
-
_anonymous_id:
|
|
26
|
+
_telemetry_enabled: bool | None = None
|
|
27
|
+
_anonymous_id: str | None = None
|
|
28
28
|
_user_identified: bool = False # Track if we've already identified the user
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _is_test_mode() -> bool:
|
|
32
|
+
"""Check if we're in test mode."""
|
|
33
|
+
return os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_posthog_disabled_in_test_mode() -> None:
|
|
37
|
+
"""Ensure PostHog is disabled when in test mode."""
|
|
38
|
+
if _is_test_mode() and not posthog.disabled:
|
|
39
|
+
posthog.disabled = True
|
|
40
|
+
|
|
41
|
+
|
|
31
42
|
def get_telemetry_config_path() -> Path:
|
|
32
43
|
"""Get the path to the telemetry configuration file."""
|
|
33
44
|
return Path.home() / ".golf" / "telemetry.json"
|
|
@@ -37,12 +48,9 @@ def save_telemetry_preference(enabled: bool) -> None:
|
|
|
37
48
|
"""Save telemetry preference to persistent storage."""
|
|
38
49
|
config_path = get_telemetry_config_path()
|
|
39
50
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
|
|
41
|
-
config = {
|
|
42
|
-
|
|
43
|
-
"version": 1
|
|
44
|
-
}
|
|
45
|
-
|
|
51
|
+
|
|
52
|
+
config = {"enabled": enabled, "version": 1}
|
|
53
|
+
|
|
46
54
|
try:
|
|
47
55
|
with open(config_path, "w") as f:
|
|
48
56
|
json.dump(config, f)
|
|
@@ -51,15 +59,15 @@ def save_telemetry_preference(enabled: bool) -> None:
|
|
|
51
59
|
pass
|
|
52
60
|
|
|
53
61
|
|
|
54
|
-
def load_telemetry_preference() ->
|
|
62
|
+
def load_telemetry_preference() -> bool | None:
|
|
55
63
|
"""Load telemetry preference from persistent storage."""
|
|
56
64
|
config_path = get_telemetry_config_path()
|
|
57
|
-
|
|
65
|
+
|
|
58
66
|
if not config_path.exists():
|
|
59
67
|
return None
|
|
60
|
-
|
|
68
|
+
|
|
61
69
|
try:
|
|
62
|
-
with open(config_path
|
|
70
|
+
with open(config_path) as f:
|
|
63
71
|
config = json.load(f)
|
|
64
72
|
return config.get("enabled")
|
|
65
73
|
except Exception:
|
|
@@ -68,19 +76,25 @@ def load_telemetry_preference() -> Optional[bool]:
|
|
|
68
76
|
|
|
69
77
|
def is_telemetry_enabled() -> bool:
|
|
70
78
|
"""Check if telemetry is enabled.
|
|
71
|
-
|
|
79
|
+
|
|
72
80
|
Checks in order:
|
|
73
81
|
1. Cached value
|
|
74
|
-
2.
|
|
75
|
-
3.
|
|
76
|
-
4.
|
|
82
|
+
2. GOLF_TEST_MODE environment variable (always disabled in test mode)
|
|
83
|
+
3. GOLF_TELEMETRY environment variable
|
|
84
|
+
4. Persistent preference file
|
|
85
|
+
5. Default to True (opt-out model)
|
|
77
86
|
"""
|
|
78
87
|
global _telemetry_enabled
|
|
79
|
-
|
|
88
|
+
|
|
80
89
|
if _telemetry_enabled is not None:
|
|
81
90
|
return _telemetry_enabled
|
|
82
|
-
|
|
83
|
-
# Check
|
|
91
|
+
|
|
92
|
+
# Check if we're in test mode (highest priority after cache)
|
|
93
|
+
if _is_test_mode():
|
|
94
|
+
_telemetry_enabled = False
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Check environment variables (second highest priority)
|
|
84
98
|
env_telemetry = os.environ.get("GOLF_TELEMETRY", "").lower()
|
|
85
99
|
if env_telemetry in ("0", "false", "no", "off"):
|
|
86
100
|
_telemetry_enabled = False
|
|
@@ -88,13 +102,13 @@ def is_telemetry_enabled() -> bool:
|
|
|
88
102
|
elif env_telemetry in ("1", "true", "yes", "on"):
|
|
89
103
|
_telemetry_enabled = True
|
|
90
104
|
return True
|
|
91
|
-
|
|
105
|
+
|
|
92
106
|
# Check persistent preference
|
|
93
107
|
saved_preference = load_telemetry_preference()
|
|
94
108
|
if saved_preference is not None:
|
|
95
109
|
_telemetry_enabled = saved_preference
|
|
96
110
|
return saved_preference
|
|
97
|
-
|
|
111
|
+
|
|
98
112
|
# Default to enabled (opt-out model)
|
|
99
113
|
_telemetry_enabled = True
|
|
100
114
|
return True
|
|
@@ -102,58 +116,64 @@ def is_telemetry_enabled() -> bool:
|
|
|
102
116
|
|
|
103
117
|
def set_telemetry_enabled(enabled: bool, persist: bool = True) -> None:
|
|
104
118
|
"""Set telemetry enabled state.
|
|
105
|
-
|
|
119
|
+
|
|
106
120
|
Args:
|
|
107
121
|
enabled: Whether telemetry should be enabled
|
|
108
122
|
persist: Whether to save this preference persistently
|
|
109
123
|
"""
|
|
110
124
|
global _telemetry_enabled
|
|
111
125
|
_telemetry_enabled = enabled
|
|
112
|
-
|
|
126
|
+
|
|
113
127
|
if persist:
|
|
114
128
|
save_telemetry_preference(enabled)
|
|
115
129
|
|
|
116
130
|
|
|
117
131
|
def get_anonymous_id() -> str:
|
|
118
132
|
"""Get or create a persistent anonymous ID for this machine.
|
|
119
|
-
|
|
133
|
+
|
|
120
134
|
The ID is stored in the user's home directory and is unique per installation.
|
|
121
135
|
"""
|
|
122
136
|
global _anonymous_id
|
|
123
|
-
|
|
137
|
+
|
|
124
138
|
if _anonymous_id:
|
|
125
139
|
return _anonymous_id
|
|
126
|
-
|
|
140
|
+
|
|
127
141
|
# Try to load existing ID
|
|
128
142
|
id_file = Path.home() / ".golf" / "telemetry_id"
|
|
129
|
-
|
|
143
|
+
|
|
130
144
|
if id_file.exists():
|
|
131
145
|
try:
|
|
132
146
|
_anonymous_id = id_file.read_text().strip()
|
|
133
147
|
# Check if ID is in the old format (no hyphen between hash and random component)
|
|
134
148
|
# Old format: golf-[8 chars hash][8 chars random]
|
|
135
149
|
# New format: golf-[8 chars hash]-[8 chars random]
|
|
136
|
-
if
|
|
150
|
+
if (
|
|
151
|
+
_anonymous_id
|
|
152
|
+
and _anonymous_id.startswith("golf-")
|
|
153
|
+
and len(_anonymous_id) == 21
|
|
154
|
+
):
|
|
137
155
|
# This is likely the old format, regenerate
|
|
138
156
|
_anonymous_id = None
|
|
139
157
|
elif _anonymous_id:
|
|
140
158
|
return _anonymous_id
|
|
141
159
|
except Exception:
|
|
142
160
|
pass
|
|
143
|
-
|
|
161
|
+
|
|
144
162
|
# Generate new ID with more unique data
|
|
145
163
|
# Use only non-identifying system information
|
|
146
|
-
|
|
164
|
+
|
|
147
165
|
# Combine non-identifying factors for uniqueness
|
|
148
|
-
machine_data =
|
|
166
|
+
machine_data = (
|
|
167
|
+
f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
|
|
168
|
+
)
|
|
149
169
|
machine_hash = hashlib.sha256(machine_data.encode()).hexdigest()[:8]
|
|
150
|
-
|
|
170
|
+
|
|
151
171
|
# Add a random component to ensure uniqueness
|
|
152
|
-
random_component = str(uuid.uuid4()).split(
|
|
153
|
-
|
|
172
|
+
random_component = str(uuid.uuid4()).split("-")[0] # First 8 chars of UUID
|
|
173
|
+
|
|
154
174
|
# Use hyphen separator for clarity and ensure PostHog treats these as different IDs
|
|
155
175
|
_anonymous_id = f"golf-{machine_hash}-{random_component}"
|
|
156
|
-
|
|
176
|
+
|
|
157
177
|
# Try to save for next time
|
|
158
178
|
try:
|
|
159
179
|
id_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -161,56 +181,70 @@ def get_anonymous_id() -> str:
|
|
|
161
181
|
except Exception:
|
|
162
182
|
# Not critical if we can't save
|
|
163
183
|
pass
|
|
164
|
-
|
|
184
|
+
|
|
165
185
|
return _anonymous_id
|
|
166
186
|
|
|
167
187
|
|
|
168
188
|
def initialize_telemetry() -> None:
|
|
169
189
|
"""Initialize PostHog telemetry if enabled."""
|
|
190
|
+
# Ensure PostHog is disabled in test mode
|
|
191
|
+
_ensure_posthog_disabled_in_test_mode()
|
|
192
|
+
|
|
193
|
+
# Don't initialize if PostHog is disabled (test mode)
|
|
194
|
+
if posthog.disabled:
|
|
195
|
+
return
|
|
196
|
+
|
|
170
197
|
if not is_telemetry_enabled():
|
|
171
198
|
return
|
|
172
|
-
|
|
199
|
+
|
|
173
200
|
# Skip initialization if no valid API key (empty or placeholder)
|
|
174
201
|
if not POSTHOG_API_KEY or POSTHOG_API_KEY.startswith("phc_YOUR"):
|
|
175
202
|
return
|
|
176
|
-
|
|
203
|
+
|
|
177
204
|
try:
|
|
178
205
|
posthog.project_api_key = POSTHOG_API_KEY
|
|
179
206
|
posthog.host = POSTHOG_HOST
|
|
180
|
-
|
|
207
|
+
|
|
181
208
|
# Disable PostHog's own logging to avoid noise
|
|
182
209
|
posthog.disabled = False
|
|
183
210
|
posthog.debug = False
|
|
184
|
-
|
|
211
|
+
|
|
185
212
|
except Exception:
|
|
186
213
|
# Telemetry should never break the application
|
|
187
214
|
pass
|
|
188
215
|
|
|
189
216
|
|
|
190
|
-
def track_event(event_name: str, properties:
|
|
217
|
+
def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
|
|
191
218
|
"""Track an anonymous event with minimal data.
|
|
192
|
-
|
|
219
|
+
|
|
193
220
|
Args:
|
|
194
221
|
event_name: Name of the event (e.g., "cli_init", "cli_build")
|
|
195
222
|
properties: Optional properties to include with the event
|
|
196
223
|
"""
|
|
197
224
|
global _user_identified
|
|
198
|
-
|
|
225
|
+
|
|
226
|
+
# Ensure PostHog is disabled in test mode
|
|
227
|
+
_ensure_posthog_disabled_in_test_mode()
|
|
228
|
+
|
|
229
|
+
# Early return if PostHog is disabled (test mode)
|
|
230
|
+
if posthog.disabled:
|
|
231
|
+
return
|
|
232
|
+
|
|
199
233
|
if not is_telemetry_enabled():
|
|
200
234
|
return
|
|
201
|
-
|
|
235
|
+
|
|
202
236
|
# Skip if no valid API key (empty or placeholder)
|
|
203
237
|
if not POSTHOG_API_KEY or POSTHOG_API_KEY.startswith("phc_YOUR"):
|
|
204
238
|
return
|
|
205
|
-
|
|
239
|
+
|
|
206
240
|
try:
|
|
207
241
|
# Initialize if needed
|
|
208
242
|
if posthog.project_api_key != POSTHOG_API_KEY:
|
|
209
243
|
initialize_telemetry()
|
|
210
|
-
|
|
244
|
+
|
|
211
245
|
# Get anonymous ID
|
|
212
246
|
anonymous_id = get_anonymous_id()
|
|
213
|
-
|
|
247
|
+
|
|
214
248
|
# Only identify the user once per session
|
|
215
249
|
if not _user_identified:
|
|
216
250
|
# Set person properties to differentiate installations
|
|
@@ -222,15 +256,12 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
|
|
|
222
256
|
"python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
|
|
223
257
|
}
|
|
224
258
|
}
|
|
225
|
-
|
|
259
|
+
|
|
226
260
|
# Identify the user with properties
|
|
227
|
-
posthog.identify(
|
|
228
|
-
|
|
229
|
-
properties=person_properties
|
|
230
|
-
)
|
|
231
|
-
|
|
261
|
+
posthog.identify(distinct_id=anonymous_id, properties=person_properties)
|
|
262
|
+
|
|
232
263
|
_user_identified = True
|
|
233
|
-
|
|
264
|
+
|
|
234
265
|
# Only include minimal, non-identifying properties
|
|
235
266
|
safe_properties = {
|
|
236
267
|
"golf_version": __version__,
|
|
@@ -239,30 +270,44 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
|
|
|
239
270
|
# Explicitly disable IP tracking
|
|
240
271
|
"$ip": None,
|
|
241
272
|
}
|
|
242
|
-
|
|
273
|
+
|
|
243
274
|
# Filter properties to only include safe ones
|
|
244
275
|
if properties:
|
|
245
276
|
# Only include specific safe properties
|
|
246
|
-
safe_keys = {
|
|
277
|
+
safe_keys = {
|
|
278
|
+
"success",
|
|
279
|
+
"environment",
|
|
280
|
+
"template",
|
|
281
|
+
"command_type",
|
|
282
|
+
"error_type",
|
|
283
|
+
"error_message",
|
|
284
|
+
"shutdown_type",
|
|
285
|
+
"exit_code",
|
|
286
|
+
}
|
|
247
287
|
for key in safe_keys:
|
|
248
288
|
if key in properties:
|
|
249
289
|
safe_properties[key] = properties[key]
|
|
250
|
-
|
|
290
|
+
|
|
251
291
|
# Send event
|
|
252
292
|
posthog.capture(
|
|
253
293
|
distinct_id=anonymous_id,
|
|
254
294
|
event=event_name,
|
|
255
295
|
properties=safe_properties,
|
|
256
296
|
)
|
|
257
|
-
|
|
297
|
+
|
|
258
298
|
except Exception:
|
|
259
299
|
# Telemetry should never break the application
|
|
260
300
|
pass
|
|
261
301
|
|
|
262
302
|
|
|
263
|
-
def track_command(
|
|
303
|
+
def track_command(
|
|
304
|
+
command: str,
|
|
305
|
+
success: bool = True,
|
|
306
|
+
error_type: str | None = None,
|
|
307
|
+
error_message: str | None = None,
|
|
308
|
+
) -> None:
|
|
264
309
|
"""Track a CLI command execution with minimal info.
|
|
265
|
-
|
|
310
|
+
|
|
266
311
|
Args:
|
|
267
312
|
command: The command being executed (e.g., "init", "build", "run")
|
|
268
313
|
success: Whether the command was successful
|
|
@@ -270,7 +315,7 @@ def track_command(command: str, success: bool = True, error_type: Optional[str]
|
|
|
270
315
|
error_message: Sanitized error message (no sensitive data)
|
|
271
316
|
"""
|
|
272
317
|
properties = {"success": success}
|
|
273
|
-
|
|
318
|
+
|
|
274
319
|
# Add error details if command failed
|
|
275
320
|
if not success and (error_type or error_message):
|
|
276
321
|
if error_type:
|
|
@@ -279,48 +324,124 @@ def track_command(command: str, success: bool = True, error_type: Optional[str]
|
|
|
279
324
|
# Sanitize error message - remove file paths and sensitive info
|
|
280
325
|
sanitized_message = _sanitize_error_message(error_message)
|
|
281
326
|
properties["error_message"] = sanitized_message
|
|
282
|
-
|
|
327
|
+
|
|
283
328
|
track_event(f"cli_{command}", properties)
|
|
284
329
|
|
|
285
330
|
|
|
286
|
-
def
|
|
287
|
-
|
|
288
|
-
|
|
331
|
+
def track_detailed_error(
|
|
332
|
+
event_name: str,
|
|
333
|
+
error: Exception,
|
|
334
|
+
context: str | None = None,
|
|
335
|
+
operation: str | None = None,
|
|
336
|
+
additional_props: dict[str, Any] | None = None,
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Track a detailed error with enhanced debugging information.
|
|
339
|
+
|
|
289
340
|
Args:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
341
|
+
event_name: Name of the error event (e.g., "cli_run_failed", "cli_build_failed")
|
|
342
|
+
error: The exception that occurred
|
|
343
|
+
context: Additional context about where the error occurred
|
|
344
|
+
operation: The specific operation that failed
|
|
345
|
+
additional_props: Additional properties to include
|
|
294
346
|
"""
|
|
347
|
+
import traceback
|
|
348
|
+
import time
|
|
349
|
+
|
|
350
|
+
properties = {
|
|
351
|
+
"success": False,
|
|
352
|
+
"error_type": type(error).__name__,
|
|
353
|
+
"error_message": _sanitize_error_message(str(error)),
|
|
354
|
+
"timestamp": int(time.time()),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Add operation context
|
|
358
|
+
if operation:
|
|
359
|
+
properties["operation"] = operation
|
|
360
|
+
if context:
|
|
361
|
+
properties["context"] = context
|
|
362
|
+
|
|
363
|
+
# Add sanitized stack trace for debugging
|
|
364
|
+
try:
|
|
365
|
+
tb_lines = traceback.format_exception(type(error), error, error.__traceback__)
|
|
366
|
+
# Get the last few frames (most relevant) and sanitize them
|
|
367
|
+
relevant_frames = tb_lines[-3:] if len(tb_lines) > 3 else tb_lines
|
|
368
|
+
sanitized_trace = []
|
|
369
|
+
|
|
370
|
+
for frame in relevant_frames:
|
|
371
|
+
# Sanitize file paths in stack trace
|
|
372
|
+
sanitized_frame = _sanitize_error_message(frame.strip())
|
|
373
|
+
# Further sanitize common traceback patterns
|
|
374
|
+
sanitized_frame = sanitized_frame.replace('File "[PATH]', 'File "[PATH]')
|
|
375
|
+
sanitized_trace.append(sanitized_frame)
|
|
376
|
+
|
|
377
|
+
properties["stack_trace"] = " | ".join(sanitized_trace)
|
|
378
|
+
|
|
379
|
+
# Add the specific line that caused the error if available
|
|
380
|
+
if hasattr(error, '__traceback__') and error.__traceback__:
|
|
381
|
+
tb = error.__traceback__
|
|
382
|
+
while tb.tb_next:
|
|
383
|
+
tb = tb.tb_next
|
|
384
|
+
properties["error_line"] = tb.tb_lineno
|
|
385
|
+
|
|
386
|
+
except Exception:
|
|
387
|
+
# Don't fail if we can't capture stack trace
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
# Add system context for debugging
|
|
391
|
+
try:
|
|
392
|
+
properties["python_executable"] = _sanitize_error_message(platform.python_implementation())
|
|
393
|
+
properties["platform_detail"] = platform.platform()[:50] # Limit length
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
# Merge additional properties
|
|
398
|
+
if additional_props:
|
|
399
|
+
# Only include safe additional properties
|
|
400
|
+
safe_additional_keys = {
|
|
401
|
+
"exit_code", "shutdown_type", "environment", "template",
|
|
402
|
+
"build_env", "transport", "component_count", "file_path",
|
|
403
|
+
"component_type", "validation_error", "config_error"
|
|
404
|
+
}
|
|
405
|
+
for key, value in additional_props.items():
|
|
406
|
+
if key in safe_additional_keys:
|
|
407
|
+
properties[key] = value
|
|
408
|
+
|
|
409
|
+
track_event(event_name, properties)
|
|
410
|
+
|
|
411
|
+
def _sanitize_error_message(message: str) -> str:
|
|
412
|
+
"""Sanitize error messages to remove sensitive information."""
|
|
295
413
|
import re
|
|
296
|
-
|
|
297
|
-
# Remove
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
414
|
+
|
|
415
|
+
# Remove file paths but preserve filenames
|
|
416
|
+
# Match paths with directories and capture the filename
|
|
417
|
+
# Unix style: /path/to/file.py -> file.py
|
|
418
|
+
message = re.sub(r"(/[^/\s]+)+/([^/\s]+)", r"\2", message)
|
|
419
|
+
# Windows style: C:\path\to\file.py -> file.py
|
|
420
|
+
message = re.sub(r"([A-Za-z]:\\[^\\]+\\)+([^\\]+)", r"\2", message)
|
|
421
|
+
# Remaining absolute paths without filename
|
|
422
|
+
message = re.sub(r"[/\\][^\s]*[/\\]", "[PATH]/", message)
|
|
423
|
+
|
|
305
424
|
# Remove potential API keys or tokens (common patterns)
|
|
306
425
|
# Generic API keys (20+ alphanumeric with underscores/hyphens)
|
|
307
|
-
message = re.sub(r
|
|
426
|
+
message = re.sub(r"\b[a-zA-Z0-9_-]{32,}\b", "[REDACTED]", message)
|
|
308
427
|
# Bearer tokens
|
|
309
|
-
message = re.sub(r
|
|
310
|
-
|
|
428
|
+
message = re.sub(r"Bearer\s+[a-zA-Z0-9_.-]+", "Bearer [REDACTED]", message)
|
|
429
|
+
|
|
311
430
|
# Remove email addresses
|
|
312
|
-
message = re.sub(
|
|
313
|
-
|
|
431
|
+
message = re.sub(
|
|
432
|
+
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message
|
|
433
|
+
)
|
|
434
|
+
|
|
314
435
|
# Remove IP addresses
|
|
315
|
-
message = re.sub(r
|
|
316
|
-
|
|
436
|
+
message = re.sub(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", "[IP]", message)
|
|
437
|
+
|
|
317
438
|
# Remove port numbers in URLs
|
|
318
|
-
message = re.sub(r
|
|
319
|
-
|
|
439
|
+
message = re.sub(r":[0-9]{2,5}(?=/|$|\s)", ":[PORT]", message)
|
|
440
|
+
|
|
320
441
|
# Truncate to reasonable length
|
|
321
442
|
if len(message) > 200:
|
|
322
443
|
message = message[:197] + "..."
|
|
323
|
-
|
|
444
|
+
|
|
324
445
|
return message
|
|
325
446
|
|
|
326
447
|
|
|
@@ -328,7 +449,7 @@ def flush() -> None:
|
|
|
328
449
|
"""Flush any pending telemetry events."""
|
|
329
450
|
if not is_telemetry_enabled():
|
|
330
451
|
return
|
|
331
|
-
|
|
452
|
+
|
|
332
453
|
try:
|
|
333
454
|
posthog.flush()
|
|
334
455
|
except Exception:
|
|
@@ -340,9 +461,9 @@ def shutdown() -> None:
|
|
|
340
461
|
"""Shutdown telemetry and flush pending events."""
|
|
341
462
|
if not is_telemetry_enabled():
|
|
342
463
|
return
|
|
343
|
-
|
|
464
|
+
|
|
344
465
|
try:
|
|
345
466
|
posthog.shutdown()
|
|
346
467
|
except Exception:
|
|
347
468
|
# Ignore shutdown errors
|
|
348
|
-
pass
|
|
469
|
+
pass
|