test_organization_events_stats.py 94 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486
  1. import uuid
  2. from datetime import timedelta
  3. from unittest import mock
  4. from uuid import uuid4
  5. import pytest
  6. from dateutil.parser import parse as parse_date
  7. from django.urls import reverse
  8. from django.utils import timezone
  9. from pytz import utc
  10. from snuba_sdk.column import Column
  11. from snuba_sdk.conditions import Condition, Op
  12. from snuba_sdk.function import Function
  13. from sentry.constants import MAX_TOP_EVENTS
  14. from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric
  15. from sentry.snuba.discover import OTHER_KEY
  16. from sentry.testutils import APITestCase, SnubaTestCase
  17. from sentry.testutils.helpers.datetime import before_now, iso_format
  18. from sentry.testutils.silo import region_silo_test
  19. from sentry.types.issues import GroupType
  20. from sentry.utils.samples import load_data
  21. from tests.sentry.issues.test_utils import SearchIssueTestMixin
  22. pytestmark = pytest.mark.sentry_metrics
  23. @region_silo_test
  24. class OrganizationEventsStatsEndpointTest(APITestCase, SnubaTestCase, SearchIssueTestMixin):
  25. endpoint = "sentry-api-0-organization-events-stats"
  26. def setUp(self):
  27. super().setUp()
  28. self.login_as(user=self.user)
  29. self.authed_user = self.user
  30. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  31. self.project = self.create_project()
  32. self.project2 = self.create_project()
  33. self.user = self.create_user()
  34. self.user2 = self.create_user()
  35. self.store_event(
  36. data={
  37. "event_id": "a" * 32,
  38. "message": "very bad",
  39. "timestamp": iso_format(self.day_ago + timedelta(minutes=1)),
  40. "fingerprint": ["group1"],
  41. "tags": {"sentry:user": self.user.email},
  42. },
  43. project_id=self.project.id,
  44. )
  45. self.store_event(
  46. data={
  47. "event_id": "b" * 32,
  48. "message": "oh my",
  49. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=1)),
  50. "fingerprint": ["group2"],
  51. "tags": {"sentry:user": self.user2.email},
  52. },
  53. project_id=self.project2.id,
  54. )
  55. self.store_event(
  56. data={
  57. "event_id": "c" * 32,
  58. "message": "very bad",
  59. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
  60. "fingerprint": ["group2"],
  61. "tags": {"sentry:user": self.user2.email},
  62. },
  63. project_id=self.project2.id,
  64. )
  65. self.url = reverse(
  66. "sentry-api-0-organization-events-stats",
  67. kwargs={"organization_slug": self.project.organization.slug},
  68. )
  69. self.features = {}
  70. def do_request(self, data, url=None, features=None):
  71. if features is None:
  72. features = {"organizations:discover-basic": True}
  73. features.update(self.features)
  74. with self.feature(features):
  75. return self.client.get(self.url if url is None else url, data=data, format="json")
  76. def test_simple(self):
  77. response = self.do_request(
  78. {
  79. "start": iso_format(self.day_ago),
  80. "end": iso_format(self.day_ago + timedelta(hours=2)),
  81. "interval": "1h",
  82. },
  83. )
  84. assert response.status_code == 200, response.content
  85. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  86. def test_generic_issue(self):
  87. _, _, group_info = self.store_search_issue(
  88. self.project.id,
  89. self.user.id,
  90. [f"{GroupType.PROFILE_BLOCKED_THREAD.value}-group1"],
  91. "prod",
  92. timezone.now().replace(hour=0, minute=0, second=0) + timedelta(minutes=1),
  93. )
  94. self.store_search_issue(
  95. self.project.id,
  96. self.user.id,
  97. [f"{GroupType.PROFILE_BLOCKED_THREAD.value}-group1"],
  98. "prod",
  99. timezone.now().replace(hour=1, minute=0, second=0) + timedelta(minutes=1),
  100. )
  101. self.store_search_issue(
  102. self.project.id,
  103. self.user.id,
  104. [f"{GroupType.PROFILE_BLOCKED_THREAD.value}-group1"],
  105. "prod",
  106. timezone.now().replace(hour=1, minute=0, second=0) + timedelta(minutes=2),
  107. )
  108. with self.feature(
  109. [
  110. "organizations:profiling",
  111. ]
  112. ):
  113. response = self.do_request(
  114. {
  115. "start": timezone.now().replace(hour=0, minute=0, second=0),
  116. "end": timezone.now().replace(hour=2, minute=0, second=0),
  117. "interval": "1h",
  118. "query": f"issue:{group_info.group.qualified_short_id}",
  119. "dataset": "issuePlatform",
  120. },
  121. )
  122. assert response.status_code == 200, response.content
  123. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  124. def test_generic_issue_calculated_interval(self):
  125. """Test that a 4h interval returns the correct generic event stats.
  126. This follows a different code path than 1h or 1d as the IssuePlatformTimeSeriesQueryBuilder
  127. does some calculation to create the time column."""
  128. _, _, group_info = self.store_search_issue(
  129. self.project.id,
  130. self.user.id,
  131. [f"{GroupType.PROFILE_BLOCKED_THREAD.value}-group1"],
  132. "prod",
  133. timezone.now().replace(hour=0, minute=0, second=0) + timedelta(minutes=1),
  134. )
  135. self.store_search_issue(
  136. self.project.id,
  137. self.user.id,
  138. [f"{GroupType.PROFILE_BLOCKED_THREAD.value}-group1"],
  139. "prod",
  140. timezone.now().replace(hour=3, minute=0, second=0) + timedelta(minutes=1),
  141. )
  142. self.store_search_issue(
  143. self.project.id,
  144. self.user.id,
  145. [f"{GroupType.PROFILE_BLOCKED_THREAD.value}-group1"],
  146. "prod",
  147. timezone.now().replace(hour=3, minute=1, second=0) + timedelta(minutes=2),
  148. )
  149. with self.feature(
  150. [
  151. "organizations:profiling",
  152. ]
  153. ):
  154. response = self.do_request(
  155. {
  156. "start": timezone.now().replace(hour=0, minute=0, second=0),
  157. "end": timezone.now().replace(hour=4, minute=0, second=0),
  158. "interval": "4h",
  159. "query": f"issue:{group_info.group.qualified_short_id}",
  160. "dataset": "issuePlatform",
  161. },
  162. )
  163. assert response.status_code == 200, response.content
  164. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 3}], [{"count": 0}]]
  165. def test_misaligned_last_bucket(self):
  166. response = self.do_request(
  167. data={
  168. "start": iso_format(self.day_ago - timedelta(minutes=30)),
  169. "end": iso_format(self.day_ago + timedelta(hours=1, minutes=30)),
  170. "interval": "1h",
  171. "partial": "1",
  172. },
  173. )
  174. assert response.status_code == 200, response.content
  175. assert [attrs for time, attrs in response.data["data"]] == [
  176. [{"count": 0}],
  177. [{"count": 1}],
  178. [{"count": 2}],
  179. ]
  180. def test_no_projects(self):
  181. org = self.create_organization(owner=self.user)
  182. self.login_as(user=self.user)
  183. url = reverse(
  184. "sentry-api-0-organization-events-stats", kwargs={"organization_slug": org.slug}
  185. )
  186. response = self.do_request({}, url)
  187. assert response.status_code == 200, response.content
  188. assert len(response.data["data"]) == 0
  189. def test_user_count(self):
  190. self.store_event(
  191. data={
  192. "event_id": "d" * 32,
  193. "message": "something",
  194. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  195. "tags": {"sentry:user": self.user2.email},
  196. "fingerprint": ["group2"],
  197. },
  198. project_id=self.project2.id,
  199. )
  200. response = self.do_request(
  201. data={
  202. "start": iso_format(self.day_ago),
  203. "end": iso_format(self.day_ago + timedelta(hours=2)),
  204. "interval": "1h",
  205. "yAxis": "user_count",
  206. },
  207. )
  208. assert response.status_code == 200, response.content
  209. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 2}], [{"count": 1}]]
  210. def test_discover2_backwards_compatibility(self):
  211. response = self.do_request(
  212. data={
  213. "project": self.project.id,
  214. "start": iso_format(self.day_ago),
  215. "end": iso_format(self.day_ago + timedelta(hours=2)),
  216. "interval": "1h",
  217. "yAxis": "user_count",
  218. },
  219. )
  220. assert response.status_code == 200, response.content
  221. assert len(response.data["data"]) > 0
  222. response = self.do_request(
  223. data={
  224. "project": self.project.id,
  225. "start": iso_format(self.day_ago),
  226. "end": iso_format(self.day_ago + timedelta(hours=2)),
  227. "interval": "1h",
  228. "yAxis": "event_count",
  229. },
  230. )
  231. assert response.status_code == 200, response.content
  232. assert len(response.data["data"]) > 0
  233. def test_with_event_count_flag(self):
  234. response = self.do_request(
  235. data={
  236. "start": iso_format(self.day_ago),
  237. "end": iso_format(self.day_ago + timedelta(hours=2)),
  238. "interval": "1h",
  239. "yAxis": "event_count",
  240. },
  241. )
  242. assert response.status_code == 200, response.content
  243. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  244. def test_performance_view_feature(self):
  245. response = self.do_request(
  246. data={
  247. "end": iso_format(before_now()),
  248. "start": iso_format(before_now(hours=2)),
  249. "query": "project_id:1",
  250. "interval": "30m",
  251. "yAxis": "count()",
  252. },
  253. features={
  254. "organizations:performance-view": True,
  255. "organizations:discover-basic": False,
  256. },
  257. )
  258. assert response.status_code == 200, response.content
  259. def test_apdex_divide_by_zero(self):
  260. ProjectTransactionThreshold.objects.create(
  261. project=self.project,
  262. organization=self.project.organization,
  263. threshold=600,
  264. metric=TransactionMetric.LCP.value,
  265. )
  266. # Shouldn't count towards apdex
  267. data = load_data(
  268. "transaction",
  269. start_timestamp=self.day_ago + timedelta(minutes=(1)),
  270. timestamp=self.day_ago + timedelta(minutes=(3)),
  271. )
  272. data["transaction"] = "/apdex/new/"
  273. data["user"] = {"email": "1@example.com"}
  274. data["measurements"] = {}
  275. self.store_event(data, project_id=self.project.id)
  276. response = self.do_request(
  277. data={
  278. "start": iso_format(self.day_ago),
  279. "end": iso_format(self.day_ago + timedelta(hours=2)),
  280. "interval": "1h",
  281. "yAxis": "apdex()",
  282. "project": [self.project.id],
  283. },
  284. )
  285. assert response.status_code == 200, response.content
  286. assert len(response.data["data"]) == 2
  287. data = response.data["data"]
  288. # 0 transactions with LCP 0/0
  289. assert [attrs for time, attrs in response.data["data"]] == [
  290. [{"count": 0}],
  291. [{"count": 0}],
  292. ]
  293. def test_aggregate_function_apdex(self):
  294. project1 = self.create_project()
  295. project2 = self.create_project()
  296. events = [
  297. ("one", 400, project1.id),
  298. ("one", 400, project1.id),
  299. ("two", 3000, project2.id),
  300. ("two", 1000, project2.id),
  301. ("three", 3000, project2.id),
  302. ]
  303. for idx, event in enumerate(events):
  304. data = load_data(
  305. "transaction",
  306. start_timestamp=self.day_ago + timedelta(minutes=(1 + idx)),
  307. timestamp=self.day_ago + timedelta(minutes=(1 + idx), milliseconds=event[1]),
  308. )
  309. data["event_id"] = f"{idx}" * 32
  310. data["transaction"] = f"/apdex/new/{event[0]}"
  311. data["user"] = {"email": f"{idx}@example.com"}
  312. self.store_event(data, project_id=event[2])
  313. response = self.do_request(
  314. data={
  315. "start": iso_format(self.day_ago),
  316. "end": iso_format(self.day_ago + timedelta(hours=2)),
  317. "interval": "1h",
  318. "yAxis": "apdex()",
  319. },
  320. )
  321. assert response.status_code == 200, response.content
  322. assert [attrs for time, attrs in response.data["data"]] == [
  323. [{"count": 0.3}],
  324. [{"count": 0}],
  325. ]
  326. ProjectTransactionThreshold.objects.create(
  327. project=project1,
  328. organization=project1.organization,
  329. threshold=100,
  330. metric=TransactionMetric.DURATION.value,
  331. )
  332. ProjectTransactionThreshold.objects.create(
  333. project=project2,
  334. organization=project1.organization,
  335. threshold=100,
  336. metric=TransactionMetric.DURATION.value,
  337. )
  338. response = self.do_request(
  339. data={
  340. "start": iso_format(self.day_ago),
  341. "end": iso_format(self.day_ago + timedelta(hours=2)),
  342. "interval": "1h",
  343. "yAxis": "apdex()",
  344. },
  345. )
  346. assert response.status_code == 200, response.content
  347. assert [attrs for time, attrs in response.data["data"]] == [
  348. [{"count": 0.2}],
  349. [{"count": 0}],
  350. ]
  351. response = self.do_request(
  352. data={
  353. "start": iso_format(self.day_ago),
  354. "end": iso_format(self.day_ago + timedelta(hours=2)),
  355. "interval": "1h",
  356. "yAxis": ["user_count", "apdex()"],
  357. },
  358. )
  359. assert response.status_code == 200, response.content
  360. assert response.data["user_count"]["order"] == 0
  361. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  362. [{"count": 5}],
  363. [{"count": 0}],
  364. ]
  365. assert response.data["apdex()"]["order"] == 1
  366. assert [attrs for time, attrs in response.data["apdex()"]["data"]] == [
  367. [{"count": 0.2}],
  368. [{"count": 0}],
  369. ]
  370. def test_aggregate_function_count(self):
  371. response = self.do_request(
  372. data={
  373. "start": iso_format(self.day_ago),
  374. "end": iso_format(self.day_ago + timedelta(hours=2)),
  375. "interval": "1h",
  376. "yAxis": "count()",
  377. },
  378. )
  379. assert response.status_code == 200, response.content
  380. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  381. def test_invalid_aggregate(self):
  382. response = self.do_request(
  383. data={
  384. "start": iso_format(self.day_ago),
  385. "end": iso_format(self.day_ago + timedelta(hours=2)),
  386. "interval": "1h",
  387. "yAxis": "rubbish",
  388. },
  389. )
  390. assert response.status_code == 400, response.content
  391. def test_aggregate_function_user_count(self):
  392. response = self.do_request(
  393. data={
  394. "start": iso_format(self.day_ago),
  395. "end": iso_format(self.day_ago + timedelta(hours=2)),
  396. "interval": "1h",
  397. "yAxis": "count_unique(user)",
  398. },
  399. )
  400. assert response.status_code == 200, response.content
  401. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 1}]]
  402. def test_aggregate_invalid(self):
  403. response = self.do_request(
  404. data={
  405. "start": iso_format(self.day_ago),
  406. "end": iso_format(self.day_ago + timedelta(hours=2)),
  407. "interval": "1h",
  408. "yAxis": "nope(lol)",
  409. },
  410. )
  411. assert response.status_code == 400, response.content
  412. def test_throughput_epm_hour_rollup(self):
  413. project = self.create_project()
  414. # Each of these denotes how many events to create in each hour
  415. event_counts = [6, 0, 6, 3, 0, 3]
  416. for hour, count in enumerate(event_counts):
  417. for minute in range(count):
  418. self.store_event(
  419. data={
  420. "event_id": str(uuid.uuid1()),
  421. "message": "very bad",
  422. "timestamp": iso_format(
  423. self.day_ago + timedelta(hours=hour, minutes=minute)
  424. ),
  425. "fingerprint": ["group1"],
  426. "tags": {"sentry:user": self.user.email},
  427. },
  428. project_id=project.id,
  429. )
  430. for axis in ["epm()", "tpm()"]:
  431. response = self.do_request(
  432. data={
  433. "start": iso_format(self.day_ago),
  434. "end": iso_format(self.day_ago + timedelta(hours=6)),
  435. "interval": "1h",
  436. "yAxis": axis,
  437. "project": project.id,
  438. },
  439. )
  440. assert response.status_code == 200, response.content
  441. data = response.data["data"]
  442. assert len(data) == 6
  443. rows = data[0:6]
  444. for test in zip(event_counts, rows):
  445. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  446. def test_throughput_epm_day_rollup(self):
  447. project = self.create_project()
  448. # Each of these denotes how many events to create in each minute
  449. event_counts = [6, 0, 6, 3, 0, 3]
  450. for hour, count in enumerate(event_counts):
  451. for minute in range(count):
  452. self.store_event(
  453. data={
  454. "event_id": str(uuid.uuid1()),
  455. "message": "very bad",
  456. "timestamp": iso_format(
  457. self.day_ago + timedelta(hours=hour, minutes=minute)
  458. ),
  459. "fingerprint": ["group1"],
  460. "tags": {"sentry:user": self.user.email},
  461. },
  462. project_id=project.id,
  463. )
  464. for axis in ["epm()", "tpm()"]:
  465. response = self.do_request(
  466. data={
  467. "start": iso_format(self.day_ago),
  468. "end": iso_format(self.day_ago + timedelta(hours=24)),
  469. "interval": "24h",
  470. "yAxis": axis,
  471. "project": project.id,
  472. },
  473. )
  474. assert response.status_code == 200, response.content
  475. data = response.data["data"]
  476. assert len(data) == 2
  477. assert data[0][1][0]["count"] == sum(event_counts) / (86400.0 / 60.0)
  478. def test_throughput_eps_minute_rollup(self):
  479. project = self.create_project()
  480. # Each of these denotes how many events to create in each minute
  481. event_counts = [6, 0, 6, 3, 0, 3]
  482. for minute, count in enumerate(event_counts):
  483. for second in range(count):
  484. self.store_event(
  485. data={
  486. "event_id": str(uuid.uuid1()),
  487. "message": "very bad",
  488. "timestamp": iso_format(
  489. self.day_ago + timedelta(minutes=minute, seconds=second)
  490. ),
  491. "fingerprint": ["group1"],
  492. "tags": {"sentry:user": self.user.email},
  493. },
  494. project_id=project.id,
  495. )
  496. for axis in ["eps()", "tps()"]:
  497. response = self.do_request(
  498. data={
  499. "start": iso_format(self.day_ago),
  500. "end": iso_format(self.day_ago + timedelta(minutes=6)),
  501. "interval": "1m",
  502. "yAxis": axis,
  503. "project": project.id,
  504. },
  505. )
  506. assert response.status_code == 200, response.content
  507. data = response.data["data"]
  508. assert len(data) == 6
  509. rows = data[0:6]
  510. for test in zip(event_counts, rows):
  511. assert test[1][1][0]["count"] == test[0] / 60.0
  512. def test_throughput_eps_no_rollup(self):
  513. project = self.create_project()
  514. # Each of these denotes how many events to create in each minute
  515. event_counts = [6, 0, 6, 3, 0, 3]
  516. for minute, count in enumerate(event_counts):
  517. for second in range(count):
  518. self.store_event(
  519. data={
  520. "event_id": str(uuid.uuid1()),
  521. "message": "very bad",
  522. "timestamp": iso_format(
  523. self.day_ago + timedelta(minutes=minute, seconds=second)
  524. ),
  525. "fingerprint": ["group1"],
  526. "tags": {"sentry:user": self.user.email},
  527. },
  528. project_id=project.id,
  529. )
  530. response = self.do_request(
  531. data={
  532. "start": iso_format(self.day_ago),
  533. "end": iso_format(self.day_ago + timedelta(minutes=1)),
  534. "interval": "1s",
  535. "yAxis": "eps()",
  536. "project": project.id,
  537. },
  538. )
  539. assert response.status_code == 200, response.content
  540. data = response.data["data"]
  541. # expect 60 data points between time span of 0 and 60 seconds
  542. assert len(data) == 60
  543. rows = data[0:6]
  544. for row in rows:
  545. assert row[1][0]["count"] == 1
  546. def test_transaction_events(self):
  547. prototype = {
  548. "type": "transaction",
  549. "transaction": "api.issue.delete",
  550. "spans": [],
  551. "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
  552. "tags": {"important": "yes"},
  553. }
  554. fixtures = (
  555. ("d" * 32, before_now(minutes=32)),
  556. ("e" * 32, before_now(hours=1, minutes=2)),
  557. ("f" * 32, before_now(hours=1, minutes=35)),
  558. )
  559. for fixture in fixtures:
  560. data = prototype.copy()
  561. data["event_id"] = fixture[0]
  562. data["timestamp"] = iso_format(fixture[1])
  563. data["start_timestamp"] = iso_format(fixture[1] - timedelta(seconds=1))
  564. self.store_event(data=data, project_id=self.project.id)
  565. response = self.do_request(
  566. data={
  567. "project": self.project.id,
  568. "end": iso_format(before_now()),
  569. "start": iso_format(before_now(hours=2)),
  570. "query": "event.type:transaction",
  571. "interval": "30m",
  572. "yAxis": "count()",
  573. },
  574. )
  575. assert response.status_code == 200, response.content
  576. items = [item for time, item in response.data["data"] if item]
  577. # We could get more results depending on where the 30 min
  578. # windows land.
  579. assert len(items) >= 3
  580. def test_project_id_query_filter(self):
  581. response = self.do_request(
  582. data={
  583. "end": iso_format(before_now()),
  584. "start": iso_format(before_now(hours=2)),
  585. "query": "project_id:1",
  586. "interval": "30m",
  587. "yAxis": "count()",
  588. },
  589. )
  590. assert response.status_code == 200
  591. def test_latest_release_query_filter(self):
  592. response = self.do_request(
  593. data={
  594. "project": self.project.id,
  595. "end": iso_format(before_now()),
  596. "start": iso_format(before_now(hours=2)),
  597. "query": "release:latest",
  598. "interval": "30m",
  599. "yAxis": "count()",
  600. },
  601. )
  602. assert response.status_code == 200
  603. def test_conditional_filter(self):
  604. response = self.do_request(
  605. data={
  606. "start": iso_format(self.day_ago),
  607. "end": iso_format(self.day_ago + timedelta(hours=2)),
  608. "query": "id:{} OR id:{}".format("a" * 32, "b" * 32),
  609. "interval": "30m",
  610. "yAxis": "count()",
  611. },
  612. )
  613. assert response.status_code == 200, response.content
  614. data = response.data["data"]
  615. assert len(data) == 4
  616. assert data[0][1][0]["count"] == 1
  617. assert data[2][1][0]["count"] == 1
  618. def test_simple_multiple_yaxis(self):
  619. response = self.do_request(
  620. data={
  621. "start": iso_format(self.day_ago),
  622. "end": iso_format(self.day_ago + timedelta(hours=2)),
  623. "interval": "1h",
  624. "yAxis": ["user_count", "event_count"],
  625. },
  626. )
  627. assert response.status_code == 200, response.content
  628. assert response.data["user_count"]["order"] == 0
  629. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  630. [{"count": 1}],
  631. [{"count": 1}],
  632. ]
  633. assert response.data["event_count"]["order"] == 1
  634. assert [attrs for time, attrs in response.data["event_count"]["data"]] == [
  635. [{"count": 1}],
  636. [{"count": 2}],
  637. ]
  638. def test_equation_yaxis(self):
  639. response = self.do_request(
  640. data={
  641. "start": iso_format(self.day_ago),
  642. "end": iso_format(self.day_ago + timedelta(hours=2)),
  643. "interval": "1h",
  644. "yAxis": ["equation|count() / 100"],
  645. },
  646. )
  647. assert response.status_code == 200, response.content
  648. assert len(response.data["data"]) == 2
  649. assert [attrs for time, attrs in response.data["data"]] == [
  650. [{"count": 0.01}],
  651. [{"count": 0.02}],
  652. ]
  653. def test_equation_mixed_multi_yaxis(self):
  654. response = self.do_request(
  655. data={
  656. "start": iso_format(self.day_ago),
  657. "end": iso_format(self.day_ago + timedelta(hours=2)),
  658. "interval": "1h",
  659. "yAxis": ["count()", "equation|count() * 100"],
  660. },
  661. )
  662. assert response.status_code == 200, response.content
  663. assert response.data["count()"]["order"] == 0
  664. assert [attrs for time, attrs in response.data["count()"]["data"]] == [
  665. [{"count": 1}],
  666. [{"count": 2}],
  667. ]
  668. assert response.data["equation|count() * 100"]["order"] == 1
  669. assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
  670. [{"count": 100}],
  671. [{"count": 200}],
  672. ]
  673. def test_equation_multi_yaxis(self):
  674. response = self.do_request(
  675. data={
  676. "start": iso_format(self.day_ago),
  677. "end": iso_format(self.day_ago + timedelta(hours=2)),
  678. "interval": "1h",
  679. "yAxis": ["equation|count() / 100", "equation|count() * 100"],
  680. },
  681. )
  682. assert response.status_code == 200, response.content
  683. assert response.data["equation|count() / 100"]["order"] == 0
  684. assert [attrs for time, attrs in response.data["equation|count() / 100"]["data"]] == [
  685. [{"count": 0.01}],
  686. [{"count": 0.02}],
  687. ]
  688. assert response.data["equation|count() * 100"]["order"] == 1
  689. assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
  690. [{"count": 100}],
  691. [{"count": 200}],
  692. ]
  693. def test_large_interval_no_drop_values(self):
  694. self.store_event(
  695. data={
  696. "event_id": "d" * 32,
  697. "message": "not good",
  698. "timestamp": iso_format(self.day_ago - timedelta(minutes=10)),
  699. "fingerprint": ["group3"],
  700. },
  701. project_id=self.project.id,
  702. )
  703. response = self.do_request(
  704. data={
  705. "project": self.project.id,
  706. "end": iso_format(self.day_ago),
  707. "start": iso_format(self.day_ago - timedelta(hours=24)),
  708. "query": 'message:"not good"',
  709. "interval": "1d",
  710. "yAxis": "count()",
  711. },
  712. )
  713. assert response.status_code == 200
  714. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 0}], [{"count": 1}]]
  715. @mock.patch("sentry.snuba.discover.timeseries_query", return_value={})
  716. def test_multiple_yaxis_only_one_query(self, mock_query):
  717. self.do_request(
  718. data={
  719. "project": self.project.id,
  720. "start": iso_format(self.day_ago),
  721. "end": iso_format(self.day_ago + timedelta(hours=2)),
  722. "interval": "1h",
  723. "yAxis": ["user_count", "event_count", "epm()", "eps()"],
  724. },
  725. )
  726. assert mock_query.call_count == 1
  727. @mock.patch("sentry.snuba.discover.bulk_snql_query", return_value=[{"data": []}])
  728. def test_invalid_interval(self, mock_query):
  729. self.do_request(
  730. data={
  731. "end": iso_format(before_now()),
  732. "start": iso_format(before_now(hours=24)),
  733. "query": "",
  734. "interval": "1s",
  735. "yAxis": "count()",
  736. },
  737. )
  738. assert mock_query.call_count == 1
  739. # Should've reset to the default for 24h
  740. assert mock_query.mock_calls[0].args[0][0].query.granularity.granularity == 300
  741. self.do_request(
  742. data={
  743. "end": iso_format(before_now()),
  744. "start": iso_format(before_now(hours=24)),
  745. "query": "",
  746. "interval": "0d",
  747. "yAxis": "count()",
  748. },
  749. )
  750. assert mock_query.call_count == 2
  751. # Should've reset to the default for 24h
  752. assert mock_query.mock_calls[1].args[0][0].query.granularity.granularity == 300
  753. def test_out_of_retention(self):
  754. with self.options({"system.event-retention-days": 10}):
  755. response = self.do_request(
  756. data={
  757. "start": iso_format(before_now(days=20)),
  758. "end": iso_format(before_now(days=15)),
  759. "query": "",
  760. "interval": "30m",
  761. "yAxis": "count()",
  762. },
  763. )
  764. assert response.status_code == 400
  765. @mock.patch("sentry.utils.snuba.quantize_time")
  766. def test_quantize_dates(self, mock_quantize):
  767. mock_quantize.return_value = before_now(days=1).replace(tzinfo=utc)
  768. # Don't quantize short time periods
  769. self.do_request(
  770. data={"statsPeriod": "1h", "query": "", "interval": "30m", "yAxis": "count()"},
  771. )
  772. # Don't quantize absolute date periods
  773. self.do_request(
  774. data={
  775. "start": iso_format(before_now(days=20)),
  776. "end": iso_format(before_now(days=15)),
  777. "query": "",
  778. "interval": "30m",
  779. "yAxis": "count()",
  780. },
  781. )
  782. assert len(mock_quantize.mock_calls) == 0
  783. # Quantize long date periods
  784. self.do_request(
  785. data={"statsPeriod": "90d", "query": "", "interval": "30m", "yAxis": "count()"},
  786. )
  787. assert len(mock_quantize.mock_calls) == 2
  788. def test_with_zerofill(self):
  789. response = self.do_request(
  790. data={
  791. "start": iso_format(self.day_ago),
  792. "end": iso_format(self.day_ago + timedelta(hours=2)),
  793. "interval": "30m",
  794. },
  795. )
  796. assert response.status_code == 200, response.content
  797. assert [attrs for time, attrs in response.data["data"]] == [
  798. [{"count": 1}],
  799. [{"count": 0}],
  800. [{"count": 2}],
  801. [{"count": 0}],
  802. ]
  803. def test_without_zerofill(self):
  804. start = iso_format(self.day_ago)
  805. end = iso_format(self.day_ago + timedelta(hours=2))
  806. response = self.do_request(
  807. data={
  808. "start": start,
  809. "end": end,
  810. "interval": "30m",
  811. "withoutZerofill": "1",
  812. },
  813. features={
  814. "organizations:performance-chart-interpolation": True,
  815. "organizations:discover-basic": True,
  816. },
  817. )
  818. assert response.status_code == 200, response.content
  819. assert [attrs for time, attrs in response.data["data"]] == [
  820. [{"count": 1}],
  821. [{"count": 2}],
  822. ]
  823. assert response.data["start"] == parse_date(start).timestamp()
  824. assert response.data["end"] == parse_date(end).timestamp()
  825. def test_comparison(self):
  826. self.store_event(
  827. data={
  828. "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=1)),
  829. },
  830. project_id=self.project.id,
  831. )
  832. self.store_event(
  833. data={
  834. "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=2)),
  835. },
  836. project_id=self.project.id,
  837. )
  838. self.store_event(
  839. data={
  840. "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1, minutes=1)),
  841. },
  842. project_id=self.project2.id,
  843. )
  844. response = self.do_request(
  845. data={
  846. "start": iso_format(self.day_ago),
  847. "end": iso_format(self.day_ago + timedelta(hours=2)),
  848. "interval": "1h",
  849. "comparisonDelta": int(timedelta(days=1).total_seconds()),
  850. }
  851. )
  852. assert response.status_code == 200, response.content
  853. assert [attrs for time, attrs in response.data["data"]] == [
  854. [{"count": 1, "comparisonCount": 2}],
  855. [{"count": 2, "comparisonCount": 1}],
  856. ]
  857. def test_comparison_invalid(self):
  858. response = self.do_request(
  859. data={
  860. "start": iso_format(self.day_ago),
  861. "end": iso_format(self.day_ago + timedelta(hours=2)),
  862. "interval": "1h",
  863. "comparisonDelta": "17h",
  864. },
  865. )
  866. assert response.status_code == 400, response.content
  867. assert response.data["detail"] == "comparisonDelta must be an integer"
  868. start = before_now(days=85)
  869. end = start + timedelta(days=7)
  870. with self.options({"system.event-retention-days": 90}):
  871. response = self.do_request(
  872. data={
  873. "start": iso_format(start),
  874. "end": iso_format(end),
  875. "interval": "1h",
  876. "comparisonDelta": int(timedelta(days=7).total_seconds()),
  877. }
  878. )
  879. assert response.status_code == 400, response.content
  880. assert response.data["detail"] == "Comparison period is outside retention window"
  881. def test_equations_divide_by_zero(self):
  882. response = self.do_request(
  883. data={
  884. "start": iso_format(self.day_ago),
  885. "end": iso_format(self.day_ago + timedelta(hours=2)),
  886. "interval": "1h",
  887. # force a 0 in the denominator by doing 1 - 1
  888. # since a 0 literal is illegal as the denominator
  889. "yAxis": ["equation|count() / (1-1)"],
  890. },
  891. )
  892. assert response.status_code == 200, response.content
  893. assert len(response.data["data"]) == 2
  894. assert [attrs for time, attrs in response.data["data"]] == [
  895. [{"count": None}],
  896. [{"count": None}],
  897. ]
  898. @mock.patch("sentry.search.events.builder.discover.raw_snql_query")
  899. def test_profiles_dataset_simple(self, mock_snql_query):
  900. mock_snql_query.side_effect = [{"meta": {}, "data": []}]
  901. query = {
  902. "yAxis": [
  903. "count()",
  904. "p75()",
  905. "p95()",
  906. "p99()",
  907. "p75(profile.duration)",
  908. "p95(profile.duration)",
  909. "p99(profile.duration)",
  910. ],
  911. "project": [self.project.id],
  912. "dataset": "profiles",
  913. }
  914. response = self.do_request(query, features={"organizations:profiling": True})
  915. assert response.status_code == 200, response.content
  916. def test_tag_with_conflicting_function_alias_simple(self):
  917. for _ in range(7):
  918. self.store_event(
  919. data={
  920. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  921. "tags": {"count": "9001"},
  922. },
  923. project_id=self.project2.id,
  924. )
  925. # Query for count and count()
  926. data = {
  927. "start": iso_format(self.day_ago),
  928. "end": iso_format(self.day_ago + timedelta(minutes=3)),
  929. "interval": "1h",
  930. "yAxis": "count()",
  931. "orderby": ["-count()"],
  932. "field": ["count()", "count"],
  933. "partial": 1,
  934. }
  935. response = self.client.get(self.url, data, format="json")
  936. assert response.status_code == 200
  937. # Expect a count of 8 because one event from setUp
  938. assert response.data["data"][0][1] == [{"count": 8}]
  939. data["query"] = "count:9001"
  940. response = self.client.get(self.url, data, format="json")
  941. assert response.status_code == 200
  942. assert response.data["data"][0][1] == [{"count": 7}]
  943. data["query"] = "count:abc"
  944. response = self.client.get(self.url, data, format="json")
  945. assert response.status_code == 200
  946. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  947. @region_silo_test
  948. class OrganizationEventsStatsTopNEvents(APITestCase, SnubaTestCase):
  949. def setUp(self):
  950. super().setUp()
  951. self.login_as(user=self.user)
  952. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  953. self.project = self.create_project()
  954. self.project2 = self.create_project()
  955. self.user2 = self.create_user()
  956. transaction_data = load_data("transaction")
  957. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  958. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=4))
  959. transaction_data["tags"] = {"shared-tag": "yup"}
  960. self.event_data = [
  961. {
  962. "data": {
  963. "message": "poof",
  964. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  965. "user": {"email": self.user.email},
  966. "tags": {"shared-tag": "yup"},
  967. "fingerprint": ["group1"],
  968. },
  969. "project": self.project2,
  970. "count": 7,
  971. },
  972. {
  973. "data": {
  974. "message": "voof",
  975. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
  976. "fingerprint": ["group2"],
  977. "user": {"email": self.user2.email},
  978. "tags": {"shared-tag": "yup"},
  979. },
  980. "project": self.project2,
  981. "count": 6,
  982. },
  983. {
  984. "data": {
  985. "message": "very bad",
  986. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  987. "fingerprint": ["group3"],
  988. "user": {"email": "foo@example.com"},
  989. "tags": {"shared-tag": "yup"},
  990. },
  991. "project": self.project,
  992. "count": 5,
  993. },
  994. {
  995. "data": {
  996. "message": "oh no",
  997. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  998. "fingerprint": ["group4"],
  999. "user": {"email": "bar@example.com"},
  1000. "tags": {"shared-tag": "yup"},
  1001. },
  1002. "project": self.project,
  1003. "count": 4,
  1004. },
  1005. {"data": transaction_data, "project": self.project, "count": 3},
  1006. # Not in the top 5
  1007. {
  1008. "data": {
  1009. "message": "sorta bad",
  1010. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1011. "fingerprint": ["group5"],
  1012. "user": {"email": "bar@example.com"},
  1013. "tags": {"shared-tag": "yup"},
  1014. },
  1015. "project": self.project,
  1016. "count": 2,
  1017. },
  1018. {
  1019. "data": {
  1020. "message": "not so bad",
  1021. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1022. "fingerprint": ["group6"],
  1023. "user": {"email": "bar@example.com"},
  1024. "tags": {"shared-tag": "yup"},
  1025. },
  1026. "project": self.project,
  1027. "count": 1,
  1028. },
  1029. ]
  1030. self.events = []
  1031. for index, event_data in enumerate(self.event_data):
  1032. data = event_data["data"].copy()
  1033. event = {}
  1034. for i in range(event_data["count"]):
  1035. data["event_id"] = f"{index}{i}" * 16
  1036. event = self.store_event(data, project_id=event_data["project"].id)
  1037. self.events.append(event)
  1038. self.transaction = self.events[4]
  1039. self.enabled_features = {
  1040. "organizations:discover-basic": True,
  1041. }
  1042. self.url = reverse(
  1043. "sentry-api-0-organization-events-stats",
  1044. kwargs={"organization_slug": self.project.organization.slug},
  1045. )
  1046. def test_no_top_events_with_project_field(self):
  1047. project = self.create_project()
  1048. with self.feature(self.enabled_features):
  1049. response = self.client.get(
  1050. self.url,
  1051. data={
  1052. # make sure to query the project with 0 events
  1053. "project": project.id,
  1054. "start": iso_format(self.day_ago),
  1055. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1056. "interval": "1h",
  1057. "yAxis": "count()",
  1058. "orderby": ["-count()"],
  1059. "field": ["count()", "project"],
  1060. "topEvents": 5,
  1061. },
  1062. format="json",
  1063. )
  1064. assert response.status_code == 200, response.content
  1065. # When there are no top events, we do not return an empty dict.
  1066. # Instead, we return a single zero-filled series for an empty graph.
  1067. data = response.data["data"]
  1068. assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
  1069. def test_no_top_events(self):
  1070. project = self.create_project()
  1071. with self.feature(self.enabled_features):
  1072. response = self.client.get(
  1073. self.url,
  1074. data={
  1075. # make sure to query the project with 0 events
  1076. "project": project.id,
  1077. "start": iso_format(self.day_ago),
  1078. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1079. "interval": "1h",
  1080. "yAxis": "count()",
  1081. "orderby": ["-count()"],
  1082. "field": ["count()", "message", "user.email"],
  1083. "topEvents": 5,
  1084. },
  1085. format="json",
  1086. )
  1087. data = response.data["data"]
  1088. assert response.status_code == 200, response.content
  1089. # When there are no top events, we do not return an empty dict.
  1090. # Instead, we return a single zero-filled series for an empty graph.
  1091. assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
  1092. def test_no_top_events_with_multi_axis(self):
  1093. project = self.create_project()
  1094. with self.feature(self.enabled_features):
  1095. response = self.client.get(
  1096. self.url,
  1097. data={
  1098. # make sure to query the project with 0 events
  1099. "project": project.id,
  1100. "start": iso_format(self.day_ago),
  1101. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1102. "interval": "1h",
  1103. "yAxis": ["count()", "count_unique(user)"],
  1104. "orderby": ["-count()"],
  1105. "field": ["count()", "count_unique(user)", "message", "user.email"],
  1106. "topEvents": 5,
  1107. },
  1108. format="json",
  1109. )
  1110. assert response.status_code == 200
  1111. data = response.data[""]
  1112. assert [attrs for time, attrs in data["count()"]["data"]] == [
  1113. [{"count": 0}],
  1114. [{"count": 0}],
  1115. ]
  1116. assert [attrs for time, attrs in data["count_unique(user)"]["data"]] == [
  1117. [{"count": 0}],
  1118. [{"count": 0}],
  1119. ]
  1120. def test_simple_top_events(self):
  1121. with self.feature(self.enabled_features):
  1122. response = self.client.get(
  1123. self.url,
  1124. data={
  1125. "start": iso_format(self.day_ago),
  1126. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1127. "interval": "1h",
  1128. "yAxis": "count()",
  1129. "orderby": ["-count()"],
  1130. "field": ["count()", "message", "user.email"],
  1131. "topEvents": 5,
  1132. },
  1133. format="json",
  1134. )
  1135. data = response.data
  1136. assert response.status_code == 200, response.content
  1137. assert len(data) == 6
  1138. for index, event in enumerate(self.events[:5]):
  1139. message = event.message or event.transaction
  1140. results = data[
  1141. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1142. ]
  1143. assert results["order"] == index
  1144. assert [{"count": self.event_data[index]["count"]}] in [
  1145. attrs for _, attrs in results["data"]
  1146. ]
  1147. other = data["Other"]
  1148. assert other["order"] == 5
  1149. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1150. def test_tag_with_conflicting_function_alias_simple(self):
  1151. event_data = {
  1152. "data": {
  1153. "message": "poof",
  1154. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1155. "user": {"email": self.user.email},
  1156. "tags": {"count": "9001"},
  1157. "fingerprint": ["group1"],
  1158. },
  1159. "project": self.project2,
  1160. "count": 7,
  1161. }
  1162. for i in range(event_data["count"]):
  1163. event_data["data"]["event_id"] = f"a{i}" * 16
  1164. self.store_event(event_data["data"], project_id=event_data["project"].id)
  1165. # Query for count and count()
  1166. data = {
  1167. "start": iso_format(self.day_ago),
  1168. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1169. "interval": "1h",
  1170. "yAxis": "count()",
  1171. "orderby": ["-count()"],
  1172. "field": ["count()", "count"],
  1173. "topEvents": 5,
  1174. "partial": 1,
  1175. }
  1176. with self.feature(self.enabled_features):
  1177. response = self.client.get(self.url, data, format="json")
  1178. assert response.status_code == 200
  1179. assert response.data["9001"]["data"][0][1] == [{"count": 7}]
  1180. data["query"] = "count:9001"
  1181. with self.feature(self.enabled_features):
  1182. response = self.client.get(self.url, data, format="json")
  1183. assert response.status_code == 200
  1184. assert response.data["9001"]["data"][0][1] == [{"count": 7}]
  1185. data["query"] = "count:abc"
  1186. with self.feature(self.enabled_features):
  1187. response = self.client.get(self.url, data, format="json")
  1188. assert response.status_code == 200
  1189. assert all([interval[1][0]["count"] == 0 for interval in response.data["data"]])
  1190. def test_tag_with_conflicting_function_alias_with_other_single_grouping(self):
  1191. event_data = [
  1192. {
  1193. "data": {
  1194. "message": "poof",
  1195. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1196. "user": {"email": self.user.email},
  1197. "tags": {"count": "9001"},
  1198. "fingerprint": ["group1"],
  1199. },
  1200. "project": self.project2,
  1201. "count": 7,
  1202. },
  1203. {
  1204. "data": {
  1205. "message": "poof2",
  1206. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1207. "user": {"email": self.user.email},
  1208. "tags": {"count": "abc"},
  1209. "fingerprint": ["group1"],
  1210. },
  1211. "project": self.project2,
  1212. "count": 3,
  1213. },
  1214. ]
  1215. for index, event in enumerate(event_data):
  1216. for i in range(event["count"]):
  1217. event["data"]["event_id"] = f"{index}{i}" * 16
  1218. self.store_event(event["data"], project_id=event["project"].id)
  1219. # Query for count and count()
  1220. data = {
  1221. "start": iso_format(self.day_ago),
  1222. "end": iso_format(self.day_ago + timedelta(hours=1)),
  1223. "interval": "1h",
  1224. "yAxis": "count()",
  1225. "orderby": ["-count"],
  1226. "field": ["count()", "count"],
  1227. "topEvents": 2,
  1228. "partial": 1,
  1229. }
  1230. with self.feature(self.enabled_features):
  1231. response = self.client.get(self.url, data, format="json")
  1232. assert response.status_code == 200
  1233. assert response.data["9001"]["data"][0][1] == [{"count": 7}]
  1234. assert response.data["abc"]["data"][0][1] == [{"count": 3}]
  1235. assert response.data["Other"]["data"][0][1] == [{"count": 16}]
  1236. def test_tag_with_conflicting_function_alias_with_other_multiple_groupings(self):
  1237. event_data = [
  1238. {
  1239. "data": {
  1240. "message": "abc",
  1241. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1242. "user": {"email": self.user.email},
  1243. "tags": {"count": "2"},
  1244. "fingerprint": ["group1"],
  1245. },
  1246. "project": self.project2,
  1247. "count": 3,
  1248. },
  1249. {
  1250. "data": {
  1251. "message": "def",
  1252. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  1253. "user": {"email": self.user.email},
  1254. "tags": {"count": "9001"},
  1255. "fingerprint": ["group1"],
  1256. },
  1257. "project": self.project2,
  1258. "count": 7,
  1259. },
  1260. ]
  1261. for index, event in enumerate(event_data):
  1262. for i in range(event["count"]):
  1263. event["data"]["event_id"] = f"{index}{i}" * 16
  1264. self.store_event(event["data"], project_id=event["project"].id)
  1265. # Query for count and count()
  1266. data = {
  1267. "start": iso_format(self.day_ago),
  1268. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1269. "interval": "2d",
  1270. "yAxis": "count()",
  1271. "orderby": ["-count"],
  1272. "field": ["count()", "count", "message"],
  1273. "topEvents": 2,
  1274. "partial": 1,
  1275. }
  1276. with self.feature(self.enabled_features):
  1277. response = self.client.get(self.url, data, format="json")
  1278. assert response.status_code == 200
  1279. assert response.data["abc,2"]["data"][0][1] == [{"count": 3}]
  1280. assert response.data["def,9001"]["data"][0][1] == [{"count": 7}]
  1281. assert response.data["Other"]["data"][0][1] == [{"count": 25}]
  1282. def test_top_events_limits(self):
  1283. data = {
  1284. "start": iso_format(self.day_ago),
  1285. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1286. "interval": "1h",
  1287. "yAxis": "count()",
  1288. "orderby": ["-count()"],
  1289. "field": ["count()", "message", "user.email"],
  1290. }
  1291. with self.feature(self.enabled_features):
  1292. data["topEvents"] = MAX_TOP_EVENTS + 1
  1293. response = self.client.get(self.url, data, format="json")
  1294. assert response.status_code == 400
  1295. data["topEvents"] = 0
  1296. response = self.client.get(self.url, data, format="json")
  1297. assert response.status_code == 400
  1298. data["topEvents"] = "a"
  1299. response = self.client.get(self.url, data, format="json")
  1300. assert response.status_code == 400
  1301. def test_top_events_with_projects(self):
  1302. with self.feature(self.enabled_features):
  1303. response = self.client.get(
  1304. self.url,
  1305. data={
  1306. "start": iso_format(self.day_ago),
  1307. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1308. "interval": "1h",
  1309. "yAxis": "count()",
  1310. "orderby": ["-count()"],
  1311. "field": ["count()", "message", "project"],
  1312. "topEvents": 5,
  1313. },
  1314. format="json",
  1315. )
  1316. data = response.data
  1317. assert response.status_code == 200, response.content
  1318. assert len(data) == 6
  1319. for index, event in enumerate(self.events[:5]):
  1320. message = event.message or event.transaction
  1321. results = data[",".join([message, event.project.slug])]
  1322. assert results["order"] == index
  1323. assert [{"count": self.event_data[index]["count"]}] in [
  1324. attrs for time, attrs in results["data"]
  1325. ]
  1326. other = data["Other"]
  1327. assert other["order"] == 5
  1328. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1329. def test_top_events_with_issue(self):
  1330. # delete a group to make sure if this happens the value becomes unknown
  1331. event_group = self.events[0].group
  1332. event_group.delete()
  1333. with self.feature(self.enabled_features):
  1334. response = self.client.get(
  1335. self.url,
  1336. data={
  1337. "start": iso_format(self.day_ago),
  1338. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1339. "interval": "1h",
  1340. "yAxis": "count()",
  1341. "orderby": ["-count()"],
  1342. "field": ["count()", "message", "issue"],
  1343. "topEvents": 5,
  1344. "query": "!event.type:transaction",
  1345. },
  1346. format="json",
  1347. )
  1348. data = response.data
  1349. assert response.status_code == 200, response.content
  1350. assert len(data) == 6
  1351. for index, event in enumerate(self.events[:4]):
  1352. message = event.message
  1353. # Because we deleted the group for event 0
  1354. if index == 0 or event.group is None:
  1355. issue = "unknown"
  1356. else:
  1357. issue = event.group.qualified_short_id
  1358. results = data[",".join([issue, message])]
  1359. assert results["order"] == index
  1360. assert [{"count": self.event_data[index]["count"]}] in [
  1361. attrs for time, attrs in results["data"]
  1362. ]
  1363. other = data["Other"]
  1364. assert other["order"] == 5
  1365. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1366. def test_top_events_with_transaction_status(self):
  1367. with self.feature(self.enabled_features):
  1368. response = self.client.get(
  1369. self.url,
  1370. data={
  1371. "start": iso_format(self.day_ago),
  1372. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1373. "interval": "1h",
  1374. "yAxis": "count()",
  1375. "orderby": ["-count()"],
  1376. "field": ["count()", "transaction.status"],
  1377. "topEvents": 5,
  1378. },
  1379. format="json",
  1380. )
  1381. data = response.data
  1382. assert response.status_code == 200, response.content
  1383. assert len(data) == 1
  1384. assert "ok" in data
  1385. @mock.patch("sentry.models.GroupManager.get_issues_mapping")
  1386. def test_top_events_with_unknown_issue(self, mock_issues_mapping):
  1387. event = self.events[0]
  1388. event_data = self.event_data[0]
  1389. # ensure that the issue mapping returns None for the issue
  1390. mock_issues_mapping.return_value = {event.group.id: None}
  1391. with self.feature(self.enabled_features):
  1392. response = self.client.get(
  1393. self.url,
  1394. data={
  1395. "start": iso_format(self.day_ago),
  1396. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1397. "interval": "1h",
  1398. "yAxis": "count()",
  1399. "orderby": ["-count()"],
  1400. "field": ["count()", "issue"],
  1401. "topEvents": 5,
  1402. # narrow the search to just one issue
  1403. "query": f"issue.id:{event.group.id}",
  1404. },
  1405. format="json",
  1406. )
  1407. assert response.status_code == 200, response.content
  1408. data = response.data
  1409. assert len(data) == 1
  1410. results = data["unknown"]
  1411. assert results["order"] == 0
  1412. assert [{"count": event_data["count"]}] in [attrs for time, attrs in results["data"]]
  1413. @mock.patch(
  1414. "sentry.search.events.builder.discover.raw_snql_query",
  1415. side_effect=[{"data": [{"issue.id": 1}], "meta": []}, {"data": [], "meta": []}],
  1416. )
  1417. def test_top_events_with_issue_check_query_conditions(self, mock_query):
  1418. """ "Intentionally separate from test_top_events_with_issue
  1419. This is to test against a bug where the condition for issues wasn't included and we'd be missing data for
  1420. the interval since we'd cap out the max rows. This was not caught by the previous test since the results
  1421. would still be correct given the smaller interval & lack of data
  1422. """
  1423. with self.feature(self.enabled_features):
  1424. self.client.get(
  1425. self.url,
  1426. data={
  1427. "start": iso_format(self.day_ago),
  1428. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1429. "interval": "1h",
  1430. "yAxis": "count()",
  1431. "orderby": ["-count()"],
  1432. "field": ["count()", "message", "issue"],
  1433. "topEvents": 5,
  1434. "query": "!event.type:transaction",
  1435. },
  1436. format="json",
  1437. )
  1438. assert (
  1439. Condition(Function("coalesce", [Column("group_id"), 0], "issue.id"), Op.IN, [1])
  1440. in mock_query.mock_calls[1].args[0].query.where
  1441. )
  1442. def test_top_events_with_functions(self):
  1443. with self.feature(self.enabled_features):
  1444. response = self.client.get(
  1445. self.url,
  1446. data={
  1447. "start": iso_format(self.day_ago),
  1448. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1449. "interval": "1h",
  1450. "yAxis": "count()",
  1451. "orderby": ["-p99()"],
  1452. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1453. "topEvents": 5,
  1454. },
  1455. format="json",
  1456. )
  1457. data = response.data
  1458. assert response.status_code == 200, response.content
  1459. assert len(data) == 1
  1460. results = data[self.transaction.transaction]
  1461. assert results["order"] == 0
  1462. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1463. def test_top_events_with_functions_on_different_transactions(self):
  1464. """Transaction2 has less events, but takes longer so order should be self.transaction then transaction2"""
  1465. transaction_data = load_data("transaction")
  1466. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1467. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  1468. transaction_data["transaction"] = "/foo_bar/"
  1469. transaction2 = self.store_event(transaction_data, project_id=self.project.id)
  1470. with self.feature(self.enabled_features):
  1471. response = self.client.get(
  1472. self.url,
  1473. data={
  1474. "start": iso_format(self.day_ago),
  1475. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1476. "interval": "1h",
  1477. "yAxis": "count()",
  1478. "orderby": ["-p99()"],
  1479. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1480. "topEvents": 5,
  1481. },
  1482. format="json",
  1483. )
  1484. data = response.data
  1485. assert response.status_code == 200, response.content
  1486. assert len(data) == 2
  1487. results = data[self.transaction.transaction]
  1488. assert results["order"] == 1
  1489. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1490. results = data[transaction2.transaction]
  1491. assert results["order"] == 0
  1492. assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
  1493. def test_top_events_with_query(self):
  1494. transaction_data = load_data("transaction")
  1495. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1496. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  1497. transaction_data["transaction"] = "/foo_bar/"
  1498. self.store_event(transaction_data, project_id=self.project.id)
  1499. with self.feature(self.enabled_features):
  1500. response = self.client.get(
  1501. self.url,
  1502. data={
  1503. "start": iso_format(self.day_ago),
  1504. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1505. "interval": "1h",
  1506. "yAxis": "count()",
  1507. "orderby": ["-p99()"],
  1508. "query": "transaction:/foo_bar/",
  1509. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  1510. "topEvents": 5,
  1511. },
  1512. format="json",
  1513. )
  1514. data = response.data
  1515. assert response.status_code == 200, response.content
  1516. assert len(data) == 1
  1517. transaction2_data = data["/foo_bar/"]
  1518. assert transaction2_data["order"] == 0
  1519. assert [attrs for time, attrs in transaction2_data["data"]] == [
  1520. [{"count": 1}],
  1521. [{"count": 0}],
  1522. ]
  1523. def test_top_events_with_negated_condition(self):
  1524. with self.feature(self.enabled_features):
  1525. response = self.client.get(
  1526. self.url,
  1527. data={
  1528. "start": iso_format(self.day_ago),
  1529. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1530. "interval": "1h",
  1531. "yAxis": "count()",
  1532. "orderby": ["-count()"],
  1533. "query": f"!message:{self.events[0].message}",
  1534. "field": ["message", "count()"],
  1535. "topEvents": 5,
  1536. },
  1537. format="json",
  1538. )
  1539. data = response.data
  1540. assert response.status_code == 200, response.content
  1541. assert len(data) == 6
  1542. for index, event in enumerate(self.events[1:5]):
  1543. message = event.message or event.transaction
  1544. results = data[message]
  1545. assert results["order"] == index
  1546. assert [{"count": self.event_data[index + 1]["count"]}] in [
  1547. attrs for _, attrs in results["data"]
  1548. ]
  1549. other = data["Other"]
  1550. assert other["order"] == 5
  1551. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1552. def test_top_events_with_epm(self):
  1553. with self.feature(self.enabled_features):
  1554. response = self.client.get(
  1555. self.url,
  1556. data={
  1557. "start": iso_format(self.day_ago),
  1558. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1559. "interval": "1h",
  1560. "yAxis": "epm()",
  1561. "orderby": ["-count()"],
  1562. "field": ["message", "user.email", "count()"],
  1563. "topEvents": 5,
  1564. },
  1565. format="json",
  1566. )
  1567. data = response.data
  1568. assert response.status_code == 200, response.content
  1569. assert len(data) == 6
  1570. for index, event in enumerate(self.events[:5]):
  1571. message = event.message or event.transaction
  1572. results = data[
  1573. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1574. ]
  1575. assert results["order"] == index
  1576. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  1577. attrs for time, attrs in results["data"]
  1578. ]
  1579. other = data["Other"]
  1580. assert other["order"] == 5
  1581. assert [{"count": 0.05}] in [attrs for _, attrs in other["data"]]
  1582. def test_top_events_with_multiple_yaxis(self):
  1583. with self.feature(self.enabled_features):
  1584. response = self.client.get(
  1585. self.url,
  1586. data={
  1587. "start": iso_format(self.day_ago),
  1588. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1589. "interval": "1h",
  1590. "yAxis": ["epm()", "count()"],
  1591. "orderby": ["-count()"],
  1592. "field": ["message", "user.email", "count()"],
  1593. "topEvents": 5,
  1594. },
  1595. format="json",
  1596. )
  1597. data = response.data
  1598. assert response.status_code == 200, response.content
  1599. assert len(data) == 6
  1600. for index, event in enumerate(self.events[:5]):
  1601. message = event.message or event.transaction
  1602. results = data[
  1603. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  1604. ]
  1605. assert results["order"] == index
  1606. assert results["epm()"]["order"] == 0
  1607. assert results["count()"]["order"] == 1
  1608. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  1609. attrs for time, attrs in results["epm()"]["data"]
  1610. ]
  1611. assert [{"count": self.event_data[index]["count"]}] in [
  1612. attrs for time, attrs in results["count()"]["data"]
  1613. ]
  1614. other = data["Other"]
  1615. assert other["order"] == 5
  1616. assert other["epm()"]["order"] == 0
  1617. assert other["count()"]["order"] == 1
  1618. assert [{"count": 0.05}] in [attrs for _, attrs in other["epm()"]["data"]]
  1619. assert [{"count": 3}] in [attrs for _, attrs in other["count()"]["data"]]
  1620. def test_top_events_with_boolean(self):
  1621. with self.feature(self.enabled_features):
  1622. response = self.client.get(
  1623. self.url,
  1624. data={
  1625. "start": iso_format(self.day_ago),
  1626. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1627. "interval": "1h",
  1628. "yAxis": "count()",
  1629. "orderby": ["-count()"],
  1630. "field": ["count()", "message", "device.charging"],
  1631. "topEvents": 5,
  1632. },
  1633. format="json",
  1634. )
  1635. data = response.data
  1636. assert response.status_code == 200, response.content
  1637. assert len(data) == 6
  1638. for index, event in enumerate(self.events[:5]):
  1639. message = event.message or event.transaction
  1640. results = data[",".join(["False", message])]
  1641. assert results["order"] == index
  1642. assert [{"count": self.event_data[index]["count"]}] in [
  1643. attrs for time, attrs in results["data"]
  1644. ]
  1645. other = data["Other"]
  1646. assert other["order"] == 5
  1647. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1648. def test_top_events_with_error_unhandled(self):
  1649. self.login_as(user=self.user)
  1650. project = self.create_project()
  1651. prototype = load_data("android-ndk")
  1652. prototype["event_id"] = "f" * 32
  1653. prototype["message"] = "not handled"
  1654. prototype["exception"]["values"][0]["value"] = "not handled"
  1655. prototype["exception"]["values"][0]["mechanism"]["handled"] = False
  1656. prototype["timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  1657. self.store_event(data=prototype, project_id=project.id)
  1658. with self.feature(self.enabled_features):
  1659. response = self.client.get(
  1660. self.url,
  1661. data={
  1662. "start": iso_format(self.day_ago),
  1663. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1664. "interval": "1h",
  1665. "yAxis": "count()",
  1666. "orderby": ["-count()"],
  1667. "field": ["count()", "error.unhandled"],
  1668. "topEvents": 5,
  1669. },
  1670. format="json",
  1671. )
  1672. data = response.data
  1673. assert response.status_code == 200, response.content
  1674. assert len(data) == 2
  1675. def test_top_events_with_timestamp(self):
  1676. with self.feature(self.enabled_features):
  1677. response = self.client.get(
  1678. self.url,
  1679. data={
  1680. "start": iso_format(self.day_ago),
  1681. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1682. "interval": "1h",
  1683. "yAxis": "count()",
  1684. "orderby": ["-count()"],
  1685. "query": "event.type:default",
  1686. "field": ["count()", "message", "timestamp"],
  1687. "topEvents": 5,
  1688. },
  1689. format="json",
  1690. )
  1691. data = response.data
  1692. assert response.status_code == 200, response.content
  1693. assert len(data) == 6
  1694. # Transactions won't be in the results because of the query
  1695. del self.events[4]
  1696. del self.event_data[4]
  1697. for index, event in enumerate(self.events[:5]):
  1698. results = data[",".join([event.message, event.timestamp])]
  1699. assert results["order"] == index
  1700. assert [{"count": self.event_data[index]["count"]}] in [
  1701. attrs for time, attrs in results["data"]
  1702. ]
  1703. other = data["Other"]
  1704. assert other["order"] == 5
  1705. assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
  1706. def test_top_events_with_int(self):
  1707. with self.feature(self.enabled_features):
  1708. response = self.client.get(
  1709. self.url,
  1710. data={
  1711. "start": iso_format(self.day_ago),
  1712. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1713. "interval": "1h",
  1714. "yAxis": "count()",
  1715. "orderby": ["-count()"],
  1716. "field": ["count()", "message", "transaction.duration"],
  1717. "topEvents": 5,
  1718. },
  1719. format="json",
  1720. )
  1721. data = response.data
  1722. assert response.status_code == 200, response.content
  1723. assert len(data) == 1
  1724. results = data[",".join([self.transaction.transaction, "120000"])]
  1725. assert results["order"] == 0
  1726. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1727. def test_top_events_with_user(self):
  1728. with self.feature(self.enabled_features):
  1729. response = self.client.get(
  1730. self.url,
  1731. data={
  1732. "start": iso_format(self.day_ago),
  1733. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1734. "interval": "1h",
  1735. "yAxis": "count()",
  1736. "orderby": ["-count()", "user"],
  1737. "field": ["user", "count()"],
  1738. "topEvents": 5,
  1739. },
  1740. format="json",
  1741. )
  1742. data = response.data
  1743. assert response.status_code == 200, response.content
  1744. assert len(data) == 5
  1745. assert data["email:bar@example.com"]["order"] == 1
  1746. assert [attrs for time, attrs in data["email:bar@example.com"]["data"]] == [
  1747. [{"count": 7}],
  1748. [{"count": 0}],
  1749. ]
  1750. assert [attrs for time, attrs in data["ip:127.0.0.1"]["data"]] == [
  1751. [{"count": 3}],
  1752. [{"count": 0}],
  1753. ]
  1754. def test_top_events_with_user_and_email(self):
  1755. with self.feature(self.enabled_features):
  1756. response = self.client.get(
  1757. self.url,
  1758. data={
  1759. "start": iso_format(self.day_ago),
  1760. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1761. "interval": "1h",
  1762. "yAxis": "count()",
  1763. "orderby": ["-count()", "user"],
  1764. "field": ["user", "user.email", "count()"],
  1765. "topEvents": 5,
  1766. },
  1767. format="json",
  1768. )
  1769. data = response.data
  1770. assert response.status_code == 200, response.content
  1771. assert len(data) == 5
  1772. assert data["email:bar@example.com,bar@example.com"]["order"] == 1
  1773. assert [attrs for time, attrs in data["email:bar@example.com,bar@example.com"]["data"]] == [
  1774. [{"count": 7}],
  1775. [{"count": 0}],
  1776. ]
  1777. assert [attrs for time, attrs in data["ip:127.0.0.1,None"]["data"]] == [
  1778. [{"count": 3}],
  1779. [{"count": 0}],
  1780. ]
  1781. def test_top_events_with_user_display(self):
  1782. with self.feature(self.enabled_features):
  1783. response = self.client.get(
  1784. self.url,
  1785. data={
  1786. "start": iso_format(self.day_ago),
  1787. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1788. "interval": "1h",
  1789. "yAxis": "count()",
  1790. "orderby": ["-count()"],
  1791. "field": ["message", "user.display", "count()"],
  1792. "topEvents": 5,
  1793. },
  1794. format="json",
  1795. )
  1796. data = response.data
  1797. assert response.status_code == 200, response.content
  1798. assert len(data) == 6
  1799. for index, event in enumerate(self.events[:5]):
  1800. message = event.message or event.transaction
  1801. user = self.event_data[index]["data"]["user"]
  1802. results = data[
  1803. ",".join([message, user.get("email", None) or user.get("ip_address", "None")])
  1804. ]
  1805. assert results["order"] == index
  1806. assert [{"count": self.event_data[index]["count"]}] in [
  1807. attrs for _, attrs in results["data"]
  1808. ]
  1809. other = data["Other"]
  1810. assert other["order"] == 5
  1811. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  1812. @pytest.mark.skip(reason="A query with group_id will not return transactions")
  1813. def test_top_events_none_filter(self):
  1814. """When a field is None in one of the top events, make sure we filter by it
  1815. In this case event[4] is a transaction and has no issue
  1816. """
  1817. with self.feature(self.enabled_features):
  1818. response = self.client.get(
  1819. self.url,
  1820. data={
  1821. "start": iso_format(self.day_ago),
  1822. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1823. "interval": "1h",
  1824. "yAxis": "count()",
  1825. "orderby": ["-count()"],
  1826. "field": ["count()", "issue"],
  1827. "topEvents": 5,
  1828. },
  1829. format="json",
  1830. )
  1831. data = response.data
  1832. assert response.status_code == 200, response.content
  1833. assert len(data) == 5
  1834. for index, event in enumerate(self.events[:5]):
  1835. if event.group is None:
  1836. issue = "unknown"
  1837. else:
  1838. issue = event.group.qualified_short_id
  1839. results = data[issue]
  1840. assert results["order"] == index
  1841. assert [{"count": self.event_data[index]["count"]}] in [
  1842. attrs for time, attrs in results["data"]
  1843. ]
  1844. @pytest.mark.skip(reason="Invalid query - transaction events don't have group_id field")
  1845. def test_top_events_one_field_with_none(self):
  1846. with self.feature(self.enabled_features):
  1847. response = self.client.get(
  1848. self.url,
  1849. data={
  1850. "start": iso_format(self.day_ago),
  1851. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1852. "interval": "1h",
  1853. "yAxis": "count()",
  1854. "orderby": ["-count()"],
  1855. "query": "event.type:transaction",
  1856. "field": ["count()", "issue"],
  1857. "topEvents": 5,
  1858. },
  1859. format="json",
  1860. )
  1861. data = response.data
  1862. assert response.status_code == 200, response.content
  1863. assert len(data) == 1
  1864. results = data["unknown"]
  1865. assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
  1866. assert results["order"] == 0
  1867. def test_top_events_with_error_handled(self):
  1868. data = self.event_data[0]
  1869. data["data"]["level"] = "error"
  1870. data["data"]["exception"] = {
  1871. "values": [
  1872. {
  1873. "type": "ValidationError",
  1874. "value": "Bad request",
  1875. "mechanism": {"handled": True, "type": "generic"},
  1876. }
  1877. ]
  1878. }
  1879. self.store_event(data["data"], project_id=data["project"].id)
  1880. data["data"]["exception"] = {
  1881. "values": [
  1882. {
  1883. "type": "ValidationError",
  1884. "value": "Bad request",
  1885. "mechanism": {"handled": False, "type": "generic"},
  1886. }
  1887. ]
  1888. }
  1889. self.store_event(data["data"], project_id=data["project"].id)
  1890. with self.feature(self.enabled_features):
  1891. response = self.client.get(
  1892. self.url,
  1893. data={
  1894. "start": iso_format(self.day_ago),
  1895. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1896. "interval": "1h",
  1897. "yAxis": "count()",
  1898. "orderby": ["-count()"],
  1899. "field": ["count()", "error.handled"],
  1900. "topEvents": 5,
  1901. "query": "!event.type:transaction",
  1902. },
  1903. format="json",
  1904. )
  1905. assert response.status_code == 200, response.content
  1906. data = response.data
  1907. assert len(data) == 2
  1908. results = data["1"]
  1909. assert [attrs for time, attrs in results["data"]] == [[{"count": 20}], [{"count": 6}]]
  1910. results = data["0"]
  1911. assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
  1912. def test_top_events_with_aggregate_condition(self):
  1913. with self.feature(self.enabled_features):
  1914. response = self.client.get(
  1915. self.url,
  1916. data={
  1917. "start": iso_format(self.day_ago),
  1918. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1919. "interval": "1h",
  1920. "yAxis": "count()",
  1921. "orderby": ["-count()"],
  1922. "field": ["message", "count()"],
  1923. "query": "count():>4",
  1924. "topEvents": 5,
  1925. },
  1926. format="json",
  1927. )
  1928. assert response.status_code == 200, response.content
  1929. data = response.data
  1930. assert len(data) == 3
  1931. for index, event in enumerate(self.events[:3]):
  1932. message = event.message or event.transaction
  1933. results = data[message]
  1934. assert results["order"] == index
  1935. assert [{"count": self.event_data[index]["count"]}] in [
  1936. attrs for time, attrs in results["data"]
  1937. ]
  1938. @pytest.mark.xfail(reason="There's only 2 rows total, which mean there shouldn't be other")
  1939. def test_top_events_with_to_other(self):
  1940. version = "version -@'\" 1.2,3+(4)"
  1941. version_escaped = "version -@'\\\" 1.2,3+(4)"
  1942. # every symbol is replaced with a underscore to make the alias
  1943. version_alias = "version_______1_2_3__4_"
  1944. # add an event in the current release
  1945. event = self.event_data[0]
  1946. event_data = event["data"].copy()
  1947. event_data["event_id"] = uuid4().hex
  1948. event_data["release"] = version
  1949. self.store_event(event_data, project_id=event["project"].id)
  1950. with self.feature(self.enabled_features):
  1951. response = self.client.get(
  1952. self.url,
  1953. data={
  1954. "start": iso_format(self.day_ago),
  1955. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1956. "interval": "1h",
  1957. "yAxis": "count()",
  1958. # the double underscores around the version alias is because of a comma and quote
  1959. "orderby": [f"-to_other_release__{version_alias}__others_current"],
  1960. "field": [
  1961. "count()",
  1962. f'to_other(release,"{version_escaped}",others,current)',
  1963. ],
  1964. "topEvents": 2,
  1965. },
  1966. format="json",
  1967. )
  1968. assert response.status_code == 200, response.content
  1969. data = response.data
  1970. assert len(data) == 2
  1971. current = data["current"]
  1972. assert current["order"] == 1
  1973. assert sum(attrs[0]["count"] for _, attrs in current["data"]) == 1
  1974. others = data["others"]
  1975. assert others["order"] == 0
  1976. assert sum(attrs[0]["count"] for _, attrs in others["data"]) == sum(
  1977. event_data["count"] for event_data in self.event_data
  1978. )
  1979. def test_top_events_with_equations(self):
  1980. with self.feature(self.enabled_features):
  1981. response = self.client.get(
  1982. self.url,
  1983. data={
  1984. "start": iso_format(self.day_ago),
  1985. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1986. "interval": "1h",
  1987. "yAxis": "equation|count() / 100",
  1988. "orderby": ["-count()"],
  1989. "field": ["count()", "message", "user.email", "equation|count() / 100"],
  1990. "topEvents": 5,
  1991. },
  1992. format="json",
  1993. )
  1994. data = response.data
  1995. assert response.status_code == 200, response.content
  1996. assert len(data) == 6
  1997. for index, event in enumerate(self.events[:5]):
  1998. message = event.message or event.transaction
  1999. results = data[
  2000. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  2001. ]
  2002. assert results["order"] == index
  2003. assert [{"count": self.event_data[index]["count"] / 100}] in [
  2004. attrs for time, attrs in results["data"]
  2005. ]
  2006. other = data["Other"]
  2007. assert other["order"] == 5
  2008. assert [{"count": 0.03}] in [attrs for _, attrs in other["data"]]
  2009. @mock.patch("sentry.snuba.discover.bulk_snql_query", return_value=[{"data": [], "meta": []}])
  2010. @mock.patch(
  2011. "sentry.search.events.builder.discover.raw_snql_query",
  2012. return_value={"data": [], "meta": []},
  2013. )
  2014. def test_invalid_interval(self, mock_raw_query, mock_bulk_query):
  2015. with self.feature(self.enabled_features):
  2016. response = self.client.get(
  2017. self.url,
  2018. format="json",
  2019. data={
  2020. "end": iso_format(before_now()),
  2021. # 7,200 points for each event
  2022. "start": iso_format(before_now(seconds=7200)),
  2023. "field": ["count()", "issue"],
  2024. "query": "",
  2025. "interval": "1s",
  2026. "yAxis": "count()",
  2027. },
  2028. )
  2029. assert response.status_code == 200
  2030. assert mock_bulk_query.call_count == 1
  2031. with self.feature(self.enabled_features):
  2032. response = self.client.get(
  2033. self.url,
  2034. format="json",
  2035. data={
  2036. "end": iso_format(before_now()),
  2037. "start": iso_format(before_now(seconds=7200)),
  2038. "field": ["count()", "issue"],
  2039. "query": "",
  2040. "interval": "1s",
  2041. "yAxis": "count()",
  2042. # 7,200 points for each event * 2, should error
  2043. "topEvents": 2,
  2044. },
  2045. )
  2046. assert response.status_code == 200
  2047. assert mock_raw_query.call_count == 2
  2048. # Should've reset to the default for between 1 and 24h
  2049. assert mock_raw_query.mock_calls[1].args[0].query.granularity.granularity == 300
  2050. with self.feature(self.enabled_features):
  2051. response = self.client.get(
  2052. self.url,
  2053. format="json",
  2054. data={
  2055. "end": iso_format(before_now()),
  2056. # 1999 points * 5 events should just be enough to not error
  2057. "start": iso_format(before_now(seconds=1999)),
  2058. "field": ["count()", "issue"],
  2059. "query": "",
  2060. "interval": "1s",
  2061. "yAxis": "count()",
  2062. "topEvents": 5,
  2063. },
  2064. )
  2065. assert response.status_code == 200
  2066. assert mock_raw_query.call_count == 4
  2067. # Should've left the interval alone since we're just below the limit
  2068. assert mock_raw_query.mock_calls[3].args[0].query.granularity.granularity == 1
  2069. with self.feature(self.enabled_features):
  2070. response = self.client.get(
  2071. self.url,
  2072. format="json",
  2073. data={
  2074. "end": iso_format(before_now()),
  2075. "start": iso_format(before_now(hours=24)),
  2076. "field": ["count()", "issue"],
  2077. "query": "",
  2078. "interval": "0d",
  2079. "yAxis": "count()",
  2080. "topEvents": 5,
  2081. },
  2082. )
  2083. assert response.status_code == 200
  2084. assert mock_raw_query.call_count == 6
  2085. # Should've default to 24h's default of 5m
  2086. assert mock_raw_query.mock_calls[5].args[0].query.granularity.granularity == 300
  2087. def test_top_events_timestamp_fields(self):
  2088. with self.feature(self.enabled_features):
  2089. response = self.client.get(
  2090. self.url,
  2091. format="json",
  2092. data={
  2093. "start": iso_format(self.day_ago),
  2094. "end": iso_format(self.day_ago + timedelta(hours=2)),
  2095. "interval": "1h",
  2096. "yAxis": "count()",
  2097. "orderby": ["-count()"],
  2098. "field": ["count()", "timestamp", "timestamp.to_hour", "timestamp.to_day"],
  2099. "topEvents": 5,
  2100. },
  2101. )
  2102. assert response.status_code == 200
  2103. data = response.data
  2104. assert len(data) == 3
  2105. # these are the timestamps corresponding to the events stored
  2106. timestamps = [
  2107. self.day_ago + timedelta(minutes=2),
  2108. self.day_ago + timedelta(hours=1, minutes=2),
  2109. self.day_ago + timedelta(minutes=4),
  2110. ]
  2111. timestamp_hours = [timestamp.replace(minute=0, second=0) for timestamp in timestamps]
  2112. timestamp_days = [timestamp.replace(hour=0, minute=0, second=0) for timestamp in timestamps]
  2113. for ts, ts_hr, ts_day in zip(timestamps, timestamp_hours, timestamp_days):
  2114. key = f"{iso_format(ts)}+00:00,{iso_format(ts_day)}+00:00,{iso_format(ts_hr)}+00:00"
  2115. count = sum(
  2116. e["count"] for e in self.event_data if e["data"]["timestamp"] == iso_format(ts)
  2117. )
  2118. results = data[key]
  2119. assert [{"count": count}] in [attrs for time, attrs in results["data"]]
  2120. def test_top_events_other_with_matching_columns(self):
  2121. with self.feature(self.enabled_features):
  2122. response = self.client.get(
  2123. self.url,
  2124. data={
  2125. "start": iso_format(self.day_ago),
  2126. "end": iso_format(self.day_ago + timedelta(hours=2)),
  2127. "interval": "1h",
  2128. "yAxis": "count()",
  2129. "orderby": ["-count()"],
  2130. "field": ["count()", "tags[shared-tag]", "message"],
  2131. "topEvents": 5,
  2132. },
  2133. format="json",
  2134. )
  2135. data = response.data
  2136. assert response.status_code == 200, response.content
  2137. assert len(data) == 6
  2138. for index, event in enumerate(self.events[:5]):
  2139. message = event.message or event.transaction
  2140. results = data[",".join([message, "yup"])]
  2141. assert results["order"] == index
  2142. assert [{"count": self.event_data[index]["count"]}] in [
  2143. attrs for _, attrs in results["data"]
  2144. ]
  2145. other = data["Other"]
  2146. assert other["order"] == 5
  2147. assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
  2148. def test_top_events_with_field_overlapping_other_key(self):
  2149. transaction_data = load_data("transaction")
  2150. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  2151. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  2152. transaction_data["transaction"] = OTHER_KEY
  2153. for i in range(5):
  2154. data = transaction_data.copy()
  2155. data["event_id"] = "ab" + f"{i}" * 30
  2156. data["contexts"]["trace"]["span_id"] = "ab" + f"{i}" * 14
  2157. self.store_event(data, project_id=self.project.id)
  2158. with self.feature(self.enabled_features):
  2159. response = self.client.get(
  2160. self.url,
  2161. data={
  2162. "start": iso_format(self.day_ago),
  2163. "end": iso_format(self.day_ago + timedelta(hours=2)),
  2164. "interval": "1h",
  2165. "yAxis": "count()",
  2166. "orderby": ["-count()"],
  2167. "field": ["count()", "message"],
  2168. "topEvents": 5,
  2169. },
  2170. format="json",
  2171. )
  2172. data = response.data
  2173. assert response.status_code == 200, response.content
  2174. assert len(data) == 6
  2175. assert f"{OTHER_KEY} (message)" in data
  2176. results = data[f"{OTHER_KEY} (message)"]
  2177. assert [{"count": 5}] in [attrs for _, attrs in results["data"]]
  2178. other = data["Other"]
  2179. assert other["order"] == 5
  2180. assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
  2181. def test_top_events_can_exclude_other_series(self):
  2182. with self.feature(self.enabled_features):
  2183. response = self.client.get(
  2184. self.url,
  2185. data={
  2186. "start": iso_format(self.day_ago),
  2187. "end": iso_format(self.day_ago + timedelta(hours=2)),
  2188. "interval": "1h",
  2189. "yAxis": "count()",
  2190. "orderby": ["count()"],
  2191. "field": ["count()", "message"],
  2192. "topEvents": 5,
  2193. "excludeOther": "1",
  2194. },
  2195. format="json",
  2196. )
  2197. data = response.data
  2198. assert response.status_code == 200, response.content
  2199. assert len(data) == 5
  2200. assert "Other" not in response.data
  2201. def test_top_events_with_equation_including_unselected_fields_passes_field_validation(self):
  2202. with self.feature(self.enabled_features):
  2203. response = self.client.get(
  2204. self.url,
  2205. data={
  2206. "start": iso_format(self.day_ago),
  2207. "end": iso_format(self.day_ago + timedelta(hours=2)),
  2208. "interval": "1h",
  2209. "yAxis": "count()",
  2210. "orderby": ["-equation[0]"],
  2211. "field": ["count()", "message", "equation|count_unique(user) * 2"],
  2212. "topEvents": 5,
  2213. },
  2214. format="json",
  2215. )
  2216. data = response.data
  2217. assert response.status_code == 200, response.content
  2218. assert len(data) == 6
  2219. other = data["Other"]
  2220. assert other["order"] == 5
  2221. assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
  2222. def test_top_events_boolean_condition_and_project_field(self):
  2223. with self.feature(self.enabled_features):
  2224. response = self.client.get(
  2225. self.url,
  2226. data={
  2227. "start": iso_format(self.day_ago),
  2228. "end": iso_format(self.day_ago + timedelta(hours=2)),
  2229. "interval": "1h",
  2230. "yAxis": "count()",
  2231. "orderby": ["-count()"],
  2232. "field": ["project", "count()"],
  2233. "topEvents": 5,
  2234. "query": "event.type:transaction (transaction:*a OR transaction:b*)",
  2235. },
  2236. format="json",
  2237. )
  2238. assert response.status_code == 200