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

Network Error Logging: backend part #55135

Merged
merged 41 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a45a219
add NEL; python part
oioki Aug 21, 2023
5919cf2
add NEL; frontend part
oioki Aug 21, 2023
354637f
hide less useful fields in the Report view
oioki Aug 21, 2023
bf5ef14
hide user-agent from report because we moved it under Headers
oioki Aug 21, 2023
c24aab4
show NEL endpoint in Settings / Client Keys
oioki Aug 22, 2023
5a2fb14
cleanup
oioki Aug 22, 2023
2862d62
add ms to elapsed_time
oioki Aug 22, 2023
8ffdf09
add NEL culprits
oioki Aug 22, 2023
27cc4ae
a couple of ugly hacks to show subtitle for NEL event
oioki Aug 22, 2023
2452fa1
remove auto grouping by digits for hackweek demo purposes
oioki Aug 22, 2023
3217336
separate page for NEL headers setup
oioki Aug 22, 2023
44306e1
add nel help
mdtro Aug 23, 2023
6679c51
add chromium-specific errors
oioki Aug 23, 2023
0905618
Merge branch 'hackweek/nel' into hackweek/mdtro/nel
oioki Aug 23, 2023
bb009d6
fix help
oioki Aug 24, 2023
8b60fe3
merge master here; fix conflict
oioki Sep 26, 2023
58047f3
Merge branch 'master' into hackweek/nel
oioki Sep 27, 2023
71f6237
correctly extract metadata for nel event type
oioki Sep 27, 2023
d2f25f4
temporary hack before relay fixes
oioki Sep 27, 2023
7e498ce
Merge branch 'master' into hackweek/nel
oioki Oct 2, 2023
a2f99cb
quick fix for nel events retrieving
oioki Oct 5, 2023
f57b305
Revert "remove auto grouping by digits for hackweek demo purposes"
oioki Oct 5, 2023
b255875
Merge branch 'master' into hackweek/nel
oioki Oct 5, 2023
65f8bcc
remove frontend changes from this PR
oioki Oct 5, 2023
ca98c92
fix typing
oioki Oct 5, 2023
5608e82
Merge branch 'master' into hackweek/nel
oioki Oct 6, 2023
3b52efa
fix api-docs
oioki Oct 6, 2023
985a207
add NelEventTest
oioki Oct 6, 2023
5f02f2e
add test for get_nel_culprit
oioki Oct 6, 2023
0b57e0d
Merge branch 'master' into hackweek/nel
oioki Oct 16, 2023
1487cab
Merge branch 'master' into hackweek/nel
oioki Oct 17, 2023
703a0c9
align with relay changes
oioki Oct 17, 2023
8b75508
merge master; fix conflict
oioki Nov 1, 2023
6b896b6
fix missing type case; add tests
oioki Nov 3, 2023
a338075
Merge branch 'master' into hackweek/nel
oioki Nov 3, 2023
b007ca9
Merge branch 'master' into hackweek/nel
oioki Nov 6, 2023
664724c
update to sentry-relay==0.8.34; remove temporary hack
oioki Nov 6, 2023
ea74050
Merge branch 'master' into hackweek/nel
oioki Nov 6, 2023
9bb5110
Merge branch 'master' into hackweek/nel
oioki Nov 8, 2023
c879879
Update src/sentry/culprit.py
oioki Nov 9, 2023
0fa2a8d
fix test
oioki Nov 9, 2023
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
5 changes: 4 additions & 1 deletion api-docs/components/schemas/key.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dsn": {
"type": "object",
"required": ["cdn", "csp", "minidump", "public", "secret", "security"],
"required": ["cdn", "csp", "minidump", "nel", "public", "secret", "security"],
"properties": {
"cdn": {
"type": "string"
Expand All @@ -36,6 +36,9 @@
"minidump": {
"type": "string"
},
"nel": {
"type": "string"
},
"public": {
"type": "string"
},
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/api/serializers/models/project_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class DSN(TypedDict):
csp: str
security: str
minidump: str
nel: str
unreal: str
cdn: str

Expand Down Expand Up @@ -85,6 +86,7 @@ def serialize(
"csp": obj.csp_endpoint,
"security": obj.security_endpoint,
"minidump": obj.minidump_endpoint,
"nel": obj.nel_endpoint,
"unreal": obj.unreal_endpoint,
"cdn": obj.js_sdk_loader_cdn_url,
},
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/apidocs/examples/project_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"csp": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/csp-report/?sentry_key=a785682ddda719b7a8a4011110d75598",
"security": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/security/?sentry_key=a785682ddda719b7a8a4011110d75598",
"minidump": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/minidump/?sentry_key=a785682ddda719b7a8a4011110d75598",
"nel": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/nel/?sentry_key=a785682ddda719b7a8a4011110d75598",
"unreal": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/unreal/a785682ddda719b7a8a4011110d75598/",
"cdn": "https://js.sentry-cdn.com/a785682ddda719b7a8a4011110d75598.min.js",
},
Expand Down Expand Up @@ -43,6 +44,7 @@
"csp": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/csp-report/?sentry_key=a785682ddda719b7a8a4011110d75598",
"security": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/security/?sentry_key=a785682ddda719b7a8a4011110d75598",
"minidump": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/minidump/?sentry_key=a785682ddda719b7a8a4011110d75598",
"nel": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/nel/?sentry_key=a785682ddda719b7a8a4011110d75598",
"unreal": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/unreal/a785682ddda719b7a8a4011110d75598/",
"cdn": "https://js.sentry-cdn.com/a785682ddda719b7a8a4011110d75598.min.js",
},
Expand Down
1 change: 1 addition & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1972,6 +1972,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
"hpkp": "sentry.interfaces.security.Hpkp",
"expectct": "sentry.interfaces.security.ExpectCT",
"expectstaple": "sentry.interfaces.security.ExpectStaple",
"nel": "sentry.interfaces.nel.Nel",
"exception": "sentry.interfaces.exception.Exception",
"logentry": "sentry.interfaces.message.Message",
"request": "sentry.interfaces.http.Http",
Expand Down
50 changes: 50 additions & 0 deletions src/sentry/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,56 @@ def from_str(cls, string: str) -> int:
"*/ping",
]


