-
Notifications
You must be signed in to change notification settings - Fork 3.1k
[WEB-5681] refactor: add new event trackers #8293
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
base: preview
Are you sure you want to change the base?
Changes from 16 commits
8bbf853
6f64e07
4d22ef2
54c32f6
72dd432
f9d42df
f958aad
96ccde5
031fb89
dd0ac58
f8f07a6
6e2f0f1
f14bf3b
a64bab7
9941459
1f89fcd
5e2f83f
91b421d
af33860
889c173
b4c40bb
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 |
|---|---|---|
| @@ -1,10 +1,15 @@ | ||
| # 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 | ||
|
|
||
|
|
||
| def process_workspace_project_invitations(user): | ||
|
|
@@ -25,6 +30,22 @@ def process_workspace_project_invitations(user): | |
| ignore_conflicts=True, | ||
| ) | ||
|
|
||
| [ | ||
| track_event.delay( | ||
| user_id=user.id, | ||
| event_name="user_joined_workspaces", | ||
|
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. Inconsistent event name uses plural instead of singularThe event name |
||
| slug=workspace_member_invite.workspace.slug, | ||
| event_properties={ | ||
| "user_id": user.id, | ||
| "workspace_id": workspace_member_invite.workspace.id, | ||
| "workspace_slug": workspace_member_invite.workspace.slug, | ||
| "role": workspace_member_invite.role, | ||
| "joined_at": str(timezone.now()), | ||
| }, | ||
| ) | ||
| for workspace_member_invite in workspace_member_invites | ||
| ] | ||
|
|
||
| [ | ||
| invalidate_cache_directly( | ||
| path=f"/api/workspaces/{str(workspace_member_invite.workspace.slug)}/members/", | ||
|
|
||
| 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,45 +10,61 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger("plane.worker") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def posthogConfiguration(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| POSTHOG_API_KEY, POSTHOG_HOST = get_configuration_value( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "key": "POSTHOG_API_KEY", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "default": os.environ.get("POSTHOG_API_KEY", None), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {"key": "POSTHOG_HOST", "default": os.environ.get("POSTHOG_HOST", None)}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| POSTHOG_API_KEY, POSTHOG_HOST = get_configuration_value([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "key": "POSTHOG_API_KEY", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "default": os.environ.get("POSTHOG_API_KEY", None), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {"key": "POSTHOG_HOST", "default": os.environ.get("POSTHOG_HOST", None)}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if POSTHOG_API_KEY and POSTHOG_HOST: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return POSTHOG_API_KEY, POSTHOG_HOST | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None, None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+42
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspace = Workspace.objects.get(slug=slug) | |
| if str(workspace.owner_id) == str(user_id): | |
| data_properties["role"] = "owner" | |
| else: | |
| data_properties["role"] = "admin" | |
| try: | |
| 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.error(f"Workspace with slug '{slug}' does not exist when processing event '{event_name}'.") | |
| data_properties["role"] = "unknown" |
Copilot
AI
Dec 15, 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 logic assumes that if a user is not the workspace owner, they must be an admin. However, the invitee could have any role (guest, member, admin). The actual role should come from data_properties['invitee_role'] which is passed in the event properties from the workspace invite view.
| data_properties["role"] = "admin" | |
| data_properties["role"] = data_properties.get("invitee_role") |
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.
Unhandled Workspace.DoesNotExist exception can break event tracking.
Workspace.objects.get(slug=slug) will raise Workspace.DoesNotExist if the workspace doesn't exist (e.g., deleted or invalid slug). This exception propagates up to track_event, where it's caught but causes the entire event to fail silently.
Consider using filter().first() with a null check:
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":
- # 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"
+ workspace = Workspace.objects.filter(slug=slug).first()
+ if workspace:
+ if str(workspace.owner_id) == str(user_id):
+ data_properties["role"] = "owner"
+ else:
+ data_properties["role"] = "admin"
+ else:
+ data_properties["role"] = "unknown"
return data_properties📝 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.
| 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": | |
| # 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" | |
| return data_properties | |
| 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": | |
| workspace = Workspace.objects.filter(slug=slug).first() | |
| if workspace: | |
| if str(workspace.owner_id) == str(user_id): | |
| data_properties["role"] = "owner" | |
| else: | |
| data_properties["role"] = "admin" | |
| else: | |
| data_properties["role"] = "unknown" | |
| return data_properties |
🤖 Prompt for AI Agents
In apps/api/plane/bgtasks/event_tracking_task.py around lines 33-44, replace the
direct Workspace.objects.get(slug=slug) call with
Workspace.objects.filter(slug=slug).first() and add a null check so the code
does not raise Workspace.DoesNotExist; if workspace is None, leave
data_properties unchanged (or set data_properties["role"]="unknown" if a role is
required), otherwise perform the owner-id comparison and set "role" accordingly;
this prevents an exception from propagating while preserving the intended role
assignment when the workspace exists.
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.
Type mismatch and inconsistent return value.
Two issues in the track_event function:
-
UUID type:
user_idis typed asuuid.UUID, butposthog.capture(distinct_id=...)typically expects a string. Ensure explicit conversion to avoid serialization issues. -
Inconsistent return: Returns
Falseon exception (line 67) but returnsNoneimplicitly on success or when tracking is disabled. Consider consistent return behavior.
@shared_task
-def track_event(user_id: uuid.UUID, event_name: str, slug: str, event_properties: Dict[str, Any]):
+def track_event(user_id: uuid.UUID, event_name: str, slug: str, event_properties: Dict[str, Any]) -> bool:
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
if not (POSTHOG_API_KEY and POSTHOG_HOST):
logger.warning("Event tracking is not configured")
- return
+ return False
try:
data_properties = preprocess_data_properties(user_id, event_name, slug, event_properties)
groups = {
"workspace": slug,
}
posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
- posthog.capture(distinct_id=user_id, event=event_name, properties=data_properties, groups=groups)
+ posthog.capture(distinct_id=str(user_id), event=event_name, properties=data_properties, groups=groups)
+ return True
except Exception as e:
log_exception(e)
return False📝 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.
| @shared_task | |
| def auth_events(user, email, user_agent, ip, event_name, medium, first_time): | |
| try: | |
| POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() | |
| def track_event(user_id: uuid.UUID, event_name: str, slug: str, event_properties: Dict[str, Any]): | |
| POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() | |
| 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, | |
| }, | |
| ) | |
| if not (POSTHOG_API_KEY and POSTHOG_HOST): | |
| logger.warning("Event tracking is not configured") | |
| return | |
| try: | |
| # preprocess the data properties for massaging the payload | |
| # in the correct format for posthog | |
| data_properties = preprocess_data_properties(user_id, event_name, slug, event_properties) | |
| groups = { | |
| "workspace": slug, | |
| } | |
| # track the event using posthog | |
| posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST) | |
| posthog.capture(distinct_id=user_id, event=event_name, properties=data_properties, groups=groups) | |
| except Exception as e: | |
| log_exception(e) | |
| return | |
| return False | |
| @shared_task | |
| def track_event(user_id: uuid.UUID, event_name: str, slug: str, event_properties: Dict[str, Any]) -> bool: | |
| POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration() | |
| if not (POSTHOG_API_KEY and POSTHOG_HOST): | |
| logger.warning("Event tracking is not configured") | |
| return False | |
| try: | |
| # preprocess the data properties for massaging the payload | |
| # in the correct format for posthog | |
| data_properties = preprocess_data_properties(user_id, event_name, slug, event_properties) | |
| groups = { | |
| "workspace": slug, | |
| } | |
| # track the event using posthog | |
| posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST) | |
| posthog.capture(distinct_id=str(user_id), event=event_name, properties=data_properties, groups=groups) | |
| return True | |
| except Exception as e: | |
| log_exception(e) | |
| return False |
🤖 Prompt for AI Agents
In apps/api/plane/bgtasks/event_tracking_task.py around lines 47 to 67, convert
user_id to a string before passing it to posthog.capture (e.g.,
distinct_id=str(user_id)) to avoid UUID serialization issues, and make the
function return consistently (return True on successful capture and return False
both when tracking is disabled and when an exception occurs) so callers receive
a boolean success indicator.
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.
Align event name with other join events and avoid list comprehension for side effects
Two things here:
Other join flows use
event_name="user_joined_workspace"(singular), but this path uses"user_joined_workspaces":Unless you explicitly want a different event type for this code path, this will fragment analytics. To keep queries consistent, consider:
The comprehension:
[ track_event.delay(...) for workspace_member_invite in workspace_member_invites ]creates an unused list purely for side effects. A plain loop is clearer and avoids unnecessary allocation:
(The
.isoformat()change is optional but recommended for consistent timestamps.)Also applies to: 12-12, 33-47
🤖 Prompt for AI Agents