Skip to content

Commit 001165f

Browse files
authored
feat: capture metrics during email validation (#16739)
1 parent a36ae29 commit 001165f

File tree

9 files changed

+64
-21
lines changed

9 files changed

+64
-21
lines changed

bin/tests

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ if [ "${COVERAGE:-yes}" != "no" ]; then
4545
python -m coverage run -m pytest --strict-markers "${COMMAND_ARGS[@]}"
4646
python -m coverage combine
4747
python -m coverage html --show-contexts
48-
python -m coverage report -m --fail-under 100
48+
python -m coverage report -m --fail-under 100 --skip-covered
4949
else
5050
python -m pytest --strict-markers "${COMMAND_ARGS[@]}"
5151
fi;

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ exclude_lines = [
2828
"if (typing\\.)?TYPE_CHECKING:",
2929
]
3030

31-
# Don't show us anything that's already 100% covered.
32-
skip_covered = true
33-
3431
[tool.curlylint]
3532
include = '\.(html|jinja|txt)$'
3633
# For jinja's i18n extension:

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def pyramid_request(pyramid_services, jinja, remote_addr, remote_addr_hashed):
212212
dummy_request._unauthenticated_userid = None
213213
dummy_request.user = None
214214
dummy_request.oidc_publisher = None
215+
dummy_request.metrics = dummy_request.find_service(IMetricsService)
215216

216217
dummy_request.registry.registerUtility(jinja, IJinja2Environment, name=".jinja2")
217218

tests/unit/accounts/test_forms.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ def mock_validate_email(email, check_deliverability=True, *args, **kwargs):
414414

415415

416416
class TestRegistrationForm:
417-
def test_validate(self):
417+
def test_validate(self, metrics):
418418
captcha_service = pretend.stub(
419419
enabled=False,
420420
verify_response=pretend.call_recorder(lambda _: None),
@@ -432,7 +432,8 @@ def test_validate(self):
432432

433433
form = forms.RegistrationForm(
434434
request=pretend.stub(
435-
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
435+
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False)),
436+
metrics=metrics,
436437
),
437438
formdata=MultiDict(
438439
{
@@ -538,10 +539,11 @@ def test_invalid_email_error(self, pyramid_request, email):
538539
str(form.email.errors.pop()) == "The email address isn't valid. Try again."
539540
)
540541

541-
def test_exotic_email_success(self):
542+
def test_exotic_email_success(self, metrics):
542543
form = forms.RegistrationForm(
543544
request=pretend.stub(
544-
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
545+
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False)),
546+
metrics=metrics,
545547
),
546548
formdata=MultiDict({"email": "[email protected]"}),
547549
user_service=pretend.stub(

tests/unit/manage/test_forms.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,13 @@ def test_name_too_long(self, pyramid_config):
170170

171171

172172
class TestAddEmailForm:
173-
def test_validate(self):
173+
def test_validate(self, metrics):
174174
user_id = pretend.stub()
175175
user_service = pretend.stub(find_userid_by_email=lambda _: None)
176176
form = forms.AddEmailForm(
177177
request=pretend.stub(
178-
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
178+
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False)),
179+
metrics=metrics,
179180
),
180181
formdata=MultiDict({"email": "[email protected]"}),
181182
user_id=user_id,

tests/unit/metrics/test_init.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DataDogMetrics,
2020
IMetricsService,
2121
NullMetrics,
22+
_metrics,
2223
event_handlers,
2324
includeme,
2425
views,
@@ -30,6 +31,7 @@ def test_include_defaults_to_null():
3031
registry=pretend.stub(settings={}),
3132
maybe_dotted=lambda i: i,
3233
register_service_factory=pretend.call_recorder(lambda factory, iface: None),
34+
add_request_method=pretend.call_recorder(lambda fn, name, reify: None),
3335
add_subscriber=pretend.call_recorder(lambda handler, event: None),
3436
add_view_deriver=pretend.call_recorder(lambda deriver, under: None),
3537
)
@@ -49,6 +51,9 @@ def test_include_defaults_to_null():
4951
assert config.add_view_deriver.calls == [
5052
pretend.call(views.timing_view, under=viewderivers.INGRESS)
5153
]
54+
assert config.add_request_method.calls == [
55+
pretend.call(_metrics, name="metrics", reify=True)
56+
]
5257

5358

5459
def test_include_sets_class():
@@ -60,6 +65,7 @@ def test_include_sets_class():
6065
pth
6166
],
6267
register_service_factory=pretend.call_recorder(lambda factory, iface: None),
68+
add_request_method=pretend.call_recorder(lambda fn, name, reify: None),
6369
add_subscriber=pretend.call_recorder(lambda handler, event: None),
6470
add_view_deriver=pretend.call_recorder(lambda deriver, under: None),
6571
)
@@ -79,3 +85,10 @@ def test_include_sets_class():
7985
assert config.add_view_deriver.calls == [
8086
pretend.call(views.timing_view, under=viewderivers.INGRESS)
8187
]
88+
assert config.add_request_method.calls == [
89+
pretend.call(_metrics, name="metrics", reify=True)
90+
]
91+
92+
93+
def test_finds_service(pyramid_request):
94+
assert _metrics(pyramid_request) == pyramid_request.find_service(IMetricsService)

