diff --git a/api/crons/core_dynamic_crons.py b/api/crons/core_dynamic_crons.py index 6b4a87dd2..f1bc87929 100644 --- a/api/crons/core_dynamic_crons.py +++ b/api/crons/core_dynamic_crons.py @@ -3,7 +3,6 @@ from apscheduler.triggers.interval import IntervalTrigger from chalicelib.core import telemetry from chalicelib.core import weekly_report, jobs, health -from chalicelib.core import assist_stats async def run_scheduled_jobs() -> None: @@ -26,10 +25,6 @@ async def weekly_health_cron() -> None: health.weekly_cron() -async def assist_events_aggregates_cron() -> None: - assist_stats.insert_aggregated_data() - - cron_jobs = [ {"func": telemetry_cron, "trigger": CronTrigger(day_of_week="*"), "misfire_grace_time": 60 * 60, "max_instances": 1}, @@ -40,7 +35,5 @@ cron_jobs = [ {"func": health_cron, "trigger": IntervalTrigger(hours=0, minutes=30, start_date="2023-04-01 0:0:0", jitter=300), "misfire_grace_time": 60 * 60, "max_instances": 1}, {"func": weekly_health_cron, "trigger": CronTrigger(day_of_week="sun", hour=5), - "misfire_grace_time": 60 * 60, "max_instances": 1}, - {"func": assist_events_aggregates_cron, - "trigger": IntervalTrigger(hours=1, start_date="2023-04-01 0:0:0", jitter=10), } + "misfire_grace_time": 60 * 60, "max_instances": 1} ] diff --git a/api/routers/core.py b/api/routers/core.py index 8b8eac480..e78c5e678 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -10,7 +10,7 @@ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assig log_tool_stackdriver, reset_password, log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, sessions, \ log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \ assist, mobile, tenants, boarding, notifications, webhook, users, \ - custom_metrics, saved_search, integrations_global, assist_stats + custom_metrics, saved_search, integrations_global from chalicelib.core.collaboration_msteams import MSTeams from chalicelib.core.collaboration_slack import Slack from or_dependencies import OR_context, OR_role @@ -860,59 +860,3 @@ async def check_recording_status(project_id: int): def health_check(): return {} - -@public_app.get('/{project_id}/assist-stats/avg', tags=["assist-stats"]) -def get_assist_stats_avg( - project_id: int, - startTimestamp: int = None, - endTimestamp: int = None, - userId: str = None -): - return assist_stats.get_averages( - project_id=project_id, - start_timestamp=startTimestamp, - end_timestamp=endTimestamp, - user_id=userId) - - -@public_app.get( - '/{project_id}/assist-stats/top-members', - tags=["assist-stats"], - response_model=schemas.AssistStatsTopMembersResponse -) -def get_assist_stats_top_members( - project_id: int, - startTimestamp: int = None, - endTimestamp: int = None, - sort: Optional[str] = Query(default="sessionsAssisted", - description="Sort options: " + ", ".join(assist_stats.event_type_mapping)), - order: str = "desc", - userId: int = None, - page: int = 0, - limit: int = 5 -): - return assist_stats.get_top_members( - project_id=project_id, - start_timestamp=startTimestamp, - end_timestamp=endTimestamp, - sort_by=sort, - sort_order=order, - user_id=userId, - page=page, - limit=limit - ) - - -@public_app.post( - '/{project_id}/assist-stats/sessions', - tags=["assist-stats"], - response_model=schemas.AssistStatsSessionsResponse -) -def get_assist_stats_sessions( - project_id: int, - data: schemas.AssistStatsSessionsRequest = Body(...), -): - return assist_stats.get_sessions( - project_id=project_id, - data=data - ) diff --git a/api/schemas/schemas.py b/api/schemas/schemas.py index be59cca12..3ddeebfdb 100644 --- a/api/schemas/schemas.py +++ b/api/schemas/schemas.py @@ -1581,75 +1581,3 @@ class ModuleStatus(BaseModel): "offline-recordings", "alerts"] = Field(..., description="Possible values: notes, bugs, live") status: bool = Field(...) - -class AssistStatsAverage(BaseModel): - key: str = Field(...) - avg: float = Field(...) - chartData: List[dict] = Field(...) - - -class AssistStatsMember(BaseModel): - name: str - count: int - assist_duration: Optional[int] = Field(default=0) - call_duration: Optional[int] = Field(default=0) - control_duration: Optional[int] = Field(default=0) - assist_count: Optional[int] = Field(default=0) - - -class AssistStatsSessionAgent(BaseModel): - name: str - id: int - - -class AssistStatsTopMembersResponse(BaseModel): - total: int - list: List[AssistStatsMember] - - -class AssistStatsSessionRecording(BaseModel): - recordId: int = Field(...) - name: str = Field(...) - duration: int = Field(...) - - -class AssistStatsSession(BaseModel): - sessionId: str = Field(...) - timestamp: int = Field(...) - teamMembers: List[AssistStatsSessionAgent] = Field(...) - assistDuration: Optional[int] = Field(default=0) - callDuration: Optional[int] = Field(default=0) - controlDuration: Optional[int] = Field(default=0) - # recordings: list[AssistStatsSessionRecording] = Field(default=[]) - - -assist_sort_options = ["timestamp", "assist_duration", "call_duration", "control_duration"] - - -class AssistStatsSessionsRequest(BaseModel): - startTimestamp: int = Field(...) - endTimestamp: int = Field(...) - limit: Optional[int] = Field(default=10) - page: Optional[int] = Field(default=1) - sort: Optional[str] = Field(default="timestamp", - enum=assist_sort_options) - order: Optional[str] = Field(default="desc", choices=["desc", "asc"]) - userId: Optional[int] = Field(default=None) - - @field_validator("sort") - def validate_sort(cls, v): - if v not in assist_sort_options: - raise ValueError(f"Invalid sort option. Allowed options: {', '.join(assist_sort_options)}") - return v - - @field_validator("order") - def validate_order(cls, v): - if v not in ["desc", "asc"]: - raise ValueError("Invalid order option. Must be 'desc' or 'asc'.") - return v - - -class AssistStatsSessionsResponse(BaseModel): - total: int = Field(...) - page: int = Field(...) - list: List[AssistStatsSession] = Field(default=[]) diff --git a/ee/api/.gitignore b/ee/api/.gitignore index f859cadc1..1f2a2cd1c 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -251,7 +251,6 @@ Pipfile.lock /routers/subs/__init__.py /routers/__init__.py /chalicelib/core/assist.py -/chalicelib/core/assist_stats.py /auth/__init__.py /auth/auth_apikey.py /build.sh diff --git a/api/chalicelib/core/assist_stats.py b/ee/api/chalicelib/core/assist_stats.py similarity index 100% rename from api/chalicelib/core/assist_stats.py rename to ee/api/chalicelib/core/assist_stats.py diff --git a/ee/api/clean-dev.sh b/ee/api/clean-dev.sh index 29e83cd9b..3e28345d6 100755 --- a/ee/api/clean-dev.sh +++ b/ee/api/clean-dev.sh @@ -73,7 +73,6 @@ rm -rf ./crons/__init__.py rm -rf ./routers/subs/__init__.py rm -rf ./routers/__init__.py rm -rf ./chalicelib/core/assist.py -rm -rf ./chalicelib/core/assist_stats.py rm -rf ./auth/__init__.py rm -rf ./auth/auth_apikey.py rm -rf ./build.sh diff --git a/ee/api/crons/ee_crons.py b/ee/api/crons/ee_crons.py index 088424386..9a4d165e4 100644 --- a/ee/api/crons/ee_crons.py +++ b/ee/api/crons/ee_crons.py @@ -1,12 +1,19 @@ from apscheduler.triggers.interval import IntervalTrigger from chalicelib.utils import events_queue +from chalicelib.core import assist_stats async def pg_events_queue() -> None: events_queue.global_queue.force_flush() +async def assist_events_aggregates_cron() -> None: + assist_stats.insert_aggregated_data() + + ee_cron_jobs = [ {"func": pg_events_queue, "trigger": IntervalTrigger(minutes=5), "misfire_grace_time": 20, "max_instances": 1}, + {"func": assist_events_aggregates_cron, + "trigger": IntervalTrigger(hours=1, start_date="2023-04-01 0:0:0", jitter=10), } ] diff --git a/ee/api/routers/ee.py b/ee/api/routers/ee.py index f485f0e62..43f1c713b 100644 --- a/ee/api/routers/ee.py +++ b/ee/api/routers/ee.py @@ -1,6 +1,6 @@ from chalicelib.core import roles, traces, assist_records, sessions from chalicelib.core import unlock, signals -from chalicelib.core import sessions_insights +from chalicelib.core import sessions_insights, assist_stats from chalicelib.utils import assist_helper unlock.check() @@ -135,3 +135,60 @@ def send_interactions(projectId: int, data: schemas.SignalsSchema = Body(...), def sessions_search(projectId: int, data: schemas.GetInsightsSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {'data': sessions_insights.fetch_selected(data=data, project_id=projectId)} + + +@public_app.get('/{project_id}/assist-stats/avg', tags=["assist-stats"]) +def get_assist_stats_avg( + project_id: int, + startTimestamp: int = None, + endTimestamp: int = None, + userId: str = None +): + return assist_stats.get_averages( + project_id=project_id, + start_timestamp=startTimestamp, + end_timestamp=endTimestamp, + user_id=userId) + + +@public_app.get( + '/{project_id}/assist-stats/top-members', + tags=["assist-stats"], + response_model=schemas.AssistStatsTopMembersResponse +) +def get_assist_stats_top_members( + project_id: int, + startTimestamp: int = None, + endTimestamp: int = None, + sort: Optional[str] = Query(default="sessionsAssisted", + description="Sort options: " + ", ".join(assist_stats.event_type_mapping)), + order: str = "desc", + userId: int = None, + page: int = 0, + limit: int = 5 +): + return assist_stats.get_top_members( + project_id=project_id, + start_timestamp=startTimestamp, + end_timestamp=endTimestamp, + sort_by=sort, + sort_order=order, + user_id=userId, + page=page, + limit=limit + ) + + +@public_app.post( + '/{project_id}/assist-stats/sessions', + tags=["assist-stats"], + response_model=schemas.AssistStatsSessionsResponse +) +def get_assist_stats_sessions( + project_id: int, + data: schemas.AssistStatsSessionsRequest = Body(...), +): + return assist_stats.get_sessions( + project_id=project_id, + data=data + ) \ No newline at end of file diff --git a/ee/api/schemas/__init__.py b/ee/api/schemas/__init__.py index 46626a1ec..37ecaab44 100644 --- a/ee/api/schemas/__init__.py +++ b/ee/api/schemas/__init__.py @@ -1,3 +1,4 @@ from .schemas import * from .schemas_ee import * +from .assist_stats_schema import * from . import overrides as _overrides diff --git a/ee/api/schemas/assist_stats_schema.py b/ee/api/schemas/assist_stats_schema.py new file mode 100644 index 000000000..b59282a09 --- /dev/null +++ b/ee/api/schemas/assist_stats_schema.py @@ -0,0 +1,78 @@ +from typing import Optional, List + +from pydantic import Field, field_validator + +from .overrides import BaseModel, Enum, ORUnion + + +class AssistStatsAverage(BaseModel): + key: str = Field(...) + avg: float = Field(...) + chartData: List[dict] = Field(...) + + +class AssistStatsMember(BaseModel): + name: str + count: int + assist_duration: Optional[int] = Field(default=0) + call_duration: Optional[int] = Field(default=0) + control_duration: Optional[int] = Field(default=0) + assist_count: Optional[int] = Field(default=0) + + +class AssistStatsSessionAgent(BaseModel): + name: str + id: int + + +class AssistStatsTopMembersResponse(BaseModel): + total: int + list: List[AssistStatsMember] + + +class AssistStatsSessionRecording(BaseModel): + recordId: int = Field(...) + name: str = Field(...) + duration: int = Field(...) + + +class AssistStatsSession(BaseModel): + sessionId: str = Field(...) + timestamp: int = Field(...) + teamMembers: List[AssistStatsSessionAgent] = Field(...) + assistDuration: Optional[int] = Field(default=0) + callDuration: Optional[int] = Field(default=0) + controlDuration: Optional[int] = Field(default=0) + # recordings: list[AssistStatsSessionRecording] = Field(default=[]) + + +assist_sort_options = ["timestamp", "assist_duration", "call_duration", "control_duration"] + + +class AssistStatsSessionsRequest(BaseModel): + startTimestamp: int = Field(...) + endTimestamp: int = Field(...) + limit: Optional[int] = Field(default=10) + page: Optional[int] = Field(default=1) + sort: Optional[str] = Field(default="timestamp", + enum=assist_sort_options) + order: Optional[str] = Field(default="desc", choices=["desc", "asc"]) + userId: Optional[int] = Field(default=None) + + @field_validator("sort") + def validate_sort(cls, v): + if v not in assist_sort_options: + raise ValueError(f"Invalid sort option. Allowed options: {', '.join(assist_sort_options)}") + return v + + @field_validator("order") + def validate_order(cls, v): + if v not in ["desc", "asc"]: + raise ValueError("Invalid order option. Must be 'desc' or 'asc'.") + return v + + +class AssistStatsSessionsResponse(BaseModel): + total: int = Field(...) + page: int = Field(...) + list: List[AssistStatsSession] = Field(default=[]) diff --git a/ee/api/schemas/schemas_ee.py b/ee/api/schemas/schemas_ee.py index ceb088c8a..602aa86da 100644 --- a/ee/api/schemas/schemas_ee.py +++ b/ee/api/schemas/schemas_ee.py @@ -156,3 +156,6 @@ class CardInsights(schemas.CardInsights): CardSchema = ORUnion(Union[schemas.__cards_union_base, CardInsights], discriminator='metric_type') + + +