Disable newsletter telemetry fallback on official hosts

Skip the limited newsletter report when the public-uri host
belongs to penpot.dev or penpot.app, so the SaaS never sends
subscriber emails to its own telemetry endpoint.

Defer the subscriptions query with delay so it only runs when
a report is actually going to be sent.

AI-assisted-by: muse-spark-1.3-contributor
This commit is contained in:
Andrey Antukh 2026-09-14 07:29:34 +00:00
parent 37f7ba4833
commit e5f375edbc
3 changed files with 84 additions and 6 deletions

View File

@ -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)})

View File

@ -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)))))))

View File

@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;