eventView.spec.tsx 110 KB

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