feat: Add Linear action item syncing for incidents#139
feat: Add Linear action item syncing for incidents#139spalmurray wants to merge 2 commits intomainfrom
Conversation
| incident = self._get_incident() | ||
|
|
||
| try: | ||
| sync_action_items_from_linear(incident) | ||
| except Exception as e: | ||
| logger.error( | ||
| f"Failed to sync action items for incident {incident.id}: {e}", | ||
| exc_info=True, | ||
| ) | ||
|
|
||
| return super().list(request, *args, **kwargs) |
There was a problem hiding this comment.
Bug: The ActionItemListView.list() method calls _get_incident() twice per request, leading to a redundant database query for the same incident object.
Severity: MEDIUM
Suggested Fix
Cache the result of the first _get_incident() call in an instance variable (e.g., self._incident). Modify get_queryset() to check if this instance variable is already populated before making a new database call, ensuring the incident is fetched only once per request.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/firetower/incidents/views.py#L341-L351
Potential issue: The `ActionItemListView.list()` endpoint executes two identical
database queries to fetch the same `Incident` object for a single request. The first
query is triggered by an explicit call to `self._get_incident()` at the start of the
`list` method. The second query is triggered implicitly when `super().list()` calls
`self.get_queryset()`, which also calls `self._get_incident()`. This redundant database
access creates an unnecessary performance overhead, representing an N+1 query pattern
where N is 1.
Did we get this right? 👍 / 👎 to inform future reviews.
| _, created = ActionItem.objects.update_or_create( | ||
| linear_issue_id=issue["id"], | ||
| defaults={ | ||
| "incident": incident, | ||
| "linear_identifier": issue["identifier"], | ||
| "title": issue["title"], | ||
| "status": issue["status"], | ||
| "assignee": assignee, | ||
| "url": issue["url"], | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Bug: An action item can be silently moved between incidents if the same Linear issue is attached to multiple incidents, due to a global unique constraint on linear_issue_id.
Severity: HIGH
Suggested Fix
Change the lookup in update_or_create to use both incident and linear_issue_id. This will correctly scope the action item to its parent incident. The model's unique constraint should also be updated to a unique_together constraint on ['incident', 'linear_issue_id'] to enforce this at the database level.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/firetower/incidents/services.py#L187-L197
Potential issue: The `ActionItem` model has a globally unique constraint on
`linear_issue_id`. The sync logic uses `ActionItem.objects.update_or_create` looking up
only by `linear_issue_id`. If a user attaches the same Linear issue to two different
Firetower incidents, the sync job for the second incident will find the existing
`ActionItem` and update its `incident` foreign key. This silently moves the action item
from the first incident to the second, causing data to disappear from the original
incident's perspective.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| firetower_url = ( | ||
| f"{settings.FIRETOWER_BASE_URL}/{settings.PROJECT_KEY}-{incident.id}" | ||
| ) | ||
| issues = _linear_service.get_issues_by_attachment_url(firetower_url) |
There was a problem hiding this comment.
URL substring matching causes cross-incident false positives
Medium Severity
The firetower_url constructed for the Linear attachment search uses a contains substring filter, which means searching for https://firetower.getsentry.net/INC-2000 will also match attachments whose URL is https://firetower.getsentry.net/INC-20000 (since the search string is a prefix of the longer URL). Given INCIDENT_ID_START is 2000, once ~18k incidents exist, syncing INC-2000 would also pull in Linear issues attached to INC-20000. Because update_or_create uses incident in defaults, these wrongly-matched issues could bounce between incidents on successive syncs, and the delete step could remove legitimate action items.
Additional Locations (1)
| exc_info=True, | ||
| ) | ||
|
|
||
| return super().list(request, *args, **kwargs) |
There was a problem hiding this comment.
Duplicate incident query per list request
Low Severity
ActionItemListView._get_incident() runs a full DB query each time it's called. The list() override calls it once for the sync, then super().list() calls get_queryset() which calls _get_incident() again — resulting in a redundant database query per request. Caching the incident on the view instance (similar to DRF's self.object pattern) would avoid the duplication.


Adds backend support for tracking Linear action items on incidents. Users create issues in Linear and attach them to the Firetower incident URL. Firetower syncs these issues into a local ActionItem model with a throttled sync (same pattern as Slack participant sync). Details: