Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7ff40c9
refactor: Add dynamic plugin loading for enterprise components
hari-kuriakose Jan 9, 2026
92b8575
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 9, 2026
6e43db3
Merge branch 'main' into chore/plugin-loading
hari-kuriakose Jan 13, 2026
7e5c60a
refactor: Use get_plugin() for API Hub usage utilities
hari-kuriakose Jan 13, 2026
16e04a7
Refactor random sampling logic in utils.py
hari-kuriakose Jan 13, 2026
d9d5d86
fix: Add Traefik port labels and clean up service ignore list
hari-kuriakose Jan 15, 2026
b5057ea
fix: Update frontend Docker config for nginx serving
hari-kuriakose Feb 5, 2026
6d6d730
Merge branch 'main' into chore/plugin-loading
hari-kuriakose Feb 8, 2026
d4bcc39
fix: Use ARG instead of ENV for BUILD_CONTEXT_PATH in frontend Docker…
hari-kuriakose Feb 8, 2026
6938350
feat: Add HubSpot integration plugin for contact event tracking
hari-kuriakose Feb 10, 2026
e010111
[FIX] Fix HITL review screen showing "Never expires" despite TTL bein…
vishnuszipstack Feb 9, 2026
01d8b90
refactor: Add dynamic plugin loading for enterprise components (#1736)
hari-kuriakose Feb 13, 2026
a5d6a0b
feat: Add auth error code for forbidden emails (#1789)
hari-kuriakose Feb 16, 2026
e67ecc5
[MISC] Improve dev experience by adding a compose debug override (#1765)
chandrasekharan-zipstack Feb 19, 2026
fed6355
[FIX] Optimize queries made by worker and retry config of worker base…
chandrasekharan-zipstack Feb 19, 2026
2c42638
UN-2971 [FEAT] Pass selectedProduct to login/signup API for OAuth pro…
hari-kuriakose Feb 24, 2026
08736d6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 24, 2026
dbd3dac
Merge branch 'main' into feat/plugin-integrations
hari-kuriakose Feb 24, 2026
7fc47ba
Merge branch 'main' into feat/plugin-integrations
vishnuszipstack Feb 26, 2026
88b5488
Merge branch 'main' into feat/plugin-integrations
vishnuszipstack Mar 3, 2026
1422b37
fix: Scope HubSpot milestone count checks to current organization
vishnuszipstack Mar 3, 2026
2cff66a
Merge branch 'main' into feat/plugin-integrations
gaya3-zipstack Mar 9, 2026
3d20e11
[REFACTOR] Extract HubSpot notification logic into shared utility
vishnuszipstack Mar 9, 2026
a0a5c0f
Merge branch 'main' into feat/plugin-integrations
vishnuszipstack Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions backend/account_v2/authentication_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,6 @@ def get_invitations(self, organization_id: str) -> list[MemberInvitation]:
def frictionless_onboarding(self, organization: Organization, user: User) -> None:
raise MethodNotImplemented()

def hubspot_signup_api(self, request: Request) -> None:
raise MethodNotImplemented()

def delete_invitation(self, organization_id: str, invitation_id: str) -> bool:
raise MethodNotImplemented()

Expand Down
35 changes: 35 additions & 0 deletions backend/api_v2/api_deployment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ def fetch_one(self, request: Request, pk: str | None = None) -> Response:
def create(
self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any]
) -> Response:
# Check deployment count before create for HubSpot notification
deployment_count_before = APIDeployment.objects.count()

Comment thread
vishnuszipstack marked this conversation as resolved.
serializer: Serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
Expand All @@ -228,13 +231,45 @@ def create(
{"api_key": api_key.api_key, **serializer.data}
)

# Notify HubSpot about API deployment
self._notify_hubspot_first_api_deploy(request.user, deployment_count_before)

headers = self.get_success_headers(serializer.data)
return Response(
response_serializer.data,
status=status.HTTP_201_CREATED,
headers=headers,
)

def _notify_hubspot_first_api_deploy(
self, user, deployment_count_before: int
) -> None:
"""Notify HubSpot when an API is deployed.

Checks if HubSpot plugin is available and notifies it about
the API deployment. The plugin decides whether to act based
on the is_first_for_org flag.
"""
hubspot_plugin = get_plugin("hubspot")
if not hubspot_plugin:
return

try:
# First API deploy if count was 0 before deploy
is_first_for_org = deployment_count_before == 0

from plugins.integrations.hubspot import HubSpotEvent

service = hubspot_plugin["service_class"]()
service.update_contact(
user=user,
events=[HubSpotEvent.API_DEPLOY],
is_first_for_org=is_first_for_org,
)
except Exception as e:
# Log but don't fail the request
logger.warning(f"Failed to notify HubSpot for API deployment: {e}")

@action(detail=False, methods=["get"])
def by_prompt_studio_tool(self, request: Request) -> Response:
"""Get API deployments for a specific prompt studio tool."""
Expand Down
137 changes: 137 additions & 0 deletions backend/prompt_studio/prompt_studio_core_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
PromptStudioDocumentHelper,
)
from prompt_studio.prompt_studio_index_manager_v2.models import IndexManager
from prompt_studio.prompt_studio_output_manager_v2.models import PromptStudioOutputManager
from prompt_studio.prompt_studio_registry_v2.models import PromptStudioRegistry
from prompt_studio.prompt_studio_registry_v2.prompt_studio_registry_helper import (
PromptStudioRegistryHelper,
)
Expand Down Expand Up @@ -118,8 +120,41 @@ def create(self, request: HttpRequest) -> Response:
PromptStudioHelper.create_default_profile_manager(
request.user, serializer.data["tool_id"]
)

# Notify HubSpot if this is the first Prompt Studio project for the org
self._notify_hubspot_first_project(request.user)

Comment thread
vishnuszipstack marked this conversation as resolved.
return Response(serializer.data, status=status.HTTP_201_CREATED)

def _notify_hubspot_first_project(self, user) -> None:
"""Notify HubSpot when a Prompt Studio project is created.

Checks if HubSpot plugin is available and notifies it about
the project creation. The plugin decides whether to act based
on the is_first_for_org flag.
"""
hubspot_plugin = get_plugin("hubspot")
if not hubspot_plugin:
return

try:
# Check if this is the first CustomTool for the organization
# (count == 1 means the one we just created is the first)
org_project_count = CustomTool.objects.count()
is_first_for_org = org_project_count == 1
Comment thread
vishnuszipstack marked this conversation as resolved.
Outdated

from plugins.integrations.hubspot import HubSpotEvent

service = hubspot_plugin["service_class"]()
service.update_contact(
user=user,
events=[HubSpotEvent.PROMPT_STUDIO_PROJECT_CREATE],
is_first_for_org=is_first_for_org,
)
except Exception as e:
# Log but don't fail the request
logger.warning(f"Failed to notify HubSpot for project creation: {e}")

def perform_destroy(self, instance: CustomTool) -> None:
organization_id = UserSessionUtils.get_organization_id(self.request)
instance.delete(organization_id)
Expand Down Expand Up @@ -408,6 +443,10 @@ def fetch_response(self, request: HttpRequest, pk: Any = None) -> Response:
if not run_id:
# Generate a run_id
run_id = CommonUtils.generate_uuid()

# Check output count before prompt run for HubSpot notification
output_count_before = PromptStudioOutputManager.objects.count()

response: dict[str, Any] = PromptStudioHelper.prompt_responder(
id=id,
tool_id=tool_id,
Expand All @@ -417,8 +456,39 @@ def fetch_response(self, request: HttpRequest, pk: Any = None) -> Response:
run_id=run_id,
profile_manager_id=profile_manager,
)

# Notify HubSpot about prompt run
self._notify_hubspot_first_prompt_run(request.user, output_count_before)

return Response(response, status=status.HTTP_200_OK)

def _notify_hubspot_first_prompt_run(self, user, output_count_before: int) -> None:
"""Notify HubSpot when a prompt is run.

Checks if HubSpot plugin is available and notifies it about
the prompt run. The plugin decides whether to act based
on the is_first_for_org flag.
"""
hubspot_plugin = get_plugin("hubspot")
if not hubspot_plugin:
return

try:
# First prompt run if count was 0 before run
is_first_for_org = output_count_before == 0

from plugins.integrations.hubspot import HubSpotEvent

service = hubspot_plugin["service_class"]()
service.update_contact(
user=user,
events=[HubSpotEvent.PROMPT_RUN],
is_first_for_org=is_first_for_org,
)
except Exception as e:
# Log but don't fail the request
logger.warning(f"Failed to notify HubSpot for prompt run: {e}")

@action(detail=True, methods=["post"])
def single_pass_extraction(self, request: HttpRequest, pk: uuid) -> Response:
"""API Entry point method to fetch response to prompt.
Expand Down Expand Up @@ -551,6 +621,9 @@ def upload_for_ide(self, request: HttpRequest, pk: Any = None) -> Response:
uploaded_files: Any = serializer.validated_data.get("file")
file_converter_plugin = get_plugin("file_converter")

# Check document count before upload for HubSpot notification
doc_count_before = DocumentManager.objects.count()

documents = []
for uploaded_file in uploaded_files:
# Store file
Expand Down Expand Up @@ -585,8 +658,39 @@ def upload_for_ide(self, request: HttpRequest, pk: Any = None) -> Response:
"tool": document.tool.tool_id,
}
documents.append(doc)

# Notify HubSpot about document upload
self._notify_hubspot_first_document(request.user, doc_count_before)

return Response({"data": documents})

def _notify_hubspot_first_document(self, user, doc_count_before: int) -> None:
"""Notify HubSpot when a document is uploaded.

Checks if HubSpot plugin is available and notifies it about
the document upload. The plugin decides whether to act based
on the is_first_for_org flag.
"""
hubspot_plugin = get_plugin("hubspot")
if not hubspot_plugin:
return

try:
# First document upload if count was 0 before upload
is_first_for_org = doc_count_before == 0

from plugins.integrations.hubspot import HubSpotEvent

service = hubspot_plugin["service_class"]()
service.update_contact(
user=user,
events=[HubSpotEvent.DOCUMENT_UPLOAD],
is_first_for_org=is_first_for_org,
)
except Exception as e:
# Log but don't fail the request
logger.warning(f"Failed to notify HubSpot for document upload: {e}")

@action(detail=True, methods=["delete"])
def delete_for_ide(self, request: HttpRequest, pk: uuid) -> Response:
custom_tool = self.get_object()
Expand Down Expand Up @@ -636,18 +740,51 @@ def export_tool(self, request: Request, pk: Any = None) -> Response:
user_ids = set(serializer.validated_data.get("user_id"))
force_export = serializer.validated_data.get("force_export")

# Check registry count before export for HubSpot notification
registry_count_before = PromptStudioRegistry.objects.count()

PromptStudioRegistryHelper.update_or_create_psr_tool(
custom_tool=custom_tool,
shared_with_org=is_shared_with_org,
user_ids=user_ids,
force_export=force_export,
)

# Notify HubSpot about tool export
self._notify_hubspot_first_tool_export(request.user, registry_count_before)

return Response(
{"message": "Custom tool exported sucessfully."},
status=status.HTTP_200_OK,
)

def _notify_hubspot_first_tool_export(self, user, registry_count_before: int) -> None:
"""Notify HubSpot when a tool is exported.

Checks if HubSpot plugin is available and notifies it about
the tool export. The plugin decides whether to act based
on the is_first_for_org flag.
"""
hubspot_plugin = get_plugin("hubspot")
if not hubspot_plugin:
return

try:
# First tool export if count was 0 before export
is_first_for_org = registry_count_before == 0

from plugins.integrations.hubspot import HubSpotEvent

service = hubspot_plugin["service_class"]()
service.update_contact(
user=user,
events=[HubSpotEvent.TOOL_EXPORT],
is_first_for_org=is_first_for_org,
)
except Exception as e:
# Log but don't fail the request
logger.warning(f"Failed to notify HubSpot for tool export: {e}")

@action(detail=True, methods=["get"])
def export_tool_info(self, request: Request, pk: Any = None) -> Response:
custom_tool = self.get_object()
Expand Down