diff --git a/libs/arcade-cli/arcade_cli/authn.py b/libs/arcade-cli/arcade_cli/authn.py index 224965e5..621d8c3f 100644 --- a/libs/arcade-cli/arcade_cli/authn.py +++ b/libs/arcade-cli/arcade_cli/authn.py @@ -5,11 +5,13 @@ from typing import Any from urllib.parse import parse_qs import yaml +from arcade_core.constants import ( + ARCADE_CONFIG_PATH, + CREDENTIALS_FILE_PATH, +) from rich.console import Console from arcade_cli.constants import ( - ARCADE_CONFIG_PATH, - CREDENTIALS_FILE_PATH, LOGIN_FAILED_HTML, LOGIN_SUCCESS_HTML, ) diff --git a/libs/arcade-cli/arcade_cli/constants.py b/libs/arcade-cli/arcade_cli/constants.py index 6e9d68df..e5dd097f 100644 --- a/libs/arcade-cli/arcade_cli/constants.py +++ b/libs/arcade-cli/arcade_cli/constants.py @@ -1,19 +1,8 @@ -import os - PROD_CLOUD_HOST = "cloud.arcade.dev" PROD_ENGINE_HOST = "api.arcade.dev" LOCALHOST = "localhost" LOCAL_AUTH_CALLBACK_PORT = 9905 -# The path to the directory containing the Arcade configuration files. Typically ~/.arcade -ARCADE_CONFIG_PATH = os.path.join(os.path.expanduser(os.getenv("ARCADE_WORK_DIR", "~")), ".arcade") - -# The path to the file containing the user's Arcade-related credentials (e.g., ARCADE_API_KEY). -CREDENTIALS_FILE_PATH = os.path.join(ARCADE_CONFIG_PATH, "credentials.yaml") - -# The path to the file containing usage analytics identity data. -USAGE_FILE_PATH = os.path.join(ARCADE_CONFIG_PATH, "usage.json") - _style_block = b""" diff --git a/libs/arcade-cli/arcade_cli/main.py b/libs/arcade-cli/arcade_cli/main.py index def8b422..7bafa33e 100644 --- a/libs/arcade-cli/arcade_cli/main.py +++ b/libs/arcade-cli/arcade_cli/main.py @@ -10,6 +10,7 @@ from typing import Optional import click import typer +from arcade_core.constants import CREDENTIALS_FILE_PATH from arcadepy import Arcade from rich.console import Console from rich.text import Text @@ -19,7 +20,6 @@ import arcade_cli.secret as secret import arcade_cli.worker as worker from arcade_cli.authn import LocalAuthCallbackServer, check_existing_login from arcade_cli.constants import ( - CREDENTIALS_FILE_PATH, PROD_CLOUD_HOST, PROD_ENGINE_HOST, ) diff --git a/libs/arcade-cli/arcade_cli/usage/command_tracker.py b/libs/arcade-cli/arcade_cli/usage/command_tracker.py index 11deeaa7..5dc1ad92 100644 --- a/libs/arcade-cli/arcade_cli/usage/command_tracker.py +++ b/libs/arcade-cli/arcade_cli/usage/command_tracker.py @@ -7,12 +7,15 @@ from importlib import metadata from typing import Any import typer -from arcade_cli.constants import ARCADE_CONFIG_PATH from arcade_cli.usage.constants import ( EVENT_CLI_COMMAND_EXECUTED, EVENT_CLI_COMMAND_FAILED, PROP_CLI_VERSION, PROP_COMMAND_NAME, +) +from arcade_core.constants import ARCADE_CONFIG_PATH +from arcade_core.usage import UsageIdentity, UsageService, is_tracking_enabled +from arcade_core.usage.constants import ( PROP_DEVICE_MONOTONIC_END, PROP_DEVICE_MONOTONIC_START, PROP_DURATION_MS, @@ -22,9 +25,6 @@ from arcade_cli.usage.constants import ( PROP_RUNTIME_LANGUAGE, PROP_RUNTIME_VERSION, ) -from arcade_cli.usage.identity import UsageIdentity -from arcade_cli.usage.usage_service import UsageService -from arcade_cli.usage.utils import is_tracking_enabled from rich.console import Console from typer.core import TyperCommand, TyperGroup from typer.models import Context diff --git a/libs/arcade-cli/arcade_cli/usage/constants.py b/libs/arcade-cli/arcade_cli/usage/constants.py index 4d87e622..651084c8 100644 --- a/libs/arcade-cli/arcade_cli/usage/constants.py +++ b/libs/arcade-cli/arcade_cli/usage/constants.py @@ -1,41 +1,7 @@ -"""Constants for usage tracking and analytics.""" - -# Event Names +# CLI Specific Event Names EVENT_CLI_COMMAND_EXECUTED = "CLI execution succeeded" EVENT_CLI_COMMAND_FAILED = "CLI execution failed" -# Property Names +# CLI Specific Property Names PROP_COMMAND_NAME = "command_name" PROP_CLI_VERSION = "cli_version" -PROP_RUNTIME_LANGUAGE = "runtime_language" -PROP_RUNTIME_VERSION = "runtime_version" -PROP_OS_TYPE = "os_type" -PROP_OS_RELEASE = "os_release" -PROP_DURATION_MS = "duration_ms" -PROP_ERROR_MESSAGE = "error_message" -PROP_DEVICE_MONOTONIC_START = "device_start_timestamp" -PROP_DEVICE_MONOTONIC_END = "device_end_timestamp" -# Only used for anonymous usage -PROP_PROCESS_PERSON_PROFILE = "$process_person_profile" - -# Identity Keys -KEY_ANON_ID = "anon_id" -KEY_LINKED_PRINCIPAL_ID = "linked_principal_id" - -# File Names -USAGE_FILE_NAME = "usage.json" - -# Environment Variables -# how props are passed to the usage tracking subprocess -ARCADE_USAGE_EVENT_DATA = "ARCADE_USAGE_EVENT_DATA" -# whether usage tracking is enabled. 1 is enabled, 0 is disabled. -ARCADE_USAGE_TRACKING = "ARCADE_USAGE_TRACKING" - -# Timeouts and Limits (in seconds) -TIMEOUT_POSTHOG_ALIAS = 2 -TIMEOUT_POSTHOG_CAPTURE = 5 -TIMEOUT_ARCADE_API = 2.0 -TIMEOUT_SUBPROCESS_EXIT = 10.0 - -# Retry Configuration -MAX_RETRIES_POSTHOG = 1 diff --git a/libs/arcade-core/arcade_core/constants.py b/libs/arcade-core/arcade_core/constants.py new file mode 100644 index 00000000..7e749b0b --- /dev/null +++ b/libs/arcade-core/arcade_core/constants.py @@ -0,0 +1,6 @@ +import os + +# The path to the directory containing the Arcade configuration files. Typically ~/.arcade +ARCADE_CONFIG_PATH = os.path.join(os.path.expanduser(os.getenv("ARCADE_WORK_DIR", "~")), ".arcade") +# The path to the file containing the user's Arcade-related credentials (e.g., ARCADE_API_KEY). +CREDENTIALS_FILE_PATH = os.path.join(ARCADE_CONFIG_PATH, "credentials.yaml") diff --git a/libs/arcade-core/arcade_core/usage/__init__.py b/libs/arcade-core/arcade_core/usage/__init__.py new file mode 100644 index 00000000..44af3478 --- /dev/null +++ b/libs/arcade-core/arcade_core/usage/__init__.py @@ -0,0 +1,5 @@ +from arcade_core.usage.identity import UsageIdentity +from arcade_core.usage.usage_service import UsageService +from arcade_core.usage.utils import is_tracking_enabled + +__all__ = ["UsageIdentity", "UsageService", "is_tracking_enabled"] diff --git a/libs/arcade-cli/arcade_cli/usage/__main__.py b/libs/arcade-core/arcade_core/usage/__main__.py similarity index 92% rename from libs/arcade-cli/arcade_cli/usage/__main__.py rename to libs/arcade-core/arcade_core/usage/__main__.py index e09792bb..68077955 100644 --- a/libs/arcade-cli/arcade_cli/usage/__main__.py +++ b/libs/arcade-core/arcade_core/usage/__main__.py @@ -1,6 +1,6 @@ """Entry point for detached usage tracking subprocess. -This module is invoked as `python -m arcade_cli.usage` and expects +This module is invoked as `python -m arcade_core.usage` and expects event data to be passed via the ARCADE_USAGE_EVENT_DATA environment variable. """ @@ -8,14 +8,15 @@ import json import os import threading -from arcade_cli.usage.constants import ( +from posthog import Posthog + +from arcade_core.usage.constants import ( ARCADE_USAGE_EVENT_DATA, MAX_RETRIES_POSTHOG, PROP_PROCESS_PERSON_PROFILE, TIMEOUT_POSTHOG_CAPTURE, TIMEOUT_SUBPROCESS_EXIT, ) -from posthog import Posthog def _timeout_exit() -> None: diff --git a/libs/arcade-core/arcade_core/usage/constants.py b/libs/arcade-core/arcade_core/usage/constants.py new file mode 100644 index 00000000..c5bb3fe8 --- /dev/null +++ b/libs/arcade-core/arcade_core/usage/constants.py @@ -0,0 +1,34 @@ +# Base (common) Property Names +PROP_RUNTIME_LANGUAGE = "runtime_language" +PROP_RUNTIME_VERSION = "runtime_version" +PROP_OS_TYPE = "os_type" +PROP_OS_RELEASE = "os_release" +PROP_DURATION_MS = "duration_ms" +PROP_ERROR_MESSAGE = "error_message" +PROP_DEVICE_MONOTONIC_START = "device_start_timestamp" +PROP_DEVICE_MONOTONIC_END = "device_end_timestamp" +PROP_DEVICE_TIMESTAMP = "device_timestamp" +# Only used for anonymous usage +PROP_PROCESS_PERSON_PROFILE = "$process_person_profile" + +# Identity Keys +KEY_ANON_ID = "anon_id" +KEY_LINKED_PRINCIPAL_ID = "linked_principal_id" + +# File Names +USAGE_FILE_NAME = "usage.json" + +# Environment Variables +# how props are passed to the usage tracking subprocess +ARCADE_USAGE_EVENT_DATA = "ARCADE_USAGE_EVENT_DATA" +# whether usage tracking is enabled. 1 is enabled, 0 is disabled. +ARCADE_USAGE_TRACKING = "ARCADE_USAGE_TRACKING" + +# Timeouts and Limits (in seconds) +TIMEOUT_POSTHOG_ALIAS = 2 +TIMEOUT_POSTHOG_CAPTURE = 5 +TIMEOUT_ARCADE_API = 2.0 +TIMEOUT_SUBPROCESS_EXIT = 10.0 + +# Retry Configuration +MAX_RETRIES_POSTHOG = 1 diff --git a/libs/arcade-cli/arcade_cli/usage/identity.py b/libs/arcade-core/arcade_core/usage/identity.py similarity index 98% rename from libs/arcade-cli/arcade_cli/usage/identity.py rename to libs/arcade-core/arcade_core/usage/identity.py index c7466283..6d75bfe2 100644 --- a/libs/arcade-cli/arcade_cli/usage/identity.py +++ b/libs/arcade-core/arcade_core/usage/identity.py @@ -15,8 +15,9 @@ from typing import Any import httpx import yaml -from arcade_cli.constants import ARCADE_CONFIG_PATH, CREDENTIALS_FILE_PATH -from arcade_cli.usage.constants import ( + +from arcade_core.constants import ARCADE_CONFIG_PATH, CREDENTIALS_FILE_PATH +from arcade_core.usage.constants import ( KEY_ANON_ID, KEY_LINKED_PRINCIPAL_ID, TIMEOUT_ARCADE_API, diff --git a/libs/arcade-cli/arcade_cli/usage/usage_service.py b/libs/arcade-core/arcade_core/usage/usage_service.py similarity index 93% rename from libs/arcade-cli/arcade_cli/usage/usage_service.py rename to libs/arcade-core/arcade_core/usage/usage_service.py index 3bd61d8b..a1de44b1 100644 --- a/libs/arcade-cli/arcade_cli/usage/usage_service.py +++ b/libs/arcade-core/arcade_core/usage/usage_service.py @@ -3,17 +3,17 @@ import os import subprocess import sys -from arcade_cli.usage.constants import ( +from arcade_core.usage.constants import ( ARCADE_USAGE_EVENT_DATA, MAX_RETRIES_POSTHOG, TIMEOUT_POSTHOG_ALIAS, ) -from arcade_cli.usage.utils import is_tracking_enabled +from arcade_core.usage.utils import is_tracking_enabled class UsageService: def __init__(self) -> None: - self.api_key = "phc_hIqUQyJpf2TP4COePO5jEpkGeUXipa7KqTEyDeRsTmB" + self.api_key = "phc_g7OuFqZEAVwIgRdtnZkjvBpy9weQ1f9VJW6YP1SzQRF" self.host = "https://us.i.posthog.com" def alias(self, previous_id: str, distinct_id: str) -> None: @@ -71,7 +71,7 @@ class UsageService: "is_anon": is_anon, }) - cmd = [sys.executable, "-m", "arcade_cli.usage"] + cmd = [sys.executable, "-m", "arcade_core.usage"] # Pass data via environment variable (works on all platforms) env = os.environ.copy() diff --git a/libs/arcade-cli/arcade_cli/usage/utils.py b/libs/arcade-core/arcade_core/usage/utils.py similarity index 84% rename from libs/arcade-cli/arcade_cli/usage/utils.py rename to libs/arcade-core/arcade_core/usage/utils.py index 46979f57..eb7d5373 100644 --- a/libs/arcade-cli/arcade_cli/usage/utils.py +++ b/libs/arcade-core/arcade_core/usage/utils.py @@ -1,6 +1,6 @@ import os -from arcade_cli.usage.constants import ARCADE_USAGE_TRACKING +from arcade_core.usage.constants import ARCADE_USAGE_TRACKING def is_tracking_enabled() -> bool: diff --git a/libs/arcade-mcp-server/README.md b/libs/arcade-mcp-server/README.md index 7a80c56b..4d98a27c 100644 --- a/libs/arcade-mcp-server/README.md +++ b/libs/arcade-mcp-server/README.md @@ -67,6 +67,29 @@ python -m arcade_mcp_server --host 0.0.0.0 --port 8080 - [Discord Community](https://discord.gg/arcade-mcp) - [Documentation](https://docs.arcade.dev) +## Analytics & Privacy + +*Arcade MCP Server* collects anonymous usage data to help us improve the service and debug issues. We track "MCP server start" events to understand server usage patterns and reliability. + +#### What We Track + +When the server starts, we collect the following information: +- **Server configuration**: transport type (`http` or `stdio`), host, port +- **Server metadata**: tool count, server version +- **Runtime environment**: Python version, OS type and release +- **Timing**: device timestamp +- **Errors**: error messages (if startup fails) + +#### Privacy + +- For **anonymous users**: Events are tracked with an anonymous ID and no user profile is created +- For **authenticated users**: Events are linked to your account to help us provide better support +- **No sensitive data** (credentials, tool inputs/outputs, or personal information) is ever collected + +#### Opt Out + +To disable usage tracking, set the environment variable ARCADE_USAGE_TRACKING to 0. + ## License Arcade MCP Server is open source software licensed under the MIT license. diff --git a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py index 8c46fd43..98acd816 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py @@ -26,6 +26,7 @@ from arcade_mcp_server.exceptions import ServerError from arcade_mcp_server.server import MCPServer from arcade_mcp_server.settings import MCPSettings, ServerSettings from arcade_mcp_server.types import Prompt, PromptMessage, Resource +from arcade_mcp_server.usage import ServerTracker from arcade_mcp_server.worker import create_arcade_mcp P = ParamSpec("P") @@ -249,6 +250,13 @@ class MCPApp: elif transport == "stdio": from arcade_mcp_server.__main__ import run_stdio_server + tracker = ServerTracker() + tracker.track_server_start( + transport="stdio", + host=None, + port=None, + tool_count=len(self._catalog), + ) asyncio.run( run_stdio_server( catalog=self._catalog, @@ -328,6 +336,13 @@ class MCPApp: **self.server_kwargs, ) + tracker = ServerTracker() + tracker.track_server_start( + transport="http", + host=host, + port=port, + tool_count=len(self._catalog), + ) uvicorn.run( app, host=host, diff --git a/libs/arcade-mcp-server/arcade_mcp_server/usage/__init__.py b/libs/arcade-mcp-server/arcade_mcp_server/usage/__init__.py new file mode 100644 index 00000000..1f4f1111 --- /dev/null +++ b/libs/arcade-mcp-server/arcade_mcp_server/usage/__init__.py @@ -0,0 +1,3 @@ +from arcade_mcp_server.usage.server_tracker import ServerTracker + +__all__ = ["ServerTracker"] diff --git a/libs/arcade-mcp-server/arcade_mcp_server/usage/constants.py b/libs/arcade-mcp-server/arcade_mcp_server/usage/constants.py new file mode 100644 index 00000000..1ece3b4e --- /dev/null +++ b/libs/arcade-mcp-server/arcade_mcp_server/usage/constants.py @@ -0,0 +1,9 @@ +# MCP Server Specific Event Names +EVENT_MCP_SERVER_STARTED = "MCP server started" + +# MCP Server Specific Property Names +PROP_TRANSPORT = "transport" +PROP_HOST = "host" +PROP_PORT = "port" +PROP_TOOL_COUNT = "tool_count" +PROP_MCP_SERVER_VERSION = "arcade_mcp_server_version" diff --git a/libs/arcade-mcp-server/arcade_mcp_server/usage/server_tracker.py b/libs/arcade-mcp-server/arcade_mcp_server/usage/server_tracker.py new file mode 100644 index 00000000..32ee41f2 --- /dev/null +++ b/libs/arcade-mcp-server/arcade_mcp_server/usage/server_tracker.py @@ -0,0 +1,109 @@ +import platform +import sys +import time +from importlib import metadata + +from arcade_core.usage import UsageIdentity, UsageService, is_tracking_enabled +from arcade_core.usage.constants import ( + PROP_DEVICE_TIMESTAMP, + PROP_OS_RELEASE, + PROP_OS_TYPE, + PROP_RUNTIME_LANGUAGE, + PROP_RUNTIME_VERSION, +) + +from arcade_mcp_server.usage.constants import ( + EVENT_MCP_SERVER_STARTED, + PROP_HOST, + PROP_MCP_SERVER_VERSION, + PROP_PORT, + PROP_TOOL_COUNT, + PROP_TRANSPORT, +) + + +class ServerTracker: + """Tracks MCP server events for usage analytics. + + To opt out, set the ARCADE_USAGE_TRACKING environment variable to 0. + """ + + def __init__(self) -> None: + self.usage_service = UsageService() + self.identity = UsageIdentity() + self._mcp_server_version: str | None = None + self._runtime_version: str | None = None + + @property + def mcp_server_version(self) -> str: + """Get the version of arcade_mcp_server package""" + if self._mcp_server_version is None: + try: + self._mcp_server_version = metadata.version("arcade-mcp-server") + except Exception: + self._mcp_server_version = "unknown" + return self._mcp_server_version + + @property + def runtime_version(self) -> str: + """Get the version of the Python runtime""" + if self._runtime_version is None: + version_info = sys.version_info + self._runtime_version = ( + f"{version_info.major}.{version_info.minor}.{version_info.micro}" + ) + return self._runtime_version + + @property + def user_id(self) -> str: + """Get the distinct_id based on developer's authentication state""" + return self.identity.get_distinct_id() + + def track_server_start( + self, + transport: str, + host: str | None, + port: int | None, + tool_count: int, + ) -> None: + """Track MCP server start event. + + Args: + transport: The transport type ("http" or "stdio") + host: The host address (None for stdio) + port: The port number (None for stdio) + tool_count: The number of tools available at server start + """ + if not is_tracking_enabled(): + return + + # Check if aliasing needed (user authenticated but not yet linked) + if self.identity.should_alias(): + principal_id = self.identity.get_principal_id() + if principal_id: + self.usage_service.alias( + previous_id=self.identity.anon_id, distinct_id=principal_id + ) + self.identity.set_linked_principal_id(principal_id) + + properties: dict[str, str | int | float] = { + PROP_TRANSPORT: transport, + PROP_TOOL_COUNT: tool_count, + PROP_MCP_SERVER_VERSION: self.mcp_server_version, + PROP_RUNTIME_LANGUAGE: "python", + PROP_RUNTIME_VERSION: self.runtime_version, + PROP_OS_TYPE: platform.system(), + PROP_OS_RELEASE: platform.release(), + PROP_DEVICE_TIMESTAMP: time.monotonic(), + } + + # HTTP Streamable specific props + if host is not None: + properties[PROP_HOST] = host + if port is not None: + properties[PROP_PORT] = port + + is_anon = self.user_id == self.identity.anon_id + self.usage_service.capture( + EVENT_MCP_SERVER_STARTED, self.user_id, properties=properties, is_anon=is_anon + ) diff --git a/libs/tests/cli/usage/test_identity.py b/libs/tests/cli/usage/test_identity.py index 29eeab75..91f4108f 100644 --- a/libs/tests/cli/usage/test_identity.py +++ b/libs/tests/cli/usage/test_identity.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest import yaml -from arcade_cli.usage.identity import UsageIdentity +from arcade_core.usage import UsageIdentity @pytest.fixture @@ -15,8 +15,8 @@ def temp_config_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: config_dir.mkdir() credentials_file = config_dir / "credentials.yaml" - monkeypatch.setattr("arcade_cli.usage.identity.ARCADE_CONFIG_PATH", str(config_dir)) - monkeypatch.setattr("arcade_cli.usage.identity.CREDENTIALS_FILE_PATH", str(credentials_file)) + monkeypatch.setattr("arcade_core.usage.identity.ARCADE_CONFIG_PATH", str(config_dir)) + monkeypatch.setattr("arcade_core.usage.identity.CREDENTIALS_FILE_PATH", str(credentials_file)) return config_dir @@ -154,7 +154,7 @@ class TestGetDistinctId: assert distinct_id == "persisted-user-123" - @patch("arcade_cli.usage.identity.UsageIdentity.get_principal_id") + @patch("arcade_core.usage.identity.UsageIdentity.get_principal_id") def test_returns_principal_id_from_api_when_not_persisted( self, mock_get_principal: MagicMock, identity: UsageIdentity ) -> None: @@ -166,7 +166,7 @@ class TestGetDistinctId: assert distinct_id == "api-user-456" mock_get_principal.assert_called_once() - @patch("arcade_cli.usage.identity.UsageIdentity.get_principal_id") + @patch("arcade_core.usage.identity.UsageIdentity.get_principal_id") def test_returns_anon_id_when_not_authenticated( self, mock_get_principal: MagicMock, identity: UsageIdentity ) -> None: @@ -257,7 +257,7 @@ class TestGetPrincipalId: class TestShouldAlias: """Tests for should_alias() method.""" - @patch("arcade_cli.usage.identity.UsageIdentity.get_principal_id") + @patch("arcade_core.usage.identity.UsageIdentity.get_principal_id") def test_returns_true_when_authenticated_but_not_linked( self, mock_get_principal: MagicMock, identity: UsageIdentity ) -> None: @@ -268,7 +268,7 @@ class TestShouldAlias: assert should_alias is True - @patch("arcade_cli.usage.identity.UsageIdentity.get_principal_id") + @patch("arcade_core.usage.identity.UsageIdentity.get_principal_id") def test_returns_false_when_already_linked( self, mock_get_principal: MagicMock, identity: UsageIdentity, temp_config_path: Path ) -> None: @@ -285,7 +285,7 @@ class TestShouldAlias: assert should_alias is False - @patch("arcade_cli.usage.identity.UsageIdentity.get_principal_id") + @patch("arcade_core.usage.identity.UsageIdentity.get_principal_id") def test_returns_false_when_not_authenticated( self, mock_get_principal: MagicMock, identity: UsageIdentity ) -> None: diff --git a/libs/tests/cli/usage/test_cache_utils.py b/libs/tests/core/usage/test_cache_utils.py similarity index 97% rename from libs/tests/cli/usage/test_cache_utils.py rename to libs/tests/core/usage/test_cache_utils.py index 4904bcba..784c08d7 100644 --- a/libs/tests/cli/usage/test_cache_utils.py +++ b/libs/tests/core/usage/test_cache_utils.py @@ -1,5 +1,5 @@ import pytest -from arcade_cli.usage.utils import is_tracking_enabled +from arcade_core.usage import is_tracking_enabled @pytest.mark.parametrize(