From 12ea8d42b515fb8128ec6c053ed9a07581f9f7d1 Mon Sep 17 00:00:00 2001
From: dpsutton <dan@dpsutton.com>
Date: Tue, 2 Apr 2024 12:34:56 -0700
Subject: [PATCH] Creator sentiment emails iterate (#40922)

* Send creator emails every week

mod the email by 52 instead of by 12 and use the current week as an
anchor

```clojure
user=> (require '[java-time.api :as t])
nil
user=> (let [every-day (take 1000 (t/iterate t/plus (t/local-date 2024 1 1) (t/days 1)))
             f         (fn [d]
                         (let [wf (java.time.temporal.WeekFields/of (java.util.Locale/getDefault))]
                           (.get d (.weekOfWeekBasedYear wf))))]
         (sort (keys (frequencies (map f every-day)))))
(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
 40 41 42 43 44 45 46 47 48 49 50 51 52)
```

```clojure
user=> (metabase.util.cron/cron-string->schedule-map "0 0 2 ? * 7")
{:schedule_minute 0,
 :schedule_day "sat",
 :schedule_frame nil,
 :schedule_hour 2,
 :schedule_type "weekly"}
```

* fix typo in creator sentiment email, clean up a bit

typo: had `:verison` instead of `version`.

And while I'm in there, make the payload all in one file and have the
email namespace json->bytes->b64-encode so the data is all in one place
and the setting that allows it to be present or not is co-located.
---
 src/metabase/email/messages.clj               |  27 ++---
 .../task/creator_sentiment_emails.clj         |  48 +++++---
 .../task/creator_sentiment_emails_test.clj    | 107 ++++++++++--------
 3 files changed, 99 insertions(+), 83 deletions(-)

diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj
index 74a9f8fb999..070c231588a 100644
--- a/src/metabase/email/messages.clj
+++ b/src/metabase/email/messages.clj
@@ -289,31 +289,24 @@
                :message      (stencil/render-file "metabase/email/follow_up_email" context)}]
     (email/send-message! email)))
 
-(defn- creator-sentiment-blob
-  "Create a blob of instance/user data to be sent to the creator sentiment survey."
-  [instance-data created_at num_dashboards num_questions num_models]
-  (-> {:instance instance-data
-       :user     {:created_at     created_at
-                  :num_dashboards num_dashboards
-                  :num_questions  num_questions
-                  :num_models     num_models}}
-      json/generate-string
-      .getBytes
-      codecs/bytes->b64-str))
+
 
 (defn send-creator-sentiment-email!
-  "Format and send an email to a creator with a link to a survey. Can include info about the instance and the creator
-  if [[public-settings/anon-tracking-enabled]] is true."
-  [{:keys [email created_at first_name num_dashboards num_questions num_models]} instance-data]
+  "Format and send an email to a creator with a link to a survey. If a [[blob]] is included, it will be turned into json
+  and then base64 encoded."
+  [{:keys [email first_name]} blob]
   {:pre [(u/email? email)]}
-  (let [blob    (when (public-settings/anon-tracking-enabled)
-                  (creator-sentiment-blob instance-data created_at num_dashboards num_questions num_models))
+  (let [encoded-info    (when blob
+                          (-> blob
+                              json/generate-string
+                              .getBytes
+                              codecs/bytes->b64-str))
         context (merge (common-context)
                        {:emailType  "notification"
                         :logoHeader true
                         :first-name first_name
                         :link       (cond-> "https://metabase.com/feedback/creator"
-                                      blob (str "?context=" blob))}
+                                      encoded-info (str "?context=" encoded-info))}
                        (when-not (premium-features/is-hosted?)
                          {:self-hosted (public-settings/site-url)}))
         message {:subject      "Metabase would love your take on something"
diff --git a/src/metabase/task/creator_sentiment_emails.clj b/src/metabase/task/creator_sentiment_emails.clj
index a45921bb793..7279557c318 100644
--- a/src/metabase/task/creator_sentiment_emails.clj
+++ b/src/metabase/task/creator_sentiment_emails.clj
@@ -14,7 +14,10 @@
    [metabase.public-settings.premium-features :as premium-features]
    [metabase.task :as task]
    [metabase.util.log :as log]
-   [toucan2.core :as t2]))
+   [toucan2.core :as t2])
+  (:import
+   (java.util Locale)
+   (java.time.temporal WeekFields)))
 
 (set! *warn-on-reflection* true)
 
@@ -61,36 +64,47 @@
 (defn- fetch-instance-data []
   {:created_at     (snowplow/instance-creation)
    :plan           (fetch-plan-info)
-   :verison        (config/mb-version-info :tag)
+   :version        (config/mb-version-info :tag)
    :num_users      (t2/count :model/User :is_active true, :type "personal")
    :num_dashboards (t2/count :model/Dashboard :archived false)
+   :num_databases  (t2/count :model/Database :is_audit false)
    :num_questions  (t2/count :model/Card :archived false :type "question")
    :num_models     (t2/count :model/Card :archived false :type "model")})
 
