12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420 |
- import datetime
- import math
- import re
- from typing import List
- from unittest import mock
- import pytest
- from django.utils import timezone
- from snuba_sdk.aliased_expression import AliasedExpression
- from snuba_sdk.column import Column
- from snuba_sdk.conditions import Condition, Op, Or
- from snuba_sdk.function import Function
- from snuba_sdk.orderby import Direction, LimitBy, OrderBy
- from sentry.exceptions import IncompatibleMetricsQuery, InvalidSearchQuery
- from sentry.search.events import constants
- from sentry.search.events.builder import (
- HistogramMetricQueryBuilder,
- MetricsQueryBuilder,
- QueryBuilder,
- TimeseriesMetricQueryBuilder,
- )
- from sentry.search.events.types import HistogramParams
- from sentry.sentry_metrics import indexer
- from sentry.testutils.cases import MetricsEnhancedPerformanceTestCase, TestCase
- from sentry.utils.snuba import Dataset, QueryOutsideRetentionError
- class QueryBuilderTest(TestCase):
- def setUp(self):
- self.start = datetime.datetime.now(tz=timezone.utc).replace(
- hour=10, minute=15, second=0, microsecond=0
- ) - datetime.timedelta(days=2)
- self.end = self.start + datetime.timedelta(days=1)
- self.projects = [1, 2, 3]
- self.params = {
- "project_id": self.projects,
- "start": self.start,
- "end": self.end,
- }
- # These conditions should always be on a query when self.params is passed
- self.default_conditions = [
- Condition(Column("timestamp"), Op.GTE, self.start),
- Condition(Column("timestamp"), Op.LT, self.end),
- Condition(Column("project_id"), Op.IN, self.projects),
- ]
- def test_simple_query(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "user.email:foo@example.com release:1.2.1",
- ["user.email", "release"],
- )
- self.assertCountEqual(
- query.where,
- [
- Condition(Column("email"), Op.EQ, "foo@example.com"),
- Condition(Column("release"), Op.IN, ["1.2.1"]),
- *self.default_conditions,
- ],
- )
- self.assertCountEqual(
- query.columns,
- [
- AliasedExpression(Column("email"), "user.email"),
- Column("release"),
- ],
- )
- query.get_snql_query().validate()
- def test_simple_orderby(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- selected_columns=["user.email", "release"],
- orderby=["user.email"],
- )
- self.assertCountEqual(query.where, self.default_conditions)
- self.assertCountEqual(
- query.orderby,
- [OrderBy(Column("email"), Direction.ASC)],
- )
- query.get_snql_query().validate()
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- selected_columns=["user.email", "release"],
- orderby=["-user.email"],
- )
- self.assertCountEqual(query.where, self.default_conditions)
- self.assertCountEqual(
- query.orderby,
- [OrderBy(Column("email"), Direction.DESC)],
- )
- query.get_snql_query().validate()
- def test_orderby_duplicate_columns(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- selected_columns=["user.email", "user.email"],
- orderby=["user.email"],
- )
- self.assertCountEqual(
- query.orderby,
- [OrderBy(Column("email"), Direction.ASC)],
- )
- def test_simple_limitby(self):
- query = QueryBuilder(
- dataset=Dataset.Discover,
- params=self.params,
- query="",
- selected_columns=["message"],
- orderby="message",
- limitby=("message", 1),
- limit=4,
- )
- assert query.limitby == LimitBy([Column("message")], 1)
- def test_environment_filter(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "environment:prod",
- ["environment"],
- )
- self.assertCountEqual(
- query.where,
- [
- Condition(Column("environment"), Op.EQ, "prod"),
- *self.default_conditions,
- ],
- )
- query.get_snql_query().validate()
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "environment:[dev, prod]",
- ["environment"],
- )
- self.assertCountEqual(
- query.where,
- [
- Condition(Column("environment"), Op.IN, ["dev", "prod"]),
- *self.default_conditions,
- ],
- )
- query.get_snql_query().validate()
- def test_environment_param(self):
- self.params["environment"] = ["", "prod"]
- query = QueryBuilder(Dataset.Discover, self.params, selected_columns=["environment"])
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- Or(
- [
- Condition(Column("environment"), Op.IS_NULL),
- Condition(Column("environment"), Op.EQ, "prod"),
- ]
- ),
- ],
- )
- query.get_snql_query().validate()
- self.params["environment"] = ["dev", "prod"]
- query = QueryBuilder(Dataset.Discover, self.params, selected_columns=["environment"])
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- Condition(Column("environment"), Op.IN, ["dev", "prod"]),
- ],
- )
- query.get_snql_query().validate()
- def test_project_in_condition_filters(self):
- # TODO(snql-boolean): Update this to match the corresponding test in test_filter
- project1 = self.create_project()
- project2 = self.create_project()
- self.params["project_id"] = [project1.id, project2.id]
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- f"project:{project1.slug}",
- selected_columns=["environment"],
- )
- self.assertCountEqual(
- query.where,
- [
- # generated by the search query on project
- Condition(Column("project_id"), Op.EQ, project1.id),
- Condition(Column("timestamp"), Op.GTE, self.start),
- Condition(Column("timestamp"), Op.LT, self.end),
- # default project filter from the params
- Condition(Column("project_id"), Op.IN, [project1.id, project2.id]),
- ],
- )
- def test_project_in_condition_filters_not_in_project_filter(self):
- # TODO(snql-boolean): Update this to match the corresponding test in test_filter
- project1 = self.create_project()
- project2 = self.create_project()
- # params is assumed to be validated at this point, so this query should be invalid
- self.params["project_id"] = [project2.id]
- with self.assertRaisesRegex(
- InvalidSearchQuery,
- re.escape(
- f"Invalid query. Project(s) {str(project1.slug)} do not exist or are not actively selected."
- ),
- ):
- QueryBuilder(
- Dataset.Discover,
- self.params,
- f"project:{project1.slug}",
- selected_columns=["environment"],
- )
- def test_project_alias_column(self):
- # TODO(snql-boolean): Update this to match the corresponding test in test_filter
- project1 = self.create_project()
- project2 = self.create_project()
- self.params["project_id"] = [project1.id, project2.id]
- query = QueryBuilder(Dataset.Discover, self.params, selected_columns=["project"])
- self.assertCountEqual(
- query.where,
- [
- Condition(Column("project_id"), Op.IN, [project1.id, project2.id]),
- Condition(Column("timestamp"), Op.GTE, self.start),
- Condition(Column("timestamp"), Op.LT, self.end),
- ],
- )
- self.assertCountEqual(
- query.columns,
- [
- Function(
- "transform",
- [
- Column("project_id"),
- [project1.id, project2.id],
- [project1.slug, project2.slug],
- "",
- ],
- "project",
- )
- ],
- )
- def test_project_alias_column_with_project_condition(self):
- project1 = self.create_project()
- project2 = self.create_project()
- self.params["project_id"] = [project1.id, project2.id]
- query = QueryBuilder(
- Dataset.Discover, self.params, f"project:{project1.slug}", selected_columns=["project"]
- )
- self.assertCountEqual(
- query.where,
- [
- # generated by the search query on project
- Condition(Column("project_id"), Op.EQ, project1.id),
- Condition(Column("timestamp"), Op.GTE, self.start),
- Condition(Column("timestamp"), Op.LT, self.end),
- # default project filter from the params
- Condition(Column("project_id"), Op.IN, [project1.id, project2.id]),
- ],
- )
- # Because of the condition on project there should only be 1 project in the transform
- self.assertCountEqual(
- query.columns,
- [
- Function(
- "transform",
- [
- Column("project_id"),
- [project1.id],
- [project1.slug],
- "",
- ],
- "project",
- )
- ],
- )
- def test_count_if(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=[
- "count_if(event.type,equals,transaction)",
- 'count_if(event.type,notEquals,"transaction")',
- ],
- )
- self.assertCountEqual(query.where, self.default_conditions)
- self.assertCountEqual(
- query.aggregates,
- [
- Function(
- "countIf",
- [
- Function("equals", [Column("type"), "transaction"]),
- ],
- "count_if_event_type_equals_transaction",
- ),
- Function(
- "countIf",
- [
- Function("notEquals", [Column("type"), "transaction"]),
- ],
- "count_if_event_type_notEquals__transaction",
- ),
- ],
- )
- def test_count_if_with_tags(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=[
- "count_if(foo,equals,bar)",
- 'count_if(foo,notEquals,"baz")',
- ],
- )
- self.assertCountEqual(query.where, self.default_conditions)
- self.assertCountEqual(
- query.aggregates,
- [
- Function(
- "countIf",
- [
- Function("equals", [Column("tags[foo]"), "bar"]),
- ],
- "count_if_foo_equals_bar",
- ),
- Function(
- "countIf",
- [
- Function("notEquals", [Column("tags[foo]"), "baz"]),
- ],
- "count_if_foo_notEquals__baz",
- ),
- ],
- )
- def test_array_join(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=["array_join(measurements_key)", "count()"],
- functions_acl=["array_join"],
- )
- array_join_column = Function(
- "arrayJoin",
- [Column("measurements.key")],
- "array_join_measurements_key",
- )
- self.assertCountEqual(query.columns, [array_join_column, Function("count", [], "count")])
- # make sure the the array join columns are present in gropuby
- self.assertCountEqual(query.groupby, [array_join_column])
- def test_retention(self):
- old_start = datetime.datetime(2015, 5, 18, 10, 15, 1, tzinfo=timezone.utc)
- old_end = datetime.datetime(2015, 5, 19, 10, 15, 1, tzinfo=timezone.utc)
- old_params = {**self.params, "start": old_start, "end": old_end}
- with self.options({"system.event-retention-days": 10}):
- with self.assertRaises(QueryOutsideRetentionError):
- QueryBuilder(
- Dataset.Discover,
- old_params,
- "",
- selected_columns=[],
- )
- def test_array_combinator(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=["sumArray(measurements_value)"],
- functions_acl=["sumArray"],
- )
- self.assertCountEqual(
- query.columns,
- [
- Function(
- "sum",
- [Function("arrayJoin", [Column("measurements.value")])],
- "sumArray_measurements_value",
- )
- ],
- )
- def test_array_combinator_is_private(self):
- with self.assertRaisesRegex(InvalidSearchQuery, "sum: no access to private function"):
- QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=["sumArray(measurements_value)"],
- )
- def test_array_combinator_with_non_array_arg(self):
- with self.assertRaisesRegex(InvalidSearchQuery, "stuff is not a valid array column"):
- QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=["sumArray(stuff)"],
- functions_acl=["sumArray"],
- )
- def test_spans_columns(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=[
- "array_join(spans_op)",
- "array_join(spans_group)",
- "sumArray(spans_exclusive_time)",
- ],
- functions_acl=["array_join", "sumArray"],
- )
- self.assertCountEqual(
- query.columns,
- [
- Function("arrayJoin", [Column("spans.op")], "array_join_spans_op"),
- Function("arrayJoin", [Column("spans.group")], "array_join_spans_group"),
- Function(
- "sum",
- [Function("arrayJoin", [Column("spans.exclusive_time")])],
- "sumArray_spans_exclusive_time",
- ),
- ],
- )
- def test_array_join_clause(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=[
- "spans_op",
- "count()",
- ],
- array_join="spans_op",
- )
- self.assertCountEqual(
- query.columns,
- [
- AliasedExpression(Column("spans.op"), "spans_op"),
- Function("count", [], "count"),
- ],
- )
- assert query.array_join == [Column("spans.op")]
- query.get_snql_query().validate()
- def test_sample_rate(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=[
- "count()",
- ],
- sample_rate=0.1,
- )
- assert query.sample_rate == 0.1
- snql_query = query.get_snql_query().query
- snql_query.validate()
- assert snql_query.match.sample == 0.1
- def test_turbo(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "",
- selected_columns=[
- "count()",
- ],
- turbo=True,
- )
- assert query.turbo
- snql_query = query.get_snql_query()
- snql_query.validate()
- assert snql_query.flags.turbo
- def test_auto_aggregation(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "count_unique(user):>10",
- selected_columns=[
- "count()",
- ],
- auto_aggregations=True,
- use_aggregate_conditions=True,
- )
- snql_query = query.get_snql_query().query
- snql_query.validate()
- self.assertCountEqual(
- snql_query.having,
- [
- Condition(Function("uniq", [Column("user")], "count_unique_user"), Op.GT, 10),
- ],
- )
- self.assertCountEqual(
- snql_query.select,
- [
- Function("uniq", [Column("user")], "count_unique_user"),
- Function("count", [], "count"),
- ],
- )
- def test_auto_aggregation_with_boolean(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- # Nonsense query but doesn't matter
- "count_unique(user):>10 OR count_unique(user):<10",
- selected_columns=[
- "count()",
- ],
- auto_aggregations=True,
- use_aggregate_conditions=True,
- )
- snql_query = query.get_snql_query().query
- snql_query.validate()
- self.assertCountEqual(
- snql_query.having,
- [
- Or(
- [
- Condition(
- Function("uniq", [Column("user")], "count_unique_user"), Op.GT, 10
- ),
- Condition(
- Function("uniq", [Column("user")], "count_unique_user"), Op.LT, 10
- ),
- ]
- )
- ],
- )
- self.assertCountEqual(
- snql_query.select,
- [
- Function("uniq", [Column("user")], "count_unique_user"),
- Function("count", [], "count"),
- ],
- )
- def test_disable_auto_aggregation(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "count_unique(user):>10",
- selected_columns=[
- "count()",
- ],
- auto_aggregations=False,
- use_aggregate_conditions=True,
- )
- # With count_unique only in a condition and no auto_aggregations this should raise a invalid search query
- with self.assertRaises(InvalidSearchQuery):
- query.get_snql_query()
- def test_query_chained_or_tip(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "field:a OR field:b OR field:c",
- selected_columns=[
- "field",
- ],
- )
- assert constants.QUERY_TIPS["CHAINED_OR"] in query.tips["query"]
- def test_chained_or_with_different_terms(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "field:a or field:b or event.type:transaction or transaction:foo",
- selected_columns=[
- "field",
- ],
- )
- # This query becomes something roughly like:
- # field:a or (field:b or (event.type:transaciton or transaction: foo))
- assert constants.QUERY_TIPS["CHAINED_OR"] in query.tips["query"]
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- "event.type:transaction or transaction:foo or field:a or field:b",
- selected_columns=[
- "field",
- ],
- )
- assert constants.QUERY_TIPS["CHAINED_OR"] in query.tips["query"]
- def test_chained_or_with_different_terms_with_and(self):
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- # There's an implicit and between field:b, and event.type:transaction
- "field:a or field:b event.type:transaction",
- selected_columns=[
- "field",
- ],
- )
- # This query becomes something roughly like:
- # field:a or (field:b and event.type:transaction)
- assert constants.QUERY_TIPS["CHAINED_OR"] not in query.tips["query"]
- query = QueryBuilder(
- Dataset.Discover,
- self.params,
- # There's an implicit and between event.type:transaction, and field:a
- "event.type:transaction field:a or field:b",
- selected_columns=[
- "field",
- ],
- )
- # This query becomes something roughly like:
- # field:a or (field:b and event.type:transaction)
- assert constants.QUERY_TIPS["CHAINED_OR"] not in query.tips["query"]
- def _metric_percentile_definition(
- org_id, quantile, field="transaction.duration", alias=None
- ) -> Function:
- if alias is None:
- alias = f"p{quantile}_{field.replace('.', '_')}"
- return Function(
- "arrayElement",
- [
- Function(
- f"quantilesIf(0.{quantile.rstrip('0')})",
- [
- Column("value"),
- Function(
- "equals",
- [
- Column("metric_id"),
- indexer.resolve(org_id, constants.METRICS_MAP[field]),
- ],
- ),
- ],
- ),
- 1,
- ],
- alias,
- )
- def _metric_conditions(org_id, metrics) -> List[Condition]:
- return [
- Condition(
- Column("metric_id"),
- Op.IN,
- sorted(indexer.resolve(org_id, constants.METRICS_MAP[metric]) for metric in metrics),
- )
- ]
- class MetricBuilderBaseTest(MetricsEnhancedPerformanceTestCase):
- METRIC_STRINGS = [
- "foo_transaction",
- "bar_transaction",
- "baz_transaction",
- ]
- DEFAULT_METRIC_TIMESTAMP = datetime.datetime(
- 2015, 1, 1, 10, 15, 0, tzinfo=timezone.utc
- ) + datetime.timedelta(minutes=1)
- def setUp(self):
- super().setUp()
- self.start = datetime.datetime.now(tz=timezone.utc).replace(
- hour=10, minute=15, second=0, microsecond=0
- ) - datetime.timedelta(days=18)
- self.end = datetime.datetime.now(tz=timezone.utc).replace(
- hour=10, minute=15, second=0, microsecond=0
- )
- self.projects = [self.project.id]
- self.params = {
- "organization_id": self.organization.id,
- "project_id": self.projects,
- "start": self.start,
- "end": self.end,
- }
- # These conditions should always be on a query when self.params is passed
- self.default_conditions = [
- Condition(Column("timestamp"), Op.GTE, self.start),
- Condition(Column("timestamp"), Op.LT, self.end),
- Condition(Column("project_id"), Op.IN, self.projects),
- Condition(Column("org_id"), Op.EQ, self.organization.id),
- ]
- for string in self.METRIC_STRINGS:
- indexer.record(self.organization.id, string)
- indexer.record(self.organization.id, "transaction")
- def setup_orderby_data(self):
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- metric="user",
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 50,
- tags={"transaction": "bar_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- metric="user",
- tags={"transaction": "bar_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 2,
- metric="user",
- tags={"transaction": "bar_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- class MetricQueryBuilderTest(MetricBuilderBaseTest):
- def test_default_conditions(self):
- query = MetricsQueryBuilder(self.params, "", selected_columns=[])
- self.assertCountEqual(query.where, self.default_conditions)
- def test_column_resolution(self):
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=["tags[transaction]", "transaction"],
- )
- self.assertCountEqual(
- query.columns,
- [
- AliasedExpression(
- Column(f"tags[{indexer.resolve(self.organization.id, 'transaction')}]"),
- "tags[transaction]",
- ),
- AliasedExpression(
- Column(f"tags[{indexer.resolve(self.organization.id, 'transaction')}]"),
- "transaction",
- ),
- ],
- )
- def test_simple_aggregates(self):
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "p50(transaction.duration)",
- "p75(measurements.lcp)",
- "p90(measurements.fcp)",
- "p95(measurements.cls)",
- "p99(measurements.fid)",
- ],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(
- self.organization.id,
- [
- "transaction.duration",
- "measurements.lcp",
- "measurements.fcp",
- "measurements.cls",
- "measurements.fid",
- ],
- ),
- ],
- )
- self.assertCountEqual(
- query.distributions,
- [
- _metric_percentile_definition(self.organization.id, "50"),
- _metric_percentile_definition(self.organization.id, "75", "measurements.lcp"),
- _metric_percentile_definition(self.organization.id, "90", "measurements.fcp"),
- _metric_percentile_definition(self.organization.id, "95", "measurements.cls"),
- _metric_percentile_definition(self.organization.id, "99", "measurements.fid"),
- ],
- )
- def test_custom_percentile_throws_error(self):
- with self.assertRaises(IncompatibleMetricsQuery):
- MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "percentile(transaction.duration, 0.11)",
- ],
- )
- def test_percentile_function(self):
- self.maxDiff = None
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "percentile(transaction.duration, 0.75)",
- ],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(
- self.organization.id,
- [
- "transaction.duration",
- ],
- ),
- ],
- )
- self.assertCountEqual(
- query.distributions,
- [
- Function(
- "arrayElement",
- [
- Function(
- "quantilesIf(0.75)",
- [
- Column("value"),
- Function(
- "equals",
- [
- Column("metric_id"),
- indexer.resolve(
- self.organization.id,
- constants.METRICS_MAP["transaction.duration"],
- ),
- ],
- ),
- ],
- ),
- 1,
- ],
- "percentile_transaction_duration_0_75",
- )
- ],
- )
- def test_metric_condition_dedupe(self):
- org_id = 1
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "p50(transaction.duration)",
- "p75(transaction.duration)",
- "p90(transaction.duration)",
- "p95(transaction.duration)",
- "p99(transaction.duration)",
- ],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(org_id, ["transaction.duration"]),
- ],
- )
- def test_p100(self):
- """While p100 isn't an actual quantile in the distributions table, its equivalent to max"""
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "p100(transaction.duration)",
- ],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(
- self.organization.id,
- [
- "transaction.duration",
- ],
- ),
- ],
- )
- self.assertCountEqual(
- query.distributions,
- [
- Function(
- "maxIf",
- [
- Column("value"),
- Function(
- "equals",
- [
- Column("metric_id"),
- indexer.resolve(
- self.organization.id,
- constants.METRICS_MAP["transaction.duration"],
- ),
- ],
- ),
- ],
- "p100_transaction_duration",
- )
- ],
- )
- def test_grouping(self):
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=["transaction", "project", "p95(transaction.duration)"],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- ],
- )
- transaction_index = indexer.resolve(self.organization.id, "transaction")
- transaction = AliasedExpression(
- Column(f"tags[{transaction_index}]"),
- "transaction",
- )
- project = Function(
- "transform",
- [
- Column("project_id"),
- [self.project.id],
- [self.project.slug],
- "",
- ],
- "project",
- )
- self.assertCountEqual(
- query.groupby,
- [
- transaction,
- project,
- ],
- )
- self.assertCountEqual(
- query.distributions, [_metric_percentile_definition(self.organization.id, "95")]
- )
- def test_transaction_filter(self):
- query = MetricsQueryBuilder(
- self.params,
- "transaction:foo_transaction",
- selected_columns=["transaction", "project", "p95(transaction.duration)"],
- )
- transaction_index = indexer.resolve(self.organization.id, "transaction")
- transaction_name = indexer.resolve(self.organization.id, "foo_transaction")
- transaction = Column(f"tags[{transaction_index}]")
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- Condition(transaction, Op.EQ, transaction_name),
- ],
- )
- def test_transaction_in_filter(self):
- query = MetricsQueryBuilder(
- self.params,
- "transaction:[foo_transaction, bar_transaction]",
- selected_columns=["transaction", "project", "p95(transaction.duration)"],
- )
- transaction_index = indexer.resolve(self.organization.id, "transaction")
- transaction_name1 = indexer.resolve(self.organization.id, "foo_transaction")
- transaction_name2 = indexer.resolve(self.organization.id, "bar_transaction")
- transaction = Column(f"tags[{transaction_index}]")
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- Condition(transaction, Op.IN, [transaction_name1, transaction_name2]),
- ],
- )
- def test_missing_transaction_index(self):
- with self.assertRaisesRegex(
- InvalidSearchQuery,
- re.escape("Tag value was not found"),
- ):
- MetricsQueryBuilder(
- self.params,
- "transaction:something_else",
- selected_columns=["transaction", "project", "p95(transaction.duration)"],
- )
- def test_missing_transaction_index_in_filter(self):
- with self.assertRaisesRegex(
- InvalidSearchQuery,
- re.escape("Tag value was not found"),
- ):
- MetricsQueryBuilder(
- self.params,
- "transaction:[something_else, something_else2]",
- selected_columns=["transaction", "project", "p95(transaction.duration)"],
- )
- def test_incorrect_parameter_for_metrics(self):
- with self.assertRaises(IncompatibleMetricsQuery):
- MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=["transaction", "count_unique(test)"],
- )
- def test_project_filter(self):
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=["transaction", "project", "p95(transaction.duration)"],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- Condition(Column("project_id"), Op.EQ, self.project.id),
- ],
- )
- def test_limit_validation(self):
- # 51 is ok
- MetricsQueryBuilder(self.params, limit=51)
- # None is ok, defaults to 50
- query = MetricsQueryBuilder(self.params)
- assert query.limit.limit == 50
- # anything higher should throw an error
- with self.assertRaises(IncompatibleMetricsQuery):
- MetricsQueryBuilder(self.params, limit=10_000)
- def test_granularity(self):
- # Need to pick granularity based on the period
- def get_granularity(start, end):
- params = {
- "organization_id": self.organization.id,
- "project_id": self.projects,
- "start": start,
- "end": end,
- }
- query = MetricsQueryBuilder(params)
- return query.granularity.granularity
- # If we're doing atleast day and its midnight we should use the daily bucket
- start = datetime.datetime(2015, 5, 18, 0, 0, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 19, 0, 0, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 86400, "A day at midnight"
- # If we're doing several days, allow more range
- start = datetime.datetime(2015, 5, 18, 0, 10, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 28, 23, 59, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 86400, "Several days"
- # We're doing a long period, use the biggest granularity
- start = datetime.datetime(2015, 5, 18, 12, 33, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 7, 28, 17, 22, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 86400, "Big range"
- # If we're on the start of the hour we should use the hour granularity
- start = datetime.datetime(2015, 5, 18, 23, 0, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 20, 1, 0, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "On the hour"
- # If we're close to the start of the hour we should use the hour granularity
- start = datetime.datetime(2015, 5, 18, 23, 3, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 21, 1, 57, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "On the hour, close"
- # A decently long period but not close to hour ends, still use hour bucket
- start = datetime.datetime(2015, 5, 18, 23, 3, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 28, 1, 57, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "On the hour, long period"
- # Even though this is >24h of data, because its a random hour in the middle of the day to the next we use minute
- # granularity
- start = datetime.datetime(2015, 5, 18, 10, 15, 1, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 18, 18, 15, 1, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 60, "A few hours, but random minute"
- # Less than a minute, no reason to work hard for such a small window, just use a minute
- start = datetime.datetime(2015, 5, 18, 10, 15, 1, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 18, 10, 15, 34, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 60, "less than a minute"
- def test_granularity_boundaries(self):
- # Need to pick granularity based on the period
- def get_granularity(start, end):
- params = {
- "organization_id": self.organization.id,
- "project_id": self.projects,
- "start": start,
- "end": end,
- }
- query = MetricsQueryBuilder(params)
- return query.granularity.granularity
- # See resolve_granularity on the MQB to see what these boundaries are
- # Exactly 30d, at the 30 minute boundary
- start = datetime.datetime(2015, 5, 1, 0, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 31, 0, 30, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 86400, "30d at boundary"
- # Near 30d, but 1 hour before the boundary for end
- start = datetime.datetime(2015, 5, 1, 0, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 30, 23, 29, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "near 30d, but 1 hour before boundary for end"
- # Near 30d, but 1 hour after the boundary for start
- start = datetime.datetime(2015, 5, 1, 1, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 31, 0, 30, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "near 30d, but 1 hour after boundary for start"
- # Exactly 3d
- start = datetime.datetime(2015, 5, 1, 0, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 4, 0, 30, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 86400, "3d at boundary"
- # Near 3d, but 1 hour before the boundary for end
- start = datetime.datetime(2015, 5, 1, 0, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 3, 23, 29, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "near 3d, but 1 hour before boundary for end"
- # Near 3d, but 1 hour after the boundary for start
- start = datetime.datetime(2015, 5, 1, 1, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 4, 0, 30, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "near 3d, but 1 hour after boundary for start"
- # exactly 12 hours
- start = datetime.datetime(2015, 5, 1, 0, 15, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 1, 12, 15, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end) == 3600, "12h at boundary"
- # Near 12h, but 15 minutes before the boundary for end
- start = datetime.datetime(2015, 5, 1, 0, 15, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 1, 12, 0, 0, tzinfo=timezone.utc)
- assert (
- get_granularity(start, end) == 60
- ), "12h at boundary, but 15 min before the boundary for end"
- # Near 12h, but 15 minutes after the boundary for start
- start = datetime.datetime(2015, 5, 1, 0, 30, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 1, 12, 15, 0, tzinfo=timezone.utc)
- assert (
- get_granularity(start, end) == 60
- ), "12h at boundary, but 15 min after the boundary for start"
- def test_get_snql_query(self):
- query = MetricsQueryBuilder(self.params, "", selected_columns=["p90(transaction.duration)"])
- snql_request = query.get_snql_query()
- assert snql_request.dataset == "metrics"
- snql_query = snql_request.query
- self.assertCountEqual(
- snql_query.select,
- [
- _metric_percentile_definition(self.organization.id, "90"),
- ],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- ],
- )
- def test_get_snql_query_errors_with_multiple_dataset(self):
- query = MetricsQueryBuilder(
- self.params, "", selected_columns=["p90(transaction.duration)", "count_unique(user)"]
- )
- with self.assertRaises(NotImplementedError):
- query.get_snql_query()
- def test_get_snql_query_errors_with_no_functions(self):
- query = MetricsQueryBuilder(self.params, "", selected_columns=["project"])
- with self.assertRaises(IncompatibleMetricsQuery):
- query.get_snql_query()
- def test_run_query(self):
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 100,
- metric="measurements.lcp",
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1000,
- metric="measurements.lcp",
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "p95(transaction.duration)",
- "p100(measurements.lcp)",
- ],
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 1
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "p95_transaction_duration": 100,
- "p100_measurements_lcp": 1000,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "p100_measurements_lcp", "type": "Float64"},
- ],
- )
- def test_run_query_multiple_tables(self):
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- metric="user",
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 1
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "p95_transaction_duration": 100,
- "count_unique_user": 1,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_run_query_with_multiple_groupby_orderby_distribution(self):
- self.setup_orderby_data()
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby="-p95(transaction.duration)",
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 2
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 100,
- "count_unique_user": 1,
- }
- assert result["data"][1] == {
- "transaction": indexer.resolve(self.organization.id, "bar_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 50,
- "count_unique_user": 2,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "project", "type": "String"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_run_query_with_multiple_groupby_orderby_set(self):
- self.setup_orderby_data()
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby="-count_unique(user)",
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 2
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "bar_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 50,
- "count_unique_user": 2,
- }
- assert result["data"][1] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 100,
- "count_unique_user": 1,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "project", "type": "String"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_run_query_with_project_orderby(self):
- project_1 = self.create_project(slug="aaaaaa")
- project_2 = self.create_project(slug="zzzzzz")
- for project in [project_1, project_2]:
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- project=project.id,
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.params["project_id"] = [project_1.id, project_2.id]
- query = MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- ],
- orderby="project",
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 2
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": project_1.slug,
- "p95_transaction_duration": 100,
- }
- assert result["data"][1] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": project_2.slug,
- "p95_transaction_duration": 100,
- }
- query = MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- ],
- orderby="-project",
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 2
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": project_2.slug,
- "p95_transaction_duration": 100,
- }
- assert result["data"][1] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": project_1.slug,
- "p95_transaction_duration": 100,
- }
- def test_run_query_with_tag_orderby(self):
- with self.assertRaises(IncompatibleMetricsQuery):
- query = MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- ],
- orderby="transaction",
- )
- query.run_query("test_query")
- # TODO: multiple groupby with counter
- def test_run_query_with_events_per_aggregates(self):
- for i in range(5):
- self.store_metric(100, timestamp=self.start + datetime.timedelta(minutes=i * 15))
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "eps()",
- "epm()",
- "tps()",
- "tpm()",
- ],
- )
- result = query.run_query("test_query")
- data = result["data"][0]
- # Check the aliases are correct
- assert data["epm"] == data["tpm"]
- assert data["eps"] == data["tps"]
- # Check the values are correct
- assert data["tpm"] == 5 / ((self.end - self.start).total_seconds() / 60)
- assert data["tpm"] / 60 == data["tps"]
- def test_count(self):
- for _ in range(3):
- self.store_metric(
- 150,
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 50,
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "count()",
- ],
- )
- result = query.run_query("test_query")
- data = result["data"][0]
- assert data["count"] == 6
- def test_avg_duration(self):
- for _ in range(3):
- self.store_metric(
- 150,
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 50,
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "avg(transaction.duration)",
- ],
- )
- result = query.run_query("test_query")
- data = result["data"][0]
- assert data["avg_transaction_duration"] == 100
- def test_avg_span_http(self):
- for _ in range(3):
- self.store_metric(
- 150,
- metric="spans.http",
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 50,
- metric="spans.http",
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "avg(spans.http)",
- ],
- )
- result = query.run_query("test_query")
- data = result["data"][0]
- assert data["avg_spans_http"] == 100
- def test_failure_rate(self):
- for _ in range(3):
- self.store_metric(
- 100,
- tags={"transaction.status": "internal_error"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 100,
- tags={"transaction.status": "ok"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[
- "failure_rate()",
- "failure_count()",
- ],
- )
- result = query.run_query("test_query")
- data = result["data"][0]
- assert data["failure_rate"] == 0.5
- assert data["failure_count"] == 3
- def test_run_query_with_multiple_groupby_orderby_null_values_in_second_entity(self):
- """Since the null value is on count_unique(user) we will still get baz_transaction since we query distributions
- first which will have it, and then just not find a unique count in the second"""
- self.setup_orderby_data()
- self.store_metric(
- 200,
- tags={"transaction": "baz_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby="p95(transaction.duration)",
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 3
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "bar_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 50,
- "count_unique_user": 2,
- }
- assert result["data"][1] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 100,
- "count_unique_user": 1,
- }
- assert result["data"][2] == {
- "transaction": indexer.resolve(self.organization.id, "baz_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 200,
- "count_unique_user": 0,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "project", "type": "String"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- @pytest.mark.skip(
- reason="Currently cannot handle the case where null values are in the first entity"
- )
- def test_run_query_with_multiple_groupby_orderby_null_values_in_first_entity(self):
- """But if the null value is in the first entity, it won't show up in the groupby values, which means the
- transaction will be missing"""
- self.setup_orderby_data()
- self.store_metric(200, tags={"transaction": "baz_transaction"})
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby="count_unique(user)",
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 3
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "baz_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 200,
- }
- assert result["data"][1] == {
- "transaction": indexer.resolve(self.organization.id, "foo_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 100,
- "count_unique_user": 1,
- }
- assert result["data"][2] == {
- "transaction": indexer.resolve(self.organization.id, "bar_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 50,
- "count_unique_user": 2,
- }
- def test_multiple_entity_orderby_fails(self):
- with self.assertRaises(IncompatibleMetricsQuery):
- query = MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug}",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby=["-count_unique(user)", "p95(transaction.duration)"],
- )
- query.run_query("test_query")
- def test_multiple_entity_query_fails(self):
- with self.assertRaises(IncompatibleMetricsQuery):
- query = MetricsQueryBuilder(
- self.params,
- "p95(transaction.duration):>5s AND count_unique(user):>0",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- use_aggregate_conditions=True,
- )
- query.run_query("test_query")
- def test_query_entity_does_not_match_orderby(self):
- with self.assertRaises(IncompatibleMetricsQuery):
- query = MetricsQueryBuilder(
- self.params,
- "count_unique(user):>0",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby=["p95(transaction.duration)"],
- use_aggregate_conditions=True,
- )
- query.run_query("test_query")
- def test_aggregate_query_with_multiple_entities_without_orderby(self):
- self.store_metric(
- 200,
- tags={"transaction": "baz_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- metric="user",
- tags={"transaction": "bar_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- metric="user",
- tags={"transaction": "baz_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 2,
- metric="user",
- tags={"transaction": "baz_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- # This will query both sets & distribution cause of selected columns
- query = MetricsQueryBuilder(
- self.params,
- # Filter by count_unique since the default primary is distributions without an orderby
- "count_unique(user):>1",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- allow_metric_aggregates=True,
- use_aggregate_conditions=True,
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 1
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "baz_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 200,
- "count_unique_user": 2,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "project", "type": "String"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_aggregate_query_with_multiple_entities_with_orderby(self):
- self.store_metric(
- 200,
- tags={"transaction": "baz_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- tags={"transaction": "bar_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 1,
- metric="user",
- tags={"transaction": "baz_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- # This will query both sets & distribution cause of selected columns
- query = MetricsQueryBuilder(
- self.params,
- "p95(transaction.duration):>100",
- selected_columns=[
- "transaction",
- "project",
- "p95(transaction.duration)",
- "count_unique(user)",
- ],
- orderby=["p95(transaction.duration)"],
- allow_metric_aggregates=True,
- use_aggregate_conditions=True,
- )
- result = query.run_query("test_query")
- assert len(result["data"]) == 1
- assert result["data"][0] == {
- "transaction": indexer.resolve(self.organization.id, "baz_transaction"),
- "project": self.project.slug,
- "p95_transaction_duration": 200,
- "count_unique_user": 1,
- }
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "transaction", "type": "UInt64"},
- {"name": "project", "type": "String"},
- {"name": "p95_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_invalid_column_arg(self):
- for function in [
- "count_unique(transaction.duration)",
- "count_miserable(measurements.fcp)",
- "p75(user)",
- "count_web_vitals(user, poor)",
- ]:
- with self.assertRaises(IncompatibleMetricsQuery):
- MetricsQueryBuilder(
- self.params,
- "",
- selected_columns=[function],
- )
- def test_orderby_field_alias(self):
- query = MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "p95()",
- ],
- orderby=["p95"],
- )
- assert len(query.orderby) == 1
- assert query.orderby[0].exp == _metric_percentile_definition(
- self.organization.id, "95", "transaction.duration", "p95"
- )
- query = MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "p95() as test",
- ],
- orderby=["test"],
- )
- assert len(query.orderby) == 1
- assert query.orderby[0].exp == _metric_percentile_definition(
- self.organization.id, "95", "transaction.duration", "test"
- )
- def test_error_if_aggregates_disallowed(self):
- def run_query(query, use_aggregate_conditions):
- with self.assertRaises(IncompatibleMetricsQuery):
- MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "p95()",
- "count_unique(user)",
- ],
- query=query,
- allow_metric_aggregates=False,
- use_aggregate_conditions=use_aggregate_conditions,
- )
- queries = [
- "p95():>5s",
- "count_unique(user):>0",
- "transaction:foo_transaction AND (!transaction:bar_transaction OR p95():>5s)",
- ]
- for query in queries:
- for use_aggregate_conditions in [True, False]:
- run_query(query, use_aggregate_conditions)
- def test_no_error_if_aggregates_disallowed_but_no_aggregates_included(self):
- MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "p95()",
- "count_unique(user)",
- ],
- query="transaction:foo_transaction",
- allow_metric_aggregates=False,
- use_aggregate_conditions=True,
- )
- MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "transaction",
- "p95()",
- "count_unique(user)",
- ],
- query="transaction:foo_transaction",
- allow_metric_aggregates=False,
- use_aggregate_conditions=False,
- )
- def test_multiple_dataset_but_no_data(self):
- """When there's no data from the primary dataset we shouldn't error out"""
- result = MetricsQueryBuilder(
- self.params,
- selected_columns=[
- "p50()",
- "count_unique(user)",
- ],
- allow_metric_aggregates=False,
- use_aggregate_conditions=True,
- ).run_query("test")
- assert len(result["data"]) == 1
- data = result["data"][0]
- assert data["count_unique_user"] == 0
- # Handled by the discover transform later so its fine that this is nan
- assert math.isnan(data["p50"])
- @mock.patch("sentry.search.events.builder.raw_snql_query")
- @mock.patch("sentry.search.events.builder.indexer.resolve", return_value=-1)
- def test_dry_run_does_not_hit_indexer_or_clickhouse(self, mock_indexer, mock_query):
- query = MetricsQueryBuilder(
- self.params,
- # Include a tag:value search as well since that resolves differently
- f"project:{self.project.slug} transaction:foo_transaction",
- selected_columns=[
- "transaction",
- "p95(transaction.duration)",
- "p100(measurements.lcp)",
- "apdex()",
- "count_web_vitals(measurements.lcp, good)",
- ],
- dry_run=True,
- )
- query.run_query("test_query")
- assert not mock_indexer.called
- assert not mock_query.called
- @mock.patch("sentry.search.events.builder.indexer.resolve", return_value=-1)
- def test_multiple_references_only_resolve_index_once(self, mock_indexer):
- MetricsQueryBuilder(
- self.params,
- f"project:{self.project.slug} transaction:foo_transaction transaction:foo_transaction",
- selected_columns=[
- "transaction",
- "count_web_vitals(measurements.lcp, good)",
- "count_web_vitals(measurements.lcp, good)",
- "count_web_vitals(measurements.lcp, good)",
- "count_web_vitals(measurements.lcp, good)",
- "count_web_vitals(measurements.lcp, good)",
- ],
- )
- self.assertCountEqual(
- mock_indexer.mock_calls,
- [
- mock.call(self.organization.id, "transaction"),
- mock.call(self.organization.id, "foo_transaction"),
- mock.call(self.organization.id, constants.METRICS_MAP["measurements.lcp"]),
- mock.call(self.organization.id, "measurement_rating"),
- mock.call(self.organization.id, "good"),
- ],
- )
- class TimeseriesMetricQueryBuilderTest(MetricBuilderBaseTest):
- def test_get_query(self):
- query = TimeseriesMetricQueryBuilder(
- self.params, interval=900, query="", selected_columns=["p50(transaction.duration)"]
- )
- snql_query = query.get_snql_query()
- assert len(snql_query) == 1
- query = snql_query[0].query
- assert query.where == [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- ]
- assert query.select == [_metric_percentile_definition(self.organization.id, "50")]
- assert query.match.name == "metrics_distributions"
- assert query.granularity.granularity == 60
- def test_default_conditions(self):
- query = TimeseriesMetricQueryBuilder(
- self.params, interval=900, query="", selected_columns=[]
- )
- self.assertCountEqual(query.where, self.default_conditions)
- def test_granularity(self):
- # Need to pick granularity based on the period and interval for timeseries
- def get_granularity(start, end, interval):
- params = {
- "organization_id": self.organization.id,
- "project_id": self.projects,
- "start": start,
- "end": end,
- }
- query = TimeseriesMetricQueryBuilder(params, interval=interval)
- return query.granularity.granularity
- # If we're doing atleast day and its midnight we should use the daily bucket
- start = datetime.datetime(2015, 5, 18, 0, 0, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 19, 0, 0, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end, 30) == 10, "A day at midnight, 30s interval"
- assert get_granularity(start, end, 900) == 60, "A day at midnight, 15min interval"
- assert get_granularity(start, end, 3600) == 60, "A day at midnight, 1hr interval"
- assert get_granularity(start, end, 86400) == 3600, "A day at midnight, 1d interval"
- # If we're on the start of the hour we should use the hour granularity
- start = datetime.datetime(2015, 5, 18, 23, 0, 0, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 20, 1, 0, 0, tzinfo=timezone.utc)
- assert get_granularity(start, end, 30) == 10, "On the hour, 30s interval"
- assert get_granularity(start, end, 900) == 60, "On the hour, 15min interval"
- assert get_granularity(start, end, 3600) == 60, "On the hour, 1hr interval"
- assert get_granularity(start, end, 86400) == 3600, "On the hour, 1d interval"
- # Even though this is >24h of data, because its a random hour in the middle of the day to the next we use minute
- # granularity
- start = datetime.datetime(2015, 5, 18, 10, 15, 1, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 19, 15, 15, 1, tzinfo=timezone.utc)
- assert get_granularity(start, end, 30) == 10, "A few hours, but random minute, 30s interval"
- assert (
- get_granularity(start, end, 900) == 60
- ), "A few hours, but random minute, 15min interval"
- assert (
- get_granularity(start, end, 3600) == 60
- ), "A few hours, but random minute, 1hr interval"
- assert (
- get_granularity(start, end, 86400) == 3600
- ), "A few hours, but random minute, 1d interval"
- # Less than a minute, no reason to work hard for such a small window, just use a minute
- start = datetime.datetime(2015, 5, 18, 10, 15, 1, tzinfo=timezone.utc)
- end = datetime.datetime(2015, 5, 19, 10, 15, 34, tzinfo=timezone.utc)
- assert get_granularity(start, end, 30) == 10, "less than a minute, 30s interval"
- assert get_granularity(start, end, 900) == 60, "less than a minute, 15min interval"
- assert get_granularity(start, end, 3600) == 60, "less than a minute, 1hr interval"
- assert get_granularity(start, end, 86400) == 3600, "less than a minute, 1d interval"
- def test_transaction_in_filter(self):
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query="transaction:[foo_transaction, bar_transaction]",
- selected_columns=["p95(transaction.duration)"],
- )
- transaction_index = indexer.resolve(self.organization.id, "transaction")
- transaction_name1 = indexer.resolve(self.organization.id, "foo_transaction")
- transaction_name2 = indexer.resolve(self.organization.id, "bar_transaction")
- transaction = Column(f"tags[{transaction_index}]")
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- Condition(transaction, Op.IN, [transaction_name1, transaction_name2]),
- ],
- )
- def test_missing_transaction_index(self):
- with self.assertRaisesRegex(
- InvalidSearchQuery,
- re.escape("Tag value was not found"),
- ):
- TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query="transaction:something_else",
- selected_columns=["project", "p95(transaction.duration)"],
- )
- def test_missing_transaction_index_in_filter(self):
- with self.assertRaisesRegex(
- InvalidSearchQuery,
- re.escape("Tag value was not found"),
- ):
- TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query="transaction:[something_else, something_else2]",
- selected_columns=["p95(transaction.duration)"],
- )
- def test_project_filter(self):
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query=f"project:{self.project.slug}",
- selected_columns=["p95(transaction.duration)"],
- )
- self.assertCountEqual(
- query.where,
- [
- *self.default_conditions,
- *_metric_conditions(self.organization.id, ["transaction.duration"]),
- Condition(Column("project_id"), Op.EQ, self.project.id),
- ],
- )
- def test_meta(self):
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- selected_columns=["p50(transaction.duration)", "count_unique(user)"],
- )
- result = query.run_query("test_query")
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "time", "type": "DateTime('Universal')"},
- {"name": "p50_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_with_aggregate_filter(self):
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query="p50(transaction.duration):>100",
- selected_columns=["p50(transaction.duration)", "count_unique(user)"],
- allow_metric_aggregates=True,
- )
- # Aggregate conditions should be dropped
- assert query.having == []
- def test_run_query(self):
- for i in range(5):
- self.store_metric(100, timestamp=self.start + datetime.timedelta(minutes=i * 15))
- self.store_metric(
- 1,
- metric="user",
- timestamp=self.start + datetime.timedelta(minutes=i * 15),
- )
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query="",
- selected_columns=["p50(transaction.duration)", "count_unique(user)"],
- )
- result = query.run_query("test_query")
- assert result["data"] == [
- {
- "time": self.start.isoformat(),
- "p50_transaction_duration": 100.0,
- "count_unique_user": 1,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=15)).isoformat(),
- "p50_transaction_duration": 100.0,
- "count_unique_user": 1,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=30)).isoformat(),
- "p50_transaction_duration": 100.0,
- "count_unique_user": 1,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=45)).isoformat(),
- "p50_transaction_duration": 100.0,
- "count_unique_user": 1,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=60)).isoformat(),
- "p50_transaction_duration": 100.0,
- "count_unique_user": 1,
- },
- ]
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "time", "type": "DateTime('Universal')"},
- {"name": "p50_transaction_duration", "type": "Float64"},
- {"name": "count_unique_user", "type": "UInt64"},
- ],
- )
- def test_run_query_with_hour_interval(self):
- # See comment on resolve_time_column for explanation of this test
- self.start = datetime.datetime.now(timezone.utc).replace(
- hour=15, minute=30, second=0, microsecond=0
- )
- self.end = datetime.datetime.fromtimestamp(self.start.timestamp() + 86400, timezone.utc)
- self.params = {
- "organization_id": self.organization.id,
- "project_id": self.projects,
- "start": self.start,
- "end": self.end,
- }
- for i in range(5):
- self.store_metric(
- 100,
- timestamp=self.start + datetime.timedelta(minutes=i * 15),
- )
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=3600,
- query="",
- selected_columns=["epm(3600)"],
- )
- result = query.run_query("test_query")
- date_prefix = self.start.strftime("%Y-%m-%dT")
- assert result["data"] == [
- {"time": f"{date_prefix}15:00:00+00:00", "epm_3600": 2 / (3600 / 60)},
- {"time": f"{date_prefix}16:00:00+00:00", "epm_3600": 3 / (3600 / 60)},
- ]
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "time", "type": "DateTime('Universal')"},
- {"name": "epm_3600", "type": "Float64"},
- ],
- )
- def test_run_query_with_granularity_larger_than_interval(self):
- """The base MetricsQueryBuilder with a perfect 1d query will try to use granularity 86400 which is larger than
- the interval of 3600, in this case we want to make sure to use a smaller granularity to get the correct
- result"""
- self.start = datetime.datetime.now(timezone.utc).replace(
- hour=0, minute=0, second=0, microsecond=0
- )
- self.end = datetime.datetime.fromtimestamp(self.start.timestamp() + 86400, timezone.utc)
- self.params = {
- "organization_id": self.organization.id,
- "project_id": self.projects,
- "start": self.start,
- "end": self.end,
- }
- for i in range(1, 5):
- self.store_metric(
- 100,
- timestamp=self.start + datetime.timedelta(minutes=i * 15),
- )
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=3600,
- query="",
- selected_columns=["epm(3600)"],
- )
- result = query.run_query("test_query")
- date_prefix = self.start.strftime("%Y-%m-%dT")
- assert result["data"] == [
- {"time": f"{date_prefix}00:00:00+00:00", "epm_3600": 3 / (3600 / 60)},
- {"time": f"{date_prefix}01:00:00+00:00", "epm_3600": 1 / (3600 / 60)},
- ]
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "time", "type": "DateTime('Universal')"},
- {"name": "epm_3600", "type": "Float64"},
- ],
- )
- def test_run_query_with_filter(self):
- for i in range(5):
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=i * 15),
- )
- self.store_metric(
- 200,
- tags={"transaction": "bar_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=i * 15),
- )
- query = TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query="transaction:foo_transaction",
- selected_columns=["p50(transaction.duration)"],
- )
- result = query.run_query("test_query")
- assert result["data"] == [
- {"time": self.start.isoformat(), "p50_transaction_duration": 100.0},
- {
- "time": (self.start + datetime.timedelta(minutes=15)).isoformat(),
- "p50_transaction_duration": 100.0,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=30)).isoformat(),
- "p50_transaction_duration": 100.0,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=45)).isoformat(),
- "p50_transaction_duration": 100.0,
- },
- {
- "time": (self.start + datetime.timedelta(minutes=60)).isoformat(),
- "p50_transaction_duration": 100.0,
- },
- ]
- self.assertCountEqual(
- result["meta"],
- [
- {"name": "time", "type": "DateTime('Universal')"},
- {"name": "p50_transaction_duration", "type": "Float64"},
- ],
- )
- def test_error_if_aggregates_disallowed(self):
- def run_query(query):
- with self.assertRaises(IncompatibleMetricsQuery):
- TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- query=query,
- selected_columns=["p50(transaction.duration)"],
- allow_metric_aggregates=False,
- )
- queries = [
- "p95():>5s",
- "count_unique(user):>0",
- "transaction:foo_transaction AND (!transaction:bar_transaction OR p95():>5s)",
- ]
- for query in queries:
- run_query(query)
- def test_no_error_if_aggregates_disallowed_but_no_aggregates_included(self):
- TimeseriesMetricQueryBuilder(
- self.params,
- interval=900,
- selected_columns=["p50(transaction.duration)"],
- query="transaction:foo_transaction",
- allow_metric_aggregates=False,
- )
- def test_invalid_semver_filter(self):
- with self.assertRaises(InvalidSearchQuery):
- QueryBuilder(
- Dataset.Discover,
- self.params,
- "user.email:foo@example.com release.build:[1.2.1]",
- ["user.email", "release"],
- )
- class HistogramMetricQueryBuilderTest(MetricBuilderBaseTest):
- def test_histogram_columns_set_on_builder(self):
- builder = HistogramMetricQueryBuilder(
- params=self.params,
- query="",
- selected_columns=[
- "histogram(transaction.duration)",
- "histogram(measurements.lcp)",
- "histogram(measurements.fcp) as test",
- ],
- histogram_params=HistogramParams(
- 5,
- 100,
- 0,
- 1, # not used by Metrics
- ),
- )
- self.assertCountEqual(
- builder.histogram_aliases,
- [
- "histogram_transaction_duration",
- "histogram_measurements_lcp",
- "test",
- ],
- )
- def test_get_query(self):
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 100,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- self.store_metric(
- 450,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = HistogramMetricQueryBuilder(
- params=self.params,
- query="",
- selected_columns=["histogram(transaction.duration)"],
- histogram_params=HistogramParams(
- 5,
- 100,
- 0,
- 1, # not used by Metrics
- ),
- )
- snql_query = query.run_query("test_query")
- assert len(snql_query["data"]) == 1
- # This data is intepolated via rebucket_histogram
- assert snql_query["data"][0]["histogram_transaction_duration"] == [
- (0.0, 100.0, 0),
- (100.0, 200.0, 2),
- (200.0, 300.0, 1),
- (300.0, 400.0, 1),
- (400.0, 500.0, 1),
- ]
- def test_query_normal_distribution(self):
- for i in range(5):
- for _ in range((5 - abs(i - 2)) ** 2):
- self.store_metric(
- 100 * i + 50,
- tags={"transaction": "foo_transaction"},
- timestamp=self.start + datetime.timedelta(minutes=5),
- )
- query = HistogramMetricQueryBuilder(
- params=self.params,
- query="",
- selected_columns=["histogram(transaction.duration)"],
- histogram_params=HistogramParams(
- 5,
- 100,
- 0,
- 1, # not used by Metrics
- ),
- )
- snql_query = query.run_query("test_query")
- assert len(snql_query["data"]) == 1
- # This data is intepolated via rebucket_histogram
- assert snql_query["data"][0]["histogram_transaction_duration"] == [
- (0.0, 100.0, 10),
- (100.0, 200.0, 17),
- (200.0, 300.0, 23),
- (300.0, 400.0, 17),
- (400.0, 500.0, 10),
- ]
|