From a45a2194b1d07a93cb1cadc623563e9a83605c72 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Mon, 21 Aug 2023 15:49:11 +0200 Subject: [PATCH 01/28] add NEL; python part --- src/sentry/conf/server.py | 1 + src/sentry/eventtypes/__init__.py | 3 +++ src/sentry/eventtypes/nel.py | 5 +++++ src/sentry/interfaces/nel.py | 11 +++++++++++ src/sentry/utils/canonical.py | 2 ++ 5 files changed, 22 insertions(+) create mode 100644 src/sentry/eventtypes/nel.py create mode 100644 src/sentry/interfaces/nel.py diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 2a20e775fa3fe8..0560bd74ccb851 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -1852,6 +1852,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "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", diff --git a/src/sentry/eventtypes/__init__.py b/src/sentry/eventtypes/__init__.py index c370aa787f3da2..3c98227d84f3d9 100644 --- a/src/sentry/eventtypes/__init__.py +++ b/src/sentry/eventtypes/__init__.py @@ -4,6 +4,7 @@ from .error import ErrorEvent from .generic import GenericEvent from .manager import EventTypeManager +from .nel import NelEvent from .security import CspEvent, ExpectCTEvent, ExpectStapleEvent, HpkpEvent from .transaction import TransactionEvent @@ -11,6 +12,7 @@ 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) @@ -24,6 +26,7 @@ DefaultEvent, ErrorEvent, CspEvent, + NelEvent, HpkpEvent, ExpectCTEvent, ExpectStapleEvent, diff --git a/src/sentry/eventtypes/nel.py b/src/sentry/eventtypes/nel.py new file mode 100644 index 00000000000000..a883e1830f76f6 --- /dev/null +++ b/src/sentry/eventtypes/nel.py @@ -0,0 +1,5 @@ +from .base import BaseEvent + + +class NelEvent(BaseEvent): + key = "nel" diff --git a/src/sentry/interfaces/nel.py b/src/sentry/interfaces/nel.py new file mode 100644 index 00000000000000..6a386b93727567 --- /dev/null +++ b/src/sentry/interfaces/nel.py @@ -0,0 +1,11 @@ +__all__ = "Nel" + +from sentry.interfaces.base import Interface + + +class Nel(Interface): + """ + A browser NEL report. + """ + + title = "NEL report" diff --git a/src/sentry/utils/canonical.py b/src/sentry/utils/canonical.py index fc75e92783f5d5..3dfed1d1efc36a 100644 --- a/src/sentry/utils/canonical.py +++ b/src/sentry/utils/canonical.py @@ -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",), @@ -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",), From 5919cf2d62cdade53b3b2451065806ad93adb9b0 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Mon, 21 Aug 2023 16:02:06 +0200 Subject: [PATCH 02/28] add NEL; frontend part --- static/app/components/events/eventEntry.tsx | 4 ++ .../events/interfaces/nel/index.tsx | 71 +++++++++++++++++++ static/app/types/event.tsx | 7 ++ .../groupEventDetailsContent.tsx | 1 + 4 files changed, 83 insertions(+) create mode 100644 static/app/components/events/interfaces/nel/index.tsx diff --git a/static/app/components/events/eventEntry.tsx b/static/app/components/events/eventEntry.tsx index 0452ac58a3f70e..a5f8c82ea6629c 100644 --- a/static/app/components/events/eventEntry.tsx +++ b/static/app/components/events/eventEntry.tsx @@ -16,6 +16,7 @@ import {DebugMeta} from './interfaces/debugMeta'; import {Exception} from './interfaces/exception'; import {Generic} from './interfaces/generic'; import {Message} from './interfaces/message'; +import {Nel} from './interfaces/nel'; import {SpanEvidenceSection} from './interfaces/performance/spanEvidence'; import {Request} from './interfaces/request'; import {Spans} from './interfaces/spans'; @@ -81,6 +82,9 @@ function EventEntryContent({ case EntryType.CSP: return ; + case EntryType.NEL: + return ; + case EntryType.EXPECTCT: case EntryType.EXPECTSTAPLE: const {data, type} = entry; diff --git a/static/app/components/events/interfaces/nel/index.tsx b/static/app/components/events/interfaces/nel/index.tsx new file mode 100644 index 00000000000000..98dc995932fc37 --- /dev/null +++ b/static/app/components/events/interfaces/nel/index.tsx @@ -0,0 +1,71 @@ +import {useState} from 'react'; + +import {EventDataSection} from 'sentry/components/events/eventDataSection'; +import KeyValueList from 'sentry/components/events/interfaces/keyValueList'; +import {SegmentedControl} from 'sentry/components/segmentedControl'; +import {t} from 'sentry/locale'; +import {EntryType, Event} from 'sentry/types/event'; + +// import Help, {HelpProps} from './help'; + +type View = 'report' | 'raw' | 'help'; + +function getView(view: View, data: Record, meta: Record) { + switch (view) { + case 'report': + return ( + { + return { + key, + subject: key, + value, + meta: meta?.[key]?.[''], + }; + })} + isContextData + /> + ); + case 'raw': + return
{JSON.stringify(data, null, 2)}
; + // case 'help': + // return ; + default: + throw new TypeError(`Invalid view: ${view}`); + } +} + +type Props = { + data: Record; + event: Event; +}; + +export function Nel({data, event}: Props) { + const [view, setView] = useState('report'); + + const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.NEL); + const meta = event._meta?.entries?.[entryIndex]?.data; + + const cleanData = + data.original_policy !== 'string' + ? data + : { + ...data, + // Hide the report-uri since this is redundant and silly + original_policy: data.original_policy.replace(/(;\s+)?report-uri [^;]+/, ''), + }; + + const actions = ( + + {t('Report')} + {t('Raw')} + {t('Help')} + + ); + + return ( + + {getView(view, cleanData, meta)} + + ); +} diff --git a/static/app/types/event.tsx b/static/app/types/event.tsx index cdbd4b307548df..edd26d5e7cb873 100644 --- a/static/app/types/event.tsx +++ b/static/app/types/event.tsx @@ -277,6 +277,7 @@ export enum EntryType { STACKTRACE = 'stacktrace', TEMPLATE = 'template', CSP = 'csp', + NEL = 'nel', EXPECTCT = 'expectct', EXPECTSTAPLE = 'expectstaple', HPKP = 'hpkp', @@ -375,6 +376,11 @@ type EntryCsp = { type: EntryType.CSP; }; +type EntryNel = { + data: Record; + type: EntryType.NEL; +}; + type EntryGeneric = { data: Record; type: EntryType.EXPECTCT | EntryType.EXPECTSTAPLE | EntryType.HPKP; @@ -396,6 +402,7 @@ export type Entry = | EntryRequest | EntryTemplate | EntryCsp + | EntryNel | EntryGeneric | EntryResources; diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx index 4f6e81b1ccefe3..a75f574ae44097 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx @@ -128,6 +128,7 @@ function GroupEventDetailsContent({ + From 354637f2b3f58caed551c5ccca403328ea24bcea Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Mon, 21 Aug 2023 16:43:28 +0200 Subject: [PATCH 03/28] hide less useful fields in the Report view --- .../events/interfaces/nel/index.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/static/app/components/events/interfaces/nel/index.tsx b/static/app/components/events/interfaces/nel/index.tsx index 98dc995932fc37..aca9059e87d3a5 100644 --- a/static/app/components/events/interfaces/nel/index.tsx +++ b/static/app/components/events/interfaces/nel/index.tsx @@ -13,9 +13,13 @@ type View = 'report' | 'raw' | 'help'; function getView(view: View, data: Record, meta: Record) { switch (view) { case 'report': + const viewData = data.body; + viewData.user_agent = data.user_agent; + viewData.url = data.url; + return ( { + data={Object.entries(viewData).map(([key, value]) => { return { key, subject: key, @@ -46,14 +50,13 @@ export function Nel({data, event}: Props) { const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.NEL); const meta = event._meta?.entries?.[entryIndex]?.data; - const cleanData = - data.original_policy !== 'string' - ? data - : { - ...data, - // Hide the report-uri since this is redundant and silly - original_policy: data.original_policy.replace(/(;\s+)?report-uri [^;]+/, ''), - }; + const viewData = {...data}; + viewData.body = {...data.body}; + if (view === 'report') { + delete viewData.age; + delete viewData.body.sampling_fraction; + delete viewData.ty; + } const actions = ( @@ -65,7 +68,7 @@ export function Nel({data, event}: Props) { return ( - {getView(view, cleanData, meta)} + {getView(view, viewData, meta)} ); } From bf5ef149ea865fcb8956123ebd0c410ff0bec94a Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Mon, 21 Aug 2023 21:22:08 +0200 Subject: [PATCH 04/28] hide user-agent from report because we moved it under Headers --- static/app/components/events/interfaces/nel/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/components/events/interfaces/nel/index.tsx b/static/app/components/events/interfaces/nel/index.tsx index aca9059e87d3a5..45d42f990c0741 100644 --- a/static/app/components/events/interfaces/nel/index.tsx +++ b/static/app/components/events/interfaces/nel/index.tsx @@ -14,7 +14,6 @@ function getView(view: View, data: Record, meta: Record) { switch (view) { case 'report': const viewData = data.body; - viewData.user_agent = data.user_agent; viewData.url = data.url; return ( From c24aab4f6886015e55154eacebc82b741183eb7f Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 22 Aug 2023 06:50:23 +0200 Subject: [PATCH 05/28] show NEL endpoint in Settings / Client Keys --- .../api/serializers/models/project_key.py | 1 + src/sentry/models/projectkey.py | 6 +++++ static/app/types/project.tsx | 1 + .../projectKeys/projectKeyCredentials.tsx | 27 +++++++++++++++++++ .../settings/project/projectKeys/types.tsx | 1 + 5 files changed, 36 insertions(+) diff --git a/src/sentry/api/serializers/models/project_key.py b/src/sentry/api/serializers/models/project_key.py index c4f80ac86c3e24..51e736a39927c4 100644 --- a/src/sentry/api/serializers/models/project_key.py +++ b/src/sentry/api/serializers/models/project_key.py @@ -85,6 +85,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, }, diff --git a/src/sentry/models/projectkey.py b/src/sentry/models/projectkey.py index 2e4162d30789b6..c0093011694dbd 100644 --- a/src/sentry/models/projectkey.py +++ b/src/sentry/models/projectkey.py @@ -205,6 +205,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() diff --git a/static/app/types/project.tsx b/static/app/types/project.tsx index 55ccd9cb2bc77a..e4801212069090 100644 --- a/static/app/types/project.tsx +++ b/static/app/types/project.tsx @@ -77,6 +77,7 @@ export type ProjectKey = { cdn: string; csp: string; minidump: string; + nel: string; public: string; secret: string; security: string; diff --git a/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx b/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx index 05290b3902827f..87be2e49fc713c 100644 --- a/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx +++ b/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx @@ -16,6 +16,7 @@ type Props = { showDsn?: boolean; showDsnPublic?: boolean; showMinidump?: boolean; + showNel?: boolean; showProjectId?: boolean; showPublicKey?: boolean; showSecretKey?: boolean; @@ -29,6 +30,7 @@ function ProjectKeyCredentials({ showDsn = true, showDsnPublic = true, showMinidump = true, + showNel = true, showProjectId = false, showPublicKey = false, showSecretKey = false, @@ -97,6 +99,31 @@ function ProjectKeyCredentials({ )} + {showNel && ( + + Network Error Logging reports + + ), + } + )} + inline={false} + flexibleControlStateSize + > + + {getDynamicText({ + value: data.dsn.nel, + fixed: '__MINIDUMP_ENDPOINT__', + })} + + + )} + {showSecurityEndpoint && ( Date: Tue, 22 Aug 2023 06:54:54 +0200 Subject: [PATCH 06/28] cleanup --- .../projectKeys/projectKeyCredentials.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx b/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx index 87be2e49fc713c..7144f2e3f073ac 100644 --- a/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx +++ b/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx @@ -102,16 +102,13 @@ function ProjectKeyCredentials({ {showNel && ( - Network Error Logging reports - - ), - } - )} + help={tct('Use this endpoint to upload [link].', { + link: ( + + Network Error Logging reports + + ), + })} inline={false} flexibleControlStateSize > From 2862d6268d9073367db736070c753ff04e336a8a Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 22 Aug 2023 14:14:58 +0200 Subject: [PATCH 07/28] add ms to elapsed_time --- static/app/components/events/interfaces/nel/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/components/events/interfaces/nel/index.tsx b/static/app/components/events/interfaces/nel/index.tsx index 45d42f990c0741..0b2af2b300b602 100644 --- a/static/app/components/events/interfaces/nel/index.tsx +++ b/static/app/components/events/interfaces/nel/index.tsx @@ -15,6 +15,7 @@ function getView(view: View, data: Record, meta: Record) { case 'report': const viewData = data.body; viewData.url = data.url; + viewData.elapsed_time = viewData.elapsed_time + ' ms'; return ( Date: Tue, 22 Aug 2023 14:40:22 +0200 Subject: [PATCH 08/28] add NEL culprits --- src/sentry/constants.py | 34 ++++++++++++++++++++++++++++++++++ src/sentry/culprit.py | 13 ++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/sentry/constants.py b/src/sentry/constants.py index bb12d26b2546ab..e6ea1825e19189 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -701,3 +701,37 @@ def from_str(cls, string: str) -> Optional[int]: "*/healthz", "*/ping", ] + + +# https://w3c.github.io/network-error-logging/#predefined-network-error-types +NEL_CULPRITS = { + "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", +} diff --git a/src/sentry/culprit.py b/src/sentry/culprit.py index 0bc567ea3a95af..0d150adf453430 100644 --- a/src/sentry/culprit.py +++ b/src/sentry/culprit.py @@ -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 @@ -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") @@ -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") + if ty == "http.error": + return NEL_CULPRITS[ty].format(body.get("status_code")) + return NEL_CULPRITS[ty] From 27cc4aed5eba2ca07a296d04b92aac0fe39a95f5 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 22 Aug 2023 15:48:00 +0200 Subject: [PATCH 09/28] a couple of ugly hacks to show subtitle for NEL event --- src/sentry/eventtypes/base.py | 8 +++++++- static/app/utils/events.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sentry/eventtypes/base.py b/src/sentry/eventtypes/base.py index 8c3091d49dc486..d1d4c7768d0242 100644 --- a/src/sentry/eventtypes/base.py +++ b/src/sentry/eventtypes/base.py @@ -97,4 +97,10 @@ def extract_metadata(self, data): else: title = "" - return {"title": title} + metadata = {"title": title} + + # ugly hack for NEL hackweek + if data.get("nel"): + metadata["uri"] = data.get("nel").get("url") + + return metadata diff --git a/static/app/utils/events.tsx b/static/app/utils/events.tsx index 92f4968e521f42..c9b54fbde88090 100644 --- a/static/app/utils/events.tsx +++ b/static/app/utils/events.tsx @@ -176,7 +176,7 @@ export function getTitle( case EventOrGroupType.DEFAULT: return { title: customTitle ?? metadata.title ?? '', - subtitle: '', + subtitle: metadata.uri ?? '', treeLabel: undefined, }; case EventOrGroupType.TRANSACTION: From 2452fa111c81bb2988fd9d66dea492a068251883 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 22 Aug 2023 16:55:16 +0200 Subject: [PATCH 10/28] remove auto grouping by digits for hackweek demo purposes --- src/sentry/grouping/strategies/message.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sentry/grouping/strategies/message.py b/src/sentry/grouping/strategies/message.py index 46e36d10b4c8f6..652a527498e27b 100644 --- a/src/sentry/grouping/strategies/message.py +++ b/src/sentry/grouping/strategies/message.py @@ -93,10 +93,6 @@ -\d+\.\d+\b | \b\d+\.\d+\b ) | - (?P - -\d+\b | - \b\d+\b - ) | (?P # The `=` here guarantees we'll only match the value half of key-value pairs, # rather than all quoted strings From 321733688841e7fec114dfe56b56bcc3508429f3 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 22 Aug 2023 18:27:37 +0200 Subject: [PATCH 11/28] separate page for NEL headers setup --- static/app/routes.tsx | 7 ++ .../project/navigationConfiguration.tsx | 4 + .../settings/project/networkErrorLogging.tsx | 91 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 static/app/views/settings/project/networkErrorLogging.tsx diff --git a/static/app/routes.tsx b/static/app/routes.tsx index f13cb18077486b..d62750c19e13cc 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -604,6 +604,13 @@ function buildRoutes() { () => import('sentry/views/settings/project/projectUserFeedback') )} /> + import('sentry/views/settings/project/networkErrorLogging') + )} + /> & { + organization: Organization; +}; + +type State = { + keyList: null | ProjectKey[]; +} & DeprecatedAsyncView['state']; + +class ProjectNelReports extends DeprecatedAsyncView { + getEndpoints(): ReturnType { + const {organization} = this.props; + const {projectId} = this.props.params; + return [['keyList', `/projects/${organization.slug}/${projectId}/keys/`]]; + } + + getTitle() { + const {projectId} = this.props.params; + return routeTitleGen(t('Network Error Logging (NEL)'), projectId, false); + } + + getInstructions(keyList: ProjectKey[]) { + return ( + 'NEL: {"report_to":"nel","include_subdomains":true,"max_age":86400,"success_fraction":0.0,"failure_fraction":1.0}\n' + + 'Report-To: {"group":"nel","max_age":86400,"endpoints":[{"url":"' + + keyList[0].dsn.nel + + '"}]}' + ); + } + + renderBody() { + const {keyList} = this.state; + if (!keyList) { + return null; + } + + return ( +
+ + + + + + {t('About')} + +

+ {tct( + `[link:Network Error Logging] + (NEL) is a mechanism that can be configured via the NEL HTTP response header. This experimental header allows websites and applications to opt-in to receive reports about failed (and, if desired, successful) network fetches from supporting browsers.`, + { + link: ( + + ), + } + )} +

+

+ To configure reports in Sentry, you'll need to configure the headers from + your server: +

+ +
{this.getInstructions(keyList)}
+ +

+ {tct('For more information, see [link:the article on MDN].', { + link: ( + + ), + })} +

+
+
+
+ ); + } +} + +export default withOrganization(ProjectNelReports); From 44306e1dd926caa2278ae4fa33196c6e728c5283 Mon Sep 17 00:00:00 2001 From: mdtro Date: Wed, 23 Aug 2023 10:56:53 -0500 Subject: [PATCH 12/28] add nel help --- .../events/interfaces/nel/help/index.tsx | 73 +++++++++++++++++++ .../interfaces/nel/help/nelProperties.tsx | 40 ++++++++++ 2 files changed, 113 insertions(+) create mode 100644 static/app/components/events/interfaces/nel/help/index.tsx create mode 100644 static/app/components/events/interfaces/nel/help/nelProperties.tsx diff --git a/static/app/components/events/interfaces/nel/help/index.tsx b/static/app/components/events/interfaces/nel/help/index.tsx new file mode 100644 index 00000000000000..6d82f156d5690f --- /dev/null +++ b/static/app/components/events/interfaces/nel/help/index.tsx @@ -0,0 +1,73 @@ +import styled from '@emotion/styled'; + +import ExternalLink from 'sentry/components/links/externalLink'; +import {IconOpen} from 'sentry/icons'; +import {space} from 'sentry/styles/space'; + +import nelProperties from './nelProperties'; + +type NelProperty = keyof typeof nelProperties; + +const linkOverrides = {'script-src': 'script-src_2'}; + +export type HelpProps = { + data: { + property: NelProperty; + }; +}; + +function NELHelp({data: {property: key}}: HelpProps) { + const getHelp = () => ({ + __html: nelProperties[key], + }); + + const getLinkHref = () => { + const baseLink = + 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging#error_reports'; + + if (key in linkOverrides) { + return `${baseLink}${linkOverrides[key]}`; + } + + return `${baseLink}${key}`; + }; + + const getLink = () => { + const href = getLinkHref(); + + return ( + + {'developer.mozilla.org'} + + + ); + }; + + return ( +
+

+ {key} +

+
+ + {'\u2014 MDN ('} + {getLink()} + {')'} + +
+ ); +} + +export default NELHelp; + +const StyledP = styled('p')` + text-align: right; + display: grid; + grid-template-columns: repeat(3, max-content); + gap: ${space(0.25)}; +`; + +const StyledExternalLink = styled(ExternalLink)` + display: inline-flex; + align-items: center; +`; diff --git a/static/app/components/events/interfaces/nel/help/nelProperties.tsx b/static/app/components/events/interfaces/nel/help/nelProperties.tsx new file mode 100644 index 00000000000000..623ce6d07d0424 --- /dev/null +++ b/static/app/components/events/interfaces/nel/help/nelProperties.tsx @@ -0,0 +1,40 @@ +import {t} from 'sentry/locale'; + +const nelProperties = { + referrer: t( + `request's referrer, as determined by the referrer policy associated with its client.` + ), + sampling_fraction: t(`sampling rate`), + server_ip: t( + `The IP address of the server to which the user agent sent the request, if available. + Otherwise, an empty string. +
    +
  • A host identified by an IPv4 address is represeented in dotted-decimal notation (a sequence of fource decimal numbers in the range 0 to 255, separated by ".").
  • +
  • A host identified by an IPv6 address is represented as an ordered list of eight 16-bit pieces (a sequence of x:x:x:x:x:x:x:x, where the 'x's are one to four hexadecimal digits of the eight 16-bit pieces of the address).
  • +
` + ), + protocol: t( + `The network protocol used to fetch the resource as identified by the ALPN Protocl ID, if available. Otherwise, "".` + ), + method: t(`request's request method.`), + requeset_headers: t( + `The result of executing 5.2 Extract request headers on request and policy.` + ), + response_headers: t( + `The result of executing 5.3 Extract response headers on response and policy.` + ), + status_code: t( + `The status code of the HTTP response, if available. Otheriwise, 0.` + ), + elapsed_time: t( + `The elapsed number of milliseconds between the start of the resource fetch and when it was completed or aborted by the user agent.` + ), + phase: t( + `If request failed, the phase of its network error. If request succeeded, "application".` + ), + type: t( + `If request failed, the type of its network error. If request succeeded, "ok".` + ), +}; + +export default nelProperties; From 6679c51cf544282ac2a74b64d29d257c53ea2f6e Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Wed, 23 Aug 2023 20:34:55 +0200 Subject: [PATCH 13/28] add chromium-specific errors --- src/sentry/constants.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/sentry/constants.py b/src/sentry/constants.py index e6ea1825e19189..ce614c8286fc8b 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -703,8 +703,8 @@ def from_str(cls, string: str) -> Optional[int]: ] -# https://w3c.github.io/network-error-logging/#predefined-network-error-types 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", @@ -734,4 +734,20 @@ def from_str(cls, string: str) -> Optional[int]: "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", } From bb009d6141351d0657666259205373fce2f029ff Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Thu, 24 Aug 2023 07:50:08 +0200 Subject: [PATCH 14/28] fix help --- .../events/interfaces/nel/help/index.tsx | 42 ++++++------------- .../interfaces/nel/help/nelProperties.tsx | 15 +++---- .../events/interfaces/nel/index.tsx | 16 +++---- 3 files changed, 23 insertions(+), 50 deletions(-) diff --git a/static/app/components/events/interfaces/nel/help/index.tsx b/static/app/components/events/interfaces/nel/help/index.tsx index 6d82f156d5690f..7fcd1cb4cdfbac 100644 --- a/static/app/components/events/interfaces/nel/help/index.tsx +++ b/static/app/components/events/interfaces/nel/help/index.tsx @@ -6,34 +6,10 @@ import {space} from 'sentry/styles/space'; import nelProperties from './nelProperties'; -type NelProperty = keyof typeof nelProperties; - -const linkOverrides = {'script-src': 'script-src_2'}; - -export type HelpProps = { - data: { - property: NelProperty; - }; -}; - -function NELHelp({data: {property: key}}: HelpProps) { - const getHelp = () => ({ - __html: nelProperties[key], - }); - - const getLinkHref = () => { - const baseLink = - 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging#error_reports'; - - if (key in linkOverrides) { - return `${baseLink}${linkOverrides[key]}`; - } - - return `${baseLink}${key}`; - }; - +function NELHelp({data}) { const getLink = () => { - const href = getLinkHref(); + const href = + 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging#error_reports'; return ( @@ -43,14 +19,20 @@ function NELHelp({data: {property: key}}: HelpProps) { ); }; + let output = ''; + for (const [nelProperty, nelExplanation] of Object.entries(nelProperties)) { + output += `${nelProperty}: ${nelExplanation}
`; + } + return (

- {key} + {data.message}

-
+
{data.culprit}
+
- {'\u2014 MDN ('} + {'\u2023 MDN ('} {getLink()} {')'} diff --git a/static/app/components/events/interfaces/nel/help/nelProperties.tsx b/static/app/components/events/interfaces/nel/help/nelProperties.tsx index 623ce6d07d0424..82d0fab9ea85cc 100644 --- a/static/app/components/events/interfaces/nel/help/nelProperties.tsx +++ b/static/app/components/events/interfaces/nel/help/nelProperties.tsx @@ -2,29 +2,24 @@ import {t} from 'sentry/locale'; const nelProperties = { referrer: t( - `request's referrer, as determined by the referrer policy associated with its client.` + `Request's referrer, as determined by the referrer policy associated with its client.` ), sampling_fraction: t(`sampling rate`), server_ip: t( - `The IP address of the server to which the user agent sent the request, if available. - Otherwise, an empty string. -
    -
  • A host identified by an IPv4 address is represeented in dotted-decimal notation (a sequence of fource decimal numbers in the range 0 to 255, separated by ".").
  • -
  • A host identified by an IPv6 address is represented as an ordered list of eight 16-bit pieces (a sequence of x:x:x:x:x:x:x:x, where the 'x's are one to four hexadecimal digits of the eight 16-bit pieces of the address).
  • -
