Skip to content

Conversation

@sriramveeraghanta
Copy link
Member

@sriramveeraghanta sriramveeraghanta commented Dec 23, 2025

Note

Centralizes analytics for key workspace actions and surfaces an additional user field.

  • Add track_event Celery task using PostHog, with preprocessing to set role based on workspace ownership and grouping by workspace slug
  • Define analytics event constants in utils/analytics_events.py
  • Emit events on workspace creation/deletion, invitations sent, and join/accept flows across workspace/base.py, workspace/invite.py, and workspace_project_join.py
  • Replace legacy invite/auth event tasks with the new unified tracker
  • Add last_login_time to UserMeSerializer fields

Written by Cursor Bugbot for commit 3ac3ce3. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features
    • Last login timestamp now displayed in user profile information.
    • Enhanced event tracking for workspace lifecycle (creation, deletion) and user invitation events (sent invitations, workspace joins).

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 23, 2025 14:13
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

Walkthrough

The changes implement a centralized event tracking system for workspace lifecycle management. New analytics event constants are defined, and workspace-related operations (creation, deletion, user invitations, and joins) now emit standardized events through a unified event tracking task that enriches event payloads with role information derived from workspace ownership.

Changes

Cohort / File(s) Summary
Analytics Infrastructure
apps/api/plane/utils/analytics_events.py, apps/api/plane/bgtasks/event_tracking_task.py
Added four new event constants (USER_JOINED_WORKSPACE, USER_INVITED_TO_WORKSPACE, WORKSPACE_CREATED, WORKSPACE_DELETED). Introduced centralized track_event function with preprocess_data_properties helper to enrich payloads with role information and send events to PostHog.
User Serializer Updates
apps/api/plane/app/serializers/user.py
Added last_login_time field to UserSerializer and UserMeSerializer for read-only access.
Workspace Lifecycle Tracking
apps/api/plane/app/views/workspace/base.py
Integrated event tracking on workspace creation (WORKSPACE_CREATED) and deletion (WORKSPACE_DELETED) with user, workspace, and timestamp metadata.
Invitation & Join Events
apps/api/plane/app/views/workspace/invite.py, apps/api/plane/authentication/utils/workspace_project_join.py
Replaced legacy workspace_invite_event with centralized track_event emissions for user invitations (USER_INVITED_TO_WORKSPACE) and workspace joins (USER_JOINED_WORKSPACE) across invitation acceptance and bulk invite flows.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/API
    participant View as View/API Handler
    participant Task as track_event<br/>(Celery Task)
    participant Preprocess as preprocess_data_<br/>properties()
    participant PostHog as PostHog Analytics

    User->>View: Trigger workspace action<br/>(create/delete/invite/join)
    View->>Task: track_event.delay(user_id, event_name, slug, properties)
    Note over View: Non-blocking async call
    
    Task->>Preprocess: Enrich payload with role
    Preprocess->>Preprocess: Query workspace ownership<br/>to derive role
    Preprocess-->>Task: Return enriched properties
    
    Task->>Task: Validate PostHog config
    Task->>PostHog: capture(distinct_id, event_name,<br/>enriched_properties, group)
    PostHog-->>Task: Event recorded
    Task-->>View: Task complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'chore: workspace events' is vague and generic, using non-descriptive terms that don't convey the specific changes like event tracking implementation or analytics centralization. Consider a more specific title such as 'chore: add workspace event tracking with PostHog' or 'chore: centralize analytics for workspace lifecycle events' to better describe the main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description provides a comprehensive summary of changes but does not follow the required template structure with explicit sections for Type of Change, Test Scenarios, and References.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore-workspace-events

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.

@sriramveeraghanta sriramveeraghanta merged commit 777200d into preview Dec 23, 2025
16 of 21 checks passed
@sriramveeraghanta sriramveeraghanta deleted the chore-workspace-events branch December 23, 2025 14:17
Copy link

@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.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on January 20

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

if event_name == USER_INVITED_TO_WORKSPACE or event_name == WORKSPACE_DELETED:
try:
# Check if the current user is the workspace owner
workspace = Workspace.objects.get(slug=slug)
Copy link

