diff --git a/backend/resources/rlimit.edn b/backend/resources/rlimit.edn index 68b3153848..86247690f1 100644 --- a/backend/resources/rlimit.edn +++ b/backend/resources/rlimit.edn @@ -98,6 +98,9 @@ :main/get-subscription-usage} [[:get-access-tokens :bucket "150/75/30s"]] + #{:main/get-environment-data} + [[:get-environment-data :bucket "250/125/30s"]] + #{:main/get-enabled-flags} [[:get-enabled-flags :bucket "250/125/30s"]] diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index a0ef5d344d..48081d6430 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -49,6 +49,9 @@ :host "localhost" :tenant "default" + ;; The SaaS host also sets penpotIsSaas in the browser. Keep this server + ;; value in sync so Admin Console can query the same deployment type. + :is-saas false :redis-uri "redis://redis/0" @@ -109,6 +112,7 @@ [:secret-key {:optional true} :string] [:tenant {:optional false} :string] + [:is-saas ::sm/boolean] [:public-uri {:optional false} ::sm/uri] [:host {:optional false} :string] diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index fceb258cf8..9b15b05e27 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -178,13 +178,45 @@ (rph/wrap nil))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; GET-ENABLED-FLAGS +;; GET-ENVIRONMENT-DATA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sv/defmethod ::get-enabled-flags +(def ^:private enabled-flags + "Subset of the backend flags that are safe to expose publicly and that + drive frontend event collection." + #{:audit-log :telemetry}) + +(defn- environment-data + [] + {:deployment (if (cf/get :is-saas) "saas" "selfhost") + :flags (set/intersection cf/flags enabled-flags)}) + +(def ^:private schema:get-environment-data-result + [:map + [:deployment [:enum "saas" "selfhost"]] + [:flags [::sm/set [:enum :audit-log :telemetry]]]]) + +(sv/defmethod ::get-environment-data + "Returns the deployment type and the enabled environment flags." {::audit/skip true ::rpc/auth false ::doc/skip true + ::doc/added "2.19" + ::sm/params [:map] + ::sm/result schema:get-environment-data-result} + [_cfg _params] + (environment-data)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GET-ENABLED-FLAGS (DEPRECATED) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(sv/defmethod ::get-enabled-flags + "Deprecated. Use `get-environment-data` instead." + {::audit/skip true + ::rpc/auth false + ::doc/skip true + ::doc/deprecated true ::doc/added "1.20"} [_cfg _params] - (set/intersection cf/flags #{:audit-log :telemetry})) + (:flags (environment-data))) diff --git a/backend/test/backend_tests/rpc_audit_test.clj b/backend/test/backend_tests/rpc_audit_test.clj index 8336e2dba3..51963bcd88 100644 --- a/backend/test/backend_tests/rpc_audit_test.clj +++ b/backend/test/backend_tests/rpc_audit_test.clj @@ -304,6 +304,41 @@ (t/is (nil? (:error out))) (t/is (= 0 (count (th/db-exec! ["select * from audit_log"]))))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; GET-ENVIRONMENT-DATA +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest get-environment-data-returns-selfhost-by-default + (with-redefs [cf/flags #{:audit-log}] + (let [out (th/command! {::th/type :get-environment-data})] + (t/is (th/success? out)) + (t/is (= "selfhost" (-> out :result :deployment)))))) + +(t/deftest get-environment-data-returns-saas-when-configured + (with-redefs [cf/flags #{:telemetry}] + (binding [cf/config (assoc cf/config :is-saas true)] + (let [out (th/command! {::th/type :get-environment-data})] + (t/is (th/success? out)) + (t/is (= "saas" (-> out :result :deployment))))))) + +(t/deftest get-environment-data-only-exposes-event-flags + (with-redefs [cf/flags #{:audit-log :telemetry :graph :nrepl-server}] + (let [out (th/command! {::th/type :get-environment-data})] + (t/is (th/success? out)) + (t/is (= #{:audit-log :telemetry} (-> out :result :flags)))))) + +(t/deftest get-environment-data-returns-empty-flags-when-none-enabled + (with-redefs [cf/flags #{:graph}] + (let [out (th/command! {::th/type :get-environment-data})] + (t/is (th/success? out)) + (t/is (= #{} (-> out :result :flags)))))) + +(t/deftest get-enabled-flags-still-returns-flat-set + (with-redefs [cf/flags #{:audit-log :telemetry :graph}] + (let [out (th/command! {::th/type :get-enabled-flags})] + (t/is (th/success? out)) + (t/is (= #{:audit-log :telemetry} (:result out)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PURE HELPER UNIT TESTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/event.cljs b/frontend/src/app/main/data/event.cljs index 0c507b3f0c..c381583e12 100644 --- a/frontend/src/app/main/data/event.cljs +++ b/frontend/src/app/main/data/event.cljs @@ -359,6 +359,23 @@ (rx/map deref) (rx/map snapshot-performance-info))))) +(defn fetch-environment-data + "Fetches the backend environment data (deployment type and enabled + flags). On RPC failure, falls back to telemetry so event collection is + not silently dropped; the backend rejects events if truly disabled." + [] + (->> (rp/cmd! :get-environment-data) + (rx/catch (fn [cause] + (l/debug :hint "unable to fetch backend environment data, proceeding with event collection" + :cause cause) + (rx/of {:flags #{:telemetry}}))))) + +(defn events-enabled? + "Returns true when the environment flags enable frontend event collection." + [flags] + (or (contains? flags :audit-log) + (contains? flags :telemetry))) + (defn initialize [] (ptk/reify ::initialize @@ -378,16 +395,14 @@ (l/debug :hint "event instrumentation initialized") - ;; Fetch backend flags and only start event collection if - ;; :audit-log or :telemetry is enabled. On RPC failure, proceed - ;; with event collection anyway (backend will reject if truly disabled). - (->> (rp/cmd! :get-enabled-flags) - (rx/catch (fn [cause] - (l/debug :hint "unable to fetch backend flags, proceeding with event collection" :cause cause) - (rx/of #{:telemetry}))) + ;; Fetch the backend environment data and only start event + ;; collection if :audit-log or :telemetry is enabled. On RPC + ;; failure, proceed with event collection anyway (backend will + ;; reject if truly disabled). + (->> (fetch-environment-data) + (rx/map :flags) (rx/mapcat (fn [flags] - (if (or (contains? flags :audit-log) - (contains? flags :telemetry)) + (if (events-enabled? flags) (do (l/debug :hint "event collection enabled" :flags (str/join " " (map name flags))) (rx/of true)) diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs index 0643ba37c3..5548a5159e 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.cljs +++ b/frontend/src/app/main/ui/dashboard/subscription.cljs @@ -279,6 +279,7 @@ (fn [] (st/emit! (ev/event {::ev/name "open-subscription-management" ::ev/origin "dashboard" + :deployment (if cf/saas? "saas" "selfhost") :section "team-settings"})) (let [href (-> (rt/get-current-href) (rt/encode-url)) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 957ffc6fa0..b779fc2c70 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -201,6 +201,7 @@ (reset! form nil) (st/emit! (ev/event {::ev/name event-name :type "unlimited" + :deployment (if cf/saas? "saas" "selfhost") :quantity min-members :subscription-mode subscription-mode}) (rt/nav-raw :href href)))))) @@ -216,6 +217,7 @@ href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)] (st/emit! (ev/event {::ev/name event-name :type "enterprise" + :deployment (if cf/saas? "saas" "selfhost") :subscription-mode subscription-mode}) (rt/nav-raw :href href))))) @@ -224,6 +226,7 @@ (fn [] (st/emit! (ev/event {::ev/name "open-subscription-management" ::ev/origin "settings" + :deployment (if cf/saas? "saas" "selfhost") :section "subscription-management-modal"})) (let [current-href (rt/get-current-href) returnUrl (js/encodeURIComponent current-href) @@ -489,6 +492,7 @@ (fn [] (st/emit! (ev/event {::ev/name "open-subscription-management" ::ev/origin "settings" + :deployment (if cf/saas? "saas" "selfhost") :section "subscription"})) (let [current-href (rt/get-current-href) returnUrl (js/encodeURIComponent current-href) @@ -500,6 +504,7 @@ (fn [] (st/emit! (ev/event {::ev/name "open-subscription-management" ::ev/origin "settings" + :deployment (if cf/saas? "saas" "selfhost") :section "nitrate:enterprise"})) (dnt/go-to-nitrate-billing))) diff --git a/frontend/test/frontend_tests/data/event_test.cljs b/frontend/test/frontend_tests/data/event_test.cljs new file mode 100644 index 0000000000..9add050dd4 --- /dev/null +++ b/frontend/test/frontend_tests/data/event_test.cljs @@ -0,0 +1,43 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.data.event-test + (:require + [app.main.data.event :as ev] + [app.main.repo :as rp] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as async] + [frontend-tests.helpers.mock :as mock])) + +(t/deftest events-enabled?-truth-table + (t/testing "enabled when audit-log is present" + (t/is (true? (ev/events-enabled? #{:audit-log})))) + (t/testing "enabled when telemetry is present" + (t/is (true? (ev/events-enabled? #{:telemetry})))) + (t/testing "enabled when both are present" + (t/is (true? (ev/events-enabled? #{:audit-log :telemetry})))) + (t/testing "disabled when no event flag is present" + (t/is (false? (ev/events-enabled? #{}))) + (t/is (false? (ev/events-enabled? #{:graph}))))) + +(t/deftest ^:async fetch-environment-data-uses-get-environment-data + (let [data (await + (mock/with-mocks* + {rp/cmd! (mock/stub + (fn [cmd] + (t/is (= :get-environment-data cmd)) + (rx/of {:deployment "saas" + :flags #{:audit-log}})))} + (await (async/->promise (ev/fetch-environment-data)))))] + (t/is (= {:deployment "saas" :flags #{:audit-log}} data)))) + +(t/deftest ^:async fetch-environment-data-falls-back-to-telemetry-on-error + (let [data (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_cmd] (rx/throw (ex-info "boom" {}))))} + (await (async/->promise (ev/fetch-environment-data)))))] + (t/is (= {:flags #{:telemetry}} data)))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 5e3facbf57..e4b3796282 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -10,6 +10,7 @@ [frontend-tests.copy-as-svg-test] [frontend-tests.data.comments-filters-test] [frontend-tests.data.dashboard-test] + [frontend-tests.data.event-test] [frontend-tests.data.exports-assets-test] [frontend-tests.data.nitrate-test] [frontend-tests.data.persistence-retry-test] @@ -137,6 +138,7 @@ 'frontend-tests.copy-as-svg-test 'frontend-tests.data.comments-filters-test 'frontend-tests.data.dashboard-test + 'frontend-tests.data.event-test 'frontend-tests.data.nitrate-test 'frontend-tests.data.persistence-retry-test 'frontend-tests.data.persistence-test