Skip to content

Commit

Permalink
feat: Add hubspot cookie tracking (#4539)
Browse files Browse the repository at this point in the history
Co-authored-by: Kim Gustyr <[email protected]>
  • Loading branch information
zachaysan and khvn26 authored Sep 9, 2024
1 parent b2a1899 commit 6714384
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 10 deletions.
73 changes: 66 additions & 7 deletions api/integrations/lead_tracking/hubspot/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import json
import logging
from typing import Any

import hubspot
import requests
from django.conf import settings
from hubspot.crm.companies import (
PublicObjectSearchRequest,
Expand All @@ -9,17 +12,22 @@
)
from hubspot.crm.contacts import BatchReadInputSimplePublicObjectId

from integrations.lead_tracking.hubspot.constants import (
HUBSPOT_FORM_ID,
HUBSPOT_PORTAL_ID,
HUBSPOT_ROOT_FORM_URL,
)
from users.models import FFAdminUser

logger = logging.getLogger(__name__)


class HubspotClient:
def __init__(self, client: hubspot.Client = None) -> None:
access_token = settings.HUBSPOT_ACCESS_TOKEN
self.client = client or hubspot.Client.create(access_token=access_token)
self.access_token = settings.HUBSPOT_ACCESS_TOKEN
self.client = client or hubspot.Client.create(access_token=self.access_token)

def get_contact(self, user: FFAdminUser) -> None | dict:
def get_contact(self, user: FFAdminUser) -> None | dict[str, Any]:
public_object_id = BatchReadInputSimplePublicObjectId(
id_property="email",
inputs=[{"id": user.email}],
Expand All @@ -42,7 +50,56 @@ def get_contact(self, user: FFAdminUser) -> None | dict:

return results[0]

def create_contact(self, user: FFAdminUser, hubspot_company_id: str) -> dict:
def create_lead_form(
self, user: FFAdminUser, hubspot_cookie: str
) -> dict[str, Any]:
fields = [
{
"objectTypeId": "0-1",
"name": "email",
"value": user.email,
},
{"objectTypeId": "0-1", "name": "firstname", "value": user.first_name},
{"objectTypeId": "0-1", "name": "lastname", "value": user.last_name},
]

context = {
"hutk": hubspot_cookie,
"pageUri": "www.flagsmith.com",
"pageName": "Homepage",
}

legal = {
"consent": {
"consentToProcess": True,
"text": "I agree to allow Flagsmith to store and process my personal data.",
"communications": [
{
"value": user.marketing_consent_given,
"subscriptionTypeId": 999,
"text": "I agree to receive marketing communications from Flagsmith.",
}
],
}
}
payload = {"legalConsentOptions": legal, "context": context, "fields": fields}
headers = {
"Content-Type": "application/json",
}
url = f"{HUBSPOT_ROOT_FORM_URL}/{HUBSPOT_PORTAL_ID}/{HUBSPOT_FORM_ID}"

response = requests.post(url, headers=headers, data=json.dumps(payload))

if response.status_code not in {200, 201}:
logger.error(
f"Problem posting data to Hubspot's form API due to {response.status_code} "
f"status code and following response: {response.text}"
)
return response.json()

def create_contact(
self, user: FFAdminUser, hubspot_company_id: str
) -> dict[str, Any]:
properties = {
"email": user.email,
"firstname": user.first_name,
Expand All @@ -68,7 +125,7 @@ def create_contact(self, user: FFAdminUser, hubspot_company_id: str) -> dict:
)
return response.to_dict()

def get_company_by_domain(self, domain: str) -> dict | None:
def get_company_by_domain(self, domain: str) -> dict[str, Any] | None:
"""
Domain should be unique in Hubspot by design, so we should only ever have
0 or 1 results.
Expand Down Expand Up @@ -102,7 +159,7 @@ def create_company(
active_subscription: str = None,
organisation_id: int = None,
domain: str | None = None,
) -> dict:
) -> dict[str, Any]:
properties = {"name": name}

if domain:
Expand All @@ -122,7 +179,9 @@ def create_company(

return response.to_dict()

def update_company(self, active_subscription: str, hubspot_company_id: str) -> dict:
def update_company(
self, active_subscription: str, hubspot_company_id: str
) -> dict[str, Any]:
properties = {
"active_subscription": active_subscription,
}
Expand Down
4 changes: 4 additions & 0 deletions api/integrations/lead_tracking/hubspot/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HUBSPOT_COOKIE_NAME = "hubspotutk"
HUBSPOT_PORTAL_ID = "143451822"
HUBSPOT_FORM_ID = "562ee023-fb3f-4645-a217-4d8c9b4e45be"
HUBSPOT_ROOT_FORM_URL = "https://api.hsforms.com/submissions/v3/integration/submit"
7 changes: 6 additions & 1 deletion api/integrations/lead_tracking/hubspot/lead_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Organisation,
Subscription,
)
from users.models import FFAdminUser, HubspotLead
from users.models import FFAdminUser, HubspotLead, HubspotTracker

from .client import HubspotClient

Expand Down Expand Up @@ -58,6 +58,11 @@ def create_lead(self, user: FFAdminUser, organisation: Organisation = None) -> N

HubspotLead.objects.create(user=user, hubspot_id=response["id"])

if tracker := HubspotTracker.objects.filter(user=user).first():
self.client.create_lead_form(
user=user, hubspot_cookie=tracker.hubspot_cookie
)

def get_or_create_organisation_hubspot_id(
self, user: FFAdminUser, organisation: Organisation = None
) -> str:
Expand Down
16 changes: 16 additions & 0 deletions api/integrations/lead_tracking/hubspot/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework.request import Request

from integrations.lead_tracking.hubspot.constants import HUBSPOT_COOKIE_NAME
from users.models import HubspotTracker


def register_hubspot_tracker(request: Request) -> None:
hubspot_cookie = request.COOKIES.get(HUBSPOT_COOKIE_NAME)

if hubspot_cookie:
HubspotTracker.objects.update_or_create(
user=request.user,
defaults={
"hubspot_cookie": hubspot_cookie,
},
)
7 changes: 7 additions & 0 deletions api/organisations/invites/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.viewsets import GenericViewSet

from integrations.lead_tracking.hubspot.services import (
register_hubspot_tracker,
)
from organisations.invites.exceptions import InviteExpiredError
from organisations.invites.models import Invite, InviteLink
from organisations.invites.serializers import (
Expand Down Expand Up @@ -44,6 +47,8 @@ def join_organisation_from_email(request, hash):
error_data = {"detail": str(e)}
return Response(data=error_data, status=status.HTTP_400_BAD_REQUEST)

register_hubspot_tracker(request)

return Response(
OrganisationSerializerFull(
invite.organisation, context={"request": request}
Expand All @@ -62,6 +67,8 @@ def join_organisation_from_link(request, hash):
if invite.is_expired:
raise InviteExpiredError()

register_hubspot_tracker(request)

request.user.join_organisation_from_invite_link(invite)

return Response(
Expand Down
5 changes: 5 additions & 0 deletions api/organisations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle

from integrations.lead_tracking.hubspot.services import (
register_hubspot_tracker,
)
from organisations.chargebee import webhook_event_types, webhook_handlers
from organisations.exceptions import OrganisationHasNoPaidSubscription
from organisations.models import (
Expand Down Expand Up @@ -109,6 +112,8 @@ def create(self, request, **kwargs):
"""
Override create method to add new organisation to authenticated user
"""

register_hubspot_tracker(request)
user = request.user
serializer = OrganisationSerializerFull(data=request.data)
if serializer.is_valid():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,82 @@
import logging

import pytest
import responses
from pytest_mock import MockerFixture
from rest_framework import status

from integrations.lead_tracking.hubspot.client import HubspotClient
from integrations.lead_tracking.hubspot.constants import (
HUBSPOT_FORM_ID,
HUBSPOT_PORTAL_ID,
HUBSPOT_ROOT_FORM_URL,
)
from tests.unit.integrations.lead_tracking.hubspot._hubspot_responses import (
generate_create_company_response,
generate_get_company_by_domain_response,
generate_get_company_by_domain_response_no_results,
)
from users.models import FFAdminUser


@pytest.fixture()
def hubspot_client(mocker: MockerFixture) -> HubspotClient:
return HubspotClient(client=mocker.MagicMock())


@responses.activate
def test_create_lead_form(
staff_user: FFAdminUser,
hubspot_client: HubspotClient,
) -> None:
# Given
hubspot_cookie = "test_hubspot_cookie"
url = f"{HUBSPOT_ROOT_FORM_URL}/{HUBSPOT_PORTAL_ID}/{HUBSPOT_FORM_ID}"
responses.add(
method="POST",
url=url,
status=status.HTTP_200_OK,
json={"inlineMessage": "Thanks for submitting the form."},
)

# When
response = hubspot_client.create_lead_form(staff_user, hubspot_cookie)

# Then
assert response == {"inlineMessage": "Thanks for submitting the form."}


@responses.activate
def test_create_lead_form_error(
staff_user: FFAdminUser,
hubspot_client: HubspotClient,
inspecting_handler: logging.Handler,
) -> None:
# Given
from integrations.lead_tracking.hubspot.client import logger

logger.addHandler(inspecting_handler)

hubspot_cookie = "test_hubspot_cookie"
url = f"{HUBSPOT_ROOT_FORM_URL}/{HUBSPOT_PORTAL_ID}/{HUBSPOT_FORM_ID}"
responses.add(
method="POST",
url=url,
status=status.HTTP_400_BAD_REQUEST,
json={"error": "Problem processing."},
)

# When
response = hubspot_client.create_lead_form(staff_user, hubspot_cookie)

# Then
assert response == {"error": "Problem processing."}
assert inspecting_handler.messages == [
"Problem posting data to Hubspot's form API due to 400 status code and following response: "
+ '{"error": "Problem processing."}'
]


def test_get_company_by_domain(hubspot_client: HubspotClient) -> None:
# Given
name = "Flagsmith"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import datetime

import responses
from pytest_django.fixtures import SettingsWrapper
from pytest_mock import MockerFixture
from rest_framework import status
from task_processor.task_run_method import TaskRunMethod

from integrations.lead_tracking.hubspot.constants import (
HUBSPOT_FORM_ID,
HUBSPOT_PORTAL_ID,
HUBSPOT_ROOT_FORM_URL,
)
from organisations.models import (
HubspotOrganisation,
Organisation,
OrganisationRole,
)
from users.models import FFAdminUser, HubspotLead
from users.models import FFAdminUser, HubspotLead, HubspotTracker


@responses.activate
def test_hubspot_with_new_contact_and_new_organisation(
organisation: Organisation,
settings: SettingsWrapper,
Expand All @@ -26,7 +34,15 @@ def test_hubspot_with_new_contact_and_new_organisation(
last_name="Louis",
marketing_consent_given=True,
)
url = f"{HUBSPOT_ROOT_FORM_URL}/{HUBSPOT_PORTAL_ID}/{HUBSPOT_FORM_ID}"
responses.add(
method="POST",
url=url,
status=status.HTTP_200_OK,
json={"inlineMessage": "Thanks for submitting the form."},
)

HubspotTracker.objects.create(user=user, hubspot_cookie="tracker")
future_hubspot_id = "10280696017"
mock_create_company = mocker.patch(
"integrations.lead_tracking.hubspot.client.HubspotClient.create_company",
Expand Down
6 changes: 6 additions & 0 deletions api/tests/unit/organisations/test_unit_organisations_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from environments.models import Environment
from environments.permissions.models import UserEnvironmentPermission
from features.models import Feature
from integrations.lead_tracking.hubspot.constants import HUBSPOT_COOKIE_NAME
from organisations.chargebee.metadata import ChargebeeObjMetadata
from organisations.invites.models import Invite
from organisations.models import (
Expand All @@ -46,6 +47,7 @@
from segments.models import Segment
from users.models import (
FFAdminUser,
HubspotTracker,
UserPermissionGroup,
UserPermissionGroupMembership,
)
Expand All @@ -69,6 +71,7 @@ def test_should_return_organisation_list_when_requested(

def test_non_superuser_can_create_new_organisation_by_default(
staff_client: APIClient,
staff_user: FFAdminUser,
) -> None:
# Given
org_name = "Test create org"
Expand All @@ -78,6 +81,8 @@ def test_non_superuser_can_create_new_organisation_by_default(
"name": org_name,
"webhook_notification_email": webhook_notification_email,
}
staff_client.cookies[HUBSPOT_COOKIE_NAME] = "test_cookie_tracker"
assert not HubspotTracker.objects.filter(user=staff_user).exists()

# When
response = staff_client.post(url, data=data)
Expand All @@ -88,6 +93,7 @@ def test_non_superuser_can_create_new_organisation_by_default(
Organisation.objects.get(name=org_name).webhook_notification_email
== webhook_notification_email
)
assert HubspotTracker.objects.filter(user=staff_user).exists()


@override_settings(RESTRICT_ORG_CREATE_TO_SUPERUSERS=True)
Expand Down
Loading

0 comments on commit 6714384

Please sign in to comment.