;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns app.tasks.telemetry "A task that is responsible to collect anonymous statistical information about the current instance and send it to the telemetry server." (:require [app.common.data :as d] [app.common.exceptions :as ex] [app.config :as cf] [app.db :as db] [app.http.client :as http] [app.main :as-alias main] [app.util.json :as json] [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.exec :as px])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TASK ENTRY POINT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (declare get-stats) (declare send!) (declare get-subscriptions-newsletter-updates) (declare get-subscriptions-newsletter-news) (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::http/client ::db/pool ::main/props])) (defmethod ig/init-key ::handler [_ {:keys [::db/pool ::main/props] :as cfg}] (fn [{:keys [send? enabled?] :or {send? true enabled? false}}] (let [subs {:newsletter-updates (get-subscriptions-newsletter-updates pool) :newsletter-news (get-subscriptions-newsletter-news pool)} enabled? (or enabled? (contains? cf/flags :telemetry) (cf/get :telemetry-enabled)) data {:subscriptions subs :version (:full cf/version) :instance-id (:instance-id props)}] (cond ;; If we have telemetry enabled, then proceed the normal ;; operation. enabled? (let [data (merge data (get-stats pool))] (when send? (px/sleep (rand-int 10000)) (send! cfg data)) data) ;; If we have telemetry disabled, but there are users that are ;; explicitly checked the newsletter subscription on the ;; 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. (seq subs) (do (when send? (px/sleep (rand-int 10000)) (send! cfg data)) data) :else data)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- send! [cfg data] (let [response (http/req! cfg {:method :post :uri (cf/get :telemetry-uri) :headers {"content-type" "application/json"} :body (json/encode-str data)} {:sync? true})] (when (> (:status response) 206) (ex/raise :type :internal :code :invalid-response :response-status (:status response) :response-body (:body response))))) (defn- get-subscriptions-newsletter-updates [conn] (let [sql "select email from profile where props->>'~:newsletter-updates' = 'true'"] (->> (db/exec! conn [sql]) (mapv :email)))) (defn- get-subscriptions-newsletter-news [conn] (let [sql "select email from profile where props->>'~:newsletter-news' = 'true'"] (->> (db/exec! conn [sql]) (mapv :email)))) (defn- retrieve-num-teams [conn] (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) (defn- retrieve-num-projects [conn] (-> (db/exec-one! conn ["select count(*) as count from project;"]) :count)) (defn- retrieve-num-files [conn] (-> (db/exec-one! conn ["select count(*) as count from file;"]) :count)) (defn- retrieve-num-file-changes [conn] (let [sql (str "select count(*) as count " " from file_change " " where date_trunc('day', created_at) = date_trunc('day', now())")] (-> (db/exec-one! conn [sql]) :count))) (defn- retrieve-num-touched-files [conn] (let [sql (str "select count(distinct file_id) as count " " from file_change " " where date_trunc('day', created_at) = date_trunc('day', now())")] (-> (db/exec-one! conn [sql]) :count))) (defn- retrieve-num-users [conn] (-> (db/exec-one! conn ["select count(*) as count from profile;"]) :count)) (defn- retrieve-num-fonts [conn] (-> (db/exec-one! conn ["select count(*) as count from team_font_variant;"]) :count)) (defn- retrieve-num-comments [conn] (-> (db/exec-one! conn ["select count(*) as count from comment;"]) :count)) (def sql:team-averages "with projects_by_team as ( select t.id, count(p.id) as num_projects from team as t left join project as p on (p.team_id = t.id) group by 1 ), files_by_project as ( select p.id, count(f.id) as num_files from project as p left join file as f on (f.project_id = p.id) group by 1 ), comment_threads_by_file as ( select f.id, count(ct.id) as num_comment_threads from file as f left join comment_thread as ct on (ct.file_id = f.id) group by 1 ), users_by_team as ( select t.id, count(tp.profile_id) as num_users from team as t left join team_profile_rel as tp on(tp.team_id = t.id) group by 1 ) select (select avg(num_projects)::integer from projects_by_team) as avg_projects_on_team, (select max(num_projects)::integer from projects_by_team) as max_projects_on_team, (select avg(num_files)::integer from files_by_project) as avg_files_on_project, (select max(num_files)::integer from files_by_project) as max_files_on_project, (select avg(num_comment_threads)::integer from comment_threads_by_file) as avg_comment_threads_on_file, (select max(num_comment_threads)::integer from comment_threads_by_file) as max_comment_threads_on_file, (select avg(num_users)::integer from users_by_team) as avg_users_on_team, (select max(num_users)::integer from users_by_team) as max_users_on_team;") (defn- retrieve-team-averages [conn] (->> [sql:team-averages] (db/exec-one! conn))) (defn- retrieve-enabled-auth-providers [conn] (let [sql (str "select auth_backend as backend, count(*) as total " " from profile group by 1") rows (db/exec! conn [sql])] (->> rows (map (fn [{:keys [backend total]}] (let [backend (or backend "penpot")] [(keyword (str "auth-backend-" backend)) total]))) (into {})))) (defn- retrieve-jvm-stats [] (let [^Runtime runtime (Runtime/getRuntime)] {:jvm-heap-current (.totalMemory runtime) :jvm-heap-max (.maxMemory runtime) :jvm-cpus (.availableProcessors runtime) :os-arch (System/getProperty "os.arch") :os-name (System/getProperty "os.name") :os-version (System/getProperty "os.version") :user-tz (System/getProperty "user.timezone")})) (defn get-stats [conn] (let [referer (if (cf/get :telemetry-with-taiga) "taiga" (cf/get :telemetry-referer))] (-> {:referer referer :public-uri (cf/get :public-uri) :total-teams (retrieve-num-teams conn) :total-projects (retrieve-num-projects conn) :total-files (retrieve-num-files conn) :total-users (retrieve-num-users conn) :total-fonts (retrieve-num-fonts conn) :total-comments (retrieve-num-comments conn) :total-file-changes (retrieve-num-file-changes conn) :total-touched-files (retrieve-num-touched-files conn)} (d/merge (retrieve-team-averages conn) (retrieve-jvm-stats) (retrieve-enabled-auth-providers conn)) (d/without-nils))))