pulled dev

This commit is contained in:
Андрей Бабушкин 2025-05-12 11:23:27 +02:00
commit c6d64fc986
113 changed files with 3535 additions and 1692 deletions

View file

@ -0,0 +1,57 @@
from typing import Optional
from chalicelib.utils import helper
from chalicelib.utils.ch_client import ClickHouseClient
def search_events(project_id: int, q: Optional[str] = None):
with ClickHouseClient() as ch_client:
full_args = {"project_id": project_id, "limit": 20}
constraints = ["project_id = %(project_id)s",
"_timestamp >= now()-INTERVAL 1 MONTH"]
if q:
constraints += ["value ILIKE %(q)s"]
full_args["q"] = helper.string_to_sql_like(q)
query = ch_client.format(
f"""SELECT value,data_count
FROM product_analytics.autocomplete_events_grouped
WHERE {" AND ".join(constraints)}
ORDER BY data_count DESC
LIMIT %(limit)s;""",
parameters=full_args)
rows = ch_client.execute(query)
return {"values": helper.list_to_camel_case(rows), "_src": 2}
def search_properties(project_id: int, property_name: Optional[str] = None, event_name: Optional[str] = None,
q: Optional[str] = None):
with ClickHouseClient() as ch_client:
select = "value"
full_args = {"project_id": project_id, "limit": 20,
"event_name": event_name, "property_name": property_name}
constraints = ["project_id = %(project_id)s",
"_timestamp >= now()-INTERVAL 1 MONTH"]
if event_name:
constraints += ["event_name = %(event_name)s"]
if property_name and q:
constraints += ["property_name = %(property_name)s"]
elif property_name:
select = "DISTINCT ON(property_name) property_name AS value"
constraints += ["property_name ILIKE %(property_name)s"]
full_args["property_name"] = helper.string_to_sql_like(property_name)
if q:
constraints += ["value ILIKE %(q)s"]
full_args["q"] = helper.string_to_sql_like(q)
query = ch_client.format(
f"""SELECT {select},data_count
FROM product_analytics.autocomplete_event_properties_grouped
WHERE {" AND ".join(constraints)}
ORDER BY data_count DESC
LIMIT %(limit)s;""",
parameters=full_args)
rows = ch_client.execute(query)
return {"values": helper.list_to_camel_case(rows), "_src": 2}

View file

@ -7,30 +7,69 @@ from chalicelib.utils.ch_client import ClickHouseClient
from chalicelib.utils.exp_ch_helper import get_sub_condition
logger = logging.getLogger(__name__)
PREDEFINED_EVENTS = {
"CLICK": "String",
"INPUT": "String",
"LOCATION": "String",
"ERROR": "String",
"PERFORMANCE": "String",
"REQUEST": "String"
}
def get_events(project_id: int, page: schemas.PaginatedSchema):
with ClickHouseClient() as ch_client:
r = ch_client.format(
"""SELECT DISTINCT ON(event_name,auto_captured)
COUNT(1) OVER () AS total,
event_name AS name, display_name, description,
auto_captured
FROM product_analytics.all_events
WHERE project_id=%(project_id)s
ORDER BY auto_captured,display_name
LIMIT %(limit)s OFFSET %(offset)s;""",
"""SELECT DISTINCT
ON(event_name,auto_captured)
COUNT (1) OVER () AS total,
event_name AS name, display_name, description,
auto_captured
FROM product_analytics.all_events
WHERE project_id=%(project_id)s
ORDER BY auto_captured, display_name
LIMIT %(limit)s
OFFSET %(offset)s;""",
parameters={"project_id": project_id, "limit": page.limit, "offset": (page.page - 1) * page.limit})
rows = ch_client.execute(r)
if len(rows) == 0:
return {"total": 0, "list": []}
return {"total": len(PREDEFINED_EVENTS), "list": [{
"name": e,
"displayName": "",
"description": "",
"autoCaptured": True,
"id": "event_0",
"dataType": "string",
"possibleTypes": [
"string"
],
"_foundInPredefinedList": False
} for e in PREDEFINED_EVENTS]}
total = rows[0]["total"]
rows = helper.list_to_camel_case(rows)
for i, row in enumerate(rows):
row["id"] = f"event_{i}"
row["icon"] = None
row["dataType"] = "string"
row["possibleTypes"] = ["string"]
row["_foundInPredefinedList"] = True
row.pop("total")
return {"total": total, "list": helper.list_to_camel_case(rows)}
keys = [r["name"] for r in rows]
for e in PREDEFINED_EVENTS:
if e not in keys:
total += 1
rows.append({
"name": e,
"displayName": "",
"description": "",
"autoCaptured": True,
"id": "event_0",
"dataType": "string",
"possibleTypes": [
"string"
],
"_foundInPredefinedList": False
})
return {"total": total, "list": rows}
def search_events(project_id: int, data: schemas.EventsSearchPayloadSchema):
@ -109,31 +148,33 @@ def search_events(project_id: int, data: schemas.EventsSearchPayloadSchema):
parameters=full_args)
rows = ch_client.execute(query)
if len(rows) == 0:
return {"total": 0, "rows": [], "src": 2}
return {"total": 0, "rows": [], "_src": 2}
total = rows[0]["total"]
for r in rows:
r.pop("total")
return {"total": total, "rows": rows, "src": 2}
return {"total": total, "rows": rows, "_src": 2}
def get_lexicon(project_id: int, page: schemas.PaginatedSchema):
with ClickHouseClient() as ch_client:
r = ch_client.format(
"""SELECT COUNT(1) OVER () AS total,
all_events.event_name AS name,
*
FROM product_analytics.all_events
WHERE project_id=%(project_id)s
ORDER BY display_name
LIMIT %(limit)s OFFSET %(offset)s;""",
"""SELECT COUNT(1) OVER () AS total, all_events.event_name AS name,
*
FROM product_analytics.all_events
WHERE project_id = %(project_id)s
ORDER BY display_name
LIMIT %(limit)s
OFFSET %(offset)s;""",
parameters={"project_id": project_id, "limit": page.limit, "offset": (page.page - 1) * page.limit})
rows = ch_client.execute(r)
if len(rows) == 0:
return {"total": 0, "list": []}
total = rows[0]["total"]
rows = helper.list_to_camel_case(rows)
for i, row in enumerate(rows):
row["id"] = f"event_{i}"
row["icon"] = None
row["dataType"] = "string"
row["possibleTypes"] = ["string"]
row["_foundInPredefinedList"] = True
row.pop("total")
return {"total": total, "list": helper.list_to_camel_case(rows)}
return {"total": total, "list": rows}

View file

@ -1,46 +1,77 @@
import re
from functools import cache
import schemas
from chalicelib.utils import helper, exp_ch_helper
from chalicelib.utils.ch_client import ClickHouseClient
@cache
def get_predefined_property_types():
with ClickHouseClient() as ch_client:
properties_type = ch_client.execute("""\
SELECT type
FROM system.columns
WHERE database = 'product_analytics'
AND table = 'events'
AND name = '$properties';""")
if len(properties_type) == 0:
return {}
properties_type = properties_type[0]["type"]
pattern = r'(\w+)\s+(Enum8\([^\)]+\)|[A-Za-z0-9_]+(?:\([^\)]+\))?)'
# Find all matches
matches = re.findall(pattern, properties_type)
# Create a dictionary of attribute names and types
attributes = {match[0]: match[1] for match in matches}
return attributes
PREDEFINED_PROPERTIES = {
"label": "String",
"hesitation_time": "UInt32",
"name": "String",
"payload": "String",
"level": "Enum8",
"source": "Enum8",
"message": "String",
"error_id": "String",
"duration": "UInt16",
"context": "Enum8",
"url_host": "String",
"url_path": "String",
"url_hostpath": "String",
"request_start": "UInt16",
"response_start": "UInt16",
"response_end": "UInt16",
"dom_content_loaded_event_start": "UInt16",
"dom_content_loaded_event_end": "UInt16",
"load_event_start": "UInt16",
"load_event_end": "UInt16",
"first_paint": "UInt16",
"first_contentful_paint_time": "UInt16",
"speed_index": "UInt16",
"visually_complete": "UInt16",
"time_to_interactive": "UInt16",
"ttfb": "UInt16",
"ttlb": "UInt16",
"response_time": "UInt16",
"dom_building_time": "UInt16",
"dom_content_loaded_event_time": "UInt16",
"load_event_time": "UInt16",
"min_fps": "UInt8",
"avg_fps": "UInt8",
"max_fps": "UInt8",
"min_cpu": "UInt8",
"avg_cpu": "UInt8",
"max_cpu": "UInt8",
"min_total_js_heap_size": "UInt64",
"avg_total_js_heap_size": "UInt64",
"max_total_js_heap_size": "UInt64",
"min_used_js_heap_size": "UInt64",
"avg_used_js_heap_size": "UInt64",
"max_used_js_heap_size": "UInt64",
"method": "Enum8",
"status": "UInt16",
"success": "UInt8",
"request_body": "String",
"response_body": "String",
"transfer_size": "UInt32",
"selector": "String",
"normalized_x": "Float32",
"normalized_y": "Float32",
"message_id": "UInt64"
}
def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
with ClickHouseClient() as ch_client:
r = ch_client.format(
"""SELECT COUNT(1) OVER () AS total,
property_name AS name, display_name,
array_agg(DISTINCT event_properties.value_type) AS possible_types
FROM product_analytics.all_properties
"""SELECT COUNT(1) OVER () AS total, property_name AS name,
display_name,
array_agg(DISTINCT event_properties.value_type) AS possible_types
FROM product_analytics.all_properties
LEFT JOIN product_analytics.event_properties USING (project_id, property_name)
WHERE all_properties.project_id=%(project_id)s
GROUP BY property_name,display_name
ORDER BY display_name
LIMIT %(limit)s OFFSET %(offset)s;""",
WHERE all_properties.project_id = %(project_id)s
GROUP BY property_name, display_name
ORDER BY display_name
LIMIT %(limit)s
OFFSET %(offset)s;""",
parameters={"project_id": project_id,
"limit": page.limit,
"offset": (page.page - 1) * page.limit})
@ -49,56 +80,80 @@ def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
return {"total": 0, "list": []}
total = properties[0]["total"]
properties = helper.list_to_camel_case(properties)
predefined_properties = get_predefined_property_types()
for i, p in enumerate(properties):
p["id"] = f"prop_{i}"
p["icon"] = None
if p["name"] in predefined_properties:
p["possibleTypes"].insert(0, predefined_properties[p["name"]])
p["possibleTypes"] = list(set(p["possibleTypes"]))
p["possibleTypes"] = exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])
p["_foundInPredefinedList"] = False
if p["name"] in PREDEFINED_PROPERTIES:
p["dataType"] = exp_ch_helper.simplify_clickhouse_type(PREDEFINED_PROPERTIES[p["name"]])
p["_foundInPredefinedList"] = True
p["possibleTypes"] = list(set(exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])))
p.pop("total")
keys = [p["name"] for p in properties]
for p in PREDEFINED_PROPERTIES:
if p not in keys:
total += 1
properties.append({
"name": p,
"displayName": "",
"possibleTypes": [
],
"id": f"prop_{len(properties) + 1}",
"_foundInPredefinedList": False,
"dataType": PREDEFINED_PROPERTIES[p]
})
return {"total": total, "list": properties}
def get_event_properties(project_id: int, event_name):
with ClickHouseClient() as ch_client:
r = ch_client.format(
"""SELECT all_properties.property_name,
all_properties.display_name
FROM product_analytics.event_properties
INNER JOIN product_analytics.all_properties USING (property_name)
WHERE event_properties.project_id=%(project_id)s
AND all_properties.project_id=%(project_id)s
AND event_properties.event_name=%(event_name)s
ORDER BY created_at;""",
"""SELECT all_properties.property_name AS name,
all_properties.display_name,
array_agg(DISTINCT event_properties.value_type) AS possible_types
FROM product_analytics.event_properties
INNER JOIN product_analytics.all_properties USING (property_name)
WHERE event_properties.project_id = %(project_id)s
AND all_properties.project_id = %(project_id)s
AND event_properties.event_name = %(event_name)s
GROUP BY ALL
ORDER BY 1;""",
parameters={"project_id": project_id, "event_name": event_name})
properties = ch_client.execute(r)
properties = helper.list_to_camel_case(properties)
for i, p in enumerate(properties):
p["id"] = f"prop_{i}"
p["_foundInPredefinedList"] = False
if p["name"] in PREDEFINED_PROPERTIES:
p["dataType"] = exp_ch_helper.simplify_clickhouse_type(PREDEFINED_PROPERTIES[p["name"]])
p["_foundInPredefinedList"] = True
p["possibleTypes"] = list(set(exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])))
return helper.list_to_camel_case(properties)
return properties
def get_lexicon(project_id: int, page: schemas.PaginatedSchema):
with ClickHouseClient() as ch_client:
r = ch_client.format(
"""SELECT COUNT(1) OVER () AS total,
all_properties.property_name AS name,
all_properties.*,
possible_types.values AS possible_types,
possible_values.values AS sample_values
FROM product_analytics.all_properties
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value_type) AS values
FROM product_analytics.event_properties
WHERE project_id=%(project_id)s
GROUP BY 1, 2) AS possible_types
USING (project_id, property_name)
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value) AS values
FROM product_analytics.property_values_samples
WHERE project_id=%(project_id)s
GROUP BY 1, 2) AS possible_values USING (project_id, property_name)
WHERE project_id=%(project_id)s
ORDER BY display_name
LIMIT %(limit)s OFFSET %(offset)s;""",
"""SELECT COUNT(1) OVER () AS total, all_properties.property_name AS name,
all_properties.*,
possible_types.values AS possible_types,
possible_values.values AS sample_values
FROM product_analytics.all_properties
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value_type) AS
values
FROM product_analytics.event_properties
WHERE project_id=%(project_id)s
GROUP BY 1, 2) AS possible_types
USING (project_id, property_name)
LEFT JOIN (SELECT project_id, property_name, array_agg(DISTINCT value) AS
values
FROM product_analytics.property_values_samples
WHERE project_id=%(project_id)s
GROUP BY 1, 2) AS possible_values USING (project_id, property_name)
WHERE project_id = %(project_id)s
ORDER BY display_name
LIMIT %(limit)s
OFFSET %(offset)s;""",
parameters={"project_id": project_id,
"limit": page.limit,
"offset": (page.page - 1) * page.limit})
@ -108,6 +163,5 @@ def get_lexicon(project_id: int, page: schemas.PaginatedSchema):
total = properties[0]["total"]
for i, p in enumerate(properties):
p["id"] = f"prop_{i}"
p["icon"] = None
p.pop("total")
return {"total": total, "list": helper.list_to_camel_case(properties)}

View file

@ -73,7 +73,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
return {
'total': 0,
'sessions': [],
'src': 2
'_src': 2
}
if project.platform == "web":
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=error_status,
@ -216,7 +216,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
return {
'total': total,
'sessions': sessions_list,
'src': 2
'_src': 2
}

View file

