* fix(chalice): fixed Math-operators validation
refactor(chalice): search for sessions that have events for heatmaps

* refactor(chalice): search for sessions that have at least 1 location event for heatmaps

* fix(chalice): fixed Math-operators validation
refactor(chalice): search for sessions that have events for heatmaps

* refactor(chalice): search for sessions that have at least 1 location event for heatmaps

* feat(chalice): get top 10 values for autocomplete CH

* feat(chalice): autocomplete return top 10 with stats

* fix(chalice): fixed autocomplete top 10 meta-filters

* fix(chalice): fixed predefined metrics
refactor(chalice): refactored schemas
refactor(chalice): refactored routers
refactor(chalice): refactored unprocessed sessions
This commit is contained in:
Kraiem Taha Yassine 2024-08-13 11:37:58 +02:00 committed by GitHub
parent 6cfecb53a3
commit b9fc397e72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 161 additions and 146 deletions

View file

@ -9,6 +9,7 @@ from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from psycopg import AsyncConnection from psycopg import AsyncConnection
from psycopg.rows import dict_row
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse
from chalicelib.utils import helper from chalicelib.utils import helper
@ -20,7 +21,7 @@ from routers.subs import insights, metrics, v1_api, health, usability_tests, spo
loglevel = config("LOGLEVEL", default=logging.WARNING) loglevel = config("LOGLEVEL", default=logging.WARNING)
print(f">Loglevel set to: {loglevel}") print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel) logging.basicConfig(level=loglevel)
from psycopg.rows import dict_row
class ORPYAsyncConnection(AsyncConnection): class ORPYAsyncConnection(AsyncConnection):

View file

@ -45,9 +45,8 @@ class JWTAuth(HTTPBearer):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid authentication scheme.") detail="Invalid authentication scheme.")
jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials) jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials)
auth_exists = jwt_payload is not None \ auth_exists = jwt_payload is not None and users.auth_exists(user_id=jwt_payload.get("userId", -1),
and users.auth_exists(user_id=jwt_payload.get("userId", -1), jwt_iat=jwt_payload.get("iat", 100))
jwt_iat=jwt_payload.get("iat", 100))
if jwt_payload is None \ if jwt_payload is None \
or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \ or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \
or not auth_exists: or not auth_exists:

View file

