Skip to content

Commit 567ffee

Browse files
Zaimwa9matthewelwellpre-commit-ci[bot]
authored
fix: trigger create hubspot user post tracker (#5711)
Co-authored-by: Matthew Elwell <matthew.elwell@flagsmith.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7bb6a58 commit 567ffee

20 files changed

Lines changed: 253 additions & 105 deletions

File tree

api/custom_auth/oauth/serializers.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
from rest_framework import serializers
88
from rest_framework.authtoken.models import Token
99

10+
from integrations.lead_tracking.hubspot.services import (
11+
register_hubspot_tracker_and_track_user,
12+
)
1013
from users.auth_type import AuthType
11-
from users.models import SignUpType
14+
from users.models import FFAdminUser, SignUpType
15+
from users.serializers import UTMDataSerializer
1216

1317
from ..serializers import InviteLinkValidationMixin
1418
from .github import GithubUser
@@ -31,7 +35,11 @@ class OAuthLoginSerializer(InviteLinkValidationMixin, serializers.Serializer):
3135
help_text="Provide information about how the user signed up (i.e. via invite or not)",
3236
write_only=True,
3337
)
34-
38+
hubspot_cookie = serializers.CharField(
39+
required=False, allow_null=True, allow_blank=True
40+
)
41+
marketing_consent_given = serializers.BooleanField(required=False, allow_null=True)
42+
utm_data = UTMDataSerializer(required=False, allow_null=True)
3543
auth_type: AuthType | None = None
3644
user_model_id_attribute: str = "id"
3745

@@ -87,9 +95,16 @@ def _get_user(self, user_data: dict): # type: ignore[type-arg,no-untyped-def]
8795
email=email, sign_up_type=self.validated_data.get("sign_up_type")
8896
)
8997