` + `The IP address of the server to which the user agent sent the request, if available. Otherwise, an empty string.` ), protocol: t( - `The network protocol used to fetch the resource as identified by the ALPN Protocl ID, if available. Otherwise, "".` + `The network protocol used to fetch the resource as identified by the ALPN Protocol ID, if available. Otherwise, "".` ), method: t(`request's request method.`), - requeset_headers: t( + request_headers: t( `The result of executing 5.2 Extract request headers on request and policy.` ), response_headers: t( `The result of executing 5.3 Extract response headers on response and policy.` ), status_code: t( - `The status code of the HTTP response, if available. Otheriwise, 0.` + `The status code of the HTTP response, if available. Otherwise, 0.` ), elapsed_time: t( `The elapsed number of milliseconds between the start of the resource fetch and when it was completed or aborted by the user agent.` diff --git a/static/app/components/events/interfaces/nel/index.tsx b/static/app/components/events/interfaces/nel/index.tsx index 0b2af2b300b602..0ffa198212a8cf 100644 --- a/static/app/components/events/interfaces/nel/index.tsx +++ b/static/app/components/events/interfaces/nel/index.tsx @@ -4,13 +4,13 @@ import {EventDataSection} from 'sentry/components/events/eventDataSection'; import KeyValueList from 'sentry/components/events/interfaces/keyValueList'; import {SegmentedControl} from 'sentry/components/segmentedControl'; import {t} from 'sentry/locale'; -import {EntryType, Event} from 'sentry/types/event'; +import {Event} from 'sentry/types/event'; -// import Help, {HelpProps} from './help'; +import Help from './help'; type View = 'report' | 'raw' | 'help'; -function getView(view: View, data: Record, meta: Record) { +function getView(view: View, data: Record, event: Record) { switch (view) { case 'report': const viewData = data.body; @@ -24,7 +24,6 @@ function getView(view: View, data: Record, meta: Record) { key, subject: key, value, - meta: meta?.[key]?.[''], }; })} isContextData @@ -32,8 +31,8 @@ function getView(view: View, data: Record, meta: Record) { ); case 'raw': return
{JSON.stringify(data, null, 2)}
; - // case 'help': - // return ; + case 'help': + return ; default: throw new TypeError(`Invalid view: ${view}`); } @@ -47,9 +46,6 @@ type Props = { export function Nel({data, event}: Props) { const [view, setView] = useState('report'); - const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.NEL); - const meta = event._meta?.entries?.[entryIndex]?.data; - const viewData = {...data}; viewData.body = {...data.body}; if (view === 'report') { @@ -68,7 +64,7 @@ export function Nel({data, event}: Props) { return ( - {getView(view, viewData, meta)} + {getView(view, viewData, event)} ); } From 71f623743f84c9bd59df652034011a719fccbc76 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Wed, 27 Sep 2023 14:43:02 +0200 Subject: [PATCH 15/28] correctly extract metadata for nel event type --- src/sentry/eventtypes/base.py | 8 +------- src/sentry/eventtypes/nel.py | 9 +++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/sentry/eventtypes/base.py b/src/sentry/eventtypes/base.py index d1d4c7768d0242..8c3091d49dc486 100644 --- a/src/sentry/eventtypes/base.py +++ b/src/sentry/eventtypes/base.py @@ -97,10 +97,4 @@ def extract_metadata(self, data): else: title = "" - metadata = {"title": title} - - # ugly hack for NEL hackweek - if data.get("nel"): - metadata["uri"] = data.get("nel").get("url") - - return metadata + return {"title": title} diff --git a/src/sentry/eventtypes/nel.py b/src/sentry/eventtypes/nel.py index a883e1830f76f6..f4caa8a24b12cc 100644 --- a/src/sentry/eventtypes/nel.py +++ b/src/sentry/eventtypes/nel.py @@ -1,5 +1,10 @@ -from .base import BaseEvent +from .base import DefaultEvent -class NelEvent(BaseEvent): +class NelEvent(DefaultEvent): key = "nel" + + def extract_metadata(self, data): + metadata = super().extract_metadata(data) + metadata["uri"] = data.get("nel").get("url") + return metadata From d2f25f4fe80237c0ca6cf00fe8738934b54fb292 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Wed, 27 Sep 2023 14:43:44 +0200 Subject: [PATCH 16/28] temporary hack before relay fixes --- src/sentry/event_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 4c31eaf6e75a35..c1b937c37449fc 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -1478,6 +1478,9 @@ def _get_event_user_impl( def get_event_type(data: Mapping[str, Any]) -> EventType: + # FIXME: shortcut to skip relay modifications (for now) + if data.get("nel"): + return eventtypes.get("nel")() return eventtypes.get(data.get("type", "default"))() From a2f99cbe1d7b731e30dccf44f6e03fbb91c95fa0 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Thu, 5 Oct 2023 15:04:36 +0200 Subject: [PATCH 17/28] quick fix for nel events retrieving --- src/sentry/event_manager.py | 3 --- src/sentry/models/event.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index ed6426df048585..fe4ce74b523c47 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -1478,9 +1478,6 @@ def _get_event_user_impl( def get_event_type(data: Mapping[str, Any]) -> EventType: - # FIXME: shortcut to skip relay modifications (for now) - if data.get("nel"): - return eventtypes.get("nel")() return eventtypes.get(data.get("type", "default"))() diff --git a/src/sentry/models/event.py b/src/sentry/models/event.py index 01fbc4d9a7efc4..54e8a67883549a 100644 --- a/src/sentry/models/event.py +++ b/src/sentry/models/event.py @@ -27,7 +27,7 @@ def __init__(self, data, skip_renormalization=False, **kwargs): # XXX: This is a hack to make generic events work (for now?). I'm not sure whether we # should include this in the rust normalizer, since we don't want people sending us # these via the sdk. - if pre_normalize_type == "generic": + if pre_normalize_type in ["generic", "nel"]: data["type"] = pre_normalize_type CanonicalKeyDict.__init__(self, data, **kwargs) From f57b305e82499c1fb315fd36213f5c7a581fc916 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Thu, 5 Oct 2023 15:11:42 +0200 Subject: [PATCH 18/28] Revert "remove auto grouping by digits for hackweek demo purposes" This reverts commit 2452fa111c81bb2988fd9d66dea492a068251883. --- src/sentry/grouping/strategies/message.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry/grouping/strategies/message.py b/src/sentry/grouping/strategies/message.py index 652a527498e27b..46e36d10b4c8f6 100644 --- a/src/sentry/grouping/strategies/message.py +++ b/src/sentry/grouping/strategies/message.py @@ -93,6 +93,10 @@ -\d+\.\d+\b | \b\d+\.\d+\b ) | + (?P + -\d+\b | + \b\d+\b + ) | (?P # The `=` here guarantees we'll only match the value half of key-value pairs, # rather than all quoted strings From 65f8bcc8b2ae07989ce7270cc165cede2e8982f9 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Thu, 5 Oct 2023 15:27:38 +0200 Subject: [PATCH 19/28] remove frontend changes from this PR --- static/app/components/events/eventEntry.tsx | 4 - .../events/interfaces/nel/help/index.tsx | 55 ----------- .../interfaces/nel/help/nelProperties.tsx | 35 ------- .../events/interfaces/nel/index.tsx | 70 -------------- static/app/routes.tsx | 7 -- static/app/types/event.tsx | 7 -- static/app/types/project.tsx | 1 - static/app/utils/events.tsx | 2 +- .../groupEventDetailsContent.tsx | 1 - .../project/navigationConfiguration.tsx | 4 - .../settings/project/networkErrorLogging.tsx | 91 ------------------- .../projectKeys/projectKeyCredentials.tsx | 24 ----- .../settings/project/projectKeys/types.tsx | 1 - 13 files changed, 1 insertion(+), 301 deletions(-) delete mode 100644 static/app/components/events/interfaces/nel/help/index.tsx delete mode 100644 static/app/components/events/interfaces/nel/help/nelProperties.tsx delete mode 100644 static/app/components/events/interfaces/nel/index.tsx delete mode 100644 static/app/views/settings/project/networkErrorLogging.tsx diff --git a/static/app/components/events/eventEntry.tsx b/static/app/components/events/eventEntry.tsx index a5f8c82ea6629c..0452ac58a3f70e 100644 --- a/static/app/components/events/eventEntry.tsx +++ b/static/app/components/events/eventEntry.tsx @@ -16,7 +16,6 @@ import {DebugMeta} from './interfaces/debugMeta'; import {Exception} from './interfaces/exception'; import {Generic} from './interfaces/generic'; import {Message} from './interfaces/message'; -import {Nel} from './interfaces/nel'; import {SpanEvidenceSection} from './interfaces/performance/spanEvidence'; import {Request} from './interfaces/request'; import {Spans} from './interfaces/spans'; @@ -82,9 +81,6 @@ function EventEntryContent({ case EntryType.CSP: return ; - case EntryType.NEL: - return ; - case EntryType.EXPECTCT: case EntryType.EXPECTSTAPLE: const {data, type} = entry; diff --git a/static/app/components/events/interfaces/nel/help/index.tsx b/static/app/components/events/interfaces/nel/help/index.tsx deleted file mode 100644 index 7fcd1cb4cdfbac..00000000000000 --- a/static/app/components/events/interfaces/nel/help/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import styled from '@emotion/styled'; - -import ExternalLink from 'sentry/components/links/externalLink'; -import {IconOpen} from 'sentry/icons'; -import {space} from 'sentry/styles/space'; - -import nelProperties from './nelProperties'; - -function NELHelp({data}) { - const getLink = () => { - const href = - 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging#error_reports'; - - return ( - - {'developer.mozilla.org'} - - - ); - }; - - let output = ''; - for (const [nelProperty, nelExplanation] of Object.entries(nelProperties)) { - output += `${nelProperty}: ${nelExplanation}
`; - } - - return ( -
-

- {data.message} -

-
{data.culprit}
-
- - {'\u2023 MDN ('} - {getLink()} - {')'} - -
- ); -} - -export default NELHelp; - -const StyledP = styled('p')` - text-align: right; - display: grid; - grid-template-columns: repeat(3, max-content); - gap: ${space(0.25)}; -`; - -const StyledExternalLink = styled(ExternalLink)` - display: inline-flex; - align-items: center; -`; diff --git a/static/app/components/events/interfaces/nel/help/nelProperties.tsx b/static/app/components/events/interfaces/nel/help/nelProperties.tsx deleted file mode 100644 index 82d0fab9ea85cc..00000000000000 --- a/static/app/components/events/interfaces/nel/help/nelProperties.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import {t} from 'sentry/locale'; - -const nelProperties = { - referrer: t( - `Request's referrer, as determined by the referrer policy associated with its client.` - ), - sampling_fraction: t(`sampling rate`), - server_ip: t( - `The IP address of the server to which the user agent sent the request, if available. Otherwise, an empty string.` - ), - protocol: t( - `The network protocol used to fetch the resource as identified by the ALPN Protocol ID, if available. Otherwise, "".` - ), - method: t(`request's request method.`), - request_headers: t( - `The result of executing 5.2 Extract request headers on request and policy.` - ), - response_headers: t( - `The result of executing 5.3 Extract response headers on response and policy.` - ), - status_code: t( - `The status code of the HTTP response, if available. Otherwise, 0.` - ), - elapsed_time: t( - `The elapsed number of milliseconds between the start of the resource fetch and when it was completed or aborted by the user agent.` - ), - phase: t( - `If request failed, the phase of its network error. If request succeeded, "application".` - ), - type: t( - `If request failed, the type of its network error. If request succeeded, "ok".` - ), -}; - -export default nelProperties; diff --git a/static/app/components/events/interfaces/nel/index.tsx b/static/app/components/events/interfaces/nel/index.tsx deleted file mode 100644 index 0ffa198212a8cf..00000000000000 --- a/static/app/components/events/interfaces/nel/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import {useState} from 'react'; - -import {EventDataSection} from 'sentry/components/events/eventDataSection'; -import KeyValueList from 'sentry/components/events/interfaces/keyValueList'; -import {SegmentedControl} from 'sentry/components/segmentedControl'; -import {t} from 'sentry/locale'; -import {Event} from 'sentry/types/event'; - -import Help from './help'; - -type View = 'report' | 'raw' | 'help'; - -function getView(view: View, data: Record, event: Record) { - switch (view) { - case 'report': - const viewData = data.body; - viewData.url = data.url; - viewData.elapsed_time = viewData.elapsed_time + ' ms'; - - return ( - { - return { - key, - subject: key, - value, - }; - })} - isContextData - /> - ); - case 'raw': - return
{JSON.stringify(data, null, 2)}
; - case 'help': - return ; - default: - throw new TypeError(`Invalid view: ${view}`); - } -} - -type Props = { - data: Record; - event: Event; -}; - -export function Nel({data, event}: Props) { - const [view, setView] = useState('report'); - - const viewData = {...data}; - viewData.body = {...data.body}; - if (view === 'report') { - delete viewData.age; - delete viewData.body.sampling_fraction; - delete viewData.ty; - } - - const actions = ( - - {t('Report')} - {t('Raw')} - {t('Help')} - - ); - - return ( - - {getView(view, viewData, event)} - - ); -} diff --git a/static/app/routes.tsx b/static/app/routes.tsx index 218b9add6187da..1937d7c0a4593b 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -618,13 +618,6 @@ function buildRoutes() { () => import('sentry/views/settings/project/projectUserFeedback') )} /> - import('sentry/views/settings/project/networkErrorLogging') - )} - /> ; - type: EntryType.NEL; -}; - type EntryGeneric = { data: Record; type: EntryType.EXPECTCT | EntryType.EXPECTSTAPLE | EntryType.HPKP; @@ -406,7 +400,6 @@ export type Entry = | EntryRequest | EntryTemplate | EntryCsp - | EntryNel | EntryGeneric | EntryResources; diff --git a/static/app/types/project.tsx b/static/app/types/project.tsx index 94044e63f3b42e..001f823605cadf 100644 --- a/static/app/types/project.tsx +++ b/static/app/types/project.tsx @@ -75,7 +75,6 @@ export type ProjectKey = { cdn: string; csp: string; minidump: string; - nel: string; public: string; secret: string; security: string; diff --git a/static/app/utils/events.tsx b/static/app/utils/events.tsx index 1ad54834b35216..a987229d693207 100644 --- a/static/app/utils/events.tsx +++ b/static/app/utils/events.tsx @@ -176,7 +176,7 @@ export function getTitle( case EventOrGroupType.DEFAULT: return { title: customTitle ?? metadata.title ?? '', - subtitle: metadata.uri ?? '', + subtitle: '', treeLabel: undefined, }; case EventOrGroupType.TRANSACTION: diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx index 68ebfb43c84a75..a9c3fd62099784 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx @@ -141,7 +141,6 @@ function DefaultGroupEventDetailsContent({ - diff --git a/static/app/views/settings/project/navigationConfiguration.tsx b/static/app/views/settings/project/navigationConfiguration.tsx index ba0d43838db854..265194f0146a8c 100644 --- a/static/app/views/settings/project/navigationConfiguration.tsx +++ b/static/app/views/settings/project/navigationConfiguration.tsx @@ -131,10 +131,6 @@ export default function getConfiguration({ path: `${pathPrefix}/release-tracking/`, title: t('Releases'), }, - { - path: `${pathPrefix}/nel/`, - title: t('Network Error Logging'), - }, { path: `${pathPrefix}/security-headers/`, title: t('Security Headers'), diff --git a/static/app/views/settings/project/networkErrorLogging.tsx b/static/app/views/settings/project/networkErrorLogging.tsx deleted file mode 100644 index 7544cc9038fb58..00000000000000 --- a/static/app/views/settings/project/networkErrorLogging.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import {RouteComponentProps} from 'react-router'; - -import ExternalLink from 'sentry/components/links/externalLink'; -import Panel from 'sentry/components/panels/panel'; -import PanelBody from 'sentry/components/panels/panelBody'; -import PanelHeader from 'sentry/components/panels/panelHeader'; -import PreviewFeature from 'sentry/components/previewFeature'; -import {t, tct} from 'sentry/locale'; -import {Organization, ProjectKey} from 'sentry/types'; -import routeTitleGen from 'sentry/utils/routeTitle'; -import withOrganization from 'sentry/utils/withOrganization'; -import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView'; -import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; - -type Props = RouteComponentProps<{projectId: string}, {}> & { - organization: Organization; -}; - -type State = { - keyList: null | ProjectKey[]; -} & DeprecatedAsyncView['state']; - -class ProjectNelReports extends DeprecatedAsyncView { - getEndpoints(): ReturnType { - const {organization} = this.props; - const {projectId} = this.props.params; - return [['keyList', `/projects/${organization.slug}/${projectId}/keys/`]]; - } - - getTitle() { - const {projectId} = this.props.params; - return routeTitleGen(t('Network Error Logging (NEL)'), projectId, false); - } - - getInstructions(keyList: ProjectKey[]) { - return ( - 'NEL: {"report_to":"nel","include_subdomains":true,"max_age":86400,"success_fraction":0.0,"failure_fraction":1.0}\n' + - 'Report-To: {"group":"nel","max_age":86400,"endpoints":[{"url":"' + - keyList[0].dsn.nel + - '"}]}' - ); - } - - renderBody() { - const {keyList} = this.state; - if (!keyList) { - return null; - } - - return ( -
- - - - - - {t('About')} - -

- {tct( - `[link:Network Error Logging] - (NEL) is a mechanism that can be configured via the NEL HTTP response header. This experimental header allows websites and applications to opt-in to receive reports about failed (and, if desired, successful) network fetches from supporting browsers.`, - { - link: ( - - ), - } - )} -

-

- To configure reports in Sentry, you'll need to configure the headers from - your server: -

- -
{this.getInstructions(keyList)}
- -

- {tct('For more information, see [link:the article on MDN].', { - link: ( - - ), - })} -

-
-
-
- ); - } -} - -export default withOrganization(ProjectNelReports); diff --git a/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx b/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx index 29eb0d794ba3ff..759f0e893b57e9 100644 --- a/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx +++ b/static/app/views/settings/project/projectKeys/projectKeyCredentials.tsx @@ -16,7 +16,6 @@ type Props = { showDsn?: boolean; showDsnPublic?: boolean; showMinidump?: boolean; - showNel?: boolean; showProjectId?: boolean; showPublicKey?: boolean; showSecretKey?: boolean; @@ -30,7 +29,6 @@ function ProjectKeyCredentials({ showDsn = true, showDsnPublic = true, showMinidump = true, - showNel = true, showProjectId = false, showPublicKey = false, showSecretKey = false, @@ -99,28 +97,6 @@ function ProjectKeyCredentials({ )} - {showNel && ( - - Network Error Logging reports - - ), - })} - inline={false} - flexibleControlStateSize - > - - {getDynamicText({ - value: data.dsn.nel, - fixed: '__MINIDUMP_ENDPOINT__', - })} - - - )} - {showSecurityEndpoint && ( Date: Thu, 5 Oct 2023 16:00:14 +0200 Subject: [PATCH 20/28] fix typing --- src/sentry/api/serializers/models/project_key.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/api/serializers/models/project_key.py b/src/sentry/api/serializers/models/project_key.py index 51e736a39927c4..e7a6a6e2ebe5e1 100644 --- a/src/sentry/api/serializers/models/project_key.py +++ b/src/sentry/api/serializers/models/project_key.py @@ -23,6 +23,7 @@ class DSN(TypedDict): csp: str security: str minidump: str + nel: str unreal: str cdn: str From 3b52efa1ebd4bd234bbdf0a2542ab6fc5baebc1e Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Fri, 6 Oct 2023 10:05:55 +0200 Subject: [PATCH 21/28] fix api-docs --- api-docs/components/schemas/key.json | 5 ++++- src/sentry/apidocs/examples/project_examples.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api-docs/components/schemas/key.json b/api-docs/components/schemas/key.json index 3fc593ca7eba68..65c3075b1e340d 100644 --- a/api-docs/components/schemas/key.json +++ b/api-docs/components/schemas/key.json @@ -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" @@ -36,6 +36,9 @@ "minidump": { "type": "string" }, + "nel": { + "type": "string" + }, "public": { "type": "string" }, diff --git a/src/sentry/apidocs/examples/project_examples.py b/src/sentry/apidocs/examples/project_examples.py index b6bcb6abf4cc16..701d705588988a 100644 --- a/src/sentry/apidocs/examples/project_examples.py +++ b/src/sentry/apidocs/examples/project_examples.py @@ -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", }, @@ -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", }, From 985a207d094ff7cd6cbf02f81f4d13b27c06d819 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Fri, 6 Oct 2023 11:06:55 +0200 Subject: [PATCH 22/28] add NelEventTest --- tests/sentry/eventtypes/test_nel.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/sentry/eventtypes/test_nel.py diff --git a/tests/sentry/eventtypes/test_nel.py b/tests/sentry/eventtypes/test_nel.py new file mode 100644 index 00000000000000..8ab6030f7ec27a --- /dev/null +++ b/tests/sentry/eventtypes/test_nel.py @@ -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"}, + "nel": {"url": "https://example.com/"}, + } + assert inst.get_metadata(data) == { + "title": "connection / tcp.refused", + "uri": "https://example.com/", + } From 5f02f2e087239b78b4758a9c727b5be4c0fe7a45 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Fri, 6 Oct 2023 11:15:56 +0200 Subject: [PATCH 23/28] add test for get_nel_culprit --- tests/sentry/event_manager/test_generate_culprit.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/sentry/event_manager/test_generate_culprit.py b/tests/sentry/event_manager/test_generate_culprit.py index 99f39b265a261f..639c933e7c5e82 100644 --- a/tests/sentry/event_manager/test_generate_culprit.py +++ b/tests/sentry/event_manager/test_generate_culprit.py @@ -105,3 +105,14 @@ 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" From 703a0c98c7a1d34c434c8e63a8b0d9ae77c5572f Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Tue, 17 Oct 2023 15:37:26 +0200 Subject: [PATCH 24/28] align with relay changes --- src/sentry/eventtypes/nel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/eventtypes/nel.py b/src/sentry/eventtypes/nel.py index f4caa8a24b12cc..d275c1157fe283 100644 --- a/src/sentry/eventtypes/nel.py +++ b/src/sentry/eventtypes/nel.py @@ -6,5 +6,5 @@ class NelEvent(DefaultEvent): def extract_metadata(self, data): metadata = super().extract_metadata(data) - metadata["uri"] = data.get("nel").get("url") + metadata["uri"] = data.get("request").get("url") return metadata From 6b896b6eecc57b902dd8db534f1ca46316ad757e Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Fri, 3 Nov 2023 12:06:19 +0100 Subject: [PATCH 25/28] fix missing type case; add tests --- src/sentry/culprit.py | 4 ++-- tests/sentry/event_manager/test_generate_culprit.py | 6 ++++++ tests/sentry/eventtypes/test_nel.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/sentry/culprit.py b/src/sentry/culprit.py index 0d150adf453430..89265ec5626074 100644 --- a/src/sentry/culprit.py +++ b/src/sentry/culprit.py @@ -76,7 +76,7 @@ def get_frame_culprit(frame, platform): def get_nel_culprit(data_nel): body = data_nel.get("body") - ty = body.get("type") + ty = body.get("type", "") if ty == "http.error": return NEL_CULPRITS[ty].format(body.get("status_code")) - return NEL_CULPRITS[ty] + return NEL_CULPRITS.get(ty, f"Unknown type: {ty}") diff --git a/tests/sentry/event_manager/test_generate_culprit.py b/tests/sentry/event_manager/test_generate_culprit.py index 639c933e7c5e82..5604014e4bb579 100644 --- a/tests/sentry/event_manager/test_generate_culprit.py +++ b/tests/sentry/event_manager/test_generate_culprit.py @@ -116,3 +116,9 @@ def test_nel_culprit(): 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) == "Unknown type: dns.weird" + + data = {"nel": {"body": {"phase": "dns"}}} + assert generate_culprit(data) == "Unknown type: " diff --git a/tests/sentry/eventtypes/test_nel.py b/tests/sentry/eventtypes/test_nel.py index 8ab6030f7ec27a..fb59e371f5df01 100644 --- a/tests/sentry/eventtypes/test_nel.py +++ b/tests/sentry/eventtypes/test_nel.py @@ -9,7 +9,7 @@ def test_get_metadata(self): inst = NelEvent() data = { "logentry": {"formatted": "connection / tcp.refused"}, - "nel": {"url": "https://example.com/"}, + "request": {"url": "https://example.com/"}, } assert inst.get_metadata(data) == { "title": "connection / tcp.refused", From 664724c58e99a44299604026af959871fafc3812 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Mon, 6 Nov 2023 14:25:29 +0100 Subject: [PATCH 26/28] update to sentry-relay==0.8.34; remove temporary hack --- requirements-base.txt | 2 +- requirements-dev-frozen.txt | 2 +- requirements-frozen.txt | 2 +- src/sentry/models/event.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-base.txt b/requirements-base.txt index b98cc0987fc7f4..cd6bf3dd5a9477 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -62,7 +62,7 @@ rfc3986-validator>=0.1.1 sentry-arroyo>=2.14.12 sentry-kafka-schemas>=0.1.33 sentry-redis-tools>=0.1.7 -sentry-relay>=0.8.33 +sentry-relay>=0.8.34 sentry-sdk>=1.31.0 snuba-sdk>=2.0.7 simplejson>=3.17.6 diff --git a/requirements-dev-frozen.txt b/requirements-dev-frozen.txt index de7e12e0e5c2f4..6e174d6ab2a84b 100644 --- a/requirements-dev-frozen.txt +++ b/requirements-dev-frozen.txt @@ -173,7 +173,7 @@ sentry-forked-django-stubs==4.2.6.post2 sentry-forked-djangorestframework-stubs==3.14.4.post1 sentry-kafka-schemas==0.1.33 sentry-redis-tools==0.1.7 -sentry-relay==0.8.33 +sentry-relay==0.8.34 sentry-sdk==1.31.0 sentry-usage-accountant==0.0.10 simplejson==3.17.6 diff --git a/requirements-frozen.txt b/requirements-frozen.txt index 4482a7625a84e3..41f7a90a5c582b 100644 --- a/requirements-frozen.txt +++ b/requirements-frozen.txt @@ -114,7 +114,7 @@ s3transfer==0.6.1 sentry-arroyo==2.14.12 sentry-kafka-schemas==0.1.33 sentry-redis-tools==0.1.7 -sentry-relay==0.8.33 +sentry-relay==0.8.34 sentry-sdk==1.31.0 sentry-usage-accountant==0.0.10 simplejson==3.17.6 diff --git a/src/sentry/models/event.py b/src/sentry/models/event.py index 54e8a67883549a..01fbc4d9a7efc4 100644 --- a/src/sentry/models/event.py +++ b/src/sentry/models/event.py @@ -27,7 +27,7 @@ def __init__(self, data, skip_renormalization=False, **kwargs): # XXX: This is a hack to make generic events work (for now?). I'm not sure whether we # should include this in the rust normalizer, since we don't want people sending us # these via the sdk. - if pre_normalize_type in ["generic", "nel"]: + if pre_normalize_type == "generic": data["type"] = pre_normalize_type CanonicalKeyDict.__init__(self, data, **kwargs) From c879879018f5956cc1da0ba86461cdb7a22d34b5 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Thu, 9 Nov 2023 12:24:16 +0100 Subject: [PATCH 27/28] Update src/sentry/culprit.py Co-authored-by: Joris Bayer --- src/sentry/culprit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/culprit.py b/src/sentry/culprit.py index 89265ec5626074..93e43938c8edf9 100644 --- a/src/sentry/culprit.py +++ b/src/sentry/culprit.py @@ -79,4 +79,4 @@ def get_nel_culprit(data_nel): ty = body.get("type", "") if ty == "http.error": return NEL_CULPRITS[ty].format(body.get("status_code")) - return NEL_CULPRITS.get(ty, f"Unknown type: {ty}") + return NEL_CULPRITS.get(ty, ty) From 0fa2a8d12bf3566796774e580bd2e047ef3115ee Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Thu, 9 Nov 2023 12:26:15 +0100 Subject: [PATCH 28/28] fix test --- tests/sentry/event_manager/test_generate_culprit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sentry/event_manager/test_generate_culprit.py b/tests/sentry/event_manager/test_generate_culprit.py index 5604014e4bb579..aa5ab69ad50afd 100644 --- a/tests/sentry/event_manager/test_generate_culprit.py +++ b/tests/sentry/event_manager/test_generate_culprit.py @@ -118,7 +118,7 @@ def test_nel_culprit(): assert generate_culprit(data) == "The TCP connection was reset" data = {"nel": {"body": {"phase": "dns", "type": "dns.weird"}}} - assert generate_culprit(data) == "Unknown type: dns.weird" + assert generate_culprit(data) == "dns.weird" data = {"nel": {"body": {"phase": "dns"}}} - assert generate_culprit(data) == "Unknown type: " + assert generate_culprit(data) == ""