fix(chalice): fixed EE sessions search for mobile projects (#3010)

refactor(chalice): enhanced sessions search payload validation
This commit is contained in:
Kraiem Taha Yassine 2025-02-05 18:50:14 +01:00 committed by GitHub
parent 500d70aa67
commit afb08cfe6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1547 additions and 45 deletions

View file

@ -80,7 +80,7 @@ def __get_sort_key(key):
}.get(key, 'max_datetime')
def search(data: schemas.SearchErrorsSchema, project_id, user_id):
def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, user_id):
empty_response = {
'total': 0,
'errors': []
@ -107,7 +107,7 @@ def search(data: schemas.SearchErrorsSchema, project_id, user_id):
data.endTimestamp = TimeUTC.now(1)
if len(data.events) > 0 or len(data.filters) > 0:
print("-- searching for sessions before errors")
statuses = sessions_search.search_sessions(data=data, project_id=project_id, user_id=user_id, errors_only=True,
statuses = sessions_search.search_sessions(data=data, project=project, user_id=user_id, errors_only=True,
error_status=data.status)
if len(statuses) == 0:
return empty_response
@ -125,7 +125,7 @@ def search(data: schemas.SearchErrorsSchema, project_id, user_id):
params = {
"startDate": data.startTimestamp,
"endDate": data.endTimestamp,
"project_id": project_id,
"project_id": project.project_id,
"userId": user_id,
"step_size": step_size}
if data.status != schemas.ErrorStatus.ALL:
@ -207,7 +207,7 @@ def search(data: schemas.SearchErrorsSchema, project_id, user_id):
"""SELECT error_id
FROM public.errors
WHERE project_id = %(project_id)s AND error_id IN %(error_ids)s;""",
{"project_id": project_id, "error_ids": tuple([r["error_id"] for r in rows]),
{"project_id": project.project_id, "error_ids": tuple([r["error_id"] for r in rows]),
"user_id": user_id})
cur.execute(query=query)
statuses = helper.list_to_camel_case(cur.fetchall())

View file

@ -42,7 +42,7 @@ def __get_errors_list(project: schemas.ProjectContext, user_id, data: schemas.Ca
"total": 0,
"errors": []
}
return errors.search(data.series[0].filter, project_id=project.project_id, user_id=user_id)
return errors.search(data.series[0].filter, project=project, user_id=user_id)
def __get_sessions_list(project: schemas.ProjectContext, user_id, data: schemas.CardSchema):
@ -52,7 +52,7 @@ def __get_sessions_list(project: schemas.ProjectContext, user_id, data: schemas.
"total": 0,
"sessions": []
}
return sessions_search.search_sessions(data=data.series[0].filter, project_id=project.project_id, user_id=user_id)
return sessions_search.search_sessions(data=data.series[0].filter, project=project, user_id=user_id)
def __get_heat_map_chart(project: schemas.ProjectContext, user_id, data: schemas.CardHeatMap,
@ -168,18 +168,18 @@ def get_chart(project: schemas.ProjectContext, data: schemas.CardSchema, user_id
return supported.get(data.metric_type, not_supported)(project=project, data=data, user_id=user_id)
def get_sessions_by_card_id(project_id, user_id, metric_id, data: schemas.CardSessionsSchema):
if not card_exists(metric_id=metric_id, project_id=project_id, user_id=user_id):
def get_sessions_by_card_id(project: schemas.ProjectContext, user_id, metric_id, data: schemas.CardSessionsSchema):
if not card_exists(metric_id=metric_id, project_id=project.project_id, user_id=user_id):
return None
results = []
for s in data.series:
results.append({"seriesId": s.series_id, "seriesName": s.name,
**sessions_search.search_sessions(data=s.filter, project_id=project_id, user_id=user_id)})
**sessions_search.search_sessions(data=s.filter, project=project, user_id=user_id)})
return results
def get_sessions(project_id, user_id, data: schemas.CardSessionsSchema):
def get_sessions(project: schemas.ProjectContext, user_id, data: schemas.CardSessionsSchema):
results = []
if len(data.series) == 0:
return results
@ -189,7 +189,7 @@ def get_sessions(project_id, user_id, data: schemas.CardSessionsSchema):
s.filter = schemas.SessionsSearchPayloadSchema(**s.filter.model_dump(by_alias=True))
results.append({"seriesId": None, "seriesName": s.name,
**sessions_search.search_sessions(data=s.filter, project_id=project_id, user_id=user_id)})
**sessions_search.search_sessions(data=s.filter, project=project, user_id=user_id)})
return results

