Skip to content

ENG-2799: Track consent status per-connection instead of per-system#7557

Open
adamsachs wants to merge 10 commits intomainfrom
asachs/ENG-2799-consent-tracking
Open

ENG-2799: Track consent status per-connection instead of per-system#7557
adamsachs wants to merge 10 commits intomainfrom
asachs/ENG-2799-consent-tracking

Conversation

@adamsachs
Copy link
Contributor

@adamsachs adamsachs commented Mar 3, 2026

Ticket ENG-2799

Dependency: Stacked on fides#7555 (many:one relationship change). Rebase onto main after that merges.

Description Of Changes

Fix the consent status tracking race condition where affected_system_status was keyed by system.fides_key, causing status overwrites when a system has multiple integrations (e.g. DSR + monitoring) processing consent for the same privacy request.

Add ConnectionConfig.consent_tracking_key property that returns self.key (unique per integration) instead of system_key (shared across all integrations on the same system). All consent tracking callsites now use this property.

This is a data-level breaking change: new affected_system_status dict entries will be keyed by connection config key instead of system fides_key. Existing historical rows are unaffected (old keys remain readable, just won't be updated). Any reporting queries that aggregate by system key will need to be aware of the change.

Code Changes

  • src/fides/api/models/connectionconfig.py - Add consent_tracking_key property, deprecate system_key for consent use
  • src/fides/api/util/consent_util.py - All 6 system_key references → consent_tracking_key
  • src/fides/api/task/graph_task.py - system_keyconsent_tracking_key in cache_system_status_for_preferences
  • src/fides/api/service/connectors/consent_email_connector.py - system_keyconsent_tracking_key in skip status caching
  • tests/ops/util/test_consent_util.py - Assertions updated from connection_config.name to connection_config.key
  • tests/ops/service/connectors/test_saas_connector.py - Assertions updated from system.fides_key/system_key to consent_tracking_key
  • tests/ops/service/privacy_request/test_email_batch_send.py - Assertions updated from system_key to consent_tracking_key
  • tests/ops/service/privacy_request/test_request_runner_service.py - Assertion updated from system.fides_key to consent_tracking_key

Steps to Confirm

  1. nox -s static_checks passes (ruff + mypy clean)
  2. Create a system with 2 integrations, trigger consent propagation — each integration gets its own entry in affected_system_status instead of overwriting
  3. grep -rn "\.system_key" src/fides/api/util/consent_util.py src/fides/api/task/graph_task.py src/fides/api/service/connectors/consent_email_connector.py returns no matches

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • No UX review needed
  • Followup issues:
    • No followup issues
  • Database migrations:
    • No migrations
  • Documentation:
    • No documentation updates required

Summary by CodeRabbit

  • Bug Fixes
    • Fixed consent status tracking to operate per-connection instead of per-system. Consent status is now correctly tracked and reported independently for each connection, ensuring accurate consent reporting when multiple integrations and connectors are configured on the same system.

adamsachs and others added 7 commits March 3, 2026 10:31
- Remove lazy-load footgun in from_orm; pass system info explicitly via SQL joins
- Return lightweight ref entities from resolve_connection_config/resolve_system
- Add SystemLinkResponse.from_entity() to reduce route handler duplication
- Move TooManyLinksError check after connection config lookup in set_links
- Remove empty __table_args__ from SystemConnectionConfigLink model
- Remove unnecessary cleanup_links autouse fixtures from tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ype annotation

- Use outerjoin instead of inner join so links with deleted systems
  are still returned (with None system info) rather than silently dropped
- Make _to_entity_with_system an instance method (called via self)
- Remove unnecessary string forward reference on SystemRef type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change System.connection_configs from a scalar (uselist=False) to a list
(uselist=True) so the ORM, schema, API endpoints, and frontend all
correctly handle multiple integrations per system. The join table already
supports this — this change propagates the list semantics through every
layer.

No new migrations required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ConnectionConfig.consent_tracking_key property that uses the
connection config's key (unique per integration) instead of
system_key (shared across all integrations on a system). This
prevents status overwrites when a system has multiple integrations
(e.g. DSR + monitoring) processing consent for the same privacy
request.

affected_system_status dict keys change from system fides_key to
connection config key. This is a data-level breaking change for
consent reporting queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change all affected_system_status assertions from system_key/
system.fides_key/connection_config.name to consent_tracking_key
(connection_config.key), matching the new per-connection keying.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Mar 6, 2026 7:44pm
fides-privacy-center Ignored Ignored Mar 6, 2026 7:44pm

Request Review

adamsachs added a commit that referenced this pull request Mar 4, 2026
Add missed callsites (seed.py, consent_util.py/graph_task.py as
Category D addressed by PR #7557), expand fidesplus discovery monitor
coverage, cite exact dead code evidence, and add verification notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base automatically changed from asachs/ENG-2799 to main March 6, 2026 14:32
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

This pull request updates consent status tracking to use per-connection keys instead of system keys. Changes include adding a new consent_tracking_key property to ConnectionConfig, updating consent-related service logic and utilities to reference this new key, and revising corresponding test assertions.

Changes

Cohort / File(s) Summary
Changelog Entry
changelog/7557-per-connection-consent-tracking.yaml
Added new changelog entry marking fix for per-connection consent status tracking, labeled high-risk.
ConnectionConfig Model
src/fides/api/models/connectionconfig.py
Added new consent_tracking_key property that returns the connection config key. Deprecated system_key for consent reporting with guidance to use consent_tracking_key instead.
Consent Service Logic
src/fides/api/service/connectors/consent_email_connector.py, src/fides/api/task/graph_task.py
Updated internal preference cache calls to use consent_tracking_key instead of system_key for skipped status tracking.
Consent Utility Functions
src/fides/api/util/consent_util.py
Replaced all system_key references with consent_tracking_key in consent reporting functions: cache status initialization, completion tracking, and error handling.
Test Assertions
tests/ops/service/connectors/test_saas_connector.py, tests/ops/service/privacy_request/test_email_batch_send.py, tests/ops/service/privacy_request/test_request_runner_service.py, tests/ops/util/test_consent_util.py
Updated test expectations to use consent_tracking_key (connection config key) instead of system_key when asserting affected system status in privacy preference history dictionaries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • galvana
  • vcruces

Poem

🐰 A key by any other name doth track,
Per connection now, we've got your back!
No more conflation of sys and conn—
Consent flows clean when keys are spun. 🔑✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the issue context, code changes, and steps to confirm; however, the CHANGELOG.md update checklist item is incomplete with only box 1 checked. Complete the CHANGELOG.md update checklist by verifying and checking the high-risk label requirement and any db-migration considerations, or explicitly confirm no new entry is necessary.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: transitioning from per-system to per-connection consent status tracking.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch asachs/ENG-2799-consent-tracking

Comment @coderabbitai help to get the list of available commands and usage tips.

Use saas_example_opt_out_only_connection_config (the actual fixture) instead
of saas_example_connection_config which is not in the test's parameter list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adamsachs adamsachs marked this pull request as ready for review March 6, 2026 20:03
@adamsachs adamsachs requested a review from a team as a code owner March 6, 2026 20:03
@adamsachs adamsachs requested review from JadeCara, Linker44 and galvana and removed request for a team and JadeCara March 6, 2026 20:03
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR fixes a race condition in consent status tracking where affected_system_status was keyed by system.fides_key, causing status overwrites when a single system had multiple integrations (e.g., DSR + monitoring) processing the same privacy request concurrently. The fix introduces ConnectionConfig.consent_tracking_key (returning self.key, which is unique per integration) and replaces all six consent-tracking callsites across consent_util.py, graph_task.py, and consent_email_connector.py. Test assertions across four test files are updated to match the new key. The change is a data-level breaking change appropriately flagged as high-risk in the changelog.

Issues for follow-up:

  • The new consent_tracking_key property lacks a dedicated unit test in test_connectionconfig.py to document its contract
  • The deprecation docstring for system_key claims it is kept for "log context," but the actual logging code in saas_connector.py accesses system.fides_key directly, making the comment misleading
  • test_consent_util.py asserts against connection_config.key directly, while all other updated test files use connection_config.consent_tracking_key — this inconsistency reduces clarity of intent

Confidence Score: 4/5

  • Safe to merge; the core logic fix is correct and all consent-tracking callsites are properly updated.
  • The fix correctly substitutes a shared system-level key with a unique per-connection key, which resolves the stated race condition. All production callsites have been updated and integration tests corroborate the new behavior. Score is 4 rather than 5 because: (1) the new consent_tracking_key property lacks a dedicated unit test in test_connectionconfig.py; (2) the system_key deprecation comment is inaccurate and could mislead maintainers; (3) test_consent_util.py inconsistently uses connection_config.key directly instead of the new property name, reducing clarity.
  • src/fides/api/models/connectionconfig.py and tests/ops/util/test_consent_util.py — add unit test for consent_tracking_key, clarify deprecation comment, and align test assertions with the other test files.

Last reviewed commit: 5db70cf

Comment on lines +293 to +301
@property
def consent_tracking_key(self) -> str:
"""Unique key used for per-connection consent status tracking.

Uses the ConnectionConfig key so that each integration on a system
gets its own entry in ``affected_system_status``, avoiding the
overwrite bug when a system has multiple integrations.
"""
return self.key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unit test for consent_tracking_key

The new consent_tracking_key property has no dedicated unit test in tests/ops/models/test_connectionconfig.py, whereas system_key has test_system_key right next to it (line 161). Since this is the new canonical property for all consent status tracking, a test that verifies it always returns self.key (regardless of whether a system is linked) would document the contract and guard against accidental regressions.

def test_consent_tracking_key(self, db, connection_config, system):
    # Always returns the connection config's own key, not the system key
    assert connection_config.consent_tracking_key == connection_config.key

    SystemIntegrationLinkRepository().create_or_update_link(
        system_id=system.id,
        connection_config_id=connection_config.id,
        session=db,
    )
    db.refresh(connection_config)

    # Still returns connection key, not system fides_key
    assert connection_config.consent_tracking_key == connection_config.key
    assert connection_config.consent_tracking_key != system.fides_key

Comment on lines 280 to 291
@property
def system_key(self) -> Optional[str]:
"""Property for caching a system identifier for systems (or connector names as a fallback) for consent reporting"""
"""Property for caching a system identifier for systems (or connector names as a fallback) for consent reporting

.. deprecated::
Use :attr:`consent_tracking_key` instead. ``system_key`` returns
the *system* fides_key which conflates multiple integrations on the
same system. Kept only for backward-compatible log context.
"""
if self.system:
return self.system.fides_key
# TODO: Remove this fallback once all connection configs are linked to systems
# This will always be None in the future. `self.system` will always be set.
return self.name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading deprecation comment on system_key

The deprecation docstring states the property is "Kept only for backward-compatible log context," but no code path in the source actually calls system_key for logging — saas_connector.py accesses self.configuration.system.fides_key directly (saas_connector.py:86). The only remaining callers of .system_key in the codebase are the existing test_system_key test and seed.py (which reads system_key from seed data YAML, not from this model).

The comment as written implies there is an active log consumer that depends on this property, which could mislead future maintainers into thinking they need to keep it around. Consider clarifying, e.g.:

    @property
    def system_key(self) -> Optional[str]:
        """Returns the system fides_key (or connector name as a fallback).

        .. deprecated::
            Use :attr:`consent_tracking_key` for consent status tracking.
            ``system_key`` returns the *system* fides_key, which conflates
            multiple integrations on the same system.  Retained for any
            external callers and backward compatibility only.
        """

Comment on lines 502 to +514
@@ -511,7 +511,7 @@ def test_cache_initial_status_and_identities_for_consent_reporting(

# non-relevant systems
assert privacy_preference_history.affected_system_status == {
connection_config.name: "skipped"
connection_config.key: "skipped"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent assertion style: .key vs .consent_tracking_key

This file now asserts against connection_config.key directly, while all other updated test files (test_saas_connector.py, test_email_batch_send.py, test_request_runner_service.py) assert against connection_config.consent_tracking_key. Using the property name makes the test's intent self-documenting and means tests will automatically reflect any future change to what consent_tracking_key returns.

Change instances at lines 505, 514, 554, 563, 603, and 612 to use connection_config.consent_tracking_key instead of connection_config.key for consistency with the other test files and clearer intent documentation.

Example:

Suggested change
== {connection_config.consent_tracking_key: "pending"}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/ops/service/privacy_request/test_request_runner_service.py (1)

2023-2027: Add a regression with two connectors on one system.

This assertion only proves the renamed key for a single connector. The bug fixed by this PR is the overwrite case when two ConnectionConfig rows share the same System; please add a case that links two connectors to one system and asserts both keys survive in affected_system_status.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/ops/service/privacy_request/test_request_runner_service.py` around
lines 2023 - 2027, Add a regression test that covers the overwrite case where
two ConnectionConfig rows share the same System: create a second
ConnectionConfig (e.g., other_email_connection_config or another config object)
that references the same System as sovrn_email_connection_config, ensure the
privacy_request_with_consent_policy is set up so both connectors are exercised,
and then assert
privacy_request_with_consent_policy.privacy_preferences[0].affected_system_status
contains both keys (sovrn_email_connection_config.consent_tracking_key and the
second connector's consent_tracking_key) with their expected statuses (e.g.,
"skipped"), verifying neither key is overwritten; update related setup helpers
or fixtures to attach both configs to the same System and include the second key
in the expected dict.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@changelog/7557-per-connection-consent-tracking.yaml`:
- Around line 2-4: Update the changelog entry to explicitly document the
reporting break: state that affected_system_status is changing its key shape
from system.fides_key to connection_config.key, that existing rows will not be
backfilled, and that any downstream reporting or queries aggregating by
system.fides_key must be updated to use connection_config.key (or migrated) to
avoid data gaps; reference the field names affected_system_status,
system.fides_key, and connection_config.key so consumers can locate and update
their logic.

In `@src/fides/api/util/consent_util.py`:
- Around line 181-190: The code writes status only under
connection_config.consent_tracking_key but preexisting entries may use the
legacy system_key; update the write paths that call pref.cache_system_status(db,
connection_config.consent_tracking_key, ...) to first check
pref.get_affected_system_status/db for an existing entry under
connection_config.system_key (legacy) and, if found, migrate/normalize it to
connection_config.consent_tracking_key (copy or rename the key and remove the
legacy entry) before performing the new write so both old and new keys aren’t
left mixed; apply this normalization logic in the same way for the other call
sites that use pref.cache_system_status and touch affected_system_status (the
blocks around where ExecutionLogStatus is set and the other occurrences
referenced).

---

Nitpick comments:
In `@tests/ops/service/privacy_request/test_request_runner_service.py`:
- Around line 2023-2027: Add a regression test that covers the overwrite case
where two ConnectionConfig rows share the same System: create a second
ConnectionConfig (e.g., other_email_connection_config or another config object)
that references the same System as sovrn_email_connection_config, ensure the
privacy_request_with_consent_policy is set up so both connectors are exercised,
and then assert
privacy_request_with_consent_policy.privacy_preferences[0].affected_system_status
contains both keys (sovrn_email_connection_config.consent_tracking_key and the
second connector's consent_tracking_key) with their expected statuses (e.g.,
"skipped"), verifying neither key is overwritten; update related setup helpers
or fixtures to attach both configs to the same System and include the second key
in the expected dict.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aaf62424-c34e-4ffa-9fec-14037643615e

📥 Commits

Reviewing files that changed from the base of the PR and between 758479f and 5db70cf.

📒 Files selected for processing (9)
  • changelog/7557-per-connection-consent-tracking.yaml
  • src/fides/api/models/connectionconfig.py
  • src/fides/api/service/connectors/consent_email_connector.py
  • src/fides/api/task/graph_task.py
  • src/fides/api/util/consent_util.py
  • tests/ops/service/connectors/test_saas_connector.py
  • tests/ops/service/privacy_request/test_email_batch_send.py
  • tests/ops/service/privacy_request/test_request_runner_service.py
  • tests/ops/util/test_consent_util.py

Comment on lines +2 to +4
description: Fixed consent status tracking to key per-connection instead of per-system
pr: 7557
labels: ["high-risk"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Document the reporting break in the changelog.

This reads like a transparent bug fix, but affected_system_status is changing shape from system.fides_key to connection_config.key. Please call out that existing rows are not backfilled and any downstream reporting/query logic aggregating by system key must be updated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@changelog/7557-per-connection-consent-tracking.yaml` around lines 2 - 4,
Update the changelog entry to explicitly document the reporting break: state
that affected_system_status is changing its key shape from system.fides_key to
connection_config.key, that existing rows will not be backfilled, and that any
downstream reporting or queries aggregating by system.fides_key must be updated
to use connection_config.key (or migrated) to avoid data gaps; reference the
field names affected_system_status, system.fides_key, and connection_config.key
so consumers can locate and update their logic.

Comment on lines 181 to +190
pref.cache_system_status(
db, connection_config.system_key, ExecutionLogStatus.pending
db,
connection_config.consent_tracking_key,
ExecutionLogStatus.pending,
)
else:
pref.cache_system_status(
db, connection_config.system_key, ExecutionLogStatus.skipped
db,
connection_config.consent_tracking_key,
ExecutionLogStatus.skipped,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve rollout compatibility for preexisting status entries.

These paths now read/write only connection_config.consent_tracking_key. For requests already in flight before deploy, affected_system_status may already contain the legacy system_key, so this can leave mixed entries behind for the same connector (for example old pending plus new complete/skipped/error). Please normalize or migrate the legacy key on first write during the rollout window.

Also applies to: 205-213, 249-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fides/api/util/consent_util.py` around lines 181 - 190, The code writes
status only under connection_config.consent_tracking_key but preexisting entries
may use the legacy system_key; update the write paths that call
pref.cache_system_status(db, connection_config.consent_tracking_key, ...) to
first check pref.get_affected_system_status/db for an existing entry under
connection_config.system_key (legacy) and, if found, migrate/normalize it to
connection_config.consent_tracking_key (copy or rename the key and remove the
legacy entry) before performing the new write so both old and new keys aren’t
left mixed; apply this normalization logic in the same way for the other call
sites that use pref.cache_system_status and touch affected_system_status (the
blocks around where ExecutionLogStatus is set and the other occurrences
referenced).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant