feat(ecosystem): Implement cross-system issue synchronization

getsentry/sentry#77754
Golden recall: 100% Extra findings: 8

Golden Comments (4/4 found)

# Comment Severity Status FriendlyReviewer Detail
#1 Shared mutable default in dataclass timestamp MEDIUM ✓ Found "The queued field uses timezone.now() as a dataclass default, which is evaluated once at class definition time, so all instances share the same timestamp. Use field(default_factory=timezone.now) to get a per-instance value" (assignment_source.py L18)
#2 The method name has a typo: test_from_dict_inalid_data should be test_from_dict_invalid_data LOW ✓ Found "Test naming issues: test_from_dict_empty_array actually passes a dict, and test_from_dict_inalid_data is misspelled. Rename for clarity" (test_assignment_source.py L8)
#3 Method name says 'empty_array' but tests empty dict — consider renaming to test_from_dict_empty_dict for clarity LOW ✓ Found Same FR candidate as golden #2 (test_assignment_source.py L8): the dataset's two naming goldens are both covered by the single FR candidate that flags the typo and the misleading empty_array name
#4 to_dict() returns a datetime for queued; if this dict is passed in task kwargs (e.g., via apply_async), JSON serialization may fail depending on the serializer, which can cause enqueue errors MEDIUM ✓ Found "to_dict/from_dict do not implement the ISO-8601 timestamp serialization contract: queued is serialized as a raw datetime and not parsed back. Emit queued.isoformat() and parse with fromisoformat/parse_datetime to align with the contract" (assignment_source.py L27)

Supplementary Findings (8 findings)

Finding File Severity Legitimate?
No test verifies that other linked integrations still receive outbound sync when assignment_source matches one integration test_groupassignee.py:221 HIGH ✓ The MR's core behavioral guarantee is untested — the most valuable finding of this review
from_dict does not validate field types; invalid dicts silently produce an AssignmentSource instead of None assignment_source.py:31 MEDIUM ✓ Real violation of the safe-deserialization contract
No test covers the deassign path with a matching assignment_source groupassignee.py:239 MEDIUM ✓ Legitimate coverage gap mirroring the assign path
Unit tests omit required edge cases (missing fields, invalid types, from_integration, round-trip, exact ISO-8601 serialization) test_assignment_source.py:7 LOW ✓ Reasonable test-completeness point
sync_status_outbound gained a decorative assignment_source parameter that is never consumed anywhere issues.py:411 LOW ✓ Scope creep and misleading dead API — accurate
should_sync keys the cycle-break on integration identity regardless of attribute, conflating assignee-specific logic with generic sync issues.py:388 LOW ✓ Legitimate design-scope concern
apply_async always passes assignment_source_dict even when assignment_source is None sync.py:134 LOW ✓ Minor but real payload redundancy — the weakest item, cosmetic
Inbound sync tests call sync_group_assignee_inbound outside self.tasks(), so outbound suppression is never executed end-to-end test_groupassignee.py:260 LOW ✓ Real test-fidelity issue

Honest Analysis

FriendlyReviewer found all 4 golden comments — 100% recall — with a caveat. Goldens #2 and #3 are both test-naming nits covered by a single finding at test_assignment_source.py L8 (the test_from_dict_inalid_data typo plus test_from_dict_empty_array actually passing a dict), so the dataset counts one finding twice. Unique-bug recall is 3/3, and the duplicated entry flatters the headline number.

The substantive hits are exact. Golden #4 (the serialization contract) matches FR's to_dict/from_dict finding at assignment_source.py L27 with the same ISO-8601 recommendation, and golden #1 (shared mutable default) matches the field(default_factory=timezone.now) fix verbatim — though FR flagged it LOW while the dataset rates it MEDIUM. Notably, the review's single high-severity finding — no test verifies that other linked integrations still receive outbound sync when one source is suppressed — is not in the dataset at all. It targets the MR's core behavioral guarantee and is the most valuable comment in the review.

Weaknesses: 7 of 11 findings are LOW and several are coverage commentary (missing edge cases, missing deassign test, tests run outside self.tasks()). That is legitimate content, but the perfect score is built on a dataset that heavily weights exactly the kind of low-hanging naming and serialization nits FR picks up reliably. The severity discordance on golden #1 (Medium in the dataset, Low by FR) is a genuine calibration gap, not a judging artifact.

