resolved conflicts

This commit is contained in:
Андрей Бабушкин 2025-05-21 16:48:48 +02:00
commit 649f30f5da
96 changed files with 1731 additions and 1921 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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 [
{

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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()

View file

@ -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

View file

@ -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
}
}

View file

@ -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)

View file

@ -75,4 +75,5 @@ EXP_AUTOCOMPLETE=true
EXP_ALERTS=true
EXP_ERRORS_SEARCH=true
EXP_METRICS=true
EXP_SESSIONS_SEARCH=true
EXP_SESSIONS_SEARCH=true
EXP_EVENTS=true

View file

@ -68,4 +68,5 @@ EXP_CH_DRIVER=true
EXP_AUTOCOMPLETE=true
EXP_ALERTS=true
EXP_ERRORS_SEARCH=true
EXP_METRICS=true
EXP_METRICS=true
EXP_EVENTS=true

View file

@ -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

View file

@ -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):

3
ee/api/.gitignore vendored
View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

@ -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;

View file

@ -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
);

View file

@ -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 (
<Suspense fallback={<Loader loading className="flex-1" />}>
<Switch key="content">
<Route
exact
strict
path={SCOPE_SETUP}
component={enhancedComponents.ScopeSetup}
/>
{redirectToSetup ? <Redirect to={SCOPE_SETUP} /> : null}
<Route path={CLIENT_PATH} component={enhancedComponents.Client} />
<Route
path={withSiteId(ONBOARDING_PATH, siteIdList)}

View file

@ -7,6 +7,7 @@ import { useModal } from 'Components/ModalContext';
import Widget from '@/mstore/types/widget';
import { useTranslation } from 'react-i18next';
import { FilterKey } from 'Types/filter/filterType';
import { observer } from 'mobx-react-lite';
interface Props {
metric?: any;
@ -128,4 +129,4 @@ function SessionsBy(props: Props) {
);
}
export default SessionsBy;
export default observer(SessionsBy);

View file

@ -3,7 +3,7 @@ import { Tag } from 'antd';
function MethodType({ data }) {
return (
<Tag bordered={false} className="rounded-lg bg-indigo-50">
<Tag bordered={false} className="rounded-lg bg-indigo-lightest">
{data.method}
</Tag>
);

View file

@ -47,7 +47,7 @@ function AddCardSelectionModal(props: Props) {
<Row gutter={16} justify="center" className="py-5">
<Col span={12}>
<div
className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3"
className="flex flex-col items-center justify-center hover:bg-indigo-lightest border rounded-lg shadow-sm cursor-pointer gap-3"
style={{ height: '80px' }}
onClick={() => onClick(true)}
>
@ -57,7 +57,7 @@ function AddCardSelectionModal(props: Props) {
</Col>
<Col span={12}>
<div
className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3"
className="flex flex-col items-center justify-center hover:bg-indigo-lightest border rounded-lg shadow-sm cursor-pointer gap-3"
style={{ height: '80px' }}
onClick={() => onClick(false)}
>

View file

@ -123,7 +123,7 @@ function AlertListItem(props: Props) {
<div className="col-span-2">
<div className="flex items-center">
<Tag
className="rounded-full bg-indigo-50 cap-first text-base"
className="rounded-full bg-indigo-lightest cap-first text-base"
bordered={false}
>
{alert.detectionMethod}

View file

@ -15,7 +15,7 @@ function CardIssueItem(props: Props) {
title={issue.name}
description={<div className="text-nowrap truncate">{issue.source}</div>}
avatar={<Icon name={issue.icon} size="24" />}
className="cursor-pointer hover:bg-indigo-50"
className="cursor-pointer hover:bg-indigo-lightest"
/>
<div>{issue.sessionCount}</div>
</>

View file

@ -60,7 +60,7 @@ function CardsLibrary(props: Props) {
onClick={(e) => onItemClick(e, metric.metricId)}
/>
<Card
className="border border-transparent hover:border-indigo-50 hover:shadow-sm rounded-lg"
className="border border-transparent hover:border-indigo-lightest hover:shadow-sm rounded-lg"
style={{
border: selectedList.includes(metric.metricId)
? '1px solid #1890ff'

View file

@ -1,195 +0,0 @@
import React, { useEffect } from 'react';
import { useObserver } from 'mobx-react-lite';
import { Icon, Loader } from 'UI';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import WidgetWrapper from '../WidgetWrapper';
import { useTranslation } from 'react-i18next';
interface IWiProps {
category: Record<string, any>;
onClick: (category: Record<string, any>) => void;
isSelected: boolean;
selectedWidgetIds: string[];
}
const ICONS: Record<string, string | null> = {
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 (
<div
className={cn('rounded p-4 border cursor-pointer hover:bg-active-blue', {
'bg-active-blue border-blue': isSelected,
'bg-white': !isSelected,
})}
onClick={() => onClick(category)}
>
<div className="font-medium text-lg mb-2 capitalize flex items-center">
{/* @ts-ignore */}
{ICONS[category.name] && (
<Icon name={ICONS[category.name]} size={18} className="mr-2" />
)}
{category.name}
</div>
<div className="mb-2 text-sm leading-tight">{category.description}</div>
{selectedCategoryWidgetsCount > 0 && (
<div className="flex items-center">
<span className="color-gray-medium text-sm">{`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`}</span>
</div>
)}
</div>
);
}
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<any>();
const [selectAllCheck, setSelectAllCheck] = React.useState(false);
const selectedWidgetIds = useObserver(() =>
dashboardStore.selectedWidgets.map((widget: any) => widget.metricId),
);
const scrollContainer = React.useRef<HTMLDivElement>(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(() => (
<Loader loading={loadingTemplates}>
<div className="grid grid-cols-12 gap-4 my-3 items-end">
<div className="col-span-3">
<div className="uppercase color-gray-medium text-lg">{t('Type')}</div>
</div>
<div className="col-span-9 flex items-center">
{activeCategory && (
<>
<div className="flex items-baseline">
<h2 className="text-2xl capitalize">{activeCategory.name}</h2>
<span className="text-2xl color-gray-medium ml-2">
{activeCategory.widgets.length}
</span>
</div>
<div className="ml-auto">
<label className="flex items-center ml-3 cursor-pointer select-none">
<input
type="checkbox"
onChange={toggleAllWidgets}
checked={selectAllCheck}
/>
<div className="ml-2">{t('Select All')}</div>
</label>
</div>
</>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-3">
<div
className="grid grid-cols-1 gap-4 py-1 pr-2"
style={{
maxHeight: `calc(100vh - ${props.isDashboardExists ? 175 : 300}px)`,
overflowY: 'auto',
}}
>
{activeCategory &&
widgetCategories.map((category, index) => (
<WidgetCategoryItem
key={category.name}
onClick={handleWidgetCategoryClick}
category={category}
isSelected={activeCategory.name === category.name}
selectedWidgetIds={selectedWidgetIds}
/>
))}
</div>
</div>
<div className="col-span-9">
<div
className="grid grid-cols-4 gap-4 -mx-4 px-4 pb-40 items-start py-1"
style={{ maxHeight: 'calc(100vh - 170px)', overflowY: 'auto' }}
ref={scrollContainer}
>
{activeCategory &&
activeCategory.widgets.map((widget: any) => (
<WidgetWrapper
key={widget.metricId}
widget={widget}
active={selectedWidgetIds.includes(widget.metricId)}
isTemplate
isSaved={widget.metricType === 'predefined'}
onClick={() => dashboardStore.toggleWidgetSelection(widget)}
/>
))}
{props.isDashboardExists && activeCategory?.name === 'custom' && (
<div
className={cn(
'relative rounded border col-span-1 cursor-pointer',
'flex flex-col items-center justify-center bg-white',
'hover:bg-active-blue hover:shadow-border-main text-center py-16',
)}
onClick={props.handleCreateNew}
>
<Icon name="plus" size="16" />
<span className="mt-2">{t('Create Metric')}</span>
</div>
)}
</div>
</div>
</div>
</Loader>
));
}
export default DashboardMetricSelection;

View file

@ -1 +0,0 @@
export { default } from './DashboardMetricSelection';

View file

@ -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(() => (
<div style={{ maxWidth: '85vw' }}>
<div
className="border-r shadow p-4 h-screen"
style={{
backgroundColor: '#FAFAFA',
zIndex: 999,
width: '100%',
maxWidth: PANEL_SIZES.maxWidth,
}}
>
<div className="mb-6 flex items-end justify-between">
<div>
<h1 className="text-2xl">
{isDashboardExists
? t('Add metrics to dashboard')
: t('Create Dashboard')}
</h1>
</div>
<div>
<span className="text-md">{t('Past 7 days data')}</span>
</div>
</div>
{!isDashboardExists && (
<>
<DashboardForm />
<p>
{t(
'Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.',
)}
</p>
</>
)}
<DashboardMetricSelection
handleCreateNew={handleCreateNew}
isDashboardExists={isDashboardExists}
/>
<div className="flex items-center absolute bottom-0 left-0 right-0 bg-white border-t p-3">
<Button
type="primary"
disabled={!dashboard.isValid || loading}
onClick={onSave}
className="flaot-left mr-2"
>
{isDashboardExists ? t('Add Selected to Dashboard') : t('Create')}
</Button>
<span className="ml-2 color-gray-medium">
{selectedWidgetsCount}&nbsp;{t('Metrics')}
</span>
</div>
</div>
</div>
));
}
export default withRouter(DashboardModal);

View file

@ -1 +0,0 @@
export { default } from './DashboardModal';

View file

@ -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(
<DashboardModal
siteId={siteId}
onMetricAdd={pushQuery}
dashboardId={dashboardId}
/>,
{ right: true },
);
};
if (!dashboard) return null;
const originStr = window.env.ORIGIN || window.location.origin;
@ -117,7 +107,6 @@ function DashboardView(props: Props) {
<DashboardWidgetGrid
siteId={siteId}
dashboardId={dashboardId}
onEditHandler={onAddWidgets}
id="report"
/>
</div>

View file

@ -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<Record<string, any>[]>([]);
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<string, any>) => {
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 (
<div style={{ maxWidth: '85vw', width: 1200 }}>
<div
className="border-l shadow h-screen"
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%' }}
>
<div className="py-6 px-8 flex items-start justify-between">
<div className="flex flex-col">
<h1 className="text-2xl">{title}</h1>
<div className="text-disabled-text">{description}</div>
</div>
<Button
variant="text"
className="text-main font-medium ml-2"
onClick={onCreateNew}
>
+&nbsp;{t('Create New')}
</Button>
</div>
<Loader loading={dashboardStore.loadingTemplates}>
<div
className="grid h-full grid-cols-4 gap-4 px-8 items-start py-1"
style={{
maxHeight: 'calc(100vh - 160px)',
overflowY: 'auto',
gridAutoRows: 'max-content',
}}
>
{metrics ? (
metrics.map((metric: any) => (
<WidgetWrapper
key={metric.metricId}
widget={metric}
active={selectedWidgetIds.includes(metric.metricId)}
isTemplate
isSaved={metric.metricType === 'predefined'}
onClick={() => dashboardStore.toggleWidgetSelection(metric)}
/>
))
) : (
<div>{t('No custom metrics created.')}</div>
)}
</div>
</Loader>
<div className="py-4 border-t px-8 bg-white w-full flex items-center justify-between">
<div>
{t('Selected')}
<span className="font-medium">{selectedWidgetIds.length}</span>
&nbsp;{t('out of')}&nbsp;
<span className="font-medium">{metrics ? metrics.length : 0}</span>
</div>
<Button
type="primary"
disabled={selectedWidgetIds.length === 0}
onClick={onSave}
>
{t('Add Selected')}
</Button>
</div>
</div>
</div>
);
}
export default withRouter(observer(AddMetric));

View file

@ -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 (
<div
onClick={onClick}
className={cn(
'flex items-center hover:bg-gray-lightest group rounded border cursor-pointer',
isPremade
? 'bg-figmaColors-primary-outlined-hover-background hover:!border-tealx'
: 'hover:!border-teal bg-figmaColors-secondary-outlined-hover-background',
isPopup ? 'p-4 z-50' : 'px-4 py-8 flex-col',
)}
style={{ borderColor: 'rgb(238, 238, 238)' }}
>
<div
className={cn(
'p-6 my-3 rounded-full group-hover:bg-gray-light',
isPremade
? 'bg-figmaColors-primary-outlined-hover-background fill-figmaColors-accent-secondary group-hover:!bg-figmaColors-accent-secondary group-hover:!fill-white'
: 'bg-figmaColors-secondary-outlined-hover-background fill-figmaColors-secondary-outlined-resting-border group-hover:!bg-teal group-hover:!fill-white',
)}
>
<Icon name={iconName} size={26} style={{ fill: 'inherit' }} />
</div>
<div
className={
isPopup
? 'flex flex-col text-left ml-4'
: 'flex flex-col text-center items-center'
}
>
<div className="font-bold text-base text-figmaColors-text-primary">
{title}
</div>
<div
className={cn(
'text-disabled-test text-figmaColors-text-primary text-base',
isPopup ? 'w-full' : 'mt-2 w-2/3 text-center',
)}
>
{description}
</div>
</div>
</div>
);
}
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(
<AddMetric
siteId={siteId}
title="Custom Metrics"
description="Metrics that are manually created by you or your team."
/>,
{ right: true },
);
};
const onAddPredefinedMetrics = () => {
onAction?.();
dashboardStore.initDashboard(dashboardStore.selectedDashboard);
showModal(
<AddPredefinedMetric
siteId={siteId}
title="Ready-Made Metrics"
description="Curated metrics predfined by OpenReplay."
/>,
{ 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 (
<div
style={{
borderColor: 'rgb(238, 238, 238)',
height: isPopup ? undefined : 300,
}}
className={classes}
>
<AddMetricButton
title="+ Add Custom Metric"
description="Metrics that are manually created by you or your team"
iconName="bar-pencil"
onClick={onAddCustomMetrics}
isPremade
isPopup={isPopup}
/>
<AddMetricButton
title="+ Add Ready-Made Metric"
description="Curated metrics predfined by OpenReplay."
iconName="grid-check"
onClick={onAddPredefinedMetrics}
isPopup={isPopup}
/>
</div>
);
}
export default observer(AddMetricContainer);

View file

@ -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 {

View file

@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next';
interface Props {
siteId: string;
dashboardId: string;
onEditHandler: () => void;
id?: string;
}

View file

@ -56,7 +56,7 @@ function ExcludeFilters(props: Props) {
))}
</div>
) : (
<Button type="link" onClick={addPageFilter}>
<Button type="link" onClick={addPageFilter} className="!text-black">
{t('Add Exclusion')}
</Button>
)}

View file

@ -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}
</Tag>

View file

@ -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 (
<div className="grid grid-cols-4 gap-4 m-4 items-start">
{list.map((metric: any) => (
<React.Fragment key={metric.metricId}>
<WidgetWrapper
key={metric.metricId}
widget={metric}
isGridView
active={selectedList.includes(metric.metricId)}
isSaved
onClick={() => onItemClick(parseInt(metric.metricId))}
/>
</React.Fragment>
))}
</div>
);
}
export default withRouter(GridView);

View file

@ -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<string, any>;
onClick: (category: Record<string, any>) => void;
isSelected: boolean;
selectedWidgetIds: string[];
}
const ICONS: Record<string, string | null> = {
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 (
<div
className={cn('rounded p-4 border cursor-pointer hover:bg-active-blue', {
'bg-active-blue border-blue': isSelected,
'bg-white': !isSelected,
})}
onClick={() => onClick(category)}
>
<div className="font-medium text-lg mb-2 capitalize flex items-center">
{/* @ts-ignore */}
{ICONS[category.name] && (
<Icon name={ICONS[category.name]} size={18} className="mr-2" />
)}
{category.name}
</div>
<div className="mb-2 text-sm leading-tight">{category.description}</div>
{selectedCategoryWidgetsCount > 0 && (
<div className="flex items-center">
<span className="color-gray-medium text-sm">{`Selected ${selectedCategoryWidgetsCount} of ${category.widgets.length}`}</span>
</div>
)}
</div>
);
}

View file

@ -0,0 +1 @@
export { WidgetCategoryItem } from './WidgetCategoryItem';

View file

@ -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 <div>{t('Unknown metric type')}</div>;
}, [data, compData, enabledRows, _metric]);
}, [data, compData, enabledRows, _metric, data]);
const showTable =
_metric.metricType === TIMESERIES &&

