Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add telemetry #4441

Merged
merged 30 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6888833
add basic telemetry
valentinsulzer Sep 15, 2024
a643c7b
add posthog dependency
valentinsulzer Sep 15, 2024
c644876
ignore posthog url
valentinsulzer Sep 15, 2024
84a8a87
changelog
valentinsulzer Sep 15, 2024
cb2d075
use uuid to identify individual users
valentinsulzer Sep 15, 2024
a76aff7
reordering
valentinsulzer Sep 15, 2024
4bf6d80
add case where config is not found
valentinsulzer Sep 15, 2024
f7ec885
agriya comments and coverage
valentinsulzer Sep 16, 2024
abac35f
more no cover
valentinsulzer Sep 16, 2024
ad0faa1
merge develop, remove autoinstall
valentinsulzer Sep 26, 2024
025b019
merge develop
valentinsulzer Oct 3, 2024
a41d3ab
update telemetry to opt-in
valentinsulzer Oct 3, 2024
3939e7f
remove duplicate test
valentinsulzer Oct 3, 2024
b8f588b
coverage
valentinsulzer Oct 3, 2024
2e222c6
implement timeout
valentinsulzer Oct 5, 2024
bc53572
Merge branch 'develop' into telemetry
valentinsulzer Oct 6, 2024
a34ff4e
Merge branch 'develop' into telemetry
valentinsulzer Oct 11, 2024
aa9a66e
Merge branch 'develop' into telemetry
kratman Oct 11, 2024
d48f8c6
Update src/pybamm/config.py
valentinsulzer Oct 12, 2024
815bf91
10 second timeout
valentinsulzer Oct 18, 2024
47311e6
Merge branch 'telemetry' of github.com:pybamm-team/PyBaMM into telemetry
valentinsulzer Oct 18, 2024
fd567d2
Merge branch 'develop' into telemetry
valentinsulzer Oct 18, 2024
8d1e192
don't use inputimeout
valentinsulzer Oct 18, 2024
1a835a4
Merge branch 'develop' into telemetry
kratman Oct 18, 2024
75d403d
Merge branch 'develop' into telemetry
valentinsulzer Oct 28, 2024
1bf5b89
add more tests for config
valentinsulzer Oct 28, 2024
7df73c4
Merge branch 'develop' into telemetry
valentinsulzer Oct 30, 2024
215252b
Merge branch 'develop' into telemetry
valentinsulzer Nov 7, 2024
0183250
coverage
valentinsulzer Nov 7, 2024
1022c6b
Merge branch 'develop' into telemetry
valentinsulzer Nov 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ input/*

# simulation outputs
out/
config.py
matplotlibrc
*.pickle
*.sav
Expand Down
3 changes: 3 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/fundamentals/pybam

# Errors in docs/source/user_guide/index.md
file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/api_docs

# Telemetry
https://us.i.posthog.com
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

- Improved `QuickPlot` accuracy for simulations with Hermite interpolation. ([#4483](https://github.com/pybamm-team/PyBaMM/pull/4483))
- Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464))
- Added basic telemetry to record which functions are being run. See [Telemetry section in the User Guide](https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry) for more information. ([#4441](https://github.com/pybamm-team/PyBaMM/pull/4441))
- Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451))
- Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415))
- Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449))
- Added phase-dependent particle options to LAM
([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369))
- Added phase-dependent particle options to LAM ([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369))
- Added a lithium ion equivalent circuit model with split open circuit voltages for each electrode (`SplitOCVR`). ([#4330](https://github.com/pybamm-team/PyBaMM/pull/4330))
- Added the `pybamm.DiscreteTimeSum` expression node to sum an expression over a sequence of data times, and accompanying `pybamm.DiscreteTimeData` class to store the data times and values ([#4501](https://github.com/pybamm-team/PyBaMM/pull/4501))

Expand Down
5 changes: 5 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ def set_random_seed():
@pytest.fixture(autouse=True)
def set_debug_value():
pybamm.settings.debug_mode = True


@pytest.fixture(autouse=True)
def disable_telemetry():
pybamm.telemetry.disable()
9 changes: 9 additions & 0 deletions docs/source/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ glob:
../examples/notebooks/creating_models/5-half-cell-model.ipynb
../examples/notebooks/creating_models/6-a-simple-SEI-model.ipynb
```

# Telemetry

PyBaMM optionally collects anonymous usage data to help improve the library. This telemetry is opt-in and can be easily disabled. Here's what you need to know:

- **What is collected**: Basic usage information like PyBaMM version, Python version, and which functions are run.
- **Why**: To understand how PyBaMM is used and prioritize development efforts.
- **Opt-out**: To disable telemetry, set the environment variable `PYBAMM_DISABLE_TELEMETRY=true` (or any value other than `false`) or use `pybamm.telemetry.disable()` in your code.
- **Privacy**: No personal information (name, email, etc) or sensitive information (parameter values, simulation results, etc) is ever collected.
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ dependencies = [
"typing-extensions>=4.10.0",
"pandas>=1.5.0",
"pooch>=1.8.1",
"posthog",
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved
"inputimeout",
]

[project.urls]
Expand Down
9 changes: 7 additions & 2 deletions src/pybamm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@
# Batch Study
from .batch_study import BatchStudy

# Callbacks
from . import callbacks
# Callbacks, telemetry, config
from . import callbacks, telemetry, config

# Pybamm Data manager using pooch
from .pybamm_data import DataLoader
Expand All @@ -208,6 +208,7 @@
"batch_study",
"callbacks",
"citations",
"config",
"discretisations",
"doc_utils",
"experiment",
Expand All @@ -223,8 +224,12 @@
"simulation",
"solvers",
"spatial_methods",
"telemetry",
"type_definitions",
"util",
"version",
"pybamm_data",
]


pybamm.config.generate()
141 changes: 141 additions & 0 deletions src/pybamm/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import uuid
import os
import platformdirs
from pathlib import Path
import pybamm
from inputimeout import inputimeout, TimeoutOccurred


def is_running_tests(): # pragma: no cover
"""
Detect if the code is being run as part of a test suite or building docs with Sphinx.

Returns:
bool: True if running tests or building docs, False otherwise.
"""
import sys
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved

# Check if pytest or unittest is running
if any(
test_module in sys.modules for test_module in ["pytest", "unittest", "nose"]
):
return True

# Check for GitHub Actions environment variable
if "GITHUB_ACTIONS" in os.environ:
return True

# Check for other common CI environment variables
ci_env_vars = ["CI", "TRAVIS", "CIRCLECI", "JENKINS_URL", "GITLAB_CI"]
if any(var in os.environ for var in ci_env_vars):
return True

# Check for common test runner names in command-line arguments
test_runners = ["pytest", "unittest", "nose", "trial", "nox", "tox"]
if any(runner in sys.argv[0].lower() for runner in test_runners):
return True
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved

# Check if building docs with Sphinx
if "sphinx" in sys.modules:
return True
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved

return False


def ask_user_opt_in(timeout=10):
"""
Ask the user if they want to opt in to telemetry.

Parameters
----------
timeout: float, optional
The timeout for the user to respond to the prompt. Default is 10 seconds.

Returns
-------
bool
True if the user opts in, False otherwise.
"""
print(
"PyBaMM can collect usage data and send it to the PyBaMM team to "
"help us improve the software.\n"
"We do not collect any sensitive information such as models, parameters, "
"or simulation results - only information on which parts of the code are "
"being used and how frequently.\n"
"This is entirely optional and does not impact the functionality of PyBaMM.\n"
"For more information, see https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry"
)

while True:
martinjrobins marked this conversation as resolved.
Show resolved Hide resolved
try:
user_input = (
inputimeout(
prompt="Do you want to enable telemetry? (Y/n): ", timeout=timeout
)
.strip()
.lower()
)
except TimeoutOccurred:
print("\nTimeout reached. Defaulting to not enabling telemetry.")
return False

if user_input in ["yes", "y", ""]:
return True
elif user_input in ["no", "n"]:
return False
else:
print("\nInvalid input. Please enter 'yes'/'y' or 'no'/'n'.")
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved


def generate(): # pragma: no cover
if is_running_tests():
return

# Check if the config file already exists
if read() is not None:
return

# Ask the user if they want to opt in to telemetry
opt_in = ask_user_opt_in()
config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml"
write_uuid_to_file(config_file, opt_in)

if opt_in:
pybamm.telemetry.capture("user-opted-in")


def read(): # pragma: no cover
config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml"
return read_uuid_from_file(config_file)


def write_uuid_to_file(config_file, opt_in):
# Create the directory if it doesn't exist
config_file.parent.mkdir(parents=True, exist_ok=True)

# Write the UUID to the config file in YAML format
with open(config_file, "w") as f:
f.write("pybamm:\n")
f.write(f" enable_telemetry: {opt_in}\n")
if opt_in:
unique_id = uuid.uuid4()
f.write(f" uuid: {unique_id}\n")


def read_uuid_from_file(config_file):
# Check if the config file exists
if not config_file.exists(): # pragma: no cover
return None

# Read the UUID from the config file
with open(config_file) as f:
content = f.read().strip()

# Extract the UUID using YAML parsing
try:
import yaml

config = yaml.safe_load(content)
return config["pybamm"]
except (yaml.YAMLError, ValueError): # pragma: no cover
return None
3 changes: 3 additions & 0 deletions src/pybamm/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import warnings
from functools import lru_cache
from datetime import timedelta
import pybamm.telemetry
from pybamm.util import import_optional_dependency

from pybamm.expression_tree.operations.serialise import Serialise
Expand Down Expand Up @@ -450,6 +451,8 @@ def solve(
Additional key-word arguments passed to `solver.solve`.
See :meth:`pybamm.BaseSolver.solve`.
"""
pybamm.telemetry.capture("simulation-solved")

# Setup
if solver is None:
solver = self._solver
Expand Down
36 changes: 36 additions & 0 deletions src/pybamm/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from posthog import Posthog
import os
import pybamm
import sys

_posthog = Posthog(
# this is the public, write only API key, so it's ok to include it here
project_api_key="phc_bLZKBW03XjgiRhbWnPsnKPr0iw0z03fA6ZZYjxgW7ej",
host="https://us.i.posthog.com",
)


def disable():
_posthog.disabled = True


_opt_out = os.getenv("PYBAMM_DISABLE_TELEMETRY", "false").lower()
valentinsulzer marked this conversation as resolved.
Show resolved Hide resolved
if _opt_out != "false": # pragma: no cover
disable()


def capture(event): # pragma: no cover
# don't capture events in automated testing
if pybamm.config.is_running_tests() or _posthog.disabled:
return

properties = {
"python_version": sys.version,
"pybamm_version": pybamm.__version__,
}

config = pybamm.config.read()
if config:
if config["enable_telemetry"]:
user_id = config["uuid"]
_posthog.capture(user_id, event, properties=properties)
56 changes: 56 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest
from inputimeout import TimeoutOccurred

import pybamm
import uuid


class TestConfig:
@pytest.mark.parametrize("write_opt_in", [True, False])
def test_write_read_uuid(self, tmp_path, write_opt_in):
# Create a temporary file path
config_file = tmp_path / "config.yml"

# Call the function to write UUID to file
pybamm.config.write_uuid_to_file(config_file, write_opt_in)

# Check that the file was created
assert config_file.exists()

# Read the UUID using the read_uuid_from_file function
config_dict = pybamm.config.read_uuid_from_file(config_file)
# Check that the UUID was read successfully
if write_opt_in:
assert config_dict["enable_telemetry"] is True
assert "uuid" in config_dict

# Verify that the UUID is valid
try:
uuid.UUID(config_dict["uuid"])
except ValueError:
pytest.fail("Invalid UUID format")
else:
assert config_dict["enable_telemetry"] is False

@pytest.mark.parametrize("user_opted_in, user_input", [(True, "y"), (False, "n")])
def test_ask_user_opt_in(self, monkeypatch, user_opted_in, user_input):
# Mock the inputimeout function to return invalid input first, then valid input
inputs = iter(["invalid", user_input])
monkeypatch.setattr(
"pybamm.config.inputimeout", lambda prompt, timeout: next(inputs)
)

# Call the function to ask the user if they want to opt in
opt_in = pybamm.config.ask_user_opt_in()
assert opt_in is user_opted_in

def test_ask_user_opt_in_timeout(self, monkeypatch):
# Mock the inputimeout function to raise a TimeoutOccurred exception
def mock_inputimeout(*args, **kwargs):
raise TimeoutOccurred

monkeypatch.setattr("pybamm.config.inputimeout", mock_inputimeout)

# Call the function to ask the user if they want to opt in
opt_in = pybamm.config.ask_user_opt_in()
assert opt_in is False
Loading