12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207 |
- import uuid
- from datetime import timedelta
- from unittest import mock
- from uuid import uuid4
- import pytest
- from dateutil.parser import parse as parse_date
- from django.urls import reverse
- from pytz import utc
- from snuba_sdk.column import Column
- from snuba_sdk.conditions import Condition, Op
- from snuba_sdk.function import Function
- from sentry.constants import MAX_TOP_EVENTS
- from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric
- from sentry.snuba.discover import OTHER_KEY
- from sentry.testutils import APITestCase, SnubaTestCase
- from sentry.testutils.helpers.datetime import before_now, iso_format
- from sentry.testutils.silo import region_silo_test
- from sentry.utils.samples import load_data
- pytestmark = pytest.mark.sentry_metrics
- @region_silo_test
- class OrganizationEventsStatsEndpointTest(APITestCase, SnubaTestCase):
- endpoint = "sentry-api-0-organization-events-stats"
- def setUp(self):
- super().setUp()
- self.login_as(user=self.user)
- self.authed_user = self.user
- self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
- self.project = self.create_project()
- self.project2 = self.create_project()
- self.user = self.create_user()
- self.user2 = self.create_user()
- self.store_event(
- data={
- "event_id": "a" * 32,
- "message": "very bad",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=1)),
- "fingerprint": ["group1"],
- "tags": {"sentry:user": self.user.email},
- },
- project_id=self.project.id,
- )
- self.store_event(
- data={
- "event_id": "b" * 32,
- "message": "oh my",
- "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=1)),
- "fingerprint": ["group2"],
- "tags": {"sentry:user": self.user2.email},
- },
- project_id=self.project2.id,
- )
- self.store_event(
- data={
- "event_id": "c" * 32,
- "message": "very bad",
- "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
- "fingerprint": ["group2"],
- "tags": {"sentry:user": self.user2.email},
- },
- project_id=self.project2.id,
- )
- self.url = reverse(
- "sentry-api-0-organization-events-stats",
- kwargs={"organization_slug": self.project.organization.slug},
- )
- self.features = {}
- def do_request(self, data, url=None, features=None):
- if features is None:
- features = {"organizations:discover-basic": True}
- features.update(self.features)
- with self.feature(features):
- return self.client.get(self.url if url is None else url, data=data, format="json")
- def test_simple(self):
- response = self.do_request(
- {
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
- def test_misaligned_last_bucket(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago - timedelta(minutes=30)),
- "end": iso_format(self.day_ago + timedelta(hours=1, minutes=30)),
- "interval": "1h",
- "partial": "1",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 0}],
- [{"count": 1}],
- [{"count": 2}],
- ]
- def test_no_projects(self):
- org = self.create_organization(owner=self.user)
- self.login_as(user=self.user)
- url = reverse(
- "sentry-api-0-organization-events-stats", kwargs={"organization_slug": org.slug}
- )
- response = self.do_request({}, url)
- assert response.status_code == 200, response.content
- assert len(response.data["data"]) == 0
- def test_user_count(self):
- self.store_event(
- data={
- "event_id": "d" * 32,
- "message": "something",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
- "tags": {"sentry:user": self.user2.email},
- "fingerprint": ["group2"],
- },
- project_id=self.project2.id,
- )
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "user_count",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [[{"count": 2}], [{"count": 1}]]
- def test_discover2_backwards_compatibility(self):
- response = self.do_request(
- data={
- "project": self.project.id,
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "user_count",
- },
- )
- assert response.status_code == 200, response.content
- assert len(response.data["data"]) > 0
- response = self.do_request(
- data={
- "project": self.project.id,
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "event_count",
- },
- )
- assert response.status_code == 200, response.content
- assert len(response.data["data"]) > 0
- def test_with_event_count_flag(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "event_count",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
- def test_performance_view_feature(self):
- response = self.do_request(
- data={
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=2)),
- "query": "project_id:1",
- "interval": "30m",
- "yAxis": "count()",
- },
- features={
- "organizations:performance-view": True,
- "organizations:discover-basic": False,
- },
- )
- assert response.status_code == 200, response.content
- def test_apdex_divide_by_zero(self):
- ProjectTransactionThreshold.objects.create(
- project=self.project,
- organization=self.project.organization,
- threshold=600,
- metric=TransactionMetric.LCP.value,
- )
- # Shouldn't count towards apdex
- data = load_data(
- "transaction",
- start_timestamp=self.day_ago + timedelta(minutes=(1)),
- timestamp=self.day_ago + timedelta(minutes=(3)),
- )
- data["transaction"] = "/apdex/new/"
- data["user"] = {"email": "1@example.com"}
- data["measurements"] = {}
- self.store_event(data, project_id=self.project.id)
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "apdex()",
- "project": [self.project.id],
- },
- )
- assert response.status_code == 200, response.content
- assert len(response.data["data"]) == 2
- data = response.data["data"]
- # 0 transactions with LCP 0/0
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 0}],
- [{"count": 0}],
- ]
- def test_aggregate_function_apdex(self):
- project1 = self.create_project()
- project2 = self.create_project()
- events = [
- ("one", 400, project1.id),
- ("one", 400, project1.id),
- ("two", 3000, project2.id),
- ("two", 1000, project2.id),
- ("three", 3000, project2.id),
- ]
- for idx, event in enumerate(events):
- data = load_data(
- "transaction",
- start_timestamp=self.day_ago + timedelta(minutes=(1 + idx)),
- timestamp=self.day_ago + timedelta(minutes=(1 + idx), milliseconds=event[1]),
- )
- data["event_id"] = f"{idx}" * 32
- data["transaction"] = f"/apdex/new/{event[0]}"
- data["user"] = {"email": f"{idx}@example.com"}
- self.store_event(data, project_id=event[2])
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "apdex()",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 0.3}],
- [{"count": 0}],
- ]
- ProjectTransactionThreshold.objects.create(
- project=project1,
- organization=project1.organization,
- threshold=100,
- metric=TransactionMetric.DURATION.value,
- )
- ProjectTransactionThreshold.objects.create(
- project=project2,
- organization=project1.organization,
- threshold=100,
- metric=TransactionMetric.DURATION.value,
- )
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "apdex()",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 0.2}],
- [{"count": 0}],
- ]
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["user_count", "apdex()"],
- },
- )
- assert response.status_code == 200, response.content
- assert response.data["user_count"]["order"] == 0
- assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
- [{"count": 5}],
- [{"count": 0}],
- ]
- assert response.data["apdex()"]["order"] == 1
- assert [attrs for time, attrs in response.data["apdex()"]["data"]] == [
- [{"count": 0.2}],
- [{"count": 0}],
- ]
- def test_aggregate_function_count(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
- def test_invalid_aggregate(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "rubbish",
- },
- )
- assert response.status_code == 400, response.content
- def test_aggregate_function_user_count(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count_unique(user)",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 1}]]
- def test_aggregate_invalid(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "nope(lol)",
- },
- )
- assert response.status_code == 400, response.content
- def test_throughput_epm_hour_rollup(self):
- project = self.create_project()
- # Each of these denotes how many events to create in each hour
- event_counts = [6, 0, 6, 3, 0, 3]
- for hour, count in enumerate(event_counts):
- for minute in range(count):
- self.store_event(
- data={
- "event_id": str(uuid.uuid1()),
- "message": "very bad",
- "timestamp": iso_format(
- self.day_ago + timedelta(hours=hour, minutes=minute)
- ),
- "fingerprint": ["group1"],
- "tags": {"sentry:user": self.user.email},
- },
- project_id=project.id,
- )
- for axis in ["epm()", "tpm()"]:
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=6)),
- "interval": "1h",
- "yAxis": axis,
- "project": project.id,
- },
- )
- assert response.status_code == 200, response.content
- data = response.data["data"]
- assert len(data) == 6
- rows = data[0:6]
- for test in zip(event_counts, rows):
- assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
- def test_throughput_epm_day_rollup(self):
- project = self.create_project()
- # Each of these denotes how many events to create in each minute
- event_counts = [6, 0, 6, 3, 0, 3]
- for hour, count in enumerate(event_counts):
- for minute in range(count):
- self.store_event(
- data={
- "event_id": str(uuid.uuid1()),
- "message": "very bad",
- "timestamp": iso_format(
- self.day_ago + timedelta(hours=hour, minutes=minute)
- ),
- "fingerprint": ["group1"],
- "tags": {"sentry:user": self.user.email},
- },
- project_id=project.id,
- )
- for axis in ["epm()", "tpm()"]:
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=24)),
- "interval": "24h",
- "yAxis": axis,
- "project": project.id,
- },
- )
- assert response.status_code == 200, response.content
- data = response.data["data"]
- assert len(data) == 2
- assert data[0][1][0]["count"] == sum(event_counts) / (86400.0 / 60.0)
- def test_throughput_eps_minute_rollup(self):
- project = self.create_project()
- # Each of these denotes how many events to create in each minute
- event_counts = [6, 0, 6, 3, 0, 3]
- for minute, count in enumerate(event_counts):
- for second in range(count):
- self.store_event(
- data={
- "event_id": str(uuid.uuid1()),
- "message": "very bad",
- "timestamp": iso_format(
- self.day_ago + timedelta(minutes=minute, seconds=second)
- ),
- "fingerprint": ["group1"],
- "tags": {"sentry:user": self.user.email},
- },
- project_id=project.id,
- )
- for axis in ["eps()", "tps()"]:
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(minutes=6)),
- "interval": "1m",
- "yAxis": axis,
- "project": project.id,
- },
- )
- assert response.status_code == 200, response.content
- data = response.data["data"]
- assert len(data) == 6
- rows = data[0:6]
- for test in zip(event_counts, rows):
- assert test[1][1][0]["count"] == test[0] / 60.0
- def test_throughput_eps_no_rollup(self):
- project = self.create_project()
- # Each of these denotes how many events to create in each minute
- event_counts = [6, 0, 6, 3, 0, 3]
- for minute, count in enumerate(event_counts):
- for second in range(count):
- self.store_event(
- data={
- "event_id": str(uuid.uuid1()),
- "message": "very bad",
- "timestamp": iso_format(
- self.day_ago + timedelta(minutes=minute, seconds=second)
- ),
- "fingerprint": ["group1"],
- "tags": {"sentry:user": self.user.email},
- },
- project_id=project.id,
- )
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(minutes=1)),
- "interval": "1s",
- "yAxis": "eps()",
- "project": project.id,
- },
- )
- assert response.status_code == 200, response.content
- data = response.data["data"]
- # expect 60 data points between time span of 0 and 60 seconds
- assert len(data) == 60
- rows = data[0:6]
- for row in rows:
- assert row[1][0]["count"] == 1
- def test_transaction_events(self):
- prototype = {
- "type": "transaction",
- "transaction": "api.issue.delete",
- "spans": [],
- "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
- "tags": {"important": "yes"},
- }
- fixtures = (
- ("d" * 32, before_now(minutes=32)),
- ("e" * 32, before_now(hours=1, minutes=2)),
- ("f" * 32, before_now(hours=1, minutes=35)),
- )
- for fixture in fixtures:
- data = prototype.copy()
- data["event_id"] = fixture[0]
- data["timestamp"] = iso_format(fixture[1])
- data["start_timestamp"] = iso_format(fixture[1] - timedelta(seconds=1))
- self.store_event(data=data, project_id=self.project.id)
- response = self.do_request(
- data={
- "project": self.project.id,
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=2)),
- "query": "event.type:transaction",
- "interval": "30m",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200, response.content
- items = [item for time, item in response.data["data"] if item]
- # We could get more results depending on where the 30 min
- # windows land.
- assert len(items) >= 3
- def test_project_id_query_filter(self):
- response = self.do_request(
- data={
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=2)),
- "query": "project_id:1",
- "interval": "30m",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200
- def test_latest_release_query_filter(self):
- response = self.do_request(
- data={
- "project": self.project.id,
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=2)),
- "query": "release:latest",
- "interval": "30m",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200
- def test_conditional_filter(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "query": "id:{} OR id:{}".format("a" * 32, "b" * 32),
- "interval": "30m",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200, response.content
- data = response.data["data"]
- assert len(data) == 4
- assert data[0][1][0]["count"] == 1
- assert data[2][1][0]["count"] == 1
- def test_simple_multiple_yaxis(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["user_count", "event_count"],
- },
- )
- assert response.status_code == 200, response.content
- assert response.data["user_count"]["order"] == 0
- assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
- [{"count": 1}],
- [{"count": 1}],
- ]
- assert response.data["event_count"]["order"] == 1
- assert [attrs for time, attrs in response.data["event_count"]["data"]] == [
- [{"count": 1}],
- [{"count": 2}],
- ]
- def test_equation_yaxis(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["equation|count() / 100"],
- },
- )
- assert response.status_code == 200, response.content
- assert len(response.data["data"]) == 2
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 0.01}],
- [{"count": 0.02}],
- ]
- def test_equation_mixed_multi_yaxis(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["count()", "equation|count() * 100"],
- },
- )
- assert response.status_code == 200, response.content
- assert response.data["count()"]["order"] == 0
- assert [attrs for time, attrs in response.data["count()"]["data"]] == [
- [{"count": 1}],
- [{"count": 2}],
- ]
- assert response.data["equation|count() * 100"]["order"] == 1
- assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
- [{"count": 100}],
- [{"count": 200}],
- ]
- def test_equation_multi_yaxis(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["equation|count() / 100", "equation|count() * 100"],
- },
- )
- assert response.status_code == 200, response.content
- assert response.data["equation|count() / 100"]["order"] == 0
- assert [attrs for time, attrs in response.data["equation|count() / 100"]["data"]] == [
- [{"count": 0.01}],
- [{"count": 0.02}],
- ]
- assert response.data["equation|count() * 100"]["order"] == 1
- assert [attrs for time, attrs in response.data["equation|count() * 100"]["data"]] == [
- [{"count": 100}],
- [{"count": 200}],
- ]
- def test_large_interval_no_drop_values(self):
- self.store_event(
- data={
- "event_id": "d" * 32,
- "message": "not good",
- "timestamp": iso_format(self.day_ago - timedelta(minutes=10)),
- "fingerprint": ["group3"],
- },
- project_id=self.project.id,
- )
- response = self.do_request(
- data={
- "project": self.project.id,
- "end": iso_format(self.day_ago),
- "start": iso_format(self.day_ago - timedelta(hours=24)),
- "query": 'message:"not good"',
- "interval": "1d",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200
- assert [attrs for time, attrs in response.data["data"]] == [[{"count": 0}], [{"count": 1}]]
- @mock.patch("sentry.snuba.discover.timeseries_query", return_value={})
- def test_multiple_yaxis_only_one_query(self, mock_query):
- self.do_request(
- data={
- "project": self.project.id,
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["user_count", "event_count", "epm()", "eps()"],
- },
- )
- assert mock_query.call_count == 1
- @mock.patch("sentry.snuba.discover.bulk_snql_query", return_value=[{"data": []}])
- def test_invalid_interval(self, mock_query):
- self.do_request(
- data={
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=24)),
- "query": "",
- "interval": "1s",
- "yAxis": "count()",
- },
- )
- assert mock_query.call_count == 1
- # Should've reset to the default for 24h
- assert mock_query.mock_calls[0].args[0][0].query.granularity.granularity == 300
- self.do_request(
- data={
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=24)),
- "query": "",
- "interval": "0d",
- "yAxis": "count()",
- },
- )
- assert mock_query.call_count == 2
- # Should've reset to the default for 24h
- assert mock_query.mock_calls[1].args[0][0].query.granularity.granularity == 300
- def test_out_of_retention(self):
- with self.options({"system.event-retention-days": 10}):
- response = self.do_request(
- data={
- "start": iso_format(before_now(days=20)),
- "end": iso_format(before_now(days=15)),
- "query": "",
- "interval": "30m",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 400
- @mock.patch("sentry.utils.snuba.quantize_time")
- def test_quantize_dates(self, mock_quantize):
- mock_quantize.return_value = before_now(days=1).replace(tzinfo=utc)
- # Don't quantize short time periods
- self.do_request(
- data={"statsPeriod": "1h", "query": "", "interval": "30m", "yAxis": "count()"},
- )
- # Don't quantize absolute date periods
- self.do_request(
- data={
- "start": iso_format(before_now(days=20)),
- "end": iso_format(before_now(days=15)),
- "query": "",
- "interval": "30m",
- "yAxis": "count()",
- },
- )
- assert len(mock_quantize.mock_calls) == 0
- # Quantize long date periods
- self.do_request(
- data={"statsPeriod": "90d", "query": "", "interval": "30m", "yAxis": "count()"},
- )
- assert len(mock_quantize.mock_calls) == 2
- def test_with_zerofill(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "30m",
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 1}],
- [{"count": 0}],
- [{"count": 2}],
- [{"count": 0}],
- ]
- def test_without_zerofill(self):
- start = iso_format(self.day_ago)
- end = iso_format(self.day_ago + timedelta(hours=2))
- response = self.do_request(
- data={
- "start": start,
- "end": end,
- "interval": "30m",
- "withoutZerofill": "1",
- },
- features={
- "organizations:performance-chart-interpolation": True,
- "organizations:discover-basic": True,
- },
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 1}],
- [{"count": 2}],
- ]
- assert response.data["start"] == parse_date(start).timestamp()
- assert response.data["end"] == parse_date(end).timestamp()
- def test_comparison(self):
- self.store_event(
- data={
- "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=1)),
- },
- project_id=self.project.id,
- )
- self.store_event(
- data={
- "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=2)),
- },
- project_id=self.project.id,
- )
- self.store_event(
- data={
- "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1, minutes=1)),
- },
- project_id=self.project2.id,
- )
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "comparisonDelta": int(timedelta(days=1).total_seconds()),
- }
- )
- assert response.status_code == 200, response.content
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": 1, "comparisonCount": 2}],
- [{"count": 2, "comparisonCount": 1}],
- ]
- def test_comparison_invalid(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "comparisonDelta": "17h",
- },
- )
- assert response.status_code == 400, response.content
- assert response.data["detail"] == "comparisonDelta must be an integer"
- start = before_now(days=85)
- end = start + timedelta(days=7)
- with self.options({"system.event-retention-days": 90}):
- response = self.do_request(
- data={
- "start": iso_format(start),
- "end": iso_format(end),
- "interval": "1h",
- "comparisonDelta": int(timedelta(days=7).total_seconds()),
- }
- )
- assert response.status_code == 400, response.content
- assert response.data["detail"] == "Comparison period is outside retention window"
- def test_equations_divide_by_zero(self):
- response = self.do_request(
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- # force a 0 in the denominator by doing 1 - 1
- # since a 0 literal is illegal as the denominator
- "yAxis": ["equation|count() / (1-1)"],
- },
- )
- assert response.status_code == 200, response.content
- assert len(response.data["data"]) == 2
- assert [attrs for time, attrs in response.data["data"]] == [
- [{"count": None}],
- [{"count": None}],
- ]
- @region_silo_test
- class OrganizationEventsStatsTopNEvents(APITestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.login_as(user=self.user)
- self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
- self.project = self.create_project()
- self.project2 = self.create_project()
- self.user2 = self.create_user()
- transaction_data = load_data("transaction")
- transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
- transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=4))
- transaction_data["tags"] = {"shared-tag": "yup"}
- self.event_data = [
- {
- "data": {
- "message": "poof",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
- "user": {"email": self.user.email},
- "tags": {"shared-tag": "yup"},
- "fingerprint": ["group1"],
- },
- "project": self.project2,
- "count": 7,
- },
- {
- "data": {
- "message": "voof",
- "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
- "fingerprint": ["group2"],
- "user": {"email": self.user2.email},
- "tags": {"shared-tag": "yup"},
- },
- "project": self.project2,
- "count": 6,
- },
- {
- "data": {
- "message": "very bad",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
- "fingerprint": ["group3"],
- "user": {"email": "foo@example.com"},
- "tags": {"shared-tag": "yup"},
- },
- "project": self.project,
- "count": 5,
- },
- {
- "data": {
- "message": "oh no",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
- "fingerprint": ["group4"],
- "user": {"email": "bar@example.com"},
- "tags": {"shared-tag": "yup"},
- },
- "project": self.project,
- "count": 4,
- },
- {"data": transaction_data, "project": self.project, "count": 3},
- # Not in the top 5
- {
- "data": {
- "message": "sorta bad",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
- "fingerprint": ["group5"],
- "user": {"email": "bar@example.com"},
- "tags": {"shared-tag": "yup"},
- },
- "project": self.project,
- "count": 2,
- },
- {
- "data": {
- "message": "not so bad",
- "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
- "fingerprint": ["group6"],
- "user": {"email": "bar@example.com"},
- "tags": {"shared-tag": "yup"},
- },
- "project": self.project,
- "count": 1,
- },
- ]
- self.events = []
- for index, event_data in enumerate(self.event_data):
- data = event_data["data"].copy()
- event = {}
- for i in range(event_data["count"]):
- data["event_id"] = f"{index}{i}" * 16
- event = self.store_event(data, project_id=event_data["project"].id)
- self.events.append(event)
- self.transaction = self.events[4]
- self.enabled_features = {
- "organizations:discover-basic": True,
- }
- self.url = reverse(
- "sentry-api-0-organization-events-stats",
- kwargs={"organization_slug": self.project.organization.slug},
- )
- def test_no_top_events_with_project_field(self):
- project = self.create_project()
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- # make sure to query the project with 0 events
- "project": project.id,
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "project"],
- "topEvents": 5,
- },
- format="json",
- )
- assert response.status_code == 200, response.content
- # When there are no top events, we do not return an empty dict.
- # Instead, we return a single zero-filled series for an empty graph.
- data = response.data["data"]
- assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
- def test_no_top_events(self):
- project = self.create_project()
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- # make sure to query the project with 0 events
- "project": project.id,
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "user.email"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data["data"]
- assert response.status_code == 200, response.content
- # When there are no top events, we do not return an empty dict.
- # Instead, we return a single zero-filled series for an empty graph.
- assert [attrs for time, attrs in data] == [[{"count": 0}], [{"count": 0}]]
- def test_no_top_events_with_multi_axis(self):
- project = self.create_project()
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- # make sure to query the project with 0 events
- "project": project.id,
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["count()", "count_unique(user)"],
- "orderby": ["-count()"],
- "field": ["count()", "count_unique(user)", "message", "user.email"],
- "topEvents": 5,
- },
- format="json",
- )
- assert response.status_code == 200
- data = response.data[""]
- assert [attrs for time, attrs in data["count()"]["data"]] == [
- [{"count": 0}],
- [{"count": 0}],
- ]
- assert [attrs for time, attrs in data["count_unique(user)"]["data"]] == [
- [{"count": 0}],
- [{"count": 0}],
- ]
- def test_simple_top_events(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "user.email"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[
- ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
- ]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for _, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
- def test_top_events_limits(self):
- data = {
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "user.email"],
- }
- with self.feature(self.enabled_features):
- data["topEvents"] = MAX_TOP_EVENTS + 1
- response = self.client.get(self.url, data, format="json")
- assert response.status_code == 400
- data["topEvents"] = 0
- response = self.client.get(self.url, data, format="json")
- assert response.status_code == 400
- data["topEvents"] = "a"
- response = self.client.get(self.url, data, format="json")
- assert response.status_code == 400
- def test_top_events_with_projects(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "project"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[",".join([message, event.project.slug])]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_issue(self):
- # delete a group to make sure if this happens the value becomes unknown
- event_group = self.events[0].group
- event_group.delete()
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "issue"],
- "topEvents": 5,
- "query": "!event.type:transaction",
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:4]):
- message = event.message
- # Because we deleted the group for event 0
- if index == 0 or event.group is None:
- issue = "unknown"
- else:
- issue = event.group.qualified_short_id
- results = data[",".join([issue, message])]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_transaction_status(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "transaction.status"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 1
- assert "ok" in data
- @mock.patch("sentry.models.GroupManager.get_issues_mapping")
- def test_top_events_with_unknown_issue(self, mock_issues_mapping):
- event = self.events[0]
- event_data = self.event_data[0]
- # ensure that the issue mapping returns None for the issue
- mock_issues_mapping.return_value = {event.group.id: None}
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "issue"],
- "topEvents": 5,
- # narrow the search to just one issue
- "query": f"issue.id:{event.group.id}",
- },
- format="json",
- )
- assert response.status_code == 200, response.content
- data = response.data
- assert len(data) == 1
- results = data["unknown"]
- assert results["order"] == 0
- assert [{"count": event_data["count"]}] in [attrs for time, attrs in results["data"]]
- @mock.patch(
- "sentry.search.events.builder.raw_snql_query",
- side_effect=[{"data": [{"issue.id": 1}], "meta": []}, {"data": [], "meta": []}],
- )
- def test_top_events_with_issue_check_query_conditions(self, mock_query):
- """ "Intentionally separate from test_top_events_with_issue
- This is to test against a bug where the condition for issues wasn't included and we'd be missing data for
- the interval since we'd cap out the max rows. This was not caught by the previous test since the results
- would still be correct given the smaller interval & lack of data
- """
- with self.feature(self.enabled_features):
- self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "issue"],
- "topEvents": 5,
- "query": "!event.type:transaction",
- },
- format="json",
- )
- assert (
- Condition(Function("coalesce", [Column("group_id"), 0], "issue.id"), Op.IN, [1])
- in mock_query.mock_calls[1].args[0].query.where
- )
- def test_top_events_with_functions(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-p99()"],
- "field": ["transaction", "avg(transaction.duration)", "p99()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 1
- results = data[self.transaction.transaction]
- assert results["order"] == 0
- assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
- def test_top_events_with_functions_on_different_transactions(self):
- """Transaction2 has less events, but takes longer so order should be self.transaction then transaction2"""
- transaction_data = load_data("transaction")
- transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
- transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
- transaction_data["transaction"] = "/foo_bar/"
- transaction2 = self.store_event(transaction_data, project_id=self.project.id)
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-p99()"],
- "field": ["transaction", "avg(transaction.duration)", "p99()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 2
- results = data[self.transaction.transaction]
- assert results["order"] == 1
- assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
- results = data[transaction2.transaction]
- assert results["order"] == 0
- assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
- def test_top_events_with_query(self):
- transaction_data = load_data("transaction")
- transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
- transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
- transaction_data["transaction"] = "/foo_bar/"
- self.store_event(transaction_data, project_id=self.project.id)
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-p99()"],
- "query": "transaction:/foo_bar/",
- "field": ["transaction", "avg(transaction.duration)", "p99()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 1
- transaction2_data = data["/foo_bar/"]
- assert transaction2_data["order"] == 0
- assert [attrs for time, attrs in transaction2_data["data"]] == [
- [{"count": 1}],
- [{"count": 0}],
- ]
- def test_top_events_with_negated_condition(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "query": f"!message:{self.events[0].message}",
- "field": ["message", "count()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[1:5]):
- message = event.message or event.transaction
- results = data[message]
- assert results["order"] == index
- assert [{"count": self.event_data[index + 1]["count"]}] in [
- attrs for _, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_epm(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "epm()",
- "orderby": ["-count()"],
- "field": ["message", "user.email", "count()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[
- ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
- ]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
- attrs for time, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 0.05}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_multiple_yaxis(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": ["epm()", "count()"],
- "orderby": ["-count()"],
- "field": ["message", "user.email", "count()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[
- ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
- ]
- assert results["order"] == index
- assert results["epm()"]["order"] == 0
- assert results["count()"]["order"] == 1
- assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
- attrs for time, attrs in results["epm()"]["data"]
- ]
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["count()"]["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert other["epm()"]["order"] == 0
- assert other["count()"]["order"] == 1
- assert [{"count": 0.05}] in [attrs for _, attrs in other["epm()"]["data"]]
- assert [{"count": 3}] in [attrs for _, attrs in other["count()"]["data"]]
- def test_top_events_with_boolean(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "device.charging"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[",".join(["False", message])]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_error_unhandled(self):
- self.login_as(user=self.user)
- project = self.create_project()
- prototype = load_data("android-ndk")
- prototype["event_id"] = "f" * 32
- prototype["message"] = "not handled"
- prototype["exception"]["values"][0]["value"] = "not handled"
- prototype["exception"]["values"][0]["mechanism"]["handled"] = False
- prototype["timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
- self.store_event(data=prototype, project_id=project.id)
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "error.unhandled"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 2
- def test_top_events_with_timestamp(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "query": "event.type:default",
- "field": ["count()", "message", "timestamp"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- # Transactions won't be in the results because of the query
- del self.events[4]
- del self.event_data[4]
- for index, event in enumerate(self.events[:5]):
- results = data[",".join([event.message, event.timestamp])]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 1}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_int(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message", "transaction.duration"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 1
- results = data[",".join([self.transaction.transaction, "120000"])]
- assert results["order"] == 0
- assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
- def test_top_events_with_user(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()", "user"],
- "field": ["user", "count()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 5
- assert data["email:bar@example.com"]["order"] == 1
- assert [attrs for time, attrs in data["email:bar@example.com"]["data"]] == [
- [{"count": 7}],
- [{"count": 0}],
- ]
- assert [attrs for time, attrs in data["ip:127.0.0.1"]["data"]] == [
- [{"count": 3}],
- [{"count": 0}],
- ]
- def test_top_events_with_user_and_email(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()", "user"],
- "field": ["user", "user.email", "count()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 5
- assert data["email:bar@example.com,bar@example.com"]["order"] == 1
- assert [attrs for time, attrs in data["email:bar@example.com,bar@example.com"]["data"]] == [
- [{"count": 7}],
- [{"count": 0}],
- ]
- assert [attrs for time, attrs in data["ip:127.0.0.1,None"]["data"]] == [
- [{"count": 3}],
- [{"count": 0}],
- ]
- def test_top_events_with_user_display(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["message", "user.display", "count()"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- user = self.event_data[index]["data"]["user"]
- results = data[
- ",".join([message, user.get("email", None) or user.get("ip_address", "None")])
- ]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for _, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
- @pytest.mark.skip(reason="A query with group_id will not return transactions")
- def test_top_events_none_filter(self):
- """When a field is None in one of the top events, make sure we filter by it
- In this case event[4] is a transaction and has no issue
- """
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "issue"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 5
- for index, event in enumerate(self.events[:5]):
- if event.group is None:
- issue = "unknown"
- else:
- issue = event.group.qualified_short_id
- results = data[issue]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["data"]
- ]
- @pytest.mark.skip(reason="Invalid query - transaction events don't have group_id field")
- def test_top_events_one_field_with_none(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "query": "event.type:transaction",
- "field": ["count()", "issue"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 1
- results = data["unknown"]
- assert [attrs for time, attrs in results["data"]] == [[{"count": 3}], [{"count": 0}]]
- assert results["order"] == 0
- def test_top_events_with_error_handled(self):
- data = self.event_data[0]
- data["data"]["level"] = "error"
- data["data"]["exception"] = {
- "values": [
- {
- "type": "ValidationError",
- "value": "Bad request",
- "mechanism": {"handled": True, "type": "generic"},
- }
- ]
- }
- self.store_event(data["data"], project_id=data["project"].id)
- data["data"]["exception"] = {
- "values": [
- {
- "type": "ValidationError",
- "value": "Bad request",
- "mechanism": {"handled": False, "type": "generic"},
- }
- ]
- }
- self.store_event(data["data"], project_id=data["project"].id)
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "error.handled"],
- "topEvents": 5,
- "query": "!event.type:transaction",
- },
- format="json",
- )
- assert response.status_code == 200, response.content
- data = response.data
- assert len(data) == 2
- results = data["1"]
- assert [attrs for time, attrs in results["data"]] == [[{"count": 20}], [{"count": 6}]]
- results = data["0"]
- assert [attrs for time, attrs in results["data"]] == [[{"count": 1}], [{"count": 0}]]
- def test_top_events_with_aggregate_condition(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["message", "count()"],
- "query": "count():>4",
- "topEvents": 5,
- },
- format="json",
- )
- assert response.status_code == 200, response.content
- data = response.data
- assert len(data) == 3
- for index, event in enumerate(self.events[:3]):
- message = event.message or event.transaction
- results = data[message]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for time, attrs in results["data"]
- ]
- @pytest.mark.xfail(reason="There's only 2 rows total, which mean there shouldn't be other")
- def test_top_events_with_to_other(self):
- version = "version -@'\" 1.2,3+(4)"
- version_escaped = "version -@'\\\" 1.2,3+(4)"
- # every symbol is replaced with a underscore to make the alias
- version_alias = "version_______1_2_3__4_"
- # add an event in the current release
- event = self.event_data[0]
- event_data = event["data"].copy()
- event_data["event_id"] = uuid4().hex
- event_data["release"] = version
- self.store_event(event_data, project_id=event["project"].id)
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- # the double underscores around the version alias is because of a comma and quote
- "orderby": [f"-to_other_release__{version_alias}__others_current"],
- "field": [
- "count()",
- f'to_other(release,"{version_escaped}",others,current)',
- ],
- "topEvents": 2,
- },
- format="json",
- )
- assert response.status_code == 200, response.content
- data = response.data
- assert len(data) == 2
- current = data["current"]
- assert current["order"] == 1
- assert sum(attrs[0]["count"] for _, attrs in current["data"]) == 1
- others = data["others"]
- assert others["order"] == 0
- assert sum(attrs[0]["count"] for _, attrs in others["data"]) == sum(
- event_data["count"] for event_data in self.event_data
- )
- def test_top_events_with_equations(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "equation|count() / 100",
- "orderby": ["-count()"],
- "field": ["count()", "message", "user.email", "equation|count() / 100"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[
- ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
- ]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"] / 100}] in [
- attrs for time, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 0.03}] in [attrs for _, attrs in other["data"]]
- @mock.patch("sentry.snuba.discover.bulk_snql_query", return_value=[{"data": [], "meta": []}])
- @mock.patch(
- "sentry.search.events.builder.raw_snql_query", return_value={"data": [], "meta": []}
- )
- def test_invalid_interval(self, mock_raw_query, mock_bulk_query):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- format="json",
- data={
- "end": iso_format(before_now()),
- # 7,200 points for each event
- "start": iso_format(before_now(seconds=7200)),
- "field": ["count()", "issue"],
- "query": "",
- "interval": "1s",
- "yAxis": "count()",
- },
- )
- assert response.status_code == 200
- assert mock_bulk_query.call_count == 1
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- format="json",
- data={
- "end": iso_format(before_now()),
- "start": iso_format(before_now(seconds=7200)),
- "field": ["count()", "issue"],
- "query": "",
- "interval": "1s",
- "yAxis": "count()",
- # 7,200 points for each event * 2, should error
- "topEvents": 2,
- },
- )
- assert response.status_code == 200
- assert mock_raw_query.call_count == 2
- # Should've reset to the default for between 1 and 24h
- assert mock_raw_query.mock_calls[1].args[0].query.granularity.granularity == 300
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- format="json",
- data={
- "end": iso_format(before_now()),
- # 1999 points * 5 events should just be enough to not error
- "start": iso_format(before_now(seconds=1999)),
- "field": ["count()", "issue"],
- "query": "",
- "interval": "1s",
- "yAxis": "count()",
- "topEvents": 5,
- },
- )
- assert response.status_code == 200
- assert mock_raw_query.call_count == 4
- # Should've left the interval alone since we're just below the limit
- assert mock_raw_query.mock_calls[3].args[0].query.granularity.granularity == 1
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- format="json",
- data={
- "end": iso_format(before_now()),
- "start": iso_format(before_now(hours=24)),
- "field": ["count()", "issue"],
- "query": "",
- "interval": "0d",
- "yAxis": "count()",
- "topEvents": 5,
- },
- )
- assert response.status_code == 200
- assert mock_raw_query.call_count == 6
- # Should've default to 24h's default of 5m
- assert mock_raw_query.mock_calls[5].args[0].query.granularity.granularity == 300
- def test_top_events_timestamp_fields(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- format="json",
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "timestamp", "timestamp.to_hour", "timestamp.to_day"],
- "topEvents": 5,
- },
- )
- assert response.status_code == 200
- data = response.data
- assert len(data) == 3
- # these are the timestamps corresponding to the events stored
- timestamps = [
- self.day_ago + timedelta(minutes=2),
- self.day_ago + timedelta(hours=1, minutes=2),
- self.day_ago + timedelta(minutes=4),
- ]
- timestamp_hours = [timestamp.replace(minute=0, second=0) for timestamp in timestamps]
- timestamp_days = [timestamp.replace(hour=0, minute=0, second=0) for timestamp in timestamps]
- for ts, ts_hr, ts_day in zip(timestamps, timestamp_hours, timestamp_days):
- key = f"{iso_format(ts)}+00:00,{iso_format(ts_day)}+00:00,{iso_format(ts_hr)}+00:00"
- count = sum(
- e["count"] for e in self.event_data if e["data"]["timestamp"] == iso_format(ts)
- )
- results = data[key]
- assert [{"count": count}] in [attrs for time, attrs in results["data"]]
- def test_top_events_other_with_matching_columns(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "tags[shared-tag]", "message"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- for index, event in enumerate(self.events[:5]):
- message = event.message or event.transaction
- results = data[",".join([message, "yup"])]
- assert results["order"] == index
- assert [{"count": self.event_data[index]["count"]}] in [
- attrs for _, attrs in results["data"]
- ]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 3}] in [attrs for _, attrs in other["data"]]
- def test_top_events_with_field_overlapping_other_key(self):
- transaction_data = load_data("transaction")
- transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
- transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
- transaction_data["transaction"] = OTHER_KEY
- for i in range(5):
- data = transaction_data.copy()
- data["event_id"] = "ab" + f"{i}" * 30
- data["contexts"]["trace"]["span_id"] = "ab" + f"{i}" * 14
- self.store_event(data, project_id=self.project.id)
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["count()", "message"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- assert f"{OTHER_KEY} (message)" in data
- results = data[f"{OTHER_KEY} (message)"]
- assert [{"count": 5}] in [attrs for _, attrs in results["data"]]
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
- def test_top_events_can_exclude_other_series(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["count()"],
- "field": ["count()", "message"],
- "topEvents": 5,
- "excludeOther": "1",
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 5
- assert "Other" not in response.data
- def test_top_events_with_equation_including_unselected_fields_passes_field_validation(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-equation[0]"],
- "field": ["count()", "message", "equation|count_unique(user) * 2"],
- "topEvents": 5,
- },
- format="json",
- )
- data = response.data
- assert response.status_code == 200, response.content
- assert len(data) == 6
- other = data["Other"]
- assert other["order"] == 5
- assert [{"count": 4}] in [attrs for _, attrs in other["data"]]
- def test_top_events_boolean_condition_and_project_field(self):
- with self.feature(self.enabled_features):
- response = self.client.get(
- self.url,
- data={
- "start": iso_format(self.day_ago),
- "end": iso_format(self.day_ago + timedelta(hours=2)),
- "interval": "1h",
- "yAxis": "count()",
- "orderby": ["-count()"],
- "field": ["project", "count()"],
- "topEvents": 5,
- "query": "event.type:transaction (transaction:*a OR transaction:b*)",
- },
- format="json",
- )
- assert response.status_code == 200
|