Choose a reason for hiding this comment

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

Workspace lookup fails during deletion events

In preprocess_data_properties, a workspace lookup by slug is performed during WORKSPACE_DELETED events. However, because the workspace is soft-deleted and its slug is modified (appended with a timestamp) before the background task executes, the lookup consistently fails. This causes the actor's role to be incorrectly reported as unknown in analytics.

Additional Locations (1)

Fix in Cursor Fix in Web

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 introduces a unified event tracking system for workspace-related activities. The implementation refactors the existing event tracking mechanism to use a consistent track_event function and adds tracking for key workspace operations including creation, deletion, invitations, and user joins.

Key Changes:

  • Introduced new analytics event constants for workspace lifecycle events
  • Refactored event tracking into a unified track_event task with preprocessing capabilities
  • Added event tracking across workspace creation, deletion, invitation, and join workflows

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
apps/api/plane/utils/analytics_events.py Defines constants for workspace event names
apps/api/plane/bgtasks/event_tracking_task.py Refactors event tracking into a unified function with role preprocessing logic
apps/api/plane/authentication/utils/workspace_project_join.py Adds event tracking when users join workspaces via invitation
apps/api/plane/app/views/workspace/invite.py Adds event tracking for workspace invitations and acceptances
apps/api/plane/app/views/workspace/base.py Adds event tracking for workspace creation and deletion
apps/api/plane/app/serializers/user.py Adds last_login_time field to user serializer (contains duplicate field issue)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"user_id": request.user.id,
"workspace_id": data["id"],
"workspace_slug": data["slug"],
"role": "owner",
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Hardcoded role value may be inaccurate: The role is hardcoded as "owner" in the event properties, but this should be determined based on the actual workspace ownership. The preprocess_data_properties function already handles role determination for certain events. Consider either using the numeric role value (20) that was set during workspace member creation on line 124, or let the preprocessing function handle it consistently.

Suggested change
"role": "owner",
"role": data.get("role"),

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +194
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"role": "owner",
"workspace_name": workspace.name,
"deleted_at": str(timezone.now().isoformat()),
},
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Redundant data in event properties: user_id, workspace_id, and workspace_slug are passed both as function parameters and included again in the event_properties dictionary. This creates redundancy and could lead to inconsistencies. Consider removing these from event_properties since they're already available as function parameters.

Copilot uses AI. Check for mistakes.
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"role": "owner",
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Hardcoded role value may be inaccurate: The role is hardcoded as "owner" in the event properties. However, the preprocess_data_properties function checks if the user is the workspace owner and sets the role accordingly. Since WORKSPACE_DELETED is one of the events that triggers this preprocessing (line 42), the hardcoded value will be overwritten. Either remove the hardcoded "role" field and let preprocessing handle it, or ensure the logic is consistent.

Suggested change
"role": "owner",

Copilot uses AI. Check for mistakes.
"workspace_id": workspace_member_invite.workspace.id,
"workspace_slug": workspace_member_invite.workspace.slug,
"role": workspace_member_invite.role,
"joined_at": str(timezone.now().isoformat()),
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The joined_at timestamp is being computed inside the loop, which means each workspace invitation will receive the same timestamp even though the track_event.delay() calls execute asynchronously. Consider capturing the timestamp once before the loop to ensure consistency across all events, or alternatively, move the timestamp computation to inside the async task itself.

