✨ Preserve subscription audit properties (#12850)

This commit is contained in:
Marina López 2026-11-05 13:21:23 +01:00 committed by GitHub
parent c6a9e62a4a
commit e42bae34cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 132 additions and 1 deletions

View File

@ -254,7 +254,7 @@ Result: crash reports only exist if the frontend collector runs and `push-audit-
Telemetry reuses the same table with **anonymized shadow rows** (`source LIKE 'telemetry:%'`): day-truncated times, `0.0.0.0` IPs, props reduced to uuid/boolean/number values plus a few allowed fields (`lang`, `auth-backend`, derived `email-domain` — never raw emails), and a minimal context allowlist. Full + shadow rows can exist together per event when both flags are on; that duplication is on purpose.
Filters (`filter-telemetry-props` / `filter-telemetry-context`): by default only uuid/boolean/number with a simple key; exceptions: login/register/update-profile and frontend `identify` keep `lang`/`auth-backend`/`email-domain`; `organization-sso-auth-failed` keeps `failure-reason` if it is a known one; `instance-start` passes whole; frontend `navigate` keeps `[:route :file-id :team-id :page-id]`. Free text drops from telemetry but **stays in the full row and in Nexus**.
Filters (`filter-telemetry-props` / `filter-telemetry-context`): by default only uuid/boolean/number with a simple key; exceptions: login/register/update-profile and frontend `identify` keep `lang`/`auth-backend`/`email-domain`; `organization-sso-auth-failed` keeps `failure-reason` if it is a known one; `instance-start` passes whole; frontend `navigate` keeps `[:route :file-id :team-id :page-id]`. Subscription actions have event-specific contracts: backend and frontend `create-subscription` / `create-trial-subscription` preserve the default uuid/boolean/number props plus `type`, `deployment`, `quantity` and `subscription-mode`; backend `update-subscription` preserves only `type`, `deployment`, `previous-status`, `status`, `previous-cancel-at-period-end`, `cancel-at-period-end`, `previous-cancel-at`, `cancel-at` and `changed-fields`; backend `subscription-summary` preserves only `type`, `deployment`, `instance-id`, `snapshot-date`, the per-status `*-count` values, `total-count`, `sync-succeeded`, `sync-failed` and `snapshot-complete`. These subscription exceptions apply only to action events. Other free text drops from telemetry but **stays in the full row and in Nexus**.
Shipping (`backend/src/app/tasks/telemetry.clj`): the cron sends shadows to `:telemetry-uri` as JSON in 10k batches and **deletes** them on success; it purges leftovers older than 7 days. It also sends a legacy report (counters, teams, email domains...). On official hosts nothing is collected or sent (`telemetry-excluded?` covers `penpot.app`/`penpot.dev`).

View File

@ -48,6 +48,46 @@
"incomplete-user-info"
"unexpected-error"})
(def ^:private subscription-creation-events
#{"create-subscription" "create-trial-subscription"})
(def ^:private subscription-creation-telemetry-props
#{:type
:deployment
:quantity
:subscription-mode})
(def ^:private subscription-update-telemetry-props
#{:type
:deployment
:previous-status
:status
:previous-cancel-at-period-end
:cancel-at-period-end
:previous-cancel-at
:cancel-at
:changed-fields})
(def ^:private subscription-summary-telemetry-props
#{:type
:deployment
:instance-id
:snapshot-date
:trialing-count
:active-count
:active-cancel-scheduled-count
:past-due-count
:canceled-count
:unpaid-count
:incomplete-count
:incomplete-expired-count
:paused-count
:other-count
:total-count
:sync-succeeded
:sync-failed
:snapshot-complete})
(def ^:private safe-backend-context-keys
#{:version
:initiator
@ -348,6 +388,24 @@
(defn filter-telemetry-props
[{:keys [source name props type] :as params}]
(cond
(and (= type "action")
(contains? subscription-creation-events name)
(or (= source "backend")
(= source "frontend")))
(let [props' (into {} xf:filter-telemetry-props props)
props' (merge props' (select-keys props subscription-creation-telemetry-props))]
(assoc params :props props'))
(and (= source "backend")
(= type "action")
(= name "update-subscription"))
(assoc params :props (select-keys props subscription-update-telemetry-props))
(and (= source "backend")
(= type "action")
(= name "subscription-summary"))
(assoc params :props (select-keys props subscription-summary-telemetry-props))
(and (= source "backend")
(= name "organization-sso-auth-failed"))
(let [props' (into {} xf:filter-telemetry-props props)

View File

@ -828,6 +828,79 @@
(t/is (not (contains? (:props result) :active)))
(t/is (not (contains? (:props result) :label)))))
(t/deftest test-filter-telemetry-props-subscription-creation-keeps-safe-fields
(let [ftp (ns-resolve 'app.loggers.audit 'filter-telemetry-props)
organization-id (uuid/next)
props {:type "nitrate:enterprise"
:deployment "saas"
:quantity 5
:subscription-mode "paid"
:organization-id organization-id
:email "owner@example.com"
:unsafe-label "should-be-stripped"}
expected {:type "nitrate:enterprise"
:deployment "saas"
:quantity 5
:subscription-mode "paid"
:organization-id organization-id}]
(doseq [source ["backend" "frontend"]
name ["create-subscription" "create-trial-subscription"]]
(t/testing (str source " " name)
(let [result (ftp {:source source
:name name
:type "action"
:props props})]
(t/is (= expected (:props result))))))))
(t/deftest test-filter-telemetry-props-subscription-update-keeps-safe-fields
(let [ftp (ns-resolve 'app.loggers.audit 'filter-telemetry-props)
props {:type "nitrate:enterprise"
:deployment "selfhost"
:previous-status "trialing"
:status "active"
:previous-cancel-at-period-end false
:cancel-at-period-end true
:previous-cancel-at nil
:cancel-at "2026-12-01T00:00:00.000Z"
:changed-fields ["status" "cancel-at-period-end" "cancel-at"]
:email "owner@example.com"
:unsafe-label "should-be-stripped"}
result (ftp {:source "backend"
:name "update-subscription"
:type "action"
:props props})]
(t/is (= (dissoc props :email :unsafe-label)
(:props result)))))
(t/deftest test-filter-telemetry-props-subscription-summary-keeps-safe-fields
(let [ftp (ns-resolve 'app.loggers.audit 'filter-telemetry-props)
props {:type "nitrate:enterprise"
:deployment "saas"
:instance-id "00000000-0000-4000-8000-000000000099"
:snapshot-date "2026-11-05"
:trialing-count 1
:active-count 2
:active-cancel-scheduled-count 3
:past-due-count 4
:canceled-count 5
:unpaid-count 6
:incomplete-count 7
:incomplete-expired-count 8
:paused-count 9
:other-count 10
:total-count 55
:sync-succeeded 54
:sync-failed 1
:snapshot-complete false
:email "owner@example.com"
:unsafe-label "should-be-stripped"}
result (ftp {:source "backend"
:name "subscription-summary"
:type "action"
:props props})]
(t/is (= (dissoc props :email :unsafe-label)
(:props result)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SEND-EVENT-BATCH PAYLOAD STRUCTURE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;