pulled dev
This commit is contained in:
commit
f45821cbb3
64 changed files with 383 additions and 210 deletions
|
|
@ -173,7 +173,7 @@ def process():
|
|||
logger.debug(alert)
|
||||
logger.debug(query)
|
||||
try:
|
||||
result = ch_cur.execute(query)
|
||||
result = ch_cur.execute(query=query)
|
||||
if len(result) > 0:
|
||||
result = result[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
|
|||
# print("------------")
|
||||
query = ch.format(query=main_ch_query, parameters=params)
|
||||
|
||||
rows = ch.execute(query)
|
||||
rows = ch.execute(query=query)
|
||||
total = rows[0]["total"] if len(rows) > 0 else 0
|
||||
|
||||
for r in rows:
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ def get_by_url(project_id, data: schemas.GetHeatMapPayloadSchema):
|
|||
logger.debug(query)
|
||||
logger.debug("---------")
|
||||
try:
|
||||
rows = cur.execute(query)
|
||||
rows = cur.execute(query=query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- HEATMAP 2 SEARCH QUERY EXCEPTION CH -----------")
|
||||
logger.warning(query)
|
||||
|
|
@ -122,7 +122,7 @@ def get_x_y_by_url_and_session_id(project_id, session_id, data: schemas.GetHeatM
|
|||
logger.debug(query)
|
||||
logger.debug("---------")
|
||||
try:
|
||||
rows = cur.execute(query)
|
||||
rows = cur.execute(query=query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- HEATMAP-session_id SEARCH QUERY EXCEPTION CH -----------")
|
||||
logger.warning(query)
|
||||
|
|
@ -160,7 +160,7 @@ def get_selectors_by_url_and_session_id(project_id, session_id, data: schemas.Ge
|
|||
logger.debug(query)
|
||||
logger.debug("---------")
|
||||
try:
|
||||
rows = cur.execute(query)
|
||||
rows = cur.execute(query=query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- HEATMAP-session_id SEARCH QUERY EXCEPTION CH -----------")
|
||||
logger.warning(query)
|
||||
|
|
@ -221,7 +221,7 @@ def __get_1_url(location_condition: schemas.SessionSearchEventSchema2 | None, se
|
|||
logger.debug(main_query)
|
||||
logger.debug("--------------------")
|
||||
try:
|
||||
url = cur.execute(main_query)
|
||||
url = cur.execute(query=main_query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- CLICK MAP BEST URL SEARCH QUERY EXCEPTION CH-----------")
|
||||
logger.warning(main_query.decode('UTF-8'))
|
||||
|
|
@ -295,7 +295,7 @@ def search_short_session(data: schemas.HeatMapSessionsSearch, project_id, user_i
|
|||
logger.debug(main_query)
|
||||
logger.debug("--------------------")
|
||||
try:
|
||||
session = cur.execute(main_query)
|
||||
session = cur.execute(query=main_query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- CLICK MAP SHORT SESSION SEARCH QUERY EXCEPTION CH -----------")
|
||||
logger.warning(main_query)
|
||||
|
|
@ -342,7 +342,7 @@ def get_selected_session(project_id, session_id):
|
|||
logger.debug(main_query)
|
||||
logger.debug("--------------------")
|
||||
try:
|
||||
session = cur.execute(main_query)
|
||||
session = cur.execute(query=main_query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- CLICK MAP GET SELECTED SESSION QUERY EXCEPTION -----------")
|
||||
logger.warning(main_query.decode('UTF-8'))
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ def get_simple_funnel(filter_d: schemas.CardSeriesFilterSchema, project: schemas
|
|||
logger.debug(query)
|
||||
logger.debug("---------------------------------------------------")
|
||||
try:
|
||||
row = cur.execute(query)
|
||||
row = cur.execute(query=query)
|
||||
except Exception as err:
|
||||
logger.warning("--------- SIMPLE FUNNEL SEARCH QUERY EXCEPTION CH-----------")
|
||||
logger.warning(query)
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
|||
SELECT event_number_in_session,
|
||||
`$event_name`,
|
||||
e_value,
|
||||
SUM(sessions_count) AS sessions_count
|
||||
SUM(n{i}.sessions_count) AS sessions_count
|
||||
FROM n{i}
|
||||
GROUP BY event_number_in_session, `$event_name`, e_value
|
||||
ORDER BY sessions_count DESC
|
||||
|
|
@ -487,10 +487,11 @@ WITH {initial_sessions_cte}
|
|||
SELECT *
|
||||
FROM pre_ranked_events;"""
|
||||
logger.debug("---------Q1-----------")
|
||||
ch.execute(query=ch_query1, parameters=params)
|
||||
ch_query1 = ch.format(query=ch_query1, parameters=params)
|
||||
ch.execute(query=ch_query1)
|
||||
if time() - _now > 2:
|
||||
logger.warning(f">>>>>>>>>PathAnalysis long query EE ({int(time() - _now)}s)<<<<<<<<<")
|
||||
logger.warning(ch.format(query=ch_query1, parameters=params))
|
||||
logger.warning(str.encode(ch_query1))
|
||||
logger.warning("----------------------")
|
||||
_now = time()
|
||||
|
||||
|
|
@ -512,10 +513,11 @@ SELECT *
|
|||
FROM ranked_events
|
||||
{q2_extra_condition if q2_extra_condition else ""};"""
|
||||
logger.debug("---------Q2-----------")
|
||||
ch.execute(query=ch_query2, parameters=params)
|
||||
ch_query2 = ch.format(query=ch_query2, parameters=params)
|
||||
ch.execute(query=ch_query2)
|
||||
if time() - _now > 2:
|
||||
logger.warning(f">>>>>>>>>PathAnalysis long query EE ({int(time() - _now)}s)<<<<<<<<<")
|
||||
logger.warning(ch.format(query=ch_query2, parameters=params))
|
||||
logger.warning(str.encode(ch_query2))
|
||||
logger.warning("----------------------")
|
||||
_now = time()
|
||||
|
||||
|
|
@ -590,7 +592,7 @@ FROM ranked_events
|
|||
NULL AS e_value,
|
||||
'OTHER' AS next_type,
|
||||
NULL AS next_value,
|
||||
SUM(sessions_count) AS sessions_count
|
||||
SUM(others_n.sessions_count) AS sessions_count
|
||||
FROM others_n
|
||||
WHERE isNotNull(others_n.next_type)
|
||||
AND others_n.event_number_in_session < %(density)s
|
||||
|
|
@ -625,10 +627,11 @@ FROM ranked_events
|
|||
) AS chart_steps
|
||||
ORDER BY event_number_in_session, sessions_count DESC;"""
|
||||
logger.debug("---------Q3-----------")
|
||||
rows = ch.execute(query=ch_query3, parameters=params)
|
||||
ch_query3 = ch.format(query=ch_query3, parameters=params)
|
||||
rows = ch.execute(query=ch_query3)
|
||||
if time() - _now > 2:
|
||||
logger.warning(f">>>>>>>>>PathAnalysis long query EE ({int(time() - _now)}s)<<<<<<<<<")
|
||||
logger.warning(ch.format(query=ch_query3, parameters=params))
|
||||
logger.warning(str.encode(ch_query3))
|
||||
logger.warning("----------------------")
|
||||
|
||||
return __transform_journey(rows=rows, reverse_path=reverse)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ def search2_series(data: schemas.SessionsSearchPayloadSchema, project_id: int, d
|
|||
logging.debug("--------------------")
|
||||
logging.debug(main_query)
|
||||
logging.debug("--------------------")
|
||||
sessions = cur.execute(main_query)
|
||||
sessions = cur.execute(query=main_query)
|
||||
|
||||
elif metric_type == schemas.MetricType.TABLE:
|
||||
full_args["limit_s"] = 0
|
||||
|
|
@ -112,7 +112,7 @@ def search2_series(data: schemas.SessionsSearchPayloadSchema, project_id: int, d
|
|||
logging.debug("--------------------")
|
||||
logging.debug(main_query)
|
||||
logging.debug("--------------------")
|
||||
sessions = cur.execute(main_query)
|
||||
sessions = cur.execute(query=main_query)
|
||||
# cur.fetchone()
|
||||
count = 0
|
||||
if len(sessions) > 0:
|
||||
|
|
@ -121,8 +121,10 @@ def search2_series(data: schemas.SessionsSearchPayloadSchema, project_id: int, d
|
|||
s.pop("main_count")
|
||||
sessions = {"count": count, "values": helper.list_to_camel_case(sessions)}
|
||||
|
||||
return helper.complete_missing_steps(rows=sessions, start_timestamp=data.startTimestamp,
|
||||
end_timestamp=data.endTimestamp, step=step_size, neutral={"count": 0})
|
||||
return metrics_helper.complete_missing_steps(rows=sessions,
|
||||
start_timestamp=data.startTimestamp,
|
||||
end_timestamp=data.endTimestamp, step=step_size,
|
||||
neutral={"count": 0})
|
||||
|
||||
|
||||
def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, density: int,
|
||||
|
|
@ -242,7 +244,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
{extra_where}
|
||||
GROUP BY {main_col}
|
||||
ORDER BY total DESC
|
||||
LIMIT %(limit_e)s OFFSET %(limit_s)s;"""
|
||||
LIMIT %(limit)s OFFSET %(limit_s)s;"""
|
||||
else:
|
||||
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
||||
{main_col} AS name,
|
||||
|
|
@ -255,13 +257,13 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
{extra_where}
|
||||
GROUP BY {main_col}
|
||||
ORDER BY total DESC
|
||||
LIMIT %(limit_e)s OFFSET %(limit_s)s;"""
|
||||
LIMIT %(limit)s OFFSET %(limit_s)s;"""
|
||||
|
||||
main_query = cur.format(query=main_query, parameters=full_args)
|
||||
logging.debug("--------------------")
|
||||
logging.debug(main_query)
|
||||
logging.debug("--------------------")
|
||||
sessions = cur.execute(main_query)
|
||||
sessions = cur.execute(query=main_query)
|
||||
count = 0
|
||||
total = 0
|
||||
if len(sessions) > 0:
|
||||
|
|
@ -1503,7 +1505,7 @@ def session_exists(project_id, session_id):
|
|||
AND project_id=%(project_id)s
|
||||
LIMIT 1""",
|
||||
parameters={"project_id": project_id, "session_id": session_id})
|
||||
row = cur.execute(query)
|
||||
row = cur.execute(query=query)
|
||||
return row is not None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ if config("CH_COMPRESSION", cast=bool, default=True):
|
|||
def transform_result(self, original_function):
|
||||
@wraps(original_function)
|
||||
def wrapper(*args, **kwargs):
|
||||
logger.debug(self.format(query=kwargs.get("query"), parameters=kwargs.get("parameters")))
|
||||
logger.debug(str.encode(self.format(query=kwargs.get("query", ""), parameters=kwargs.get("parameters"))))
|
||||
result = original_function(*args, **kwargs)
|
||||
if isinstance(result, clickhouse_connect.driver.query.QueryResult):
|
||||
column_names = result.column_names
|
||||
|
|
|
|||
|
|
@ -336,19 +336,3 @@ def cast_session_id_to_string(data):
|
|||
return data
|
||||
|
||||
|
||||
from typing import List
|
||||
|
||||
|
||||
def complete_missing_steps(rows: List[dict], start_timestamp: int, end_timestamp: int, step: int, neutral: dict,
|
||||
time_key: str = "timestamp") -> List[dict]:
|
||||
result = []
|
||||
i = 0
|
||||
for t in range(start_timestamp, end_timestamp, step):
|
||||
if i >= len(rows) or rows[i][time_key] > t:
|
||||
neutral[time_key] = t
|
||||
result.append(neutral.copy())
|
||||
elif i < len(rows) and rows[i][time_key] == t:
|
||||
result.append(rows[i])
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from typing import List
|
||||
|
||||
|
||||
def get_step_size(startTimestamp, endTimestamp, density, decimal=False, factor=1000):
|
||||
step_size = (endTimestamp // factor - startTimestamp // factor)
|
||||
if density <= 1:
|
||||
|
|
@ -5,3 +8,17 @@ def get_step_size(startTimestamp, endTimestamp, density, decimal=False, factor=1
|
|||
if decimal:
|
||||
return step_size / density
|
||||
return step_size // density
|
||||
|
||||
|
||||
def complete_missing_steps(rows: List[dict], start_timestamp: int, end_timestamp: int, step: int, neutral: dict,
|
||||
time_key: str = "timestamp") -> List[dict]:
|
||||
result = []
|
||||
i = 0
|
||||
for t in range(start_timestamp, end_timestamp, step):
|
||||
if i >= len(rows) or rows[i][time_key] > t:
|
||||
neutral[time_key] = t
|
||||
result.append(neutral.copy())
|
||||
elif i < len(rows) and rows[i][time_key] == t:
|
||||
result.append(rows[i])
|
||||
i += 1
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ class RequestGraphqlFilterSchema(BaseModel):
|
|||
@classmethod
|
||||
def _transform_data(cls, values):
|
||||
if values.get("type") in [FetchFilterType.FETCH_DURATION, FetchFilterType.FETCH_STATUS_CODE]:
|
||||
values["value"] = [int(v) for v in values["value"] if v is not None and v.isnumeric()]
|
||||
values["value"] = [int(v) for v in values["value"] if v is not None and str(v).isnumeric()]
|
||||
return values
|
||||
|
||||
|
||||
|
|
@ -851,18 +851,21 @@ class MetricTimeseriesViewType(str, Enum):
|
|||
LINE_CHART = "lineChart"
|
||||
AREA_CHART = "areaChart"
|
||||
BAR_CHART = "barChart"
|
||||
PIE_CHART = "pieChart"
|
||||
PROGRESS_CHART = "progressChart"
|
||||
TABLE_CHART = "table"
|
||||
PIE_CHART = "pieChart"
|
||||
METRIC_CHART = "metric"
|
||||
TABLE_CHART = "table"
|
||||
|
||||
|
||||
class MetricTableViewType(str, Enum):
|
||||
TABLE = "table"
|
||||
TABLE_CHART = "table"
|
||||
|
||||
|
||||
class MetricOtherViewType(str, Enum):
|
||||
OTHER_CHART = "chart"
|
||||
COLUMN_CHART = "columnChart"
|
||||
METRIC_CHART = "metric"
|
||||
TABLE_CHART = "table"
|
||||
LIST_CHART = "list"
|
||||
|
||||
|
||||
|
|
@ -876,8 +879,6 @@ class MetricType(str, Enum):
|
|||
HEAT_MAP = "heatMap"
|
||||
|
||||
|
||||
|
||||
|
||||
class MetricOfTable(str, Enum):
|
||||
USER_BROWSER = FilterType.USER_BROWSER.value
|
||||
USER_DEVICE = FilterType.USER_DEVICE.value
|
||||
|
|
@ -1086,7 +1087,7 @@ class CardFunnel(__CardSchema):
|
|||
def __enforce_default(cls, values):
|
||||
if values.get("metricOf") and not MetricOfFunnels.has_value(values["metricOf"]):
|
||||
values["metricOf"] = MetricOfFunnels.SESSION_COUNT
|
||||
values["viewType"] = MetricOtherViewType.OTHER_CHART
|
||||
# values["viewType"] = MetricOtherViewType.OTHER_CHART
|
||||
if values.get("series") is not None and len(values["series"]) > 0:
|
||||
values["series"] = [values["series"][0]]
|
||||
return values
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
|
|
@ -58,7 +59,9 @@ func main() {
|
|||
|
||||
if isSessionEnd(data) {
|
||||
if err := srv.PackSessionCanvases(sessCtx, sessID); err != nil {
|
||||
log.Error(sessCtx, "can't pack session's canvases: %s", err)
|
||||
if !strings.Contains(err.Error(), "no such file or directory") {
|
||||
log.Error(sessCtx, "can't pack session's canvases: %s", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := srv.SaveCanvasToDisk(sessCtx, sessID, data); err != nil {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ go 1.23
|
|||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.1
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.32.1
|
||||
github.com/DataDog/datadog-api-client-go/v2 v2.34.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
|
|
@ -36,12 +36,12 @@ require (
|
|||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/net v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.63.1 // indirect
|
||||
github.com/ClickHouse/ch-go v0.65.0 // indirect
|
||||
github.com/DataDog/zstd v1.5.6 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
|
@ -77,10 +77,10 @@ require (
|
|||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
|
|
|||
|
|
@ -12,20 +12,21 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP
|
|||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM=
|
||||
github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0=
|
||||
github.com/ClickHouse/ch-go v0.65.0 h1:vZAXfTQliuNNefqkPDewX3kgRxN6Q4vUENnnY+ynTRY=
|
||||
github.com/ClickHouse/ch-go v0.65.0/go.mod h1:tCM0XEH5oWngoi9Iu/8+tjPBo04I/FxNIffpdjtwx3k=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.1 h1:Dy0n0l+cMbPXs8hFkeeWGaPKrB+MDByUNQBSmRO3W6k=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.1/go.mod h1:szk8BMoQV/NgHXZ20ZbwDyvPWmpfhRKjFkc6wzASGxM=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.32.1 h1:RLhkxA6iH/bLTXeDtEj/u4yUx9Q03Y95P+cjHScQK78=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.32.1/go.mod h1:YtaiIFlHCGNPbOpAvFGYobtcVnmgYvD/WmzitixxWYc=
|
||||
github.com/DataDog/datadog-api-client-go/v2 v2.34.0 h1:0VVmv8uZg8vdBuEpiF2nBGUezl2QITrxdEsLgh38j8M=
|
||||
github.com/DataDog/datadog-api-client-go/v2 v2.34.0/go.mod h1:d3tOEgUd2kfsr9uuHQdY+nXrWp4uikgTgVCPdKNK30U=
|
||||
github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
|
||||
|
|
@ -198,8 +199,6 @@ github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
|
|||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
|
|
@ -527,8 +526,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
|
|||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
|
||||
github.com/ua-parser/uap-go v0.0.0-20241012191800-bbb40edc15aa h1:VzPR4xFM7HARqNocjdHg75ZL9SAgFtaF3P57ZdDcG6I=
|
||||
github.com/ua-parser/uap-go v0.0.0-20241012191800-bbb40edc15aa/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E=
|
||||
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff h1:NwMEGwb7JJ8wPjT8OPKP5hO1Xz6AQ7Z00+GLSJfW21s=
|
||||
github.com/ua-parser/uap-go v0.0.0-20250126222208-a52596c19dff/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
|
|
@ -618,6 +615,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
|
@ -642,6 +641,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -676,6 +677,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
@ -695,6 +698,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -720,16 +725,12 @@ google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa h1:ePqxpG3LVx+feAU
|
|||
google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:CnZenrTdRJb7jc+jOm0Rkywq+9wh0QC4U8tyiRbEPPM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func (v *ImageStorage) SaveCanvasToDisk(ctx context.Context, sessID uint64, data
|
|||
|
||||
func (v *ImageStorage) writeToDisk(payload interface{}) {
|
||||
task := payload.(*saveTask)
|
||||
path := fmt.Sprintf("%s/%d/", v.basePath, task.sessionID)
|
||||
path := fmt.Sprintf("%s%d/", v.basePath, task.sessionID)
|
||||
|
||||
// Ensure the directory exists
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
|
|
@ -102,7 +102,7 @@ func (v *ImageStorage) writeToDisk(payload interface{}) {
|
|||
}
|
||||
|
||||
func (v *ImageStorage) PackSessionCanvases(ctx context.Context, sessID uint64) error {
|
||||
path := fmt.Sprintf("%s/%d/", v.basePath, sessID)
|
||||
path := fmt.Sprintf("%s%d/", v.basePath, sessID)
|
||||
|
||||
// Check that the directory exists
|
||||
files, err := os.ReadDir(path)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ requests = "==2.32.3"
|
|||
boto3 = "==1.36.12"
|
||||
pyjwt = "==2.10.1"
|
||||
psycopg2-binary = "==2.9.10"
|
||||
psycopg = {extras = ["binary", "pool"], version = "==3.2.4"}
|
||||
psycopg = {extras = ["pool", "binary"], version = "==3.2.4"}
|
||||
clickhouse-driver = {extras = ["lz4"], version = "==0.2.9"}
|
||||
clickhouse-connect = "==0.8.15"
|
||||
elasticsearch = "==8.17.1"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Optional
|
||||
|
||||
from chalicelib.core import roles, traces, assist_records, sessions
|
||||
from chalicelib.core import roles, traces, assist_records
|
||||
from chalicelib.core.sessions import sessions
|
||||
from chalicelib.core import assist_stats
|
||||
from chalicelib.core import unlock, signals
|
||||
from chalicelib.utils import assist_helper
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ DROP TABLE IF EXISTS public.user_favorite_errors;
|
|||
DROP TABLE IF EXISTS public.user_viewed_errors;
|
||||
|
||||
ALTER TABLE IF EXISTS public.sessions_notes
|
||||
ADD COLUMN start_at integer,
|
||||
ADD COLUMN end_at integer,
|
||||
ADD COLUMN thumbnail text,
|
||||
ADD COLUMN updated_at timestamp DEFAULT NULL,
|
||||
ADD COLUMN IF NOT EXISTS start_at integer,
|
||||
ADD COLUMN IF NOT EXISTS end_at integer,
|
||||
ADD COLUMN IF NOT EXISTS thumbnail text,
|
||||
ADD COLUMN IF NOT EXISTS updated_at timestamp DEFAULT NULL,
|
||||
ALTER COLUMN message DROP NOT NULL;
|
||||
|
||||
DELETE
|
||||
|
|
|
|||
|
|
@ -22,5 +22,5 @@ MINIO_ACCESS_KEY = ''
|
|||
MINIO_SECRET_KEY = ''
|
||||
|
||||
# APP and TRACKER VERSIONS
|
||||
VERSION = 1.21.0
|
||||
TRACKER_VERSION = '15.0.0'
|
||||
VERSION = 1.22.0
|
||||
TRACKER_VERSION = '15.0.5'
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const enhancedComponents: any = {
|
|||
SpotsList: withSiteIdUpdater(components.SpotsListPure),
|
||||
Spot: components.SpotPure,
|
||||
ScopeSetup: components.ScopeSetup,
|
||||
Highlights: components.HighlightsPure,
|
||||
Highlights: withSiteIdUpdater(components.HighlightsPure),
|
||||
};
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function ORBarChart(props: BarChartProps) {
|
|||
type: 'value',
|
||||
data: undefined,
|
||||
name: props.label ?? 'Number of Sessions',
|
||||
nameLocation: 'middle',
|
||||
nameLocation: 'center',
|
||||
nameGap: 45,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ function ColumnChart(props: ColumnChartProps) {
|
|||
xAxis: {
|
||||
type: 'value',
|
||||
name: label ?? 'Total',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 45,
|
||||
nameLocation: 'center',
|
||||
nameGap: 35,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
|
|
|
|||
|
|
@ -69,8 +69,11 @@ function ORLineChart(props: Props) {
|
|||
},
|
||||
yAxis: {
|
||||
name: props.label ?? 'Number of Sessions',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 45,
|
||||
// nameLocation: 'center',
|
||||
// nameGap: 40,
|
||||
nameTextStyle: {
|
||||
padding: [0, 0, 0, 15],
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
...defaultOptions.tooltip,
|
||||
|
|
|
|||
|
|
@ -77,13 +77,18 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
});
|
||||
|
||||
setFinalNodeCount(filteredNodes.length);
|
||||
|
||||
const nodeValues: Record<string, number> = {};
|
||||
const echartNodes = filteredNodes
|
||||
.map((n) => {
|
||||
.map((n, i) => {
|
||||
let computedName = getNodeName(n.eventType || 'Minor Paths', n.name);
|
||||
if (computedName === 'Other') {
|
||||
computedName = 'Others';
|
||||
}
|
||||
if (n.id) {
|
||||
nodeValues[n.id] = 0;
|
||||
} else {
|
||||
nodeValues[i] = 0;
|
||||
}
|
||||
const itemColor =
|
||||
computedName === 'Others'
|
||||
? 'rgba(34,44,154,.9)'
|
||||
|
|
@ -124,6 +129,17 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
.filter((link) => link.source === 0)
|
||||
.reduce((sum, link) => sum + link.value, 0);
|
||||
|
||||
Object.keys(nodeValues).forEach((nodeId) => {
|
||||
const intId = parseInt(nodeId as string);
|
||||
const outgoingValues = echartLinks
|
||||
.filter((l) => l.source === intId)
|
||||
.reduce((p, c) => p + c.value, 0);
|
||||
const incomingValues = echartLinks
|
||||
.filter((l) => l.target === intId)
|
||||
.reduce((p, c) => p + c.value, 0);
|
||||
nodeValues[nodeId] = Math.max(outgoingValues, incomingValues);
|
||||
});
|
||||
|
||||
const option = {
|
||||
...defaultOptions,
|
||||
tooltip: {
|
||||
|
|
@ -172,10 +188,10 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
params.name.slice(-(maxLen / 2 - 2))
|
||||
: params.name;
|
||||
const nodeType = params.data.type;
|
||||
|
||||
|
||||
const icon = getIcon(nodeType)
|
||||
return (
|
||||
`${icon}{header|${safeName}}\n` +
|
||||
`${icon}{header| ${safeName}}\n` +
|
||||
`{body|}{percentage|${percentage}} {sessions|${nodeVal}}`
|
||||
);
|
||||
},
|
||||
|
|
@ -233,11 +249,25 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
},
|
||||
height: 20,
|
||||
width: 14,
|
||||
},
|
||||
dropEventIcon: {
|
||||
backgroundColor: {
|
||||
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1kb3duIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48cGF0aCBkPSJtOCAxMiA0IDQgNC00Ii8+PC9zdmc+',
|
||||
},
|
||||
height: 20,
|
||||
width: 14,
|
||||
},
|
||||
groupIcon: {
|
||||
backgroundColor: {
|
||||
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvbXBvbmVudCI+PHBhdGggZD0iTTE1LjUzNiAxMS4yOTNhMSAxIDAgMCAwIDAgMS40MTRsMi4zNzYgMi4zNzdhMSAxIDAgMCAwIDEuNDE0IDBsMi4zNzctMi4zNzdhMSAxIDAgMCAwIDAtMS40MTRsLTIuMzc3LTIuMzc3YTEgMSAwIDAgMC0xLjQxNCAweiIvPjxwYXRoIGQ9Ik0yLjI5NyAxMS4yOTNhMSAxIDAgMCAwIDAgMS40MTRsMi4zNzcgMi4zNzdhMSAxIDAgMCAwIDEuNDE0IDBsMi4zNzctMi4zNzdhMSAxIDAgMCAwIDAtMS40MTRMNi4wODggOC45MTZhMSAxIDAgMCAwLTEuNDE0IDB6Ii8+PHBhdGggZD0iTTguOTE2IDE3LjkxMmExIDEgMCAwIDAgMCAxLjQxNWwyLjM3NyAyLjM3NmExIDEgMCAwIDAgMS40MTQgMGwyLjM3Ny0yLjM3NmExIDEgMCAwIDAgMC0xLjQxNWwtMi4zNzctMi4zNzZhMSAxIDAgMCAwLTEuNDE0IDB6Ii8+PHBhdGggZD0iTTguOTE2IDQuNjc0YTEgMSAwIDAgMCAwIDEuNDE0bDIuMzc3IDIuMzc2YTEgMSAwIDAgMCAxLjQxNCAwbDIuMzc3LTIuMzc2YTEgMSAwIDAgMCAwLTEuNDE0bC0yLjM3Ny0yLjM3N2ExIDEgMCAwIDAtMS40MTQgMHoiLz48L3N2Zz4=',
|
||||
},
|
||||
height: 20,
|
||||
width: 14,
|
||||
}
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
formatter: sankeyTooltip(echartNodes, []),
|
||||
formatter: sankeyTooltip(echartNodes, nodeValues),
|
||||
},
|
||||
nodeAlign: 'left',
|
||||
nodeWidth: 40,
|
||||
|
|
@ -415,7 +445,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
const dynamicMinHeight = finalNodeCount * 15;
|
||||
containerStyle = {
|
||||
width: '100%',
|
||||
minHeight: dynamicMinHeight,
|
||||
minHeight: Math.max(550, dynamicMinHeight),
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
};
|
||||
|
|
@ -427,7 +457,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxHeight: 620, overflow: 'auto', maxWidth: 1240, }}>
|
||||
<div style={{ maxHeight: 620, overflow: 'auto', maxWidth: 1240, minHeight: 240 }}>
|
||||
<div
|
||||
ref={chartRef}
|
||||
style={containerStyle}
|
||||
|
|
@ -450,6 +480,12 @@ function getIcon(type: string) {
|
|||
if (type === 'CLICK') {
|
||||
return '{clickIcon|}';
|
||||
}
|
||||
if (type === 'DROP') {
|
||||
return '{dropEventIcon|}';
|
||||
}
|
||||
if (type === 'OTHER') {
|
||||
return '{groupIcon|}';
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
// sankeyUtils.ts
|
||||
export function sankeyTooltip(echartNodes: any[], nodeValues: number[]) {
|
||||
export function sankeyTooltip(
|
||||
echartNodes: any[],
|
||||
nodeValues: Record<string, number>
|
||||
) {
|
||||
return (params: any) => {
|
||||
if ('source' in params.data && 'target' in params.data) {
|
||||
const sourceName = echartNodes[params.data.source].name;
|
||||
const targetName = echartNodes[params.data.target].name;
|
||||
const sourceValue = nodeValues[params.data.source];
|
||||
|
||||
const safeSourceName = shortenString(sourceName);
|
||||
const safeTargetName = shortenString(targetName);
|
||||
return `
|
||||
<div class="flex gap-2 w-fit px-2 bg-white items-center rounded-xl">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col text-sm">
|
||||
<div class="font-semibold">
|
||||
<span class="text-base" style="color:#394eff">←</span> ${sourceName}
|
||||
<span class="text-base" style="color:#394eff">←</span> ${safeSourceName}
|
||||
</div>
|
||||
<div class="text-black">
|
||||
${sourceValue} <span class="text-disabled-text">Sessions</span>
|
||||
</div>
|
||||
<div class="font-semibold mt-2">
|
||||
<span class="text-base" style="color:#394eff">→</span> ${targetName}
|
||||
<span class="text-base" style="color:#394eff">→</span> ${safeTargetName}
|
||||
</div>
|
||||
<div class="flex items-baseline gap-2 text-black">
|
||||
<span>${params.data.value} ( ${params.data.percentage.toFixed(2)}% )</span>
|
||||
<span>${params.data.value} ( ${params.data.percentage.toFixed(
|
||||
2
|
||||
)}% )</span>
|
||||
<span class="text-disabled-text">Sessions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,6 +45,21 @@ export function sankeyTooltip(echartNodes: any[], nodeValues: number[]) {
|
|||
};
|
||||
}
|
||||
|
||||
const shortenString = (str: string) => {
|
||||
const limit = 60;
|
||||
const leftPart = 25;
|
||||
const rightPart = 20;
|
||||
const safeStr =
|
||||
str.length > limit
|
||||
? `${str.slice(0, leftPart)}...${str.slice(
|
||||
str.length - rightPart,
|
||||
str.length
|
||||
)}`
|
||||
: str;
|
||||
|
||||
return safeStr;
|
||||
};
|
||||
|
||||
export const getEventPriority = (type: string): number => {
|
||||
switch (type) {
|
||||
case 'DROP':
|
||||
|
|
@ -49,9 +71,12 @@ export const getEventPriority = (type: string): number => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getNodeName = (eventType: string, nodeName: string | null): string => {
|
||||
export const getNodeName = (
|
||||
eventType: string,
|
||||
nodeName: string | null
|
||||
): string => {
|
||||
if (!nodeName) {
|
||||
return eventType.charAt(0) + eventType.slice(1).toLowerCase();
|
||||
}
|
||||
return nodeName;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { List, Space, Typography, Button, Tooltip } from 'antd';
|
||||
import { PencilIcon, PlusIcon, Tags } from 'lucide-react';
|
||||
import { List, Space, Typography, Button, Tooltip, Empty } from 'antd';
|
||||
import { PlusIcon, Tags } from 'lucide-react';
|
||||
import {EditOutlined } from '@ant-design/icons';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
import { Empty } from '.store/antd-virtual-7db13b4af6/package';
|
||||
|
||||
|
||||
const CustomFields = () => {
|
||||
usePageTitle('Metadata - OpenReplay Preferences');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export { default } from './Modules';
|
|||
|
||||
export const enum MODULES {
|
||||
ASSIST = 'assist',
|
||||
NOTES = 'notes',
|
||||
HIGHLIGHTS = 'notes',
|
||||
BUG_REPORTS = 'bug-reports',
|
||||
OFFLINE_RECORDINGS = 'offline-recordings',
|
||||
ALERTS = 'alerts',
|
||||
|
|
@ -43,10 +43,10 @@ export const modules = [
|
|||
enterprise: true
|
||||
},
|
||||
{
|
||||
label: 'Notes',
|
||||
description: 'Add notes to sessions and share with your team.',
|
||||
key: MODULES.NOTES,
|
||||
icon: 'stickies',
|
||||
label: 'Highlights',
|
||||
description: 'Add highlights to sessions and share with your team.',
|
||||
key: MODULES.HIGHLIGHTS,
|
||||
icon: 'chat-square-quote',
|
||||
isEnabled: true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ function ClickMapCard() {
|
|||
const url = metricStore.instance.data.path;
|
||||
const operator = metricStore.instance.series[0]?.filter.filters[0]?.operator ? metricStore.instance.series[0].filter.filters[0].operator : 'startsWith'
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => setCustomSession(null);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -289,11 +289,13 @@ const AddCardSection = observer(
|
|||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<Segmented
|
||||
options={options}
|
||||
value={tab}
|
||||
onChange={(value) => setTab(value)}
|
||||
/>
|
||||
{options.length > 1 ? (
|
||||
<Segmented
|
||||
options={options}
|
||||
value={tab}
|
||||
onChange={(value) => setTab(value)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ type Props = IProps & RouteComponentProps;
|
|||
|
||||
function DashboardHeader(props: Props) {
|
||||
const { siteId } = props;
|
||||
const [popoverOpen, setPopoverOpen] = React.useState(false);
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setPopoverOpen(open);
|
||||
};
|
||||
const { dashboardStore } = useStore();
|
||||
const [focusTitle, setFocusedInput] = React.useState(true);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
|
|
@ -82,7 +86,9 @@ function DashboardHeader(props: Props) {
|
|||
|
||||
<Popover
|
||||
trigger="click"
|
||||
content={<AddCardSection />}
|
||||
open={popoverOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
content={<AddCardSection handleOpenChange={handleOpenChange} />}
|
||||
overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }}
|
||||
>
|
||||
<Button type="primary" icon={<PlusOutlined />} size="middle">
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ export const CARD_LIST: CardType[] = [
|
|||
cardType: HEATMAP,
|
||||
metricOf: 'heatMapUrl',
|
||||
category: CARD_CATEGORIES[0].key,
|
||||
example: HeatmapsExample
|
||||
example: HeatmapsExample,
|
||||
viewType: 'chart',
|
||||
},
|
||||
{
|
||||
title: 'Untitled Journey',
|
||||
|
|
@ -135,7 +136,8 @@ export const CARD_LIST: CardType[] = [
|
|||
cardType: TABLE,
|
||||
metricOf: FilterKey.USERID,
|
||||
category: CARD_CATEGORIES[1].key,
|
||||
example: ByUser
|
||||
example: ByUser,
|
||||
viewType: 'table',
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -144,7 +146,8 @@ export const CARD_LIST: CardType[] = [
|
|||
cardType: TABLE,
|
||||
metricOf: FilterKey.USER_BROWSER,
|
||||
category: CARD_CATEGORIES[1].key,
|
||||
example: ByBrowser
|
||||
example: ByBrowser,
|
||||
viewType: 'table',
|
||||
},
|
||||
// {
|
||||
// title: 'Sessions by System',
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ function FilterSeries(props: Props) {
|
|||
onToggleCollapse,
|
||||
excludeCategory
|
||||
} = props;
|
||||
const expanded = !collapseState
|
||||
const expanded = isHeatmap || !collapseState
|
||||
const setExpanded = onToggleCollapse
|
||||
const { series, seriesIndex } = props;
|
||||
|
||||
|
|
@ -168,6 +168,7 @@ function FilterSeries(props: Props) {
|
|||
};
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
filter.autoOpen = true;
|
||||
series.filter.addFilter(filter);
|
||||
observeChanges();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export const renderClickmapThumbnail = () => {
|
|||
return html2canvas(
|
||||
element,
|
||||
{
|
||||
allowTaint: false,
|
||||
logging: true,
|
||||
scale: 1,
|
||||
// allowTaint: true,
|
||||
useCORS: true,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { debounce } from 'App/utils';
|
|||
import useIsMounted from 'App/hooks/useIsMounted';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import { HEATMAP } from 'App/constants/card';
|
||||
import { HEATMAP, USER_PATH } from "App/constants/card";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -107,10 +107,15 @@ function WidgetSessions(props: Props) {
|
|||
};
|
||||
debounceClickMapSearch(customFilter);
|
||||
} else {
|
||||
const usedSeries = focusedSeries ? widget.series.filter((s) => s.name === focusedSeries) : widget.series;
|
||||
const hasStartPoint = !!widget.startPoint && widget.metricType === USER_PATH
|
||||
const activeSeries = focusedSeries ? widget.series.filter((s) => s.name === focusedSeries) : widget.series
|
||||
const seriesJson = activeSeries.map((s) => s.toJson());
|
||||
if (hasStartPoint) {
|
||||
seriesJson[0].filter.filters.push(widget.startPoint.toJson());
|
||||
}
|
||||
debounceRequest(widget.metricId, {
|
||||
...filter,
|
||||
series: usedSeries.map((s) => s.toJson()),
|
||||
series: seriesJson,
|
||||
page: metricStore.sessionsPage,
|
||||
limit: metricStore.sessionsPageSize
|
||||
});
|
||||
|
|
@ -125,7 +130,8 @@ function WidgetSessions(props: Props) {
|
|||
filter.filters,
|
||||
depsString,
|
||||
metricStore.clickMapSearch,
|
||||
focusedSeries
|
||||
focusedSeries,
|
||||
widget.startPoint,
|
||||
]);
|
||||
useEffect(loadData, [metricStore.sessionsPage]);
|
||||
useEffect(() => {
|
||||
|
|
@ -207,7 +213,7 @@ function WidgetSessions(props: Props) {
|
|||
>
|
||||
{filteredSessions.sessions.map((session: any) => (
|
||||
<React.Fragment key={session.sessionId}>
|
||||
<SessionItem session={session} metaList={metaList} />
|
||||
<SessionItem disableUser session={session} metaList={metaList} />
|
||||
<div className="border-b" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const CardViewMenu = () => {
|
|||
key: 'alert',
|
||||
label: 'Set Alerts',
|
||||
icon: <BellIcon size={16} />,
|
||||
disabled: !widget.exists() || widget.metricType === 'predefined',
|
||||
disabled: !widget.exists() || widget.metricType !== 'timeseries',
|
||||
onClick: showAlertModal,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -68,7 +68,10 @@ function WidgetView({ match: { params: { siteId, dashboardId, metricId } } }: Pr
|
|||
name: selectedCard.title,
|
||||
metricOf: selectedCard.metricOf,
|
||||
category: mk,
|
||||
viewType: selectedCard.viewType ? selectedCard.viewType : selectedCard.cardType === FUNNEL ? 'chart' : 'lineChart',
|
||||
viewType:
|
||||
selectedCard.viewType
|
||||
? selectedCard.viewType
|
||||
: selectedCard.cardType === FUNNEL ? 'chart' : 'lineChart',
|
||||
};
|
||||
if (selectedCard.filters) {
|
||||
cardData.series = [
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ function HighlightClip({
|
|||
openEdit = () => undefined,
|
||||
onItemClick = () => undefined,
|
||||
onDelete = () => undefined,
|
||||
canEdit = false,
|
||||
}: {
|
||||
note: string | null;
|
||||
tag: string;
|
||||
|
|
@ -30,6 +31,7 @@ function HighlightClip({
|
|||
openEdit: (id: any) => any;
|
||||
onItemClick: (id: any) => any;
|
||||
onDelete: (id: any) => any;
|
||||
canEdit: boolean;
|
||||
}) {
|
||||
const noteMsg = note || noNoteMsg
|
||||
const copyToClipboard = () => {
|
||||
|
|
@ -48,16 +50,19 @@ function HighlightClip({
|
|||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
label: 'Edit',
|
||||
disabled: !canEdit,
|
||||
},
|
||||
{
|
||||
key: 'visibility',
|
||||
icon: <Eye strokeWidth={1} size={14} />,
|
||||
label: 'Visibility',
|
||||
disabled: !canEdit,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
label: 'Delete',
|
||||
disabled: !canEdit,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,28 @@ import { toast } from 'react-toastify';
|
|||
import EditHlModal from './EditHlModal';
|
||||
import HighlightsListHeader from './HighlightsListHeader';
|
||||
import withPermissions from 'HOCs/withPermissions';
|
||||
import { useHistory } from 'react-router';
|
||||
import { highlights, withSiteId } from 'App/routes'
|
||||
|
||||
function HighlightsList() {
|
||||
const { notesStore, projectsStore } = useStore();
|
||||
const history = useHistory();
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hlId = params.get('highlight');
|
||||
const { notesStore, projectsStore, userStore } = useStore();
|
||||
const [activeId, setActiveId] = React.useState<string | null>(null);
|
||||
const [editModalOpen, setEditModalOpen] = React.useState(false);
|
||||
const [editHl, setEditHl] = React.useState<Record<string, any>>({
|
||||
message: '',
|
||||
isPublic: false
|
||||
});
|
||||
const currentUserId = userStore.account.id;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hlId) {
|
||||
setActiveId(hlId);
|
||||
history.replace(withSiteId(highlights(), projectsStore.siteId));
|
||||
}
|
||||
}, [hlId])
|
||||
|
||||
const activeProject = projectsStore.activeSiteId;
|
||||
const query = notesStore.query;
|
||||
|
|
@ -150,6 +163,7 @@ function HighlightsList() {
|
|||
createdAt={note.createdAt}
|
||||
hId={note.noteId}
|
||||
thumbnail={note.thumbnail}
|
||||
canEdit={note.userId === currentUserId}
|
||||
openEdit={() => onEdit(note.noteId)}
|
||||
onDelete={() => onDelete(note.noteId)}
|
||||
onItemClick={() => onItemClick(note.noteId)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { KEYS } from 'Types/filter/customFilter';
|
|||
import { capitalize } from 'App/utils';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import AssistSearchField from 'App/components/Assist/AssistSearchActions';
|
||||
import AssistSearchActions from 'App/components/Assist/AssistSearchActions';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import cn from 'classnames';
|
||||
import Session from 'App/mstore/types/session';
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ function DateRangePopup(props: any) {
|
|||
if (!range.end || value > range.end) {
|
||||
return;
|
||||
}
|
||||
setRange(Interval.fromDateTimes(value, range.end));
|
||||
const newRange = range.start.set({ hour: value.hour, minute: value.minute });
|
||||
setRange(Interval.fromDateTimes(newRange, range.end));
|
||||
setValue(CUSTOM_RANGE);
|
||||
};
|
||||
|
||||
|
|
@ -52,7 +53,8 @@ function DateRangePopup(props: any) {
|
|||
if (!range.start || (value && value < range.start)) {
|
||||
return;
|
||||
}
|
||||
setRange(Interval.fromDateTimes(range.start, value));
|
||||
const newRange = range.end.set({ hour: value.hour, minute: value.minute });
|
||||
setRange(Interval.fromDateTimes(range.start, newRange));
|
||||
setValue(CUSTOM_RANGE);
|
||||
};
|
||||
|
||||
|
|
@ -92,11 +94,6 @@ function DateRangePopup(props: any) {
|
|||
<div className="flex justify-center h-fit w-full items-center dateRangeContainer">
|
||||
<DateRangePicker
|
||||
name="dateRangePicker"
|
||||
// onSelect={this.selectCustomRange} -> onChange
|
||||
// numberOfCalendars={2}
|
||||
// selectionType="range"
|
||||
// maximumDate={new Date()}
|
||||
// singleDateRange={true}
|
||||
onChange={selectCustomRange}
|
||||
shouldCloseCalendar={() => false}
|
||||
isOpen
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ interface Props {
|
|||
placeholder?: string;
|
||||
modalProps?: any;
|
||||
mapValues?: (value: string) => string;
|
||||
isAutoOpen?: boolean;
|
||||
}
|
||||
|
||||
export function AutoCompleteContainer(props: Props) {
|
||||
|
|
@ -241,6 +242,15 @@ export function AutoCompleteContainer(props: Props) {
|
|||
const [showValueModal, setShowValueModal] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isEmpty = props.value.length === 0 || !props.value[0];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.isAutoOpen) {
|
||||
setTimeout(() => {
|
||||
setShowValueModal(true);
|
||||
}, 1)
|
||||
}
|
||||
}, [props.isAutoOpen])
|
||||
|
||||
const onClose = () => setShowValueModal(false);
|
||||
const onApply = (values: string[]) => {
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ interface Props {
|
|||
hideOrText?: boolean;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
modalProps?: Record<string, any>
|
||||
isAutoOpen?: boolean;
|
||||
}
|
||||
|
||||
const FilterAutoComplete = observer(
|
||||
|
|
@ -59,11 +60,16 @@ const FilterAutoComplete = observer(
|
|||
);
|
||||
const [initialFocus, setInitialFocus] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { filterStore } = useStore();
|
||||
const { filterStore, projectsStore } = useStore();
|
||||
const _params = processKey(params);
|
||||
const filterKey = `${_params.type}${_params.key || ''}`;
|
||||
const topValues = filterStore.topValues[filterKey] || [];
|
||||
|
||||
React.useEffect(() => {
|
||||
filterStore.resetValues()
|
||||
setOptions([])
|
||||
}, [projectsStore.siteId])
|
||||
|
||||
const loadTopValues = async () => {
|
||||
setLoading(true)
|
||||
await filterStore.fetchTopValues(_params.type, _params.key);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface Props {
|
|||
allowDecimals?: boolean;
|
||||
modalProps?: Record<string, any>;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
isAutoOpen?: boolean;
|
||||
}
|
||||
|
||||
function FilterAutoCompleteLocal(props: { params: any, values: string[], onClose: () => void, onApply: (values: string[]) => void, placeholder?: string }) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ interface Props {
|
|||
}
|
||||
function FilterValue(props: Props) {
|
||||
const { filter } = props;
|
||||
const isAutoOpen = filter.autoOpen;
|
||||
|
||||
const [durationValues, setDurationValues] = useState({
|
||||
minDuration: filter.value?.[0],
|
||||
maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0],
|
||||
|
|
@ -99,6 +101,7 @@ function FilterValue(props: Props) {
|
|||
onSelect={(e, item, index) => debounceOnSelect(e, item, index)}
|
||||
icon={filter.icon}
|
||||
placeholder={filter.placeholder}
|
||||
isAutoOpen={isAutoOpen}
|
||||
modalProps={{ placeholder: '' }}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -106,6 +109,7 @@ function FilterValue(props: Props) {
|
|||
const BaseDropDown = (props) => (
|
||||
<FilterValueDropdown
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onApplyValues={onApplyValues}
|
||||
|
|
@ -157,6 +161,7 @@ function FilterValue(props: Props) {
|
|||
return (
|
||||
<FilterAutoComplete
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
onApplyValues={onApplyValues}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function LiveSessionSearch() {
|
|||
}, []);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
filter.autoOpen = true;
|
||||
searchStoreLive.addFilter(filter);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ function SessionFilters() {
|
|||
}, [appliedFilter.filters]);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
filter.autoOpen = true;
|
||||
searchStore.addFilter(filter);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ import { useStore } from 'App/mstore';
|
|||
const PLAY_ICON_NAMES = {
|
||||
notPlayed: 'play-fill',
|
||||
played: 'play-circle-light',
|
||||
hovered: 'play-hover',
|
||||
} as const
|
||||
|
||||
const getDefaultIconName = (isViewed: any) =>
|
||||
const getIconName = (isViewed: any) =>
|
||||
!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played;
|
||||
|
||||
interface Props {
|
||||
|
|
@ -33,15 +32,13 @@ function PlayLink(props: Props) {
|
|||
const { projectsStore } = useStore();
|
||||
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
|
||||
const history = useHistory();
|
||||
const defaultIconName = getDefaultIconName(viewed);
|
||||
const defaultIconName = getIconName(viewed);
|
||||
|
||||
const [isHovered, toggleHover] = useState(false);
|
||||
const [iconName, setIconName] = useState<typeof PLAY_ICON_NAMES[keyof typeof PLAY_ICON_NAMES]>(defaultIconName);
|
||||
|
||||
useEffect(() => {
|
||||
if (isHovered) setIconName(PLAY_ICON_NAMES.hovered);
|
||||
else setIconName(getDefaultIconName(viewed));
|
||||
}, [isHovered, viewed]);
|
||||
setIconName(getIconName(viewed));
|
||||
}, [viewed]);
|
||||
|
||||
const link = isAssist
|
||||
? liveSessionRoute(sessionId, queryParams)
|
||||
|
|
@ -68,22 +65,20 @@ function PlayLink(props: Props) {
|
|||
|
||||
const onLinkClick = props.beforeOpen ? handleBeforeOpen : onClick;
|
||||
|
||||
const onLeave = () => {
|
||||
toggleHover(false);
|
||||
};
|
||||
const onOver = () => {
|
||||
toggleHover(true);
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
className={'group'}
|
||||
onClick={onLinkClick}
|
||||
to={link + (props.query ? props.query : '')}
|
||||
onMouseOver={onOver}
|
||||
onMouseOut={onLeave}
|
||||
target={props.newTab ? '_blank' : undefined}
|
||||
rel={props.newTab ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
||||
<div className={'group-hover:block hidden'}>
|
||||
<Icon name={'play-hover'} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
||||
</div>
|
||||
<div className={'group-hover:hidden block'}>
|
||||
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function Layout(props: Props) {
|
|||
collapsed={settingsStore.menuCollapsed || collapsed}
|
||||
width={250}
|
||||
>
|
||||
<SideMenu siteId={siteId!} />
|
||||
<SideMenu siteId={siteId!} isCollapsed={settingsStore.menuCollapsed || collapsed} />
|
||||
</Sider>
|
||||
) : null}
|
||||
<Content style={{ padding: isPlayer ? '0' : '20px', minHeight: 'calc(100vh - 60px)' }}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Divider, Menu, Tag, Typography } from 'antd';
|
||||
import { Divider, Menu, Tag, Typography, Popover, Button } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
|
@ -26,6 +26,7 @@ import {
|
|||
spotOnlyCats
|
||||
} from './data';
|
||||
import { useStore } from 'App/mstore';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -36,7 +37,8 @@ interface Props extends RouteComponentProps {
|
|||
|
||||
function SideMenu(props: Props) {
|
||||
const {
|
||||
location
|
||||
location,
|
||||
isCollapsed
|
||||
} = props;
|
||||
|
||||
const isPreferencesActive = location.pathname.includes('/client/');
|
||||
|
|
@ -50,16 +52,6 @@ function SideMenu(props: Props) {
|
|||
const siteId = projectsStore.siteId;
|
||||
const isMobile = projectsStore.isMobile;
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = React.useState(false);
|
||||
|
||||
const handleModalOpen = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
let menu: any[] = React.useMemo(() => {
|
||||
const sourceMenu = isPreferencesActive ? preferences : main_menu;
|
||||
|
||||
|
|
@ -103,7 +95,7 @@ function SideMenu(props: Props) {
|
|||
modules.includes(MODULES.RECOMMENDATIONS),
|
||||
item.key === MENU.FEATURE_FLAGS &&
|
||||
modules.includes(MODULES.FEATURE_FLAGS),
|
||||
item.key === MENU.NOTES && modules.includes(MODULES.NOTES),
|
||||
item.key === MENU.HIGHLIGHTS && modules.includes(MODULES.HIGHLIGHTS),
|
||||
item.key === MENU.LIVE_SESSIONS && (modules.includes(MODULES.ASSIST) || isMobile),
|
||||
item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS),
|
||||
item.key === MENU.USABILITY_TESTS && modules.includes(MODULES.USABILITY_TESTS),
|
||||
|
|
@ -130,7 +122,6 @@ function SideMenu(props: Props) {
|
|||
[MENU.SESSIONS]: () => withSiteId(routes.sessions(), siteId),
|
||||
[MENU.BOOKMARKS]: () => withSiteId(routes.bookmarks(), siteId),
|
||||
[MENU.VAULT]: () => withSiteId(routes.bookmarks(), siteId),
|
||||
[MENU.NOTES]: () => withSiteId(routes.notes(), siteId),
|
||||
[MENU.LIVE_SESSIONS]: () => withSiteId(routes.assist(), siteId),
|
||||
[MENU.DASHBOARDS]: () => withSiteId(routes.dashboard(), siteId),
|
||||
[MENU.CARDS]: () => withSiteId(routes.metrics(), siteId),
|
||||
|
|
@ -151,7 +142,7 @@ function SideMenu(props: Props) {
|
|||
[PREFERENCES_MENU.NOTIFICATIONS]: () => client(CLIENT_TABS.NOTIFICATIONS),
|
||||
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
|
||||
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
|
||||
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId),
|
||||
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId)
|
||||
};
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
|
|
@ -326,13 +317,7 @@ function SideMenu(props: Props) {
|
|||
))}
|
||||
</Menu>
|
||||
{spotOnly && !isPreferencesActive ? (
|
||||
<>
|
||||
<InitORCard onOpenModal={handleModalOpen} />
|
||||
<SpotToOpenReplayPrompt
|
||||
isVisible={isModalVisible}
|
||||
onCancel={handleModalClose}
|
||||
/>
|
||||
</>
|
||||
<SpotMenuItem isCollapsed={isCollapsed} />
|
||||
) : null}
|
||||
<SupportModal onClose={() => setSupportOpen(false)} open={supportOpen} />
|
||||
</>
|
||||
|
|
@ -340,3 +325,32 @@ function SideMenu(props: Props) {
|
|||
}
|
||||
|
||||
export default withRouter(observer(SideMenu));
|
||||
|
||||
|
||||
const SpotMenuItem = ({ isCollapsed }: any) => {
|
||||
const [isModalVisible, setIsModalVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpotToOpenReplayPrompt
|
||||
isVisible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
/>
|
||||
{isCollapsed ? (
|
||||
<Popover
|
||||
content={<InitORCard onOpenModal={() => setIsModalVisible(true)} />}
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
>
|
||||
<Button type="text" className="ml-2 mt-2 py-2">
|
||||
<AnimatedSVG name={ICONS.LOGO_SMALL} size={20} />
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
<>
|
||||
<InitORCard onOpenModal={() => setIsModalVisible(true)} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export const enum MENU {
|
|||
RECOMMENDATIONS = 'recommendations',
|
||||
VAULT = 'vault',
|
||||
BOOKMARKS = 'bookmarks',
|
||||
NOTES = 'notes',
|
||||
HIGHLIGHTS = 'highlights',
|
||||
LIVE_SESSIONS = 'live-sessions',
|
||||
DASHBOARDS = 'dashboards',
|
||||
|
|
@ -64,7 +63,6 @@ export const categories: Category[] = [
|
|||
{ label: 'Recommendations', key: MENU.RECOMMENDATIONS, icon: 'magic', hidden: true },
|
||||
{ label: 'Vault', key: MENU.VAULT, icon: 'safe', hidden: true },
|
||||
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'bookmark' },
|
||||
//{ label: 'Notes', key: MENU.NOTES, icon: 'stickies' },
|
||||
{ label: 'Highlights', key: MENU.HIGHLIGHTS, icon: 'chat-square-quote' }
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export default class FilterStore {
|
|||
this.topValues[key] = vals?.filter((value) => value !== null && value.value !== '');
|
||||
};
|
||||
|
||||
resetValues = () => {
|
||||
this.topValues = {};
|
||||
}
|
||||
|
||||
fetchTopValues = async (key: string, source?: string) => {
|
||||
if (this.topValues.hasOwnProperty(key)) {
|
||||
return Promise.resolve(this.topValues[key]);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface IFilter {
|
|||
eventsHeader: string;
|
||||
page: number;
|
||||
limit: number;
|
||||
autoOpen: boolean;
|
||||
|
||||
merge(filter: any): void;
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ export default class Filter implements IFilter {
|
|||
|
||||
filterId: string = '';
|
||||
name: string = '';
|
||||
autoOpen = false;
|
||||
filters: FilterItem[] = [];
|
||||
excludes: FilterItem[] = [];
|
||||
eventsOrder: string = 'then';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { pageUrlOperators } from '../../constants/filterOptions';
|
|||
export default class FilterItem {
|
||||
type: string = '';
|
||||
category: FilterCategory = FilterCategory.METADATA;
|
||||
subCategory: string = '';
|
||||
key: string = '';
|
||||
label: string = '';
|
||||
value: any = [''];
|
||||
|
|
@ -63,6 +64,7 @@ export default class FilterItem {
|
|||
this.operatorOptions = data.operatorOptions;
|
||||
this.hasSource = data.hasSource;
|
||||
this.category = data.category;
|
||||
this.subCategory = data.subCategory;
|
||||
this.sourceOperatorOptions = data.sourceOperatorOptions;
|
||||
this.value = data.value;
|
||||
this.isEvent = Boolean(data.isEvent);
|
||||
|
|
@ -109,6 +111,7 @@ export default class FilterItem {
|
|||
this.operatorOptions = _filter.operatorOptions;
|
||||
this.hasSource = _filter.hasSource;
|
||||
this.category = _filter.category;
|
||||
this.subCategory = _filter.subCategory;
|
||||
this.sourceOperatorOptions = _filter.sourceOperatorOptions;
|
||||
if (isHeatmap && this.key === FilterKey.LOCATION) {
|
||||
this.operatorOptions = pageUrlOperators;
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ export default class Widget {
|
|||
updateStartPoint(startPoint: any) {
|
||||
runInAction(() => {
|
||||
this.startPoint = new FilterItem(startPoint);
|
||||
this.hasChanged = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -314,6 +315,20 @@ export default class Widget {
|
|||
}
|
||||
|
||||
if (this.metricType === HEATMAP) {
|
||||
const defaults = {
|
||||
domURL: undefined,
|
||||
duration: 0,
|
||||
events: [],
|
||||
mobsUrl: [],
|
||||
path: '',
|
||||
projectId: 0,
|
||||
sessionId: null,
|
||||
startTs: 0
|
||||
};
|
||||
if (!data || !data.domURL) {
|
||||
this.data = defaults;
|
||||
}
|
||||
Object.assign(this.data, data);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,11 @@ function getRange(rangeName, offset) {
|
|||
const now = DateTime.now().setZone(offset);
|
||||
switch (rangeName) {
|
||||
case TODAY:
|
||||
return Interval.fromDateTimes(now.startOf("day"), now.endOf("day"));
|
||||
return Interval.fromDateTimes(now.startOf("day"), now.plus({ days:1 }).startOf("day"));
|
||||
case YESTERDAY:
|
||||
const yesterday = now.minus({ days: 1 });
|
||||
return Interval.fromDateTimes(
|
||||
yesterday.startOf("day"),
|
||||
yesterday.endOf("day")
|
||||
now.minus({ days: 1 }).startOf("day"),
|
||||
now.startOf("day")
|
||||
);
|
||||
case LAST_24_HOURS:
|
||||
return Interval.fromDateTimes(now.minus({ hours: 24 }), now);
|
||||
|
|
@ -36,13 +35,13 @@ function getRange(rangeName, offset) {
|
|||
);
|
||||
case LAST_7_DAYS:
|
||||
return Interval.fromDateTimes(
|
||||
now.minus({ days: 7 }).endOf("day"),
|
||||
now.endOf("day")
|
||||
now.minus({ days: 6 }).startOf("day"),
|
||||
now.plus({ days: 1 }).startOf("day")
|
||||
);
|
||||
case LAST_30_DAYS:
|
||||
return Interval.fromDateTimes(
|
||||
now.minus({ days: 30 }).startOf("day"),
|
||||
now.endOf("day")
|
||||
now.minus({ days: 29 }).startOf("day"),
|
||||
now.plus({ days: 1 }).startOf("day")
|
||||
);
|
||||
case THIS_MONTH:
|
||||
return Interval.fromDateTimes(now.startOf("month"), now.endOf("month"));
|
||||
|
|
@ -55,13 +54,13 @@ function getRange(rangeName, offset) {
|
|||
return Interval.fromDateTimes(now.minus({ hours: 48 }), now.minus({ hours: 24 }));
|
||||
case PREV_7_DAYS:
|
||||
return Interval.fromDateTimes(
|
||||
now.minus({ days: 14 }).startOf("day"),
|
||||
now.minus({ days: 7 }).endOf("day")
|
||||
now.minus({ days: 13 }).startOf("day"),
|
||||
now.minus({ days: 6 }).startOf("day")
|
||||
);
|
||||
case PREV_30_DAYS:
|
||||
return Interval.fromDateTimes(
|
||||
now.minus({ days: 60 }).startOf("day"),
|
||||
now.minus({ days: 30 }).endOf("day")
|
||||
now.minus({ days: 59 }).startOf("day"),
|
||||
now.minus({ days: 29 }).startOf("day")
|
||||
);
|
||||
default:
|
||||
return Interval.fromDateTimes(now, now);
|
||||
|
|
|
|||
|
|
@ -902,7 +902,8 @@ export const clickmapFilter = {
|
|||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.EVENTS,
|
||||
subCategory: FilterCategory.AUTOCAPTURE,
|
||||
label: 'Visited URL', placeholder: 'Enter URL or path',
|
||||
label: 'Visited URL',
|
||||
placeholder: 'Enter URL or path',
|
||||
operator: filterOptions.pageUrlOperators[0].value,
|
||||
operatorOptions: filterOptions.pageUrlOperators,
|
||||
icon: 'filters/location',
|
||||
|
|
|
|||
|
|
@ -72,9 +72,9 @@ service:
|
|||
dataPort: 8123
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 1
|
||||
memory: 4Gi
|
||||
requests: {}
|
||||
# cpu: 1
|
||||
# memory: 4Gi
|
||||
limits: {}
|
||||
|
||||
nodeSelector: {}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ kafka:
|
|||
# Enterprise dbs
|
||||
clickhouse:
|
||||
image:
|
||||
tag: "24.12-alpine"
|
||||
tag: "25.1-alpine"
|
||||
enabled: false
|
||||
|
||||
postgreql:
|
||||
|
|
|
|||
|
|
@ -227,8 +227,8 @@ spec:
|
|||
- -c
|
||||
args:
|
||||
- |
|
||||
lowVersion=24.9
|
||||
highVersion=24
|
||||
lowVersion=25.1
|
||||
highVersion=25
|
||||
[[ "${CH_PASSWORD}" == "" ]] || {
|
||||
CH_PASSWORD="--password $CH_PASSWORD"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@ global:
|
|||
pvcRWXName: "hostPath"
|
||||
s3:
|
||||
region: "us-east-1"
|
||||
# if you're using iam roles for authentication, keep the value empty.
|
||||
# endpoint: ""
|
||||
endpoint: "http://minio.db.svc.cluster.local:9000"
|
||||
assetsBucket: "sessions-assets"
|
||||
recordingsBucket: "mobs"
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ DROP TABLE IF EXISTS public.user_favorite_errors;
|
|||
DROP TABLE IF EXISTS public.user_viewed_errors;
|
||||
|
||||
ALTER TABLE IF EXISTS public.sessions_notes
|
||||
ADD COLUMN start_at integer,
|
||||
ADD COLUMN end_at integer,
|
||||
ADD COLUMN thumbnail text,
|
||||
ADD COLUMN updated_at timestamp DEFAULT NULL,
|
||||
ADD COLUMN IF NOT EXISTS start_at integer,
|
||||
ADD COLUMN IF NOT EXISTS end_at integer,
|
||||
ADD COLUMN IF NOT EXISTS thumbnail text,
|
||||
ADD COLUMN IF NOT EXISTS updated_at timestamp DEFAULT NULL,
|
||||
ALTER COLUMN message DROP NOT NULL;
|
||||
|
||||
DELETE
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
- update medv/finder to 4.0.2 for better support of css-in-js libs
|
||||
- fixes for single tab recording
|
||||
- add option to disable network completely `{ network: { disabled: true } }`
|
||||
- fix for batching during offline recording syncs
|
||||
|
||||
## 15.0.4
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "15.0.5",
|
||||
"version": "15.0.5-beta.1",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -918,7 +918,6 @@ export default class App {
|
|||
private postToWorker(messages: Array<Message>) {
|
||||
this.worker?.postMessage(messages)
|
||||
this.commitCallbacks.forEach((cb) => cb(messages))
|
||||
messages.length = 0
|
||||
}
|
||||
|
||||
private delay = 0
|
||||
|
|
@ -1638,16 +1637,18 @@ export default class App {
|
|||
|
||||
flushBuffer = async (buffer: Message[]) => {
|
||||
return new Promise((res) => {
|
||||
let ended = false
|
||||
const messagesBatch: Message[] = [buffer.shift() as unknown as Message]
|
||||
while (!ended) {
|
||||
const nextMsg = buffer[0]
|
||||
if (!nextMsg || nextMsg[0] === MType.Timestamp) {
|
||||
ended = true
|
||||
} else {
|
||||
messagesBatch.push(buffer.shift() as unknown as Message)
|
||||
}
|
||||
if (buffer.length === 0) {
|
||||
res(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Since the first element is always a Timestamp, include it by default.
|
||||
let endIndex = 1
|
||||
while (endIndex < buffer.length && buffer[endIndex][0] !== MType.Timestamp) {
|
||||
endIndex++
|
||||
}
|
||||
|
||||
const messagesBatch = buffer.splice(0, endIndex)
|
||||
this.postToWorker(messagesBatch)
|
||||
res(null)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import App, { DEFAULT_INGEST_POINT } from './app/index.js'
|
||||
import App from './app/index.js'
|
||||
|
||||
export { default as App } from './app/index.js'
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue