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's full review
As posted on the PR — the complete output from FriendlyReviewer.
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 whenassignment_sourcematches 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_dictdoes not validate field types; invalid dicts (e.g.integration_idas a string,queuedas a string) silently produce anAssignmentSourceinstead of returningNone, 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_dictdo not implement the ISO-8601 timestamp serialization contract:queuedis serialized as a rawdatetimeand not parsed back. Emitqueued.isoformat()and parse withfromisoformat/parse_datetimeto align with the contract. - 🟡 src/sentry/models/groupassignee.py (L239)
No test covers the deassign path with a matchingassignment_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)
Thequeuedfield usestimezone.now()as a dataclass default, which is evaluated once at class definition time, so all instances share the same timestamp. Usefield(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_dict→from_dict), and exact serialization ofqueuedas ISO-8601 are not asserted. - 🔵 tests/sentry/integrations/services/test_assignment_source.py (L8)
Test naming issues:test_from_dict_empty_arrayactually passes a dict, andtest_from_dict_inalid_datais misspelled. Rename for clarity. - 🔵 src/sentry/integrations/mixins/issues.py (L411)
sync_status_outboundgained a decorativeassignment_sourceparameter 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_syncnow 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 ofsync_sourceshould be documented to avoid accidental reuse. - 🔵 src/sentry/integrations/utils/sync.py (L134)
apply_asyncalways passesassignment_source_dicteven whenassignment_sourceisNone. This is backward-compatible but a redundantNonekey; omitting it whenNonewould be cleaner and reduce payload size. - 🔵 tests/sentry/models/test_groupassignee.py (L260)
Inbound sync tests callsync_group_assignee_inboundoutsideself.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).
TheAssignmentSourcemechanism correctly suppresses outbound sync to the source integration viashould_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.
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_datatypo plustest_from_dict_empty_arrayactually 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_dictfinding at assignment_source.py L27 with the same ISO-8601 recommendation, and golden #1 (shared mutable default) matches thefield(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_syncscope 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 theapply_asyncredundantNonekey (sync.py L134), true but cosmetic.