Repository: PrefectHQ/prefect

Issue:
Add flow run information to Slack notification
The built-in [Slack notifier](https://docs.prefect.io/core/advanced_tutorials/slack-notifications.html#slack-notifications) is neat but doesn't convey quite enough information to be useful, especially in a "production" context where many users might be running flows simultaneously.

Including information about the specific flow *run* (for example, the run name or a link to the flow run in the Cloud UI) would go a long way towards making the notification more informative+actionable. It might also be nice to be able to pass some sort of formatter function to `slack_notifier` that could customize the message at run-time based on information about the task/flow, some variables in the context, things like that.

cc @cicdw who it sounds like thinks this would be pretty easy 😁


Discussion:
Full support for this customizability! We have a good amount of information available to us in context that the notifier could use and for the Cloud UI URL portion we already do something similar in the CLI when creating a run and it should be pretty straightforward to adopt here:

https://github.com/PrefectHQ/prefect/blob/b49db66c23132dea9471663026ada21e80b1b2dc/src/prefect/cli/run.py#L171-L182

Resolving patch:
diff --git a/src/prefect/cli/run.py b/src/prefect/cli/run.py
--- a/src/prefect/cli/run.py
+++ b/src/prefect/cli/run.py
@@ -1,11 +1,9 @@
 import json
-import re
 import time
 
 import click
 from tabulate import tabulate
 
-from prefect import config
 from prefect.client import Client
 from prefect.utilities.graphql import EnumValue, with_args
 
@@ -168,17 +166,7 @@ def cloud(
     if no_url:
         click.echo("Flow Run ID: {}".format(flow_run_id))
     else:
-        # Generate direct link to Cloud run
-        tenant_slug = client.get_default_tenant_slug()
-
-        url = (
-            re.sub("api-", "", config.cloud.api)
-            if re.search("api-", config.cloud.api)
-            else re.sub("api", "cloud", config.cloud.api)
-        )
-
-        flow_run_url = "/".join([url.rstrip("/"), tenant_slug, "flow-run", flow_run_id])
-
+        flow_run_url = client.get_cloud_url("flow-run", flow_run_id)
         click.echo("Flow Run: {}".format(flow_run_url))
 
     if watch:
diff --git a/src/prefect/client/client.py b/src/prefect/client/client.py
--- a/src/prefect/client/client.py
+++ b/src/prefect/client/client.py
@@ -654,33 +654,73 @@ def register(
 
         if not no_url:
             # Generate direct link to Cloud flow
-            tenant_slug = self.get_default_tenant_slug()
-
-            url = (
-                re.sub("api-", "", prefect.config.cloud.api)
-                if re.search("api-", prefect.config.cloud.api)
-                else re.sub("api", "cloud", prefect.config.cloud.api)
-            )
-
-            flow_url = "/".join([url.rstrip("/"), tenant_slug, "flow", flow_id])
+            flow_url = self.get_cloud_url("flow", flow_id)
 
             print("Flow: {}".format(flow_url))
 
         return flow_id
 
-    def get_default_tenant_slug(self) -> str:
+    def get_cloud_url(self, subdirectory: str, id: str, as_user: bool = True) -> str:
+        """
+        Convenience method for creating Prefect Cloud URLs for a given subdirectory.
+
+        Args:
+            - subdirectory (str): the subdirectory to use (e.g., `"flow-run"`)
+            - id (str): the ID of the page
+            - as_user (bool, optional): whether this query is being made from a USER scoped token;
+                defaults to `True`. Only used internally for queries made from RUNNERs
+
+        Returns:
+            - str: the URL corresponding to the appropriate base URL, tenant slug, subdirectory and ID
+
+        Example:
+
+        ```python
+        from prefect import Client
+
+        client = Client()
+        client.get_cloud_url("flow-run", "424242-ca-94611-111-55")
+        # returns "https://cloud.prefect.io/my-tenant-slug/flow-run/424242-ca-94611-111-55"
+        ```
+        """
+        # Generate direct link to Cloud flow
+        tenant_slug = self.get_default_tenant_slug(as_user=as_user)
+
+        base_url = (
+            re.sub("api-", "", prefect.config.cloud.api)
+            if re.search("api-", prefect.config.cloud.api)
+            else re.sub("api", "cloud", prefect.config.cloud.api)
+        )
+
+        full_url = "/".join([base_url.rstrip("/"), tenant_slug, subdirectory, id])
+        return full_url
+
+    def get_default_tenant_slug(self, as_user: bool = True) -> str:
         """
         Get the default tenant slug for the currently authenticated user
 
+        Args:
+            - as_user (bool, optional): whether this query is being made from a USER scoped token;
+                defaults to `True`. Only used internally for queries made from RUNNERs
+
         Returns:
             - str: the slug of the current default tenant for this user
         """
-        res = self.graphql(
-            query={"query": {"user": {"default_membership": {"tenant": "slug"}}}}
-        )
+        if as_user:
+            query = {
+                "query": {"user": {"default_membership": {"tenant": "slug"}}}
+            }  # type: dict
+        else:
+            query = {"query": {"tenant": {"slug"}}}
 
-        user = res.get("data").user[0]
-        return user.default_membership.tenant.slug
+        res = self.graphql(query)
+
+        if as_user:
+            user = res.get("data").user[0]
+            slug = user.default_membership.tenant.slug
+        else:
+            slug = res.get("data").tenant[0].slug
+        return slug
 
     def create_project(self, project_name: str, project_description: str = None) -> str:
         """
diff --git a/src/prefect/utilities/notifications/notifications.py b/src/prefect/utilities/notifications/notifications.py
--- a/src/prefect/utilities/notifications/notifications.py
+++ b/src/prefect/utilities/notifications/notifications.py
@@ -131,24 +131,37 @@ def slack_message_formatter(
     if value is not None:
         fields.append({"title": "Message", "value": value, "short": False})
 
-    data = {
-        "attachments": [
-            {
-                "fallback": "State change notification",
-                "color": state.color,
-                "author_name": "Prefect",
-                "author_link": "https://www.prefect.io/",
-                "author_icon": "https://emoji.slack-edge.com/TAN3D79AL/prefect/2497370f58500a5a.png",
-                "title": type(state).__name__,
-                "fields": fields,
-                #                "title_link": "https://www.prefect.io/",
-                "text": "{0} is now in a {1} state".format(
-                    tracked_obj.name, type(state).__name__
-                ),
-                "footer": "Prefect notification",
-            }
-        ]
+    notification_payload = {
+        "fallback": "State change notification",
+        "color": state.color,
+        "author_name": "Prefect",
+        "author_link": "https://www.prefect.io/",
+        "author_icon": "https://emoji.slack-edge.com/TAN3D79AL/prefect/2497370f58500a5a.png",
+        "title": type(state).__name__,
+        "fields": fields,
+        #                "title_link": "https://www.prefect.io/",
+        "text": "{0} is now in a {1} state".format(
+            tracked_obj.name, type(state).__name__
+        ),
+        "footer": "Prefect notification",
     }
+
+    if prefect.context.get("flow_run_id"):
+        url = None
+
+        if isinstance(tracked_obj, prefect.Flow):
+            url = prefect.client.Client().get_cloud_url(
+                "flow-run", prefect.context["flow_run_id"], as_user=False
+            )
+        elif isinstance(tracked_obj, prefect.Task):
+            url = prefect.client.Client().get_cloud_url(
+                "task-run", prefect.context.get("task_run_id", ""), as_user=False
+            )
+
+        if url:
+            notification_payload.update(title_link=url)
+
+    data = {"attachments": [notification_payload]}
     return data
 
 


Test patch:
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -879,3 +879,88 @@ def test_create_flow_run_with_input(patch_post, kwargs):
         client = Client()
 
     assert client.create_flow_run(**kwargs) == "FOO"
+
+
+def test_get_default_tenant_slug_as_user(patch_post):
+    response = {
+        "data": {"user": [{"default_membership": {"tenant": {"slug": "tslug"}}}]}
+    }
+
+    patch_post(response)
+
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
+        slug = client.get_default_tenant_slug()
+
+        assert slug == "tslug"
+
+
+def test_get_default_tenant_slug_not_as_user(patch_post):
+    response = {"data": {"tenant": [{"slug": "tslug"}]}}
+
+    patch_post(response)
+
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
+        slug = client.get_default_tenant_slug(as_user=False)
+
+        assert slug == "tslug"
+
+
+def test_get_cloud_url_as_user(patch_post):
+    response = {
+        "data": {"user": [{"default_membership": {"tenant": {"slug": "tslug"}}}]}
+    }
+
+    patch_post(response)
+
+    with set_temporary_config(
+        {"cloud.api": "http://api.prefect.io", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
+
+        url = client.get_cloud_url(subdirectory="flow", id="id")
+        assert url == "http://cloud.prefect.io/tslug/flow/id"
+
+        url = client.get_cloud_url(subdirectory="flow-run", id="id2")
+        assert url == "http://cloud.prefect.io/tslug/flow-run/id2"
+
+
+def test_get_cloud_url_not_as_user(patch_post):
+    response = {"data": {"tenant": [{"slug": "tslug"}]}}
+
+    patch_post(response)
+
+    with set_temporary_config(
+        {"cloud.api": "http://api.prefect.io", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
+
+        url = client.get_cloud_url(subdirectory="flow", id="id", as_user=False)
+        assert url == "http://cloud.prefect.io/tslug/flow/id"
+
+        url = client.get_cloud_url(subdirectory="flow-run", id="id2", as_user=False)
+        assert url == "http://cloud.prefect.io/tslug/flow-run/id2"
+
+
+def test_get_cloud_url_different_regex(patch_post):
+    response = {
+        "data": {"user": [{"default_membership": {"tenant": {"slug": "tslug"}}}]}
+    }
+
+    patch_post(response)
+
+    with set_temporary_config(
+        {"cloud.api": "http://api-hello.prefect.io", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
+
+        url = client.get_cloud_url(subdirectory="flow", id="id")
+        assert url == "http://hello.prefect.io/tslug/flow/id"
+
+        url = client.get_cloud_url(subdirectory="flow-run", id="id2")
+        assert url == "http://hello.prefect.io/tslug/flow-run/id2"