From f29e7397b1da3a765d31f0e832668159e7e80da3 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:23:28 +0100 Subject: [PATCH] Complete schema 14 privacy disclosure Name every outbound alert-quality field in both operator-facing privacy documents, lock the current schema value in verification, and align the alert-manager adoption test with the telemetry contract. Change-source: pulse-maintainer --- docs/PRIVACY.md | 31 +++-- .../internal/subsystems/security-privacy.md | 4 + frontend-modern/public/docs/PRIVACY.md | 31 +++-- ...oad_outcome_counts_branchcov0723am_test.go | 15 +-- internal/telemetry/telemetry_test.go | 107 +++++++++--------- 5 files changed, 112 insertions(+), 76 deletions(-) diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index af1859101..cee11e11b 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -37,7 +37,7 @@ Every field is listed below with the reason it exists. Nothing else is included | Field | Example | Purpose | |-------|---------|---------| -| Schema version | `13` | Identify the exact payload contract so old and new signals are not mixed silently | +| Schema version | `14` | Identify the exact payload contract so old and new signals are not mixed silently | | Sent at | `2026-07-23T08:30:00Z` | Date the individual heartbeat without sending a history of client activity | | Install ID | `a1b2c3d4-...` | Distinguish active installations within one rotation window without tying telemetry to an account or person | | Version | `6.0.0-rc.1` | Track the canonical release identity currently deployed | @@ -93,17 +93,30 @@ Every field is listed below with the reason it exists. Nothing else is included | Alerts fired 30d | `18` | Count unique locally retained alert occurrences in the current 30-day window without sending alert text, resource IDs, or timestamps | | Alerts acknowledged 30d | `7` | Count acknowledgements in the current 30-day window without sending actors, reasons, alert IDs, or timestamps | | Alerts resolved 30d | `12` | Count resolved alert records in the current 30-day window without sending resolution details, alert IDs, or resource IDs | -| Active alerts by severity | `1` info, `2` warning, `1` critical | Three aggregate counts that reconcile to active alerts | -| Active alert age buckets | `1` under 1h, `2` 1h-24h, `1` 1d-7d, `0` 7d+ | Four closed age buckets with no alert timestamps | -| Alerts fired by severity 30d | `2` info, `10` warning, `6` critical | Three aggregate occurrence counts that reconcile to alerts fired 30d | -| Alerts resolved by severity 30d | `1` info, `7` warning, `4` critical | Three aggregate occurrence counts that reconcile to alerts resolved 30d | -| Alert resolution duration buckets 30d | `3` under 15m, `2` 15m-1h, `5` 1h-24h, `2` 1d-7d, `0` 7d+ | Five closed time-to-resolution buckets with no occurrence timestamps | -| Repeat alert occurrences 30d | `4` | Count occurrences after the first local occurrence of the same canonical alert identity, without sending that identity | -| Snoozed alert occurrences 30d | `3` | Count occurrences with a durable suppression lifecycle transition | +| Active alerts info | `1` | Count currently active info alerts without sending alert content or identity | +| Active alerts warning | `2` | Count currently active warning alerts without sending alert content or identity | +| Active alerts critical | `1` | Count currently active critical alerts without sending alert content or identity | +| Active alerts age under 1h | `1` | Count active alerts in the closed under-one-hour age bucket without sending timestamps | +| Active alerts age 1h 24h | `2` | Count active alerts in the closed one-hour-to-one-day age bucket without sending timestamps | +| Active alerts age 1d 7d | `1` | Count active alerts in the closed one-day-to-seven-day age bucket without sending timestamps | +| Active alerts age 7d plus | `0` | Count active alerts in the closed seven-days-or-more age bucket without sending timestamps | +| Alerts fired info 30d | `2` | Count info alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts fired warning 30d | `10` | Count warning alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts fired critical 30d | `6` | Count critical alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolved info 30d | `1` | Count resolved info alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolved warning 30d | `7` | Count resolved warning alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolved critical 30d | `4` | Count resolved critical alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolution under 15m 30d | `3` | Count resolved occurrences in the closed under-15-minute duration bucket without sending timestamps | +| Alerts resolution 15m 1h 30d | `2` | Count resolved occurrences in the closed 15-minute-to-one-hour duration bucket without sending timestamps | +| Alerts resolution 1h 24h 30d | `5` | Count resolved occurrences in the closed one-hour-to-one-day duration bucket without sending timestamps | +| Alerts resolution 1d 7d 30d | `2` | Count resolved occurrences in the closed one-day-to-seven-day duration bucket without sending timestamps | +| Alerts resolution 7d plus 30d | `0` | Count resolved occurrences in the closed seven-days-or-more duration bucket without sending timestamps | +| Alerts repeat occurrences 30d | `4` | Count occurrences after the first local occurrence of the same canonical alert identity, without sending that identity | +| Alerts snoozed occurrences 30d | `3` | Count occurrences with a durable suppression lifecycle transition | | Alerts resolved while snoozed 30d | `2` | Count snoozed occurrences whose recovery transition arrived before an unsnooze transition | | Alert manager tenants | `2` | Denominator for tenant-level alert configuration and persistence fields | | Alert delivery active tenants | `2` | Count tenant alert managers with delivery enabled and activation state active | -| Alert flapping detection enabled tenants | `2` | Count tenant alert managers with flapping detection configured, not detected flapping episodes | +| Alert flapping enabled tenants | `2` | Count tenant alert managers with flapping detection configured, not detected flapping episodes | | Alert intent policy configured tenants | `1` | Count tenant alert managers with an explicitly revised default, resource-type, or resource intent policy | | Alert event history authoritative tenants | `2` | Count tenant alert managers whose durable event history is authoritative | | Alert active state authoritative tenants | `2` | Count tenant alert managers whose durable active-state projection is authoritative | diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 6ba66d72c..4ed4dd434 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -74,6 +74,10 @@ than accepting secret values. The canonical and shipped privacy documents must state this retained-data and telemetry boundary explicitly and remain byte-for-byte synchronized, so the promise visible inside the product cannot drift from the repository policy. +Their telemetry table must identify the current schema version and name every +outbound JSON field individually. Grouped prose may explain related aggregates, +but it cannot replace field-level disclosure or make a newly added counter +invisible to operators reviewing exactly what Pulse sends. ## Canonical Files diff --git a/frontend-modern/public/docs/PRIVACY.md b/frontend-modern/public/docs/PRIVACY.md index af1859101..cee11e11b 100644 --- a/frontend-modern/public/docs/PRIVACY.md +++ b/frontend-modern/public/docs/PRIVACY.md @@ -37,7 +37,7 @@ Every field is listed below with the reason it exists. Nothing else is included | Field | Example | Purpose | |-------|---------|---------| -| Schema version | `13` | Identify the exact payload contract so old and new signals are not mixed silently | +| Schema version | `14` | Identify the exact payload contract so old and new signals are not mixed silently | | Sent at | `2026-07-23T08:30:00Z` | Date the individual heartbeat without sending a history of client activity | | Install ID | `a1b2c3d4-...` | Distinguish active installations within one rotation window without tying telemetry to an account or person | | Version | `6.0.0-rc.1` | Track the canonical release identity currently deployed | @@ -93,17 +93,30 @@ Every field is listed below with the reason it exists. Nothing else is included | Alerts fired 30d | `18` | Count unique locally retained alert occurrences in the current 30-day window without sending alert text, resource IDs, or timestamps | | Alerts acknowledged 30d | `7` | Count acknowledgements in the current 30-day window without sending actors, reasons, alert IDs, or timestamps | | Alerts resolved 30d | `12` | Count resolved alert records in the current 30-day window without sending resolution details, alert IDs, or resource IDs | -| Active alerts by severity | `1` info, `2` warning, `1` critical | Three aggregate counts that reconcile to active alerts | -| Active alert age buckets | `1` under 1h, `2` 1h-24h, `1` 1d-7d, `0` 7d+ | Four closed age buckets with no alert timestamps | -| Alerts fired by severity 30d | `2` info, `10` warning, `6` critical | Three aggregate occurrence counts that reconcile to alerts fired 30d | -| Alerts resolved by severity 30d | `1` info, `7` warning, `4` critical | Three aggregate occurrence counts that reconcile to alerts resolved 30d | -| Alert resolution duration buckets 30d | `3` under 15m, `2` 15m-1h, `5` 1h-24h, `2` 1d-7d, `0` 7d+ | Five closed time-to-resolution buckets with no occurrence timestamps | -| Repeat alert occurrences 30d | `4` | Count occurrences after the first local occurrence of the same canonical alert identity, without sending that identity | -| Snoozed alert occurrences 30d | `3` | Count occurrences with a durable suppression lifecycle transition | +| Active alerts info | `1` | Count currently active info alerts without sending alert content or identity | +| Active alerts warning | `2` | Count currently active warning alerts without sending alert content or identity | +| Active alerts critical | `1` | Count currently active critical alerts without sending alert content or identity | +| Active alerts age under 1h | `1` | Count active alerts in the closed under-one-hour age bucket without sending timestamps | +| Active alerts age 1h 24h | `2` | Count active alerts in the closed one-hour-to-one-day age bucket without sending timestamps | +| Active alerts age 1d 7d | `1` | Count active alerts in the closed one-day-to-seven-day age bucket without sending timestamps | +| Active alerts age 7d plus | `0` | Count active alerts in the closed seven-days-or-more age bucket without sending timestamps | +| Alerts fired info 30d | `2` | Count info alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts fired warning 30d | `10` | Count warning alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts fired critical 30d | `6` | Count critical alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolved info 30d | `1` | Count resolved info alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolved warning 30d | `7` | Count resolved warning alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolved critical 30d | `4` | Count resolved critical alert occurrences in the current 30-day window without sending alert content or identity | +| Alerts resolution under 15m 30d | `3` | Count resolved occurrences in the closed under-15-minute duration bucket without sending timestamps | +| Alerts resolution 15m 1h 30d | `2` | Count resolved occurrences in the closed 15-minute-to-one-hour duration bucket without sending timestamps | +| Alerts resolution 1h 24h 30d | `5` | Count resolved occurrences in the closed one-hour-to-one-day duration bucket without sending timestamps | +| Alerts resolution 1d 7d 30d | `2` | Count resolved occurrences in the closed one-day-to-seven-day duration bucket without sending timestamps | +| Alerts resolution 7d plus 30d | `0` | Count resolved occurrences in the closed seven-days-or-more duration bucket without sending timestamps | +| Alerts repeat occurrences 30d | `4` | Count occurrences after the first local occurrence of the same canonical alert identity, without sending that identity | +| Alerts snoozed occurrences 30d | `3` | Count occurrences with a durable suppression lifecycle transition | | Alerts resolved while snoozed 30d | `2` | Count snoozed occurrences whose recovery transition arrived before an unsnooze transition | | Alert manager tenants | `2` | Denominator for tenant-level alert configuration and persistence fields | | Alert delivery active tenants | `2` | Count tenant alert managers with delivery enabled and activation state active | -| Alert flapping detection enabled tenants | `2` | Count tenant alert managers with flapping detection configured, not detected flapping episodes | +| Alert flapping enabled tenants | `2` | Count tenant alert managers with flapping detection configured, not detected flapping episodes | | Alert intent policy configured tenants | `1` | Count tenant alert managers with an explicitly revised default, resource-type, or resource intent policy | | Alert event history authoritative tenants | `2` | Count tenant alert managers whose durable event history is authoritative | | Alert active state authoritative tenants | `2` | Count tenant alert managers whose durable active-state projection is authoritative | diff --git a/internal/monitoring/reload_outcome_counts_branchcov0723am_test.go b/internal/monitoring/reload_outcome_counts_branchcov0723am_test.go index 7d692d614..3e2369640 100644 --- a/internal/monitoring/reload_outcome_counts_branchcov0723am_test.go +++ b/internal/monitoring/reload_outcome_counts_branchcov0723am_test.go @@ -157,13 +157,11 @@ func TestBranchcov0723Am_AccumulateInstallOutcomeCounts(t *testing.T) { assert.Equal(t, sentryCounts(), counts) }) - t.Run("alert_manager_with_empty_history_increments_nothing", func(t *testing.T) { + t.Run("alert_manager_with_empty_history_reports_configuration_adoption", func(t *testing.T) { // Covers the alertManager != nil true-arm. A freshly-constructed alert - // manager has no history, so GetAlertHistorySince returns nothing and - // no alert-outcome counts accrue. (Deeper history-driven counting is - // exercised directly on accumulateAlertOutcomeCounts above; history - // cannot be injected into an alerts.Manager from outside the alerts - // package without driving the full evaluation pipeline.) + // manager has no history, so no lifecycle outcome counts accrue. It does + // still contribute one manager and its default-enabled flapping posture; + // those are configuration adoption denominators rather than outcomes. alertMgr := alerts.NewManagerWithDataDir(t.TempDir()) defer alertMgr.Stop() @@ -172,7 +170,10 @@ func TestBranchcov0723Am_AccumulateInstallOutcomeCounts(t *testing.T) { counts := sentryCounts() accumulateInstallOutcomeCounts(&counts, mon, now) - assert.Equal(t, sentryCounts(), counts, "empty history must not change any count") + expected := sentryCounts() + expected.AlertQuality.ManagerTenants = 1 + expected.AlertQuality.FlappingEnabledTenants = 1 + assert.Equal(t, expected, counts, "empty history must only add configuration adoption") }) } diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go index caa2acc52..10c45051e 100644 --- a/internal/telemetry/telemetry_test.go +++ b/internal/telemetry/telemetry_test.go @@ -2,6 +2,7 @@ package telemetry import ( "encoding/json" + "fmt" "io" "io/fs" "net/http" @@ -335,57 +336,57 @@ func TestApplySnapshot(t *testing.T) { snap := func() Snapshot { return Snapshot{ - PVENodes: 3, - VMs: 10, - Containers: 5, - AgentHosts: 2, - DockerContainers: 12, - KubernetesPods: 18, - StoragePools: 4, - PhysicalDisks: 9, - TrueNASSystems: 1, - TrueNASApps: 3, - VMwareHosts: 2, - AvailabilityTargets: 6, - AIEnabled: true, - PatrolEnabled: true, - DiscoveryEnabled: true, - NotificationsEnabled: true, - AIActionsEnabled: true, - AlertAIEnabled: true, - ActiveAlerts: 2, - PaidLicense: true, - HasAPITokens: true, - RBACCustomRoles: 3, - RBACUserAssignments: 7, - AuditReads30d: 41, - ReportSchedules: 5, - ReportSchedulesEnabled: 4, - ReportSchedulesRun30d: 2, - AgentProfiles: 9, - UpdateAttempts30d: 4, - UpdateSuccesses30d: 2, - UpdateFailures30d: 1, - UpdateLastFailureCategory: "download", - ActiveAlertsCritical: 2, - ActiveAlertsAge1h24h: 2, - AlertsFiredCritical30d: 5, - AlertsResolvedCritical30d: 3, - AlertsResolution1h24h30d: 3, - AlertsRepeatOccurrences30d: 2, - AlertsSnoozedOccurrences30d: 2, - AlertsResolvedWhileSnoozed30d: 1, - AlertManagerTenants: 2, - AlertDeliveryActiveTenants: 1, - AlertFlappingEnabledTenants: 2, - AlertIntentPolicyConfiguredTenants: 1, - AlertEventHistoryAuthoritativeTenants: 2, - AlertActiveStateAuthoritativeTenants: 2, - PulseIntelligenceLoopConfigured: true, - PulseIntelligenceLoopActive30d: true, - PulseIntelligenceCompleteOperationsLoop30d: true, - PulseIntelligenceApprovedExecutionLoop30d: true, - PulseIntelligenceResolvedOperationsLoop30d: true, + PVENodes: 3, + VMs: 10, + Containers: 5, + AgentHosts: 2, + DockerContainers: 12, + KubernetesPods: 18, + StoragePools: 4, + PhysicalDisks: 9, + TrueNASSystems: 1, + TrueNASApps: 3, + VMwareHosts: 2, + AvailabilityTargets: 6, + AIEnabled: true, + PatrolEnabled: true, + DiscoveryEnabled: true, + NotificationsEnabled: true, + AIActionsEnabled: true, + AlertAIEnabled: true, + ActiveAlerts: 2, + PaidLicense: true, + HasAPITokens: true, + RBACCustomRoles: 3, + RBACUserAssignments: 7, + AuditReads30d: 41, + ReportSchedules: 5, + ReportSchedulesEnabled: 4, + ReportSchedulesRun30d: 2, + AgentProfiles: 9, + UpdateAttempts30d: 4, + UpdateSuccesses30d: 2, + UpdateFailures30d: 1, + UpdateLastFailureCategory: "download", + ActiveAlertsCritical: 2, + ActiveAlertsAge1h24h: 2, + AlertsFiredCritical30d: 5, + AlertsResolvedCritical30d: 3, + AlertsResolution1h24h30d: 3, + AlertsRepeatOccurrences30d: 2, + AlertsSnoozedOccurrences30d: 2, + AlertsResolvedWhileSnoozed30d: 1, + AlertManagerTenants: 2, + AlertDeliveryActiveTenants: 1, + AlertFlappingEnabledTenants: 2, + AlertIntentPolicyConfiguredTenants: 1, + AlertEventHistoryAuthoritativeTenants: 2, + AlertActiveStateAuthoritativeTenants: 2, + PulseIntelligenceLoopConfigured: true, + PulseIntelligenceLoopActive30d: true, + PulseIntelligenceCompleteOperationsLoop30d: true, + PulseIntelligenceApprovedExecutionLoop30d: true, + PulseIntelligenceResolvedOperationsLoop30d: true, PulseIntelligencePatrolControlCompletedOperationsLoop30d: true, PulseIntelligencePatrolControlResolvedOperationsLoop30d: true, PulseIntelligencePatrolControlPaidCompletedOperationsLoop30d: true, @@ -697,6 +698,10 @@ func TestAllTelemetryFieldsAreDisclosed(t *testing.T) { t.Fatalf("read %s: %v", relativePath, err) } doc := normalizedTelemetryDisclosureTableText(string(raw)) + schemaDisclosure := fmt.Sprintf("schema version %d", TelemetrySchemaVersion) + if !strings.Contains(doc, schemaDisclosure) { + t.Errorf("%s must identify the current telemetry contract as %q", relativePath, schemaDisclosure) + } for _, label := range fieldLabels { if !strings.Contains(doc, label) { t.Errorf("%s must disclose telemetry field %q", relativePath, label)