Updated telemetry to support game dumps.
This commit is contained in:
@@ -15,6 +15,10 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from ...command import CommandOutcome
|
||||
from ...diagnostics.crash_dumps import (
|
||||
inspect_minidump,
|
||||
prepare_minidump_attachment,
|
||||
)
|
||||
from ...diagnostics.native_logs import LogFileState
|
||||
from ...game.runtime import RuntimeErrorPT, runtime_settings_for_realm
|
||||
from ...launcher import LauncherError
|
||||
@@ -383,15 +387,12 @@ def play_locked(
|
||||
runtime_issues["game_window_ready"] = game_window_ready
|
||||
if game_readiness_signal is not None:
|
||||
runtime_issues["game_readiness_signal"] = game_readiness_signal
|
||||
if (
|
||||
selection.kind == "proton7-container"
|
||||
and runtime_issues["host_futex_fatal_count"]
|
||||
):
|
||||
runtime_issues["recommended_one_variable_comparison"] = "proton7"
|
||||
elif selection.kind == "proton7" and runtime_issues["gstreamer_missing_libraries"]:
|
||||
runtime_issues["recommended_one_variable_comparison"] = "proton7-container"
|
||||
elif runtime_issues["dbghelp_symdeferred_assertion_count"]:
|
||||
runtime_issues["recommended_one_variable_comparison"] = "umu"
|
||||
recommended_comparison = recommended_runtime_comparison(
|
||||
selection.kind,
|
||||
runtime_issues,
|
||||
)
|
||||
if recommended_comparison is not None:
|
||||
runtime_issues["recommended_one_variable_comparison"] = recommended_comparison
|
||||
clean_exit = outcome.returncode == 0 and current_game_crash is None
|
||||
launch_evidence = game_readiness_signal
|
||||
if launch_evidence is None and current_game_log is not None:
|
||||
@@ -439,6 +440,38 @@ def play_locked(
|
||||
"realm": start_option.name,
|
||||
**api._game_exit_metadata(outcome.returncode, selection.kind),
|
||||
}
|
||||
if not clean_exit:
|
||||
common_game_metadata["failure_reason"] = game_failure_reason(
|
||||
outcome,
|
||||
current_game_log,
|
||||
current_game_crash,
|
||||
common_game_metadata,
|
||||
)
|
||||
failure_evidence: dict[str, object] = {
|
||||
"native_game_log_present": current_game_log is not None,
|
||||
}
|
||||
if current_game_log is not None:
|
||||
failure_evidence["native_game_log"] = game_log_telemetry_summary(
|
||||
current_game_log
|
||||
)
|
||||
if current_game_crash is not None:
|
||||
failure_evidence["crash_dump_info"] = crash_dump_telemetry_summary(
|
||||
current_game_crash
|
||||
)
|
||||
crash_dump_attachment: dict[str, object] | None = None
|
||||
if (
|
||||
current_game_crash is not None
|
||||
and settings.game_crash_dump_upload_enabled
|
||||
and telemetry.enabled
|
||||
):
|
||||
crash_path = current_game_crash.get("path")
|
||||
if isinstance(crash_path, str):
|
||||
upload_status, crash_dump_attachment = prepare_minidump_attachment(
|
||||
Path(crash_path)
|
||||
)
|
||||
else:
|
||||
upload_status = "path_unavailable"
|
||||
failure_evidence["crash_dump_upload_status"] = upload_status
|
||||
if launch_succeeded:
|
||||
if not launch_success_reported:
|
||||
assert launch_evidence is not None
|
||||
@@ -461,7 +494,7 @@ def play_locked(
|
||||
"game_launch_failed",
|
||||
severity="error",
|
||||
message=(
|
||||
"Game client wrote a crash dump"
|
||||
game_crash_message(current_game_log)
|
||||
if current_game_crash is not None
|
||||
else "Game session timed out"
|
||||
if outcome.timed_out
|
||||
@@ -469,23 +502,28 @@ def play_locked(
|
||||
),
|
||||
metadata={
|
||||
**common_game_metadata,
|
||||
**failure_evidence,
|
||||
"runtime_issues": runtime_issues,
|
||||
"stdout": outcome.stdout,
|
||||
"stderr": outcome.stderr,
|
||||
},
|
||||
context={"operation": "launch_game", "stage": "game_start"},
|
||||
attachment=crash_dump_attachment,
|
||||
)
|
||||
if launch_succeeded:
|
||||
exit_message = None
|
||||
if current_game_crash is not None:
|
||||
exit_message = "Game client wrote a crash dump"
|
||||
exit_message = game_crash_message(current_game_log)
|
||||
elif outcome.timed_out:
|
||||
exit_message = "Game session timed out"
|
||||
elif common_game_metadata.get("signal_inferred") is True:
|
||||
signal_wrapper = (
|
||||
"Proton" if selection.kind == "proton7" else "UMU"
|
||||
)
|
||||
exit_message = (
|
||||
"Game session ended with status "
|
||||
f"{common_game_metadata['raw_exit_code']}, consistent with "
|
||||
f"{common_game_metadata['signal_name']} wrapped by UMU"
|
||||
f"{common_game_metadata['signal_name']} wrapped by {signal_wrapper}"
|
||||
)
|
||||
elif "signal" in common_game_metadata:
|
||||
exit_message = (
|
||||
@@ -507,12 +545,14 @@ def play_locked(
|
||||
if clean_exit
|
||||
else {
|
||||
**common_game_metadata,
|
||||
**failure_evidence,
|
||||
"runtime_issues": runtime_issues,
|
||||
"stdout": outcome.stdout,
|
||||
"stderr": outcome.stderr,
|
||||
}
|
||||
),
|
||||
context={"operation": "launch_game", "stage": "exited"},
|
||||
attachment=crash_dump_attachment,
|
||||
)
|
||||
if as_json or dry_run:
|
||||
api._emit(report, as_json)
|
||||
@@ -553,10 +593,15 @@ def game_exit_metadata(
|
||||
)
|
||||
return metadata
|
||||
|
||||
# UMU returns Popen's negative child status from a Python main function.
|
||||
# SystemExit encodes -SIGKILL as 247 before our Python gamebox sees it.
|
||||
# UMU and the retained Proton 7 Python entry point return a negative child
|
||||
# status from subprocess. SystemExit encodes -SIGKILL as 247 before our
|
||||
# Python gamebox sees it.
|
||||
wrapped_sigkill = 256 - int(signal.SIGKILL)
|
||||
if runtime_kind in {"umu", "proton7-container"} and returncode == wrapped_sigkill:
|
||||
if (
|
||||
runtime_kind in {"umu", "proton7", "proton7-container"}
|
||||
and returncode == wrapped_sigkill
|
||||
):
|
||||
wrapper = "proton" if runtime_kind == "proton7" else "umu"
|
||||
metadata.update(
|
||||
{
|
||||
"exit_code": 128 + int(signal.SIGKILL),
|
||||
@@ -564,12 +609,65 @@ def game_exit_metadata(
|
||||
"signal": int(signal.SIGKILL),
|
||||
"signal_name": signal.SIGKILL.name,
|
||||
"signal_inferred": True,
|
||||
"signal_inference": "umu_python_system_exit",
|
||||
"signal_inference": f"{wrapper}_python_system_exit",
|
||||
}
|
||||
)
|
||||
return metadata
|
||||
|
||||
|
||||
def recommended_runtime_comparison(
|
||||
runtime_kind: str,
|
||||
runtime_issues: dict[str, object],
|
||||
) -> str | None:
|
||||
"""Choose a useful one-variable comparison from launch-relevant evidence."""
|
||||
|
||||
if (
|
||||
runtime_kind == "proton7-container"
|
||||
and runtime_issues.get("host_futex_fatal_count")
|
||||
):
|
||||
return "proton7"
|
||||
if (
|
||||
runtime_kind == "proton7"
|
||||
and runtime_issues.get("gstreamer_missing_libraries")
|
||||
and (
|
||||
runtime_issues.get("gstreamer_stream_error") is True
|
||||
or runtime_issues.get("game_window_ready") is not True
|
||||
)
|
||||
):
|
||||
return "proton7-container"
|
||||
if runtime_issues.get("dbghelp_symdeferred_assertion_count"):
|
||||
return "umu"
|
||||
return None
|
||||
|
||||
|
||||
def game_failure_reason(
|
||||
outcome: CommandOutcome,
|
||||
current_game_log: dict[str, object] | None,
|
||||
current_game_crash: dict[str, object] | None,
|
||||
exit_metadata: dict[str, object],
|
||||
) -> str:
|
||||
"""Classify the strongest evidence recorded for a failed game session."""
|
||||
|
||||
if current_game_log and current_game_log.get("runtime_out_of_memory") is True:
|
||||
return "direct3d_resource_exhaustion"
|
||||
if current_game_crash is not None:
|
||||
return "client_crash_dump"
|
||||
if outcome.timed_out:
|
||||
return "session_timeout"
|
||||
if "signal" in exit_metadata:
|
||||
return "process_signal"
|
||||
return "nonzero_exit"
|
||||
|
||||
|
||||
def game_crash_message(current_game_log: dict[str, object] | None) -> str:
|
||||
if (
|
||||
current_game_log is not None
|
||||
and current_game_log.get("runtime_out_of_memory") is True
|
||||
):
|
||||
return "Game client logged Direct3D resource exhaustion and wrote a crash dump"
|
||||
return "Game client wrote a crash dump"
|
||||
|
||||
|
||||
def signal_name(signal_number: int) -> str:
|
||||
try:
|
||||
return signal.Signals(signal_number).name
|
||||
@@ -654,6 +752,7 @@ def current_game_log(
|
||||
],
|
||||
"failed_import_scenes": analysis.failed_import_scenes,
|
||||
"stage_without_new_stage": analysis.stage_without_new_stage,
|
||||
"tex_reclaim_count": analysis.tex_reclaim_count,
|
||||
"module_code_sync_error": analysis.module_code_sync_error,
|
||||
"module_code_sync_left": analysis.module_code_sync_left,
|
||||
"module_code_sync_right": analysis.module_code_sync_right,
|
||||
@@ -702,13 +801,59 @@ def current_game_crash(
|
||||
)
|
||||
if state is None:
|
||||
return None
|
||||
return {
|
||||
report: dict[str, object] = {
|
||||
"path": str(state.path),
|
||||
"modified_at": datetime.fromtimestamp(
|
||||
state.mtime_ns / 1_000_000_000
|
||||
).astimezone().isoformat(),
|
||||
"size": state.size,
|
||||
}
|
||||
minidump = inspect_minidump(state.path)
|
||||
if minidump is not None:
|
||||
report["minidump"] = minidump
|
||||
return report
|
||||
|
||||
|
||||
def game_log_telemetry_summary(report: dict[str, object]) -> dict[str, object]:
|
||||
"""Keep the native-log evidence useful while bounding telemetry cardinality."""
|
||||
|
||||
keys = (
|
||||
"modified_at",
|
||||
"size",
|
||||
"load_texture_to_material_failed",
|
||||
"import_scene_threw",
|
||||
"asset_pack_health_failed",
|
||||
"managed_asset_integrity_failed",
|
||||
"runtime_out_of_memory",
|
||||
"out_of_memory_count",
|
||||
"out_of_memory_effect_paths",
|
||||
"failed_import_scenes",
|
||||
"stage_without_new_stage",
|
||||
"tex_reclaim_count",
|
||||
"module_code_sync_error",
|
||||
"module_code_sync_left",
|
||||
"module_code_sync_right",
|
||||
"module_code_sync_delta",
|
||||
"banned_code_1",
|
||||
"failed_file_find_count",
|
||||
"failed_file_load_count",
|
||||
"failed_file_read_count",
|
||||
"failed_asset_file_count",
|
||||
"failed_texture_file_count",
|
||||
"failed_effect_file_count",
|
||||
"missing_texture_reference_count",
|
||||
"missing_effect_reference_count",
|
||||
"missing_image_reference_count",
|
||||
"missing_pack_image_reference_count",
|
||||
)
|
||||
return {key: report[key] for key in keys if key in report}
|
||||
|
||||
|
||||
def crash_dump_telemetry_summary(report: dict[str, object]) -> dict[str, object]:
|
||||
"""Exclude the local dump path while retaining its bounded signature."""
|
||||
|
||||
keys = ("modified_at", "size", "minidump")
|
||||
return {key: report[key] for key in keys if key in report}
|
||||
|
||||
|
||||
def runtime_issue_summary(stderr: str) -> dict[str, object]:
|
||||
|
||||
@@ -53,6 +53,10 @@ WINETRICKS_CODEPAGE_PROBE = (
|
||||
)
|
||||
WINETRICKS_PROTON7_PROBE = 'winetricks_early_wine_arch cmd.exe /c "echo %$1%"'
|
||||
RUNTIME_DEFAULT_SYNC = "runtime-default"
|
||||
PROTON_DEBUG_ENVIRONMENT_VARIABLE = "PTLINUX_PROTON_DEBUG"
|
||||
PROTON_DEBUG_WINE_CHANNELS = (
|
||||
"+timestamp,+pid,+tid,+seh,+unwind,+debugstr,+loaddll,+mscoree"
|
||||
)
|
||||
CACHYOS_MARKER = "cachyos"
|
||||
CURRENT_UMU_DISTRIBUTIONS = frozenset(
|
||||
{"arch", "cachyos", "endeavouros", "garuda", "manjaro"}
|
||||
@@ -770,6 +774,7 @@ def _proton_environment(
|
||||
) -> dict[str, str]:
|
||||
if not selection.steam_root:
|
||||
raise RuntimeErrorPT("Steam root is missing for Proton")
|
||||
debug_logging = os.environ.get(PROTON_DEBUG_ENVIRONMENT_VARIABLE) == "1"
|
||||
environment = {
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": selection.steam_root,
|
||||
"STEAM_COMPAT_DATA_PATH": str(paths.compat_data),
|
||||
@@ -777,10 +782,12 @@ def _proton_environment(
|
||||
"SteamAppId": "0",
|
||||
"SteamGameId": "0",
|
||||
"WINEPREFIX": str(paths.proton_prefix),
|
||||
"WINEDEBUG": "-all",
|
||||
"PROTON_LOG": "0",
|
||||
"WINEDEBUG": PROTON_DEBUG_WINE_CHANNELS if debug_logging else "-all",
|
||||
"PROTON_LOG": "1" if debug_logging else "0",
|
||||
"DXVK_STATE_CACHE_PATH": str(paths.dxvk_cache),
|
||||
}
|
||||
if debug_logging:
|
||||
environment["PROTON_LOG_DIR"] = str(paths.logs)
|
||||
environment.update(_proton_sync_environment(synchronization))
|
||||
return environment
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class Settings:
|
||||
realm: str = "Babel"
|
||||
ui_scale: str = "auto"
|
||||
telemetry_enabled: bool = True
|
||||
game_crash_dump_upload_enabled: bool = False
|
||||
borderless: bool = False
|
||||
suppress_unresponsive_warnings: bool = True
|
||||
runtime: str = "auto"
|
||||
@@ -158,6 +159,11 @@ def _settings_from_object(value: dict[str, object]) -> Settings:
|
||||
realm=_string(value, "realm", "Babel").strip(),
|
||||
ui_scale=_string(value, "ui_scale", "auto"),
|
||||
telemetry_enabled=_boolean(value, "telemetry_enabled", True),
|
||||
game_crash_dump_upload_enabled=_boolean(
|
||||
value,
|
||||
"game_crash_dump_upload_enabled",
|
||||
False,
|
||||
),
|
||||
borderless=_migrated_borderless(value),
|
||||
allow_window_resizing=_boolean(
|
||||
value,
|
||||
|
||||
@@ -19,6 +19,11 @@ from types import TracebackType
|
||||
from typing import Any
|
||||
|
||||
from .. import __version__
|
||||
from ..diagnostics.crash_dumps import (
|
||||
MAX_CRASH_DUMP_ATTACHMENT_CHARACTERS,
|
||||
MAX_CRASH_DUMP_COMPRESSED_BYTES,
|
||||
MAX_CRASH_DUMP_UPLOAD_BYTES,
|
||||
)
|
||||
from ..paths import AppPaths
|
||||
|
||||
|
||||
@@ -41,6 +46,7 @@ MAX_VALUE_DEPTH = 8
|
||||
QUEUE_CAPACITY = 128
|
||||
DELIVERY_TIMEOUT_SECONDS = 3.0
|
||||
SHUTDOWN_WAIT_SECONDS = 0.25
|
||||
CRASH_DUMP_SHUTDOWN_WAIT_SECONDS = DELIVERY_TIMEOUT_SECONDS + 0.5
|
||||
ERROR_SUPPRESSION_WINDOW_SECONDS = 60.0
|
||||
MAX_ERROR_FINGERPRINTS = 128
|
||||
|
||||
@@ -63,6 +69,7 @@ _RESOURCE_USAGE_EVENTS = frozenset(
|
||||
"update_success",
|
||||
}
|
||||
)
|
||||
_CRASH_DUMP_ATTACHMENT_EVENTS = frozenset({"game_exited", "game_launch_failed"})
|
||||
VALID_SEVERITIES = frozenset({"debug", "info", "warning", "error", "critical"})
|
||||
VALID_EVENTS = frozenset(
|
||||
{
|
||||
@@ -254,6 +261,7 @@ class TelemetryService:
|
||||
self._worker: threading.Thread | None = None
|
||||
self._environment_worker: threading.Thread | None = None
|
||||
self._state_lock = threading.RLock()
|
||||
self._crash_dump_attachment_enqueued = False
|
||||
self._repeated_errors: dict[str, _RepeatedErrorState] = {}
|
||||
self._previous_sys_excepthook: Any = None
|
||||
self._previous_threading_excepthook: Any = None
|
||||
@@ -372,6 +380,7 @@ class TelemetryService:
|
||||
message: str | None = None,
|
||||
metadata: Mapping[str, object] | None = None,
|
||||
context: Mapping[str, object] | None = None,
|
||||
attachment: Mapping[str, object] | None = None,
|
||||
) -> bool:
|
||||
"""Sanitize and enqueue one event without blocking on network I/O."""
|
||||
|
||||
@@ -431,10 +440,29 @@ class TelemetryService:
|
||||
sanitized_metadata,
|
||||
MAX_METADATA_BYTES,
|
||||
)
|
||||
if attachment:
|
||||
sanitized_attachment = (
|
||||
self._sanitize_attachment(attachment)
|
||||
if event in _CRASH_DUMP_ATTACHMENT_EVENTS
|
||||
else None
|
||||
)
|
||||
if sanitized_attachment is not None:
|
||||
payload["attachment"] = sanitized_attachment
|
||||
if len(self._facade._json_bytes(payload)) > MAX_REQUEST_BYTES:
|
||||
payload.pop("attachment", None)
|
||||
self._set_attachment_status(
|
||||
payload,
|
||||
"payload_too_large",
|
||||
)
|
||||
else:
|
||||
self._set_attachment_status(payload, "invalid_attachment")
|
||||
body = self._facade._encode_payload(payload)
|
||||
if body is None:
|
||||
return False
|
||||
self._queue.put_nowait(body)
|
||||
if "attachment" in payload:
|
||||
with self._state_lock:
|
||||
self._crash_dump_attachment_enqueued = True
|
||||
if repeated_error is not None:
|
||||
evicted = self._remember_repeated_error(repeated_error)
|
||||
if evicted is not None:
|
||||
@@ -663,7 +691,15 @@ class TelemetryService:
|
||||
if not self._active or self._closing:
|
||||
return
|
||||
self._closing = True
|
||||
deadline = time.monotonic() + max(0.0, timeout_seconds)
|
||||
crash_dump_attachment_enqueued = self._crash_dump_attachment_enqueued
|
||||
effective_timeout = max(
|
||||
0.0,
|
||||
timeout_seconds,
|
||||
CRASH_DUMP_SHUTDOWN_WAIT_SECONDS
|
||||
if crash_dump_attachment_enqueued
|
||||
else 0.0,
|
||||
)
|
||||
deadline = time.monotonic() + effective_timeout
|
||||
environment_worker = self._environment_worker
|
||||
if isinstance(environment_worker, threading.Thread) and (
|
||||
environment_worker is not threading.current_thread()
|
||||
@@ -931,6 +967,79 @@ class TelemetryService:
|
||||
truncate=self._facade._truncate_characters,
|
||||
)
|
||||
|
||||
def _sanitize_attachment(
|
||||
self,
|
||||
attachment: Mapping[str, object],
|
||||
) -> dict[str, object] | None:
|
||||
"""Accept only the bounded, path-free minidump envelope we generate."""
|
||||
|
||||
value = dict(attachment)
|
||||
if set(value) != {
|
||||
"schema",
|
||||
"kind",
|
||||
"encoding",
|
||||
"original_size",
|
||||
"compressed_size",
|
||||
"sha256",
|
||||
"data_base64url",
|
||||
}:
|
||||
return None
|
||||
if (
|
||||
value.get("schema") != 1
|
||||
or isinstance(value.get("schema"), bool)
|
||||
or value.get("kind") != "windows_minidump"
|
||||
or value.get("encoding") != "gzip+base64url"
|
||||
):
|
||||
return None
|
||||
original_size = value.get("original_size")
|
||||
compressed_size = value.get("compressed_size")
|
||||
if (
|
||||
isinstance(original_size, bool)
|
||||
or not isinstance(original_size, int)
|
||||
or original_size < 32
|
||||
or original_size > MAX_CRASH_DUMP_UPLOAD_BYTES
|
||||
or isinstance(compressed_size, bool)
|
||||
or not isinstance(compressed_size, int)
|
||||
or compressed_size < 1
|
||||
or compressed_size > MAX_CRASH_DUMP_COMPRESSED_BYTES
|
||||
):
|
||||
return None
|
||||
sha256 = value.get("sha256")
|
||||
data = value.get("data_base64url")
|
||||
if (
|
||||
not isinstance(sha256, str)
|
||||
or re.fullmatch(r"[0-9a-f]{64}", sha256) is None
|
||||
or not isinstance(data, str)
|
||||
or len(data) != (compressed_size * 4 + 2) // 3
|
||||
or len(data) > MAX_CRASH_DUMP_ATTACHMENT_CHARACTERS
|
||||
or re.fullmatch(r"[A-Za-z0-9_-]+", data) is None
|
||||
):
|
||||
return None
|
||||
metadata = {key: item for key, item in value.items() if key != "data_base64url"}
|
||||
sanitized = self._sanitize_value(metadata, key="attachment", depth=0)
|
||||
if not isinstance(sanitized, dict):
|
||||
return None
|
||||
sanitized["data_base64url"] = data
|
||||
return sanitized
|
||||
|
||||
def _set_attachment_status(
|
||||
self,
|
||||
payload: dict[str, object],
|
||||
status: str,
|
||||
) -> None:
|
||||
current_metadata = payload.get("metadata")
|
||||
metadata: dict[str, object] = {"crash_dump_upload_status": status}
|
||||
if isinstance(current_metadata, dict):
|
||||
metadata.update(
|
||||
(key, value)
|
||||
for key, value in current_metadata.items()
|
||||
if key != "crash_dump_upload_status"
|
||||
)
|
||||
payload["metadata"] = self._facade._bound_mapping(
|
||||
metadata,
|
||||
MAX_METADATA_BYTES,
|
||||
)
|
||||
|
||||
def _sanitize_text(self, value: str) -> str:
|
||||
return self._facade._privacy_sanitize_text(
|
||||
value,
|
||||
|
||||
@@ -185,6 +185,9 @@ def open_settings_dialog(
|
||||
value=setting_label(current_settings.ui_scale, UI_SCALE_LABELS)
|
||||
)
|
||||
telemetry_value = tk.BooleanVar(value=current_settings.telemetry_enabled)
|
||||
crash_dump_upload_value = tk.BooleanVar(
|
||||
value=current_settings.game_crash_dump_upload_enabled
|
||||
)
|
||||
suppress_unresponsive_value = tk.BooleanVar(
|
||||
value=current_settings.suppress_unresponsive_warnings
|
||||
)
|
||||
@@ -322,6 +325,7 @@ def open_settings_dialog(
|
||||
allow_window_resizing=allow_window_resizing_value.get(),
|
||||
ui_scale=setting_value(ui_scale_value.get(), UI_SCALE_LABELS),
|
||||
telemetry_enabled=telemetry_value.get(),
|
||||
game_crash_dump_upload_enabled=crash_dump_upload_value.get(),
|
||||
suppress_unresponsive_warnings=suppress_unresponsive_value.get(),
|
||||
runtime=setting_value(runtime_value.get(), RUNTIME_LABELS),
|
||||
proton_sync=setting_value(
|
||||
@@ -590,7 +594,7 @@ def open_settings_dialog(
|
||||
ttk.Checkbutton(
|
||||
diagnostics_card,
|
||||
name="settings_telemetry_enabled",
|
||||
text="Send anonymous diagnostic and usage information",
|
||||
text="Send launcher diagnostic and usage information",
|
||||
variable=telemetry_value,
|
||||
style="Settings.TCheckbutton",
|
||||
cursor="hand2",
|
||||
@@ -600,14 +604,35 @@ def open_settings_dialog(
|
||||
diagnostics_card,
|
||||
text=(
|
||||
"Helps improve PristonTale EU Linux compatibility by sending anonymous "
|
||||
"launcher errors, Linux and display versions, graphics/runtime compatibility, "
|
||||
"and installation or update reliability. Game accounts, personal files, "
|
||||
"hostnames, and hardware serial numbers are never collected."
|
||||
"launcher errors, Linux and display versions, graphics/runtime "
|
||||
"compatibility, and installation or update reliability. Routine "
|
||||
"telemetry does not collect game accounts, personal files, hostnames, "
|
||||
"or hardware serial numbers."
|
||||
),
|
||||
style="SettingsHint.TLabel",
|
||||
wraplength=720,
|
||||
)
|
||||
diagnostics_hint.grid(row=1, column=0, sticky="w", pady=(7, 0))
|
||||
ttk.Checkbutton(
|
||||
diagnostics_card,
|
||||
name="settings_game_crash_dump_upload_enabled",
|
||||
text="Include compressed game crash dumps in error reports",
|
||||
variable=crash_dump_upload_value,
|
||||
style="Settings.TCheckbutton",
|
||||
cursor="hand2",
|
||||
takefocus=True,
|
||||
).grid(row=2, column=0, sticky="w", pady=(12, 0))
|
||||
crash_dump_hint = ttk.Label(
|
||||
diagnostics_card,
|
||||
text=(
|
||||
"Off by default and requires telemetry above. A crash dump can contain "
|
||||
"fragments of account, chat, or other game-process memory. Uploads are "
|
||||
"compressed, integrity checked, and limited to 2 MiB before compression."
|
||||
),
|
||||
style="SettingsHint.TLabel",
|
||||
wraplength=720,
|
||||
)
|
||||
crash_dump_hint.grid(row=3, column=0, sticky="w", pady=(7, 0))
|
||||
|
||||
overrides_card = ttk.LabelFrame(
|
||||
content,
|
||||
@@ -705,6 +730,9 @@ def open_settings_dialog(
|
||||
diagnostics_hint.configure(
|
||||
wraplength=max(140, viewport_width - round(42 * launcher_ui_scale))
|
||||
)
|
||||
crash_dump_hint.configure(
|
||||
wraplength=max(140, viewport_width - round(42 * launcher_ui_scale))
|
||||
)
|
||||
footer_note.configure(
|
||||
wraplength=(
|
||||
max(140, viewport_width - round(42 * launcher_ui_scale))
|
||||
|
||||
Reference in New Issue
Block a user