projectEventRedirect.tsx 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import {useEffect, useState} from 'react';
  2. import DetailedError from 'sentry/components/errors/detailedError';
  3. import * as Layout from 'sentry/components/layouts/thirds';
  4. import {t} from 'sentry/locale';
  5. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  6. import {useParams} from 'sentry/utils/useParams';
  7. type Props = RouteComponentProps<{}, {}>;
  8. /**
  9. * This component performs a client-side redirect to Event Details given only
  10. * an event ID (which normally additionally requires the event's Issue/Group ID).
  11. * It does this by using an XHR against the identically-named ProjectEventRedirect
  12. * _Django_ view, which responds with a 302 with the Location of the corresponding
  13. * Event Details page (if it exists).
  14. *
  15. * See:
  16. * https://github.com/getsentry/sentry/blob/824c03089907ad22a9282303a5eaca33989ce481/src/sentry/web/urls.py#L578
  17. */
  18. function ProjectEventRedirect({router}: Props) {
  19. const [error, setError] = useState<string | null>(null);
  20. const params = useParams();
  21. useEffect(() => {
  22. // This presumes that _this_ React view/route is only reachable at
  23. // /:org/:project/events/:eventId (the same URL which serves the ProjectEventRedirect
  24. // Django view).
  25. const endpoint = `/organizations/${params.orgId}/projects/${params.projectId}/events/${params.eventId}/`;
  26. // Use XmlHttpRequest directly instead of our client API helper (fetch),
  27. // because you can't reach the underlying XHR via $.ajax, and we need
  28. // access to `xhr.responseURL`.
  29. //
  30. // TODO(epurkhiser): We can likely replace tihs with fetch
  31. const xhr = new XMLHttpRequest();
  32. // Hitting this endpoint will return a 302 with a new location, which
  33. // the XHR will follow and make a _second_ request. Using HEAD instead
  34. // of GET returns an empty response instead of the entire HTML content.
  35. xhr.open('HEAD', endpoint);
  36. xhr.send();
  37. xhr.onload = () => {
  38. if (xhr.status === 404) {
  39. setError(t('Could not find an issue for the provided event id'));
  40. return;
  41. }
  42. // responseURL is the URL of the document the browser ultimately loaded,
  43. // after following any redirects. It _should_ be the page we're trying
  44. // to reach; use the router to go there.
  45. //
  46. // Use `replace` so that hitting the browser back button will skip all
  47. // this redirect business.
  48. const url = new URL(xhr.responseURL);
  49. if (url.origin === window.location.origin) {
  50. router.replace(url.pathname);
  51. } else {
  52. // If the origin has changed, we cannot do a simple replace with the router.
  53. // Instead, we opt to do a full redirect.
  54. window.location.replace(xhr.responseURL);
  55. }
  56. };
  57. xhr.onerror = () => {
  58. setError(t('Could not load the requested event'));
  59. };
  60. }, [params, router]);
  61. return error ? (
  62. <DetailedError heading={t('Not found')} message={error} hideSupportLinks />
  63. ) : (
  64. <Layout.Page withPadding />
  65. );
  66. }
  67. export default ProjectEventRedirect;