def test_translate_results_missing_slots(_1, _2, monkeypatch): monkeypatch.setattr("sentry.sentry_metrics.indexer.reverse_resolve", MockIndexer().reverse_resolve) query_params = MultiValueDict({ "field": [ "sum(sentry.sessions.session)", ], "interval": ["1d"], "statsPeriod": ["3d"], }) query_definition = QueryDefinition(query_params) results = { "metrics_counters": { "totals": { "data": [ { "metric_id": 9, # session "sum(sentry.sessions.session)": 400, }, ], }, "series": { "data": [ { "metric_id": 9, # session "bucketed_time": "2021-08-23T00:00Z", "sum(sentry.sessions.session)": 100, }, # no data for 2021-08-24 { "metric_id": 9, # session "bucketed_time": "2021-08-25T00:00Z", "sum(sentry.sessions.session)": 300, }, ], }, }, } intervals = list(get_intervals(query_definition)) assert SnubaResultConverter(1, query_definition, intervals, results).translate_results() == [ { "by": {}, "totals": { "sum(sentry.sessions.session)": 400, }, "series": { # No data for 2021-08-24 "sum(sentry.sessions.session)": [100, 0, 300], }, }, ]
def test_translate_results_missing_slots(_1, _2): query_params = MultiValueDict( { "field": [ "sum(session)", ], "interval": ["1d"], "statsPeriod": ["3d"], } ) query_definition = QueryDefinition(query_params) results = { "metrics_counters": { "totals": { "data": [ { "metric_id": 9, # session "value": 400, }, ], }, "series": { "data": [ { "metric_id": 9, # session "bucketed_time": datetime(2021, 8, 23, tzinfo=pytz.utc), "value": 100, }, # no data for 2021-08-24 { "metric_id": 9, # session "bucketed_time": datetime(2021, 8, 25, tzinfo=pytz.utc), "value": 300, }, ], }, }, } intervals = list(get_intervals(query_definition)) assert SnubaResultConverter(1, query_definition, intervals, results).translate_results() == [ { "by": {}, "totals": { "sum(session)": 400, }, "series": { # No data for 2021-08-24 "sum(session)": [100, 0, 300], }, }, ]
def test_translate_results(_1, _2): query_params = MultiValueDict( { "groupBy": ["session.status"], "field": [ "sum(session)", "max(session.duration)", "p50(session.duration)", "p95(session.duration)", ], "interval": ["1d"], "statsPeriod": ["2d"], } ) query_definition = QueryDefinition(query_params) intervals = list(get_intervals(query_definition)) results = { "metrics_counters": { "totals": { "data": [ { "metric_id": 9, # session "tags[8]": 4, # session.status:healthy "value": 300, }, { "metric_id": 9, # session "tags[8]": 0, # session.status:abnormal "value": 330, }, ], }, "series": { "data": [ { "metric_id": 9, # session "tags[8]": 4, "bucketed_time": datetime(2021, 8, 24, tzinfo=pytz.utc), "value": 100, }, { "metric_id": 9, # session "tags[8]": 0, "bucketed_time": datetime(2021, 8, 24, tzinfo=pytz.utc), "value": 110, }, { "metric_id": 9, # session "tags[8]": 4, "bucketed_time": datetime(2021, 8, 25, tzinfo=pytz.utc), "value": 200, }, { "metric_id": 9, # session "tags[8]": 0, "bucketed_time": datetime(2021, 8, 25, tzinfo=pytz.utc), "value": 220, }, ], }, }, "metrics_distributions": { "totals": { "data": [ { "metric_id": 7, # session.duration "tags[8]": 4, "max": 123.4, "percentiles": [1, 2, 3, 4, 5], }, { "metric_id": 7, # session.duration "tags[8]": 0, "max": 456.7, "percentiles": [1.5, 2.5, 3.5, 4.5, 5.5], }, ], }, "series": { "data": [ { "metric_id": 7, # session.duration "tags[8]": 4, "bucketed_time": datetime(2021, 8, 24, tzinfo=pytz.utc), "max": 10.1, "percentiles": [1.1, 2.1, 3.1, 4.1, 5.1], }, { "metric_id": 7, # session.duration "tags[8]": 0, "bucketed_time": datetime(2021, 8, 24, tzinfo=pytz.utc), "max": 20.2, "percentiles": [1.2, 2.2, 3.2, 4.2, 5.2], }, { "metric_id": 7, # session.duration "tags[8]": 4, "bucketed_time": datetime(2021, 8, 25, tzinfo=pytz.utc), "max": 30.3, "percentiles": [1.3, 2.3, 3.3, 4.3, 5.3], }, { "metric_id": 7, # session.duration "tags[8]": 0, "bucketed_time": datetime(2021, 8, 25, tzinfo=pytz.utc), "max": 40.4, "percentiles": [1.4, 2.4, 3.4, 4.4, 5.4], }, ], }, }, } assert SnubaResultConverter(1, query_definition, intervals, results).translate_results() == [ { "by": {"session.status": "healthy"}, "totals": { "sum(session)": 300, "max(session.duration)": 123.4, "p50(session.duration)": 1, "p95(session.duration)": 4, }, "series": { "sum(session)": [100, 200], "max(session.duration)": [10.1, 30.3], "p50(session.duration)": [1.1, 1.3], "p95(session.duration)": [4.1, 4.3], }, }, { "by": {"session.status": "abnormal"}, "totals": { "sum(session)": 330, "max(session.duration)": 456.7, "p50(session.duration)": 1.5, "p95(session.duration)": 4.5, }, "series": { "sum(session)": [110, 220], "max(session.duration)": [20.2, 40.4], "p50(session.duration)": [1.2, 1.4], "p95(session.duration)": [4.2, 4.4], }, }, ]
def run_sessions_query( org_id: int, query: QueryDefinition, span_op: str, ) -> SessionsQueryResult: """Convert a QueryDefinition to multiple snuba queries and reformat the results""" # This is necessary so that we do not mutate the query object shared between different # backend runs query_clone = deepcopy(query) data, metric_to_output_field = _fetch_data(org_id, query_clone) data_points = _flatten_data(org_id, data) intervals = list(get_intervals(query_clone)) timestamp_index = { timestamp.isoformat(): index for index, timestamp in enumerate(intervals) } def default_for(field: SessionsQueryFunction) -> SessionsQueryValue: return 0 if field in ("sum(session)", "count_unique(user)") else None GroupKey = Tuple[Tuple[GroupByFieldName, Union[str, int]], ...] class Group(TypedDict): series: MutableMapping[SessionsQueryFunction, List[SessionsQueryValue]] totals: MutableMapping[SessionsQueryFunction, SessionsQueryValue] groups: MutableMapping[GroupKey, Group] = defaultdict( lambda: { "totals": {field: default_for(field) for field in query_clone.raw_fields}, "series": { field: len(intervals) * [default_for(field)] for field in query_clone.raw_fields }, }) if len(data_points) == 0: # We're only interested in `session.status` group-byes. The rest of the # conditions require work (e.g. getting all environments) that we can't # get without querying the DB, including group-byes consisting of # multiple parameters (even if `session.status` is one of them). if query_clone.raw_groupby == ["session.status"]: for status in get_args(_SessionStatus): gkey: GroupKey = (("session.status", status), ) groups[gkey] else: for key in data_points.keys(): try: output_field = metric_to_output_field[key.metric_key, key.column] except KeyError: continue # secondary metric, like session.error by: MutableMapping[GroupByFieldName, Union[str, int]] = {} if key.release is not None: # Every session has a release, so this should not throw by["release"] = reverse_resolve(key.release) if key.environment is not None: # To match behavior of the old sessions backend, session data # without environment is grouped under the empty string. by["environment"] = reverse_resolve_weak(key.environment) or "" if key.project_id is not None: by["project"] = key.project_id for status_value in output_field.get_values(data_points, key): if status_value.session_status is not None: by["session.status"] = status_value.session_status # ! group_key: GroupKey = tuple(sorted(by.items())) group: Group = groups[group_key] value = status_value.value if value is not None: value = finite_or_none(value) if key.bucketed_time is None: group["totals"][output_field.get_name()] = value else: index = timestamp_index[key.bucketed_time] group["series"][output_field.get_name()][index] = value groups_as_list: List[SessionsQueryGroup] = [{ "by": dict(by), "totals": group["totals"], "series": group["series"], } for by, group in groups.items()] def format_datetime(dt: datetime) -> str: return dt.isoformat().replace("+00:00", "Z") return { "start": format_datetime(query_clone.start), "end": format_datetime(query_clone.end), "query": query_clone.query, "intervals": [format_datetime(dt) for dt in intervals], "groups": groups_as_list, }
def run_sessions_query( org_id: int, query: QueryDefinition, span_op: str, ) -> SessionsQueryResult: """Convert a QueryDefinition to multiple snuba queries and reformat the results""" data, metric_to_output_field = _fetch_data(org_id, query) data_points = _flatten_data(org_id, data) intervals = list(get_intervals(query)) timestamp_index = { timestamp.isoformat(): index for index, timestamp in enumerate(intervals) } def default_for(field: SessionsQueryFunction) -> SessionsQueryValue: return 0 if field in ("sum(session)", "count_unique(user)") else None GroupKey = Tuple[Tuple[GroupByFieldName, Union[str, int]], ...] class Group(TypedDict): series: MutableMapping[SessionsQueryFunction, List[SessionsQueryValue]] totals: MutableMapping[SessionsQueryFunction, SessionsQueryValue] groups: MutableMapping[GroupKey, Group] = defaultdict( lambda: { "totals": {field: default_for(field) for field in query.raw_fields}, "series": { field: len(intervals) * [default_for(field)] for field in query.raw_fields }, }) for key in data_points.keys(): try: output_field = metric_to_output_field[key.metric_name, key.column] except KeyError: continue # secondary metric, like session.error by: MutableMapping[GroupByFieldName, Union[str, int]] = {} if key.release is not None: # Note: If the tag value reverse-resolves to None here, it's a bug in the tag indexer by["release"] = _reverse_resolve_ensured(key.release) if key.environment is not None: by["environment"] = _reverse_resolve_ensured(key.environment) if key.project_id is not None: by["project"] = key.project_id for status_value in output_field.get_values(data_points, key): if status_value.session_status is not None: by["session.status"] = status_value.session_status group_key: GroupKey = tuple(sorted(by.items())) group = groups[group_key] value = status_value.value if value is not None: value = finite_or_none(value) if key.bucketed_time is None: group["totals"][output_field.get_name()] = value else: index = timestamp_index[key.bucketed_time] group["series"][output_field.get_name()][index] = value groups_as_list: List[SessionsQueryGroup] = [{ "by": dict(by), "totals": group["totals"], "series": group["series"], } for by, group in groups.items()] def format_datetime(dt: datetime) -> str: return dt.isoformat().replace("+00:00", "Z") return { "start": format_datetime(query.start), "end": format_datetime(query.end), "query": query.query, "intervals": [format_datetime(dt) for dt in intervals], "groups": groups_as_list, }
def test_translate_results_derived_metrics(_1, _2, monkeypatch): monkeypatch.setattr("sentry.sentry_metrics.indexer.reverse_resolve", MockIndexer().reverse_resolve) query_params = MultiValueDict({ "groupBy": [], "field": [ "session.errored", "session.crash_free_rate", "session.all", ], "interval": ["1d"], "statsPeriod": ["2d"], }) query_definition = QueryDefinition(query_params) fields_in_entities = { "metrics_counters": [ (None, "session.errored_preaggregated"), (None, "session.crash_free_rate"), (None, "session.all"), ], "metrics_sets": [ (None, "session.errored_set"), ], } intervals = list(get_intervals(query_definition)) results = { "metrics_counters": { "totals": { "data": [{ "session.crash_free_rate": 0.5, "session.all": 8.0, "session.errored_preaggregated": 3, }], }, "series": { "data": [ { "bucketed_time": "2021-08-24T00:00Z", "session.crash_free_rate": 0.5, "session.all": 4, "session.errored_preaggregated": 1, }, { "bucketed_time": "2021-08-25T00:00Z", "session.crash_free_rate": 0.5, "session.all": 4, "session.errored_preaggregated": 2, }, ], }, }, "metrics_sets": { "totals": { "data": [ { "session.errored_set": 3, }, ], }, "series": { "data": [ { "bucketed_time": "2021-08-24T00:00Z", "session.errored_set": 2 }, { "bucketed_time": "2021-08-25T00:00Z", "session.errored_set": 1 }, ], }, }, } assert SnubaResultConverter(1, query_definition, fields_in_entities, intervals, results).translate_results() == [ { "by": {}, "totals": { "session.all": 8, "session.crash_free_rate": 0.5, "session.errored": 6, }, "series": { "session.all": [4, 4], "session.crash_free_rate": [0.5, 0.5], "session.errored": [3, 3], }, }, ]
def test_translate_results(_1, _2, monkeypatch): monkeypatch.setattr("sentry.sentry_metrics.indexer.reverse_resolve", MockIndexer().reverse_resolve) query_params = MultiValueDict({ "groupBy": ["session.status"], "field": [ "sum(sentry.sessions.session)", "max(sentry.sessions.session.duration)", "p50(sentry.sessions.session.duration)", "p95(sentry.sessions.session.duration)", ], "interval": ["1d"], "statsPeriod": ["2d"], }) query_definition = QueryDefinition(query_params) fields_in_entities = { "metrics_counters": [("sum", "sentry.sessions.session")], "metrics_distributions": [ ("max", "sentry.sessions.session.duration"), ("p50", "sentry.sessions.session.duration"), ("p95", "sentry.sessions.session.duration"), ], } intervals = list(get_intervals(query_definition)) results = { "metrics_counters": { "totals": { "data": [ { "metric_id": 9, # session "tags[8]": 4, # session.status:healthy "sum(sentry.sessions.session)": 300, }, { "metric_id": 9, # session "tags[8]": 14, # session.status:abnormal "sum(sentry.sessions.session)": 330, }, ], }, "series": { "data": [ { "metric_id": 9, # session "tags[8]": 4, "bucketed_time": "2021-08-24T00:00Z", "sum(sentry.sessions.session)": 100, }, { "metric_id": 9, # session "tags[8]": 14, "bucketed_time": "2021-08-24T00:00Z", "sum(sentry.sessions.session)": 110, }, { "metric_id": 9, # session "tags[8]": 4, "bucketed_time": "2021-08-25T00:00Z", "sum(sentry.sessions.session)": 200, }, { "metric_id": 9, # session "tags[8]": 14, "bucketed_time": "2021-08-25T00:00Z", "sum(sentry.sessions.session)": 220, }, ], }, }, "metrics_distributions": { "totals": { "data": [ { "metric_id": 7, # session.duration "tags[8]": 4, "max(sentry.sessions.session.duration)": 123.4, "p50(sentry.sessions.session.duration)": [1], "p95(sentry.sessions.session.duration)": [4], }, { "metric_id": 7, # session.duration "tags[8]": 14, "max(sentry.sessions.session.duration)": 456.7, "p50(sentry.sessions.session.duration)": [1.5], "p95(sentry.sessions.session.duration)": [4.5], }, ], }, "series": { "data": [ { "metric_id": 7, # session.duration "tags[8]": 4, "bucketed_time": "2021-08-24T00:00Z", "max(sentry.sessions.session.duration)": 10.1, "p50(sentry.sessions.session.duration)": [1.1], "p95(sentry.sessions.session.duration)": [4.1], }, { "metric_id": 7, # session.duration "tags[8]": 14, "bucketed_time": "2021-08-24T00:00Z", "max(sentry.sessions.session.duration)": 20.2, "p50(sentry.sessions.session.duration)": [1.2], "p95(sentry.sessions.session.duration)": [4.2], }, { "metric_id": 7, # session.duration "tags[8]": 4, "bucketed_time": "2021-08-25T00:00Z", "max(sentry.sessions.session.duration)": 30.3, "p50(sentry.sessions.session.duration)": [1.3], "p95(sentry.sessions.session.duration)": [4.3], }, { "metric_id": 7, # session.duration "tags[8]": 14, "bucketed_time": "2021-08-25T00:00Z", "max(sentry.sessions.session.duration)": 40.4, "p50(sentry.sessions.session.duration)": [1.4], "p95(sentry.sessions.session.duration)": [4.4], }, ], }, }, } assert SnubaResultConverter( 1, query_definition, fields_in_entities, intervals, results).translate_results() == [ { "by": { "session.status": "healthy" }, "totals": { "sum(sentry.sessions.session)": 300, "max(sentry.sessions.session.duration)": 123.4, "p50(sentry.sessions.session.duration)": 1, "p95(sentry.sessions.session.duration)": 4, }, "series": { "sum(sentry.sessions.session)": [100, 200], "max(sentry.sessions.session.duration)": [10.1, 30.3], "p50(sentry.sessions.session.duration)": [1.1, 1.3], "p95(sentry.sessions.session.duration)": [4.1, 4.3], }, }, { "by": { "session.status": "abnormal" }, "totals": { "sum(sentry.sessions.session)": 330, "max(sentry.sessions.session.duration)": 456.7, "p50(sentry.sessions.session.duration)": 1.5, "p95(sentry.sessions.session.duration)": 4.5, }, "series": { "sum(sentry.sessions.session)": [110, 220], "max(sentry.sessions.session.duration)": [20.2, 40.4], "p50(sentry.sessions.session.duration)": [1.2, 1.4], "p95(sentry.sessions.session.duration)": [4.2, 4.4], }, }, ]