warehouse/accounts/forms.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ def validate_email(self, field):
282282
try:
283283
resp = email_validator.validate_email(field.data, check_deliverability=True)
284284
except email_validator.EmailNotValidError as e:
285+
self.request.metrics.increment(
286+
"warehouse.accounts.forms.validate_email",
287+
tags=["result:invalid", "reason:email_validator"],
288+
)
285289
raise wtforms.validators.ValidationError(
286290
self.request._("The email address isn't valid. Try again.")
287291
) from e
@@ -310,6 +314,10 @@ def validate_email(self, field):
310314
)
311315
).scalar()
312316
):
317+
self.request.metrics.increment(
318+
"warehouse.accounts.forms.validate_email",
319+
tags=["result:invalid", "reason:prohibited_domain"],
320+
)
313321
raise wtforms.validators.ValidationError(
314322
self.request._(
315323
"You can't use an email address from this domain. Use a "
@@ -321,20 +329,33 @@ def validate_email(self, field):
321329
userid = self.user_service.find_userid_by_email(field.data)
322330

323331
if userid and userid == self.user_id:
332+
self.request.metrics.increment(
333+
"warehouse.accounts.forms.validate_email",
334+
tags=["result:invalid", "reason:email_in_use_by_self"],
335+
)
324336
raise wtforms.validators.ValidationError(
325337
self.request._(
326338
"This email address is already being used by this account. "
327339
"Use a different email."
328340
)
329341
)
330342
if userid:
343+
self.request.metrics.increment(
344+
"warehouse.accounts.forms.validate_email",
345+
tags=["result:invalid", "reason:email_in_use_by_other"],
346+
)
331347
raise wtforms.validators.ValidationError(
332348
self.request._(
333349
"This email address is already being used "
334350
"by another account. Use a different email."
335351
)
336352
)
337353

354+
self.request.metrics.increment(
355+
"warehouse.accounts.forms.validate_email",
356+
tags=["result:valid"],
357+
)
358+
338359

339360
class HoneypotMixin:
340361
"""A mixin to catch spammers. This field should always be blank"""

warehouse/locale/messages.pot

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ msgstr ""
1414
msgid "Locale updated"
1515
msgstr ""
1616

17-
#: warehouse/accounts/forms.py:52 warehouse/accounts/forms.py:286
17+
#: warehouse/accounts/forms.py:52 warehouse/accounts/forms.py:290
1818
msgid "The email address isn't valid. Try again."
1919
msgstr ""
2020

@@ -74,47 +74,47 @@ msgstr ""
7474
msgid "The email address is too long. Try again."
7575
msgstr ""
7676

77-
#: warehouse/accounts/forms.py:314
77+
#: warehouse/accounts/forms.py:322
7878
msgid "You can't use an email address from this domain. Use a different email."
7979
msgstr ""
8080

81-
#: warehouse/accounts/forms.py:325
81+
#: warehouse/accounts/forms.py:337
8282
msgid ""
8383
"This email address is already being used by this account. Use a different"
8484
" email."
8585
msgstr ""
8686

87-
#: warehouse/accounts/forms.py:332
87+
#: warehouse/accounts/forms.py:348
8888
msgid ""
8989
"This email address is already being used by another account. Use a "
9090
"different email."
9191
msgstr ""
9292

93-
#: warehouse/accounts/forms.py:367 warehouse/manage/forms.py:139
93+
#: warehouse/accounts/forms.py:388 warehouse/manage/forms.py:139
9494
msgid "The name is too long. Choose a name with 100 characters or less."
9595
msgstr ""
9696

97-
#: warehouse/accounts/forms.py:374
97+
#: warehouse/accounts/forms.py:395
9898
msgid "URLs are not allowed in the name field."
9999
msgstr ""
100100

101-
#: warehouse/accounts/forms.py:463
101+
#: warehouse/accounts/forms.py:484
102102
msgid "Invalid TOTP code."
103103
msgstr ""
104104

105-
#: warehouse/accounts/forms.py:480
105+
#: warehouse/accounts/forms.py:501
106106
msgid "Invalid WebAuthn assertion: Bad payload"
107107
msgstr ""
108108

109-
#: warehouse/accounts/forms.py:549
109+
#: warehouse/accounts/forms.py:570
110110
msgid "Invalid recovery code."
111111
msgstr ""
112112

113-
#: warehouse/accounts/forms.py:558
113+
#: warehouse/accounts/forms.py:579
114114
msgid "Recovery code has been previously used."
115115
msgstr ""
116116

117-
#: warehouse/accounts/forms.py:588
117+
#: warehouse/accounts/forms.py:609
118118
msgid "The username isn't valid. Try again."
119119
msgstr ""
120120

warehouse/metrics/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# limitations under the License.
1212

1313
from pyramid import events, viewderivers
14+
from pyramid.request import Request
1415
from pyramid_retry import IBeforeRetry
1516

1617
from warehouse.metrics import event_handlers
@@ -21,6 +22,10 @@
2122
__all__ = ["IMetricsService", "NullMetrics", "DataDogMetrics", "includeme"]
2223

2324

25+
def _metrics(request: Request) -> IMetricsService:
26+
return request.find_service(IMetricsService)
27+
28+
2429
def includeme(config):
2530
# Register our metrics service.
2631
metrics_class = config.maybe_dotted(
@@ -38,3 +43,6 @@ def includeme(config):
3843

3944
# Register our view deriver that ensures we get our view timed.
4045
config.add_view_deriver(timing_view, under=viewderivers.INGRESS)
46+
47+
# Add the metrics service to the request.
48+
config.add_request_method(_metrics, name="metrics", reify=True)

0 commit comments

Comments
 (0)