From 267872b7e4aa9b55c4e7fc6238c8bde5d4969dab Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 30 Oct 2024 13:15:57 -0700 Subject: [PATCH] feat: add opt-in analytics collection with privacy notice --- aider/analytics.py | 46 ++++++++++++++++++++-------- aider/main.py | 29 +++++++++++++++--- aider/urls.py | 1 + aider/website/docs/more/analytics.md | 9 +++--- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/aider/analytics.py b/aider/analytics.py index 80dadee6b..c492506a9 100644 --- a/aider/analytics.py +++ b/aider/analytics.py @@ -17,32 +17,53 @@ posthog_host = "https://us.i.posthog.com" class Analytics: + # providers mp = None ph = None + + # saved user_id = None permanently_disable = None + asked_opt_in = None + + # ephemeral logfile = None - def __init__(self, enable=False, logfile=None, permanently_disable=False): + def __init__(self, logfile=None, permanently_disable=False): self.logfile = logfile - self.asked_opt_in = False self.get_or_create_uuid() - if not enable or self.permanently_disable or permanently_disable: + if self.permanently_disable or permanently_disable or not self.asked_opt_in: self.disable(permanently_disable) + + def enable(self): + if not self.user_id: + self.disable(False) return - if self.user_id and not self.permanently_disable: - self.mp = Mixpanel(mixpanel_project_token) - self.ph = Posthog(project_api_key=posthog_project_api_key, host=posthog_host) + if self.permanently_disable: + self.disable(False) + return - def disable(self, permanently_disable): + if not self.asked_opt_in: + self.disable(False) + return + + self.mp = Mixpanel(mixpanel_project_token) + self.ph = Posthog(project_api_key=posthog_project_api_key, host=posthog_host) + + def disable(self, permanently): self.mp = None self.ph = None - if permanently_disable and not self.permanently_disable: + + if permanently: + self.asked_opt_in = True self.permanently_disable = True self.save_data() + def need_to_ask(self): + return not self.asked_opt_in and not self.permanently_disable + def get_data_file_path(self): data_file = Path.home() / ".aider" / "analytics.json" data_file.parent.mkdir(parents=True, exist_ok=True) @@ -64,8 +85,8 @@ class Analytics: self.permanently_disable = data.get("permanently_disable") self.user_id = data.get("uuid") self.asked_opt_in = data.get("asked_opt_in", False) - except json.decoder.JSONDecodeError: - pass + except (json.decoder.JSONDecodeError, OSError): + self.disable(permanently=False) def save_data(self): data_file = self.get_data_file_path() @@ -75,6 +96,7 @@ class Analytics: asked_opt_in=self.asked_opt_in, ) + # Allow exceptions; crash if we can't record permanently_disabled=True, etc data_file.write_text(json.dumps(data, indent=4)) def get_system_info(self): @@ -110,10 +132,10 @@ class Analytics: properties["aider_version"] = __version__ if self.mp: - self.mp.track(self.user_id, event_name, properties) + self.mp.track(self.user_id, event_name, dict(properties)) if self.ph: - self.ph.capture(self.user_id, event_name, properties) + self.ph.capture(self.user_id, event_name, dict(properties)) if self.logfile: log_entry = { diff --git a/aider/main.py b/aider/main.py index e618c2bb1..32504b31b 100644 --- a/aider/main.py +++ b/aider/main.py @@ -430,7 +430,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F args = parser.parse_args(argv) if args.analytics_disable: - analytics = Analytics(enable=False, permanently_disable=True) + analytics = Analytics(permanently_disable=True) print("Analytics have been permanently disabled.") return @@ -495,9 +495,28 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F io = get_io(False) io.tool_warning("Terminal does not support pretty output (UnicodeDecodeError)") - analytics = Analytics( - args.analytics, logfile=args.analytics_log, permanently_disable=args.analytics_disable - ) + analytics = Analytics(logfile=args.analytics_log, permanently_disable=args.analytics_disable) + if args.analytics: + if analytics.need_to_ask(): + io.tool_output( + "Aider respects your privacy and never collects your code, prompts, chats, keys or" + " any personal info." + ) + io.tool_output(f"For more info: {urls.analytics}") + disable = not io.confirm_ask( + "Allow collection of anonymous analytics to help improve aider?" + ) + + analytics.asked_opt_in = True + if disable: + analytics.disable(permanently=True) + io.tool_output("Analytics have been permanently disabled.") + + analytics.save_data() + io.tool_output() + + analytics.enable() + analytics.event("launched") if args.gui and not return_coder: @@ -796,7 +815,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if args.exit: return - analytics.event("cli session", main_model=main_model) + analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format) thread = threading.Thread(target=load_slow_imports) thread.daemon = True diff --git a/aider/urls.py b/aider/urls.py index 52ed0ec10..63870c829 100644 --- a/aider/urls.py +++ b/aider/urls.py @@ -11,3 +11,4 @@ large_repos = "https://aider.chat/docs/faq.html#can-i-use-aider-in-a-large-mono- github_issues = "https://github.com/Aider-AI/aider/issues/new" git_index_version = "https://github.com/Aider-AI/aider/issues/211" install_properly = "https://aider.chat/docs/troubleshooting/imports.html" +analytics = "https://aider.chat/docs/more/analytics.html" diff --git a/aider/website/docs/more/analytics.md b/aider/website/docs/more/analytics.md index f7ad32c94..52aad298f 100644 --- a/aider/website/docs/more/analytics.md +++ b/aider/website/docs/more/analytics.md @@ -5,11 +5,12 @@ nav_order: 500 # Analytics -Aider collects anonymous analytics that are used to help +Aider can collect anonymous analytics to help improve aider's ability to work with LLMs, edit code and complete user requests. -**Analytics are currently turned off by default**, but are -expected to be turned on by default in -a future release. + +## Opt-in + +Analytics are only collected if you agree and opt-in. ## Anonymous, no personal info