Copilot uses AI. Check for mistakes.
event_name=USER_JOINED_WORKSPACE,
slug=workspace_member_invite.workspace.slug,
event_properties={
"user_id": user.id,
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Redundant conversion: user.id is already a UUID, so passing it to track_event.delay() and then converting it to string twice in event_properties (as "user_id": user.id) is inefficient. The user_id parameter already captures this information, so including it again in event_properties is redundant.

Suggested change
"user_id": user.id,

Copilot uses AI. Check for mistakes.
"workspace_id": invitation.workspace.id,
"workspace_slug": invitation.workspace.slug,
"role": invitation.role,
"joined_at": str(timezone.now()),
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Inconsistent timestamp formatting: str(timezone.now()) is used here, while str(timezone.now().isoformat()) is used in other places (e.g., line 50 in workspace_project_join.py and line 193 in workspace/base.py). This creates inconsistent data in analytics. Consider standardizing to either isoformat() or regular string formatting across all event tracking calls.

Suggested change
"joined_at": str(timezone.now()),
"joined_at": str(timezone.now().isoformat()),

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +52
try:
# Check if the current user is the workspace owner
workspace = Workspace.objects.get(slug=slug)
if str(workspace.owner_id) == str(user_id):
data_properties["role"] = "owner"
else:
data_properties["role"] = "admin"
except Workspace.DoesNotExist:
logger.warning(f"Workspace {slug} does not exist while sending event {event_name} for user {user_id}")
data_properties["role"] = "unknown"
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Potential race condition: The workspace lookup in the database (line 45) could fail if the workspace is deleted between the time the event is queued and when this preprocessing function runs. While the exception is caught, this creates a scenario where a role of "unknown" is assigned. Consider if this is the intended behavior, or if the role should be passed explicitly in the event properties to avoid this race condition.

Copilot uses AI. Check for mistakes.
"user_timezone",
"username",
"is_password_autoset",
"is_email_verified",
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Duplicate field in serializer: is_email_verified appears twice in the fields list (lines 75 and 79). This is redundant and should be removed. Keep only one occurrence of this field.

Suggested change
"is_email_verified",

Copilot uses AI. Check for mistakes.
"workspace_id": workspace_invite.workspace.id,
"workspace_slug": workspace_invite.workspace.slug,
"role": workspace_invite.role,
"joined_at": str(timezone.now()),
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Inconsistent timestamp formatting: str(timezone.now()) is used here, while str(timezone.now().isoformat()) is used in other places (e.g., line 50 in workspace_project_join.py and line 193 in workspace/base.py). This creates inconsistent data in analytics. Consider standardizing to either isoformat() or regular string formatting across all event tracking calls.

Suggested change
"joined_at": str(timezone.now()),
"joined_at": str(timezone.now().isoformat()),

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +147
event_properties={
"user_id": request.user.id,
"workspace_id": data["id"],
"workspace_slug": data["slug"],
"role": "owner",
"workspace_name": data["name"],
"created_at": data["created_at"],
},
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Redundant data in event properties: user_id, workspace_id, and workspace_slug are passed both as function parameters and included again in the event_properties dictionary. This creates redundancy and could lead to inconsistencies. Consider removing these from event_properties since they're already available as function parameters.

Copilot uses AI. Check for mistakes.
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: 5

🧹 Nitpick comments (2)
apps/api/plane/authentication/utils/workspace_project_join.py (1)

34-52: Remove redundant str() wrapper around .isoformat().

Line 50 uses str(timezone.now().isoformat()), but .isoformat() already returns a string, making the str() wrapper redundant.

🔎 Proposed fix
-                "joined_at": str(timezone.now().isoformat()),
+                "joined_at": timezone.now().isoformat(),
apps/api/plane/bgtasks/event_tracking_task.py (1)

58-77: Inconsistent return values.

The function returns None (implicitly) when PostHog is not configured (line 63) but returns False on exception (line 77). While this doesn't affect callers using .delay(), it's better to be consistent.

🔎 Proposed fix
     if not (POSTHOG_API_KEY and POSTHOG_HOST):
         logger.warning("Event tracking is not configured")
-        return
+        return False
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d09c91b and 3ac3ce3.

📒 Files selected for processing (6)
  • apps/api/plane/app/serializers/user.py
  • apps/api/plane/app/views/workspace/base.py
  • apps/api/plane/app/views/workspace/invite.py
  • apps/api/plane/authentication/utils/workspace_project_join.py
  • apps/api/plane/bgtasks/event_tracking_task.py
  • apps/api/plane/utils/analytics_events.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-23T18:18:06.875Z
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.

Applied to files:

  • apps/api/plane/authentication/utils/workspace_project_join.py
🧬 Code graph analysis (3)
apps/api/plane/authentication/utils/workspace_project_join.py (3)
apps/api/plane/db/models/project.py (2)
  • ProjectMember (205-249)
  • ProjectMemberInvite (187-202)
apps/api/plane/db/models/workspace.py (2)
  • WorkspaceMember (194-224)
  • WorkspaceMemberInvite (227-251)
apps/api/plane/bgtasks/event_tracking_task.py (1)
  • track_event (58-77)
apps/api/plane/app/views/workspace/base.py (1)
apps/api/plane/bgtasks/event_tracking_task.py (1)
  • track_event (58-77)
apps/api/plane/bgtasks/event_tracking_task.py (3)
apps/api/plane/db/models/workspace.py (1)
  • Workspace (115-178)
apps/api/plane/app/views/workspace/base.py (3)
  • get (204-235)
  • get (239-249)
  • get (258-343)
apps/api/plane/utils/exception_logger.py (1)
  • log_exception (9-20)
⏰ 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). (3)
  • GitHub Check: Agent
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apps/api/plane/app/serializers/user.py (1)

