useMetricReleases.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  4. import MarkLine from 'sentry/components/charts/components/markLine';
  5. import {t} from 'sentry/locale';
  6. import type {DateString} from 'sentry/types/core';
  7. import {escape} from 'sentry/utils';
  8. import {getFormattedDate, getTimeFormat, getUtcDateString} from 'sentry/utils/dates';
  9. import {formatVersion} from 'sentry/utils/formatters';
  10. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  11. import useApi from 'sentry/utils/useApi';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import usePageFilters from 'sentry/utils/usePageFilters';
  14. import useRouter from 'sentry/utils/useRouter';
  15. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  16. import type {CombinedMetricChartProps} from 'sentry/views/metrics/chart/types';
  17. interface Release {
  18. date: string;
  19. version: string;
  20. }
  21. interface ReleaseQuery {
  22. end: DateString;
  23. environment: Readonly<string[]>;
  24. project: Readonly<number[]>;
  25. start: DateString;
  26. cursor?: string;
  27. query?: string;
  28. statsPeriod?: string | null;
  29. }
  30. function getQuery(conditions) {
  31. const query = {};
  32. Object.keys(conditions).forEach(key => {
  33. let value = conditions[key];
  34. if (value && (key === 'start' || key === 'end')) {
  35. value = getUtcDateString(value);
  36. }
  37. if (value) {
  38. query[key] = value;
  39. }
  40. });
  41. return query;
  42. }
  43. export function useReleases() {
  44. const [releases, setReleases] = useState<Release[] | null>(null);
  45. const organization = useOrganization();
  46. const api = useApi();
  47. const {selection} = usePageFilters();
  48. const {
  49. datetime: {start, end, period},
  50. projects,
  51. environments,
  52. } = selection;
  53. const fetchData = useCallback(async () => {
  54. const queryObj: ReleaseQuery = {
  55. start,
  56. end,
  57. project: projects,
  58. environment: environments,
  59. statsPeriod: period,
  60. };
  61. let hasMore = true;
  62. const newReleases: Release[] = [];
  63. while (hasMore) {
  64. try {
  65. api.clear();
  66. const [releaseBatch, , resp] = await api.requestPromise(
  67. `/organizations/${organization.slug}/releases/stats/`,
  68. {
  69. includeAllArgs: true,
  70. method: 'GET',
  71. query: getQuery(queryObj),
  72. }
  73. );
  74. newReleases.push(...releaseBatch);
  75. const pageLinks = resp?.getResponseHeader('Link');
  76. if (pageLinks) {
  77. const paginationObject = parseLinkHeader(pageLinks);
  78. hasMore = paginationObject?.next?.results ?? false;
  79. queryObj.cursor = paginationObject.next.cursor;
  80. } else {
  81. hasMore = false;
  82. }
  83. } catch {
  84. addErrorMessage(t('Error fetching releases'));
  85. hasMore = false;
  86. }
  87. }
  88. setReleases(newReleases);
  89. }, [api, start, end, period, projects, environments, organization]);
  90. useEffect(() => {
  91. fetchData();
  92. }, [fetchData]);
  93. return releases;
  94. }
  95. export function useReleaseSeries() {
  96. const releases = useReleases();
  97. const organization = useOrganization();
  98. const router = useRouter();
  99. const theme = useTheme();
  100. const {selection} = usePageFilters();
  101. const releaseSeries = useMemo(() => {
  102. const query = organization.features.includes('global-views')
  103. ? {project: router.location.query.project}
  104. : {};
  105. const markLine = MarkLine({
  106. animation: false,
  107. lineStyle: {
  108. color: theme.purple300,
  109. opacity: 0.3,
  110. type: 'solid',
  111. },
  112. label: {
  113. show: false,
  114. },
  115. data: (releases ?? []).map(release => ({
  116. xAxis: +new Date(release.date),
  117. name: formatVersion(release.version, true),
  118. value: formatVersion(release.version, true),
  119. onClick: () => {
  120. router.push(
  121. normalizeUrl({
  122. pathname: `/organizations/${
  123. organization.slug
  124. }/releases/${encodeURIComponent(release.version)}/`,
  125. query,
  126. })
  127. );
  128. },
  129. label: {
  130. formatter: () => formatVersion(release.version, true),
  131. },
  132. })),
  133. tooltip: {
  134. trigger: 'item',
  135. formatter: ({data}: any) => {
  136. if (!data) {
  137. return '';
  138. }
  139. const format = `MMM D, YYYY ${getTimeFormat()} z`.trim();
  140. const time = getFormattedDate(data.value, format, {
  141. local: !selection.datetime.utc,
  142. });
  143. const version = escape(formatVersion(data.name, true));
  144. return [
  145. '<div class="tooltip-series">',
  146. `<div><span class="tooltip-label"><strong>${t(
  147. 'Release'
  148. )}</strong></span> ${version}</div>`,
  149. '</div>',
  150. '<div class="tooltip-footer">',
  151. time,
  152. '</div>',
  153. '</div>',
  154. '<div class="tooltip-arrow"></div>',
  155. ].join('');
  156. },
  157. },
  158. });
  159. return {
  160. seriesName: 'Releases',
  161. color: theme.purple200,
  162. data: [],
  163. markLine,
  164. type: 'line' as any,
  165. name: 'Releases',
  166. };
  167. }, [organization, releases, router, theme, selection.datetime.utc]);
  168. const applyChartProps = useCallback(
  169. (baseProps: CombinedMetricChartProps): CombinedMetricChartProps => {
  170. return {
  171. ...baseProps,
  172. additionalSeries: baseProps.additionalSeries
  173. ? [...baseProps.additionalSeries, releaseSeries]
  174. : [releaseSeries],
  175. };
  176. },
  177. [releaseSeries]
  178. );
  179. return {applyChartProps};
  180. }
  181. export type UseMetricReleasesResult = ReturnType<typeof useReleaseSeries>;