telemetry.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """Anonymous telemetry for Reflex."""
  2. from __future__ import annotations
  3. import asyncio
  4. import dataclasses
  5. import multiprocessing
  6. import platform
  7. import warnings
  8. from contextlib import suppress
  9. from reflex.config import environment
  10. try:
  11. from datetime import UTC, datetime
  12. except ImportError:
  13. from datetime import datetime
  14. UTC = None
  15. import httpx
  16. import psutil
  17. from reflex import constants
  18. from reflex.utils import console
  19. from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash
  20. POSTHOG_API_URL: str = "https://app.posthog.com/capture/"
  21. def get_os() -> str:
  22. """Get the operating system.
  23. Returns:
  24. The operating system.
  25. """
  26. return platform.system()
  27. def get_detailed_platform_str() -> str:
  28. """Get the detailed os/platform string.
  29. Returns:
  30. The platform string
  31. """
  32. return platform.platform()
  33. def get_python_version() -> str:
  34. """Get the Python version.
  35. Returns:
  36. The Python version.
  37. """
  38. # Remove the "+" from the version string in case user is using a pre-release version.
  39. return platform.python_version().rstrip("+")
  40. def get_reflex_version() -> str:
  41. """Get the Reflex version.
  42. Returns:
  43. The Reflex version.
  44. """
  45. return constants.Reflex.VERSION
  46. def get_cpu_count() -> int:
  47. """Get the number of CPUs.
  48. Returns:
  49. The number of CPUs.
  50. """
  51. return multiprocessing.cpu_count()
  52. def get_memory() -> int:
  53. """Get the total memory in MB.
  54. Returns:
  55. The total memory in MB.
  56. """
  57. try:
  58. return psutil.virtual_memory().total >> 20
  59. except ValueError: # needed to pass ubuntu test
  60. return 0
  61. def _raise_on_missing_project_hash() -> bool:
  62. """Check if an error should be raised when project hash is missing.
  63. When running reflex with --backend-only, or doing database migration
  64. operations, there is no requirement for a .web directory, so the reflex.json
  65. file may not exist, and this should not be considered an error.
  66. Returns:
  67. False when compilation should be skipped (i.e. no .web directory is required).
  68. Otherwise return True.
  69. """
  70. return not environment.REFLEX_SKIP_COMPILE.get()
  71. def _prepare_event(event: str, **kwargs) -> dict:
  72. """Prepare the event to be sent to the PostHog server.
  73. Args:
  74. event: The event name.
  75. kwargs: Additional data to send with the event.
  76. Returns:
  77. The event data.
  78. """
  79. from reflex.utils.prerequisites import get_cpu_info
  80. installation_id = ensure_reflex_installation_id()
  81. project_hash = get_project_hash(raise_on_fail=_raise_on_missing_project_hash())
  82. if installation_id is None or project_hash is None:
  83. console.debug(
  84. f"Could not get installation_id or project_hash: {installation_id}, {project_hash}"
  85. )
  86. return {}
  87. if UTC is None:
  88. # for python 3.10
  89. stamp = datetime.utcnow().isoformat()
  90. else:
  91. # for python 3.11 & 3.12
  92. stamp = datetime.now(UTC).isoformat()
  93. cpuinfo = get_cpu_info()
  94. additional_keys = ["template", "context", "detail", "user_uuid"]
  95. additional_fields = {
  96. key: value for key in additional_keys if (value := kwargs.get(key)) is not None
  97. }
  98. return {
  99. "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
  100. "event": event,
  101. "properties": {
  102. "distinct_id": installation_id,
  103. "distinct_app_id": project_hash,
  104. "user_os": get_os(),
  105. "user_os_detail": get_detailed_platform_str(),
  106. "reflex_version": get_reflex_version(),
  107. "python_version": get_python_version(),
  108. "cpu_count": get_cpu_count(),
  109. "memory": get_memory(),
  110. "cpu_info": dataclasses.asdict(cpuinfo) if cpuinfo else {},
  111. **additional_fields,
  112. },
  113. "timestamp": stamp,
  114. }
  115. def _send_event(event_data: dict) -> bool:
  116. try:
  117. httpx.post(POSTHOG_API_URL, json=event_data)
  118. except Exception:
  119. return False
  120. else:
  121. return True
  122. def _send(event, telemetry_enabled, **kwargs):
  123. from reflex.config import get_config
  124. # Get the telemetry_enabled from the config if it is not specified.
  125. if telemetry_enabled is None:
  126. telemetry_enabled = get_config().telemetry_enabled
  127. # Return if telemetry is disabled.
  128. if not telemetry_enabled:
  129. return False
  130. with suppress(Exception):
  131. event_data = _prepare_event(event, **kwargs)
  132. if not event_data:
  133. return False
  134. return _send_event(event_data)
  135. def send(event: str, telemetry_enabled: bool | None = None, **kwargs):
  136. """Send anonymous telemetry for Reflex.
  137. Args:
  138. event: The event name.
  139. telemetry_enabled: Whether to send the telemetry (If None, get from config).
  140. kwargs: Additional data to send with the event.
  141. """
  142. async def async_send(event, telemetry_enabled, **kwargs):
  143. return _send(event, telemetry_enabled, **kwargs)
  144. try:
  145. # Within an event loop context, send the event asynchronously.
  146. asyncio.create_task(async_send(event, telemetry_enabled, **kwargs))
  147. except RuntimeError:
  148. # If there is no event loop, send the event synchronously.
  149. warnings.filterwarnings("ignore", category=RuntimeWarning)
  150. _send(event, telemetry_enabled, **kwargs)
  151. def send_error(error: Exception, context: str):
  152. """Send an error event.
  153. Args:
  154. error: The error to send.
  155. context: The context of the error (e.g. "frontend" or "backend")
  156. Returns:
  157. Whether the telemetry was sent successfully.
  158. """
  159. return send("error", detail=type(error).__name__, context=context)