Repository: meltano/meltano

Issue:
Telemetry - State change not firing
Related to https://github.com/meltano/meltano/issues/6032

I havent seen any state change events come through yet and I tried my best to get them to fire locally with no luck. I confirmed I didnt have any anonymous usage settings in my environment, then ran these steps with my pip install version 2.0.0:

1. Init project `MELTANO_SNOWPLOW_COLLECTOR_ENDPOINTS='["http://localhost:9090"]' meltano init test_opt_out_1`
1. `cd test_opt_out_1` Note I dont see an analytics.json file in my .meltano folder
```yaml
version: 1
default_environment: dev
project_id: 327b8c41-e2ce-4dce-a70c-fac6adeb42d9
environments:
- name: dev
- name: staging
- name: prod
```
3. Add tap-csv `MELTANO_SNOWPLOW_COLLECTOR_ENDPOINTS='["http://localhost:9090"]' meltano add extractor tap-csv`
- I see a structured event fire (unstruct events were added but havent been released yet)
4. Copy/Paste config to tap-csv so it will run without failing
```yaml
version: 1
default_environment: dev
project_id: 327b8c41-e2ce-4dce-a70c-fac6adeb42d9
plugins:
  extractors:
  - name: tap-csv
    variant: meltanolabs
    pip_url: git+https://github.com/MeltanoLabs/tap-csv.git
environments:
- name: dev
  config:
    plugins:
      extractors:
      - name: tap-csv
        config:
          files:
          - entity: customers
            path: [MY_PATH]/customers.csv
            keys:
            - id
- name: staging
- name: prod
```
6. Invoke tap-csv `MELTANO_SNOWPLOW_COLLECTOR_ENDPOINTS='["http://localhost:9090"]' meltano invoke tap-csv`
- I see unstruct start and complete events plus the legacy struct event
7. Add `send_anonymous_usage_stats: false` to my meltano.yml and run again `MELTANO_SNOWPLOW_COLLECTOR_ENDPOINTS='["http://localhost:9090"]' meltano invoke tap-csv` 
- No events fired at all


cc @tayloramurphy @aaronsteers @WillDaSilva @pandemicsyn 


Discussion:

Resolving patch:
diff --git a/src/meltano/core/tracking/tracker.py b/src/meltano/core/tracking/tracker.py
--- a/src/meltano/core/tracking/tracker.py
+++ b/src/meltano/core/tracking/tracker.py
@@ -157,14 +157,8 @@ def telemetry_state_change_check(
         Args:
             stored_telemetry_settings: the prior analytics settings
         """
-        save_settings = False
-
-        if (
-            stored_telemetry_settings.send_anonymous_usage_stats is None
-            and not self.send_anonymous_usage_stats
-        ):
-            # Do nothing. Tracking is disabled and no tracking marker to update.
-            return
+        # If `stored_telemetry_settings` is all `None`, then the settings have never been saved yet
+        save_settings = all(x is None for x in stored_telemetry_settings)
 
         if (
             stored_telemetry_settings.project_id is not None
@@ -183,7 +177,7 @@ def telemetry_state_change_check(
         ):
             # Telemetry state has changed
             self.track_telemetry_state_change_event(
-                "project_id",
+                "send_anonymous_usage_stats",
                 stored_telemetry_settings.send_anonymous_usage_stats,
                 self.send_anonymous_usage_stats,
             )
@@ -316,6 +310,9 @@ def track_telemetry_state_change_event(
             from_value: the old value
             to_value: the new value
         """
+        if self.snowplow_tracker is None:
+            return # The Snowplow tracker is not available (e.g. because no endpoints are set)
+
         logger.debug(
             "Telemetry state change detected. A one-time "
             + "'telemetry_state_change' event will now be sent.",


Test patch:
diff --git a/tests/meltano/core/tracking/test_tracker.py b/tests/meltano/core/tracking/test_tracker.py
new file mode 100644
--- /dev/null
+++ b/tests/meltano/core/tracking/test_tracker.py
@@ -0,0 +1,41 @@
+import json
+from unittest import mock
+
+from meltano.core.tracking.tracker import TelemetrySettings, Tracker
+
+
+class TestTracker:
+    def test_telemetry_state_change_check(self, project):
+        with mock.patch.object(Tracker, "save_telemetry_settings") as mocked_method:
+            Tracker(project)
+        assert mocked_method.call_count == 1
+
+    def test_update_analytics_json(self, project):
+        tracker = Tracker(project)
+        inital_send_anonymous_usage_stats = tracker.send_anonymous_usage_stats
+
+        # Check the inital value of `send_anonymous_usage_stats`
+        with open(project.meltano_dir() / "analytics.json") as analytics_json_file:
+            analytics_json_pre = json.load(analytics_json_file)
+        assert (
+            inital_send_anonymous_usage_stats
+            == analytics_json_pre["send_anonymous_usage_stats"]
+        )
+
+        # Flip the value of `send_anonymous_usage_stats`
+        tracker.send_anonymous_usage_stats = not inital_send_anonymous_usage_stats
+        tracker.telemetry_state_change_check(
+            TelemetrySettings(
+                analytics_json_pre["client_id"],
+                analytics_json_pre["project_id"],
+                analytics_json_pre["send_anonymous_usage_stats"],
+            )
+        )
+
+        # Ensure `send_anonymous_usage_stats` has been flipped on disk
+        with open(project.meltano_dir() / "analytics.json") as analytics_json_file:
+            analytics_json_post = json.load(analytics_json_file)
+        assert (
+            inital_send_anonymous_usage_stats
+            != analytics_json_post["send_anonymous_usage_stats"]
+        )