-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(demo-mode): email prompt api endpoint (#83725)
- Loading branch information
1 parent
da017ea
commit a13ce7b
Showing
5 changed files
with
178 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from rest_framework import serializers | ||
from rest_framework.request import Request | ||
from rest_framework.response import Response | ||
|
||
from sentry import options | ||
from sentry.api.api_owners import ApiOwner | ||
from sentry.api.api_publish_status import ApiPublishStatus | ||
from sentry.api.base import Endpoint, region_silo_endpoint | ||
from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer | ||
from sentry.utils.marketo_client import MarketoClient | ||
|
||
client = MarketoClient() | ||
|
||
|
||
class EmailCaptureSerializer(CamelSnakeSerializer): | ||
email = serializers.EmailField(required=True) | ||
|
||
|
||
@region_silo_endpoint | ||
class EmailCaptureEndpoint(Endpoint): | ||
publish_status = { | ||
"POST": ApiPublishStatus.PRIVATE, | ||
} | ||
owner = ApiOwner.TELEMETRY_EXPERIENCE | ||
# Disable authentication and permission requirements. | ||
permission_classes = () | ||
|
||
def post(self, request: Request) -> Response: | ||
if not options.get("demo-mode.enabled"): | ||
return Response(status=404) | ||
|
||
serializer = EmailCaptureSerializer(data=request.data) | ||
serializer.is_valid(raise_exception=True) | ||
|
||
email = serializer.validated_data["email"] | ||
|
||
# Include other fields in the request and send them to Marketo together. | ||
# There are a undetermined number of optional fields in request.data and we don't validate them. | ||
# Only the email field is required. | ||
form = request.data | ||
form["email"] = email | ||
client.submit_form(form) | ||
return Response(status=200) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from typing import TypedDict | ||
|
||
from django.conf import settings | ||
|
||
from sentry import http | ||
|
||
|
||
def marketo_option(field): | ||
return settings.MARKETO[field] | ||
|
||
|
||
class ErrorDict(TypedDict): | ||
code: str | ||
message: str | ||
|
||
|
||
class MarketoErrorResponse(TypedDict): | ||
errors: list[ErrorDict] | ||
|
||
|
||
class MarketoError(Exception): | ||
def __init__(self, data: MarketoErrorResponse): | ||
# just use the first error | ||
error = data["errors"][0] | ||
self.code = error["code"] | ||
self.message = error["message"] | ||
|
||
def __str__(self): | ||
return f"MarketoError: {self.code} - {self.message}" | ||
|
||
|
||
class MarketoClient: | ||
OAUTH_URL = "/identity/oauth/token" | ||
SUBMIT_FORM_URL = "/rest/v1/leads/submitForm.json" | ||
|
||
def make_request(self, url: str, *args, method="GET", **kwargs): | ||
base_url = marketo_option("base-url") | ||
full_url = base_url + url | ||
method = kwargs.pop("method", "GET") | ||
session = http.build_session() | ||
resp = getattr(session, method.lower())(full_url, *args, **kwargs) | ||
resp.raise_for_status() | ||
return resp.json() | ||
|
||
def make_rest_request(self, url: str, *args, headers=None, **kwargs): | ||
if headers is None: | ||
headers = {} | ||
headers["Authorization"] = f"Bearer {self.token}" | ||
headers["Content-Type"] = "application/json" | ||
data = self.make_request(url, *args, headers=headers, **kwargs) | ||
|
||
if not data.get("success"): | ||
raise MarketoError(data) | ||
|
||
# not handling field level errors where success=True | ||
|
||
return data | ||
|
||
def retrieve_token(self): | ||
client_id = marketo_option("client-id") | ||
clint_secret = marketo_option("client-secret") | ||
|
||
url = f"{self.OAUTH_URL}?grant_type=client_credentials&client_id={client_id}&client_secret={clint_secret}" | ||
return self.make_request(url) | ||
|
||
def submit_form(self, fields): | ||
body = { | ||
"formId": marketo_option("form-id"), | ||
"input": [ | ||
{ | ||
"leadFormFields": fields, | ||
} | ||
], | ||
} | ||
|
||
return self.make_rest_request(self.SUBMIT_FORM_URL, method="POST", json=body) | ||
|
||
@property | ||
def token(self): | ||
resp = self.retrieve_token() | ||
return resp["access_token"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from unittest import mock | ||
|
||
from django.urls import reverse | ||
|
||
from sentry.testutils.cases import APITestCase | ||
from sentry.testutils.helpers.options import override_options | ||
from sentry.utils.marketo_client import MarketoClient | ||
|
||
|
||
class EmailCaptureTest(APITestCase): | ||
def setUp(self): | ||
super().setUp() | ||
self.organization = self.create_organization() | ||
# demo user | ||
self.demo_user = self.create_user() | ||
self.demo_om = self.create_member( | ||
organization=self.organization, user=self.demo_user, role="member" | ||
) | ||
|
||
@mock.patch.object(MarketoClient, "submit_form") | ||
@override_options({"demo-mode.enabled": True}) | ||
def test_capture_endpoint(self, mock_submit_form): | ||
self.login_as(self.demo_user) | ||
url = reverse("sentry-demo-mode-email-capture") | ||
response = self.client.post(url, {"email": "[email protected]"}) | ||
assert response.status_code == 200, response.content | ||
mock_submit_form.assert_called_once_with({"email": "[email protected]"}) | ||
|
||
@override_options({"demo-mode.enabled": False}) | ||
def test_capture_endpoint_disabled(self): | ||
self.login_as(self.demo_user) | ||
url = reverse("sentry-demo-mode-email-capture") | ||
response = self.client.post(url, {"email": "[email protected]"}) | ||
assert response.status_code == 404 | ||
|
||
@override_options({"demo-mode.enabled": True}) | ||
def test_capture_endpoint_bad_request(self): | ||
self.login_as(self.demo_user) | ||
url = reverse("sentry-demo-mode-email-capture") | ||
response = self.client.post(url, {"email": "test123"}) | ||
assert response.status_code == 400 | ||
assert response.data == {"email": ["Enter a valid email address."]} |