utils.tsx 8.1 KB

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