Skip to content

Conversation

@NarayanBavisetti
Copy link
Collaborator

@NarayanBavisetti NarayanBavisetti commented Sep 8, 2025

Description

this pull request adds support for the keys parent, state, assignee or label in the external endpoint to track the activity whenever they are changed.

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)

Summary by CodeRabbit

  • New Features

    • Broader support for varied input formats (parent, state, labels, assignees) so updates are tracked consistently.
    • Newly assigned users are automatically subscribed when added.
  • Bug Fixes

    • More reliable detection of parent/state changes and label/assignee additions/removals.
    • Improved identifier validation and normalization to reduce missed or incorrect activity logs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Normalize input keys and ID resolution in issue_activities_task.py: add helper to extract IDs from primary or fallback keys; update trackers for parent, state, labels, and assignees to read both internal and external payload keys; validate UUIDs; fetch related models; emit IssueActivity entries and manage IssueSubscriber creation/removal; extend ISSUE_ACTIVITY_MAPPER.

Changes

Cohort / File(s) Summary
Issue activity tracking normalization & enrichment
apps/api/plane/bgtasks/issue_activities_task.py
Added `extract_ids(data: dict

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant Updater as update_issue_activity
  participant Normalizer as extract_ids / normalizer
  participant Mapper as ISSUE_ACTIVITY_MAPPER
  participant Tracker as tracker funcs (parent/state/labels/assignees)
  participant DB as DB (Issue/State/Label/User/Subscriber)
  participant Activity as IssueActivity

  Caller->>Updater: call with current_instance, requested_data
  Updater->>Normalizer: extract/normalize IDs (primary or fallback keys) and validate UUIDs
  Updater->>Mapper: map payload keys to tracker functions (includes external keys)
  Mapper->>Tracker: invoke specific tracker(s)
  rect rgba(200,230,255,0.18)
    Tracker->>DB: query old/new entities as needed (guarded by UUID checks)
    Tracker->>Activity: append activity entries (field, old, new, context)
    alt added assignees
      Tracker->>DB: bulk create IssueSubscriber
    else removed assignees
      Tracker->>DB: remove IssueSubscriber
    end
  end
  Updater-->>Caller: return aggregated IssueActivity entries
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks (1 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The current description provides a brief overview of the new external keys supported and correctly marks the type of change but omits the required Test Scenarios and References sections specified in the repository’s PR template. Without details on how the changes were tested and links to related issues, the description does not fully adhere to the template. Please add a “Test Scenarios” section describing the tests performed to verify these changes and populate the “References” section with links to any related issues or tickets so that the description fully matches the repository’s PR template.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly summarizes the main change by indicating that the pull request updates the activity keys used for work items and avoids unnecessary details or broad terminology. It clearly reflects the core purpose of the changeset and uses concise phrasing that teammates can quickly understand.

Poem

I hop through payloads, sniffing id trails,
parent or parent_id — I follow both tails.
States and labels shuffled into view,
Assignees join and some bid adieu.
A hop, a write — tidy activity trails. 🐇✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore-external-work-item-activity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@makeplane
Copy link

makeplane bot commented Sep 8, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for alternative activity tracking keys (parent, state, assignees, labels) in the external endpoint to complement existing internal keys (parent_id, state_id, assignee_ids, label_ids).

  • Updated tracking functions to handle both internal and external key formats
  • Modified label and assignee tracking logic to use conditional key checking
  • Added external endpoint key mappings to the activity tracker dispatcher

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20d139c and 25754bd.

📒 Files selected for processing (1)
  • apps/api/plane/bgtasks/issue_activities_task.py (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/plane/bgtasks/issue_activities_task.py (1)
apps/api/plane/db/models/issue.py (1)
  • Issue (104-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (5)
apps/api/plane/bgtasks/issue_activities_task.py (5)

119-125: LGTM! Improved normalization for parent ID tracking.

The change properly normalizes parent identifier access by supporting both parent_id and parent keys from external and internal payloads, making the tracking more robust and compatible with different data sources.


203-206: LGTM! Enhanced state tracking with dual key support.

The normalization allows the function to handle both state_id (internal) and state (external) keys, improving compatibility with external endpoints while maintaining backward compatibility.


311-330: LGTM! Enhanced label tracking with flexible key support.

The changes properly handle both label_ids and labels keys, providing better compatibility with external payloads while maintaining existing functionality.


395-413: LGTM! Improved assignee tracking with dual key support.

The normalization handles both assignee_ids and assignees keys effectively, with proper null checks and set conversion for comparison logic.


671-676: LGTM! Added external key mappings for activity tracking.

The addition of external endpoint keys (parent, state, assignees, labels) to the mapper enables the same tracking functions to handle both internal and external payloads seamlessly.

Copy link
Contributor

@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: 3

♻️ Duplicate comments (2)
apps/api/plane/bgtasks/issue_activities_task.py (2)

211-217: State lookups can crash; wrap/guard DoesNotExist/invalid IDs.

Current .get() calls raise on invalid/None IDs, aborting the task. Handle safely. (Echoing prior review.)

-    if current_state_id != requested_state_id:
-        new_state = State.objects.get(pk=requested_state_id)
-        old_state = State.objects.get(pk=current_state_id)
+    if current_state_id != requested_state_id:
+        try:
+            new_state = State.objects.get(pk=requested_state_id)
+            old_state = State.objects.get(pk=current_state_id)
+        except (State.DoesNotExist, ValueError, TypeError):
+            return

Optional: also constrain by project for consistency with closed_to.

#!/bin/bash
# Verify State has project relation to safely filter by project_id
rg -nP 'class\s+State\b' -C3
rg -nP '\bproject_id\b' --type=py -C2 | rg -nP 'class\s+State\b|models\.ForeignKey|project'

127-143: Validate UUIDs before querying parent to prevent ValueError on malformed IDs.

Django will raise on UUIDField conversion if given a bad UUID. Guard both lookups.

-    if current_parent_id != requested_parent_id:
-        old_parent = (
-            Issue.objects.filter(pk=current_parent_id).first()
-            if current_parent_id is not None
-            else None
-        )
-        new_parent = (
-            Issue.objects.filter(pk=requested_parent_id).first()
-            if requested_parent_id is not None
-            else None
-        )
+    if current_parent_id != requested_parent_id:
+        old_parent = (
+            Issue.objects.filter(pk=current_parent_id).first()
+            if current_parent_id and is_valid_uuid(str(current_parent_id))
+            else None
+        )
+        new_parent = (
+            Issue.objects.filter(pk=requested_parent_id).first()
+            if requested_parent_id and is_valid_uuid(str(requested_parent_id))
+            else None
+        )
🧹 Nitpick comments (1)
apps/api/plane/bgtasks/issue_activities_task.py (1)

647-651: Avoid double activities when both canonical and external keys are present.

If payload contains, e.g., state_id and state, track_state runs twice. Normalize keys before dispatch.

Changes needed outside the selected lines:

-    for key in requested_data:
-        func = ISSUE_ACTIVITY_MAPPER.get(key)
+    synonyms = {"parent": "parent_id", "state": "state_id", "assignees": "assignee_ids", "labels": "label_ids"}
+    seen = set()
+    for key in requested_data:
+        canonical = synonyms.get(key, key)
+        if canonical in seen:
+            continue
+        func = ISSUE_ACTIVITY_MAPPER.get(canonical)
         if func is not None:
             func(
-                requested_data=requested_data,
+                requested_data=requested_data,
                 current_instance=current_instance,
                 issue_id=issue_id,
                 project_id=project_id,
                 workspace_id=workspace_id,
                 actor_id=actor_id,
                 issue_activities=issue_activities,
                 epoch=epoch,
             )
+        seen.add(canonical)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25754bd and 9e5df0f.

📒 Files selected for processing (1)
  • apps/api/plane/bgtasks/issue_activities_task.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/plane/bgtasks/issue_activities_task.py (2)
apps/api/plane/api/views/issue.py (14)
  • get (219-239)
  • get (296-417)
  • get (564-580)
  • get (957-968)
  • get (994-1001)
  • get (1126-1137)
  • get (1232-1253)
  • get (1400-1411)
  • get (1555-1564)
  • get (1716-1741)
  • get (1773-1798)
  • get (2006-2021)
  • get (2123-2159)
  • get (2270-2319)
apps/api/plane/db/models/issue.py (1)
  • Issue (104-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)

Copy link
Contributor

@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

♻️ Duplicate comments (3)
apps/api/plane/bgtasks/issue_activities_task.py (3)

37-43: Fix None/scalar handling in extract_ids to prevent TypeError.

If a key exists with value None, the set comprehension iterates over None and crashes. Also accept single scalars.

-def extract_ids(data: dict | None, primary_key: str, fallback_key: str) -> set[str]:
-    if not data:
-        return set()
-    if primary_key in data:
-        return {str(x) for x in data.get(primary_key, [])}
-    return {str(x) for x in data.get(fallback_key, [])}
+def extract_ids(data: dict | None, primary_key: str, fallback_key: str) -> set[str]:
+    if not data:
+        return set()
+    def _normalize(v):
+        if v is None:
+            return []
+        if isinstance(v, (list, tuple, set)):
+            return v
+        return [v]
+    source = primary_key if primary_key in data else fallback_key
+    return {str(x) for x in _normalize(data.get(source))}

326-330: Harden label fetches to avoid DoesNotExist aborts; optionally batch to cut N+1 queries.

Use safe lookups and skip missing rows; current .get(...) can crash the task.

-        label = Label.objects.get(pk=added_label)
+        label = Label.objects.filter(pk=added_label).only("id", "name").first()
+        if not label:
+            continue
@@
-        label = Label.objects.get(pk=dropped_label)
+        label = Label.objects.filter(pk=dropped_label).only("id", "name").first()
+        if not label:
+            continue

Optional (nice): prefetch both sets once to reduce queries; I can provide a batched refactor if you want.

Also applies to: 340-356, 358-380


394-397: Assignees: fix typo and use safe user lookups to avoid crashes.

  • dropped_assginees typo;
  • User.objects.get(...) raises; prefer safe filter().first().
-    added_assignees = requested_assignees - current_assignees
-    dropped_assginees = current_assignees - requested_assignees
+    added_assignees = requested_assignees - current_assignees
+    dropped_assignees = current_assignees - requested_assignees
@@
-        assignee = User.objects.get(pk=added_asignee)
+        assignee = User.objects.filter(pk=added_asignee).only("id", "display_name").first()
+        if not assignee:
+            continue
@@
-    for dropped_assignee in dropped_assginees:
+    for dropped_assignee in dropped_assignees:
@@
-        assignee = User.objects.get(pk=dropped_assignee)
+        assignee = User.objects.filter(pk=dropped_assignee).only("id", "display_name").first()
+        if not assignee:
+            continue

Verification: confirm created_by_id/updated_by_id should be the actor (who assigns) rather than the assignee. If needed, switch to actor_id.

Also applies to: 399-459

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5df0f and 1fb627a.

📒 Files selected for processing (1)
  • apps/api/plane/bgtasks/issue_activities_task.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/plane/bgtasks/issue_activities_task.py (1)
apps/api/plane/db/models/issue.py (1)
  • Issue (104-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
apps/api/plane/bgtasks/issue_activities_task.py (1)

654-659: LGTM: external keys mapped to trackers.

The additions make the external payload keys first-class. No further concerns here.

Copy link
Contributor

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/api/plane/bgtasks/issue_activities_task.py (3)

345-345: Replace Label.objects.get() with safe lookups to avoid DoesNotExist exceptions

Using .get() will raise Label.DoesNotExist if a UUID is valid but the label doesn't exist in the database, causing the entire task to fail.

Apply these changes to handle missing labels gracefully:

-        label = Label.objects.get(pk=added_label)
+        label = Label.objects.filter(pk=added_label).only("id", "name").first()
+        if not label:
+            continue

And similarly for dropped labels at line 369:

-        label = Label.objects.get(pk=dropped_label)
+        label = Label.objects.filter(pk=dropped_label).only("id", "name").first()
+        if not label:
+            continue

Also applies to: 369-369


412-412: Replace User.objects.get() with safe lookups to avoid DoesNotExist exceptions

Using .get() will raise User.DoesNotExist if a UUID is valid but the user doesn't exist, causing the task to fail.

Apply these changes to handle missing users gracefully:

-        assignee = User.objects.get(pk=added_asignee)
+        assignee = User.objects.filter(pk=added_asignee).only("id", "display_name").first()
+        if not assignee:
+            continue

And similarly for dropped assignees at line 449:

-        assignee = User.objects.get(pk=dropped_assignee)
+        assignee = User.objects.filter(pk=dropped_assignee).only("id", "display_name").first()
+        if not assignee:
+            continue

Also applies to: 449-449


509-509: Fix potential AttributeError when new_estimate is None

When new_estimate is None (estimate point removed), accessing new_estimate.estimate.type will raise an AttributeError.

-                field="estimate_" + new_estimate.estimate.type,
+                field="estimate_" + (new_estimate.estimate.type if new_estimate else "point"),
♻️ Duplicate comments (2)
apps/api/plane/bgtasks/issue_activities_task.py (2)

37-42: Fix potential TypeError when key exists with None value

The extract_ids function will raise a TypeError if a key exists but has a None value, as the set comprehension will try to iterate over None.

Apply this fix to handle None values safely:

 def extract_ids(data: dict | None, primary_key: str, fallback_key: str) -> set[str]:
     if not data:
         return set()
     if primary_key in data:
-        return {str(x) for x in data.get(primary_key, [])}
-    return {str(x) for x in data.get(fallback_key, [])}
+        return {str(x) for x in (data.get(primary_key) or [])}
+    return {str(x) for x in (data.get(fallback_key) or [])}

127-139: Good UUID validation, but consider treating invalid IDs as None instead of returning early

The validation is good, but returning early when parent IDs are invalid might skip legitimate activity logging. Consider treating invalid IDs as None and continuing with the activity tracking.

     # Validate UUIDs before database queries
     if current_parent_id is not None and not is_valid_uuid(current_parent_id):
-        return
+        current_parent_id = None
     if requested_parent_id is not None and not is_valid_uuid(requested_parent_id):
-        return
+        requested_parent_id = None
🧹 Nitpick comments (2)
apps/api/plane/bgtasks/issue_activities_task.py (2)

140-150: Add query optimization and project scoping for parent lookups

The parent Issue queries could be optimized with only() to fetch just the required fields, improving performance.

         old_parent = (
-            Issue.objects.filter(pk=current_parent_id).first()
+            Issue.objects.filter(pk=current_parent_id)
+            .only("id", "sequence_id", "project__identifier")
+            .first()
             if current_parent_id is not None
             else None
         )
         new_parent = (
-            Issue.objects.filter(pk=requested_parent_id).first()
+            Issue.objects.filter(pk=requested_parent_id)
+            .only("id", "sequence_id", "project__identifier")
+            .first()
             if requested_parent_id is not None
             else None
         )

404-404: Fix typo in variable name

There's a typo in the variable name that should be corrected for consistency.

-    dropped_assginees = current_assignees - requested_assignees
+    dropped_assignees = current_assignees - requested_assignees

Also update the usage at line 444:

-    for dropped_assignee in dropped_assginees:
+    for dropped_assignee in dropped_assignees:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb627a and d2687b0.

📒 Files selected for processing (1)
  • apps/api/plane/bgtasks/issue_activities_task.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/plane/bgtasks/issue_activities_task.py (2)
apps/api/plane/db/models/issue.py (1)
  • Issue (104-260)
apps/api/plane/api/views/base.py (1)
  • project_id (144-150)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (4)
apps/api/plane/bgtasks/issue_activities_task.py (4)

218-229: LGTM! Well-implemented state tracking with proper validation

The state tracking implementation correctly handles both internal (state_id) and external (state) keys, validates UUIDs, normalizes invalid values to None, and scopes queries by project_id to prevent cross-project data leaks.


333-334: LGTM! Clean implementation using the extract_ids helper

The label tracking correctly uses the new extract_ids helper function to handle both internal and external keys.


399-401: LGTM! Consistent use of extract_ids for assignees

The assignee tracking implementation correctly uses the extract_ids helper for both internal and external keys.


659-663: LGTM! Well-structured mapper extension for external keys

The ISSUE_ACTIVITY_MAPPER correctly maps external endpoint keys to their respective tracking functions, enabling support for both internal and external API formats.

@pushya22 pushya22 merged commit 9ede04f into preview Sep 18, 2025
6 of 8 checks passed
@pushya22 pushya22 deleted the chore-external-work-item-activity branch September 18, 2025 14:55
yarikoptic pushed a commit to yarikoptic/plane that referenced this pull request Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants