Send telemetry event batch as plain JSON vector

The event batch sent to the telemetry server was encoded as a
fressian+zstd base64 blob. Send it as a plain vector of event maps
instead: the JSON encoder handles UUID and temporal types natively,
the payload becomes inspectable, and the receiver schema coerces
values back to proper types.

The receiver (penpot-telemetry) now accepts both the blob and the
plain vector, so it must be deployed before this backend change.

AI-assisted-by: omen-alpha
This commit is contained in:
Andrey Antukh 2026-09-07 12:57:47 +00:00
parent 3b1290c6a2
commit e96a75d366
2 changed files with 20 additions and 29 deletions

View File

@ -17,7 +17,6 @@
[app.http.client :as http] [app.http.client :as http]
[app.main :as-alias main] [app.main :as-alias main]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.util.blob :as blob]
[app.util.json :as json] [app.util.json :as json]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px])) [promesa.exec :as px]))
@ -248,20 +247,16 @@
:props (or (some-> props db/decode-transit-pgobject) {}) :props (or (some-> props db/decode-transit-pgobject) {})
:context (or (some-> context db/decode-transit-pgobject) {})})) :context (or (some-> context db/decode-transit-pgobject) {})}))
(defn- encode-batch
"Encode a sequence of event maps into a fressian+zstd base64 string
suitable for JSON transport."
^String [events]
(blob/encode-str events {:version 4}))
(defn send-event-batch (defn send-event-batch
"Send a single batch of events to the telemetry endpoint. Returns "Send a single batch of events to the telemetry endpoint. Returns
true on success." true on success. The events are sent as a plain vector of event
maps; the JSON encoder handles UUID and temporal types natively and
the receiver coerces them back to proper types."
[{:keys [::setup/props] :as cfg} batch] [{:keys [::setup/props] :as cfg} batch]
(let [payload {:type :telemetry-events (let [payload {:type :telemetry-events
:version (:full cf/version) :version (:full cf/version)
:instance-id (:instance-id props) :instance-id (:instance-id props)
:events (encode-batch batch)} :events (vec batch)}
request {:method :post request {:method :post
:uri (cf/get :telemetry-uri) :uri (cf/get :telemetry-uri)
:headers {"content-type" "application/json"} :headers {"content-type" "application/json"}

View File

@ -12,7 +12,6 @@
[app.db :as db] [app.db :as db]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.tasks.telemetry :as telemetry] [app.tasks.telemetry :as telemetry]
[app.util.blob :as blob]
[app.util.json :as json] [app.util.json :as json]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@ -59,11 +58,6 @@
:cnt :cnt
long)) long))
(defn- decode-event-batch
"Decode the base64+fressian+zstd event-batch sent to the mock."
[b64-str]
(blob/decode-str b64-str))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; STATS / REPORT STRUCTURE TESTS (existing behaviour, extended) ;; STATS / REPORT STRUCTURE TESTS (existing behaviour, extended)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -245,21 +239,19 @@
(t/is (not (contains? ev :ip-addr))))))))) (t/is (not (contains? ev :ip-addr)))))))))
(t/deftest test-batch-encoding-is-decodable (t/deftest test-batch-encoding-is-decodable
;; Verify that encode-batch produces a blob that round-trips back ;; Events are sent as a plain vector of raw event maps (no blob
;; through blob/decode to the original data. ;; encoding): every batch must JSON round-trip unchanged, because
;; the receiver coerces types from the plain JSON representation.
(let [events [{:name "navigate" :type "action" :source "telemetry" (let [events [{:name "navigate" :type "action" :source "telemetry"
:tracked-at (ct/now)} :tracked-at (ct/now)}
{:name "create-file" :type "action" :source "telemetry" {:name "create-file" :type "action" :source "telemetry"
:tracked-at (ct/now)}] :tracked-at (ct/now)}]
;; Call the private fn through the ns-mapped var encoded (json/encode-str {:events (vec events)})
encode (ns-resolve 'app.tasks.telemetry 'encode-batch) decoded (json/decode encoded)]
encoded (encode events) (t/is (vector? (:events decoded)))
decoded (decode-event-batch encoded)] (t/is (= (count events) (count (:events decoded))))
(t/is (string? encoded)) (t/is (= "navigate" (:name (first (:events decoded)))))
(t/is (seq decoded)) (t/is (= "create-file" (:name (second (:events decoded)))))))
(t/is (= (count events) (count decoded)))
(t/is (= "navigate" (:name (first decoded))))
(t/is (= "create-file" (:name (second decoded))))))
(t/deftest test-multiple-batches-when-many-events (t/deftest test-multiple-batches-when-many-events
;; Lower batch-size to 1 so that 3 events produce 3 separate ;; Lower batch-size to 1 so that 3 events produce 3 separate
@ -787,9 +779,13 @@
(t/is (= "telemetry-events" (name (:type body)))) (t/is (= "telemetry-events" (name (:type body))))
(t/is (string? (:version body))) (t/is (string? (:version body)))
(t/is (some? (:instance-id body))) (t/is (some? (:instance-id body)))
;; :events is a base64-encoded blob ;; :events is a plain vector of raw event maps
(t/is (string? (:events body))) (t/is (vector? (:events body)))
(t/is (pos? (count (:events body)))))))))) (t/is (pos? (count (:events body))))
(doseq [ev (:events body)]
(t/is (string? (:name ev)))
(t/is (string? (:source ev)))
(t/is (string? (:tracked-at ev))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK BRANCH COVERAGE ;; TASK BRANCH COVERAGE