View file

@ -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',

View file

@ -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;

View file

@ -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));

View file

@ -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 (
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
<div className={'w-full rounded-lg overflow-hidden border shadow'}>
<div
className={'w-full rounded-lg overflow-hidden border shadow relative'}
>
<ChatHeader
chatTitle={chatTitle}
openChats={openChats}
@ -133,69 +134,4 @@ function KaiChat() {
);
}
function ChatsModal({
onSelect,
projectId,
}: {
onSelect: (threadId: string, title: string) => 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 (
<div className={'h-screen w-full flex flex-col gap-2 p-4'}>
<div className={'flex items-center font-semibold text-lg gap-2'}>
<MessagesSquare size={16} />
<span>Chats</span>
</div>
{isPending ? (
<div className="animate-pulse text-disabled-text">Loading chats...</div>
) : (
<div className="flex flex-col overflow-y-auto -mx-4 px-4">
{data.map((chat) => (
<div
key={chat.thread_id}
className="flex items-center relative group min-h-8"
>
<div
style={{ width: 270 - 28 - 4 }}
className="rounded-l pl-2 h-full w-full hover:bg-active-blue flex items-center"
>
<div
onClick={() => onSelect(chat.thread_id, chat.title)}
className="cursor-pointer hover:underline truncate"
>
{chat.title}
</div>
</div>
<div
onClick={() => 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"
>
<Trash size={14} className="text-disabled-text" />
</div>
</div>
))}
</div>
)}
</div>
);
}
export default observer(KaiChat);

View file

@ -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;
};
}

