useRouteActivatedHook.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import {useCallback, useEffect, useState} from 'react';
  2. import type {Hooks} from 'sentry/types/hooks';
  3. import type {Organization} from 'sentry/types/organization';
  4. import usePrevious from 'sentry/utils/usePrevious';
  5. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  6. import rawTrackAnalyticsEvent from 'getsentry/utils/rawTrackAnalyticsEvent';
  7. import {
  8. convertToReloadPath,
  9. getEventPath,
  10. getUrlFromLocation,
  11. } from 'getsentry/utils/routeAnalytics';
  12. import trackMetric from 'getsentry/utils/trackMetric';
  13. /**
  14. * @internal exported for tests only
  15. * give up to 7s for things to load
  16. */
  17. export const DELAY_TIME_MS = 7000;
  18. type Props = Parameters<Hooks['react-hook:route-activated']>[0];
  19. export default function useRouteActivatedHook(props: Props) {
  20. const {routes, location} = props;
  21. const [analyticsParams, _setRouteAnalyticsParams] = useState({});
  22. const [disableAnalytics, _setDisableRouteAnalytics] = useState(false);
  23. const [hasSentAnalytics, setHasSentAnalytics] = useState(false);
  24. const [readyToSend, setReadyToSend] = useState(false);
  25. const [mountTime, setMountTime] = useState(0);
  26. const prevRoutes = usePrevious(routes);
  27. const prevLocation = usePrevious(location);
  28. // Reload event name
  29. const [_eventKey, setEventKey] = useState<string | undefined>(undefined);
  30. // Amplitude event name
  31. const [_eventName, setEventName] = useState<string | undefined>(undefined);
  32. // this hook is above the normal organization context so we have to
  33. // set it from a lower level and pass it here
  34. const [organization, setOrganization] = useState<Organization | null>(null);
  35. // keep track of the previous URL so we can detect trigger hooks after resetting the route params
  36. const previousUrl = getUrlFromLocation(prevLocation);
  37. const currentRoute = getEventPath(routes);
  38. const prevRoute = getEventPath(prevRoutes);
  39. const considerSendingAnalytics =
  40. organization && !hasSentAnalytics && !disableAnalytics && mountTime > 0;
  41. useEffect(() => {
  42. setMountTime(Date.now());
  43. }, []);
  44. const sendRouteParams = useCallback(
  45. (route: string, localOrganization: Organization) => {
  46. const reloadPath = convertToReloadPath(route);
  47. SubscriptionStore.get(localOrganization.slug, (subscription: any) => {
  48. // optional way to override the event name for Reload and Amplitude
  49. // note null means something different than undefined for eventName so
  50. // checking for that explicitly
  51. const eventKey = _eventKey !== undefined ? _eventKey : `page_view.${reloadPath}`;
  52. const eventName = _eventName !== undefined ? _eventName : `Page View: ${route}`;
  53. rawTrackAnalyticsEvent(
  54. {
  55. eventKey,
  56. eventName,
  57. organization: localOrganization,
  58. subscription,
  59. url: previousUrl, // pass in the previous URL
  60. // pass in the parameterized path as well
  61. parameterized_path: reloadPath,
  62. ...analyticsParams,
  63. },
  64. {time: mountTime}
  65. );
  66. // Also track page veiw as a reload metric. This will be propegated to
  67. // DataDog and can be used for page view SLOs
  68. trackMetric(eventKey, 1);
  69. });
  70. },
  71. [analyticsParams, mountTime, previousUrl, _eventName, _eventKey]
  72. );
  73. // This hook is called when the route changes
  74. // we need to send analytics for the previous route if we haven't yet
  75. // then reset the route params
  76. useEffect(() => {
  77. // if the only reason we haven't analytics is because we haven't hit DELAY_TIME_MS
  78. // the we need to emit it immediately before handing the new route
  79. if (considerSendingAnalytics && !readyToSend) {
  80. sendRouteParams(prevRoute, organization);
  81. }
  82. // when the route changes, reset the analytics params
  83. setHasSentAnalytics(false);
  84. _setDisableRouteAnalytics(false);
  85. _setRouteAnalyticsParams({});
  86. setEventKey(undefined);
  87. setEventName(undefined);
  88. setMountTime(Date.now());
  89. // this hook should only fire when the route changes and nothing else
  90. // eslint-disable-next-line react-hooks/exhaustive-deps
  91. }, [currentRoute]);
  92. // This hook is in charge of sending analytics after DELAY_TIME_MS has passed
  93. useEffect(() => {
  94. if (readyToSend && considerSendingAnalytics) {
  95. sendRouteParams(currentRoute, organization);
  96. // mark that we have sent the analytics
  97. setHasSentAnalytics(true);
  98. setReadyToSend(false);
  99. }
  100. }, [
  101. sendRouteParams,
  102. readyToSend,
  103. organization,
  104. considerSendingAnalytics,
  105. currentRoute,
  106. ]);
  107. const setDisableRouteAnalytics = useCallback((disabled = true) => {
  108. _setDisableRouteAnalytics(disabled);
  109. }, []);
  110. const setRouteAnalyticsParams = useCallback((params: Record<string, any>) => {
  111. // add to existing params
  112. _setRouteAnalyticsParams(existingParams => ({...existingParams, ...params}));
  113. }, []);
  114. const setEventNames = useCallback((eventKey: string, eventName: string) => {
  115. setEventKey(eventKey);
  116. setEventName(eventName);
  117. }, []);
  118. /**
  119. * This hook is in charge of setting readyToSend to true after a delay after initial mounting
  120. */
  121. useEffect(() => {
  122. if (!organization) {
  123. return () => {};
  124. }
  125. // after the context first loads, we need to wait DELAY_TIME_MS
  126. // before we send the analytics event
  127. const timeoutId = window.setTimeout(() => {
  128. if (!hasSentAnalytics) {
  129. setReadyToSend(true);
  130. }
  131. }, DELAY_TIME_MS);
  132. return () => {
  133. if (timeoutId) {
  134. clearTimeout(timeoutId);
  135. }
  136. };
  137. }, [organization, analyticsParams, hasSentAnalytics]);
  138. return {
  139. setDisableRouteAnalytics,
  140. setRouteAnalyticsParams,
  141. setOrganization,
  142. setEventNames,
  143. previousUrl,
  144. };
  145. }