@ -1,9 +1,6 @@
import logging import logging
from typing import Union from typing import Union
import logging
from typing import Union
import schemas import schemas
from chalicelib.core import metrics from chalicelib.core import metrics
@ -30,7 +27,7 @@ def get_metric(key: Union[schemas.MetricOfWebVitals, schemas.MetricOfErrors, \
schemas.MetricOfWebVitals.COUNT_REQUESTS: metrics.get_top_metrics_count_requests, schemas.MetricOfWebVitals.COUNT_REQUESTS: metrics.get_top_metrics_count_requests,
schemas.MetricOfWebVitals.AVG_TIME_TO_RENDER: metrics.get_time_to_render, schemas.MetricOfWebVitals.AVG_TIME_TO_RENDER: metrics.get_time_to_render,
schemas.MetricOfWebVitals.AVG_USED_JS_HEAP_SIZE: metrics.get_memory_consumption, schemas.MetricOfWebVitals.AVG_USED_JS_HEAP_SIZE: metrics.get_memory_consumption,
schemas.MetricOfWebVitals.avg_cpu: metrics.get_avg_cpu, schemas.MetricOfWebVitals.AVG_CPU: metrics.get_avg_cpu,
schemas.MetricOfWebVitals.AVG_FPS: metrics.get_avg_fps, schemas.MetricOfWebVitals.AVG_FPS: metrics.get_avg_fps,
schemas.MetricOfErrors.IMPACTED_SESSIONS_BY_JS_ERRORS: metrics.get_impacted_sessions_by_js_errors, schemas.MetricOfErrors.IMPACTED_SESSIONS_BY_JS_ERRORS: metrics.get_impacted_sessions_by_js_errors,
schemas.MetricOfErrors.DOMAINS_ERRORS_4XX: metrics.get_domains_errors_4xx, schemas.MetricOfErrors.DOMAINS_ERRORS_4XX: metrics.get_domains_errors_4xx,

View file

@ -0,0 +1,18 @@
import logging
from chalicelib.core import sessions, assist
logger = logging.getLogger(__name__)
def check_exists(project_id, session_id, not_found_response) -> (int | None, dict | None):
if session_id is None or not session_id.isnumeric():
return session_id, not_found_response
else:
session_id = int(session_id)
if not sessions.session_exists(project_id=project_id, session_id=session_id):
logger.warning(f"{project_id}/{session_id} not found in DB.")
if not assist.session_exists(project_id=project_id, session_id=session_id):
logger.warning(f"{project_id}/{session_id} not found in Assist.")
return session_id, not_found_response
return session_id, None

View file

@ -28,7 +28,7 @@ class TimeUTC:
.astimezone(UTC_ZI) .astimezone(UTC_ZI)
@staticmethod @staticmethod
def now(delta_days=0, delta_minutes=0, delta_seconds=0): def now(delta_days: int = 0, delta_minutes: int = 0, delta_seconds: int = 0) -> int:
return int(TimeUTC.__now(delta_days=delta_days, delta_minutes=delta_minutes, return int(TimeUTC.__now(delta_days=delta_days, delta_minutes=delta_minutes,
delta_seconds=delta_seconds).timestamp() * 1000) delta_seconds=delta_seconds).timestamp() * 1000)

View file

@ -4,13 +4,11 @@ from decouple import config
from fastapi import Depends, Body, BackgroundTasks from fastapi import Depends, Body, BackgroundTasks
import schemas import schemas
from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assignments, projects, \ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assignments, projects, alerts, issues, \
alerts, issues, integrations_manager, metadata, \ integrations_manager, metadata, log_tool_elasticsearch, log_tool_datadog, log_tool_stackdriver, reset_password, \
log_tool_elasticsearch, log_tool_datadog, \ log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, sessions, log_tool_newrelic, announcements, \
log_tool_stackdriver, reset_password, log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, sessions, \ log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, assist, mobile, tenants, boarding, \
log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \ notifications, webhook, users, custom_metrics, saved_search, integrations_global, tags, autocomplete
assist, mobile, tenants, boarding, notifications, webhook, users, \
custom_metrics, saved_search, integrations_global, tags, autocomplete
from chalicelib.core.collaboration_msteams import MSTeams from chalicelib.core.collaboration_msteams import MSTeams
from chalicelib.core.collaboration_slack import Slack from chalicelib.core.collaboration_slack import Slack
from or_dependencies import OR_context, OR_role from or_dependencies import OR_context, OR_role
@ -556,7 +554,7 @@ def get_all_alerts(projectId: int, context: schemas.CurrentContext = Depends(OR_
@app.get('/{projectId}/alerts/triggers', tags=["alerts", "customMetrics"]) @app.get('/{projectId}/alerts/triggers', tags=["alerts", "customMetrics"])
def get_alerts_triggers(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): def get_alerts_triggers(projectId: int, context: schemas.CurrentContext = Depends(OR_context)):
return {"data": alerts.get_predefined_values() \ return {"data": alerts.get_predefined_values()
+ custom_metrics.get_series_for_alert(project_id=projectId, user_id=context.user_id)} + custom_metrics.get_series_for_alert(project_id=projectId, user_id=context.user_id)}
@ -839,8 +837,8 @@ def edit_msteams_integration(webhookId: int, data: schemas.EditCollaborationSche
if old["endpoint"] != data.url.unicode_string(): if old["endpoint"] != data.url.unicode_string():
if not MSTeams.say_hello(data.url.unicode_string()): if not MSTeams.say_hello(data.url.unicode_string()):
return { return {
"errors": [ "errors": ["We couldn't send you a test message on your Microsoft Teams channel. "
"We couldn't send you a test message on your Microsoft Teams channel. Please verify your webhook url."] "Please verify your webhook url."]
} }
return {"data": webhook.update(tenant_id=context.tenant_id, webhook_id=webhookId, return {"data": webhook.update(tenant_id=context.tenant_id, webhook_id=webhookId,
changes={"name": data.name, "endpoint": data.url.unicode_string()})} changes={"name": data.name, "endpoint": data.url.unicode_string()})}

View file

@ -1,3 +1,4 @@
import logging
from typing import Optional, Union from typing import Optional, Union
from decouple import config from decouple import config
@ -6,12 +7,13 @@ from fastapi import HTTPException, status
from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response
import schemas import schemas
from chalicelib.core import scope
from chalicelib.core import sessions, errors, errors_viewed, errors_favorite, sessions_assignments, heatmaps, \ from chalicelib.core import sessions, errors, errors_viewed, errors_favorite, sessions_assignments, heatmaps, \
sessions_favorite, assist, sessions_notes, sessions_replay, signup, feature_flags sessions_favorite, assist, sessions_notes, sessions_replay, signup, feature_flags
from chalicelib.core import sessions_viewed from chalicelib.core import sessions_viewed
from chalicelib.core import tenants, users, projects, license from chalicelib.core import tenants, users, projects, license
from chalicelib.core import unprocessed_sessions
from chalicelib.core import webhook from chalicelib.core import webhook
from chalicelib.core import scope
from chalicelib.core.collaboration_slack import Slack from chalicelib.core.collaboration_slack import Slack
from chalicelib.utils import captcha, smtp from chalicelib.utils import captcha, smtp
from chalicelib.utils import helper from chalicelib.utils import helper
@ -19,6 +21,7 @@ from chalicelib.utils.TimeUTC import TimeUTC
from or_dependencies import OR_context, OR_role from or_dependencies import OR_context, OR_role
from routers.base import get_routers from routers.base import get_routers
logger = logging.getLogger(__name__)
public_app, app, app_apikey = get_routers() public_app, app, app_apikey = get_routers()
COOKIE_PATH = "/api/refresh" COOKIE_PATH = "/api/refresh"
@ -249,7 +252,7 @@ def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema
@app.get('/{projectId}/sessions/{sessionId}/first-mob', tags=["sessions", "replay"]) @app.get('/{projectId}/sessions/{sessionId}/first-mob', tags=["sessions", "replay"])
def get_first_mob_file(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks, def get_first_mob_file(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
if not sessionId.isnumeric(): if not sessionId.isnumeric():
return {"errors": ["session not found"]} return {"errors": ["session not found"]}
@ -368,16 +371,10 @@ def get_live_session(projectId: int, sessionId: str, background_tasks: Backgroun
def get_live_session_replay_file(projectId: int, sessionId: Union[int, str], def get_live_session_replay_file(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
not_found = {"errors": ["Replay file not found"]} not_found = {"errors": ["Replay file not found"]}
if not sessionId.isnumeric(): sessionId, err = unprocessed_sessions.check_exists(project_id=projectId, session_id=sessionId,
return not_found not_found_response=not_found)
else: if err is not None:
sessionId = int(sessionId) return err
if not sessions.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in DB.")
if not assist.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in Assist.")
return not_found
path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId) path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId)
if path is None: if path is None:
return not_found return not_found
@ -389,19 +386,13 @@ def get_live_session_replay_file(projectId: int, sessionId: Union[int, str],
def get_live_session_devtools_file(projectId: int, sessionId: Union[int, str], def get_live_session_devtools_file(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
not_found = {"errors": ["Devtools file not found"]} not_found = {"errors": ["Devtools file not found"]}
if not sessionId.isnumeric(): sessionId, err = unprocessed_sessions.check_exists(project_id=projectId, session_id=sessionId,
return not_found not_found_response=not_found)
else: if err is not None:
sessionId = int(sessionId) return err
if not sessions.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in DB.")
if not assist.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in Assist.")
return not_found
path = assist.get_raw_devtools_by_id(project_id=projectId, session_id=sessionId) path = assist.get_raw_devtools_by_id(project_id=projectId, session_id=sessionId)
if path is None: if path is None:
return {"errors": ["Devtools file not found"]} return not_found
return FileResponse(path=path, media_type="application/octet-stream") return FileResponse(path=path, media_type="application/octet-stream")

View file

@ -1,2 +1,2 @@
from .schemas import * from .schemas import *
from . import overrides as _overrides from . import overrides as _overrides

View file

@ -34,6 +34,6 @@ T = TypeVar('T')
class ORUnion: class ORUnion:
def __new__(self, union_types: Union[AnyType], discriminator: str) -> T: def __new__(cls, union_types: Union[AnyType], discriminator: str) -> T:
return lambda **args: TypeAdapter(Annotated[union_types, Field(discriminator=discriminator)]) \ return lambda **args: TypeAdapter(Annotated[union_types, Field(discriminator=discriminator)]) \
.validate_python(args) .validate_python(args)

View file

@ -1,14 +1,14 @@
from typing import Annotated, Any from typing import Annotated, Any
from typing import Optional, List, Union, Literal from typing import Optional, List, Union, Literal
from pydantic import Field, EmailStr, HttpUrl, SecretStr, AnyHttpUrl, validator from pydantic import Field, EmailStr, HttpUrl, SecretStr, AnyHttpUrl
from pydantic import field_validator, model_validator, computed_field from pydantic import field_validator, model_validator, computed_field
from pydantic.functional_validators import BeforeValidator
from chalicelib.utils.TimeUTC import TimeUTC from chalicelib.utils.TimeUTC import TimeUTC
from .overrides import BaseModel, Enum, ORUnion from .overrides import BaseModel, Enum, ORUnion
from .transformers_validators import transform_email, remove_whitespace, remove_duplicate_values, single_to_list, \ from .transformers_validators import transform_email, remove_whitespace, remove_duplicate_values, single_to_list, \
force_is_event, NAME_PATTERN, int_to_string force_is_event, NAME_PATTERN, int_to_string
from pydantic.functional_validators import BeforeValidator
def transform_old_filter_type(cls, values): def transform_old_filter_type(cls, values):
@ -162,7 +162,7 @@ class _TimedSchema(BaseModel):
endTimestamp: int = Field(default=None) endTimestamp: int = Field(default=None)
@model_validator(mode='before') @model_validator(mode='before')
def transform_time(cls, values): def transform_time(self, values):
if values.get("startTimestamp") is None and values.get("startDate") is not None: if values.get("startTimestamp") is None and values.get("startDate") is not None:
values["startTimestamp"] = values["startDate"] values["startTimestamp"] = values["startDate"]
if values.get("endTimestamp") is None and values.get("endDate") is not None: if values.get("endTimestamp") is None and values.get("endDate") is not None:
@ -170,7 +170,7 @@ class _TimedSchema(BaseModel):
return values return values
@model_validator(mode='after') @model_validator(mode='after')
def __time_validator(cls, values): def __time_validator(self, values):
if values.startTimestamp is not None: if values.startTimestamp is not None:
assert 0 <= values.startTimestamp, "startTimestamp must be greater or equal to 0" assert 0 <= values.startTimestamp, "startTimestamp must be greater or equal to 0"
if values.endTimestamp is not None: if values.endTimestamp is not None:
@ -435,7 +435,7 @@ class AlertSchema(BaseModel):
series_id: Optional[int] = Field(default=None, doc_hidden=True) series_id: Optional[int] = Field(default=None, doc_hidden=True)
@model_validator(mode="after") @model_validator(mode="after")
def transform_alert(cls, values): def transform_alert(self, values):
values.series_id = None values.series_id = None
if isinstance(values.query.left, int): if isinstance(values.query.left, int):
values.series_id = values.query.left values.series_id = values.query.left
@ -626,7 +626,7 @@ class SessionSearchEventSchema2(BaseModel):
_transform = model_validator(mode='before')(transform_old_filter_type) _transform = model_validator(mode='before')(transform_old_filter_type)
@model_validator(mode='after') @model_validator(mode='after')
def event_validator(cls, values): def event_validator(self, values):
if isinstance(values.type, PerformanceEventType): if isinstance(values.type, PerformanceEventType):
if values.type == PerformanceEventType.FETCH_FAILED: if values.type == PerformanceEventType.FETCH_FAILED:
return values return values
@ -666,7 +666,7 @@ class SessionSearchFilterSchema(BaseModel):
_single_to_list_values = field_validator('value', mode='before')(single_to_list) _single_to_list_values = field_validator('value', mode='before')(single_to_list)
@model_validator(mode='before') @model_validator(mode='before')
def _transform_data(cls, values): def _transform_data(self, values):
if values.get("source") is not None: if values.get("source") is not None:
if isinstance(values["source"], list): if isinstance(values["source"], list):
if len(values["source"]) == 0: if len(values["source"]) == 0:
@ -678,20 +678,20 @@ class SessionSearchFilterSchema(BaseModel):
return values return values
@model_validator(mode='after') @model_validator(mode='after')
def filter_validator(cls, values): def filter_validator(self, values):
if values.type == FilterType.METADATA: if values.type == FilterType.METADATA:
assert values.source is not None and len(values.source) > 0, \ assert values.source is not None and len(values.source) > 0, \
"must specify a valid 'source' for metadata filter" "must specify a valid 'source' for metadata filter"
elif values.type == FilterType.ISSUE: elif values.type == FilterType.ISSUE:
for v in values.value: for i, v in enumerate(values.value):
if IssueType.has_value(v): if IssueType.has_value(v):
v = IssueType(v) values.value[i] = IssueType(v)
else: else:
raise ValueError(f"value should be of type IssueType for {values.type} filter") raise ValueError(f"value should be of type IssueType for {values.type} filter")
elif values.type == FilterType.PLATFORM: elif values.type == FilterType.PLATFORM:
for v in values.value: for i, v in enumerate(values.value):
if PlatformType.has_value(v): if PlatformType.has_value(v):
v = PlatformType(v) values.value[i] = PlatformType(v)
else: else:
raise ValueError(f"value should be of type PlatformType for {values.type} filter") raise ValueError(f"value should be of type PlatformType for {values.type} filter")
elif values.type == FilterType.EVENTS_COUNT: elif values.type == FilterType.EVENTS_COUNT:
@ -730,8 +730,8 @@ def add_missing_is_event(values: dict):
# this type is created to allow mixing events&filters and specifying a discriminator # this type is created to allow mixing events&filters and specifying a discriminator
GroupedFilterType = Annotated[Union[SessionSearchFilterSchema, SessionSearchEventSchema2], \ GroupedFilterType = Annotated[Union[SessionSearchFilterSchema, SessionSearchEventSchema2],
Field(discriminator='is_event'), BeforeValidator(add_missing_is_event)] Field(discriminator='is_event'), BeforeValidator(add_missing_is_event)]
class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema): class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
@ -744,7 +744,7 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
bookmarked: bool = Field(default=False) bookmarked: bool = Field(default=False)
@model_validator(mode="before") @model_validator(mode="before")
def transform_order(cls, values): def transform_order(self, values):
if values.get("sort") is None: if values.get("sort") is None:
values["sort"] = "startTs" values["sort"] = "startTs"
@ -755,7 +755,7 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
return values return values
@model_validator(mode="before") @model_validator(mode="before")
def add_missing_attributes(cls, values): def add_missing_attributes(self, values):
# in case isEvent is wrong: # in case isEvent is wrong:
for f in values.get("filters") or []: for f in values.get("filters") or []:
if EventType.has_value(f["type"]) and not f.get("isEvent"): if EventType.has_value(f["type"]) and not f.get("isEvent"):
@ -770,7 +770,7 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
return values return values
@model_validator(mode="before") @model_validator(mode="before")
def remove_wrong_filter_values(cls, values): def remove_wrong_filter_values(self, values):
for f in values.get("filters", []): for f in values.get("filters", []):
vals = [] vals = []
for v in f.get("value", []): for v in f.get("value", []):
@ -780,7 +780,7 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def split_filters_events(cls, values): def split_filters_events(self, values):
n_filters = [] n_filters = []
n_events = [] n_events = []
for v in values.filters: for v in values.filters:
@ -793,7 +793,7 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
return values return values
@field_validator("filters", mode="after") @field_validator("filters", mode="after")
def merge_identical_filters(cls, values): def merge_identical_filters(self, values):
# ignore 'issue' type as it could be used for step-filters and tab-filters at the same time # ignore 'issue' type as it could be used for step-filters and tab-filters at the same time
i = 0 i = 0
while i < len(values): while i < len(values):
@ -853,7 +853,7 @@ class PathAnalysisSubFilterSchema(BaseModel):
_remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values) _remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values)
@model_validator(mode="before") @model_validator(mode="before")
def __force_is_event(cls, values): def __force_is_event(self, values):
values["isEvent"] = True values["isEvent"] = True
return values return values
@ -879,8 +879,8 @@ class _ProductAnalyticsEventFilter(BaseModel):
# this type is created to allow mixing events&filters and specifying a discriminator for PathAnalysis series filter # this type is created to allow mixing events&filters and specifying a discriminator for PathAnalysis series filter
ProductAnalyticsFilter = Annotated[Union[_ProductAnalyticsFilter, _ProductAnalyticsEventFilter], \ ProductAnalyticsFilter = Annotated[Union[_ProductAnalyticsFilter, _ProductAnalyticsEventFilter],
Field(discriminator='is_event')] Field(discriminator='is_event')]
class PathAnalysisSchema(_TimedSchema, _PaginatedSchema): class PathAnalysisSchema(_TimedSchema, _PaginatedSchema):
@ -1033,7 +1033,7 @@ class MetricOfPathAnalysis(str, Enum):
# class CardSessionsSchema(SessionsSearchPayloadSchema): # class CardSessionsSchema(SessionsSearchPayloadSchema):
class CardSessionsSchema(_TimedSchema, _PaginatedSchema): class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
startTimestamp: int = Field(default=TimeUTC.now(-7)) startTimestamp: int = Field(default=TimeUTC.now(-7))
endTimestamp: int = Field(defautl=TimeUTC.now()) endTimestamp: int = Field(default=TimeUTC.now())
density: int = Field(default=7, ge=1, le=200) density: int = Field(default=7, ge=1, le=200)
series: List[CardSeriesSchema] = Field(default=[]) series: List[CardSeriesSchema] = Field(default=[])
@ -1047,7 +1047,7 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
(force_is_event(events_enum=[EventType, PerformanceEventType])) (force_is_event(events_enum=[EventType, PerformanceEventType]))
@model_validator(mode="before") @model_validator(mode="before")
def remove_wrong_filter_values(cls, values): def remove_wrong_filter_values(self, values):
for f in values.get("filters", []): for f in values.get("filters", []):
vals = [] vals = []
for v in f.get("value", []): for v in f.get("value", []):
@ -1057,7 +1057,7 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
return values return values
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
if values.get("startTimestamp") is None: if values.get("startTimestamp") is None:
values["startTimestamp"] = TimeUTC.now(-7) values["startTimestamp"] = TimeUTC.now(-7)
@ -1067,7 +1067,7 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __enforce_default_after(cls, values): def __enforce_default_after(self, values):
for s in values.series: for s in values.series:
if s.filter is not None: if s.filter is not None:
s.filter.limit = values.limit s.filter.limit = values.limit
@ -1078,7 +1078,7 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __merge_out_filters_with_series(cls, values): def __merge_out_filters_with_series(self, values):
if len(values.filters) > 0: if len(values.filters) > 0:
for f in values.filters: for f in values.filters:
for s in values.series: for s in values.series:
@ -1140,12 +1140,12 @@ class CardTimeSeries(__CardSchema):
view_type: MetricTimeseriesViewType view_type: MetricTimeseriesViewType
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["metricValue"] = [] values["metricValue"] = []
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfTimeseries(values.metric_of) values.metric_of = MetricOfTimeseries(values.metric_of)
return values return values
@ -1157,18 +1157,18 @@ class CardTable(__CardSchema):
metric_format: MetricExtendedFormatType = Field(default=MetricExtendedFormatType.SESSION_COUNT) metric_format: MetricExtendedFormatType = Field(default=MetricExtendedFormatType.SESSION_COUNT)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
if values.get("metricOf") is not None and values.get("metricOf") != MetricOfTable.ISSUES: if values.get("metricOf") is not None and values.get("metricOf") != MetricOfTable.ISSUES:
values["metricValue"] = [] values["metricValue"] = []
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfTable(values.metric_of) values.metric_of = MetricOfTable(values.metric_of)
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __validator(cls, values): def __validator(self, values):
if values.metric_of not in (MetricOfTable.ISSUES, MetricOfTable.USER_BROWSER, if values.metric_of not in (MetricOfTable.ISSUES, MetricOfTable.USER_BROWSER,
MetricOfTable.USER_DEVICE, MetricOfTable.USER_COUNTRY, MetricOfTable.USER_DEVICE, MetricOfTable.USER_COUNTRY,
MetricOfTable.VISITED_URL): MetricOfTable.VISITED_URL):
@ -1183,7 +1183,7 @@ class CardFunnel(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
if values.get("metricOf") and not MetricOfFunnels.has_value(values["metricOf"]): if values.get("metricOf") and not MetricOfFunnels.has_value(values["metricOf"]):
values["metricOf"] = MetricOfFunnels.SESSION_COUNT values["metricOf"] = MetricOfFunnels.SESSION_COUNT
values["viewType"] = MetricOtherViewType.OTHER_CHART values["viewType"] = MetricOtherViewType.OTHER_CHART
@ -1192,7 +1192,7 @@ class CardFunnel(__CardSchema):
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfTimeseries(values.metric_of) values.metric_of = MetricOfTimeseries(values.metric_of)
return values return values
@ -1203,12 +1203,12 @@ class CardErrors(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["series"] = [] values["series"] = []
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfErrors(values.metric_of) values.metric_of = MetricOfErrors(values.metric_of)
return values return values
@ -1219,12 +1219,12 @@ class CardPerformance(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["series"] = [] values["series"] = []
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfPerformance(values.metric_of) values.metric_of = MetricOfPerformance(values.metric_of)
return values return values
@ -1235,12 +1235,12 @@ class CardResources(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["series"] = [] values["series"] = []
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfResources(values.metric_of) values.metric_of = MetricOfResources(values.metric_of)
return values return values
@ -1251,12 +1251,12 @@ class CardWebVital(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["series"] = [] values["series"] = []
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfWebVitals(values.metric_of) values.metric_of = MetricOfWebVitals(values.metric_of)
return values return values
@ -1267,11 +1267,11 @@ class CardHeatMap(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfHeatMap(values.metric_of) values.metric_of = MetricOfHeatMap(values.metric_of)
return values return values
@ -1286,17 +1286,17 @@ class CardInsights(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["view_type"] = MetricOtherViewType.LIST_CHART values["view_type"] = MetricOtherViewType.LIST_CHART
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __transform(cls, values): def __transform(self, values):
values.metric_of = MetricOfInsights(values.metric_of) values.metric_of = MetricOfInsights(values.metric_of)
return values return values
@model_validator(mode='after') @model_validator(mode='after')
def restrictions(cls, values): def restrictions(self, values):
raise ValueError(f"metricType:{MetricType.INSIGHTS} not supported yet.") raise ValueError(f"metricType:{MetricType.INSIGHTS} not supported yet.")
@ -1306,7 +1306,7 @@ class CardPathAnalysisSeriesSchema(CardSeriesSchema):
density: int = Field(default=4, ge=2, le=10) density: int = Field(default=4, ge=2, le=10)
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
if values.get("filter") is None and values.get("startTimestamp") and values.get("endTimestamp"): if values.get("filter") is None and values.get("startTimestamp") and values.get("endTimestamp"):
values["filter"] = PathAnalysisSchema(startTimestamp=values["startTimestamp"], values["filter"] = PathAnalysisSchema(startTimestamp=values["startTimestamp"],
endTimestamp=values["endTimestamp"], endTimestamp=values["endTimestamp"],
@ -1328,14 +1328,14 @@ class CardPathAnalysis(__CardSchema):
series: List[CardPathAnalysisSeriesSchema] = Field(default=[]) series: List[CardPathAnalysisSeriesSchema] = Field(default=[])
@model_validator(mode="before") @model_validator(mode="before")
def __enforce_default(cls, values): def __enforce_default(self, values):
values["viewType"] = MetricOtherViewType.OTHER_CHART.value values["viewType"] = MetricOtherViewType.OTHER_CHART.value
if values.get("series") is not None and len(values["series"]) > 0: if values.get("series") is not None and len(values["series"]) > 0:
values["series"] = [values["series"][0]] values["series"] = [values["series"][0]]
return values return values
@model_validator(mode="after") @model_validator(mode="after")
def __clean_start_point_and_enforce_metric_value(cls, values): def __clean_start_point_and_enforce_metric_value(self, values):
start_point = [] start_point = []
for s in values.start_point: for s in values.start_point:
if len(s.value) == 0: if len(s.value) == 0:
@ -1349,7 +1349,7 @@ class CardPathAnalysis(__CardSchema):
return values return values
@model_validator(mode='after') @model_validator(mode='after')
def __validator(cls, values): def __validator(self, values):
s_e_values = {} s_e_values = {}
exclude_values = {} exclude_values = {}
for f in values.start_point: for f in values.start_point:
@ -1359,7 +1359,8 @@ class CardPathAnalysis(__CardSchema):
exclude_values[f.type] = exclude_values.get(f.type, []) + f.value exclude_values[f.type] = exclude_values.get(f.type, []) + f.value
assert len( assert len(
values.start_point) <= 1, f"Only 1 startPoint with multiple values OR 1 endPoint with multiple values is allowed" values.start_point) <= 1, \
f"Only 1 startPoint with multiple values OR 1 endPoint with multiple values is allowed"
for t in exclude_values: for t in exclude_values:
for v in t: for v in t:
assert v not in s_e_values.get(t, []), f"startPoint and endPoint cannot be excluded, value: {v}" assert v not in s_e_values.get(t, []), f"startPoint and endPoint cannot be excluded, value: {v}"
@ -1452,13 +1453,13 @@ class LiveSessionSearchFilterSchema(BaseModel):
value: Union[List[str], str] = Field(...) value: Union[List[str], str] = Field(...)
type: LiveFilterType = Field(...) type: LiveFilterType = Field(...)
source: Optional[str] = Field(default=None) source: Optional[str] = Field(default=None)
operator: Literal[SearchEventOperator.IS, \ operator: Literal[SearchEventOperator.IS, SearchEventOperator.CONTAINS] \
SearchEventOperator.CONTAINS] = Field(default=SearchEventOperator.CONTAINS) = Field(default=SearchEventOperator.CONTAINS)
_transform = model_validator(mode='before')(transform_old_filter_type) _transform = model_validator(mode='before')(transform_old_filter_type)
@model_validator(mode='after') @model_validator(mode='after')
def __validator(cls, values): def __validator(self, values):
if values.type is not None and values.type == LiveFilterType.METADATA: if values.type is not None and values.type == LiveFilterType.METADATA:
assert values.source is not None, "source should not be null for METADATA type" assert values.source is not None, "source should not be null for METADATA type"
assert len(values.source) > 0, "source should not be empty for METADATA type" assert len(values.source) > 0, "source should not be empty for METADATA type"
@ -1471,7 +1472,7 @@ class LiveSessionsSearchPayloadSchema(_PaginatedSchema):
order: SortOrderType = Field(default=SortOrderType.DESC) order: SortOrderType = Field(default=SortOrderType.DESC)
@model_validator(mode="before") @model_validator(mode="before")
def __transform(cls, values): def __transform(self, values):
if values.get("order") is not None: if values.get("order") is not None:
values["order"] = values["order"].upper() values["order"] = values["order"].upper()
if values.get("filters") is not None: if values.get("filters") is not None:
@ -1527,8 +1528,9 @@ class SessionUpdateNoteSchema(SessionNoteSchema):
is_public: Optional[bool] = Field(default=None) is_public: Optional[bool] = Field(default=None)
@model_validator(mode='after') @model_validator(mode='after')
def __validator(cls, values): def __validator(self, values):
assert values.message is not None or values.timestamp is not None or values.is_public is not None, "at least 1 attribute should be provided for update" assert values.message is not None or values.timestamp is not None or values.is_public is not None, \
"at least 1 attribute should be provided for update"
return values return values
@ -1555,7 +1557,7 @@ class HeatMapSessionsSearch(SessionsSearchPayloadSchema):
filters: List[Union[SessionSearchFilterSchema, _HeatMapSearchEventRaw]] = Field(default=[]) filters: List[Union[SessionSearchFilterSchema, _HeatMapSearchEventRaw]] = Field(default=[])
@model_validator(mode="before") @model_validator(mode="before")
def __transform(cls, values): def __transform(self, values):
for f in values.get("filters", []): for f in values.get("filters", []):
if f.get("type") == FilterType.DURATION: if f.get("type") == FilterType.DURATION:
return values return values
@ -1598,7 +1600,7 @@ class FeatureFlagConditionFilterSchema(BaseModel):
sourceOperator: Optional[Union[SearchEventOperator, MathOperator]] = Field(default=None) sourceOperator: Optional[Union[SearchEventOperator, MathOperator]] = Field(default=None)
@model_validator(mode="before") @model_validator(mode="before")
def __force_is_event(cls, values): def __force_is_event(self, values):
values["isEvent"] = False values["isEvent"] = False
return values return values
@ -1638,10 +1640,19 @@ class FeatureFlagSchema(BaseModel):
variants: List[FeatureFlagVariant] = Field(default=[]) variants: List[FeatureFlagVariant] = Field(default=[])
class ModuleType(str, Enum):
ASSIST = "assist"
NOTES = "notes"
BUG_REPORTS = "bug-reports"
OFFLINE_RECORDINGS = "offline-recordings"
ALERTS = "alerts"
ASSIST_STATTS = "assist-statts"
RECOMMENDATIONS = "recommendations"
FEATURE_FLAGS = "feature-flags"
class ModuleStatus(BaseModel): class ModuleStatus(BaseModel):
module: Literal["assist", "notes", "bug-reports", module: ModuleType = Field(...)
"offline-recordings", "alerts", "assist-statts", "recommendations", "feature-flags"] = Field(...,
description="Possible values: assist, notes, bug-reports, offline-recordings, alerts, assist-statts, recommendations, feature-flags")
status: bool = Field(...) status: bool = Field(...)

2
ee/api/.gitignore vendored
View file

@ -276,3 +276,5 @@ Pipfile.lock
/routers/subs/spot.py /routers/subs/spot.py
/chalicelib/utils/or_cache/ /chalicelib/utils/or_cache/
/routers/subs/health.py /routers/subs/health.py
/chalicelib/core/spot.py
/chalicelib/core/unprocessed_sessions.py

View file

@ -10,6 +10,7 @@ from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from psycopg import AsyncConnection from psycopg import AsyncConnection
from psycopg.rows import dict_row
from starlette import status from starlette import status
from starlette.responses import StreamingResponse, JSONResponse from starlette.responses import StreamingResponse, JSONResponse
@ -17,21 +18,19 @@ from chalicelib.core import traces
from chalicelib.utils import events_queue from chalicelib.utils import events_queue
from chalicelib.utils import helper from chalicelib.utils import helper
from chalicelib.utils import pg_client from chalicelib.utils import pg_client
from crons import core_crons, ee_crons, core_dynamic_crons
from routers import core, core_dynamic from routers import core, core_dynamic
from routers import ee from routers import ee
from routers.subs import insights, metrics, v1_api, health, usability_tests, spot
from routers.subs import v1_api_ee
if config("ENABLE_SSO", cast=bool, default=True): if config("ENABLE_SSO", cast=bool, default=True):
from routers import saml from routers import saml
from crons import core_crons, ee_crons, core_dynamic_crons
from routers.subs import insights, metrics, v1_api, health, usability_tests, spot
from routers.subs import v1_api_ee
loglevel = config("LOGLEVEL", default=logging.WARNING) loglevel = config("LOGLEVEL", default=logging.WARNING)
print(f">Loglevel set to: {loglevel}") print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel) logging.basicConfig(level=loglevel)
from psycopg.rows import dict_row
class ORPYAsyncConnection(AsyncConnection): class ORPYAsyncConnection(AsyncConnection):

View file

@ -53,10 +53,9 @@ class JWTAuth(HTTPBearer):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid authentication scheme.") detail="Invalid authentication scheme.")
jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials) jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials)
auth_exists = jwt_payload is not None \ auth_exists = jwt_payload is not None and users.auth_exists(user_id=jwt_payload.get("userId", -1),
and users.auth_exists(user_id=jwt_payload.get("userId", -1), tenant_id=jwt_payload.get("tenantId", -1),
tenant_id=jwt_payload.get("tenantId", -1), jwt_iat=jwt_payload.get("iat", 100))
jwt_iat=jwt_payload.get("iat", 100))
if jwt_payload is None \ if jwt_payload is None \
or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \ or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \
or not auth_exists: or not auth_exists:

View file

@ -263,6 +263,12 @@ def __search_metadata(project_id, value, key=None, source=None):
return helper.list_to_camel_case(results) return helper.list_to_camel_case(results)
class TableColumn:
def __init__(self, table, column):
self.table = table
self.column = column
TYPE_TO_COLUMN = { TYPE_TO_COLUMN = {
schemas.EventType.CLICK: "label", schemas.EventType.CLICK: "label",
schemas.EventType.INPUT: "label", schemas.EventType.INPUT: "label",

View file

@ -96,4 +96,6 @@ rm -rf ./chalicelib/core/db_request_handler.py
rm -rf ./chalicelib/core/db_request_handler.py rm -rf ./chalicelib/core/db_request_handler.py
rm -rf ./routers/subs/spot.py rm -rf ./routers/subs/spot.py
rm -rf ./chalicelib/utils/or_cache rm -rf ./chalicelib/utils/or_cache
rm -rf ./routers/subs/health.py rm -rf ./routers/subs/health.py
rm -rf ./chalicelib/core/spot.py
rm -rf ./chalicelib/core/unprocessed_sessions.py

View file

@ -1,3 +1,4 @@
import logging
from typing import Optional, Union from typing import Optional, Union
from decouple import config from decouple import config
@ -6,12 +7,13 @@ from fastapi import HTTPException, status
from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response
import schemas import schemas
from chalicelib.core import scope
from chalicelib.core import sessions, assist, heatmaps, sessions_favorite, sessions_assignments, errors, errors_viewed, \ from chalicelib.core import sessions, assist, heatmaps, sessions_favorite, sessions_assignments, errors, errors_viewed, \
errors_favorite, sessions_notes, sessions_replay, signup, feature_flags errors_favorite, sessions_notes, sessions_replay, signup, feature_flags
from chalicelib.core import sessions_viewed from chalicelib.core import sessions_viewed
from chalicelib.core import tenants, users, projects, license from chalicelib.core import tenants, users, projects, license
from chalicelib.core import unprocessed_sessions
from chalicelib.core import webhook from chalicelib.core import webhook
from chalicelib.core import scope
from chalicelib.core.collaboration_slack import Slack from chalicelib.core.collaboration_slack import Slack
from chalicelib.core.users import get_user_settings from chalicelib.core.users import get_user_settings
from chalicelib.utils import SAML2_helper, smtp from chalicelib.utils import SAML2_helper, smtp
@ -24,7 +26,7 @@ from schemas import Permissions, ServicePermissions
if config("ENABLE_SSO", cast=bool, default=True): if config("ENABLE_SSO", cast=bool, default=True):
from routers import saml from routers import saml
logger = logging.getLogger(__name__)
public_app, app, app_apikey = get_routers() public_app, app, app_apikey = get_routers()
COOKIE_PATH = "/api/refresh" COOKIE_PATH = "/api/refresh"
@ -79,7 +81,7 @@ def login_user(response: JSONResponse, spot: Optional[bool] = False, data: schem
content = { content = {
'jwt': r.pop('jwt'), 'jwt': r.pop('jwt'),
'data': { 'data': {
"scope":scope.get_scope(r["tenantId"]), "scope": scope.get_scope(r["tenantId"]),
"user": r "user": r
} }
} }
@ -140,6 +142,8 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)):
def edit_account(data: schemas.EditAccountSchema = Body(...), def edit_account(data: schemas.EditAccountSchema = Body(...),
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
return users.edit_account(tenant_id=context.tenant_id, user_id=context.user_id, changes=data) return users.edit_account(tenant_id=context.tenant_id, user_id=context.user_id, changes=data)
@app.post('/account/scope', tags=["account"]) @app.post('/account/scope', tags=["account"])
def change_scope(data: schemas.ScopeSchema = Body(), def change_scope(data: schemas.ScopeSchema = Body(),
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
@ -392,16 +396,10 @@ def get_live_session(projectId: int, sessionId: str, background_tasks: Backgroun
def get_live_session_replay_file(projectId: int, sessionId: Union[int, str], def get_live_session_replay_file(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
not_found = {"errors": ["Replay file not found"]} not_found = {"errors": ["Replay file not found"]}
if not sessionId.isnumeric(): sessionId, err = unprocessed_sessions.check_exists(project_id=projectId, session_id=sessionId,
return not_found not_found_response=not_found)
else: if err is not None:
sessionId = int(sessionId) return err
if not sessions.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in DB.")
if not assist.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in Assist.")
return not_found
path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId) path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId)
if path is None: if path is None:
return not_found return not_found
@ -416,19 +414,13 @@ def get_live_session_replay_file(projectId: int, sessionId: Union[int, str],
def get_live_session_devtools_file(projectId: int, sessionId: Union[int, str], def get_live_session_devtools_file(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
not_found = {"errors": ["Devtools file not found"]} not_found = {"errors": ["Devtools file not found"]}
if not sessionId.isnumeric(): sessionId, err = unprocessed_sessions.check_exists(project_id=projectId, session_id=sessionId,
return not_found not_found_response=not_found)
else: if err is not None:
sessionId = int(sessionId) return err
if not sessions.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in DB.")
if not assist.session_exists(project_id=projectId, session_id=sessionId):
print(f"{projectId}/{sessionId} not found in Assist.")
return not_found
path = assist.get_raw_devtools_by_id(project_id=projectId, session_id=sessionId) path = assist.get_raw_devtools_by_id(project_id=projectId, session_id=sessionId)
if path is None: if path is None:
return {"errors": ["Devtools file not found"]} return not_found
return FileResponse(path=path, media_type="application/octet-stream") return FileResponse(path=path, media_type="application/octet-stream")

View file

@ -60,13 +60,13 @@ class AssistStatsSessionsRequest(BaseModel):
userId: Optional[int] = Field(default=None) userId: Optional[int] = Field(default=None)
@field_validator("sort") @field_validator("sort")
def validate_sort(cls, v): def validate_sort(self, v):
if v not in assist_sort_options: if v not in assist_sort_options:
raise ValueError(f"Invalid sort option. Allowed options: {', '.join(assist_sort_options)}") raise ValueError(f"Invalid sort option. Allowed options: {', '.join(assist_sort_options)}")
return v return v
@field_validator("order") @field_validator("order")
def validate_order(cls, v): def validate_order(self, v):
if v not in ["desc", "asc"]: if v not in ["desc", "asc"]:
raise ValueError("Invalid order option. Must be 'desc' or 'asc'.") raise ValueError("Invalid order option. Must be 'desc' or 'asc'.")
return v return v

View file

@ -32,7 +32,7 @@ class CurrentContext(schemas.CurrentContext):
service_account: bool = Field(default=False) service_account: bool = Field(default=False)
@model_validator(mode="before") @model_validator(mode="before")
def remove_unsupported_perms(cls, values): def remove_unsupported_perms(self, values):
if values.get("permissions") is not None: if values.get("permissions") is not None:
perms = [] perms = []
for p in values["permissions"]: for p in values["permissions"]:
@ -94,7 +94,7 @@ class TrailSearchPayloadSchema(schemas._PaginatedSchema):
order: schemas.SortOrderType = Field(default=schemas.SortOrderType.DESC) order: schemas.SortOrderType = Field(default=schemas.SortOrderType.DESC)
@model_validator(mode="before") @model_validator(mode="before")
def transform_order(cls, values): def transform_order(self, values):
if values.get("order") is None: if values.get("order") is None:
values["order"] = schemas.SortOrderType.DESC values["order"] = schemas.SortOrderType.DESC
else: else:
@ -154,7 +154,7 @@ class CardInsights(schemas.CardInsights):
metric_value: List[InsightCategories] = Field(default=[]) metric_value: List[InsightCategories] = Field(default=[])
@model_validator(mode='after') @model_validator(mode='after')
def restrictions(cls, values): def restrictions(self, values):
return values return values