resolved conflicts
This commit is contained in:
commit
649f30f5da
96 changed files with 1731 additions and 1921 deletions
8
.github/workflows/patch-build-old.yaml
vendored
8
.github/workflows/patch-build-old.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
11
api/chalicelib/core/events/__init__.py
Normal file
11
api/chalicelib/core/events/__init__.py
Normal 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
|
||||
97
api/chalicelib/core/events/events_ch.py
Normal file
97
api/chalicelib/core/events/events_ch.py
Normal 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
|
||||
|
|
@ -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
|
||||
209
api/chalicelib/core/events/events_pg.py
Normal file
209
api/chalicelib/core/events/events_pg.py
Normal 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}
|
||||
11
api/chalicelib/core/issues/__init__.py
Normal file
11
api/chalicelib/core/issues/__init__.py
Normal 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
|
||||
56
api/chalicelib/core/issues/issues_ch.py
Normal file
56
api/chalicelib/core/issues/issues_ch.py
Normal 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
|
||||
|
|
@ -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 [
|
||||
{
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
3
ee/api/.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './DashboardMetricSelection';
|
||||
|
|
@ -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} {t('Metrics')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default withRouter(DashboardModal);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './DashboardModal';
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
+ {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>
|
||||
{t('out of')}
|
||||
<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));
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next';
|
|||
interface Props {
|
||||
siteId: string;
|
||||
dashboardId: string;
|
||||
onEditHandler: () => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { WidgetCategoryItem } from './WidgetCategoryItem';
|
||||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
136
frontend/app/components/Kai/components/ChatsModal.tsx
Normal file
136
frontend/app/components/Kai/components/ChatsModal.tsx
Normal 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);
|
||||
31
frontend/app/components/Kai/components/Usage.tsx
Normal file
31
frontend/app/components/Kai/components/Usage.tsx
Normal 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);
|
||||
36
frontend/app/components/Kai/utils.ts
Normal file
36
frontend/app/components/Kai/utils.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export function LoadingFetch({ provider }: { provider: string }) {
|
|||
<LoadingOutlined size={32} />
|
||||
<div>
|
||||
{t('Fetching logs from')}
|
||||
|
||||
{provider}
|
||||
...
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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="You’re viewing the original recording. Processed Spot will be available here shortly."
|
||||
showIcon
|
||||
type="info"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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:')}
|
||||
|
|
|
|||
|
|
@ -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} {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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue