From b36680fefdc89c06ec2a6b18b40a87f5c1227efd Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Thu, 16 Feb 2023 00:11:53 -0800 Subject: [PATCH] Anonymous Telemetry Opt Out Available (#550) * Added anonymous telemetry with opt-out. --- poetry.lock | 18 ++--- pynecone/.templates/web/pcversion.txt | 1 - pynecone/.templates/web/pynecone.json | 3 + pynecone/config.py | 3 + pynecone/constants.py | 4 +- pynecone/pc.py | 14 ++++ pynecone/telemetry.py | 103 ++++++++++++++++++++------ pynecone/utils.py | 11 ++- tests/test_telemetry.py | 9 --- 9 files changed, 122 insertions(+), 44 deletions(-) delete mode 100644 pynecone/.templates/web/pcversion.txt create mode 100644 pynecone/.templates/web/pynecone.json diff --git a/poetry.lock b/poetry.lock index 7471e7e47..a9e23043e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -628,14 +628,14 @@ plugins = ["importlib-metadata"] [[package]] name = "pyright" -version = "1.1.293" +version = "1.1.294" description = "Command line wrapper for pyright" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.293-py3-none-any.whl", hash = "sha256:afc05309e775a9869c864da4e8c0c7a3e3be9d8fe202e780c3bae981bbb13936"}, - {file = "pyright-1.1.293.tar.gz", hash = "sha256:9397fdfcbc684fe5b87abbf9c27f540fe3b8d75999a5f187519cae1d065be38c"}, + {file = "pyright-1.1.294-py3-none-any.whl", hash = "sha256:5b27e28a1cfc60cea707fd3b644769fa6dd0b194481cdcc2399cf2a51cc5a846"}, + {file = "pyright-1.1.294.tar.gz", hash = "sha256:fea5fed3d6a3f02259e622c901e86a7b8bcf237d35e1cdfe01d0e0723768dcb6"}, ] [package.dependencies] @@ -832,14 +832,14 @@ files = [ [[package]] name = "setuptools" -version = "67.2.0" +version = "67.3.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"}, - {file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"}, + {file = "setuptools-67.3.2-py3-none-any.whl", hash = "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"}, + {file = "setuptools-67.3.2.tar.gz", hash = "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012"}, ] [package.extras] @@ -1082,14 +1082,14 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6. [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] [[package]] diff --git a/pynecone/.templates/web/pcversion.txt b/pynecone/.templates/web/pcversion.txt deleted file mode 100644 index 99a25fc45..000000000 --- a/pynecone/.templates/web/pcversion.txt +++ /dev/null @@ -1 +0,0 @@ -0.1.16 \ No newline at end of file diff --git a/pynecone/.templates/web/pynecone.json b/pynecone/.templates/web/pynecone.json new file mode 100644 index 000000000..7800b188b --- /dev/null +++ b/pynecone/.templates/web/pynecone.json @@ -0,0 +1,3 @@ +{ + "version": "0.1.16" +} \ No newline at end of file diff --git a/pynecone/config.py b/pynecone/config.py index 7a9ef787f..3be9a522c 100644 --- a/pynecone/config.py +++ b/pynecone/config.py @@ -27,6 +27,9 @@ class Config(Base): # The redis url. redis_url: Optional[str] = None + # Telemetry opt-in. + telemetry_enabled: bool = True + # The deploy url. deploy_url: Optional[str] = None diff --git a/pynecone/constants.py b/pynecone/constants.py index fcf3f9525..58eae5ab1 100644 --- a/pynecone/constants.py +++ b/pynecone/constants.py @@ -55,9 +55,9 @@ NODE_MODULES = "node_modules" # The package lock file. PACKAGE_LOCK = "package-lock.json" # The pcversion template file. -PCVERSION_TEMPLATE_FILE = os.path.join(WEB_TEMPLATE_DIR, "pcversion.txt") +PCVERSION_TEMPLATE_FILE = os.path.join(WEB_TEMPLATE_DIR, "pynecone.json") # The pcversion app file. -PCVERSION_APP_FILE = os.path.join(WEB_DIR, "pcversion.txt") +PCVERSION_APP_FILE = os.path.join(WEB_DIR, "pynecone.json") # Commands to run the app. diff --git a/pynecone/pc.py b/pynecone/pc.py index 139115c4a..5635a3957 100644 --- a/pynecone/pc.py +++ b/pynecone/pc.py @@ -7,6 +7,7 @@ import httpx import typer from pynecone import constants, utils +from pynecone.telemetry import pynecone_telemetry # Create the app. cli = typer.Typer() @@ -43,6 +44,12 @@ def init(): # Initialize the .gitignore. utils.initialize_gitignore() + # Set the pynecone project hash. + utils.set_pynecone_project_hash() + + # Post a telemetry event. + pynecone_telemetry("init", utils.get_config().telemetry_enabled) + # Finish initializing the app. utils.console.log(f"[bold green]Finished Initializing: {app_name}") @@ -100,6 +107,9 @@ def run( frontend_cmd, backend_cmd = utils.run_frontend_prod, utils.run_backend_prod assert frontend_cmd and backend_cmd, "Invalid env" + # Post a telemetry event. + pynecone_telemetry(f"run-{env.value}", utils.get_config().telemetry_enabled) + # Run the frontend and backend. try: if frontend: @@ -174,6 +184,10 @@ def export( utils.console.rule("[bold]Compiling production app and preparing for export.") app = utils.get_app().app utils.export_app(app, backend=backend, frontend=frontend, zip=zipping) + + # Post a telemetry event. + pynecone_telemetry("export", utils.get_config().telemetry_enabled) + if zipping: utils.console.rule( """Backend & Frontend compiled. See [green bold]backend.zip[/green bold] diff --git a/pynecone/telemetry.py b/pynecone/telemetry.py index 28c62367d..73b4b493f 100644 --- a/pynecone/telemetry.py +++ b/pynecone/telemetry.py @@ -1,39 +1,98 @@ """Anonymous telemetry for Pynecone.""" +import json import multiprocessing import platform +from datetime import datetime +import httpx import psutil from pynecone import constants from pynecone.base import Base +def get_os() -> str: + """Get the operating system. + + Returns: + The operating system. + """ + return platform.system() + + +def get_python_version() -> str: + """Get the Python version. + + Returns: + The Python version. + """ + return platform.python_version() + + +def get_pynecone_version() -> str: + """Get the Pynecone version. + + Returns: + The Pynecone version. + """ + return constants.VERSION + + +def get_cpu_count() -> int: + """Get the number of CPUs. + + Returns: + The number of CPUs. + """ + return multiprocessing.cpu_count() + + +def get_memory() -> int: + """Get the total memory in MB. + + Returns: + The total memory in MB. + """ + return psutil.virtual_memory().total >> 20 + + class Telemetry(Base): """Anonymous telemetry for Pynecone.""" - user_os: str = "" - cpu_count: int = 0 - memory: int = 0 - pynecone_version: str = "" - python_version: str = "" + user_os: str = get_os() + cpu_count: int = get_cpu_count() + memory: int = get_memory() + pynecone_version: str = get_pynecone_version() + python_version: str = get_python_version() - def get_os(self) -> None: - """Get the operating system.""" - self.user_os = platform.system() - def get_python_version(self) -> None: - """Get the Python version.""" - self.python_version = platform.python_version() +def pynecone_telemetry(event: str, telemetry_enabled: bool) -> None: + """Send anonymous telemetry for Pynecone. - def get_pynecone_version(self) -> None: - """Get the Pynecone version.""" - self.pynecone_version = constants.VERSION - - def get_cpu_count(self) -> None: - """Get the number of CPUs.""" - self.cpu_count = multiprocessing.cpu_count() - - def get_memory(self) -> None: - """Get the total memory in MB.""" - self.memory = psutil.virtual_memory().total >> 20 + Args: + event: The event name. + telemetry_enabled: Whether to send the telemetry. + """ + try: + if telemetry_enabled: + telemetry = Telemetry() + with open(constants.PCVERSION_APP_FILE) as f: # type: ignore + pynecone_json = json.load(f) + distinct_id = pynecone_json["project_hash"] + post_hog = { + "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb", + "event": event, + "properties": { + "distinct_id": distinct_id, + "user_os": telemetry.user_os, + "pynecone_version": telemetry.pynecone_version, + "python_version": telemetry.python_version, + "cpu_count": telemetry.cpu_count, + "memory": telemetry.memory, + }, + "timestamp": datetime.utcnow().isoformat(), + } + httpx.post("https://app.posthog.com/capture/", json=post_hog) + except Exception: + pass diff --git a/pynecone/utils.py b/pynecone/utils.py index 492f2145a..ac7b34e74 100644 --- a/pynecone/utils.py +++ b/pynecone/utils.py @@ -476,7 +476,7 @@ def is_latest_template() -> bool: Whether the app is using the latest template. """ with open(constants.PCVERSION_TEMPLATE_FILE) as f: # type: ignore - template_version = f.read() + template_version = json.load(f)["version"] if not os.path.exists(constants.PCVERSION_APP_FILE): return False with open(constants.PCVERSION_APP_FILE) as f: # type: ignore @@ -484,6 +484,15 @@ def is_latest_template() -> bool: return app_version >= template_version +def set_pynecone_project_hash(): + """Write the hash of the Pynecone project to a PCVERSION_APP_FILE.""" + with open(constants.PCVERSION_APP_FILE) as f: # type: ignore + pynecone_json = json.load(f) + pynecone_json["project_hash"] = random.getrandbits(128) + with open(constants.PCVERSION_APP_FILE, "w") as f: + json.dump(pynecone_json, f, ensure_ascii=False) + + def export_app( app: App, backend: bool = True, frontend: bool = True, zip: bool = False ): diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 4ed83ac2d..533b70d3c 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -12,28 +12,19 @@ def test_telemetry(): tel = telemetry.Telemetry() # Check that the user OS is one of the supported operating systems. - tel.get_os() - assert tel.user_os is not None assert tel.user_os in ["Linux", "Darwin", "Java", "Windows"] # Check that the CPU count and memory are greater than 0. - tel.get_cpu_count() - assert tel.cpu_count > 0 # Check that the available memory is greater than 0 - tel.get_memory() - assert tel.memory > 0 # Check that the Pynecone version is not None. - tel.get_python_version() assert tel.pynecone_version is not None # Check that the Python version is greater than 3.7. - tel.get_pynecone_version() - assert tel.python_version is not None assert versiontuple(tel.python_version) >= versiontuple("3.7")