+(defn- user-instance-info
+  "Create a blob of instance/user data to be sent to the creator sentiment survey."
+  [instance-data {:keys [created_at num_dashboards num_questions num_models]}]
+  {:instance instance-data
+   :user     {:created_at     created_at
+              :num_dashboards num_dashboards
+              :num_questions  num_questions
+              :num_models     num_models}})
+
 (defn- send-creator-sentiment-emails!
   "Send an email to the instance admin following up on their experience with Metabase thus far."
-  []
+  [current-week]
   ;; we need access to email AND the instance must have surveys enabled.
   (when (and (email/email-configured?)
              (email/surveys-enabled))
-    (let [instance-data (when (public-settings/anon-tracking-enabled)
-                          (fetch-instance-data))
+    (let [instance-data (fetch-instance-data)
           all-creators  (fetch-creators (premium-features/enable-whitelabeling?))
-          month         (- (.getValue (t/month)) 1)
-          this-month    (fn [c] (= month (-> c :email hash (mod 12))))
-          recipients    (filter this-month all-creators)]
+          this-week?    (fn [c] (= current-week (-> c :email hash (mod 52))))
+          recipients    (filter this-week? all-creators)
+          blob          (if (public-settings/anon-tracking-enabled)
+                          (fn [creator]
+                            (user-instance-info instance-data creator))
+                          (constantly nil))]
       (log/infof "Sending surveys to %d creators of a total %d"
                  (count all-creators) (count recipients))
       (doseq [creator recipients]
-        ;; Send the email if the creator's email hash matches the current month
-        (when (= (-> creator :email hash (mod 12)) month)
-          (try
-            (messages/send-creator-sentiment-email! creator instance-data)
-            (catch Throwable e
-              (log/error e "Problem sending creator sentiment email:"))))))))
+        (try
+          (messages/send-creator-sentiment-email! creator (blob creator))
+          (catch Throwable e
+            (log/error e "Problem sending creator sentiment email:")))))))
 
 (jobs/defjob ^{:doc "Sends out a monthly survey to a portion of the creators."} CreatorSentimentEmail [_]
-  (send-creator-sentiment-emails!))
+  (let [current-week (.get (t/local-date) (.weekOfWeekBasedYear (WeekFields/of (Locale/getDefault))))]
+    (send-creator-sentiment-emails! current-week)))
 
 (def ^:private creator-sentiment-emails-job-key     "metabase.task.creator-sentiment-emails.job")
 (def ^:private creator-sentiment-emails-trigger-key "metabase.task.creator-sentiment-emails.trigger")
@@ -103,6 +117,6 @@
                  (triggers/with-identity (triggers/key creator-sentiment-emails-trigger-key))
                  (triggers/start-now)
                  (triggers/with-schedule
-                   ;; Fire at 2am on the second saturday of the month
-                   (cron/cron-schedule "0 0 2 ? * 7#2")))]
+                   ;; Fire at 2am every saturday
+                   (cron/cron-schedule "0 0 2 ? * 7")))]
     (task/schedule-task! job trigger)))
