groupStatsProvider.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {createContext, useContext, useEffect, useMemo, useRef} from 'react';
  2. import {dropUndefinedKeys} from '@sentry/utils';
  3. import * as reactQuery from '@tanstack/react-query';
  4. import {ApiResult} from 'sentry/api';
  5. import type {Group, GroupStats, Organization, PageFilters} from 'sentry/types';
  6. import {getUtcDateString} from 'sentry/utils/dates';
  7. import {UseQueryResult} from 'sentry/utils/queryClient';
  8. import RequestError from 'sentry/utils/requestError/requestError';
  9. import useApi from 'sentry/utils/useApi';
  10. function getEndpointParams(
  11. p: Pick<GroupStatsProviderProps, 'selection' | 'period' | 'query' | 'groupIds'>
  12. ): StatEndpointParams {
  13. const params: StatEndpointParams = {
  14. project: p.selection.projects,
  15. environment: p.selection.environments,
  16. groupStatsPeriod: p.period,
  17. query: p.query,
  18. groups: p.groupIds,
  19. ...p.selection.datetime,
  20. };
  21. if (p.selection.datetime.period) {
  22. delete params.period;
  23. params.statsPeriod = p.selection.datetime.period;
  24. }
  25. if (params.end) {
  26. params.end = getUtcDateString(params.end);
  27. }
  28. if (params.start) {
  29. params.start = getUtcDateString(params.start);
  30. }
  31. return dropUndefinedKeys(params);
  32. }
  33. const GroupStatsContext = createContext<Record<string, GroupStats> | null>(null);
  34. export function useGroupStats(group: Group): GroupStats {
  35. const ctx = useContext(GroupStatsContext);
  36. if (!ctx) {
  37. return group;
  38. }
  39. return ctx?.[group.id] ?? group;
  40. }
  41. interface StatEndpointParams extends Partial<PageFilters['datetime']> {
  42. environment: string[];
  43. groups: Group['id'][];
  44. project: number[];
  45. cursor?: string;
  46. expand?: string | string[];
  47. groupStatsPeriod?: string | null;
  48. page?: number | string;
  49. query?: string | undefined;
  50. sort?: string;
  51. statsPeriod?: string | null;
  52. }
  53. export type GroupStatsQuery = UseQueryResult<Record<string, GroupStats>, RequestError>;
  54. export interface GroupStatsProviderProps {
  55. children: React.ReactNode;
  56. groupIds: Group['id'][];
  57. organization: Organization;
  58. period: string;
  59. selection: PageFilters;
  60. onStatsQuery?: (
  61. query: UseQueryResult<Record<string, GroupStats>, RequestError>
  62. ) => void;
  63. query?: string;
  64. }
  65. class CacheNode<T> {
  66. value: T;
  67. constructor(value: T) {
  68. this.value = value;
  69. }
  70. }
  71. class Cache<T> {
  72. private map: Map<string, CacheNode<T>>;
  73. constructor() {
  74. this.map = new Map();
  75. }
  76. get(key: string): T | undefined {
  77. return this.map.get(key)?.value;
  78. }
  79. set(key: string, value: T): T {
  80. const node = new CacheNode(value);
  81. this.map.set(key, node);
  82. return node.value;
  83. }
  84. }
  85. export function GroupStatsProvider(props: GroupStatsProviderProps) {
  86. const api = useApi();
  87. const cache = useRef<Cache<Record<string, GroupStats>> | null>(null);
  88. if (!cache.current) {
  89. cache.current = new Cache<Record<string, GroupStats>>();
  90. }
  91. const cacheKey = useMemo(() => {
  92. const query = getEndpointParams({
  93. selection: props.selection,
  94. period: props.period,
  95. query: props.query,
  96. groupIds: props.groupIds,
  97. });
  98. const {groups: _groups, ...rest} = query;
  99. return JSON.stringify({...rest, organization: props.organization.slug});
  100. }, [
  101. props.selection,
  102. props.period,
  103. props.query,
  104. props.groupIds,
  105. props.organization.slug,
  106. ]);
  107. const queryFn = (): Promise<Record<string, GroupStats>> => {
  108. const query = getEndpointParams({
  109. selection: props.selection,
  110. period: props.period,
  111. query: props.query,
  112. groupIds: props.groupIds,
  113. });
  114. const entry = cache.current && cache.current.get(cacheKey);
  115. if (entry) {
  116. query.groups = query.groups.filter(id => !entry[id]);
  117. }
  118. // Dont make a request if there are no groups to fetch data from
  119. // and we have a cached entry that we can resolve with
  120. if (!query.groups.length && entry) {
  121. return Promise.resolve(entry);
  122. }
  123. // Dont make a request if there are no groups
  124. if (!query.groups.length) {
  125. return Promise.resolve({});
  126. }
  127. const promise = api
  128. .requestPromise<true>(`/organizations/${props.organization.slug}/issues-stats/`, {
  129. method: 'GET',
  130. query,
  131. includeAllArgs: true,
  132. })
  133. .then((resp: ApiResult<GroupStats[]>): Record<string, GroupStats> => {
  134. const map = cache.current?.get(cacheKey) ?? {};
  135. if (!resp || !Array.isArray(resp[0])) {
  136. return {...map};
  137. }
  138. for (const stat of resp[0]) {
  139. map[stat.id] = stat;
  140. }
  141. if (cache.current) {
  142. cache.current.set(cacheKey, map);
  143. }
  144. // Return a copy so that callers cannot mutate the cache
  145. return {...map};
  146. })
  147. .catch(() => {
  148. return {};
  149. });
  150. return promise;
  151. };
  152. const statsQuery = reactQuery.useQuery<Record<string, GroupStats>, RequestError>(
  153. [
  154. `/organizations/${props.organization.slug}/issues-stats/`,
  155. props.selection,
  156. props.period,
  157. props.query,
  158. props.groupIds,
  159. ],
  160. queryFn,
  161. {
  162. enabled: props.groupIds.length > 0,
  163. staleTime: Infinity,
  164. }
  165. );
  166. const onStatsQuery = props.onStatsQuery;
  167. useEffect(() => {
  168. onStatsQuery?.(statsQuery);
  169. // We only want to fire the observer when the status changes
  170. // eslint-disable-next-line react-hooks/exhaustive-deps
  171. }, [statsQuery.status, onStatsQuery]);
  172. return (
  173. <GroupStatsContext.Provider value={cache.current.get(cacheKey) ?? {}}>
  174. {props.children}
  175. </GroupStatsContext.Provider>
  176. );
  177. }