diff --git a/.github/workflows/patch-build-old.yaml b/.github/workflows/patch-build-old.yaml index 719f2bfdb..27e025da7 100644 --- a/.github/workflows/patch-build-old.yaml +++ b/.github/workflows/patch-build-old.yaml @@ -8,7 +8,11 @@ on: required: true default: 'chalice,frontend' tag: - description: 'Tag to build patches from.' + description: 'Tag to update.' + required: true + type: string + branch: + description: 'Branch to build patches from. Make sure the branch is uptodate with tag. Else itll cause missing commits.' required: true type: string @@ -73,7 +77,7 @@ jobs: - name: Get HEAD Commit ID run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV - name: Define Branch Name - run: echo "BRANCH_NAME=patch/main/${HEAD_COMMIT_ID}" >> $GITHUB_ENV + run: echo "BRANCH_NAME=${{inputs.branch}}" >> $GITHUB_ENV - name: Build id: build-image diff --git a/api/chalicelib/core/autocomplete/autocomplete.py b/api/chalicelib/core/autocomplete/autocomplete.py index 648f0e652..8b5222769 100644 --- a/api/chalicelib/core/autocomplete/autocomplete.py +++ b/api/chalicelib/core/autocomplete/autocomplete.py @@ -1,9 +1,9 @@ import logging + import schemas -from chalicelib.core import countries, events, metadata +from chalicelib.core import countries, metadata from chalicelib.utils import helper from chalicelib.utils import pg_client -from chalicelib.utils.event_filter_definition import Event logger = logging.getLogger(__name__) TABLE = "public.autocomplete" @@ -112,10 +112,10 @@ def __generic_query(typename, value_length=None): LIMIT 10;""" -def __generic_autocomplete(event: Event): +def __generic_autocomplete(event: str): def f(project_id, value, key=None, source=None): with pg_client.PostgresClient() as cur: - query = __generic_query(event.ui_type, value_length=len(value)) + query = __generic_query(event, value_length=len(value)) params = {"project_id": project_id, "value": helper.string_to_sql_like(value), "svalue": helper.string_to_sql_like("^" + value)} cur.execute(cur.mogrify(query, params)) @@ -148,8 +148,8 @@ def __errors_query(source=None, value_length=None): return f"""((SELECT DISTINCT ON(lg.message) lg.message AS value, source, - '{events.EventType.ERROR.ui_type}' AS type - FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR}' AS type + FROM events.errors INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.message ILIKE %(svalue)s @@ -160,8 +160,8 @@ def __errors_query(source=None, value_length=None): (SELECT DISTINCT ON(lg.name) lg.name AS value, source, - '{events.EventType.ERROR.ui_type}' AS type - FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR}' AS type + FROM events.errors INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.name ILIKE %(svalue)s @@ -172,8 +172,8 @@ def __errors_query(source=None, value_length=None): (SELECT DISTINCT ON(lg.message) lg.message AS value, source, - '{events.EventType.ERROR.ui_type}' AS type - FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR}' AS type + FROM events.errors INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.message ILIKE %(value)s @@ -184,8 +184,8 @@ def __errors_query(source=None, value_length=None): (SELECT DISTINCT ON(lg.name) lg.name AS value, source, - '{events.EventType.ERROR.ui_type}' AS type - FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR}' AS type + FROM events.errors INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.name ILIKE %(value)s @@ -195,8 +195,8 @@ def __errors_query(source=None, value_length=None): return f"""((SELECT DISTINCT ON(lg.message) lg.message AS value, source, - '{events.EventType.ERROR.ui_type}' AS type - FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR}' AS type + FROM events.errors INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.message ILIKE %(svalue)s @@ -207,8 +207,8 @@ def __errors_query(source=None, value_length=None): (SELECT DISTINCT ON(lg.name) lg.name AS value, source, - '{events.EventType.ERROR.ui_type}' AS type - FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR}' AS type + FROM events.errors INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.name ILIKE %(svalue)s @@ -233,8 +233,8 @@ def __search_errors_mobile(project_id, value, key=None, source=None): if len(value) > 2: query = f"""(SELECT DISTINCT ON(lg.reason) lg.reason AS value, - '{events.EventType.CRASH_MOBILE.ui_type}' AS type - FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR_MOBILE}' AS type + FROM events_common.crashes INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.project_id = %(project_id)s @@ -243,8 +243,8 @@ def __search_errors_mobile(project_id, value, key=None, source=None): UNION ALL (SELECT DISTINCT ON(lg.name) lg.name AS value, - '{events.EventType.CRASH_MOBILE.ui_type}' AS type - FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR_MOBILE}' AS type + FROM events_common.crashes INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.project_id = %(project_id)s @@ -253,8 +253,8 @@ def __search_errors_mobile(project_id, value, key=None, source=None): UNION ALL (SELECT DISTINCT ON(lg.reason) lg.reason AS value, - '{events.EventType.CRASH_MOBILE.ui_type}' AS type - FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR_MOBILE}' AS type + FROM events_common.crashes INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.project_id = %(project_id)s @@ -263,8 +263,8 @@ def __search_errors_mobile(project_id, value, key=None, source=None): UNION ALL (SELECT DISTINCT ON(lg.name) lg.name AS value, - '{events.EventType.CRASH_MOBILE.ui_type}' AS type - FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR_MOBILE}' AS type + FROM events_common.crashes INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.project_id = %(project_id)s @@ -273,8 +273,8 @@ def __search_errors_mobile(project_id, value, key=None, source=None): else: query = f"""(SELECT DISTINCT ON(lg.reason) lg.reason AS value, - '{events.EventType.CRASH_MOBILE.ui_type}' AS type - FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR_MOBILE}' AS type + FROM events_common.crashes INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.project_id = %(project_id)s @@ -283,8 +283,8 @@ def __search_errors_mobile(project_id, value, key=None, source=None): UNION ALL (SELECT DISTINCT ON(lg.name) lg.name AS value, - '{events.EventType.CRASH_MOBILE.ui_type}' AS type - FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) + '{schemas.EventType.ERROR_MOBILE}' AS type + FROM events_common.crashes INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id) WHERE s.project_id = %(project_id)s AND lg.project_id = %(project_id)s diff --git a/api/chalicelib/core/autocomplete/autocomplete_ch.py b/api/chalicelib/core/autocomplete/autocomplete_ch.py index cbabf3ff5..daba725ce 100644 --- a/api/chalicelib/core/autocomplete/autocomplete_ch.py +++ b/api/chalicelib/core/autocomplete/autocomplete_ch.py @@ -1,9 +1,9 @@ import logging + import schemas -from chalicelib.core import countries, events, metadata +from chalicelib.core import countries, metadata from chalicelib.utils import ch_client from chalicelib.utils import helper, exp_ch_helper -from chalicelib.utils.event_filter_definition import Event logger = logging.getLogger(__name__) TABLE = "experimental.autocomplete" @@ -113,7 +113,7 @@ def __generic_query(typename, value_length=None): LIMIT 10;""" -def __generic_autocomplete(event: Event): +def __generic_autocomplete(event: str): def f(project_id, value, key=None, source=None): with ch_client.ClickHouseClient() as cur: query = __generic_query(event.ui_type, value_length=len(value)) @@ -149,7 +149,7 @@ def __pg_errors_query(source=None, value_length=None): return f"""((SELECT DISTINCT ON(message) message AS value, source, - '{events.EventType.ERROR.ui_type}' AS type + '{schemas.EventType.ERROR}' AS type FROM {MAIN_TABLE} WHERE project_id = %(project_id)s @@ -161,7 +161,7 @@ def __pg_errors_query(source=None, value_length=None): (SELECT DISTINCT ON(name) name AS value, source, - '{events.EventType.ERROR.ui_type}' AS type + '{schemas.EventType.ERROR}' AS type FROM {MAIN_TABLE} WHERE project_id = %(project_id)s @@ -172,7 +172,7 @@ def __pg_errors_query(source=None, value_length=None): (SELECT DISTINCT ON(message) message AS value, source, - '{events.EventType.ERROR.ui_type}' AS type + '{schemas.EventType.ERROR}' AS type FROM {MAIN_TABLE} WHERE project_id = %(project_id)s @@ -183,7 +183,7 @@ def __pg_errors_query(source=None, value_length=None): (SELECT DISTINCT ON(name) name AS value, source, - '{events.EventType.ERROR.ui_type}' AS type + '{schemas.EventType.ERROR}' AS type FROM {MAIN_TABLE} WHERE project_id = %(project_id)s @@ -193,7 +193,7 @@ def __pg_errors_query(source=None, value_length=None): return f"""((SELECT DISTINCT ON(message) message AS value, source, - '{events.EventType.ERROR.ui_type}' AS type + '{schemas.EventType.ERROR}' AS type FROM {MAIN_TABLE} WHERE project_id = %(project_id)s @@ -204,7 +204,7 @@ def __pg_errors_query(source=None, value_length=None): (SELECT DISTINCT ON(name) name AS value, source, - '{events.EventType.ERROR.ui_type}' AS type + '{schemas.EventType.ERROR}' AS type FROM {MAIN_TABLE} WHERE project_id = %(project_id)s diff --git a/api/chalicelib/core/events.py b/api/chalicelib/core/events.py deleted file mode 100644 index dcb4d88d6..000000000 --- a/api/chalicelib/core/events.py +++ /dev/null @@ -1,226 +0,0 @@ -from functools import cache -from typing import Optional - -import schemas -from chalicelib.core import issues -from chalicelib.core.autocomplete import autocomplete -from chalicelib.core.sessions import sessions_metas -from chalicelib.utils import pg_client, helper -from chalicelib.utils.TimeUTC import TimeUTC -from chalicelib.utils.event_filter_definition import SupportedFilter, Event - - -def get_customs_by_session_id(session_id, project_id): - with pg_client.PostgresClient() as cur: - cur.execute(cur.mogrify("""\ - SELECT - c.*, - 'CUSTOM' AS type - FROM events_common.customs AS c - WHERE - c.session_id = %(session_id)s - ORDER BY c.timestamp;""", - {"project_id": project_id, "session_id": session_id}) - ) - rows = cur.fetchall() - return helper.dict_to_camel_case(rows) - - -def __merge_cells(rows, start, count, replacement): - rows[start] = replacement - rows = rows[:start + 1] + rows[start + count:] - return rows - - -def __get_grouped_clickrage(rows, session_id, project_id): - click_rage_issues = issues.get_by_session_id(session_id=session_id, issue_type="click_rage", project_id=project_id) - if len(click_rage_issues) == 0: - return rows - - for c in click_rage_issues: - merge_count = c.get("payload") - if merge_count is not None: - merge_count = merge_count.get("Count", 3) - else: - merge_count = 3 - for i in range(len(rows)): - if rows[i]["timestamp"] == c["timestamp"]: - rows = __merge_cells(rows=rows, - start=i, - count=merge_count, - replacement={**rows[i], "type": "CLICKRAGE", "count": merge_count}) - break - return rows - - -def get_by_session_id(session_id, project_id, group_clickrage=False, event_type: Optional[schemas.EventType] = None): - with pg_client.PostgresClient() as cur: - rows = [] - if event_type is None or event_type == schemas.EventType.CLICK: - cur.execute(cur.mogrify("""\ - SELECT - c.*, - 'CLICK' AS type - FROM events.clicks AS c - WHERE - c.session_id = %(session_id)s - ORDER BY c.timestamp;""", - {"project_id": project_id, "session_id": session_id}) - ) - rows += cur.fetchall() - if group_clickrage: - rows = __get_grouped_clickrage(rows=rows, session_id=session_id, project_id=project_id) - if event_type is None or event_type == schemas.EventType.INPUT: - cur.execute(cur.mogrify(""" - SELECT - i.*, - 'INPUT' AS type - FROM events.inputs AS i - WHERE - i.session_id = %(session_id)s - ORDER BY i.timestamp;""", - {"project_id": project_id, "session_id": session_id}) - ) - rows += cur.fetchall() - if event_type is None or event_type == schemas.EventType.LOCATION: - cur.execute(cur.mogrify("""\ - SELECT - l.*, - l.path AS value, - l.path AS url, - 'LOCATION' AS type - FROM events.pages AS l - WHERE - l.session_id = %(session_id)s - ORDER BY l.timestamp;""", {"project_id": project_id, "session_id": session_id})) - rows += cur.fetchall() - rows = helper.list_to_camel_case(rows) - rows = sorted(rows, key=lambda k: (k["timestamp"], k["messageId"])) - return rows - - -def _search_tags(project_id, value, key=None, source=None): - with pg_client.PostgresClient() as cur: - query = f""" - SELECT public.tags.name - 'TAG' AS type - FROM public.tags - WHERE public.tags.project_id = %(project_id)s - ORDER BY SIMILARITY(public.tags.name, %(value)s) DESC - LIMIT 10 - """ - query = cur.mogrify(query, {'project_id': project_id, 'value': value}) - cur.execute(query) - results = helper.list_to_camel_case(cur.fetchall()) - return results - - -class EventType: - CLICK = Event(ui_type=schemas.EventType.CLICK, table="events.clicks", column="label") - INPUT = Event(ui_type=schemas.EventType.INPUT, table="events.inputs", column="label") - LOCATION = Event(ui_type=schemas.EventType.LOCATION, table="events.pages", column="path") - CUSTOM = Event(ui_type=schemas.EventType.CUSTOM, table="events_common.customs", column="name") - REQUEST = Event(ui_type=schemas.EventType.REQUEST, table="events_common.requests", column="path") - GRAPHQL = Event(ui_type=schemas.EventType.GRAPHQL, table="events.graphql", column="name") - STATEACTION = Event(ui_type=schemas.EventType.STATE_ACTION, table="events.state_actions", column="name") - TAG = Event(ui_type=schemas.EventType.TAG, table="events.tags", column="tag_id") - ERROR = Event(ui_type=schemas.EventType.ERROR, table="events.errors", - column=None) # column=None because errors are searched by name or message - METADATA = Event(ui_type=schemas.FilterType.METADATA, table="public.sessions", column=None) - # MOBILE - CLICK_MOBILE = Event(ui_type=schemas.EventType.CLICK_MOBILE, table="events_ios.taps", column="label") - INPUT_MOBILE = Event(ui_type=schemas.EventType.INPUT_MOBILE, table="events_ios.inputs", column="label") - VIEW_MOBILE = Event(ui_type=schemas.EventType.VIEW_MOBILE, table="events_ios.views", column="name") - SWIPE_MOBILE = Event(ui_type=schemas.EventType.SWIPE_MOBILE, table="events_ios.swipes", column="label") - CUSTOM_MOBILE = Event(ui_type=schemas.EventType.CUSTOM_MOBILE, table="events_common.customs", column="name") - REQUEST_MOBILE = Event(ui_type=schemas.EventType.REQUEST_MOBILE, table="events_common.requests", column="path") - CRASH_MOBILE = Event(ui_type=schemas.EventType.ERROR_MOBILE, table="events_common.crashes", - column=None) # column=None because errors are searched by name or message - - -@cache -def supported_types(): - return { - EventType.CLICK.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CLICK), - query=autocomplete.__generic_query(typename=EventType.CLICK.ui_type)), - EventType.INPUT.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.INPUT), - query=autocomplete.__generic_query(typename=EventType.INPUT.ui_type)), - EventType.LOCATION.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.LOCATION), - query=autocomplete.__generic_query( - typename=EventType.LOCATION.ui_type)), - EventType.CUSTOM.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CUSTOM), - query=autocomplete.__generic_query( - typename=EventType.CUSTOM.ui_type)), - EventType.REQUEST.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.REQUEST), - query=autocomplete.__generic_query( - typename=EventType.REQUEST.ui_type)), - EventType.GRAPHQL.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.GRAPHQL), - query=autocomplete.__generic_query( - typename=EventType.GRAPHQL.ui_type)), - EventType.STATEACTION.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.STATEACTION), - query=autocomplete.__generic_query( - typename=EventType.STATEACTION.ui_type)), - EventType.TAG.ui_type: SupportedFilter(get=_search_tags, query=None), - EventType.ERROR.ui_type: SupportedFilter(get=autocomplete.__search_errors, - query=None), - EventType.METADATA.ui_type: SupportedFilter(get=autocomplete.__search_metadata, - query=None), - # MOBILE - EventType.CLICK_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CLICK_MOBILE), - query=autocomplete.__generic_query( - typename=EventType.CLICK_MOBILE.ui_type)), - EventType.SWIPE_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.SWIPE_MOBILE), - query=autocomplete.__generic_query( - typename=EventType.SWIPE_MOBILE.ui_type)), - EventType.INPUT_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.INPUT_MOBILE), - query=autocomplete.__generic_query( - typename=EventType.INPUT_MOBILE.ui_type)), - EventType.VIEW_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.VIEW_MOBILE), - query=autocomplete.__generic_query( - typename=EventType.VIEW_MOBILE.ui_type)), - EventType.CUSTOM_MOBILE.ui_type: SupportedFilter( - get=autocomplete.__generic_autocomplete(EventType.CUSTOM_MOBILE), - query=autocomplete.__generic_query( - typename=EventType.CUSTOM_MOBILE.ui_type)), - EventType.REQUEST_MOBILE.ui_type: SupportedFilter( - get=autocomplete.__generic_autocomplete(EventType.REQUEST_MOBILE), - query=autocomplete.__generic_query( - typename=EventType.REQUEST_MOBILE.ui_type)), - EventType.CRASH_MOBILE.ui_type: SupportedFilter(get=autocomplete.__search_errors_mobile, - query=None), - } - - -def get_errors_by_session_id(session_id, project_id): - with pg_client.PostgresClient() as cur: - cur.execute(cur.mogrify(f"""\ - SELECT er.*,ur.*, er.timestamp - s.start_ts AS time - FROM {EventType.ERROR.table} AS er INNER JOIN public.errors AS ur USING (error_id) INNER JOIN public.sessions AS s USING (session_id) - WHERE er.session_id = %(session_id)s AND s.project_id=%(project_id)s - ORDER BY timestamp;""", {"session_id": session_id, "project_id": project_id})) - errors = cur.fetchall() - for e in errors: - e["stacktrace_parsed_at"] = TimeUTC.datetime_to_timestamp(e["stacktrace_parsed_at"]) - return helper.list_to_camel_case(errors) - - -def search(text, event_type, project_id, source, key): - if not event_type: - return {"data": autocomplete.__get_autocomplete_table(text, project_id)} - - if event_type in supported_types().keys(): - rows = supported_types()[event_type].get(project_id=project_id, value=text, key=key, source=source) - elif event_type + "_MOBILE" in supported_types().keys(): - rows = supported_types()[event_type + "_MOBILE"].get(project_id=project_id, value=text, key=key, source=source) - elif event_type in sessions_metas.supported_types().keys(): - return sessions_metas.search(text, event_type, project_id) - elif event_type.endswith("_IOS") \ - and event_type[:-len("_IOS")] in sessions_metas.supported_types().keys(): - return sessions_metas.search(text, event_type, project_id) - elif event_type.endswith("_MOBILE") \ - and event_type[:-len("_MOBILE")] in sessions_metas.supported_types().keys(): - return sessions_metas.search(text, event_type, project_id) - else: - return {"errors": ["unsupported event"]} - - return {"data": rows} diff --git a/api/chalicelib/core/events/__init__.py b/api/chalicelib/core/events/__init__.py new file mode 100644 index 000000000..897cb7fa5 --- /dev/null +++ b/api/chalicelib/core/events/__init__.py @@ -0,0 +1,11 @@ +import logging + +from decouple import config + +logger = logging.getLogger(__name__) + +if config("EXP_EVENTS", cast=bool, default=False): + logger.info(">>> Using experimental events replay") + from . import events_ch as events +else: + from . import events_pg as events diff --git a/api/chalicelib/core/events/events_ch.py b/api/chalicelib/core/events/events_ch.py new file mode 100644 index 000000000..f1f28a9e7 --- /dev/null +++ b/api/chalicelib/core/events/events_ch.py @@ -0,0 +1,97 @@ +from chalicelib.utils import ch_client +from .events_pg import * + + +def __explode_properties(rows): + for i in range(len(rows)): + rows[i] = {**rows[i], **rows[i]["$properties"]} + rows[i].pop("$properties") + return rows + + +def get_customs_by_session_id(session_id, project_id): + with ch_client.ClickHouseClient() as cur: + rows = cur.execute(""" \ + SELECT `$properties`, + created_at, + 'CUSTOM' AS type + FROM product_analytics.events + WHERE session_id = %(session_id)s + AND NOT `$auto_captured` + AND `$event_name`!='INCIDENT' + ORDER BY created_at;""", + {"project_id": project_id, "session_id": session_id}) + rows = __explode_properties(rows) + return helper.list_to_camel_case(rows) + + +def __merge_cells(rows, start, count, replacement): + rows[start] = replacement + rows = rows[:start + 1] + rows[start + count:] + return rows + + +def __get_grouped_clickrage(rows, session_id, project_id): + click_rage_issues = issues.get_by_session_id(session_id=session_id, issue_type="click_rage", project_id=project_id) + if len(click_rage_issues) == 0: + return rows + + for c in click_rage_issues: + merge_count = c.get("payload") + if merge_count is not None: + merge_count = merge_count.get("Count", 3) + else: + merge_count = 3 + for i in range(len(rows)): + if rows[i]["created_at"] == c["createdAt"]: + rows = __merge_cells(rows=rows, + start=i, + count=merge_count, + replacement={**rows[i], "type": "CLICKRAGE", "count": merge_count}) + break + return rows + + +def get_by_session_id(session_id, project_id, group_clickrage=False, event_type: Optional[schemas.EventType] = None): + with ch_client.ClickHouseClient() as cur: + select_events = ('CLICK', 'INPUT', 'LOCATION') + if event_type is not None: + select_events = (event_type,) + query = cur.format(query=""" \ + SELECT created_at, + `$properties`, + `$event_name` AS type + FROM product_analytics.events + WHERE session_id = %(session_id)s + AND `$event_name` IN %(select_events)s + AND `$auto_captured` + ORDER BY created_at;""", + parameters={"project_id": project_id, "session_id": session_id, + "select_events": select_events}) + rows = cur.execute(query) + rows = __explode_properties(rows) + if group_clickrage and 'CLICK' in select_events: + rows = __get_grouped_clickrage(rows=rows, session_id=session_id, project_id=project_id) + + rows = helper.list_to_camel_case(rows) + rows = sorted(rows, key=lambda k: k["createdAt"]) + return rows + + +def get_incidents_by_session_id(session_id, project_id): + with ch_client.ClickHouseClient() as cur: + query = cur.format(query=""" \ + SELECT created_at, + `$properties`, + `$event_name` AS type + FROM product_analytics.events + WHERE session_id = %(session_id)s + AND `$event_name` = 'INCIDENT' + AND `$auto_captured` + ORDER BY created_at;""", + parameters={"project_id": project_id, "session_id": session_id}) + rows = cur.execute(query) + rows = __explode_properties(rows) + rows = helper.list_to_camel_case(rows) + rows = sorted(rows, key=lambda k: k["createdAt"]) + return rows diff --git a/api/chalicelib/core/events_mobile.py b/api/chalicelib/core/events/events_mobile.py similarity index 95% rename from api/chalicelib/core/events_mobile.py rename to api/chalicelib/core/events/events_mobile.py index 166a7b633..346ab7832 100644 --- a/api/chalicelib/core/events_mobile.py +++ b/api/chalicelib/core/events/events_mobile.py @@ -1,5 +1,5 @@ from chalicelib.utils import pg_client, helper -from chalicelib.core import events +from . import events def get_customs_by_session_id(session_id, project_id): @@ -58,7 +58,7 @@ def get_crashes_by_session_id(session_id): with pg_client.PostgresClient() as cur: cur.execute(cur.mogrify(f""" SELECT cr.*,uc.*, cr.timestamp - s.start_ts AS time - FROM {events.EventType.CRASH_MOBILE.table} AS cr + FROM events_common.crashes AS cr INNER JOIN public.crashes_ios AS uc USING (crash_ios_id) INNER JOIN public.sessions AS s USING (session_id) WHERE diff --git a/api/chalicelib/core/events/events_pg.py b/api/chalicelib/core/events/events_pg.py new file mode 100644 index 000000000..06b524f51 --- /dev/null +++ b/api/chalicelib/core/events/events_pg.py @@ -0,0 +1,209 @@ +import logging +from functools import cache +from typing import Optional + +import schemas +from chalicelib.core.autocomplete import autocomplete +from chalicelib.core.issues import issues +from chalicelib.core.sessions import sessions_metas +from chalicelib.utils import pg_client, helper +from chalicelib.utils.TimeUTC import TimeUTC +from chalicelib.utils.event_filter_definition import SupportedFilter + +logger = logging.getLogger(__name__) + + +def get_customs_by_session_id(session_id, project_id): + with pg_client.PostgresClient() as cur: + cur.execute(cur.mogrify(""" \ + SELECT c.*, + 'CUSTOM' AS type + FROM events_common.customs AS c + WHERE c.session_id = %(session_id)s + ORDER BY c.timestamp;""", + {"project_id": project_id, "session_id": session_id}) + ) + rows = cur.fetchall() + return helper.list_to_camel_case(rows) + + +def __merge_cells(rows, start, count, replacement): + rows[start] = replacement + rows = rows[:start + 1] + rows[start + count:] + return rows + + +def __get_grouped_clickrage(rows, session_id, project_id): + click_rage_issues = issues.get_by_session_id(session_id=session_id, issue_type="click_rage", project_id=project_id) + if len(click_rage_issues) == 0: + return rows + + for c in click_rage_issues: + merge_count = c.get("payload") + if merge_count is not None: + merge_count = merge_count.get("Count", 3) + else: + merge_count = 3 + for i in range(len(rows)): + if rows[i]["timestamp"] == c["timestamp"]: + rows = __merge_cells(rows=rows, + start=i, + count=merge_count, + replacement={**rows[i], "type": "CLICKRAGE", "count": merge_count}) + break + return rows + + +def get_by_session_id(session_id, project_id, group_clickrage=False, event_type: Optional[schemas.EventType] = None): + with pg_client.PostgresClient() as cur: + rows = [] + if event_type is None or event_type == schemas.EventType.CLICK: + cur.execute(cur.mogrify(""" \ + SELECT c.*, + 'CLICK' AS type + FROM events.clicks AS c + WHERE c.session_id = %(session_id)s + ORDER BY c.timestamp;""", + {"project_id": project_id, "session_id": session_id}) + ) + rows += cur.fetchall() + if group_clickrage: + rows = __get_grouped_clickrage(rows=rows, session_id=session_id, project_id=project_id) + if event_type is None or event_type == schemas.EventType.INPUT: + cur.execute(cur.mogrify(""" + SELECT i.*, + 'INPUT' AS type + FROM events.inputs AS i + WHERE i.session_id = %(session_id)s + ORDER BY i.timestamp;""", + {"project_id": project_id, "session_id": session_id}) + ) + rows += cur.fetchall() + if event_type is None or event_type == schemas.EventType.LOCATION: + cur.execute(cur.mogrify(""" \ + SELECT l.*, + l.path AS value, + l.path AS url, + 'LOCATION' AS type + FROM events.pages AS l + WHERE + l.session_id = %(session_id)s + ORDER BY l.timestamp;""", {"project_id": project_id, "session_id": session_id})) + rows += cur.fetchall() + rows = helper.list_to_camel_case(rows) + rows = sorted(rows, key=lambda k: (k["timestamp"], k["messageId"])) + return rows + + +def _search_tags(project_id, value, key=None, source=None): + with pg_client.PostgresClient() as cur: + query = f""" + SELECT public.tags.name + 'TAG' AS type + FROM public.tags + WHERE public.tags.project_id = %(project_id)s + ORDER BY SIMILARITY(public.tags.name, %(value)s) DESC + LIMIT 10 + """ + query = cur.mogrify(query, {'project_id': project_id, 'value': value}) + cur.execute(query) + results = helper.list_to_camel_case(cur.fetchall()) + return results + + +@cache +def supported_types(): + return { + schemas.EventType.CLICK: SupportedFilter(get=autocomplete.__generic_autocomplete(schemas.EventType.CLICK), + query=autocomplete.__generic_query(typename=schemas.EventType.CLICK)), + schemas.EventType.INPUT: SupportedFilter(get=autocomplete.__generic_autocomplete(schemas.EventType.INPUT), + query=autocomplete.__generic_query(typename=schemas.EventType.INPUT)), + schemas.EventType.LOCATION: SupportedFilter(get=autocomplete.__generic_autocomplete(schemas.EventType.LOCATION), + query=autocomplete.__generic_query( + typename=schemas.EventType.LOCATION)), + schemas.EventType.CUSTOM: SupportedFilter(get=autocomplete.__generic_autocomplete(schemas.EventType.CUSTOM), + query=autocomplete.__generic_query( + typename=schemas.EventType.CUSTOM)), + schemas.EventType.REQUEST: SupportedFilter(get=autocomplete.__generic_autocomplete(schemas.EventType.REQUEST), + query=autocomplete.__generic_query( + typename=schemas.EventType.REQUEST)), + schemas.EventType.GRAPHQL: SupportedFilter(get=autocomplete.__generic_autocomplete(schemas.EventType.GRAPHQL), + query=autocomplete.__generic_query( + typename=schemas.EventType.GRAPHQL)), + schemas.EventType.STATE_ACTION: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.STATEACTION), + query=autocomplete.__generic_query( + typename=schemas.EventType.STATE_ACTION)), + schemas.EventType.TAG: SupportedFilter(get=_search_tags, query=None), + schemas.EventType.ERROR: SupportedFilter(get=autocomplete.__search_errors, + query=None), + schemas.FilterType.METADATA: SupportedFilter(get=autocomplete.__search_metadata, + query=None), + # MOBILE + schemas.EventType.CLICK_MOBILE: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.CLICK_MOBILE), + query=autocomplete.__generic_query( + typename=schemas.EventType.CLICK_MOBILE)), + schemas.EventType.SWIPE_MOBILE: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.SWIPE_MOBILE), + query=autocomplete.__generic_query( + typename=schemas.EventType.SWIPE_MOBILE)), + schemas.EventType.INPUT_MOBILE: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.INPUT_MOBILE), + query=autocomplete.__generic_query( + typename=schemas.EventType.INPUT_MOBILE)), + schemas.EventType.VIEW_MOBILE: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.VIEW_MOBILE), + query=autocomplete.__generic_query( + typename=schemas.EventType.VIEW_MOBILE)), + schemas.EventType.CUSTOM_MOBILE: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.CUSTOM_MOBILE), + query=autocomplete.__generic_query( + typename=schemas.EventType.CUSTOM_MOBILE)), + schemas.EventType.REQUEST_MOBILE: SupportedFilter( + get=autocomplete.__generic_autocomplete(schemas.EventType.REQUEST_MOBILE), + query=autocomplete.__generic_query( + typename=schemas.EventType.REQUEST_MOBILE)), + schemas.EventType.ERROR_MOBILE: SupportedFilter(get=autocomplete.__search_errors_mobile, + query=None), + } + + +def get_errors_by_session_id(session_id, project_id): + with pg_client.PostgresClient() as cur: + cur.execute(cur.mogrify(f"""\ + SELECT er.*,ur.*, er.timestamp - s.start_ts AS time + FROM events.errors AS er INNER JOIN public.errors AS ur USING (error_id) INNER JOIN public.sessions AS s USING (session_id) + WHERE er.session_id = %(session_id)s AND s.project_id=%(project_id)s + ORDER BY timestamp;""", {"session_id": session_id, "project_id": project_id})) + errors = cur.fetchall() + for e in errors: + e["stacktrace_parsed_at"] = TimeUTC.datetime_to_timestamp(e["stacktrace_parsed_at"]) + return helper.list_to_camel_case(errors) + + +def get_incidents_by_session_id(session_id, project_id): + logger.warning("INCIDENTS not supported in PG") + return [] + + +def search(text, event_type, project_id, source, key): + if not event_type: + return {"data": autocomplete.__get_autocomplete_table(text, project_id)} + + if event_type in supported_types().keys(): + rows = supported_types()[event_type].get(project_id=project_id, value=text, key=key, source=source) + elif event_type + "_MOBILE" in supported_types().keys(): + rows = supported_types()[event_type + "_MOBILE"].get(project_id=project_id, value=text, key=key, source=source) + elif event_type in sessions_metas.supported_types().keys(): + return sessions_metas.search(text, event_type, project_id) + elif event_type.endswith("_IOS") \ + and event_type[:-len("_IOS")] in sessions_metas.supported_types().keys(): + return sessions_metas.search(text, event_type, project_id) + elif event_type.endswith("_MOBILE") \ + and event_type[:-len("_MOBILE")] in sessions_metas.supported_types().keys(): + return sessions_metas.search(text, event_type, project_id) + else: + return {"errors": ["unsupported event"]} + + return {"data": rows} diff --git a/api/chalicelib/core/issues/__init__.py b/api/chalicelib/core/issues/__init__.py new file mode 100644 index 000000000..bdb52cb86 --- /dev/null +++ b/api/chalicelib/core/issues/__init__.py @@ -0,0 +1,11 @@ +import logging + +from decouple import config + +logger = logging.getLogger(__name__) + +if config("EXP_EVENTS", cast=bool, default=False): + logger.info(">>> Using experimental issues") + from . import issues_ch as issues +else: + from . import issues_pg as issues diff --git a/api/chalicelib/core/issues/issues_ch.py b/api/chalicelib/core/issues/issues_ch.py new file mode 100644 index 000000000..b6297f598 --- /dev/null +++ b/api/chalicelib/core/issues/issues_ch.py @@ -0,0 +1,56 @@ +from chalicelib.utils import ch_client, helper +import datetime +from .issues_pg import get_all_types + + +def get(project_id, issue_id): + with ch_client.ClickHouseClient() as cur: + query = cur.format(query=""" \ + SELECT * + FROM product_analytics.events + WHERE project_id = %(project_id)s + AND issue_id = %(issue_id)s;""", + parameters={"project_id": project_id, "issue_id": issue_id}) + data = cur.execute(query=query) + if data is not None and len(data) > 0: + data = data[0] + data["title"] = helper.get_issue_title(data["type"]) + return helper.dict_to_camel_case(data) + + +def get_by_session_id(session_id, project_id, issue_type=None): + with ch_client.ClickHouseClient() as cur: + query = cur.format(query=f"""\ + SELECT * + FROM product_analytics.events + WHERE session_id = %(session_id)s + AND project_id= %(project_id)s + AND `$event_name`='ISSUE' + {"AND issue_type = %(type)s" if issue_type is not None else ""} + ORDER BY created_at;""", + parameters={"session_id": session_id, "project_id": project_id, "type": issue_type}) + data = cur.execute(query) + return helper.list_to_camel_case(data) + + +# To reduce the number of issues in the replay; +# will be removed once we agree on how to show issues +def reduce_issues(issues_list): + if issues_list is None: + return None + i = 0 + # remove same-type issues if the time between them is <2s + while i < len(issues_list) - 1: + for j in range(i + 1, len(issues_list)): + if issues_list[i]["issueType"] == issues_list[j]["issueType"]: + break + else: + i += 1 + break + + if issues_list[i]["createdAt"] - issues_list[j]["createdAt"] < datetime.timedelta(seconds=2): + issues_list.pop(j) + else: + i += 1 + + return issues_list diff --git a/api/chalicelib/core/issues.py b/api/chalicelib/core/issues/issues_pg.py similarity index 76% rename from api/chalicelib/core/issues.py rename to api/chalicelib/core/issues/issues_pg.py index 66edcdf45..b2ca0b142 100644 --- a/api/chalicelib/core/issues.py +++ b/api/chalicelib/core/issues/issues_pg.py @@ -4,12 +4,11 @@ from chalicelib.utils import pg_client, helper def get(project_id, issue_id): with pg_client.PostgresClient() as cur: query = cur.mogrify( - """\ - SELECT - * + """ \ + SELECT * FROM public.issues WHERE project_id = %(project_id)s - AND issue_id = %(issue_id)s;""", + AND issue_id = %(issue_id)s;""", {"project_id": project_id, "issue_id": issue_id} ) cur.execute(query=query) @@ -35,6 +34,29 @@ def get_by_session_id(session_id, project_id, issue_type=None): return helper.list_to_camel_case(cur.fetchall()) +# To reduce the number of issues in the replay; +# will be removed once we agree on how to show issues +def reduce_issues(issues_list): + if issues_list is None: + return None + i = 0 + # remove same-type issues if the time between them is <2s + while i < len(issues_list) - 1: + for j in range(i + 1, len(issues_list)): + if issues_list[i]["type"] == issues_list[j]["type"]: + break + else: + i += 1 + break + + if issues_list[i]["timestamp"] - issues_list[j]["timestamp"] < 2000: + issues_list.pop(j) + else: + i += 1 + + return issues_list + + def get_all_types(): return [ { diff --git a/api/chalicelib/core/metrics/custom_metrics.py b/api/chalicelib/core/metrics/custom_metrics.py index c73bd282f..2a2dd3b30 100644 --- a/api/chalicelib/core/metrics/custom_metrics.py +++ b/api/chalicelib/core/metrics/custom_metrics.py @@ -4,7 +4,7 @@ import logging from fastapi import HTTPException, status import schemas -from chalicelib.core import issues +from chalicelib.core.issues import issues from chalicelib.core.errors import errors from chalicelib.core.metrics import heatmaps, product_analytics, funnels from chalicelib.core.sessions import sessions, sessions_search diff --git a/api/chalicelib/core/metrics/heatmaps/heatmaps_ch.py b/api/chalicelib/core/metrics/heatmaps/heatmaps_ch.py index 519f6ddcb..e7399f230 100644 --- a/api/chalicelib/core/metrics/heatmaps/heatmaps_ch.py +++ b/api/chalicelib/core/metrics/heatmaps/heatmaps_ch.py @@ -3,7 +3,7 @@ import logging from decouple import config import schemas -from chalicelib.core import events +from chalicelib.core.events import events from chalicelib.core.metrics.modules import sessions, sessions_mobs from chalicelib.utils import sql_helper as sh diff --git a/api/chalicelib/core/metrics/modules/significance/significance.py b/api/chalicelib/core/metrics/modules/significance/significance.py index 38815c806..8352feb94 100644 --- a/api/chalicelib/core/metrics/modules/significance/significance.py +++ b/api/chalicelib/core/metrics/modules/significance/significance.py @@ -7,7 +7,8 @@ from typing import List from psycopg2.extras import RealDictRow import schemas -from chalicelib.core import events, metadata +from chalicelib.core import metadata +from chalicelib.core.events import events from chalicelib.utils import pg_client, helper from chalicelib.utils import sql_helper as sh @@ -76,10 +77,10 @@ def get_stages_and_events(filter_d: schemas.CardSeriesFilterSchema, project_id) values["maxDuration"] = f.value[1] elif filter_type == schemas.FilterType.REFERRER: # events_query_part = events_query_part + f"INNER JOIN events.pages AS p USING(session_id)" - filter_extra_from = [f"INNER JOIN {events.EventType.LOCATION.table} AS p USING(session_id)"] + filter_extra_from = [f"INNER JOIN {"events.pages"} AS p USING(session_id)"] first_stage_extra_constraints.append( sh.multi_conditions(f"p.base_referrer {op} %({f_k})s", f.value, is_not=is_not, value_key=f_k)) - elif filter_type == events.EventType.METADATA.ui_type: + elif filter_type == schemas.FilterType.METADATA: if meta_keys is None: meta_keys = metadata.get(project_id=project_id) meta_keys = {m["key"]: m["index"] for m in meta_keys} @@ -121,31 +122,31 @@ def get_stages_and_events(filter_d: schemas.CardSeriesFilterSchema, project_id) op = sh.get_sql_operator(s.operator) # event_type = s["type"].upper() event_type = s.type - if event_type == events.EventType.CLICK.ui_type: - next_table = events.EventType.CLICK.table - next_col_name = events.EventType.CLICK.column - elif event_type == events.EventType.INPUT.ui_type: - next_table = events.EventType.INPUT.table - next_col_name = events.EventType.INPUT.column - elif event_type == events.EventType.LOCATION.ui_type: - next_table = events.EventType.LOCATION.table - next_col_name = events.EventType.LOCATION.column - elif event_type == events.EventType.CUSTOM.ui_type: - next_table = events.EventType.CUSTOM.table - next_col_name = events.EventType.CUSTOM.column + if event_type == schemas.EventType.CLICK: + next_table = "events.clicks" + next_col_name = "label" + elif event_type == schemas.EventType.INPUT: + next_table = "events.inputs" + next_col_name = "label" + elif event_type == schemas.EventType.LOCATION: + next_table = "events.pages" + next_col_name = "path" + elif event_type == schemas.EventType.CUSTOM: + next_table = "events_common.customs" + next_col_name = "name" # IOS -------------- - elif event_type == events.EventType.CLICK_MOBILE.ui_type: - next_table = events.EventType.CLICK_MOBILE.table - next_col_name = events.EventType.CLICK_MOBILE.column - elif event_type == events.EventType.INPUT_MOBILE.ui_type: - next_table = events.EventType.INPUT_MOBILE.table - next_col_name = events.EventType.INPUT_MOBILE.column - elif event_type == events.EventType.VIEW_MOBILE.ui_type: - next_table = events.EventType.VIEW_MOBILE.table - next_col_name = events.EventType.VIEW_MOBILE.column - elif event_type == events.EventType.CUSTOM_MOBILE.ui_type: - next_table = events.EventType.CUSTOM_MOBILE.table - next_col_name = events.EventType.CUSTOM_MOBILE.column + elif event_type == schemas.EventType.CLICK_MOBILE: + next_table = "events_ios.taps" + next_col_name = "label" + elif event_type == schemas.EventType.INPUT_MOBILE: + next_table = "events_ios.inputs" + next_col_name = "label" + elif event_type == schemas.EventType.VIEW_MOBILE: + next_table = "events_ios.views" + next_col_name = "name" + elif event_type == schemas.EventType.CUSTOM_MOBILE: + next_table = "events_common.customs" + next_col_name = "name" else: logger.warning(f"=================UNDEFINED:{event_type}") continue @@ -297,10 +298,10 @@ def get_simple_funnel(filter_d: schemas.CardSeriesFilterSchema, project: schemas values["maxDuration"] = f.value[1] elif filter_type == schemas.FilterType.REFERRER: # events_query_part = events_query_part + f"INNER JOIN events.pages AS p USING(session_id)" - filter_extra_from = [f"INNER JOIN {events.EventType.LOCATION.table} AS p USING(session_id)"] + filter_extra_from = [f"INNER JOIN {"events.pages"} AS p USING(session_id)"] first_stage_extra_constraints.append( sh.multi_conditions(f"p.base_referrer {op} %({f_k})s", f.value, is_not=is_not, value_key=f_k)) - elif filter_type == events.EventType.METADATA.ui_type: + elif filter_type == schemas.FilterType.METADATA: if meta_keys is None: meta_keys = metadata.get(project_id=project.project_id) meta_keys = {m["key"]: m["index"] for m in meta_keys} @@ -342,31 +343,31 @@ def get_simple_funnel(filter_d: schemas.CardSeriesFilterSchema, project: schemas op = sh.get_sql_operator(s.operator) # event_type = s["type"].upper() event_type = s.type - if event_type == events.EventType.CLICK.ui_type: - next_table = events.EventType.CLICK.table - next_col_name = events.EventType.CLICK.column - elif event_type == events.EventType.INPUT.ui_type: - next_table = events.EventType.INPUT.table - next_col_name = events.EventType.INPUT.column - elif event_type == events.EventType.LOCATION.ui_type: - next_table = events.EventType.LOCATION.table - next_col_name = events.EventType.LOCATION.column - elif event_type == events.EventType.CUSTOM.ui_type: - next_table = events.EventType.CUSTOM.table - next_col_name = events.EventType.CUSTOM.column + if event_type == schemas.EventType.CLICK: + next_table = "events.clicks" + next_col_name = "label" + elif event_type == schemas.EventType.INPUT: + next_table = "events.inputs" + next_col_name = "label" + elif event_type == schemas.EventType.LOCATION: + next_table = "events.pages" + next_col_name = "path" + elif event_type == schemas.EventType.CUSTOM: + next_table = "events_common.customs" + next_col_name = "name" # IOS -------------- - elif event_type == events.EventType.CLICK_MOBILE.ui_type: - next_table = events.EventType.CLICK_MOBILE.table - next_col_name = events.EventType.CLICK_MOBILE.column - elif event_type == events.EventType.INPUT_MOBILE.ui_type: - next_table = events.EventType.INPUT_MOBILE.table - next_col_name = events.EventType.INPUT_MOBILE.column - elif event_type == events.EventType.VIEW_MOBILE.ui_type: - next_table = events.EventType.VIEW_MOBILE.table - next_col_name = events.EventType.VIEW_MOBILE.column - elif event_type == events.EventType.CUSTOM_MOBILE.ui_type: - next_table = events.EventType.CUSTOM_MOBILE.table - next_col_name = events.EventType.CUSTOM_MOBILE.column + elif event_type == schemas.EventType.CLICK_MOBILE: + next_table = "events_ios.taps" + next_col_name = "label" + elif event_type == schemas.EventType.INPUT_MOBILE: + next_table = "events_ios.inputs" + next_col_name = "label" + elif event_type == schemas.EventType.VIEW_MOBILE: + next_table = "events_ios.views" + next_col_name = "name" + elif event_type == schemas.EventType.CUSTOM_MOBILE: + next_table = "events_common.customs" + next_col_name = "name" else: logger.warning(f"=================UNDEFINED:{event_type}") continue diff --git a/api/chalicelib/core/metrics/modules/significance/significance_ch.py b/api/chalicelib/core/metrics/modules/significance/significance_ch.py index 0cceaf928..5d4e89182 100644 --- a/api/chalicelib/core/metrics/modules/significance/significance_ch.py +++ b/api/chalicelib/core/metrics/modules/significance/significance_ch.py @@ -8,7 +8,7 @@ from chalicelib.utils import ch_client from chalicelib.utils import exp_ch_helper from chalicelib.utils import helper from chalicelib.utils import sql_helper as sh -from chalicelib.core import events +from chalicelib.core.events import events logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def get_simple_funnel(filter_d: schemas.CardSeriesFilterSchema, project: schemas elif filter_type == schemas.FilterType.REFERRER: constraints.append( sh.multi_conditions(f"s.base_referrer {op} %({f_k})s", f.value, is_not=is_not, value_key=f_k)) - elif filter_type == events.EventType.METADATA.ui_type: + elif filter_type == schemas.FilterType.METADATA: if meta_keys is None: meta_keys = metadata.get(project_id=project.project_id) meta_keys = {m["key"]: m["index"] for m in meta_keys} @@ -125,29 +125,29 @@ def get_simple_funnel(filter_d: schemas.CardSeriesFilterSchema, project: schemas e_k = f"e_value{i}" event_type = s.type next_event_type = exp_ch_helper.get_event_type(event_type, platform=platform) - if event_type == events.EventType.CLICK.ui_type: + if event_type == schemas.EventType.CLICK: if platform == "web": - next_col_name = events.EventType.CLICK.column + next_col_name = "label" if not is_any: if schemas.ClickEventExtraOperator.has_value(s.operator): specific_condition = sh.multi_conditions(f"selector {op} %({e_k})s", s.value, value_key=e_k) else: - next_col_name = events.EventType.CLICK_MOBILE.column - elif event_type == events.EventType.INPUT.ui_type: - next_col_name = events.EventType.INPUT.column - elif event_type == events.EventType.LOCATION.ui_type: + next_col_name = "label" + elif event_type == schemas.EventType.INPUT: + next_col_name = "label" + elif event_type == schemas.EventType.LOCATION: next_col_name = 'url_path' - elif event_type == events.EventType.CUSTOM.ui_type: - next_col_name = events.EventType.CUSTOM.column + elif event_type == schemas.EventType.CUSTOM: + next_col_name = "name" # IOS -------------- - elif event_type == events.EventType.CLICK_MOBILE.ui_type: - next_col_name = events.EventType.CLICK_MOBILE.column - elif event_type == events.EventType.INPUT_MOBILE.ui_type: - next_col_name = events.EventType.INPUT_MOBILE.column - elif event_type == events.EventType.VIEW_MOBILE.ui_type: - next_col_name = events.EventType.VIEW_MOBILE.column - elif event_type == events.EventType.CUSTOM_MOBILE.ui_type: - next_col_name = events.EventType.CUSTOM_MOBILE.column + elif event_type == schemas.EventType.CLICK_MOBILE: + next_col_name = "label" + elif event_type == schemas.EventType.INPUT_MOBILE: + next_col_name = "label" + elif event_type == schemas.EventType.VIEW_MOBILE: + next_col_name = "name" + elif event_type == schemas.EventType.CUSTOM_MOBILE: + next_col_name = "name" else: logger.warning(f"=================UNDEFINED:{event_type}") continue diff --git a/api/chalicelib/core/sessions/sessions_ch.py b/api/chalicelib/core/sessions/sessions_ch.py index 8d1929c70..7851ed1c8 100644 --- a/api/chalicelib/core/sessions/sessions_ch.py +++ b/api/chalicelib/core/sessions/sessions_ch.py @@ -2,7 +2,8 @@ import logging from typing import List, Union import schemas -from chalicelib.core import events, metadata +from chalicelib.core import metadata +from chalicelib.core.events import events from . 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 @@ -378,6 +379,34 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu events_conditions_where = ["main.project_id = %(projectId)s", "main.created_at >= toDateTime(%(startDate)s/1000)", "main.created_at <= toDateTime(%(endDate)s/1000)"] + any_incident = False + for i, e in enumerate(data.events): + if e.type == schemas.EventType.INCIDENT and e.operator == schemas.SearchEventOperator.IS_ANY: + any_incident = True + data.events.pop(i) + # don't stop here because we could have multiple filters looking for any incident + + if any_incident: + any_incident = False + for f in data.filters: + if f.type == schemas.FilterType.ISSUE: + any_incident = True + if f.value.index(schemas.IssueType.INCIDENT) < 0: + f.value.append(schemas.IssueType.INCIDENT) + if f.operator == schemas.SearchEventOperator.IS_ANY: + f.operator = schemas.SearchEventOperator.IS + break + + if not any_incident: + data.filters.append(schemas.SessionSearchFilterSchema(**{ + "type": "issue", + "isEvent": False, + "value": [ + "incident" + ], + "operator": "is" + })) + 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 @@ -521,7 +550,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ss_constraints.append( sh.multi_conditions(f"ms.base_referrer {op} toString(%({f_k})s)", f.value, is_not=is_not, value_key=f_k)) - elif filter_type == events.EventType.METADATA.ui_type: + elif filter_type == schemas.FilterType.METADATA: # get metadata list only if you need it if meta_keys is None: meta_keys = metadata.get(project_id=project_id) @@ -668,10 +697,10 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu **sh.multi_values(event.source, value_key=s_k), e_k: event.value[0] if len(event.value) > 0 else event.value} - if event_type == events.EventType.CLICK.ui_type: + if event_type == schemas.EventType.CLICK: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " if platform == "web": - _column = events.EventType.CLICK.column + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -718,7 +747,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ) events_conditions[-1]["condition"] = event_where[-1] else: - _column = events.EventType.CLICK_MOBILE.column + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -737,10 +766,10 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.INPUT.ui_type: + elif event_type == schemas.EventType.INPUT: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " if platform == "web": - _column = events.EventType.INPUT.column + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -765,7 +794,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu full_args = {**full_args, **sh.multi_values(event.source, value_key=f"custom{i}")} else: - _column = events.EventType.INPUT_MOBILE.column + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -785,7 +814,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.LOCATION.ui_type: + elif event_type == schemas.EventType.LOCATION: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " if platform == "web": _column = 'url_path' @@ -807,7 +836,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ) events_conditions[-1]["condition"] = event_where[-1] else: - _column = events.EventType.VIEW_MOBILE.column + _column = "name" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -824,9 +853,9 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(sh.multi_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.CUSTOM.ui_type: + elif event_type == schemas.EventType.CUSTOM: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " - _column = events.EventType.CUSTOM.column + _column = "name" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -844,7 +873,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu "main", "$properties", _column, op, event.value, e_k )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.REQUEST.ui_type: + elif event_type == schemas.EventType.REQUEST: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " _column = 'url_path' event_where.append( @@ -865,9 +894,9 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.STATEACTION.ui_type: + elif event_type == schemas.EventType.STATE_ACTION: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " - _column = events.EventType.STATEACTION.column + _column = "name" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -886,7 +915,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu )) events_conditions[-1]["condition"] = event_where[-1] # TODO: isNot for ERROR - elif event_type == events.EventType.ERROR.ui_type: + elif event_type == schemas.EventType.ERROR: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main" events_extra_join = f"SELECT * FROM {MAIN_EVENTS_TABLE} AS main1 WHERE main1.project_id=%(project_id)s" event_where.append( @@ -911,8 +940,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu events_conditions[-1]["condition"] = " AND ".join(events_conditions[-1]["condition"]) # ----- Mobile - elif event_type == events.EventType.CLICK_MOBILE.ui_type: - _column = events.EventType.CLICK_MOBILE.column + elif event_type == schemas.EventType.CLICK_MOBILE: + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -930,8 +959,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu "main", "$properties", _column, op, event.value, e_k )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.INPUT_MOBILE.ui_type: - _column = events.EventType.INPUT_MOBILE.column + elif event_type == schemas.EventType.INPUT_MOBILE: + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -949,8 +978,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu "main", "$properties", _column, op, event.value, e_k )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.VIEW_MOBILE.ui_type: - _column = events.EventType.VIEW_MOBILE.column + elif event_type == schemas.EventType.VIEW_MOBILE: + _column = "name" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -968,8 +997,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu "main", "$properties", _column, op, event.value, e_k )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.CUSTOM_MOBILE.ui_type: - _column = events.EventType.CUSTOM_MOBILE.column + elif event_type == schemas.EventType.CUSTOM_MOBILE: + _column = "name" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -988,7 +1017,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.REQUEST_MOBILE.ui_type: + elif event_type == schemas.EventType.REQUEST_MOBILE: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " _column = 'url_path' event_where.append( @@ -1008,8 +1037,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu "main", "$properties", _column, op, event.value, e_k )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.CRASH_MOBILE.ui_type: - _column = events.EventType.CRASH_MOBILE.column + elif event_type == schemas.EventType.ERROR_MOBILE: + _column = "name" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -1028,8 +1057,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu "main", "$properties", _column, op, event.value, e_k )) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.SWIPE_MOBILE.ui_type and platform != "web": - _column = events.EventType.SWIPE_MOBILE.column + elif event_type == schemas.EventType.SWIPE_MOBILE and platform != "web": + _column = "label" event_where.append( f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -1230,7 +1259,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)} if f.type == schemas.GraphqlFilterType.GRAPHQL_NAME: event_where.append(json_condition( - "main", "$properties", events.EventType.GRAPHQL.column, op, f.value, e_k_f + "main", "$properties", "name", op, f.value, e_k_f )) events_conditions[-1]["condition"].append(event_where[-1]) elif f.type == schemas.GraphqlFilterType.GRAPHQL_METHOD: @@ -1253,9 +1282,42 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu events_conditions[-1]["condition"] = " AND ".join(events_conditions[-1]["condition"]) elif event_type == schemas.EventType.EVENT: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " - _column = events.EventType.CLICK.column + _column = "label" event_where.append(f"main.`$event_name`=%({e_k})s AND main.session_id>0") events_conditions.append({"type": event_where[-1], "condition": ""}) + elif event_type == schemas.EventType.INCIDENT: + event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " + _column = "label" + event_where.append( + f"main.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") + events_conditions.append({"type": event_where[-1]}) + + if is_not: + # event_where.append(json_condition( + # "sub", "$properties", _column, op, event.value, e_k + # )) + event_where.append( + sh.multi_conditions( + get_sub_condition(col_name=f"sub.`$properties`.{_column}", + val_name=e_k, operator=event.operator), + event.value, value_key=e_k) + ) + events_conditions_not.append( + { + "type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'" + } + ) + events_conditions_not[-1]["condition"] = event_where[-1] + else: + + event_where.append( + sh.multi_conditions( + get_sub_condition(col_name=f"main.`$properties`.{_column}", + val_name=e_k, operator=event.operator), + event.value, value_key=e_k) + ) + events_conditions[-1]["condition"] = event_where[-1] + else: continue diff --git a/api/chalicelib/core/sessions/sessions_legacy_mobil.py b/api/chalicelib/core/sessions/sessions_legacy_mobil.py index 69044812c..885d08b5f 100644 --- a/api/chalicelib/core/sessions/sessions_legacy_mobil.py +++ b/api/chalicelib/core/sessions/sessions_legacy_mobil.py @@ -2,7 +2,8 @@ import ast import logging import schemas -from chalicelib.core import events, metadata, projects +from chalicelib.core import metadata, projects +from chalicelib.core.events import events from chalicelib.core.sessions import performance_event, sessions_favorite, sessions_legacy from chalicelib.utils import pg_client, helper, ch_client, exp_ch_helper from chalicelib.utils import sql_helper as sh @@ -410,7 +411,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu ss_constraints.append( _multiple_conditions(f"ms.base_referrer {op} toString(%({f_k})s)", f.value, is_not=is_not, value_key=f_k)) - elif filter_type == events.EventType.METADATA.ui_type: + elif filter_type == schemas.FilterType.METADATA: # get metadata list only if you need it if meta_keys is None: meta_keys = metadata.get(project_id=project_id) @@ -556,10 +557,10 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu **_multiple_values(event.value, value_key=e_k), **_multiple_values(event.source, value_key=s_k)} - if event_type == events.EventType.CLICK.ui_type: + if event_type == schemas.EventType.CLICK: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " if platform == "web": - _column = events.EventType.CLICK.column + _column = "label" event_where.append( f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -581,7 +582,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] else: - _column = events.EventType.CLICK_MOBILE.column + _column = "label" event_where.append( f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -598,10 +599,10 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.INPUT.ui_type: + elif event_type == schemas.EventType.INPUT: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " if platform == "web": - _column = events.EventType.INPUT.column + _column = "label" event_where.append( f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -622,7 +623,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu value_key=f"custom{i}")) full_args = {**full_args, **_multiple_values(event.source, value_key=f"custom{i}")} else: - _column = events.EventType.INPUT_MOBILE.column + _column = "label" event_where.append( f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -639,7 +640,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.LOCATION.ui_type: + elif event_type == schemas.EventType.LOCATION: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " if platform == "web": _column = 'url_path' @@ -659,7 +660,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] else: - _column = events.EventType.VIEW_MOBILE.column + _column = "name" event_where.append( f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) @@ -675,9 +676,9 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.CUSTOM.ui_type: + elif event_type == schemas.EventType.CUSTOM: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " - _column = events.EventType.CUSTOM.column + _column = "name" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -691,7 +692,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.REQUEST.ui_type: + elif event_type == schemas.EventType.REQUEST: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " _column = 'url_path' event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") @@ -708,9 +709,9 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.STATEACTION.ui_type: + elif event_type == schemas.EventType.STATE_ACTION: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " - _column = events.EventType.STATEACTION.column + _column = "name" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -725,7 +726,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] # TODO: isNot for ERROR - elif event_type == events.EventType.ERROR.ui_type: + elif event_type == schemas.EventType.ERROR: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main" events_extra_join = f"SELECT * FROM {MAIN_EVENTS_TABLE} AS main1 WHERE main1.project_id=%(project_id)s" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") @@ -746,8 +747,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu events_conditions[-1]["condition"] = " AND ".join(events_conditions[-1]["condition"]) # ----- Mobile - elif event_type == events.EventType.CLICK_MOBILE.ui_type: - _column = events.EventType.CLICK_MOBILE.column + elif event_type == schemas.EventType.CLICK_MOBILE: + _column = "label" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -761,8 +762,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.INPUT_MOBILE.ui_type: - _column = events.EventType.INPUT_MOBILE.column + elif event_type == schemas.EventType.INPUT_MOBILE: + _column = "label" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -776,8 +777,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.VIEW_MOBILE.ui_type: - _column = events.EventType.VIEW_MOBILE.column + elif event_type == schemas.EventType.VIEW_MOBILE: + _column = "name" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -791,8 +792,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.CUSTOM_MOBILE.ui_type: - _column = events.EventType.CUSTOM_MOBILE.column + elif event_type == schemas.EventType.CUSTOM_MOBILE: + _column = "name" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -806,7 +807,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.REQUEST_MOBILE.ui_type: + elif event_type == schemas.EventType.REQUEST_MOBILE: event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main " _column = 'url_path' event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") @@ -822,8 +823,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.CRASH_MOBILE.ui_type: - _column = events.EventType.CRASH_MOBILE.column + elif event_type == schemas.EventType.ERROR_MOBILE: + _column = "name" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -837,8 +838,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value, value_key=e_k)) events_conditions[-1]["condition"] = event_where[-1] - elif event_type == events.EventType.SWIPE_MOBILE.ui_type and platform != "web": - _column = events.EventType.SWIPE_MOBILE.column + elif event_type == schemas.EventType.SWIPE_MOBILE and platform != "web": + _column = "label" event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'") events_conditions.append({"type": event_where[-1]}) if not is_any: @@ -992,7 +993,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu full_args = {**full_args, **_multiple_values(f.value, value_key=e_k_f)} if f.type == schemas.GraphqlFilterType.GRAPHQL_NAME: event_where.append( - _multiple_conditions(f"main.{events.EventType.GRAPHQL.column} {op} %({e_k_f})s", f.value, + _multiple_conditions(f"main.name {op} %({e_k_f})s", f.value, value_key=e_k_f)) events_conditions[-1]["condition"].append(event_where[-1]) elif f.type == schemas.GraphqlFilterType.GRAPHQL_METHOD: @@ -1221,7 +1222,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu c.value = helper.values_for_operator(value=c.value, op=c.operator) full_args = {**full_args, **_multiple_values(c.value, value_key=e_k)} - if c.type == events.EventType.LOCATION.ui_type: + if c.type == schemas.EventType.LOCATION: _extra_or_condition.append( _multiple_conditions(f"extra_event.url_path {op} %({e_k})s", c.value, value_key=e_k)) @@ -1358,18 +1359,15 @@ def get_user_sessions(project_id, user_id, start_date, end_date): def get_session_user(project_id, user_id): with pg_client.PostgresClient() as cur: query = cur.mogrify( - """\ - SELECT - user_id, - count(*) as session_count, - max(start_ts) as last_seen, - min(start_ts) as first_seen - FROM - "public".sessions - WHERE - project_id = %(project_id)s - AND user_id = %(userId)s - AND duration is not null + """ \ + SELECT user_id, + count(*) as session_count, + max(start_ts) as last_seen, + min(start_ts) as first_seen + FROM "public".sessions + WHERE project_id = %(project_id)s + AND user_id = %(userId)s + AND duration is not null GROUP BY user_id; """, {"project_id": project_id, "userId": user_id} diff --git a/api/chalicelib/core/sessions/sessions_pg.py b/api/chalicelib/core/sessions/sessions_pg.py index 3032affcb..fe42f4ebd 100644 --- a/api/chalicelib/core/sessions/sessions_pg.py +++ b/api/chalicelib/core/sessions/sessions_pg.py @@ -2,7 +2,8 @@ import logging from typing import List, Union import schemas -from chalicelib.core import events, metadata +from chalicelib.core.events import events +from chalicelib.core import metadata from . import performance_event from chalicelib.utils import pg_client, helper, metrics_helper from chalicelib.utils import sql_helper as sh @@ -439,7 +440,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, extra_constraints.append( sh.multi_conditions(f"s.base_referrer {op} %({f_k})s", f.value, is_not=is_not, value_key=f_k)) - elif filter_type == events.EventType.METADATA.ui_type: + elif filter_type == schemas.FilterType.METADATA: # get metadata list only if you need it if meta_keys is None: meta_keys = metadata.get(project_id=project_id) @@ -580,36 +581,36 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, **sh.multi_values(event.value, value_key=e_k), **sh.multi_values(event.source, value_key=s_k)} - if event_type == events.EventType.CLICK.ui_type: + if event_type == schemas.EventType.CLICK: if platform == "web": - event_from = event_from % f"{events.EventType.CLICK.table} AS main " + event_from = event_from % f"events.clicks AS main " if not is_any: if schemas.ClickEventExtraOperator.has_value(event.operator): event_where.append( sh.multi_conditions(f"main.selector {op} %({e_k})s", event.value, value_key=e_k)) else: event_where.append( - sh.multi_conditions(f"main.{events.EventType.CLICK.column} {op} %({e_k})s", event.value, + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) else: - event_from = event_from % f"{events.EventType.CLICK_MOBILE.table} AS main " + event_from = event_from % f"events_ios.taps AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.CLICK_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.TAG.ui_type: - event_from = event_from % f"{events.EventType.TAG.table} AS main " + elif event_type == schemas.EventType.TAG: + event_from = event_from % f"events.tags AS main " if not is_any: event_where.append( sh.multi_conditions(f"main.tag_id = %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.INPUT.ui_type: + elif event_type == schemas.EventType.INPUT: if platform == "web": - event_from = event_from % f"{events.EventType.INPUT.table} AS main " + event_from = event_from % f"events.inputs AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.INPUT.column} {op} %({e_k})s", event.value, + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) if event.source is not None and len(event.source) > 0: event_where.append(sh.multi_conditions(f"main.value ILIKE %(custom{i})s", event.source, @@ -617,53 +618,53 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, full_args = {**full_args, **sh.multi_values(event.source, value_key=f"custom{i}")} else: - event_from = event_from % f"{events.EventType.INPUT_MOBILE.table} AS main " + event_from = event_from % f"events_ios.inputs AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.INPUT_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.LOCATION.ui_type: + elif event_type == schemas.EventType.LOCATION: if platform == "web": - event_from = event_from % f"{events.EventType.LOCATION.table} AS main " + event_from = event_from % f"events.pages AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.LOCATION.column} {op} %({e_k})s", + sh.multi_conditions(f"main.path {op} %({e_k})s", event.value, value_key=e_k)) else: - event_from = event_from % f"{events.EventType.VIEW_MOBILE.table} AS main " + event_from = event_from % f"events_ios.views AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.VIEW_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.name {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.CUSTOM.ui_type: - event_from = event_from % f"{events.EventType.CUSTOM.table} AS main " + elif event_type == schemas.EventType.CUSTOM: + event_from = event_from % f"events_common.customs AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.CUSTOM.column} {op} %({e_k})s", event.value, + sh.multi_conditions(f"main.name {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.REQUEST.ui_type: - event_from = event_from % f"{events.EventType.REQUEST.table} AS main " + elif event_type == schemas.EventType.REQUEST: + event_from = event_from % f"events_common.requests AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.REQUEST.column} {op} %({e_k})s", event.value, + sh.multi_conditions(f"main.path {op} %({e_k})s", event.value, value_key=e_k)) - # elif event_type == events.event_type.GRAPHQL.ui_type: + # elif event_type == schemas.event_type.GRAPHQL: # event_from = event_from % f"{events.event_type.GRAPHQL.table} AS main " # if not is_any: # event_where.append( # _multiple_conditions(f"main.{events.event_type.GRAPHQL.column} {op} %({e_k})s", event.value, # value_key=e_k)) - elif event_type == events.EventType.STATEACTION.ui_type: - event_from = event_from % f"{events.EventType.STATEACTION.table} AS main " + elif event_type == schemas.EventType.STATE_ACTION: + event_from = event_from % f"events.state_actions AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.STATEACTION.column} {op} %({e_k})s", + sh.multi_conditions(f"main.name {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.ERROR.ui_type: - event_from = event_from % f"{events.EventType.ERROR.table} AS main INNER JOIN public.errors AS main1 USING(error_id)" + elif event_type == schemas.EventType.ERROR: + event_from = event_from % f"events.errors AS main INNER JOIN public.errors AS main1 USING(error_id)" event.source = list(set(event.source)) if not is_any and event.value not in [None, "*", ""]: event_where.append( @@ -674,59 +675,59 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, # ----- Mobile - elif event_type == events.EventType.CLICK_MOBILE.ui_type: - event_from = event_from % f"{events.EventType.CLICK_MOBILE.table} AS main " + elif event_type == schemas.EventType.CLICK_MOBILE: + event_from = event_from % f"events_ios.taps AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.CLICK_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.INPUT_MOBILE.ui_type: - event_from = event_from % f"{events.EventType.INPUT_MOBILE.table} AS main " + elif event_type == schemas.EventType.INPUT_MOBILE: + event_from = event_from % f"events_ios.inputs AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.INPUT_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) if event.source is not None and len(event.source) > 0: event_where.append(sh.multi_conditions(f"main.value ILIKE %(custom{i})s", event.source, value_key="custom{i}")) full_args = {**full_args, **sh.multi_values(event.source, f"custom{i}")} - elif event_type == events.EventType.VIEW_MOBILE.ui_type: - event_from = event_from % f"{events.EventType.VIEW_MOBILE.table} AS main " + elif event_type == schemas.EventType.VIEW_MOBILE: + event_from = event_from % f"events_ios.views AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.VIEW_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.name {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.CUSTOM_MOBILE.ui_type: - event_from = event_from % f"{events.EventType.CUSTOM_MOBILE.table} AS main " + elif event_type == schemas.EventType.CUSTOM_MOBILE: + event_from = event_from % f"events_common.customs AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.CUSTOM_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.name {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.REQUEST_MOBILE.ui_type: - event_from = event_from % f"{events.EventType.REQUEST_MOBILE.table} AS main " + elif event_type == schemas.EventType.REQUEST_MOBILE: + event_from = event_from % f"events_common.requests AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.REQUEST_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.path {op} %({e_k})s", event.value, value_key=e_k)) - elif event_type == events.EventType.CRASH_MOBILE.ui_type: - event_from = event_from % f"{events.EventType.CRASH_MOBILE.table} AS main INNER JOIN public.crashes_ios AS main1 USING(crash_ios_id)" + elif event_type == schemas.EventType.ERROR_MOBILE: + event_from = event_from % f"events_common.crashes AS main INNER JOIN public.crashes_ios AS main1 USING(crash_ios_id)" if not is_any and event.value not in [None, "*", ""]: event_where.append( sh.multi_conditions(f"(main1.reason {op} %({e_k})s OR main1.name {op} %({e_k})s)", event.value, value_key=e_k)) - elif event_type == events.EventType.SWIPE_MOBILE.ui_type and platform != "web": - event_from = event_from % f"{events.EventType.SWIPE_MOBILE.table} AS main " + elif event_type == schemas.EventType.SWIPE_MOBILE and platform != "web": + event_from = event_from % f"events_ios.swipes AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.SWIPE_MOBILE.column} {op} %({e_k})s", + sh.multi_conditions(f"main.label {op} %({e_k})s", event.value, value_key=e_k)) elif event_type == schemas.PerformanceEventType.FETCH_FAILED: - event_from = event_from % f"{events.EventType.REQUEST.table} AS main " + event_from = event_from % f"events_common.requests AS main " if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.REQUEST.column} {op} %({e_k})s", + sh.multi_conditions(f"main.path {op} %({e_k})s", event.value, value_key=e_k)) col = performance_event.get_col(event_type) colname = col["column"] @@ -751,7 +752,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD, schemas.PerformanceEventType.LOCATION_AVG_MEMORY_USAGE ]: - event_from = event_from % f"{events.EventType.LOCATION.table} AS main " + event_from = event_from % f"events.pages AS main " col = performance_event.get_col(event_type) colname = col["column"] tname = "main" @@ -762,7 +763,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, f"{tname}.timestamp <= %(endDate)s"] if not is_any: event_where.append( - sh.multi_conditions(f"main.{events.EventType.LOCATION.column} {op} %({e_k})s", + sh.multi_conditions(f"main.path {op} %({e_k})s", event.value, value_key=e_k)) e_k += "_custom" full_args = {**full_args, **sh.multi_values(event.source, value_key=e_k)} @@ -772,7 +773,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, event.source, value_key=e_k)) elif event_type == schemas.EventType.REQUEST_DETAILS: - event_from = event_from % f"{events.EventType.REQUEST.table} AS main " + event_from = event_from % f"events_common.requests AS main " apply = False for j, f in enumerate(event.filters): is_any = sh.isAny_opreator(f.operator) @@ -784,7 +785,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)} if f.type == schemas.FetchFilterType.FETCH_URL: event_where.append( - sh.multi_conditions(f"main.{events.EventType.REQUEST.column} {op} %({e_k_f})s::text", + sh.multi_conditions(f"main.path {op} %({e_k_f})s::text", f.value, value_key=e_k_f)) apply = True elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE: @@ -816,7 +817,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, if not apply: continue elif event_type == schemas.EventType.GRAPHQL: - event_from = event_from % f"{events.EventType.GRAPHQL.table} AS main " + event_from = event_from % f"events.graphql AS main " for j, f in enumerate(event.filters): is_any = sh.isAny_opreator(f.operator) if is_any or len(f.value) == 0: @@ -827,7 +828,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)} if f.type == schemas.GraphqlFilterType.GRAPHQL_NAME: event_where.append( - sh.multi_conditions(f"main.{events.EventType.GRAPHQL.column} {op} %({e_k_f})s", f.value, + sh.multi_conditions(f"main.name {op} %({e_k_f})s", f.value, value_key=e_k_f)) elif f.type == schemas.GraphqlFilterType.GRAPHQL_METHOD: event_where.append( @@ -908,7 +909,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, # b"s.user_os in ('Chrome OS','Fedora','Firefox OS','Linux','Mac OS X','Ubuntu','Windows')") if errors_only: - extra_from += f" INNER JOIN {events.EventType.ERROR.table} AS er USING (session_id) INNER JOIN public.errors AS ser USING (error_id)" + extra_from += f" INNER JOIN events.errors AS er USING (session_id) INNER JOIN public.errors AS ser USING (error_id)" extra_constraints.append("ser.source = 'js_exception'") extra_constraints.append("ser.project_id = %(project_id)s") # if error_status != schemas.ErrorStatus.all: @@ -984,9 +985,9 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, c.value = helper.values_for_operator(value=c.value, op=c.operator) full_args = {**full_args, **sh.multi_values(c.value, value_key=e_k)} - if c.type == events.EventType.LOCATION.ui_type: + if c.type == schemas.EventType.LOCATION: _extra_or_condition.append( - sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s", + sh.multi_conditions(f"ev.path {op} %({e_k})s", c.value, value_key=e_k)) else: logger.warning(f"unsupported extra_event type:${c.type}") @@ -1044,18 +1045,15 @@ def get_user_sessions(project_id, user_id, start_date, end_date): def get_session_user(project_id, user_id): with pg_client.PostgresClient() as cur: query = cur.mogrify( - """\ - SELECT - user_id, - count(*) as session_count, - max(start_ts) as last_seen, - min(start_ts) as first_seen - FROM - "public".sessions - WHERE - project_id = %(project_id)s - AND user_id = %(userId)s - AND duration is not null + """ \ + SELECT user_id, + count(*) as session_count, + max(start_ts) as last_seen, + min(start_ts) as first_seen + FROM "public".sessions + WHERE project_id = %(project_id)s + AND user_id = %(userId)s + AND duration is not null GROUP BY user_id; """, {"project_id": project_id, "userId": user_id} @@ -1074,11 +1072,10 @@ def count_all(): def session_exists(project_id, session_id): with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT 1 - FROM public.sessions - WHERE session_id=%(session_id)s - AND project_id=%(project_id)s - LIMIT 1;""", + query = cur.mogrify("""SELECT 1 + FROM public.sessions + WHERE session_id = %(session_id)s + AND project_id = %(project_id)s LIMIT 1;""", {"project_id": project_id, "session_id": session_id}) cur.execute(query) row = cur.fetchone() diff --git a/api/chalicelib/core/sessions/sessions_replay.py b/api/chalicelib/core/sessions/sessions_replay.py index 24e8a9478..b53d60a29 100644 --- a/api/chalicelib/core/sessions/sessions_replay.py +++ b/api/chalicelib/core/sessions/sessions_replay.py @@ -1,6 +1,7 @@ import schemas -from chalicelib.core import events, metadata, events_mobile, \ - issues, assist, canvas, user_testing +from chalicelib.core import metadata, assist, canvas, user_testing +from chalicelib.core.issues import issues +from chalicelib.core.events import events, events_mobile from . import sessions_mobs, sessions_devtool from chalicelib.core.errors.modules import errors_helper from chalicelib.utils import pg_client, helper @@ -128,30 +129,8 @@ def get_events(project_id, session_id): data['userTesting'] = user_testing.get_test_signals(session_id=session_id, project_id=project_id) data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id) - data['issues'] = reduce_issues(data['issues']) + data['issues'] = issues.reduce_issues(data['issues']) + data['incidents'] = events.get_incidents_by_session_id(session_id=session_id, project_id=project_id) return data else: return None - - -# To reduce the number of issues in the replay; -# will be removed once we agree on how to show issues -def reduce_issues(issues_list): - if issues_list is None: - return None - i = 0 - # remove same-type issues if the time between them is <2s - while i < len(issues_list) - 1: - for j in range(i + 1, len(issues_list)): - if issues_list[i]["type"] == issues_list[j]["type"]: - break - else: - i += 1 - break - - if issues_list[i]["timestamp"] - issues_list[j]["timestamp"] < 2000: - issues_list.pop(j) - else: - i += 1 - - return issues_list diff --git a/api/chalicelib/core/signup.py b/api/chalicelib/core/signup.py index 2c8c850ef..3819784ad 100644 --- a/api/chalicelib/core/signup.py +++ b/api/chalicelib/core/signup.py @@ -87,7 +87,7 @@ async def create_tenant(data: schemas.UserSignupSchema): "spotRefreshToken": r.pop("spotRefreshToken"), "spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"), 'data': { - "scopeState": 0, + "scopeState": 2, "user": r } } diff --git a/api/chalicelib/utils/exp_ch_helper.py b/api/chalicelib/utils/exp_ch_helper.py index aaf41afb2..a0d02a524 100644 --- a/api/chalicelib/utils/exp_ch_helper.py +++ b/api/chalicelib/utils/exp_ch_helper.py @@ -56,7 +56,8 @@ def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEvent schemas.EventType.ERROR: "ERROR", schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE', schemas.PerformanceEventType.LOCATION_AVG_MEMORY_USAGE: 'PERFORMANCE', - schemas.FetchFilterType.FETCH_URL: 'REQUEST' + schemas.FetchFilterType.FETCH_URL: 'REQUEST', + schemas.EventType.INCIDENT: "INCIDENT", } defs_mobile = { schemas.EventType.CLICK_MOBILE: "TAP", @@ -65,7 +66,8 @@ def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEvent schemas.EventType.REQUEST_MOBILE: "REQUEST", schemas.EventType.ERROR_MOBILE: "CRASH", schemas.EventType.VIEW_MOBILE: "VIEW", - schemas.EventType.SWIPE_MOBILE: "SWIPE" + schemas.EventType.SWIPE_MOBILE: "SWIPE", + schemas.EventType.INCIDENT: "INCIDENT" } if platform != "web" and event_type in defs_mobile: return defs_mobile.get(event_type) diff --git a/api/env.default b/api/env.default index 383e74273..6c53a6b12 100644 --- a/api/env.default +++ b/api/env.default @@ -75,4 +75,5 @@ EXP_AUTOCOMPLETE=true EXP_ALERTS=true EXP_ERRORS_SEARCH=true EXP_METRICS=true -EXP_SESSIONS_SEARCH=true \ No newline at end of file +EXP_SESSIONS_SEARCH=true +EXP_EVENTS=true \ No newline at end of file diff --git a/api/env.dev b/api/env.dev index 74f9f8e1f..9183efad6 100644 --- a/api/env.dev +++ b/api/env.dev @@ -68,4 +68,5 @@ EXP_CH_DRIVER=true EXP_AUTOCOMPLETE=true EXP_ALERTS=true EXP_ERRORS_SEARCH=true -EXP_METRICS=true \ No newline at end of file +EXP_METRICS=true +EXP_EVENTS=true \ No newline at end of file diff --git a/api/routers/core.py b/api/routers/core.py index 42ba9b281..260df30f2 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -4,8 +4,9 @@ from decouple import config from fastapi import Depends, Body, BackgroundTasks import schemas -from chalicelib.core import events, projects, issues, metadata, reset_password, log_tools, \ +from chalicelib.core import events, projects, metadata, reset_password, log_tools, \ announcements, weekly_report, assist, mobile, tenants, boarding, notifications, webhook, users, saved_search, tags +from chalicelib.core.issues import issues from chalicelib.core.sourcemaps import sourcemaps from chalicelib.core.metrics import custom_metrics from chalicelib.core.alerts import alerts diff --git a/api/schemas/schemas.py b/api/schemas/schemas.py index 973dce8f1..6c38868a4 100644 --- a/api/schemas/schemas.py +++ b/api/schemas/schemas.py @@ -406,6 +406,7 @@ class EventType(str, Enum): ERROR_MOBILE = "errorMobile" SWIPE_MOBILE = "swipeMobile" EVENT = "event" + INCIDENT = "incident" class PerformanceEventType(str, Enum): @@ -506,8 +507,8 @@ class IssueType(str, Enum): CUSTOM = 'custom' JS_EXCEPTION = 'js_exception' MOUSE_THRASHING = 'mouse_thrashing' - # IOS - TAP_RAGE = 'tap_rage' + TAP_RAGE = 'tap_rage' # IOS + INCIDENT = 'incident' class MetricFormatType(str, Enum): diff --git a/ee/api/.gitignore b/ee/api/.gitignore index d5392c84d..7ad13e838 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -201,8 +201,7 @@ Pipfile.lock /chalicelib/core/metrics/heatmaps /chalicelib/core/metrics/product_analytics /chalicelib/core/metrics/product_anaytics2.py -/chalicelib/core/events.py -/chalicelib/core/events_mobile.py +/chalicelib/core/events /chalicelib/core/feature_flags.py /chalicelib/core/issue_tracking/* /chalicelib/core/issues.py diff --git a/ee/api/chalicelib/core/signup.py b/ee/api/chalicelib/core/signup.py index a2ee66d04..d7a031957 100644 --- a/ee/api/chalicelib/core/signup.py +++ b/ee/api/chalicelib/core/signup.py @@ -98,7 +98,7 @@ async def create_tenant(data: schemas.UserSignupSchema): "spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"), "tenantId": t["tenant_id"], 'data': { - "scopeState": 0, + "scopeState": 2, "user": r } } diff --git a/ee/api/clean-dev.sh b/ee/api/clean-dev.sh index 2e42c9d29..cc093f1a2 100755 --- a/ee/api/clean-dev.sh +++ b/ee/api/clean-dev.sh @@ -21,8 +21,7 @@ rm -rf ./chalicelib/core/metrics/dashboards.py rm -rf ./chalicelib/core/metrics/heatmaps rm -rf ./chalicelib/core/metrics/product_analytics rm -rf ./chalicelib/core/metrics/product_anaytics2.py -rm -rf ./chalicelib/core/events.py -rm -rf ./chalicelib/core/events_mobile.py +rm -rf ./chalicelib/core/events rm -rf ./chalicelib/core/feature_flags.py rm -rf ./chalicelib/core/issue_tracking rm -rf ./chalicelib/core/integrations_manager.py diff --git a/ee/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql b/ee/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql index f3c024fb5..718a6e693 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql @@ -20,6 +20,8 @@ $fn_def$, :'next_version') -- DROP SCHEMA IF EXISTS or_cache CASCADE; +ALTER TABLE public.tenants + ALTER COLUMN scope_state SET DEFAULT 2; COMMIT; diff --git a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql index bc95e97d4..4701bc53f 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -104,7 +104,7 @@ CREATE TABLE public.tenants t_users integer NOT NULL DEFAULT 1, t_integrations integer NOT NULL DEFAULT 0, last_telemetry bigint NOT NULL DEFAULT CAST(EXTRACT(epoch FROM date_trunc('day', now())) * 1000 AS BIGINT), - scope_state smallint NOT NULL DEFAULT 0 + scope_state smallint NOT NULL DEFAULT 2 ); diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index f16e41523..45b5d9dd4 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -32,7 +32,6 @@ const components: any = { ), SpotsListPure: lazy(() => import('Components/Spots/SpotsList')), SpotPure: lazy(() => import('Components/Spots/SpotPlayer')), - ScopeSetup: lazy(() => import('Components/ScopeForm')), HighlightsPure: lazy(() => import('Components/Highlights/HighlightsList')), KaiPure: lazy(() => import('Components/Kai/KaiChat')), }; @@ -111,7 +110,6 @@ function PrivateRoutes() { const sites = projectsStore.list; const { siteId } = projectsStore; const hasRecordings = sites.some((s) => s.recorded); - const redirectToSetup = scope === 0; const redirectToOnboarding = !onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || @@ -138,13 +136,6 @@ function PrivateRoutes() { return ( }> - - {redirectToSetup ? : null} + {data.method} ); diff --git a/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx b/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx index 63b2521b2..b7829ee37 100644 --- a/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx +++ b/frontend/app/components/Dashboard/components/AddCardSelectionModal.tsx @@ -47,7 +47,7 @@ function AddCardSelectionModal(props: Props) {
onClick(true)} > @@ -57,7 +57,7 @@ function AddCardSelectionModal(props: Props) {
onClick(false)} > diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx index f86870c31..a815c095f 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertListItem.tsx @@ -123,7 +123,7 @@ function AlertListItem(props: Props) {
{alert.detectionMethod} diff --git a/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx b/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx index 074ebf35c..1c1129daa 100644 --- a/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx +++ b/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx @@ -15,7 +15,7 @@ function CardIssueItem(props: Props) { title={issue.name} description={
{issue.source}
} avatar={} - className="cursor-pointer hover:bg-indigo-50" + className="cursor-pointer hover:bg-indigo-lightest" />
{issue.sessionCount}
diff --git a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx index f4766acc2..e3854de28 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary.tsx @@ -60,7 +60,7 @@ function CardsLibrary(props: Props) { onClick={(e) => onItemClick(e, metric.metricId)} /> ; - onClick: (category: Record) => void; - isSelected: boolean; - selectedWidgetIds: string[]; -} - -const ICONS: Record = { - errors: 'errors-icon', - performance: 'performance-icon', - resources: 'resources-icon', - overview: null, - custom: null, -}; - -export function WidgetCategoryItem({ - category, - isSelected, - onClick, - selectedWidgetIds, -}: IWiProps) { - const selectedCategoryWidgetsCount = useObserver( - () => - category.widgets.filter((widget: any) => - selectedWidgetIds.includes(widget.metricId), - ).length, - ); - return ( -
onClick(category)} - > -
- {/* @ts-ignore */} - {ICONS[category.name] && ( - - )} - {category.name} -
-
{category.description}
- {selectedCategoryWidgetsCount > 0 && ( -
- {`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`} -
- )} -
- ); -} - -interface IProps { - handleCreateNew?: () => void; - isDashboardExists?: boolean; -} - -function DashboardMetricSelection(props: IProps) { - const { t } = useTranslation(); - const { dashboardStore } = useStore(); - const widgetCategories: any[] = useObserver( - () => dashboardStore.widgetCategories, - ); - const loadingTemplates = useObserver(() => dashboardStore.loadingTemplates); - const [activeCategory, setActiveCategory] = React.useState(); - const [selectAllCheck, setSelectAllCheck] = React.useState(false); - const selectedWidgetIds = useObserver(() => - dashboardStore.selectedWidgets.map((widget: any) => widget.metricId), - ); - const scrollContainer = React.useRef(null); - - useEffect(() => { - dashboardStore?.fetchTemplates(true).then((categories) => { - setActiveCategory(categories[0]); - }); - }, []); - - useEffect(() => { - if (scrollContainer.current) { - scrollContainer.current.scrollTop = 0; - } - }, [activeCategory, scrollContainer.current]); - - const handleWidgetCategoryClick = (category: any) => { - setActiveCategory(category); - setSelectAllCheck(false); - }; - - const toggleAllWidgets = ({ target: { checked } }) => { - setSelectAllCheck(checked); - if (checked) { - dashboardStore.selectWidgetsByCategory(activeCategory.name); - } else { - dashboardStore.removeSelectedWidgetByCategory(activeCategory); - } - }; - - return useObserver(() => ( - -
-
-
{t('Type')}
-
- -
- {activeCategory && ( - <> -
-

{activeCategory.name}

- - {activeCategory.widgets.length} - -
- -
- -
- - )} -
-
-
-
-
- {activeCategory && - widgetCategories.map((category, index) => ( - - ))} -
-
-
-
- {activeCategory && - activeCategory.widgets.map((widget: any) => ( - dashboardStore.toggleWidgetSelection(widget)} - /> - ))} - {props.isDashboardExists && activeCategory?.name === 'custom' && ( -
- - {t('Create Metric')} -
- )} -
-
-
-
- )); -} - -export default DashboardMetricSelection; diff --git a/frontend/app/components/Dashboard/components/DashboardMetricSelection/index.ts b/frontend/app/components/Dashboard/components/DashboardMetricSelection/index.ts deleted file mode 100644 index 443a7a919..000000000 --- a/frontend/app/components/Dashboard/components/DashboardMetricSelection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './DashboardMetricSelection'; diff --git a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx deleted file mode 100644 index e175c8ccb..000000000 --- a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import { useObserver } from 'mobx-react-lite'; -import { Button } from 'antd'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { useStore } from 'App/mstore'; -import { useModal } from 'App/components/Modal'; -import { dashboardMetricCreate, withSiteId } from 'App/routes'; -import DashboardForm from '../DashboardForm'; -import DashboardMetricSelection from '../DashboardMetricSelection'; -import { useTranslation } from 'react-i18next'; -import { PANEL_SIZES } from 'App/constants/panelSizes' - -interface Props extends RouteComponentProps { - history: any; - siteId?: string; - dashboardId?: string; - onMetricAdd?: () => void; -} -function DashboardModal(props: Props) { - const { t } = useTranslation(); - const { history, siteId, dashboardId } = props; - const { dashboardStore } = useStore(); - const selectedWidgetsCount = useObserver( - () => dashboardStore.selectedWidgets.length, - ); - const { hideModal } = useModal(); - const dashboard = useObserver(() => dashboardStore.dashboardInstance); - const loading = useObserver(() => dashboardStore.isSaving); - - const onSave = () => { - dashboardStore - .save(dashboard) - .then(async (syncedDashboard) => { - if (dashboard.exists()) { - await dashboardStore.fetch(dashboard.dashboardId); - } - dashboardStore.selectDashboardById(syncedDashboard.dashboardId); - history.push( - withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId), - ); - }) - .then(hideModal); - }; - - const handleCreateNew = () => { - const path = withSiteId(dashboardMetricCreate(dashboardId), siteId); - props.onMetricAdd(); - history.push(path); - hideModal(); - }; - const isDashboardExists = dashboard.exists(); - - return useObserver(() => ( -
-
-
-
-

- {isDashboardExists - ? t('Add metrics to dashboard') - : t('Create Dashboard')} -

-
-
- {t('Past 7 days data')} -
-
- {!isDashboardExists && ( - <> - -

- {t( - 'Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.', - )} -

- - )} - - -
- - - {selectedWidgetsCount} {t('Metrics')} - -
-
-
- )); -} - -export default withRouter(DashboardModal); diff --git a/frontend/app/components/Dashboard/components/DashboardModal/index.ts b/frontend/app/components/Dashboard/components/DashboardModal/index.ts deleted file mode 100644 index ff9b51745..000000000 --- a/frontend/app/components/Dashboard/components/DashboardModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './DashboardModal'; diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index 85b15b17e..02c7d73fe 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -11,7 +11,6 @@ import withPageTitle from 'HOCs/withPageTitle'; import withReport from 'App/components/hocs/withReport'; import { useHistory } from 'react-router'; import DashboardHeader from '../DashboardHeader'; -import DashboardModal from '../DashboardModal'; import DashboardWidgetGrid from '../DashboardWidgetGrid'; import AiQuery from './AiQuery'; import { PANEL_SIZES } from 'App/constants/panelSizes' @@ -69,15 +68,18 @@ function DashboardView(props: Props) { onAddWidgets(); trimQuery(); } + dashboardStore.resetDensity(); return () => dashboardStore.resetSelectedDashboard(); }, []); useEffect(() => { - const isExists = dashboardStore.getDashboardById(dashboardId); - if (!isExists) { - history.push(withSiteId('/dashboard', siteId)); - } + const isExists = async () => dashboardStore.getDashboardById(dashboardId); + isExists().then((res) => { + if (!res) { + history.push(withSiteId('/dashboard', siteId)); + } + }) }, [dashboardId]); useEffect(() => { @@ -85,18 +87,6 @@ function DashboardView(props: Props) { dashboardStore.fetch(dashboard.dashboardId); }, [dashboard]); - const onAddWidgets = () => { - dashboardStore.initDashboard(dashboard); - showModal( - , - { right: true }, - ); - }; - if (!dashboard) return null; const originStr = window.env.ORIGIN || window.location.origin; @@ -117,7 +107,6 @@ function DashboardView(props: Props) {
diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetric.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetric.tsx deleted file mode 100644 index 4220045a4..000000000 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetric.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import { Loader } from 'UI'; -import { Button } from 'antd'; -import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper'; -import { useStore } from 'App/mstore'; -import { useModal } from 'App/components/Modal'; -import { dashboardMetricCreate, withSiteId } from 'App/routes'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -interface IProps extends RouteComponentProps { - siteId: string; - title: string; - description: string; -} - -function AddMetric({ history, siteId, title, description }: IProps) { - const { t } = useTranslation(); - const [metrics, setMetrics] = React.useState[]>([]); - - const { dashboardStore } = useStore(); - const { hideModal } = useModal(); - - React.useEffect(() => { - dashboardStore?.fetchTemplates(true).then((cats: any[]) => { - const customMetrics = - cats.find((category) => category.name === 'custom')?.widgets || []; - - setMetrics(customMetrics); - }); - }, []); - - const dashboard = dashboardStore.selectedDashboard; - const selectedWidgetIds = dashboardStore.selectedWidgets.map( - (widget: any) => widget.metricId, - ); - const queryParams = new URLSearchParams(location.search); - - const onSave = () => { - if (selectedWidgetIds.length === 0) return; - dashboardStore - .save(dashboard) - .then(async (syncedDashboard: Record) => { - if (dashboard.exists()) { - await dashboardStore.fetch(dashboard.dashboardId); - } - dashboardStore.selectDashboardById(syncedDashboard.dashboardId); - }) - .then(hideModal); - }; - - const onCreateNew = () => { - const path = withSiteId( - dashboardMetricCreate(dashboard.dashboardId), - siteId, - ); - if (!queryParams.has('modal')) history.push('?modal=addMetric'); - history.push(path); - hideModal(); - }; - - return ( -
-
-
-
-

{title}

-
{description}
-
- - -
- -
- {metrics ? ( - metrics.map((metric: any) => ( - dashboardStore.toggleWidgetSelection(metric)} - /> - )) - ) : ( -
{t('No custom metrics created.')}
- )} -
-
- -
-
- {t('Selected')} - {selectedWidgetIds.length} -  {t('out of')}  - {metrics ? metrics.length : 0} -
- -
-
-
- ); -} - -export default withRouter(observer(AddMetric)); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx deleted file mode 100644 index 7f768002e..000000000 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; -import { Icon } from 'UI'; -import { useModal } from 'App/components/Modal'; -import { useStore } from 'App/mstore'; -import cn from 'classnames'; -import AddMetric from './AddMetric'; -import AddPredefinedMetric from './AddPredefinedMetric'; - -interface AddMetricButtonProps { - iconName: 'bar-pencil' | 'grid-check'; - title: string; - description: string; - isPremade?: boolean; - isPopup?: boolean; - onClick: () => void; -} - -function AddMetricButton({ - iconName, - title, - description, - onClick, - isPremade, - isPopup, -}: AddMetricButtonProps) { - return ( -
-
- -
-
-
- {title} -
-
- {description} -
-
-
- ); -} - -interface Props { - siteId: string; - isPopup?: boolean; - onAction?: () => void; -} - -function AddMetricContainer({ siteId, isPopup, onAction }: Props) { - const { showModal } = useModal(); - const { dashboardStore } = useStore(); - - const onAddCustomMetrics = () => { - onAction?.(); - dashboardStore.initDashboard(dashboardStore.selectedDashboard); - showModal( - , - { right: true }, - ); - }; - - const onAddPredefinedMetrics = () => { - onAction?.(); - dashboardStore.initDashboard(dashboardStore.selectedDashboard); - showModal( - , - { right: true }, - ); - }; - - const classes = isPopup - ? 'bg-white border rounded p-4 grid grid-rows-2 gap-4' - : 'bg-white border border-dashed hover:!border-gray-medium rounded p-8 grid grid-cols-2 gap-8'; - return ( -
- - -
- ); -} - -export default observer(AddMetricContainer); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx index ffeb45183..4f34d4384 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx @@ -7,7 +7,7 @@ import { useStore } from 'App/mstore'; import { useModal } from 'App/components/Modal'; import { dashboardMetricCreate, withSiteId } from 'App/routes'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { WidgetCategoryItem } from 'App/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection'; +import { WidgetCategoryItem } from 'App/components/Dashboard/components/WidgetCategoryItem'; import { useTranslation } from 'react-i18next'; interface IProps extends RouteComponentProps { diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index 15241457d..117719eaa 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next'; interface Props { siteId: string; dashboardId: string; - onEditHandler: () => void; id?: string; } diff --git a/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx index 532176702..10bcc278a 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx @@ -56,7 +56,7 @@ function ExcludeFilters(props: Props) { ))}
) : ( - )} diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx index f54c1a17d..03d9a1017 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSelectedFilters/FunnelIssuesSelectedFilters.tsx @@ -21,7 +21,7 @@ function FunnelIssuesSelectedFilters(props: Props) { key={index} closable onClose={() => removeSelectedValue(option.value)} - className="select-none rounded-lg text-base gap-1 bg-indigo-50 flex items-center" + className="select-none rounded-lg text-base gap-1 bg-indigo-lightest flex items-center" > {option.label} diff --git a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx deleted file mode 100644 index a7b989871..000000000 --- a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { withSiteId } from 'App/routes'; - -interface Props extends RouteComponentProps { - list: any; - siteId: any; - selectedList: any; -} -function GridView(props: Props) { - const { siteId, list, selectedList, history } = props; - - const onItemClick = (metricId: number) => { - const path = withSiteId(`/metrics/${metricId}`, siteId); - history.push(path); - }; - - return ( -
- {list.map((metric: any) => ( - - onItemClick(parseInt(metric.metricId))} - /> - - ))} -
- ); -} - -export default withRouter(GridView); diff --git a/frontend/app/components/Dashboard/components/WidgetCategoryItem/WidgetCategoryItem.tsx b/frontend/app/components/Dashboard/components/WidgetCategoryItem/WidgetCategoryItem.tsx new file mode 100644 index 000000000..3872ead56 --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetCategoryItem/WidgetCategoryItem.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useObserver } from 'mobx-react-lite'; +import { Icon } from 'UI'; +import cn from 'classnames'; + +interface IWiProps { + category: Record; + onClick: (category: Record) => void; + isSelected: boolean; + selectedWidgetIds: string[]; +} + +const ICONS: Record = { + errors: 'errors-icon', + performance: 'performance-icon', + resources: 'resources-icon', + overview: null, + custom: null, +}; + +export function WidgetCategoryItem({ + category, + isSelected, + onClick, + selectedWidgetIds, +}: IWiProps) { + const selectedCategoryWidgetsCount = useObserver( + () => + category.widgets.filter((widget: any) => + selectedWidgetIds.includes(widget.metricId), + ).length, + ); + return ( +
onClick(category)} + > +
+ {/* @ts-ignore */} + {ICONS[category.name] && ( + + )} + {category.name} +
+
{category.description}
+ {selectedCategoryWidgetsCount > 0 && ( +
+ {`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`} +
+ )} +
+ ); +} diff --git a/frontend/app/components/Dashboard/components/WidgetCategoryItem/index.ts b/frontend/app/components/Dashboard/components/WidgetCategoryItem/index.ts new file mode 100644 index 000000000..3bad83500 --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetCategoryItem/index.ts @@ -0,0 +1 @@ +export { WidgetCategoryItem } from './WidgetCategoryItem'; diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index abdbc9066..3106177d0 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -54,8 +54,8 @@ function WidgetChart(props: Props) { }); const { isSaved = false, metric, isTemplate } = props; const { dashboardStore, metricStore } = useStore(); - const _metric: any = props.isPreview ? metricStore.instance : props.metric; - const { data } = _metric; + const _metric: any = props.metric; + const data = _metric.data; const { period } = dashboardStore; const { drillDownPeriod } = dashboardStore; const { drillDownFilter } = dashboardStore; @@ -158,7 +158,7 @@ function WidgetChart(props: Props) { }, 4000); dashboardStore .fetchMetricChartData(metric, payload, isSaved, period, isComparison) - .then((res: any) => { + .then((res) => { if (isComparison) setCompData(res); clearTimeout(tm); setStale(false); @@ -181,10 +181,10 @@ function WidgetChart(props: Props) { } prevMetricRef.current = _metric; const timestmaps = drillDownPeriod.toTimestamps(); - const density = props.isPreview ? metric.density : dashboardStore.selectedDensity + const density = dashboardStore.selectedDensity; const payload = isSaved - ? { ...metricParams, density } - : { ...params, ...timestmaps, ..._metric.toJson(), density }; + ? { ...metricParams, density } + : { ...params, ...timestmaps, ..._metric.toJson(), density }; debounceRequest( _metric, payload, @@ -561,7 +561,7 @@ function WidgetChart(props: Props) { } console.log('Unknown metric type', metricType); return
{t('Unknown metric type')}
; - }, [data, compData, enabledRows, _metric]); + }, [data, compData, enabledRows, _metric, data]); const showTable = _metric.metricType === TIMESERIES && diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx index ad11b6703..7a3a2e915 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/WidgetDateRange.tsx @@ -6,7 +6,6 @@ import { Space } from 'antd'; import { CUSTOM_RANGE, DATE_RANGE_COMPARISON_OPTIONS } from 'App/dateRange'; import Period from 'Types/app/period'; import RangeGranularity from './RangeGranularity'; -import { useTranslation } from 'react-i18next'; function WidgetDateRange({ label = 'Time Range', diff --git a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx index d8472bc76..b3b24118f 100644 --- a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx @@ -1,13 +1,12 @@ import React, { useRef, lazy } from 'react'; import cn from 'classnames'; -import { ItemMenu, TextEllipsis } from 'UI'; +import { TextEllipsis } from 'UI'; import { useDrag, useDrop } from 'react-dnd'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withSiteId, dashboardMetricDetails } from 'App/routes'; import { FilterKey } from 'App/types/filter/filterType'; -import { TIMESERIES } from 'App/constants/card'; import TemplateOverlay from './TemplateOverlay'; const WidgetChart = lazy( @@ -45,7 +44,6 @@ function WidgetWrapper(props: Props & RouteComponentProps) { isGridView = false, } = props; const { widget } = props; - const isTimeSeries = widget.metricType === TIMESERIES; const isPredefined = widget.metricType === 'predefined'; const dashboard = dashboardStore.selectedDashboard; @@ -73,13 +71,6 @@ function WidgetWrapper(props: Props & RouteComponentProps) { }), }); - const onDelete = async () => { - dashboardStore.deleteDashboardWidget( - dashboard?.dashboardId!, - widget.widgetId, - ); - }; - const onChartClick = () => { if (!isSaved || isPredefined) return; diff --git a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapperNew.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapperNew.tsx index 1b8bf5cb9..a6569da29 100644 --- a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapperNew.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapperNew.tsx @@ -38,7 +38,7 @@ interface Props { isSaved?: boolean; } -function WidgetWrapperNew(props: Props & RouteComponentProps) { +function WidgetWrapperDashboard(props: Props & RouteComponentProps) { const { dashboardStore, metricStore } = useStore(); const { isWidget = false, @@ -178,4 +178,4 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) { ); } -export default withRouter(observer(WidgetWrapperNew)); +export default withRouter(observer(WidgetWrapperDashboard)); diff --git a/frontend/app/components/Kai/KaiChat.tsx b/frontend/app/components/Kai/KaiChat.tsx index 904181460..17bb4c820 100644 --- a/frontend/app/components/Kai/KaiChat.tsx +++ b/frontend/app/components/Kai/KaiChat.tsx @@ -1,16 +1,15 @@ import React from 'react'; import { useModal } from 'App/components/Modal'; -import { MessagesSquare, Trash } from 'lucide-react'; import ChatHeader from './components/ChatHeader'; import { PANEL_SIZES } from 'App/constants/panelSizes'; import ChatLog from './components/ChatLog'; import IntroSection from './components/IntroSection'; -import { useQuery } from '@tanstack/react-query'; import { kaiService } from 'App/services'; import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { useHistory, useLocation } from 'react-router-dom'; +import ChatsModal from './components/ChatsModal'; function KaiChat() { const { userStore, projectsStore } = useStore(); @@ -99,7 +98,9 @@ function KaiChat() { }; return (
-
+
void; - projectId: string; -}) { - const { - data = [], - isPending, - refetch, - } = useQuery({ - queryKey: ['kai', 'chats', projectId], - queryFn: () => kaiService.getKaiChats(projectId), - staleTime: 1000 * 60, - }); - - const onDelete = async (id: string) => { - try { - await kaiService.deleteKaiChat(projectId, id); - } catch (e) { - toast.error("Something wen't wrong. Please try again later."); - } - refetch(); - }; - return ( -
-
- - Chats -
- {isPending ? ( -
Loading chats...
- ) : ( -
- {data.map((chat) => ( -
-
-
onSelect(chat.thread_id, chat.title)} - className="cursor-pointer hover:underline truncate" - > - {chat.title} -
-
-
onDelete(chat.thread_id)} - className="cursor-pointer opacity-0 group-hover:opacity-100 rounded-r h-full px-2 flex items-center group-hover:bg-active-blue" - > - -
-
- ))} -
- )} -
- ); -} - export default observer(KaiChat); diff --git a/frontend/app/components/Kai/KaiService.ts b/frontend/app/components/Kai/KaiService.ts index 045553b4e..984f7e3f4 100644 --- a/frontend/app/components/Kai/KaiService.ts +++ b/frontend/app/components/Kai/KaiService.ts @@ -3,7 +3,7 @@ import AiService from '@/services/AiService'; export default class KaiService extends AiService { getKaiChats = async ( projectId: string, - ): Promise<{ title: string; thread_id: string }[]> => { + ): Promise<{ title: string; thread_id: string; datetime: string }[]> => { const r = await this.client.get(`/kai/${projectId}/chats`); if (!r.ok) { throw new Error('Failed to fetch chats'); @@ -31,8 +31,11 @@ export default class KaiService extends AiService { role: string; content: string; message_id: any; - duration?: number; + duration: number; feedback: boolean | null; + supports_visualization: boolean; + chart: string; + chart_data: string; }[] > => { const r = await this.client.get(`/kai/${projectId}/chats/${threadId}`); @@ -77,4 +80,46 @@ export default class KaiService extends AiService { const data = await r.json(); return data; }; + + getMsgChart = async ( + messageId: string, + projectId: string, + ): Promise<{ filters: any[]; chart: string; eventsOrder: string }> => { + const r = await this.client.get( + `/kai/${projectId}/chats/data/${messageId}`, + ); + if (!r.ok) { + throw new Error('Failed to fetch chart data'); + } + const data = await r.json(); + return data; + }; + + saveChartData = async ( + messageId: string, + projectId: string, + chartData: any, + ) => { + const r = await this.client.post( + `/kai/${projectId}/chats/data/${messageId}`, + { + chart_data: JSON.stringify(chartData), + }, + ); + if (!r.ok) { + throw new Error('Failed to save chart data'); + } + + const data = await r.json(); + return data; + }; + + checkUsage = async (): Promise<{ total: number; used: number }> => { + const r = await this.client.get(`/kai/usage`); + if (!r.ok) { + throw new Error('Failed to fetch usage'); + } + const data = await r.json(); + return data; + }; } diff --git a/frontend/app/components/Kai/KaiStore.ts b/frontend/app/components/Kai/KaiStore.ts index 6c84d5a62..d4d85526a 100644 --- a/frontend/app/components/Kai/KaiStore.ts +++ b/frontend/app/components/Kai/KaiStore.ts @@ -1,18 +1,45 @@ import { makeAutoObservable, runInAction } from 'mobx'; -import { BotChunk, ChatManager, Message } from './SocketManager'; +import { BotChunk, ChatManager } from './SocketManager'; import { kaiService as aiService, kaiService } from 'App/services'; import { toast } from 'react-toastify'; +import Widget from 'App/mstore/types/widget'; + +export interface Message { + text: string; + isUser: boolean; + messageId: string; + /** filters to get chart */ + chart: string; + /** chart data */ + chart_data: string; + supports_visualization: boolean; + feedback: boolean | null; + duration: number; +} +export interface SentMessage + extends Omit< + Message, + 'duration' | 'feedback' | 'chart' | 'supports_visualization' + > { + replace: boolean; +} class KaiStore { chatManager: ChatManager | null = null; processingStage: BotChunk | null = null; - messages: Message[] = []; + messages: Array = []; queryText = ''; loadingChat = false; - replacing = false; + replacing: string | null = null; + usage = { + total: 0, + used: 0, + percent: 0, + }; constructor() { makeAutoObservable(this); + this.checkUsage(); } get lastHumanMessage() { @@ -67,9 +94,9 @@ class KaiStore { this.messages.push(message); }; - editMessage = (text: string) => { + editMessage = (text: string, messageId: string) => { this.setQueryText(text); - this.setReplacing(true); + this.setReplacing(messageId); }; replaceAtIndex = (message: Message, index: number) => { @@ -100,6 +127,9 @@ class KaiStore { messageId: m.message_id, duration: m.duration, feedback: m.feedback, + chart: m.chart, + supports_visualization: m.supports_visualization, + chart_data: m.chart_data, }; }), ); @@ -122,21 +152,25 @@ class KaiStore { console.error('No token found'); return; } + this.checkUsage(); this.chatManager = new ChatManager({ ...settings, token }); this.chatManager.setOnMsgHook({ msgCallback: (msg) => { - if ('state' in msg) { + if (msg.type === 'state') { if (msg.state === 'running') { this.setProcessingStage({ content: 'Processing your request...', stage: 'chart', messageId: Date.now().toPrecision(), duration: msg.start_time ? Date.now() - msg.start_time : 0, + type: 'chunk', + supports_visualization: false, }); } else { this.setProcessingStage(null); } - } else { + } + if (msg.type === 'chunk') { if (msg.stage === 'start') { this.setProcessingStage({ ...msg, @@ -153,7 +187,11 @@ class KaiStore { messageId: msg.messageId, duration: msg.duration, feedback: null, + chart: '', + supports_visualization: msg.supports_visualization, + chart_data: '', }; + this.bumpUsage(); this.addMessage(msgObj); this.setProcessingStage(null); } @@ -167,13 +205,18 @@ class KaiStore { } }; - setReplacing = (replacing: boolean) => { + setReplacing = (replacing: string | null) => { this.replacing = replacing; }; + bumpUsage = () => { + this.usage.used += 1; + this.usage.percent = (this.usage.used / this.usage.total) * 100; + }; + sendMessage = (message: string) => { if (this.chatManager) { - this.chatManager.sendMessage(message, this.replacing); + this.chatManager.sendMessage(message, !!this.replacing); } if (this.replacing) { console.log( @@ -197,6 +240,9 @@ class KaiStore { messageId: Date.now().toString(), feedback: null, duration: 0, + supports_visualization: false, + chart: '', + chart_data: '', }); }; @@ -251,6 +297,64 @@ class KaiStore { this.chatManager = null; } }; + + getMessageChart = async (msgId: string, projectId: string) => { + this.setProcessingStage({ + content: 'Generating visualization...', + stage: 'chart', + messageId: msgId, + duration: 0, + type: 'chunk', + supports_visualization: false, + }); + try { + const filters = await kaiService.getMsgChart(msgId, projectId); + const data = { + metricId: undefined, + dashboardId: undefined, + widgetId: undefined, + metricOf: undefined, + metricType: undefined, + metricFormat: undefined, + viewType: undefined, + name: 'Kai Visualization', + series: [ + { + name: 'Kai Visualization', + filter: { + eventsOrder: filters.eventsOrder, + filters: filters.filters, + }, + }, + ], + }; + const metric = new Widget().fromJson(data); + return metric; + } catch (e) { + console.error(e); + throw new Error('Failed to generate visualization'); + } finally { + this.setProcessingStage(null); + } + }; + + getParsedChart = (data: string) => { + const parsedData = JSON.parse(data); + return new Widget().fromJson(parsedData); + }; + + checkUsage = async () => { + try { + const { total, used } = await kaiService.checkUsage(); + this.usage = { + total, + used, + percent: (used / total) * 100, + }; + } catch (e) { + console.error(e); + } + }; } export const kaiStore = new KaiStore(); diff --git a/frontend/app/components/Kai/SocketManager.ts b/frontend/app/components/Kai/SocketManager.ts index e34ccc155..7fcda3171 100644 --- a/frontend/app/components/Kai/SocketManager.ts +++ b/frontend/app/components/Kai/SocketManager.ts @@ -1,4 +1,5 @@ import io from 'socket.io-client'; +import { toast } from 'react-toastify'; export class ChatManager { socket: ReturnType; @@ -41,6 +42,9 @@ export class ChatManager { console.log('Disconnected from server'); }); socket.on('error', (err) => { + if (err.message) { + toast.error(err.message); + } console.error('Socket error:', err); }); @@ -74,12 +78,12 @@ export class ChatManager { titleCallback, }: { msgCallback: ( - msg: BotChunk | { state: string; type: 'state'; start_time?: number }, + msg: StateEvent | BotChunk, ) => void; titleCallback: (title: string) => void; }) => { this.socket.on('chunk', (msg: BotChunk) => { - msgCallback(msg); + msgCallback({ ...msg, type: 'chunk' }); }); this.socket.on('title', (msg: { content: string }) => { titleCallback(msg.content); @@ -105,16 +109,13 @@ export interface BotChunk { stage: 'start' | 'chart' | 'final' | 'title'; content: string; messageId: string; - duration?: number; -} -export interface Message { - text: string; - isUser: boolean; - messageId: string; - duration?: number; - feedback: boolean | null; + duration: number; + supports_visualization: boolean; + type: 'chunk' } -export interface SentMessage extends Message { - replace: boolean; +interface StateEvent { + state: string; + start_time?: number; + type: 'state'; } diff --git a/frontend/app/components/Kai/components/ChatHeader.tsx b/frontend/app/components/Kai/components/ChatHeader.tsx index 756aa9767..ae3cb4068 100644 --- a/frontend/app/components/Kai/components/ChatHeader.tsx +++ b/frontend/app/components/Kai/components/ChatHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Icon } from 'UI'; import { MessagesSquare, ArrowLeft } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; function ChatHeader({ openChats = () => {}, @@ -11,6 +12,7 @@ function ChatHeader({ openChats?: () => void; chatTitle: string | null; }) { + const { t } = useTranslation(); return (
{goBack ? (
-
Back
+
{t('Back')}
) : null}
{chatTitle ? ( -
{chatTitle}
+
+ {chatTitle} +
) : ( <> @@ -38,14 +44,14 @@ function ChatHeader({ )}
-
- -
Chats
+
+
+ +
{t('Chats')}
+
); diff --git a/frontend/app/components/Kai/components/ChatInput.tsx b/frontend/app/components/Kai/components/ChatInput.tsx index 94bfeb89f..237b79d6c 100644 --- a/frontend/app/components/Kai/components/ChatInput.tsx +++ b/frontend/app/components/Kai/components/ChatInput.tsx @@ -1,55 +1,114 @@ -import React from 'react' -import { Button, Input } from "antd"; -import { SendHorizonal, OctagonX } from "lucide-react"; -import { kaiStore } from "../KaiStore"; -import { observer } from "mobx-react-lite"; +import React from 'react'; +import { Button, Input, Tooltip } from 'antd'; +import { SendHorizonal, OctagonX } from 'lucide-react'; +import { kaiStore } from '../KaiStore'; +import { observer } from 'mobx-react-lite'; +import Usage from './Usage'; -function ChatInput({ isLoading, onSubmit, threadId }: { isLoading?: boolean, onSubmit: (str: string) => void, threadId: string }) { - const inputRef = React.useRef(null); +function ChatInput({ + isLoading, + onSubmit, + threadId, +}: { + isLoading?: boolean; + onSubmit: (str: string) => void; + threadId: string; +}) { + const inputRef = React.useRef(null); + const usage = kaiStore.usage; + const limited = usage.percent >= 100; const inputValue = kaiStore.queryText; - const isProcessing = kaiStore.processingStage !== null + const isProcessing = kaiStore.processingStage !== null; const setInputValue = (text: string) => { - kaiStore.setQueryText(text) - } + kaiStore.setQueryText(text); + }; const submit = () => { + if (limited) { + return; + } if (isProcessing) { - const settings = { projectId: '2325', userId: '0', threadId, }; - void kaiStore.cancelGeneration(settings) + const settings = { projectId: '2325', userId: '0', threadId }; + void kaiStore.cancelGeneration(settings); } else { if (inputValue.length > 0) { - onSubmit(inputValue) - setInputValue('') + onSubmit(inputValue); + setInputValue(''); } } - } + }; + + const cancelReplace = () => { + setInputValue(''); + kaiStore.setReplacing(null); + }; React.useEffect(() => { if (inputRef.current) { - inputRef.current.focus() + inputRef.current.focus(); } - }, [inputValue]) + }, [inputValue]); + + const isReplacing = kaiStore.replacing !== null; return ( - setInputValue(e.target.value)} - suffix={ -
+ ); } -export default observer(ChatInput) +export default observer(ChatInput); diff --git a/frontend/app/components/Kai/components/ChatLog.tsx b/frontend/app/components/Kai/components/ChatLog.tsx index 5609642b5..9b015ad22 100644 --- a/frontend/app/components/Kai/components/ChatLog.tsx +++ b/frontend/app/components/Kai/components/ChatLog.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ChatInput from './ChatInput'; -import { ChatMsg, ChatNotice } from './ChatMsg'; +import ChatMsg, { ChatNotice } from './ChatMsg'; import { Loader } from 'UI'; import { kaiStore } from '../KaiStore'; import { observer } from 'mobx-react-lite'; @@ -61,17 +61,14 @@ function ChatLog({ >
{messages.map((msg, index) => ( - + + + ))} {processingStage ? ( (null); + const [loadingChart, setLoadingChart] = React.useState(false); + const { + text, + isUser, + messageId, + duration, + feedback, + supports_visualization, + chart_data, + } = message; + const isEditing = kaiStore.replacing && messageId === kaiStore.replacing; const [isProcessing, setIsProcessing] = React.useState(false); const bodyRef = React.useRef(null); - const onRetry = () => { - kaiStore.editMessage(text); + const onEdit = () => { + kaiStore.editMessage(text, messageId); }; + const onCancelEdit = () => { + kaiStore.setQueryText(''); + kaiStore.setReplacing(null); + } const onFeedback = (feedback: 'like' | 'dislike', messageId: string) => { kaiStore.sendMsgFeedback(feedback, messageId, siteId); }; @@ -74,6 +88,25 @@ export function ChatMsg({ setIsProcessing(false); }); }; + + React.useEffect(() => { + if (chart_data) { + const metric = kaiStore.getParsedChart(chart_data); + setMetric(metric); + } + }, [chart_data]); + + const getChart = async () => { + try { + setLoadingChart(true); + const metric = await kaiStore.getMessageChart(messageId, siteId); + setMetric(metric); + } catch (e) { + toast.error(e.message); + } finally { + setLoadingChart(false); + } + }; return (
{userName} @@ -92,28 +125,54 @@ export function ChatMsg({ ) : (
)}
-
+
{text}
+ {metric ? ( +
+ +
+ ) : null} {isUser ? ( - isLast ? ( + <>
-
Edit
+
{t('Edit')}
- ) : null +
+
{t('Cancel')}
+
+ ) : (
{duration ? : null} @@ -132,6 +191,15 @@ export function ChatMsg({ > + {supports_visualization ? ( + + + + ) : null} bodyRef.current?.innerHTML} content={text} @@ -215,3 +283,5 @@ function MsgDuration({ duration }: { duration: number }) {
); } + +export default observer(ChatMsg); diff --git a/frontend/app/components/Kai/components/ChatsModal.tsx b/frontend/app/components/Kai/components/ChatsModal.tsx new file mode 100644 index 000000000..a39d358f0 --- /dev/null +++ b/frontend/app/components/Kai/components/ChatsModal.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { splitByDate } from '../utils'; +import { useQuery } from '@tanstack/react-query'; +import { MessagesSquare, Trash } from 'lucide-react'; +import { kaiService } from 'App/services'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; +import { kaiStore } from '../KaiStore'; +import { observer } from 'mobx-react-lite'; + +function ChatsModal({ + onSelect, + projectId, +}: { + onSelect: (threadId: string, title: string) => void; + projectId: string; +}) { + const { t } = useTranslation(); + const { usage } = kaiStore; + const { + data = [], + isPending, + refetch, + } = useQuery({ + queryKey: ['kai', 'chats', projectId], + queryFn: () => kaiService.getKaiChats(projectId), + staleTime: 1000 * 60, + }); + + React.useEffect(() => { + kaiStore.checkUsage(); + }, []); + + const datedCollections = React.useMemo(() => { + return data.length ? splitByDate(data) : []; + }, [data.length]); + + const onDelete = async (id: string) => { + try { + await kaiService.deleteKaiChat(projectId, id); + } catch (e) { + toast.error("Something wen't wrong. Please try again later."); + } + refetch(); + }; + return ( +
+
+ + {t('Chats')} +
+ {usage.percent > 80 ? ( +
+ {t('You have used {{used}} out of {{total}} daily requests', { + used: usage.used, + total: usage.total, + })} +
+ ) : null} + {isPending ? ( +
{t('Loading chats')}...
+ ) : ( +
+ {datedCollections.map((col) => ( + + ))} +
+ )} +
+ ); +} + +function ChatCollection({ + data, + onSelect, + onDelete, + date, +}: { + data: { title: string; thread_id: string }[]; + onSelect: (threadId: string, title: string) => void; + onDelete: (threadId: string) => void; + date: string; +}) { + return ( +
+
{date}
+ +
+ ); +} + +function ChatsList({ + data, + onSelect, + onDelete, +}: { + data: { title: string; thread_id: string }[]; + onSelect: (threadId: string, title: string) => void; + onDelete: (threadId: string) => void; +}) { + return ( +
+ {data.map((chat) => ( +
+
+
onSelect(chat.thread_id, chat.title)} + className="cursor-pointer hover:underline truncate" + > + {chat.title} +
+
+
onDelete(chat.thread_id)} + className="cursor-pointer opacity-0 group-hover:opacity-100 rounded-r min-h-7 h-full px-2 flex items-center group-hover:bg-active-blue" + > + +
+
+ ))} +
+ ); +} + +export default observer(ChatsModal); diff --git a/frontend/app/components/Kai/components/Usage.tsx b/frontend/app/components/Kai/components/Usage.tsx new file mode 100644 index 000000000..9870c2b87 --- /dev/null +++ b/frontend/app/components/Kai/components/Usage.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { kaiStore } from '../KaiStore'; +import { observer } from 'mobx-react-lite'; +import { Progress, Tooltip } from 'antd'; +const getUsageColor = (percent: number) => { + return 'disabled-text'; +}; + +function Usage() { + const { usage } = kaiStore; + const color = getUsageColor(usage.percent); + + if (usage.total === 0) { + return null; + } + return ( +
+ + + +
+ ); +} + +export default observer(Usage); diff --git a/frontend/app/components/Kai/utils.ts b/frontend/app/components/Kai/utils.ts new file mode 100644 index 000000000..ea581217e --- /dev/null +++ b/frontend/app/components/Kai/utils.ts @@ -0,0 +1,36 @@ +import { DateTime } from 'luxon'; + +type DatedEntry = { + date: string; + entries: { datetime: string }[]; +} + +export function splitByDate(entries: { datetime: string }[]) { + const today = DateTime.now().startOf('day'); + const yesterday = today.minus({ days: 1 }); + + const result: DatedEntry[] = [ + { date: 'Today', entries: [] }, + { date: 'Yesterday', entries: [] }, + ]; + + entries.forEach((ent) => { + const entryDate = DateTime.fromISO(ent.datetime).startOf('day'); + + if (entryDate.toMillis() === today.toMillis()) { + result[0].entries.push(ent); + } else if (entryDate.toMillis() === yesterday.toMillis()) { + result[1].entries.push(ent); + } else { + const date = entryDate.toFormat('dd LLL, yyyy') + const existingEntry = result.find((r) => r.date === date); + if (existingEntry) { + existingEntry.entries.push(ent); + } else { + result.push({ entries: [ent], date }); + } + } + }); + + return result.filter((r) => r.entries.length > 0); +} diff --git a/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx b/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx index 6bc61da82..ddf267669 100644 --- a/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx +++ b/frontend/app/components/Session/Player/ClipPlayer/ClipPlayerContent.tsx @@ -54,11 +54,22 @@ function ClipPlayerContent(props: Props) { if (!playerContext.player) return null; + const outerHeight = props.isHighlight ? 556 + 39 : 556; + const innerHeight = props.isHighlight ? 504 + 39 : 504; return (
-
+
diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx index 5112752a7..3d9a8576e 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx @@ -199,6 +199,8 @@ function BottomBlock({ panelHeight, block }: { panelHeight: number; block: numbe return ; case LONG_TASK: return ; + case OVERVIEW: + return ; default: return null; } diff --git a/frontend/app/components/Session/Player/SharedComponents/BackendLogs/StatusMessages.tsx b/frontend/app/components/Session/Player/SharedComponents/BackendLogs/StatusMessages.tsx index 73ec560b0..6d28638c5 100644 --- a/frontend/app/components/Session/Player/SharedComponents/BackendLogs/StatusMessages.tsx +++ b/frontend/app/components/Session/Player/SharedComponents/BackendLogs/StatusMessages.tsx @@ -13,6 +13,7 @@ export function LoadingFetch({ provider }: { provider: string }) {
{t('Fetching logs from')} +   {provider} ...
diff --git a/frontend/app/components/Session_/Highlight/HighlightPanel.tsx b/frontend/app/components/Session_/Highlight/HighlightPanel.tsx index 8e524a689..b2277ffc5 100644 --- a/frontend/app/components/Session_/Highlight/HighlightPanel.tsx +++ b/frontend/app/components/Session_/Highlight/HighlightPanel.tsx @@ -206,7 +206,7 @@ function HighlightPanel({ onClose }: { onClose: () => void }) { addTag(tag)} key={tag} - className="cursor-pointer rounded-lg hover:bg-indigo-50 mr-0" + className="cursor-pointer rounded-lg hover:bg-indigo-lightest mr-0" color={tagProps[tag]} bordered={false} > diff --git a/frontend/app/components/Session_/Subheader.tsx b/frontend/app/components/Session_/Subheader.tsx index 3cf6cfe28..313b346ca 100644 --- a/frontend/app/components/Session_/Subheader.tsx +++ b/frontend/app/components/Session_/Subheader.tsx @@ -142,7 +142,7 @@ function SubHeader(props) { currentLocation={currentLocation} version={currentSession?.trackerVersion ?? ''} containerStyle={{ position: 'relative', left: 0, top: 0, transform: 'none', zIndex: 10 }} - trackerWarnStyle={{ backgroundColor: 'var(--color-yellow)' }} + trackerWarnStyle={{ backgroundColor: 'var(--color-yellow)', color: 'black' }} virtualElsFailed={showVModeBadge} onVMode={onVMode} /> diff --git a/frontend/app/components/Session_/WarnBadge.tsx b/frontend/app/components/Session_/WarnBadge.tsx index c00879178..dda00d758 100644 --- a/frontend/app/components/Session_/WarnBadge.tsx +++ b/frontend/app/components/Session_/WarnBadge.tsx @@ -188,7 +188,7 @@ const WarnBadge = React.memo( className="py-1 ml-3 cursor-pointer" onClick={() => closeWarning(1)} > - +
) : null} diff --git a/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx b/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx index a5c83ee34..695719529 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/AccessModal.tsx @@ -121,7 +121,7 @@ function AccessModal() {
{t('Link for internal team members')}
-
+
{spotLink}
@@ -155,7 +155,7 @@ function AccessModal() {
{t('Anyone with the following link can access this Spot')}
-
+
{spotLink}
diff --git a/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx b/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx index ab1a3c3be..d534d9a34 100644 --- a/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx +++ b/frontend/app/components/Spots/SpotPlayer/components/SpotVideoContainer.tsx @@ -201,7 +201,7 @@ function SpotVideoContainer({ > {processingState === ProcessingState.Processing ? (
-
+
{spot.duration}
@@ -213,7 +213,7 @@ export function GridItem({ return (
- +
diff --git a/frontend/app/components/Spots/SpotsList/SpotsListHeader.tsx b/frontend/app/components/Spots/SpotsList/SpotsListHeader.tsx index de0f1f0b0..5041857be 100644 --- a/frontend/app/components/Spots/SpotsList/SpotsListHeader.tsx +++ b/frontend/app/components/Spots/SpotsList/SpotsListHeader.tsx @@ -64,10 +64,11 @@ const SpotsListHeader = observer( type="text" onClick={onClearSelection} className="mr-2 px-3" + size='small' > {t('Clear')} - diff --git a/frontend/app/components/UsabilityTesting/TestOverview.tsx b/frontend/app/components/UsabilityTesting/TestOverview.tsx index 0d2e822b8..0c47ad2da 100644 --- a/frontend/app/components/UsabilityTesting/TestOverview.tsx +++ b/frontend/app/components/UsabilityTesting/TestOverview.tsx @@ -408,7 +408,7 @@ const TaskSummary = observer(() => { {t('Task Summary')} {uxtestingStore.taskStats.length ? ( -
+
{t('Average completion time of all tasks:')} diff --git a/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx b/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx index 2b138537d..8fa99e940 100644 --- a/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx +++ b/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx @@ -23,7 +23,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{t('Name')}
@@ -35,7 +35,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{t('Request Method')}
{resource.method} @@ -49,7 +49,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) { @@ -61,7 +61,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{t('Type')}
{resource.type} @@ -72,7 +72,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{t('Size')}
{formatBytes(resource.decodedBodySize)} @@ -84,7 +84,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{t('Duration')}
{_duration} {t('ms')} @@ -96,7 +96,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{t('Time')}
{timestamp} diff --git a/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTimings.tsx b/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTimings.tsx index 55739c215..82cb4164c 100644 --- a/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTimings.tsx +++ b/frontend/app/components/shared/FetchDetailsModal/components/FetchTabs/FetchTimings.tsx @@ -128,7 +128,7 @@ function FetchTimings({ timings }: { timings: Record }) { key={index} className="grid grid-cols-12 items-center gap-2 space-y-2" > -
+
@@ -160,11 +160,11 @@ function FetchTimings({ timings }: { timings: Record }) { ))}
-
+
Total:
-
+
{formatTime(total)}{' '} {isAdjusted ? ( diff --git a/frontend/app/layout/SupportModal.tsx b/frontend/app/layout/SupportModal.tsx index fffca5ea5..c6013e3dd 100644 --- a/frontend/app/layout/SupportModal.tsx +++ b/frontend/app/layout/SupportModal.tsx @@ -29,7 +29,7 @@ function SupportModal(props: Props) { className="!bg-stone-50" >
-
+
@@ -61,7 +61,7 @@ function SupportModal(props: Props) {
-
+
@@ -92,7 +92,7 @@ function SupportModal(props: Props) {
-
+
diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index ebf8b118b..a36393f29 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -14,70 +14,39 @@ interface DashboardFilter { } export default class DashboardStore { siteId: any = null; - dashboards: Dashboard[] = []; - selectedDashboard: Dashboard | null = null; - dashboardInstance: Dashboard = new Dashboard(); - selectedWidgets: Widget[] = []; - currentWidget: Widget = new Widget(); - widgetCategories: any[] = []; - widgets: Widget[] = []; - period: Record = Period({ rangeName: LAST_24_HOURS }); - drillDownFilter: Filter = new Filter(); - comparisonFilter: Filter = new Filter(); - drillDownPeriod: Record = Period({ rangeName: LAST_24_HOURS }); - selectedDensity: number = 7; - comparisonPeriods: Record = {}; - startTimestamp: number = 0; - endTimestamp: number = 0; - pendingRequests: number = 0; - filter: DashboardFilter = { showMine: false, query: '' }; - // Metrics metricsPage: number = 1; - metricsPageSize: number = 10; - metricsSearch: string = ''; - // Loading states isLoading: boolean = false; - isSaving: boolean = false; - isDeleting: boolean = false; - loadingTemplates: boolean = false; - fetchingDashboard: boolean = false; - sessionsLoading: boolean = false; - showAlertModal: boolean = false; - // Pagination page: number = 1; - pageSize: number = 10; - dashboardsSearch: string = ''; - sort: any = { by: 'desc' }; constructor() { @@ -94,6 +63,10 @@ export default class DashboardStore { ) } + resetDensity = () => { + this.createDensity(this.period.getDuration()); + } + createDensity = (duration: number) => { const densityOpts = calculateGranularities(duration); const defaultOption = densityOpts[densityOpts.length - 2]; @@ -212,6 +185,7 @@ export default class DashboardStore { this.currentWidget.update(widget); } + listFetched = false; fetchList(): Promise { this.isLoading = true; @@ -226,6 +200,7 @@ export default class DashboardStore { }) .finally(() => { runInAction(() => { + this.listFetched = true; this.isLoading = false; }); }); @@ -388,7 +363,24 @@ export default class DashboardStore { new Dashboard(); }; - getDashboardById = (dashboardId: string) => { + getDashboardById = async (dashboardId: string) => { + if (!this.listFetched) { + const maxWait = (5*1000)/250; + let count = 0; + await new Promise((resolve) => { + const interval = setInterval(() => { + if (this.listFetched) { + clearInterval(interval); + resolve(true); + } + if (count >= maxWait) { + clearInterval(interval); + resolve(false); + } + count++; + }, 250); + }) + } const dashboard = this.dashboards.find((d) => d.dashboardId == dashboardId); if (dashboard) { @@ -522,7 +514,6 @@ export default class DashboardStore { isComparison?: boolean, ): Promise { period = period.toTimestamps(); - const { density } = data; const params = { ...period, ...data, key: metric.predefinedKey }; if (!isComparison && metric.page && metric.limit) { @@ -547,7 +538,8 @@ export default class DashboardStore { params, isSaved ); - resolve(metric.setData(data, period, isComparison, density)); + const res = metric.setData(data, period, isComparison, data.density) + resolve(res); } catch (error) { reject(error); } finally { diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 2f0ae0842..04975e425 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -396,9 +396,10 @@ export default class Widget { _data.funnel = new Funnel().fromJSON(data); } else if (this.metricType === TABLE) { const count = data[0]['count']; - _data['values'] = data[0]['values'].map((s: any) => + const vals = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, count, this.metricOf), ); + _data['values'] = vals _data['total'] = data[0]['total']; } else { if (data.hasOwnProperty('chart')) { diff --git a/frontend/app/mstore/userStore.ts b/frontend/app/mstore/userStore.ts index 7412b0968..ccbfb9e65 100644 --- a/frontend/app/mstore/userStore.ts +++ b/frontend/app/mstore/userStore.ts @@ -13,45 +13,25 @@ import i18next, { TFunction } from 'i18next'; class UserStore { t: TFunction; - list: User[] = []; - instance: User | null = null; - page: number = 1; - pageSize: number = 10; - searchQuery: string = ''; - modifiedCount: number = 0; - loading: boolean = false; - saving: boolean = false; - limits: any = {}; - initialDataFetched: boolean = false; - account = new Account(); - siteId: string | null = null; - passwordRequestError: boolean = false; - passwordErrors: string[] = []; - tenants: any[] = []; - onboarding: boolean = false; - sites: any[] = []; - jwt: string | null = null; - spotJwt: string | null = null; - errors: any[] = []; loginRequest = { @@ -119,7 +99,11 @@ class UserStore { } get isSSOSupported() { - return this.isEnterprise || this.account?.edition === 'msaas' || this.authStore.authDetails?.edition === 'msaas'; + return ( + this.isEnterprise || + this.account?.edition === 'msaas' || + this.authStore.authDetails?.edition === 'msaas' + ); } get isLoggedIn() { @@ -242,7 +226,7 @@ class UserStore { resolve(response); }) .catch(async (e) => { - toast.error(e.message || this.t("Failed to save user's data.")); + toast.error(e.message || this.t("Failed to save user's data.")); reject(e); }) .finally(() => { @@ -383,8 +367,12 @@ class UserStore { }); } catch (error) { const inUse = error.message.includes('already in use'); - const inUseMsg = this.t('An account with this email already exists. Please log in or use a different email address.') - const genericMsg = this.t('Error signing up; please check your data and try again') + const inUseMsg = this.t( + 'An account with this email already exists. Please log in or use a different email address.', + ); + const genericMsg = this.t( + 'Error signing up; please check your data and try again', + ); runInAction(() => { this.signUpRequest = { loading: false, @@ -411,7 +399,9 @@ class UserStore { this.spotJwt = data.spotJwt; }); } catch (e) { - toast.error(e.message || this.t('Error resetting your password; please try again')); + toast.error( + e.message || this.t('Error resetting your password; please try again'), + ); throw e; } finally { runInAction(() => { diff --git a/frontend/app/player-ui/PlayButton.tsx b/frontend/app/player-ui/PlayButton.tsx index 123793f47..3eec984a8 100644 --- a/frontend/app/player-ui/PlayButton.tsx +++ b/frontend/app/player-ui/PlayButton.tsx @@ -44,7 +44,7 @@ export function PlayButton({ togglePlay, iconSize, state }: IProps) { >
diff --git a/frontend/app/styles/colors-autogen.css b/frontend/app/styles/colors-autogen.css index 148df49f3..c8523449f 100644 --- a/frontend/app/styles/colors-autogen.css +++ b/frontend/app/styles/colors-autogen.css @@ -28,6 +28,7 @@ .fill-green-dark { fill: var(--color-green-dark) } .fill-red { fill: var(--color-red) } .fill-red2 { fill: var(--color-red2) } +.fill-red-light { fill: var(--color-red-light) } .fill-red-lightest { fill: var(--color-red-lightest) } .fill-blue { fill: var(--color-blue) } .fill-blue2 { fill: var(--color-blue2) } @@ -47,12 +48,15 @@ .fill-transparent { fill: var(--color-transparent) } .fill-cyan { fill: var(--color-cyan) } .fill-amber { fill: var(--color-amber) } +.fill-amber-medium { fill: var(--color-amber-medium) } .fill-glassWhite { fill: var(--color-glassWhite) } .fill-glassMint { fill: var(--color-glassMint) } .fill-glassLavander { fill: var(--color-glassLavander) } .fill-blueLight { fill: var(--color-blueLight) } .fill-offWhite { fill: var(--color-offWhite) } .fill-disabled-text { fill: var(--color-disabled-text) } +.fill-indigo-lightest { fill: var(--color-indigo-lightest) } +.fill-indigo { fill: var(--color-indigo) } .fill-figmaColors-accent-secondary { fill: var(--color-figmaColors-accent-secondary) } .fill-figmaColors-main { fill: var(--color-figmaColors-main) } .fill-figmaColors-primary-outlined-hover-background { fill: var(--color-figmaColors-primary-outlined-hover-background) } @@ -89,6 +93,7 @@ .hover-fill-green-dark:hover svg { fill: var(--color-green-dark) } .hover-fill-red:hover svg { fill: var(--color-red) } .hover-fill-red2:hover svg { fill: var(--color-red2) } +.hover-fill-red-light:hover svg { fill: var(--color-red-light) } .hover-fill-red-lightest:hover svg { fill: var(--color-red-lightest) } .hover-fill-blue:hover svg { fill: var(--color-blue) } .hover-fill-blue2:hover svg { fill: var(--color-blue2) } @@ -108,12 +113,15 @@ .hover-fill-transparent:hover svg { fill: var(--color-transparent) } .hover-fill-cyan:hover svg { fill: var(--color-cyan) } .hover-fill-amber:hover svg { fill: var(--color-amber) } +.hover-fill-amber-medium:hover svg { fill: var(--color-amber-medium) } .hover-fill-glassWhite:hover svg { fill: var(--color-glassWhite) } .hover-fill-glassMint:hover svg { fill: var(--color-glassMint) } .hover-fill-glassLavander:hover svg { fill: var(--color-glassLavander) } .hover-fill-blueLight:hover svg { fill: var(--color-blueLight) } .hover-fill-offWhite:hover svg { fill: var(--color-offWhite) } .hover-fill-disabled-text:hover svg { fill: var(--color-disabled-text) } +.hover-fill-indigo-lightest:hover svg { fill: var(--color-indigo-lightest) } +.hover-fill-indigo:hover svg { fill: var(--color-indigo) } .hover-fill-figmaColors-accent-secondary:hover svg { fill: var(--color-figmaColors-accent-secondary) } .hover-fill-figmaColors-main:hover svg { fill: var(--color-figmaColors-main) } .hover-fill-figmaColors-primary-outlined-hover-background:hover svg { fill: var(--color-figmaColors-primary-outlined-hover-background) } @@ -152,6 +160,7 @@ .color-green-dark { color: var(--color-green-dark) } .color-red { color: var(--color-red) } .color-red2 { color: var(--color-red2) } +.color-red-light { color: var(--color-red-light) } .color-red-lightest { color: var(--color-red-lightest) } .color-blue { color: var(--color-blue) } .color-blue2 { color: var(--color-blue2) } @@ -171,12 +180,15 @@ .color-transparent { color: var(--color-transparent) } .color-cyan { color: var(--color-cyan) } .color-amber { color: var(--color-amber) } +.color-amber-medium { color: var(--color-amber-medium) } .color-glassWhite { color: var(--color-glassWhite) } .color-glassMint { color: var(--color-glassMint) } .color-glassLavander { color: var(--color-glassLavander) } .color-blueLight { color: var(--color-blueLight) } .color-offWhite { color: var(--color-offWhite) } .color-disabled-text { color: var(--color-disabled-text) } +.color-indigo-lightest { color: var(--color-indigo-lightest) } +.color-indigo { color: var(--color-indigo) } .color-figmaColors-accent-secondary { color: var(--color-figmaColors-accent-secondary) } .color-figmaColors-main { color: var(--color-figmaColors-main) } .color-figmaColors-primary-outlined-hover-background { color: var(--color-figmaColors-primary-outlined-hover-background) } @@ -215,6 +227,7 @@ .hover-green-dark:hover { color: var(--color-green-dark) } .hover-red:hover { color: var(--color-red) } .hover-red2:hover { color: var(--color-red2) } +.hover-red-light:hover { color: var(--color-red-light) } .hover-red-lightest:hover { color: var(--color-red-lightest) } .hover-blue:hover { color: var(--color-blue) } .hover-blue2:hover { color: var(--color-blue2) } @@ -234,12 +247,15 @@ .hover-transparent:hover { color: var(--color-transparent) } .hover-cyan:hover { color: var(--color-cyan) } .hover-amber:hover { color: var(--color-amber) } +.hover-amber-medium:hover { color: var(--color-amber-medium) } .hover-glassWhite:hover { color: var(--color-glassWhite) } .hover-glassMint:hover { color: var(--color-glassMint) } .hover-glassLavander:hover { color: var(--color-glassLavander) } .hover-blueLight:hover { color: var(--color-blueLight) } .hover-offWhite:hover { color: var(--color-offWhite) } .hover-disabled-text:hover { color: var(--color-disabled-text) } +.hover-indigo-lightest:hover { color: var(--color-indigo-lightest) } +.hover-indigo:hover { color: var(--color-indigo) } .hover-figmaColors-accent-secondary:hover { color: var(--color-figmaColors-accent-secondary) } .hover-figmaColors-main:hover { color: var(--color-figmaColors-main) } .hover-figmaColors-primary-outlined-hover-background:hover { color: var(--color-figmaColors-primary-outlined-hover-background) } @@ -278,6 +294,7 @@ .border-green-dark { border-color: var(--color-green-dark) } .border-red { border-color: var(--color-red) } .border-red2 { border-color: var(--color-red2) } +.border-red-light { border-color: var(--color-red-light) } .border-red-lightest { border-color: var(--color-red-lightest) } .border-blue { border-color: var(--color-blue) } .border-blue2 { border-color: var(--color-blue2) } @@ -297,12 +314,15 @@ .border-transparent { border-color: var(--color-transparent) } .border-cyan { border-color: var(--color-cyan) } .border-amber { border-color: var(--color-amber) } +.border-amber-medium { border-color: var(--color-amber-medium) } .border-glassWhite { border-color: var(--color-glassWhite) } .border-glassMint { border-color: var(--color-glassMint) } .border-glassLavander { border-color: var(--color-glassLavander) } .border-blueLight { border-color: var(--color-blueLight) } .border-offWhite { border-color: var(--color-offWhite) } .border-disabled-text { border-color: var(--color-disabled-text) } +.border-indigo-lightest { border-color: var(--color-indigo-lightest) } +.border-indigo { border-color: var(--color-indigo) } .border-figmaColors-accent-secondary { border-color: var(--color-figmaColors-accent-secondary) } .border-figmaColors-main { border-color: var(--color-figmaColors-main) } .border-figmaColors-primary-outlined-hover-background { border-color: var(--color-figmaColors-primary-outlined-hover-background) } @@ -341,6 +361,7 @@ .bg-green-dark { background-color: var(--color-green-dark) } .bg-red { background-color: var(--color-red) } .bg-red2 { background-color: var(--color-red2) } +.bg-red-light { background-color: var(--color-red-light) } .bg-red-lightest { background-color: var(--color-red-lightest) } .bg-blue { background-color: var(--color-blue) } .bg-blue2 { background-color: var(--color-blue2) } @@ -360,12 +381,15 @@ .bg-transparent { background-color: var(--color-transparent) } .bg-cyan { background-color: var(--color-cyan) } .bg-amber { background-color: var(--color-amber) } +.bg-amber-medium { background-color: var(--color-amber-medium) } .bg-glassWhite { background-color: var(--color-glassWhite) } .bg-glassMint { background-color: var(--color-glassMint) } .bg-glassLavander { background-color: var(--color-glassLavander) } .bg-blueLight { background-color: var(--color-blueLight) } .bg-offWhite { background-color: var(--color-offWhite) } .bg-disabled-text { background-color: var(--color-disabled-text) } +.bg-indigo-lightest { background-color: var(--color-indigo-lightest) } +.bg-indigo { background-color: var(--color-indigo) } .bg-figmaColors-accent-secondary { background-color: var(--color-figmaColors-accent-secondary) } .bg-figmaColors-main { background-color: var(--color-figmaColors-main) } .bg-figmaColors-primary-outlined-hover-background { background-color: var(--color-figmaColors-primary-outlined-hover-background) } diff --git a/frontend/app/styles/general.css b/frontend/app/styles/general.css index 0d1c41aa9..13c55cc59 100644 --- a/frontend/app/styles/general.css +++ b/frontend/app/styles/general.css @@ -29,23 +29,23 @@ .info.info.info.info.info { /* BAD HACK >:) */ - background-color: rgba(242, 248, 255, 0.6); + background-color: var(--color-glassMint); &:hover { - background-color: rgba(242, 248, 255, 1); + background-color: var(--color-indigo-lightest); } } .warn.warn.warn.warn { - background-color: rgba(253, 248, 240, 0.6); + background-color: var(--color-amber); &:hover { - background-color: rgba(253, 248, 240, 1); + background-color: var(--color-amber-medium); } } .error.error.error.error { - background-color: rgba(252, 242, 242, 0.6); + background-color: var(--color-red-light); &:hover { - background-color: rgba(252, 242, 242, 1); + background-color: var(--color-red-lightest); } } diff --git a/frontend/app/theme/colors.js b/frontend/app/theme/colors.js index 17d472398..ae82642dc 100644 --- a/frontend/app/theme/colors.js +++ b/frontend/app/theme/colors.js @@ -25,7 +25,8 @@ module.exports = { 'green-dark': '#2C9848', red: '#cc0000', red2: '#F5A623', - 'red-lightest': 'rgba(204, 0, 0, 0.1)', + 'red-light': 'oklch(93.6% 0.032 17.717)', + 'red-lightest': 'oklch(97.1% 0.013 17.38)', blue: '#366CD9', blue2: '#0076FF', 'active-blue': '#F6F7FF', @@ -46,13 +47,17 @@ module.exports = { transparent: 'transparent', cyan: '#EBF4F5', amber: 'oklch(98.7% 0.022 95.277)', + 'amber-medium': 'oklch(96.2% 0.059 95.617)', glassWhite: 'rgba(255, 255, 255, 0.5)', glassMint: 'rgba(248, 255, 254, 0.5)', glassLavander: 'rgba(243, 241, 255, 0.5)', blueLight: 'rgba(235, 235, 255, 1)', offWhite: 'rgba(250, 250, 255, 1)', 'disabled-text': 'rgba(0,0,0, 0.38)', + 'indigo-lightest': 'oklch(96.2% 0.018 272.314)', + 'indigo': 'oklch(58.5% 0.233 277.117)', + /** DEPRECATED */ figmaColors: { 'accent-secondary': 'rgba(62, 170, 175, 1)', main: 'rgba(57, 78, 255, 1)', @@ -79,6 +84,11 @@ module.exports = { 'background': 'oklch(20.5% 0 0)', 'surface': '#1E1E1E', amber: 'oklch(41.4% 0.112 45.904)', + 'amber-medium': 'oklch(55.5% 0.163 48.998)', + 'red-lightest': 'oklch(25.8% 0.092 26.042)', + 'indigo-lightest': 'oklch(35.9% 0.144 278.697)', + 'indigo': 'oklch(58.5% 0.233 277.117)', + 'red-light': 'oklch(39.6% 0.141 25.723)', 'gray-light-shade': 'oklch(37.1% 0 0)', 'gray-lightest': 'oklch(26.9% 0 0)', diff --git a/frontend/tests/mocks/sessionResponse.js b/frontend/tests/mocks/sessionResponse.js index d7f269362..5e8d9141b 100644 --- a/frontend/tests/mocks/sessionResponse.js +++ b/frontend/tests/mocks/sessionResponse.js @@ -1292,7 +1292,7 @@ export const session = { sessionId: 8119081922378909, messageId: 37522, timestamp: 1673887715900, - label: 'hover-main color-main cursor-pointer rounded-full hover:bg-indigo-50', + label: 'hover-main color-main cursor-pointer rounded-full hover:bg-indigo-lightest', url: 'app.openreplay.com/5095/session/8118843021704432', selector: '#app > div.relative > div.flex > div.w-full > div.session-module__session--PKpp5.relative > div.playerBlock-module__playerBlock--c8_Ul.flex.flex-col.overflow-x-hidden > div.flex-1.player-module__playerBody--aoTX_.flex.flex-col.relative > div.controls-module__controls--fXp80 > div.controls-module__buttons--vje3y > div.flex.items-center > div.flex.items-center > div.relative > div > div.hover-main.color-main.cursor-pointer.rounded.hover:bg-gray-light-shade', @@ -2780,7 +2780,7 @@ export const session = { sessionId: 8119081922378909, messageId: 67718, timestamp: 1673888012602, - label: 'hover-main color-main cursor-pointer rounded-full hover:bg-indigo-50', + label: 'hover-main color-main cursor-pointer rounded-full hover:bg-indigo-lightest', url: 'app.openreplay.com/5095/session/8118633985734291', selector: '#app > div.relative > div.flex > div.w-full > div.session-module__session--PKpp5.relative > div.playerBlock-module__playerBlock--c8_Ul.flex.flex-col.overflow-x-hidden > div.flex-1.player-module__playerBody--aoTX_.flex.flex-col.relative > div.controls-module__controls--fXp80 > div.controls-module__buttons--vje3y > div.flex.items-center > div.flex.items-center > div.relative > div > div.hover-main.color-main.cursor-pointer.rounded.hover:bg-gray-light-shade', @@ -4046,7 +4046,7 @@ export const session = { sessionId: 8119081922378909, messageId: 90402, timestamp: 1673888133249, - label: 'hover-main color-main cursor-pointer rounded-full hover:bg-indigo-50', + label: 'hover-main color-main cursor-pointer rounded-full hover:bg-indigo-lightest', url: 'app.openreplay.com/5095/session/8118556979885624', selector: '#app > div.relative > div.flex > div.w-full > div.session-module__session--PKpp5.relative > div.playerBlock-module__playerBlock--c8_Ul.flex.flex-col.overflow-x-hidden > div.flex-1.player-module__playerBody--aoTX_.flex.flex-col.relative > div.controls-module__controls--fXp80 > div.controls-module__buttons--vje3y > div.flex.items-center > div.flex.items-center > div.relative > div > div.hover-main.color-main.cursor-pointer.rounded.hover:bg-gray-light-shade', diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d1cfeec97..37acfb81f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -23,11 +23,11 @@ __metadata: linkType: hard "@ant-design/colors@npm:^7.0.0, @ant-design/colors@npm:^7.2.0": - version: 7.2.0 - resolution: "@ant-design/colors@npm:7.2.0" + version: 7.2.1 + resolution: "@ant-design/colors@npm:7.2.1" dependencies: "@ant-design/fast-color": "npm:^2.0.6" - checksum: 10c1/cf9eec1bf6ccc6f6757194dccdcc11f2dd84e14e8be2d3db6f85bca20e05432340a3df55632eed1d880bc8691efc1869fa0f18cb1f494aafb85b1565c71c2609 + checksum: 10c1/b8f3c98a55877da647fe4158e88600500713ba00d90ecd6a98c6cff068bd556771c833e65853d9e3443298ef114dd262f81b518c8b9fe364b80500c475c72788 languageName: node linkType: hard @@ -1745,7 +1745,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.4.0": +"@emnapi/core@npm:^1.4.3": version: 1.4.3 resolution: "@emnapi/core@npm:1.4.3" dependencies: @@ -1755,7 +1755,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.0": +"@emnapi/runtime@npm:^1.4.3": version: 1.4.3 resolution: "@emnapi/runtime@npm:1.4.3" dependencies: @@ -2115,12 +2115,12 @@ __metadata: languageName: node linkType: hard -"@eslint/core@npm:^0.13.0": - version: 0.13.0 - resolution: "@eslint/core@npm:0.13.0" +"@eslint/core@npm:^0.14.0": + version: 0.14.0 + resolution: "@eslint/core@npm:0.14.0" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c1/2b9a0aefab71f000dead614dc8c8d34f76778b19649824d252f08a6b9dc76763407cdece736d4ce2e2434521b8f82dbf30bded9b15d01cb377ddc0686fbdf5c4 + checksum: 10c1/3ed950bd65e73d3599f5333afe0e7ea38938580d78205d58a07c201159e19cf23cdcd9ae961c8d1f15334e92d0c734e4e27a414e6883d424d226f86357ff8579 languageName: node linkType: hard @@ -2141,10 +2141,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.26.0, @eslint/js@npm:^9.26.0": - version: 9.26.0 - resolution: "@eslint/js@npm:9.26.0" - checksum: 10c1/c022348fe10ed6f008b2f0eb7f2ff84caca0715a20f37f4705294bad73493d3e8f81d6405ff1856873a42a0cd8d1519334a7f1a8e60d467fea06baa858ffdc9e +"@eslint/js@npm:9.27.0, @eslint/js@npm:^9.26.0": + version: 9.27.0 + resolution: "@eslint/js@npm:9.27.0" + checksum: 10c1/c1fad371a9925516fcbb992542c37c4829179dc9c75e0a5614ef72853c2098c858354adb16101ccfef5fdb4e88e1d5e1a614494bfd5a44089b7248ca6474ef5c languageName: node linkType: hard @@ -2155,13 +2155,13 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.2.8": - version: 0.2.8 - resolution: "@eslint/plugin-kit@npm:0.2.8" +"@eslint/plugin-kit@npm:^0.3.1": + version: 0.3.1 + resolution: "@eslint/plugin-kit@npm:0.3.1" dependencies: - "@eslint/core": "npm:^0.13.0" + "@eslint/core": "npm:^0.14.0" levn: "npm:^0.4.1" - checksum: 10c1/59131b1e2be7a8af0abac04c72411c804b2aba45bab3c74d9334da606dd6aea43a7731c63f247f52313533fe639728ca9fed26ca376c10b55af9f212094b6cc7 + checksum: 10c1/7c6d916aa609b63a3e7fdfd8ddd8c00c4bba9cc11e7f6ce493f74b32975fe7ed11d0f5e8f2e7b496733d3cf3dccfe199ce4a8a6ee3b072caa2f3c62a1c1c5013 languageName: node linkType: hard @@ -2617,32 +2617,14 @@ __metadata: languageName: node linkType: hard -"@modelcontextprotocol/sdk@npm:^1.8.0": - version: 1.11.3 - resolution: "@modelcontextprotocol/sdk@npm:1.11.3" - dependencies: - content-type: "npm:^1.0.5" - cors: "npm:^2.8.5" - cross-spawn: "npm:^7.0.5" - eventsource: "npm:^3.0.2" - express: "npm:^5.0.1" - express-rate-limit: "npm:^7.5.0" - pkce-challenge: "npm:^5.0.0" - raw-body: "npm:^3.0.0" - zod: "npm:^3.23.8" - zod-to-json-schema: "npm:^3.24.1" - checksum: 10c1/054f51274162a462eb4d25d014eef44c18d83471a2cd562ab7ffa94746531098d800ea92bfd27c5610457b53bccf60a7c534bb0c903f73cd86bca9b925e953ee - languageName: node - linkType: hard - "@napi-rs/wasm-runtime@npm:^0.2.9": - version: 0.2.9 - resolution: "@napi-rs/wasm-runtime@npm:0.2.9" + version: 0.2.10 + resolution: "@napi-rs/wasm-runtime@npm:0.2.10" dependencies: - "@emnapi/core": "npm:^1.4.0" - "@emnapi/runtime": "npm:^1.4.0" + "@emnapi/core": "npm:^1.4.3" + "@emnapi/runtime": "npm:^1.4.3" "@tybys/wasm-util": "npm:^0.9.0" - checksum: 10c1/9f5686a099d59146619a1e9e7ca37a4856a2e9809f6e9e5babe7c3f75ecad51cc3f8aa356f5e11fce84337b2f14a749982cf9543aaffe3242636908d325e6160 + checksum: 10c1/908dfbd14a853aa0fb541de9394f6b8a014cf04bab4297963d5ced10ce9b060ec012f18ac01ed21f3919c86d91278babaeb7c2c8cffa7092c0c7185472c80853 languageName: node linkType: hard @@ -3056,61 +3038,61 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:9.19.0": - version: 9.19.0 - resolution: "@sentry-internal/browser-utils@npm:9.19.0" +"@sentry-internal/browser-utils@npm:9.22.0": + version: 9.22.0 + resolution: "@sentry-internal/browser-utils@npm:9.22.0" dependencies: - "@sentry/core": "npm:9.19.0" - checksum: 10c1/0d299747fef8b8f3b5f42d428248a4d33c7f43176ad2d71f6c87d9a6ec279bb2104597a8b9ff30e41919491deff5b9050410e9c84201f648557585e6ec06c1f3 + "@sentry/core": "npm:9.22.0" + checksum: 10c1/526a908c4597d8d081dcaf87072f4d55bd438ea9f12a46417b81ebd5c44751a23cfe35f3f16e1805f9e7efd9b27b1848f6cbe3f75f8ba8115c67b10f53cba2e8 languageName: node linkType: hard -"@sentry-internal/feedback@npm:9.19.0": - version: 9.19.0 - resolution: "@sentry-internal/feedback@npm:9.19.0" +"@sentry-internal/feedback@npm:9.22.0": + version: 9.22.0 + resolution: "@sentry-internal/feedback@npm:9.22.0" dependencies: - "@sentry/core": "npm:9.19.0" - checksum: 10c1/7bb55584edd3e54db946e9eb0f49cdc78716bf740287641dffba488af28f8472676d3c21e5e71e51fd97e36a77450f8f71505ebd3a3837471e1d216a88cfbde8 + "@sentry/core": "npm:9.22.0" + checksum: 10c1/31e1d61691022678df190f69a9c81cbf1e6b9c3e8218681dc4e30e2dc75e1ec28ca1fda0bd4e13f92af473a68c797f03af3440bcefa16aff44cca5b8224373d6 languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:9.19.0": - version: 9.19.0 - resolution: "@sentry-internal/replay-canvas@npm:9.19.0" +"@sentry-internal/replay-canvas@npm:9.22.0": + version: 9.22.0 + resolution: "@sentry-internal/replay-canvas@npm:9.22.0" dependencies: - "@sentry-internal/replay": "npm:9.19.0" - "@sentry/core": "npm:9.19.0" - checksum: 10c1/44e7f1caadd20f4b4fd75c6f771b56ffb1f41516d44b33ea9cefdf239119c223442f4c7c7f85d2db3ec226a35fbe740f2a6d382d7b24b08d9c46df3f0e9a360c + "@sentry-internal/replay": "npm:9.22.0" + "@sentry/core": "npm:9.22.0" + checksum: 10c1/3108fe76bf0dd458b2e0ff748b42ccb59a28756720b2e55c1c1e44111bf93e4d4297a09b2cc08e6039abac5692bd2c33d79e7d859b31d142bd0128ee32738a30 languageName: node linkType: hard -"@sentry-internal/replay@npm:9.19.0": - version: 9.19.0 - resolution: "@sentry-internal/replay@npm:9.19.0" +"@sentry-internal/replay@npm:9.22.0": + version: 9.22.0 + resolution: "@sentry-internal/replay@npm:9.22.0" dependencies: - "@sentry-internal/browser-utils": "npm:9.19.0" - "@sentry/core": "npm:9.19.0" - checksum: 10c1/6607eeee91267cb9f2fa0726a53b61bde709761d69e979fa909fd60890a415fcf3482ec7c1a55ed62180792de17c6630b91c7e9a4514f9e456e588ded790c3e8 + "@sentry-internal/browser-utils": "npm:9.22.0" + "@sentry/core": "npm:9.22.0" + checksum: 10c1/3e4b81005e6995437f842e05f0d13894e888ef4ef0b770f65e02d396985420192af0d69e613f4b9f96fb86d160bcd63b56874aba88022e28f394c8756c7cbda0 languageName: node linkType: hard "@sentry/browser@npm:^9.18.0": - version: 9.19.0 - resolution: "@sentry/browser@npm:9.19.0" + version: 9.22.0 + resolution: "@sentry/browser@npm:9.22.0" dependencies: - "@sentry-internal/browser-utils": "npm:9.19.0" - "@sentry-internal/feedback": "npm:9.19.0" - "@sentry-internal/replay": "npm:9.19.0" - "@sentry-internal/replay-canvas": "npm:9.19.0" - "@sentry/core": "npm:9.19.0" - checksum: 10c1/f7a1dedc2ca3dd72ea3f7b2901c913abe5a45f34f3dba5d12456a1792847cc6ed97cdccfb51c3e1ed1f3f95f3100d90a3c881865be67d0059d1e616aac5a7523 + "@sentry-internal/browser-utils": "npm:9.22.0" + "@sentry-internal/feedback": "npm:9.22.0" + "@sentry-internal/replay": "npm:9.22.0" + "@sentry-internal/replay-canvas": "npm:9.22.0" + "@sentry/core": "npm:9.22.0" + checksum: 10c1/94e4b79cf04d6b4ee935d4200e26094b2e20f119ba54f2234ec78b5aea9499eefef8c82260846843c95fc8ecdff79435fcb7e1c6a054c157273304e545bbe94c languageName: node linkType: hard -"@sentry/core@npm:9.19.0": - version: 9.19.0 - resolution: "@sentry/core@npm:9.19.0" - checksum: 10c1/c7e1e897be8543a08cfb49b9accc054fc4b0d258421ccc9a58edb29b3150370e0d8edc5350c701ff1ef34153737fffc975aed493e31872670dae16202ccae752 +"@sentry/core@npm:9.22.0": + version: 9.22.0 + resolution: "@sentry/core@npm:9.22.0" + checksum: 10c1/6203d394b62110219b6b751698e89d08ce6ab52dd90e1df74778a5cb966558334d4273aff4d4249249b76530856d5ae0c8ec2018193f096462e6d9ce753d4933 languageName: node linkType: hard @@ -3476,25 +3458,25 @@ __metadata: linkType: hard "@types/express@npm:*": - version: 5.0.1 - resolution: "@types/express@npm:5.0.1" + version: 5.0.2 + resolution: "@types/express@npm:5.0.2" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^5.0.0" "@types/serve-static": "npm:*" - checksum: 10c1/13845103cdaca4a61f610a51c0d3aecada5a8cde28a7a47c01f3cc01bcc38ff77a539cec2badff3dcfc0a81f6a0ddfba2c3009cc257a885de4a3c7d3d6599590 + checksum: 10c1/c90906eacd7a50aaf9b3706f228da66d3655e6d18673b44e0653cb128a41e24a42627652426ff12b08ddb2c514eca3767e57093e01d8b2c279f914a47c46588a languageName: node linkType: hard "@types/express@npm:^4.17.21": - version: 4.17.21 - resolution: "@types/express@npm:4.17.21" + version: 4.17.22 + resolution: "@types/express@npm:4.17.22" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^4.17.33" "@types/qs": "npm:*" "@types/serve-static": "npm:*" - checksum: 10c1/c7e08a480043ea66c18832e84b1930dcb9803bd037d58406efafbe37d26c0bd682db61c65f576798278a1ca12fa79ec07bbf11b80f282ed58b7689cf77c1ffe6 + checksum: 10c1/89c6569c6139a8030860435ff78e5dbbd8b2ccbe496f0125ab1427afc8271b603dd6be5c04ceb0fd9b3ed549495e08750103cc8eaba023ee5466cd0a8bf1bfe5 languageName: node linkType: hard @@ -3646,11 +3628,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:^22.7.8": - version: 22.15.18 - resolution: "@types/node@npm:22.15.18" + version: 22.15.21 + resolution: "@types/node@npm:22.15.21" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c1/00433092cf1ce4cf961a6d29b693a67957a20eece2649c4e199973c013c363b3647608fe8969bc6c905af39983aefbc4c89af1ac50c6e5319be66940f6067f56 + checksum: 10c1/dade6e3665167bd82d4ee2374f98eff78777c0ffa069b4c1146f3cbdacd7f412e9f1cb24e7d978b40fd832956121ba86acba0612cddbfe093bf660682f84eaeb languageName: node linkType: hard @@ -3669,9 +3651,9 @@ __metadata: linkType: hard "@types/qs@npm:*": - version: 6.9.18 - resolution: "@types/qs@npm:6.9.18" - checksum: 10c1/f3ceb2d647f2fbba7b28dff0606dea73557346a09345a1530aa05cc09444e43ac8c43e9a18716508081ff5ac5e3c9d802ee3cd56ab93cd4a0461405b1eb8d346 + version: 6.14.0 + resolution: "@types/qs@npm:6.14.0" + checksum: 10c1/36548b14899854c22cb8202d9382f47d9e1da88ba06bc0f50db591952de2111737e197910fe78d8b424bce445c463c66036aaa61cde4371157b98c4990fc2085 languageName: node linkType: hard @@ -3738,11 +3720,11 @@ __metadata: linkType: hard "@types/react@npm:*, @types/react@npm:^19.0.10": - version: 19.1.4 - resolution: "@types/react@npm:19.1.4" + version: 19.1.5 + resolution: "@types/react@npm:19.1.5" dependencies: csstype: "npm:^3.0.2" - checksum: 10c1/725f4d9dbee82273ee358c2a5fbc634c847c7167118715ba5bb44e8f37c4c288fbfd3ada0b77b46af1792c33d97c97b006f5b557e9a1690631901cf3474f5aa7 + checksum: 10c1/3eaa6c8cad7ec0fce16a01c1c83cf44931ee0c09d4f5c9c20c650ecb42405bd8a800649342f3721a1f2cbd14393718e9fb72423f26fec5b01bc9a62688668846 languageName: node linkType: hard @@ -4375,16 +4357,6 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^2.0.0": - version: 2.0.0 - resolution: "accepts@npm:2.0.0" - dependencies: - mime-types: "npm:^3.0.0" - negotiator: "npm:^1.0.0" - checksum: 10c1/22bbe1a016b383ff4a89048f9492c2d8cc38c95c20d596cdd61a2d6e772a1d64fdafef8aad5a005b283c8a55fd4c3a18484b37a90315aa0d4430563b67757621 - languageName: node - linkType: hard - "accepts@npm:~1.3.4, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -4620,8 +4592,8 @@ __metadata: linkType: hard "antd@npm:^5.25.1": - version: 5.25.1 - resolution: "antd@npm:5.25.1" + version: 5.25.2 + resolution: "antd@npm:5.25.2" dependencies: "@ant-design/colors": "npm:^7.2.0" "@ant-design/cssinjs": "npm:^1.23.0" @@ -4658,11 +4630,11 @@ __metadata: rc-rate: "npm:~2.13.1" rc-resize-observer: "npm:^1.4.3" rc-segmented: "npm:~2.7.0" - rc-select: "npm:~14.16.7" + rc-select: "npm:~14.16.8" rc-slider: "npm:~11.1.8" rc-steps: "npm:~6.0.1" rc-switch: "npm:~4.1.0" - rc-table: "npm:~7.50.4" + rc-table: "npm:~7.50.5" rc-tabs: "npm:~15.6.1" rc-textarea: "npm:~1.10.0" rc-tooltip: "npm:~6.4.0" @@ -4675,7 +4647,7 @@ __metadata: peerDependencies: react: ">=16.9.0" react-dom: ">=16.9.0" - checksum: 10c1/819bf63d49bc5e2fd8c4abd2d61f5a3665725d59d49bc5513387b9784be11dbd31d7a2d2d97f47a6220a9091bac02237c763cd56d811892efe5f0e2223048be6 + checksum: 10c1/9f9a2363eba424cdf634dab2629366808ce2239a28dfb0316a33ea306e3e6a5a6bcb5504c6b1835dc15700c10817e27a16e5829f828eeced4e1857b664e0db7f languageName: node linkType: hard @@ -5300,23 +5272,6 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:^2.2.0": - version: 2.2.0 - resolution: "body-parser@npm:2.2.0" - dependencies: - bytes: "npm:^3.1.2" - content-type: "npm:^1.0.5" - debug: "npm:^4.4.0" - http-errors: "npm:^2.0.0" - iconv-lite: "npm:^0.6.3" - on-finished: "npm:^2.4.1" - qs: "npm:^6.14.0" - raw-body: "npm:^3.0.0" - type-is: "npm:^2.0.0" - checksum: 10c1/275502d25be9064b63a5bfd5abfcc98270870e807914a26748a9da982ccfbac49ee057708be8cba09e0fa426438c85877cc57b8b7c9c3a3bcb225aef9e917fa2 - languageName: node - linkType: hard - "bonjour-service@npm:^1.2.1": version: 1.3.0 resolution: "bonjour-service@npm:1.3.0" @@ -5443,7 +5398,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:^3.1.2": +"bytes@npm:3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: 10c1/102066f8fe70d48c60d33e79f25b39689b858bd47b5c6332e16a07738aa72e11e8dab3b035d137a914d4bfb6edd95afd896bc20b3be3b0b6300d85aa55bf4ec7 @@ -6103,16 +6058,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:^1.0.0": - version: 1.0.0 - resolution: "content-disposition@npm:1.0.0" - dependencies: - safe-buffer: "npm:5.2.1" - checksum: 10c1/aaa3feebb92998e2b8c405b85e309f3442d1b721001f0a92424854656ead13d9731cc77bc157e856a7e30ce58ca0992789f3a1b8a44176dc07e1ebec2320fb4b - languageName: node - linkType: hard - -"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c1/984f6dc8716c32916530ea74ad05f395a2696ba7a63d01b9cbc5b9bef41dcfcff6b368a55fd98ec7b3a0617329e094884bc3953dd3956e128161163d23dd5633 @@ -6140,13 +6086,6 @@ __metadata: languageName: node linkType: hard -"cookie-signature@npm:^1.2.1": - version: 1.2.2 - resolution: "cookie-signature@npm:1.2.2" - checksum: 10c1/52acd2b690fa942f6a096e62a5e5b4e93724c7fd485f38de7b7a59e80ce7b7803738f57ad25cdb367ac99311903d04015717297bad708f541f774e00f9bca911 - languageName: node - linkType: hard - "cookie@npm:0.7.1": version: 0.7.1 resolution: "cookie@npm:0.7.1" @@ -6154,13 +6093,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.7.1": - version: 0.7.2 - resolution: "cookie@npm:0.7.2" - checksum: 10c1/6335a587d568ca025f51f42f5811886c3d4bd751c96396f33fca4154b37a732baf223c42b5921bf62d357b7c77f4a0179ed24c59689e92bf090461196bb7ad94 - languageName: node - linkType: hard - "copy-to-clipboard@npm:^3.3.3": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -6223,16 +6155,6 @@ __metadata: languageName: node linkType: hard -"cors@npm:^2.8.5": - version: 2.8.5 - resolution: "cors@npm:2.8.5" - dependencies: - object-assign: "npm:^4" - vary: "npm:^1" - checksum: 10c1/6cd6176012752a2b5a6dc423ec9d798811b45631b0bfb6b87481bf29103a1470dc70535a0913a751612527790cd5e652f2eaebc195b58f7b8610c1fa884208a3 - languageName: node - linkType: hard - "cosmiconfig@npm:^7.0.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" @@ -6317,7 +6239,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -6815,7 +6737,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.4.0": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -6964,7 +6886,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0, depd@npm:^2.0.0": +"depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10c1/2e8be449f7aa2dfcc5b58babf090f0ca5bfefb1dee51392296795a9aa6347f842516c7b18e01fd50067e6b4b22f0aee1b2005b6a489e6fbab05dcacb0ae2e0c9 @@ -7171,14 +7093,14 @@ __metadata: linkType: hard "dompurify@npm:^3.2.4": - version: 3.2.5 - resolution: "dompurify@npm:3.2.5" + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10c1/bf25db12edc7a97b9600c06594c89cc85099f33a34464b70810348b196b51571fe1c2999d4bb4e9e8927d0b935b8865296e585fb37639f1ddbc6f10bbac91f41 + checksum: 10c1/19f97c5a9024216e5d5ccd010d156d3d5b2651e207c7e43fd2b36f7e7551c6526ad00c93ea356c5e643dd5f75a049be63c1728b3879fcc7da7b75bd3ae78f886 languageName: node linkType: hard @@ -7312,13 +7234,6 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0": - version: 2.0.0 - resolution: "encodeurl@npm:2.0.0" - checksum: 10c1/26dbea2452b7001172f3db9af5aa33d3b7983a9dff16c5afabc127c3025f09a40cb99da9ece81caf788984dca025a282ee1d4b834eb149fd8fb63863fe9c8977 - languageName: node - linkType: hard - "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -7326,6 +7241,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10c1/26dbea2452b7001172f3db9af5aa33d3b7983a9dff16c5afabc127c3025f09a40cb99da9ece81caf788984dca025a282ee1d4b834eb149fd8fb63863fe9c8977 + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -7696,7 +7618,7 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": +"escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" checksum: 10c1/8c1e1b5b46985dfb4520bbfaeb636b57df376dff35f40030ccbc7155a0a2253d6f2074a547ea9680dac2e8f8c1bd18fcecac76c3f41c98221a23de5d0d27237a @@ -8026,21 +7948,20 @@ __metadata: linkType: hard "eslint@npm:^9.21.0": - version: 9.26.0 - resolution: "eslint@npm:9.26.0" + version: 9.27.0 + resolution: "eslint@npm:9.27.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" "@eslint/config-array": "npm:^0.20.0" "@eslint/config-helpers": "npm:^0.2.1" - "@eslint/core": "npm:^0.13.0" + "@eslint/core": "npm:^0.14.0" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.26.0" - "@eslint/plugin-kit": "npm:^0.2.8" + "@eslint/js": "npm:9.27.0" + "@eslint/plugin-kit": "npm:^0.3.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" - "@modelcontextprotocol/sdk": "npm:^1.8.0" "@types/estree": "npm:^1.0.6" "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" @@ -8065,7 +7986,6 @@ __metadata: minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - zod: "npm:^3.24.2" peerDependencies: jiti: "*" peerDependenciesMeta: @@ -8073,7 +7993,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c1/4774817af0581d4d387bcf55c0a3ce2eb47d9473178e6c33808853bfb7d6633ab42d5e4346a2c68f3cad0e9a99b67a3ae9fe132990663f293fc6042c3d1b1289 + checksum: 10c1/91e469d8daa411529180d7238a2a83c223e542e6ee677a3a43fed7aeb6864aa6c7517ed3c74a14e647a0b5cd6985ff915387a1b97a4f2dd650b36254f642cd04 languageName: node linkType: hard @@ -8144,7 +8064,7 @@ __metadata: languageName: node linkType: hard -"etag@npm:^1.8.1, etag@npm:~1.8.1": +"etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" checksum: 10c1/65bd37eebafe53c524d5a3e62cc29deb4b2fed397aabf9324edbe1702f754992bc7872f3379743d4056fd3ad69b3867784bef2187ff53267005042018bfb703b @@ -8172,22 +8092,6 @@ __metadata: languageName: node linkType: hard -"eventsource-parser@npm:^3.0.1": - version: 3.0.2 - resolution: "eventsource-parser@npm:3.0.2" - checksum: 10c1/dc96c5049b694a0f504b387e597a0a4a2b1875f198c91eeaf57218f5c5efa170112a6b6faffce02948216159ef739cbadfda12d0d3bb40a35d416cbc807733a9 - languageName: node - linkType: hard - -"eventsource@npm:^3.0.2": - version: 3.0.7 - resolution: "eventsource@npm:3.0.7" - dependencies: - eventsource-parser: "npm:^3.0.1" - checksum: 10c1/de566b3809fe88dbb1cf921d016ca9da493b9943cdad299cd5d1a93f599f805ebd3a45c3c77ea80384b31bdb1897cfa9b5587901f3de9a0d7b979b3766276621 - languageName: node - linkType: hard - "execa@npm:4.1.0": version: 4.1.0 resolution: "execa@npm:4.1.0" @@ -8273,15 +8177,6 @@ __metadata: languageName: node linkType: hard -"express-rate-limit@npm:^7.5.0": - version: 7.5.0 - resolution: "express-rate-limit@npm:7.5.0" - peerDependencies: - express: ^4.11 || 5 || ^5.0.0-beta.1 - checksum: 10c1/05e32bb2627f2f341a5a6bf5340ce819490ad700ce1d740800b204a4fd7e6e2f86d017e39cdebf4b80cff7415c03875bb05e3ee4087068685f02831ddde4bfcc - languageName: node - linkType: hard - "express@npm:^4.21.2": version: 4.21.2 resolution: "express@npm:4.21.2" @@ -8321,41 +8216,6 @@ __metadata: languageName: node linkType: hard -"express@npm:^5.0.1": - version: 5.1.0 - resolution: "express@npm:5.1.0" - dependencies: - accepts: "npm:^2.0.0" - body-parser: "npm:^2.2.0" - content-disposition: "npm:^1.0.0" - content-type: "npm:^1.0.5" - cookie: "npm:^0.7.1" - cookie-signature: "npm:^1.2.1" - debug: "npm:^4.4.0" - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - etag: "npm:^1.8.1" - finalhandler: "npm:^2.1.0" - fresh: "npm:^2.0.0" - http-errors: "npm:^2.0.0" - merge-descriptors: "npm:^2.0.0" - mime-types: "npm:^3.0.0" - on-finished: "npm:^2.4.1" - once: "npm:^1.4.0" - parseurl: "npm:^1.3.3" - proxy-addr: "npm:^2.0.7" - qs: "npm:^6.14.0" - range-parser: "npm:^1.2.1" - router: "npm:^2.2.0" - send: "npm:^1.1.0" - serve-static: "npm:^2.2.0" - statuses: "npm:^2.0.1" - type-is: "npm:^2.0.1" - vary: "npm:^1.1.2" - checksum: 10c1/e7a1ce5b5322b5abb48b2de434e8e734df68fdb2502aa32b4b69b301041fd3f4556f0bc7d760cb4cc57b30db5448e2777fa2380a9c845209076c4433efd2db05 - languageName: node - linkType: hard - "extend@npm:^3.0.0, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -8592,20 +8452,6 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:^2.1.0": - version: 2.1.0 - resolution: "finalhandler@npm:2.1.0" - dependencies: - debug: "npm:^4.4.0" - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - on-finished: "npm:^2.4.1" - parseurl: "npm:^1.3.3" - statuses: "npm:^2.0.1" - checksum: 10c1/4a7c653c37d454a034ea36c0c2408bfa6d18597f1752c49690545abc3537acea840a24cffb831e7aa6e6d9f1bfe95a44c92dcf792d5e89e8816c760c5f3aa385 - languageName: node - linkType: hard - "find-cache-dir@npm:^2.0.0": version: 2.1.0 resolution: "find-cache-dir@npm:2.1.0" @@ -8768,13 +8614,6 @@ __metadata: languageName: node linkType: hard -"fresh@npm:^2.0.0": - version: 2.0.0 - resolution: "fresh@npm:2.0.0" - checksum: 10c1/13be1f0577fc652a412a1ccb2343dd5bcf70dbb8912c4dd2beb75ff5fead552d763f33d5add437f020552864dec3cebbf1a6245a1f1be2562f89b90f28d22321 - languageName: node - linkType: hard - "fs-extra@npm:^7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" @@ -8993,11 +8832,11 @@ __metadata: linkType: hard "get-tsconfig@npm:^4.10.0, get-tsconfig@npm:^4.7.0": - version: 4.10.0 - resolution: "get-tsconfig@npm:4.10.0" + version: 4.10.1 + resolution: "get-tsconfig@npm:4.10.1" dependencies: resolve-pkg-maps: "npm:^1.0.0" - checksum: 10c1/9c77b946c79ede0940c3257ec02067b49faee25d7fb1f1376e3b7fab3b72250a33a4fdf9f970f79ceebedf04c92390f300d2647c91c2ce20695f4ed98ed90736 + checksum: 10c1/ef21fd27464678c41e7e6516103097e1fd327a8cb3f21b07cd20cdeb4a134030fd00b30d6bbac7ff8268b73f367a87e1e702faa5827442041c329ab959cd0c61 languageName: node linkType: hard @@ -9479,7 +9318,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": +"http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -9654,7 +9493,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -10139,13 +9978,6 @@ __metadata: languageName: node linkType: hard -"is-promise@npm:^4.0.0": - version: 4.0.0 - resolution: "is-promise@npm:4.0.0" - checksum: 10c1/5dedc059652258cffc4509b6e7e060c94599a65fa7db9d50464b13eb6e0b7212bfe60629c907945de04ff3c53c0211ab05efcb74cf0a712a2c3c1be071e07c50 - languageName: node - linkType: hard - "is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -11890,13 +11722,6 @@ __metadata: languageName: node linkType: hard -"media-typer@npm:^1.1.0": - version: 1.1.0 - resolution: "media-typer@npm:1.1.0" - checksum: 10c1/668b46f687eec72516db5ddc736013c6196137249da67e02c15136a066294ada219141f15ed0366478c5942eb8e5d2e36d6ebc1b290aaf2ebc16d49a419c4251 - languageName: node - linkType: hard - "mem@npm:^8.0.0": version: 8.1.1 resolution: "mem@npm:8.1.1" @@ -11933,13 +11758,6 @@ __metadata: languageName: node linkType: hard -"merge-descriptors@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-descriptors@npm:2.0.0" - checksum: 10c1/90b0ab10f3016e2aa0d6e2d5e095aafe45cbf503bf97e7a0b41ca837333647ec2f7244e370aa0ea0b0fd6a4f0eaf54c64e85169fb58f4d1ffb8b34c617cfd0b7 - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -12314,7 +12132,7 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.54.0": +"mime-db@npm:>= 1.43.0 < 2": version: 1.54.0 resolution: "mime-db@npm:1.54.0" checksum: 10c1/c8da99e264bc5db086f56f633c0640730da754f99a3a3858c8c5365779273e985e8c8f7e8da9e68deeec2ae99c923809cfc37fe3ef1b195333c2468a635d13b7 @@ -12330,15 +12148,6 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": - version: 3.0.1 - resolution: "mime-types@npm:3.0.1" - dependencies: - mime-db: "npm:^1.54.0" - checksum: 10c1/f4267a18893fedf4afdb4d4f945ef489614d217ab572367eb257df72bbf4a2f3802d16b8bcef96abfc62b056bcef46678264a9ba4ed9192717f502705ef23051 - languageName: node - linkType: hard - "mime@npm:1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -12893,7 +12702,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:4.x, object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": +"object-assign@npm:4.x, object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c1/e42b3d041acc8d82fca7bdd57b9d11c277c430fb7dccc33c479c1c10dbe4f34e33025c541f5bac77589ee2413a4d7ece62b1c0035b52119bdb193abf15c0a755 @@ -13407,7 +13216,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10c1/7256a6e3ff8a851297a0ae1373834443cbfeb082e655eb123274af8d2b8806618b3ded58a14cf3eb061f610a8f73245d5fdd611b608ece758e34575d0d2d214a @@ -13499,13 +13308,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^8.0.0": - version: 8.2.0 - resolution: "path-to-regexp@npm:8.2.0" - checksum: 10c1/3ea2a84a685c549c66d68cb9f4b81524359700df7af610da949294deeff0497b2c8f4309637621c1d9ed55074cf9ea681bb8ec49544f1089c8efc22fa8c080f0 - languageName: node - linkType: hard - "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -13587,13 +13389,6 @@ __metadata: languageName: node linkType: hard -"pkce-challenge@npm:^5.0.0": - version: 5.0.0 - resolution: "pkce-challenge@npm:5.0.0" - checksum: 10c1/824413edf08e71edc03402f7ac1191115c5e53bec0a3a1e8c9d643dd4fcac79e6aaaa7bc242afcd8a5695e7a22d84f6634ebe5efed406be1af4ced2ddb43ea2c - languageName: node - linkType: hard - "pkg-dir@npm:^3.0.0": version: 3.0.0 resolution: "pkg-dir@npm:3.0.0" @@ -14299,7 +14094,7 @@ __metadata: languageName: node linkType: hard -"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7": +"proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" dependencies: @@ -14358,7 +14153,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.14.0, qs@npm:^6.14.0": +"qs@npm:6.14.0": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -14430,18 +14225,6 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:^3.0.0": - version: 3.0.0 - resolution: "raw-body@npm:3.0.0" - dependencies: - bytes: "npm:3.1.2" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.6.3" - unpipe: "npm:1.0.0" - checksum: 10c1/fb87972020036f51fe82b313124425697739057311ebe0d712c28ba80701b0c86b890989b088b8bdb0de796ed9e3e04e3fec0fc9d456887aa97f5d944d151413 - languageName: node - linkType: hard - "rc-align@npm:^2.4.0": version: 2.4.5 resolution: "rc-align@npm:2.4.5" @@ -14803,7 +14586,7 @@ __metadata: languageName: node linkType: hard -"rc-select@npm:~14.16.2, rc-select@npm:~14.16.7": +"rc-select@npm:~14.16.2, rc-select@npm:~14.16.8": version: 14.16.8 resolution: "rc-select@npm:14.16.8" dependencies: @@ -14863,7 +14646,7 @@ __metadata: languageName: node linkType: hard -"rc-table@npm:~7.50.4": +"rc-table@npm:~7.50.5": version: 7.50.5 resolution: "rc-table@npm:7.50.5" dependencies: @@ -15861,19 +15644,6 @@ __metadata: languageName: node linkType: hard -"router@npm:^2.2.0": - version: 2.2.0 - resolution: "router@npm:2.2.0" - dependencies: - debug: "npm:^4.4.0" - depd: "npm:^2.0.0" - is-promise: "npm:^4.0.0" - parseurl: "npm:^1.3.3" - path-to-regexp: "npm:^8.0.0" - checksum: 10c1/3ac1c459224f12f80c06163075e0970130ecce554cab6f75c8995a863635a3c67b968ab93ad7877a44ee53f491ae38f4594a33ddc8cf83631b41327d754e46b5 - languageName: node - linkType: hard - "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -16115,25 +15885,6 @@ __metadata: languageName: node linkType: hard -"send@npm:^1.1.0, send@npm:^1.2.0": - version: 1.2.0 - resolution: "send@npm:1.2.0" - dependencies: - debug: "npm:^4.3.5" - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - etag: "npm:^1.8.1" - fresh: "npm:^2.0.0" - http-errors: "npm:^2.0.0" - mime-types: "npm:^3.0.1" - ms: "npm:^2.1.3" - on-finished: "npm:^2.4.1" - range-parser: "npm:^1.2.1" - statuses: "npm:^2.0.1" - checksum: 10c1/5bf65110948e8834068b069ba7508ef55ac63b74f7e2233e42d39f52452f513c99795a8aaf834161686189193cb5938a2225bbc7cc0a491dd8f068638e31feaf - languageName: node - linkType: hard - "serialize-javascript@npm:^6.0.0, serialize-javascript@npm:^6.0.2": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" @@ -16170,18 +15921,6 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:^2.2.0": - version: 2.2.0 - resolution: "serve-static@npm:2.2.0" - dependencies: - encodeurl: "npm:^2.0.0" - escape-html: "npm:^1.0.3" - parseurl: "npm:^1.3.3" - send: "npm:^1.2.0" - checksum: 10c1/343e6baca7d8bf6a6c6176a18c0a57351fd54964a6ba825cd5577c09e6618414eb61067c02fc3e5206604e402c8b0ceaf5e8747e6bc8c89cb1194b10f5f97a93 - languageName: node - linkType: hard - "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -16665,7 +16404,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1, statuses@npm:^2.0.1": +"statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 10c1/6230edd96de95e58310c9bdc24d9487ea633f17994daad44c1d86055e6cf667de5834c8acaa376dc7ebd44198792797ff2caf04b4bb549bc83e31865bc57fdfb @@ -17083,12 +16822,11 @@ __metadata: linkType: hard "synckit@npm:^0.11.0": - version: 0.11.5 - resolution: "synckit@npm:0.11.5" + version: 0.11.6 + resolution: "synckit@npm:0.11.6" dependencies: "@pkgr/core": "npm:^0.2.4" - tslib: "npm:^2.8.1" - checksum: 10c1/a4fcf437959f0c4465c11b30e3910077375f875ced6f4b8e237c18f04a25e7a55a6b65b46e9b4909bece3367974d42556110701272d129f64afc6b25fd56fb9a + checksum: 10c1/f5d6fd3561e4b78fb2cc05c68428a3cff410104d4ce07b6d186a8f138d49586182201e2fdbbba18f6d08889c9824d70d6015b61df43681c407a66f8597ad76c9 languageName: node linkType: hard @@ -17133,9 +16871,9 @@ __metadata: linkType: hard "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": - version: 2.2.1 - resolution: "tapable@npm:2.2.1" - checksum: 10c1/3835d697b2c9269546fd4971dbce9026a53d19207f90457085437f85c0abe809e83389e8a0e098bbba14af932d752905184bd87fad68818f275fcafc73ac4f2a + version: 2.2.2 + resolution: "tapable@npm:2.2.2" + checksum: 10c1/87fee52177ff587369d76ff8a8ee94f90b8a7bf2b16ed9128130bd7ff7a6cb17a53b28c13833e3bc344533415ebbc40f368df8f65f04d4bc40e8832472ee1025 languageName: node linkType: hard @@ -17428,11 +17166,11 @@ __metadata: linkType: hard "tree-dump@npm:^1.0.1": - version: 1.0.2 - resolution: "tree-dump@npm:1.0.2" + version: 1.0.3 + resolution: "tree-dump@npm:1.0.3" peerDependencies: tslib: 2 - checksum: 10c1/fbb190f064098b16541f8168b3ea31cc7bcab986c7dee03ae8631a7ec80ea6bb9ad4edd64ca8a0a057bceee5f4048dec8ae8bad8a116efda8060777363484990 + checksum: 10c1/c49e7af32b097a316c029f3662ce967d361502259148f8b37417f9f4d98baac110c0639f0d3a501ad69c5223ddc614326f0f72987ace2b7337c7bccc2e6c215c languageName: node linkType: hard @@ -17476,8 +17214,8 @@ __metadata: linkType: hard "ts-jest@npm:^29.3.3": - version: 29.3.3 - resolution: "ts-jest@npm:29.3.3" + version: 29.3.4 + resolution: "ts-jest@npm:29.3.4" dependencies: bs-logger: "npm:^0.2.6" ejs: "npm:^3.1.10" @@ -17509,7 +17247,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: 10c1/ba465e96c89b28019f67a17e6843c6e9a9eefb3897b2fc096fba8b4706f92114e812a860032c8b180cdbb9d07573f33f96e4242c3faecb4edd135211358a20fa + checksum: 10c1/298e3d4b6dba4a6a1a07d59b03d72f076a307e4af28d2054e6b32e400b0869b77bb0560414cbb94f35a389c99040048d84b8207d3fd3353a5792c58763f4efb0 languageName: node linkType: hard @@ -17570,7 +17308,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1": +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c1/22a6e86110cc6556a51eef13055c67961df40b8376ba34d5b3d608c671c9284e10d533cb077d6cd270bc3fffb30bd406644e33f83df6fae5de9c43d84fee54a6 @@ -17623,17 +17361,6 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^2.0.0, type-is@npm:^2.0.1": - version: 2.0.1 - resolution: "type-is@npm:2.0.1" - dependencies: - content-type: "npm:^1.0.5" - media-typer: "npm:^1.1.0" - mime-types: "npm:^3.0.0" - checksum: 10c1/2982ddcca2231d5e6b5e7b8a087744d82fb8a425487516102eca665e52d7a87053fa59ede6003a13c90d89d28d88cade80b36fedbbf7382302142fd659887ae0 - languageName: node - linkType: hard - "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -18031,14 +17758,14 @@ __metadata: linkType: hard "use-isomorphic-layout-effect@npm:^1.2.0": - version: 1.2.0 - resolution: "use-isomorphic-layout-effect@npm:1.2.0" + version: 1.2.1 + resolution: "use-isomorphic-layout-effect@npm:1.2.1" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10c1/decd26ea500f6fa93319167b9b674532107c621ad7bf0e3ca79b5265289dc70e1979e49c4cb497595de1478706a0166bd43406378a0b1a6ce6143c02152ec311 + checksum: 10c1/3bed041911b8aa1143e13e28331f6c8dcd1f9a8b8647a4a3a9d0b04dc882827e33b0bdb1e009f71b4b7dc0f7d1506b612f4cbbb5a66600d119b9a5e11070c77a languageName: node linkType: hard @@ -18137,7 +17864,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": +"vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: 10c1/9664fb39d625fdd82df935d5883ca6e1593203f47e49860e7d36e147908dec73753f5a0f59739f71df563c635e75a5d31a1330683373f0967df8a7dba1efd01a @@ -18256,12 +17983,12 @@ __metadata: linkType: hard "watchpack@npm:^2.4.1": - version: 2.4.2 - resolution: "watchpack@npm:2.4.2" + version: 2.4.4 + resolution: "watchpack@npm:2.4.4" dependencies: glob-to-regexp: "npm:^0.4.1" graceful-fs: "npm:^4.1.2" - checksum: 10c1/badd7fb089b8f486055a8f17d783e54b96dcba6f3ad39bf481eab17d2d174b4a4cb68a659fe425d8c2c463589be41bf0cbd4686fc6fba6aafd6d03df17d9c8a1 + checksum: 10c1/64e6d92b1d4728dd2ef20d56b26415a9565c47f3902462b171f85f3e996b1f3afe208b8f54fae414458841397655f9f22d474d7a6b2a998bfeda565e957f3b90 languageName: node linkType: hard @@ -18419,8 +18146,8 @@ __metadata: linkType: hard "webpack@npm:^5.99.8": - version: 5.99.8 - resolution: "webpack@npm:5.99.8" + version: 5.99.9 + resolution: "webpack@npm:5.99.9" dependencies: "@types/eslint-scope": "npm:^3.7.7" "@types/estree": "npm:^1.0.6" @@ -18451,7 +18178,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10c1/5732aa765582af0650c12f274339864eea36250e3edc028605fedf6364adc68eaa0ad41e422630254ee4423a6ac87a5b5ead115e4d474e8ce98f416103c678ca + checksum: 10c1/446154d34349333600efd47505f3146325aaeb2b1b1c1bc83870c4c4a7769bcae8304cfba9fd95dfed40faf7aeb3524ecb362f922ad4d5c8fd5dd6e7574c3db0 languageName: node linkType: hard @@ -18845,22 +18572,6 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.24.1": - version: 3.24.5 - resolution: "zod-to-json-schema@npm:3.24.5" - peerDependencies: - zod: ^3.24.1 - checksum: 10c1/f78198944e43dbf34cf9b588278a5f7c56617e4731a1ad7849558a5c22b8c9ce2e089b7434b0e6b5c410bb3eb7142e214df9ca47db357009442555a939c8d4ee - languageName: node - linkType: hard - -"zod@npm:^3.23.8, zod@npm:^3.24.2": - version: 3.24.4 - resolution: "zod@npm:3.24.4" - checksum: 10c1/9573d5ca32ee8c8aaf5d44c4c40a118f5abe67abaa0786bcb0f71a89e9845536efeb0ad9dd5d81ef4c78727abd7d50af252ef527f12da9f71e57c768cee6b073 - languageName: node - linkType: hard - "zrender@npm:5.6.1": version: 5.6.1 resolution: "zrender@npm:5.6.1" diff --git a/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql b/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql index 79076b23e..8003f6753 100644 --- a/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql +++ b/scripts/schema/db/init_dbs/postgresql/1.23.0/1.23.0.sql @@ -20,6 +20,8 @@ $fn_def$, :'next_version') -- DROP SCHEMA IF EXISTS or_cache CASCADE; +ALTER TABLE public.tenants + ALTER COLUMN scope_state SET DEFAULT 2; COMMIT; diff --git a/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/scripts/schema/db/init_dbs/postgresql/init_schema.sql index 9db52343b..7c6eb396e 100644 --- a/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -103,7 +103,7 @@ CREATE TABLE public.tenants t_users integer NOT NULL DEFAULT 1, t_integrations integer NOT NULL DEFAULT 0, last_telemetry bigint NOT NULL DEFAULT CAST(EXTRACT(epoch FROM date_trunc('day', now())) * 1000 AS BIGINT), - scope_state smallint NOT NULL DEFAULT 0, + scope_state smallint NOT NULL DEFAULT 2, CONSTRAINT onerow_uni CHECK (tenant_id = 1) ); diff --git a/spot/entrypoints/audio/index.html b/spot/entrypoints/audio/index.html index 45000e227..5cdac81b9 100644 --- a/spot/entrypoints/audio/index.html +++ b/spot/entrypoints/audio/index.html @@ -8,10 +8,10 @@ -
- - - +
+ + +

@@ -23,7 +23,7 @@ - + Allow microphone access diff --git a/spot/entrypoints/content/SavingControls.tsx b/spot/entrypoints/content/SavingControls.tsx index d79c84ed5..bbf1c2568 100644 --- a/spot/entrypoints/content/SavingControls.tsx +++ b/spot/entrypoints/content/SavingControls.tsx @@ -380,7 +380,7 @@ function SavingControls({ ) : null}
{playing() ? (
void }) {
-
+

View Recording

@@ -142,7 +142,7 @@ function Settings({ goBack }: { goBack: () => void }) {

-
+

Include DevTools @@ -164,7 +164,7 @@ function Settings({ goBack }: { goBack: () => void }) {

-
+

Use Debugger @@ -185,7 +185,7 @@ function Settings({ goBack }: { goBack: () => void }) {

-
+

Ingest Point

diff --git a/spot/entrypoints/popup/components/AudioPicker.tsx b/spot/entrypoints/popup/components/AudioPicker.tsx index 627b5c840..9d24198eb 100644 --- a/spot/entrypoints/popup/components/AudioPicker.tsx +++ b/spot/entrypoints/popup/components/AudioPicker.tsx @@ -19,7 +19,7 @@ const AudioPicker: Component = (props) => { return (
diff --git a/spot/entrypoints/popup/components/Header.tsx b/spot/entrypoints/popup/components/Header.tsx index ec6f9d5b8..4f194105b 100644 --- a/spot/entrypoints/popup/components/Header.tsx +++ b/spot/entrypoints/popup/components/Header.tsx @@ -35,7 +35,7 @@ const Header: Component = (props) => {
-
+
@@ -50,7 +50,7 @@ const Header: Component = (props) => { target="_blank" rel="noopener noreferrer" > -
+
@@ -61,7 +61,7 @@ const Header: Component = (props) => { data-tip="Settings" onClick={props.openSettings} > -
+