From fb9005d20dca20ad15b51e243d44b5ad425bfd4e Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Wed, 4 Jun 2025 16:45:45 +0200 Subject: [PATCH] feat(chalice): search sessions by global properties feat(chalice): search sessions by negative global properties --- api/chalicelib/core/sessions/sessions_ch.py | 37 ++++++++++++++++++++- api/schemas/schemas.py | 9 ++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/api/chalicelib/core/sessions/sessions_ch.py b/api/chalicelib/core/sessions/sessions_ch.py index 69e0d5111..b24cfc240 100644 --- a/api/chalicelib/core/sessions/sessions_ch.py +++ b/api/chalicelib/core/sessions/sessions_ch.py @@ -410,7 +410,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ], "operator": "is" })) - + global_properties = [] + global_properties_negative = [] if len(data.filters) > 0: meta_keys = None # to reduce include a sub-query of sessions inside events query, in order to reduce the selected data @@ -429,6 +430,23 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu is_not = False if sh.is_negation_operator(f.operator): is_not = True + if not f.is_predefined: + cast = get_col_cast(data_type=f.data_type, value=f.value) + if is_any: + global_properties.append(f'isNotNull(e.properties.`{f.type}`)') + else: + if is_not: + op = sh.reverse_sql_operator(op) + global_properties_negative.append(sh.multi_conditions(get_sub_condition( + col_name=f"accurateCastOrNull(e.properties.`{f.type}`,'{cast}')", + val_name=f_k, operator=op), f.value, is_not=False, value_key=f_k)) + else: + global_properties.append(sh.multi_conditions(get_sub_condition( + col_name=f"accurateCastOrNull(e.properties.`{f.type}`,'{cast}')", + val_name=f_k, operator=f.operator), f.value, is_not=False, value_key=f_k)) + + continue + if filter_type == schemas.FilterType.USER_BROWSER: if is_any: extra_constraints.append('isNotNull(s.user_browser)') @@ -658,6 +676,11 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu events_conditions_where.append(f"""main.session_id IN (SELECT s.session_id FROM {MAIN_SESSIONS_TABLE} AS s WHERE {" AND ".join(extra_constraints)})""") + + if len(global_properties) > 0: + global_properties += ["e.project_id=%(project_id)s", + "e.created_at >= toDateTime(%(startDate)s/1000)", + "e.created_at <= toDateTime(%(endDate)s/1000)"] # --------------------------------------------------------------------------- events_extra_join = "" if len(data.events) > 0: @@ -1612,6 +1635,18 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ORDER BY _timestamp DESC) AS s ON(s.session_id=f.session_id)""" else: deduplication_keys = ["session_id"] + extra_deduplication + if len(global_properties) > 0: + extra_join += f""" INNER JOIN (SELECT DISTINCT session_id + FROM {MAIN_EVENTS_TABLE} AS e + WHERE {" AND ".join(global_properties)}) AS global_filters USING(session_id)""" + if len(global_properties_negative) > 0: + extra_join += f""" LEFT JOIN (SELECT DISTINCT session_id + FROM {MAIN_EVENTS_TABLE} AS e + WHERE project_id=%(project_id)s + AND created_at >= toDateTime(%(startDate)s/1000) + AND created_at <= toDateTime(%(endDate)s/1000) + AND ({" OR ".join(global_properties_negative)})) AS negative_global_filters USING(session_id)""" + extra_constraints.append("isNull(negative_global_filters.session_id)") extra_join = f"""(SELECT * FROM {MAIN_SESSIONS_TABLE} AS s {extra_join} {extra_event} WHERE {" AND ".join(extra_constraints)} diff --git a/api/schemas/schemas.py b/api/schemas/schemas.py index e9a0647bc..a0592a923 100644 --- a/api/schemas/schemas.py +++ b/api/schemas/schemas.py @@ -682,13 +682,20 @@ class SessionSearchEventSchema(BaseModel): class SessionSearchFilterSchema(BaseModel): is_event: Literal[False] = False value: List[Union[IssueType, PlatformType, int, str]] = Field(default_factory=list) - type: FilterType = Field(...) + type: Union[FilterType, str] = Field(...) operator: Union[SearchEventOperator, MathOperator] = Field(...) source: Optional[Union[ErrorSource, str]] = Field(default=None) + # used for global-properties + data_type: Optional[PropertyType] = Field(default=PropertyType.STRING.value) _remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values) _single_to_list_values = field_validator('value', mode='before')(single_to_list) + @computed_field + @property + def is_predefined(self) -> bool: + return FilterType.has_value(self.type) + @model_validator(mode="before") @classmethod def _transform_data(cls, values):