diff --git a/test/metabase/task/creator_sentiment_emails_test.clj b/test/metabase/task/creator_sentiment_emails_test.clj
index d47dbe2e62a..488e2891cc1 100644
--- a/test/metabase/task/creator_sentiment_emails_test.clj
+++ b/test/metabase/task/creator_sentiment_emails_test.clj
@@ -1,71 +1,80 @@
 (ns ^:mb/once metabase.task.creator-sentiment-emails-test
   (:require
+   [buddy.core.codecs :as codecs]
+   [cheshire.core :as json]
    [clojure.test :refer :all]
    [java-time.api :as t]
    [metabase.email-test :as et :refer [inbox]]
-   [metabase.public-settings :as public-settings]
    [metabase.public-settings.premium-features :as premium-features]
    [metabase.task.creator-sentiment-emails :as creator-sentiment-emails]
    [metabase.test :as mt]
+   [metabase.util.malli.schema :as ms]
    [toucan2.tools.with-temp :as t2.with-temp]))
 
 (deftest send-creator-sentiment-emails!-test
   (mt/with-fake-inbox
     (testing "Make sure we only send emails when surveys-enabled is true"
-      (mt/with-temporary-setting-values [surveys-enabled false]
-        (with-redefs [creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}   ;; mods to 1, this email would be sent if surveys-enabled was true
-                                                                       {:email "b@metabase.com"}   ;; mods to 4
-                                                                       {:email "c@metabase.com"}]) ;; mods to 2
-                      t/month (constantly (t/month 2))]
-          (#'creator-sentiment-emails/send-creator-sentiment-emails!)
-          (is (= 0
-                 (-> @inbox vals first count))))))
+      (doseq [enabled? [true false]]
+        (mt/reset-inbox!)
+        (mt/with-temporary-setting-values [surveys-enabled enabled?]
+          (with-redefs [creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"} ;; mods to 45, this email would be sent if surveys-enabled was true
+                                                                         {:email "b@metabase.com"} ;; mods to 4
+                                                                         {:email "c@metabase.com"}]) ;; mods to 26
+                        ]
+            (#'creator-sentiment-emails/send-creator-sentiment-emails! 45)
+            (is (= (if enabled? 1 0)
+                   (-> @inbox vals first count))
+                (str "error when enabled? is " enabled?))))))
 
     (mt/with-temporary-setting-values [surveys-enabled true]
-      (testing "Make sure that send-creator-sentiment-emails! only sends emails to creators with the correct month hash."
-        (with-redefs [creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}   ;; mods to 1
+      (testing "Make sure that send-creator-sentiment-emails! only sends emails to creators with the correct week hash."
+        (with-redefs [creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}   ;; mods to 45
                                                                        {:email "b@metabase.com"}   ;; mods to 4
-                                                                       {:email "c@metabase.com"}]) ;; mods to 2
-                      t/month (constantly (t/month 2))]
-          (#'creator-sentiment-emails/send-creator-sentiment-emails!)
-          (is (= 1
-                 (-> @inbox vals first count)))))
-
-      (mt/reset-inbox!)
-      (testing "Make sure context is included when anon tracking is enabled"
-        (with-redefs [public-settings/anon-tracking-enabled (constantly true)
-                      creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}])
-                      t/month (constantly (t/month 2))]
-          (#'creator-sentiment-emails/send-creator-sentiment-emails!)
+                                                                       {:email "c@metabase.com"}]) ;; mods to 26
+                      ]
+          (#'creator-sentiment-emails/send-creator-sentiment-emails! 45)
           (is (= 1
-                 (count (et/regex-email-bodies #"creator\?context=")))))))
-
-    (mt/reset-inbox!)
-    (testing "Make sure context isn't included when anon tracking is enabled"
-      (with-redefs [public-settings/anon-tracking-enabled (constantly false)
-                    creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}])
-                    t/month (constantly (t/month 2))]
-        (#'creator-sentiment-emails/send-creator-sentiment-emails!)
-        (is (= 0
-               (count (et/regex-email-bodies #"creator\?context="))))))
+                 (-> @inbox vals first count))))))
 
-    (mt/reset-inbox!)
+    (testing "Make sure context is included when anon tracking is enabled"
+      (doseq [tracking-enabled? [true false]]
+        (mt/reset-inbox!)
+        (mt/with-temporary-setting-values [anon-tracking-enabled tracking-enabled?]
+          (with-redefs [creator-sentiment-emails/fetch-creators (fn [_] [{:email          "a@metabase.com"
+                                                                          :created_at     (t/local-date-time)
+                                                                          :num_dashboards 4
+                                                                          :num_questions  7
+                                                                          :num_models     2}])]
+            (#'creator-sentiment-emails/send-creator-sentiment-emails! 45)
+            (is (= (if tracking-enabled? 1 0)
+                   (count (et/regex-email-bodies #"creator\?context="))))
+            (when tracking-enabled?
+              (let [email-body (-> @inbox vals first first :body first :content)]
+                (is (string? email-body))
+                (let [[_ query-params] (re-find #"creator\?context=(.*)\"" email-body)
+                      decoded          (some-> query-params codecs/b64->str json/parse-string )]
+                  (is (=? {"instance" {"created_at"     (mt/malli=? ms/TemporalString)
+                                       "plan"           (mt/malli=? :string)
+                                       "version"        (mt/malli=? :string)
+                                       "num_users"      (mt/malli=? :int)
+                                       "num_dashboards" (mt/malli=? :int)
+                                       "num_databases"  (mt/malli=? :int)
+                                       "num_questions"  (mt/malli=? :int)
+                                       "num_models"     (mt/malli=? :int)}
+                           "user"     {"created_at"     (mt/malli=? ms/TemporalString)
+                                       "num_dashboards" 4
+                                       "num_questions"  7
+                                       "num_models"     2}}
+                          decoded)))))))))
     (testing "Make sure external services message is included when is self hosted"
-      (with-redefs [premium-features/is-hosted? (constantly false)
-                    creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}])
-                    t/month (constantly (t/month 2))]
-        (#'creator-sentiment-emails/send-creator-sentiment-emails!)
-        (is (= 1
-               (count (et/regex-email-bodies #"external services"))))))))
-
-    (mt/reset-inbox!)
-    (testing "Make sure external services isn't included when not self hosted"
-        (with-redefs [premium-features/is-hosted? (constantly true)
-                      creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}])
-                      t/month (constantly (t/month 2))]
-          (#'creator-sentiment-emails/send-creator-sentiment-emails!)
-          (is (= 0
-                 (count (et/regex-email-bodies #"external services"))))))
+      (doseq [hosted? [true false]]
+        (mt/reset-inbox!)
+        (with-redefs [creator-sentiment-emails/fetch-creators (fn [_] [{:email "a@metabase.com"}])
+                      ;; can't use mt/with-temporary-setting-values because of a custom :getter
+                      premium-features/is-hosted?             (constantly hosted?)]
+          (#'creator-sentiment-emails/send-creator-sentiment-emails! 45)
+          (is (= (if hosted? 0 1)
+                 (count (et/regex-email-bodies #"external services")))))))))
 
 (deftest fetch-creators-test
   (let [creator-id 33
-- 
GitLab