tagStore.tsx 9.0 KB

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