From 94496eb579f860385e38847cfc6d169a4525c008 Mon Sep 17 00:00:00 2001
From: bryan <bryan.maass@gmail.com>
Date: Wed, 16 Oct 2024 12:00:20 -0600
Subject: [PATCH] SDK embedding prometheus (#48578)

* add prometheus tracking for sdk-embedding

* indentation

* improve track-sdk-response api

* register :metabase-sdk/response-{ok,error} + test

* avoid cyclic dependency whe nusing prometheus

fixes:
[ /metabase/api/common ]
<- /metabase/models/setting
<- /metabase/analytics/settings
<- /metabase/analytics/prometheus
<- /metabase/analytics/sdk
<- /metabase/models/view_log
<- /metabase/events/schema
<- /metabase/events
<- [ /metabase/api/common ]
<- /metabase/public_settings/premium_features
<- /metabase/auth_provider
<- /metabase/driver
<- /dev/debug_qp
<- /dev

* nix dependency: events.schema --> models.view-log

- make them both read context from view-log-impl instead

* cljfmt
---
 src/metabase/analytics/prometheus.clj        |  6 ++-
 src/metabase/analytics/sdk.clj               | 38 ++++++++++++---
 src/metabase/events/schema.clj               |  4 +-
 src/metabase/models/view_log.clj             |  4 +-
 src/metabase/models/view_log_impl.clj        |  5 ++
 src/metabase/server/handler.clj              |  2 +-
 test/metabase/server/middleware/sdk_test.clj | 51 +++++++++++++++-----
 7 files changed, 87 insertions(+), 23 deletions(-)
 create mode 100644 src/metabase/models/view_log_impl.clj

diff --git a/src/metabase/analytics/prometheus.clj b/src/metabase/analytics/prometheus.clj
index 7ef9cf4fa32..7b449ad4120 100644
--- a/src/metabase/analytics/prometheus.clj
+++ b/src/metabase/analytics/prometheus.clj
@@ -198,7 +198,11 @@
   [(prometheus/counter :metabase-email/messages
                        {:description (trs "Number of emails sent.")})
    (prometheus/counter :metabase-email/message-errors
-                       {:description (trs "Number of errors when sending emails.")})])
+                       {:description (trs "Number of errors when sending emails.")})
+   (prometheus/counter :metabase-sdk/response-ok
+                       {:description (trs "Number of successful SDK requests.")})
+   (prometheus/counter :metabase-sdk/response-error
+                       {:description (trs "Number of errors when responding to SDK requests.")})])
 
 (defn- setup-metrics!
   "Instrument the application. Conditionally done when some setting is set. If [[prometheus-server-port]] is not set it
diff --git a/src/metabase/analytics/sdk.clj b/src/metabase/analytics/sdk.clj
index df6109a8da3..208033559a2 100644
--- a/src/metabase/analytics/sdk.clj
+++ b/src/metabase/analytics/sdk.clj
@@ -8,7 +8,8 @@
 
   then we can use the information on the tables to track information about the embedding client,
   and TODO: send it out in `summarize-execution`."
-  (:require [metabase.util.malli :as mu]))
+  (:require [metabase.analytics.prometheus :as prometheus]
+            [metabase.util.malli :as mu]))
 
 (def ^:dynamic *version* "Used to track information about the metabase embedding client version." nil)
 (def ^:dynamic *client* "Used to track information about the metabase embedding client." nil)
@@ -21,11 +22,36 @@
       (update :embedding_client (fn [client] (or *client* client)))
       (update :embedding_version (fn [version] (or *version* version)))))
 
