<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #44077 # Details * Adds `historical_data` key to app and team config (and gitops) with `uptime` and `vulnerabilities` subkeys. Keys default to `true`, meaning "collect this data" * Adds `enabled_historical_dataset` and `disabled_historical_dataset` activities when these values are flipped via GitOps or the config APIs The majority of the file changes in here are GitOps test files that need to be updated to have the new config in them. **This PR does _not_ implement using these configs to actually disable data collection or purge data; that will come in a follow-up PR (as well as the front-end)** # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. n/a, unreleased ## Testing - [X] Added/updated automated tests - [ ] QA'd all new/changed functionality manually #### Defaults - [X] Fresh install: `GET /api/v1/fleet/config` returns `features.historical_data.uptime: true` and `features.historical_data.vulnerabilities: true` - [X] Created a new fleet via `POST /api/v1/fleet/teams`, then `GET /api/v1/fleet/fleets/{id}` returns `features.historical_data.uptime: true` and `features.historical_data.vulnerabilities: true` #### Global PATCH (`POST /api/v1/fleet/config`) - [X] PATCHed `{"features": {"historical_data": {"vulnerabilities": false}}}` — `vulnerabilities` flipped to `false`, `uptime` unchanged at `true` - [X] PATCHed `{"features": {"historical_data": {"uptime": false, "vulnerabilities": true}}}` — both values applied as sent - [X] PATCHed `{"features": {"historical_data": {"vulnerabilites": false}}}` (typo in sub-key) — request rejected with 4xx, stored config unchanged #### Fleet PATCH (`PATCH /api/v1/fleet/fleets/{id}`) - [X] PATCHed a fleet with `{"features": {"historical_data": {"uptime": false}}}` — fleet's `uptime` flipped to `false`, `vulnerabilities` unchanged - [X] Subsequent `GET /api/v1/fleet/fleets/{id}` returns the toggled values under `features.historical_data` (storage shape is symmetric with global) - [X] PATCHed a fleet with `{"features": {"enable_host_users": false}}` (a non-`historical_data` features sub-field) — request returned 200 but the fleet's `enable_host_users` is unchanged (silently ignored, per existing endpoint convention) #### GitOps — global (`fleetctl gitops -f global.yml`) - [X] Applied a YAML with `features.historical_data: {uptime: true, vulnerabilities: false}` — `vulnerabilities` is `false` after apply, `uptime` is `true` - [X] Applied a YAML whose `org_settings` omits `features` entirely — both sub-keys are `true` after apply (defaults injected even if previously disabled) - [X] Applied a YAML where `historical_data` only contains `uptime: false` — `uptime: false` is honored, `vulnerabilities` defaults to `true` - [X] Disabled `vulnerabilities` via the API, then ran `fleetctl gitops` with a YAML that doesn't pin it — `vulnerabilities` flips back to `true` (this is intentional; gitops is the source of truth) #### GitOps — fleet - [X] Applied a fleet YAML with `features.historical_data: {uptime: false}` — that fleet has `uptime: false`, `vulnerabilities: true` after apply - [X] Applied a fleet YAML whose `team_settings.features` omits `historical_data` — both sub-keys are `true` after apply - [X] Applied a fleet YAML that omits `features` entirely — both sub-keys are `true` after apply #### `fleetctl apply` (legacy, partial-merge) - [ ] Disabled `vulnerabilities` via the API, then ran `fleetctl apply` with a YAML that doesn't mention `historical_data` — `vulnerabilities` is still `false` (apply leaves omitted fields alone) #### Activities — global - [X] After PATCHing global to disable `vulnerabilities`, the latest activity is `disabled_historical_dataset` with payload `{"dataset": "vulnerabilities", "fleet_id": null, "fleet_name": null}` - [X] After PATCHing global with both sub-keys flipping in one request, two activities are emitted (one per sub-key) - [X] After PATCHing global with the same values that are already stored, zero new activities are emitted - [X] After re-enabling a previously disabled dataset, the activity type is `enabled_historical_dataset` #### Activities — per fleet - [X] After PATCHing fleet `workstations` to disable `uptime`, the activity is `disabled_historical_dataset` with payload `{"dataset": "uptime", "fleet_id": <workstations id>, "fleet_name": "workstations"}` - [X] Toggling the same dataset on two different fleets produces two distinct activities, one per fleet - [X] After a fleet PATCH with the same values already stored, zero new activities are emitted For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [X] Verified that the setting is exported via `fleetctl generate-gitops` - [X] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - https://github.com/fleetdm/fleet/pull/44703 - [X] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [X] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Historical-data controls: per-org and per-team toggles for uptime and vulnerability time‑series, with defaults applied when keys are omitted and enable/disable activities emitted on changes. * **Bug Fixes** * Partial updates and PATCH/GitOps flows preserve unspecified historical-data sub-keys instead of clearing them. * **Tests** * Expanded unit and integration tests covering defaults, partial PATCH/GitOps behavior, idempotency, and activity emission. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
13 KiB
Backend — types & defaults
-
Add
HistoricalDataSettingstype andFeatures.HistoricalDatafield Inserver/fleet/app.go, defineHistoricalDataSettingswithUptimeandVulnerabilitiesbool fields (JSON tagsuptime,vulnerabilities, noomitempty). Add aHistoricalData HistoricalDataSettingsfield toFeatures(JSON taghistorical_data, noomitempty). Update the "WARNING: account in the Features Clone implementation" comment block. -
Wire up
ApplyDefaults()so both sub-fields default true InFeatures.ApplyDefaults(), setf.HistoricalData.Uptime = trueandf.HistoricalData.Vulnerabilities = true.ApplyDefaultsForNewInstalls()already delegates toApplyDefaults(); no change needed there. -
Verify
Features.Copy()/Features.Clone()deep-copy correctnessHistoricalDataSettingsis value-type-only (two bools), so the existingclone := *falready deep-copies it. Verify this in the unit test rather than adding branch logic. If a future field adds a pointer/slice/map, revisit. -
Add the
Enabled(dataset string) (bool, error)helper Method onHistoricalDataSettingswith a safelist switch:"uptime"→Uptime,"cve"→Vulnerabilities, default → errorunknown dataset %q. This is the single mapping point between internal dataset names and config keys. -
Unit tests for defaults, copy, and helper In
server/fleet/app_test.go(or the closest existing file):ApplyDefaults()sets both sub-fields true.Features.Copy()produces an independentHistoricalDatavalue (mutate the original, assert the copy is unchanged).Enabled("uptime")andEnabled("cve")return the corresponding bool with no error.Enabled("unknown")returns an error andfalse.
Backend — global config endpoint
-
Integration test:
ModifyAppConfigPATCH preserves untouched sub-field Inserver/service/integration_core_test.goor equivalent: PATCH only{"features": {"historical_data": {"vulnerabilities": false}}}and assertuptimestaystrue. Regression guard for the JSON-unmarshal-into-existing-struct behavior. -
Integration test: GitOps overwrite resets omitted sub-fields to defaults Apply a global GitOps config with
features: {}(omittinghistorical_data) and assert both sub-fields return to their defaults (true,true). Documents and pins the snap-back behavior. -
Integration test: existing pre-change rows read back as
true/trueSet up anapp_config_jsonrow whose stored JSON omitshistorical_data(simulating an upgraded deployment). Read viaAppConfigand assert both sub-fields aretrue. Confirms theApplyDefaults-before-unmarshal path handles pre-upgrade data.
Backend — fleet config endpoint
-
Integration test:
ModifyTeamPATCH preserves untouched sub-field PATCH{"features": {"historical_data": {"uptime": false}}}toPATCH /api/v1/fleet/teams/{id}and assert the fleet'suptimeflips tofalsewhilevulnerabilitiesstays at its prior value. ConfirmApplyDefaultsForNewInstalls-before-unmarshal is exercised on the team read path. -
Integration test: fleet GitOps overwrite resets omitted fields to defaults Apply a fleet GitOps YAML with
features: {}(omittinghistorical_data) and assert both sub-fields return to their defaults on that fleet. -
Integration test: new fleet defaults to
true/trueCreate a new fleet, read its config back, assertfeatures.historical_data.uptimeand.vulnerabilitiesare bothtrue. Catches any regression whereTeam.Config.Featuresisn't primed with defaults on create.
Backend — activities
-
Add
ActivityTypeEnabledHistoricalDatasetandActivityTypeDisabledHistoricalDatasetInserver/fleet/activities.go, define both types with payload:Dataset string `json:"dataset"` FleetID *uint `json:"fleet_id"` FleetName *string `json:"fleet_name"`Activity-type strings:
enabled_historical_dataset/disabled_historical_dataset. Norenametotag — these are new types. -
Emit activities on toggle in
ModifyAppConfigAfter save, diffoldAppConfig.Features.HistoricalDataagainstappConfig.Features.HistoricalData. For each sub-field that flipped, emit the corresponding activity withFleetID/FleetNamenilandDatasetset to the config key ("uptime"or"vulnerabilities"). One activity per dataset that changed; zero activities if nothing changed. -
Emit activities on toggle in
ModifyTeamSame diff against the fleet's priorFeatures.HistoricalData. PopulateFleetIDandFleetNamefrom the fleet being modified. One activity per dataset whose value changed for that fleet. -
Integration test: global activity emission Three cases in
server/service/integration_core_test.go:- PATCH that flips
vulnerabilitiesonly → onedisabled_historical_datasetactivity withdataset="vulnerabilities",fleet_id=null,fleet_name=null. - PATCH that flips both → two activities (one enabled, one disabled).
- PATCH that sends the same values back → zero activities.
- PATCH that flips
-
Integration test: fleet-scoped activity emission Three cases against
PATCH /api/v1/fleet/teams/{id}:- Flip
uptimeonly on a fleet → one activity with the fleet'sfleet_idandfleet_namepopulated. - Flip both on one fleet → two activities with fleet fields populated.
- PATCH the same values back → zero activities.
- Flip the same dataset on two different fleets in separate requests → two activities, one per fleet.
- Flip
Backend — activities (GitOps fix)
-
Rename
EmitHistoricalDataActivities→OnHistoricalDataChangedRename the method inserver/service/appconfig.go(ModifyAppConfig call site) andee/server/service/teams.go(ModifyTeam call site). No functional change — this clarifies that the method is a lifecycle hook that will gain scrub-job logic in future changes. -
Fix GitOps fleet activity emission in
editTeamFromSpecInee/server/service/teams.go, in theeditTeamFromSpecfunction:- Snapshot
oldHistoricalData := team.Config.Features.HistoricalDataBEFORE the wholesaleFeaturesreplace (line 1576). - After
SaveTeamsucceeds (around line 1851), callOnHistoricalDataChanged(ctx, svc, authz.UserFromContext(ctx), oldHistoricalData, team.Config.Features.HistoricalData, &team.ID, &team.Name). This ensures activities fire when fleet-level GitOps flips historical_data, matching the behavior of PATCH endpoints.
- Snapshot
-
Integration test: fleet GitOps emits activities on historical_data flip In
cmd/fleetctl/fleetctl/gitops_test.go, add a test case (alongsideTestGitOpsHistoricalDataDefaults) that:- Applies a fleet GitOps YAML that flips
features.historical_data.uptimetofalse. - Asserts a
disabled_historical_datasetactivity exists withdataset="uptime", the fleet'sfleet_id, andfleet_name.
- Applies a fleet GitOps YAML that flips
GitOps / fleetctl
-
fleetctl GitOps test: global YAML accepts
features.historical_dataTest case incmd/fleetctl/fleetctl/gitops_test.go(or the integration suite) that applies a global YAML withfeatures.historical_dataset and confirms the resulting config matches. -
fleetctl GitOps test: fleet YAML accepts
features.historical_dataTest case that applies a fleet YAML withfeatures.historical_dataset and confirms the resulting fleet config matches. -
fleetctl GitOps test: omitted
historical_datasnaps to defaults Two cases (global and fleet) — apply a YAML withfeatures: {}and assert both sub-fields aretrueafter apply, even if they werefalsein the prior state. Documents the overwrite semantic.
Docs
-
REST API reference — global config Update
docs/REST API/rest-api.md(Modify configuration section) with the newfeatures.historical_datashape and example PATCH payload. -
REST API reference — fleet config Update the fleet-modify section of
docs/REST API/rest-api.mdwith thefeatures.historical_datashape and example PATCH payload. -
GitOps YAML reference — global Update
docs/Configuration/yaml-files.mdto documentfeatures.historical_data(global section), default values, and the overwrite-on-omit behavior with a one-paragraph note recommending GitOps-managed deployments include the key explicitly. -
GitOps YAML reference — fleet Update the fleet-YAML section of
docs/Configuration/yaml-files.mdwithfeatures.historical_datadocs. Note the AND-with-global effective-value rule even though enforcement lives in the cron change. -
Audit log reference Update
docs/Contributing/reference/audit-logs.mdwith entries forenabled_historical_datasetanddisabled_historical_dataset. Document thedataset,fleet_id,fleet_namepayload fields with examples for both global (fleet_id: null) and fleet-scoped (fleet_id: 2, fleet_name: "EMEA") variants. Note thatdatasetcarries the config key, not the internal dataset name.
Lint & verify
-
Run
make lint-go-incrementaland address any findings Standard hygiene before considering the change ready for review. -
Run
MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/... ./server/fleet/...Confirms the new types, defaults, and activity emission don't break adjacent tests (uninitialized mocks crashing other tests is the usual failure mode when adding interface methods).
Defect fix: stored-JSON round-trip clobber → backfill migration
Discovered after the change was initially marked complete: earlier
migrations using updateAppConfigJSON (and the inline TeamConfig
round-trip pattern) re-marshal the whole struct on save, persisting the
zero value of the new HistoricalData sub-fields as false/false in
stored JSON. The next read sees explicit false and the pre-unmarshal
true default loses. See Design Decision #3 for the rationale; this
PR ships the targeted backfill described there.
-
Backfill historical_data in
20260423161823_AddHostSCDDataSame migration that createshost_scd_data. UseupdateAppConfigJSONto set the AppConfig sub-keys totrue(round-trip is safe here since the values being written are non-zero). UseJSON_MERGE_PATCHforteams.configrows to add or replacefeatures.historical_dataper row without round-tripping the whole TeamConfig struct. -
Migration test asserts the backfill
20260423161823_AddHostSCDData_test.go: insert a team with a pre-existingfeaturesblock (nohistorical_data), apply the migration, then assert bothapp_config_jsonand the team'sconfighavehistorical_data.{uptime,vulnerabilities} = true. Also confirm pre-existingfeatures.*keys on the team surviveJSON_MERGE_PATCH. -
Regenerate
server/datastore/mysql/schema.sqlmake dump-test-schema. Expected diff: the seededapp_config_json.json_valuenow shows"historical_data": {"uptime": true, "vulnerabilities": true}. -
Update fleetctl testdata golden files Files rendered from the schema seed now show
historical_data: {uptime: true, vulnerabilities: true}:cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.jsonand the matching YAML / TeamMaintainer / IncludeServerConfig variants; thegenerateGitops/andmacosSetup*golden files for the same reason. -
Verify lint and the originally failing tests pass
make lint-go-incrementalthen targeted runs ofTestIntegrationsEnterprise/TestModifyTeamHistoricalData,TestIntegrationsEnterprise/TestTeamConfigHistoricalDataGitOps,TestIntegrationsCore/TestAppConfigHistoricalData, plusTestUp_20260423161823.
CI infrastructure (incidental)
- Wire fleetctl suite to build a local preview image
integrationtest/preview/preview_test.goand thefleetctljob in.github/workflows/test-go-suite.yamlwere updated so the suite builds a localfleetdm/fleet:devimage and pinsFLEET_PREVIEW_TAG=devinstead of pulling:main. Needed because new integration tests added by this change exercise paths the published:mainimage didn't yet cover. Not a spec concern — recorded here for future bisecting.
Backlog (out of scope here)
- Refactor
updateAppConfigJSONto use surgicalJSON_SETThe targeted backfill fixeshistorical_datafor this PR, but the same trap will fire for the next field added with a non-zero default on top of existing rows. The general fix is to convert the migration helper from struct-round-trip to path-basedJSON_SET, plus a sibling helper forteam_config. ~12 merged migrations need callsite conversion. Mechanical but non-trivial; carry as a separate change when there's appetite.