@ -49,7 +49,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
return {
'total': 0,
'sessions': [],
'src': 1
'_src': 1
}
full_args, query_part = sessions_legacy.search_query_parts(data=data, error_status=error_status,
errors_only=errors_only,
@ -177,7 +177,7 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
return {
'total': total,
'sessions': helper.list_to_camel_case(sessions),
'src': 1
'_src': 1
}
@ -240,7 +240,7 @@ def search_by_metadata(tenant_id, user_id, m_key, m_value, project_id=None):
cur.execute("\nUNION\n".join(sub_queries))
rows = cur.fetchall()
for i in rows:
i["src"] = 1
i["_src"] = 1
results[str(i["project_id"])]["sessions"].append(helper.dict_to_camel_case(i))
return results
@ -248,7 +248,7 @@ def search_by_metadata(tenant_id, user_id, m_key, m_value, project_id=None):
def search_sessions_by_ids(project_id: int, session_ids: list, sort_by: str = 'session_id',
ascending: bool = False) -> dict:
if session_ids is None or len(session_ids) == 0:
return {"total": 0, "sessions": [], "src": 1}
return {"total": 0, "sessions": [], "_src": 1}
with pg_client.PostgresClient() as cur:
meta_keys = metadata.get(project_id=project_id)
params = {"project_id": project_id, "session_ids": tuple(session_ids)}
@ -267,4 +267,4 @@ def search_sessions_by_ids(project_id: int, session_ids: list, sort_by: str = 's
s["metadata"] = {}
for m in meta_keys:
s["metadata"][m["key"]] = s.pop(f'metadata_{m["index"]}')
return {"total": len(rows), "sessions": helper.list_to_camel_case(rows), "src": 1}
return {"total": len(rows), "sessions": helper.list_to_camel_case(rows), "_src": 1}

View file

@ -99,12 +99,13 @@ def simplify_clickhouse_type(ch_type: str) -> str:
return "int"
# Floats: Float32, Float64
if re.match(r'^float(32|64)$', normalized_type):
if re.match(r'^float(32|64)|double$', normalized_type):
return "float"
# Decimal: Decimal(P, S)
if normalized_type.startswith("decimal"):
return "decimal"
# return "decimal"
return "float"
# Date/DateTime
if normalized_type.startswith("date"):
@ -120,11 +121,13 @@ def simplify_clickhouse_type(ch_type: str) -> str:
# UUID
if normalized_type.startswith("uuid"):
return "uuid"
# return "uuid"
return "string"
# Enums: Enum8(...) or Enum16(...)
if normalized_type.startswith("enum8") or normalized_type.startswith("enum16"):
return "enum"
# return "enum"
return "string"
# Arrays: Array(T)
if normalized_type.startswith("array"):

View file

@ -1,16 +1,16 @@
urllib3==2.3.0
urllib3==2.4.0
requests==2.32.3
boto3==1.37.21
boto3==1.38.10
pyjwt==2.10.1
psycopg2-binary==2.9.10
psycopg[pool,binary]==3.2.6
clickhouse-connect==0.8.15
elasticsearch==8.17.2
psycopg[pool,binary]==3.2.7
clickhouse-connect==0.8.17
elasticsearch==9.0.1
jira==3.8.0
cachetools==5.5.2
fastapi==0.115.12
uvicorn[standard]==0.34.0
uvicorn[standard]==0.34.2
python-decouple==3.8
pydantic[email]==2.10.6
pydantic[email]==2.11.4
apscheduler==3.11.0

View file

@ -1,18 +1,18 @@
urllib3==2.3.0
urllib3==2.4.0
requests==2.32.3
boto3==1.37.21
boto3==1.38.10
pyjwt==2.10.1
psycopg2-binary==2.9.10
psycopg[pool,binary]==3.2.6
clickhouse-connect==0.8.15
elasticsearch==8.17.2
psycopg[pool,binary]==3.2.7
clickhouse-connect==0.8.17
elasticsearch==9.0.1
jira==3.8.0
cachetools==5.5.2
fastapi==0.115.12
uvicorn[standard]==0.34.0
uvicorn[standard]==0.34.2
python-decouple==3.8
pydantic[email]==2.10.6
pydantic[email]==2.11.4
apscheduler==3.11.0
redis==5.2.1
redis==6.0.0

View file

@ -4,9 +4,10 @@ from fastapi import Body, Depends, Query
import schemas
from chalicelib.core import metadata
from chalicelib.core.product_analytics import events, properties
from chalicelib.core.product_analytics import events, properties, autocomplete
from or_dependencies import OR_context
from routers.base import get_routers
from typing import Optional
public_app, app, app_apikey = get_routers()
@ -53,3 +54,20 @@ def get_all_lexicon_events(projectId: int, filter_query: Annotated[schemas.Pagin
def get_all_lexicon_properties(projectId: int, filter_query: Annotated[schemas.PaginatedSchema, Query()],
context: schemas.CurrentContext = Depends(OR_context)):
return {"data": properties.get_lexicon(project_id=projectId, page=filter_query)}
@app.get('/{projectId}/events/autocomplete', tags=["autocomplete"])
def autocomplete_events(projectId: int, q: Optional[str] = None,
context: schemas.CurrentContext = Depends(OR_context)):
return {"data": autocomplete.search_events(project_id=projectId, q=None if not q or len(q) == 0 else q)}
@app.get('/{projectId}/properties/autocomplete', tags=["autocomplete"])
def autocomplete_properties(projectId: int, propertyName: str, eventName: Optional[str] = None,
q: Optional[str] = None, context: schemas.CurrentContext = Depends(OR_context)):
return {"data": autocomplete.search_properties(project_id=projectId,
event_name=None if not eventName \
or len(eventName) == 0 else eventName,
property_name=None if not propertyName \
or len(propertyName) == 0 else propertyName,
q=None if not q or len(q) == 0 else q)}

View file

@ -66,7 +66,7 @@ func main() {
messages.MsgMetadata, messages.MsgIssueEvent, messages.MsgSessionStart, messages.MsgSessionEnd,
messages.MsgUserID, messages.MsgUserAnonymousID, messages.MsgIntegrationEvent, messages.MsgPerformanceTrackAggr,
messages.MsgJSException, messages.MsgResourceTiming, messages.MsgCustomEvent, messages.MsgCustomIssue,
messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick,
messages.MsgFetch, messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick,
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange,

View file

@ -100,6 +100,7 @@ func main() {
// Process assets
if msg.TypeID() == messages.MsgSetNodeAttributeURLBased ||
msg.TypeID() == messages.MsgSetCSSDataURLBased ||
msg.TypeID() == messages.MsgCSSInsertRuleURLBased ||
msg.TypeID() == messages.MsgAdoptedSSReplaceURLBased ||
msg.TypeID() == messages.MsgAdoptedSSInsertRuleURLBased {
m := msg.Decode()

View file

@ -133,6 +133,17 @@ func (e *AssetsCache) ParseAssets(msg messages.Message) messages.Message {
}
newMsg.SetMeta(msg.Meta())
return newMsg
case *messages.CSSInsertRuleURLBased:
if e.shouldSkipAsset(m.BaseURL) {
return msg
}
newMsg := &messages.CSSInsertRule{
ID: m.ID,
Index: m.Index,
Rule: e.handleCSS(m.SessionID(), m.BaseURL, m.Rule),
}
newMsg.SetMeta(msg.Meta())
return newMsg
case *messages.AdoptedSSReplaceURLBased:
if e.shouldSkipAsset(m.BaseURL) {
return msg

View file

@ -251,6 +251,7 @@ func (c *connectorImpl) InsertWebInputDuration(session *sessions.Session, msg *m
"hesitation_time": nullableUint32(uint32(msg.HesitationTime)),
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal input event: %s", err)
@ -298,6 +299,7 @@ func (c *connectorImpl) InsertMouseThrashing(session *sessions.Session, msg *mes
"url_hostpath": hostpath,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal issue event: %s", err)
@ -361,6 +363,7 @@ func (c *connectorImpl) InsertIssue(session *sessions.Session, msg *messages.Iss
"url_hostpath": hostpath,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal issue event: %s", err)
@ -457,6 +460,7 @@ func (c *connectorImpl) InsertWebPageEvent(session *sessions.Session, msg *messa
"load_event_time": loadEventTime,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal page event: %s", err)
@ -523,6 +527,7 @@ func (c *connectorImpl) InsertWebClickEvent(session *sessions.Session, msg *mess
"url_hostpath": hostpath,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal click event: %s", err)
@ -568,6 +573,7 @@ func (c *connectorImpl) InsertWebErrorEvent(session *sessions.Session, msg *type
"message": msg.Message,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal error event: %s", err)
@ -625,6 +631,7 @@ func (c *connectorImpl) InsertWebPerformanceTrackAggr(session *sessions.Session,
"max_used_js_heap_size": msg.MaxUsedJSHeapSize,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal performance event: %s", err)
@ -683,6 +690,7 @@ func (c *connectorImpl) InsertRequest(session *sessions.Session, msg *messages.N
"url_hostpath": hostpath,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal request event: %s", err)
@ -721,6 +729,7 @@ func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.Cu
"payload": msg.Payload,
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal custom event: %s", err)
@ -759,6 +768,7 @@ func (c *connectorImpl) InsertGraphQL(session *sessions.Session, msg *messages.G
"response_body": nullableString(msg.Response),
"user_device": session.UserDevice,
"user_device_type": session.UserDeviceType,
"page_title ": msg.PageTitle,
})
if err != nil {
return fmt.Errorf("can't marshal graphql event: %s", err)

View file

@ -25,6 +25,7 @@ type ErrorEvent struct {
Tags map[string]*string
OriginType int
Url string
PageTitle string
}
func WrapJSException(m *JSException) (*ErrorEvent, error) {
@ -37,6 +38,7 @@ func WrapJSException(m *JSException) (*ErrorEvent, error) {
Payload: m.Payload,
OriginType: m.TypeID(),
Url: m.Url,
PageTitle: m.PageTitle,
}, nil
}
@ -50,6 +52,7 @@ func WrapIntegrationEvent(m *IntegrationEvent) *ErrorEvent {
Payload: m.Payload,
OriginType: m.TypeID(),
Url: m.Url,
PageTitle: m.PageTitle,
}
}

View file

@ -77,6 +77,8 @@ func (d *DeadClickDetector) Handle(message Message, timestamp uint64) Message {
*MoveNode,
*RemoveNode,
*SetCSSData,
*CSSInsertRule,
*CSSDeleteRule,
*SetInputValue,
*SetInputChecked:
return d.Build()

View file

@ -2,7 +2,7 @@
package messages
func IsReplayerType(id int) bool {
return 1 != id && 17 != id && 23 != id && 24 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 42 != id && 56 != id && 63 != id && 64 != id && 66 != id && 78 != id && 81 != id && 82 != id && 112 != id && 115 != id && 124 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 124 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id
}
func IsMobileType(id int) bool {
@ -10,5 +10,5 @@ func IsMobileType(id int) bool {
}
func IsDOMType(id int) bool {
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 60 == id || 61 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
}
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
}

View file

@ -44,8 +44,9 @@ func NewMessageIterator(log logger.Logger, messageHandler MessageHandler, messag
iter.filter = filter
}
iter.preFilter = map[int]struct{}{
MsgBatchMetadata: {}, MsgTimestamp: {}, MsgSessionStart: {},
MsgSessionEnd: {}, MsgSetPageLocation: {}, MsgMobileBatchMeta: {},
MsgBatchMetadata: {}, MsgBatchMeta: {}, MsgTimestamp: {},
MsgSessionStart: {}, MsgSessionEnd: {}, MsgSetPageLocation: {},
MsgMobileBatchMeta: {},
}
return iter
}
@ -151,6 +152,20 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
i.version = m.Version
i.batchInfo.version = m.Version
case *BatchMeta: // Is not required to be present in batch since Mobile doesn't have it (though we might change it)
if i.messageInfo.Index > 1 { // Might be several 0-0 BatchMeta in a row without an error though
return fmt.Errorf("batchMeta found at the end of the batch, info: %s", i.batchInfo.Info())
}
i.messageInfo.Index = m.PageNo<<32 + m.FirstIndex // 2^32 is the maximum count of messages per page (ha-ha)
i.messageInfo.Timestamp = uint64(m.Timestamp)
if m.Timestamp == 0 {
i.zeroTsLog("BatchMeta")
}
// Try to get saved session's page url
if savedURL := i.urls.Get(i.messageInfo.batch.sessionID); savedURL != "" {
i.messageInfo.Url = savedURL
}
case *Timestamp:
i.messageInfo.Timestamp = m.Timestamp
if m.Timestamp == 0 {
@ -176,6 +191,7 @@ func (i *messageIteratorImpl) preprocessing(msg Message) error {
case *SetPageLocation:
i.messageInfo.Url = m.URL
i.messageInfo.PageTitle = m.DocumentTitle
// Save session page url in cache for using in next batches
i.urls.Set(i.messageInfo.batch.sessionID, m.URL)

View file

@ -2,6 +2,34 @@ package messages
func transformDeprecated(msg Message) Message {
switch m := msg.(type) {
case *JSExceptionDeprecated:
return &JSException{
Name: m.Name,
Message: m.Message,
Payload: m.Payload,
Metadata: "{}",
}
case *Fetch:
return &NetworkRequest{
Type: "fetch",
Method: m.Method,
URL: m.URL,
Request: m.Request,
Response: m.Response,
Status: m.Status,
Timestamp: m.Timestamp,
Duration: m.Duration,
}
case *IssueEventDeprecated:
return &IssueEvent{
MessageID: m.MessageID,
Timestamp: m.Timestamp,
Type: m.Type,
ContextString: m.ContextString,
Context: m.Context,
Payload: m.Payload,
URL: "",
}
case *ResourceTimingDeprecated:
return &ResourceTiming{
Timestamp: m.Timestamp,

View file

@ -54,6 +54,7 @@ type message struct {
Timestamp uint64
Index uint64
Url string
PageTitle string
batch *BatchInfo
}
@ -70,6 +71,7 @@ func (m *message) SetMeta(origin *message) {
m.Timestamp = origin.Timestamp
m.Index = origin.Index
m.Url = origin.Url
m.PageTitle = origin.PageTitle
}
func (m *message) SessionID() uint64 {

View file

@ -186,6 +186,27 @@ func (msg *SessionStart) TypeID() int {
return 1
}
type SessionEndDeprecated struct {
message
Timestamp uint64
}
func (msg *SessionEndDeprecated) Encode() []byte {
buf := make([]byte, 11)
buf[0] = 3
p := 1
p = WriteUint(msg.Timestamp, buf, p)
return buf[:p]
}
func (msg *SessionEndDeprecated) Decode() Message {
return msg
}
func (msg *SessionEndDeprecated) TypeID() int {
return 3
}
type SetPageLocationDeprecated struct {
message
URL string
@ -711,6 +732,31 @@ func (msg *PageRenderTiming) TypeID() int {
return 24
}
type JSExceptionDeprecated struct {
message
Name string
Message string
Payload string
}
func (msg *JSExceptionDeprecated) Encode() []byte {
buf := make([]byte, 31+len(msg.Name)+len(msg.Message)+len(msg.Payload))
buf[0] = 25
p := 1
p = WriteString(msg.Name, buf, p)
p = WriteString(msg.Message, buf, p)
p = WriteString(msg.Payload, buf, p)
return buf[:p]
}
func (msg *JSExceptionDeprecated) Decode() Message {
return msg
}
func (msg *JSExceptionDeprecated) TypeID() int {
return 25
}
type IntegrationEvent struct {
message
Timestamp uint64
@ -1013,6 +1059,87 @@ func (msg *SetNodeAttributeDictGlobal) TypeID() int {
return 35
}
type CSSInsertRule struct {
message
ID uint64
Rule string
Index uint64
}
func (msg *CSSInsertRule) Encode() []byte {
buf := make([]byte, 31+len(msg.Rule))
buf[0] = 37
p := 1
p = WriteUint(msg.ID, buf, p)
p = WriteString(msg.Rule, buf, p)
p = WriteUint(msg.Index, buf, p)
return buf[:p]
}
func (msg *CSSInsertRule) Decode() Message {
return msg
}
func (msg *CSSInsertRule) TypeID() int {
return 37
}
type CSSDeleteRule struct {
message
ID uint64
Index uint64
}
func (msg *CSSDeleteRule) Encode() []byte {
buf := make([]byte, 21)
buf[0] = 38
p := 1
p = WriteUint(msg.ID, buf, p)
p = WriteUint(msg.Index, buf, p)
return buf[:p]
}
func (msg *CSSDeleteRule) Decode() Message {
return msg
}
func (msg *CSSDeleteRule) TypeID() int {
return 38
}
type Fetch struct {
message
Method string
URL string
Request string
Response string
Status uint64
Timestamp uint64
Duration uint64
}
func (msg *Fetch) Encode() []byte {
buf := make([]byte, 71+len(msg.Method)+len(msg.URL)+len(msg.Request)+len(msg.Response))
buf[0] = 39
p := 1
p = WriteString(msg.Method, buf, p)
p = WriteString(msg.URL, buf, p)
p = WriteString(msg.Request, buf, p)
p = WriteString(msg.Response, buf, p)
p = WriteUint(msg.Status, buf, p)
p = WriteUint(msg.Timestamp, buf, p)
p = WriteUint(msg.Duration, buf, p)
return buf[:p]
}
func (msg *Fetch) Decode() Message {
return msg
}
func (msg *Fetch) TypeID() int {
return 39
}
type Profiler struct {
message
Name string
@ -1506,6 +1633,39 @@ func (msg *SetNodeFocus) TypeID() int {
return 58
}
type LongTask struct {
message
Timestamp uint64
Duration uint64
Context uint64
ContainerType uint64
ContainerSrc string
ContainerId string
ContainerName string
}
func (msg *LongTask) Encode() []byte {
buf := make([]byte, 71+len(msg.ContainerSrc)+len(msg.ContainerId)+len(msg.ContainerName))
buf[0] = 59
p := 1
p = WriteUint(msg.Timestamp, buf, p)
p = WriteUint(msg.Duration, buf, p)
p = WriteUint(msg.Context, buf, p)
p = WriteUint(msg.ContainerType, buf, p)
p = WriteString(msg.ContainerSrc, buf, p)
p = WriteString(msg.ContainerId, buf, p)
p = WriteString(msg.ContainerName, buf, p)
return buf[:p]
}
func (msg *LongTask) Decode() Message {
return msg
}
func (msg *LongTask) TypeID() int {
return 59
}
type SetNodeAttributeURLBased struct {
message
ID uint64
@ -1558,6 +1718,37 @@ func (msg *SetCSSDataURLBased) TypeID() int {
return 61
}
type IssueEventDeprecated struct {
message
MessageID uint64
Timestamp uint64
Type string
ContextString string
Context string
Payload string
}
func (msg *IssueEventDeprecated) Encode() []byte {
buf := make([]byte, 61+len(msg.Type)+len(msg.ContextString)+len(msg.Context)+len(msg.Payload))
buf[0] = 62
p := 1
p = WriteUint(msg.MessageID, buf, p)
p = WriteUint(msg.Timestamp, buf, p)
p = WriteString(msg.Type, buf, p)
p = WriteString(msg.ContextString, buf, p)
p = WriteString(msg.Context, buf, p)
p = WriteString(msg.Payload, buf, p)
return buf[:p]
}
func (msg *IssueEventDeprecated) Decode() Message {
return msg
}
func (msg *IssueEventDeprecated) TypeID() int {
return 62
}
type TechnicalInfo struct {
message
Type string
@ -1625,6 +1816,33 @@ func (msg *AssetCache) TypeID() int {
return 66
}
type CSSInsertRuleURLBased struct {
message
ID uint64
Rule string
Index uint64
BaseURL string
}
func (msg *CSSInsertRuleURLBased) Encode() []byte {
buf := make([]byte, 41+len(msg.Rule)+len(msg.BaseURL))
buf[0] = 67
p := 1
p = WriteUint(msg.ID, buf, p)
p = WriteString(msg.Rule, buf, p)
p = WriteUint(msg.Index, buf, p)
p = WriteString(msg.BaseURL, buf, p)
return buf[:p]
}
func (msg *CSSInsertRuleURLBased) Decode() Message {
return msg
}
func (msg *CSSInsertRuleURLBased) TypeID() int {
return 67
}
type MouseClick struct {
message
ID uint64
@ -1925,6 +2143,31 @@ func (msg *Zustand) TypeID() int {
return 79
}
type BatchMeta struct {
message
PageNo uint64
FirstIndex uint64
Timestamp int64
}
func (msg *BatchMeta) Encode() []byte {
buf := make([]byte, 31)
buf[0] = 80
p := 1
p = WriteUint(msg.PageNo, buf, p)
p = WriteUint(msg.FirstIndex, buf, p)
p = WriteInt(msg.Timestamp, buf, p)
return buf[:p]
}
func (msg *BatchMeta) Decode() Message {
return msg
}
func (msg *BatchMeta) TypeID() int {
return 80
}
type BatchMetadata struct {
message
Version uint64

View file

@ -68,6 +68,15 @@ func DecodeSessionStart(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeSessionEndDeprecated(reader BytesReader) (Message, error) {
var err error = nil
msg := &SessionEndDeprecated{}
if msg.Timestamp, err = reader.ReadUint(); err != nil {
return nil, err
}
return msg, err
}
func DecodeSetPageLocationDeprecated(reader BytesReader) (Message, error) {
var err error = nil
msg := &SetPageLocationDeprecated{}
@ -381,6 +390,21 @@ func DecodePageRenderTiming(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeJSExceptionDeprecated(reader BytesReader) (Message, error) {
var err error = nil
msg := &JSExceptionDeprecated{}
if msg.Name, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Message, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Payload, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
func DecodeIntegrationEvent(reader BytesReader) (Message, error) {
var err error = nil
msg := &IntegrationEvent{}
@ -609,6 +633,60 @@ func DecodeSetNodeAttributeDictGlobal(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeCSSInsertRule(reader BytesReader) (Message, error) {
var err error = nil
msg := &CSSInsertRule{}
if msg.ID, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Rule, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Index, err = reader.ReadUint(); err != nil {
return nil, err
}
return msg, err
}
func DecodeCSSDeleteRule(reader BytesReader) (Message, error) {
var err error = nil
msg := &CSSDeleteRule{}
if msg.ID, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Index, err = reader.ReadUint(); err != nil {
return nil, err
}
return msg, err
}
func DecodeFetch(reader BytesReader) (Message, error) {
var err error = nil
msg := &Fetch{}
if msg.Method, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.URL, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Request, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Response, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Status, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Timestamp, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Duration, err = reader.ReadUint(); err != nil {
return nil, err
}
return msg, err
}
func DecodeProfiler(reader BytesReader) (Message, error) {
var err error = nil
msg := &Profiler{}
@ -921,6 +999,33 @@ func DecodeSetNodeFocus(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeLongTask(reader BytesReader) (Message, error) {
var err error = nil
msg := &LongTask{}
if msg.Timestamp, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Duration, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Context, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.ContainerType, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.ContainerSrc, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.ContainerId, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.ContainerName, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
func DecodeSetNodeAttributeURLBased(reader BytesReader) (Message, error) {
var err error = nil
msg := &SetNodeAttributeURLBased{}
@ -954,6 +1059,30 @@ func DecodeSetCSSDataURLBased(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeIssueEventDeprecated(reader BytesReader) (Message, error) {
var err error = nil
msg := &IssueEventDeprecated{}
if msg.MessageID, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Timestamp, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Type, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.ContextString, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Context, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Payload, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
func DecodeTechnicalInfo(reader BytesReader) (Message, error) {
var err error = nil
msg := &TechnicalInfo{}
@ -987,6 +1116,24 @@ func DecodeAssetCache(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeCSSInsertRuleURLBased(reader BytesReader) (Message, error) {
var err error = nil
msg := &CSSInsertRuleURLBased{}
if msg.ID, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Rule, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Index, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.BaseURL, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
func DecodeMouseClick(reader BytesReader) (Message, error) {
var err error = nil
msg := &MouseClick{}
@ -1167,6 +1314,21 @@ func DecodeZustand(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeBatchMeta(reader BytesReader) (Message, error) {
var err error = nil
msg := &BatchMeta{}
if msg.PageNo, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.FirstIndex, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Timestamp, err = reader.ReadInt(); err != nil {
return nil, err
}
return msg, err
}
func DecodeBatchMetadata(reader BytesReader) (Message, error) {
var err error = nil
msg := &BatchMetadata{}
@ -1941,6 +2103,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeTimestamp(reader)
case 1:
return DecodeSessionStart(reader)
case 3:
return DecodeSessionEndDeprecated(reader)
case 4:
return DecodeSetPageLocationDeprecated(reader)
case 5:
@ -1983,6 +2147,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodePageLoadTiming(reader)
case 24:
return DecodePageRenderTiming(reader)
case 25:
return DecodeJSExceptionDeprecated(reader)
case 26:
return DecodeIntegrationEvent(reader)
case 27:
@ -2003,6 +2169,12 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeStringDictGlobal(reader)
case 35:
return DecodeSetNodeAttributeDictGlobal(reader)
case 37:
return DecodeCSSInsertRule(reader)
case 38:
return DecodeCSSDeleteRule(reader)
case 39:
return DecodeFetch(reader)
case 40:
return DecodeProfiler(reader)
case 41:
@ -2041,16 +2213,22 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeLoadFontFace(reader)
case 58:
return DecodeSetNodeFocus(reader)
case 59:
return DecodeLongTask(reader)
case 60:
return DecodeSetNodeAttributeURLBased(reader)
case 61:
return DecodeSetCSSDataURLBased(reader)
case 62:
return DecodeIssueEventDeprecated(reader)
case 63:
return DecodeTechnicalInfo(reader)
case 64:
return DecodeCustomIssue(reader)
case 66:
return DecodeAssetCache(reader)
case 67:
return DecodeCSSInsertRuleURLBased(reader)
case 68:
return DecodeMouseClick(reader)
case 69:
@ -2075,6 +2253,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeJSException(reader)
case 79:
return DecodeZustand(reader)
case 80:
return DecodeBatchMeta(reader)
case 81:
return DecodeBatchMetadata(reader)
case 82:

View file

@ -89,13 +89,15 @@ func (m *messageReaderImpl) Parse() (err error) {
if err != nil {
return fmt.Errorf("read message err: %s", err)
}
if m.msgType == MsgBatchMetadata {
if m.msgType == MsgBatchMeta || m.msgType == MsgBatchMetadata {
if len(m.list) > 0 {
return fmt.Errorf("batch meta not at the start of batch")
}
switch message := msg.(type) {
case *BatchMetadata:
m.version = int(message.Version)
case *BatchMeta:
m.version = 0
}
if m.version != 1 {
// Unsupported tracker version, reset reader

View file

@ -79,34 +79,31 @@ func (e *handlersImpl) GetAll() []*api.Description {
}
}
func getSessionTimestamp(req *StartSessionRequest, startTimeMili int64) uint64 {
func getSessionTimestamp(req *StartSessionRequest, startTimeMili int64) (ts uint64) {
ts = uint64(req.Timestamp)
if req.IsOffline {
return uint64(req.Timestamp)
return
}
ts := uint64(startTimeMili)
if req.BufferDiff > 0 && req.BufferDiff < 5*60*1000 {
ts -= req.BufferDiff
}
return ts
}
func validateTrackerVersion(ver string) error {
c, err := semver.NewConstraint(">=6.0.0")
c, err := semver.NewConstraint(">=4.1.6")
if err != nil {
return err
return
}
ver := req.TrackerVersion
parts := strings.Split(ver, "-")
if len(parts) > 1 {
ver = parts[0]
}
v, err := semver.NewVersion(ver)
if err != nil {
return err
return
}
if !c.Check(v) {
return errors.New("unsupported tracker version")
if c.Check(v) {
ts = uint64(startTimeMili)
if req.BufferDiff > 0 && req.BufferDiff < 5*60*1000 {
ts -= req.BufferDiff
}
}
return nil
return
}
func (e *handlersImpl) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) {

View file

@ -71,7 +71,7 @@ def get_details(project_id, error_id, user_id, **data):
MAIN_EVENTS_TABLE = exp_ch_helper.get_main_events_table(0)
ch_basic_query = errors_helper.__get_basic_constraints_ch(time_constraint=False)
ch_basic_query.append("toString(`$properties`.error_id) = %(error_id)s")
ch_basic_query.append("error_id = %(error_id)s")
with ch_client.ClickHouseClient() as ch:
data["startDate24"] = TimeUTC.now(-1)
@ -95,7 +95,7 @@ def get_details(project_id, error_id, user_id, **data):
"error_id": error_id}
main_ch_query = f"""\
WITH pre_processed AS (SELECT toString(`$properties`.error_id) AS error_id,
WITH pre_processed AS (SELECT error_id,
toString(`$properties`.name) AS name,
toString(`$properties`.message) AS message,
session_id,
@ -183,7 +183,7 @@ def get_details(project_id, error_id, user_id, **data):
AND `$event_name` = 'ERROR'
AND events.created_at >= toDateTime(timestamp / 1000)
AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000)
AND toString(`$properties`.error_id) = %(error_id)s
AND error_id = %(error_id)s
GROUP BY timestamp
ORDER BY timestamp) AS chart_details
) AS chart_details24 ON TRUE
@ -196,7 +196,7 @@ def get_details(project_id, error_id, user_id, **data):
AND `$event_name` = 'ERROR'
AND events.created_at >= toDateTime(timestamp / 1000)
AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000)
AND toString(`$properties`.error_id) = %(error_id)s
AND error_id = %(error_id)s
GROUP BY timestamp
ORDER BY timestamp) AS chart_details
) AS chart_details30 ON TRUE;"""

View file

@ -1,18 +1,18 @@
urllib3==2.3.0
urllib3==2.4.0
requests==2.32.3
boto3==1.37.21
boto3==1.38.10
pyjwt==2.10.1
psycopg2-binary==2.9.10
psycopg[pool,binary]==3.2.6
clickhouse-connect==0.8.15
elasticsearch==8.17.2
psycopg[pool,binary]==3.2.7
clickhouse-connect==0.8.17
elasticsearch==9.0.1
jira==3.8.0
cachetools==5.5.2
fastapi==0.115.12
uvicorn[standard]==0.34.0
uvicorn[standard]==0.34.2
python-decouple==3.8
pydantic[email]==2.10.6
pydantic[email]==2.11.4
apscheduler==3.11.0
azure-storage-blob==12.25.0
azure-storage-blob==12.25.1

View file

@ -3,16 +3,16 @@ requests==2.32.3
boto3==1.37.21
pyjwt==2.10.1
psycopg2-binary==2.9.10
psycopg[pool,binary]==3.2.6
clickhouse-connect==0.8.15
elasticsearch==8.17.2
psycopg[pool,binary]==3.2.7
clickhouse-connect==0.8.17
elasticsearch==9.0.1
jira==3.8.0
cachetools==5.5.2
fastapi==0.115.12
python-decouple==3.8
pydantic[email]==2.10.6
pydantic[email]==2.11.4
apscheduler==3.11.0
redis==5.2.1
azure-storage-blob==12.25.0
redis==6.0.0
azure-storage-blob==12.25.1

View file

@ -1,19 +1,19 @@
urllib3==2.3.0
urllib3==2.4.0
requests==2.32.3
boto3==1.37.21
boto3==1.38.10
pyjwt==2.10.1
psycopg2-binary==2.9.10
psycopg[pool,binary]==3.2.6
clickhouse-connect==0.8.15
elasticsearch==8.17.2
psycopg[pool,binary]==3.2.7
clickhouse-connect==0.8.17
elasticsearch==9.0.1
jira==3.8.0
cachetools==5.5.2
fastapi==0.115.12
uvicorn[standard]==0.34.0
uvicorn[standard]==0.34.2
gunicorn==23.0.0
python-decouple==3.8
pydantic[email]==2.10.6
pydantic[email]==2.11.4
apscheduler==3.11.0
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
@ -21,6 +21,6 @@ apscheduler==3.11.0
python3-saml==1.16.0 --no-binary=lxml
python-multipart==0.0.20
redis==5.2.1
redis==6.0.0
#confluent-kafka==2.1.0
azure-storage-blob==12.25.0
azure-storage-blob==12.25.1

View file

@ -35,6 +35,13 @@ class SessionStart(Message):
self.user_id = user_id
class SessionEndDeprecated(Message):
__id__ = 3
def __init__(self, timestamp):
self.timestamp = timestamp
class SetPageLocationDeprecated(Message):
__id__ = 4
@ -224,6 +231,15 @@ class PageRenderTiming(Message):
self.time_to_interactive = time_to_interactive
class JSExceptionDeprecated(Message):
__id__ = 25
def __init__(self, name, message, payload):
self.name = name
self.message = message
self.payload = payload
class IntegrationEvent(Message):
__id__ = 26
@ -323,21 +339,34 @@ class PageEvent(Message):
self.web_vitals = web_vitals
class StringDictGlobal(Message):
__id__ = 34
class CSSInsertRule(Message):
__id__ = 37
def __init__(self, key, value):
self.key = key
self.value = value
class SetNodeAttributeDictGlobal(Message):
__id__ = 35
def __init__(self, id, name, value):
def __init__(self, id, rule, index):
self.id = id
self.name = name
self.value = value
self.rule = rule
self.index = index
class CSSDeleteRule(Message):
__id__ = 38
def __init__(self, id, index):
self.id = id
self.index = index
class Fetch(Message):
__id__ = 39
def __init__(self, method, url, request, response, status, timestamp, duration):
self.method = method
self.url = url
self.request = request
self.response = response
self.status = status
self.timestamp = timestamp
self.duration = duration
class Profiler(Message):
@ -520,6 +549,19 @@ class SetNodeFocus(Message):
self.id = id
class LongTask(Message):
__id__ = 59
def __init__(self, timestamp, duration, context, container_type, container_src, container_id, container_name):
self.timestamp = timestamp
self.duration = duration
self.context = context
self.container_type = container_type
self.container_src = container_src
self.container_id = container_id
self.container_name = container_name
class SetNodeAttributeURLBased(Message):
__id__ = 60
@ -539,6 +581,18 @@ class SetCSSDataURLBased(Message):
self.base_url = base_url
class IssueEventDeprecated(Message):
__id__ = 62
def __init__(self, message_id, timestamp, type, context_string, context, payload):
self.message_id = message_id
self.timestamp = timestamp
self.type = type
self.context_string = context_string
self.context = context
self.payload = payload
class TechnicalInfo(Message):
__id__ = 63
@ -562,6 +616,16 @@ class AssetCache(Message):
self.url = url
class CSSInsertRuleURLBased(Message):
__id__ = 67
def __init__(self, id, rule, index, base_url):
self.id = id
self.rule = rule
self.index = index
self.base_url = base_url
class MouseClick(Message):
__id__ = 68
@ -670,6 +734,15 @@ class Zustand(Message):
self.state = state
class BatchMeta(Message):
__id__ = 80
def __init__(self, page_no, first_index, timestamp):
self.page_no = page_no
self.first_index = first_index
self.timestamp = timestamp
class BatchMetadata(Message):
__id__ = 81

View file

@ -58,6 +58,15 @@ cdef class SessionStart(PyMessage):
self.user_id = user_id
cdef class SessionEndDeprecated(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
def __init__(self, unsigned long timestamp):
self.__id__ = 3
self.timestamp = timestamp
cdef class SetPageLocationDeprecated(PyMessage):
cdef public int __id__
cdef public str url
@ -331,6 +340,19 @@ cdef class PageRenderTiming(PyMessage):
self.time_to_interactive = time_to_interactive
cdef class JSExceptionDeprecated(PyMessage):
cdef public int __id__
cdef public str name
cdef public str message
cdef public str payload
def __init__(self, str name, str message, str payload):
self.__id__ = 25
self.name = name
self.message = message
self.payload = payload
cdef class IntegrationEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
@ -489,28 +511,49 @@ cdef class PageEvent(PyMessage):
self.web_vitals = web_vitals
cdef class StringDictGlobal(PyMessage):
cdef public int __id__
cdef public unsigned long key
cdef public str value
def __init__(self, unsigned long key, str value):
self.__id__ = 34
self.key = key
self.value = value
cdef class SetNodeAttributeDictGlobal(PyMessage):
cdef class CSSInsertRule(PyMessage):
cdef public int __id__
cdef public unsigned long id
cdef public unsigned long name
cdef public unsigned long value
cdef public str rule
cdef public unsigned long index
def __init__(self, unsigned long id, unsigned long name, unsigned long value):
self.__id__ = 35
def __init__(self, unsigned long id, str rule, unsigned long index):
self.__id__ = 37
self.id = id
self.name = name
self.value = value
self.rule = rule
self.index = index
cdef class CSSDeleteRule(PyMessage):
cdef public int __id__
cdef public unsigned long id
cdef public unsigned long index
def __init__(self, unsigned long id, unsigned long index):
self.__id__ = 38
self.id = id
self.index = index
cdef class Fetch(PyMessage):
cdef public int __id__
cdef public str method
cdef public str url
cdef public str request
cdef public str response
cdef public unsigned long status
cdef public unsigned long timestamp
cdef public unsigned long duration
def __init__(self, str method, str url, str request, str response, unsigned long status, unsigned long timestamp, unsigned long duration):
self.__id__ = 39
self.method = method
self.url = url
self.request = request
self.response = response
self.status = status
self.timestamp = timestamp
self.duration = duration
cdef class Profiler(PyMessage):
@ -778,6 +821,27 @@ cdef class SetNodeFocus(PyMessage):
self.id = id
cdef class LongTask(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long duration
cdef public unsigned long context
cdef public unsigned long container_type
cdef public str container_src
cdef public str container_id
cdef public str container_name
def __init__(self, unsigned long timestamp, unsigned long duration, unsigned long context, unsigned long container_type, str container_src, str container_id, str container_name):
self.__id__ = 59
self.timestamp = timestamp
self.duration = duration
self.context = context
self.container_type = container_type
self.container_src = container_src
self.container_id = container_id
self.container_name = container_name
cdef class SetNodeAttributeURLBased(PyMessage):
cdef public int __id__
cdef public unsigned long id
@ -806,6 +870,25 @@ cdef class SetCSSDataURLBased(PyMessage):
self.base_url = base_url
cdef class IssueEventDeprecated(PyMessage):
cdef public int __id__
cdef public unsigned long message_id
cdef public unsigned long timestamp
cdef public str type
cdef public str context_string
cdef public str context
cdef public str payload
def __init__(self, unsigned long message_id, unsigned long timestamp, str type, str context_string, str context, str payload):
self.__id__ = 62
self.message_id = message_id
self.timestamp = timestamp
self.type = type
self.context_string = context_string
self.context = context
self.payload = payload
cdef class TechnicalInfo(PyMessage):
cdef public int __id__
cdef public str type
@ -837,6 +920,21 @@ cdef class AssetCache(PyMessage):
self.url = url
cdef class CSSInsertRuleURLBased(PyMessage):
cdef public int __id__
cdef public unsigned long id
cdef public str rule
cdef public unsigned long index
cdef public str base_url
def __init__(self, unsigned long id, str rule, unsigned long index, str base_url):
self.__id__ = 67
self.id = id
self.rule = rule
self.index = index
self.base_url = base_url
cdef class MouseClick(PyMessage):
cdef public int __id__
cdef public unsigned long id
@ -993,6 +1091,19 @@ cdef class Zustand(PyMessage):
self.state = state
cdef class BatchMeta(PyMessage):
cdef public int __id__
cdef public unsigned long page_no
cdef public unsigned long first_index
cdef public long timestamp
def __init__(self, unsigned long page_no, unsigned long first_index, long timestamp):
self.__id__ = 80
self.page_no = page_no
self.first_index = first_index
self.timestamp = timestamp
cdef class BatchMetadata(PyMessage):
cdef public int __id__
cdef public unsigned long version

View file

@ -118,6 +118,11 @@ class MessageCodec(Codec):
user_id=self.read_string(reader)
)
if message_id == 3:
return SessionEndDeprecated(
timestamp=self.read_uint(reader)
)
if message_id == 4:
return SetPageLocationDeprecated(
url=self.read_string(reader),
@ -265,6 +270,13 @@ class MessageCodec(Codec):
time_to_interactive=self.read_uint(reader)
)
if message_id == 25:
return JSExceptionDeprecated(
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 26:
return IntegrationEvent(
timestamp=self.read_uint(reader),
@ -348,17 +360,28 @@ class MessageCodec(Codec):
web_vitals=self.read_string(reader)
)
if message_id == 34:
return StringDictGlobal(
key=self.read_uint(reader),
value=self.read_string(reader)
if message_id == 37:
return CSSInsertRule(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader)
)
if message_id == 35:
return SetNodeAttributeDictGlobal(
if message_id == 38:
return CSSDeleteRule(
id=self.read_uint(reader),
name=self.read_uint(reader),
value=self.read_uint(reader)
index=self.read_uint(reader)
)
if message_id == 39:
return Fetch(
method=self.read_string(reader),
url=self.read_string(reader),
request=self.read_string(reader),
response=self.read_string(reader),
status=self.read_uint(reader),
timestamp=self.read_uint(reader),
duration=self.read_uint(reader)
)
if message_id == 40:
@ -503,6 +526,17 @@ class MessageCodec(Codec):
id=self.read_int(reader)
)
if message_id == 59:
return LongTask(
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
context=self.read_uint(reader),
container_type=self.read_uint(reader),
container_src=self.read_string(reader),
container_id=self.read_string(reader),
container_name=self.read_string(reader)
)
if message_id == 60:
return SetNodeAttributeURLBased(
id=self.read_uint(reader),
@ -518,6 +552,16 @@ class MessageCodec(Codec):
base_url=self.read_string(reader)
)
if message_id == 62:
return IssueEventDeprecated(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),
context=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 63:
return TechnicalInfo(
type=self.read_string(reader),
@ -535,6 +579,14 @@ class MessageCodec(Codec):
url=self.read_string(reader)
)
if message_id == 67:
return CSSInsertRuleURLBased(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader),
base_url=self.read_string(reader)
)
if message_id == 68:
return MouseClick(
id=self.read_uint(reader),
@ -619,6 +671,13 @@ class MessageCodec(Codec):
state=self.read_string(reader)
)
if message_id == 80:
return BatchMeta(
page_no=self.read_uint(reader),
first_index=self.read_uint(reader),
timestamp=self.read_int(reader)
)
if message_id == 81:
return BatchMetadata(
version=self.read_uint(reader),

View file

@ -216,6 +216,11 @@ cdef class MessageCodec:
user_id=self.read_string(reader)
)
if message_id == 3:
return SessionEndDeprecated(
timestamp=self.read_uint(reader)
)
if message_id == 4:
return SetPageLocationDeprecated(
url=self.read_string(reader),
@ -363,6 +368,13 @@ cdef class MessageCodec:
time_to_interactive=self.read_uint(reader)
)
if message_id == 25:
return JSExceptionDeprecated(
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 26:
return IntegrationEvent(
timestamp=self.read_uint(reader),
@ -446,17 +458,28 @@ cdef class MessageCodec:
web_vitals=self.read_string(reader)
)
if message_id == 34:
return StringDictGlobal(
key=self.read_uint(reader),
value=self.read_string(reader)
if message_id == 37:
return CSSInsertRule(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader)
)
if message_id == 35:
return SetNodeAttributeDictGlobal(
if message_id == 38:
return CSSDeleteRule(
id=self.read_uint(reader),
name=self.read_uint(reader),
value=self.read_uint(reader)
index=self.read_uint(reader)
)
if message_id == 39:
return Fetch(
method=self.read_string(reader),
url=self.read_string(reader),
request=self.read_string(reader),
response=self.read_string(reader),
status=self.read_uint(reader),
timestamp=self.read_uint(reader),
duration=self.read_uint(reader)
)
if message_id == 40:
@ -601,6 +624,17 @@ cdef class MessageCodec:
id=self.read_int(reader)
)
if message_id == 59:
return LongTask(
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
context=self.read_uint(reader),
container_type=self.read_uint(reader),
container_src=self.read_string(reader),
container_id=self.read_string(reader),
container_name=self.read_string(reader)
)
if message_id == 60:
return SetNodeAttributeURLBased(
id=self.read_uint(reader),
@ -616,6 +650,16 @@ cdef class MessageCodec:
base_url=self.read_string(reader)
)
if message_id == 62:
return IssueEventDeprecated(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),
context=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 63:
return TechnicalInfo(
type=self.read_string(reader),
@ -633,6 +677,14 @@ cdef class MessageCodec:
url=self.read_string(reader)
)
if message_id == 67:
return CSSInsertRuleURLBased(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader),
base_url=self.read_string(reader)
)
if message_id == 68:
return MouseClick(
id=self.read_uint(reader),
@ -717,6 +769,13 @@ cdef class MessageCodec:
state=self.read_string(reader)
)
if message_id == 80:
return BatchMeta(
page_no=self.read_uint(reader),
first_index=self.read_uint(reader),
timestamp=self.read_int(reader)
)
if message_id == 81:
return BatchMetadata(
version=self.read_uint(reader),

View file

@ -1,65 +1,5 @@
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.23.0-ee';
SET allow_experimental_json_type = 1;
SET enable_json_type = 1;
ALTER TABLE product_analytics.events
MODIFY COLUMN `$properties` JSON(
max_dynamic_paths=0,
label String ,
hesitation_time UInt32 ,
name String ,
payload String ,
level Enum8 ('info'=0, 'error'=1),
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
message String ,
error_id String ,
duration UInt16,
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
url_host String ,
url_path String ,
url_hostpath String ,
request_start UInt16 ,
response_start UInt16 ,
response_end UInt16 ,
dom_content_loaded_event_start UInt16 ,
dom_content_loaded_event_end UInt16 ,
load_event_start UInt16 ,
load_event_end UInt16 ,
first_paint UInt16 ,
first_contentful_paint_time UInt16 ,
speed_index UInt16 ,
visually_complete UInt16 ,
time_to_interactive UInt16,
ttfb UInt16,
ttlb UInt16,
response_time UInt16,
dom_building_time UInt16,
dom_content_loaded_event_time UInt16,
load_event_time UInt16,
min_fps UInt8,
avg_fps UInt8,
max_fps UInt8,
min_cpu UInt8,
avg_cpu UInt8,
max_cpu UInt8,
min_total_js_heap_size UInt64,
avg_total_js_heap_size UInt64,
max_total_js_heap_size UInt64,
min_used_js_heap_size UInt64,
avg_used_js_heap_size UInt64,
max_used_js_heap_size UInt64,
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
status UInt16,
success UInt8,
request_body String,
response_body String,
transfer_size UInt32,
selector String,
normalized_x Float32,
normalized_y Float32,
message_id UInt64
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events';
DROP TABLE IF EXISTS product_analytics.all_events;
CREATE TABLE IF NOT EXISTS product_analytics.all_events
(
@ -225,3 +165,92 @@ FROM product_analytics.events
WHERE randCanonical() < 0.5 -- This randomly skips inserts
AND value != ''
LIMIT 2 BY project_id,property_name;
-- Autocomplete
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
(
project_id UInt16,
value String COMMENT 'The $event_name',
_timestamp DateTime
) ENGINE = MergeTree()
ORDER BY (project_id, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
TO product_analytics.autocomplete_events AS
SELECT project_id,
`$event_name` AS value,
_timestamp
FROM product_analytics.events
WHERE _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
(
project_id UInt16,
value String COMMENT 'The $event_name',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
SELECT project_id,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_events
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, value;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
_timestamp DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY (project_id, event_name, property_name, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
TO product_analytics.autocomplete_event_properties AS
SELECT project_id,
`$event_name` AS event_name,
property_name,
JSONExtractString(toString(`$properties`), property_name) AS value,
_timestamp
FROM product_analytics.events
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
AND _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, event_name, property_name, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
SELECT project_id,
event_name,
property_name,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_event_properties
WHERE length(value) > 0
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, event_name, property_name, value;

View file

@ -431,62 +431,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
"$source" LowCardinality(String) DEFAULT '' COMMENT 'the name of the integration that sent the event',
"$duration_s" UInt16 DEFAULT 0 COMMENT 'the duration from session-start in seconds',
properties JSON DEFAULT '{}',
"$properties" JSON(
max_dynamic_paths=0,
label String ,
hesitation_time UInt32 ,
name String ,
payload String ,
level Enum8 ('info'=0, 'error'=1),
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
message String ,
error_id String ,
duration UInt16,
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
url_host String ,
url_path String ,
url_hostpath String ,
request_start UInt16 ,
response_start UInt16 ,
response_end UInt16 ,
dom_content_loaded_event_start UInt16 ,
dom_content_loaded_event_end UInt16 ,
load_event_start UInt16 ,
load_event_end UInt16 ,
first_paint UInt16 ,
first_contentful_paint_time UInt16 ,
speed_index UInt16 ,
visually_complete UInt16 ,
time_to_interactive UInt16,
ttfb UInt16,
ttlb UInt16,
response_time UInt16,
dom_building_time UInt16,
dom_content_loaded_event_time UInt16,
load_event_time UInt16,
min_fps UInt8,
avg_fps UInt8,
max_fps UInt8,
min_cpu UInt8,
avg_cpu UInt8,
max_cpu UInt8,
min_total_js_heap_size UInt64,
avg_total_js_heap_size UInt64,
max_total_js_heap_size UInt64,
min_used_js_heap_size UInt64,
avg_used_js_heap_size UInt64,
max_used_js_heap_size UInt64,
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
status UInt16,
success UInt8,
request_body String,
response_body String,
transfer_size UInt32,
selector String,
normalized_x Float32,
normalized_y Float32,
message_id UInt64
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
"$properties" JSON DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
description String DEFAULT '',
group_id1 Array(String) DEFAULT [],
group_id2 Array(String) DEFAULT [],
@ -868,3 +813,92 @@ FROM product_analytics.events
WHERE randCanonical() < 0.5 -- This randomly skips inserts
AND value != ''
LIMIT 2 BY project_id,property_name;
-- Autocomplete
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
(
project_id UInt16,
value String COMMENT 'The $event_name',
_timestamp DateTime
) ENGINE = MergeTree()
ORDER BY (project_id, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
TO product_analytics.autocomplete_events AS
SELECT project_id,
`$event_name` AS value,
_timestamp
FROM product_analytics.events
WHERE _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
(
project_id UInt16,
value String COMMENT 'The $event_name',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
SELECT project_id,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_events
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, value;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
_timestamp DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY (project_id, event_name, property_name, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
TO product_analytics.autocomplete_event_properties AS
SELECT project_id,
`$event_name` AS event_name,
property_name,
JSONExtractString(toString(`$properties`), property_name) AS value,
_timestamp
FROM product_analytics.events
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
AND _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, event_name, property_name, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
SELECT project_id,
event_name,
property_name,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_event_properties
WHERE length(value) > 0
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, event_name, property_name, value;

View file

@ -0,0 +1,64 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
type ThemeType = 'light' | 'dark';
interface ThemeContextType {
theme: ThemeType;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const getInitialTheme = (): ThemeType => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
return savedTheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const [theme, setTheme] = useState<ThemeType>(getInitialTheme);
useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
// Only apply system preference if user hasn't manually set a preference
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

View file

@ -111,9 +111,13 @@ const EChartsSankey: React.FC<Props> = (props) => {
if (echartNodes.length === 0) return;
const mainNodeLink = startPoint === 'end' ? echartNodes.findIndex(n => n.id === 0) : 0;
const startDepth = startPoint === 'end' ? Math.max(...echartNodes.map(n => n.depth ?? 0)) : 0;
const mainNodeLinks = echartNodes.filter(n => n.depth === startDepth).map(n => echartNodes.findIndex(node => node.id === n.id))
const startNodeValue = echartLinks
.filter((link) => startPoint === 'start' ? link.source === mainNodeLink : link.target === mainNodeLink)
.filter((link) => startPoint === 'start'
? mainNodeLinks.includes(link.source)
: mainNodeLinks.includes(link.target)
)
.reduce((sum, link) => sum + link.value, 0);
Object.keys(nodeValues).forEach((nodeId) => {

View file

@ -114,7 +114,7 @@ const ProjectCodeSnippet: React.FC<Props> = (props) => {
<div
className={cn(
stl.info,
'rounded-lg bg-gray mb-4 ml-8 bg-amber-50 w-fit text-sm mt-2',
'rounded-lg bg-gray mb-4 ml-8 bg-amber w-fit text-sm mt-2',
{ hidden: !changed },
)}
>

View file

@ -87,7 +87,7 @@ function UserForm() {
/>
</div>
{!isSmtp && (
<div className={cn('mb-4 p-2 bg-yellow rounded')}>
<div className={cn('mb-4 p-2 bg-amber rounded')}>
{t('SMTP is not configured (see')}&nbsp;
<a
className="link"

View file

@ -44,16 +44,6 @@ function AddCardSelectionModal(props: Props) {
className="addCard"
width={isSaas ? 900 : undefined}
>
{isSaas ? (
<>
<Row gutter={16} justify="center" className="py-2">
<AiQuery />
</Row>
<div className="flex items-center justify-center w-full text-disabled-text">
{t('or')}
</div>
</>
) : null}
<Row gutter={16} justify="center" className="py-5">
<Col span={12}>
<div

View file

@ -43,7 +43,7 @@ function ClickMapRagePicker() {
<Checkbox onChange={onToggle} label={t('Include rage clicks')} />
<Button size="small" onClick={refreshHeatmapSession}>
{t('Get new session')}
{t('Get new image')}
</Button>
</div>
);

View file

@ -181,9 +181,10 @@ function WidgetChart(props: Props) {
}
prevMetricRef.current = _metric;
const timestmaps = drillDownPeriod.toTimestamps();
const density = props.isPreview ? metric.density : dashboardStore.selectedDensity
const payload = isSaved
? { ...metricParams }
: { ...params, ...timestmaps, ..._metric.toJson() };
? { ...metricParams, density }
: { ...params, ...timestmaps, ..._metric.toJson(), density };
debounceRequest(
_metric,
payload,

View file

@ -55,7 +55,7 @@ function RangeGranularity({
}
const PAST_24_HR_MS = 24 * 60 * 60 * 1000;
function calculateGranularities(periodDurationMs: number) {
export function calculateGranularities(periodDurationMs: number) {
const granularities = [
{ label: 'Hourly', durationMs: 60 * 60 * 1000 },
{ label: 'Daily', durationMs: 24 * 60 * 60 * 1000 },

View file

@ -116,13 +116,11 @@ function PlayerBlockHeader(props: any) {
)}
{_metaList.length > 0 && (
<div className="h-full flex items-center px-2 gap-1">
<SessionMetaList
className=""
metaList={_metaList}
maxLength={2}
/>
</div>
<SessionMetaList
horizontal
metaList={_metaList}
maxLength={2}
/>
)}
</div>
{uiPlayerStore.showSearchEventsSwitchButton ? (

View file

@ -2,7 +2,7 @@
height: 50px;
border-bottom: solid thin $gray-lighter;
padding-right: 0;
background-color: white;
background-color: $white;
}
.divider {

View file

@ -233,7 +233,7 @@ function EventsBlock(props: IProps) {
<div
className={cn(
styles.header,
'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-50 h-[57px]',
'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-gray-lightest h-[57px]',
)}
>
{uxtestingStore.isUxt() ? (

View file

@ -13,7 +13,7 @@
.event {
position: relative;
background: #f6f6f6;
background: $gray-lightest;
/* border-radius: 3px; */
user-select: none;
transition: all 0.2s;
@ -147,5 +147,5 @@
}
.lastInGroup {
background: white;
background: $white;
}

View file

@ -22,7 +22,7 @@ function TimelineScale(props: Props) {
el.style.opacity = '0.8';
el.innerHTML = `${txt}`;
el.style.fontSize = '12px';
el.style.color = 'white';
el.classList.add('text-white')
container.appendChild(el);
}

View file

@ -7,7 +7,7 @@ function Key({ label }: { label: string }) {
return (
<div
style={{ minWidth: 52 }}
className="whitespace-nowrap font-normal bg-indigo-50 rounded-lg px-2 py-1 text-figmaColors-text-primary text-center font-mono"
className="whitespace-nowrap font-normal bg-active-blue rounded-lg px-2 py-1 text-black text-center font-mono"
>
{label}
</div>

View file

@ -66,7 +66,7 @@ function ReadNote(props: Props) {
className="flex items-center justify-center"
>
<div
className="flex items-start !text-lg flex-col p-4 border gap-2 rounded-lg bg-amber-50"
className="flex items-start !text-lg flex-col p-4 border gap-2 rounded-lg bg-amber"
style={{ width: 500 }}
>
<div className="flex items-center w-full">

View file

@ -13,7 +13,7 @@
}
.checkers {
background: repeating-linear-gradient(135deg, #f3f3f3, #f3f3f3 1px, #f6f6f6 1px, #FFF 4px);
background: repeating-linear-gradient(135deg, var(--color-gray-lighter), var(--color-gray-lighter) 1px, var(--color-gray-lightest) 1px, var(--color-white) 4px);
}
.solidBg {
background: $gray-lightest;

View file

@ -3,7 +3,7 @@
border-bottom: solid thin $gray-light;
padding-left: 15px;
padding-right: 0;
background-color: white;
background-color: $white;
}
.divider {

View file

@ -57,7 +57,7 @@ function SpotPlayerControls() {
<div className="w-full p-4 flex items-center gap-4 bg-white">
<PlayButton togglePlay={togglePlay} state={playState} iconSize={36} />
<div className="px-2 py-1 bg-white rounded font-semibold text-black flex items-center gap-2">
<div className="px-2 py-1 bg-white rounded font-semibold flex items-center gap-2">
<PlayTime isCustom time={spotPlayerStore.time * 1000} format="mm:ss" />
<span>/</span>
<div>{spotPlayerStore.durationString}</div>

View file

@ -32,6 +32,7 @@ import { Avatar, Icon } from 'UI';
import { TABS, Tab } from '../consts';
import AccessModal from './AccessModal';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify'
const spotLink = spotsList();
@ -89,8 +90,12 @@ function SpotPlayerHeader({
const onMenuClick = async ({ key }: { key: string }) => {
if (key === '1') {
const loader = toast.loading('Retrieving Spot video...')
const { url } = await spotStore.getVideo(spotStore.currentSpot!.spotId);
await downloadFile(url, `${spotStore.currentSpot!.title}.mp4`);
setTimeout(() => {
toast.dismiss(loader)
}, 0)
} else if (key === '2') {
spotStore.deleteSpot([spotStore.currentSpot!.spotId]).then(() => {
history.push(spotsList());
@ -245,12 +250,11 @@ function SpotPlayerHeader({
}
async function downloadFile(url: string, fileName: string) {
const { t } = useTranslation();
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(t('Network response was not ok'));
throw new Error('Network response was not ok');
}
const blob = await response.blob();
@ -263,6 +267,7 @@ async function downloadFile(url: string, fileName: string) {
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch (error) {
toast.error('Error downloading file.')
console.error('Error downloading file:', error);
}
}

View file

@ -80,8 +80,12 @@ function SpotListItem({
case 'rename':
return setIsEdit(true);
case 'download':
const loader = toast.loading('Retrieving Spot video...')
const { url } = await onVideo(spot.spotId);
await downloadFile(url, `${spot.title}.mp4`);
setTimeout(() => {
toast.dismiss(loader)
}, 0)
return;
case 'copy':
copy(

View file

@ -0,0 +1,31 @@
import React from 'react';
import { Button } from 'antd';
import { BulbOutlined, BulbFilled } from '@ant-design/icons';
import { useTheme } from 'App/ThemeContext';
interface ThemeToggleProps {
className?: string;
style?: React.CSSProperties;
size?: 'large' | 'middle' | 'small';
}
const ThemeToggle: React.FC<ThemeToggleProps> = ({
className = '',
style = {},
size = 'middle'
}) => {
const { theme, toggleTheme } = useTheme();
return (
<Button
type="text"
icon={theme === 'dark' ? <BulbFilled /> : <BulbOutlined />}
onClick={toggleTheme}
className={className}
style={style}
size={size}
/>
);
};
export default ThemeToggle;

View file

@ -125,7 +125,7 @@ export function AutocompleteModal({
if (index === blocksAmount - 1 && blocksAmount > 1) {
str += ' and ';
}
str += `"${block.trim()}"`;
str += block.trim();
if (index < blocksAmount - 2) {
str += ', ';
}
@ -188,10 +188,10 @@ export function AutocompleteModal({
{query.length ? (
<div className="border-y border-y-gray-light py-2">
<div
className="whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
className="whitespace-nowrap truncate w-full rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
onClick={applyQuery}
>
{t('Apply')}&nbsp;{queryStr}
{t('Apply')}&nbsp;<span className='font-semibold'>{queryStr}</span>
</div>
</div>
) : null}

View file

@ -128,8 +128,10 @@ const FilterAutoComplete = observer(
};
const handleFocus = () => {
if (!initialFocus) {
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
}
setInitialFocus(true);
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
};
return (

View file

@ -1,68 +1,6 @@
import React from 'react';
import Select from 'Shared/Select';
const dropdownStyles = {
control: (provided: any) => {
const obj = {
...provided,
border: 'solid thin #ddd',
boxShadow: 'none !important',
cursor: 'pointer',
height: '26px',
minHeight: '26px',
backgroundColor: 'white',
borderRadius: '.5rem',
'&:hover': {
borderColor: 'rgb(115 115 115 / 0.9)',
},
};
return obj;
},
valueContainer: (provided: any) => ({
...provided,
width: 'fit-content',
height: 26,
'& input': {
marginTop: '-3px',
},
}),
placeholder: (provided: any) => ({
...provided,
}),
indicatorsContainer: (provided: any) => ({
display: 'none',
}),
// option: (provided: any, state: any) => ({
// ...provided,
// whiteSpace: 'nowrap',
// }),
menu: (provided: any, state: any) => ({
...provided,
marginTop: '0.5rem',
left: 0,
minWidth: 'fit-content',
overflow: 'hidden',
zIndex: 100,
border: 'none',
boxShadow: '0px 4px 10px rgba(0,0,0, 0.15)',
}),
container: (provided: any) => ({
...provided,
minWidth: 'max-content',
}),
singleValue: (provided: any, state: { isDisabled: any }) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return {
...provided,
opacity,
transition,
marginTop: '-3px',
};
},
};
interface Props {
onChange: (e: any, { name, value }: any) => void;
className?: string;
@ -84,7 +22,8 @@ function FilterOperator(props: Props) {
<Select
name="operator"
options={options || []}
styles={dropdownStyles}
styles={{ height: 26 }}
popupMatchSelectWidth={false}
placeholder="Select"
isDisabled={isDisabled}
value={value ? options?.find((i: any) => i.value === value) : null}

View file

@ -33,7 +33,7 @@ function SearchActions() {
// @ts-ignore
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
const showAiField = isSaas && activeTab.type === 'sessions';
const showAiField = isSaas && activeTab?.type === 'sessions';
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<div className="mb-2">

View file

@ -1,9 +1,5 @@
import React from 'react';
import Select, { components, DropdownIndicatorProps } from 'react-select';
import { Icon } from 'UI';
import colors from 'App/theme/colors';
const { ValueContainer } = components;
import { Select } from 'antd';
type ValueObject = {
value: string | number;
@ -12,190 +8,44 @@ type ValueObject = {
interface Props<Value extends ValueObject> {
options: Value[];
isSearchable?: boolean;
defaultValue?: string | number;
plain?: boolean;
components?: any;
styles?: Record<string, any>;
controlStyle?: Record<string, any>;
onChange: (newValue: { name: string; value: Value }) => void;
showSearch?: boolean;
defaultValue?: string | number | (string | number)[];
onChange: (value: any, option: any) => void;
name?: string;
placeholder?: string;
className?: string;
mode?: 'multiple' | 'tags';
[x: string]: any;
}
export default function <Value extends ValueObject>({
export default function CustomSelect<Value extends ValueObject>({
placeholder = 'Select',
name = '',
onChange,
right = false,
plain = false,
options,
isSearchable = false,
components = {},
styles = {},
showSearch = false,
defaultValue = '',
controlStyle = {},
className = '',
mode,
styles,
...rest
}: Props<Value>) {
const defaultSelected = Array.isArray(defaultValue)
? defaultValue.map((value) =>
options.find((option) => option.value === value),
)
: options.find((option) => option.value === defaultValue) || null;
const customStyles = {
option: (provided: any, state: any) => ({
...provided,
whiteSpace: 'nowrap',
transition: 'all 0.3s',
backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent',
color: state.isFocused ? colors.teal : 'black',
fontSize: '14px',
'&:hover': {
transition: 'all 0.2s',
backgroundColor: colors['active-blue'],
},
'&:focus': {
transition: 'all 0.2s',
backgroundColor: colors['active-blue'],
},
}),
menu: (provided: any, state: any) => ({
...provided,
top: 31,
borderRadius: '.5rem',
right: right ? 0 : undefined,
border: `1px solid ${colors['gray-light']}`,
// borderRadius: '3px',
backgroundColor: '#fff',
boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)',
position: 'absolute',
minWidth: 'fit-content',
// zIndex: 99,
overflow: 'hidden',
zIndex: 100,
...(right && { right: 0 }),
}),
menuList: (provided: any, state: any) => ({
...provided,
padding: 0,
}),
control: (provided: any) => {
const obj = {
...provided,
border: 'solid thin #ddd',
cursor: 'pointer',
minHeight: '36px',
transition: 'all 0.5s',
'&:hover': {
backgroundColor: colors['gray-lightest'],
transition: 'all 0.2s ease-in-out',
},
...controlStyle,
};
if (plain) {
obj.backgroundColor = 'transparent';
obj.border = '1px solid transparent';
obj.backgroundColor = 'transparent';
obj['&:hover'] = {
borderColor: 'transparent',
backgroundColor: colors['gray-light'],
transition: 'all 0.2s ease-in-out',
};
obj['&:focus'] = {
borderColor: 'transparent',
};
obj['&:active'] = {
borderColor: 'transparent',
};
}
return obj;
},
indicatorsContainer: (provided: any) => ({
...provided,
maxHeight: '34px',
padding: 0,
}),
valueContainer: (provided: any) => ({
...provided,
paddingRight: '0px',
}),
singleValue: (provided: any, state: { isDisabled: any }) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return {
...provided,
opacity,
transition,
fontWeight: '500',
};
},
input: (provided: any) => ({
...provided,
'& input:focus': {
border: 'none !important',
},
}),
noOptionsMessage: (provided: any) => ({
...provided,
whiteSpace: 'nowrap !important',
// minWidth: 'fit-content',
}),
// Handle onChange to maintain compatibility with the original component
const handleChange = (value: any, option: any) => {
onChange({ name, value: option });
};
return (
<Select
className={`${className} btn-event-condition`}
className={className}
options={options}
isSearchable={isSearchable}
defaultValue={defaultSelected}
components={{
IndicatorSeparator: () => null,
DropdownIndicator,
ValueContainer: CustomValueContainer,
...components,
}}
onChange={(value) => onChange({ name, value })}
styles={{ ...customStyles, ...styles }}
theme={(theme) => ({
...theme,
colors: {
...theme.colors,
primary: '#394EFF',
},
})}
blurInputOnSelect
showSearch={showSearch}
defaultValue={defaultValue}
onChange={handleChange}
placeholder={placeholder}
mode={mode}
style={styles}
{...rest}
/>
);
}
function DropdownIndicator(props: DropdownIndicatorProps<true>) {
return (
<components.DropdownIndicator {...props}>
<Icon name="chevron-down" size="16" />
</components.DropdownIndicator>
);
}
function CustomValueContainer({ children, ...rest }: any) {
const selectedCount = rest.getValue().length;
const conditional = selectedCount < 3;
let firstChild: any = [];
if (!conditional) {
firstChild = [children[0].shift(), children[1]];
}
return (
<ValueContainer {...rest}>
{conditional ? children : firstChild}
{!conditional && ` and ${selectedCount - 1} others`}
</ValueContainer>
);
}

View file

@ -12,18 +12,20 @@ export default function MetaItem(props: Props) {
return (
<div
className={cn(
'text-sm flex flex-row items-center px-2 py-0 gap-1 rounded-lg bg-white border border-neutral-100 overflow-hidden',
'text-sm flex flex-row items-center px-2 py-0 gap-1 rounded-lg bg-white border border-gray-light overflow-hidden',
className,
)}
>
<TextEllipsis
text={label}
className="p-0"
maxWidth={'300px'}
popupProps={{ size: 'small', disabled: true }}
/>
<span className="bg-neutral-200 inline-block w-[1px] min-h-[17px]"></span>
<span className="bg-gray-light inline-block w-[1px] min-h-[17px]"></span>
<TextEllipsis
text={value}
maxWidth={'350px'}
className="p-0 text-neutral-500"
popupProps={{ size: 'small', disabled: true }}
/>

View file

@ -8,13 +8,14 @@ interface Props {
metaList: any[];
maxLength?: number;
onMetaClick?: (meta: { name: string, value: string }) => void;
horizontal?: boolean;
}
export default function SessionMetaList(props: Props) {
const { className = '', metaList, maxLength = 14 } = props;
const { className = '', metaList, maxLength = 14, horizontal = false } = props;
return (
<div className={cn('flex items-center flex-wrap gap-1', className)}>
<div className={cn('flex items-center gap-1', horizontal ? '' : 'flex-wrap', className)}>
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
<div key={index} className='cursor-pointer' onClick={() => props.onMetaClick?.({ name: `_${label}`, value })}>
<MetaItem label={label} value={`${value}`} />

View file

@ -1,8 +1,6 @@
.sessionItem {
background-color: #fff;
background-color: $white;
user-select: none;
/* border-radius: 3px; */
/* border: solid thin #EEEEEE; */
transition: all 0.4s;
& .favorite {

View file

@ -47,6 +47,7 @@ function ListingVisibility() {
onChange={({ value }) => {
changeSettings({ operator: value.value });
}}
className='w-full'
/>
</div>
<div className="col-span-2">
@ -55,6 +56,7 @@ function ListingVisibility() {
type="number"
name="count"
min={0}
height={32}
placeholder="E.g 10"
onChange={({ target: { value } }: any) => {
changeSettings({ count: value > 0 ? value : '' });
@ -63,6 +65,7 @@ function ListingVisibility() {
</div>
<div className="col-span-3">
<Select
className='w-full'
defaultValue={durationSettings.countType || periodOptions[0].value}
options={periodOptions}
onChange={({ value }) => {

View file

@ -16,8 +16,7 @@ function LatestSessionsMessage() {
return count > 0 ? (
<div
className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link"
style={{ backgroundColor: 'rgb(255 251 235)' }}
className="bg-amber p-1 flex w-full border-b text-center justify-center link"
onClick={onShowNewSessions}
>
{t('Show')} {numberWithCommas(count)} {t('New')}{' '}

View file

@ -52,7 +52,7 @@ function NoteItem(props: Props) {
? `${props.note.message.slice(0, 150)}...`
: props.note.message;
return (
<div className="flex items-center px-2 border-b hover:bg-amber-50 justify-between py-2">
<div className="flex items-center px-2 border-b hover:bg-amber justify-between py-2">
<Link
style={{ width: '90%' }}
to={

View file

@ -15,10 +15,8 @@ export default class SlideModal extends React.PureComponent {
if (prevProps.isDisplayed !== this.props.isDisplayed) {
if (this.props.isDisplayed) {
document.addEventListener('keydown', this.keyPressHandler);
// document.body.classList.add('no-scroll');
} else {
document.removeEventListener('keydown', this.keyPressHandler);
// document.body.classList.remove('no-scroll');
}
}
}

View file

@ -1,75 +0,0 @@
import './styles/index.scss';
import React from 'react';
import { createRoot } from 'react-dom/client';
import './init';
import { Provider } from 'react-redux';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { ConfigProvider, theme } from 'antd';
import colors from 'App/theme/colors';
import { StoreProvider, RootStore } from './mstore';
import Router from './Router';
import store from './store';
// @ts-ignore
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
const customTheme = {
// algorithm: theme.compactAlgorithm,
components: {
Layout: {
colorBgBody: colors['gray-lightest'],
colorBgHeader: colors['gray-lightest'],
},
Menu: {
// algorithm: true,
// itemColor: colors['red'],
// "itemActiveBg": "rgb(242, 21, 158)",
// itemBgHover: colors['red'],
// colorText: colors['red'],
// colorIcon: colors['red'],
// colorBg: colors['gray-lightest'],
// colorItemText: '#394EFF',
// colorItemTextSelected: colors['teal'],
// colorItemBg: colors['gray-lightest']
},
Button: {
colorPrimary: colors.teal,
algorithm: true, // Enable algorithm
},
},
token: {
colorPrimary: colors.teal,
colorPrimaryActive: '#394EFF',
colorSecondary: '#3EAAAF',
colorBgLayout: colors['gray-lightest'],
colorBgContainer: colors.white,
colorLink: colors.teal,
colorLinkHover: colors['teal-dark'],
borderRadius: 4,
fontSize: 14,
fontFamily: '\'Roboto\', \'ArialMT\', \'Arial\'',
siderBackgroundColor: colors['gray-lightest'],
siderCollapsedWidth: 800,
},
};
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('app');
const root = createRoot(container);
// const theme = window.localStorage.getItem('theme');
root.render(
<ConfigProvider theme={customTheme}>
<Provider store={store}>
<StoreProvider store={new RootStore()}>
<DndProvider backend={HTML5Backend}>
<Router />
</DndProvider>
</StoreProvider>
</Provider>
</ConfigProvider>,
);
});

View file

@ -5,66 +5,163 @@ import { createRoot } from 'react-dom/client';
import './init';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { ConfigProvider, App, theme, ThemeConfig } from 'antd';
import colors from 'App/theme/colors';
import { ConfigProvider, App, theme as antdTheme, ThemeConfig } from 'antd';
import { BrowserRouter } from 'react-router-dom';
import { Notification, MountPoint } from 'UI';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StoreProvider, RootStore } from './mstore';
import Router from './Router';
import './i18n';
import { ThemeProvider, useTheme } from './ThemeContext';
// @ts-ignore
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
const queryClient = new QueryClient();
const customTheme: ThemeConfig = {
// algorithm: theme.compactAlgorithm,
components: {
Layout: {
headerBg: colors['gray-lightest'],
siderBg: colors['gray-lightest'],
},
Segmented: {
itemSelectedBg: '#FFFFFF',
itemSelectedColor: colors.main,
},
Menu: {
colorPrimary: colors.teal,
colorBgContainer: colors['gray-lightest'],
colorFillTertiary: colors['gray-lightest'],
colorBgLayout: colors['gray-lightest'],
subMenuItemBg: colors['gray-lightest'],
itemHoverBg: colors['active-blue'],
itemHoverColor: colors.teal,
const cssVar = (name: string) => `var(--color-${name})`;
itemActiveBg: colors['active-blue'],
itemSelectedBg: colors['active-blue'],
itemSelectedColor: colors.teal,
const ThemedApp: React.FC = () => {
const { theme } = useTheme();
itemMarginBlock: 0,
// itemPaddingInline: 50,
// iconMarginInlineEnd: 14,
collapsedWidth: 180,
// Create theme based on current theme setting
const customTheme: ThemeConfig = {
algorithm: theme === 'dark' ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
components: {
Layout: {
headerBg: cssVar('gray-lightest'),
siderBg: cssVar('gray-lightest'),
},
Segmented: {
itemSelectedBg: theme === 'dark' ? cssVar('gray-darkest') : '#FFFFFF',
itemSelectedColor: cssVar('main'),
},
Menu: {
colorPrimary: cssVar('teal'),
colorBgContainer: cssVar('gray-lightest'),
colorFillTertiary: cssVar('gray-lightest'),
colorBgLayout: cssVar('gray-lightest'),
subMenuItemBg: cssVar('gray-lightest'),
itemHoverBg: cssVar('active-blue'),
itemHoverColor: cssVar('teal'),
itemActiveBg: cssVar('tealx-light'),
itemSelectedBg: cssVar('tealx-light'),
itemSelectedColor: cssVar('teal'),
itemColor: cssVar('gray-darkest'),
itemMarginBlock: 0,
collapsedWidth: 180,
},
Button: {
colorPrimary: cssVar('main'),
},
Select: {
colorBgContainer: cssVar('white'),
colorBgElevated: cssVar('white'),
colorBorder: cssVar('gray-light'),
colorPrimaryHover: cssVar('main'),
colorPrimary: cssVar('main'),
colorText: cssVar('gray-darkest'),
colorTextPlaceholder: cssVar('gray-medium'),
colorTextQuaternary: cssVar('gray-medium'),
controlItemBgActive: cssVar('active-blue'),
controlItemBgHover: cssVar('active-blue'),
},
Radio: {
colorPrimary: cssVar('main'),
colorBorder: cssVar('gray-medium'),
colorBgContainer: cssVar('white'),
},
Switch: {
colorPrimary: cssVar('main'),
colorPrimaryHover: cssVar('teal-dark'),
colorTextQuaternary: cssVar('gray-light'),
colorTextTertiary: cssVar('gray-medium'),
colorBgContainer: cssVar('white'),
},
Input: {
colorBgContainer: cssVar('white'),
colorBorder: cssVar('gray-light'),
colorText: cssVar('gray-darkest'),
colorTextPlaceholder: cssVar('gray-medium'),
activeBorderColor: cssVar('main'),
hoverBorderColor: cssVar('main'),
},
Checkbox: {
colorPrimary: cssVar('main'),
colorBgContainer: cssVar('white'),
colorBorder: cssVar('gray-medium'),
},
Table: {
colorBgContainer: cssVar('white'),
colorText: cssVar('gray-darkest'),
colorTextHeading: cssVar('gray-darkest'),
colorBorderSecondary: cssVar('gray-light'),
headerBg: cssVar('gray-lightest'),
rowHoverBg: cssVar('gray-lightest'),
headerSortHoverBg: cssVar('gray-lighter'),
headerSortActiveBg: cssVar('gray-lighter')
},
Modal: {
colorBgElevated: cssVar('white'),
colorText: cssVar('gray-darkest'),
},
Card: {
colorBgContainer: cssVar('white'),
colorBorderSecondary: cssVar('gray-light'),
},
Tooltip: {
colorBgSpotlight: cssVar('white'),
colorTextLightSolid: cssVar('gray-darkest'),
},
Tabs: {
itemActiveColor: cssVar('main'),
inkBarColor: cssVar('main'),
itemSelectedColor: cssVar('main')
},
Tag: {
defaultBg: cssVar('gray-lightest'),
defaultColor: cssVar('gray-darkest')
}
},
Button: {
colorPrimary: colors.teal,
token: {
colorPrimary: cssVar('main'),
colorPrimaryActive: cssVar('teal-dark'),
colorPrimaryHover: cssVar('main'),
colorPrimaryBorder: cssVar('main'),
colorBorder: cssVar('gray-light'),
colorBgLayout: cssVar('gray-lightest'),
colorBgContainer: cssVar('white'),
controlItemBgActive: cssVar('active-blue'),
controlItemBgActiveHover: cssVar('active-blue'),
controlItemBgHover: cssVar('active-blue'),
colorLink: cssVar('teal'),
colorLinkHover: cssVar('teal-dark'),
colorText: cssVar('gray-darkest'),
colorTextSecondary: cssVar('gray-dark'),
colorTextDisabled: cssVar('disabled-text'),
borderRadius: 4,
fontSize: 14,
fontFamily: "'Roboto', 'ArialMT', 'Arial'",
fontWeightStrong: 400,
colorSplit: cssVar('gray-light')
},
},
token: {
colorPrimary: colors.teal,
colorPrimaryActive: '#394EFF',
colorBgLayout: colors['gray-lightest'],
colorBgContainer: colors.white,
colorLink: colors.teal,
colorLinkHover: colors['teal-dark'],
};
borderRadius: 4,
fontSize: 14,
fontFamily: "'Roboto', 'ArialMT', 'Arial'",
fontWeightStrong: 400,
},
return (
<ConfigProvider theme={customTheme}>
<App>
<StoreProvider store={new RootStore()}>
<DndProvider backend={HTML5Backend}>
<BrowserRouter>
<Notification />
<Router />
</BrowserRouter>
</DndProvider>
<MountPoint />
</StoreProvider>
</App>
</ConfigProvider>
);
};
document.addEventListener('DOMContentLoaded', () => {
@ -72,22 +169,11 @@ document.addEventListener('DOMContentLoaded', () => {
// @ts-ignore
const root = createRoot(container);
// const theme = window.localStorage.getItem('theme');
root.render(
<QueryClientProvider client={queryClient}>
<ConfigProvider theme={customTheme}>
<App>
<StoreProvider store={new RootStore()}>
<DndProvider backend={HTML5Backend}>
<BrowserRouter>
<Notification />
<Router />
</BrowserRouter>
</DndProvider>
<MountPoint />
</StoreProvider>
</App>
</ConfigProvider>
</QueryClientProvider>,
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<ThemedApp />
</ThemeProvider>
</QueryClientProvider>
);
});

View file

@ -210,10 +210,10 @@ function SideMenu(props: Props) {
<Icon
name={item.icon}
size={16}
color={isActive ? 'teal' : ''}
color={isActive ? 'teal' : 'black'}
/>
}
className={cn('!rounded-lg hover-fill-teal')}
className={cn('!rounded-lg hover-fill-teal', isActive ? 'color-main' : 'color-black')}
>
{item.label}
</Menu.Item>
@ -228,17 +228,17 @@ function SideMenu(props: Props) {
<Icon
name={item.icon}
size={16}
color={isActive ? 'teal' : ''}
color={isActive ? 'teal' : 'black'}
/>
}
style={{ paddingLeft: '20px' }}
className={cn('!rounded-lg !pe-0')}
className={cn('!rounded-lg !pe-0', isActive ? 'color-main' : 'color-black')}
itemIcon={
item.leading ? (
<Icon
name={item.leading}
size={16}
color={isActive ? 'teal' : ''}
color={isActive ? 'teal' : 'black'}
/>
) : null
}
@ -293,18 +293,18 @@ function SideMenu(props: Props) {
<Icon
name={item.icon}
size={16}
color={isActive ? 'teal' : ''}
color={isActive ? 'teal' : 'black'}
className="hover-fill-teal"
/>
}
style={{ paddingLeft: '20px' }}
className={cn('!rounded-lg hover-fill-teal')}
className={cn('!rounded-lg hover-fill-teal', isActive ? 'color-main' : 'color-black')}
itemIcon={
item.leading ? (
<Icon
name={item.leading}
size={16}
color={isActive ? 'teal' : ''}
color={isActive ? 'teal' : 'black'}
/>
) : null
}

View file

@ -10,6 +10,7 @@ import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress
import ProjectDropdown from 'Shared/ProjectDropdown';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import ThemeToggle from 'Components/ThemeToggle';
function TopRight() {
const { userStore } = useStore();
@ -27,6 +28,7 @@ function TopRight() {
{account.name ? <HealthStatus /> : null}
</>
)}
<ThemeToggle />
<Popover content={<UserMenu />} placement="topRight">
<div className="flex items-center cursor-pointer">

View file

@ -503,7 +503,7 @@
"Returning users between": "Returning users between",
"Sessions": "Sessions",
"No recordings found.": "No recordings found.",
"Get new session": "Get new session",
"Get new image": "Get new image",
"The number of cards in one dashboard is limited to 30.": "The number of cards in one dashboard is limited to 30.",
"Add Card": "Add Card",
"Create Dashboard": "Create Dashboard",

View file

@ -503,7 +503,7 @@
"Returning users between": "Usuarios recurrentes entre",
"Sessions": "Sesiones",
"No recordings found.": "No se encontraron grabaciones.",
"Get new session": "Obtener nueva sesión",
"Get new image": "Obtener nueva sesión",
"The number of cards in one dashboard is limited to 30.": "El número de tarjetas en un panel está limitado a 30.",
"Add Card": "Agregar tarjeta",
"Create Dashboard": "Crear panel",

View file

@ -503,7 +503,7 @@
"Returning users between": "Utilisateurs récurrents entre",
"Sessions": "Sessions",
"No recordings found.": "Aucun enregistrement trouvé.",
"Get new session": "Obtenir une nouvelle session",
"Get new image": "Obtenir une nouvelle session",
"The number of cards in one dashboard is limited to 30.": "Le nombre de cartes dans un tableau de bord est limité à 30.",
"Add Card": "Ajouter une carte",
"Create Dashboard": "Créer un tableau de bord",

View file

@ -504,7 +504,7 @@
"Returning users between": "Возвращающиеся пользователи за период",
"Sessions": "Сессии",
"No recordings found.": "Записей не найдено.",
"Get new session": "Получить новую сессию",
"Get new image": "Получить новую сессию",
"The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.",
"Add Card": "Добавить карточку",
"Create Dashboard": "Создать дашборд",
@ -1502,4 +1502,4 @@
"Interface Language": "Язык интерфейса",
"Select the language in which OpenReplay will appear.": "Выберите язык, на котором будет отображаться OpenReplay.",
"Language": "Язык"
}
}

View file

@ -503,7 +503,7 @@
"Returning users between": "回访用户区间",
"Sessions": "会话",
"No recordings found.": "未找到录制。",
"Get new session": "获取新会话",
"Get new image": "获取新会话",
"The number of cards in one dashboard is limited to 30.": "一个仪表板最多可包含30个卡片。",
"Add Card": "添加卡片",
"Create Dashboard": "创建仪表板",

View file

@ -1,4 +1,4 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { makeAutoObservable, runInAction, reaction } from 'mobx';
import { dashboardService, metricService } from 'App/services';
import { toast } from 'react-toastify';
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
@ -6,6 +6,7 @@ import { getRE } from 'App/utils';
import Filter from './types/filter';
import Widget from './types/widget';
import Dashboard from './types/dashboard';
import { calculateGranularities } from '@/components/Dashboard/components/WidgetDateRange/RangeGranularity';
interface DashboardFilter {
query?: string;
@ -36,7 +37,7 @@ export default class DashboardStore {
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
selectedDensity: number = 7; // depends on default drilldown, 7 points here!!!;
selectedDensity: number = 7;
comparisonPeriods: Record<string, any> = {};
@ -56,7 +57,7 @@ export default class DashboardStore {
metricsSearch: string = '';
// Loading states
isLoading: boolean = true;
isLoading: boolean = false;
isSaving: boolean = false;
@ -83,10 +84,25 @@ export default class DashboardStore {
makeAutoObservable(this);
this.resetDrillDownFilter();
this.createDensity(this.period.getDuration());
reaction(
() => this.period,
(period) => {
this.createDensity(period.getDuration());
}
)
}
setDensity = (density: any) => {
this.selectedDensity = parseInt(density, 10);
createDensity = (duration: number) => {
const densityOpts = calculateGranularities(duration);
const defaultOption = densityOpts[densityOpts.length - 2];
this.setDensity(defaultOption.key)
}
setDensity = (density: number) => {
this.selectedDensity = density;
};
get sortedDashboards() {
@ -529,7 +545,7 @@ export default class DashboardStore {
const data = await metricService.getMetricChartData(
metric,
params,
isSaved,
isSaved
);
resolve(metric.setData(data, period, isComparison, density));
} catch (error) {

View file

@ -163,6 +163,7 @@ export default class Widget {
fromJson(json: any, period?: any) {
json.config = json.config || {};
runInAction(() => {
this.dashboardId = json.dashboardId;
this.metricId = json.metricId;
this.widgetId = json.widgetId;
this.metricValue = this.metricValueFromArray(

View file

@ -140,11 +140,16 @@ class SimpleHeatmap {
ctx.drawImage(this.circle, p[0] - this.r, p[1] - this.r);
});
const colored = ctx.getImageData(0, 0, this.width, this.height);
this.colorize(colored.data, this.grad);
ctx.putImageData(colored, 0, 0);
return this;
try {
const colored = ctx.getImageData(0, 0, this.width, this.height);
this.colorize(colored.data, this.grad);
ctx.putImageData(colored, 0, 0);
} catch (e) {
// usually happens if session is corrupted ?
console.error('Error while colorizing heatmap:', e);
} finally {
return this;
}
}
private colorize(

View file

@ -255,6 +255,48 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 37: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const rule = this.readString(); if (rule === null) { return resetPointer() }
const index = this.readUint(); if (index === null) { return resetPointer() }
return {
tp: MType.CssInsertRule,
id,
rule,
index,
};
}
case 38: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const index = this.readUint(); if (index === null) { return resetPointer() }
return {
tp: MType.CssDeleteRule,
id,
index,
};
}
case 39: {
const method = this.readString(); if (method === null) { return resetPointer() }
const url = this.readString(); if (url === null) { return resetPointer() }
const request = this.readString(); if (request === null) { return resetPointer() }
const response = this.readString(); if (response === null) { return resetPointer() }
const status = this.readUint(); if (status === null) { return resetPointer() }
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
return {
tp: MType.Fetch,
method,
url,
request,
response,
status,
timestamp,
duration,
};
}
case 40: {
const name = this.readString(); if (name === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
@ -459,6 +501,26 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 59: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
const context = this.readUint(); if (context === null) { return resetPointer() }
const containerType = this.readUint(); if (containerType === null) { return resetPointer() }
const containerSrc = this.readString(); if (containerSrc === null) { return resetPointer() }
const containerId = this.readString(); if (containerId === null) { return resetPointer() }
const containerName = this.readString(); if (containerName === null) { return resetPointer() }
return {
tp: MType.LongTask,
timestamp,
duration,
context,
containerType,
containerSrc,
containerId,
containerName,
};
}
case 60: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const name = this.readString(); if (name === null) { return resetPointer() }
@ -485,6 +547,20 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 67: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const rule = this.readString(); if (rule === null) { return resetPointer() }
const index = this.readUint(); if (index === null) { return resetPointer() }
const baseURL = this.readString(); if (baseURL === null) { return resetPointer() }
return {
tp: MType.CssInsertRuleURLBased,
id,
rule,
index,
baseURL,
};
}
case 68: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const hesitationTime = this.readUint(); if (hesitationTime === null) { return resetPointer() }

View file

@ -4,7 +4,7 @@
import { MType } from './raw.gen'
const IOS_TYPES = [90,91,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,110,111]
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,34,35,49,50,51,43,52,54,55,57,58,60,61,68,69,70,71,72,73,74,75,76,77,113,114,117,118,119,122]
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,34,35,37,38,49,50,51,43,52,54,55,57,58,59,60,61,67,68,69,70,71,72,73,74,75,76,77,113,114,117,118,119,122]
export function isDOMType(t: MType) {
return DOM_TYPES.includes(t)
}

View file

@ -25,6 +25,9 @@ import type {
RawConsoleLog,
RawStringDictGlobal,
RawSetNodeAttributeDictGlobal,
RawCssInsertRule,
RawCssDeleteRule,
RawFetch,
RawProfiler,
RawOTable,
RawReduxDeprecated,
@ -42,8 +45,10 @@ import type {
RawSetPageVisibility,
RawLoadFontFace,
RawSetNodeFocus,
RawLongTask,
RawSetNodeAttributeURLBased,
RawSetCssDataURLBased,
RawCssInsertRuleURLBased,
RawMouseClick,
RawMouseClickDeprecated,
RawCreateIFrameDocument,
@ -125,6 +130,12 @@ export type StringDictGlobal = RawStringDictGlobal & Timed
export type SetNodeAttributeDictGlobal = RawSetNodeAttributeDictGlobal & Timed
export type CssInsertRule = RawCssInsertRule & Timed
export type CssDeleteRule = RawCssDeleteRule & Timed
export type Fetch = RawFetch & Timed
export type Profiler = RawProfiler & Timed
export type OTable = RawOTable & Timed
@ -159,10 +170,14 @@ export type LoadFontFace = RawLoadFontFace & Timed
export type SetNodeFocus = RawSetNodeFocus & Timed
export type LongTask = RawLongTask & Timed
export type SetNodeAttributeURLBased = RawSetNodeAttributeURLBased & Timed
export type SetCssDataURLBased = RawSetCssDataURLBased & Timed
export type CssInsertRuleURLBased = RawCssInsertRuleURLBased & Timed
export type MouseClick = RawMouseClick & Timed
export type MouseClickDeprecated = RawMouseClickDeprecated & Timed

View file

@ -23,6 +23,9 @@ export const enum MType {
ConsoleLog = 22,
StringDictGlobal = 34,
SetNodeAttributeDictGlobal = 35,
CssInsertRule = 37,
CssDeleteRule = 38,
Fetch = 39,
Profiler = 40,
OTable = 41,
ReduxDeprecated = 44,
@ -40,8 +43,10 @@ export const enum MType {
SetPageVisibility = 55,
LoadFontFace = 57,
SetNodeFocus = 58,
LongTask = 59,
SetNodeAttributeURLBased = 60,
SetCssDataURLBased = 61,
CssInsertRuleURLBased = 67,
MouseClick = 68,
MouseClickDeprecated = 69,
CreateIFrameDocument = 70,
@ -218,6 +223,30 @@ export interface RawSetNodeAttributeDictGlobal {
value: number,
}
export interface RawCssInsertRule {
tp: MType.CssInsertRule,
id: number,
rule: string,
index: number,
}
export interface RawCssDeleteRule {
tp: MType.CssDeleteRule,
id: number,
index: number,
}
export interface RawFetch {
tp: MType.Fetch,
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
}
export interface RawProfiler {
tp: MType.Profiler,
name: string,
@ -337,6 +366,17 @@ export interface RawSetNodeFocus {
id: number,
}
export interface RawLongTask {
tp: MType.LongTask,
timestamp: number,
duration: number,
context: number,
containerType: number,
containerSrc: string,
containerId: string,
containerName: string,
}
export interface RawSetNodeAttributeURLBased {
tp: MType.SetNodeAttributeURLBased,
id: number,
@ -352,6 +392,14 @@ export interface RawSetCssDataURLBased {
baseURL: string,
}
export interface RawCssInsertRuleURLBased {
tp: MType.CssInsertRuleURLBased,
id: number,
rule: string,
index: number,
baseURL: string,
}
export interface RawMouseClick {
tp: MType.MouseClick,
id: number,
@ -622,4 +670,4 @@ export interface RawMobileIssueEvent {
}
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawStringDictGlobal | RawSetNodeAttributeDictGlobal | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQlDeprecated | RawPerformanceTrack | RawStringDictDeprecated | RawSetNodeAttributeDictDeprecated | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawIncident | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawGraphQl | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawStringDictGlobal | RawSetNodeAttributeDictGlobal | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQlDeprecated | RawPerformanceTrack | RawStringDictDeprecated | RawSetNodeAttributeDictDeprecated | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawIncident | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawGraphQl | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;

View file

@ -24,6 +24,9 @@ export const TP_MAP = {
22: MType.ConsoleLog,
34: MType.StringDictGlobal,
35: MType.SetNodeAttributeDictGlobal,
37: MType.CssInsertRule,
38: MType.CssDeleteRule,
39: MType.Fetch,
40: MType.Profiler,
41: MType.OTable,
44: MType.ReduxDeprecated,
@ -41,8 +44,10 @@ export const TP_MAP = {
55: MType.SetPageVisibility,
57: MType.LoadFontFace,
58: MType.SetNodeFocus,
59: MType.LongTask,
60: MType.SetNodeAttributeURLBased,
61: MType.SetCssDataURLBased,
67: MType.CssInsertRuleURLBased,
68: MType.MouseClick,
69: MType.MouseClickDeprecated,
70: MType.CreateIFrameDocument,

View file

@ -186,6 +186,30 @@ type TrSetNodeAttributeDictGlobal = [
value: number,
]
type TrCSSInsertRule = [
type: 37,
id: number,
rule: string,
index: number,
]
type TrCSSDeleteRule = [
type: 38,
id: number,
index: number,
]
type TrFetch = [
type: 39,
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
]
type TrProfiler = [
type: 40,
name: string,
@ -310,6 +334,17 @@ type TrSetNodeFocus = [
id: number,
]
type TrLongTask = [
type: 59,
timestamp: number,
duration: number,
context: number,
containerType: number,
containerSrc: string,
containerId: string,
containerName: string,
]
type TrSetNodeAttributeURLBased = [
type: 60,
id: number,
@ -337,6 +372,14 @@ type TrCustomIssue = [
payload: string,
]
type TrCSSInsertRuleURLBased = [
type: 67,
id: number,
rule: string,
index: number,
baseURL: string,
]
type TrMouseClick = [
type: 68,
id: number,
@ -547,7 +590,7 @@ type TrWebVitals = [
]
export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrStringDictGlobal | TrSetNodeAttributeDictGlobal | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDictDeprecated | TrSetNodeAttributeDictDeprecated | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrIncident | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals
export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrStringDictGlobal | TrSetNodeAttributeDictGlobal | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDictDeprecated | TrSetNodeAttributeDictDeprecated | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrIncident | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
@ -725,6 +768,36 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 37: {
return {
tp: MType.CssInsertRule,
id: tMsg[1],
rule: tMsg[2],
index: tMsg[3],
}
}
case 38: {
return {
tp: MType.CssDeleteRule,
id: tMsg[1],
index: tMsg[2],
}
}
case 39: {
return {
tp: MType.Fetch,
method: tMsg[1],
url: tMsg[2],
request: tMsg[3],
response: tMsg[4],
status: tMsg[5],
timestamp: tMsg[6],
duration: tMsg[7],
}
}
case 40: {
return {
tp: MType.Profiler,
@ -878,6 +951,19 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 59: {
return {
tp: MType.LongTask,
timestamp: tMsg[1],
duration: tMsg[2],
context: tMsg[3],
containerType: tMsg[4],
containerSrc: tMsg[5],
containerId: tMsg[6],
containerName: tMsg[7],
}
}
case 60: {
return {
tp: MType.SetNodeAttributeURLBased,
@ -897,6 +983,16 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 67: {
return {
tp: MType.CssInsertRuleURLBased,
id: tMsg[1],
rule: tMsg[2],
index: tMsg[3],
baseURL: tMsg[4],
}
}
case 68: {
return {
tp: MType.MouseClick,

View file

@ -1,268 +1,342 @@
/* Auto-generated, DO NOT EDIT */
/* Uses CSS variables (--color-*) generated by Tailwind config */
/* fill */
.fill-main { fill: #394EFF }
.fill-gray-light-shade { fill: #EEEEEE }
.fill-gray-lightest { fill: #f6f6f6 }
.fill-gray-lighter { fill: #f1f1f1 }
.fill-gray-light { fill: #ddd }
.fill-gray-bg { fill: #CCC }
.fill-gray-medium { fill: #888 }
.fill-gray-dark { fill: #666 }
.fill-gray-darkest { fill: #333 }
.fill-gray-light-blue { fill: #F8F8FA }
.fill-teal { fill: #394EFF }
.fill-teal-dark { fill: #2331A8 }
.fill-teal-light { fill: rgba(57, 78, 255, 0.1) }
.fill-tealx { fill: #3EAAAF }
.fill-tealx-light { fill: #E2F0EE }
.fill-tealx-light-border { fill: #C6DCDA }
.fill-tealx-lightest { fill: rgba(62, 170, 175, 0.1) }
.fill-orange { fill: #E28940 }
.fill-yellow { fill: #FFFBE5 }
.fill-yellow2 { fill: #F5A623 }
.fill-orange-dark { fill: #C26822 }
.fill-green { fill: #42AE5E }
.fill-green2 { fill: #00dc69 }
.fill-green-dark { fill: #2C9848 }
.fill-red { fill: #cc0000 }
.fill-red2 { fill: #F5A623 }
.fill-red-lightest { fill: rgba(204, 0, 0, 0.1) }
.fill-blue { fill: #366CD9 }
.fill-blue2 { fill: #0076FF }
.fill-active-blue { fill: #F6F7FF }
.fill-active-dark-blue { fill: #E2E4F6 }
.fill-bg-blue { fill: #e3e6ff }
.fill-active-blue-border { fill: #D0D4F2 }
.fill-pink { fill: #ffb9b9 }
.fill-light-blue-bg { fill: #E5F7F7 }
.fill-white { fill: #fff }
.fill-borderColor-default { fill: #DDDDDD }
.fill-borderColor-gray-light-shade { fill: #EEEEEE }
.fill-borderColor-primary { fill: #3490dc }
.fill-borderColor-transparent { fill: transparent }
.fill-transparent { fill: transparent }
.fill-cyan { fill: #EBF4F5 }
.fill-figmaColors-accent-secondary { fill: rgba(62, 170, 175, 1) }
.fill-figmaColors-main { fill: rgba(57, 78, 255, 1) }
.fill-figmaColors-primary-outlined-hover-background { fill: rgba(62, 170, 175, 0.08) }
.fill-figmaColors-primary-outlined-resting-border { fill: rgba(62, 170, 175, 0.5) }
.fill-figmaColors-secondary-outlined-hover-background { fill: rgba(63, 81, 181, 0.08) }
.fill-figmaColors-secondary-outlined-resting-border { fill: rgba(63, 81, 181, 0.5) }
.fill-figmaColors-text-disabled { fill: rgba(0,0,0, 0.38) }
.fill-figmaColors-text-primary { fill: rgba(0,0,0, 0.87) }
.fill-figmaColors-outlined-border { fill: rgba(0,0,0, 0.23) }
.fill-figmaColors-divider { fill: rgba(0, 0, 0, 0.12) }
.hover-fill-main:hover svg { fill: #394EFF }
.hover-fill-gray-light-shade:hover svg { fill: #EEEEEE }
.hover-fill-gray-lightest:hover svg { fill: #f6f6f6 }
.hover-fill-gray-lighter:hover svg { fill: #f1f1f1 }
.hover-fill-gray-light:hover svg { fill: #ddd }
.hover-fill-gray-bg:hover svg { fill: #CCC }
.hover-fill-gray-medium:hover svg { fill: #888 }
.hover-fill-gray-dark:hover svg { fill: #666 }
.hover-fill-gray-darkest:hover svg { fill: #333 }
.hover-fill-gray-light-blue:hover svg { fill: #F8F8FA }
.hover-fill-teal:hover svg { fill: #394EFF }
.hover-fill-teal-dark:hover svg { fill: #2331A8 }
.hover-fill-teal-light:hover svg { fill: rgba(57, 78, 255, 0.1) }
.hover-fill-tealx:hover svg { fill: #3EAAAF }
.hover-fill-tealx-light:hover svg { fill: #E2F0EE }
.hover-fill-tealx-light-border:hover svg { fill: #C6DCDA }
.hover-fill-tealx-lightest:hover svg { fill: rgba(62, 170, 175, 0.1) }
.hover-fill-orange:hover svg { fill: #E28940 }
.hover-fill-yellow:hover svg { fill: #FFFBE5 }
.hover-fill-yellow2:hover svg { fill: #F5A623 }
.hover-fill-orange-dark:hover svg { fill: #C26822 }
.hover-fill-green:hover svg { fill: #42AE5E }
.hover-fill-green2:hover svg { fill: #00dc69 }
.hover-fill-green-dark:hover svg { fill: #2C9848 }
.hover-fill-red:hover svg { fill: #cc0000 }
.hover-fill-red2:hover svg { fill: #F5A623 }
.hover-fill-red-lightest:hover svg { fill: rgba(204, 0, 0, 0.1) }
.hover-fill-blue:hover svg { fill: #366CD9 }
.hover-fill-blue2:hover svg { fill: #0076FF }
.hover-fill-active-blue:hover svg { fill: #F6F7FF }
.hover-fill-active-dark-blue:hover svg { fill: #E2E4F6 }
.hover-fill-bg-blue:hover svg { fill: #e3e6ff }
.hover-fill-active-blue-border:hover svg { fill: #D0D4F2 }
.hover-fill-pink:hover svg { fill: #ffb9b9 }
.hover-fill-light-blue-bg:hover svg { fill: #E5F7F7 }
.hover-fill-white:hover svg { fill: #fff }
.hover-fill-borderColor-default:hover svg { fill: #DDDDDD }
.hover-fill-borderColor-gray-light-shade:hover svg { fill: #EEEEEE }
.hover-fill-borderColor-primary:hover svg { fill: #3490dc }
.hover-fill-borderColor-transparent:hover svg { fill: transparent }
.hover-fill-transparent:hover svg { fill: transparent }
.hover-fill-cyan:hover svg { fill: #EBF4F5 }
.hover-fill-figmaColors-accent-secondary:hover svg { fill: rgba(62, 170, 175, 1) }
.hover-fill-figmaColors-main:hover svg { fill: rgba(57, 78, 255, 1) }
.hover-fill-figmaColors-primary-outlined-hover-background:hover svg { fill: rgba(62, 170, 175, 0.08) }
.hover-fill-figmaColors-primary-outlined-resting-border:hover svg { fill: rgba(62, 170, 175, 0.5) }
.hover-fill-figmaColors-secondary-outlined-hover-background:hover svg { fill: rgba(63, 81, 181, 0.08) }
.hover-fill-figmaColors-secondary-outlined-resting-border:hover svg { fill: rgba(63, 81, 181, 0.5) }
.hover-fill-figmaColors-text-disabled:hover svg { fill: rgba(0,0,0, 0.38) }
.hover-fill-figmaColors-text-primary:hover svg { fill: rgba(0,0,0, 0.87) }
.hover-fill-figmaColors-outlined-border:hover svg { fill: rgba(0,0,0, 0.23) }
.hover-fill-figmaColors-divider:hover svg { fill: rgba(0, 0, 0, 0.12) }
.fill-main { fill: var(--color-main) }
.fill-gray-light-shade { fill: var(--color-gray-light-shade) }
.fill-gray-lightest { fill: var(--color-gray-lightest) }
.fill-gray-lighter { fill: var(--color-gray-lighter) }
.fill-gray-light { fill: var(--color-gray-light) }
.fill-gray-bg { fill: var(--color-gray-bg) }
.fill-gray-medium { fill: var(--color-gray-medium) }
.fill-gray-dark { fill: var(--color-gray-dark) }
.fill-gray-darkest { fill: var(--color-gray-darkest) }
.fill-gray-light-blue { fill: var(--color-gray-light-blue) }
.fill-teal { fill: var(--color-teal) }
.fill-teal-dark { fill: var(--color-teal-dark) }
.fill-teal-light { fill: var(--color-teal-light) }
.fill-tealx { fill: var(--color-tealx) }
.fill-tealx-light { fill: var(--color-tealx-light) }
.fill-tealx-light-border { fill: var(--color-tealx-light-border) }
.fill-tealx-lightest { fill: var(--color-tealx-lightest) }
.fill-orange { fill: var(--color-orange) }
.fill-yellow { fill: var(--color-yellow) }
.fill-yellow2 { fill: var(--color-yellow2) }
.fill-orange-dark { fill: var(--color-orange-dark) }
.fill-green { fill: var(--color-green) }
.fill-green2 { fill: var(--color-green2) }
.fill-green-dark { fill: var(--color-green-dark) }
.fill-red { fill: var(--color-red) }
.fill-red2 { fill: var(--color-red2) }
.fill-red-lightest { fill: var(--color-red-lightest) }
.fill-blue { fill: var(--color-blue) }
.fill-blue2 { fill: var(--color-blue2) }
.fill-active-blue { fill: var(--color-active-blue) }
.fill-active-dark-blue { fill: var(--color-active-dark-blue) }
.fill-bg-blue { fill: var(--color-bg-blue) }
.fill-active-blue-border { fill: var(--color-active-blue-border) }
.fill-pink { fill: var(--color-pink) }
.fill-light-blue-bg { fill: var(--color-light-blue-bg) }
.fill-white { fill: var(--color-white) }
.fill-black { fill: var(--color-black) }
.fill-gray-border { fill: var(--color-gray-border) }
.fill-borderColor-default { fill: var(--color-borderColor-default) }
.fill-borderColor-gray-light-shade { fill: var(--color-borderColor-gray-light-shade) }
.fill-borderColor-primary { fill: var(--color-borderColor-primary) }
.fill-borderColor-transparent { fill: var(--color-borderColor-transparent) }
.fill-transparent { fill: var(--color-transparent) }
.fill-cyan { fill: var(--color-cyan) }
.fill-amber { fill: var(--color-amber) }
.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) }
.fill-figmaColors-primary-outlined-resting-border { fill: var(--color-figmaColors-primary-outlined-resting-border) }
.fill-figmaColors-secondary-outlined-hover-background { fill: var(--color-figmaColors-secondary-outlined-hover-background) }
.fill-figmaColors-secondary-outlined-resting-border { fill: var(--color-figmaColors-secondary-outlined-resting-border) }
.fill-figmaColors-text-disabled { fill: var(--color-figmaColors-text-disabled) }
.fill-figmaColors-text-primary { fill: var(--color-figmaColors-text-primary) }
.fill-figmaColors-outlined-border { fill: var(--color-figmaColors-outlined-border) }
.fill-figmaColors-divider { fill: var(--color-figmaColors-divider) }
.hover-fill-main:hover svg { fill: var(--color-main) }
.hover-fill-gray-light-shade:hover svg { fill: var(--color-gray-light-shade) }
.hover-fill-gray-lightest:hover svg { fill: var(--color-gray-lightest) }
.hover-fill-gray-lighter:hover svg { fill: var(--color-gray-lighter) }
.hover-fill-gray-light:hover svg { fill: var(--color-gray-light) }
.hover-fill-gray-bg:hover svg { fill: var(--color-gray-bg) }
.hover-fill-gray-medium:hover svg { fill: var(--color-gray-medium) }
.hover-fill-gray-dark:hover svg { fill: var(--color-gray-dark) }
.hover-fill-gray-darkest:hover svg { fill: var(--color-gray-darkest) }
.hover-fill-gray-light-blue:hover svg { fill: var(--color-gray-light-blue) }
.hover-fill-teal:hover svg { fill: var(--color-teal) }
.hover-fill-teal-dark:hover svg { fill: var(--color-teal-dark) }
.hover-fill-teal-light:hover svg { fill: var(--color-teal-light) }
.hover-fill-tealx:hover svg { fill: var(--color-tealx) }
.hover-fill-tealx-light:hover svg { fill: var(--color-tealx-light) }
.hover-fill-tealx-light-border:hover svg { fill: var(--color-tealx-light-border) }
.hover-fill-tealx-lightest:hover svg { fill: var(--color-tealx-lightest) }
.hover-fill-orange:hover svg { fill: var(--color-orange) }
.hover-fill-yellow:hover svg { fill: var(--color-yellow) }
.hover-fill-yellow2:hover svg { fill: var(--color-yellow2) }
.hover-fill-orange-dark:hover svg { fill: var(--color-orange-dark) }
.hover-fill-green:hover svg { fill: var(--color-green) }
.hover-fill-green2:hover svg { fill: var(--color-green2) }
.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-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) }
.hover-fill-active-blue:hover svg { fill: var(--color-active-blue) }
.hover-fill-active-dark-blue:hover svg { fill: var(--color-active-dark-blue) }
.hover-fill-bg-blue:hover svg { fill: var(--color-bg-blue) }
.hover-fill-active-blue-border:hover svg { fill: var(--color-active-blue-border) }
.hover-fill-pink:hover svg { fill: var(--color-pink) }
.hover-fill-light-blue-bg:hover svg { fill: var(--color-light-blue-bg) }
.hover-fill-white:hover svg { fill: var(--color-white) }
.hover-fill-black:hover svg { fill: var(--color-black) }
.hover-fill-gray-border:hover svg { fill: var(--color-gray-border) }
.hover-fill-borderColor-default:hover svg { fill: var(--color-borderColor-default) }
.hover-fill-borderColor-gray-light-shade:hover svg { fill: var(--color-borderColor-gray-light-shade) }
.hover-fill-borderColor-primary:hover svg { fill: var(--color-borderColor-primary) }
.hover-fill-borderColor-transparent:hover svg { fill: var(--color-borderColor-transparent) }
.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-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) }
.hover-fill-figmaColors-primary-outlined-resting-border:hover svg { fill: var(--color-figmaColors-primary-outlined-resting-border) }
.hover-fill-figmaColors-secondary-outlined-hover-background:hover svg { fill: var(--color-figmaColors-secondary-outlined-hover-background) }
.hover-fill-figmaColors-secondary-outlined-resting-border:hover svg { fill: var(--color-figmaColors-secondary-outlined-resting-border) }
.hover-fill-figmaColors-text-disabled:hover svg { fill: var(--color-figmaColors-text-disabled) }
.hover-fill-figmaColors-text-primary:hover svg { fill: var(--color-figmaColors-text-primary) }
.hover-fill-figmaColors-outlined-border:hover svg { fill: var(--color-figmaColors-outlined-border) }
.hover-fill-figmaColors-divider:hover svg { fill: var(--color-figmaColors-divider) }
/* color */
.color-main { color: #394EFF }
.color-gray-light-shade { color: #EEEEEE }
.color-gray-lightest { color: #f6f6f6 }
.color-gray-lighter { color: #f1f1f1 }
.color-gray-light { color: #ddd }
.color-gray-bg { color: #CCC }
.color-gray-medium { color: #888 }
.color-gray-dark { color: #666 }
.color-gray-darkest { color: #333 }
.color-gray-light-blue { color: #F8F8FA }
.color-teal { color: #394EFF }
.color-teal-dark { color: #2331A8 }
.color-teal-light { color: rgba(57, 78, 255, 0.1) }
.color-tealx { color: #3EAAAF }
.color-tealx-light { color: #E2F0EE }
.color-tealx-light-border { color: #C6DCDA }
.color-tealx-lightest { color: rgba(62, 170, 175, 0.1) }
.color-orange { color: #E28940 }
.color-yellow { color: #FFFBE5 }
.color-yellow2 { color: #F5A623 }
.color-orange-dark { color: #C26822 }
.color-green { color: #42AE5E }
.color-green2 { color: #00dc69 }
.color-green-dark { color: #2C9848 }
.color-red { color: #cc0000 }
.color-red2 { color: #F5A623 }
.color-red-lightest { color: rgba(204, 0, 0, 0.1) }
.color-blue { color: #366CD9 }
.color-blue2 { color: #0076FF }
.color-active-blue { color: #F6F7FF }
.color-active-dark-blue { color: #E2E4F6 }
.color-bg-blue { color: #e3e6ff }
.color-active-blue-border { color: #D0D4F2 }
.color-pink { color: #ffb9b9 }
.color-light-blue-bg { color: #E5F7F7 }
.color-white { color: #fff }
.color-borderColor-default { color: #DDDDDD }
.color-borderColor-gray-light-shade { color: #EEEEEE }
.color-borderColor-primary { color: #3490dc }
.color-borderColor-transparent { color: transparent }
.color-transparent { color: transparent }
.color-cyan { color: #EBF4F5 }
.color-figmaColors-accent-secondary { color: rgba(62, 170, 175, 1) }
.color-figmaColors-main { color: rgba(57, 78, 255, 1) }
.color-figmaColors-primary-outlined-hover-background { color: rgba(62, 170, 175, 0.08) }
.color-figmaColors-primary-outlined-resting-border { color: rgba(62, 170, 175, 0.5) }
.color-figmaColors-secondary-outlined-hover-background { color: rgba(63, 81, 181, 0.08) }
.color-figmaColors-secondary-outlined-resting-border { color: rgba(63, 81, 181, 0.5) }
.color-figmaColors-text-disabled { color: rgba(0,0,0, 0.38) }
.color-figmaColors-text-primary { color: rgba(0,0,0, 0.87) }
.color-figmaColors-outlined-border { color: rgba(0,0,0, 0.23) }
.color-figmaColors-divider { color: rgba(0, 0, 0, 0.12) }
.color-main { color: var(--color-main) }
.color-gray-light-shade { color: var(--color-gray-light-shade) }
.color-gray-lightest { color: var(--color-gray-lightest) }
.color-gray-lighter { color: var(--color-gray-lighter) }
.color-gray-light { color: var(--color-gray-light) }
.color-gray-bg { color: var(--color-gray-bg) }
.color-gray-medium { color: var(--color-gray-medium) }
.color-gray-dark { color: var(--color-gray-dark) }
.color-gray-darkest { color: var(--color-gray-darkest) }
.color-gray-light-blue { color: var(--color-gray-light-blue) }
.color-teal { color: var(--color-teal) }
.color-teal-dark { color: var(--color-teal-dark) }
.color-teal-light { color: var(--color-teal-light) }
.color-tealx { color: var(--color-tealx) }
.color-tealx-light { color: var(--color-tealx-light) }
.color-tealx-light-border { color: var(--color-tealx-light-border) }
.color-tealx-lightest { color: var(--color-tealx-lightest) }
.color-orange { color: var(--color-orange) }
.color-yellow { color: var(--color-yellow) }
.color-yellow2 { color: var(--color-yellow2) }
.color-orange-dark { color: var(--color-orange-dark) }
.color-green { color: var(--color-green) }
.color-green2 { color: var(--color-green2) }
.color-green-dark { color: var(--color-green-dark) }
.color-red { color: var(--color-red) }
.color-red2 { color: var(--color-red2) }
.color-red-lightest { color: var(--color-red-lightest) }
.color-blue { color: var(--color-blue) }
.color-blue2 { color: var(--color-blue2) }
.color-active-blue { color: var(--color-active-blue) }
.color-active-dark-blue { color: var(--color-active-dark-blue) }
.color-bg-blue { color: var(--color-bg-blue) }
.color-active-blue-border { color: var(--color-active-blue-border) }
.color-pink { color: var(--color-pink) }
.color-light-blue-bg { color: var(--color-light-blue-bg) }
.color-white { color: var(--color-white) }
.color-black { color: var(--color-black) }
.color-gray-border { color: var(--color-gray-border) }
.color-borderColor-default { color: var(--color-borderColor-default) }
.color-borderColor-gray-light-shade { color: var(--color-borderColor-gray-light-shade) }
.color-borderColor-primary { color: var(--color-borderColor-primary) }
.color-borderColor-transparent { color: var(--color-borderColor-transparent) }
.color-transparent { color: var(--color-transparent) }
.color-cyan { color: var(--color-cyan) }
.color-amber { color: var(--color-amber) }
.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) }
.color-figmaColors-primary-outlined-resting-border { color: var(--color-figmaColors-primary-outlined-resting-border) }
.color-figmaColors-secondary-outlined-hover-background { color: var(--color-figmaColors-secondary-outlined-hover-background) }
.color-figmaColors-secondary-outlined-resting-border { color: var(--color-figmaColors-secondary-outlined-resting-border) }
.color-figmaColors-text-disabled { color: var(--color-figmaColors-text-disabled) }
.color-figmaColors-text-primary { color: var(--color-figmaColors-text-primary) }
.color-figmaColors-outlined-border { color: var(--color-figmaColors-outlined-border) }
.color-figmaColors-divider { color: var(--color-figmaColors-divider) }
/* hover color */
.hover-main:hover { color: #394EFF }
.hover-gray-light-shade:hover { color: #EEEEEE }
.hover-gray-lightest:hover { color: #f6f6f6 }
.hover-gray-lighter:hover { color: #f1f1f1 }
.hover-gray-light:hover { color: #ddd }
.hover-gray-bg:hover { color: #CCC }
.hover-gray-medium:hover { color: #888 }
.hover-gray-dark:hover { color: #666 }
.hover-gray-darkest:hover { color: #333 }
.hover-gray-light-blue:hover { color: #F8F8FA }
.hover-teal:hover { color: #394EFF }
.hover-teal-dark:hover { color: #2331A8 }
.hover-teal-light:hover { color: rgba(57, 78, 255, 0.1) }
.hover-tealx:hover { color: #3EAAAF }
.hover-tealx-light:hover { color: #E2F0EE }
.hover-tealx-light-border:hover { color: #C6DCDA }
.hover-tealx-lightest:hover { color: rgba(62, 170, 175, 0.1) }
.hover-orange:hover { color: #E28940 }
.hover-yellow:hover { color: #FFFBE5 }
.hover-yellow2:hover { color: #F5A623 }
.hover-orange-dark:hover { color: #C26822 }
.hover-green:hover { color: #42AE5E }
.hover-green2:hover { color: #00dc69 }
.hover-green-dark:hover { color: #2C9848 }
.hover-red:hover { color: #cc0000 }
.hover-red2:hover { color: #F5A623 }
.hover-red-lightest:hover { color: rgba(204, 0, 0, 0.1) }
.hover-blue:hover { color: #366CD9 }
.hover-blue2:hover { color: #0076FF }
.hover-active-blue:hover { color: #F6F7FF }
.hover-active-dark-blue:hover { color: #E2E4F6 }
.hover-bg-blue:hover { color: #e3e6ff }
.hover-active-blue-border:hover { color: #D0D4F2 }
.hover-pink:hover { color: #ffb9b9 }
.hover-light-blue-bg:hover { color: #E5F7F7 }
.hover-white:hover { color: #fff }
.hover-borderColor-default:hover { color: #DDDDDD }
.hover-borderColor-gray-light-shade:hover { color: #EEEEEE }
.hover-borderColor-primary:hover { color: #3490dc }
.hover-borderColor-transparent:hover { color: transparent }
.hover-transparent:hover { color: transparent }
.hover-cyan:hover { color: #EBF4F5 }
.hover-figmaColors-accent-secondary:hover { color: rgba(62, 170, 175, 1) }
.hover-figmaColors-main:hover { color: rgba(57, 78, 255, 1) }
.hover-figmaColors-primary-outlined-hover-background:hover { color: rgba(62, 170, 175, 0.08) }
.hover-figmaColors-primary-outlined-resting-border:hover { color: rgba(62, 170, 175, 0.5) }
.hover-figmaColors-secondary-outlined-hover-background:hover { color: rgba(63, 81, 181, 0.08) }
.hover-figmaColors-secondary-outlined-resting-border:hover { color: rgba(63, 81, 181, 0.5) }
.hover-figmaColors-text-disabled:hover { color: rgba(0,0,0, 0.38) }
.hover-figmaColors-text-primary:hover { color: rgba(0,0,0, 0.87) }
.hover-figmaColors-outlined-border:hover { color: rgba(0,0,0, 0.23) }
.hover-figmaColors-divider:hover { color: rgba(0, 0, 0, 0.12) }
.hover-main:hover { color: var(--color-main) }
.hover-gray-light-shade:hover { color: var(--color-gray-light-shade) }
.hover-gray-lightest:hover { color: var(--color-gray-lightest) }
.hover-gray-lighter:hover { color: var(--color-gray-lighter) }
.hover-gray-light:hover { color: var(--color-gray-light) }
.hover-gray-bg:hover { color: var(--color-gray-bg) }
.hover-gray-medium:hover { color: var(--color-gray-medium) }
.hover-gray-dark:hover { color: var(--color-gray-dark) }
.hover-gray-darkest:hover { color: var(--color-gray-darkest) }
.hover-gray-light-blue:hover { color: var(--color-gray-light-blue) }
.hover-teal:hover { color: var(--color-teal) }
.hover-teal-dark:hover { color: var(--color-teal-dark) }
.hover-teal-light:hover { color: var(--color-teal-light) }
.hover-tealx:hover { color: var(--color-tealx) }
.hover-tealx-light:hover { color: var(--color-tealx-light) }
.hover-tealx-light-border:hover { color: var(--color-tealx-light-border) }
.hover-tealx-lightest:hover { color: var(--color-tealx-lightest) }
.hover-orange:hover { color: var(--color-orange) }
.hover-yellow:hover { color: var(--color-yellow) }
.hover-yellow2:hover { color: var(--color-yellow2) }
.hover-orange-dark:hover { color: var(--color-orange-dark) }
.hover-green:hover { color: var(--color-green) }
.hover-green2:hover { color: var(--color-green2) }
.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-lightest:hover { color: var(--color-red-lightest) }
.hover-blue:hover { color: var(--color-blue) }
.hover-blue2:hover { color: var(--color-blue2) }
.hover-active-blue:hover { color: var(--color-active-blue) }
.hover-active-dark-blue:hover { color: var(--color-active-dark-blue) }
.hover-bg-blue:hover { color: var(--color-bg-blue) }
.hover-active-blue-border:hover { color: var(--color-active-blue-border) }
.hover-pink:hover { color: var(--color-pink) }
.hover-light-blue-bg:hover { color: var(--color-light-blue-bg) }
.hover-white:hover { color: var(--color-white) }
.hover-black:hover { color: var(--color-black) }
.hover-gray-border:hover { color: var(--color-gray-border) }
.hover-borderColor-default:hover { color: var(--color-borderColor-default) }
.hover-borderColor-gray-light-shade:hover { color: var(--color-borderColor-gray-light-shade) }
.hover-borderColor-primary:hover { color: var(--color-borderColor-primary) }
.hover-borderColor-transparent:hover { color: var(--color-borderColor-transparent) }
.hover-transparent:hover { color: var(--color-transparent) }
.hover-cyan:hover { color: var(--color-cyan) }
.hover-amber:hover { color: var(--color-amber) }
.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) }
.hover-figmaColors-primary-outlined-resting-border:hover { color: var(--color-figmaColors-primary-outlined-resting-border) }
.hover-figmaColors-secondary-outlined-hover-background:hover { color: var(--color-figmaColors-secondary-outlined-hover-background) }
.hover-figmaColors-secondary-outlined-resting-border:hover { color: var(--color-figmaColors-secondary-outlined-resting-border) }
.hover-figmaColors-text-disabled:hover { color: var(--color-figmaColors-text-disabled) }
.hover-figmaColors-text-primary:hover { color: var(--color-figmaColors-text-primary) }
.hover-figmaColors-outlined-border:hover { color: var(--color-figmaColors-outlined-border) }
.hover-figmaColors-divider:hover { color: var(--color-figmaColors-divider) }
.border-main { border-color: #394EFF }
.border-gray-light-shade { border-color: #EEEEEE }
.border-gray-lightest { border-color: #f6f6f6 }
.border-gray-lighter { border-color: #f1f1f1 }
.border-gray-light { border-color: #ddd }
.border-gray-bg { border-color: #CCC }
.border-gray-medium { border-color: #888 }
.border-gray-dark { border-color: #666 }
.border-gray-darkest { border-color: #333 }
.border-gray-light-blue { border-color: #F8F8FA }
.border-teal { border-color: #394EFF }
.border-teal-dark { border-color: #2331A8 }
.border-teal-light { border-color: rgba(57, 78, 255, 0.1) }
.border-tealx { border-color: #3EAAAF }
.border-tealx-light { border-color: #E2F0EE }
.border-tealx-light-border { border-color: #C6DCDA }
.border-tealx-lightest { border-color: rgba(62, 170, 175, 0.1) }
.border-orange { border-color: #E28940 }
.border-yellow { border-color: #FFFBE5 }
.border-yellow2 { border-color: #F5A623 }
.border-orange-dark { border-color: #C26822 }
.border-green { border-color: #42AE5E }
.border-green2 { border-color: #00dc69 }
.border-green-dark { border-color: #2C9848 }
.border-red { border-color: #cc0000 }
.border-red2 { border-color: #F5A623 }
.border-red-lightest { border-color: rgba(204, 0, 0, 0.1) }
.border-blue { border-color: #366CD9 }
.border-blue2 { border-color: #0076FF }
.border-active-blue { border-color: #F6F7FF }
.border-active-dark-blue { border-color: #E2E4F6 }
.border-bg-blue { border-color: #e3e6ff }
.border-active-blue-border { border-color: #D0D4F2 }
.border-pink { border-color: #ffb9b9 }
.border-light-blue-bg { border-color: #E5F7F7 }
.border-white { border-color: #fff }
.border-borderColor-default { border-color: #DDDDDD }
.border-borderColor-gray-light-shade { border-color: #EEEEEE }
.border-borderColor-primary { border-color: #3490dc }
.border-borderColor-transparent { border-color: transparent }
.border-transparent { border-color: transparent }
.border-cyan { border-color: #EBF4F5 }
.border-figmaColors-accent-secondary { border-color: rgba(62, 170, 175, 1) }
.border-figmaColors-main { border-color: rgba(57, 78, 255, 1) }
.border-figmaColors-primary-outlined-hover-background { border-color: rgba(62, 170, 175, 0.08) }
.border-figmaColors-primary-outlined-resting-border { border-color: rgba(62, 170, 175, 0.5) }
.border-figmaColors-secondary-outlined-hover-background { border-color: rgba(63, 81, 181, 0.08) }
.border-figmaColors-secondary-outlined-resting-border { border-color: rgba(63, 81, 181, 0.5) }
.border-figmaColors-text-disabled { border-color: rgba(0,0,0, 0.38) }
.border-figmaColors-text-primary { border-color: rgba(0,0,0, 0.87) }
.border-figmaColors-outlined-border { border-color: rgba(0,0,0, 0.23) }
.border-figmaColors-divider { border-color: rgba(0, 0, 0, 0.12) }
/* border color */
.border-main { border-color: var(--color-main) }
.border-gray-light-shade { border-color: var(--color-gray-light-shade) }
.border-gray-lightest { border-color: var(--color-gray-lightest) }
.border-gray-lighter { border-color: var(--color-gray-lighter) }
.border-gray-light { border-color: var(--color-gray-light) }
.border-gray-bg { border-color: var(--color-gray-bg) }
.border-gray-medium { border-color: var(--color-gray-medium) }
.border-gray-dark { border-color: var(--color-gray-dark) }
.border-gray-darkest { border-color: var(--color-gray-darkest) }
.border-gray-light-blue { border-color: var(--color-gray-light-blue) }
.border-teal { border-color: var(--color-teal) }
.border-teal-dark { border-color: var(--color-teal-dark) }
.border-teal-light { border-color: var(--color-teal-light) }
.border-tealx { border-color: var(--color-tealx) }
.border-tealx-light { border-color: var(--color-tealx-light) }
.border-tealx-light-border { border-color: var(--color-tealx-light-border) }
.border-tealx-lightest { border-color: var(--color-tealx-lightest) }
.border-orange { border-color: var(--color-orange) }
.border-yellow { border-color: var(--color-yellow) }
.border-yellow2 { border-color: var(--color-yellow2) }
.border-orange-dark { border-color: var(--color-orange-dark) }
.border-green { border-color: var(--color-green) }
.border-green2 { border-color: var(--color-green2) }
.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-lightest { border-color: var(--color-red-lightest) }
.border-blue { border-color: var(--color-blue) }
.border-blue2 { border-color: var(--color-blue2) }
.border-active-blue { border-color: var(--color-active-blue) }
.border-active-dark-blue { border-color: var(--color-active-dark-blue) }
.border-bg-blue { border-color: var(--color-bg-blue) }
.border-active-blue-border { border-color: var(--color-active-blue-border) }
.border-pink { border-color: var(--color-pink) }
.border-light-blue-bg { border-color: var(--color-light-blue-bg) }
.border-white { border-color: var(--color-white) }
.border-black { border-color: var(--color-black) }
.border-gray-border { border-color: var(--color-gray-border) }
.border-borderColor-default { border-color: var(--color-borderColor-default) }
.border-borderColor-gray-light-shade { border-color: var(--color-borderColor-gray-light-shade) }
.border-borderColor-primary { border-color: var(--color-borderColor-primary) }
.border-borderColor-transparent { border-color: var(--color-borderColor-transparent) }
.border-transparent { border-color: var(--color-transparent) }
.border-cyan { border-color: var(--color-cyan) }
.border-amber { border-color: var(--color-amber) }
.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) }
.border-figmaColors-primary-outlined-resting-border { border-color: var(--color-figmaColors-primary-outlined-resting-border) }
.border-figmaColors-secondary-outlined-hover-background { border-color: var(--color-figmaColors-secondary-outlined-hover-background) }
.border-figmaColors-secondary-outlined-resting-border { border-color: var(--color-figmaColors-secondary-outlined-resting-border) }
.border-figmaColors-text-disabled { border-color: var(--color-figmaColors-text-disabled) }
.border-figmaColors-text-primary { border-color: var(--color-figmaColors-text-primary) }
.border-figmaColors-outlined-border { border-color: var(--color-figmaColors-outlined-border) }
.border-figmaColors-divider { border-color: var(--color-figmaColors-divider) }
/* background color */
.bg-main { background-color: var(--color-main) }
.bg-gray-light-shade { background-color: var(--color-gray-light-shade) }
.bg-gray-lightest { background-color: var(--color-gray-lightest) }
.bg-gray-lighter { background-color: var(--color-gray-lighter) }
.bg-gray-light { background-color: var(--color-gray-light) }
.bg-gray-bg { background-color: var(--color-gray-bg) }
.bg-gray-medium { background-color: var(--color-gray-medium) }
.bg-gray-dark { background-color: var(--color-gray-dark) }
.bg-gray-darkest { background-color: var(--color-gray-darkest) }
.bg-gray-light-blue { background-color: var(--color-gray-light-blue) }
.bg-teal { background-color: var(--color-teal) }
.bg-teal-dark { background-color: var(--color-teal-dark) }
.bg-teal-light { background-color: var(--color-teal-light) }
.bg-tealx { background-color: var(--color-tealx) }
.bg-tealx-light { background-color: var(--color-tealx-light) }
.bg-tealx-light-border { background-color: var(--color-tealx-light-border) }
.bg-tealx-lightest { background-color: var(--color-tealx-lightest) }
.bg-orange { background-color: var(--color-orange) }
.bg-yellow { background-color: var(--color-yellow) }
.bg-yellow2 { background-color: var(--color-yellow2) }
.bg-orange-dark { background-color: var(--color-orange-dark) }
.bg-green { background-color: var(--color-green) }
.bg-green2 { background-color: var(--color-green2) }
.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-lightest { background-color: var(--color-red-lightest) }
.bg-blue { background-color: var(--color-blue) }
.bg-blue2 { background-color: var(--color-blue2) }
.bg-active-blue { background-color: var(--color-active-blue) }
.bg-active-dark-blue { background-color: var(--color-active-dark-blue) }
.bg-bg-blue { background-color: var(--color-bg-blue) }
.bg-active-blue-border { background-color: var(--color-active-blue-border) }
.bg-pink { background-color: var(--color-pink) }
.bg-light-blue-bg { background-color: var(--color-light-blue-bg) }
.bg-white { background-color: var(--color-white) }
.bg-black { background-color: var(--color-black) }
.bg-gray-border { background-color: var(--color-gray-border) }
.bg-borderColor-default { background-color: var(--color-borderColor-default) }
.bg-borderColor-gray-light-shade { background-color: var(--color-borderColor-gray-light-shade) }
.bg-borderColor-primary { background-color: var(--color-borderColor-primary) }
.bg-borderColor-transparent { background-color: var(--color-borderColor-transparent) }
.bg-transparent { background-color: var(--color-transparent) }
.bg-cyan { background-color: var(--color-cyan) }
.bg-amber { background-color: var(--color-amber) }
.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) }
.bg-figmaColors-primary-outlined-resting-border { background-color: var(--color-figmaColors-primary-outlined-resting-border) }
.bg-figmaColors-secondary-outlined-hover-background { background-color: var(--color-figmaColors-secondary-outlined-hover-background) }
.bg-figmaColors-secondary-outlined-resting-border { background-color: var(--color-figmaColors-secondary-outlined-resting-border) }
.bg-figmaColors-text-disabled { background-color: var(--color-figmaColors-text-disabled) }
.bg-figmaColors-text-primary { background-color: var(--color-figmaColors-text-primary) }
.bg-figmaColors-outlined-border { background-color: var(--color-figmaColors-outlined-border) }
.bg-figmaColors-divider { background-color: var(--color-figmaColors-divider) }

View file

@ -173,7 +173,7 @@
}
.border-gray-light {
border: solid thin rgb(229 231 235 / var(--tw-text-opacity, 1))
border: solid thin var(--color-gray-light)
}
.btn-disabled {
@ -425,10 +425,6 @@ p {
background-color: #ffff;
}
.ant-menu-light .ant-menu-item-selected, :where(.css-dev-only-do-not-override).ant-menu-light>.ant-menu .ant-menu-item-selected{
background-color: #E6E9FA;
}
.pref-projects-menu .ant-menu-light .ant-menu-item-selected{
background-color: #F6F7FF;
color: rgba(0,0,0,.7);

View file

@ -14,7 +14,7 @@ input.no-focus:focus {
}
.widget-wrapper {
@apply rounded-lg shadow-sm border bg-white;
@apply rounded-lg shadow-sm border bg-white border-gray-light;
}
img {

View file

@ -6,110 +6,17 @@
}
* {
border-color: #eeeeee;
border-color: $gray-light;
}
.page {
padding-top: 50px;
}
.page-margin {
padding-top: 81px;
}
.container-fit {
margin: 0 30px 0px;
}
.container {
margin: 0 30px 30px;
}
@media only screen and (max-width: 1380px) {
.container-70 {
width: 90%;
}
}
@media only screen and (min-width: 1380px) {
.container-70 {
width: 1280px;
}
}
.container-70 {
position: relative;
margin: 0 auto;
}
.container-90 {
width: 98%;
margin: 0 auto;
}
.side-menu {
width: 250px;
height: calc(100vh - 80px);
overflow-y: auto;
padding-right: 20px;
position: fixed;
top: 81px;
&
::-webkit-scrollbar {
width: 0px;
}
&
:hover {
&
::-webkit-scrollbar {
width: 0px;
}
}
}
.side-menu-margined {
margin-left: 250px;
}
.top-header {
margin-bottom: 25px;
/* border: dashed thin gray; */
min-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
}
.page-title {
font-size: 22px;
margin-right: 15px;
&
> span {
font-weight: 300;
}
&
.title {
margin-right: 15px;
&
span {
color: $ gray-medium;
font-weight: 300;
}
}
}
.page-title-flex {
display: flex;
align-items: center;
}
[data-hidden='true'] {
display: none !important;
}
@ -135,15 +42,6 @@ label {
pointer-events: none;
}
.hover {
&
:hover {
background-color: $ active-blue;
}
}
.hover-teal:hover {
background-color: $ active-blue;
color: $ teal;
@ -155,36 +53,6 @@ svg {
}
.note-hover {
border: solid thin transparent;
&
:hover {
background-color: #FFFEF5;
border-color: $ gray-lightest;
}
}
.note-hover-bg {
&
:hover {
background-color: #FFFEF5;
}
}
.text-dotted-underline {
text-decoration: underline dotted !important;
}
.no-scroll {
height: 100vh;
overflow-y: hidden;
padding-right: 15px;
}
.json-view {
display: block;
color: #4d4d4d;
@ -416,3 +284,6 @@ svg {
cursor: grab;
}
.text-black {
color: $black;
}

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-dot"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-dot"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/></svg>

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 284 B

View file

@ -1,6 +1,5 @@
module.exports = {
main: '#394EFF',
main: 'oklch(54.6% 0.245 262.881)',
'gray-light-shade': '#EEEEEE',
'gray-lightest': '#f6f6f6',
'gray-lighter': '#f1f1f1',
@ -10,17 +9,13 @@ module.exports = {
'gray-dark': '#666',
'gray-darkest': '#333',
'gray-light-blue': '#F8F8FA',
teal: '#394EFF' /* blue */,
'teal-dark': '#2331A8' /* "blue-dark" */,
'teal-light': 'rgba(57, 78, 255, 0.1)' /* "blue-light" */,
teal: '#394EFF', /* blue */
'teal-dark': '#2331A8', /* "blue-dark" */
'teal-light': 'rgba(57, 78, 255, 0.1)', /* "blue-light" */
tealx: '#3EAAAF',
'tealx-light': '#E2F0EE',
'tealx-light-border': '#C6DCDA',
'tealx-lightest': 'rgba(62, 170, 175, 0.1)',
orange: '#E28940',
yellow: '#FFFBE5',
yellow2: '#F5A623',
@ -39,8 +34,9 @@ module.exports = {
'active-blue-border': '#D0D4F2',
pink: '#ffb9b9',
'light-blue-bg': '#E5F7F7',
white: '#fff',
black: 'black',
'gray-border': '#999', // Added for border-gray shadow
borderColor: {
default: '#DDDDDD',
'gray-light-shade': '#EEEEEE',
@ -49,8 +45,8 @@ module.exports = {
},
transparent: 'transparent',
cyan: '#EBF4F5',
amber: 'oklch(98.7% 0.022 95.277)',
// actual theme colors - use this for new components
figmaColors: {
'accent-secondary': 'rgba(62, 170, 175, 1)',
main: 'rgba(57, 78, 255, 1)',
@ -63,4 +59,49 @@ module.exports = {
'outlined-border': 'rgba(0,0,0, 0.23)',
divider: 'rgba(0, 0, 0, 0.12)',
},
dark: {
// used as background in multiple places
white: 'oklch(20.5% 0 0)',
black: '#fff',
teal: 'oklch(70.7% 0.165 254.624)',
main: 'oklch(70.7% 0.165 254.624)',
'text-primary': 'oklch(97% 0 0)',
'text-disabled': 'rgba(255, 255, 255, 0.38)',
'outlined-border': 'rgba(255, 255, 255, 0.23)',
divider: 'rgba(255, 255, 255, 0.12)',
'background': 'oklch(20.5% 0 0)',
'surface': '#1E1E1E',
amber: 'oklch(41.4% 0.112 45.904)',
'gray-light-shade': 'oklch(37.1% 0 0)',
'gray-lightest': 'oklch(26.9% 0 0)',
'gray-lighter': 'oklch(29.9% 0 0)',
'gray-light': 'oklch(37.1% 0 0)',
'gray-bg': 'oklch(37.1% 0 0)',
'gray-medium': 'oklch(70.7% 0.022 261.325)',
'gray-dark': 'oklch(87% 0 0)',
'gray-darkest': 'oklch(97% 0 0)',
'gray-light-blue': 'oklch(55.4% 0.046 257.417)',
'gray-border': '#888',
'active-blue': 'oklch(43.2% 0.232 292.759)',
'active-dark-blue': 'oklch(43.2% 0.232 292.759)',
'bg-blue': 'oklch(35.9% 0.144 278.697)',
'active-blue-border': 'oklch(45.7% 0.24 277.023)',
'tealx': 'oklch(77.7% 0.152 181.912)',
'tealx-light': 'oklch(38.6% 0.063 188.416)',
'tealx-light-border': 'oklch(43.7% 0.078 188.216)',
'light-blue-bg': 'oklch(39.8% 0.07 227.392)',
'disabled-text': 'rgba(255, 255, 255, 0.38)',
figmaColors: {
'accent-secondary': 'rgba(82, 190, 195, 1)',
'text-disabled': 'rgba(255, 255, 255, 0.38)',
'text-primary': 'rgba(255, 255, 255, 0.87)',
'outlined-border': 'rgba(255, 255, 255, 0.23)',
divider: 'rgba(255, 255, 255, 0.12)',
}
}
};

View file

@ -2,14 +2,39 @@ const path = require('path');
const colors = require('./app/theme/colors');
const cssnanoOptions = { zindex: false };
const transformColorsToCssVars = (colorsObj) => {
const result = {};
for (const [key, value] of Object.entries(colorsObj)) {
if (typeof value === 'object' && value !== null && key !== 'dark') {
// Handle nested objects
const transformedNested = {};
for (const [nestedKey, nestedValue] of Object.entries(value)) {
// Create CSS variable reference for nested values
transformedNested[nestedKey] = `var(--color-${key}-${nestedKey})`;
}
result[key] = transformedNested;
} else if (key !== 'dark') {
// Create CSS variable reference for direct values
result[key] = `var(--color-${key})`;
}
}
return result;
};
const cssVarColors = transformColorsToCssVars(colors);
module.exports = ({ file, options, env }) => ({
// parser: 'sugarss', // syntax check ?
// parser: 'sugarss', // syntax check ?
plugins: {
'postcss-import': {
path: path.join(__dirname, 'app/styles/import')
},
'postcss-mixins': {},
'postcss-simple-vars': { variables: colors },
'postcss-simple-vars': {
variables: cssVarColors
},
'postcss-nesting': {},
// 'postcss-inline-svg': {
// path: path.join(__dirname, 'app/svg'),
@ -20,4 +45,4 @@ module.exports = ({ file, options, env }) => ({
//'postcss-preset-env': {}, //includes autoprefixer
cssnano: env === 'production' ? cssnanoOptions : false,
}
});
});

View file

@ -1,38 +1,40 @@
const fs = require('fs');
const colors = require('../app/theme/colors.js');
// Helper function to flatten the nested color objects
const flattenColors = (colors) => {
let flatColors = {};
for (const [key, value] of Object.entries(colors)) {
if (typeof value === 'object') {
if (typeof value === 'object' && value !== null && key !== 'dark') {
for (const [nestedKey, nestedValue] of Object.entries(value)) {
flatColors[`${key}-${nestedKey}`] = nestedValue;
flatColors[`${key}-${nestedKey}`] = `${key}-${nestedKey}`;
}
} else {
flatColors[key] = value;
} else if (key !== 'dark') {
flatColors[key] = key;
}
}
return flatColors;
};
const flatColors = flattenColors(colors);
const generatedCSS = `/* Auto-generated, DO NOT EDIT */
/* Uses CSS variables (--color-*) generated by Tailwind config */
/* fill */
${ Object.entries(flatColors).map(([name, value]) => `.fill-${ name.replace(/ /g, '-') } { fill: ${ value } }`).join('\n') }
${ Object.entries(flatColors).map(([name, value]) => `.hover-fill-${ name.replace(/ /g, '-') }:hover svg { fill: ${ value } }`).join('\n') }
${Object.entries(flatColors).map(([name, key]) => `.fill-${name.replace(/ /g, '-')} { fill: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
${Object.entries(flatColors).map(([name, key]) => `.hover-fill-${name.replace(/ /g, '-')}:hover svg { fill: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
/* color */
${ Object.entries(flatColors).map(([name, value]) => `.color-${ name.replace(/ /g, '-') } { color: ${ value } }`).join('\n') }
${Object.entries(flatColors).map(([name, key]) => `.color-${name.replace(/ /g, '-')} { color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
/* hover color */
${ Object.entries(flatColors).map(([name, value]) => `.hover-${ name.replace(/ /g, '-') }:hover { color: ${ value } }`).join('\n') }
${Object.entries(flatColors).map(([name, key]) => `.hover-${name.replace(/ /g, '-')}:hover { color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
${ Object.entries(flatColors).map(([name, value]) => `.border-${ name.replace(/ /g, '-') } { border-color: ${ value } }`).join('\n') }
/* border color */
${Object.entries(flatColors).map(([name, key]) => `.border-${name.replace(/ /g, '-')} { border-color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
/* background color */
${Object.entries(flatColors).map(([name, key]) => `.bg-${name.replace(/ /g, '-')} { background-color: var(--color-${key.replace(/ /g, '-')}) }`).join('\n')}
`;
// Write the generated CSS to a file

View file

@ -1,29 +1,64 @@
const colors = require('./app/theme/colors');
const defaultColors = require('tailwindcss/colors');
const plugin = require('tailwindcss/plugin');
const deprecatedDefaults = ['lightBlue', 'warmGray', 'trueGray', 'coolGray', 'blueGray']
deprecatedDefaults.forEach(color => {
delete defaultColors[color]
})
const deprecatedDefaults = [
'lightBlue',
'warmGray',
'trueGray',
'coolGray',
'blueGray',
];
deprecatedDefaults.forEach((color) => {
delete defaultColors[color];
});
const cssVar = (name) => `var(--${name})`;
function createColorVariables(colors, darkColors) {
const result = {};
// Process all colors
Object.entries(colors).forEach(([key, value]) => {
// Skip nested objects for now (we'll handle them separately)
if (typeof value !== 'object' || value === null) {
result[key] = cssVar(`color-${key}`);
}
});
// Handle nested color objects
Object.entries(colors).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null && key !== 'dark') {
result[key] = {};
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
result[key][nestedKey] = cssVar(`color-${key}-${nestedKey}`);
});
}
});
return result;
}
const variableBasedColors = createColorVariables(colors);
module.exports = {
mode: 'jit',
darkMode: 'class',
content: ['./app/**/*.tsx', './app/**/*.js'],
theme: {
// Use variable references instead of hard-coded colors
colors: {
...defaultColors,
...colors,
...variableBasedColors,
},
extend: {
keyframes: {
'fade-in': {
'0%': {
opacity: '0',
// transform: 'translateY(-10px)'
},
'100%': {
opacity: '1',
// transform: 'translateY(0)'
},
},
'bg-spin': {
@ -43,12 +78,12 @@ module.exports = {
'bg-spin': 'bg-spin 1s ease infinite',
},
colors: {
'disabled-text': 'rgba(0,0,0, 0.38)',
'disabled-text': cssVar('color-disabled-text'),
},
boxShadow: {
'border-blue': `0 0 0 1px ${colors['active-blue-border']}`,
'border-main': `0 0 0 1px ${colors['main']}`,
'border-gray': '0 0 0 1px #999',
'border-blue': `0 0 0 1px ${cssVar('color-active-blue-border')}`,
'border-main': `0 0 0 1px ${cssVar('color-main')}`,
'border-gray': `0 0 0 1px ${cssVar('color-gray-border')}`,
},
button: {
'background-color': 'red',
@ -58,7 +93,59 @@ module.exports = {
variants: {
visibility: ['responsive', 'hover', 'focus', 'group-hover'],
},
plugins: [],
plugins: [
plugin(function ({ addBase }) {
const lightModeVars = {};
Object.entries(colors).forEach(([key, value]) => {
if (typeof value !== 'object' || value === null || key === 'dark') {
lightModeVars[`--color-${key}`] = value;
}
});
Object.entries(colors).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null && key !== 'dark') {
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
lightModeVars[`--color-${key}-${nestedKey}`] = nestedValue;
});
}
});
const darkModeVars = {};
if (colors.dark) {
// Process flat dark colors
Object.entries(colors.dark).forEach(([key, value]) => {
if (typeof value !== 'object') {
// Find the corresponding light mode key
const lightKey = key.replace('dark-', '');
darkModeVars[`--color-${lightKey}`] = value;
}
});
Object.entries(colors.dark).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
darkModeVars[`--color-${key}-${nestedKey}`] = nestedValue;
});
}
});
if (colors['gray-light'] && colors.dark['gray-light']) {
darkModeVars['--color-gray-light'] = colors.dark['gray-light'];
}
if (colors['gray-dark'] && colors.dark['gray-dark']) {
darkModeVars['--color-gray-dark'] = colors.dark['gray-dark'];
}
darkModeVars['--color-disabled-text'] =
colors.dark['text-disabled'] || 'rgba(255, 255, 255, 0.38)';
}
addBase({
':root': lightModeVars,
'.dark': darkModeVars,
});
}),
],
corePlugins: {
preflight: false,
},

View file

@ -22,6 +22,11 @@ message 1, 'SessionStart', :tracker => false, :replayer => false do
string 'UserID'
end
# DEPRECATED; backend only (TODO: remove in the next release)
message 3, 'SessionEndDeprecated', :tracker => false, :replayer => false do
uint 'Timestamp'
end
# DEPRECATED since 14.0.0 -> goto 122
message 4, 'SetPageLocationDeprecated' do
string 'URL'
@ -129,6 +134,12 @@ message 24, 'PageRenderTiming', :replayer => false do
uint 'VisuallyComplete'
uint 'TimeToInteractive'
end
# DEPRECATED since 4.1.6 / 1.8.2 in favor of #78
message 25, 'JSExceptionDeprecated', :replayer => false, :tracker => false do
string 'Name'
string 'Message'
string 'Payload'
end
message 26, 'IntegrationEvent', :tracker => false, :replayer => false do
uint 'Timestamp'
string 'Source'
@ -208,6 +219,29 @@ message 35, 'SetNodeAttributeDictGlobal' do
uint 'Name'
uint 'Value'
end
# DEPRECATED since 4.0.2 in favor of AdoptedSSInsertRule + AdoptedSSAddOwner
message 37, 'CSSInsertRule' do
uint 'ID'
string 'Rule'
uint 'Index'
end
# DEPRECATED since 4.0.2
message 38, 'CSSDeleteRule' do
uint 'ID'
uint 'Index'
end
# DEPRECATED since 4.1.10 in favor of NetworkRequest
message 39, 'Fetch', :replayer => :devtools do
string 'Method'
string 'URL'
string 'Request'
string 'Response'
uint 'Status'
uint 'Timestamp'
uint 'Duration'
end
message 40, 'Profiler', :replayer => :devtools do
string 'Name'
uint 'Duration'
@ -308,8 +342,9 @@ message 56, 'PerformanceTrackAggr', :tracker => false, :replayer => false do
uint 'AvgUsedJSHeapSize'
uint 'MaxUsedJSHeapSize'
end
# Since 4.1.7 / 1.9.0
message 57, 'LoadFontFace' do
message 57, 'LoadFontFace' do
uint 'ParentID'
string 'Family'
string 'Source'
@ -319,6 +354,17 @@ end
message 58, 'SetNodeFocus' do
int 'ID'
end
#DEPRECATED (since 3.0.?)
message 59, 'LongTask' do
uint 'Timestamp'
uint 'Duration'
uint 'Context'
uint 'ContainerType'
string 'ContainerSrc'
string 'ContainerId'
string 'ContainerName'
end
message 60, 'SetNodeAttributeURLBased' do
uint 'ID'
string 'Name'
@ -331,6 +377,15 @@ message 61, 'SetCSSDataURLBased' do
string 'Data'
string 'BaseURL'
end
# DEPRECATED; backend only (TODO: remove in the next release)
message 62, 'IssueEventDeprecated', :replayer => false, :tracker => false do
uint 'MessageID'
uint 'Timestamp'
string 'Type'
string 'ContextString'
string 'Context'
string 'Payload'
end
message 63, 'TechnicalInfo', :replayer => false do
string 'Type'
string 'Value'
@ -343,6 +398,13 @@ end
message 66, 'AssetCache', :replayer => false, :tracker => false do
string 'URL'
end
message 67, 'CSSInsertRuleURLBased' do
uint 'ID'
string 'Rule'
uint 'Index'
string 'BaseURL'
end
message 68, 'MouseClick' do
uint 'ID'
uint 'HesitationTime'
@ -351,6 +413,7 @@ message 68, 'MouseClick' do
uint 'NormalizedX'
uint 'NormalizedY'
end
message 69, 'MouseClickDeprecated' do
uint 'ID'
uint 'HesitationTime'
@ -414,6 +477,13 @@ end
# Special one for Batch Metadata. Message id could define the version
# DEPRECATED since tracker 3.6.0 in favor of BatchMetadata
message 80, 'BatchMeta', :replayer => false, :tracker => false do
uint 'PageNo'
uint 'FirstIndex'
int 'Timestamp'
end
# since tracker 3.6.0 TODO: for webworker only
message 81, 'BatchMetadata', :replayer => false do
uint 'Version'

View file

@ -22,7 +22,7 @@ usr=$(whoami)
# Installing k3s
function install_k8s() {
echo "nameserver 1.1.1.1" | sudo tee /etc/k3s-resolv.conf
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.31.5+k3s1' INSTALL_K3S_EXEC="--disable=traefik server --resolv-conf=/etc/k3s-resolv.conf" sh -
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" K3S_RESOLV_CONF="/etc/k3s-resolv.conf" INSTALL_K3S_VERSION='v1.31.5+k3s1' INSTALL_K3S_EXEC="--disable=traefik" sh -
[[ -d ~/.kube ]] || mkdir ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chmod 0644 ~/.kube/config

View file

@ -9,68 +9,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, session_id)
TTL _timestamp + INTERVAL 3 MONTH;
SET allow_experimental_json_type = 1;
SET enable_json_type = 1;
ALTER TABLE product_analytics.events
MODIFY COLUMN `$properties` JSON(
max_dynamic_paths=0,
label String ,
hesitation_time UInt32 ,
name String ,
payload String ,
level Enum8 ('info'=0, 'error'=1),
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
message String ,
error_id String ,
duration UInt16,
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
url_host String ,
url_path String ,
url_hostpath String ,
request_start UInt16 ,
response_start UInt16 ,
response_end UInt16 ,
dom_content_loaded_event_start UInt16 ,
dom_content_loaded_event_end UInt16 ,
load_event_start UInt16 ,
load_event_end UInt16 ,
first_paint UInt16 ,
first_contentful_paint_time UInt16 ,
speed_index UInt16 ,
visually_complete UInt16 ,
time_to_interactive UInt16,
ttfb UInt16,
ttlb UInt16,
response_time UInt16,
dom_building_time UInt16,
dom_content_loaded_event_time UInt16,
load_event_time UInt16,
min_fps UInt8,
avg_fps UInt8,
max_fps UInt8,
min_cpu UInt8,
avg_cpu UInt8,
max_cpu UInt8,
min_total_js_heap_size UInt64,
avg_total_js_heap_size UInt64,
max_total_js_heap_size UInt64,
min_used_js_heap_size UInt64,
avg_used_js_heap_size UInt64,
max_used_js_heap_size UInt64,
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
status UInt16,
success UInt8,
request_body String,
response_body String,
transfer_size UInt32,
selector String,
normalized_x Float32,
normalized_y Float32,
message_id UInt64
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events';
ORDER BY (project_id, user_id, session_id);
DROP TABLE IF EXISTS product_analytics.all_events;
CREATE TABLE IF NOT EXISTS product_analytics.all_events
@ -216,7 +155,8 @@ CREATE TABLE IF NOT EXISTS product_analytics.property_values_samples
ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, property_name, is_event_property);
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.property_values_sampler_mvREFRESHEVERY30HOURTOproduct_analytics.property_values_samples AS
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.property_values_sampler_mv
REFRESH EVERY 30 HOUR TO product_analytics.property_values_samples AS
SELECT project_id,
property_name,
TRUE AS is_event_property,
@ -236,3 +176,93 @@ FROM product_analytics.events
WHERE randCanonical() < 0.5 -- This randomly skips inserts
AND value != ''
LIMIT 2 BY project_id,property_name;
-- Autocomplete
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
(
project_id UInt16,
value String COMMENT 'The $event_name',
_timestamp DateTime
) ENGINE = MergeTree()
ORDER BY (project_id, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
TO product_analytics.autocomplete_events AS
SELECT project_id,
`$event_name` AS value,
_timestamp
FROM product_analytics.events
WHERE _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
(
project_id UInt16,
value String COMMENT 'The $event_name',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
SELECT project_id,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_events
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, value;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
_timestamp DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY (project_id, event_name, property_name, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
TO product_analytics.autocomplete_event_properties AS
SELECT project_id,
`$event_name` AS event_name,
property_name,
JSONExtractString(toString(`$properties`), property_name) AS value,
_timestamp
FROM product_analytics.events
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
AND _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, event_name, property_name, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
SELECT project_id,
event_name,
property_name,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_event_properties
WHERE length(value) > 0
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, event_name, property_name, value;

View file

@ -149,8 +149,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
sign Int8
) ENGINE = CollapsingMergeTree(sign)
PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, session_id)
TTL _timestamp + INTERVAL 3 MONTH;
ORDER BY (project_id, user_id, session_id);
CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
(
@ -160,8 +159,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, session_id)
TTL _timestamp + INTERVAL 3 MONTH;
ORDER BY (project_id, user_id, session_id);
CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
(
@ -171,8 +169,7 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, error_id)
TTL _timestamp + INTERVAL 3 MONTH;
ORDER BY (project_id, user_id, error_id);
CREATE TABLE IF NOT EXISTS experimental.issues
(
@ -330,62 +327,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
"$source" LowCardinality(String) DEFAULT '' COMMENT 'the name of the integration that sent the event',
"$duration_s" UInt16 DEFAULT 0 COMMENT 'the duration from session-start in seconds',
properties JSON DEFAULT '{}',
"$properties" JSON(
max_dynamic_paths=0,
label String ,
hesitation_time UInt32 ,
name String ,
payload String ,
level Enum8 ('info'=0, 'error'=1),
source Enum8 ('js_exception'=0, 'bugsnag'=1, 'cloudwatch'=2, 'datadog'=3, 'elasticsearch'=4, 'newrelic'=5, 'rollbar'=6, 'sentry'=7, 'stackdriver'=8, 'sumologic'=9),
message String ,
error_id String ,
duration UInt16,
context Enum8('unknown'=0, 'self'=1, 'same-origin-ancestor'=2, 'same-origin-descendant'=3, 'same-origin'=4, 'cross-origin-ancestor'=5, 'cross-origin-descendant'=6, 'cross-origin-unreachable'=7, 'multiple-contexts'=8),
url_host String ,
url_path String ,
url_hostpath String ,
request_start UInt16 ,
response_start UInt16 ,
response_end UInt16 ,
dom_content_loaded_event_start UInt16 ,
dom_content_loaded_event_end UInt16 ,
load_event_start UInt16 ,
load_event_end UInt16 ,
first_paint UInt16 ,
first_contentful_paint_time UInt16 ,
speed_index UInt16 ,
visually_complete UInt16 ,
time_to_interactive UInt16,
ttfb UInt16,
ttlb UInt16,
response_time UInt16,
dom_building_time UInt16,
dom_content_loaded_event_time UInt16,
load_event_time UInt16,
min_fps UInt8,
avg_fps UInt8,
max_fps UInt8,
min_cpu UInt8,
avg_cpu UInt8,
max_cpu UInt8,
min_total_js_heap_size UInt64,
avg_total_js_heap_size UInt64,
max_total_js_heap_size UInt64,
min_used_js_heap_size UInt64,
avg_used_js_heap_size UInt64,
max_used_js_heap_size UInt64,
method Enum8('GET' = 0, 'HEAD' = 1, 'POST' = 2, 'PUT' = 3, 'DELETE' = 4, 'CONNECT' = 5, 'OPTIONS' = 6, 'TRACE' = 7, 'PATCH' = 8),
status UInt16,
success UInt8,
request_body String,
response_body String,
transfer_size UInt32,
selector String,
normalized_x Float32,
normalized_y Float32,
message_id UInt64
) DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
"$properties" JSON DEFAULT '{}' COMMENT 'these properties belongs to the auto-captured events',
description String DEFAULT '',
group_id1 Array(String) DEFAULT [],
group_id2 Array(String) DEFAULT [],
@ -767,3 +709,92 @@ FROM product_analytics.events
WHERE randCanonical() < 0.5 -- This randomly skips inserts
AND value != ''
LIMIT 2 BY project_id,property_name;
-- Autocomplete
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events
(
project_id UInt16,
value String COMMENT 'The $event_name',
_timestamp DateTime
) ENGINE = MergeTree()
ORDER BY (project_id, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_mv
TO product_analytics.autocomplete_events AS
SELECT project_id,
`$event_name` AS value,
_timestamp
FROM product_analytics.events
WHERE _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_events_grouped
(
project_id UInt16,
value String COMMENT 'The $event_name',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_events_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_events_grouped AS
SELECT project_id,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_events
WHERE autocomplete_events._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, value;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
_timestamp DateTime DEFAULT now()
) ENGINE = MergeTree()
ORDER BY (project_id, event_name, property_name, value, _timestamp)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_mv
TO product_analytics.autocomplete_event_properties AS
SELECT project_id,
`$event_name` AS event_name,
property_name,
JSONExtractString(toString(`$properties`), property_name) AS value,
_timestamp
FROM product_analytics.events
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
AND _timestamp > now() - INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped
(
project_id UInt16,
event_name String COMMENT 'The $event_name',
property_name String,
value String COMMENT 'The property-value as a string',
data_count UInt16 COMMENT 'The number of appearance during the past month',
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, event_name, property_name, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.autocomplete_event_properties_grouped_mv
REFRESH EVERY 30 MINUTE TO product_analytics.autocomplete_event_properties_grouped AS
SELECT project_id,
event_name,
property_name,
value,
count(1) AS data_count,
max(_timestamp) AS _timestamp
FROM product_analytics.autocomplete_event_properties
WHERE length(value) > 0
AND autocomplete_event_properties._timestamp > now() - INTERVAL 1 MONTH
GROUP BY project_id, event_name, property_name, value;

View file

@ -1,165 +1,49 @@
import orLogo from "~/assets/orSpot.svg";
import micOff from "~/assets/mic-off-red.svg";
import micOn from "~/assets/mic-on-dark.svg";
import { createEffect, onMount } from "solid-js";
import Login from "~/entrypoints/popup/Login";
import Settings from "~/entrypoints/popup/Settings";
import { createSignal, createEffect, onMount } from "solid-js";
import Dropdown from "~/entrypoints/popup/Dropdown";
import Button from "~/entrypoints/popup/Button";
import {
ChevronSvg,
RecordDesktopSvg,
RecordTabSvg,
HomePageSvg,
SlackSvg,
SettingsSvg,
} from "./Icons";
async function getAudioDevices() {
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
const devices = await navigator.mediaDevices.enumerateDevices();
const audioDevices = devices
.filter((device) => device.kind === "audioinput")
.map((device) => ({ label: device.label, id: device.deviceId }));
return { granted: true, audioDevices };
} catch (error) {
console.error("Error accessing audio devices:", error);
const msg = error.message ?? "";
return {
granted: false,
denied: msg.includes("denied"),
audioDevices: [],
};
}
}
const orSite = () => {
window.open("https://openreplay.com", "_blank");
};
function Header({ openSettings }: { openSettings: () => void }) {
const openHomePage = async () => {
const { settings } = await chrome.storage.local.get("settings");
return window.open(`${settings.ingestPoint}/spots`, "_blank");
};
return (
<div class={"flex items-center gap-1"}>
<div
class="flex items-center gap-1 cursor-pointer hover:opacity-50"
onClick={orSite}
>
<img src={orLogo} class="w-5" alt={"OpenReplay Spot"} />
<div class={"text-neutral-600"}>
<span class={"text-lg font-semibold text-black"}>OpenReplay Spot</span>
</div>
</div>
<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"}>
<HomePageSvg />
</div>
</div>
</div>
<div
class="text-sm tooltip tooltip-bottom"
data-tip="Get help on Slack"
>
<a
href={
"https://join.slack.com/t/openreplay/shared_invite/zt-2brqlwcis-k7OtqHkW53EAoTRqPjCmyg"
}
target={"_blank"}
>
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
<SlackSvg />
</div>
</a>
</div>
<div
class="text-sm tooltip tooltip-bottom"
data-tip="Settings"
onClick={openSettings}
>
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
<SettingsSvg />
</div>
</div>
</div>
</div>
);
}
const STATE = {
empty: "empty",
login: "login",
ready: "ready",
starting: "starting",
recording: "recording",
};
import Header from "./components/Header";
import RecordingControls from "./components/RecordingControls";
import AudioPicker from "./components/AudioPicker";
import { useAppState } from "./hooks/useAppState";
import { useAudioDevices } from "./hooks/useAudioDevices";
import { AppState } from "./types";
function App() {
const [state, setState] = createSignal(STATE.empty);
const [isSettingsOpen, setIsSettingsOpen] = createSignal(false);
const [mic, setMic] = createSignal(false);
const [selectedAudioDevice, setSelectedAudioDevice] = createSignal("");
const [hasPermissions, setHasPermissions] = createSignal(false);
const {
state,
isSettingsOpen,
startRecording,
stopRecording,
openSettings,
closeSettings,
} = useAppState();
const {
audioDevices,
selectedAudioDevice,
mic,
hasPermissions,
isChecking,
checkAudioDevices,
handleMicToggle,
selectAudioDevice,
} = useAudioDevices();
// Listen for mic status updates from background
onMount(() => {
browser.runtime.onMessage.addListener((message) => {
if (message.type === "popup:no-login") {
setState(STATE.login);
}
if (message.type === "popup:login") {
setState(STATE.ready);
}
if (message.type === "popup:stopped") {
setState(STATE.ready);
}
if (message.type === "popup:started") {
setState(STATE.recording);
}
if (message.type === "popup:mic-status") {
setMic(message.status);
}
});
void browser.runtime.sendMessage({ type: "popup:check-status" });
});
const startRecording = async (reqTab: "tab" | "desktop") => {
setState(STATE.starting);
await browser.runtime.sendMessage({
type: "popup:start",
area: reqTab,
mic: mic(),
audioId: selectedAudioDevice(),
permissions: hasPermissions(),
});
window.close();
const handleStartRecording = (area: "tab" | "desktop") => {
startRecording(area, mic(), selectedAudioDevice(), hasPermissions());
};
const stopRecording = () => {
void browser.runtime.sendMessage({
type: "popup:stop",
mic: mic(),
audioId: selectedAudioDevice(),
});
};
const toggleMic = async () => {
setMic(!mic());
};
const openSettings = () => {
setIsSettingsOpen(true);
};
const closeSettings = () => {
setIsSettingsOpen(false);
const handleStopRecording = () => {
stopRecording(mic(), selectedAudioDevice());
};
return (
@ -167,58 +51,30 @@ function App() {
{isSettingsOpen() ? (
<Settings goBack={closeSettings} />
) : (
<div class={"flex flex-col gap-4 p-5"}>
<div class="flex flex-col gap-4 p-5">
<Header openSettings={openSettings} />
{state() === STATE.login ? (
{state() === AppState.LOGIN ? (
<Login />
) : (
<>
{state() === STATE.recording ? (
<Button
name={"End Recording"}
onClick={() => stopRecording()}
/>
) : null}
{state() === STATE.starting ? (
<div
class={
"flex flex-row items-center gap-2 w-full justify-center"
}
>
<div class="py-4">Your recording is starting</div>
</div>
) : null}
{state() === STATE.ready ? (
<>
<div class="flex flex-row items-center gap-2 w-full justify-center">
<button
class="btn bg-indigo-100 text-base hover:bg-primary hover:text-white w-6/12"
name="Record Tab"
onClick={() => startRecording("tab")}
>
<RecordTabSvg />
Record Tab
</button>
<RecordingControls
state={state()}
startRecording={handleStartRecording}
stopRecording={handleStopRecording}
/>
<button
class="btn bg-teal-50 text-base hover:bg-primary hover:text-white"
name={"Record Desktop"}
onClick={() => startRecording("desktop")}
>
<RecordDesktopSvg />
Record Desktop
</button>
</div>
<AudioPicker
mic={mic}
toggleMic={toggleMic}
selectedAudioDevice={selectedAudioDevice}
setSelectedAudioDevice={setSelectedAudioDevice}
setHasPermissions={setHasPermissions}
/>
</>
) : null}
{state() === AppState.READY && (
<AudioPicker
mic={mic}
audioDevices={audioDevices}
selectedAudioDevice={selectedAudioDevice}
isChecking={isChecking}
onMicToggle={handleMicToggle}
onCheckAudio={checkAudioDevices}
onSelectDevice={selectAudioDevice}
/>
)}
</>
)}
</div>
@ -227,111 +83,4 @@ function App() {
);
}
interface IAudioPicker {
mic: () => boolean;
toggleMic: () => void;
selectedAudioDevice: () => string;
setSelectedAudioDevice: (value: string) => void;
setHasPermissions: (value: boolean) => void;
}
function AudioPicker(props: IAudioPicker) {
const [audioDevices, setAudioDevices] = createSignal(
[] as { label: string; id: string }[],
);
const [checkedAudioDevices, setCheckedAudioDevices] = createSignal(0);
createEffect(() => {
chrome.storage.local.get("audioPerm", (data) => {
if (data.audioPerm && audioDevices().length === 0) {
props.setHasPermissions(true);
void checkAudioDevices();
}
});
});
const checkAudioDevices = async () => {
const { granted, audioDevices, denied } = await getAudioDevices();
if (!granted && !denied) {
void browser.runtime.sendMessage({
type: "popup:get-audio-perm",
});
browser.runtime.onMessage.addListener((message) => {
if (message.type === "popup:audio-perm") {
void checkAudioDevices();
}
});
} else if (audioDevices.length > 0) {
chrome.storage.local.set({ audioPerm: granted });
setAudioDevices(audioDevices);
props.setSelectedAudioDevice(audioDevices[0]?.id || "");
}
};
const checkAudio = async () => {
if (checkedAudioDevices() > 0) {
return;
}
setCheckedAudioDevices(1);
await checkAudioDevices();
setCheckedAudioDevices(2);
};
const onSelect = (value) => {
props.setSelectedAudioDevice(value);
if (!props.mic()) {
props.toggleMic();
}
};
const onMicToggle = async () => {
if (!audioDevices().length) {
return await checkAudioDevices();
}
if (!props.selectedAudioDevice() && audioDevices().length) {
onSelect(audioDevices()[0].id);
} else {
props.toggleMic();
}
};
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"
}
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
onClick={onMicToggle}
>
<img
src={props.mic() ? micOn : micOff}
alt={props.mic() ? "microphone on" : "microphone off"}
width={16}
height={16}
/>
</div>
<div
class={
"flex items-center gap-1 btn btn-xs btn-ghost hover:bg-neutral/20 rounded-lg pointer-events-auto"
}
onClick={checkAudio}
>
{audioDevices().length === 0 ? (
<div class="max-w-64 block leading-tight cursor-pointer whitespace-nowrap overflow-hidden font-normal">
{checkedAudioDevices() === 1
? "Loading audio devices"
: "Grant microphone access"}
</div>
) : (
<Dropdown
options={audioDevices()}
selected={props.selectedAudioDevice()}
onChange={onSelect}
/>
)}
<ChevronSvg />
</div>
</div>
);
}
export default App;

View file

@ -0,0 +1,57 @@
import { Component, For } from "solid-js";
import micOff from "~/assets/mic-off-red.svg";
import micOn from "~/assets/mic-on-dark.svg";
import Dropdown from "~/entrypoints/popup/Dropdown";
import { ChevronSvg } from "../Icons";
import { AudioDevice } from "../types";
interface AudioPickerProps {
mic: () => boolean;
audioDevices: () => AudioDevice[];
selectedAudioDevice: () => string;
isChecking: () => boolean;
onMicToggle: () => void;
onCheckAudio: () => void;
onSelectDevice: (deviceId: string) => void;
}
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"
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
onClick={props.onMicToggle}
>
<img
src={props.mic() ? micOn : micOff}
alt={props.mic() ? "microphone on" : "microphone off"}
width={16}
height={16}
/>
</div>
<div
class="flex items-center gap-1 btn btn-xs btn-ghost hover:bg-neutral/20 rounded-lg pointer-events-auto"
onClick={props.onCheckAudio}
>
{props.audioDevices().length === 0 ? (
<div class="max-w-64 block leading-tight cursor-pointer whitespace-nowrap overflow-hidden font-normal">
{props.isChecking()
? "Loading audio devices"
: "Grant microphone access"}
</div>
) : (
<Dropdown
options={props.audioDevices()}
selected={props.selectedAudioDevice()}
onChange={props.onSelectDevice}
/>
)}
<ChevronSvg />
</div>
</div>
);
};
export default AudioPicker;

View file

@ -0,0 +1,73 @@
import { Component } from "solid-js";
import orLogo from "~/assets/orSpot.svg";
import {
HomePageSvg,
SlackSvg,
SettingsSvg,
} from "../Icons";
interface HeaderProps {
openSettings: () => void;
}
const Header: Component<HeaderProps> = (props) => {
const openHomePage = async () => {
const { settings } = await chrome.storage.local.get("settings");
return window.open(`${settings.ingestPoint}/spots`, "_blank");
};
const openOrSite = () => {
window.open("https://openreplay.com", "_blank");
};
return (
<div class="flex items-center gap-1">
<div
class="flex items-center gap-1 cursor-pointer hover:opacity-50"
onClick={openOrSite}
>
<img src={orLogo} class="w-5" alt="OpenReplay Spot" />
<div class="text-neutral-600">
<span class="text-lg font-semibold text-black">OpenReplay Spot</span>
</div>
</div>
<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">
<HomePageSvg />
</div>
</div>
</div>
<div
class="text-sm tooltip tooltip-bottom"
data-tip="Get help on Slack"
>
<a
href="https://join.slack.com/t/openreplay/shared_invite/zt-2brqlwcis-k7OtqHkW53EAoTRqPjCmyg"
target="_blank"
rel="noopener noreferrer"
>
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
<SlackSvg />
</div>
</a>
</div>
<div
class="text-sm tooltip tooltip-bottom"
data-tip="Settings"
onClick={props.openSettings}
>
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
<SettingsSvg />
</div>
</div>
</div>
</div>
);
};
export default Header;

Some files were not shown because too many files have changed in this diff Show more