The decorative-parameter and should_sync scope findings (issues.py L411/L388) show FR reading beyond the dataclass into the surrounding sync machinery — useful, but hard to credit without a human verdict. All 8 supplementary findings are legitimate; the weakest is the apply_async redundant None key (sync.py L134), true but cosmetic.

FriendlyReviewer's full review

As posted on the PR — the complete output from FriendlyReviewer.

friendly-reviewer bot commented now

Hi there 👋

🌥️ Tech 🌤️ Feat
1 high, 3 medium, 7 low 1 ok, 1 mitigated

The MR introduces an AssignmentSource dataclass to break assignment sync cycles by suppressing outbound propagation back to the integration that triggered the change. The core cycle-breaking logic is well designed and correctly threaded through groupassignee.assign/deassign and sync_assignee_outbound. However, the new dataclass does not fully meet its stated serialization/deserialization contract, and the most important behavioral guarantee—preserving outbound propagation to other linked integrations—is not covered by tests. These gaps should be addressed before merging.


These might need a close look

  • 🔴 tests/sentry/models/test_groupassignee.py (L221)
    No test verifies that when assignment_source matches one integration, other linked integrations still receive outbound sync. This is the core guarantee of the MR (breaking the loop while preserving cross-integration propagation) and is untested.

Worth checking

  • 🟡 src/sentry/integrations/services/assignment_source.py (L31)
    from_dict does not validate field types; invalid dicts (e.g. integration_id as a string, queued as a string) silently produce an AssignmentSource instead of returning None, violating the safe-deserialization contract. Validate types and parse ISO-8601 timestamps as needed.
  • 🟡 src/sentry/integrations/services/assignment_source.py (L27)
    to_dict/from_dict do not implement the ISO-8601 timestamp serialization contract: queued is serialized as a raw datetime and not parsed back. Emit queued.isoformat() and parse with fromisoformat/parse_datetime to align with the contract.
  • 🟡 src/sentry/models/groupassignee.py (L239)
    No test covers the deassign path with a matching assignment_source (inbound-driven unassign should suppress outbound to the source). Add a deassign analogue of the assign-with-matching-source test.
Small things (take or leave)
  • 🔵 src/sentry/integrations/services/assignment_source.py (L18)
    The queued field uses timezone.now() as a dataclass default, which is evaluated once at class definition time, so all instances share the same timestamp. Use field(default_factory=timezone.now) to get a per-instance value.
  • 🔵 tests/sentry/integrations/services/test_assignment_source.py (L7)
    Unit tests omit required edge cases: missing required fields, invalid types, from_integration, round-trip (to_dictfrom_dict), and exact serialization of queued as ISO-8601 are not asserted.
  • 🔵 tests/sentry/integrations/services/test_assignment_source.py (L8)
    Test naming issues: test_from_dict_empty_array actually passes a dict, and test_from_dict_inalid_data is misspelled. Rename for clarity.
  • 🔵 src/sentry/integrations/mixins/issues.py (L411)
    sync_status_outbound gained a decorative assignment_source parameter that is never consumed anywhere and is not wired through the status-outbound flow. This is scope creep and misleading dead API; either wire it through or revert.
  • 🔵 src/sentry/integrations/mixins/issues.py (L388)
    should_sync now keys the cycle-break on integration identity regardless of attribute, conflating assignee-specific logic with generic sync. Only assignee passes a source today, but the scope of sync_source should be documented to avoid accidental reuse.
  • 🔵 src/sentry/integrations/utils/sync.py (L134)
    apply_async always passes assignment_source_dict even when assignment_source is None. This is backward-compatible but a redundant None key; omitting it when None would be cleaner and reduce payload size.
  • 🔵 tests/sentry/models/test_groupassignee.py (L260)
    Inbound sync tests call sync_group_assignee_inbound outside self.tasks(), so the resulting outbound suppression logic is never executed and the assignment-source suppression is not asserted end-to-end.

✅ Feature-level checklist

Looks good

  • Assignment changes triggered by an inbound webhook from a given integration must not be propagated back to the same integration (cycle break).
    The AssignmentSource mechanism correctly suppresses outbound sync to the source integration via should_sync, and the flow is traced correctly for assign and deassign. However, the deassign path lacks a dedicated test for this suppression.

Partially covered

  • ⚠️ Other linked integrations must continue to receive outbound assignment updates when the source integration is suppressed.
    The code correctly dispatches outbound sync to all linked integrations, but there is no test covering the multi-integration scenario. The core guarantee is implemented but unverified, leaving a risk of regression.