90-
return UserModel.objects.create(
98+
user = FFAdminUser.objects.create(
9199
**user_data, email=email.lower(), sign_up_type=sign_up_type
92100
)
101+
102+
# On first OAuth signup, we register the hubspot cookies and utms before creating the hubspot contact
103+
if request := self.context.get("request"):
104+
register_hubspot_tracker_and_track_user(request, user)
105+
106+
return user
107+
93108
elif existing_user.auth_type != self.get_auth_type().value:
94109
# In this scenario, we're seeing a user that had previously
95110
# authenticated with another authentication method and is now

api/custom_auth/oauth/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
@swagger_auto_schema(
2828
method="post",
2929
request_body=GoogleLoginSerializer,
30-
responses={200: CustomTokenSerializer(), 502: ErrorSerializer()},
30+
responses={200: CustomTokenSerializer, 502: ErrorSerializer},
3131
)
3232
@api_view(["POST"])
3333
@permission_classes([AllowAny])
@@ -52,7 +52,7 @@ def login_with_google(request): # type: ignore[no-untyped-def]
5252
@swagger_auto_schema(
5353
method="post",
5454
request_body=GithubLoginSerializer,
55-
responses={200: CustomTokenSerializer(), 502: ErrorSerializer()},
55+
responses={200: CustomTokenSerializer, 502: ErrorSerializer},
5656
)
5757
@api_view(["POST"])
5858
@permission_classes([AllowAny])

api/custom_auth/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from custom_auth.mfa.trench.utils import user_token_generator
3131
from custom_auth.serializers import CustomUserDelete
3232
from integrations.lead_tracking.hubspot.services import (
33-
register_hubspot_tracker,
33+
register_hubspot_tracker_and_track_user,
3434
)
3535
from users.constants import DEFAULT_DELETE_ORPHAN_ORGANISATIONS_VALUE
3636
from users.models import FFAdminUser
@@ -128,7 +128,8 @@ def get_throttles(self): # type: ignore[no-untyped-def]
128128

129129
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
130130
response = super().create(request, *args, **kwargs)
131-
register_hubspot_tracker(request, user=self.user)
131+
register_hubspot_tracker_and_track_user(request, user=self.user)
132+
132133
if settings.COOKIE_AUTH_ENABLED:
133134
authorise_response(self.user, response)
134135
return response # type: ignore[no-any-return]

api/integrations/lead_tracking/hubspot/lead_tracker.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,8 @@ def create_user_hubspot_contact(self, user: FFAdminUser) -> str | None:
7070
create_lead_form_kwargs: dict[str, Any] = {"user": user}
7171
if tracker:
7272
create_lead_form_kwargs.update(
73-
{
74-
"hubspot_cookie": tracker.hubspot_cookie,
75-
"utm_data": tracker.utm_data,
76-
}
73+
hubspot_cookie=tracker.hubspot_cookie,
74+
utm_data=tracker.utm_data,
7775
)
7876
self.client.create_lead_form(**create_lead_form_kwargs)
7977

api/integrations/lead_tracking/hubspot/services.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import logging
22

3+
from django.conf import settings
34
from rest_framework.request import Request
45

56
from integrations.lead_tracking.hubspot.client import HubspotClient
67
from integrations.lead_tracking.hubspot.constants import (
78
HUBSPOT_ACTIVE_SUBSCRIPTION_SELF_HOSTED,
89
HUBSPOT_COOKIE_NAME,
910
)
11+
from integrations.lead_tracking.hubspot.tasks import (
12+
create_hubspot_contact_for_user,
13+
)
1014
from users.models import FFAdminUser, HubspotTracker
1115
from users.serializers import UTMDataSerializer
1216

1317
logger = logging.getLogger(__name__)
1418

1519

20+
def register_hubspot_tracker_and_track_user(
21+
request: Request,
22+
user: FFAdminUser | None = None,
23+
) -> None:
24+
user_id = user.id if user else request.user.id
25+
register_hubspot_tracker(request, user)
26+
if settings.ENABLE_HUBSPOT_LEAD_TRACKING:
27+
create_hubspot_contact_for_user.delay(args=(user_id,))
28+
29+
1630
def register_hubspot_tracker(
1731
request: Request,
1832
user: FFAdminUser | None = None,

api/organisations/invites/views.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
from rest_framework.throttling import ScopedRateThrottle
1515
from rest_framework.viewsets import GenericViewSet
1616

17-
from integrations.lead_tracking.hubspot.services import (
18-
register_hubspot_tracker,
19-
)
2017
from organisations.invites.exceptions import InviteExpiredError
2118
from organisations.invites.models import Invite, InviteLink
2219
from organisations.invites.serializers import (
@@ -47,8 +44,6 @@ def join_organisation_from_email(request, hash): # type: ignore[no-untyped-def]
4744
error_data = {"detail": str(e)}
4845
return Response(data=error_data, status=status.HTTP_400_BAD_REQUEST)
4946

50-
register_hubspot_tracker(request)
51-
5247
return Response(
5348
OrganisationSerializerFull(
5449
invite.organisation, context={"request": request}
@@ -67,8 +62,6 @@ def join_organisation_from_link(request, hash): # type: ignore[no-untyped-def]
6762
if invite.is_expired:
6863
raise InviteExpiredError()
6964

70-
register_hubspot_tracker(request)
71-
7265
request.user.join_organisation_from_invite_link(invite)
7366

7467
return Response(

api/organisations/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import unicode_literals
22

3+
from datetime import timedelta
34
from typing import TYPE_CHECKING, Any
45

56
from common.core.utils import is_enterprise, is_saas
@@ -224,7 +225,8 @@ def register_hubspot_lead_tracking(self): # type: ignore[no-untyped-def]
224225
args=(
225226
self.user.id,
226227
self.organisation.id,
227-
)
228+
),
229+
delay_until=timezone.now() + timedelta(minutes=3),
228230
)
229231

230232

api/organisations/views.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@
2222
get_multiple_event_list_for_organisation,
2323
)
2424
from core.helpers import get_current_site_url
25-
from integrations.lead_tracking.hubspot.services import (
26-
register_hubspot_tracker,
27-
)
2825
from organisations.chargebee import webhook_event_types, webhook_handlers
2926
from organisations.exceptions import OrganisationHasNoPaidSubscription
3027
from organisations.models import (
@@ -114,7 +111,6 @@ def create(self, request, **kwargs): # type: ignore[no-untyped-def]
114111
Override create method to add new organisation to authenticated user
115112
"""
116113

117-
register_hubspot_tracker(request)
118114
user = request.user
119115
serializer = OrganisationSerializerFull(data=request.data)
120116
if serializer.is_valid():

api/tests/unit/custom_auth/oauth/test_unit_oauth_serializers.py

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,54 @@
11
from typing import Type
22
from unittest import mock
3+
from unittest.mock import MagicMock
34

45
import pytest
56
from django.test import RequestFactory
67
from django.utils import timezone
78
from pytest_django.fixtures import SettingsWrapper
89
from pytest_mock import MockerFixture
910
from rest_framework.authtoken.models import Token
11+
from rest_framework.parsers import JSONParser
12+
from rest_framework.request import Request
13+
from rest_framework.test import APIRequestFactory
1014

1115
from custom_auth.oauth.serializers import (
1216
GithubLoginSerializer,
1317
GoogleLoginSerializer,
1418
OAuthLoginSerializer,
1519
)
1620
from organisations.invites.models import InviteLink
17-
from users.models import FFAdminUser, SignUpType
21+
from users.models import FFAdminUser, HubspotTracker, SignUpType
1822

1923

24+
@pytest.mark.parametrize(
25+
"enable_hubspot_lead_tracking",
26+
(
27+
True,
28+
False,
29+
),
30+
)
2031
@mock.patch("custom_auth.oauth.serializers.get_user_info")
2132
def test_create_oauth_login_serializer(
22-
mock_get_user_info: mock.MagicMock, db: None
33+
mock_get_user_info: mock.MagicMock,
34+
db: None,
35+
settings: SettingsWrapper,
36+
mocker: MagicMock,
37+
enable_hubspot_lead_tracking: bool,
2338
) -> None:
2439
# Given
40+
settings.ENABLE_HUBSPOT_LEAD_TRACKING = enable_hubspot_lead_tracking
2541
access_token = "access-token"
2642
sign_up_type = "NO_INVITE"
27-
data = {"access_token": access_token, "sign_up_type": sign_up_type}
28-
rf = RequestFactory()
29-
request = rf.post("/api/v1/auth/oauth/google/")
43+
data = {
44+
"access_token": access_token,
45+
"sign_up_type": sign_up_type,
46+
"hubspotutk": "test-hubspot-utk",
47+
"utm_data": {"utm_source": "test-utm-data"},
48+
}
49+
rf = APIRequestFactory()
50+
django_request = rf.post("/api/v1/auth/oauth/google/", data=data, format="json")
51+
request = Request(django_request, parsers=[JSONParser()])
3052
email = "testytester@example.com"
3153
first_name = "testy"
3254
last_name = "tester"
@@ -39,7 +61,9 @@ def test_create_oauth_login_serializer(
3961
"google_user_id": google_user_id,
4062
}
4163
serializer = OAuthLoginSerializer(data=data, context={"request": request}) # type: ignore[abstract]
42-
64+
mock_create_hubspot_contact_for_user = mocker.patch(
65+
"integrations.lead_tracking.hubspot.services.create_hubspot_contact_for_user"
66+
)
4367
# monkey patch the get_user_info method to return the mock user data
4468
serializer.get_user_info = lambda: mock_user_data # type: ignore[method-assign]
4569

@@ -48,7 +72,17 @@ def test_create_oauth_login_serializer(
4872
response = serializer.save()
4973

5074
# Then
51-
assert FFAdminUser.objects.filter(email=email, sign_up_type=sign_up_type).exists()
75+
user = FFAdminUser.objects.filter(email=email, sign_up_type=sign_up_type).first()
76+
assert user is not None
77+
hubspot_tracker = HubspotTracker.objects.get(user=user)
78+
assert hubspot_tracker.utm_data == {"utm_source": "test-utm-data"}
79+
assert hubspot_tracker.hubspot_cookie == "test-hubspot-utk"
80+
if enable_hubspot_lead_tracking:
81+
mock_create_hubspot_contact_for_user.delay.assert_called_once_with(
82+
args=(user.id,)
83+
)
84+
else:
85+
mock_create_hubspot_contact_for_user.delay.assert_not_called()
5286
assert isinstance(response, Token)
5387
assert (timezone.now() - response.user.last_login).seconds < 5
5488
assert response.user.email == email
@@ -103,16 +137,18 @@ def test_OAuthLoginSerializer_calls_is_authentication_method_valid_correctly_if_
103137
):
104138
# Given
105139
settings.AUTH_CONTROLLER_INSTALLED = True
106-
107-
request = rf.post("/some-login/url")
140+
rf = APIRequestFactory()
141+
data = {"access_token": "some_token"}
142+
django_request = rf.post("/some-login/url", data=data, format="json")
143+
request = Request(django_request, parsers=[JSONParser()])
108144
user_email = "test_user@test.com"
109145
mocked_auth_controller = mocker.MagicMock()
110146
mocker.patch.dict(
111147
"sys.modules", {"auth_controller.controller": mocked_auth_controller}
112148
)
113149

114150
serializer = OAuthLoginSerializer( # type: ignore[abstract]
115-
data={"access_token": "some_token"}, context={"request": request}
151+
data=data, context={"request": request}
116152
)
117153
# monkey patch the get_user_info method to return the mock user data
118154
serializer.get_user_info = lambda: {"email": user_email} # type: ignore[method-assign]
@@ -139,16 +175,22 @@ def test_OAuthLoginSerializer_allows_registration_if_sign_up_type_is_invite_link
139175
):
140176
# Given
141177
settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False
142-
143-
request = rf.post("/api/v1/auth/users/")
178+
data = {
179+
"access_token": "some_token",
180+
"sign_up_type": SignUpType.INVITE_LINK.value,
181+
"invite_hash": invite_link.hash,
182+
}
183+
rf = APIRequestFactory()
184+
django_request = rf.post(
185+
"/api/v1/auth/oauth/google/",
186+
data=data,
187+
format="json",
188+
)
189+
request = Request(django_request, parsers=[JSONParser()])
144190
user_email = "test_user@test.com"
145191

146192
serializer = OAuthLoginSerializer( # type: ignore[abstract]
147-
data={
148-
"access_token": "some_token",
149-
"sign_up_type": SignUpType.INVITE_LINK.value,
150-
"invite_hash": invite_link.hash,
151-
},
193+
data=data,
152194
context={"request": request},
153195
)
154196
# monkey patch the get_user_info method to return the mock user data

0 commit comments

Comments
 (0)