NEL_CULPRITS = {
# https://w3c.github.io/network-error-logging/#predefined-network-error-types
"dns.unreachable": "DNS server is unreachable",
"dns.name_not_resolved": "DNS server responded but is unable to resolve the address",
"dns.failed": "Request to the DNS server failed due to reasons not covered by previous errors",
"dns.address_changed": "Indicates that the resolved IP address for a request's origin has changed since the corresponding NEL policy was received",
"tcp.timed_out": "TCP connection to the server timed out",
"tcp.closed": "The TCP connection was closed by the server",
"tcp.reset": "The TCP connection was reset",
"tcp.refused": "The TCP connection was refused by the server",
"tcp.aborted": "The TCP connection was aborted",
"tcp.address_invalid": "The IP address is invalid",
"tcp.address_unreachable": "The IP address is unreachable",
"tcp.failed": "The TCP connection failed due to reasons not covered by previous errors",
"tls.version_or_cipher_mismatch": "The TLS connection was aborted due to version or cipher mismatch",
"tls.bad_client_auth_cert": "The TLS connection was aborted due to invalid client certificate",
"tls.cert.name_invalid": "The TLS connection was aborted due to invalid name",
"tls.cert.date_invalid": "The TLS connection was aborted due to invalid certificate date",
"tls.cert.authority_invalid": "The TLS connection was aborted due to invalid issuing authority",
"tls.cert.invalid": "The TLS connection was aborted due to invalid certificate",
"tls.cert.revoked": "The TLS connection was aborted due to revoked server certificate",
"tls.cert.pinned_key_not_in_cert_chain": "The TLS connection was aborted due to a key pinning error",
"tls.protocol.error": "The TLS connection was aborted due to a TLS protocol error",
"tls.failed": "The TLS connection failed due to reasons not covered by previous errors",
"http.error": "The user agent successfully received a response, but it had a {} status code",
"http.protocol.error": "The connection was aborted due to an HTTP protocol error",
"http.response.invalid": "Response is empty, has a content-length mismatch, has improper encoding, and/or other conditions that prevent user agent from processing the response",
"http.response.redirect_loop": "The request was aborted due to a detected redirect loop",
"http.failed": "The connection failed due to errors in HTTP protocol not covered by previous errors",
"abandoned": "User aborted the resource fetch before it is complete",
"unknown": "error type is unknown",
# Chromium-specific errors, not documented in the spec
# https://chromium.googlesource.com/chromium/src/+/HEAD/net/network_error_logging/network_error_logging_service.cc
"dns.protocol": "ERR_DNS_MALFORMED_RESPONSE",
"dns.server": "ERR_DNS_SERVER_FAILED",
"tls.unrecognized_name_alert": "ERR_SSL_UNRECOGNIZED_NAME_ALERT",
"h2.ping_failed": "ERR_HTTP2_PING_FAILED",
"h2.protocol.error": "ERR_HTTP2_PROTOCOL_ERROR",
"h3.protocol.error": "ERR_QUIC_PROTOCOL_ERROR",
"http.response.invalid.empty": "ERR_EMPTY_RESPONSE",
"http.response.invalid.content_length_mismatch": "ERR_CONTENT_LENGTH_MISMATCH",
"http.response.invalid.incomplete_chunked_encoding": "ERR_INCOMPLETE_CHUNKED_ENCODING",
"http.response.invalid.invalid_chunked_encoding": "ERR_INVALID_CHUNKED_ENCODING",
"http.request.range_not_satisfiable": "ERR_REQUEST_RANGE_NOT_SATISFIABLE",
"http.response.headers.truncated": "ERR_RESPONSE_HEADERS_TRUNCATED",
"http.response.headers.multiple_content_disposition": "ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION",
"http.response.headers.multiple_content_length": "ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH",
}

