-
Notifications
You must be signed in to change notification settings - Fork 3.1k
chore: workspace events #8439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: workspace events #8439
Conversation
WalkthroughThe 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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) |
There was a problem hiding this comment.
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)
There was a problem hiding this 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_eventtask 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", |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "role": "owner", | |
| "role": data.get("role"), |
| 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()), | ||
| }, |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "user_id": request.user.id, | ||
| "workspace_id": workspace.id, | ||
| "workspace_slug": workspace.slug, | ||
| "role": "owner", |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "role": "owner", |
| "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()), |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| event_name=USER_JOINED_WORKSPACE, | ||
| slug=workspace_member_invite.workspace.slug, | ||
| event_properties={ | ||
| "user_id": user.id, |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "user_id": user.id, |
| "workspace_id": invitation.workspace.id, | ||
| "workspace_slug": invitation.workspace.slug, | ||
| "role": invitation.role, | ||
| "joined_at": str(timezone.now()), |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "joined_at": str(timezone.now()), | |
| "joined_at": str(timezone.now().isoformat()), |
| 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" |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "user_timezone", | ||
| "username", | ||
| "is_password_autoset", | ||
| "is_email_verified", |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "is_email_verified", |
| "workspace_id": workspace_invite.workspace.id, | ||
| "workspace_slug": workspace_invite.workspace.slug, | ||
| "role": workspace_invite.role, | ||
| "joined_at": str(timezone.now()), |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
| "joined_at": str(timezone.now()), | |
| "joined_at": str(timezone.now().isoformat()), |
| 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"], | ||
| }, |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this 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 redundantstr()wrapper around.isoformat().Line 50 uses
str(timezone.now().isoformat()), but.isoformat()already returns a string, making thestr()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 returnsFalseon 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
📒 Files selected for processing (6)
apps/api/plane/app/serializers/user.pyapps/api/plane/app/views/workspace/base.pyapps/api/plane/app/views/workspace/invite.pyapps/api/plane/authentication/utils/workspace_project_join.pyapps/api/plane/bgtasks/event_tracking_task.pyapps/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_timetoUserMeSerializeris 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.
| 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()), | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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 10Repository: 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}")
EOFRepository: makeplane/plane
Length of output: 145
🏁 Script executed:
# Find Workspace model
rg "class Workspace" --type py -A 5 | head -50Repository: 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 workspaceRepository: 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 -100Repository: 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 -50Repository: 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 -150Repository: 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 5Repository: makeplane/plane
Length of output: 41
🏁 Script executed:
# Check AuditModel which BaseModel inherits from
rg "class AuditModel" apps/api/plane --type py -A 20Repository: 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 pyRepository: makeplane/plane
Length of output: 174
🏁 Script executed:
# Check the SoftDeleteModel implementation
rg "class SoftDeleteModel" apps/api/plane/db/mixins.py -A 50Repository: makeplane/plane
Length of output: 1771
🏁 Script executed:
# Find the SoftDeletionManager implementation
rg "class SoftDeletionManager" apps/api/plane --type py -A 30Repository: 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.
| 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).
| 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, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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).
| 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()), | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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()).
| 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()), | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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()).
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Extending
preprocess_data_propertiesto handle all event types and standardize role to string format, or - 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.
Note
Centralizes analytics for key workspace actions and surfaces an additional user field.
track_eventCelery task using PostHog, with preprocessing to setrolebased on workspace ownership and grouping byworkspaceslugutils/analytics_events.pyworkspace/base.py,workspace/invite.py, andworkspace_project_join.pylast_login_timetoUserMeSerializerfieldsWritten by Cursor Bugbot for commit 3ac3ce3. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.