Skip to content

Commit 8d1b648

Browse files
drishrehanvdm
andauthored
feat(domains): add custom click tracking support (#195)
Co-authored-by: Rehan van der Merwe <rehan.vdm4@gmail.com>
1 parent db679b1 commit 8d1b648

5 files changed

Lines changed: 237 additions & 4 deletions

File tree

resend/domains/_domain.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List, Union
22

3-
from typing_extensions import TypedDict
3+
from typing_extensions import NotRequired, TypedDict
44

55
from resend.domains._record import Record
66

@@ -34,3 +34,15 @@ class Domain(TypedDict):
3434
"""
3535
Wether the domain is deleted or not
3636
"""
37+
open_tracking: NotRequired[bool]
38+
"""
39+
Track the open rate of each email.
40+
"""
41+
click_tracking: NotRequired[bool]
42+
"""
43+
Track clicks within the body of each HTML email.
44+
"""
45+
tracking_subdomain: NotRequired[str]
46+
"""
47+
The custom subdomain used for click and open tracking links (e.g., "links").
48+
"""

resend/domains/_domains.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ class CreateDomainResponse(BaseResponse):
7171
status (str): Status of the domain
7272
region (str): The region where emails will be sent from
7373
records (Union[List[Record], None]): The list of domain records
74+
open_tracking (bool): Whether open tracking is enabled
75+
click_tracking (bool): Whether click tracking is enabled
76+
tracking_subdomain (str): The custom subdomain for tracking links
7477
"""
7578

7679
id: str
@@ -97,6 +100,18 @@ class CreateDomainResponse(BaseResponse):
97100
"""
98101
The list of domain records
99102
"""
103+
open_tracking: NotRequired[bool]
104+
"""
105+
Track email opens
106+
"""
107+
click_tracking: NotRequired[bool]
108+
"""
109+
Track clicks within the body of HTML emails
110+
"""
111+
tracking_subdomain: NotRequired[str]
112+
"""
113+
The custom subdomain used for click and open tracking links (e.g., "links")
114+
"""
100115

101116
class UpdateParams(TypedDict):
102117
id: str
@@ -122,6 +137,10 @@ class UpdateParams(TypedDict):
122137
communication must use TLS no matter what.
123138
If the receiving server does not support TLS, the email will not be sent.
124139
"""
140+
tracking_subdomain: NotRequired[str]
141+
"""
142+
The custom subdomain used for click and open tracking links (e.g., "links").
143+
"""
125144

126145
class CreateParams(TypedDict):
127146
name: str
@@ -139,6 +158,10 @@ class CreateParams(TypedDict):
139158
You can change this by setting the optional `custom_return_path` parameter
140159
when creating a domain via the API or under Advanced options in the dashboard.
141160
"""
161+
tracking_subdomain: NotRequired[str]
162+
"""
163+
The custom subdomain used for click and open tracking links (e.g., "links").
164+
"""
142165

143166
@classmethod
144167
def create(cls, params: CreateParams) -> CreateDomainResponse:

resend/domains/_record.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from typing_extensions import TypedDict
1+
from typing_extensions import NotRequired, TypedDict
22

33

44
class Record(TypedDict):
55
record: str
66
"""
7-
The domain record type, ie: SPF.
7+
The domain record type, ie: SPF, DKIM, Inbound, Tracking.
88
"""
99
name: str
1010
"""
@@ -26,7 +26,7 @@ class Record(TypedDict):
2626
"""
2727
The domain record value.
2828
"""
29-
priority: int
29+
priority: NotRequired[int]
3030
"""
3131
The domain record priority.
3232
"""

tests/domains_async_test.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,89 @@ async def test_should_list_domains_async_raise_exception_when_no_content(
146146
with pytest.raises(NoContentError):
147147
_ = await resend.Domains.list_async()
148148

149+
async def test_domains_create_async_with_tracking_subdomain(self) -> None:
150+
self.set_mock_json(
151+
{
152+
"id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
153+
"name": "example.com",
154+
"created_at": "2023-03-28T17:12:02.059593+00:00",
155+
"status": "not_started",
156+
"open_tracking": True,
157+
"click_tracking": True,
158+
"tracking_subdomain": "links",
159+
"records": [
160+
{
161+
"record": "DKIM",
162+
"name": "nhapbbryle57yxg3fbjytyodgbt2kyyg._domainkey",
163+
"value": "nhapbbryle57yxg3fbjytyodgbt2kyyg.dkim.amazonses.com.",
164+
"type": "CNAME",
165+
"status": "not_started",
166+
"ttl": "Auto",
167+
},
168+
{
169+
"record": "Tracking",
170+
"name": "links.example.com",
171+
"value": "links1.resend-dns.com",
172+
"type": "CNAME",
173+
"ttl": "Auto",
174+
"status": "not_started",
175+
},
176+
],
177+
"region": "us-east-1",
178+
}
179+
)
180+
181+
create_params: resend.Domains.CreateParams = {
182+
"name": "example.com",
183+
"region": "us-east-1",
184+
"tracking_subdomain": "links",
185+
}
186+
domain = await resend.Domains.create_async(params=create_params)
187+
assert domain["id"] == "4dd369bc-aa82-4ff3-97de-514ae3000ee0"
188+
assert domain["open_tracking"] is True
189+
assert domain["click_tracking"] is True
190+
assert domain["tracking_subdomain"] == "links"
191+
tracking_record = next(
192+
(r for r in (domain["records"] or []) if r["record"] == "Tracking"), None
193+
)
194+
assert tracking_record is not None
195+
assert tracking_record["name"] == "links.example.com"
196+
assert tracking_record["value"] == "links1.resend-dns.com"
197+
assert tracking_record["type"] == "CNAME"
198+
199+
async def test_domains_get_async_with_tracking_fields(self) -> None:
200+
self.set_mock_json(
201+
{
202+
"object": "domain",
203+
"id": "d91cd9bd-1176-453e-8fc1-35364d380206",
204+
"name": "example.com",
205+
"status": "not_started",
206+
"created_at": "2023-04-26T20:21:26.347412+00:00",
207+
"region": "us-east-1",
208+
"open_tracking": True,
209+
"click_tracking": True,
210+
"tracking_subdomain": "links",
211+
"records": [
212+
{
213+
"record": "Tracking",
214+
"name": "links.example.com",
215+
"value": "links1.resend-dns.com",
216+
"type": "CNAME",
217+
"ttl": "Auto",
218+
"status": "verified",
219+
}
220+
],
221+
}
222+
)
223+
224+
domain = await resend.Domains.get_async(
225+
domain_id="d91cd9bd-1176-453e-8fc1-35364d380206",
226+
)
227+
assert domain["id"] == "d91cd9bd-1176-453e-8fc1-35364d380206"
228+
assert domain["open_tracking"] is True
229+
assert domain["click_tracking"] is True
230+
assert domain["tracking_subdomain"] == "links"
231+
149232
async def test_domains_update_async(self) -> None:
150233
self.set_mock_json(
151234
{
@@ -171,6 +254,21 @@ async def test_domains_update_async(self) -> None:
171254
assert domain["created_at"] == "2023-04-26T20:21:26.347412+00:00"
172255
assert domain["region"] == "us-east-1"
173256

257+
async def test_domains_update_async_with_tracking_subdomain(self) -> None:
258+
self.set_mock_json(
259+
{
260+
"object": "domain",
261+
"id": "d91cd9bd-1176-453e-8fc1-35364d380206",
262+
}
263+
)
264+
265+
update_params: resend.Domains.UpdateParams = {
266+
"id": "d91cd9bd-1176-453e-8fc1-35364d380206",
267+
"tracking_subdomain": "links",
268+
}
269+
domain = await resend.Domains.update_async(params=update_params)
270+
assert domain["id"] == "d91cd9bd-1176-453e-8fc1-35364d380206"
271+
174272
async def test_should_update_domains_async_raise_exception_when_no_content(
175273
self,
176274
) -> None:

tests/domains_test.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,91 @@ def test_should_verify_domains_raise_exception_when_no_content(self) -> None:
180180
domain_id="d91cd9bd-1176-453e-8fc1-35364d380206",
181181
)
182182

183+
def test_domains_create_with_tracking_subdomain(self) -> None:
184+
self.set_mock_json(
185+
{
186+
"id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
187+
"name": "example.com",
188+
"created_at": "2023-03-28T17:12:02.059593+00:00",
189+
"status": "not_started",
190+
"open_tracking": True,
191+
"click_tracking": True,
192+
"tracking_subdomain": "links",
193+
"records": [
194+
{
195+
"record": "DKIM",
196+
"name": "nhapbbryle57yxg3fbjytyodgbt2kyyg._domainkey",
197+
"value": "nhapbbryle57yxg3fbjytyodgbt2kyyg.dkim.amazonses.com.",
198+
"type": "CNAME",
199+
"status": "not_started",
200+
"ttl": "Auto",
201+
},
202+
{
203+
"record": "Tracking",
204+
"name": "links.example.com",
205+
"value": "links1.resend-dns.com",
206+
"type": "CNAME",
207+
"ttl": "Auto",
208+
"status": "not_started",
209+
},
210+
],
211+
"region": "us-east-1",
212+
}
213+
)
214+
215+
create_params: resend.Domains.CreateParams = {
216+
"name": "example.com",
217+
"region": "us-east-1",
218+
"tracking_subdomain": "links",
219+
}
220+
domain: resend.Domains.CreateDomainResponse = resend.Domains.create(
221+
params=create_params
222+
)
223+
assert domain["id"] == "4dd369bc-aa82-4ff3-97de-514ae3000ee0"
224+
assert domain["open_tracking"] is True
225+
assert domain["click_tracking"] is True
226+
assert domain["tracking_subdomain"] == "links"
227+
tracking_record = next(
228+
(r for r in (domain["records"] or []) if r["record"] == "Tracking"), None
229+
)
230+
assert tracking_record is not None
231+
assert tracking_record["name"] == "links.example.com"
232+
assert tracking_record["value"] == "links1.resend-dns.com"
233+
assert tracking_record["type"] == "CNAME"
234+
235+
def test_domains_get_with_tracking_fields(self) -> None:
236+
self.set_mock_json(
237+
{
238+
"object": "domain",
239+
"id": "d91cd9bd-1176-453e-8fc1-35364d380206",
240+
"name": "example.com",
241+
"status": "not_started",
242+
"created_at": "2023-04-26T20:21:26.347412+00:00",
243+
"region": "us-east-1",
244+
"open_tracking": True,
245+
"click_tracking": True,
246+
"tracking_subdomain": "links",
247+
"records": [
248+
{
249+
"record": "Tracking",
250+
"name": "links.example.com",
251+
"value": "links1.resend-dns.com",
252+
"type": "CNAME",
253+
"ttl": "Auto",
254+
"status": "verified",
255+
}
256+
],
257+
}
258+
)
259+
260+
domain = resend.Domains.get(
261+
domain_id="d91cd9bd-1176-453e-8fc1-35364d380206",
262+
)
263+
assert domain["id"] == "d91cd9bd-1176-453e-8fc1-35364d380206"
264+
assert domain["open_tracking"] is True
265+
assert domain["click_tracking"] is True
266+
assert domain["tracking_subdomain"] == "links"
267+
183268
def test_domains_update(self) -> None:
184269
self.set_mock_json(
185270
{
@@ -197,6 +282,21 @@ def test_domains_update(self) -> None:
197282
domain = resend.Domains.update(params)
198283
assert domain["id"] == "479e3145-dd38-476b-932c-529ceb705947"
199284

285+
def test_domains_update_with_tracking_subdomain(self) -> None:
286+
self.set_mock_json(
287+
{
288+
"object": "domain",
289+
"id": "479e3145-dd38-476b-932c-529ceb705947",
290+
}
291+
)
292+
293+
params: resend.Domains.UpdateParams = {
294+
"id": "479e3145-dd38-476b-932c-529ceb705947",
295+
"tracking_subdomain": "links",
296+
}
297+
domain = resend.Domains.update(params)
298+
assert domain["id"] == "479e3145-dd38-476b-932c-529ceb705947"
299+
200300
def test_should_update_domains_raise_exception_when_no_content(self) -> None:
201301
self.set_mock_json(None)
202302
params: resend.Domains.UpdateParams = {

0 commit comments

Comments
 (0)