View file

@ -1,11 +1,13 @@
import ast
import logging
from typing import List, Union
import logging
from typing import List, Union
import schemas
from chalicelib.core import events, metadata, projects
from chalicelib.core import events, metadata
from chalicelib.core.metrics import metrics
from chalicelib.core.sessions import sessions_favorite, performance_event, sessions_legacy
from chalicelib.core.sessions import performance_event, sessions_legacy
from chalicelib.utils import pg_client, helper, metrics_helper, ch_client, exp_ch_helper
from chalicelib.utils import sql_helper as sh

View file

@ -40,16 +40,16 @@ COALESCE((SELECT TRUE
# This function executes the query and return result
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, errors_only=False,
error_status=schemas.ErrorStatus.ALL, count_only=False, issue=None, ids_only=False,
platform="web"):
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.ProjectContext,
user_id, errors_only=False, error_status=schemas.ErrorStatus.ALL,
count_only=False, issue=None, ids_only=False, platform="web"):
if data.bookmarked:
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project_id, user_id)
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project.project_id, user_id)
full_args, query_part = sessions_legacy.search_query_parts(data=data, error_status=error_status,
errors_only=errors_only,
favorite_only=data.bookmarked, issue=issue,
project_id=project_id,
project_id=project.project_id,
user_id=user_id, platform=platform)
if data.limit is not None and data.page is not None:
full_args["sessions_limit"] = data.limit
@ -86,7 +86,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_
else:
sort = 'start_ts'
meta_keys = metadata.get(project_id=project_id)
meta_keys = metadata.get(project_id=project.project_id)
main_query = cur.mogrify(f"""SELECT COUNT(*) AS count,
COALESCE(JSONB_AGG(users_sessions)
FILTER (WHERE rn>%(sessions_limit_s)s AND rn<=%(sessions_limit_e)s), '[]'::JSONB) AS sessions
@ -120,7 +120,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_
# sort += " " + data.order + "," + helper.key_to_snake_case(data.sort)
sort = helper.key_to_snake_case(data.sort)
meta_keys = metadata.get(project_id=project_id)
meta_keys = metadata.get(project_id=project.project_id)
main_query = cur.mogrify(f"""SELECT COUNT(full_sessions) AS count,
COALESCE(JSONB_AGG(full_sessions)
FILTER (WHERE rn>%(sessions_limit_s)s AND rn<=%(sessions_limit_e)s), '[]'::JSONB) AS sessions

View file

@ -0,0 +1,36 @@
import schemas
from fastapi import HTTPException, Depends
from or_dependencies import OR_context
def validate_contextual_payload(
item: schemas.SessionsSearchPayloadSchema,
context: schemas.CurrentContext = Depends(OR_context)
) -> schemas.SessionsSearchPayloadSchema:
if context.project.platform == "web":
for e in item.events:
if e.type in [schemas.EventType.CLICK_MOBILE,
schemas.EventType.INPUT_MOBILE,
schemas.EventType.VIEW_MOBILE,
schemas.EventType.CUSTOM_MOBILE,
schemas.EventType.REQUEST_MOBILE,
schemas.EventType.ERROR_MOBILE,
schemas.EventType.SWIPE_MOBILE]:
raise HTTPException(status_code=422,
detail=f"Mobile event '{e.type}' not supported for web project")
else:
for e in item.events:
if e.type in [schemas.EventType.CLICK,
schemas.EventType.INPUT,
schemas.EventType.LOCATION,
schemas.EventType.CUSTOM,
schemas.EventType.REQUEST,
schemas.EventType.REQUEST_DETAILS,
schemas.EventType.GRAPHQL,
schemas.EventType.STATE_ACTION,
schemas.EventType.ERROR,
schemas.EventType.TAG]:
raise HTTPException(status_code=422,
detail=f"Web event '{e.type}' not supported for mobile project")
return item

View file

@ -22,6 +22,7 @@ from chalicelib.utils.TimeUTC import TimeUTC
from or_dependencies import OR_context, OR_role
from routers.base import get_routers
from routers.subs import spot
from chalicelib.utils import contextual_validators
logger = logging.getLogger(__name__)
public_app, app, app_apikey = get_routers()
@ -252,17 +253,19 @@ def get_projects(context: schemas.CurrentContext = Depends(OR_context)):
@app.post('/{projectId}/sessions/search', tags=["sessions"])
def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema = Body(...),
def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
Depends(contextual_validators.validate_contextual_payload),
context: schemas.CurrentContext = Depends(OR_context)):
data = sessions_search.search_sessions(data=data, project_id=projectId, user_id=context.user_id,
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id,
platform=context.project.platform)
return {'data': data}
@app.post('/{projectId}/sessions/search/ids', tags=["sessions"])
def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema = Body(...),
def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
Depends(contextual_validators.validate_contextual_payload),
context: schemas.CurrentContext = Depends(OR_context)):
data = sessions_search.search_sessions(data=data, project_id=projectId, user_id=context.user_id, ids_only=True,
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id, ids_only=True,
platform=context.project.platform)
return {'data': data}