81-81: LGTM! Appropriate field addition.

The addition of last_login_time to UserMeSerializer is consistent with the existing read-only fields pattern and aligns with other timestamp fields already exposed in this serializer.

apps/api/plane/app/views/workspace/base.py (1)

136-148: LGTM! Workspace creation event tracking is well-integrated.

The event is emitted after the workspace is created and seeded, with all necessary properties included. The role "owner" is appropriate for workspace creation since the creating user is always the owner.

apps/api/plane/utils/analytics_events.py (1)

1-4: LGTM! Clean constant definitions.

The analytics event constants are well-named and follow consistent naming conventions. This centralization improves maintainability and prevents typos across the codebase.

Comment on lines +183 to +195
track_event.delay(
user_id=request.user.id,
event_name=WORKSPACE_DELETED,
slug=workspace.slug,
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"role": "owner",
"workspace_name": workspace.name,
"deleted_at": str(timezone.now().isoformat()),
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the Workspace model to understand soft delete behavior
fd -t f "models.py" apps/api/plane | head -20

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Find event_tracking_task.py
fd -t f "event_tracking_task.py" apps/

Repository: makeplane/plane

Length of output: 104


🏁 Script executed:

# Search for preprocess_data_properties function
rg "preprocess_data_properties" --type py -B 2 -A 10

Repository: makeplane/plane

Length of output: 2295


🏁 Script executed:

# Verify isoformat() return type with Python
python3 << 'EOF'
from datetime import datetime
result = datetime.now().isoformat()
print(f"Type of isoformat(): {type(result)}")
print(f"Value: {result}")
print(f"str(isoformat()) == isoformat(): {str(result) == result}")
EOF

Repository: makeplane/plane

Length of output: 145


🏁 Script executed:

# Find Workspace model
rg "class Workspace" --type py -A 5 | head -50

Repository: makeplane/plane

Length of output: 3360


🏁 Script executed:

# Check for soft delete implementation (is_deleted field, deleted_at, etc.)
rg "is_deleted|deleted_at" apps/api/plane/db/models/ --type py | grep -i workspace

Repository: makeplane/plane

Length of output: 3379


🏁 Script executed:

# Look at the actual destroy method in the view file
cat -n apps/api/plane/app/views/workspace/base.py | sed -n '180,200p'

Repository: makeplane/plane

Length of output: 955


🏁 Script executed:

# Find the Workspace model definition
fd -t f "workspace.py" apps/api/plane/db/models/

Repository: makeplane/plane

Length of output: 96


🏁 Script executed:

# Get the Workspace model class definition
cat -n apps/api/plane/db/models/workspace.py | head -100

Repository: makeplane/plane

Length of output: 3306


🏁 Script executed:

# Look for custom manager or queryset in Workspace model
rg "class Workspace" apps/api/plane/db/models/workspace.py -A 30 | head -50

Repository: makeplane/plane

Length of output: 1952


🏁 Script executed:

# Check the BaseModel to understand soft delete implementation
cat -n apps/api/plane/db/models/base.py | head -150

Repository: makeplane/plane

Length of output: 1710


🏁 Script executed:

# Search for the manager or queryset that filters deleted records
rg "deleted_at" apps/api/plane/db/models/base.py -B 5 -A 5

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Check AuditModel which BaseModel inherits from
rg "class AuditModel" apps/api/plane --type py -A 20

Repository: makeplane/plane

Length of output: 1389


🏁 Script executed:

# Also search for where deleted_at is defined in the codebase
rg "deleted_at = models" apps/api/plane/db/ --type py

Repository: makeplane/plane

Length of output: 174


🏁 Script executed:

# Check the SoftDeleteModel implementation
rg "class SoftDeleteModel" apps/api/plane/db/mixins.py -A 50

Repository: makeplane/plane

Length of output: 1771


🏁 Script executed:

# Find the SoftDeletionManager implementation
rg "class SoftDeletionManager" apps/api/plane --type py -A 30

Repository: makeplane/plane

Length of output: 1966


Remove redundant str() wrapper and address async workspace retrieval risk.

Line 193 uses str(timezone.now().isoformat()), but .isoformat() already returns a string—the str() wrapper is redundant.

Additionally, the async track_event.delay() call at line 183 is executed before super().destroy() at line 196. When the async task later calls Workspace.objects.get(slug=slug) in preprocess_data_properties, the workspace has been soft-deleted and is filtered out by the default manager (which filters deleted_at__isnull=True), causing the query to fail. While the exception is caught and the event is still tracked with the pre-populated role: "owner", the role enrichment attempt fails silently. Consider either moving track_event.delay() after super().destroy() or using Workspace.all_objects.get() to query the deleted workspace.

-                "deleted_at": str(timezone.now().isoformat()),
+                "deleted_at": timezone.now().isoformat(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
track_event.delay(
user_id=request.user.id,
event_name=WORKSPACE_DELETED,
slug=workspace.slug,
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"role": "owner",
"workspace_name": workspace.name,
"deleted_at": str(timezone.now().isoformat()),
},
)
track_event.delay(
user_id=request.user.id,
event_name=WORKSPACE_DELETED,
slug=workspace.slug,
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"role": "owner",
"workspace_name": workspace.name,
"deleted_at": timezone.now().isoformat(),
},
)
🤖 Prompt for AI Agents
In apps/api/plane/app/views/workspace/base.py around lines 183 to 195, remove
the redundant str() wrapper around timezone.now().isoformat() and move the
track_event.delay(...) call to after the super().destroy() call (so scheduling
happens once deletion is complete); also ensure the event_properties include all
necessary workspace info (id, slug, name, role) so the async task does not need
to re-query the default Workspace manager for a soft-deleted record
(alternatively, change the async task to use Workspace.all_objects.get(...) when
enriching).

