123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- import {createContext, useContext, useEffect, useMemo, useRef} from 'react';
- import {dropUndefinedKeys} from '@sentry/utils';
- import * as reactQuery from '@tanstack/react-query';
- import {ApiResult} from 'sentry/api';
- import type {Group, GroupStats, Organization, PageFilters} from 'sentry/types';
- import {getUtcDateString} from 'sentry/utils/dates';
- import {UseQueryResult} from 'sentry/utils/queryClient';
- import RequestError from 'sentry/utils/requestError/requestError';
- import useApi from 'sentry/utils/useApi';
- function getEndpointParams(
- p: Pick<GroupStatsProviderProps, 'selection' | 'period' | 'query' | 'groupIds'>
- ): StatEndpointParams {
- const params: StatEndpointParams = {
- project: p.selection.projects,
- environment: p.selection.environments,
- groupStatsPeriod: p.period,
- query: p.query,
- groups: p.groupIds,
- ...p.selection.datetime,
- };
- if (p.selection.datetime.period) {
- delete params.period;
- params.statsPeriod = p.selection.datetime.period;
- }
- if (params.end) {
- params.end = getUtcDateString(params.end);
- }
- if (params.start) {
- params.start = getUtcDateString(params.start);
- }
- return dropUndefinedKeys(params);
- }
- const GroupStatsContext = createContext<Record<string, GroupStats> | null>(null);
- export function useGroupStats(group: Group): GroupStats {
- const ctx = useContext(GroupStatsContext);
- if (!ctx) {
- return group;
- }
- return ctx?.[group.id] ?? group;
- }
- interface StatEndpointParams extends Partial<PageFilters['datetime']> {
- environment: string[];
- groups: Group['id'][];
- project: number[];
- cursor?: string;
- expand?: string | string[];
- groupStatsPeriod?: string | null;
- page?: number | string;
- query?: string | undefined;
- sort?: string;
- statsPeriod?: string | null;
- }
- export type GroupStatsQuery = UseQueryResult<Record<string, GroupStats>, RequestError>;
- export interface GroupStatsProviderProps {
- children: React.ReactNode;
- groupIds: Group['id'][];
- organization: Organization;
- period: string;
- selection: PageFilters;
- onStatsQuery?: (
- query: UseQueryResult<Record<string, GroupStats>, RequestError>
- ) => void;
- query?: string;
- }
- class CacheNode<T> {
- value: T;
- constructor(value: T) {
- this.value = value;
- }
- }
- class Cache<T> {
- private map: Map<string, CacheNode<T>>;
- constructor() {
- this.map = new Map();
- }
- get(key: string): T | undefined {
- return this.map.get(key)?.value;
- }
- set(key: string, value: T): T {
- const node = new CacheNode(value);
- this.map.set(key, node);
- return node.value;
- }
- }
- export function GroupStatsProvider(props: GroupStatsProviderProps) {
- const api = useApi();
- const cache = useRef<Cache<Record<string, GroupStats>> | null>(null);
- if (!cache.current) {
- cache.current = new Cache<Record<string, GroupStats>>();
- }
- const cacheKey = useMemo(() => {
- const query = getEndpointParams({
- selection: props.selection,
- period: props.period,
- query: props.query,
- groupIds: props.groupIds,
- });
- const {groups: _groups, ...rest} = query;
- return JSON.stringify({...rest, organization: props.organization.slug});
- }, [
- props.selection,
- props.period,
- props.query,
- props.groupIds,
- props.organization.slug,
- ]);
- const queryFn = (): Promise<Record<string, GroupStats>> => {
- const query = getEndpointParams({
- selection: props.selection,
- period: props.period,
- query: props.query,
- groupIds: props.groupIds,
- });
- const entry = cache.current && cache.current.get(cacheKey);
- if (entry) {
- query.groups = query.groups.filter(id => !entry[id]);
- }
- // Dont make a request if there are no groups to fetch data from
- // and we have a cached entry that we can resolve with
- if (!query.groups.length && entry) {
- return Promise.resolve(entry);
- }
- // Dont make a request if there are no groups
- if (!query.groups.length) {
- return Promise.resolve({});
- }
- const promise = api
- .requestPromise<true>(`/organizations/${props.organization.slug}/issues-stats/`, {
- method: 'GET',
- query,
- includeAllArgs: true,
- })
- .then((resp: ApiResult<GroupStats[]>): Record<string, GroupStats> => {
- const map = cache.current?.get(cacheKey) ?? {};
- if (!resp || !Array.isArray(resp[0])) {
- return {...map};
- }
- for (const stat of resp[0]) {
- map[stat.id] = stat;
- }
- if (cache.current) {
- cache.current.set(cacheKey, map);
- }
- // Return a copy so that callers cannot mutate the cache
- return {...map};
- })
- .catch(() => {
- return {};
- });
- return promise;
- };
- const statsQuery = reactQuery.useQuery<Record<string, GroupStats>, RequestError>(
- [
- `/organizations/${props.organization.slug}/issues-stats/`,
- props.selection,
- props.period,
- props.query,
- props.groupIds,
- ],
- queryFn,
- {
- enabled: props.groupIds.length > 0,
- staleTime: Infinity,
- }
- );
- const onStatsQuery = props.onStatsQuery;
- useEffect(() => {
- onStatsQuery?.(statsQuery);
- // We only want to fire the observer when the status changes
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [statsQuery.status, onStatsQuery]);
- return (
- <GroupStatsContext.Provider value={cache.current.get(cacheKey) ?? {}}>
- {props.children}
- </GroupStatsContext.Provider>
- );
- }
|