Repository: dbt-labs/dbt-core

Issue:
[CT-3560] Remove usage of flags and tracking in WritableManifest
### Housekeeping

- [X] I am a maintainer of dbt-core

### Short description

Remove usage of dbt.tracking and dbt.flags from dbt/artifacts/schemas/manifest.py. If they were to be left in, these would represent a dependency on dbt internals from an upstream component, and it could not be packaged separately. 

Current usage: https://github.com/dbt-labs/dbt-core/pull/9377/files#diff-a0f1c7a8ab56c28a02547801a741a8139854ac72e0e2e8b556f9ebb7d5b9f7ffR15-R16

### Acceptance criteria

dbt/artifacts does not contain imports or usage of dbt.tracking or dbt.flags.

### Impact to Other Teams

N/A - should be a refactor. 

### Will backports be required?

nope

### Context
* The usage of flags here is quite minimal. get_flags().SEND_ANONYMOUS_USAGE_STATS is used as a reference to a global. It could instead be obtained by: 
   * having the core caller set send_anonymous_usage_stats based on the global value instead of relying on the post_init
   * https://github.com/dbt-labs/dbt-core/pull/9377/files#diff-a0f1c7a8ab56c28a02547801a741a8139854ac72e0e2e8b556f9ebb7d5b9f7ffR78

* tracking usage is similar: tracking.active_user.id is referenced as a global when it could be set when the caller provides `user_id`
  *  https://github.com/dbt-labs/dbt-core/pull/9377/files#diff-a0f1c7a8ab56c28a02547801a741a8139854ac72e0e2e8b556f9ebb7d5b9f7ffR75

* only one callsite for WritableManifest instantiation: https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/contracts/graph/manifest.py#L1031

* Testing: ensure tests exist for setting of user_id and anonymous_usage_stats on manifest.json artifact



Discussion:

Resolving patch:
diff --git a/core/dbt/artifacts/schemas/manifest/v12/manifest.py b/core/dbt/artifacts/schemas/manifest/v12/manifest.py
--- a/core/dbt/artifacts/schemas/manifest/v12/manifest.py
+++ b/core/dbt/artifacts/schemas/manifest/v12/manifest.py
@@ -20,8 +20,6 @@
 )
 
 # TODO: remove usage of dbt modules other than dbt.artifacts
-from dbt import tracking
-from dbt.flags import get_flags
 from dbt.contracts.graph.nodes import (
     GraphMemberNode,
     ManifestNode,
@@ -70,16 +68,6 @@ class ManifestMetadata(BaseArtifactMetadata):
         metadata=dict(description="The type name of the adapter"),
     )
 
-    def __post_init__(self):
-        if tracking.active_user is None:
-            return
-
-        if self.user_id is None:
-            self.user_id = tracking.active_user.id
-
-        if self.send_anonymous_usage_stats is None:
-            self.send_anonymous_usage_stats = get_flags().SEND_ANONYMOUS_USAGE_STATS
-
     @classmethod
     def default(cls):
         return cls(
diff --git a/core/dbt/config/runtime.py b/core/dbt/config/runtime.py
--- a/core/dbt/config/runtime.py
+++ b/core/dbt/config/runtime.py
@@ -15,6 +15,7 @@
     Type,
 )
 
+from dbt import tracking
 from dbt.adapters.factory import get_include_paths, get_relation_class_by_name
 from dbt.adapters.contracts.connection import AdapterRequiredConfig, Credentials, HasCredentials
 from dbt.adapters.contracts.relation import ComponentName
@@ -283,6 +284,10 @@ def get_metadata(self) -> ManifestMetadata:
         return ManifestMetadata(
             project_name=self.project_name,
             project_id=self.hashed_name(),
+            user_id=tracking.active_user.id if tracking.active_user else None,
+            send_anonymous_usage_stats=get_flags().SEND_ANONYMOUS_USAGE_STATS
+            if tracking.active_user
+            else None,
             adapter_type=self.credentials.type,
         )
 
diff --git a/core/dbt/contracts/graph/manifest.py b/core/dbt/contracts/graph/manifest.py
--- a/core/dbt/contracts/graph/manifest.py
+++ b/core/dbt/contracts/graph/manifest.py
@@ -23,6 +23,7 @@
 )
 from typing_extensions import Protocol
 