View file

@ -87,7 +87,7 @@ def try_card(projectId: int, data: schemas.CardSchema = Body(...),
@app.post('/{projectId}/cards/try/sessions', tags=["cards"])
def try_card_sessions(projectId: int, data: schemas.CardSessionsSchema = Body(...),
context: schemas.CurrentContext = Depends(OR_context)):
data = custom_metrics.get_sessions(project_id=projectId, user_id=context.user_id, data=data)
data = custom_metrics.get_sessions(project=context.project, user_id=context.user_id, data=data)
return {"data": data}
@ -130,7 +130,7 @@ def get_card(projectId: int, metric_id: Union[int, str], context: schemas.Curren
def get_card_sessions(projectId: int, metric_id: int,
data: schemas.CardSessionsSchema = Body(...),
context: schemas.CurrentContext = Depends(OR_context)):
data = custom_metrics.get_sessions_by_card_id(project_id=projectId, user_id=context.user_id, metric_id=metric_id,
data = custom_metrics.get_sessions_by_card_id(project=context.project, user_id=context.user_id, metric_id=metric_id,
data=data)
if data is None:
return {"errors": ["custom metric not found"]}

1
ee/api/.gitignore vendored
View file

@ -292,3 +292,4 @@ Pipfile.lock
/chalicelib/core/errors/errors.py
/chalicelib/core/errors/errors_ch.py
/chalicelib/core/errors/errors_details.py
/chalicelib/utils/contextual_validators.py

View file

@ -26,6 +26,7 @@ def _get_current_auth_context(request: Request, jwt_payload: dict) -> schemas.Cu
role=user["role"],
permissions=user["permissions"],
serviceAccount=user["serviceAccount"])
print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>VC")
return request.state.currentContext

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ import logging
import schemas
from chalicelib.core import metadata, projects
from chalicelib.core.sessions import sessions_favorite, sessions_legacy, sessions
from chalicelib.core.sessions import sessions_favorite, sessions_legacy, sessions, sessions_legacy_mobil
from chalicelib.utils import pg_client, helper, ch_client, exp_ch_helper
logger = logging.getLogger(__name__)
@ -58,16 +58,24 @@ SESSION_PROJECTION_COLS_CH_MAP = """\
# This function executes the query and return result
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, errors_only=False,
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.ProjectContext,
user_id, errors_only=False,
error_status=schemas.ErrorStatus.ALL, count_only=False, issue=None, ids_only=False,
platform="web"):
if data.bookmarked:
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project_id, user_id)
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=error_status,
errors_only=errors_only,
favorite_only=data.bookmarked, issue=issue,
project_id=project_id,
user_id=user_id, platform=platform)
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project.project_id, user_id)
if project.platform == "web":
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=error_status,
errors_only=errors_only,
favorite_only=data.bookmarked, issue=issue,
project_id=project.project_id,
user_id=user_id, platform=platform)
else:
full_args, query_part = sessions_legacy_mobil.search_query_parts_ch(data=data, error_status=error_status,
errors_only=errors_only,
favorite_only=data.bookmarked, issue=issue,
project_id=project.project_id,
user_id=user_id, platform=platform)
if data.sort == "startTs":
data.sort = "datetime"
if data.limit is not None and data.page is not None:
@ -106,7 +114,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_
else:
sort = 'start_ts'
meta_keys = metadata.get(project_id=project_id)
meta_keys = metadata.get(project_id=project.project_id)
meta_map = ",map(%s) AS 'metadata'" \
% ','.join([f"'{m['key']}',coalesce(metadata_{m['index']},'None')" for m in meta_keys])
main_query = cur.mogrify(f"""SELECT COUNT(*) AS count,
@ -141,7 +149,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_
# sort += " " + data.order + "," + helper.key_to_snake_case(data.sort)
sort = helper.key_to_snake_case(data.sort)
meta_keys = metadata.get(project_id=project_id)
meta_keys = metadata.get(project_id=project.project_id)
meta_map = ",'metadata',toString(map(%s))" \
% ','.join([f"'{m['key']}',coalesce(metadata_{m['index']},'None')" for m in meta_keys])
main_query = cur.format(query=f"""SELECT any(total) AS count,

View file

@ -112,3 +112,4 @@ rm -rf ./chalicelib/core/errors/modules
rm -rf ./chalicelib/core/errors/errors.py
rm -rf ./chalicelib/core/errors/errors_ch.py
rm -rf ./chalicelib/core/errors/errors_details.py
rm -rf ./chalicelib/utils/contextual_validators.py

View file

@ -7,17 +7,18 @@ from fastapi import HTTPException, status
from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response
import schemas
from chalicelib.core import scope
from chalicelib.core import assist, signup, feature_flags
from chalicelib.core import scope
from chalicelib.core import tenants, users, projects, license
from chalicelib.core import webhook
from chalicelib.core.collaborations.collaboration_slack import Slack
from chalicelib.core.errors import errors
from chalicelib.core.metrics import heatmaps
from chalicelib.core.sessions import sessions, sessions_notes, sessions_replay, sessions_favorite, sessions_assignments, \
sessions_viewed, unprocessed_sessions, sessions_search
from chalicelib.core import tenants, users, projects, license
from chalicelib.core import webhook
from chalicelib.core.collaborations.collaboration_slack import Slack
from chalicelib.utils import SAML2_helper, smtp
from chalicelib.utils import captcha
from chalicelib.utils import contextual_validators
from chalicelib.utils import helper
from chalicelib.utils.TimeUTC import TimeUTC
from or_dependencies import OR_context, OR_scope, OR_role
@ -266,18 +267,20 @@ def get_projects(context: schemas.CurrentContext = Depends(OR_context)):
@app.post('/{projectId}/sessions/search', tags=["sessions"],
dependencies=[OR_scope(Permissions.SESSION_REPLAY)])
def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema = Body(...),
def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
Depends(contextual_validators.validate_contextual_payload),
context: schemas.CurrentContext = Depends(OR_context)):
data = sessions_search.search_sessions(data=data, project_id=projectId, user_id=context.user_id,
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id,
platform=context.project.platform)
return {'data': data}
@app.post('/{projectId}/sessions/search/ids', tags=["sessions"],
dependencies=[OR_scope(Permissions.SESSION_REPLAY)])
def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema = Body(...),
def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
Depends(contextual_validators.validate_contextual_payload),
context: schemas.CurrentContext = Depends(OR_context)):
data = sessions_search.search_sessions(data=data, project_id=projectId, user_id=context.user_id, ids_only=True,
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id, ids_only=True,
platform=context.project.platform)
return {'data': data}