Comment on lines +125 to +137
track_event.delay(
user_id=request.user.id,
event_name=USER_INVITED_TO_WORKSPACE,
slug=slug,
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"invitee_role": invitation.role,
"invited_at": str(timezone.now()),
"invitee_email": invitation.email,
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use .isoformat() for consistent timestamp format.

Line 134 uses str(timezone.now()) which produces a different format than the .isoformat() method used elsewhere in the codebase. This inconsistency can cause issues in analytics systems expecting ISO 8601 format.

🔎 Proposed fix
-                    "invited_at": str(timezone.now()),
+                    "invited_at": timezone.now().isoformat(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
track_event.delay(
user_id=request.user.id,
event_name=USER_INVITED_TO_WORKSPACE,
slug=slug,
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"invitee_role": invitation.role,
"invited_at": str(timezone.now()),
"invitee_email": invitation.email,
},
)
track_event.delay(
user_id=request.user.id,
event_name=USER_INVITED_TO_WORKSPACE,
slug=slug,
event_properties={
"user_id": request.user.id,
"workspace_id": workspace.id,
"workspace_slug": workspace.slug,
"invitee_role": invitation.role,
"invited_at": timezone.now().isoformat(),
"invitee_email": invitation.email,
},
)
🤖 Prompt for AI Agents
In apps/api/plane/app/views/workspace/invite.py around lines 125 to 137, the
tracked event uses str(timezone.now()) which yields a non-ISO timestamp; replace
that call with timezone.now().isoformat() so the event's "invited_at" field uses
a consistent ISO 8601 formatted timestamp (preserving timezone awareness).

Comment on lines +203 to +214
track_event.delay(
user_id=user.id,
event_name=USER_JOINED_WORKSPACE,
slug=slug,
event_properties={
"user_id": user.id,
"workspace_id": workspace_invite.workspace.id,
"workspace_slug": workspace_invite.workspace.slug,
"role": workspace_invite.role,
"joined_at": str(timezone.now()),
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use .isoformat() for consistent timestamp format.

Line 212 uses str(timezone.now()) which produces a different format than the .isoformat() method used elsewhere in the codebase. This inconsistency can cause issues in analytics systems expecting ISO 8601 format.

🔎 Proposed fix
-                            "joined_at": str(timezone.now()),
+                            "joined_at": timezone.now().isoformat(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
track_event.delay(
user_id=user.id,
event_name=USER_JOINED_WORKSPACE,
slug=slug,
event_properties={
"user_id": user.id,
"workspace_id": workspace_invite.workspace.id,
"workspace_slug": workspace_invite.workspace.slug,
"role": workspace_invite.role,
"joined_at": str(timezone.now()),
},
)
track_event.delay(
user_id=user.id,
event_name=USER_JOINED_WORKSPACE,
slug=slug,
event_properties={
"user_id": user.id,
"workspace_id": workspace_invite.workspace.id,
"workspace_slug": workspace_invite.workspace.slug,
"role": workspace_invite.role,
"joined_at": timezone.now().isoformat(),
},
)
🤖 Prompt for AI Agents
In apps/api/plane/app/views/workspace/invite.py around lines 203 to 214, the
event payload uses str(timezone.now()) which creates a non-ISO timestamp;
replace that with timezone.now().isoformat() so the timestamp is consistently
ISO 8601 formatted (no code block provided—update the "joined_at" value to
timezone.now().isoformat()).

Comment on lines +272 to +283
track_event.delay(
user_id=request.user.id,
event_name=USER_JOINED_WORKSPACE,
slug=invitation.workspace.slug,
event_properties={
"user_id": request.user.id,
"workspace_id": invitation.workspace.id,
"workspace_slug": invitation.workspace.slug,
"role": invitation.role,
"joined_at": str(timezone.now()),
},
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use .isoformat() for consistent timestamp format.

Line 281 uses str(timezone.now()) which produces a different format than the .isoformat() method used elsewhere in the codebase. This inconsistency can cause issues in analytics systems expecting ISO 8601 format.

🔎 Proposed fix
-                    "joined_at": str(timezone.now()),
+                    "joined_at": timezone.now().isoformat(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
track_event.delay(
user_id=request.user.id,
event_name=USER_JOINED_WORKSPACE,
slug=invitation.workspace.slug,
event_properties={
"user_id": request.user.id,
"workspace_id": invitation.workspace.id,
"workspace_slug": invitation.workspace.slug,
"role": invitation.role,
"joined_at": str(timezone.now()),
},
)
track_event.delay(
user_id=request.user.id,
event_name=USER_JOINED_WORKSPACE,
slug=invitation.workspace.slug,
event_properties={
"user_id": request.user.id,
"workspace_id": invitation.workspace.id,
"workspace_slug": invitation.workspace.slug,
"role": invitation.role,
"joined_at": timezone.now().isoformat(),
},
)
🤖 Prompt for AI Agents
In apps/api/plane/app/views/workspace/invite.py around lines 272 to 283, the
event payload uses str(timezone.now()) which yields a non-ISO timestamp; replace
it with timezone.now().isoformat() so the "joined_at" field uses a consistent
ISO 8601 format (ensure timezone-aware datetime remains preserved when calling
.isoformat()).

Comment on lines +39 to +54
def preprocess_data_properties(
user_id: uuid.UUID, event_name: str, slug: str, data_properties: Dict[str, Any]
) -> Dict[str, Any]:
if event_name == USER_INVITED_TO_WORKSPACE or event_name == WORKSPACE_DELETED:
try:
# Check if the current user is the workspace owner
workspace = Workspace.objects.get(slug=slug)
if str(workspace.owner_id) == str(user_id):
data_properties["role"] = "owner"
else:
data_properties["role"] = "admin"
except Workspace.DoesNotExist:
logger.warning(f"Workspace {slug} does not exist while sending event {event_name} for user {user_id}")
data_properties["role"] = "unknown"

if POSTHOG_API_KEY and POSTHOG_HOST:
posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
posthog.capture(
email,
event=event_name,
properties={
"event_id": uuid.uuid4().hex,
"user": {"email": email, "id": str(user)},
"device_ctx": {"ip": ip, "user_agent": user_agent},
"medium": medium,
"first_time": first_time,
},
)
except Exception as e:
log_exception(e)
return
return data_properties
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Role type inconsistency: integers vs strings.

The preprocess_data_properties function enriches role for only two event types (USER_INVITED_TO_WORKSPACE and WORKSPACE_DELETED), setting role as strings ("owner", "admin", "unknown"). However, other events like USER_JOINED_WORKSPACE pass role as integers (5, 15, 20) from WorkspaceMemberInvite.role. This creates inconsistent role data types across events, making analytics queries more complex and error-prone.

Consider either:

  1. Extending preprocess_data_properties to handle all event types and standardize role to string format, or
  2. Converting integer roles to human-readable strings at the source before calling track_event.
🔎 Proposed fix to extend role enrichment to all events
+# Add helper function to map role integer to string
+def get_role_string(role_int, is_owner=False):
+    if is_owner:
+        return "owner"
+    role_map = {5: "guest", 10: "viewer", 15: "member", 20: "admin"}
+    return role_map.get(role_int, "unknown")
+
 def preprocess_data_properties(
     user_id: uuid.UUID, event_name: str, slug: str, data_properties: Dict[str, Any]
 ) -> Dict[str, Any]:
-    if event_name == USER_INVITED_TO_WORKSPACE or event_name == WORKSPACE_DELETED:
+    # Standardize role for all events
+    if "role" in data_properties:
         try:
-            # Check if the current user is the workspace owner
             workspace = Workspace.objects.get(slug=slug)
-            if str(workspace.owner_id) == str(user_id):
+            is_owner = str(workspace.owner_id) == str(user_id)
+            
+            # If role is already a string, keep it; otherwise convert from integer
+            if isinstance(data_properties["role"], int):
+                data_properties["role"] = get_role_string(data_properties["role"], is_owner)
+            elif data_properties["role"] not in ["owner", "admin", "member", "viewer", "guest"]:
+                # Fallback for unexpected string values
                 data_properties["role"] = "owner"
-            else:
-                data_properties["role"] = "admin"
         except Workspace.DoesNotExist:
             logger.warning(f"Workspace {slug} does not exist while sending event {event_name} for user {user_id}")
             data_properties["role"] = "unknown"
🤖 Prompt for AI Agents
In apps/api/plane/bgtasks/event_tracking_task.py around lines 39 to 54, the
function only sets role strings for USER_INVITED_TO_WORKSPACE and
WORKSPACE_DELETED while other events provide integer roles, causing inconsistent
types; update the function to standardize role to a human-readable string for
all workspace-related events by: 1) adding a mapping dict from integer role
codes (e.g., WorkspaceMemberInvite.role values) to strings
("owner","admin","member" or similar) and using it when
data_properties.get("role") is an int; 2) extending the conditional to include
other relevant events (e.g., USER_JOINED_WORKSPACE) so role enrichment runs for
them too; and 3) keeping the existing workspace-owner lookup as a fallback and
setting "unknown" on exceptions or missing data to ensure output is always a
string.

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.

3 participants