-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,7 +42,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.license.utils.instance_value import get_configuration_value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.bgtasks.workspace_seed_task import workspace_seed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.bgtasks.event_tracking_task import track_event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.url import contains_url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.analytics_events import WORKSPACE_CREATED, WORKSPACE_DELETED | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class WorkSpaceViewSet(BaseViewSet): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -131,6 +133,20 @@ def create(self, request): | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspace_seed.delay(serializer.data["id"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| track_event.delay( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id=request.user.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event_name=WORKSPACE_CREATED, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slug=data["slug"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event_properties={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "user_id": request.user.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "workspace_id": data["id"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "workspace_slug": data["slug"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "role": "owner", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "role": "owner", | |
| "role": data.get("role"), |
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.
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", |
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.
🧩 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).
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,12 +21,13 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| WorkSpaceMemberSerializer, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.app.views.base import BaseAPIView | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.bgtasks.event_tracking_task import workspace_invite_event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.bgtasks.event_tracking_task import track_event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.bgtasks.workspace_invitation_task import workspace_invitation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.db.models import User, Workspace, WorkspaceMember, WorkspaceMemberInvite | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.cache import invalidate_cache, invalidate_cache_directly | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.host import base_host | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.ip_address import get_client_ip | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.utils.analytics_events import USER_JOINED_WORKSPACE, USER_INVITED_TO_WORKSPACE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .. import BaseViewSet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,6 +122,19 @@ def create(self, request, slug): | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_site, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.user.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": str(timezone.now()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "invited_at": str(timezone.now()), | |
| "invited_at": 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.
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).
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": user.id, | |
| "workspace_id": workspace_invite.workspace.id, | |
| "workspace_slug": workspace_invite.workspace.slug, |
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()), |
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()).
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": invitation.workspace.id, | |
| "workspace_slug": invitation.workspace.slug, |
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()), |
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()).
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,10 +1,16 @@ | ||||||
| # Django imports | ||||||
| from django.utils import timezone | ||||||
|
|
||||||
| # Module imports | ||||||
| from plane.db.models import ( | ||||||
| ProjectMember, | ||||||
| ProjectMemberInvite, | ||||||
| WorkspaceMember, | ||||||
| WorkspaceMemberInvite, | ||||||
| ) | ||||||
| from plane.utils.cache import invalidate_cache_directly | ||||||
| from plane.bgtasks.event_tracking_task import track_event | ||||||
| from plane.utils.analytics_events import USER_JOINED_WORKSPACE | ||||||
|
|
||||||
|
|
||||||
| def process_workspace_project_invitations(user): | ||||||
|
|
@@ -25,15 +31,25 @@ def process_workspace_project_invitations(user): | |||||
| ignore_conflicts=True, | ||||||
| ) | ||||||
|
|
||||||
| [ | ||||||
| for workspace_member_invite in workspace_member_invites: | ||||||
| invalidate_cache_directly( | ||||||
| path=f"/api/workspaces/{str(workspace_member_invite.workspace.slug)}/members/", | ||||||
| url_params=False, | ||||||
| user=False, | ||||||
| multiple=True, | ||||||
| ) | ||||||
| for workspace_member_invite in workspace_member_invites | ||||||
| ] | ||||||
| track_event.delay( | ||||||
| user_id=user.id, | ||||||
| event_name=USER_JOINED_WORKSPACE, | ||||||
| slug=workspace_member_invite.workspace.slug, | ||||||
| event_properties={ | ||||||
| "user_id": user.id, | ||||||
|
||||||
| "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.
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
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().isoformat()) is used here, while str(timezone.now()) is used in other places (e.g., line 134, 212, 281). 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().isoformat()), | |
| "joined_at": str(timezone.now()), |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,7 @@ | ||||||||||||||||||
| import logging | ||||||||||||||||||
| import os | ||||||||||||||||||
| import uuid | ||||||||||||||||||
| from typing import Dict, Any | ||||||||||||||||||
|
|
||||||||||||||||||
| # third party imports | ||||||||||||||||||
| from celery import shared_task | ||||||||||||||||||
|
|
@@ -8,6 +10,11 @@ | |||||||||||||||||
| # module imports | ||||||||||||||||||
| from plane.license.utils.instance_value import get_configuration_value | ||||||||||||||||||
| from plane.utils.exception_logger import log_exception | ||||||||||||||||||
| from plane.db.models import Workspace | ||||||||||||||||||
| from plane.utils.analytics_events import USER_INVITED_TO_WORKSPACE, WORKSPACE_DELETED | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| logger = logging.getLogger("plane.worker") | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def posthogConfiguration(): | ||||||||||||||||||
|
|
@@ -17,7 +24,10 @@ def posthogConfiguration(): | |||||||||||||||||
| "key": "POSTHOG_API_KEY", | ||||||||||||||||||
| "default": os.environ.get("POSTHOG_API_KEY", None), | ||||||||||||||||||
| }, | ||||||||||||||||||
| {"key": "POSTHOG_HOST", "default": os.environ.get("POSTHOG_HOST", None)}, | ||||||||||||||||||
| { | ||||||||||||||||||
| "key": "POSTHOG_HOST", | ||||||||||||||||||
| "default": os.environ.get("POSTHOG_HOST", None), | ||||||||||||||||||
| }, | ||||||||||||||||||
| ] | ||||||||||||||||||
| ) | ||||||||||||||||||
| if POSTHOG_API_KEY and POSTHOG_HOST: | ||||||||||||||||||
|
|
@@ -26,46 +36,42 @@ def posthogConfiguration(): | |||||||||||||||||
| return None, None | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @shared_task | ||||||||||||||||||
| def auth_events(user, email, user_agent, ip, event_name, medium, first_time): | ||||||||||||||||||
| try: | ||||||||||||||||||
| POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() | ||||||||||||||||||
| 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) | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Workspace lookup fails during deletion eventsIn Additional Locations (1) |
||||||||||||||||||
| 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" | ||||||||||||||||||
|
||||||||||||||||||
| data_properties["role"] = "unknown" | |
| data_properties["role"] = "unknown" | |
| else: | |
| # No preprocessing is required for this event; return properties unchanged | |
| logger.debug( | |
| "No preprocessing applied for event '%s'; returning properties unchanged", | |
| event_name, | |
| ) |
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.
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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| USER_JOINED_WORKSPACE = "user_joined_workspace" | ||
| USER_INVITED_TO_WORKSPACE = "user_invited_to_workspace" | ||
| WORKSPACE_CREATED = "workspace_created" | ||
| WORKSPACE_DELETED = "workspace_deleted" |
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_verifiedappears twice in the fields list (lines 75 and 79). This is redundant and should be removed. Keep only one occurrence of this field.