Skip to content

feat: Add Linear action item syncing for incidents#139

Open
spalmurray wants to merge 2 commits intomainfrom
spalmurray/linear-action-items
Open

feat: Add Linear action item syncing for incidents#139
spalmurray wants to merge 2 commits intomainfrom
spalmurray/linear-action-items

Conversation

@spalmurray
Copy link
Copy Markdown
Contributor

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:

  • New ActionItem model linked to incidents via Linear issue attachments
  • LinearService that queries Linear's GraphQL API for issues attached to a given Firetower URL
  • Throttled sync on the action items list endpoint, with a force sync endpoint
  • Django admin action to sync action items (matches existing Slack participant sync pattern)
  • Auto-creates users for Linear assignees who haven't logged into Firetower yet
  • Extracts parse_incident_id helper to deduplicate incident ID parsing across views
  • New firetower_base_url config to ensure attachment URL matching is specific enough to avoid cross-environment collisions (e.g. TESTINC-2000 vs INC-2000)

@spalmurray spalmurray marked this pull request as ready for review April 1, 2026 20:44
@spalmurray spalmurray requested a review from a team as a code owner April 1, 2026 20:44
Comment on lines +341 to +351
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +187 to +197
_, 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"],
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

exc_info=True,
)

return super().list(request, *args, **kwargs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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