+from dbt import tracking
 from dbt.contracts.graph.nodes import (
     BaseNode,
     Documentation,
@@ -43,6 +44,7 @@
     UnitTestFileFixture,
 )
 from dbt.contracts.graph.unparsed import SourcePatch, UnparsedVersion
+from dbt.flags import get_flags
 
 # to preserve import paths
 from dbt.artifacts.resources import NodeVersion, DeferRelation
@@ -1022,6 +1024,10 @@ def build_group_map(self):
                     group_map[node.group].append(node.unique_id)
         self.group_map = group_map
 
+    def fill_tracking_metadata(self):
+        self.metadata.user_id = tracking.active_user.id if tracking.active_user else None
+        self.metadata.send_anonymous_usage_stats = get_flags().SEND_ANONYMOUS_USAGE_STATS
+
     @classmethod
     def _map_nodes_to_map_resources(cls, nodes_map: MutableMapping[str, NodeClassT]):
         return {node_id: node.to_resource() for node_id, node in nodes_map.items()}
@@ -1029,6 +1035,8 @@ def _map_nodes_to_map_resources(cls, nodes_map: MutableMapping[str, NodeClassT])
     def writable_manifest(self) -> "WritableManifest":
         self.build_parent_and_child_maps()
         self.build_group_map()