-(defn bind-embedding-mw
+(mu/defn- categorize-request :- [:maybe [:enum :ok :error]]
+  [{:keys [status]}]
+  (when status
+    (cond
+      (<= 200 status 299) :ok
+      (<= 400 status 599) :error
+      ;; ignore other status codes
+      :else nil)))
+
+(defn- track-sdk-response
+  "Tabulates the number of ok and erroring requests made by clients of the SDK."
+  [category]
+  (case category
+    :ok (prometheus/inc! :metabase-sdk/response-ok)
+    :error (prometheus/inc! :metabase-sdk/response-error)
+    nil nil))
+
+(defn embedding-mw
   "Reads Metabase Client and Version headers and binds them to *metabase-client{-version}*."
   [handler]
-  (fn bound-embedding
+  (fn embedding-mw-fn
     [request respond raise]
-    (binding [*client* (get-in request [:headers "x-metabase-client"])
-              *version* (get-in request [:headers "x-metabase-client-version"])]
-      (handler request respond raise))))
+    (let [sdk-client (get-in request [:headers "x-metabase-client"])
+          version (get-in request [:headers "x-metabase-client-version"])]
+      (binding [*client* sdk-client
+                *version* version]
+        (handler request
+                 (fn responder [response]
+                   (when sdk-client (track-sdk-response (categorize-request response)))
+                   (respond response))
+                 (fn raiser [response]
+                   (when sdk-client (track-sdk-response :error))
+                   (raise response)))))))
diff --git a/src/metabase/events/schema.clj b/src/metabase/events/schema.clj
index 28eaf3c787f..1a2f0c9a6d2 100644
--- a/src/metabase/events/schema.clj
+++ b/src/metabase/events/schema.clj
@@ -2,7 +2,7 @@
   (:require
    [malli.core :as mc]
    [malli.util :as mut]
-   [metabase.models.view-log :as view-log]
+   [metabase.models.view-log-impl :as view-log-impl]
    [metabase.util.malli.schema :as ms]
    [toucan2.core :as t2]))
 