View file

@ -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<Message> = [];
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();

View file

@ -1,4 +1,5 @@
import io from 'socket.io-client';
import { toast } from 'react-toastify';
export class ChatManager {
socket: ReturnType<typeof io>;
@ -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';
}

View file

@ -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 (
<div
className={
@ -20,17 +22,21 @@ function ChatHeader({
<div className={'flex-1'}>
{goBack ? (
<div
className={'flex items-center gap-2 font-semibold cursor-pointer'}
className={
'w-fit flex items-center gap-2 font-semibold cursor-pointer'
}
onClick={goBack}
>
<ArrowLeft size={14} />
<div>Back</div>
<div>{t('Back')}</div>
</div>
) : null}
</div>
<div className={'flex items-center gap-2 mx-auto max-w-[80%]'}>
{chatTitle ? (
<div className="font-semibold text-xl whitespace-nowrap truncate">{chatTitle}</div>
<div className="font-semibold text-xl whitespace-nowrap truncate">
{chatTitle}
</div>
) : (
<>
<Icon name={'kai_colored'} size={18} />
@ -38,14 +44,14 @@ function ChatHeader({
</>
)}
</div>
<div
className={
'font-semibold cursor-pointer flex items-center gap-2 flex-1 justify-end'
}
onClick={openChats}
>
<MessagesSquare size={14} />
<div>Chats</div>
<div className={'flex-1 justify-end flex items-center gap-2'}>
<div
className="font-semibold w-fit cursor-pointer flex items-center gap-2"
onClick={openChats}
>
<MessagesSquare size={14} />
<div>{t('Chats')}</div>
</div>
</div>
</div>
);

View file

@ -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<Input>(null);
function ChatInput({
isLoading,
onSubmit,
threadId,
}: {
isLoading?: boolean;
onSubmit: (str: string) => void;
threadId: string;
}) {
const inputRef = React.useRef<typeof Input>(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 (
<Input
onPressEnter={submit}
ref={inputRef}
placeholder={'Ask anything about your product and users...'}
size={'large'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
suffix={
<Button
loading={isLoading}
onClick={submit}
icon={isProcessing ? <OctagonX size={16} /> : <SendHorizonal size={16} />}
type={'text'}
size={'small'}
shape={'circle'}
/>
}
/>
)
<div className="relative">
<Input
onPressEnter={submit}
onKeyDown={(e) => {
if (e.key === 'Escape') {
cancelReplace();
}
}}
ref={inputRef}
placeholder={
limited
? `You've reached the daily limit for queries, come again tomorrow!`
: 'Ask anything about your product and users...'
}
size={'large'}
disabled={limited}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
suffix={
<>
{isReplacing ? (
<Tooltip title={'Cancel Editing'}>
<Button
onClick={cancelReplace}
icon={<OctagonX size={16} />}
type={'text'}
size={'small'}
shape={'circle'}
disabled={limited}
/>
</Tooltip>
) : null}
<Tooltip title={'Send message'}>
<Button
loading={isLoading}
onClick={submit}
disabled={limited}
icon={
isProcessing ? (
<OctagonX size={16} />
) : (
<SendHorizonal size={16} />
)
}
type={'text'}
size={'small'}
shape={'circle'}
/>
</Tooltip>
</>
}
/>
<div className="absolute ml-1 top-2 -right-11">
<Usage />
</div>
</div>
);
}
export default observer(ChatInput)
export default observer(ChatInput);

View file

@ -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({
>
<div className={'flex flex-col gap-4 w-2/3 min-h-max'}>
{messages.map((msg, index) => (
<ChatMsg
key={index}
text={msg.text}
isUser={msg.isUser}
userName={userLetter}
messageId={msg.messageId}
isLast={index === lastHumanMsgInd}
duration={msg.duration}
feedback={msg.feedback}
siteId={projectId}
/>
<React.Fragment key={msg.messageId ?? index}>
<ChatMsg
userName={userLetter}
siteId={projectId}
message={msg}
canEdit={processingStage === null && msg.isUser && index === lastHumanMsgInd}
/>
</React.Fragment>
))}
{processingStage ? (
<ChatNotice

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Icon, CopyButton } from 'UI';
import { observer } from 'mobx-react-lite';
import cn from 'classnames';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@ -10,36 +11,49 @@ import {
ListRestart,
FileDown,
Clock,
ChartLine,
} from 'lucide-react';
import { Button, Tooltip } from 'antd';
import { kaiStore } from '../KaiStore';
import { kaiStore, Message } from '../KaiStore';
import { toast } from 'react-toastify';
import { durationFormatted } from 'App/date';
import WidgetChart from '@/components/Dashboard/components/WidgetChart';
import Widget from 'App/mstore/types/widget';
import { useTranslation } from 'react-i18next';
export function ChatMsg({
text,
isUser,
function ChatMsg({
userName,
messageId,
isLast,
duration,
feedback,
siteId,
canEdit,
message,
}: {
text: string;
isUser: boolean;
messageId: string;
message: Message;
userName?: string;
isLast?: boolean;
duration?: number;
feedback: boolean | null;
canEdit?: boolean;
siteId: string;
}) {
const { t } = useTranslation();
const [metric, setMetric] = React.useState<Widget | null>(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<HTMLDivElement>(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 (
<div
className={cn(
@ -84,7 +117,7 @@ export function ChatMsg({
{isUser ? (
<div
className={
'rounded-full bg-main text-white min-w-8 min-h-8 flex items-center justify-center sticky top-0'
'rounded-full bg-main text-white min-w-8 min-h-8 flex items-center justify-center sticky top-0 shadow'
}
>
<span className={'font-semibold'}>{userName}</span>
@ -92,28 +125,54 @@ export function ChatMsg({
) : (
<div
className={
'rounded-full bg-white shadow min-w-8 min-h-8 flex items-center justify-center sticky top-0'
'rounded-full bg-gray-lightest shadow min-w-8 min-h-8 flex items-center justify-center sticky top-0'
}
>
<Icon name={'kai_colored'} size={18} />
</div>
)}
<div className={'mt-1 flex flex-col'}>
<div className="markdown-body" ref={bodyRef}>
<div
className={cn(
'markdown-body',
isEditing ? 'border-l border-l-main pl-2' : '',
)}
data-openreplay-obscured
ref={bodyRef}
>
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
</div>
{metric ? (
<div className="p-2 border-gray-light rounded-lg shadow">
<WidgetChart metric={metric} isPreview />
</div>
) : null}
{isUser ? (
isLast ? (
<>
<div
onClick={onRetry}
className={
'ml-auto flex items-center gap-2 px-2 rounded-lg border border-gray-medium text-sm cursor-pointer hover:border-main hover:text-main w-fit'
}
onClick={onEdit}
className={cn(
'ml-auto flex items-center gap-2 px-2',
'rounded-lg border border-gray-medium text-sm cursor-pointer',
'hover:border-main hover:text-main w-fit',
canEdit && !isEditing ? '' : 'hidden',
)}
>
<ListRestart size={16} />
<div>Edit</div>
<div>{t('Edit')}</div>
</div>
) : null
<div
onClick={onCancelEdit}
className={cn(
'ml-auto flex items-center gap-2 px-2',
'rounded-lg border border-gray-medium text-sm cursor-pointer',
'hover:border-main hover:text-main w-fit',
isEditing ? '' : 'hidden',
)}
>
<div>{t('Cancel')}</div>
</div>
</>
) : (
<div className={'flex items-center gap-2'}>
{duration ? <MsgDuration duration={duration} /> : null}
@ -132,6 +191,15 @@ export function ChatMsg({
>
<ThumbsDown size={16} />
</IconButton>
{supports_visualization ? (
<IconButton
tooltip="Visualize this answer"
onClick={getChart}
processing={loadingChart}
>
<ChartLine size={16} />
</IconButton>
) : null}
<CopyButton
getHtml={() => bodyRef.current?.innerHTML}
content={text}
@ -215,3 +283,5 @@ function MsgDuration({ duration }: { duration: number }) {
</div>
);
}
export default observer(ChatMsg);

View file

@ -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 (
<div className={'h-screen w-full flex flex-col gap-2 p-4'}>
<div className={'flex items-center font-semibold text-lg gap-2'}>
<MessagesSquare size={16} />
<span>{t('Chats')}</span>
</div>
{usage.percent > 80 ? (
<div className="text-red text-sm">
{t('You have used {{used}} out of {{total}} daily requests', {
used: usage.used,
total: usage.total,
})}
</div>
) : null}
{isPending ? (
<div className="animate-pulse text-disabled-text">{t('Loading chats')}...</div>
) : (
<div className="overflow-y-auto flex flex-col gap-2">
{datedCollections.map((col) => (
<ChatCollection
data={col.entries}
date={col.date}
onSelect={onSelect}
onDelete={onDelete}
/>
))}
</div>
)}
</div>
);
}
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 (
<div>
<div className="text-disabled-text">{date}</div>
<ChatsList data={data} onSelect={onSelect} onDelete={onDelete} />
</div>
);
}
function ChatsList({
data,
onSelect,
onDelete,
}: {
data: { title: string; thread_id: string }[];
onSelect: (threadId: string, title: string) => void;
onDelete: (threadId: string) => void;
}) {
return (
<div className="flex flex-col gap-1 -mx-4 px-4">
{data.map((chat) => (
<div
key={chat.thread_id}
className="flex items-center relative group min-h-7"
>
<div
style={{ width: 270 - 28 - 4 }}
className="rounded-l pl-2 min-h-7 h-full w-full hover:bg-active-blue flex items-center"
>
<div
onClick={() => onSelect(chat.thread_id, chat.title)}
className="cursor-pointer hover:underline truncate"
>
{chat.title}
</div>
</div>
<div
onClick={() => 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"
>
<Trash size={14} className="text-disabled-text" />
</div>
</div>
))}
</div>
);
}
export default observer(ChatsModal);

View file

@ -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 (
<div>
<Tooltip title={`Daily response limit (${usage.used}/${usage.total})`}>
<Progress
percent={usage.percent}
strokeColor={usage.percent < 99 ? 'var(--color-main)' : 'var(--color-red)'}
showInfo={false}
type="circle"
size={24}
/>
</Tooltip>
</div>
);
}
export default observer(Usage);

View file

@ -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);
}

View file

@ -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 (
<div
className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden max-h-[556px] h-[556px]')}
className={cn(
styles.playerBlock,
'flex flex-col',
`overflow-x-hidden max-h-[${outerHeight}px] h-[${outerHeight}px]`,
)}
>
<div className={cn(stl.playerBody, 'flex-1 flex flex-col relative max-h-[504px] h-[504px]')}>
<div
className={cn(
stl.playerBody,
`flex-1 flex flex-col relative max-h-[${innerHeight}px] h-[${innerHeight}px]`,
)}
>
<div className={cn(stl.playerBody, 'flex flex-1 flex-col relative')}>
<div className="relative flex-1 overflow-hidden group">
<ClipPlayerOverlay autoplay={props.autoplay} />

View file

@ -199,6 +199,8 @@ function BottomBlock({ panelHeight, block }: { panelHeight: number; block: numbe
return <BackendLogsPanel />;
case LONG_TASK:
return <LongTaskPanel />;
case OVERVIEW:
return <OverviewPanel />;
default:
return null;
}

View file

@ -13,6 +13,7 @@ export function LoadingFetch({ provider }: { provider: string }) {
<LoadingOutlined size={32} />
<div>
{t('Fetching logs from')}
&nbsp;
{provider}
...
</div>

View file

@ -206,7 +206,7 @@ function HighlightPanel({ onClose }: { onClose: () => void }) {
<Tag
onClick={() => 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}
>

View file

@ -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}
/>

View file

@ -188,7 +188,7 @@ const WarnBadge = React.memo(
className="py-1 ml-3 cursor-pointer"
onClick={() => closeWarning(1)}
>
<Icon name="close" size={16} color="black" />
<Icon name="close" size={16} color="#000000" />
</div>
</div>
) : null}

View file

@ -121,7 +121,7 @@ function AccessModal() {
<div className="text-black/50">
{t('Link for internal team members')}
</div>
<div className="px-2 py-1 rounded-lg bg-indigo-50 whitespace-nowrap overflow-ellipsis overflow-hidden">
<div className="px-2 py-1 rounded-lg bg-indigo-lightest whitespace-nowrap overflow-ellipsis overflow-hidden">
{spotLink}
</div>
</div>
@ -155,7 +155,7 @@ function AccessModal() {
<div className="text-black/50">
{t('Anyone with the following link can access this Spot')}
</div>
<div className="px-2 py-1 rounded-lg bg-indigo-50 whitespace-nowrap overflow-ellipsis overflow-hidden">
<div className="px-2 py-1 rounded-lg bg-indigo-lightest whitespace-nowrap overflow-ellipsis overflow-hidden">
{spotLink}
</div>
</div>

View file

@ -201,7 +201,7 @@ function SpotVideoContainer({
>
{processingState === ProcessingState.Processing ? (
<Alert
className="trimIsProcessing rounded-lg shadow-sm border-indigo-500 bg-indigo-50"
className="trimIsProcessing rounded-lg shadow-sm border-indigo-500 bg-indigo-lightest"
message="Youre viewing the original recording. Processed Spot will be available here shortly."
showIcon
type="info"

View file

@ -148,14 +148,14 @@ function SpotListItem({
<div className="absolute left-0 bottom-8 flex relative gap-2 justify-end pe-2 pb-2 ">
<Tooltip title={tooltipText} className="capitalize">
<div
className="bg-black/70 text-white p-1 px-2 text-xs rounded-lg transition-transform transform translate-y-14 group-hover:translate-y-0 "
className="bg-gray-dark text-white p-1 px-2 text-xs rounded-lg transition-transform transform translate-y-14 group-hover:translate-y-0 "
onClick={copyToClipboard}
style={{ cursor: 'pointer' }}
>
<Link2 size={16} strokeWidth={1} />
</div>
</Tooltip>
<div className="bg-black/70 text-white p-1 px-2 text-xs rounded-lg flex items-center cursor-normal">
<div className="bg-gray-dark text-white p-1 px-2 text-xs rounded-lg flex items-center cursor-normal">
{spot.duration}
</div>
</div>
@ -213,7 +213,7 @@ export function GridItem({
return (
<div
className={`bg-white rounded-lg overflow-hidden shadow-sm border ${
isSelected ? 'border-teal/30' : 'border-transparent'
isSelected ? 'border-teal/30' : ''
} transition flex flex-col items-start hover:border-teal`}
>
<div
@ -277,7 +277,7 @@ export function GridItem({
<div>
<UserOutlined />
</div>
<TextEllipsis text={user} className="capitalize" />
<TextEllipsis text={user} />
<div className="ml-auto">
<ClockCircleOutlined />
</div>

View file

@ -64,10 +64,11 @@ const SpotsListHeader = observer(
type="text"
onClick={onClearSelection}
className="mr-2 px-3"
size='small'
>
{t('Clear')}
</Button>
<Button onClick={onDelete} type="primary" ghost>
<Button onClick={onDelete} type="primary" ghost size='small'>
{t('Delete')} ({selectedCount})
</Button>
</>

View file

@ -408,7 +408,7 @@ const TaskSummary = observer(() => {
<Typography.Title level={5}>{t('Task Summary')}</Typography.Title>
{uxtestingStore.taskStats.length ? (
<div className="p-2 rounded-lg bg-indigo-50 flex items-center gap-1 px-4">
<div className="p-2 rounded-lg bg-indigo-lightest flex items-center gap-1 px-4">
<ClockCircleOutlined rev={undefined} />
<Typography.Text>
{t('Average completion time of all tasks:')}

View file

@ -23,7 +23,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-start py-1">
<div className="font-medium w-36">{t('Name')}</div>
<Tag
className="text-base rounded-lg bg-indigo-50 whitespace-normal break-words"
className="text-base rounded-lg bg-indigo-lightest whitespace-normal break-words"
bordered={false}
style={{ maxWidth: '300px' }}
>
@ -35,7 +35,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="font-medium w-36">{t('Request Method')}</div>
<Tag
className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-ellipsis"
className="text-base rounded-lg bg-indigo-lightest whitespace-nowrap overflow-hidden text-ellipsis"
bordered={false}
>
{resource.method}
@ -49,7 +49,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<Tag
bordered={false}
className={cn(
'text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-ellipsis flex items-center',
'text-base rounded-lg bg-indigo-lightest whitespace-nowrap overflow-hidden text-ellipsis flex items-center',
{ 'error color-red': !resource.success },
)}
>
@ -61,7 +61,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="font-medium w-36">{t('Type')}</div>
<Tag
className="text-base capitalize rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-ellipsis"
className="text-base capitalize rounded-lg bg-indigo-lightest whitespace-nowrap overflow-hidden text-ellipsis"
bordered={false}
>
{resource.type}
@ -72,7 +72,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="font-medium w-36">{t('Size')}</div>
<Tag
className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-ellipsis"
className="text-base rounded-lg bg-indigo-lightest whitespace-nowrap overflow-hidden text-ellipsis"
bordered={false}
>
{formatBytes(resource.decodedBodySize)}
@ -84,7 +84,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="font-medium w-36">{t('Duration')}</div>
<Tag
className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-ellipsis"
className="text-base rounded-lg bg-indigo-lightest whitespace-nowrap overflow-hidden text-ellipsis"
bordered={false}
>
{_duration}&nbsp;{t('ms')}
@ -96,7 +96,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="font-medium w-36">{t('Time')}</div>
<Tag
className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-ellipsis"
className="text-base rounded-lg bg-indigo-lightest whitespace-nowrap overflow-hidden text-ellipsis"
bordered={false}
>
{timestamp}

View file

@ -128,7 +128,7 @@ function FetchTimings({ timings }: { timings: Record<string, number> }) {
key={index}
className="grid grid-cols-12 items-center gap-2 space-y-2"
>
<div className="col-span-4 text-sm text-neutral-950 font-medium flex items-center gap-2">
<div className="col-span-4 text-sm text-black font-medium flex items-center gap-2">
<Tooltip title={phase.description}>
<HelpCircle size={12} />
</Tooltip>
@ -160,11 +160,11 @@ function FetchTimings({ timings }: { timings: Record<string, number> }) {
))}
<div className="grid grid-cols-12 items-center gap-2 pt-2 border-t border-t-gray-light mt-2">
<div className="col-span-3 text-sm text-neutral-950 font-semibold">
<div className="col-span-3 text-sm text-black font-semibold">
Total:
</div>
<div className="col-span-7"></div>
<div className="col-span-2 text-right font-mono text-sm text-neutral-950 font-semibold">
<div className="col-span-2 text-right font-mono text-sm text-black font-semibold">
{formatTime(total)}{' '}
{isAdjusted ? (
<span className="ml-1 text-xs text-yellow">

View file

@ -29,7 +29,7 @@ function SupportModal(props: Props) {
className="!bg-stone-50"
>
<div className="flex flex-col items-center gap-2">
<div className="p-3 bg-white flex rounded-lg shadow-sm hover:bg-indigo-50">
<div className="p-3 bg-white flex rounded-lg shadow-sm hover:bg-indigo-lightest">
<div className="shrink-0 w-10 mt-2">
<Icon name="bookmark" size={18} />
</div>
@ -61,7 +61,7 @@ function SupportModal(props: Props) {
</div>
</div>
<div className="p-3 bg-white flex rounded-lg shadow-sm hover:bg-indigo-50">
<div className="p-3 bg-white flex rounded-lg shadow-sm hover:bg-indigo-lightest">
<div className="shrink-0 w-10 mt-2">
<Icon name="slack" size={18} />
</div>
@ -92,7 +92,7 @@ function SupportModal(props: Props) {
</div>
</div>
<div className="p-3 bg-white flex rounded-lg shadow-sm hover:bg-indigo-50">
<div className="p-3 bg-white flex rounded-lg shadow-sm hover:bg-indigo-lightest">
<div className="shrink-0 w-10 mt-2">
<Icon name="github" size={18} />
</div>

View file

@ -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<string, any> = Period({ rangeName: LAST_24_HOURS });
drillDownFilter: Filter = new Filter();
comparisonFilter: Filter = new Filter();
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
selectedDensity: number = 7;
comparisonPeriods: Record<string, any> = {};
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<any> {
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<any> {
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 {

View file

@ -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')) {

View file

@ -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(() => {

View file

@ -44,7 +44,7 @@ export function PlayButton({ togglePlay, iconSize, state }: IProps) {
>
<div
onClick={togglePlay}
className="hover-main color-main cursor-pointer rounded-full hover:bg-indigo-50"
className="hover-main color-main cursor-pointer rounded-full hover:bg-indigo-lightest"
>
<Icon name={icon} size={iconSize} color="inherit" />
</div>

View file

@ -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) }

View file

@ -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);
}
}

View file

@ -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)',

View file

@ -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',

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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)
);

View file

@ -8,10 +8,10 @@
<link href="../../assets/main.css" rel="stylesheet" />
</head>
<body>
<div class="w-full h-screen flex flex-col justify-center items-center bg-indigo-50 p-10 gap-5">
<div class="w-full h-screen flex flex-col justify-center items-center bg-indigo-lightest p-10 gap-5">
<h1 class="font-bold text-4xl flex flex-col gap-2 items-start">
<svg width="147" height="235" viewBox="0 0 147 235" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_252_6)">
@ -23,7 +23,7 @@
</clipPath>
</defs>
</svg>
<span class="flex flex-col items-center">
<span class=" rounded-full w-12 h-12 shadow-sm mb-2 bg-white flex items-center p-2"><svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg></span>
Allow microphone access

View file

@ -380,7 +380,7 @@ function SavingControls({
) : null}
<div class={"flex items-center gap-2"}>
<div
class={`${playing() ? "" : "bg-indigo-100"} cursor-pointer btn btn-ghost btn-circle btn-sm hover:bg-indigo-50 border border-slate-100`}
class={`${playing() ? "" : "bg-indigo-100"} cursor-pointer btn btn-ghost btn-circle btn-sm hover:bg-indigo-lightest border border-slate-100`}
>
{playing() ? (
<div

View file

@ -109,7 +109,7 @@ function Settings({ goBack }: { goBack: () => void }) {
<div class={"flex flex-col"}>
<div class={"flex gap-2 items-center justify-between p-4"}>
<button
class="btn btn-xs btn-circle bg-white hover:bg-indigo-50"
class="btn btn-xs btn-circle bg-white hover:bg-indigo-lightest"
onClick={goBack}
>
<img src={arrowLeft} alt={"Go back"} />
@ -124,7 +124,7 @@ function Settings({ goBack }: { goBack: () => void }) {
</div>
<div class="flex flex-col">
<div class="p-4 border-b border-slate-300 hover:bg-indigo-50">
<div class="p-4 border-b border-slate-300 hover:bg-indigo-lightest">
<div class="flex flex-row justify-between items-center">
<p class="font-semibold mb-1 flex items-center">View Recording</p>
@ -142,7 +142,7 @@ function Settings({ goBack }: { goBack: () => void }) {
</p>
</div>
<div class="flex flex-col border-b border-slate-300 cursor-default justify-between p-4 hover:bg-indigo-50">
<div class="flex flex-col border-b border-slate-300 cursor-default justify-between p-4 hover:bg-indigo-lightest">
<div class="flex flex-row justify-between items-center">
<p class="font-semibold mb-1 flex items-center">
<span>Include DevTools</span>
@ -164,7 +164,7 @@ function Settings({ goBack }: { goBack: () => void }) {
</p>
</div>
<div class="p-4 border-b border-slate-300 hover:bg-indigo-50 cursor-default">
<div class="p-4 border-b border-slate-300 hover:bg-indigo-lightest cursor-default">
<div class="flex flex-row justify-between items-center">
<p class="font-semibold mb-1 flex items-center">
<span>Use Debugger</span>
@ -185,7 +185,7 @@ function Settings({ goBack }: { goBack: () => void }) {
</p>
</div>
<div class="p-4 hover:bg-indigo-50 cursor-default">
<div class="p-4 hover:bg-indigo-lightest cursor-default">
<div class="flex flex-row justify-between">
<p class="font-semibold mb-1">Ingest Point</p>
<div>

View file

@ -19,7 +19,7 @@ const AudioPicker: Component<AudioPickerProps> = (props) => {
return (
<div class="inline-flex items-center gap-1 text-xs">
<div
class="p-1 cursor-pointer btn btn-xs bg-white hover:bg-indigo-50 pointer-events-auto tooltip tooltip-right text-sm font-normal"
class="p-1 cursor-pointer btn btn-xs bg-white hover:bg-indigo-lightest pointer-events-auto tooltip tooltip-right text-sm font-normal"
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
onClick={props.onMicToggle}
>

View file

@ -35,7 +35,7 @@ const Header: Component<HeaderProps> = (props) => {
<div class="ml-auto flex items-center gap-2">
<div class="text-sm tooltip tooltip-bottom" data-tip="My Spots">
<div onClick={openHomePage}>
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
<div class="cursor-pointer p-2 hover:bg-indigo-lightest rounded-full">
<HomePageSvg />
</div>
</div>
@ -50,7 +50,7 @@ const Header: Component<HeaderProps> = (props) => {
target="_blank"
rel="noopener noreferrer"
>
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
<div class="cursor-pointer p-2 hover:bg-indigo-lightest rounded-full">
<SlackSvg />
</div>
</a>
@ -61,7 +61,7 @@ const Header: Component<HeaderProps> = (props) => {
data-tip="Settings"
onClick={props.openSettings}
>
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
<div class="cursor-pointer p-2 hover:bg-indigo-lightest rounded-full">
<SettingsSvg />
</div>
</div>