# Generated from https://raw.githubusercontent.com/github-linguist/linguist/master/lib/linguist/languages.yml and our list of platforms/languages
EXTENSION_LANGUAGE_MAP = {
"c": "c",
Expand Down
13 changes: 12 additions & 1 deletion src/sentry/culprit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
code that generates it.
"""

from sentry.constants import MAX_CULPRIT_LENGTH
from sentry.constants import MAX_CULPRIT_LENGTH, NEL_CULPRITS
from sentry.utils.safe import get_path
from sentry.utils.strings import truncatechars

Expand All @@ -34,6 +34,9 @@ def generate_culprit(data):
if not culprit and stacktraces:
culprit = get_stacktrace_culprit(get_path(stacktraces, -1), platform=platform)

if not culprit and data.get("nel"):
culprit = get_nel_culprit(data.get("nel"))

if not culprit and data.get("request"):
culprit = get_path(data, "request", "url")

Expand Down Expand Up @@ -69,3 +72,11 @@ def get_frame_culprit(frame, platform):
# to a unicode string if needed.
return "{}({})".format(frame.get("function") or "?", fileloc)
return "{} in {}".format(fileloc, frame.get("function") or "?")


def get_nel_culprit(data_nel):
body = data_nel.get("body")
ty = body.get("type", "<missing>")
if ty == "http.error":
return NEL_CULPRITS[ty].format(body.get("status_code"))
return NEL_CULPRITS.get(ty, ty)
3 changes: 3 additions & 0 deletions src/sentry/eventtypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from sentry.eventtypes.feedback import FeedbackEvent
from sentry.eventtypes.generic import GenericEvent
from sentry.eventtypes.manager import EventTypeManager
from sentry.eventtypes.nel import NelEvent
from sentry.eventtypes.security import CspEvent, ExpectCTEvent, ExpectStapleEvent, HpkpEvent
from sentry.eventtypes.transaction import TransactionEvent

default_manager = EventTypeManager()
default_manager.register(DefaultEvent)
default_manager.register(ErrorEvent)
default_manager.register(CspEvent)
default_manager.register(NelEvent)
default_manager.register(HpkpEvent)
default_manager.register(ExpectCTEvent)
default_manager.register(ExpectStapleEvent)
Expand All @@ -27,6 +29,7 @@
DefaultEvent,
ErrorEvent,
CspEvent,
NelEvent,
HpkpEvent,
ExpectCTEvent,
ExpectStapleEvent,
Expand Down
10 changes: 10 additions & 0 deletions src/sentry/eventtypes/nel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .base import DefaultEvent


class NelEvent(DefaultEvent):
key = "nel"

def extract_metadata(self, data):
metadata = super().extract_metadata(data)
metadata["uri"] = data.get("request").get("url")
return metadata
11 changes: 11 additions & 0 deletions src/sentry/interfaces/nel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__all__ = "Nel"

from sentry.interfaces.base import Interface


class Nel(Interface):
"""
A browser NEL report.
"""

title = "NEL report"
6 changes: 6 additions & 0 deletions src/sentry/models/projectkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ def security_endpoint(self):

return f"{endpoint}/api/{self.project_id}/security/?sentry_key={self.public_key}"

@property
def nel_endpoint(self):
endpoint = self.get_endpoint()

return f"{endpoint}/api/{self.project_id}/nel/?sentry_key={self.public_key}"

@property
def minidump_endpoint(self):
endpoint = self.get_endpoint()
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/utils/canonical.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"request": ("sentry.interfaces.Http",),
"user": ("sentry.interfaces.User",),
"csp": ("sentry.interfaces.Csp",),
"nel": ("sentry.interfaces.Nel",),
"breadcrumbs": ("sentry.interfaces.Breadcrumbs",),
"contexts": ("sentry.interfaces.Contexts",),
"threads": ("sentry.interfaces.Threads",),
Expand All @@ -35,6 +36,7 @@
"sentry.interfaces.Http": ("request",),
"sentry.interfaces.User": ("user",),
"sentry.interfaces.Csp": ("csp",),
"sentry.interfaces.Nel": ("nel",),
"sentry.interfaces.Breadcrumbs": ("breadcrumbs",),
"sentry.interfaces.Contexts": ("contexts",),
"sentry.interfaces.Threads": ("threads",),
Expand Down
17 changes: 17 additions & 0 deletions tests/sentry/event_manager/test_generate_culprit.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,20 @@ def test_truncation():
def test_hash_from_values():
result = hash_from_values(["foo", "bar", "foô"])
assert result == "6d81588029ed4190110b2779ba952a00"


def test_nel_culprit():
data = {"nel": {"body": {"phase": "application", "type": "http.error", "status_code": 418}}}
assert (
generate_culprit(data)
== "The user agent successfully received a response, but it had a 418 status code"
)

data = {"nel": {"body": {"phase": "connection", "type": "tcp.reset"}}}
assert generate_culprit(data) == "The TCP connection was reset"

data = {"nel": {"body": {"phase": "dns", "type": "dns.weird"}}}
assert generate_culprit(data) == "dns.weird"

data = {"nel": {"body": {"phase": "dns"}}}
assert generate_culprit(data) == "<missing>"
17 changes: 17 additions & 0 deletions tests/sentry/eventtypes/test_nel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from sentry.eventtypes.nel import NelEvent
from sentry.testutils.cases import TestCase
from sentry.testutils.silo import region_silo_test


@region_silo_test(stable=True)
class NelEventTest(TestCase):
def test_get_metadata(self):
inst = NelEvent()
data = {
"logentry": {"formatted": "connection / tcp.refused"},
"request": {"url": "https://example.com/"},
}
assert inst.get_metadata(data) == {
"title": "connection / tcp.refused",
"uri": "https://example.com/",
}