diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index f3a4b7f517..1124a64b83 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -298,6 +298,18 @@ [:ssrf-allowed-hosts {:optional true} [::sm/set :string]] [:ssrf-extra-blocked-cidrs {:optional true} [::sm/set :string]]])) +(defn telemetry-excluded-host? + "Returns true when the given host belongs to the official SaaS + instances, where telemetry must be fully disabled." + [host] + (let [host (some-> host (str/lower) (str/trim))] + (and (string? host) + (not (str/blank? host)) + (or (= host "penpot.dev") + (= host "penpot.app") + (str/ends-with? host ".penpot.dev") + (str/ends-with? host ".penpot.app"))))) + (defn- parse-flags [config] (let [public-uri (c/get config :public-uri) @@ -386,6 +398,15 @@ ([key default] (c/get config key default))) +(defn telemetry-excluded? + "Returns true when telemetry must be fully disabled because the + public-uri host points to an official instance (penpot.dev or + penpot.app). When true, no telemetry data is collected or sent, + not even the limited newsletter report." + [] + (let [host (some-> (c/get config :public-uri) (u/uri) :host)] + (telemetry-excluded-host? host))) + (defn logging-context [] {:backend/version (:full version)}) diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 8d70def6a7..9db2f303f3 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -26,7 +26,8 @@ (let [sql "SELECT email FROM profile where props->>'~:newsletter-updates' = 'true'"] (db/run! cfg (fn [{:keys [::db/conn]}] (->> (db/exec! conn [sql]) - (mapv :email)))))) + (into [] (map :email)) + (not-empty)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; LEGACY DATA COLLECTION @@ -312,8 +313,9 @@ send? (get params :send? true) enabled? (or (get params :enabled? false) (contains? cf/flags :telemetry)) - subs (get-subscriptions cfg)] - + ;; Deferred so the query runs only when a report is + ;; actually going to be sent. + subs (delay (get-subscriptions cfg))] ;; If we have telemetry enabled, then proceed the normal ;; operation sending legacy report @@ -327,7 +329,7 @@ (try (let [stats (db/run! cfg get-legacy-stats)] - (send-legacy-data cfg stats subs)) + (send-legacy-data cfg stats @subs)) (catch Exception cause (l/wrn :hint "unable to send legacy report" :cause cause))) @@ -346,7 +348,10 @@ ;; onboarding dialog or the profile section, then proceed to ;; send a limited telemetry data, that consists in the list of ;; subscribed emails and the running penpot version. - (when (and send? (seq subs)) + ;; Official instances (penpot.dev / penpot.app) are excluded: + ;; they must never send subscriber emails to the telemetry + ;; endpoint (which is ourselves). + (when (and (not (cf/telemetry-excluded?)) send? @subs) (px/sleep (rand-int 10000)) (ex/ignoring - (send-legacy-data cfg nil subs))))))) + (send-legacy-data cfg nil @subs))))))) diff --git a/backend/test/backend_tests/tasks_telemetry_test.clj b/backend/test/backend_tests/tasks_telemetry_test.clj index 1f49c2f7c1..01f72977ed 100644 --- a/backend/test/backend_tests/tasks_telemetry_test.clj +++ b/backend/test/backend_tests/tasks_telemetry_test.clj @@ -135,6 +135,58 @@ (th/run-task! :telemetry {:send? false :enabled? true}) (t/is (not (:called? @mock)))))) +(t/deftest test-telemetry-excluded-host-predicate + (t/is (true? (cf/telemetry-excluded-host? "penpot.app"))) + (t/is (true? (cf/telemetry-excluded-host? "penpot.dev"))) + (t/is (true? (cf/telemetry-excluded-host? "design.penpot.app"))) + (t/is (true? (cf/telemetry-excluded-host? "design.penpot.dev"))) + (t/is (true? (cf/telemetry-excluded-host? "DESIGN.PENPOT.APP"))) + (t/is (false? (cf/telemetry-excluded-host? "localhost"))) + (t/is (false? (cf/telemetry-excluded-host? "example.com"))) + (t/is (false? (cf/telemetry-excluded-host? "mypenpot.app.example.com"))) + (t/is (false? (cf/telemetry-excluded-host? nil))) + (t/is (false? (cf/telemetry-excluded-host? "")))) + +(t/deftest test-telemetry-disabled-on-official-host-newsletter-only + ;; The limited newsletter report must not be sent from official + ;; instances, even when subscriptions exist. + (doseq [[idx public-uri] (map-indexed vector ["https://design.penpot.app" + "https://penpot.app" + "https://design.penpot.dev" + "https://penpot.dev"])] + (with-mocks [mock {:target 'app.tasks.telemetry/make-legacy-request + :return nil}] + (with-redefs [cf/flags #{} + cf/config (assoc cf/config :public-uri public-uri)] + (th/create-profile* (+ 10 idx) {:is-active true + :props {:newsletter-updates true}}) + (th/run-task! :telemetry {:send? true}) + (t/is (not (:called? @mock)) (str "newsletter report must not send for " public-uri)))))) + +(t/deftest test-telemetry-excluded-skips-subscriptions-query + ;; On official hosts the subscriptions query must not even run, + ;; since nothing is going to be sent. + (with-mocks [mock {:target 'app.tasks.telemetry/get-subscriptions + :return []}] + (with-redefs [cf/flags #{} + cf/config (assoc cf/config :public-uri "https://design.penpot.app")] + (th/create-profile* 1 {:is-active true + :props {:newsletter-updates true}}) + (th/run-task! :telemetry {:send? true}) + (t/is (not (:called? @mock)))))) + +(t/deftest test-telemetry-enabled-still-sends-on-official-host + ;; An explicitly enabled telemetry still reports on official hosts; + ;; only the implicit newsletter fallback is excluded. + (with-mocks [mock {:target 'app.tasks.telemetry/make-legacy-request + :return nil}] + (with-redefs [cf/flags #{:telemetry} + cf/config (assoc cf/config :public-uri "https://design.penpot.app")] + (th/create-profile* 1 {:is-active true + :props {:newsletter-updates true}}) + (th/run-task! :telemetry {:send? true :enabled? true}) + (t/is (:called? @mock))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; AUDIT-EVENT BATCH COLLECTION TESTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;