pulled dev

This commit is contained in:
Андрей Бабушкин 2025-02-21 11:10:10 +01:00
commit f45821cbb3
64 changed files with 383 additions and 210 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,7 +53,7 @@ function ORBarChart(props: BarChartProps) {
type: 'value',
data: undefined,
name: props.label ?? 'Number of Sessions',
nameLocation: 'middle',
nameLocation: 'center',
nameGap: 45,
};

View file

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

View file

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

View file

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

View file

@ -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">&#8592;</span> ${sourceName}
<span class="text-base" style="color:#394eff">&#8592;</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">&#8594;</span> ${targetName}
<span class="text-base" style="color:#394eff">&#8594;</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;
};
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,8 @@ export const renderClickmapThumbnail = () => {
return html2canvas(
element,
{
allowTaint: false,
logging: true,
scale: 1,
// allowTaint: true,
useCORS: true,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ function LiveSessionSearch() {
}, []);
const onAddFilter = (filter: any) => {
filter.autoOpen = true;
searchStoreLive.addFilter(filter);
};

View file

@ -57,6 +57,7 @@ function SessionFilters() {
}, [appliedFilter.filters]);
const onAddFilter = (filter: any) => {
filter.autoOpen = true;
searchStore.addFilter(filter);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -72,9 +72,9 @@ service:
dataPort: 8123
resources:
requests:
cpu: 1
memory: 4Gi
requests: {}
# cpu: 1
# memory: 4Gi
limits: {}
nodeSelector: {}

View file

@ -166,7 +166,7 @@ kafka:
# Enterprise dbs
clickhouse:
image:
tag: "24.12-alpine"
tag: "25.1-alpine"
enabled: false
postgreql:

View file

@ -227,8 +227,8 @@ spec:
- -c
args:
- |
lowVersion=24.9
highVersion=24
lowVersion=25.1
highVersion=25
[[ "${CH_PASSWORD}" == "" ]] || {
CH_PASSWORD="--password $CH_PASSWORD"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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