utils.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import {useMemo} from 'react';
  2. import orderBy from 'lodash/orderBy';
  3. import {
  4. bulkUpdate,
  5. useFetchIssueTag,
  6. useFetchIssueTagValues,
  7. } from 'sentry/actionCreators/group';
  8. import type {Client} from 'sentry/api';
  9. import {t} from 'sentry/locale';
  10. import ConfigStore from 'sentry/stores/configStore';
  11. import GroupStore from 'sentry/stores/groupStore';
  12. import IssueListCacheStore from 'sentry/stores/IssueListCacheStore';
  13. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  14. import type {Event} from 'sentry/types/event';
  15. import type {Group, GroupActivity, TagValue} from 'sentry/types/group';
  16. import {defined} from 'sentry/utils';
  17. import type {ApiQueryKey} from 'sentry/utils/queryClient';
  18. import {useLocation} from 'sentry/utils/useLocation';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import {useParams} from 'sentry/utils/useParams';
  21. import {useUser} from 'sentry/utils/useUser';
  22. import {useGroupTagsReadable} from 'sentry/views/issueDetails/groupTags/useGroupTags';
  23. export function markEventSeen(
  24. api: Client,
  25. orgId: string,
  26. projectId: string,
  27. groupId: string
  28. ) {
  29. bulkUpdate(
  30. api,
  31. {
  32. orgId,
  33. projectId,
  34. itemIds: [groupId],
  35. failSilently: true,
  36. data: {hasSeen: true},
  37. },
  38. {}
  39. );
  40. IssueListCacheStore.markGroupAsSeen(groupId);
  41. }
  42. export function useDefaultIssueEvent() {
  43. const user = useLegacyStore(ConfigStore).user;
  44. const options = user ? user.options : null;
  45. return options?.defaultIssueEvent ?? 'recommended';
  46. }
  47. /**
  48. * Combines two TagValue arrays and combines TagValue.count upon conflict
  49. */
  50. export function mergeAndSortTagValues(
  51. tagValues1: TagValue[],
  52. tagValues2: TagValue[],
  53. sort: 'count' | 'lastSeen' = 'lastSeen'
  54. ): TagValue[] {
  55. const tagValueCollection = tagValues1.reduce<Record<string, TagValue>>(
  56. (acc, tagValue) => {
  57. acc[tagValue.value] = tagValue;
  58. return acc;
  59. },
  60. {}
  61. );
  62. tagValues2.forEach(tagValue => {
  63. if (tagValueCollection[tagValue.value]) {
  64. tagValueCollection[tagValue.value]!.count += tagValue.count;
  65. if (tagValue.lastSeen > tagValueCollection[tagValue.value]!.lastSeen) {
  66. tagValueCollection[tagValue.value]!.lastSeen = tagValue.lastSeen;
  67. }
  68. } else {
  69. tagValueCollection[tagValue.value] = tagValue;
  70. }
  71. });
  72. const allTagValues: TagValue[] = Object.values(tagValueCollection);
  73. if (sort === 'count') {
  74. allTagValues.sort((a, b) => b.count - a.count);
  75. } else {
  76. allTagValues.sort((a, b) => (b.lastSeen < a.lastSeen ? -1 : 1));
  77. }
  78. return allTagValues;
  79. }
  80. /**
  81. * Returns the environment name for an event or null
  82. *
  83. * @param event
  84. */
  85. export function getEventEnvironment(event: Event) {
  86. const tag = event.tags.find(({key}) => key === 'environment');
  87. return tag ? tag.value : null;
  88. }
  89. const SUBSCRIPTION_REASONS = {
  90. commented: t(
  91. "You're receiving workflow notifications because you have commented on this issue."
  92. ),
  93. assigned: t(
  94. "You're receiving workflow notifications because you were assigned to this issue."
  95. ),
  96. bookmarked: t(
  97. "You're receiving workflow notifications because you have bookmarked this issue."
  98. ),
  99. changed_status: t(
  100. "You're receiving workflow notifications because you have changed the status of this issue."
  101. ),
  102. mentioned: t(
  103. "You're receiving workflow notifications because you have been mentioned in this issue."
  104. ),
  105. };
  106. /**
  107. * @param group
  108. * @param removeLinks add/remove links to subscription reasons text (default: false)
  109. * @returns Reason for subscription
  110. */
  111. export function getSubscriptionReason(group: Group) {
  112. if (group.subscriptionDetails?.disabled) {
  113. return t('You have disabled workflow notifications for this project.');
  114. }
  115. if (!group.isSubscribed) {
  116. return t('Subscribe to workflow notifications for this issue');
  117. }
  118. if (group.subscriptionDetails) {
  119. const {reason} = group.subscriptionDetails;
  120. if (reason === 'unknown') {
  121. return t(
  122. "You're receiving workflow notifications because you are subscribed to this issue."
  123. );
  124. }
  125. if (reason && SUBSCRIPTION_REASONS.hasOwnProperty(reason)) {
  126. return SUBSCRIPTION_REASONS[reason as keyof typeof SUBSCRIPTION_REASONS];
  127. }
  128. }
  129. return t(
  130. "You're receiving updates because you are subscribed to workflow notifications for this project."
  131. );
  132. }
  133. export function getGroupMostRecentActivity(
  134. activities: GroupActivity[] | undefined
  135. ): GroupActivity | undefined {
  136. // Most recent activity
  137. return activities
  138. ? orderBy([...activities], ({dateCreated}) => new Date(dateCreated), ['desc'])[0]
  139. : undefined;
  140. }
  141. export enum ReprocessingStatus {
  142. REPROCESSED_AND_HASNT_EVENT = 'reprocessed_and_hasnt_event',
  143. REPROCESSED_AND_HAS_EVENT = 'reprocessed_and_has_event',
  144. REPROCESSING = 'reprocessing',
  145. NO_STATUS = 'no_status',
  146. }
  147. // Reprocessing Checks
  148. export function getGroupReprocessingStatus(
  149. group: Group,
  150. mostRecentActivity?: GroupActivity
  151. ) {
  152. const {status, count, activity: activities} = group;
  153. const groupCount = Number(count);
  154. switch (status) {
  155. case 'reprocessing':
  156. return ReprocessingStatus.REPROCESSING;
  157. case 'unresolved': {
  158. const groupMostRecentActivity =
  159. mostRecentActivity ?? getGroupMostRecentActivity(activities);
  160. if (groupMostRecentActivity?.type === 'reprocess') {
  161. if (groupCount === 0) {
  162. return ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT;
  163. }
  164. return ReprocessingStatus.REPROCESSED_AND_HAS_EVENT;
  165. }
  166. return ReprocessingStatus.NO_STATUS;
  167. }
  168. default:
  169. return ReprocessingStatus.NO_STATUS;
  170. }
  171. }
  172. export function useEnvironmentsFromUrl(): string[] {
  173. const location = useLocation();
  174. const envs = location.query.environment;
  175. const envsArray = useMemo(() => {
  176. return typeof envs === 'string' ? [envs] : (envs ?? []);
  177. }, [envs]);
  178. return envsArray;
  179. }
  180. export function getGroupEventDetailsQueryData({
  181. environments,
  182. query,
  183. start,
  184. end,
  185. statsPeriod,
  186. }: {
  187. query: string | undefined;
  188. end?: string;
  189. environments?: string[];
  190. start?: string;
  191. statsPeriod?: string;
  192. }): Record<string, string | string[]> {
  193. const params: Record<string, string | string[]> = {
  194. collapse: ['fullRelease'],
  195. };
  196. if (query) {
  197. params.query = query;
  198. }
  199. if (environments && environments.length > 0) {
  200. params.environment = environments;
  201. }
  202. if (start) {
  203. params.start = start;
  204. }
  205. if (end) {
  206. params.end = end;
  207. }
  208. if (statsPeriod) {
  209. params.statsPeriod = statsPeriod;
  210. }
  211. return params;
  212. }
  213. export function getGroupEventQueryKey({
  214. orgSlug,
  215. groupId,
  216. eventId,
  217. environments,
  218. query,
  219. start,
  220. end,
  221. statsPeriod,
  222. }: {
  223. environments: string[];
  224. eventId: string;
  225. groupId: string;
  226. orgSlug: string;
  227. end?: string;
  228. query?: string;
  229. start?: string;
  230. statsPeriod?: string;
  231. }): ApiQueryKey {
  232. return [
  233. `/organizations/${orgSlug}/issues/${groupId}/events/${eventId}/`,
  234. {
  235. query: getGroupEventDetailsQueryData({
  236. environments,
  237. query,
  238. start,
  239. end,
  240. statsPeriod,
  241. }),
  242. },
  243. ];
  244. }
  245. export function useHasStreamlinedUI() {
  246. const location = useLocation();
  247. const user = useUser();
  248. const organization = useOrganization();
  249. // Allow query param to override all other settings to set the UI.
  250. if (defined(location.query.streamline)) {
  251. return location.query.streamline === '1';
  252. }
  253. // If the organzation option is set to true, the new UI is used.
  254. if (organization.streamlineOnly) {
  255. return true;
  256. }
  257. // If the enforce flag is set for the organization, ignore user preferences and enable the UI
  258. if (organization.features.includes('issue-details-streamline-enforce')) {
  259. return true;
  260. }
  261. // Apply the UI based on user preferences
  262. return !!user?.options?.prefersIssueDetailsStreamlinedUI;
  263. }
  264. export function useIsSampleEvent(): boolean {
  265. const params = useParams<{groupId: string}>();
  266. const environments = useEnvironmentsFromUrl();
  267. const groupId = params.groupId;
  268. const group = GroupStore.get(groupId);
  269. const {data} = useGroupTagsReadable(
  270. {
  271. groupId,
  272. environment: environments,
  273. },
  274. // Don't want this query to take precedence over the main requests
  275. {enabled: defined(group)}
  276. );
  277. return data?.some(tag => tag.key === 'sample_event') ?? false;
  278. }
  279. type TagSort = 'date' | 'count';
  280. const DEFAULT_SORT: TagSort = 'count';
  281. export function usePrefetchTagValues(tagKey: string, groupId: string, enabled: boolean) {
  282. const organization = useOrganization();
  283. const location = useLocation();
  284. const sort: TagSort =
  285. (location.query.tagDrawerSort as TagSort | undefined) ?? DEFAULT_SORT;
  286. useFetchIssueTagValues(
  287. {
  288. orgSlug: organization.slug,
  289. groupId,
  290. tagKey,
  291. sort,
  292. cursor: location.query.tagDrawerCursor as string | undefined,
  293. },
  294. {enabled}
  295. );
  296. useFetchIssueTag(
  297. {
  298. orgSlug: organization.slug,
  299. groupId,
  300. tagKey,
  301. },
  302. {enabled}
  303. );
  304. }
  305. export function getUserTagValue(tagValue: TagValue): {
  306. subtitle: string | null;
  307. subtitleType: string | null;
  308. title: string | null;
  309. } {
  310. let title: string | null = null;
  311. let subtitle: string | null = null;
  312. let subtitleType: string | null = null;
  313. if (defined(tagValue?.name)) {
  314. title = tagValue?.name;
  315. } else if (defined(tagValue?.email)) {
  316. title = tagValue?.email;
  317. } else if (defined(tagValue?.username)) {
  318. title = title ? title : tagValue?.username;
  319. } else if (defined(tagValue?.ip_address) || (defined(tagValue?.ipAddress) && !title)) {
  320. title = tagValue?.ip_address ?? tagValue?.ipAddress;
  321. }
  322. if (defined(tagValue?.id)) {
  323. title = title ? title : tagValue?.id;
  324. if (tagValue?.id && tagValue?.id !== 'None') {
  325. subtitle = tagValue?.id;
  326. subtitleType = t('ID');
  327. }
  328. }
  329. if (title === subtitle) {
  330. subtitle = null;
  331. }
  332. return {title, subtitle, subtitleType};
  333. }