tagStore.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {createStore} from 'reflux';
  2. import {ItemType, type SearchGroup} from 'sentry/components/smartSearchBar/types';
  3. import type {Tag, TagCollection} from 'sentry/types/group';
  4. import {
  5. getIssueTitleFromType,
  6. IssueCategory,
  7. IssueType,
  8. PriorityLevel,
  9. } from 'sentry/types/group';
  10. import type {Organization} from 'sentry/types/organization';
  11. import {SEMVER_TAGS} from 'sentry/utils/discover/fields';
  12. import {FieldKey, ISSUE_FIELDS} from 'sentry/utils/fields';
  13. import type {StrictStoreDefinition} from './types';
  14. // This list is only used on issues. Events/discover
  15. // have their own field list that exists elsewhere.
  16. // contexts.key and contexts.value omitted on purpose.
  17. const BUILTIN_TAGS = ISSUE_FIELDS.reduce<TagCollection>((acc, tag) => {
  18. acc[tag] = {key: tag, name: tag};
  19. return acc;
  20. }, {});
  21. interface TagStoreDefinition extends StrictStoreDefinition<TagCollection> {
  22. getIssueAttributes(): TagCollection;
  23. getIssueTags(org: Organization): TagCollection;
  24. loadTagsSuccess(data: Tag[]): void;
  25. reset(): void;
  26. }
  27. const storeConfig: TagStoreDefinition = {
  28. state: {},
  29. init() {
  30. // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux
  31. // listeners due to their leaky nature in tests.
  32. this.state = {};
  33. },
  34. /**
  35. * Gets only predefined issue attributes
  36. */
  37. getIssueAttributes() {
  38. // TODO(mitsuhiko): what do we do with translations here?
  39. const isSuggestions = [
  40. 'resolved',
  41. 'unresolved',
  42. ...['archived', 'escalating', 'new', 'ongoing', 'regressed'],
  43. 'assigned',
  44. 'unassigned',
  45. 'for_review',
  46. 'linked',
  47. 'unlinked',
  48. ];
  49. const sortedTagKeys = Object.keys(this.state).sort((a, b) => {
  50. return a.toLowerCase().localeCompare(b.toLowerCase());
  51. });
  52. const tagCollection = {
  53. [FieldKey.IS]: {
  54. key: FieldKey.IS,
  55. name: 'Status',
  56. values: isSuggestions,
  57. maxSuggestedValues: isSuggestions.length,
  58. predefined: true,
  59. },
  60. [FieldKey.HAS]: {
  61. key: FieldKey.HAS,
  62. name: 'Has Tag',
  63. values: sortedTagKeys,
  64. predefined: true,
  65. },
  66. [FieldKey.ASSIGNED]: {
  67. key: FieldKey.ASSIGNED,
  68. name: 'Assigned To',
  69. values: [],
  70. predefined: true,
  71. },
  72. [FieldKey.BOOKMARKS]: {
  73. key: FieldKey.BOOKMARKS,
  74. name: 'Bookmarked By',
  75. values: [],
  76. predefined: true,
  77. },
  78. [FieldKey.ISSUE_CATEGORY]: {
  79. key: FieldKey.ISSUE_CATEGORY,
  80. name: 'Issue Category',
  81. values: [
  82. IssueCategory.ERROR,
  83. IssueCategory.PERFORMANCE,
  84. IssueCategory.REPLAY,
  85. IssueCategory.CRON,
  86. ],
  87. predefined: true,
  88. },
  89. [FieldKey.ISSUE_TYPE]: {
  90. key: FieldKey.ISSUE_TYPE,
  91. name: 'Issue Type',
  92. values: [
  93. IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES,
  94. IssueType.PERFORMANCE_N_PLUS_ONE_API_CALLS,
  95. IssueType.PERFORMANCE_CONSECUTIVE_DB_QUERIES,
  96. IssueType.PERFORMANCE_SLOW_DB_QUERY,
  97. IssueType.PERFORMANCE_RENDER_BLOCKING_ASSET,
  98. IssueType.PERFORMANCE_UNCOMPRESSED_ASSET,
  99. IssueType.PERFORMANCE_ENDPOINT_REGRESSION,
  100. IssueType.PROFILE_FILE_IO_MAIN_THREAD,
  101. IssueType.PROFILE_IMAGE_DECODE_MAIN_THREAD,
  102. IssueType.PROFILE_JSON_DECODE_MAIN_THREAD,
  103. IssueType.PROFILE_REGEX_MAIN_THREAD,
  104. IssueType.PROFILE_FUNCTION_REGRESSION,
  105. ].map(value => ({
  106. icon: null,
  107. title: value,
  108. name: value,
  109. documentation: getIssueTitleFromType(value),
  110. value,
  111. type: ItemType.TAG_VALUE,
  112. children: [],
  113. })) as SearchGroup[],
  114. predefined: true,
  115. },
  116. [FieldKey.LAST_SEEN]: {
  117. key: FieldKey.LAST_SEEN,
  118. name: 'Last Seen',
  119. values: [],
  120. predefined: false,
  121. },
  122. [FieldKey.FIRST_SEEN]: {
  123. key: FieldKey.FIRST_SEEN,
  124. name: 'First Seen',
  125. values: [],
  126. predefined: false,
  127. },
  128. [FieldKey.FIRST_RELEASE]: {
  129. key: FieldKey.FIRST_RELEASE,
  130. name: 'First Release',
  131. values: ['latest'],
  132. predefined: true,
  133. },
  134. [FieldKey.EVENT_TIMESTAMP]: {
  135. key: FieldKey.EVENT_TIMESTAMP,
  136. name: 'Event Timestamp',
  137. values: [],
  138. predefined: true,
  139. },
  140. [FieldKey.TIMES_SEEN]: {
  141. key: FieldKey.TIMES_SEEN,
  142. name: 'Times Seen',
  143. isInput: true,
  144. // Below values are required or else SearchBar will attempt to get values
  145. // This is required or else SearchBar will attempt to get values
  146. values: [],
  147. predefined: true,
  148. },
  149. [FieldKey.ASSIGNED_OR_SUGGESTED]: {
  150. key: FieldKey.ASSIGNED_OR_SUGGESTED,
  151. name: 'Assigned or Suggested',
  152. isInput: true,
  153. values: [],
  154. predefined: true,
  155. },
  156. [FieldKey.ISSUE_PRIORITY]: {
  157. key: FieldKey.ISSUE_PRIORITY,
  158. name: 'Issue Priority',
  159. values: [PriorityLevel.HIGH, PriorityLevel.MEDIUM, PriorityLevel.LOW],
  160. predefined: true,
  161. },
  162. };
  163. // Ony include fields that that are part of the ISSUE_FIELDS. This is
  164. // because we may sometimes have fields that are turned off by removing
  165. // them from ISSUE_FIELDS
  166. const filteredCollection = Object.entries(tagCollection).filter(([key]) =>
  167. ISSUE_FIELDS.includes(key as FieldKey)
  168. );
  169. return Object.fromEntries(filteredCollection);
  170. },
  171. /**
  172. * Get all tags including builtin issue tags and issue attributes
  173. */
  174. getIssueTags(org: Organization) {
  175. const issueTags = {
  176. ...BUILTIN_TAGS,
  177. ...SEMVER_TAGS,
  178. // State tags should overwrite built ins.
  179. ...this.state,
  180. // We want issue attributes to overwrite any built in and state tags
  181. ...this.getIssueAttributes(),
  182. };
  183. if (!org.features.includes('device-classification')) {
  184. delete issueTags[FieldKey.DEVICE_CLASS];
  185. }
  186. return issueTags;
  187. },
  188. getState() {
  189. return this.state;
  190. },
  191. reset() {
  192. this.state = {};
  193. this.trigger(this.state);
  194. },
  195. loadTagsSuccess(data) {
  196. // Note: We could probably stop cloning the data here and just
  197. // assign to this.state directly, but there is a change someone may
  198. // be relying on referential equality somewhere in the codebase and
  199. // we dont want to risk breaking that.
  200. const newState: TagCollection = {};
  201. for (let i = 0; i < data.length; i++) {
  202. const tag = data[i];
  203. newState[tag.key] = {
  204. values: [],
  205. ...tag,
  206. };
  207. }
  208. // We will iterate through the previous tags in reverse so that previously
  209. // added tags are carried over first. We rely on browser implementation
  210. // of Object.keys() to return keys in insertion order.
  211. const previousTagKeys = Object.keys(this.state);
  212. const MAX_STORE_SIZE = 2000;
  213. // We will carry over the previous tags until we reach the max store size
  214. const toCarryOver = Math.max(0, MAX_STORE_SIZE - data.length);
  215. let carriedOver = 0;
  216. while (previousTagKeys.length > 0 && carriedOver < toCarryOver) {
  217. const tagKey = previousTagKeys.pop();
  218. if (tagKey === undefined) {
  219. // Should be unreachable, but just in case
  220. break;
  221. }
  222. // If the new state already has a previous tag then we will not carry it over
  223. // and use the latest tag in the store instead.
  224. if (newState[tagKey]) {
  225. continue;
  226. }
  227. // Else override the tag with the previous tag
  228. newState[tagKey] = this.state[tagKey];
  229. carriedOver++;
  230. }
  231. this.state = newState;
  232. this.trigger(newState);
  233. },
  234. };
  235. const TagStore = createStore(storeConfig);
  236. export default TagStore;