From adee10929f3138070b95e6c67ab41dd177979795 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Fri, 24 Dec 2021 21:01:30 +0100 Subject: [PATCH] feat(api): sessions-search TIME_BETWEEN_EVENTS --- api/chalicelib/core/sessions.py | 68 +++++++++++++++++++++++++++------ api/schemas.py | 46 +++++++++++++++++++--- 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 3981ff990..70858481c 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -1,6 +1,6 @@ import schemas from chalicelib.core import events, sessions_metas, metadata, events_ios, \ - sessions_mobs, issues, projects, errors, resources, assist + sessions_mobs, issues, projects, errors, resources, assist, performance_event from chalicelib.utils import pg_client, helper, dev SESSION_PROJECTION_COLS = """s.project_id, @@ -318,16 +318,16 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f "main.session_id=event_0.session_id"] if data.events_order == schemas.SearchEventOrder._then: event_where.append(f"event_{event_index - 1}.timestamp <= main.timestamp") - - event.value = helper.values_for_operator(value=event.value, op=event.operator) - # event_args = _multiple_values(event.value) e_k = f"e_value{i}" - full_args = {**full_args, **_multiple_values(event.value, value_key=e_k)} - if event_type not in list(events.SUPPORTED_TYPES.keys()) \ - or event.value in [None, "", "*"] \ - and (event_type != events.event_type.ERROR.ui_type \ - or event_type != events.event_type.ERROR_IOS.ui_type): - continue + if event.type != schemas.PerformanceEventType.time_between_events: + event.value = helper.values_for_operator(value=event.value, op=event.operator) + full_args = {**full_args, **_multiple_values(event.value, value_key=e_k)} + + # if event_type not in list(events.SUPPORTED_TYPES.keys()) \ + # or event.value in [None, "", "*"] \ + # and (event_type != events.event_type.ERROR.ui_type \ + # or event_type != events.event_type.ERROR_IOS.ui_type): + # continue if event_type == events.event_type.CLICK.ui_type: event_from = event_from % f"{events.event_type.CLICK.table} AS main " if not is_any: @@ -432,6 +432,52 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f event_where.append( _multiple_conditions(f"(main1.reason {op} %({e_k})s OR main1.name {op} %({e_k})s)", event.value, value_key=e_k)) + elif event_type == [schemas.PerformanceEventType.location_dom_complete, + schemas.PerformanceEventType.location_largest_contentful_paint_time]: + event_from = event_from % f"{events.event_type.LOCATION.table} AS main " + if not is_any: + event_where.append( + _multiple_conditions(f"main.{events.event_type.LOCATION.column} {op} %({e_k})s", + event.value, value_key=e_k)) + e_k += "_custom" + full_args = {**full_args, **_multiple_values(event.custom, value_key=e_k)} + event_where.append( + _multiple_conditions( + f"main.{performance_event.get_col(event_type)} {event.customOperator} %({e_k})s", + event.custom, value_key=e_k)) + elif event_type == schemas.PerformanceEventType.time_between_events: + event_from = event_from % f"{getattr(events.event_type, event.value[0].type).table} AS main INNER JOIN {getattr(events.event_type, event.value[1].type).table} AS main2 USING(session_id) " + if not isinstance(event.value[0].value, list): + event.value[0].value = [event.value[0].value] + if not isinstance(event.value[1].value, list): + event.value[1].value = [event.value[1].value] + event.value[0].value = helper.values_for_operator(value=event.value[0].value, op=event.operator) + event.value[1].value = helper.values_for_operator(value=event.value[1].value, op=event.operator) + e_k1 = e_k + "_e1" + e_k2 = e_k + "_e2" + full_args = {**full_args, + **_multiple_values(event.value[0].value, value_key=e_k1), + **_multiple_values(event.value[1].value, value_key=e_k2)} + s_op = __get_sql_operator(event.value[0].operator) + event_where += ["main2.timestamp >= %(startDate)s", "main2.timestamp <= %(endDate)s"] + if event_index > 0 and not or_events: + event_where.append("main2.session_id=event_0.session_id") + event_where.append( + _multiple_conditions( + f"main.{getattr(events.event_type, event.value[0].type).column} {s_op} %({e_k1})s", + event.value[0].value, value_key=e_k1)) + s_op = __get_sql_operator(event.value[1].operator) + event_where.append( + _multiple_conditions( + f"main2.{getattr(events.event_type, event.value[1].type).column} {s_op} %({e_k2})s", + event.value[1].value, value_key=e_k2)) + + e_k += "_custom" + full_args = {**full_args, **_multiple_values(event.custom, value_key=e_k)} + event_where.append( + _multiple_conditions(f"main2.timestamp - main.timestamp {event.customOperator} %({e_k})s", + event.custom, value_key=e_k)) + else: continue @@ -464,7 +510,7 @@ def search2_pg(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, f """) else: events_query_from.append(f"""\ - (SELECT main.session_id, MIN(timestamp) AS timestamp + (SELECT main.session_id, MIN(main.timestamp) AS timestamp FROM {event_from} WHERE {" AND ".join(event_where)} GROUP BY 1 diff --git a/api/schemas.py b/api/schemas.py index 9ad5c3295..47966c854 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, List, Literal, Union +from typing import Optional, List, Union from pydantic import BaseModel, Field, EmailStr, HttpUrl, root_validator @@ -312,10 +312,18 @@ class AlertColumn(str, Enum): errors__backend__count = "errors.backend.count" +class MathOperator(str, Enum): + _less = "<" + _greater = ">" + _less_eq = "<=" + _greater_eq = ">=" + + class _AlertQuerySchema(BaseModel): left: AlertColumn = Field(...) right: float = Field(...) - operator: Literal["<", ">", "<=", ">="] = Field(...) + # operator: Literal["<", ">", "<=", ">="] = Field(...) + operator: MathOperator = Field(...) class AlertSchema(BaseModel): @@ -360,6 +368,12 @@ class EventType(str, Enum): error_ios = "ERROR_IOS" +class PerformanceEventType(str, Enum): + location_dom_complete = "DOM_COMPLETE" + location_largest_contentful_paint_time = "LARGEST_CONTENTFUL_PAINT_TIME" + time_between_events = "TIME_BETWEEN_EVENTS" + + class FilterType(str, Enum): user_os = "USEROS" user_browser = "USERBROWSER" @@ -399,6 +413,7 @@ class SearchEventOperator(str, Enum): class PlatformType(str, Enum): mobile = "mobile" desktop = "desktop" + tablet = "tablet" class SearchEventOrder(str, Enum): @@ -422,14 +437,33 @@ class IssueType(str, Enum): js_exception = 'js_exception' -class _SessionSearchEventSchema(BaseModel): - custom: Optional[List[str]] = Field(None) +class _SessionSearchEventRaw(BaseModel): + custom: Optional[List[Union[str, int]]] = Field(None) + customOperator: Optional[MathOperator] = Field(None) key: Optional[str] = Field(None) - value: Union[Optional[str], Optional[List[str]]] = Field(...) - type: EventType = Field(...) + value: Union[str, List[str]] = Field(...) + type: Union[EventType, PerformanceEventType] = Field(...) operator: SearchEventOperator = Field(...) source: Optional[ErrorSource] = Field(default=ErrorSource.js_exception) + @root_validator + def check_card_number_omitted(cls, values): + if isinstance(values.get("type"), PerformanceEventType): + assert values.get("custom") is not None, "custom should not be None for PerformanceEventType" + assert values.get("customOperator") is not None \ + , "customOperator should not be None for PerformanceEventType" + if values["type"] == PerformanceEventType.time_between_events: + assert len(values.get("value", [])) == 2, \ + f"must provide 2 Events as value for {PerformanceEventType.time_between_events}" + assert isinstance(values["value"][0], _SessionSearchEventRaw) \ + and isinstance(values["value"][1], _SessionSearchEventRaw) \ + , f"event should be of type _SessionSearchEventRaw for {PerformanceEventType.time_between_events}" + return values + + +class _SessionSearchEventSchema(_SessionSearchEventRaw): + value: Union[List[_SessionSearchEventRaw], str, List[str]] = Field(...) + class _SessionSearchFilterSchema(BaseModel): custom: Optional[List[str]] = Field(None)