@@ -68,7 +68,7 @@
      :event/card-read   (mc/schema
                          [:map {:closed true}
                           ;; context is deliberately coupled to view-log's context
-                          [:context [:and :some ::view-log/context]]
+                          [:context view-log-impl/context]
                           [:user-id [:maybe pos-int?]]
                           [:object-id [:maybe pos-int?]]])
      :event/card-query  [:map {:closed true}
diff --git a/src/metabase/models/view_log.clj b/src/metabase/models/view_log.clj
index 4302b1833ed..b00e23263ae 100644
--- a/src/metabase/models/view_log.clj
+++ b/src/metabase/models/view_log.clj
@@ -3,6 +3,7 @@
   (:require
    [metabase.analytics.sdk :as sdk]
    [metabase.models.interface :as mi]
+   [metabase.models.view-log-impl :as view-log-impl]
    [metabase.util.malli :as mu]
    [metabase.util.malli.fn :as mu.fn]
    [metabase.util.malli.registry :as mr]
@@ -21,8 +22,7 @@
   (derive ::mi/read-policy.always-allow)
   (derive ::mi/write-policy.always-allow))
 
-(mr/def ::context
-  [:maybe [:enum :dashboard :question :collection]])
+(mr/def ::context [:maybe view-log-impl/context])
 
 (t2/define-before-insert :model/ViewLog
   [log-entry]
diff --git a/src/metabase/models/view_log_impl.clj b/src/metabase/models/view_log_impl.clj
new file mode 100644
index 00000000000..040bba22669
--- /dev/null
+++ b/src/metabase/models/view_log_impl.clj
@@ -0,0 +1,5 @@
+(ns metabase.models.view-log-impl)
+
+(def context
+  "The context of a view log entry. In other words, what sort of page generated this view?."
+  [:enum :dashboard :question :collection])
diff --git a/src/metabase/server/handler.clj b/src/metabase/server/handler.clj
index 6220fc31afe..4db434eb0f3 100644
--- a/src/metabase/server/handler.clj
+++ b/src/metabase/server/handler.clj
@@ -57,7 +57,7 @@
    #'mw.session/reset-session-timeout           ; Resets the timeout cookie for user activity to [[mw.session/session-timeout]]
    #'mw.session/bind-current-user               ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil
    #'mw.session/wrap-current-user-info          ; looks for :metabase-session-id and sets :metabase-user-id and other info if Session ID is valid
-   #'sdk/bind-embedding-mw                      ; reads Metabase Client and Version headers and binds them to sdk/*client* and sdk/*version*
+   #'sdk/embedding-mw                           ; reads sdk client headers, binds them to *client* and *version*, and tracks sdk-response metrics
    #'mw.session/wrap-session-id                 ; looks for a Metabase Session ID and assoc as :metabase-session-id
    #'wrap-cookies                               ; Parses cookies in the request map and assocs as :cookies
    #'mw.misc/add-content-type                   ; Adds a Content-Type header for any response that doesn't already have one
diff --git a/test/metabase/server/middleware/sdk_test.clj b/test/metabase/server/middleware/sdk_test.clj
index 572ed27d1e8..87f85236d48 100644
--- a/test/metabase/server/middleware/sdk_test.clj
+++ b/test/metabase/server/middleware/sdk_test.clj
@@ -2,11 +2,12 @@
   (:require
    [clojure.string :as str]
    [clojure.test :refer [are deftest is]]
+   [metabase.analytics.prometheus :as prometheus]
    [metabase.analytics.sdk :as sdk]
    [metabase.util :as u]
    [ring.mock.request :as ring.mock]))
 
-(defn wonk-case [s]
+(defn- wonk-case [s]
   (str/join (for [char s]
               (let [f (if (rand-nth [true false]) u/upper-case-en u/lower-case-en)]
                 (f char)))))
@@ -20,25 +21,53 @@
 (deftest bind-client-test
   (are [client]
        (let [request (mock-request {:client client})
-             handler (sdk/bind-embedding-mw
-                      (fn [_request respond _raise] (respond client)))]
-         (handler request
-                  (fn [response] (= sdk/*client* response))
-                  (fn [e] (throw e))))
+             handler (sdk/embedding-mw
+                      (fn [_ respond _] (respond {:status 200 :body client})))
+             response (handler request identity identity)]
+         (is (= (:body response "no-body") client)))
     nil
     "embedding-iframe"))
 
 (deftest bind-client-version-test
   (are [version]
        (let [request (mock-request {:version version})
-             handler (sdk/bind-embedding-mw
-                      (fn [_request respond _raise] (respond version)))]
-         (handler request
-                  (fn [response] (=  sdk/*version* response))
-                  (fn [e] (throw e))))
+             handler (sdk/embedding-mw
+                      (fn [_ respond _] (respond {:status 200 :body version})))
+             response (handler request identity identity)]
+         (is (= (:body response "no-body") version)))
     nil
     "1.1.1"))
 
+(deftest embeding-mw-bumps-metrics-with-sdk-header
+  (let [prometheus-standin (atom {})]
+    (with-redefs [prometheus/inc! (fn [k] (swap! prometheus-standin update k (fnil inc 0)))]
+      (let [request (mock-request {:client "my-client"}) ;; <-- has X-Metabase-Client header => SDK context
+            good (sdk/embedding-mw (fn [_ respond _] (respond {:status 200})))
+            bad (sdk/embedding-mw (fn [_ respond _] (respond {:status 400})))
+            exception (sdk/embedding-mw (fn [_ _respond raise] (raise {})))]
+        (good request identity identity)
+        (is (= {:metabase-sdk/response-ok 1} @prometheus-standin))
+        (bad request identity identity)
+        (is (= {:metabase-sdk/response-ok 1
+                :metabase-sdk/response-error 1} @prometheus-standin))
+        (exception request identity identity)
+        (is (= {:metabase-sdk/response-ok 1
+                :metabase-sdk/response-error 2} @prometheus-standin))))))
+
+(deftest embeding-mw-does-not-bump-sdk-metrics-without-sdk-header
+  (let [prometheus-standin (atom {})]
+    (with-redefs [prometheus/inc! (fn [k] (swap! prometheus-standin update k (fnil inc 0)))]
+      (let [request (mock-request {}) ;; <= no X-Metabase-Client header => no SDK context
+            good (sdk/embedding-mw (fn [_ respond _] (respond {:status 200})))
+            bad (sdk/embedding-mw (fn [_ respond _] (respond {:status 400})))
+            exception (sdk/embedding-mw (fn [_ _respond raise] (raise {})))]
+        (good request identity identity)
+        (is (= {} @prometheus-standin))
+        (bad request identity identity)
+        (is (= {} @prometheus-standin))
+        (exception request identity identity)
+        (is (= {} @prometheus-standin))))))
+
 (deftest include-analytics-is-idempotent
   (let [m (atom {})]
     (binding [sdk/*client* "client-C"
-- 
GitLab