Merge branch 'main' into dev
This commit is contained in:
commit
9210897bf5
64 changed files with 787 additions and 662 deletions
|
|
@ -1,6 +1,7 @@
|
|||
FROM python:3.10-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
RUN apk add --no-cache build-base nodejs npm tini
|
||||
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
ARG envarg
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from routers.crons import core_crons
|
|||
from routers.crons import core_dynamic_crons
|
||||
from routers.subs import dashboard, insights, metrics, v1_api
|
||||
|
||||
app = FastAPI(root_path="/api")
|
||||
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from fastapi import FastAPI
|
|||
|
||||
from chalicelib.core import alerts_processor
|
||||
|
||||
app = FastAPI()
|
||||
app = FastAPI(root_path="/alerts", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
|
||||
print("============= ALERTS =============")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ def get_live_sessions_ws(project_id, body: schemas.LiveSessionsSearchPayloadSche
|
|||
}
|
||||
for f in body.filters:
|
||||
if f.type == schemas.LiveFilterType.metadata:
|
||||
data["filter"][f.source] = f.value
|
||||
data["filter"][f.source] = {"values": f.value, "operator": f.operator}
|
||||
|
||||
else:
|
||||
data["filter"][f.type.value] = f.value
|
||||
data["filter"][f.type.value] = {"values": f.value, "operator": f.operator}
|
||||
return __get_live_sessions_ws(project_id=project_id, data=data)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -390,14 +390,14 @@ def search2_series(data: schemas.SessionsSearchPayloadSchema, project_id: int, d
|
|||
|
||||
def __is_valid_event(is_any: bool, event: schemas._SessionSearchEventSchema):
|
||||
return not (not is_any and len(event.value) == 0 and event.type not in [schemas.EventType.request_details,
|
||||
schemas.EventType.graphql_details] \
|
||||
schemas.EventType.graphql] \
|
||||
or event.type in [schemas.PerformanceEventType.location_dom_complete,
|
||||
schemas.PerformanceEventType.location_largest_contentful_paint_time,
|
||||
schemas.PerformanceEventType.location_ttfb,
|
||||
schemas.PerformanceEventType.location_avg_cpu_load,
|
||||
schemas.PerformanceEventType.location_avg_memory_usage
|
||||
] and (event.source is None or len(event.source) == 0) \
|
||||
or event.type in [schemas.EventType.request_details, schemas.EventType.graphql_details] and (
|
||||
or event.type in [schemas.EventType.request_details, schemas.EventType.graphql] and (
|
||||
event.filters is None or len(event.filters) == 0))
|
||||
|
||||
|
||||
|
|
@ -698,12 +698,12 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
event_where.append(
|
||||
_multiple_conditions(f"main.{events.event_type.REQUEST.column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
elif event_type == events.event_type.GRAPHQL.ui_type:
|
||||
event_from = event_from % f"{events.event_type.GRAPHQL.table} AS main "
|
||||
if not is_any:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.{events.event_type.GRAPHQL.column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
# elif event_type == events.event_type.GRAPHQL.ui_type:
|
||||
# event_from = event_from % f"{events.event_type.GRAPHQL.table} AS main "
|
||||
# if not is_any:
|
||||
# event_where.append(
|
||||
# _multiple_conditions(f"main.{events.event_type.GRAPHQL.column} {op} %({e_k})s", event.value,
|
||||
# value_key=e_k))
|
||||
elif event_type == events.event_type.STATEACTION.ui_type:
|
||||
event_from = event_from % f"{events.event_type.STATEACTION.table} AS main "
|
||||
if not is_any:
|
||||
|
|
@ -891,7 +891,7 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
print(f"undefined FETCH filter: {f.type}")
|
||||
if not apply:
|
||||
continue
|
||||
elif event_type == schemas.EventType.graphql_details:
|
||||
elif event_type == schemas.EventType.graphql:
|
||||
event_from = event_from % f"{events.event_type.GRAPHQL.table} AS main "
|
||||
for j, f in enumerate(event.filters):
|
||||
is_any = _isAny_opreator(f.operator)
|
||||
|
|
|
|||
|
|
@ -241,7 +241,6 @@ def get(user_id, tenant_id):
|
|||
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
api_key,
|
||||
TRUE AS has_password
|
||||
FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
|
||||
WHERE
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ def cron():
|
|||
if not helper.has_smtp():
|
||||
print("!!! No SMTP configuration found, ignoring weekly report")
|
||||
return
|
||||
with pg_client.PostgresClient(long_query=True) as cur:
|
||||
_now = TimeUTC.now()
|
||||
with pg_client.PostgresClient(unlimited_query=True) as cur:
|
||||
params = {"tomorrow": TimeUTC.midnight(delta_days=1),
|
||||
"3_days_ago": TimeUTC.midnight(delta_days=-3),
|
||||
"1_week_ago": TimeUTC.midnight(delta_days=-7),
|
||||
|
|
@ -86,6 +87,9 @@ def cron():
|
|||
AND issues.timestamp >= %(5_week_ago)s
|
||||
) AS month_1_issues ON (TRUE);"""), params)
|
||||
projects_data = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report query: {_now2 - _now} ms")
|
||||
_now = _now2
|
||||
emails_to_send = []
|
||||
for p in projects_data:
|
||||
params["project_id"] = p["project_id"]
|
||||
|
|
@ -116,6 +120,9 @@ def cron():
|
|||
) AS timestamp_i
|
||||
ORDER BY timestamp_i;""", params))
|
||||
days_partition = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
max_days_partition = max(x['issues_count'] for x in days_partition)
|
||||
for d in days_partition:
|
||||
if max_days_partition <= 0:
|
||||
|
|
@ -132,6 +139,9 @@ def cron():
|
|||
ORDER BY count DESC, type
|
||||
LIMIT 4;""", params))
|
||||
issues_by_type = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
max_issues_by_type = sum(i["count"] for i in issues_by_type)
|
||||
for i in issues_by_type:
|
||||
i["type"] = get_issue_title(i["type"])
|
||||
|
|
@ -161,6 +171,9 @@ def cron():
|
|||
GROUP BY timestamp_i
|
||||
ORDER BY timestamp_i;""", params))
|
||||
issues_breakdown_by_day = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
for i in issues_breakdown_by_day:
|
||||
i["sum"] = sum(x["count"] for x in i["partition"])
|
||||
for j in i["partition"]:
|
||||
|
|
@ -207,6 +220,9 @@ def cron():
|
|||
GROUP BY type
|
||||
ORDER BY issue_count DESC;""", params))
|
||||
issues_breakdown_list = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
if len(issues_breakdown_list) > 4:
|
||||
others = {"type": "Others",
|
||||
"sessions_count": sum(i["sessions_count"] for i in issues_breakdown_list[4:]),
|
||||
|
|
|
|||
|
|
@ -75,9 +75,11 @@ class PostgresClient:
|
|||
connection = None
|
||||
cursor = None
|
||||
long_query = False
|
||||
unlimited_query = False
|
||||
|
||||
def __init__(self, long_query=False, unlimited_query=False):
|
||||
self.long_query = long_query
|
||||
self.unlimited_query = unlimited_query
|
||||
if unlimited_query:
|
||||
long_config = dict(_PG_CONFIG)
|
||||
long_config["application_name"] += "-UNLIMITED"
|
||||
|
|
@ -85,7 +87,7 @@ class PostgresClient:
|
|||
elif long_query:
|
||||
long_config = dict(_PG_CONFIG)
|
||||
long_config["application_name"] += "-LONG"
|
||||
long_config["options"] = f"-c statement_timeout={config('pg_long_timeout', cast=int, default=5*60) * 1000}"
|
||||
long_config["options"] = f"-c statement_timeout={config('pg_long_timeout', cast=int, default=5 * 60) * 1000}"
|
||||
self.connection = psycopg2.connect(**long_config)
|
||||
else:
|
||||
self.connection = postgreSQL_pool.getconn()
|
||||
|
|
@ -99,11 +101,11 @@ class PostgresClient:
|
|||
try:
|
||||
self.connection.commit()
|
||||
self.cursor.close()
|
||||
if self.long_query:
|
||||
if self.long_query or self.unlimited_query:
|
||||
self.connection.close()
|
||||
except Exception as error:
|
||||
print("Error while committing/closing PG-connection", error)
|
||||
if str(error) == "connection already closed":
|
||||
if str(error) == "connection already closed" and not self.long_query and not self.unlimited_query:
|
||||
print("Recreating the connexion pool")
|
||||
make_pool()
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1171,4 +1171,5 @@ def get_limits(context: schemas.CurrentContext = Depends(OR_context)):
|
|||
@public_app.put('/', tags=["health"])
|
||||
@public_app.delete('/', tags=["health"])
|
||||
def health_check():
|
||||
return {"data": f"live {config('version_number', default='')}"}
|
||||
return {"data": {"stage": f"live {config('version_number', default='')}",
|
||||
"internalCrons": config("LOCAL_CRONS", default=False, cast=bool)}}
|
||||
|
|
|
|||
|
|
@ -389,7 +389,6 @@ class EventType(str, Enum):
|
|||
request = "REQUEST"
|
||||
request_details = "FETCH"
|
||||
graphql = "GRAPHQL"
|
||||
graphql_details = "GRAPHQL_DETAILS"
|
||||
state_action = "STATEACTION"
|
||||
error = "ERROR"
|
||||
click_ios = "CLICK_IOS"
|
||||
|
|
@ -568,9 +567,9 @@ class _SessionSearchEventRaw(__MixedSearchFilter):
|
|||
elif values.get("type") == EventType.request_details:
|
||||
assert isinstance(values.get("filters"), List) and len(values.get("filters", [])) > 0, \
|
||||
f"filters should be defined for {EventType.request_details.value}"
|
||||
elif values.get("type") == EventType.graphql_details:
|
||||
elif values.get("type") == EventType.graphql:
|
||||
assert isinstance(values.get("filters"), List) and len(values.get("filters", [])) > 0, \
|
||||
f"filters should be defined for {EventType.graphql_details.value}"
|
||||
f"filters should be defined for {EventType.graphql.value}"
|
||||
|
||||
return values
|
||||
|
||||
|
|
@ -1032,6 +1031,8 @@ class LiveSessionSearchFilterSchema(BaseModel):
|
|||
value: Union[List[str], str] = Field(...)
|
||||
type: LiveFilterType = Field(...)
|
||||
source: Optional[str] = Field(None)
|
||||
operator: Literal[SearchEventOperator._is.value,
|
||||
SearchEventOperator._contains.value] = Field(SearchEventOperator._contains.value)
|
||||
|
||||
@root_validator
|
||||
def validator(cls, values):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
FROM python:3.10-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec nodejs npm tini
|
||||
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
ARG envarg
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from routers.crons import core_crons
|
|||
from routers.crons import core_dynamic_crons
|
||||
from routers.subs import dashboard, insights, metrics, v1_api_ee
|
||||
|
||||
app = FastAPI(root_path="/api")
|
||||
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
|
|
|
|||
|
|
@ -274,7 +274,6 @@ def get(user_id, tenant_id):
|
|||
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
|
||||
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
|
||||
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member,
|
||||
api_key,
|
||||
origin,
|
||||
role_id,
|
||||
roles.name AS role_name,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ def cron():
|
|||
if not helper.has_smtp():
|
||||
print("!!! No SMTP configuration found, ignoring weekly report")
|
||||
return
|
||||
with pg_client.PostgresClient(long_query=True) as cur:
|
||||
_now = TimeUTC.now()
|
||||
with pg_client.PostgresClient(unlimited_query=True) as cur:
|
||||
params = {"tomorrow": TimeUTC.midnight(delta_days=1),
|
||||
"3_days_ago": TimeUTC.midnight(delta_days=-3),
|
||||
"1_week_ago": TimeUTC.midnight(delta_days=-7),
|
||||
|
|
@ -87,6 +88,9 @@ def cron():
|
|||
AND issues.timestamp >= %(5_week_ago)s
|
||||
) AS month_1_issues ON (TRUE);"""), params)
|
||||
projects_data = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report query: {_now2 - _now} ms")
|
||||
_now = _now2
|
||||
emails_to_send = []
|
||||
for p in projects_data:
|
||||
params["project_id"] = p["project_id"]
|
||||
|
|
@ -117,6 +121,9 @@ def cron():
|
|||
) AS timestamp_i
|
||||
ORDER BY timestamp_i;""", params))
|
||||
days_partition = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
max_days_partition = max(x['issues_count'] for x in days_partition)
|
||||
for d in days_partition:
|
||||
if max_days_partition <= 0:
|
||||
|
|
@ -133,6 +140,9 @@ def cron():
|
|||
ORDER BY count DESC, type
|
||||
LIMIT 4;""", params))
|
||||
issues_by_type = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
max_issues_by_type = sum(i["count"] for i in issues_by_type)
|
||||
for i in issues_by_type:
|
||||
i["type"] = get_issue_title(i["type"])
|
||||
|
|
@ -162,6 +172,9 @@ def cron():
|
|||
GROUP BY timestamp_i
|
||||
ORDER BY timestamp_i;""", params))
|
||||
issues_breakdown_by_day = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
for i in issues_breakdown_by_day:
|
||||
i["sum"] = sum(x["count"] for x in i["partition"])
|
||||
for j in i["partition"]:
|
||||
|
|
@ -208,6 +221,9 @@ def cron():
|
|||
GROUP BY type
|
||||
ORDER BY issue_count DESC;""", params))
|
||||
issues_breakdown_list = cur.fetchall()
|
||||
_now2 = TimeUTC.now()
|
||||
print(f">> Weekly report s-query-1: {_now2 - _now} ms project_id: {p['project_id']}")
|
||||
_now = _now2
|
||||
if len(issues_breakdown_list) > 4:
|
||||
others = {"type": "Others",
|
||||
"sessions_count": sum(i["sessions_count"] for i in issues_breakdown_list[4:]),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh
|
||||
sh env_vars.sh
|
||||
source .env.override
|
||||
source /tmp/.env.override
|
||||
cd sourcemap-reader
|
||||
nohup npm start &> /tmp/sourcemap-reader.log &
|
||||
cd ..
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
sh env_vars.sh
|
||||
source .env.override
|
||||
source /tmp/.env.override
|
||||
uvicorn app:app --host 0.0.0.0 --reload
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
sh env_vars.sh
|
||||
source .env.override
|
||||
source /tmp/.env.override
|
||||
python app_crons.py $ACTION
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
#!/bin/sh
|
||||
|
||||
touch .env.override
|
||||
touch /tmp/.env.override
|
||||
if [[ -z "${ENV_CONFIG_OVERRIDE_PATH}" ]]; then
|
||||
echo 'no env-override'
|
||||
else
|
||||
override=$ENV_CONFIG_OVERRIDE_PATH
|
||||
if [ -f "$override" ]; then
|
||||
cp $override .env.override
|
||||
cp $override /tmp/.env.override
|
||||
else
|
||||
echo "$override does not exist."
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ ALTER TABLE IF EXISTS events.resources
|
|||
PRIMARY KEY (session_id, message_id, timestamp);
|
||||
|
||||
COMMIT;
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS autocomplete_unique_project_id_md5value_type_idx ON autocomplete (project_id, md5(value), type);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS projects_tenant_id_idx ON public.projects (tenant_id);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS projects_project_id_deleted_at_n_idx ON public.projects (project_id) WHERE deleted_at IS NULL;
|
||||
ALTER TYPE metric_type ADD VALUE IF NOT EXISTS 'funnel';
|
||||
|
|
@ -211,4 +212,5 @@ $$
|
|||
END IF;
|
||||
END
|
||||
$$;
|
||||
DROP INDEX IF EXISTS autocomplete_unique;
|
||||
COMMIT;
|
||||
|
|
@ -658,7 +658,7 @@ $$
|
|||
project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE unique index IF NOT EXISTS autocomplete_unique ON autocomplete (project_id, value, type);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS autocomplete_unique_project_id_md5value_type_idx ON autocomplete (project_id, md5(value), type);
|
||||
CREATE index IF NOT EXISTS autocomplete_project_id_idx ON autocomplete (project_id);
|
||||
CREATE INDEX IF NOT EXISTS autocomplete_type_idx ON public.autocomplete (type);
|
||||
|
||||
|
|
|
|||
12
ee/utilities/package-lock.json
generated
12
ee/utilities/package-lock.json
generated
|
|
@ -112,9 +112,9 @@
|
|||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz",
|
||||
"integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ=="
|
||||
"version": "18.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
|
||||
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
|
|
@ -1179,9 +1179,9 @@
|
|||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz",
|
||||
"integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ=="
|
||||
"version": "18.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
|
||||
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.8",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ const extractPayloadFromRequest = async function (req, res) {
|
|||
return helper.extractPayloadFromRequest(req);
|
||||
}
|
||||
filters.filter = helper.objectToObjectOfArrays(filters.filter);
|
||||
filters.filter = helper.transformFilters(filters.filter);
|
||||
debug && console.log("payload/filters:" + JSON.stringify(filters))
|
||||
return Object.keys(filters).length > 0 ? filters : undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ function SessionList(props: Props) {
|
|||
|
||||
return (
|
||||
<div style={{ width: '50vw' }}>
|
||||
<div className="border-r shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}>
|
||||
<div className="border-r shadow h-screen overflow-y-auto" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}>
|
||||
<div className="p-4">
|
||||
<div className="text-2xl">
|
||||
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
|||
|
||||
// const ALL = 'all';
|
||||
const PER_PAGE = 10;
|
||||
const AUTOREFRESH_INTERVAL = 3 * 60 * 1000;
|
||||
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
|
||||
var timeoutId;
|
||||
|
||||
@connect(state => ({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import SortDropdown from '../Filters/SortDropdown';
|
|||
import { numberWithCommas } from 'App/utils';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { applyFilter } from 'Duck/search';
|
||||
import Period from 'Types/app/period';
|
||||
import Record from 'Types/app/period';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { moment } from 'App/dateRange';
|
||||
|
||||
const sortOptionsMap = {
|
||||
'startTs-desc': 'Newest',
|
||||
|
|
@ -15,13 +18,32 @@ const sortOptionsMap = {
|
|||
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label }));
|
||||
|
||||
function SessionListHeader({ activeTab, count, applyFilter, filter }) {
|
||||
const { settingsStore } = useStore();
|
||||
|
||||
const label = useObserver(() => settingsStore.sessionSettings.timezone.label);
|
||||
const getTimeZoneOffset = React.useCallback(() => {
|
||||
return label.slice(-6);
|
||||
}, [label]);
|
||||
|
||||
const { startDate, endDate, rangeValue } = filter;
|
||||
const period = new Period({ start: startDate, end: endDate, rangeName: rangeValue });
|
||||
const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() });
|
||||
|
||||
const onDateChange = (e) => {
|
||||
const dateValues = e.toJSON();
|
||||
dateValues.startDate = moment(dateValues.startDate).utcOffset(getTimeZoneOffset(), true).valueOf();
|
||||
dateValues.endDate = moment(dateValues.endDate).utcOffset(getTimeZoneOffset(), true).valueOf();
|
||||
applyFilter(dateValues);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (label) {
|
||||
const dateValues = period.toJSON();
|
||||
dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
|
||||
dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
|
||||
applyFilter(dateValues);
|
||||
}
|
||||
}, [label]);
|
||||
|
||||
return (
|
||||
<div className="flex mb-2 justify-between items-end">
|
||||
<div className="flex items-baseline">
|
||||
|
|
@ -32,7 +54,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) {
|
|||
{
|
||||
<div className="ml-3 flex items-center">
|
||||
<span className="mr-2 color-gray-medium">Sessions Captured in</span>
|
||||
<SelectDateRange period={period} onChange={onDateChange} />
|
||||
<SelectDateRange period={period} onChange={onDateChange} timezone={getTimeZoneOffset()} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export default class IntegrationForm extends React.PureComponent {
|
|||
|
||||
onChangeSelect = ({ value }) => {
|
||||
const { sites, list, name } = this.props;
|
||||
const site = sites.find(s => s.id === value);
|
||||
const site = sites.find(s => s.id === value.value);
|
||||
this.setState({ currentSiteId: site.id })
|
||||
this.init(value);
|
||||
this.init(value.value);
|
||||
}
|
||||
|
||||
init = (siteId) => {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ function UserForm(props: Props) {
|
|||
));
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
export default connect((state: any) => ({
|
||||
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
|
||||
isSmtp: state.getIn([ 'user', 'account', 'smtp' ]),
|
||||
}))(UserForm);
|
||||
|
|
@ -6,52 +6,55 @@ import cls from './distributionBar.module.css';
|
|||
import { colorScale } from 'App/utils';
|
||||
|
||||
function DistributionBar({ className, title, partitions }) {
|
||||
if (partitions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (partitions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = Array(partitions.length).fill().map((element, index) => index + 0);
|
||||
const colors = colorScale(values, Styles.colors);
|
||||
const values = Array(partitions.length)
|
||||
.fill()
|
||||
.map((element, index) => index + 0);
|
||||
const colors = colorScale(values, Styles.colors);
|
||||
|
||||
return (
|
||||
<div className={ className } >
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<div className="capitalize">{ title }</div>
|
||||
<div className="flex items-center">
|
||||
<div className="font-thin capitalize" style={{ maxWidth: '80px', height: '19px'}}>
|
||||
<TextEllipsis
|
||||
text={ partitions[0].label }
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2">{ `${ Math.round(partitions[0].prc) }% ` }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ cn("border-radius-3 overflow-hidden flex", cls.bar) }>
|
||||
{ partitions.map((p, index) =>
|
||||
<Popup
|
||||
key={p.label}
|
||||
content={
|
||||
<div className="text-center">
|
||||
<span className="capitalize">{ p.label }</span><br/>
|
||||
{`${ Math.round(p.prc) }%`}
|
||||
</div>
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<div
|
||||
className="h-full bg-tealx"
|
||||
style={{
|
||||
marginLeft: '1px',
|
||||
width: `${ p.prc }%`,
|
||||
backgroundColor: colors(index)
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<div className="capitalize">{title}</div>
|
||||
<div className="flex items-center">
|
||||
<div className="font-thin capitalize" style={{ maxWidth: '80px', height: '19px' }}>
|
||||
<TextEllipsis text={partitions[0].label} />
|
||||
</div>
|
||||
<div className="ml-2">{`${Math.round(partitions[0].prc)}% `}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('border-radius-3 overflow-hidden flex', cls.bar)}>
|
||||
{partitions.map((p, index) => (
|
||||
<Popup
|
||||
key={p.label}
|
||||
content={
|
||||
<div className="text-center">
|
||||
<span className="capitalize">{p.label}</span>
|
||||
<br />
|
||||
{`${Math.round(p.prc)}%`}
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
marginLeft: '1px',
|
||||
width: `${p.prc}%`,
|
||||
backgroundColor: colors(index),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-tealx"
|
||||
style={{
|
||||
backgroundColor: colors(index),
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DistributionBar.displayName = "DistributionBar";
|
||||
export default DistributionBar;
|
||||
DistributionBar.displayName = 'DistributionBar';
|
||||
export default DistributionBar;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const Header = (props) => {
|
|||
}, [siteId])
|
||||
|
||||
return (
|
||||
<div className={ cn(styles.header) }>
|
||||
<div className={ cn(styles.header) } style={{ height: '50px'}}>
|
||||
<NavLink to={ withSiteId(SESSIONS_PATH, siteId) }>
|
||||
<div className="relative">
|
||||
<div className="p-2">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import stl from './installDocs.module.css'
|
|||
import cn from 'classnames'
|
||||
import Highlight from 'react-highlight'
|
||||
import CircleNumber from '../../CircleNumber'
|
||||
import { Slider, CopyButton } from 'UI'
|
||||
import { CopyButton } from 'UI'
|
||||
import { Toggler } from 'UI';
|
||||
|
||||
const installationCommand = 'npm i @openreplay/tracker'
|
||||
|
|
@ -30,8 +30,7 @@ function MyApp() {
|
|||
//...
|
||||
}`
|
||||
|
||||
function InstallDocs({ siteId, sites }) {
|
||||
const site = sites.find(s => s.id === siteId);
|
||||
function InstallDocs({ site }) {
|
||||
const _usageCode = usageCode.replace('PROJECT_KEY', site.projectKey)
|
||||
const _usageCodeSST = usageCodeSST.replace('PROJECT_KEY', site.projectKey)
|
||||
const [isSpa, setIsSpa] = useState(true)
|
||||
|
|
@ -98,9 +97,6 @@ function InstallDocs({ siteId, sites }) {
|
|||
)
|
||||
}
|
||||
|
||||
// export default InstallDocs
|
||||
|
||||
export default connect(state => ({
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
site: state.getIn([ 'site', 'instance' ]),
|
||||
}))(InstallDocs)
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Dropdown, Loader, Icon } from 'UI';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { Loader, Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchInsights } from 'Duck/sessions';
|
||||
import SelectorsList from './components/SelectorsList/SelectorsList';
|
||||
|
|
@ -11,100 +10,103 @@ import Period from 'Types/app/period';
|
|||
|
||||
const JUMP_OFFSET = 1000;
|
||||
interface Props {
|
||||
filters: any
|
||||
fetchInsights: (filters: Record<string, any>) => void
|
||||
urls: []
|
||||
insights: any
|
||||
events: Array<any>
|
||||
urlOptions: Array<any>
|
||||
loading: boolean
|
||||
host: string
|
||||
setActiveTab: (tab: string) => void
|
||||
filters: any;
|
||||
fetchInsights: (filters: Record<string, any>) => void;
|
||||
urls: [];
|
||||
insights: any;
|
||||
events: Array<any>;
|
||||
urlOptions: Array<any>;
|
||||
loading: boolean;
|
||||
host: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
function PageInsightsPanel({
|
||||
filters, fetchInsights, events = [], insights, urlOptions, host, loading = true, setActiveTab
|
||||
}: Props) {
|
||||
const [insightsFilters, setInsightsFilters] = useState(filters)
|
||||
const defaultValue = (urlOptions && urlOptions[0]) ? urlOptions[0].value : ''
|
||||
function PageInsightsPanel({ filters, fetchInsights, events = [], insights, urlOptions, host, loading = true, setActiveTab }: Props) {
|
||||
const [insightsFilters, setInsightsFilters] = useState(filters);
|
||||
const defaultValue = urlOptions && urlOptions[0] ? urlOptions[0].value : '';
|
||||
|
||||
const period = new Period({
|
||||
start: insightsFilters.startDate,
|
||||
end: insightsFilters.endDate,
|
||||
rangeName: insightsFilters.rangeValue
|
||||
});
|
||||
const period = Period({
|
||||
start: insightsFilters.startDate,
|
||||
end: insightsFilters.endDate,
|
||||
rangeName: insightsFilters.rangeValue,
|
||||
});
|
||||
|
||||
const onDateChange = (e) => {
|
||||
const { startDate, endDate, rangeValue } = e.toJSON();
|
||||
setInsightsFilters({ ...insightsFilters, startDate, endDate, rangeValue })
|
||||
}
|
||||
const onDateChange = (e: any) => {
|
||||
const { startDate, endDate, rangeValue } = e.toJSON();
|
||||
setInsightsFilters({ ...insightsFilters, startDate, endDate, rangeValue });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
markTargets(insights.toJS());
|
||||
return () => {
|
||||
markTargets(null)
|
||||
}
|
||||
}, [insights])
|
||||
useEffect(() => {
|
||||
markTargets(insights.toJS());
|
||||
return () => {
|
||||
markTargets(null);
|
||||
};
|
||||
}, [insights]);
|
||||
|
||||
useEffect(() => {
|
||||
if (urlOptions && urlOptions[0]) {
|
||||
const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value;
|
||||
Player.pause();
|
||||
fetchInsights({ ...insightsFilters, url })
|
||||
}
|
||||
}, [insightsFilters])
|
||||
useEffect(() => {
|
||||
if (urlOptions && urlOptions[0]) {
|
||||
const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value;
|
||||
Player.pause();
|
||||
fetchInsights({ ...insightsFilters, url });
|
||||
}
|
||||
}, [insightsFilters]);
|
||||
|
||||
const onPageSelect = ({ value }: { value: Array<any> }) => {
|
||||
const event = events.find(item => item.url === value)
|
||||
Player.jump(event.time + JUMP_OFFSET)
|
||||
setInsightsFilters({ ...insightsFilters, url: host + value })
|
||||
markTargets([])
|
||||
};
|
||||
const onPageSelect = ({ value }: any) => {
|
||||
const event = events.find((item) => item.url === value.value);
|
||||
Player.jump(event.time + JUMP_OFFSET);
|
||||
setInsightsFilters({ ...insightsFilters, url: host + value.value });
|
||||
markTargets([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white">
|
||||
<div className="pb-3 flex items-center" style={{ maxWidth: '241px', paddingTop: '5px' }}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1 text-xl">Clicks</span>
|
||||
<SelectDateRange period={period} onChange={onDateChange} disableCustom />
|
||||
return (
|
||||
<div className="p-4 bg-white">
|
||||
<div className="pb-3 flex items-center" style={{ maxWidth: '241px', paddingTop: '5px' }}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1 text-xl">Clicks</span>
|
||||
<SelectDateRange period={period} onChange={onDateChange} disableCustom />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
}}
|
||||
className="ml-auto flex items-center justify-center bg-white cursor-pointer"
|
||||
>
|
||||
<Icon name="close" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="mr-2 flex-shrink-0">In Page</div>
|
||||
<Select
|
||||
isSearchable={true}
|
||||
right
|
||||
placeholder="change"
|
||||
options={urlOptions}
|
||||
name="url"
|
||||
defaultValue={defaultValue}
|
||||
onChange={onPageSelect}
|
||||
id="change-dropdown"
|
||||
className="w-full"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<SelectorsList />
|
||||
</Loader>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => { setActiveTab(''); }}
|
||||
className="ml-auto flex items-center justify-center bg-white cursor-pointer"
|
||||
>
|
||||
<Icon name="close" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="mr-2 flex-shrink-0">In Page</div>
|
||||
<Select
|
||||
isSearchable={true}
|
||||
right
|
||||
placeholder="change"
|
||||
options={ urlOptions }
|
||||
name="url"
|
||||
defaultValue={defaultValue}
|
||||
onChange={ onPageSelect }
|
||||
id="change-dropdown"
|
||||
className="w-full"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<Loader loading={ loading }>
|
||||
<SelectorsList />
|
||||
</Loader>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => {
|
||||
const events = state.getIn([ 'sessions', 'visitedEvents' ])
|
||||
return {
|
||||
filters: state.getIn(['sessions', 'insightFilters']),
|
||||
host: state.getIn([ 'sessions', 'host' ]),
|
||||
insights: state.getIn([ 'sessions', 'insights' ]),
|
||||
events: events,
|
||||
urlOptions: events.map(({ url, host }: any) => ({ label: url, value: url, host })),
|
||||
loading: state.getIn([ 'sessions', 'fetchInsightsRequest', 'loading' ]),
|
||||
}
|
||||
}, { fetchInsights })(PageInsightsPanel);
|
||||
export default connect(
|
||||
(state) => {
|
||||
const events = state.getIn(['sessions', 'visitedEvents']);
|
||||
return {
|
||||
filters: state.getIn(['sessions', 'insightFilters']),
|
||||
host: state.getIn(['sessions', 'host']),
|
||||
insights: state.getIn(['sessions', 'insights']),
|
||||
events: events,
|
||||
urlOptions: events.map(({ url, host }: any) => ({ label: url, value: url, host })),
|
||||
loading: state.getIn(['sessions', 'fetchInsightsRequest', 'loading']),
|
||||
};
|
||||
},
|
||||
{ fetchInsights }
|
||||
)(PageInsightsPanel);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,34 @@
|
|||
import React, { useState } from 'react'
|
||||
import stl from './SelectorCard.module.css'
|
||||
import React, { useState } from 'react';
|
||||
import stl from './SelectorCard.module.css';
|
||||
import cn from 'classnames';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
import { activeTarget } from 'Player';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
interface Props {
|
||||
index?: number,
|
||||
target: MarkedTarget,
|
||||
showContent: boolean
|
||||
index?: number;
|
||||
target: MarkedTarget;
|
||||
showContent: boolean;
|
||||
}
|
||||
|
||||
export default function SelectorCard({ index = 1, target, showContent } : Props) {
|
||||
return (
|
||||
<div className={cn(stl.wrapper, { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
|
||||
<div className={stl.top}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip position='top' title="Rank of the most clicked element"><div className={stl.index}>{index + 1}</div></Tooltip>
|
||||
<div className="truncate">{target.selector}</div>
|
||||
</div>
|
||||
{ showContent && (
|
||||
<div className={stl.counts}>
|
||||
<div>{target.count} Clicks - {target.percent}%</div>
|
||||
<div className="color-gray-medium">TOTAL CLICKS</div>
|
||||
export default function SelectorCard({ index = 1, target, showContent }: Props) {
|
||||
return (
|
||||
<div className={cn(stl.wrapper, { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
|
||||
<div className={stl.top}>
|
||||
{/* @ts-ignore */}
|
||||
<Tooltip position="top" title="Rank of the most clicked element">
|
||||
<div className={stl.index}>{index + 1}</div>
|
||||
</Tooltip>
|
||||
<div className="truncate">{target.selector}</div>
|
||||
</div>
|
||||
{showContent && (
|
||||
<div className={stl.counts}>
|
||||
<div>
|
||||
{target.count} Clicks - {target.percent}%
|
||||
</div>
|
||||
<div className="color-gray-medium">TOTAL CLICKS</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,26 @@
|
|||
import React, { useState } from 'react'
|
||||
import { NoContent } from 'UI'
|
||||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { connectPlayer } from 'Player/store';
|
||||
import SelectorCard from '../SelectorCard/SelectorCard';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
import stl from './selectorList.module.css'
|
||||
import stl from './selectorList.module.css';
|
||||
|
||||
interface Props {
|
||||
targets: Array<MarkedTarget>,
|
||||
activeTargetIndex: number
|
||||
targets: Array<MarkedTarget>;
|
||||
activeTargetIndex: number;
|
||||
}
|
||||
|
||||
function SelectorsList({ targets, activeTargetIndex }: Props) {
|
||||
return (
|
||||
<NoContent
|
||||
title="No data available."
|
||||
size="small"
|
||||
show={ targets && targets.length === 0 }
|
||||
>
|
||||
<div className={stl.wrapper}>
|
||||
{ targets && targets.map((target, index) => (
|
||||
<SelectorCard target={target} index={index} showContent={activeTargetIndex === index} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
)
|
||||
function SelectorsList({ targets, activeTargetIndex }: Props) {
|
||||
return (
|
||||
<NoContent title="No data available." size="small" show={targets && targets.length === 0}>
|
||||
<div className={stl.wrapper}>
|
||||
{targets && targets.map((target, index) => <SelectorCard target={target} index={index} showContent={activeTargetIndex === index} />)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connectPlayer(state => ({
|
||||
targets: state.markedTargets,
|
||||
activeTargetIndex: state.activeTargetIndex,
|
||||
}))(SelectorsList)
|
||||
export default connectPlayer((state: any) => ({
|
||||
targets: state.markedTargets,
|
||||
activeTargetIndex: state.activeTargetIndex,
|
||||
}))(SelectorsList);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SubFilterItem from '../SubFilterItem';
|
|||
interface Props {
|
||||
filterIndex: number;
|
||||
filter: any; // event/filter
|
||||
onUpdate: (filter) => void;
|
||||
onUpdate: (filter: any) => void;
|
||||
onRemoveFilter: () => void;
|
||||
isFilter?: boolean;
|
||||
saveRequestPayloads?: boolean;
|
||||
|
|
@ -20,26 +20,26 @@ function FilterItem(props: Props) {
|
|||
const canShowValues = !(filter.operator === 'isAny' || filter.operator === 'onAny' || filter.operator === 'isUndefined');
|
||||
const isSubFilter = filter.type === FilterType.SUB_FILTERS;
|
||||
|
||||
const replaceFilter = (filter) => {
|
||||
const replaceFilter = (filter: any) => {
|
||||
props.onUpdate({
|
||||
...filter,
|
||||
value: [''],
|
||||
filters: filter.filters ? filter.filters.map((i) => ({ ...i, value: [''] })) : [],
|
||||
filters: filter.filters ? filter.filters.map((i: any) => ({ ...i, value: [''] })) : [],
|
||||
});
|
||||
};
|
||||
|
||||
const onOperatorChange = (e, { name, value }) => {
|
||||
const onOperatorChange = (e: any, { name, value }: any) => {
|
||||
props.onUpdate({ ...filter, operator: value.value });
|
||||
};
|
||||
|
||||
const onSourceOperatorChange = (e, { name, value }) => {
|
||||
const onSourceOperatorChange = (e: any, { name, value }: any) => {
|
||||
props.onUpdate({ ...filter, sourceOperator: value.value });
|
||||
};
|
||||
|
||||
const onUpdateSubFilter = (subFilter, subFilterIndex) => {
|
||||
const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => {
|
||||
props.onUpdate({
|
||||
...filter,
|
||||
filters: filter.filters.map((i, index) => {
|
||||
filters: filter.filters.map((i: any, index: any) => {
|
||||
if (index === subFilterIndex) {
|
||||
return subFilter;
|
||||
}
|
||||
|
|
@ -90,8 +90,8 @@ function FilterItem(props: Props) {
|
|||
{isSubFilter && (
|
||||
<div className="grid grid-col ml-3 w-full">
|
||||
{filter.filters
|
||||
.filter((i) => (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || saveRequestPayloads)
|
||||
.map((subFilter, subFilterIndex) => (
|
||||
.filter((i: any) => (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || saveRequestPayloads)
|
||||
.map((subFilter: any, subFilterIndex: any) => (
|
||||
<SubFilterItem
|
||||
filterIndex={subFilterIndex}
|
||||
filter={subFilter}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.inputField {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
/* margin-right: 10px; */
|
||||
border: solid thin $gray-light;
|
||||
border-radius: 3px;
|
||||
height: 26px;
|
||||
|
|
|
|||
|
|
@ -1,50 +1,41 @@
|
|||
import { FilterType } from 'App/types/filter/filterType';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import stl from './FilterSource.module.css';
|
||||
import { debounce } from 'App/utils';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
filter: any,
|
||||
onUpdate: (filter) => void;
|
||||
filter: any;
|
||||
onUpdate: (filter: any) => void;
|
||||
}
|
||||
function FilterSource(props: Props) {
|
||||
const { filter } = props;
|
||||
const [value, setValue] = useState(filter.source[0] || '');
|
||||
const { filter } = props;
|
||||
const [value, setValue] = useState(filter.source[0] || '');
|
||||
const debounceUpdate: any = React.useCallback(debounce(props.onUpdate, 1000), [props.onUpdate]);
|
||||
|
||||
const onChange = ({ target: { value, name } }) => {
|
||||
props.onUpdate({ ...filter, [name]: [value] })
|
||||
}
|
||||
useEffect(() => {
|
||||
setValue(filter.source[0] || '');
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(filter.source[0] || '');
|
||||
}, [filter])
|
||||
useEffect(() => {
|
||||
debounceUpdate({ ...filter, source: [value] });
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
props.onUpdate({ ...filter, source: [value] })
|
||||
}, [value])
|
||||
const write = ({ target: { value, name } }: any) => setValue(value);
|
||||
|
||||
const write = ({ target: { value, name } }) => setValue(value)
|
||||
const renderFiled = () => {
|
||||
switch (filter.sourceType) {
|
||||
case FilterType.NUMBER:
|
||||
return (
|
||||
<div className="relative">
|
||||
<input name="source" className={cn(stl.inputField, "rounded-l px-1 block")} value={value} onBlur={write} onChange={write} type="number" />
|
||||
<div className="absolute right-0 top-0 bottom-0 bg-gray-lightest rounded-r px-1 border-l border-color-gray-light flex items-center" style={{ margin: '1px', minWidth: '24px'}}>{filter.sourceUnit}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFiled = () => {
|
||||
switch(filter.sourceType) {
|
||||
case FilterType.NUMBER:
|
||||
return (
|
||||
<input
|
||||
name="source"
|
||||
className={stl.inputField}
|
||||
value={value}
|
||||
onBlur={write}
|
||||
onChange={write}
|
||||
type="number"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ renderFiled()}
|
||||
</div>
|
||||
);
|
||||
return <div>{renderFiled()}</div>;
|
||||
}
|
||||
|
||||
export default FilterSource;
|
||||
export default FilterSource;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import FilterValueDropdown from '../FilterValueDropdown';
|
|||
import FilterDuration from '../FilterDuration';
|
||||
import { debounce } from 'App/utils';
|
||||
import { assist as assistRoute, isRoute } from 'App/routes';
|
||||
import cn from 'classnames';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
|
|
@ -172,7 +173,8 @@ function FilterValue(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3 w-full">
|
||||
//
|
||||
<div className={cn("grid gap-3 w-full", { 'grid-cols-2': filter.hasSource, 'grid-cols-3' : !filter.hasSource })}>
|
||||
{filter.type === FilterType.DURATION
|
||||
? renderValueFiled(filter.value, 0)
|
||||
: filter.value &&
|
||||
|
|
|
|||
|
|
@ -6,17 +6,19 @@ import { components } from 'react-select';
|
|||
import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
period: any;
|
||||
onChange: (data: any) => void;
|
||||
disableCustom?: boolean;
|
||||
right?: boolean;
|
||||
timezone?: string;
|
||||
[x: string]: any;
|
||||
}
|
||||
function SelectDateRange(props: Props) {
|
||||
const [isCustom, setIsCustom] = React.useState(false);
|
||||
const { right = false, period, disableCustom = false, ...rest } = props;
|
||||
const { right = false, period, disableCustom = false, timezone, ...rest } = props;
|
||||
let selectedValue = DATE_RANGE_OPTIONS.find((obj: any) => obj.value === period.rangeName);
|
||||
const options = DATE_RANGE_OPTIONS.filter((obj: any) => (disableCustom ? obj.value !== CUSTOM_RANGE : true));
|
||||
|
||||
|
|
@ -24,15 +26,20 @@ function SelectDateRange(props: Props) {
|
|||
if (value === CUSTOM_RANGE) {
|
||||
setIsCustom(true);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
props.onChange(new Period({ rangeName: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const onApplyDateRange = (value: any) => {
|
||||
props.onChange(new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end }));
|
||||
// @ts-ignore
|
||||
const range = new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end })
|
||||
props.onChange(range);
|
||||
setIsCustom(false);
|
||||
};
|
||||
|
||||
const isCustomRange = period.rangeName === CUSTOM_RANGE;
|
||||
const customRange = isCustomRange ? period.rangeFormatted() : '';
|
||||
return (
|
||||
<div className="relative">
|
||||
<Select
|
||||
|
|
@ -44,7 +51,7 @@ function SelectDateRange(props: Props) {
|
|||
SingleValue: ({ children, ...props }: any) => {
|
||||
return (
|
||||
<components.SingleValue {...props}>
|
||||
{period.rangeName === CUSTOM_RANGE ? period.rangeFormatted() : children}
|
||||
{isCustomRange ? customRange : children}
|
||||
</components.SingleValue>
|
||||
);
|
||||
},
|
||||
|
|
@ -66,11 +73,9 @@ function SelectDateRange(props: Props) {
|
|||
className={cn('absolute top-0 mt-10 z-40', { 'right-0': right })}
|
||||
style={{
|
||||
width: '770px',
|
||||
// margin: 'auto 50vh 0',
|
||||
// transform: 'translateX(-50%)'
|
||||
}}
|
||||
>
|
||||
<DateRangePopup onApply={onApplyDateRange} onCancel={() => setIsCustom(false)} selectedDateRange={period.range} />
|
||||
<DateRangePopup timezone={timezone} onApply={onApplyDateRange} onCancel={() => setIsCustom(false)} selectedDateRange={period.range} />
|
||||
</div>
|
||||
</OutsideClickDetectingDiv>
|
||||
)}
|
||||
|
|
@ -78,4 +83,4 @@ function SelectDateRange(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default SelectDateRange;
|
||||
export default observer(SelectDateRange);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ interface Props {
|
|||
}
|
||||
|
||||
function Counter({ startTime, className }: Props) {
|
||||
let intervalId;
|
||||
const [duration, setDuration] = useState(new Date().getTime() - convertTimestampToUtcTimestamp(startTime));
|
||||
let intervalId: NodeJS.Timer;
|
||||
const [duration, setDuration] = useState(convertTimestampToUtcTimestamp(new Date().getTime()) - convertTimestampToUtcTimestamp(startTime));
|
||||
const formattedDuration = durationFormatted(Duration.fromMillis(duration));
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,42 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Link,
|
||||
Icon,
|
||||
} from 'UI';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, Icon } from 'UI';
|
||||
import { session as sessionRoute, liveSession as liveSessionRoute } from 'App/routes';
|
||||
|
||||
const PLAY_ICON_NAMES = {
|
||||
notPlayed: 'play-fill',
|
||||
played: 'play-circle-light',
|
||||
hovered: 'play-hover'
|
||||
}
|
||||
hovered: 'play-hover',
|
||||
};
|
||||
|
||||
const getDefaultIconName = (isViewed) => !isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played
|
||||
const getDefaultIconName = (isViewed: any) => (!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played);
|
||||
|
||||
interface Props {
|
||||
isAssist: boolean;
|
||||
viewed: boolean;
|
||||
sessionId: string;
|
||||
onClick?: () => void;
|
||||
queryParams: any;
|
||||
}
|
||||
export default function PlayLink(props: Props) {
|
||||
const { isAssist, viewed, sessionId, onClick = null } = props
|
||||
const defaultIconName = getDefaultIconName(viewed)
|
||||
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
|
||||
const defaultIconName = getDefaultIconName(viewed);
|
||||
|
||||
const [isHovered, toggleHover] = useState(false)
|
||||
const [iconName, setIconName] = useState(defaultIconName)
|
||||
const [isHovered, toggleHover] = useState(false);
|
||||
const [iconName, setIconName] = useState(defaultIconName);
|
||||
|
||||
useEffect(() => {
|
||||
if (isHovered) setIconName(PLAY_ICON_NAMES.hovered)
|
||||
else setIconName(getDefaultIconName(viewed))
|
||||
}, [isHovered, viewed])
|
||||
if (isHovered) setIconName(PLAY_ICON_NAMES.hovered);
|
||||
else setIconName(getDefaultIconName(viewed));
|
||||
}, [isHovered, viewed]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
onClick={onClick ? onClick : () => {}}
|
||||
to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }
|
||||
to={isAssist ? liveSessionRoute(sessionId, queryParams) : sessionRoute(sessionId)}
|
||||
onMouseEnter={() => toggleHover(true)}
|
||||
onMouseLeave={() => toggleHover(false)}
|
||||
>
|
||||
<Icon name={iconName} size={38} color={isAssist ? "tealx" : "teal"} />
|
||||
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,190 +1,191 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
CountryFlag,
|
||||
Avatar,
|
||||
TextEllipsis,
|
||||
Label,
|
||||
Icon,
|
||||
} from 'UI';
|
||||
import { CountryFlag, Avatar, TextEllipsis, Label, Icon } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { durationFormatted, formatTimeOrDate } from 'App/date';
|
||||
import stl from './sessionItem.module.css';
|
||||
import Counter from './Counter'
|
||||
import Counter from './Counter';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import PlayLink from './PlayLink';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
|
||||
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from 'App/routes';
|
||||
import { capitalize } from 'App/utils';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
const ASSIST_LIVE_SESSION = liveSession()
|
||||
const ASSIST_LIVE_SESSION = liveSession();
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
interface Props {
|
||||
session: {
|
||||
sessionId: string;
|
||||
userBrowser: string;
|
||||
userOs: string;
|
||||
userId: string;
|
||||
userAnonymousId: string;
|
||||
userDisplayName: string;
|
||||
userCountry: string;
|
||||
startedAt: number;
|
||||
duration: string;
|
||||
eventsCount: number;
|
||||
errorsCount: number;
|
||||
pagesCount: number;
|
||||
viewed: boolean;
|
||||
favorite: boolean;
|
||||
userDeviceType: string;
|
||||
userUuid: string;
|
||||
userNumericHash: number;
|
||||
live: boolean;
|
||||
metadata: Record<string, any>;
|
||||
userSessionsCount: number;
|
||||
issueTypes: [];
|
||||
active: boolean;
|
||||
},
|
||||
onUserClick?: (userId: string, userAnonymousId: string) => void;
|
||||
hasUserFilter?: boolean;
|
||||
disableUser?: boolean;
|
||||
metaList?: Array<any>;
|
||||
// showActive?: boolean;
|
||||
lastPlayedSessionId?: string;
|
||||
live?: boolean;
|
||||
onClick?: any
|
||||
session: {
|
||||
sessionId: string;
|
||||
userBrowser: string;
|
||||
userOs: string;
|
||||
userId: string;
|
||||
userAnonymousId: string;
|
||||
userDisplayName: string;
|
||||
userCountry: string;
|
||||
startedAt: number;
|
||||
duration: string;
|
||||
eventsCount: number;
|
||||
errorsCount: number;
|
||||
pagesCount: number;
|
||||
viewed: boolean;
|
||||
favorite: boolean;
|
||||
userDeviceType: string;
|
||||
userUuid: string;
|
||||
userNumericHash: number;
|
||||
live: boolean;
|
||||
metadata: Record<string, any>;
|
||||
userSessionsCount: number;
|
||||
issueTypes: [];
|
||||
active: boolean;
|
||||
};
|
||||
onUserClick?: (userId: string, userAnonymousId: string) => void;
|
||||
hasUserFilter?: boolean;
|
||||
disableUser?: boolean;
|
||||
metaList?: Array<any>;
|
||||
// showActive?: boolean;
|
||||
lastPlayedSessionId?: string;
|
||||
live?: boolean;
|
||||
onClick?: any;
|
||||
}
|
||||
|
||||
function SessionItem(props: RouteComponentProps & Props) {
|
||||
const { settingsStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
const { settingsStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
const [isIframe, setIsIframe] = React.useState(false);
|
||||
|
||||
const {
|
||||
session,
|
||||
onUserClick = () => null,
|
||||
hasUserFilter = false,
|
||||
disableUser = false,
|
||||
metaList = [],
|
||||
lastPlayedSessionId,
|
||||
onClick = null,
|
||||
} = props;
|
||||
const {
|
||||
session,
|
||||
onUserClick = () => null,
|
||||
hasUserFilter = false,
|
||||
disableUser = false,
|
||||
metaList = [],
|
||||
lastPlayedSessionId,
|
||||
onClick = null,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
sessionId,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userId,
|
||||
userAnonymousId,
|
||||
userDisplayName,
|
||||
userCountry,
|
||||
startedAt,
|
||||
duration,
|
||||
eventsCount,
|
||||
viewed,
|
||||
userDeviceType,
|
||||
userNumericHash,
|
||||
live,
|
||||
metadata,
|
||||
issueTypes,
|
||||
active,
|
||||
} = session;
|
||||
const {
|
||||
sessionId,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userId,
|
||||
userAnonymousId,
|
||||
userDisplayName,
|
||||
userCountry,
|
||||
startedAt,
|
||||
duration,
|
||||
eventsCount,
|
||||
viewed,
|
||||
userDeviceType,
|
||||
userNumericHash,
|
||||
live,
|
||||
metadata,
|
||||
issueTypes,
|
||||
active,
|
||||
} = session;
|
||||
|
||||
const location = props.location;
|
||||
const location = props.location;
|
||||
const queryParams = Object.fromEntries(new URLSearchParams(location.search));
|
||||
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
||||
const isAssist = isRoute(ASSIST_ROUTE, location.pathname) || isRoute(ASSIST_LIVE_SESSION, location.pathname);
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
|
||||
const isAssist = isRoute(ASSIST_ROUTE, location.pathname) || isRoute(ASSIST_LIVE_SESSION, location.pathname);
|
||||
const isLastPlayed = lastPlayedSessionId === sessionId;
|
||||
|
||||
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
const _metaList = Object.keys(metadata)
|
||||
.filter((i) => metaList.includes(i))
|
||||
.map((key) => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ cn(stl.sessionItem, "flex flex-col p-2") } id="session-item" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-start">
|
||||
<div className={ cn('flex items-center w-full')}>
|
||||
<div className="flex items-center pr-2" style={{ width: "30%"}}>
|
||||
<div><Avatar isActive={active} seed={ userNumericHash } isAssist={isAssist} /></div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
|
||||
<div
|
||||
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, [stl.userName]: !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
|
||||
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
<TextEllipsis text={userDisplayName} maxWidth="200px" popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
return (
|
||||
<div className={cn(stl.sessionItem, 'flex flex-col p-2')} id="session-item" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-start">
|
||||
<div className={cn('flex items-center w-full')}>
|
||||
<div className="flex items-center pr-2 shrink-0" style={{ width: '40%' }}>
|
||||
<div>
|
||||
<Avatar isActive={active} seed={userNumericHash} isAssist={isAssist} />
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
|
||||
<div
|
||||
className={cn('text-lg', {
|
||||
'color-teal cursor-pointer': !disableUser && hasUserId,
|
||||
[stl.userName]: !disableUser && hasUserId,
|
||||
'color-gray-medium': disableUser || !hasUserId,
|
||||
})}
|
||||
onClick={() => !disableUser && !hasUserFilter && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
<TextEllipsis text={userDisplayName} maxWidth="200px" popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: '20%' }} className="px-2 flex flex-col justify-between">
|
||||
<div>
|
||||
<TextEllipsis text={formatTimeOrDate(startedAt, timezone)} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</div>
|
||||
<div className="flex items-center color-gray-medium py-1">
|
||||
{!isAssist && (
|
||||
<>
|
||||
<div className="color-gray-medium">
|
||||
<span className="mr-1">{eventsCount}</span>
|
||||
<span>{eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event'}</span>
|
||||
</div>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
</>
|
||||
)}
|
||||
<div>{live ? <Counter startTime={startedAt} /> : formattedDuration}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: '30%' }} className="px-2 flex flex-col justify-between">
|
||||
<div style={{ height: '21px' }}>
|
||||
<CountryFlag country={userCountry} style={{ paddingTop: '4px' }} label />
|
||||
</div>
|
||||
<div className="color-gray-medium flex items-center py-1">
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis text={capitalize(userBrowser)} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</span>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis text={capitalize(userOs)} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</span>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||
<TextEllipsis text={capitalize(userDeviceType)} popupProps={{ inverted: true, size: 'tiny' }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{isSessions && (
|
||||
<div style={{ width: '10%' }} className="self-center px-2 flex items-center">
|
||||
<ErrorBars count={issueTypes.length} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "30%" }} className="px-2 flex flex-col justify-between">
|
||||
<div>{formatTimeOrDate(startedAt, timezone) }</div>
|
||||
<div className="flex items-center color-gray-medium py-1">
|
||||
{!isAssist && (
|
||||
<>
|
||||
<div className="color-gray-medium">
|
||||
<span className="mr-1">{ eventsCount }</span>
|
||||
<span>{ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }</span>
|
||||
</div>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
</>
|
||||
)}
|
||||
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "30%" }} className="px-2 flex flex-col justify-between">
|
||||
<div style={{ height: '21px'}}>
|
||||
<CountryFlag country={ userCountry } style={{ paddingTop: '4px' }} label />
|
||||
</div>
|
||||
<div className="color-gray-medium flex items-center py-1">
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ capitalize(userDeviceType) } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{ isSessions && (
|
||||
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
|
||||
<ErrorBars count={issueTypes.length} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
|
||||
{ isSessions && (
|
||||
<div className="mr-4 flex-shrink-0 w-24">
|
||||
{ isLastPlayed && (
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span className="color-gray-medium text-xs" style={{ whiteSpace: 'nowrap'}}>LAST PLAYED</span>
|
||||
</Label>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<div className={stl.playLink} id="play-button" data-viewed={viewed}>
|
||||
{isSessions && (
|
||||
<div className="mr-4 flex-shrink-0 w-24">
|
||||
{isLastPlayed && (
|
||||
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
|
||||
<span className="color-gray-medium text-xs" style={{ whiteSpace: 'nowrap' }}>
|
||||
LAST PLAYED
|
||||
</span>
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<PlayLink isAssist={isAssist} sessionId={sessionId} viewed={viewed} onClick={onClick} queryParams={queryParams} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<PlayLink
|
||||
isAssist={isAssist}
|
||||
sessionId={sessionId}
|
||||
viewed={viewed}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{_metaList.length > 0 && <SessionMetaList className="mt-4" metaList={_metaList} />}
|
||||
</div>
|
||||
{ _metaList.length > 0 && (
|
||||
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter<Props>(observer<Props>(SessionItem))
|
||||
export default withRouter<Props>(observer<Props>(SessionItem));
|
||||
|
|
|
|||
|
|
@ -8,36 +8,10 @@ import { toast } from 'react-toastify';
|
|||
|
||||
type TimezonesDropdown = Timezone[]
|
||||
|
||||
const generateGMTZones = (): TimezonesDropdown => {
|
||||
const timezones: TimezonesDropdown = []
|
||||
|
||||
const positiveNumbers = [...Array(12).keys()];
|
||||
const negativeNumbers = [...Array(12).keys()].reverse();
|
||||
negativeNumbers.pop(); // remove trailing zero since we have one in positive numbers array
|
||||
|
||||
const combinedArray = [...negativeNumbers, ...positiveNumbers];
|
||||
|
||||
for (let i = 0; i < combinedArray.length; i++) {
|
||||
let symbol = i < 11 ? '-' : '+';
|
||||
let isUTC = i === 11
|
||||
let prefix = isUTC ? 'UTC / GMT' : 'GMT';
|
||||
let value = String(combinedArray[i]).padStart(2, '0');
|
||||
|
||||
let tz = `${prefix} ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00`
|
||||
|
||||
let dropdownValue = `UTC${symbol}${value}`
|
||||
timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue })
|
||||
}
|
||||
|
||||
timezones.splice(17, 0, { label: 'GMT +05:30', value: 'UTC+05:30' })
|
||||
return timezones
|
||||
}
|
||||
|
||||
const timezoneOptions: TimezonesDropdown = [...generateGMTZones()]
|
||||
|
||||
function DefaultTimezone() {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const timezoneOptions: TimezonesDropdown = settingsStore.sessionSettings.defaultTimezones;
|
||||
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import origMoment from "moment";
|
|||
import { extendMoment } from "moment-range";
|
||||
export const moment = extendMoment(origMoment);
|
||||
import { DateTime } from "luxon";
|
||||
import { TIMEZONE } from 'App/constants/storageKeys';
|
||||
|
||||
export const CUSTOM_RANGE = "CUSTOM_RANGE";
|
||||
|
||||
|
|
@ -42,39 +43,42 @@ export function getDateRangeLabel(value) {
|
|||
}
|
||||
|
||||
export function getDateRangeFromValue(value) {
|
||||
const tz = JSON.parse(localStorage.getItem(TIMEZONE));
|
||||
const offset = tz ? tz.label.slice(-6) : 0;
|
||||
|
||||
switch (value) {
|
||||
case DATE_RANGE_VALUES.LAST_30_MINUTES:
|
||||
return moment.range(
|
||||
moment().startOf("hour").subtract(30, "minutes"),
|
||||
moment().startOf("hour")
|
||||
moment().utcOffset(offset, true).startOf("hour").subtract(30, "minutes"),
|
||||
moment().utcOffset(offset, true).startOf("hour")
|
||||
);
|
||||
case DATE_RANGE_VALUES.YESTERDAY:
|
||||
return moment.range(
|
||||
moment().utcOffset(offset, true).subtract(1, "days").startOf("day"),
|
||||
moment().utcOffset(offset, true).subtract(1, "days").endOf("day")
|
||||
);
|
||||
case DATE_RANGE_VALUES.TODAY:
|
||||
return moment.range(moment().startOf("day"), moment().endOf("day"));
|
||||
case DATE_RANGE_VALUES.YESTERDAY:
|
||||
return moment.range(
|
||||
moment().subtract(1, "days").startOf("day"),
|
||||
moment().subtract(1, "days").endOf("day")
|
||||
);
|
||||
return moment.range(moment().utcOffset(offset, true).startOf("day"), moment().utcOffset(offset, true).endOf("day"));
|
||||
case DATE_RANGE_VALUES.LAST_24_HOURS:
|
||||
return moment.range(moment().subtract(24, "hours"), moment());
|
||||
return moment.range(moment().utcOffset(offset, true).subtract(24, "hours"), moment().utcOffset(offset, true));
|
||||
case DATE_RANGE_VALUES.LAST_7_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(7, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
moment().utcOffset(offset, true).subtract(7, "days").startOf("day"),
|
||||
moment().utcOffset(offset, true).endOf("day")
|
||||
);
|
||||
case DATE_RANGE_VALUES.LAST_30_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(30, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
moment().utcOffset(offset, true).subtract(30, "days").startOf("day"),
|
||||
moment().utcOffset(offset, true).endOf("day")
|
||||
);
|
||||
case DATE_RANGE_VALUES.THIS_MONTH:
|
||||
return moment().range("month");
|
||||
return moment().utcOffset(offset, true).range("month");
|
||||
case DATE_RANGE_VALUES.LAST_MONTH:
|
||||
return moment().subtract(1, "months").range("month");
|
||||
return moment().utcOffset(offset, true).subtract(1, "months").range("month");
|
||||
case DATE_RANGE_VALUES.THIS_YEAR:
|
||||
return moment().range("year");
|
||||
return moment().utcOffset(offset, true).range("year");
|
||||
case DATE_RANGE_VALUES.CUSTOM_RANGE:
|
||||
return moment.range(moment(), moment());
|
||||
return moment.range(moment().utcOffset(offset, true), moment().utcOffset(offset, true));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export const reduceThenFetchResource =
|
|||
filter.filters = filter.filters.map(filterMap);
|
||||
filter.limit = 10;
|
||||
filter.page = getState().getIn(['search', 'currentPage']);
|
||||
const forceFetch = filter.filters.length === 0;
|
||||
|
||||
// duration filter from local storage
|
||||
if (!filter.filters.find((f) => f.type === FilterKey.DURATION)) {
|
||||
|
|
@ -172,7 +173,7 @@ export const reduceThenFetchResource =
|
|||
}
|
||||
}
|
||||
|
||||
return isRoute(ERRORS_ROUTE, window.location.pathname) ? dispatch(fetchErrorsList(filter)) : dispatch(fetchSessionList(filter));
|
||||
return isRoute(ERRORS_ROUTE, window.location.pathname) ? dispatch(fetchErrorsList(filter)) : dispatch(fetchSessionList(filter, forceFetch));
|
||||
};
|
||||
|
||||
export const edit = reduceThenFetchResource((instance) => ({
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ const reducer = (state = initialState, action = {}) => {
|
|||
switch (action.type) {
|
||||
case INIT:
|
||||
return state.set('current', Session(action.session));
|
||||
case FETCH_LIST.REQUEST:
|
||||
return action.clear ? state.set('list', List()) : state;
|
||||
// case FETCH_LIST.REQUEST:
|
||||
// return action.clear ? state.set('list', List()) : state;
|
||||
case FETCH_ERROR_STACK.SUCCESS:
|
||||
return state.set('errorStack', List(action.data.trace).map(ErrorStack)).set('sourcemapUploaded', action.data.sourcemapUploaded);
|
||||
case FETCH_LIVE_LIST.SUCCESS:
|
||||
|
|
@ -224,7 +224,7 @@ function init(session) {
|
|||
}
|
||||
|
||||
export const fetchList =
|
||||
(params = {}, clear = false, force = false) =>
|
||||
(params = {}, force = false) =>
|
||||
(dispatch, getState) => {
|
||||
if (!force) { // compare with the last fetched filter
|
||||
const oldFilters = getSessionFilter();
|
||||
|
|
@ -237,7 +237,6 @@ export const fetchList =
|
|||
return dispatch({
|
||||
types: FETCH_LIST.toArray(),
|
||||
call: (client) => client.post('/sessions/search2', params),
|
||||
clear,
|
||||
params: cleanParams(params),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,24 +1,54 @@
|
|||
import { makeAutoObservable, runInAction, action } from "mobx"
|
||||
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
|
||||
import { makeAutoObservable, runInAction, action } from 'mobx';
|
||||
import moment from 'moment';
|
||||
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys';
|
||||
|
||||
export type Timezone = {
|
||||
label: string,
|
||||
value: string,
|
||||
}
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const generateGMTZones = (): Timezone[] => {
|
||||
const timezones: Timezone[] = [];
|
||||
|
||||
const positiveNumbers = [...Array(12).keys()];
|
||||
const negativeNumbers = [...Array(12).keys()].reverse();
|
||||
negativeNumbers.pop(); // remove trailing zero since we have one in positive numbers array
|
||||
|
||||
const combinedArray = [...negativeNumbers, ...positiveNumbers];
|
||||
|
||||
for (let i = 0; i < combinedArray.length; i++) {
|
||||
let symbol = i < 11 ? '-' : '+';
|
||||
let isUTC = i === 11;
|
||||
let prefix = isUTC ? 'UTC / GMT' : 'GMT';
|
||||
let value = String(combinedArray[i]).padStart(2, '0');
|
||||
|
||||
let tz = `${prefix} ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00`;
|
||||
|
||||
let dropdownValue = `UTC${symbol}${value}`;
|
||||
timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue });
|
||||
}
|
||||
|
||||
timezones.splice(17, 0, { label: 'GMT +05:30', value: 'UTC+05:30' });
|
||||
return timezones;
|
||||
};
|
||||
|
||||
export default class SessionSettings {
|
||||
defaultTimezones = [...generateGMTZones()]
|
||||
skipToIssue: boolean = localStorage.getItem(SKIP_TO_ISSUE) === 'true';
|
||||
timezone: Timezone;
|
||||
durationFilter: any = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{}');
|
||||
captureRate: string = '0'
|
||||
captureAll: boolean = false
|
||||
captureRate: string = '0';
|
||||
captureAll: boolean = false;
|
||||
|
||||
constructor() {
|
||||
// compatibility fix for old timezone storage
|
||||
// TODO: remove after a while (1.7.1?)
|
||||
this.timezoneFix()
|
||||
this.timezone = JSON.parse(localStorage.getItem(TIMEZONE)) || { label: 'UTC / GMT +00:00', value: 'UTC' }
|
||||
makeAutoObservable(this)
|
||||
const userTimezoneOffset = moment().format('Z');
|
||||
const defaultTimezone = this.defaultTimezones.find(tz => tz.value.includes('UTC' + userTimezoneOffset.slice(0,3))) || { label: 'Local', value: `UTC${userTimezoneOffset}` };
|
||||
|
||||
this.timezoneFix(defaultTimezone);
|
||||
this.timezone = JSON.parse(localStorage.getItem(TIMEZONE)) || defaultTimezone;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
merge = (settings: any) => {
|
||||
|
|
@ -27,35 +57,35 @@ export default class SessionSettings {
|
|||
this.updateKey(key, settings[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changeCaptureRate = (rate: string) => {
|
||||
if (!rate) return this.captureRate = '0';
|
||||
if (!rate) return (this.captureRate = '0');
|
||||
// react do no see the difference between 01 and 1 decimals, this is why we have to use string casting
|
||||
if (parseInt(rate, 10) <= 100) this.captureRate = `${parseInt(rate, 10)}`;
|
||||
}
|
||||
};
|
||||
|
||||
changeCaptureAll = (all: boolean) => {
|
||||
this.captureAll = all;
|
||||
}
|
||||
};
|
||||
|
||||
timezoneFix() {
|
||||
if (localStorage.getItem(TIMEZONE) === '[object Object]') {
|
||||
localStorage.setItem(TIMEZONE, JSON.stringify({ label: 'UTC / GMT +00:00', value: 'UTC' }));
|
||||
timezoneFix(defaultTimezone: Record<string, string>) {
|
||||
if (localStorage.getItem(TIMEZONE) === '[object Object]' || !localStorage.getItem(TIMEZONE)) {
|
||||
localStorage.setItem(TIMEZONE, JSON.stringify(defaultTimezone));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateKey = (key: string, value: any) => {
|
||||
runInAction(() => {
|
||||
this[key] = value
|
||||
})
|
||||
this[key] = value;
|
||||
});
|
||||
|
||||
if (key === 'captureRate' || key === 'captureAll') return
|
||||
if (key === 'captureRate' || key === 'captureAll') return;
|
||||
|
||||
if (key === 'durationFilter' || key === 'timezone') {
|
||||
localStorage.setItem(`__$session-${key}$__`, JSON.stringify(value));
|
||||
} else {
|
||||
localStorage.setItem(`__$session-${key}$__`, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,8 @@ export default abstract class BaseScreen {
|
|||
getElementBySelector(selector: string): Element | null {
|
||||
if (!selector) return null;
|
||||
try {
|
||||
return this.document?.querySelector(selector) || null;
|
||||
const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/');
|
||||
return this.document?.querySelector(safeSelector) || null;
|
||||
} catch (e) {
|
||||
console.error("Can not select element. ", e)
|
||||
return null
|
||||
|
|
@ -186,4 +187,4 @@ export default abstract class BaseScreen {
|
|||
clean() {
|
||||
window.removeEventListener('resize', this.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@ export default class StatedScreen extends Screen {
|
|||
...s,
|
||||
el,
|
||||
index: index++,
|
||||
percent: 0,
|
||||
percent: Math.round((s.count * 100) / totalCount),
|
||||
boundingRect: this.calculateRelativeBoundingRect(el),
|
||||
count: Math.round((s.count * 100) / totalCount)
|
||||
count: s.count,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export const sessions = params => queried('/sessions', params);
|
|||
export const assist = params => queried('/assist', params);
|
||||
|
||||
export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash);
|
||||
export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/assist/${ sessionId }`, hash);
|
||||
export const liveSession = (sessionId = ':sessionId', params, hash) => hashed(queried(`/assist/${ sessionId }`, params), hash);
|
||||
// export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/live/session/${ sessionId }`, hash);
|
||||
|
||||
export const errors = params => queried('/errors', params);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createStore, applyMiddleware } from 'redux';
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Map } from 'immutable';
|
||||
import indexReducer from './duck';
|
||||
|
|
@ -9,13 +9,16 @@ const storage = new LocalStorage({
|
|||
jwt: String,
|
||||
});
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && window.env.NODE_ENV === "development"
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
|
||||
|
||||
const storageState = storage.state();
|
||||
const initialState = Map({
|
||||
jwt: storageState.jwt,
|
||||
// TODO: store user
|
||||
});
|
||||
|
||||
const store = createStore(indexReducer, initialState, applyMiddleware(thunk, apiMiddleware));
|
||||
const store = createStore(indexReducer, initialState, composeEnhancers(applyMiddleware(thunk, apiMiddleware)));
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
storage.sync({
|
||||
|
|
|
|||
|
|
@ -26,43 +26,43 @@ const RANGE_LABELS = {
|
|||
[THIS_YEAR]: "This Year",
|
||||
};
|
||||
|
||||
function getRange(rangeName) {
|
||||
function getRange(rangeName, offset) {
|
||||
switch (rangeName) {
|
||||
case TODAY:
|
||||
return moment.range(moment().startOf("day"), moment().endOf("day"));
|
||||
case YESTERDAY:
|
||||
return moment.range(
|
||||
moment().subtract(1, "days").startOf("day"),
|
||||
moment().subtract(1, "days").endOf("day")
|
||||
moment().utcOffset(offset).subtract(1, "days").startOf("day"),
|
||||
moment().utcOffset(offset).subtract(1, "days").endOf("day")
|
||||
);
|
||||
case LAST_24_HOURS:
|
||||
return moment.range(
|
||||
// moment().startOf("hour").subtract(24, "hours"),
|
||||
// moment().startOf("hour")
|
||||
moment().subtract(24, 'hours'),
|
||||
moment(),
|
||||
moment().utcOffset(offset).subtract(24, 'hours'),
|
||||
moment().utcOffset(offset),
|
||||
);
|
||||
case LAST_30_MINUTES:
|
||||
return moment.range(
|
||||
moment().startOf("hour").subtract(30, "minutes"),
|
||||
moment().startOf("hour")
|
||||
moment().utcOffset(offset).startOf("hour").subtract(30, "minutes"),
|
||||
moment().utcOffset(offset).startOf("hour")
|
||||
);
|
||||
case LAST_7_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(7, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
moment().utcOffset(offset).subtract(7, "days").startOf("day"),
|
||||
moment().utcOffset(offset).endOf("day")
|
||||
);
|
||||
case LAST_30_DAYS:
|
||||
return moment.range(
|
||||
moment().subtract(30, "days").startOf("day"),
|
||||
moment().endOf("day")
|
||||
moment().utcOffset(offset).subtract(30, "days").startOf("day"),
|
||||
moment().utcOffset(offset).endOf("day")
|
||||
);
|
||||
case THIS_MONTH:
|
||||
return moment().range("month");
|
||||
return moment().utcOffset(offset).range("month");
|
||||
case LAST_MONTH:
|
||||
return moment().subtract(1, "months").range("month");
|
||||
return moment().utcOffset(offset).subtract(1, "months").range("month");
|
||||
case THIS_YEAR:
|
||||
return moment().range("year");
|
||||
return moment().utcOffset(offset).range("year");
|
||||
default:
|
||||
return moment.range();
|
||||
}
|
||||
|
|
@ -77,10 +77,11 @@ export default Record(
|
|||
},
|
||||
{
|
||||
fromJS: (period) => {
|
||||
const offset = period.timezoneOffset || 0
|
||||
if (!period.rangeName || period.rangeName === CUSTOM_RANGE) {
|
||||
const range = moment.range(
|
||||
moment(period.start || 0),
|
||||
moment(period.end || 0)
|
||||
moment(period.start || 0).utcOffset(offset),
|
||||
moment(period.end || 0).utcOffset(offset)
|
||||
);
|
||||
return {
|
||||
...period,
|
||||
|
|
@ -89,7 +90,7 @@ export default Record(
|
|||
end: range.end.unix() * 1000,
|
||||
};
|
||||
}
|
||||
const range = getRange(period.rangeName);
|
||||
const range = getRange(period.rangeName, offset);
|
||||
return {
|
||||
...period,
|
||||
range,
|
||||
|
|
@ -97,14 +98,6 @@ export default Record(
|
|||
end: range.end.unix() * 1000,
|
||||
};
|
||||
},
|
||||
// fromFilter: filter => {
|
||||
// const range = getRange(filter.rangeName);
|
||||
// return {
|
||||
// start: range.start.unix() * 1000,
|
||||
// end: range.end.unix() * 1000,
|
||||
// rangeName: filter.rangeName,
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
toJSON() {
|
||||
return {
|
||||
|
|
@ -120,7 +113,16 @@ export default Record(
|
|||
endTimestamp: this.end,
|
||||
};
|
||||
},
|
||||
rangeFormatted(format = "MMM Do YY, HH:mm") {
|
||||
rangeFormatted(format = "MMM Do YY, HH:mm", tz) {
|
||||
if (tz) {
|
||||
const start = this.range.start.clone();
|
||||
const end = this.range.end.clone();
|
||||
return (
|
||||
start.utcOffset(tz).format(format) +
|
||||
" - " +
|
||||
end.utcOffset(tz).format(format)
|
||||
)
|
||||
}
|
||||
return (
|
||||
this.range.start.format(format) +
|
||||
" - " +
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ export const filters = [
|
|||
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||
|
||||
// PERFORMANCE
|
||||
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: '%', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'mb', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
|
||||
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
|
||||
];
|
||||
|
|
@ -142,6 +142,7 @@ export default Record({
|
|||
source: [""],
|
||||
sourceType: '',
|
||||
sourceOperator: '=',
|
||||
sourceUnit: '',
|
||||
sourceOperatorOptions: [],
|
||||
|
||||
operator: '',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from k8s.gcr.io/ingress-nginx/controller
|
||||
from k8s.gcr.io/ingress-nginx/controller:v1.3.0
|
||||
# Fix critical vulnerability
|
||||
user 0
|
||||
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
user 101
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ ALTER TABLE IF EXISTS events.resources
|
|||
PRIMARY KEY (session_id, message_id, timestamp);
|
||||
|
||||
COMMIT;
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS autocomplete_unique_project_id_md5value_type_idx ON autocomplete (project_id, md5(value), type);
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS projects_project_id_deleted_at_n_idx ON public.projects (project_id) WHERE deleted_at IS NULL;
|
||||
ALTER TYPE metric_type ADD VALUE IF NOT EXISTS 'funnel';
|
||||
|
||||
|
|
@ -199,4 +200,5 @@ $$
|
|||
END IF;
|
||||
END
|
||||
$$;
|
||||
DROP INDEX IF EXISTS autocomplete_unique;
|
||||
COMMIT;
|
||||
|
|
@ -839,7 +839,7 @@ $$
|
|||
project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE unique index autocomplete_unique ON autocomplete (project_id, value, type);
|
||||
CREATE UNIQUE INDEX autocomplete_unique_project_id_md5value_type_idx ON autocomplete (project_id, md5(value), type);
|
||||
CREATE index autocomplete_project_id_idx ON autocomplete (project_id);
|
||||
CREATE INDEX autocomplete_type_idx ON public.autocomplete (type);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-assist",
|
||||
"description": "Tracker plugin for screen assistance through the WebRTC",
|
||||
"version": "3.5.15",
|
||||
"version": "3.5.16",
|
||||
"keywords": [
|
||||
"WebRTC",
|
||||
"assistance",
|
||||
|
|
|
|||
|
|
@ -216,4 +216,4 @@ export default function(opts: Partial<Options> = {}) {
|
|||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "3.5.15",
|
||||
"version": "3.5.16",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ export default class App {
|
|||
});
|
||||
}
|
||||
}
|
||||
stop(calledFromAPI = false): void {
|
||||
stop(calledFromAPI = false, restarting = false): void {
|
||||
if (this.activityState !== ActivityState.NotActive) {
|
||||
try {
|
||||
this.sanitizer.clear();
|
||||
|
|
@ -479,7 +479,7 @@ export default class App {
|
|||
this.session.reset();
|
||||
}
|
||||
this.notify.log('OpenReplay tracking stopped.');
|
||||
if (this.worker) {
|
||||
if (this.worker && !restarting) {
|
||||
this.worker.postMessage('stop');
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -487,4 +487,8 @@ export default class App {
|
|||
}
|
||||
}
|
||||
}
|
||||
restart() {
|
||||
this.stop(false, true);
|
||||
this.start({ forceNew: false });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ type NodeCallback = (node: Node, isStart: boolean) => void;
|
|||
type ElementListener = [string, EventListener];
|
||||
|
||||
export default class Nodes {
|
||||
private readonly nodes: Array<Node | undefined>;
|
||||
private readonly nodeCallbacks: Array<NodeCallback>;
|
||||
private readonly elementListeners: Map<number, Array<ElementListener>>;
|
||||
constructor(private readonly node_id: string) {
|
||||
this.nodes = [];
|
||||
this.nodeCallbacks = [];
|
||||
this.elementListeners = new Map();
|
||||
}
|
||||
private nodes: Array<Node | void> = [];
|
||||
private readonly nodeCallbacks: Array<NodeCallback> = [];
|
||||
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map();
|
||||
|
||||
constructor(private readonly node_id: string) {}
|
||||
|
||||
attachNodeCallback(nodeCallback: NodeCallback): void {
|
||||
this.nodeCallbacks.push(nodeCallback);
|
||||
}
|
||||
|
|
@ -42,7 +40,7 @@ export default class Nodes {
|
|||
const id = (node as any)[this.node_id];
|
||||
if (id !== undefined) {
|
||||
delete (node as any)[this.node_id];
|
||||
this.nodes[id] = undefined;
|
||||
delete this.nodes[id];
|
||||
const listeners = this.elementListeners.get(id);
|
||||
if (listeners !== undefined) {
|
||||
this.elementListeners.delete(id);
|
||||
|
|
@ -51,13 +49,25 @@ export default class Nodes {
|
|||
}
|
||||
return id;
|
||||
}
|
||||
cleanTree() {
|
||||
// sadly we keep empty items in array here resulting in some memory still being used
|
||||
// but its still better than keeping dead nodes or undef elements
|
||||
// plus we keep our index positions for new/alive nodes
|
||||
// performance test: 3ms for 30k nodes with 17k dead ones
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
const node = this.nodes[i];
|
||||
if (node && !document.contains(node)) {
|
||||
this.unregisterNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
callNodeCallbacks(node: Node, isStart: boolean): void {
|
||||
this.nodeCallbacks.forEach((cb) => cb(node, isStart));
|
||||
}
|
||||
getID(node: Node): number | undefined {
|
||||
return (node as any)[this.node_id];
|
||||
}
|
||||
getNode(id: number): Node | undefined {
|
||||
getNode(id: number) {
|
||||
return this.nodes[id];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,17 @@ function isObservable(node: Node): boolean {
|
|||
- use document as a 0-node in the upper context (should be updated in player at first)
|
||||
*/
|
||||
|
||||
/*
|
||||
Nikita:
|
||||
- rn we only send unbind event for parent (all child nodes will be cut in the live replay anyways)
|
||||
to prevent sending 1k+ unbinds for child nodes and making replay file bigger than it should be
|
||||
*/
|
||||
|
||||
enum RecentsType {
|
||||
New,
|
||||
Removed,
|
||||
Changed,
|
||||
RemovedChild,
|
||||
}
|
||||
|
||||
export default abstract class Observer {
|
||||
|
|
@ -69,7 +76,7 @@ export default abstract class Observer {
|
|||
}
|
||||
if (type === 'childList') {
|
||||
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
||||
this.bindTree(mutation.removedNodes[i]);
|
||||
this.bindTree(mutation.removedNodes[i], true);
|
||||
}
|
||||
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
||||
this.bindTree(mutation.addedNodes[i]);
|
||||
|
|
@ -180,8 +187,12 @@ export default abstract class Observer {
|
|||
this.recents.set(id, RecentsType.Removed);
|
||||
}
|
||||
}
|
||||
private unbindChildNode(node: Node): void {
|
||||
const [id] = this.app.nodes.registerNode(node);
|
||||
this.recents.set(id, RecentsType.RemovedChild);
|
||||
}
|
||||
|
||||
private bindTree(node: Node): void {
|
||||
private bindTree(node: Node, isChildUnbinding = false): void {
|
||||
if (!isObservable(node)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -191,7 +202,7 @@ export default abstract class Observer {
|
|||
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node) =>
|
||||
isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
||||
isIgnored(node) || (this.app.nodes.getID(node) !== undefined && !isChildUnbinding)
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT,
|
||||
},
|
||||
|
|
@ -199,11 +210,15 @@ export default abstract class Observer {
|
|||
false,
|
||||
);
|
||||
while (walker.nextNode()) {
|
||||
this.bindNode(walker.currentNode);
|
||||
if (isChildUnbinding) {
|
||||
this.unbindChildNode(walker.currentNode);
|
||||
} else {
|
||||
this.bindNode(walker.currentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unbindNode(node: Node): void {
|
||||
private unbindNode(node: Node) {
|
||||
const id = this.app.nodes.unregisterNode(node);
|
||||
if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
|
||||
this.app.send(new RemoveNode(id));
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
app.ticker.attach((): void => {
|
||||
inputValues.forEach((value, id) => {
|
||||
const node = app.nodes.getNode(id);
|
||||
if (!node) return;
|
||||
if (!isTextEditable(node)) {
|
||||
inputValues.delete(id);
|
||||
return;
|
||||
|
|
@ -157,6 +158,7 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
});
|
||||
checkableValues.forEach((checked, id) => {
|
||||
const node = app.nodes.getNode(id);
|
||||
if (!node) return;
|
||||
if (!isCheckable(node)) {
|
||||
checkableValues.delete(id);
|
||||
return;
|
||||
|
|
|
|||
112
utilities/package-lock.json
generated
112
utilities/package-lock.json
generated
|
|
@ -42,9 +42,9 @@
|
|||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.42.tgz",
|
||||
"integrity": "sha512-Q5BPGyGKcvQgAMbsr7qEGN/kIPN6zZecYYABeTDBizOsau+2NMdSVTar9UQw21A2+JyA2KRNDYaYrPB0Rpk2oQ=="
|
||||
"version": "18.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
|
||||
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
|
|
@ -61,12 +61,12 @@
|
|||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
|
|
@ -175,9 +175,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
|
@ -185,12 +185,12 @@
|
|||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
|
|
@ -270,6 +270,14 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
|
@ -345,18 +353,10 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
|
||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
]
|
||||
|
|
@ -504,7 +504,7 @@
|
|||
"node_modules/lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
|
||||
},
|
||||
"node_modules/map-obj": {
|
||||
"version": "4.3.0",
|
||||
|
|
@ -541,12 +541,12 @@
|
|||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
|
@ -641,7 +641,7 @@
|
|||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
|
|
@ -808,9 +808,9 @@
|
|||
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz",
|
||||
"integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==",
|
||||
"dependencies": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
|
|
@ -938,7 +938,7 @@
|
|||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
|
|
@ -946,7 +946,7 @@
|
|||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
|
|
@ -954,7 +954,7 @@
|
|||
"node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
|
|
@ -1013,9 +1013,9 @@
|
|||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.42.tgz",
|
||||
"integrity": "sha512-Q5BPGyGKcvQgAMbsr7qEGN/kIPN6zZecYYABeTDBizOsau+2NMdSVTar9UQw21A2+JyA2KRNDYaYrPB0Rpk2oQ=="
|
||||
"version": "18.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
|
||||
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.8",
|
||||
|
|
@ -1029,12 +1029,12 @@
|
|||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
|
||||
},
|
||||
"base64id": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -1109,19 +1109,19 @@
|
|||
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
|
||||
},
|
||||
"cors": {
|
||||
"version": "2.8.5",
|
||||
|
|
@ -1177,6 +1177,11 @@
|
|||
"ws": "~8.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
|
@ -1243,19 +1248,12 @@
|
|||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.2.0",
|
||||
|
|
@ -1367,7 +1365,7 @@
|
|||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg=="
|
||||
},
|
||||
"map-obj": {
|
||||
"version": "4.3.0",
|
||||
|
|
@ -1391,12 +1389,12 @@
|
|||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
|
|
@ -1457,7 +1455,7 @@
|
|||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
|
|
@ -1594,9 +1592,9 @@
|
|||
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
|
||||
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz",
|
||||
"integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==",
|
||||
"requires": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"component-emitter": "~1.3.0",
|
||||
|
|
@ -1660,17 +1658,17 @@
|
|||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
|
|
|
|||
|
|
@ -40,19 +40,20 @@ const extractSessionIdFromRequest = function (req) {
|
|||
}
|
||||
const isValidSession = function (sessionInfo, filters) {
|
||||
let foundAll = true;
|
||||
for (const [key, values] of Object.entries(filters)) {
|
||||
for (const [key, body] of Object.entries(filters)) {
|
||||
let found = false;
|
||||
if (values !== undefined && values !== null) {
|
||||
if (body.values !== undefined && body.values !== null) {
|
||||
for (const [skey, svalue] of Object.entries(sessionInfo)) {
|
||||
if (svalue !== undefined && svalue !== null) {
|
||||
if (typeof (svalue) === "object") {
|
||||
if (isValidSession(svalue, {[key]: values})) {
|
||||
if (isValidSession(svalue, {[key]: body})) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else if (skey.toLowerCase() === key.toLowerCase()) {
|
||||
for (let v of values) {
|
||||
if (String(svalue).toLowerCase().indexOf(v.toLowerCase()) >= 0) {
|
||||
for (let v of body["values"]) {
|
||||
if (body.operator === "is" && String(svalue).toLowerCase() === v.toLowerCase()
|
||||
|| body.operator !== "is" && String(svalue).toLowerCase().indexOf(v.toLowerCase()) >= 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -97,21 +98,37 @@ const objectToObjectOfArrays = function (obj) {
|
|||
for (let k of Object.keys(obj)) {
|
||||
if (obj[k] !== undefined && obj[k] !== null) {
|
||||
_obj[k] = obj[k];
|
||||
if (!Array.isArray(_obj[k])) {
|
||||
if (!Array.isArray(_obj[k].values)) {
|
||||
_obj[k] = [_obj[k]];
|
||||
}
|
||||
for (let i = 0; i < _obj[k].length; i++) {
|
||||
_obj[k][i] = String(_obj[k][i]);
|
||||
for (let i = 0; i < _obj[k].values.length; i++) {
|
||||
_obj[k].values[i] = String(_obj[k].values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _obj;
|
||||
}
|
||||
const transformFilters = function (filter) {
|
||||
for (let key of Object.keys(filter)) {
|
||||
//To support old v1.7.0 payload
|
||||
if (Array.isArray(filter[key]) || filter[key] === undefined || filter[key] === null) {
|
||||
debug && console.log(`[WS]old format for key=${key}`);
|
||||
filter[key] = {"values": filter[key]};
|
||||
}
|
||||
if (filter[key].operator) {
|
||||
debug && console.log(`[WS]where operator=${filter[key].operator}`);
|
||||
} else {
|
||||
debug && console.log(`[WS]where operator=DEFAULT-contains`);
|
||||
filter[key].operator = "contains";
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
const extractPayloadFromRequest = function (req) {
|
||||
let filters = {
|
||||
"query": {},
|
||||
"filter": {},
|
||||
"query": {}, // for autocomplete
|
||||
"filter": {}, // for sessions search
|
||||
"sort": {
|
||||
"key": req.body.sort && req.body.sort.key ? req.body.sort.key : undefined,
|
||||
"order": req.body.sort && req.body.sort.order === "DESC"
|
||||
|
|
@ -135,6 +152,7 @@ const extractPayloadFromRequest = function (req) {
|
|||
}
|
||||
filters.filter = objectToObjectOfArrays(filters.filter);
|
||||
filters.filter = {...filters.filter, ...(req.body.filter || {})};
|
||||
filters.filter = transformFilters(filters.filter);
|
||||
debug && console.log("payload/filters:" + JSON.stringify(filters))
|
||||
return filters;
|
||||
}
|
||||
|
|
@ -194,6 +212,7 @@ const uniqueAutocomplete = function (list) {
|
|||
return _list;
|
||||
}
|
||||
module.exports = {
|
||||
transformFilters,
|
||||
extractPeerId,
|
||||
request_logger,
|
||||
getValidAttributes,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue