eventView.spec.tsx 110 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886
  1. import shuffle from 'lodash/shuffle';
  2. import {Organization} from 'sentry-fixture/organization';
  3. import {PageFilters} from 'sentry-fixture/pageFilters';
  4. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  5. import {NewQuery, SavedQuery} from 'sentry/types';
  6. import EventView, {
  7. isAPIPayloadSimilar,
  8. MetaType,
  9. pickRelevantLocationQueryStrings,
  10. } from 'sentry/utils/discover/eventView';
  11. import {Column} from 'sentry/utils/discover/fields';
  12. import {
  13. CHART_AXIS_OPTIONS,
  14. DiscoverDatasets,
  15. DISPLAY_MODE_OPTIONS,
  16. DisplayModes,
  17. } from 'sentry/utils/discover/types';
  18. import {AggregationKey, WebVital} from 'sentry/utils/fields';
  19. import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
  20. import {EventsDisplayFilterName} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
  21. const generateFields = fields =>
  22. fields.map(field => ({
  23. field,
  24. }));
  25. const generateSorts = sorts =>
  26. sorts.map(sortName => ({
  27. field: sortName,
  28. kind: 'desc',
  29. }));
  30. const REQUIRED_CONSTRUCTOR_PROPS = {
  31. createdBy: undefined,
  32. end: undefined,
  33. environment: [],
  34. fields: [],
  35. name: undefined,
  36. project: [],
  37. query: '',
  38. start: undefined,
  39. team: [],
  40. sorts: [],
  41. statsPeriod: undefined,
  42. topEvents: undefined,
  43. id: undefined,
  44. display: undefined,
  45. };
  46. describe('EventView constructor', function () {
  47. it('instantiates default values', function () {
  48. const eventView = new EventView(REQUIRED_CONSTRUCTOR_PROPS);
  49. expect(eventView).toMatchObject({
  50. id: undefined,
  51. name: undefined,
  52. fields: [],
  53. sorts: [],
  54. query: '',
  55. project: [],
  56. start: undefined,
  57. end: undefined,
  58. statsPeriod: undefined,
  59. environment: [],
  60. yAxis: undefined,
  61. display: undefined,
  62. });
  63. });
  64. });
  65. describe('EventView.fromLocation()', function () {
  66. it('maps query strings', function () {
  67. const location = TestStubs.location({
  68. query: {
  69. id: '42',
  70. name: 'best query',
  71. field: ['count()', 'id'],
  72. widths: ['123', '456'],
  73. sort: ['title', '-count'],
  74. query: 'event.type:transaction',
  75. project: ['123'],
  76. team: ['myteams', '1', '2'],
  77. start: '2019-10-01T00:00:00',
  78. end: '2019-10-02T00:00:00',
  79. statsPeriod: '14d',
  80. environment: ['staging'],
  81. yAxis: 'p95',
  82. display: 'previous',
  83. dataset: DiscoverDatasets.DISCOVER,
  84. },
  85. });
  86. const eventView = EventView.fromLocation(location);
  87. expect(eventView).toMatchObject({
  88. id: '42',
  89. name: 'best query',
  90. fields: [
  91. {field: 'count()', width: 123},
  92. {field: 'id', width: 456},
  93. ],
  94. sorts: generateSorts([AggregationKey.COUNT]),
  95. query: 'event.type:transaction',
  96. project: [123],
  97. team: ['myteams', 1, 2],
  98. start: undefined,
  99. end: undefined,
  100. statsPeriod: '14d',
  101. environment: ['staging'],
  102. yAxis: 'p95',
  103. display: 'previous',
  104. dataset: DiscoverDatasets.DISCOVER,
  105. });
  106. });
  107. it('includes first valid statsPeriod', function () {
  108. const location = TestStubs.location({
  109. query: {
  110. id: '42',
  111. name: 'best query',
  112. field: ['count()', 'id'],
  113. widths: ['123', '456'],
  114. sort: ['title', '-count'],
  115. query: 'event.type:transaction',
  116. project: ['123'],
  117. start: '2019-10-01T00:00:00',
  118. end: '2019-10-02T00:00:00',
  119. statsPeriod: ['invalid', '28d'],
  120. environment: ['staging'],
  121. },
  122. });
  123. const eventView = EventView.fromLocation(location);
  124. expect(eventView).toMatchObject({
  125. id: '42',
  126. name: 'best query',
  127. fields: [
  128. {field: 'count()', width: 123},
  129. {field: 'id', width: 456},
  130. ],
  131. sorts: generateSorts([AggregationKey.COUNT]),
  132. query: 'event.type:transaction',
  133. project: [123],
  134. start: undefined,
  135. end: undefined,
  136. statsPeriod: '28d',
  137. environment: ['staging'],
  138. });
  139. });
  140. it('includes start and end', function () {
  141. const location = TestStubs.location({
  142. query: {
  143. id: '42',
  144. name: 'best query',
  145. field: ['count()', 'id'],
  146. widths: ['123', '456'],
  147. sort: ['title', '-count'],
  148. query: 'event.type:transaction',
  149. project: ['123'],
  150. start: '2019-10-01T00:00:00',
  151. end: '2019-10-02T00:00:00',
  152. environment: ['staging'],
  153. },
  154. });
  155. const eventView = EventView.fromLocation(location);
  156. expect(eventView).toMatchObject({
  157. id: '42',
  158. name: 'best query',
  159. fields: [
  160. {field: 'count()', width: 123},
  161. {field: 'id', width: 456},
  162. ],
  163. sorts: generateSorts([AggregationKey.COUNT]),
  164. query: 'event.type:transaction',
  165. project: [123],
  166. start: '2019-10-01T00:00:00.000',
  167. end: '2019-10-02T00:00:00.000',
  168. environment: ['staging'],
  169. });
  170. });
  171. it('generates event view when there are no query strings', function () {
  172. const location = TestStubs.location({
  173. query: {},
  174. });
  175. const eventView = EventView.fromLocation(location);
  176. expect(eventView).toMatchObject({
  177. id: void 0,
  178. name: void 0,
  179. fields: [],
  180. sorts: [],
  181. query: '',
  182. project: [],
  183. start: void 0,
  184. end: void 0,
  185. statsPeriod: '14d',
  186. environment: [],
  187. yAxis: void 0,
  188. });
  189. });
  190. });
  191. describe('EventView.fromSavedQuery()', function () {
  192. it('maps basic properties of saved query', function () {
  193. const saved: SavedQuery = {
  194. id: '42',
  195. name: 'best query',
  196. fields: ['count()', 'id'],
  197. query: 'event.type:transaction',
  198. projects: [123],
  199. teams: ['myteams', 1],
  200. range: '14d',
  201. start: '2019-10-01T00:00:00',
  202. end: '2019-10-02T00:00:00',
  203. orderby: '-id',
  204. environment: ['staging'],
  205. display: 'previous',
  206. dataset: DiscoverDatasets.DISCOVER,
  207. dateCreated: '2019-10-30T06:13:17.632078Z',
  208. dateUpdated: '2019-10-30T06:13:17.632096Z',
  209. version: 2,
  210. };
  211. const eventView = EventView.fromSavedQuery(saved);
  212. expect(eventView).toMatchObject({
  213. id: saved.id,
  214. name: saved.name,
  215. fields: [
  216. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  217. {field: 'id', width: COL_WIDTH_UNDEFINED},
  218. ],
  219. sorts: [{field: 'id', kind: 'desc'}],
  220. query: 'event.type:transaction',
  221. project: [123],
  222. team: ['myteams', 1],
  223. start: undefined,
  224. end: undefined,
  225. // statsPeriod has precedence
  226. statsPeriod: '14d',
  227. environment: ['staging'],
  228. yAxis: undefined,
  229. display: 'previous',
  230. dataset: DiscoverDatasets.DISCOVER,
  231. });
  232. const eventView2 = EventView.fromSavedQuery({
  233. ...saved,
  234. range: undefined,
  235. });
  236. expect(eventView2).toMatchObject({
  237. id: saved.id,
  238. name: saved.name,
  239. fields: [
  240. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  241. {field: 'id', width: COL_WIDTH_UNDEFINED},
  242. ],
  243. sorts: [{field: 'id', kind: 'desc'}],
  244. query: 'event.type:transaction',
  245. project: [123],
  246. team: ['myteams', 1],
  247. start: '2019-10-01T00:00:00.000',
  248. end: '2019-10-02T00:00:00.000',
  249. statsPeriod: undefined,
  250. environment: ['staging'],
  251. });
  252. });
  253. it('maps saved query with no conditions', function () {
  254. const saved: SavedQuery = {
  255. orderby: '-count',
  256. name: 'foo bar',
  257. fields: ['release', 'count()'],
  258. widths: ['111', '222'],
  259. dateCreated: '2019-10-30T06:13:17.632078Z',
  260. environment: ['dev', 'production'],
  261. version: 2,
  262. dateUpdated: '2019-10-30T06:13:17.632096Z',
  263. id: '5',
  264. projects: [1],
  265. yAxis: ['count()'],
  266. };
  267. const eventView = EventView.fromSavedQuery(saved);
  268. const expected = {
  269. id: '5',
  270. name: 'foo bar',
  271. fields: [
  272. {field: 'release', width: 111},
  273. {field: 'count()', width: 222},
  274. ],
  275. sorts: generateSorts([AggregationKey.COUNT]),
  276. query: '',
  277. project: [1],
  278. environment: ['dev', 'production'],
  279. yAxis: 'count()',
  280. };
  281. expect(eventView).toMatchObject(expected);
  282. });
  283. it('maps properties from v2 saved query', function () {
  284. const saved: SavedQuery = {
  285. id: '42',
  286. projects: [123],
  287. name: 'best query',
  288. fields: ['count()', 'title'],
  289. range: '14d',
  290. start: '',
  291. end: '',
  292. dateCreated: '2019-10-30T06:13:17.632078Z',
  293. dateUpdated: '2019-10-30T06:13:17.632096Z',
  294. version: 2,
  295. };
  296. const eventView = EventView.fromSavedQuery(saved);
  297. expect(eventView.fields).toEqual([
  298. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  299. {field: 'title', width: COL_WIDTH_UNDEFINED},
  300. ]);
  301. expect(eventView.name).toEqual(saved.name);
  302. expect(eventView.statsPeriod).toEqual('14d');
  303. expect(eventView.start).toEqual(undefined);
  304. expect(eventView.end).toEqual(undefined);
  305. });
  306. it('saved queries are equal when start and end datetime differ in format', function () {
  307. const saved: SavedQuery = {
  308. orderby: '-count_timestamp',
  309. end: '2019-10-23T19:27:04+0000',
  310. name: 'release query',
  311. fields: ['release', 'count(timestamp)'],
  312. dateCreated: '2019-10-30T05:10:23.718937Z',
  313. environment: ['dev', 'production'],
  314. start: '2019-10-20T21:02:51+0000',
  315. version: 2,
  316. dateUpdated: '2019-10-30T07:25:58.291917Z',
  317. id: '3',
  318. projects: [1],
  319. };
  320. const eventView = EventView.fromSavedQuery(saved);
  321. const eventView2 = EventView.fromSavedQuery({
  322. ...saved,
  323. start: '2019-10-20T21:02:51Z',
  324. end: '2019-10-23T19:27:04Z',
  325. });
  326. expect(eventView.isEqualTo(eventView2)).toBe(true);
  327. const eventView3 = EventView.fromSavedQuery({
  328. ...saved,
  329. start: '2019-10-20T21:02:51Z',
  330. });
  331. expect(eventView.isEqualTo(eventView3)).toBe(true);
  332. const eventView4 = EventView.fromSavedQuery({
  333. ...saved,
  334. end: '2019-10-23T19:27:04Z',
  335. });
  336. expect(eventView.isEqualTo(eventView4)).toBe(true);
  337. });
  338. it('saved queries are not equal when datetime selection are invalid', function () {
  339. const saved: SavedQuery = {
  340. orderby: '-count_timestamp',
  341. end: '2019-10-23T19:27:04+0000',
  342. name: 'release query',
  343. fields: ['release', 'count(timestamp)'],
  344. dateCreated: '2019-10-30T05:10:23.718937Z',
  345. environment: ['dev', 'production'],
  346. start: '2019-10-20T21:02:51+0000',
  347. version: 2,
  348. dateUpdated: '2019-10-30T07:25:58.291917Z',
  349. id: '3',
  350. projects: [1],
  351. };
  352. const eventView = EventView.fromSavedQuery(saved);
  353. const eventView2 = EventView.fromSavedQuery({
  354. ...saved,
  355. start: '',
  356. });
  357. expect(eventView.isEqualTo(eventView2)).toBe(false);
  358. const eventView3 = EventView.fromSavedQuery({
  359. ...saved,
  360. end: '',
  361. });
  362. expect(eventView.isEqualTo(eventView3)).toBe(false);
  363. // this is expected since datetime (start and end) are normalized
  364. expect(eventView2.isEqualTo(eventView3)).toBe(true);
  365. });
  366. it('saved queries with undefined yAxis are defaulted to count() when comparing with isEqualTo', function () {
  367. const saved: SavedQuery = {
  368. orderby: '-count_timestamp',
  369. end: '2019-10-23T19:27:04+0000',
  370. name: 'release query',
  371. fields: ['release', 'count(timestamp)'],
  372. dateCreated: '2019-10-30T05:10:23.718937Z',
  373. environment: ['dev', 'production'],
  374. start: '2019-10-20T21:02:51+0000',
  375. version: 2,
  376. dateUpdated: '2019-10-30T07:25:58.291917Z',
  377. id: '3',
  378. projects: [1],
  379. };
  380. const eventView = EventView.fromSavedQuery(saved);
  381. const eventView2 = EventView.fromSavedQuery({
  382. ...saved,
  383. yAxis: ['count()'],
  384. });
  385. expect(eventView.isEqualTo(eventView2)).toBe(true);
  386. });
  387. it('uses the first yAxis from the SavedQuery', function () {
  388. const saved: SavedQuery = {
  389. id: '42',
  390. name: 'best query',
  391. fields: ['count()', 'id'],
  392. query: 'event.type:transaction',
  393. projects: [123],
  394. teams: ['myteams', 1],
  395. range: '14d',
  396. start: '2019-10-01T00:00:00',
  397. end: '2019-10-02T00:00:00',
  398. orderby: '-id',
  399. environment: ['staging'],
  400. display: 'previous',
  401. dataset: DiscoverDatasets.DISCOVER,
  402. yAxis: ['count()'],
  403. dateCreated: '2019-10-30T06:13:17.632078Z',
  404. dateUpdated: '2019-10-30T06:13:17.632096Z',
  405. version: 2,
  406. };
  407. const eventView = EventView.fromSavedQuery(saved);
  408. expect(eventView).toMatchObject({
  409. id: saved.id,
  410. name: saved.name,
  411. fields: [
  412. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  413. {field: 'id', width: COL_WIDTH_UNDEFINED},
  414. ],
  415. sorts: [{field: 'id', kind: 'desc'}],
  416. query: 'event.type:transaction',
  417. project: [123],
  418. team: ['myteams', 1],
  419. start: undefined,
  420. end: undefined,
  421. statsPeriod: '14d',
  422. environment: ['staging'],
  423. yAxis: 'count()',
  424. dataset: DiscoverDatasets.DISCOVER,
  425. display: 'previous',
  426. });
  427. });
  428. it('preserves utc with start/end', function () {
  429. const saved: SavedQuery = {
  430. id: '42',
  431. version: 2,
  432. projects: [123],
  433. name: 'best query',
  434. query: 'event.type:transaction',
  435. fields: ['count()', 'title'],
  436. start: '2019-10-20T21:02:51+0000',
  437. end: '2019-10-23T19:27:04+0000',
  438. utc: 'true',
  439. dateCreated: '2019-10-30T06:13:17.632078Z',
  440. dateUpdated: '2019-10-30T06:13:17.632096Z',
  441. };
  442. const eventView = EventView.fromSavedQuery(saved);
  443. expect(eventView).toMatchObject({
  444. id: saved.id,
  445. name: saved.name,
  446. fields: [
  447. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  448. {field: 'title', width: COL_WIDTH_UNDEFINED},
  449. ],
  450. query: 'event.type:transaction',
  451. start: '2019-10-20T21:02:51.000',
  452. end: '2019-10-23T19:27:04.000',
  453. utc: 'true',
  454. });
  455. });
  456. });
  457. describe('EventView.fromNewQueryWithPageFilters()', function () {
  458. const prebuiltQuery: NewQuery = {
  459. id: undefined,
  460. name: 'Page Filter Events',
  461. query: '',
  462. projects: undefined,
  463. fields: ['title', 'project', 'timestamp'],
  464. orderby: '-timestamp',
  465. version: 2,
  466. };
  467. it('maps basic properties of a prebuilt query', function () {
  468. const pageFilters = PageFilters();
  469. const eventView = EventView.fromNewQueryWithPageFilters(prebuiltQuery, pageFilters);
  470. expect(eventView).toMatchObject({
  471. id: undefined,
  472. name: 'Page Filter Events',
  473. fields: [{field: 'title'}, {field: 'project'}, {field: 'timestamp'}],
  474. sorts: [{field: 'timestamp', kind: 'desc'}],
  475. query: '',
  476. project: [],
  477. start: undefined,
  478. end: undefined,
  479. statsPeriod: '14d',
  480. environment: [],
  481. yAxis: undefined,
  482. });
  483. });
  484. it('merges page filter values', function () {
  485. const pageFilters = TestStubs.PageFilters({
  486. datetime: {
  487. period: '3d',
  488. },
  489. projects: [42],
  490. environments: ['prod'],
  491. });
  492. const eventView = EventView.fromNewQueryWithPageFilters(prebuiltQuery, pageFilters);
  493. expect(eventView).toMatchObject({
  494. id: undefined,
  495. name: 'Page Filter Events',
  496. fields: [{field: 'title'}, {field: 'project'}, {field: 'timestamp'}],
  497. sorts: [{field: 'timestamp', kind: 'desc'}],
  498. query: '',
  499. project: [42],
  500. start: undefined,
  501. end: undefined,
  502. statsPeriod: '3d',
  503. environment: ['prod'],
  504. yAxis: undefined,
  505. });
  506. });
  507. });
  508. describe('EventView.fromNewQueryWithLocation()', function () {
  509. const prebuiltQuery: NewQuery = {
  510. id: undefined,
  511. name: 'Sampled Events',
  512. query: '',
  513. projects: [],
  514. fields: ['title', 'event.type', 'project', 'user', 'timestamp'],
  515. orderby: '-timestamp',
  516. version: 2,
  517. };
  518. it('maps basic properties of a prebuilt query', function () {
  519. const location = TestStubs.location({
  520. query: {
  521. statsPeriod: '99d',
  522. },
  523. });
  524. const eventView = EventView.fromNewQueryWithLocation(prebuiltQuery, location);
  525. expect(eventView).toMatchObject({
  526. id: undefined,
  527. name: 'Sampled Events',
  528. fields: [
  529. {field: 'title'},
  530. {field: 'event.type'},
  531. {field: 'project'},
  532. {field: 'user'},
  533. {field: 'timestamp'},
  534. ],
  535. sorts: [{field: 'timestamp', kind: 'desc'}],
  536. query: '',
  537. project: [],
  538. start: undefined,
  539. end: undefined,
  540. // statsPeriod has precedence
  541. statsPeriod: '99d',
  542. environment: [],
  543. yAxis: undefined,
  544. });
  545. });
  546. it('merges global selection values', function () {
  547. const location = TestStubs.location({
  548. query: {
  549. statsPeriod: '99d',
  550. project: ['456'],
  551. environment: ['prod'],
  552. },
  553. });
  554. const eventView = EventView.fromNewQueryWithLocation(prebuiltQuery, location);
  555. expect(eventView).toMatchObject({
  556. id: undefined,
  557. name: 'Sampled Events',
  558. fields: [
  559. {field: 'title'},
  560. {field: 'event.type'},
  561. {field: 'project'},
  562. {field: 'user'},
  563. {field: 'timestamp'},
  564. ],
  565. sorts: [{field: 'timestamp', kind: 'desc'}],
  566. query: '',
  567. project: [456],
  568. start: undefined,
  569. end: undefined,
  570. statsPeriod: '99d',
  571. environment: ['prod'],
  572. yAxis: undefined,
  573. });
  574. });
  575. it('new query takes precedence over global selection values', function () {
  576. const location = TestStubs.location({
  577. query: {
  578. statsPeriod: '99d',
  579. project: ['456'],
  580. environment: ['prod'],
  581. },
  582. });
  583. const prebuiltQuery2: NewQuery = {
  584. ...prebuiltQuery,
  585. range: '42d',
  586. projects: [987],
  587. environment: ['staging'],
  588. };
  589. const eventView = EventView.fromNewQueryWithLocation(prebuiltQuery2, location);
  590. expect(eventView).toMatchObject({
  591. id: undefined,
  592. name: 'Sampled Events',
  593. fields: [
  594. {field: 'title'},
  595. {field: 'event.type'},
  596. {field: 'project'},
  597. {field: 'user'},
  598. {field: 'timestamp'},
  599. ],
  600. sorts: [{field: 'timestamp', kind: 'desc'}],
  601. query: '',
  602. project: [987],
  603. start: undefined,
  604. end: undefined,
  605. statsPeriod: '42d',
  606. environment: ['staging'],
  607. yAxis: undefined,
  608. });
  609. // also test start and end
  610. const location2 = TestStubs.location({
  611. query: {
  612. start: '2019-10-01T00:00:00',
  613. end: '2019-10-02T00:00:00',
  614. project: ['456'],
  615. environment: ['prod'],
  616. },
  617. });
  618. const prebuiltQuery3: NewQuery = {
  619. ...prebuiltQuery,
  620. start: '2019-10-01T00:00:00',
  621. end: '2019-10-02T00:00:00',
  622. projects: [987],
  623. environment: ['staging'],
  624. };
  625. const eventView2 = EventView.fromNewQueryWithLocation(prebuiltQuery3, location2);
  626. expect(eventView2).toMatchObject({
  627. id: undefined,
  628. name: 'Sampled Events',
  629. fields: [
  630. {field: 'title'},
  631. {field: 'event.type'},
  632. {field: 'project'},
  633. {field: 'user'},
  634. {field: 'timestamp'},
  635. ],
  636. sorts: [{field: 'timestamp', kind: 'desc'}],
  637. query: '',
  638. project: [987],
  639. start: '2019-10-01T00:00:00.000',
  640. end: '2019-10-02T00:00:00.000',
  641. statsPeriod: undefined,
  642. environment: ['staging'],
  643. yAxis: undefined,
  644. });
  645. });
  646. });
  647. describe('EventView.fromSavedQueryOrLocation()', function () {
  648. it('maps basic properties of saved query', function () {
  649. const saved: SavedQuery = {
  650. id: '42',
  651. name: 'best query',
  652. fields: ['count()', 'id'],
  653. query: 'event.type:transaction',
  654. projects: [123],
  655. range: '14d',
  656. start: '2019-10-01T00:00:00',
  657. end: '2019-10-02T00:00:00',
  658. orderby: '-id',
  659. environment: ['staging'],
  660. display: 'previous',
  661. dataset: DiscoverDatasets.DISCOVER,
  662. dateUpdated: '2019-10-30T06:13:17.632096Z',
  663. dateCreated: '2019-10-30T06:13:17.632078Z',
  664. version: 2,
  665. };
  666. const location = TestStubs.location({
  667. query: {
  668. statsPeriod: '14d',
  669. project: ['123'],
  670. team: ['myteams', '1', '2'],
  671. environment: ['staging'],
  672. },
  673. });
  674. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  675. expect(eventView).toMatchObject({
  676. id: saved.id,
  677. name: saved.name,
  678. fields: [
  679. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  680. {field: 'id', width: COL_WIDTH_UNDEFINED},
  681. ],
  682. sorts: [{field: 'id', kind: 'desc'}],
  683. query: 'event.type:transaction',
  684. project: [123],
  685. team: ['myteams', 1, 2],
  686. start: undefined,
  687. end: undefined,
  688. // statsPeriod has precedence
  689. statsPeriod: '14d',
  690. environment: ['staging'],
  691. yAxis: undefined,
  692. display: 'previous',
  693. dataset: DiscoverDatasets.DISCOVER,
  694. });
  695. const savedQuery2: SavedQuery = {...saved, range: undefined};
  696. const location2 = TestStubs.location({
  697. query: {
  698. project: ['123'],
  699. environment: ['staging'],
  700. start: '2019-10-01T00:00:00',
  701. end: '2019-10-02T00:00:00',
  702. },
  703. });
  704. const eventView2 = EventView.fromSavedQueryOrLocation(savedQuery2, location2);
  705. expect(eventView2).toMatchObject({
  706. id: saved.id,
  707. name: saved.name,
  708. fields: [
  709. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  710. {field: 'id', width: COL_WIDTH_UNDEFINED},
  711. ],
  712. sorts: [{field: 'id', kind: 'desc'}],
  713. query: 'event.type:transaction',
  714. project: [123],
  715. start: '2019-10-01T00:00:00.000',
  716. end: '2019-10-02T00:00:00.000',
  717. statsPeriod: undefined,
  718. environment: ['staging'],
  719. });
  720. });
  721. it('overrides saved query params with location params', function () {
  722. const saved: SavedQuery = {
  723. id: '42',
  724. name: 'best query',
  725. fields: ['count()', 'id'],
  726. query: 'event.type:transaction',
  727. projects: [123],
  728. range: '14d',
  729. start: '2019-10-01T00:00:00',
  730. end: '2019-10-02T00:00:00',
  731. orderby: '-id',
  732. environment: ['staging'],
  733. display: 'previous',
  734. dataset: DiscoverDatasets.DISCOVER,
  735. dateUpdated: '2019-10-30T06:13:17.632096Z',
  736. dateCreated: '2019-10-30T06:13:17.632078Z',
  737. version: 2,
  738. };
  739. const location = TestStubs.location({
  740. query: {
  741. id: '42',
  742. statsPeriod: '7d',
  743. project: ['3'],
  744. },
  745. });
  746. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  747. expect(eventView).toMatchObject({
  748. id: saved.id,
  749. name: saved.name,
  750. fields: [
  751. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  752. {field: 'id', width: COL_WIDTH_UNDEFINED},
  753. ],
  754. sorts: [{field: 'id', kind: 'desc'}],
  755. query: 'event.type:transaction',
  756. project: [3],
  757. start: undefined,
  758. end: undefined,
  759. // statsPeriod has precedence
  760. statsPeriod: '7d',
  761. environment: [],
  762. yAxis: undefined,
  763. display: 'previous',
  764. dataset: DiscoverDatasets.DISCOVER,
  765. });
  766. });
  767. it('maps saved query with no conditions', function () {
  768. const saved: SavedQuery = {
  769. orderby: '-count',
  770. name: 'foo bar',
  771. fields: ['release', 'count()'],
  772. widths: ['111', '222'],
  773. dateCreated: '2019-10-30T06:13:17.632078Z',
  774. query: '',
  775. environment: [],
  776. version: 2,
  777. dateUpdated: '2019-10-30T06:13:17.632096Z',
  778. projects: [123],
  779. id: '5',
  780. yAxis: ['count()'],
  781. };
  782. const location = TestStubs.location({
  783. query: {
  784. id: '5',
  785. project: ['1'],
  786. },
  787. });
  788. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  789. const expected = {
  790. id: '5',
  791. name: 'foo bar',
  792. fields: [
  793. {field: 'release', width: 111},
  794. {field: 'count()', width: 222},
  795. ],
  796. sorts: generateSorts([AggregationKey.COUNT]),
  797. query: '',
  798. project: [1],
  799. yAxis: 'count()',
  800. };
  801. expect(eventView).toMatchObject(expected);
  802. });
  803. it('maps query with cleared conditions', function () {
  804. const saved: SavedQuery = {
  805. id: '42',
  806. name: 'best query',
  807. fields: ['count()', 'id'],
  808. query: 'event.type:transaction',
  809. projects: [123],
  810. range: '14d',
  811. start: '2019-10-01T00:00:00',
  812. end: '2019-10-02T00:00:00',
  813. orderby: '-id',
  814. environment: ['staging'],
  815. display: 'previous',
  816. dateUpdated: '2019-10-30T06:13:17.632096Z',
  817. dateCreated: '2019-10-30T06:13:17.632078Z',
  818. version: 2,
  819. };
  820. const location = TestStubs.location({
  821. query: {
  822. id: '42',
  823. statsPeriod: '7d',
  824. },
  825. });
  826. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  827. expect(eventView).toMatchObject({
  828. id: saved.id,
  829. name: saved.name,
  830. fields: [
  831. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  832. {field: 'id', width: COL_WIDTH_UNDEFINED},
  833. ],
  834. sorts: [{field: 'id', kind: 'desc'}],
  835. query: 'event.type:transaction',
  836. start: undefined,
  837. end: undefined,
  838. // statsPeriod has precedence
  839. statsPeriod: '7d',
  840. environment: [],
  841. yAxis: undefined,
  842. display: 'previous',
  843. });
  844. const location2 = TestStubs.location({
  845. query: {
  846. id: '42',
  847. statsPeriod: '7d',
  848. query: '',
  849. },
  850. });
  851. const eventView2 = EventView.fromSavedQueryOrLocation(saved, location2);
  852. expect(eventView2).toMatchObject({
  853. id: saved.id,
  854. name: saved.name,
  855. fields: [
  856. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  857. {field: 'id', width: COL_WIDTH_UNDEFINED},
  858. ],
  859. sorts: [{field: 'id', kind: 'desc'}],
  860. query: '',
  861. start: undefined,
  862. end: undefined,
  863. // statsPeriod has precedence
  864. statsPeriod: '7d',
  865. environment: [],
  866. yAxis: undefined,
  867. display: 'previous',
  868. });
  869. });
  870. it('event views are equal when start and end datetime differ in format', function () {
  871. const saved: SavedQuery = {
  872. orderby: '-count_timestamp',
  873. end: '2019-10-23T19:27:04+0000',
  874. name: 'release query',
  875. fields: ['release', 'count(timestamp)'],
  876. dateCreated: '2019-10-30T05:10:23.718937Z',
  877. environment: ['dev', 'production'],
  878. start: '2019-10-20T21:02:51+0000',
  879. projects: [123],
  880. version: 2,
  881. dateUpdated: '2019-10-30T07:25:58.291917Z',
  882. id: '3',
  883. };
  884. const location = TestStubs.location({
  885. query: {
  886. id: '3',
  887. start: '2019-10-20T21:02:51+0000',
  888. end: '2019-10-23T19:27:04+0000',
  889. },
  890. });
  891. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  892. const location2 = TestStubs.location({
  893. query: {
  894. id: '3',
  895. start: '2019-10-20T21:02:51Z',
  896. end: '2019-10-23T19:27:04Z',
  897. },
  898. });
  899. const eventView2 = EventView.fromSavedQueryOrLocation(saved, location2);
  900. expect(eventView.isEqualTo(eventView2)).toBe(true);
  901. const location3 = TestStubs.location({
  902. query: {
  903. id: '3',
  904. start: '2019-10-20T21:02:51Z',
  905. end: '2019-10-23T19:27:04+0000',
  906. },
  907. });
  908. const eventView3 = EventView.fromSavedQueryOrLocation(saved, location3);
  909. expect(eventView.isEqualTo(eventView3)).toBe(true);
  910. const location4 = TestStubs.location({
  911. query: {
  912. id: '3',
  913. start: '2019-10-20T21:02:51+0000',
  914. end: '2019-10-23T19:27:04Z',
  915. },
  916. });
  917. const eventView4 = EventView.fromSavedQueryOrLocation(saved, location4);
  918. expect(eventView.isEqualTo(eventView4)).toBe(true);
  919. });
  920. it('event views are not equal when datetime selection are invalid', function () {
  921. const saved: SavedQuery = {
  922. orderby: '-count_timestamp',
  923. end: '2019-10-23T19:27:04+0000',
  924. name: 'release query',
  925. fields: ['release', 'count(timestamp)'],
  926. dateCreated: '2019-10-30T05:10:23.718937Z',
  927. environment: ['dev', 'production'],
  928. start: '2019-10-20T21:02:51+0000',
  929. version: 2,
  930. dateUpdated: '2019-10-30T07:25:58.291917Z',
  931. id: '3',
  932. projects: [1],
  933. };
  934. const location = TestStubs.location({
  935. query: {
  936. id: '3',
  937. end: '2019-10-23T19:27:04+0000',
  938. start: '2019-10-20T21:02:51+0000',
  939. },
  940. });
  941. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  942. const location2 = TestStubs.location({
  943. query: {
  944. id: '3',
  945. end: '2019-10-23T19:27:04+0000',
  946. start: '',
  947. },
  948. });
  949. const eventView2 = EventView.fromSavedQueryOrLocation(saved, location2);
  950. expect(eventView.isEqualTo(eventView2)).toBe(false);
  951. const location3 = TestStubs.location({
  952. query: {
  953. id: '3',
  954. end: '',
  955. start: '2019-10-20T21:02:51+0000',
  956. },
  957. });
  958. const eventView3 = EventView.fromSavedQueryOrLocation(saved, location3);
  959. expect(eventView.isEqualTo(eventView3)).toBe(false);
  960. // this is expected since datetime (start and end) are normalized
  961. expect(eventView2.isEqualTo(eventView3)).toBe(true);
  962. });
  963. it('uses the first yAxis from the SavedQuery', function () {
  964. const saved: SavedQuery = {
  965. id: '42',
  966. name: 'best query',
  967. fields: ['count()', 'id'],
  968. query: 'event.type:transaction',
  969. projects: [123],
  970. range: '14d',
  971. start: '2019-10-01T00:00:00',
  972. end: '2019-10-02T00:00:00',
  973. orderby: '-id',
  974. environment: ['staging'],
  975. display: 'previous',
  976. yAxis: ['count()', 'failure_count()'],
  977. dateCreated: '2019-10-30T05:10:23.718937Z',
  978. dateUpdated: '2019-10-30T07:25:58.291917Z',
  979. version: 2,
  980. };
  981. const location = TestStubs.location({
  982. query: {
  983. statsPeriod: '14d',
  984. project: ['123'],
  985. team: ['myteams', '1', '2'],
  986. environment: ['staging'],
  987. },
  988. });
  989. const eventView = EventView.fromSavedQueryOrLocation(saved, location);
  990. expect(eventView).toMatchObject({
  991. id: saved.id,
  992. name: saved.name,
  993. fields: [
  994. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  995. {field: 'id', width: COL_WIDTH_UNDEFINED},
  996. ],
  997. sorts: [{field: 'id', kind: 'desc'}],
  998. query: 'event.type:transaction',
  999. project: [123],
  1000. team: ['myteams', 1, 2],
  1001. start: undefined,
  1002. end: undefined,
  1003. statsPeriod: '14d',
  1004. environment: ['staging'],
  1005. yAxis: 'count()',
  1006. display: 'previous',
  1007. });
  1008. });
  1009. it('filters out invalid teams', function () {
  1010. const eventView = EventView.fromSavedQueryOrLocation(
  1011. undefined,
  1012. TestStubs.location({
  1013. query: {
  1014. statsPeriod: '14d',
  1015. project: ['123'],
  1016. team: ['myteams', '1', 'unassigned'],
  1017. environment: ['staging'],
  1018. },
  1019. })
  1020. );
  1021. expect(eventView.team).toEqual(['myteams', 1]);
  1022. });
  1023. });
  1024. describe('EventView.generateQueryStringObject()', function () {
  1025. it('skips empty values', function () {
  1026. const eventView = new EventView({
  1027. ...REQUIRED_CONSTRUCTOR_PROPS,
  1028. fields: generateFields(['id', 'title']),
  1029. sorts: [],
  1030. project: [],
  1031. environment: [],
  1032. statsPeriod: '',
  1033. start: undefined,
  1034. end: undefined,
  1035. yAxis: undefined,
  1036. display: 'previous',
  1037. });
  1038. const expected = {
  1039. id: undefined,
  1040. name: undefined,
  1041. field: ['id', 'title'],
  1042. widths: [],
  1043. sort: [],
  1044. query: '',
  1045. project: [],
  1046. environment: [],
  1047. display: 'previous',
  1048. yAxis: 'count()',
  1049. };
  1050. expect(eventView.generateQueryStringObject()).toEqual(expected);
  1051. });
  1052. it('generates query string object', function () {
  1053. const state: ConstructorParameters<typeof EventView>[0] = {
  1054. ...REQUIRED_CONSTRUCTOR_PROPS,
  1055. id: '1234',
  1056. name: 'best query',
  1057. fields: [
  1058. {field: 'count()', width: 123},
  1059. {field: 'project.id', width: 456},
  1060. ],
  1061. sorts: generateSorts([AggregationKey.COUNT]),
  1062. query: 'event.type:error',
  1063. project: [42],
  1064. start: '2019-10-01T00:00:00',
  1065. end: '2019-10-02T00:00:00',
  1066. statsPeriod: '14d',
  1067. environment: ['staging'],
  1068. yAxis: 'count()',
  1069. display: 'releases',
  1070. interval: '1m',
  1071. };
  1072. const eventView = new EventView(state);
  1073. const expected = {
  1074. id: '1234',
  1075. name: 'best query',
  1076. field: ['count()', 'project.id'],
  1077. widths: [123, 456],
  1078. sort: ['-count'],
  1079. query: 'event.type:error',
  1080. project: [42],
  1081. start: '2019-10-01T00:00:00',
  1082. end: '2019-10-02T00:00:00',
  1083. statsPeriod: '14d',
  1084. environment: ['staging'],
  1085. yAxis: 'count()',
  1086. display: 'releases',
  1087. interval: '1m',
  1088. };
  1089. expect(eventView.generateQueryStringObject()).toEqual(expected);
  1090. });
  1091. it('encodes fields', function () {
  1092. const eventView = new EventView({
  1093. ...REQUIRED_CONSTRUCTOR_PROPS,
  1094. fields: [{field: 'id'}, {field: 'title'}],
  1095. sorts: [],
  1096. });
  1097. const query = eventView.generateQueryStringObject();
  1098. expect(query.field).toEqual(['id', 'title']);
  1099. });
  1100. it('returns a copy of data preventing mutation', function () {
  1101. const eventView = new EventView({
  1102. ...REQUIRED_CONSTRUCTOR_PROPS,
  1103. fields: [{field: 'id'}, {field: 'title'}],
  1104. sorts: [],
  1105. });
  1106. const query = eventView.generateQueryStringObject();
  1107. if (Array.isArray(query.field)) {
  1108. query.field.push('newthing');
  1109. }
  1110. // Getting the query again should return the original values.
  1111. const secondQuery = eventView.generateQueryStringObject();
  1112. expect(secondQuery.field).toEqual(['id', 'title']);
  1113. expect(query).not.toEqual(secondQuery);
  1114. });
  1115. });
  1116. describe('EventView.getEventsAPIPayload()', function () {
  1117. it('generates the API payload', function () {
  1118. const eventView = new EventView({
  1119. ...REQUIRED_CONSTRUCTOR_PROPS,
  1120. id: '34',
  1121. name: 'amazing query',
  1122. fields: generateFields(['id']),
  1123. sorts: generateSorts(['id']),
  1124. query: 'event.type:csp',
  1125. project: [567],
  1126. environment: ['prod'],
  1127. yAxis: 'users',
  1128. display: 'releases',
  1129. });
  1130. expect(eventView.getEventsAPIPayload(TestStubs.location())).toEqual({
  1131. field: ['id'],
  1132. per_page: 50,
  1133. sort: '-id',
  1134. query: 'event.type:csp',
  1135. project: ['567'],
  1136. environment: ['prod'],
  1137. statsPeriod: '14d',
  1138. });
  1139. });
  1140. it('does not append query conditions in location', function () {
  1141. const eventView = new EventView({
  1142. ...REQUIRED_CONSTRUCTOR_PROPS,
  1143. fields: generateFields(['id']),
  1144. sorts: [],
  1145. query: 'event.type:csp',
  1146. });
  1147. const location = TestStubs.location({
  1148. query: {
  1149. query: 'TypeError',
  1150. },
  1151. });
  1152. expect(eventView.getEventsAPIPayload(location).query).toEqual('event.type:csp');
  1153. });
  1154. it('only includes at most one sort key', function () {
  1155. const eventView = new EventView({
  1156. ...REQUIRED_CONSTRUCTOR_PROPS,
  1157. fields: generateFields(['count()', 'title']),
  1158. sorts: generateSorts(['title', AggregationKey.COUNT]),
  1159. query: 'event.type:csp',
  1160. });
  1161. const location = TestStubs.location({
  1162. query: {},
  1163. });
  1164. expect(eventView.getEventsAPIPayload(location).sort).toEqual('-title');
  1165. });
  1166. it('only includes sort keys that are defined in fields', function () {
  1167. const eventView = new EventView({
  1168. ...REQUIRED_CONSTRUCTOR_PROPS,
  1169. fields: generateFields(['title', 'count()']),
  1170. sorts: generateSorts(['project', AggregationKey.COUNT]),
  1171. query: 'event.type:csp',
  1172. });
  1173. const location = TestStubs.location({
  1174. query: {},
  1175. });
  1176. expect(eventView.getEventsAPIPayload(location).sort).toEqual('-count');
  1177. });
  1178. it('only includes relevant query strings', function () {
  1179. const eventView = new EventView({
  1180. ...REQUIRED_CONSTRUCTOR_PROPS,
  1181. fields: generateFields(['title', 'count()']),
  1182. sorts: generateSorts(['project', AggregationKey.COUNT]),
  1183. query: 'event.type:csp',
  1184. });
  1185. const location = TestStubs.location({
  1186. query: {
  1187. start: '2020-08-12 12:13:14',
  1188. end: '2020-08-26 12:13:14',
  1189. utc: 'true',
  1190. statsPeriod: '14d',
  1191. cursor: 'some cursor',
  1192. yAxis: 'count()',
  1193. // irrelevant query strings
  1194. bestCountry: 'canada',
  1195. project: '1234',
  1196. environment: ['staging'],
  1197. },
  1198. });
  1199. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1200. project: [],
  1201. environment: [],
  1202. utc: 'true',
  1203. statsPeriod: '14d',
  1204. field: ['title', 'count()'],
  1205. per_page: 50,
  1206. query: 'event.type:csp',
  1207. sort: '-count',
  1208. cursor: 'some cursor',
  1209. });
  1210. });
  1211. it('includes default coerced statsPeriod when omitted or is invalid', function () {
  1212. const eventView = new EventView({
  1213. ...REQUIRED_CONSTRUCTOR_PROPS,
  1214. fields: generateFields(['title', 'count()']),
  1215. sorts: generateSorts(['project', AggregationKey.COUNT]),
  1216. query: 'event.type:csp',
  1217. project: [1234],
  1218. environment: ['staging'],
  1219. });
  1220. const location = TestStubs.location({
  1221. query: {
  1222. start: '',
  1223. end: '',
  1224. utc: 'true',
  1225. // invalid statsPeriod string
  1226. statsPeriod: 'invalid',
  1227. cursor: 'some cursor',
  1228. },
  1229. });
  1230. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1231. project: ['1234'],
  1232. environment: ['staging'],
  1233. utc: 'true',
  1234. statsPeriod: '14d',
  1235. field: ['title', 'count()'],
  1236. per_page: 50,
  1237. query: 'event.type:csp',
  1238. sort: '-count',
  1239. cursor: 'some cursor',
  1240. });
  1241. const location2 = TestStubs.location({
  1242. query: {
  1243. start: '',
  1244. end: '',
  1245. utc: 'true',
  1246. // statsPeriod is omitted here
  1247. cursor: 'some cursor',
  1248. },
  1249. });
  1250. expect(eventView.getEventsAPIPayload(location2)).toEqual({
  1251. project: ['1234'],
  1252. environment: ['staging'],
  1253. utc: 'true',
  1254. statsPeriod: '14d',
  1255. field: ['title', 'count()'],
  1256. per_page: 50,
  1257. query: 'event.type:csp',
  1258. sort: '-count',
  1259. cursor: 'some cursor',
  1260. });
  1261. });
  1262. it('includes default coerced statsPeriod when either start or end is only provided', function () {
  1263. const eventView = new EventView({
  1264. ...REQUIRED_CONSTRUCTOR_PROPS,
  1265. fields: generateFields(['title', 'count()']),
  1266. sorts: generateSorts(['project', AggregationKey.COUNT]),
  1267. query: 'event.type:csp',
  1268. project: [1234],
  1269. environment: ['staging'],
  1270. });
  1271. const location = TestStubs.location({
  1272. query: {
  1273. start: '',
  1274. utc: 'true',
  1275. statsPeriod: 'invalid',
  1276. cursor: 'some cursor',
  1277. },
  1278. });
  1279. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1280. project: ['1234'],
  1281. environment: ['staging'],
  1282. utc: 'true',
  1283. statsPeriod: '14d',
  1284. field: ['title', 'count()'],
  1285. per_page: 50,
  1286. query: 'event.type:csp',
  1287. sort: '-count',
  1288. cursor: 'some cursor',
  1289. });
  1290. const location2 = TestStubs.location({
  1291. query: {
  1292. end: '',
  1293. utc: 'true',
  1294. statsPeriod: 'invalid',
  1295. cursor: 'some cursor',
  1296. },
  1297. });
  1298. expect(eventView.getEventsAPIPayload(location2)).toEqual({
  1299. project: ['1234'],
  1300. environment: ['staging'],
  1301. utc: 'true',
  1302. statsPeriod: '14d',
  1303. field: ['title', 'count()'],
  1304. per_page: 50,
  1305. query: 'event.type:csp',
  1306. sort: '-count',
  1307. cursor: 'some cursor',
  1308. });
  1309. });
  1310. it('includes start and end', function () {
  1311. const eventView = new EventView({
  1312. ...REQUIRED_CONSTRUCTOR_PROPS,
  1313. fields: generateFields(['title', 'count()']),
  1314. sorts: generateSorts([AggregationKey.COUNT]),
  1315. query: 'event.type:csp',
  1316. start: '2019-10-01T00:00:00',
  1317. end: '2019-10-02T00:00:00',
  1318. environment: [],
  1319. project: [],
  1320. });
  1321. const location = TestStubs.location({
  1322. query: {
  1323. // these should not be part of the API payload
  1324. statsPeriod: '55d',
  1325. period: '55d',
  1326. },
  1327. });
  1328. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1329. field: ['title', 'count()'],
  1330. sort: '-count',
  1331. query: 'event.type:csp',
  1332. start: '2019-10-01T00:00:00.000',
  1333. end: '2019-10-02T00:00:00.000',
  1334. per_page: 50,
  1335. project: [],
  1336. environment: [],
  1337. });
  1338. });
  1339. it("an eventview's date selection has higher precedence than the date selection in the query string", function () {
  1340. const initialState = {
  1341. fields: generateFields(['title', 'count()']),
  1342. sorts: generateSorts([AggregationKey.COUNT]),
  1343. query: 'event.type:csp',
  1344. environment: [],
  1345. project: [],
  1346. };
  1347. const output = {
  1348. field: ['title', 'count()'],
  1349. sort: '-count',
  1350. query: 'event.type:csp',
  1351. per_page: 50,
  1352. project: [],
  1353. environment: [],
  1354. };
  1355. // eventview's statsPeriod has highest precedence
  1356. let eventView = new EventView({
  1357. ...REQUIRED_CONSTRUCTOR_PROPS,
  1358. ...initialState,
  1359. statsPeriod: '90d',
  1360. start: '2019-10-01T00:00:00',
  1361. end: '2019-10-02T00:00:00',
  1362. });
  1363. let location = TestStubs.location({
  1364. query: {
  1365. // these should not be part of the API payload
  1366. statsPeriod: '55d',
  1367. period: '30d',
  1368. start: '2020-10-01T00:00:00',
  1369. end: '2020-10-02T00:00:00',
  1370. },
  1371. });
  1372. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1373. ...output,
  1374. statsPeriod: '90d',
  1375. });
  1376. // eventview's start/end has higher precedence than the date selection in the query string
  1377. eventView = new EventView({
  1378. ...REQUIRED_CONSTRUCTOR_PROPS,
  1379. ...initialState,
  1380. start: '2019-10-01T00:00:00',
  1381. end: '2019-10-02T00:00:00',
  1382. });
  1383. location = TestStubs.location({
  1384. query: {
  1385. // these should not be part of the API payload
  1386. statsPeriod: '55d',
  1387. period: '30d',
  1388. start: '2020-10-01T00:00:00',
  1389. end: '2020-10-02T00:00:00',
  1390. },
  1391. });
  1392. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1393. ...output,
  1394. start: '2019-10-01T00:00:00.000',
  1395. end: '2019-10-02T00:00:00.000',
  1396. });
  1397. // the date selection in the query string should be applied as expected
  1398. eventView = new EventView({
  1399. ...REQUIRED_CONSTRUCTOR_PROPS,
  1400. ...initialState,
  1401. });
  1402. location = TestStubs.location({
  1403. query: {
  1404. statsPeriod: '55d',
  1405. period: '30d',
  1406. start: '2020-10-01T00:00:00',
  1407. end: '2020-10-02T00:00:00',
  1408. },
  1409. });
  1410. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1411. ...output,
  1412. statsPeriod: '55d',
  1413. });
  1414. location = TestStubs.location({
  1415. query: {
  1416. period: '30d',
  1417. start: '2020-10-01T00:00:00',
  1418. end: '2020-10-02T00:00:00',
  1419. },
  1420. });
  1421. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1422. ...output,
  1423. statsPeriod: '30d',
  1424. });
  1425. location = TestStubs.location({
  1426. query: {
  1427. start: '2020-10-01T00:00:00',
  1428. end: '2020-10-02T00:00:00',
  1429. },
  1430. });
  1431. expect(eventView.getEventsAPIPayload(location)).toEqual({
  1432. ...output,
  1433. start: '2020-10-01T00:00:00.000',
  1434. end: '2020-10-02T00:00:00.000',
  1435. });
  1436. });
  1437. });
  1438. describe('EventView.getFacetsAPIPayload()', function () {
  1439. it('only includes relevant query strings', function () {
  1440. const eventView = new EventView({
  1441. ...REQUIRED_CONSTRUCTOR_PROPS,
  1442. fields: generateFields(['title', 'count()']),
  1443. sorts: generateSorts(['project', AggregationKey.COUNT]),
  1444. query: 'event.type:csp',
  1445. });
  1446. const location = TestStubs.location({
  1447. query: {
  1448. start: '',
  1449. end: '',
  1450. utc: 'true',
  1451. statsPeriod: '14d',
  1452. // irrelevant query strings
  1453. bestCountry: 'canada',
  1454. cursor: 'some cursor',
  1455. sort: 'the world',
  1456. project: '1234',
  1457. environment: ['staging'],
  1458. display: 'releases',
  1459. },
  1460. });
  1461. expect(eventView.getFacetsAPIPayload(location)).toEqual({
  1462. project: [],
  1463. environment: [],
  1464. utc: 'true',
  1465. statsPeriod: '14d',
  1466. query: 'event.type:csp',
  1467. });
  1468. });
  1469. });
  1470. describe('EventView.toNewQuery()', function () {
  1471. const state: ConstructorParameters<typeof EventView>[0] = {
  1472. ...REQUIRED_CONSTRUCTOR_PROPS,
  1473. id: '1234',
  1474. name: 'best query',
  1475. fields: [
  1476. {field: 'count()', width: 123},
  1477. {field: 'project.id', width: 456},
  1478. ],
  1479. sorts: generateSorts([AggregationKey.COUNT]),
  1480. query: 'event.type:error',
  1481. project: [42],
  1482. start: '2019-10-01T00:00:00',
  1483. end: '2019-10-02T00:00:00',
  1484. statsPeriod: '14d',
  1485. environment: ['staging'],
  1486. display: 'releases',
  1487. dataset: DiscoverDatasets.DISCOVER,
  1488. };
  1489. it('outputs the right fields', function () {
  1490. const eventView = new EventView(state);
  1491. const output = eventView.toNewQuery();
  1492. const expected = {
  1493. version: 2,
  1494. id: '1234',
  1495. name: 'best query',
  1496. fields: ['count()', 'project.id'],
  1497. widths: ['123', '456'],
  1498. orderby: '-count',
  1499. query: 'event.type:error',
  1500. projects: [42],
  1501. start: '2019-10-01T00:00:00',
  1502. end: '2019-10-02T00:00:00',
  1503. range: '14d',
  1504. environment: ['staging'],
  1505. display: 'releases',
  1506. dataset: DiscoverDatasets.DISCOVER,
  1507. };
  1508. expect(output).toEqual(expected);
  1509. });
  1510. it('omits query when query is an empty string', function () {
  1511. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  1512. ...state,
  1513. };
  1514. modifiedState.query = '';
  1515. const eventView = new EventView(modifiedState);
  1516. const output = eventView.toNewQuery();
  1517. const expected = {
  1518. version: 2,
  1519. id: '1234',
  1520. name: 'best query',
  1521. fields: ['count()', 'project.id'],
  1522. widths: ['123', '456'],
  1523. orderby: '-count',
  1524. projects: [42],
  1525. start: '2019-10-01T00:00:00',
  1526. end: '2019-10-02T00:00:00',
  1527. range: '14d',
  1528. environment: ['staging'],
  1529. display: 'releases',
  1530. dataset: DiscoverDatasets.DISCOVER,
  1531. };
  1532. expect(output).toEqual(expected);
  1533. });
  1534. it('omits query when query is not defined', function () {
  1535. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  1536. ...state,
  1537. };
  1538. modifiedState.query = '';
  1539. const eventView = new EventView(modifiedState);
  1540. const output = eventView.toNewQuery();
  1541. const expected = {
  1542. version: 2,
  1543. id: '1234',
  1544. name: 'best query',
  1545. fields: ['count()', 'project.id'],
  1546. widths: ['123', '456'],
  1547. orderby: '-count',
  1548. projects: [42],
  1549. start: '2019-10-01T00:00:00',
  1550. end: '2019-10-02T00:00:00',
  1551. range: '14d',
  1552. environment: ['staging'],
  1553. display: 'releases',
  1554. dataset: DiscoverDatasets.DISCOVER,
  1555. };
  1556. expect(output).toEqual(expected);
  1557. });
  1558. });
  1559. describe('EventView.isValid()', function () {
  1560. it('event view is valid when there is at least one field', function () {
  1561. const eventView = new EventView({
  1562. ...REQUIRED_CONSTRUCTOR_PROPS,
  1563. fields: [{field: 'count()'}, {field: 'project.id'}],
  1564. sorts: [],
  1565. project: [],
  1566. });
  1567. expect(eventView.isValid()).toBe(true);
  1568. });
  1569. it('event view is not valid when there are no fields', function () {
  1570. const eventView = new EventView({
  1571. ...REQUIRED_CONSTRUCTOR_PROPS,
  1572. fields: [],
  1573. sorts: [],
  1574. project: [],
  1575. });
  1576. expect(eventView.isValid()).toBe(false);
  1577. });
  1578. });
  1579. describe('EventView.getWidths()', function () {
  1580. it('returns widths', function () {
  1581. const eventView = new EventView({
  1582. ...REQUIRED_CONSTRUCTOR_PROPS,
  1583. fields: [
  1584. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  1585. {field: 'project.id', width: 2020},
  1586. {field: 'title', width: COL_WIDTH_UNDEFINED},
  1587. {field: 'time', width: 420},
  1588. {field: 'lcp', width: 69},
  1589. {field: 'lcp', width: COL_WIDTH_UNDEFINED},
  1590. {field: 'fcp', width: COL_WIDTH_UNDEFINED},
  1591. {field: 'cls', width: COL_WIDTH_UNDEFINED},
  1592. ],
  1593. sorts: [],
  1594. project: [],
  1595. });
  1596. expect(eventView.getWidths()).toEqual([
  1597. COL_WIDTH_UNDEFINED,
  1598. 2020,
  1599. COL_WIDTH_UNDEFINED,
  1600. 420,
  1601. 69,
  1602. ]);
  1603. });
  1604. });
  1605. describe('EventView.getFields()', function () {
  1606. it('returns fields', function () {
  1607. const eventView = new EventView({
  1608. ...REQUIRED_CONSTRUCTOR_PROPS,
  1609. fields: [{field: 'count()'}, {field: 'project.id'}],
  1610. sorts: [],
  1611. project: [],
  1612. });
  1613. expect(eventView.getFields()).toEqual(['count()', 'project.id']);
  1614. });
  1615. });
  1616. describe('EventView.numOfColumns()', function () {
  1617. it('returns correct number of columns', function () {
  1618. // has columns
  1619. const eventView = new EventView({
  1620. ...REQUIRED_CONSTRUCTOR_PROPS,
  1621. fields: [{field: 'count()'}, {field: 'project.id'}],
  1622. sorts: [],
  1623. project: [],
  1624. });
  1625. expect(eventView.numOfColumns()).toBe(2);
  1626. // has no columns
  1627. const eventView2 = new EventView({
  1628. ...REQUIRED_CONSTRUCTOR_PROPS,
  1629. fields: [],
  1630. sorts: [],
  1631. project: [],
  1632. });
  1633. expect(eventView2.numOfColumns()).toBe(0);
  1634. });
  1635. });
  1636. describe('EventView.getDays()', function () {
  1637. it('returns the right number of days for statsPeriod', function () {
  1638. const eventView = new EventView({
  1639. ...REQUIRED_CONSTRUCTOR_PROPS,
  1640. statsPeriod: '14d',
  1641. });
  1642. expect(eventView.getDays()).toBe(14);
  1643. const eventView2 = new EventView({
  1644. ...REQUIRED_CONSTRUCTOR_PROPS,
  1645. statsPeriod: '12h',
  1646. });
  1647. expect(eventView2.getDays()).toBe(0.5);
  1648. });
  1649. it('returns the right number of days for start/end', function () {
  1650. const eventView = new EventView({
  1651. ...REQUIRED_CONSTRUCTOR_PROPS,
  1652. start: '2019-10-01T00:00:00',
  1653. end: '2019-10-02T00:00:00',
  1654. });
  1655. expect(eventView.getDays()).toBe(1);
  1656. const eventView2 = new EventView({
  1657. ...REQUIRED_CONSTRUCTOR_PROPS,
  1658. start: '2019-10-01T00:00:00',
  1659. end: '2019-10-15T00:00:00',
  1660. });
  1661. expect(eventView2.getDays()).toBe(14);
  1662. });
  1663. });
  1664. describe('EventView.clone()', function () {
  1665. it('returns a unique instance', function () {
  1666. const state: ConstructorParameters<typeof EventView>[0] = {
  1667. ...REQUIRED_CONSTRUCTOR_PROPS,
  1668. id: '1234',
  1669. name: 'best query',
  1670. fields: [{field: 'count()'}, {field: 'project.id'}],
  1671. sorts: generateSorts([AggregationKey.COUNT]),
  1672. query: 'event.type:error',
  1673. project: [42],
  1674. start: '2019-10-01T00:00:00',
  1675. end: '2019-10-02T00:00:00',
  1676. statsPeriod: '14d',
  1677. environment: ['staging'],
  1678. interval: '5m',
  1679. display: 'releases',
  1680. dataset: DiscoverDatasets.DISCOVER,
  1681. };
  1682. const eventView = new EventView(state);
  1683. const eventView2 = eventView.clone();
  1684. expect(eventView2 !== eventView).toBeTruthy();
  1685. expect(eventView).toMatchObject(state);
  1686. expect(eventView2).toMatchObject(state);
  1687. expect(eventView.isEqualTo(eventView2)).toBe(true);
  1688. expect(
  1689. eventView.additionalConditions === eventView2.additionalConditions
  1690. ).toBeFalsy();
  1691. });
  1692. });
  1693. describe('EventView.withColumns()', function () {
  1694. const state: ConstructorParameters<typeof EventView>[0] = {
  1695. ...REQUIRED_CONSTRUCTOR_PROPS,
  1696. id: '1234',
  1697. name: 'best query',
  1698. fields: [
  1699. {field: 'count()', width: 30},
  1700. {field: 'project.id', width: 99},
  1701. {field: 'failure_count()', width: 30},
  1702. ],
  1703. yAxis: 'failure_count()',
  1704. sorts: generateSorts([AggregationKey.COUNT]),
  1705. query: 'event.type:error',
  1706. project: [42],
  1707. start: '2019-10-01T00:00:00',
  1708. end: '2019-10-02T00:00:00',
  1709. statsPeriod: '14d',
  1710. environment: ['staging'],
  1711. };
  1712. const eventView = new EventView(state);
  1713. it('adds new columns, and replaces existing ones', function () {
  1714. const newView = eventView.withColumns([
  1715. {kind: 'field', field: 'title'},
  1716. {kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]},
  1717. {kind: 'field', field: 'project.id'},
  1718. {kind: 'field', field: 'culprit'},
  1719. ]);
  1720. // Views should be different.
  1721. expect(newView.isEqualTo(eventView)).toBe(false);
  1722. expect(newView.fields).toEqual([
  1723. {field: 'title', width: COL_WIDTH_UNDEFINED},
  1724. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  1725. {field: 'project.id', width: COL_WIDTH_UNDEFINED},
  1726. {field: 'culprit', width: COL_WIDTH_UNDEFINED},
  1727. ]);
  1728. });
  1729. it('drops empty columns', function () {
  1730. const newView = eventView.withColumns([
  1731. {kind: 'field', field: 'issue'},
  1732. {kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]},
  1733. {kind: 'field', field: ''},
  1734. {kind: 'function', function: ['', '', undefined, undefined]},
  1735. {kind: 'function', function: ['', '', undefined, undefined]},
  1736. ]);
  1737. expect(newView.fields).toEqual([
  1738. {field: 'issue', width: COL_WIDTH_UNDEFINED},
  1739. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  1740. ]);
  1741. });
  1742. it('inherits widths from existing columns when names match', function () {
  1743. const newView = eventView.withColumns([
  1744. {kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]},
  1745. {kind: 'field', field: 'project.id'},
  1746. {kind: 'field', field: 'title'},
  1747. {kind: 'field', field: 'time'},
  1748. ]);
  1749. expect(newView.fields).toEqual([
  1750. {field: 'count()', width: 30},
  1751. {field: 'project.id', width: 99},
  1752. {field: 'title', width: COL_WIDTH_UNDEFINED},
  1753. {field: 'time', width: COL_WIDTH_UNDEFINED},
  1754. ]);
  1755. });
  1756. it('retains sorts when sorted field is included', function () {
  1757. const newView = eventView.withColumns([
  1758. {kind: 'field', field: 'title'},
  1759. {kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]},
  1760. ]);
  1761. expect(newView.fields).toEqual([
  1762. {field: 'title', width: COL_WIDTH_UNDEFINED},
  1763. {field: 'count()', width: COL_WIDTH_UNDEFINED},
  1764. ]);
  1765. expect(newView.sorts).toEqual([{field: AggregationKey.COUNT, kind: 'desc'}]);
  1766. });
  1767. it('updates sorts when sorted field is removed', function () {
  1768. const newView = eventView.withColumns([{kind: 'field', field: 'title'}]);
  1769. expect(newView.fields).toEqual([{field: 'title', width: COL_WIDTH_UNDEFINED}]);
  1770. // Should pick a sortable field.
  1771. expect(newView.sorts).toEqual([{field: 'title', kind: 'desc'}]);
  1772. });
  1773. it('has no sort if no sortable fields remain', function () {
  1774. const newView = eventView.withColumns([{kind: 'field', field: 'issue'}]);
  1775. expect(newView.fields).toEqual([{field: 'issue', width: COL_WIDTH_UNDEFINED}]);
  1776. expect(newView.sorts).toEqual([]);
  1777. });
  1778. it('updates yAxis if column is dropped', function () {
  1779. const newView = eventView.withColumns([
  1780. {kind: 'field', field: 'count()'},
  1781. {kind: 'field', field: 'project.id'},
  1782. ]);
  1783. expect(newView.fields).toEqual([
  1784. {field: 'count()', width: 30},
  1785. {field: 'project.id', width: 99},
  1786. ]);
  1787. expect(eventView.yAxis).toEqual('failure_count()');
  1788. expect(newView.yAxis).toEqual('count()');
  1789. });
  1790. });
  1791. describe('EventView.withNewColumn()', function () {
  1792. const state: ConstructorParameters<typeof EventView>[0] = {
  1793. ...REQUIRED_CONSTRUCTOR_PROPS,
  1794. id: '1234',
  1795. name: 'best query',
  1796. fields: [
  1797. {field: 'count()', width: 30},
  1798. {field: 'project.id', width: 99},
  1799. ],
  1800. sorts: generateSorts([AggregationKey.COUNT]),
  1801. query: 'event.type:error',
  1802. project: [42],
  1803. start: '2019-10-01T00:00:00',
  1804. end: '2019-10-02T00:00:00',
  1805. statsPeriod: '14d',
  1806. environment: ['staging'],
  1807. };
  1808. it('adds a field', function () {
  1809. const eventView = new EventView(state);
  1810. const newColumn: Column = {
  1811. kind: 'field',
  1812. field: 'title',
  1813. };
  1814. const eventView2 = eventView.withNewColumn(newColumn);
  1815. expect(eventView2 !== eventView).toBeTruthy();
  1816. expect(eventView).toMatchObject(state);
  1817. const nextState = {
  1818. ...state,
  1819. fields: [...state.fields, {field: 'title'}],
  1820. };
  1821. expect(eventView2).toMatchObject(nextState);
  1822. });
  1823. it('adds an aggregate function with no arguments', function () {
  1824. const eventView = new EventView(state);
  1825. const newColumn: Column = {
  1826. kind: 'function',
  1827. function: [AggregationKey.COUNT, '', undefined, undefined],
  1828. };
  1829. const eventView2 = eventView.withNewColumn(newColumn);
  1830. expect(eventView2 !== eventView).toBeTruthy();
  1831. expect(eventView).toMatchObject(state);
  1832. const nextState = {
  1833. ...state,
  1834. fields: [...state.fields, {field: 'count()'}],
  1835. };
  1836. expect(eventView2).toMatchObject(nextState);
  1837. });
  1838. it('add an aggregate function with field', function () {
  1839. const eventView = new EventView(state);
  1840. const newColumn: Column = {
  1841. kind: 'function',
  1842. function: [AggregationKey.AVG, 'transaction.duration', undefined, undefined],
  1843. };
  1844. const eventView2 = eventView.withNewColumn(newColumn);
  1845. expect(eventView2 !== eventView).toBeTruthy();
  1846. expect(eventView).toMatchObject(state);
  1847. const nextState = {
  1848. ...state,
  1849. fields: [...state.fields, {field: 'avg(transaction.duration)'}],
  1850. };
  1851. expect(eventView2).toMatchObject(nextState);
  1852. });
  1853. it('add an aggregate function with field & refinement', function () {
  1854. const eventView = new EventView(state);
  1855. const newColumn: Column = {
  1856. kind: 'function',
  1857. function: [AggregationKey.PERCENTILE, 'transaction.duration', '0.5', undefined],
  1858. };
  1859. const updated = eventView.withNewColumn(newColumn);
  1860. expect(updated.fields).toEqual([
  1861. ...state.fields,
  1862. {field: 'percentile(transaction.duration,0.5)', width: COL_WIDTH_UNDEFINED},
  1863. ]);
  1864. });
  1865. });
  1866. describe('EventView.withResizedColumn()', function () {
  1867. const state: ConstructorParameters<typeof EventView>[0] = {
  1868. ...REQUIRED_CONSTRUCTOR_PROPS,
  1869. id: '1234',
  1870. name: 'best query',
  1871. fields: [{field: 'count()'}, {field: 'project.id'}],
  1872. sorts: generateSorts([AggregationKey.COUNT]),
  1873. query: 'event.type:error',
  1874. project: [42],
  1875. start: '2019-10-01T00:00:00',
  1876. end: '2019-10-02T00:00:00',
  1877. statsPeriod: '14d',
  1878. environment: ['staging'],
  1879. };
  1880. const view = new EventView(state);
  1881. it('updates a column that exists', function () {
  1882. const newView = view.withResizedColumn(0, 99);
  1883. expect(view.fields[0].width).toBeUndefined();
  1884. expect(newView.fields[0].width).toEqual(99);
  1885. });
  1886. it('ignores columns that do not exist', function () {
  1887. const newView = view.withResizedColumn(100, 99);
  1888. expect(view.fields).toEqual(newView.fields);
  1889. });
  1890. });
  1891. describe('EventView.withUpdatedColumn()', function () {
  1892. const state: ConstructorParameters<typeof EventView>[0] = {
  1893. ...REQUIRED_CONSTRUCTOR_PROPS,
  1894. id: '1234',
  1895. name: 'best query',
  1896. fields: [{field: 'count()'}, {field: 'project.id'}],
  1897. sorts: generateSorts([AggregationKey.COUNT]),
  1898. query: 'event.type:error',
  1899. project: [42],
  1900. start: '2019-10-01T00:00:00',
  1901. end: '2019-10-02T00:00:00',
  1902. statsPeriod: '14d',
  1903. environment: ['staging'],
  1904. };
  1905. const meta: MetaType = {
  1906. count: 'integer',
  1907. title: 'string',
  1908. };
  1909. it('update a column with no changes', function () {
  1910. const eventView = new EventView(state);
  1911. const newColumn: Column = {
  1912. kind: 'function',
  1913. function: [AggregationKey.COUNT, '', undefined, undefined],
  1914. };
  1915. const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
  1916. expect(eventView2 === eventView).toBeTruthy();
  1917. expect(eventView).toMatchObject(state);
  1918. });
  1919. it('update a column to a field', function () {
  1920. const eventView = new EventView(state);
  1921. const newColumn: Column = {
  1922. kind: 'field',
  1923. field: 'title',
  1924. };
  1925. const eventView2 = eventView.withUpdatedColumn(1, newColumn, meta);
  1926. expect(eventView2 !== eventView).toBeTruthy();
  1927. expect(eventView).toMatchObject(state);
  1928. const nextState = {
  1929. ...state,
  1930. fields: [state.fields[0], {field: 'title'}],
  1931. };
  1932. expect(eventView2).toMatchObject(nextState);
  1933. });
  1934. it('update a column to an aggregate function with no arguments', function () {
  1935. const eventView = new EventView(state);
  1936. const newColumn: Column = {
  1937. kind: 'function',
  1938. function: [AggregationKey.COUNT, '', undefined, undefined],
  1939. };
  1940. const eventView2 = eventView.withUpdatedColumn(1, newColumn, meta);
  1941. expect(eventView2 !== eventView).toBeTruthy();
  1942. expect(eventView).toMatchObject(state);
  1943. const nextState = {
  1944. ...state,
  1945. fields: [state.fields[0], {field: 'count()'}],
  1946. };
  1947. expect(eventView2).toMatchObject(nextState);
  1948. });
  1949. it('update a column to an aggregate function with field', function () {
  1950. const eventView = new EventView(state);
  1951. const newColumn: Column = {
  1952. kind: 'function',
  1953. function: [AggregationKey.AVG, 'transaction.duration', undefined, undefined],
  1954. };
  1955. const eventView2 = eventView.withUpdatedColumn(1, newColumn, meta);
  1956. expect(eventView2 !== eventView).toBeTruthy();
  1957. expect(eventView).toMatchObject(state);
  1958. const nextState = {
  1959. ...state,
  1960. fields: [state.fields[0], {field: 'avg(transaction.duration)'}],
  1961. };
  1962. expect(eventView2).toMatchObject(nextState);
  1963. });
  1964. it('update a column to an aggregate function with field & refinement', function () {
  1965. const eventView = new EventView(state);
  1966. const newColumn: Column = {
  1967. kind: 'function',
  1968. function: [AggregationKey.PERCENTILE, 'transaction.duration', '0.5', undefined],
  1969. };
  1970. const newView = eventView.withUpdatedColumn(1, newColumn, meta);
  1971. expect(newView.fields).toEqual([
  1972. state.fields[0],
  1973. {field: 'percentile(transaction.duration,0.5)', width: COL_WIDTH_UNDEFINED},
  1974. ]);
  1975. });
  1976. describe('update a column that is sorted', function () {
  1977. it('the sorted column is the only sorted column', function () {
  1978. const eventView = new EventView(state);
  1979. const newColumn: Column = {
  1980. kind: 'field',
  1981. field: 'title',
  1982. };
  1983. const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
  1984. expect(eventView2 !== eventView).toBeTruthy();
  1985. expect(eventView).toMatchObject(state);
  1986. const nextState = {
  1987. ...state,
  1988. sorts: [{field: 'title', kind: 'desc'}],
  1989. fields: [{field: 'title'}, state.fields[1]],
  1990. };
  1991. expect(eventView2).toMatchObject(nextState);
  1992. });
  1993. it('the sorted column occurs at least twice', function () {
  1994. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  1995. ...state,
  1996. fields: [...state.fields, {field: 'count()'}],
  1997. };
  1998. const eventView = new EventView(modifiedState);
  1999. const newColumn: Column = {
  2000. kind: 'field',
  2001. field: 'title',
  2002. };
  2003. const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
  2004. expect(eventView2 !== eventView).toBeTruthy();
  2005. expect(eventView).toMatchObject(modifiedState);
  2006. const nextState = {
  2007. ...state,
  2008. fields: [{field: 'title'}, state.fields[1], {field: 'count()'}],
  2009. };
  2010. expect(eventView2).toMatchObject(nextState);
  2011. });
  2012. it('using no provided table meta', function () {
  2013. // table meta may not be provided in the invalid query state;
  2014. // we will still want to be able to update columns
  2015. const eventView = new EventView(state);
  2016. const expected = {
  2017. ...state,
  2018. sorts: [{field: 'title', kind: 'desc'}],
  2019. fields: [{field: 'title'}, state.fields[1]],
  2020. };
  2021. const newColumn: Column = {
  2022. kind: 'field',
  2023. field: 'title',
  2024. };
  2025. const eventView2 = eventView.withUpdatedColumn(0, newColumn, {});
  2026. expect(eventView2).toMatchObject(expected);
  2027. const eventView3 = eventView.withUpdatedColumn(0, newColumn, undefined);
  2028. expect(eventView3).toMatchObject(expected);
  2029. });
  2030. });
  2031. describe('update a column to a non-sortable column', function () {
  2032. it('default to a sortable column', function () {
  2033. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2034. ...state,
  2035. fields: [{field: 'count()'}, {field: 'title'}],
  2036. };
  2037. const eventView = new EventView(modifiedState);
  2038. // this column is expected to be non-sortable
  2039. const newColumn: Column = {
  2040. kind: 'field',
  2041. field: 'project.id',
  2042. };
  2043. const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
  2044. expect(eventView2 !== eventView).toBeTruthy();
  2045. expect(eventView).toMatchObject(modifiedState);
  2046. const nextState = {
  2047. ...state,
  2048. sorts: [{field: 'title', kind: 'desc'}],
  2049. fields: [{field: 'project.id'}, {field: 'title'}],
  2050. };
  2051. expect(eventView2).toMatchObject(nextState);
  2052. });
  2053. it('has no sort if there are no sortable columns', function () {
  2054. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2055. ...state,
  2056. fields: [{field: 'count()'}],
  2057. };
  2058. const eventView = new EventView(modifiedState);
  2059. // this column is expected to be non-sortable
  2060. const newColumn: Column = {
  2061. kind: 'field',
  2062. field: 'project.id',
  2063. };
  2064. const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta);
  2065. expect(eventView2 !== eventView).toBeTruthy();
  2066. expect(eventView).toMatchObject(modifiedState);
  2067. const nextState = {
  2068. ...state,
  2069. sorts: [],
  2070. fields: [{field: 'project.id'}],
  2071. };
  2072. expect(eventView2).toMatchObject(nextState);
  2073. });
  2074. });
  2075. });
  2076. describe('EventView.withDeletedColumn()', function () {
  2077. const state: ConstructorParameters<typeof EventView>[0] = {
  2078. ...REQUIRED_CONSTRUCTOR_PROPS,
  2079. id: '1234',
  2080. name: 'best query',
  2081. fields: [{field: 'count()'}, {field: 'project.id'}],
  2082. sorts: generateSorts([AggregationKey.COUNT]),
  2083. query: 'event.type:error',
  2084. project: [42],
  2085. start: '2019-10-01T00:00:00',
  2086. end: '2019-10-02T00:00:00',
  2087. statsPeriod: '14d',
  2088. environment: ['staging'],
  2089. };
  2090. const meta: MetaType = {
  2091. count: 'integer',
  2092. title: 'string',
  2093. };
  2094. it('returns itself when attempting to delete the last remaining column', function () {
  2095. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2096. ...state,
  2097. fields: [{field: 'count()'}],
  2098. };
  2099. const eventView = new EventView(modifiedState);
  2100. const eventView2 = eventView.withDeletedColumn(0, meta);
  2101. expect(eventView2 === eventView).toBeTruthy();
  2102. expect(eventView).toMatchObject(modifiedState);
  2103. });
  2104. describe('deletes column, and use any remaining sortable column', function () {
  2105. it('using no provided table meta', function () {
  2106. // table meta may not be provided in the invalid query state;
  2107. // we will still want to be able to delete columns
  2108. const state2 = {
  2109. ...state,
  2110. fields: [{field: 'title'}, {field: 'timestamp'}, {field: 'count()'}],
  2111. sorts: generateSorts(['timestamp']),
  2112. };
  2113. const eventView = new EventView(state2);
  2114. const expected = {
  2115. ...state,
  2116. sorts: generateSorts(['title']),
  2117. fields: [{field: 'title'}, {field: 'count()'}],
  2118. };
  2119. const eventView2 = eventView.withDeletedColumn(1, {});
  2120. expect(eventView2).toMatchObject(expected);
  2121. const eventView3 = eventView.withDeletedColumn(1, undefined);
  2122. expect(eventView3).toMatchObject(expected);
  2123. });
  2124. it('has no remaining sortable column', function () {
  2125. const eventView = new EventView(state);
  2126. const eventView2 = eventView.withDeletedColumn(0, meta);
  2127. expect(eventView2 !== eventView).toBeTruthy();
  2128. expect(eventView).toMatchObject(state);
  2129. const nextState = {
  2130. ...state,
  2131. // we expect sorts to be empty since project.id is non-sortable
  2132. sorts: [],
  2133. fields: [state.fields[1]],
  2134. };
  2135. expect(eventView2).toMatchObject(nextState);
  2136. });
  2137. it('has a remaining sortable column', function () {
  2138. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2139. ...state,
  2140. fields: [{field: 'count()'}, {field: 'project.id'}, {field: 'title'}],
  2141. };
  2142. const eventView = new EventView(modifiedState);
  2143. const eventView2 = eventView.withDeletedColumn(0, meta);
  2144. expect(eventView2 !== eventView).toBeTruthy();
  2145. expect(eventView).toMatchObject(modifiedState);
  2146. const nextState = {
  2147. ...state,
  2148. sorts: [{field: 'title', kind: 'desc'}],
  2149. fields: [{field: 'project.id'}, {field: 'title'}],
  2150. };
  2151. expect(eventView2).toMatchObject(nextState);
  2152. });
  2153. it('sorted column occurs at least twice', function () {
  2154. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2155. ...state,
  2156. fields: [...state.fields, state.fields[0]],
  2157. };
  2158. const eventView = new EventView(modifiedState);
  2159. const eventView2 = eventView.withDeletedColumn(0, meta);
  2160. expect(eventView2 !== eventView).toBeTruthy();
  2161. expect(eventView).toMatchObject(modifiedState);
  2162. const nextState = {
  2163. ...state,
  2164. fields: [state.fields[1], state.fields[0]],
  2165. };
  2166. expect(eventView2).toMatchObject(nextState);
  2167. });
  2168. it('ensures there is at one auto-width column on deletion', function () {
  2169. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2170. ...state,
  2171. fields: [
  2172. {field: 'id', width: 75},
  2173. {field: 'title', width: 100},
  2174. {field: 'project', width: 80},
  2175. {field: 'environment', width: 99},
  2176. ],
  2177. };
  2178. const eventView = new EventView(modifiedState);
  2179. let updated = eventView.withDeletedColumn(0, meta);
  2180. let updatedFields = [
  2181. {field: 'title', width: -1},
  2182. {field: 'project', width: 80},
  2183. {field: 'environment', width: 99},
  2184. ];
  2185. expect(updated.fields).toEqual(updatedFields);
  2186. updated = updated.withDeletedColumn(0, meta);
  2187. updatedFields = [
  2188. {field: 'project', width: -1},
  2189. {field: 'environment', width: 99},
  2190. ];
  2191. expect(updated.fields).toEqual(updatedFields);
  2192. });
  2193. });
  2194. });
  2195. describe('EventView.getSorts()', function () {
  2196. it('returns fields', function () {
  2197. const eventView = new EventView({
  2198. ...REQUIRED_CONSTRUCTOR_PROPS,
  2199. fields: [{field: 'count()'}, {field: 'project.id'}],
  2200. sorts: generateSorts([AggregationKey.COUNT]),
  2201. project: [],
  2202. });
  2203. expect(eventView.getSorts()).toEqual([
  2204. {
  2205. key: AggregationKey.COUNT,
  2206. order: 'desc',
  2207. },
  2208. ]);
  2209. });
  2210. });
  2211. describe('EventView.getQuery()', function () {
  2212. it('with query', function () {
  2213. const eventView = new EventView({
  2214. ...REQUIRED_CONSTRUCTOR_PROPS,
  2215. fields: [],
  2216. sorts: [],
  2217. project: [],
  2218. query: 'event.type:error',
  2219. });
  2220. expect(eventView.getQuery()).toEqual('event.type:error');
  2221. expect(eventView.getQuery(null)).toEqual('event.type:error');
  2222. expect(eventView.getQuery('hello')).toEqual('event.type:error hello');
  2223. expect(eventView.getQuery(['event.type:error', 'hello'])).toEqual(
  2224. 'event.type:error hello'
  2225. );
  2226. });
  2227. it('without query', function () {
  2228. const eventView = new EventView({
  2229. ...REQUIRED_CONSTRUCTOR_PROPS,
  2230. fields: [],
  2231. sorts: [],
  2232. project: [],
  2233. });
  2234. expect(eventView.getQuery()).toEqual('');
  2235. expect(eventView.getQuery(null)).toEqual('');
  2236. expect(eventView.getQuery('hello')).toEqual('hello');
  2237. expect(eventView.getQuery(['event.type:error', 'hello'])).toEqual(
  2238. 'event.type:error hello'
  2239. );
  2240. });
  2241. });
  2242. describe('EventView.getQueryWithAdditionalConditions', function () {
  2243. it('with overlapping conditions', function () {
  2244. const eventView = new EventView({
  2245. ...REQUIRED_CONSTRUCTOR_PROPS,
  2246. fields: [],
  2247. sorts: [],
  2248. project: [],
  2249. query: 'event.type:transaction foo:bar',
  2250. });
  2251. eventView.additionalConditions.setFilterValues('event.type', ['transaction']);
  2252. expect(eventView.getQueryWithAdditionalConditions()).toEqual(
  2253. 'event.type:transaction foo:bar'
  2254. );
  2255. });
  2256. });
  2257. describe('EventView.sortForField()', function () {
  2258. const state: ConstructorParameters<typeof EventView>[0] = {
  2259. ...REQUIRED_CONSTRUCTOR_PROPS,
  2260. id: '1234',
  2261. name: 'best query',
  2262. fields: [{field: 'count()'}, {field: 'project.id'}],
  2263. sorts: generateSorts([AggregationKey.COUNT]),
  2264. query: 'event.type:error',
  2265. project: [42],
  2266. start: '2019-10-01T00:00:00',
  2267. end: '2019-10-02T00:00:00',
  2268. statsPeriod: '14d',
  2269. environment: ['staging'],
  2270. };
  2271. const eventView = new EventView(state);
  2272. const meta: MetaType = {count: 'integer'};
  2273. it('returns the sort when selected field is sorted', function () {
  2274. const field = {
  2275. field: 'count()',
  2276. };
  2277. const actual = eventView.sortForField(field, meta);
  2278. expect(actual).toEqual({
  2279. field: AggregationKey.COUNT,
  2280. kind: 'desc',
  2281. });
  2282. });
  2283. it('returns undefined when selected field is not sorted', function () {
  2284. const field = {
  2285. field: 'project.id',
  2286. };
  2287. expect(eventView.sortForField(field, meta)).toBeUndefined();
  2288. });
  2289. it('returns undefined when no meta is provided', function () {
  2290. const field = {
  2291. field: 'project.id',
  2292. };
  2293. expect(eventView.sortForField(field, undefined)).toBeUndefined();
  2294. });
  2295. });
  2296. describe('EventView.sortOnField()', function () {
  2297. const state: ConstructorParameters<typeof EventView>[0] = {
  2298. ...REQUIRED_CONSTRUCTOR_PROPS,
  2299. id: '1234',
  2300. name: 'best query',
  2301. fields: [{field: 'count()'}, {field: 'project.id'}],
  2302. sorts: generateSorts([AggregationKey.COUNT]),
  2303. query: 'event.type:error',
  2304. project: [42],
  2305. start: '2019-10-01T00:00:00',
  2306. end: '2019-10-02T00:00:00',
  2307. statsPeriod: '14d',
  2308. environment: ['staging'],
  2309. };
  2310. const meta: MetaType = {count: 'integer', title: 'string'};
  2311. it('returns itself when attempting to sort on un-sortable field', function () {
  2312. const eventView = new EventView(state);
  2313. expect(eventView).toMatchObject(state);
  2314. const field = state.fields[1];
  2315. const eventView2 = eventView.sortOnField(field, meta);
  2316. expect(eventView2 === eventView).toBe(true);
  2317. });
  2318. it('reverses the sorted field', function () {
  2319. const eventView = new EventView(state);
  2320. expect(eventView).toMatchObject(state);
  2321. const field = state.fields[0];
  2322. const eventView2 = eventView.sortOnField(field, meta);
  2323. expect(eventView2 !== eventView).toBe(true);
  2324. const nextState = {
  2325. ...state,
  2326. sorts: [{field: AggregationKey.COUNT, kind: 'asc'}],
  2327. };
  2328. expect(eventView2).toMatchObject(nextState);
  2329. });
  2330. it('enforce sort order on sorted field', function () {
  2331. const eventView = new EventView(state);
  2332. expect(eventView).toMatchObject(state);
  2333. const field = state.fields[0];
  2334. const eventView2 = eventView.sortOnField(field, meta, 'asc');
  2335. expect(eventView2).toMatchObject({
  2336. ...state,
  2337. sorts: [{field: AggregationKey.COUNT, kind: 'asc'}],
  2338. });
  2339. const eventView3 = eventView.sortOnField(field, meta, 'desc');
  2340. expect(eventView3).toMatchObject({
  2341. ...state,
  2342. sorts: [{field: AggregationKey.COUNT, kind: 'desc'}],
  2343. });
  2344. });
  2345. it('supports function format on equation sorts', function () {
  2346. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2347. ...state,
  2348. fields: [{field: 'count()'}, {field: 'equation|count() + 100'}],
  2349. sorts: [{field: 'equation|count() + 100', kind: 'desc'}],
  2350. };
  2351. const eventView = new EventView(modifiedState);
  2352. expect(eventView).toMatchObject(modifiedState);
  2353. });
  2354. it('supports index format on equation sorts', function () {
  2355. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2356. ...state,
  2357. fields: [{field: 'count()'}, {field: 'equation|count() + 100'}],
  2358. sorts: [{field: 'equation[0]', kind: 'desc'}],
  2359. };
  2360. const eventView = new EventView(modifiedState);
  2361. expect(eventView).toMatchObject(modifiedState);
  2362. });
  2363. it('sort on new field', function () {
  2364. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2365. ...state,
  2366. fields: [...state.fields, {field: 'title'}],
  2367. };
  2368. const eventView = new EventView(modifiedState);
  2369. expect(eventView).toMatchObject(modifiedState);
  2370. const field = modifiedState.fields[2];
  2371. const eventView2 = eventView.sortOnField(field, meta);
  2372. expect(eventView2 !== eventView).toBe(true);
  2373. const nextState = {
  2374. ...modifiedState,
  2375. sorts: [{field: 'title', kind: 'desc'}],
  2376. };
  2377. expect(eventView2).toMatchObject(nextState);
  2378. // enforce asc sort order
  2379. const eventView3 = eventView.sortOnField(field, meta, 'asc');
  2380. expect(eventView3).toMatchObject({
  2381. ...modifiedState,
  2382. sorts: [{field: 'title', kind: 'asc'}],
  2383. });
  2384. // enforce desc sort order
  2385. const eventView4 = eventView.sortOnField(field, meta, 'desc');
  2386. expect(eventView4).toMatchObject({
  2387. ...modifiedState,
  2388. sorts: [{field: 'title', kind: 'desc'}],
  2389. });
  2390. });
  2391. it('sorts on a field using function format', function () {
  2392. const modifiedState: ConstructorParameters<typeof EventView>[0] = {
  2393. ...state,
  2394. fields: [...state.fields, {field: 'count()'}],
  2395. };
  2396. const eventView = new EventView(modifiedState);
  2397. expect(eventView).toMatchObject(modifiedState);
  2398. const field = modifiedState.fields[2];
  2399. let sortedEventView = eventView.sortOnField(field, meta, undefined, true);
  2400. expect(sortedEventView.sorts).toEqual([{field: 'count()', kind: 'asc'}]);
  2401. sortedEventView = sortedEventView.sortOnField(field, meta, undefined, true);
  2402. expect(sortedEventView.sorts).toEqual([{field: 'count()', kind: 'desc'}]);
  2403. });
  2404. });
  2405. describe('EventView.withSorts()', function () {
  2406. it('returns a clone', function () {
  2407. const eventView = new EventView({
  2408. ...REQUIRED_CONSTRUCTOR_PROPS,
  2409. fields: [{field: 'event.type'}],
  2410. });
  2411. const updated = eventView.withSorts([{kind: 'desc', field: 'event.type'}]);
  2412. expect(updated.sorts).not.toEqual(eventView.sorts);
  2413. });
  2414. it('only accepts sorting on fields in the view', function () {
  2415. const eventView = new EventView({
  2416. ...REQUIRED_CONSTRUCTOR_PROPS,
  2417. fields: [{field: 'event.type'}],
  2418. });
  2419. const updated = eventView.withSorts([
  2420. {kind: 'desc', field: 'event.type'},
  2421. {kind: 'asc', field: 'unknown'},
  2422. ]);
  2423. expect(updated.sorts).toEqual([{kind: 'desc', field: 'event.type'}]);
  2424. });
  2425. it('accepts aggregate field sorts', function () {
  2426. const eventView = new EventView({
  2427. ...REQUIRED_CONSTRUCTOR_PROPS,
  2428. fields: [{field: 'p50()'}],
  2429. });
  2430. const updated = eventView.withSorts([
  2431. {kind: 'desc', field: 'p50'},
  2432. {kind: 'asc', field: 'unknown'},
  2433. ]);
  2434. expect(updated.sorts).toEqual([{kind: 'desc', field: 'p50'}]);
  2435. });
  2436. });
  2437. describe('EventView.isEqualTo()', function () {
  2438. it('should be true when equal', function () {
  2439. const state: ConstructorParameters<typeof EventView>[0] = {
  2440. ...REQUIRED_CONSTRUCTOR_PROPS,
  2441. id: '1234',
  2442. name: 'best query',
  2443. fields: [{field: 'count()'}, {field: 'project.id'}],
  2444. sorts: generateSorts([AggregationKey.COUNT]),
  2445. query: 'event.type:error',
  2446. project: [42],
  2447. start: '2019-10-01T00:00:00',
  2448. end: '2019-10-02T00:00:00',
  2449. statsPeriod: '14d',
  2450. environment: ['staging'],
  2451. yAxis: 'fam',
  2452. display: 'releases',
  2453. dataset: DiscoverDatasets.DISCOVER,
  2454. };
  2455. const eventView = new EventView(state);
  2456. const eventView2 = new EventView(state);
  2457. expect(eventView2 !== eventView).toBeTruthy();
  2458. expect(eventView).toMatchObject(state);
  2459. expect(eventView2).toMatchObject(state);
  2460. expect(eventView.isEqualTo(eventView2)).toBe(true);
  2461. // commutativity property holds
  2462. expect(eventView2.isEqualTo(eventView)).toBe(true);
  2463. });
  2464. it('should be true when datetime are equal but differ in format', function () {
  2465. const state: ConstructorParameters<typeof EventView>[0] = {
  2466. ...REQUIRED_CONSTRUCTOR_PROPS,
  2467. id: '1234',
  2468. name: 'best query',
  2469. fields: [{field: 'count()'}, {field: 'project.id'}],
  2470. sorts: generateSorts([AggregationKey.COUNT]),
  2471. query: 'event.type:error',
  2472. project: [42],
  2473. start: '2019-10-20T21:02:51+0000',
  2474. end: '2019-10-23T19:27:04+0000',
  2475. environment: ['staging'],
  2476. };
  2477. const eventView = new EventView(state);
  2478. const eventView2 = new EventView({
  2479. ...state,
  2480. start: '2019-10-20T21:02:51Z',
  2481. end: '2019-10-23T19:27:04Z',
  2482. });
  2483. expect(eventView.isEqualTo(eventView2)).toBe(true);
  2484. });
  2485. it('should be false when not equal', function () {
  2486. const state: ConstructorParameters<typeof EventView>[0] = {
  2487. ...REQUIRED_CONSTRUCTOR_PROPS,
  2488. id: '1234',
  2489. name: 'best query',
  2490. fields: [{field: 'count()'}, {field: 'project.id'}],
  2491. sorts: generateSorts([AggregationKey.COUNT]),
  2492. query: 'event.type:error',
  2493. project: [42],
  2494. start: '2019-10-01T00:00:00',
  2495. end: '2019-10-02T00:00:00',
  2496. statsPeriod: '14d',
  2497. environment: ['staging'],
  2498. yAxis: 'fam',
  2499. display: 'releases',
  2500. dataset: DiscoverDatasets.DISCOVER,
  2501. };
  2502. const differences = {
  2503. id: '12',
  2504. name: 'new query',
  2505. fields: [{field: 'project.id'}, {field: 'count()'}],
  2506. sorts: [{field: AggregationKey.COUNT, kind: 'asc'}],
  2507. query: 'event.type:transaction',
  2508. project: [24],
  2509. start: '2019-09-01T00:00:00',
  2510. end: '2020-09-01T00:00:00',
  2511. statsPeriod: '24d',
  2512. environment: [],
  2513. yAxis: 'ok boomer',
  2514. display: 'previous',
  2515. dataset: DiscoverDatasets.ISSUE_PLATFORM,
  2516. };
  2517. const eventView = new EventView(state);
  2518. for (const key in differences) {
  2519. const eventView2 = new EventView({...state, [key]: differences[key]});
  2520. expect(eventView.isEqualTo(eventView2)).toBe(false);
  2521. }
  2522. });
  2523. it('undefined display type equals default display type', function () {
  2524. const state: ConstructorParameters<typeof EventView>[0] = {
  2525. ...REQUIRED_CONSTRUCTOR_PROPS,
  2526. id: '1234',
  2527. name: 'best query',
  2528. fields: [{field: 'count()'}, {field: 'project.id'}],
  2529. sorts: generateSorts([AggregationKey.COUNT]),
  2530. query: 'event.type:error',
  2531. project: [42],
  2532. start: '2019-10-01T00:00:00',
  2533. end: '2019-10-02T00:00:00',
  2534. statsPeriod: '14d',
  2535. environment: ['staging'],
  2536. yAxis: 'fam',
  2537. };
  2538. const eventView = new EventView(state);
  2539. const eventView2 = new EventView({...state, display: 'default'});
  2540. expect(eventView.isEqualTo(eventView2)).toBe(true);
  2541. });
  2542. });
  2543. describe('EventView.getResultsViewUrlTarget()', function () {
  2544. beforeEach(function () {
  2545. window.__initialData = {
  2546. ...window.__initialData,
  2547. customerDomain: {
  2548. subdomain: 'albertos-apples',
  2549. organizationUrl: 'https://albertos-apples.sentry.io',
  2550. sentryUrl: 'https://sentry.io',
  2551. },
  2552. };
  2553. });
  2554. afterEach(function () {
  2555. window.__initialData = {
  2556. ...window.__initialData,
  2557. customerDomain: null,
  2558. };
  2559. });
  2560. const state: ConstructorParameters<typeof EventView>[0] = {
  2561. ...REQUIRED_CONSTRUCTOR_PROPS,
  2562. id: '1234',
  2563. name: 'best query',
  2564. fields: [{field: 'count()'}, {field: 'project.id'}],
  2565. sorts: generateSorts([AggregationKey.COUNT]),
  2566. query: 'event.type:error',
  2567. project: [42],
  2568. start: '2019-10-01T00:00:00',
  2569. end: '2019-10-02T00:00:00',
  2570. statsPeriod: '14d',
  2571. environment: ['staging'],
  2572. display: 'previous',
  2573. dataset: DiscoverDatasets.DISCOVER,
  2574. };
  2575. const organization = Organization();
  2576. it('generates a URL with non-customer domain context', function () {
  2577. window.__initialData.customerDomain = null;
  2578. const view = new EventView(state);
  2579. const result = view.getResultsViewUrlTarget(organization.slug);
  2580. expect(result.pathname).toEqual('/organizations/org-slug/discover/results/');
  2581. expect(result.query.query).toEqual(state.query);
  2582. expect(result.query.project).toEqual(state.project);
  2583. expect(result.query.display).toEqual(state.display);
  2584. });
  2585. it('generates a URL with customer domain context', function () {
  2586. const view = new EventView(state);
  2587. const result = view.getResultsViewUrlTarget(organization.slug);
  2588. expect(result.pathname).toEqual('/discover/results/');
  2589. expect(result.query.query).toEqual(state.query);
  2590. expect(result.query.project).toEqual(state.project);
  2591. expect(result.query.display).toEqual(state.display);
  2592. });
  2593. });
  2594. describe('EventView.getResultsViewShortUrlTarget()', function () {
  2595. beforeEach(function () {
  2596. window.__initialData = {
  2597. ...window.__initialData,
  2598. customerDomain: {
  2599. subdomain: 'albertos-apples',
  2600. organizationUrl: 'https://albertos-apples.sentry.io',
  2601. sentryUrl: 'https://sentry.io',
  2602. },
  2603. };
  2604. });
  2605. afterEach(function () {
  2606. window.__initialData = {
  2607. ...window.__initialData,
  2608. customerDomain: null,
  2609. };
  2610. });
  2611. const state: ConstructorParameters<typeof EventView>[0] = {
  2612. ...REQUIRED_CONSTRUCTOR_PROPS,
  2613. id: '1234',
  2614. name: 'best query',
  2615. fields: [{field: 'count()'}, {field: 'project.id'}],
  2616. sorts: generateSorts([AggregationKey.COUNT]),
  2617. query: 'event.type:error',
  2618. project: [42],
  2619. start: '2019-10-01T00:00:00',
  2620. end: '2019-10-02T00:00:00',
  2621. statsPeriod: '14d',
  2622. environment: ['staging'],
  2623. display: 'previous',
  2624. dataset: DiscoverDatasets.DISCOVER,
  2625. };
  2626. const organization = Organization();
  2627. it('generates a URL with non-customer domain context', function () {
  2628. window.__initialData.customerDomain = null;
  2629. const view = new EventView(state);
  2630. const result = view.getResultsViewShortUrlTarget(organization.slug);
  2631. expect(result.pathname).toEqual('/organizations/org-slug/discover/results/');
  2632. expect(result.query).not.toHaveProperty('name');
  2633. expect(result.query).not.toHaveProperty('fields');
  2634. expect(result.query).not.toHaveProperty('query');
  2635. expect(result.query.id).toEqual(state.id);
  2636. expect(result.query.statsPeriod).toEqual(state.statsPeriod);
  2637. expect(result.query.project).toEqual(state.project);
  2638. expect(result.query.environment).toEqual(state.environment);
  2639. });
  2640. it('generates a URL with customer domain context', function () {
  2641. const view = new EventView(state);
  2642. const result = view.getResultsViewShortUrlTarget(organization.slug);
  2643. expect(result.pathname).toEqual('/discover/results/');
  2644. expect(result.query).not.toHaveProperty('name');
  2645. expect(result.query).not.toHaveProperty('fields');
  2646. expect(result.query).not.toHaveProperty('query');
  2647. expect(result.query.id).toEqual(state.id);
  2648. expect(result.query.statsPeriod).toEqual(state.statsPeriod);
  2649. expect(result.query.project).toEqual(state.project);
  2650. expect(result.query.environment).toEqual(state.environment);
  2651. });
  2652. });
  2653. describe('EventView.getPerformanceTransactionEventsViewUrlTarget()', function () {
  2654. beforeEach(function () {
  2655. window.__initialData = {
  2656. ...window.__initialData,
  2657. customerDomain: {
  2658. subdomain: 'albertos-apples',
  2659. organizationUrl: 'https://albertos-apples.sentry.io',
  2660. sentryUrl: 'https://sentry.io',
  2661. },
  2662. };
  2663. });
  2664. afterEach(function () {
  2665. window.__initialData = {
  2666. ...window.__initialData,
  2667. customerDomain: null,
  2668. };
  2669. });
  2670. const state: ConstructorParameters<typeof EventView>[0] = {
  2671. ...REQUIRED_CONSTRUCTOR_PROPS,
  2672. id: '1234',
  2673. name: 'best query',
  2674. fields: [{field: 'count()'}, {field: 'project.id'}],
  2675. sorts: generateSorts([AggregationKey.COUNT]),
  2676. query: 'event.type:error',
  2677. project: [42],
  2678. start: '2019-10-01T00:00:00',
  2679. end: '2019-10-02T00:00:00',
  2680. statsPeriod: '14d',
  2681. environment: ['staging'],
  2682. display: 'previous',
  2683. dataset: DiscoverDatasets.DISCOVER,
  2684. };
  2685. const organization = Organization();
  2686. const showTransactions = EventsDisplayFilterName.P99;
  2687. const breakdown = SpanOperationBreakdownFilter.HTTP;
  2688. const webVital = WebVital.LCP;
  2689. it('generates a URL with non-customer domain context', function () {
  2690. window.__initialData.customerDomain = null;
  2691. const view = new EventView(state);
  2692. const result = view.getPerformanceTransactionEventsViewUrlTarget(organization.slug, {
  2693. showTransactions,
  2694. breakdown,
  2695. webVital,
  2696. });
  2697. expect(result.pathname).toEqual(
  2698. '/organizations/org-slug/performance/summary/events/'
  2699. );
  2700. expect(result.query.query).toEqual(state.query);
  2701. expect(result.query.project).toEqual(state.project);
  2702. expect(result.query.sort).toEqual(['-count']);
  2703. expect(result.query.transaction).toEqual(state.name);
  2704. expect(result.query.showTransactions).toEqual(showTransactions);
  2705. expect(result.query.breakdown).toEqual(breakdown);
  2706. expect(result.query.webVital).toEqual(webVital);
  2707. });
  2708. it('generates a URL with customer domain context', function () {
  2709. const view = new EventView(state);
  2710. const result = view.getPerformanceTransactionEventsViewUrlTarget(organization.slug, {
  2711. showTransactions,
  2712. breakdown,
  2713. webVital,
  2714. });
  2715. expect(result.pathname).toEqual('/performance/summary/events/');
  2716. expect(result.query.query).toEqual(state.query);
  2717. expect(result.query.project).toEqual(state.project);
  2718. expect(result.query.sort).toEqual(['-count']);
  2719. expect(result.query.transaction).toEqual(state.name);
  2720. expect(result.query.showTransactions).toEqual(showTransactions);
  2721. expect(result.query.breakdown).toEqual(breakdown);
  2722. expect(result.query.webVital).toEqual(webVital);
  2723. });
  2724. });
  2725. describe('EventView.getPageFilters()', function () {
  2726. it('return default global selection', function () {
  2727. const eventView = new EventView({
  2728. ...REQUIRED_CONSTRUCTOR_PROPS,
  2729. });
  2730. expect(eventView.getPageFilters()).toMatchObject({
  2731. projects: [],
  2732. environments: [],
  2733. datetime: {
  2734. start: null,
  2735. end: null,
  2736. period: null,
  2737. // event views currently do not support the utc option,
  2738. // see comment in EventView.getPageFilters
  2739. utc: true,
  2740. },
  2741. });
  2742. });
  2743. it('returns global selection query', function () {
  2744. const state2 = {
  2745. ...REQUIRED_CONSTRUCTOR_PROPS,
  2746. project: [42],
  2747. start: 'start',
  2748. end: 'end',
  2749. statsPeriod: '42d',
  2750. environment: ['prod'],
  2751. };
  2752. const eventView = new EventView(state2);
  2753. expect(eventView.getPageFilters()).toMatchObject({
  2754. projects: state2.project,
  2755. environments: state2.environment,
  2756. datetime: {
  2757. start: state2.start,
  2758. end: state2.end,
  2759. period: state2.statsPeriod,
  2760. // event views currently do not support the utc option,
  2761. // see comment in EventView.getPageFilters
  2762. utc: true,
  2763. },
  2764. });
  2765. });
  2766. });
  2767. describe('EventView.getPageFiltersQuery()', function () {
  2768. it('return default global selection query', function () {
  2769. const eventView = new EventView({
  2770. ...REQUIRED_CONSTRUCTOR_PROPS,
  2771. });
  2772. expect(eventView.getPageFiltersQuery()).toMatchObject({
  2773. project: [],
  2774. start: undefined,
  2775. end: undefined,
  2776. statsPeriod: undefined,
  2777. environment: [],
  2778. // event views currently do not support the utc option,
  2779. // see comment in EventView.getPageFilters
  2780. utc: 'true',
  2781. });
  2782. });
  2783. it('returns global selection query', function () {
  2784. const state2 = {
  2785. ...REQUIRED_CONSTRUCTOR_PROPS,
  2786. project: [42],
  2787. start: 'start',
  2788. end: 'end',
  2789. statsPeriod: '42d',
  2790. environment: ['prod'],
  2791. };
  2792. const eventView = new EventView(state2);
  2793. expect(eventView.getPageFiltersQuery()).toEqual({
  2794. end: 'end',
  2795. start: 'start',
  2796. statsPeriod: '42d',
  2797. environment: ['prod'],
  2798. project: ['42'],
  2799. utc: 'true',
  2800. });
  2801. });
  2802. });
  2803. describe('EventView.generateBlankQueryStringObject()', function () {
  2804. it('should return blank values', function () {
  2805. const eventView = new EventView({
  2806. ...REQUIRED_CONSTRUCTOR_PROPS,
  2807. });
  2808. expect(eventView.generateBlankQueryStringObject()).toEqual({
  2809. id: undefined,
  2810. name: undefined,
  2811. fields: undefined,
  2812. sorts: undefined,
  2813. query: undefined,
  2814. project: undefined,
  2815. start: undefined,
  2816. end: undefined,
  2817. statsPeriod: undefined,
  2818. environment: undefined,
  2819. yAxis: undefined,
  2820. cursor: undefined,
  2821. });
  2822. });
  2823. });
  2824. describe('EventView.getYAxisOptions()', function () {
  2825. const state: ConstructorParameters<typeof EventView>[0] = {
  2826. ...REQUIRED_CONSTRUCTOR_PROPS,
  2827. fields: [],
  2828. sorts: [],
  2829. query: '',
  2830. project: [],
  2831. statsPeriod: '42d',
  2832. environment: [],
  2833. };
  2834. function generateYaxis(value) {
  2835. return {
  2836. value,
  2837. label: value,
  2838. };
  2839. }
  2840. it('should return default options', function () {
  2841. const thisEventView = new EventView(state);
  2842. expect(thisEventView.getYAxisOptions()).toEqual(CHART_AXIS_OPTIONS);
  2843. });
  2844. it('should add aggregate fields as options', function () {
  2845. let thisEventView = new EventView({
  2846. ...state,
  2847. fields: generateFields(['ignored-field', 'count_unique(issue)']),
  2848. });
  2849. expect(thisEventView.getYAxisOptions()).toEqual([
  2850. generateYaxis('count_unique(issue)'),
  2851. ...CHART_AXIS_OPTIONS,
  2852. ]);
  2853. // should de-duplicate entries
  2854. thisEventView = new EventView({
  2855. ...state,
  2856. fields: generateFields(['ignored-field', 'count()']),
  2857. });
  2858. expect(thisEventView.getYAxisOptions()).toEqual([...CHART_AXIS_OPTIONS]);
  2859. });
  2860. it('should exclude yAxis options that are not useful', function () {
  2861. const thisEventView = new EventView({
  2862. ...state,
  2863. fields: generateFields([
  2864. 'ignored-field',
  2865. 'count_unique(issue)',
  2866. 'last_seen()',
  2867. 'max(timestamp)',
  2868. ]),
  2869. });
  2870. expect(thisEventView.getYAxisOptions()).toEqual([
  2871. generateYaxis('count_unique(issue)'),
  2872. ...CHART_AXIS_OPTIONS,
  2873. ]);
  2874. });
  2875. });
  2876. describe('EventView.getYAxis()', function () {
  2877. const state: ConstructorParameters<typeof EventView>[0] = {
  2878. ...REQUIRED_CONSTRUCTOR_PROPS,
  2879. fields: [],
  2880. sorts: [],
  2881. query: '',
  2882. project: [],
  2883. statsPeriod: '42d',
  2884. environment: [],
  2885. };
  2886. it('should return first default yAxis', function () {
  2887. const thisEventView = new EventView(state);
  2888. expect(thisEventView.getYAxis()).toEqual('count()');
  2889. });
  2890. it('should return valid yAxis', function () {
  2891. const thisEventView = new EventView({
  2892. ...state,
  2893. fields: generateFields(['ignored-field', 'count_unique(user)', 'last_seen']),
  2894. yAxis: 'count_unique(user)',
  2895. });
  2896. expect(thisEventView.getYAxis()).toEqual('count_unique(user)');
  2897. });
  2898. it('should ignore invalid yAxis', function () {
  2899. const invalid = [
  2900. 'last_seen',
  2901. 'latest_event',
  2902. 'count_unique(issue)', // this is not one of the selected fields
  2903. ];
  2904. for (const option of invalid) {
  2905. const thisEventView = new EventView({
  2906. ...state,
  2907. fields: generateFields(['ignored-field', 'last_seen', 'latest_event']),
  2908. yAxis: option,
  2909. });
  2910. // yAxis defaults to the first entry of the default yAxis options
  2911. expect(thisEventView.getYAxis()).toEqual('count()');
  2912. }
  2913. });
  2914. });
  2915. describe('EventView.getDisplayOptions()', function () {
  2916. const state: ConstructorParameters<typeof EventView>[0] = {
  2917. ...REQUIRED_CONSTRUCTOR_PROPS,
  2918. fields: [],
  2919. sorts: [],
  2920. query: '',
  2921. project: [],
  2922. statsPeriod: '42d',
  2923. environment: [],
  2924. };
  2925. it('should return default options', function () {
  2926. const eventView = new EventView({
  2927. ...state,
  2928. // there needs to exist an aggregate or TOP 5 modes will be disabled
  2929. fields: [{field: 'count()'}],
  2930. });
  2931. expect(eventView.getDisplayOptions()).toEqual(DISPLAY_MODE_OPTIONS);
  2932. });
  2933. it('should disable previous when start/end are used.', function () {
  2934. const eventView = new EventView({
  2935. ...state,
  2936. end: '2020-04-13T12:13:14',
  2937. start: '2020-04-01T12:13:14',
  2938. });
  2939. const options = eventView.getDisplayOptions();
  2940. expect(options[1].value).toEqual('previous');
  2941. expect(options[1].disabled).toBeTruthy();
  2942. });
  2943. it('should disable top 5 period/daily if no aggregates present', function () {
  2944. const eventView = new EventView({
  2945. ...state,
  2946. });
  2947. const options = eventView.getDisplayOptions();
  2948. expect(options[2].value).toEqual('top5');
  2949. expect(options[2].disabled).toBeTruthy();
  2950. expect(options[4].value).toEqual('dailytop5');
  2951. expect(options[4].disabled).toBeTruthy();
  2952. });
  2953. });
  2954. describe('EventView.getDisplayMode()', function () {
  2955. const state: ConstructorParameters<typeof EventView>[0] = {
  2956. ...REQUIRED_CONSTRUCTOR_PROPS,
  2957. fields: [],
  2958. sorts: [],
  2959. query: '',
  2960. project: [],
  2961. statsPeriod: '42d',
  2962. environment: [],
  2963. };
  2964. it('should have default', function () {
  2965. const eventView = new EventView({
  2966. ...state,
  2967. });
  2968. const displayMode = eventView.getDisplayMode();
  2969. expect(displayMode).toEqual(DisplayModes.DEFAULT);
  2970. });
  2971. it('should return current mode when not disabled', function () {
  2972. const eventView = new EventView({
  2973. ...state,
  2974. display: DisplayModes.DAILY,
  2975. });
  2976. const displayMode = eventView.getDisplayMode();
  2977. expect(displayMode).toEqual(DisplayModes.DAILY);
  2978. });
  2979. it('should return default mode when disabled', function () {
  2980. const eventView = new EventView({
  2981. ...state,
  2982. // the existence of start and end will disable the PREVIOUS mode
  2983. end: '2020-04-13T12:13:14',
  2984. start: '2020-04-01T12:13:14',
  2985. display: DisplayModes.PREVIOUS,
  2986. });
  2987. const displayMode = eventView.getDisplayMode();
  2988. expect(displayMode).toEqual(DisplayModes.DEFAULT);
  2989. });
  2990. it('top 5 should fallback to default when disabled', function () {
  2991. const eventView = new EventView({
  2992. ...state,
  2993. // the lack of an aggregate will disable the TOP5 mode
  2994. display: DisplayModes.TOP5,
  2995. });
  2996. const displayMode = eventView.getDisplayMode();
  2997. expect(displayMode).toEqual(DisplayModes.DEFAULT);
  2998. });
  2999. it('top 5 daily should fallback to daily when disabled', function () {
  3000. const eventView = new EventView({
  3001. ...state,
  3002. // the lack of an aggregate will disable the DAILYTOP5 mode
  3003. display: DisplayModes.DAILYTOP5,
  3004. });
  3005. const displayMode = eventView.getDisplayMode();
  3006. expect(displayMode).toEqual(DisplayModes.DAILY);
  3007. });
  3008. it('daily mode should fall back to default when disabled', function () {
  3009. const eventView = new EventView({
  3010. ...state,
  3011. // the period being less than 24h will disable the DAILY mode
  3012. statsPeriod: '1h',
  3013. display: DisplayModes.DAILY,
  3014. });
  3015. const displayMode = eventView.getDisplayMode();
  3016. expect(displayMode).toEqual(DisplayModes.DEFAULT);
  3017. });
  3018. it('top 5 daily mode should fall back to default when daily is disabled', function () {
  3019. const eventView = new EventView({
  3020. ...state,
  3021. // the period being less than 24h will disable the DAILY mode
  3022. statsPeriod: undefined,
  3023. start: '2020-04-01T12:13:14',
  3024. end: '2020-04-02T12:10:14',
  3025. display: DisplayModes.DAILYTOP5,
  3026. });
  3027. const displayMode = eventView.getDisplayMode();
  3028. expect(displayMode).toEqual(DisplayModes.DEFAULT);
  3029. });
  3030. });
  3031. describe('EventView.getAggregateFields()', function () {
  3032. const state: ConstructorParameters<typeof EventView>[0] = {
  3033. ...REQUIRED_CONSTRUCTOR_PROPS,
  3034. fields: [
  3035. {field: 'title'},
  3036. {field: 'count()'},
  3037. {field: 'count_unique(user)'},
  3038. {field: 'apdex(300)'},
  3039. {field: 'transaction'},
  3040. ],
  3041. sorts: [],
  3042. query: '',
  3043. project: [],
  3044. statsPeriod: '42d',
  3045. environment: [],
  3046. };
  3047. it('getAggregateFields() returns only aggregates', function () {
  3048. const eventView = new EventView(state);
  3049. const expected = [
  3050. {field: 'count()'},
  3051. {field: 'count_unique(user)'},
  3052. {field: 'apdex(300)'},
  3053. ];
  3054. expect(eventView.getAggregateFields()).toEqual(expected);
  3055. });
  3056. });
  3057. describe('EventView.hasAggregateField', function () {
  3058. it('ensures an eventview has an aggregate field', function () {
  3059. let eventView = new EventView({
  3060. ...REQUIRED_CONSTRUCTOR_PROPS,
  3061. fields: [{field: 'foobar'}],
  3062. sorts: [],
  3063. query: '',
  3064. project: [],
  3065. environment: [],
  3066. });
  3067. expect(eventView.hasAggregateField()).toBe(false);
  3068. eventView = new EventView({
  3069. ...REQUIRED_CONSTRUCTOR_PROPS,
  3070. fields: [{field: 'count(foo.bar.is-Enterprise_42)'}],
  3071. sorts: [],
  3072. query: '',
  3073. project: [],
  3074. environment: [],
  3075. });
  3076. expect(eventView.hasAggregateField()).toBe(true);
  3077. });
  3078. });
  3079. describe('isAPIPayloadSimilar', function () {
  3080. const state: ConstructorParameters<typeof EventView>[0] = {
  3081. ...REQUIRED_CONSTRUCTOR_PROPS,
  3082. id: '1234',
  3083. name: 'best query',
  3084. fields: [{field: 'count()'}, {field: 'project.id'}],
  3085. sorts: generateSorts([AggregationKey.COUNT]),
  3086. query: 'event.type:error',
  3087. project: [42],
  3088. start: '2019-10-01T00:00:00',
  3089. end: '2019-10-02T00:00:00',
  3090. statsPeriod: '14d',
  3091. environment: ['staging'],
  3092. };
  3093. const meta: MetaType = {
  3094. count: 'integer',
  3095. title: 'string',
  3096. };
  3097. describe('getEventsAPIPayload', function () {
  3098. it('is not similar when relevant query string keys are present in the Location object', function () {
  3099. const thisEventView = new EventView(state);
  3100. const location = TestStubs.location({
  3101. query: {
  3102. project: 'project',
  3103. environment: 'environment',
  3104. start: 'start',
  3105. end: 'end',
  3106. utc: 'utc',
  3107. statsPeriod: 'statsPeriod',
  3108. cursor: 'cursor',
  3109. },
  3110. });
  3111. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3112. const otherLocation = TestStubs.location({});
  3113. const otherAPIPayload = thisEventView.getEventsAPIPayload(otherLocation);
  3114. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3115. expect(results).toBe(false);
  3116. });
  3117. it('is similar when irrelevant query string keys are present in the Location object', function () {
  3118. const thisEventView = new EventView(state);
  3119. const location = TestStubs.location({
  3120. query: {
  3121. bestCountry: 'canada',
  3122. },
  3123. });
  3124. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3125. const otherLocation = TestStubs.location({});
  3126. const otherAPIPayload = thisEventView.getEventsAPIPayload(otherLocation);
  3127. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3128. expect(results).toBe(true);
  3129. });
  3130. it('is not similar on sort key sorted in opposite directions', function () {
  3131. const thisEventView = new EventView(state);
  3132. const location = TestStubs.location({});
  3133. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3134. const otherEventView = thisEventView.sortOnField({field: 'count()'}, meta);
  3135. const otherLocation = TestStubs.location({});
  3136. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3137. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3138. expect(results).toBe(false);
  3139. });
  3140. it('is not similar when a new column is added', function () {
  3141. const thisEventView = new EventView(state);
  3142. const location = TestStubs.location({});
  3143. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3144. const otherEventView = new EventView({
  3145. ...state,
  3146. fields: [...state.fields, {field: 'title', width: COL_WIDTH_UNDEFINED}],
  3147. });
  3148. const otherLocation = TestStubs.location({});
  3149. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3150. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3151. expect(results).toBe(false);
  3152. });
  3153. it('is similar when a column is updated with no changes', function () {
  3154. const thisEventView = new EventView(state);
  3155. const location = TestStubs.location({});
  3156. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3157. const newColumn: Column = {
  3158. kind: 'function',
  3159. function: [AggregationKey.COUNT, '', undefined, undefined],
  3160. };
  3161. const otherEventView = thisEventView.withUpdatedColumn(0, newColumn, meta);
  3162. const otherLocation = TestStubs.location({});
  3163. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3164. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3165. expect(results).toBe(true);
  3166. });
  3167. it('is not similar when a column is updated with a replaced field', function () {
  3168. const thisEventView = new EventView(state);
  3169. const location = TestStubs.location({});
  3170. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3171. const newColumn: Column = {
  3172. kind: 'field',
  3173. field: 'title',
  3174. };
  3175. const otherEventView = thisEventView.withUpdatedColumn(0, newColumn, meta);
  3176. const otherLocation = TestStubs.location({});
  3177. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3178. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3179. expect(results).toBe(false);
  3180. });
  3181. it('is not similar when a column is updated with a replaced aggregation', function () {
  3182. const thisEventView = new EventView(state);
  3183. const location = TestStubs.location({});
  3184. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3185. const newColumn: Column = {
  3186. kind: 'function',
  3187. function: [AggregationKey.AVG, '', undefined, undefined],
  3188. };
  3189. const otherEventView = thisEventView.withUpdatedColumn(0, newColumn, meta);
  3190. const otherLocation = TestStubs.location({});
  3191. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3192. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3193. expect(results).toBe(false);
  3194. });
  3195. it('is similar when a column is renamed', function () {
  3196. const thisEventView = new EventView(state);
  3197. const location = TestStubs.location({});
  3198. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3199. const newColumn: Column = {
  3200. kind: 'function',
  3201. function: [AggregationKey.COUNT, '', undefined, undefined],
  3202. };
  3203. const otherEventView = thisEventView.withUpdatedColumn(0, newColumn, meta);
  3204. const otherLocation = TestStubs.location({});
  3205. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3206. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3207. expect(results).toBe(true);
  3208. });
  3209. it('is not similar when a column is deleted', function () {
  3210. const thisEventView = new EventView(state);
  3211. const location = TestStubs.location({});
  3212. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3213. const otherEventView = thisEventView.withDeletedColumn(0, meta);
  3214. const otherLocation = TestStubs.location({});
  3215. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3216. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3217. expect(results).toBe(false);
  3218. });
  3219. it('is similar if column order changes', function () {
  3220. const thisEventView = new EventView(state);
  3221. const location = TestStubs.location({});
  3222. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3223. const otherEventView = new EventView({...state, fields: shuffle(state.fields)});
  3224. const otherLocation = TestStubs.location({});
  3225. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3226. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3227. expect(results).toBe(true);
  3228. });
  3229. it('is similar if equation order relatively same', function () {
  3230. const equationField = {field: 'equation|failure_count() / count()'};
  3231. const otherEquationField = {field: 'equation|failure_count() / 2'};
  3232. state.fields = [
  3233. {field: 'project.id'},
  3234. {field: 'count()'},
  3235. equationField,
  3236. otherEquationField,
  3237. ];
  3238. const thisEventView = new EventView(state);
  3239. const location = TestStubs.location({});
  3240. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3241. state.fields = [
  3242. equationField,
  3243. {field: 'project.id'},
  3244. {field: 'count()'},
  3245. otherEquationField,
  3246. ];
  3247. const otherEventView = new EventView(state);
  3248. const otherLocation = TestStubs.location({});
  3249. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3250. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3251. expect(results).toBe(true);
  3252. });
  3253. it('is not similar if equation order changes', function () {
  3254. const equationField = {field: 'equation|failure_count() / count()'};
  3255. const otherEquationField = {field: 'equation|failure_count() / 2'};
  3256. state.fields = [
  3257. {field: 'project.id'},
  3258. {field: 'count()'},
  3259. equationField,
  3260. otherEquationField,
  3261. ];
  3262. const thisEventView = new EventView(state);
  3263. const location = TestStubs.location({});
  3264. const thisAPIPayload = thisEventView.getEventsAPIPayload(location);
  3265. state.fields = [
  3266. {field: 'project.id'},
  3267. {field: 'count()'},
  3268. otherEquationField,
  3269. equationField,
  3270. ];
  3271. const otherEventView = new EventView(state);
  3272. const otherLocation = TestStubs.location({});
  3273. const otherAPIPayload = otherEventView.getEventsAPIPayload(otherLocation);
  3274. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3275. expect(results).toBe(false);
  3276. });
  3277. });
  3278. describe('getFacetsAPIPayload', function () {
  3279. it('only includes relevant parameters', function () {
  3280. const thisEventView = new EventView(state);
  3281. const location = TestStubs.location({});
  3282. const results = thisEventView.getFacetsAPIPayload(location);
  3283. const expected = {
  3284. query: state.query,
  3285. project: ['42'],
  3286. statsPeriod: state.statsPeriod,
  3287. environment: state.environment,
  3288. };
  3289. expect(results).toEqual(expected);
  3290. });
  3291. it('is similar on sort key sorted in opposite directions', function () {
  3292. const thisEventView = new EventView(state);
  3293. const location = TestStubs.location({});
  3294. const thisAPIPayload = thisEventView.getFacetsAPIPayload(location);
  3295. const newColumn: Column = {
  3296. kind: 'function',
  3297. function: [AggregationKey.COUNT, '', undefined, undefined],
  3298. };
  3299. const otherEventView = thisEventView.withUpdatedColumn(0, newColumn, meta);
  3300. const otherLocation = TestStubs.location({});
  3301. const otherAPIPayload = otherEventView.getFacetsAPIPayload(otherLocation);
  3302. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3303. expect(results).toBe(true);
  3304. });
  3305. it('is similar when a columns are different', function () {
  3306. const thisEventView = new EventView(state);
  3307. const location = TestStubs.location({});
  3308. const thisAPIPayload = thisEventView.getFacetsAPIPayload(location);
  3309. const otherEventView = new EventView({
  3310. ...state,
  3311. fields: [...state.fields, {field: 'title', width: COL_WIDTH_UNDEFINED}],
  3312. });
  3313. const otherLocation = TestStubs.location({});
  3314. const otherAPIPayload = otherEventView.getFacetsAPIPayload(otherLocation);
  3315. const results = isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  3316. expect(results).toBe(true);
  3317. });
  3318. });
  3319. });
  3320. describe('pickRelevantLocationQueryStrings', function () {
  3321. it('picks relevant query strings', function () {
  3322. const location = TestStubs.location({
  3323. query: {
  3324. project: 'project',
  3325. environment: 'environment',
  3326. start: 'start',
  3327. end: 'end',
  3328. utc: 'utc',
  3329. statsPeriod: 'statsPeriod',
  3330. cursor: 'cursor',
  3331. // irrelevant query strings
  3332. bestCountry: 'canada',
  3333. },
  3334. });
  3335. const actual = pickRelevantLocationQueryStrings(location);
  3336. const expected = {
  3337. start: 'start',
  3338. end: 'end',
  3339. utc: 'utc',
  3340. statsPeriod: 'statsPeriod',
  3341. cursor: 'cursor',
  3342. };
  3343. expect(actual).toEqual(expected);
  3344. });
  3345. });