+        self.fill_tracking_metadata()
+
         return WritableManifest(
             nodes=self.nodes,
             sources=self._map_nodes_to_map_resources(self.sources),
@@ -1578,7 +1586,12 @@ def __reduce_ex__(self, protocol):
 class MacroManifest(MacroMethods):
     def __init__(self, macros) -> None:
         self.macros = macros
-        self.metadata = ManifestMetadata()
+        self.metadata = ManifestMetadata(
+            user_id=tracking.active_user.id if tracking.active_user else None,
+            send_anonymous_usage_stats=get_flags().SEND_ANONYMOUS_USAGE_STATS
+            if tracking.active_user
+            else None,
+        )
         # This is returned by the 'graph' context property
         # in the ProviderContext class.
         self.flat_graph: Dict[str, Any] = {}


Test patch:
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -14,7 +14,7 @@
 import dbt.config
 from dbt.constants import DEPENDENCIES_FILE_NAME, PACKAGES_FILE_NAME
 import dbt.exceptions
-import dbt.tracking
+from dbt import tracking
 from dbt import flags
 from dbt.adapters.factory import load_plugin
 from dbt.adapters.postgres import PostgresCredentials
@@ -1135,6 +1135,20 @@ def test__warn_for_unused_resource_config_paths_empty(self):
         finally:
             dbt.flags.WARN_ERROR = False
 
+    @mock.patch.object(tracking, "active_user")
+    def test_get_metadata(self, mock_user):
+        project = self.get_project()
+        profile = self.get_profile()
+        config = dbt.config.RuntimeConfig.from_parts(project, profile, self.args)
+
+        mock_user.id = "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf"
+        set_from_args(Namespace(SEND_ANONYMOUS_USAGE_STATS=False), None)
+
+        metadata = config.get_metadata()
+        # ensure user_id and send_anonymous_usage_stats are set correctly
+        self.assertEqual(metadata.user_id, mock_user.id)
+        self.assertFalse(metadata.send_anonymous_usage_stats)
+
 
 class TestRuntimeConfigWithConfigs(BaseConfigTest):
     def setUp(self):
diff --git a/tests/unit/test_manifest.py b/tests/unit/test_manifest.py
--- a/tests/unit/test_manifest.py
+++ b/tests/unit/test_manifest.py
@@ -359,8 +359,9 @@ def tearDown(self):
         del os.environ["DBT_ENV_CUSTOM_ENV_key"]
         reset_metadata_vars()
 
+    @mock.patch.object(tracking, "active_user")
     @freezegun.freeze_time("2018-02-14T09:15:13Z")
-    def test_no_nodes(self):
+    def test_no_nodes(self, mock_user):
         manifest = Manifest(
             nodes={},
             sources={},
@@ -377,6 +378,8 @@ def test_no_nodes(self):
         )
 
         invocation_id = dbt_common.invocation._INVOCATION_ID
+        mock_user.id = "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf"
+        set_from_args(Namespace(SEND_ANONYMOUS_USAGE_STATS=False), None)
         self.assertEqual(
             manifest.writable_manifest().to_dict(omit_none=True),
             {
@@ -396,6 +399,8 @@ def test_no_nodes(self):
                     "dbt_version": dbt.version.__version__,
                     "env": {ENV_KEY_NAME: "value"},
                     "invocation_id": invocation_id,
+                    "send_anonymous_usage_stats": False,
+                    "user_id": "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf",
                 },
                 "docs": {},
                 "disabled": {},
@@ -406,7 +411,10 @@ def test_no_nodes(self):
         )
 
     @freezegun.freeze_time("2018-02-14T09:15:13Z")
-    def test_nested_nodes(self):
+    @mock.patch.object(tracking, "active_user")
+    def test_nested_nodes(self, mock_user):
+        set_from_args(Namespace(SEND_ANONYMOUS_USAGE_STATS=False), None)
+        mock_user.id = "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf"
         nodes = deepcopy(self.nested_nodes)
         manifest = Manifest(
             nodes=nodes,
@@ -422,6 +430,8 @@ def test_nested_nodes(self):
         )
         serialized = manifest.writable_manifest().to_dict(omit_none=True)
         self.assertEqual(serialized["metadata"]["generated_at"], "2018-02-14T09:15:13Z")
+        self.assertEqual(serialized["metadata"]["user_id"], mock_user.id)
+        self.assertFalse(serialized["metadata"]["send_anonymous_usage_stats"])
         self.assertEqual(serialized["docs"], {})
         self.assertEqual(serialized["disabled"], {})
         parent_map = serialized["parent_map"]
@@ -511,28 +521,6 @@ def test_build_flat_graph(self):
         for node in flat_nodes.values():
             self.assertEqual(frozenset(node), REQUIRED_PARSED_NODE_KEYS)
 
-    @mock.patch.object(tracking, "active_user")
-    def test_metadata(self, mock_user):
-        mock_user.id = "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf"
-        dbt_common.invocation._INVOCATION_ID = "01234567-0123-0123-0123-0123456789ab"
-        set_from_args(Namespace(SEND_ANONYMOUS_USAGE_STATS=False), None)
-        now = datetime.utcnow()
-        self.assertEqual(
-            ManifestMetadata(
-                project_id="098f6bcd4621d373cade4e832627b4f6",
-                adapter_type="postgres",
-                generated_at=now,
-            ),
-            ManifestMetadata(
-                project_id="098f6bcd4621d373cade4e832627b4f6",
-                user_id="cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf",
-                send_anonymous_usage_stats=False,
-                adapter_type="postgres",
-                generated_at=now,
-                invocation_id="01234567-0123-0123-0123-0123456789ab",
-            ),
-        )
-
     @mock.patch.object(tracking, "active_user")
     @freezegun.freeze_time("2018-02-14T09:15:13Z")
     def test_no_nodes_with_metadata(self, mock_user):
@@ -543,6 +531,8 @@ def test_no_nodes_with_metadata(self, mock_user):
             project_id="098f6bcd4621d373cade4e832627b4f6",
             adapter_type="postgres",
             generated_at=datetime.utcnow(),
+            user_id="cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf",
+            send_anonymous_usage_stats=False,
         )
         manifest = Manifest(
             nodes={},
@@ -884,8 +874,11 @@ def setUp(self):
     def tearDown(self):
         del os.environ["DBT_ENV_CUSTOM_ENV_key"]
 
+    @mock.patch.object(tracking, "active_user")
     @freezegun.freeze_time("2018-02-14T09:15:13Z")
-    def test_no_nodes(self):
+    def test_no_nodes(self, mock_user):
+        mock_user.id = "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf"
+        set_from_args(Namespace(SEND_ANONYMOUS_USAGE_STATS=False), None)
         metadata = ManifestMetadata(
             generated_at=datetime.utcnow(), invocation_id="01234567-0123-0123-0123-0123456789ab"
         )
@@ -921,6 +914,8 @@ def test_no_nodes(self):
                     "dbt_version": dbt.version.__version__,
                     "invocation_id": "01234567-0123-0123-0123-0123456789ab",
                     "env": {ENV_KEY_NAME: "value"},
+                    "send_anonymous_usage_stats": False,
+                    "user_id": "cfc9500f-dc7f-4c83-9ea7-2c581c1b38cf",
                 },
                 "docs": {},
                 "disabled": {},