Skip to content
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

feat: Add hubspot cookie tracking #4539

Merged
merged 18 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
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
Loading