Compare commits
42 commits
main
...
ui-sentry-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6edf729bd9 | ||
|
|
b43a35e458 | ||
|
|
28a9b53d05 | ||
|
|
111e9c6474 | ||
|
|
f8d8cc5150 | ||
|
|
aa25b0e882 | ||
|
|
b53b14ae5f | ||
|
|
e3f6a8fadc | ||
|
|
e95611c1a6 | ||
|
|
46aebe9a8c | ||
|
|
e9a9d2ff2a | ||
|
|
1f7d587796 | ||
|
|
7c20b608c5 | ||
|
|
88a82acb8b | ||
|
|
36c9b5e234 | ||
|
|
4cfdee28c3 | ||
|
|
62ef3ca2dd | ||
|
|
9d0f3b34ae | ||
|
|
93c605a28e | ||
|
|
872263624d | ||
|
|
1dee5853a5 | ||
|
|
5cf584e8e1 | ||
|
|
cfc1f807ec | ||
|
|
de19f0397d | ||
|
|
a11c683baf | ||
|
|
f5949cc08e | ||
|
|
d7cb49d490 | ||
|
|
6e5d92ed79 | ||
|
|
018bf9c0be | ||
|
|
c56a2c2d25 | ||
|
|
5d786bde56 | ||
|
|
c7e6f31941 | ||
|
|
ad0ef00842 | ||
|
|
2ffec26d02 | ||
|
|
b63962b51a | ||
|
|
abe440f729 | ||
|
|
71e7552899 | ||
|
|
7906384fe7 | ||
|
|
bdd564f49c | ||
|
|
b89248067a | ||
|
|
9ed207abb1 | ||
|
|
cbe2d62def |
53 changed files with 1279 additions and 821 deletions
6
.github/workflows/patch-build.yaml
vendored
6
.github/workflows/patch-build.yaml
vendored
|
|
@ -83,8 +83,12 @@ jobs:
|
|||
[ -d $MSAAS_REPO_FOLDER ] || {
|
||||
git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
|
||||
cd $MSAAS_REPO_FOLDER
|
||||
cd openreplay && git fetch origin && git checkout main # This have to be changed to specific tag
|
||||
git log -1
|
||||
cd $MSAAS_REPO_FOLDER
|
||||
bash git-init.sh
|
||||
git checkout
|
||||
git --git-dir=./openreplay/.git status
|
||||
}
|
||||
}
|
||||
function build_managed() {
|
||||
|
|
@ -97,7 +101,7 @@ jobs:
|
|||
else
|
||||
cd $MSAAS_REPO_FOLDER/openreplay/$service
|
||||
fi
|
||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt
|
||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash -x build.sh >> /tmp/arm.txt
|
||||
}
|
||||
# Checking for backend images
|
||||
ls backend/cmd >> /tmp/backend.txt
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from chalicelib.core.collaboration_slack import Slack
|
|||
from chalicelib.utils import pg_client, helper, email_helper, smtp
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get(id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
|
|
|
|||
|
|
@ -26,17 +26,23 @@ class MSTeams(BaseCollaboration):
|
|||
|
||||
@classmethod
|
||||
def say_hello(cls, url):
|
||||
r = requests.post(
|
||||
url=url,
|
||||
json={
|
||||
"@type": "MessageCard",
|
||||
"@context": "https://schema.org/extensions",
|
||||
"summary": "Welcome to OpenReplay",
|
||||
"title": "Welcome to OpenReplay"
|
||||
})
|
||||
if r.status_code != 200:
|
||||
logger.warning("MSTeams integration failed")
|
||||
logger.warning(r.text)
|
||||
try:
|
||||
r = requests.post(
|
||||
url=url,
|
||||
json={
|
||||
"@type": "MessageCard",
|
||||
"@context": "https://schema.org/extensions",
|
||||
"summary": "Welcome to OpenReplay",
|
||||
"title": "Welcome to OpenReplay"
|
||||
},
|
||||
timeout=3)
|
||||
if r.status_code != 200:
|
||||
logger.warning("MSTeams integration failed")
|
||||
logger.warning(r.text)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("!!! MSTeams integration failed")
|
||||
logger.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class JIRAIntegration(integration_base.BaseIntegration):
|
|||
except Exception as e:
|
||||
self._issue_handler = None
|
||||
self.integration["valid"] = False
|
||||
return {"errors": ["Something went wrong, please check your JIRA credentials."]}
|
||||
return self._issue_handler
|
||||
|
||||
# TODO: remove this once jira-oauth is done
|
||||
|
|
|
|||
|
|
@ -336,10 +336,13 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
if v not in extra_conditions[e.operator].value:
|
||||
extra_conditions[e.operator].value.append(v)
|
||||
extra_conditions = list(extra_conditions.values())
|
||||
|
||||
elif metric_of == schemas.MetricOfTable.ISSUES and len(metric_value) > 0:
|
||||
data.filters.append(schemas.SessionSearchFilterSchema(value=metric_value, type=schemas.FilterType.ISSUE,
|
||||
operator=schemas.SearchEventOperator.IS))
|
||||
elif metric_of == schemas.MetricOfTable.REFERRER:
|
||||
data.filters.append(schemas.SessionSearchFilterSchema(value=metric_value, type=schemas.FilterType.REFERRER,
|
||||
operator=schemas.SearchEventOperator.IS_ANY))
|
||||
|
||||
full_args, query_part = search_query_parts(data=data, error_status=None, errors_only=False,
|
||||
favorite_only=False, issue=None, project_id=project_id,
|
||||
user_id=None, extra_event=extra_event, extra_conditions=extra_conditions)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from decouple import config
|
|||
from . import smtp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=config("LOGLEVEL", default=logging.info))
|
||||
logging.basicConfig(level=config("LOGLEVEL", default=logging.INFO))
|
||||
|
||||
if smtp.has_smtp():
|
||||
logger.info("valid SMTP configuration found")
|
||||
|
|
|
|||
|
|
@ -394,8 +394,11 @@ def get_all_issue_tracking_projects(context: schemas.CurrentContext = Depends(OR
|
|||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
return error
|
||||
data = integration.issue_handler.get_projects()
|
||||
if "errors" in data:
|
||||
data = integration.issue_handler
|
||||
if isinstance(data, dict) and "errors" in data:
|
||||
return data
|
||||
data = data.get_projects()
|
||||
if isinstance(data, dict) and "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
|
|
@ -406,8 +409,11 @@ def get_integration_metadata(integrationProjectId: int, context: schemas.Current
|
|||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
return error
|
||||
data = integration.issue_handler.get_metas(integrationProjectId)
|
||||
if "errors" in data.keys():
|
||||
data = integration
|
||||
if isinstance(data, dict) and "errors" in data:
|
||||
return data
|
||||
data = data.issue_handler.get_metas(integrationProjectId)
|
||||
if isinstance(data, dict) and "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
|
|
|
|||
|
|
@ -777,7 +777,8 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
|
|||
for f in values.get("filters", []):
|
||||
vals = []
|
||||
for v in f.get("value", []):
|
||||
if v is not None:
|
||||
if v is not None and (f.get("type", "") != FilterType.DURATION.value
|
||||
or str(v).isnumeric()):
|
||||
vals.append(v)
|
||||
f["value"] = vals
|
||||
return values
|
||||
|
|
|
|||
1
ee/api/.gitignore
vendored
1
ee/api/.gitignore
vendored
|
|
@ -273,7 +273,6 @@ Pipfile.lock
|
|||
/chalicelib/core/usability_testing/
|
||||
/NOTES.md
|
||||
/chalicelib/core/db_request_handler.py
|
||||
/routers/subs/spot.py
|
||||
/chalicelib/utils/or_cache/
|
||||
/routers/subs/health.py
|
||||
/chalicelib/core/spot.py
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import chalicelib.utils.exp_ch_helper
|
||||
import schemas
|
||||
from chalicelib.core import countries, events, metadata
|
||||
from chalicelib.utils import ch_client
|
||||
|
|
@ -325,12 +326,13 @@ def get_top_values(project_id, event_type, event_key=None):
|
|||
FROM raw;"""
|
||||
else:
|
||||
colname = TYPE_TO_COLUMN.get(event_type)
|
||||
event_type = exp_ch_helper.get_event_type(event_type)
|
||||
query = f"""WITH raw AS (SELECT DISTINCT {colname} AS c_value,
|
||||
COUNT(1) OVER (PARTITION BY c_value) AS row_count,
|
||||
COUNT(1) OVER () AS total_count
|
||||
FROM experimental.events
|
||||
WHERE project_id = %(project_id)s
|
||||
AND event_type = '{event_type.upper()}'
|
||||
AND event_type = '{event_type}'
|
||||
AND isNotNull(c_value)
|
||||
AND notEmpty(c_value)
|
||||
ORDER BY row_count DESC
|
||||
|
|
|
|||
|
|
@ -450,6 +450,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
|||
elif metric_of == schemas.MetricOfTable.REFERRER:
|
||||
main_col = "referrer"
|
||||
extra_col = ", referrer"
|
||||
extra_where = "WHERE isNotNull(referrer)"
|
||||
elif metric_of == schemas.MetricOfTable.FETCH:
|
||||
main_col = "url_path"
|
||||
extra_col = ", s.url_path"
|
||||
|
|
@ -554,39 +555,6 @@ def __is_valid_event(is_any: bool, event: schemas.SessionSearchEventSchema2):
|
|||
event.filters is None or len(event.filters) == 0))
|
||||
|
||||
|
||||
def __get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEventType], platform="web"):
|
||||
defs = {
|
||||
schemas.EventType.CLICK: "CLICK",
|
||||
schemas.EventType.INPUT: "INPUT",
|
||||
schemas.EventType.LOCATION: "LOCATION",
|
||||
schemas.PerformanceEventType.LOCATION_DOM_COMPLETE: "LOCATION",
|
||||
schemas.PerformanceEventType.LOCATION_LARGEST_CONTENTFUL_PAINT_TIME: "LOCATION",
|
||||
schemas.PerformanceEventType.LOCATION_TTFB: "LOCATION",
|
||||
schemas.EventType.CUSTOM: "CUSTOM",
|
||||
schemas.EventType.REQUEST: "REQUEST",
|
||||
schemas.EventType.REQUEST_DETAILS: "REQUEST",
|
||||
schemas.PerformanceEventType.FETCH_FAILED: "REQUEST",
|
||||
schemas.EventType.STATE_ACTION: "STATEACTION",
|
||||
schemas.EventType.ERROR: "ERROR",
|
||||
schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE',
|
||||
schemas.PerformanceEventType.LOCATION_AVG_MEMORY_USAGE: 'PERFORMANCE'
|
||||
}
|
||||
defs_mobile = {
|
||||
schemas.EventType.CLICK_MOBILE: "TAP",
|
||||
schemas.EventType.INPUT_MOBILE: "INPUT",
|
||||
schemas.EventType.CUSTOM_MOBILE: "CUSTOM",
|
||||
schemas.EventType.REQUEST_MOBILE: "REQUEST",
|
||||
schemas.EventType.ERROR_MOBILE: "CRASH",
|
||||
schemas.EventType.VIEW_MOBILE: "VIEW",
|
||||
schemas.EventType.SWIPE_MOBILE: "SWIPE"
|
||||
}
|
||||
if platform != "web" and event_type in defs_mobile:
|
||||
return defs_mobile.get(event_type)
|
||||
if event_type not in defs:
|
||||
raise Exception(f"unsupported EventType:{event_type}")
|
||||
return defs.get(event_type)
|
||||
|
||||
|
||||
# this function generates the query and return the generated-query with the dict of query arguments
|
||||
def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_status, errors_only, favorite_only, issue,
|
||||
project_id, user_id, platform="web", extra_event=None, extra_deduplication=[],
|
||||
|
|
@ -925,7 +893,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
if platform == "web":
|
||||
_column = events.EventType.CLICK.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(
|
||||
f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if schemas.ClickEventExtraOperator.has_value(event.operator):
|
||||
|
|
@ -937,7 +906,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -945,14 +915,16 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
_column = events.EventType.CLICK_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(
|
||||
f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -963,14 +935,16 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
if platform == "web":
|
||||
_column = events.EventType.INPUT.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(
|
||||
f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -982,14 +956,16 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
full_args = {**full_args, **_multiple_values(event.source, value_key=f"custom{i}")}
|
||||
else:
|
||||
_column = events.EventType.INPUT_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(
|
||||
f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -1000,14 +976,16 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
if platform == "web":
|
||||
_column = 'url_path'
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(
|
||||
f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1015,14 +993,16 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
_column = events.EventType.VIEW_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(
|
||||
f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{
|
||||
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1031,14 +1011,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type == events.EventType.CUSTOM.ui_type:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
_column = events.EventType.CUSTOM.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -1047,14 +1027,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type == events.EventType.REQUEST.ui_type:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
_column = 'url_path'
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -1072,14 +1052,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type == events.EventType.STATEACTION.ui_type:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
_column = events.EventType.STATEACTION.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1089,7 +1069,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type == events.EventType.ERROR.ui_type:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main"
|
||||
events_extra_join = f"SELECT * FROM {MAIN_EVENTS_TABLE} AS main1 WHERE main1.project_id=%(project_id)s"
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
event.source = tuple(event.source)
|
||||
events_conditions[-1]["condition"] = []
|
||||
|
|
@ -1109,14 +1089,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
# ----- Mobile
|
||||
elif event_type == events.EventType.CLICK_MOBILE.ui_type:
|
||||
_column = events.EventType.CLICK_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -1124,14 +1104,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
elif event_type == events.EventType.INPUT_MOBILE.ui_type:
|
||||
_column = events.EventType.INPUT_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -1139,14 +1119,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
elif event_type == events.EventType.VIEW_MOBILE.ui_type:
|
||||
_column = events.EventType.VIEW_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1154,14 +1134,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
elif event_type == events.EventType.CUSTOM_MOBILE.ui_type:
|
||||
_column = events.EventType.CUSTOM_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1170,14 +1150,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type == events.EventType.REQUEST_MOBILE.ui_type:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
_column = 'url_path'
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
|
||||
|
|
@ -1185,14 +1165,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
elif event_type == events.EventType.CRASH_MOBILE.ui_type:
|
||||
_column = events.EventType.CRASH_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1200,14 +1180,14 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
events_conditions[-1]["condition"] = event_where[-1]
|
||||
elif event_type == events.EventType.SWIPE_MOBILE.ui_type and platform != "web":
|
||||
_column = events.EventType.SWIPE_MOBILE.column
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
if not is_any:
|
||||
if is_not:
|
||||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1217,7 +1197,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type == schemas.PerformanceEventType.FETCH_FAILED:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
_column = 'url_path'
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
events_conditions[-1]["condition"] = []
|
||||
if not is_any:
|
||||
|
|
@ -1225,7 +1205,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
event_where.append(_multiple_conditions(f"sub.{_column} {op} %({e_k})s", event.value,
|
||||
value_key=e_k))
|
||||
events_conditions_not.append(
|
||||
{"type": f"sub.event_type='{__get_event_type(event_type, platform=platform)}'"})
|
||||
{"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
|
||||
events_conditions_not[-1]["condition"] = event_where[-1]
|
||||
else:
|
||||
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
|
||||
|
|
@ -1256,7 +1236,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
schemas.PerformanceEventType.LOCATION_LARGEST_CONTENTFUL_PAINT_TIME,
|
||||
schemas.PerformanceEventType.LOCATION_TTFB]:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
events_conditions[-1]["condition"] = []
|
||||
col = performance_event.get_col(event_type)
|
||||
|
|
@ -1279,7 +1259,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
elif event_type in [schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD,
|
||||
schemas.PerformanceEventType.LOCATION_AVG_MEMORY_USAGE]:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
events_conditions[-1]["condition"] = []
|
||||
col = performance_event.get_col(event_type)
|
||||
|
|
@ -1302,9 +1282,9 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
# elif event_type == schemas.PerformanceEventType.time_between_events:
|
||||
# event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
# # event_from = event_from % f"{getattr(events.event_type, event.value[0].type).table} AS main INNER JOIN {getattr(events.event_type, event.value[1].type).table} AS main2 USING(session_id) "
|
||||
# event_where.append(f"main.event_type='{__get_event_type(event.value[0].type, platform=platform)}'")
|
||||
# event_where.append(f"main.event_type='{__exp_ch_helper.get_event_type(event.value[0].type, platform=platform)}'")
|
||||
# events_conditions.append({"type": event_where[-1]})
|
||||
# event_where.append(f"main.event_type='{__get_event_type(event.value[0].type, platform=platform)}'")
|
||||
# event_where.append(f"main.event_type='{__exp_ch_helper.get_event_type(event.value[0].type, platform=platform)}'")
|
||||
# events_conditions.append({"type": event_where[-1]})
|
||||
#
|
||||
# if not isinstance(event.value[0].value, list):
|
||||
|
|
@ -1352,7 +1332,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
|||
# TODO: no isNot for RequestDetails
|
||||
elif event_type == schemas.EventType.REQUEST_DETAILS:
|
||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||
event_where.append(f"main.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'")
|
||||
events_conditions.append({"type": event_where[-1]})
|
||||
apply = False
|
||||
events_conditions[-1]["condition"] = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from typing import Union
|
||||
|
||||
import schemas
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from decouple import config
|
||||
import logging
|
||||
|
|
@ -51,3 +54,37 @@ def get_main_js_errors_sessions_table(timestamp=0):
|
|||
# return "experimental.js_errors_sessions_mv" # \
|
||||
# if config("EXP_7D_MV", cast=bool, default=True) \
|
||||
# and timestamp >= TimeUTC.now(delta_days=-7) else "experimental.events"
|
||||
|
||||
|
||||
def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEventType], platform="web"):
|
||||
defs = {
|
||||
schemas.EventType.CLICK: "CLICK",
|
||||
schemas.EventType.INPUT: "INPUT",
|
||||
schemas.EventType.LOCATION: "LOCATION",
|
||||
schemas.PerformanceEventType.LOCATION_DOM_COMPLETE: "LOCATION",
|
||||
schemas.PerformanceEventType.LOCATION_LARGEST_CONTENTFUL_PAINT_TIME: "LOCATION",
|
||||
schemas.PerformanceEventType.LOCATION_TTFB: "LOCATION",
|
||||
schemas.EventType.CUSTOM: "CUSTOM",
|
||||
schemas.EventType.REQUEST: "REQUEST",
|
||||
schemas.EventType.REQUEST_DETAILS: "REQUEST",
|
||||
schemas.PerformanceEventType.FETCH_FAILED: "REQUEST",
|
||||
schemas.EventType.STATE_ACTION: "STATEACTION",
|
||||
schemas.EventType.ERROR: "ERROR",
|
||||
schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE',
|
||||
schemas.PerformanceEventType.LOCATION_AVG_MEMORY_USAGE: 'PERFORMANCE',
|
||||
schemas.FetchFilterType.FETCH_URL: 'REQUEST'
|
||||
}
|
||||
defs_mobile = {
|
||||
schemas.EventType.CLICK_MOBILE: "TAP",
|
||||
schemas.EventType.INPUT_MOBILE: "INPUT",
|
||||
schemas.EventType.CUSTOM_MOBILE: "CUSTOM",
|
||||
schemas.EventType.REQUEST_MOBILE: "REQUEST",
|
||||
schemas.EventType.ERROR_MOBILE: "CRASH",
|
||||
schemas.EventType.VIEW_MOBILE: "VIEW",
|
||||
schemas.EventType.SWIPE_MOBILE: "SWIPE"
|
||||
}
|
||||
if platform != "web" and event_type in defs_mobile:
|
||||
return defs_mobile.get(event_type)
|
||||
if event_type not in defs:
|
||||
raise Exception(f"unsupported EventType:{event_type}")
|
||||
return defs.get(event_type)
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ rm -rf ./orpy.py
|
|||
rm -rf ./chalicelib/core/usability_testing/
|
||||
rm -rf ./chalicelib/core/db_request_handler.py
|
||||
rm -rf ./chalicelib/core/db_request_handler.py
|
||||
rm -rf ./routers/subs/spot.py
|
||||
rm -rf ./chalicelib/utils/or_cache
|
||||
rm -rf ./routers/subs/health.py
|
||||
rm -rf ./chalicelib/core/spot.py
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ psycopg2-binary==2.9.9
|
|||
psycopg[pool,binary]==3.2.1
|
||||
elasticsearch==8.15.0
|
||||
jira==3.8.0
|
||||
cachetools==5.5.0
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ psycopg2-binary==2.9.9
|
|||
psycopg[pool,binary]==3.2.1
|
||||
elasticsearch==8.15.0
|
||||
jira==3.8.0
|
||||
cachetools==5.5.0
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -90,26 +90,35 @@ async def __process_assertion(request: Request, tenant_key=None) -> Response | d
|
|||
return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]}
|
||||
existing = users.get_by_email_only(email)
|
||||
role_names = user_data.get("role", [])
|
||||
if len(role_names) == 0:
|
||||
logger.info("No role specified, setting role to member")
|
||||
role_names = ["member"]
|
||||
role = None
|
||||
for r in role_names:
|
||||
if r.lower() == existing["roleName"].lower():
|
||||
role = {"roleId": existing["roleId"], "name": r}
|
||||
if len(role_names) == 0:
|
||||
if existing is None:
|
||||
logger.info("No role specified, setting role to member")
|
||||
role_names = ["member"]
|
||||
else:
|
||||
role = roles.get_role_by_name(tenant_id=t['tenantId'], name=r)
|
||||
role_names = [existing["roleName"]]
|
||||
role = {"name": existing["roleName"], "roleId": existing["roleId"]}
|
||||
if role is None:
|
||||
for r in role_names:
|
||||
if r.lower() == existing["roleName"].lower():
|
||||
role = {"roleId": existing["roleId"], "name": r}
|
||||
else:
|
||||
role = roles.get_role_by_name(tenant_id=t['tenantId'], name=r)
|
||||
|
||||
if role is not None:
|
||||
break
|
||||
if role is not None:
|
||||
break
|
||||
|
||||
if role is None:
|
||||
return {"errors": [f"role '{role_names}' not found, please create it in OpenReplay first"]}
|
||||
logger.info(f"received roles:{role_names}; using:{role['name']}")
|
||||
admin_privileges = user_data.get("adminPrivileges", [])
|
||||
admin_privileges = not (len(admin_privileges) == 0
|
||||
or admin_privileges[0] is None
|
||||
or admin_privileges[0].lower() == "false")
|
||||
if len(admin_privileges) == 0:
|
||||
if existing is None:
|
||||
admin_privileges = not (len(admin_privileges) == 0
|
||||
or admin_privileges[0] is None
|
||||
or admin_privileges[0].lower() == "false")
|
||||
else:
|
||||
admin_privileges = existing["admin"]
|
||||
|
||||
internal_id = next(iter(user_data.get("internalId", [])), None)
|
||||
full_name = " ".join(user_data.get("firstName", []) + user_data.get("lastName", []))
|
||||
|
|
|
|||
32
ee/api/routers/subs/spot.py
Normal file
32
ee/api/routers/subs/spot.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from fastapi import Depends
|
||||
from starlette.responses import JSONResponse, Response
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import spot, webhook
|
||||
from or_dependencies import OR_context
|
||||
from routers.base import get_routers
|
||||
|
||||
public_app, app, app_apikey = get_routers(prefix="/spot", tags=["spot"])
|
||||
|
||||
COOKIE_PATH = "/api/spot/refresh"
|
||||
|
||||
|
||||
@app.get('/logout')
|
||||
def logout_spot(response: Response, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
spot.logout(user_id=context.user_id)
|
||||
response.delete_cookie(key="spotRefreshToken", path=COOKIE_PATH)
|
||||
return {"data": "success"}
|
||||
|
||||
|
||||
@app.get('/refresh')
|
||||
def refresh_spot_login(response: JSONResponse, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
r = spot.refresh(user_id=context.user_id, tenant_id=context.tenant_id)
|
||||
content = {"jwt": r.get("jwt")}
|
||||
response.set_cookie(key="spotRefreshToken", value=r.get("refreshToken"), path=COOKIE_PATH,
|
||||
max_age=r.pop("refreshTokenMaxAge"), secure=True, httponly=True)
|
||||
return content
|
||||
|
||||
|
||||
@app.get('/integrations/slack/channels', tags=["integrations"])
|
||||
def get_slack_channels(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": webhook.get_by_type(tenant_id=context.tenant_id, webhook_type=schemas.WebhookType.SLACK)}
|
||||
|
|
@ -23,4 +23,4 @@ MINIO_SECRET_KEY = ''
|
|||
|
||||
# APP and TRACKER VERSIONS
|
||||
VERSION = 1.20.0
|
||||
TRACKER_VERSION = '14.0.6'
|
||||
TRACKER_VERSION = '14.0.7'
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ interface Props {
|
|||
function PrivateRoutes(props: Props) {
|
||||
const { onboarding, sites, siteId } = props;
|
||||
const hasRecordings = sites.some(s => s.recorded);
|
||||
const redirectToSetup = props.scope === 0;
|
||||
const redirectToOnboarding =
|
||||
!onboarding && (localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true' || !hasRecordings) && props.scope > 0;
|
||||
const siteIdList: any = sites.map(({ id }) => id).toJS();
|
||||
|
|
@ -126,6 +127,13 @@ function PrivateRoutes(props: Props) {
|
|||
return (
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<Switch key="content">
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={SCOPE_SETUP}
|
||||
component={enhancedComponents.ScopeSetup}
|
||||
/>
|
||||
{redirectToSetup ? <Redirect to={SCOPE_SETUP} /> : null}
|
||||
<Route path={CLIENT_PATH} component={enhancedComponents.Client} />
|
||||
<Route
|
||||
path={withSiteId(ONBOARDING_PATH, siteIdList)}
|
||||
|
|
@ -143,12 +151,6 @@ function PrivateRoutes(props: Props) {
|
|||
path={SPOT_PATH}
|
||||
component={enhancedComponents.Spot}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={SCOPE_SETUP}
|
||||
component={enhancedComponents.ScopeSetup}
|
||||
/>
|
||||
{props.scope === 1 ? <Redirect to={SPOTS_LIST_PATH} /> : null}
|
||||
<Route
|
||||
path="/integrations/"
|
||||
|
|
|
|||
|
|
@ -10,21 +10,19 @@ import {
|
|||
GLOBAL_DESTINATION_PATH,
|
||||
IFRAME,
|
||||
JWT_PARAM,
|
||||
SPOT_ONBOARDING
|
||||
} from "App/constants/storageKeys";
|
||||
SPOT_ONBOARDING,
|
||||
} from 'App/constants/storageKeys';
|
||||
import Layout from 'App/layout/Layout';
|
||||
import { withStore } from "App/mstore";
|
||||
import { checkParam, handleSpotJWT, isTokenExpired } from "App/utils";
|
||||
import { withStore } from 'App/mstore';
|
||||
import { checkParam, handleSpotJWT, isTokenExpired } from 'App/utils';
|
||||
import { ModalProvider } from 'Components/Modal';
|
||||
import { ModalProvider as NewModalProvider } from 'Components/ModalContext';
|
||||
import { fetchListActive as fetchMetadata } from 'Duck/customField';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { init as initSite } from 'Duck/site';
|
||||
import { fetchUserInfo, getScope, setJwt, logout } from "Duck/user";
|
||||
import { fetchTenants } from 'Duck/user';
|
||||
import { fetchUserInfo, getScope, logout, setJwt } from 'Duck/user';
|
||||
import { Loader } from 'UI';
|
||||
import { spotsList } from "./routes";
|
||||
import * as routes from './routes';
|
||||
|
||||
interface RouterProps
|
||||
|
|
@ -36,7 +34,6 @@ interface RouterProps
|
|||
changePassword: boolean;
|
||||
isEnterprise: boolean;
|
||||
fetchUserInfo: () => any;
|
||||
fetchTenants: () => any;
|
||||
setSessionPath: (path: any) => any;
|
||||
fetchSiteList: (siteId?: number) => any;
|
||||
match: {
|
||||
|
|
@ -45,7 +42,7 @@ interface RouterProps
|
|||
};
|
||||
};
|
||||
mstore: any;
|
||||
setJwt: (params: { jwt: string, spotJwt: string | null }) => any;
|
||||
setJwt: (params: { jwt: string; spotJwt: string | null }) => any;
|
||||
fetchMetadata: (siteId: string) => void;
|
||||
initSite: (site: any) => void;
|
||||
scopeSetup: boolean;
|
||||
|
|
@ -68,15 +65,16 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
logout,
|
||||
} = props;
|
||||
|
||||
const params = new URLSearchParams(location.search)
|
||||
const params = new URLSearchParams(location.search);
|
||||
const spotCb = params.get('spotCallback');
|
||||
const spotReqSent = React.useRef(false)
|
||||
const spotReqSent = React.useRef(false);
|
||||
const [isSpotCb, setIsSpotCb] = React.useState(false);
|
||||
const [isSignup, setIsSignup] = React.useState(false);
|
||||
const [isIframe, setIsIframe] = React.useState(false);
|
||||
const [isJwt, setIsJwt] = React.useState(false);
|
||||
|
||||
const handleJwtFromUrl = () => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const params = new URLSearchParams(location.search);
|
||||
const urlJWT = params.get('jwt');
|
||||
const spotJwt = params.get('spotJwt');
|
||||
if (spotJwt) {
|
||||
|
|
@ -92,6 +90,7 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
return;
|
||||
} else {
|
||||
spotReqSent.current = true;
|
||||
setIsSpotCb(false);
|
||||
}
|
||||
handleSpotJWT(jwt);
|
||||
};
|
||||
|
|
@ -107,13 +106,17 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
|
||||
const handleUserLogin = async () => {
|
||||
if (isSpotCb) {
|
||||
localStorage.setItem(SPOT_ONBOARDING, 'true')
|
||||
localStorage.setItem(SPOT_ONBOARDING, 'true');
|
||||
}
|
||||
await fetchUserInfo();
|
||||
const siteIdFromPath = parseInt(location.pathname.split('/')[1]);
|
||||
await fetchSiteList(siteIdFromPath);
|
||||
props.mstore.initClient();
|
||||
|
||||
if (localSpotJwt && !isTokenExpired(localSpotJwt)) {
|
||||
handleSpotLogin(localSpotJwt);
|
||||
}
|
||||
|
||||
const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH);
|
||||
if (
|
||||
destinationPath &&
|
||||
|
|
@ -144,7 +147,10 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
if (spotCb) {
|
||||
setIsSpotCb(true);
|
||||
}
|
||||
}, [spotCb])
|
||||
if (location.pathname.includes('signup')) {
|
||||
setIsSignup(true);
|
||||
}
|
||||
}, [spotCb]);
|
||||
|
||||
useEffect(() => {
|
||||
handleDestinationPath();
|
||||
|
|
@ -159,22 +165,14 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
}, [isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scopeSetup) {
|
||||
history.push(routes.scopeSetup())
|
||||
}
|
||||
}, [scopeSetup])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && (location.pathname.includes('login') || isSpotCb)) {
|
||||
if (localSpotJwt) {
|
||||
if (!isTokenExpired(localSpotJwt)) {
|
||||
handleSpotLogin(localSpotJwt);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
if (isLoggedIn && isSpotCb && !isSignup) {
|
||||
if (localSpotJwt && !isTokenExpired(localSpotJwt)) {
|
||||
handleSpotLogin(localSpotJwt);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
}, [isSpotCb, location, isLoggedIn, localSpotJwt])
|
||||
}, [isSpotCb, isLoggedIn, localSpotJwt, isSignup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId && siteId !== lastFetchedSiteIdRef.current) {
|
||||
|
|
@ -204,8 +202,7 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
location.pathname.includes('multiview') ||
|
||||
location.pathname.includes('/view-spot/') ||
|
||||
location.pathname.includes('/spots/') ||
|
||||
location.pathname.includes('/scope-setup')
|
||||
|
||||
location.pathname.includes('/scope-setup');
|
||||
|
||||
if (isIframe) {
|
||||
return (
|
||||
|
|
@ -238,8 +235,11 @@ const mapStateToProps = (state: Map<string, any>) => {
|
|||
'loading',
|
||||
]);
|
||||
const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']);
|
||||
const scopeSetup = getScope(state) === 0
|
||||
const loading = Boolean(userInfoLoading) || Boolean(sitesLoading) || (!scopeSetup && !siteId);
|
||||
const scopeSetup = getScope(state) === 0;
|
||||
const loading =
|
||||
Boolean(userInfoLoading) ||
|
||||
Boolean(sitesLoading) ||
|
||||
(!scopeSetup && !siteId);
|
||||
return {
|
||||
siteId,
|
||||
changePassword,
|
||||
|
|
@ -262,7 +262,6 @@ const mapStateToProps = (state: Map<string, any>) => {
|
|||
|
||||
const mapDispatchToProps = {
|
||||
fetchUserInfo,
|
||||
fetchTenants,
|
||||
setSessionPath,
|
||||
fetchSiteList,
|
||||
setJwt,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ArrowRightOutlined } from '@ant-design/icons';
|
|||
import { Button, Card, Radio } from 'antd';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { upgradeScope, downgradeScope } from "App/duck/user";
|
||||
import { upgradeScope, downgradeScope, getScope } from 'App/duck/user';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import * as routes from 'App/routes'
|
||||
import { SPOT_ONBOARDING } from "../../constants/storageKeys";
|
||||
|
|
@ -15,8 +15,18 @@ const Scope = {
|
|||
function ScopeForm({
|
||||
upgradeScope,
|
||||
downgradeScope,
|
||||
scopeState,
|
||||
}: any) {
|
||||
const [scope, setScope] = React.useState(Scope.FULL);
|
||||
React.useEffect(() => {
|
||||
if (scopeState !== 0) {
|
||||
if (scopeState === 2) {
|
||||
history.replace(routes.onboarding())
|
||||
} else {
|
||||
history.replace(routes.spotsList())
|
||||
}
|
||||
}
|
||||
}, [scopeState])
|
||||
React.useEffect(() => {
|
||||
const isSpotSetup = localStorage.getItem(SPOT_ONBOARDING)
|
||||
if (isSpotSetup) {
|
||||
|
|
@ -36,50 +46,52 @@ function ScopeForm({
|
|||
};
|
||||
return (
|
||||
<div className={'flex items-center justify-center w-screen h-screen'}>
|
||||
<Card
|
||||
style={{ width: 540 }}
|
||||
title={'👋 Welcome to OpenReplay'}
|
||||
classNames={{
|
||||
header: 'text-2xl font-semibold text-center',
|
||||
body: 'flex flex-col gap-2',
|
||||
}}
|
||||
>
|
||||
<div className={'font-semibold'}>
|
||||
How will you primarily use OpenReplay?{' '}
|
||||
</div>
|
||||
<div className={'text-disabled-text'}>
|
||||
<div>
|
||||
You will have access to all OpenReplay features regardless of your
|
||||
choice.
|
||||
</div>
|
||||
<div>
|
||||
Your preference will simply help us tailor your onboarding experience.
|
||||
</div>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={scope}
|
||||
onChange={(e) => setScope(e.target.value)}
|
||||
className={'flex flex-col gap-2 mt-4 '}
|
||||
<Card
|
||||
style={{ width: 540 }}
|
||||
title={'👋 Welcome to OpenReplay'}
|
||||
classNames={{
|
||||
header: 'text-2xl font-semibold text-center',
|
||||
body: 'flex flex-col gap-2',
|
||||
}}
|
||||
>
|
||||
<Radio value={'full'}>
|
||||
Session Replay & Debugging, Customer Support and more
|
||||
</Radio>
|
||||
<Radio value={'spot'}>Report bugs via Spot</Radio>
|
||||
</Radio.Group>
|
||||
|
||||
<div className={'self-end'}>
|
||||
<Button
|
||||
type={'primary'}
|
||||
onClick={() => onContinue()}
|
||||
icon={<ArrowRightOutlined />}
|
||||
iconPosition={'end'}
|
||||
<div className={'font-semibold'}>
|
||||
How will you primarily use OpenReplay?{' '}
|
||||
</div>
|
||||
<div className={'text-disabled-text'}>
|
||||
<div>
|
||||
You will have access to all OpenReplay features regardless of your
|
||||
choice.
|
||||
</div>
|
||||
<div>
|
||||
Your preference will simply help us tailor your onboarding experience.
|
||||
</div>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={scope}
|
||||
onChange={(e) => setScope(e.target.value)}
|
||||
className={'flex flex-col gap-2 mt-4 '}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Radio value={'full'}>
|
||||
Session Replay & Debugging, Customer Support and more
|
||||
</Radio>
|
||||
<Radio value={'spot'}>Report bugs via Spot</Radio>
|
||||
</Radio.Group>
|
||||
|
||||
<div className={'self-end'}>
|
||||
<Button
|
||||
type={'primary'}
|
||||
onClick={() => onContinue()}
|
||||
icon={<ArrowRightOutlined />}
|
||||
iconPosition={'end'}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, { upgradeScope, downgradeScope })(ScopeForm);
|
||||
export default connect((state) => ({
|
||||
scopeState: getScope(state),
|
||||
}), { upgradeScope, downgradeScope })(ScopeForm);
|
||||
|
|
|
|||
|
|
@ -83,9 +83,8 @@ function Player(props: IProps) {
|
|||
<div
|
||||
onMouseDown={handleResize}
|
||||
className={'w-full h-2 cursor-ns-resize absolute top-0 left-0 z-20'}
|
||||
>
|
||||
/>
|
||||
<ConsolePanel isLive />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!fullView && !isMultiview ? <LiveControls jump={playerContext.player.jump} /> : null}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { Button, InputNumber, Popover } from 'antd';
|
||||
import { Slider } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
|
@ -24,17 +25,38 @@ function DropdownAudioPlayer({
|
|||
const [isMuted, setIsMuted] = useState(false);
|
||||
const lastPlayerTime = useRef(0);
|
||||
const audioRefs = useRef<Record<string, HTMLAudioElement | null>>({});
|
||||
|
||||
const fileLengths = useRef<Record<string, number>>({});
|
||||
const { time = 0, speed = 1, playing, sessionStart } = store?.get() ?? {};
|
||||
|
||||
const files = audioEvents.map((pa) => {
|
||||
const data = pa.payload;
|
||||
return {
|
||||
url: data.url,
|
||||
timestamp: data.timestamp,
|
||||
start: pa.timestamp - sessionStart,
|
||||
};
|
||||
});
|
||||
const files = React.useMemo(
|
||||
() =>
|
||||
audioEvents.map((pa) => {
|
||||
const data = pa.payload;
|
||||
const nativeTs = data.timestamp;
|
||||
const startTs = nativeTs
|
||||
? nativeTs > sessionStart
|
||||
? nativeTs - sessionStart
|
||||
: nativeTs
|
||||
: pa.timestamp - sessionStart;
|
||||
return {
|
||||
url: data.url,
|
||||
timestamp: data.timestamp,
|
||||
start: startTs,
|
||||
};
|
||||
}),
|
||||
[audioEvents.length, sessionStart]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
Object.entries(audioRefs.current).forEach(([url, audio]) => {
|
||||
if (audio) {
|
||||
audio.loop = false;
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
fileLengths.current[url] = audio.duration;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [audioRefs.current]);
|
||||
|
||||
const toggleMute = () => {
|
||||
Object.values(audioRefs.current).forEach((audio) => {
|
||||
|
|
@ -89,10 +111,15 @@ function DropdownAudioPlayer({
|
|||
if (audio) {
|
||||
const file = files.find((f) => f.url === key);
|
||||
if (file) {
|
||||
audio.currentTime = Math.max(
|
||||
(timeMs + delta * 1000 - file.start) / 1000,
|
||||
0
|
||||
);
|
||||
const targetTime = (timeMs + delta * 1000 - file.start) / 1000;
|
||||
const fileLength = fileLengths.current[key];
|
||||
if (targetTime < 0 || (fileLength && targetTime > fileLength)) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
return;
|
||||
} else {
|
||||
audio.currentTime = targetTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -108,27 +135,39 @@ function DropdownAudioPlayer({
|
|||
|
||||
useEffect(() => {
|
||||
const deltaMs = delta * 1000;
|
||||
if (Math.abs(lastPlayerTime.current - time - deltaMs) >= 250) {
|
||||
const deltaTime = Math.abs(lastPlayerTime.current - time - deltaMs);
|
||||
if (deltaTime >= 250) {
|
||||
handleSeek(time);
|
||||
}
|
||||
Object.entries(audioRefs.current).forEach(([url, audio]) => {
|
||||
if (audio) {
|
||||
const file = files.find((f) => f.url === url);
|
||||
if (file && time >= file.start) {
|
||||
if (audio.paused && playing) {
|
||||
audio.play();
|
||||
const fileLength = fileLengths.current[url];
|
||||
if (file) {
|
||||
if (fileLength && fileLength * 1000 + file.start < time) {
|
||||
return;
|
||||
}
|
||||
if (time >= file.start) {
|
||||
if (audio.paused && playing) {
|
||||
audio.play();
|
||||
}
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
if (audio.muted !== isMuted) {
|
||||
audio.muted = isMuted;
|
||||
}
|
||||
}
|
||||
});
|
||||
lastPlayerTime.current = time + deltaMs;
|
||||
}, [time, delta]);
|
||||
|
||||
useEffect(() => {
|
||||
Object.values(audioRefs.current).forEach((audio) => {
|
||||
if (audio) {
|
||||
audio.muted = isMuted;
|
||||
}
|
||||
});
|
||||
}, [isMuted]);
|
||||
|
||||
useEffect(() => {
|
||||
changePlaybackSpeed(speed);
|
||||
}, [speed]);
|
||||
|
|
@ -137,22 +176,30 @@ function DropdownAudioPlayer({
|
|||
Object.entries(audioRefs.current).forEach(([url, audio]) => {
|
||||
if (audio) {
|
||||
const file = files.find((f) => f.url === url);
|
||||
if (file && playing && time >= file.start) {
|
||||
audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
const fileLength = fileLengths.current[url];
|
||||
if (file) {
|
||||
if (fileLength && fileLength * 1000 + file.start < time) {
|
||||
audio.pause();
|
||||
return;
|
||||
}
|
||||
if (playing && time >= file.start) {
|
||||
audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setVolume(isMuted ? 0 : volume);
|
||||
}, [playing]);
|
||||
|
||||
const buttonIcon =
|
||||
'px-2 cursor-pointer border border-gray-light hover:border-main hover:text-main hover:z-10 h-fit';
|
||||
return (
|
||||
<div className={'relative'}>
|
||||
<div className={'flex items-center'} style={{ height: 24 }}>
|
||||
<Popover
|
||||
trigger={'click'}
|
||||
className={'h-full'}
|
||||
content={
|
||||
<div
|
||||
className={'flex flex-col gap-2 rounded'}
|
||||
|
|
@ -169,20 +216,14 @@ function DropdownAudioPlayer({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'px-2 h-full cursor-pointer border rounded-l border-gray-light hover:border-main hover:text-main hover:z-10'
|
||||
}
|
||||
>
|
||||
<div className={cn(buttonIcon, 'rounded-l')}>
|
||||
{isMuted ? <MutedOutlined /> : <SoundOutlined />}
|
||||
</div>
|
||||
</Popover>
|
||||
<div
|
||||
onClick={toggleVisible}
|
||||
style={{ marginLeft: -1 }}
|
||||
className={
|
||||
'px-2 h-full border rounded-r border-gray-light cursor-pointer hover:border-main hover:text-main hover:z-10'
|
||||
}
|
||||
className={cn(buttonIcon, 'rounded-r')}
|
||||
>
|
||||
<CaretDownOutlined />
|
||||
</div>
|
||||
|
|
@ -236,6 +277,7 @@ function DropdownAudioPlayer({
|
|||
<div style={{ display: 'none' }}>
|
||||
{files.map((file) => (
|
||||
<audio
|
||||
loop={false}
|
||||
key={file.url}
|
||||
ref={(el) => (audioRefs.current[file.url] = el)}
|
||||
controls
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function ConsoleRow(props: Props) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={cn(stl.line, 'flex py-2 px-4 overflow-hidden group relative select-none', {
|
||||
className={cn(stl.line, 'flex py-2 px-4 overflow-hidden group relative', {
|
||||
info: !log.isYellow && !log.isRed,
|
||||
warn: log.isYellow,
|
||||
error: log.isRed,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const SpotsListHeader = observer(
|
|||
onDelete,
|
||||
selectedCount,
|
||||
onClearSelection,
|
||||
isEmpty,
|
||||
tenantHasSpots,
|
||||
onRefresh,
|
||||
}: {
|
||||
onDelete: () => void;
|
||||
|
|
@ -18,6 +18,7 @@ const SpotsListHeader = observer(
|
|||
onClearSelection: () => void;
|
||||
onRefresh: () => void;
|
||||
isEmpty?: boolean;
|
||||
tenantHasSpots: boolean;
|
||||
}) => {
|
||||
const { spotStore } = useStore();
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ const SpotsListHeader = observer(
|
|||
<ReloadButton buttonSize={'small'} onClick={onRefresh} iconSize={16} />
|
||||
</div>
|
||||
|
||||
{isEmpty ? null : (
|
||||
{tenantHasSpots ? (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className={'ml-auto'}>
|
||||
{selectedCount > 0 && (
|
||||
|
|
@ -90,7 +91,7 @@ const SpotsListHeader = observer(
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ function SpotsList() {
|
|||
selectedCount={selectedSpots.length}
|
||||
onClearSelection={clearSelection}
|
||||
isEmpty={isEmpty}
|
||||
tenantHasSpots={spotStore.tenantHasSpots}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ function ConsolePanel({
|
|||
exceptionsList = [],
|
||||
logListNow = [],
|
||||
exceptionsListNow = [],
|
||||
} = tabStates[currentTab];
|
||||
} = tabStates[currentTab] ?? {};
|
||||
|
||||
const list = isLive
|
||||
? (useMemo(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function ConsoleRow(props: Props) {
|
|||
<div
|
||||
style={style}
|
||||
className={cn(
|
||||
'border-b flex items-start py-1 px-4 pe-8 overflow-hidden group relative select-none',
|
||||
'border-b flex items-start py-1 px-4 pe-8 overflow-hidden group relative',
|
||||
{
|
||||
info: !log.isYellow && !log.isRed,
|
||||
warn: log.isYellow,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const ALL = 'ALL';
|
|||
const TAB_KEYS = [ALL, ...typeList] as const;
|
||||
const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }));
|
||||
|
||||
type EventsList = Array<Timed & { name: string; source: string; key: string }>;
|
||||
type EventsList = Array<Timed & { name: string; source: string; key: string; payload?: string[] }>;
|
||||
|
||||
const WebStackEventPanelComp = observer(
|
||||
({
|
||||
|
|
@ -95,7 +95,7 @@ export const MobileStackEventPanel = connect((state: Record<string, any>) => ({
|
|||
zoomEndTs: state.getIn(['components', 'player']).timelineZoom.endTs,
|
||||
}))(MobileStackEventPanelComp);
|
||||
|
||||
function EventsPanel({
|
||||
const EventsPanel = observer(({
|
||||
list,
|
||||
listNow,
|
||||
jump,
|
||||
|
|
@ -109,7 +109,7 @@ function EventsPanel({
|
|||
zoomEnabled: boolean;
|
||||
zoomStartTs: number;
|
||||
zoomEndTs: number;
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
} = useStore();
|
||||
|
|
@ -126,13 +126,19 @@ function EventsPanel({
|
|||
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
|
||||
);
|
||||
|
||||
let filteredList = useRegExListFilterMemo(inZoomRangeList, (it) => it.name, filter);
|
||||
let filteredList = useRegExListFilterMemo(inZoomRangeList, (it) => {
|
||||
const searchBy = [it.name]
|
||||
if (it.payload) {
|
||||
const payload = Array.isArray(it.payload) ? it.payload.join(',') : JSON.stringify(it.payload);
|
||||
searchBy.push(payload);
|
||||
}
|
||||
return searchBy
|
||||
}, filter);
|
||||
filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab);
|
||||
|
||||
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
|
||||
devTools.update(INDEX_KEY, { activeTab });
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
|
||||
devTools.update(INDEX_KEY, { filter: value });
|
||||
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value });
|
||||
const tabs = useMemo(
|
||||
() => TABS.filter(({ key }) => key === ALL || inZoomRangeList.some(({ source }) => key === source)),
|
||||
[inZoomRangeList.length]
|
||||
|
|
@ -229,4 +235,4 @@ function EventsPanel({
|
|||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
|
||||
|
||||
import { spotService } from 'App/services';
|
||||
import { UpdateSpotRequest } from 'App/services/spotService';
|
||||
|
||||
|
||||
|
||||
import { Spot } from './types/spot';
|
||||
|
||||
|
||||
export default class SpotStore {
|
||||
isLoading: boolean = false;
|
||||
spots: Spot[] = [];
|
||||
|
|
@ -18,6 +23,7 @@ export default class SpotStore {
|
|||
pubKey: { value: string; expiration: number } | null = null;
|
||||
readonly order = 'desc';
|
||||
accessError = false;
|
||||
tenantHasSpots = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
|
@ -81,13 +87,18 @@ export default class SpotStore {
|
|||
limit: this.limit,
|
||||
} as const;
|
||||
|
||||
const response = await this.withLoader(() =>
|
||||
const { spots, tenantHasSpots, total } = await this.withLoader(() =>
|
||||
spotService.fetchSpots(filters)
|
||||
);
|
||||
this.setSpots(response.spots.map((spot: any) => new Spot(spot)));
|
||||
this.setTotal(response.total);
|
||||
this.setSpots(spots.map((spot: any) => new Spot(spot)));
|
||||
this.setTotal(total);
|
||||
this.setTenantHasSpots(tenantHasSpots);
|
||||
};
|
||||
|
||||
setTenantHasSpots(hasSpots: boolean) {
|
||||
this.tenantHasSpots = hasSpots;
|
||||
}
|
||||
|
||||
async fetchSpotById(id: string) {
|
||||
try {
|
||||
const response = await this.withLoader(() =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import logger from 'App/logger';
|
||||
import { resolveURL } from "../../messages/rewriter/urlResolve";
|
||||
|
||||
import type Screen from '../../Screen/Screen';
|
||||
import type { Message, SetNodeScroll } from '../../messages';
|
||||
|
|
@ -32,6 +31,8 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
private readonly vTexts: Map<number, VText> = new Map() // map vs object here?
|
||||
private readonly vElements: Map<number, VElement> = new Map()
|
||||
private readonly olVRoots: Map<number, OnloadVRoot> = new Map()
|
||||
/** required to keep track of iframes, frameId : vnodeId */
|
||||
private readonly iframeRoots: Record<number, number> = {}
|
||||
/** Constructed StyleSheets https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
|
||||
* as well as <style> tag owned StyleSheets
|
||||
*/
|
||||
|
|
@ -219,6 +220,10 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) {
|
||||
vElem.prioritized = true
|
||||
}
|
||||
if (this.vElements.has(msg.id)) {
|
||||
logger.error("CreateElementNode: Node already exists", msg)
|
||||
return
|
||||
}
|
||||
this.vElements.set(msg.id, vElem)
|
||||
this.insertNode(msg)
|
||||
this.removeBodyScroll(msg.id, vElem)
|
||||
|
|
@ -316,6 +321,10 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
case MType.CreateIFrameDocument: {
|
||||
const vElem = this.vElements.get(msg.frameID)
|
||||
if (!vElem) { logger.error("CreateIFrameDocument: Node not found", msg); return }
|
||||
if (this.iframeRoots[msg.frameID] && !this.olVRoots.has(msg.id)) {
|
||||
this.olVRoots.delete(this.iframeRoots[msg.frameID])
|
||||
}
|
||||
this.iframeRoots[msg.frameID] = msg.id
|
||||
const vRoot = OnloadVRoot.fromVElement(vElem)
|
||||
vRoot.catch(e => logger.warn(e, msg))
|
||||
this.olVRoots.set(msg.id, vRoot)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ interface AddCommentRequest {
|
|||
interface GetSpotsResponse {
|
||||
spots: SpotInfo[];
|
||||
total: number;
|
||||
tenantHasSpots: boolean;
|
||||
}
|
||||
|
||||
interface GetSpotsRequest {
|
||||
|
|
|
|||
|
|
@ -504,7 +504,6 @@ export function truncateStringToFit(string: string, screenWidth: number, charWid
|
|||
|
||||
let sendingRequest = false;
|
||||
export const handleSpotJWT = (jwt: string) => {
|
||||
console.log(jwt, sendingRequest)
|
||||
let tries = 0;
|
||||
if (!jwt || sendingRequest) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"@floating-ui/react-dom-interactions": "^0.10.3",
|
||||
"@medv/finder": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.2",
|
||||
"@sentry/browser": "^5.21.1",
|
||||
"@sentry/browser": "^8.34.0",
|
||||
"@svg-maps/world": "^1.0.1",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@wojtekmaj/react-daterange-picker": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -3188,67 +3188,90 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/browser@npm:^5.21.1":
|
||||
version: 5.30.0
|
||||
resolution: "@sentry/browser@npm:5.30.0"
|
||||
"@sentry-internal/browser-utils@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry-internal/browser-utils@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry/core": 5.30.0
|
||||
"@sentry/types": 5.30.0
|
||||
"@sentry/utils": 5.30.0
|
||||
tslib: ^1.9.3
|
||||
checksum: 6793e1b49a8cdb1f025115bcc591bf67c97b6515f62a33ffcbb7b1ab66e459ebc471797d02e471be1ebf14092b56eb25ed914f043962388cc224bc961e334a17
|
||||
"@sentry/core": 8.34.0
|
||||
"@sentry/types": 8.34.0
|
||||
"@sentry/utils": 8.34.0
|
||||
checksum: fb764f52a989307bb6369a2ae24bb83ef9880c108d2cc2aba94106c846dae743ba379291c84539306d26c3f35f16b6cd2341fa32e85cb942057f2905a33e82bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/core@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@sentry/core@npm:5.30.0"
|
||||
"@sentry-internal/feedback@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry-internal/feedback@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry/hub": 5.30.0
|
||||
"@sentry/minimal": 5.30.0
|
||||
"@sentry/types": 5.30.0
|
||||
"@sentry/utils": 5.30.0
|
||||
tslib: ^1.9.3
|
||||
checksum: 6407b9c2a6a56f90c198f5714b3257df24d89d1b4ca6726bd44760d0adabc25798b69fef2c88ccea461c7e79e3c78861aaebfd51fd3cb892aee656c3f7e11801
|
||||
"@sentry/core": 8.34.0
|
||||
"@sentry/types": 8.34.0
|
||||
"@sentry/utils": 8.34.0
|
||||
checksum: 7137a6b589cb56b541df52abd75a73280d3f8fd09f1983f298e29647c5dd941d5fc404d32599d4c1fe2bdcb7693d0e7886f2c08c10ad1eb7c8e17cad650e4cb3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/hub@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@sentry/hub@npm:5.30.0"
|
||||
"@sentry-internal/replay-canvas@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry-internal/replay-canvas@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry/types": 5.30.0
|
||||
"@sentry/utils": 5.30.0
|
||||
tslib: ^1.9.3
|
||||
checksum: 386c91d06aa44be0465fc11330d748a113e464d41cd562a9e1d222a682cbcb14e697a3e640953e7a0239997ad8a02b223a0df3d9e1d8816cb823fd3613be3e2f
|
||||
"@sentry-internal/replay": 8.34.0
|
||||
"@sentry/core": 8.34.0
|
||||
"@sentry/types": 8.34.0
|
||||
"@sentry/utils": 8.34.0
|
||||
checksum: 55c53be37e0c06706e099a96d1485636b8d3f11b72078c279fda6e7992205d217d27dae9e609db2c0466db0755bd038087e76cfe746eaff9ce39bbfd1f1571a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/minimal@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@sentry/minimal@npm:5.30.0"
|
||||
"@sentry-internal/replay@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry-internal/replay@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry/hub": 5.30.0
|
||||
"@sentry/types": 5.30.0
|
||||
tslib: ^1.9.3
|
||||
checksum: 34ec05503de46d01f98c94701475d5d89cc044892c86ccce30e01f62f28344eb23b718e7cf573815e46f30a4ac9da3129bed9b3d20c822938acfb40cbe72437b
|
||||
"@sentry-internal/browser-utils": 8.34.0
|
||||
"@sentry/core": 8.34.0
|
||||
"@sentry/types": 8.34.0
|
||||
"@sentry/utils": 8.34.0
|
||||
checksum: 8a4b6f1f169584ddd62c372760168ea2d63ca0d6ebd6433e45d760fcbb2610418a2bf6546bbda49ecd619deddf39b4ac268b87a15adbb56efc0b86edf4c40dd9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/types@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@sentry/types@npm:5.30.0"
|
||||
checksum: 99c6e55c0a82c8ca95be2e9dbb35f581b29e4ff7af74b23bc62b690de4e35febfa15868184a2303480ef86babd4fea5273cf3b5ddf4a27685b841a72f13a0c88
|
||||
"@sentry/browser@npm:^8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry/browser@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils": 8.34.0
|
||||
"@sentry-internal/feedback": 8.34.0
|
||||
"@sentry-internal/replay": 8.34.0
|
||||
"@sentry-internal/replay-canvas": 8.34.0
|
||||
"@sentry/core": 8.34.0
|
||||
"@sentry/types": 8.34.0
|
||||
"@sentry/utils": 8.34.0
|
||||
checksum: 8a08033fce2908018cc3fc81cf1110a93a338c0d370628a2e9aaa9f43703041824462474037e59b2b166141835b7e94b437325bd6a46bb8371e37b659b216d10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/utils@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@sentry/utils@npm:5.30.0"
|
||||
"@sentry/core@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry/core@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry/types": 5.30.0
|
||||
tslib: ^1.9.3
|
||||
checksum: ca8eebfea7ac7db6d16f6c0b8a66ac62587df12a79ce9d0d8393f4d69880bb8d40d438f9810f7fb107a9880fe0d68bbf797b89cbafd113e89a0829eb06b205f8
|
||||
"@sentry/types": 8.34.0
|
||||
"@sentry/utils": 8.34.0
|
||||
checksum: 0ab7e11bd382cb47ade38f3c9615e6fb876bad43eba4b376a51e44b1c57e00efe2e74f3cc0790a8da6c0be16093086bc65d89cf5387453f93ae96e10a41a0d60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/types@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry/types@npm:8.34.0"
|
||||
checksum: d35bf72129f621af2f7916b0805c6948d210791757bee690fc6b68f2412bbe80c8ec704a0f8eb8ee45eb78deeadbd3c69830469b62fba4827506ea30c235f4e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/utils@npm:8.34.0":
|
||||
version: 8.34.0
|
||||
resolution: "@sentry/utils@npm:8.34.0"
|
||||
dependencies:
|
||||
"@sentry/types": 8.34.0
|
||||
checksum: 60612dba8320c736f9559ba2fb4efe2927fd9d4a1f29bff36f116ad30c9ce210f6677013052a69cb7e16c5d28f1d8d7465d9278e72f7384b84e924cf3ed2790c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -18149,7 +18172,7 @@ __metadata:
|
|||
"@medv/finder": ^3.1.0
|
||||
"@openreplay/sourcemap-uploader": ^3.0.8
|
||||
"@reduxjs/toolkit": ^2.2.2
|
||||
"@sentry/browser": ^5.21.1
|
||||
"@sentry/browser": ^8.34.0
|
||||
"@storybook/addon-actions": ^6.5.12
|
||||
"@storybook/addon-docs": ^6.5.12
|
||||
"@storybook/addon-essentials": ^6.5.12
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ docker rmi alpine || true
|
|||
# Signing image
|
||||
# cosign sign --key awskms:///alias/openreplay-container-sign image_url:tag
|
||||
export SIGN_IMAGE=1
|
||||
export ARCH=${ARCH:-"amd64"}
|
||||
export PUSH_IMAGE=0
|
||||
export AWS_DEFAULT_REGION="eu-central-1"
|
||||
export SIGN_KEY="awskms:///alias/openreplay-container-sign"
|
||||
|
|
@ -21,17 +22,17 @@ echo $DOCKER_REPO
|
|||
} || {
|
||||
# docker login $DOCKER_REPO
|
||||
# tmux set-option remain-on-exit on
|
||||
tmux split-window "cd ../../backend && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../assist && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../backend && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../assist && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux select-layout tiled
|
||||
tmux split-window "cd ../../peers && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../frontend && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../peers && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../frontend && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux select-layout tiled
|
||||
tmux split-window "cd ../../sourcemapreader && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../api && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@ \
|
||||
&& DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build_alerts.sh $@ \
|
||||
&& DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build_crons.sh $@ \
|
||||
&& cd ../assist-stats && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../sourcemapreader && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux split-window "cd ../../api && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@ \
|
||||
&& DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build_alerts.sh $@ \
|
||||
&& DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build_crons.sh $@ \
|
||||
&& cd ../assist-stats && DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=$ARCH IMAGE_TAG=$IMAGE_TAG DOCKER_REPO=$DOCKER_REPO PUSH_IMAGE=0 bash build.sh $@; read"
|
||||
tmux select-layout tiled
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ function install_openreplay_actions() {
|
|||
sudo rm -rf $openreplay_code_dir
|
||||
fi
|
||||
sudo cp -rfb ./vars.yaml $openreplay_home_dir
|
||||
sudo cp -rf "$(cd ../.. && pwd)" $openreplay_code_dir
|
||||
sudo cp -rf "$(cd ../.. && pwd)" $openreplay_home_dir
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ function status() {
|
|||
return
|
||||
}
|
||||
|
||||
# Create OR version patch with gith sha
|
||||
function patch_version() {
|
||||
# Patching config version for console
|
||||
version=$(/var/lib/openreplay/yq '.fromVersion' vars.yaml)-$(sudo git rev-parse --short HEAD)
|
||||
|
|
@ -385,7 +386,7 @@ function upgrade() {
|
|||
|
||||
time_now=$(date +%m-%d-%Y-%I%M%S)
|
||||
# Creating backup dir of current installation
|
||||
[[ -d "$OR_DIR/openreplay" ]] && sudo mv "$OR_DIR/openreplay" "$OR_DIR/openreplay_${or_version//\"/}_${time_now}"
|
||||
[[ -d "$OR_DIR/openreplay" ]] && sudo cp -rf "$OR_DIR/openreplay" "$OR_DIR/openreplay_${or_version//\"/}_${time_now}"
|
||||
|
||||
clone_repo
|
||||
err_cd openreplay/scripts/helmcharts
|
||||
|
|
@ -406,7 +407,8 @@ function upgrade() {
|
|||
sudo mv ./openreplay-cli /bin/openreplay
|
||||
sudo chmod +x /bin/openreplay
|
||||
sudo mv ./vars.yaml "$OR_DIR"
|
||||
sudo cp -rf ../../../openreplay "$OR_DIR/"
|
||||
sudo rm -rf "$OR_DIR/openreplay" || true
|
||||
sudo cp -rf "${tmp_dir}/openreplay" "$OR_DIR/"
|
||||
log info "Configuration file is saved in /var/lib/openreplay/vars.yaml"
|
||||
log info "Run ${BWHITE}openreplay -h${GREEN} to see the cli information to manage OpenReplay."
|
||||
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ version: 0.1.1
|
|||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
AppVersion: "v1.20.1"
|
||||
AppVersion: "v1.20.5"
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ version: 0.1.7
|
|||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
AppVersion: "v1.20.0"
|
||||
AppVersion: "v1.20.7"
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ version: 0.1.10
|
|||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
AppVersion: "v1.20.4"
|
||||
AppVersion: "v1.20.13"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
apiVersion: v2
|
||||
name: spot
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
|
|
@ -11,14 +10,12 @@ description: A Helm chart for Kubernetes
|
|||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
AppVersion: "v1.20.0"
|
||||
AppVersion: "v1.20.1"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,23 @@ spec:
|
|||
- |
|
||||
set -x
|
||||
mkdir -p /opt/openreplay/openreplay && cd /opt/openreplay/openreplay
|
||||
git clone {{ .Values.global.dbMigrationUpstreamRepoURL | default "https://github.com/openreplay/openreplay" }} .
|
||||
|
||||
# Function to check if GitHub is available
|
||||
check_github() {
|
||||
for i in {1..10}; do
|
||||
if ping -c 1 github.com &> /dev/null || wget -q --spider https://github.com; then
|
||||
echo "GitHub is available."
|
||||
git clone {{ .Values.global.dbMigrationUpstreamRepoURL | default "https://github.com/openreplay/openreplay" }} .
|
||||
break
|
||||
else
|
||||
echo "GitHub is not available. Retrying in 3 seconds..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_github
|
||||
|
||||
ls /opt/openreplay/openreplay
|
||||
git checkout {{ default .Chart.AppVersion .Values.dbMigrationUpstreamBranch }} || exit 10
|
||||
git log -1
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "wxt-starter",
|
||||
"description": "manifest.json description",
|
||||
"private": true,
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 519 KiB |
|
|
@ -52,6 +52,12 @@ export interface StartOptions {
|
|||
forceNew?: boolean
|
||||
sessionHash?: string
|
||||
assistOnly?: boolean
|
||||
/**
|
||||
* @deprecated We strongly advise to use .start().then instead.
|
||||
*
|
||||
* This method is kept for snippet compatibility only
|
||||
* */
|
||||
startCallback?: (result: StartPromiseReturn) => void
|
||||
}
|
||||
|
||||
interface OnStartInfo {
|
||||
|
|
@ -161,6 +167,12 @@ type AppOptions = {
|
|||
}
|
||||
|
||||
network?: NetworkOptions
|
||||
/**
|
||||
* use this flag if you're using Angular
|
||||
* basically goes around window.Zone api changes to mutation observer
|
||||
* and event listeners
|
||||
* */
|
||||
angularMode?: boolean
|
||||
} & WebworkerOptions &
|
||||
SessOptions
|
||||
|
||||
|
|
@ -185,12 +197,14 @@ const proto = {
|
|||
resp: 'never-gonna-let-you-down',
|
||||
// regenerating id (copied other tab)
|
||||
reg: 'never-gonna-run-around-and-desert-you',
|
||||
// tracker inside a child iframe
|
||||
iframeSignal: 'never-gonna-make-you-cry',
|
||||
// getting node id for child iframe
|
||||
iframeId: 'never-gonna-say-goodbye',
|
||||
// batch of messages from an iframe window
|
||||
iframeBatch: 'never-gonna-tell-a-lie-and-hurt-you',
|
||||
iframeSignal: 'tracker inside a child iframe',
|
||||
iframeId: 'getting node id for child iframe',
|
||||
iframeBatch: 'batch of messages from an iframe window',
|
||||
parentAlive: 'signal that parent is live',
|
||||
killIframe: 'stop tracker inside frame',
|
||||
startIframe: 'start tracker inside frame',
|
||||
// checking updates
|
||||
polling: 'hello-how-are-you-im-under-the-water-please-help-me',
|
||||
} as const
|
||||
|
||||
export default class App {
|
||||
|
|
@ -237,7 +251,6 @@ export default class App {
|
|||
private rootId: number | null = null
|
||||
private pageFrames: HTMLIFrameElement[] = []
|
||||
private frameOderNumber = 0
|
||||
private readonly initialHostName = location.hostname
|
||||
private features = {
|
||||
'feature-flags': true,
|
||||
'usability-test': true,
|
||||
|
|
@ -248,7 +261,7 @@ export default class App {
|
|||
sessionToken: string | undefined,
|
||||
options: Partial<Options>,
|
||||
private readonly signalError: (error: string, apis: string[]) => void,
|
||||
private readonly insideIframe: boolean,
|
||||
public readonly insideIframe: boolean,
|
||||
) {
|
||||
this.contextId = Math.random().toString(36).slice(2)
|
||||
this.projectKey = projectKey
|
||||
|
|
@ -305,6 +318,7 @@ export default class App {
|
|||
__save_canvas_locally: false,
|
||||
useAnimationFrame: false,
|
||||
},
|
||||
angularMode: false,
|
||||
}
|
||||
this.options = simpleMerge(defaultOptions, options)
|
||||
|
||||
|
|
@ -322,7 +336,7 @@ export default class App {
|
|||
this.localStorage = this.options.localStorage ?? window.localStorage
|
||||
this.sessionStorage = this.options.sessionStorage ?? window.sessionStorage
|
||||
this.sanitizer = new Sanitizer(this, options)
|
||||
this.nodes = new Nodes(this.options.node_id)
|
||||
this.nodes = new Nodes(this.options.node_id, Boolean(options.angularMode))
|
||||
this.observer = new Observer(this, options)
|
||||
this.ticker = new Ticker(this)
|
||||
this.ticker.attach(() => this.commit())
|
||||
|
|
@ -348,136 +362,31 @@ export default class App {
|
|||
this.session.applySessionHash(sessionToken)
|
||||
}
|
||||
|
||||
this.initWorker()
|
||||
|
||||
const thisTab = this.session.getTabId()
|
||||
|
||||
if (this.insideIframe) {
|
||||
/**
|
||||
* listen for messages from parent window, so we can signal that we're alive
|
||||
* */
|
||||
window.addEventListener('message', this.parentCrossDomainFrameListener)
|
||||
setInterval(() => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
line: proto.polling,
|
||||
context: this.contextId,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}, 250)
|
||||
} else {
|
||||
this.initWorker()
|
||||
}
|
||||
if (!this.insideIframe) {
|
||||
/**
|
||||
* if we get a signal from child iframes, we check for their node_id and send it back,
|
||||
* so they can act as if it was just a same-domain iframe
|
||||
* */
|
||||
let crossdomainFrameCount = 0
|
||||
const catchIframeMessage = (event: MessageEvent) => {
|
||||
const { data } = event
|
||||
if (data.line === proto.iframeSignal) {
|
||||
const childIframeDomain = data.domain
|
||||
const pageIframes = Array.from(document.querySelectorAll('iframe'))
|
||||
this.pageFrames = pageIframes
|
||||
const signalId = async () => {
|
||||
let tries = 0
|
||||
while (tries < 10) {
|
||||
const id = this.checkNodeId(pageIframes, childIframeDomain)
|
||||
if (id) {
|
||||
this.waitStarted()
|
||||
.then(() => {
|
||||
crossdomainFrameCount++
|
||||
const token = this.session.getSessionToken()
|
||||
const iframeData = {
|
||||
line: proto.iframeId,
|
||||
context: this.contextId,
|
||||
domain: childIframeDomain,
|
||||
id,
|
||||
token,
|
||||
frameOrderNumber: crossdomainFrameCount,
|
||||
}
|
||||
this.debug.log('iframe_data', iframeData)
|
||||
// @ts-ignore
|
||||
event.source?.postMessage(iframeData, '*')
|
||||
})
|
||||
.catch(console.error)
|
||||
tries = 10
|
||||
break
|
||||
}
|
||||
tries++
|
||||
await delay(100)
|
||||
}
|
||||
}
|
||||
void signalId()
|
||||
}
|
||||
/**
|
||||
* proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
|
||||
* plus we rewrite some of the messages to be relative to the main context/window
|
||||
* */
|
||||
if (data.line === proto.iframeBatch) {
|
||||
const msgBatch = data.messages
|
||||
const mappedMessages: Message[] = msgBatch.map((msg: Message) => {
|
||||
if (msg[0] === MType.MouseMove) {
|
||||
let fixedMessage = msg
|
||||
this.pageFrames.forEach((frame) => {
|
||||
if (frame.dataset.domain === event.data.domain) {
|
||||
const [type, x, y] = msg
|
||||
const { left, top } = frame.getBoundingClientRect()
|
||||
fixedMessage = [type, x + left, y + top]
|
||||
}
|
||||
})
|
||||
return fixedMessage
|
||||
}
|
||||
if (msg[0] === MType.MouseClick) {
|
||||
let fixedMessage = msg
|
||||
this.pageFrames.forEach((frame) => {
|
||||
if (frame.dataset.domain === event.data.domain) {
|
||||
const [type, id, hesitationTime, label, selector, normX, normY] = msg
|
||||
const { left, top, width, height } = frame.getBoundingClientRect()
|
||||
|
||||
const contentWidth = document.documentElement.scrollWidth
|
||||
const contentHeight = document.documentElement.scrollHeight
|
||||
// (normalizedX * frameWidth + frameLeftOffset)/docSize
|
||||
const fullX = (normX / 100) * width + left
|
||||
const fullY = (normY / 100) * height + top
|
||||
const fixedX = fullX / contentWidth
|
||||
const fixedY = fullY / contentHeight
|
||||
|
||||
fixedMessage = [
|
||||
type,
|
||||
id,
|
||||
hesitationTime,
|
||||
label,
|
||||
selector,
|
||||
Math.round(fixedX * 1e3) / 1e1,
|
||||
Math.round(fixedY * 1e3) / 1e1,
|
||||
]
|
||||
}
|
||||
})
|
||||
return fixedMessage
|
||||
}
|
||||
return msg
|
||||
})
|
||||
this.messages.push(...mappedMessages)
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', catchIframeMessage)
|
||||
this.attachStopCallback(() => {
|
||||
window.removeEventListener('message', catchIframeMessage)
|
||||
})
|
||||
} else {
|
||||
const catchParentMessage = (event: MessageEvent) => {
|
||||
const { data } = event
|
||||
if (data.line !== proto.iframeId) {
|
||||
return
|
||||
}
|
||||
this.rootId = data.id
|
||||
this.session.setSessionToken(data.token as string)
|
||||
this.frameOderNumber = data.frameOrderNumber
|
||||
this.debug.log('starting iframe tracking', data)
|
||||
this.allowAppStart()
|
||||
}
|
||||
window.addEventListener('message', catchParentMessage)
|
||||
this.attachStopCallback(() => {
|
||||
window.removeEventListener('message', catchParentMessage)
|
||||
})
|
||||
// communicating with parent window,
|
||||
// even if its crossdomain is possible via postMessage api
|
||||
const domain = this.initialHostName
|
||||
window.parent.postMessage(
|
||||
{
|
||||
line: proto.iframeSignal,
|
||||
source: thisTab,
|
||||
context: this.contextId,
|
||||
domain,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
window.addEventListener('message', this.crossDomainIframeListener)
|
||||
}
|
||||
|
||||
if (this.bc !== null) {
|
||||
|
|
@ -488,7 +397,7 @@ export default class App {
|
|||
})
|
||||
this.startTimeout = setTimeout(() => {
|
||||
this.allowAppStart()
|
||||
}, 500)
|
||||
}, 250)
|
||||
this.bc.onmessage = (ev: MessageEvent<RickRoll>) => {
|
||||
if (ev.data.context === this.contextId) {
|
||||
return
|
||||
|
|
@ -519,8 +428,204 @@ export default class App {
|
|||
}
|
||||
}
|
||||
|
||||
/** used by child iframes for crossdomain only */
|
||||
/** used by child iframes for crossdomain only */
|
||||
parentActive = false
|
||||
checkStatus = () => {
|
||||
return this.parentActive
|
||||
}
|
||||
parentCrossDomainFrameListener = (event: MessageEvent) => {
|
||||
const { data } = event
|
||||
if (!data || event.source === window) return
|
||||
if (data.line === proto.startIframe) {
|
||||
if (this.active()) return
|
||||
try {
|
||||
this.allowAppStart()
|
||||
void this.start()
|
||||
} catch (e) {
|
||||
console.error('children frame restart failed:', e)
|
||||
}
|
||||
}
|
||||
if (data.line === proto.parentAlive) {
|
||||
this.parentActive = true
|
||||
}
|
||||
if (data.line === proto.iframeId) {
|
||||
this.parentActive = true
|
||||
this.rootId = data.id
|
||||
this.session.setSessionToken(data.token as string)
|
||||
this.frameOderNumber = data.frameOrderNumber
|
||||
this.debug.log('starting iframe tracking', data)
|
||||
this.allowAppStart()
|
||||
}
|
||||
if (data.line === proto.killIframe) {
|
||||
if (this.active()) {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* context ids for iframes,
|
||||
* order is not so important as long as its consistent
|
||||
* */
|
||||
trackedFrames: string[] = []
|
||||
crossDomainIframeListener = (event: MessageEvent) => {
|
||||
if (!this.active() || event.source === window) return
|
||||
const { data } = event
|
||||
if (!data) return
|
||||
if (data.line === proto.iframeSignal) {
|
||||
// @ts-ignore
|
||||
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*')
|
||||
const pageIframes = Array.from(document.querySelectorAll('iframe'))
|
||||
this.pageFrames = pageIframes
|
||||
const signalId = async () => {
|
||||
if (event.source === null) {
|
||||
return console.error('Couldnt connect to event.source for child iframe tracking')
|
||||
}
|
||||
const id = await this.checkNodeId(pageIframes, event.source)
|
||||
if (id && !this.trackedFrames.includes(data.context)) {
|
||||
try {
|
||||
this.trackedFrames.push(data.context)
|
||||
await this.waitStarted()
|
||||
const token = this.session.getSessionToken()
|
||||
const order = this.trackedFrames.findIndex((f) => f === data.context) + 1
|
||||
if (order === 0) {
|
||||
this.debug.error(
|
||||
'Couldnt get order number for iframe',
|
||||
data.context,
|
||||
this.trackedFrames,
|
||||
)
|
||||
}
|
||||
const iframeData = {
|
||||
line: proto.iframeId,
|
||||
id,
|
||||
token,
|
||||
// since indexes go from 0 we +1
|
||||
frameOrderNumber: order,
|
||||
}
|
||||
this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData)
|
||||
// @ts-ignore
|
||||
event.source?.postMessage(iframeData, '*')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
} else {
|
||||
this.debug.log('Couldnt get node id for iframe', event.source, pageIframes)
|
||||
}
|
||||
}
|
||||
void signalId()
|
||||
}
|
||||
/**
|
||||
* proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
|
||||
* plus we rewrite some of the messages to be relative to the main context/window
|
||||
* */
|
||||
if (data.line === proto.iframeBatch) {
|
||||
const msgBatch = data.messages
|
||||
const mappedMessages: Message[] = msgBatch.map((msg: Message) => {
|
||||
if (msg[0] === MType.MouseMove) {
|
||||
let fixedMessage = msg
|
||||
this.pageFrames.forEach((frame) => {
|
||||
if (frame.contentWindow === event.source) {
|
||||
const [type, x, y] = msg
|
||||
const { left, top } = frame.getBoundingClientRect()
|
||||
fixedMessage = [type, x + left, y + top]
|
||||
}
|
||||
})
|
||||
return fixedMessage
|
||||
}
|
||||
if (msg[0] === MType.MouseClick) {
|
||||
let fixedMessage = msg
|
||||
this.pageFrames.forEach((frame) => {
|
||||
if (frame.contentWindow === event.source) {
|
||||
const [type, id, hesitationTime, label, selector, normX, normY] = msg
|
||||
const { left, top, width, height } = frame.getBoundingClientRect()
|
||||
|
||||
const contentWidth = document.documentElement.scrollWidth
|
||||
const contentHeight = document.documentElement.scrollHeight
|
||||
// (normalizedX * frameWidth + frameLeftOffset)/docSize
|
||||
const fullX = (normX / 100) * width + left
|
||||
const fullY = (normY / 100) * height + top
|
||||
const fixedX = fullX / contentWidth
|
||||
const fixedY = fullY / contentHeight
|
||||
|
||||
fixedMessage = [
|
||||
type,
|
||||
id,
|
||||
hesitationTime,
|
||||
label,
|
||||
selector,
|
||||
Math.round(fixedX * 1e3) / 1e1,
|
||||
Math.round(fixedY * 1e3) / 1e1,
|
||||
]
|
||||
}
|
||||
})
|
||||
return fixedMessage
|
||||
}
|
||||
return msg
|
||||
})
|
||||
this.messages.push(...mappedMessages)
|
||||
}
|
||||
if (data.line === proto.polling) {
|
||||
if (!this.pollingQueue.order.length) {
|
||||
return
|
||||
}
|
||||
const nextCommand = this.pollingQueue.order[0]
|
||||
if (this.pollingQueue[nextCommand].includes(data.context)) {
|
||||
this.pollingQueue[nextCommand] = this.pollingQueue[nextCommand].filter(
|
||||
(c: string) => c !== data.context,
|
||||
)
|
||||
// @ts-ignore
|
||||
event.source?.postMessage({ line: nextCommand }, '*')
|
||||
if (this.pollingQueue[nextCommand].length === 0) {
|
||||
this.pollingQueue.order.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* { command : [remaining iframes] }
|
||||
* + order of commands
|
||||
**/
|
||||
pollingQueue: Record<string, any> = {
|
||||
order: [],
|
||||
}
|
||||
private readonly addCommand = (cmd: string) => {
|
||||
this.pollingQueue.order.push(cmd)
|
||||
this.pollingQueue[cmd] = [...this.trackedFrames]
|
||||
}
|
||||
|
||||
public bootChildrenFrames = async () => {
|
||||
await this.waitStarted()
|
||||
this.addCommand(proto.startIframe)
|
||||
}
|
||||
|
||||
public killChildrenFrames = () => {
|
||||
this.addCommand(proto.killIframe)
|
||||
}
|
||||
|
||||
signalIframeTracker = () => {
|
||||
const thisTab = this.session.getTabId()
|
||||
const signalToParent = (n: number) => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
line: proto.iframeSignal,
|
||||
source: thisTab,
|
||||
context: this.contextId,
|
||||
},
|
||||
this.options.crossdomain?.parentDomain ?? '*',
|
||||
)
|
||||
setTimeout(() => {
|
||||
if (!this.checkStatus() && n < 100) {
|
||||
void signalToParent(n + 1)
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
void signalToParent(1)
|
||||
}
|
||||
|
||||
startTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
private allowAppStart() {
|
||||
public allowAppStart() {
|
||||
this.canStart = true
|
||||
if (this.startTimeout) {
|
||||
clearTimeout(this.startTimeout)
|
||||
|
|
@ -528,15 +633,38 @@ export default class App {
|
|||
}
|
||||
}
|
||||
|
||||
private checkNodeId(iframes: HTMLIFrameElement[], domain: string) {
|
||||
private async checkNodeId(
|
||||
iframes: HTMLIFrameElement[],
|
||||
source: MessageEventSource,
|
||||
): Promise<number | null> {
|
||||
for (const iframe of iframes) {
|
||||
if (iframe.dataset.domain === domain) {
|
||||
// @ts-ignore
|
||||
return iframe[this.options.node_id] as number | undefined
|
||||
if (iframe.contentWindow && iframe.contentWindow === source) {
|
||||
/**
|
||||
* Here we're trying to get node id from the iframe (which is kept in observer)
|
||||
* because of async nature of dom initialization, we give 100 retries with 100ms delay each
|
||||
* which equals to 10 seconds. This way we have a period where we give app some time to load
|
||||
* and tracker some time to parse the initial DOM tree even on slower devices
|
||||
* */
|
||||
let tries = 0
|
||||
while (tries < 100) {
|
||||
// @ts-ignore
|
||||
const potentialId = iframe[this.options.node_id]
|
||||
if (potentialId !== undefined) {
|
||||
tries = 100
|
||||
return potentialId
|
||||
} else {
|
||||
tries++
|
||||
await delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private initWorker() {
|
||||
try {
|
||||
this.worker = new Worker(
|
||||
|
|
@ -647,28 +775,28 @@ export default class App {
|
|||
this.messages.length = 0
|
||||
return
|
||||
}
|
||||
if (this.worker === undefined || !this.messages.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.insideIframe) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
line: proto.iframeBatch,
|
||||
messages: this.messages,
|
||||
domain: this.initialHostName,
|
||||
},
|
||||
'*',
|
||||
this.options.crossdomain?.parentDomain ?? '*',
|
||||
)
|
||||
this.commitCallbacks.forEach((cb) => cb(this.messages))
|
||||
this.messages.length = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (this.worker === undefined || !this.messages.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
requestIdleCb(() => {
|
||||
this.messages.unshift(TabData(this.session.getTabId()))
|
||||
this.messages.unshift(Timestamp(this.timestamp()))
|
||||
// why I need to add opt chaining?
|
||||
this.worker?.postMessage(this.messages)
|
||||
this.commitCallbacks.forEach((cb) => cb(this.messages))
|
||||
this.messages.length = 0
|
||||
|
|
@ -740,36 +868,39 @@ export default class App {
|
|||
this.commitCallbacks.push(cb)
|
||||
}
|
||||
|
||||
attachStartCallback(cb: StartCallback, useSafe = false): void {
|
||||
attachStartCallback = (cb: StartCallback, useSafe = false): void => {
|
||||
if (useSafe) {
|
||||
cb = this.safe(cb)
|
||||
}
|
||||
this.startCallbacks.push(cb)
|
||||
}
|
||||
|
||||
attachStopCallback(cb: () => any, useSafe = false): void {
|
||||
attachStopCallback = (cb: () => any, useSafe = false): void => {
|
||||
if (useSafe) {
|
||||
cb = this.safe(cb)
|
||||
}
|
||||
this.stopCallbacks.push(cb)
|
||||
}
|
||||
|
||||
// Use app.nodes.attachNodeListener for registered nodes instead
|
||||
attachEventListener(
|
||||
attachEventListener = (
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
useSafe = true,
|
||||
useCapture = true,
|
||||
): void {
|
||||
): void => {
|
||||
if (useSafe) {
|
||||
listener = this.safe(listener)
|
||||
}
|
||||
|
||||
const createListener = () =>
|
||||
target ? createEventListener(target, type, listener, useCapture) : null
|
||||
target
|
||||
? createEventListener(target, type, listener, useCapture, this.options.angularMode)
|
||||
: null
|
||||
const deleteListener = () =>
|
||||
target ? deleteEventListener(target, type, listener, useCapture) : null
|
||||
target
|
||||
? deleteEventListener(target, type, listener, useCapture, this.options.angularMode)
|
||||
: null
|
||||
|
||||
this.attachStartCallback(createListener, useSafe)
|
||||
this.attachStopCallback(deleteListener, useSafe)
|
||||
|
|
@ -1157,7 +1288,7 @@ export default class App {
|
|||
if (isColdStart && this.coldInterval) {
|
||||
clearInterval(this.coldInterval)
|
||||
}
|
||||
if (!this.worker) {
|
||||
if (!this.worker && !this.insideIframe) {
|
||||
const reason = 'No worker found: perhaps, CSP is not set.'
|
||||
this.signalError(reason, [])
|
||||
return Promise.resolve(UnsuccessfulStart(reason))
|
||||
|
|
@ -1189,7 +1320,7 @@ export default class App {
|
|||
})
|
||||
|
||||
const timestamp = now()
|
||||
this.worker.postMessage({
|
||||
this.worker?.postMessage({
|
||||
type: 'start',
|
||||
pageNo: this.session.incPageNo(),
|
||||
ingestPoint: this.options.ingestPoint,
|
||||
|
|
@ -1237,7 +1368,7 @@ export default class App {
|
|||
const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`
|
||||
return UnsuccessfulStart(reason)
|
||||
}
|
||||
if (!this.worker) {
|
||||
if (!this.worker && !this.insideIframe) {
|
||||
const reason = 'no worker found after start request (this should not happen in real world)'
|
||||
this.signalError(reason, [])
|
||||
return UnsuccessfulStart(reason)
|
||||
|
|
@ -1295,9 +1426,9 @@ export default class App {
|
|||
|
||||
if (socketOnly) {
|
||||
this.socketMode = true
|
||||
this.worker.postMessage('stop')
|
||||
this.worker?.postMessage('stop')
|
||||
} else {
|
||||
this.worker.postMessage({
|
||||
this.worker?.postMessage({
|
||||
type: 'auth',
|
||||
token,
|
||||
beaconSizeLimit,
|
||||
|
|
@ -1320,11 +1451,17 @@ export default class App {
|
|||
// TODO: start as early as possible (before receiving the token)
|
||||
/** after start */
|
||||
this.startCallbacks.forEach((cb) => cb(onStartInfo)) // MBTODO: callbacks after DOM "mounted" (observed)
|
||||
if (startOpts.startCallback) {
|
||||
startOpts.startCallback(SuccessfulStart(onStartInfo))
|
||||
}
|
||||
if (this.features['feature-flags']) {
|
||||
void this.featureFlags.reloadFlags()
|
||||
}
|
||||
await this.tagWatcher.fetchTags(this.options.ingestPoint, token)
|
||||
this.activityState = ActivityState.Active
|
||||
if (this.options.crossdomain?.enabled && !this.insideIframe) {
|
||||
void this.bootChildrenFrames()
|
||||
}
|
||||
|
||||
if (canvasEnabled && !this.options.canvas.disableCanvas) {
|
||||
this.canvasRecorder =
|
||||
|
|
@ -1336,7 +1473,6 @@ export default class App {
|
|||
fixedScaling: this.options.canvas.fixedCanvasScaling,
|
||||
useAnimationFrame: this.options.canvas.useAnimationFrame,
|
||||
})
|
||||
this.canvasRecorder.startTracking()
|
||||
}
|
||||
|
||||
/** --------------- COLD START BUFFER ------------------*/
|
||||
|
|
@ -1359,9 +1495,12 @@ export default class App {
|
|||
}
|
||||
this.ticker.start()
|
||||
}
|
||||
this.canvasRecorder?.startTracking()
|
||||
|
||||
if (this.features['usability-test']) {
|
||||
this.uxtManager = this.uxtManager ? this.uxtManager : new UserTestManager(this, uxtStorageKey)
|
||||
this.uxtManager = this.uxtManager
|
||||
? this.uxtManager
|
||||
: new UserTestManager(this, uxtStorageKey)
|
||||
let uxtId: number | undefined
|
||||
const savedUxtTag = this.localStorage.getItem(uxtStorageKey)
|
||||
if (savedUxtTag) {
|
||||
|
|
@ -1394,6 +1533,11 @@ export default class App {
|
|||
} catch (reason) {
|
||||
this.stop()
|
||||
this.session.reset()
|
||||
if (!reason) {
|
||||
console.error('Unknown error during start')
|
||||
this.signalError('Unknown error', [])
|
||||
return UnsuccessfulStart('Unknown error')
|
||||
}
|
||||
if (reason === CANCELED) {
|
||||
this.signalError(CANCELED, [])
|
||||
return UnsuccessfulStart(CANCELED)
|
||||
|
|
@ -1452,9 +1596,13 @@ export default class App {
|
|||
}
|
||||
|
||||
async waitStarted() {
|
||||
return this.waitStatus(ActivityState.Active)
|
||||
}
|
||||
|
||||
async waitStatus(status: ActivityState) {
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
if (this.activityState === ActivityState.Active) {
|
||||
if (this.activityState === status) {
|
||||
resolve(true)
|
||||
} else {
|
||||
setTimeout(check, 25)
|
||||
|
|
@ -1478,6 +1626,10 @@ export default class App {
|
|||
return Promise.resolve(UnsuccessfulStart(reason))
|
||||
}
|
||||
|
||||
if (this.insideIframe) {
|
||||
this.signalIframeTracker()
|
||||
}
|
||||
|
||||
if (!document.hidden) {
|
||||
await this.waitStart()
|
||||
return this._start(...args)
|
||||
|
|
@ -1533,20 +1685,28 @@ export default class App {
|
|||
stop(stopWorker = true): void {
|
||||
if (this.activityState !== ActivityState.NotActive) {
|
||||
try {
|
||||
if (!this.insideIframe && this.options.crossdomain?.enabled) {
|
||||
this.killChildrenFrames()
|
||||
}
|
||||
this.attributeSender.clear()
|
||||
this.sanitizer.clear()
|
||||
this.observer.disconnect()
|
||||
this.nodes.clear()
|
||||
this.ticker.stop()
|
||||
this.stopCallbacks.forEach((cb) => cb())
|
||||
this.debug.log('OpenReplay tracking stopped.')
|
||||
this.tagWatcher.clear()
|
||||
if (this.worker && stopWorker) {
|
||||
this.worker.postMessage('stop')
|
||||
}
|
||||
this.canvasRecorder?.clear()
|
||||
this.messages.length = 0
|
||||
this.trackedFrames = []
|
||||
this.parentActive = false
|
||||
this.canStart = false
|
||||
this.pollingQueue = { order: [] }
|
||||
} finally {
|
||||
this.activityState = ActivityState.NotActive
|
||||
this.debug.log('OpenReplay tracking stopped.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ export default class Nodes {
|
|||
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
|
||||
private nextNodeId = 0
|
||||
|
||||
constructor(private readonly node_id: string) {}
|
||||
constructor(
|
||||
private readonly node_id: string,
|
||||
private readonly angularMode: boolean,
|
||||
) {}
|
||||
|
||||
syntheticMode(frameOrder: number) {
|
||||
const maxSafeNumber = 9007199254740900
|
||||
const maxSafeNumber = Number.MAX_SAFE_INTEGER
|
||||
const placeholderSize = 99999999
|
||||
const nextFrameId = placeholderSize * frameOrder
|
||||
// I highly doubt that this will ever happen,
|
||||
|
|
@ -25,7 +28,7 @@ export default class Nodes {
|
|||
}
|
||||
|
||||
// Attached once per Tracker instance
|
||||
attachNodeCallback(nodeCallback: NodeCallback): void {
|
||||
attachNodeCallback = (nodeCallback: NodeCallback): void => {
|
||||
this.nodeCallbacks.push(nodeCallback)
|
||||
}
|
||||
|
||||
|
|
@ -33,12 +36,12 @@ export default class Nodes {
|
|||
this.nodes.forEach((node) => cb(node))
|
||||
}
|
||||
|
||||
attachNodeListener(node: Node, type: string, listener: EventListener, useCapture = true): void {
|
||||
attachNodeListener = (node: Node, type: string, listener: EventListener, useCapture = true): void => {
|
||||
const id = this.getID(node)
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
createEventListener(node, type, listener, useCapture)
|
||||
createEventListener(node, type, listener, useCapture, this.angularMode)
|
||||
let listeners = this.elementListeners.get(id)
|
||||
if (listeners === undefined) {
|
||||
listeners = []
|
||||
|
|
@ -70,7 +73,7 @@ export default class Nodes {
|
|||
if (listeners !== undefined) {
|
||||
this.elementListeners.delete(id)
|
||||
listeners.forEach((listener) =>
|
||||
deleteEventListener(node, listener[0], listener[1], listener[2]),
|
||||
deleteEventListener(node, listener[0], listener[1], listener[2], this.angularMode),
|
||||
)
|
||||
}
|
||||
this.totalNodeAmount--
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ export default class IFrameObserver extends Observer {
|
|||
})
|
||||
}
|
||||
|
||||
syntheticObserve(selfId: number, doc: Document) {
|
||||
syntheticObserve(rootNodeId: number, doc: Document) {
|
||||
this.observeRoot(doc, (docID) => {
|
||||
if (docID === undefined) {
|
||||
this.app.debug.log('OpenReplay: Iframe document not bound')
|
||||
return
|
||||
}
|
||||
this.app.send(CreateIFrameDocument(selfId, docID))
|
||||
this.app.send(CreateIFrameDocument(rootNodeId, docID))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createMutationObserver, ngSafeBrowserMethod } from '../../utils.js'
|
||||
import { createMutationObserver } from '../../utils.js'
|
||||
import {
|
||||
RemoveNodeAttribute,
|
||||
SetNodeAttributeURLBased,
|
||||
|
|
@ -105,6 +105,9 @@ export default abstract class Observer {
|
|||
if (name === null) {
|
||||
continue
|
||||
}
|
||||
if (target instanceof HTMLIFrameElement && name === 'src') {
|
||||
this.handleIframeSrcChange(target)
|
||||
}
|
||||
let attr = this.attributesMap.get(id)
|
||||
if (attr === undefined) {
|
||||
this.attributesMap.set(id, (attr = new Set()))
|
||||
|
|
@ -119,6 +122,7 @@ export default abstract class Observer {
|
|||
}
|
||||
this.commitNodes()
|
||||
}) as MutationCallback,
|
||||
this.app.options.angularMode,
|
||||
)
|
||||
}
|
||||
private clear(): void {
|
||||
|
|
@ -129,10 +133,49 @@ export default abstract class Observer {
|
|||
this.textSet.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds the removed nodes in case of iframe src change.
|
||||
*/
|
||||
private handleIframeSrcChange(iframe: HTMLIFrameElement): void {
|
||||
const oldContentDocument = iframe.contentDocument
|
||||
if (oldContentDocument) {
|
||||
const id = this.app.nodes.getID(oldContentDocument)
|
||||
if (id !== undefined) {
|
||||
const walker = document.createTreeWalker(
|
||||
oldContentDocument,
|
||||
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node) =>
|
||||
isIgnored(node) || this.app.nodes.getID(node) === undefined
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT,
|
||||
},
|
||||
// @ts-ignore
|
||||
false,
|
||||
)
|
||||
|
||||
let removed = 0
|
||||
const totalBeforeRemove = this.app.nodes.getNodeCount()
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (!iframe.contentDocument.contains(walker.currentNode)) {
|
||||
removed += 1
|
||||
this.app.nodes.unregisterNode(walker.currentNode)
|
||||
}
|
||||
}
|
||||
|
||||
const removedPercent = Math.floor((removed / totalBeforeRemove) * 100)
|
||||
if (removedPercent > 30) {
|
||||
this.app.send(UnbindNodes(removedPercent))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendNodeAttribute(id: number, node: Element, name: string, value: string | null): void {
|
||||
if (isSVGElement(node)) {
|
||||
if (name.substr(0, 6) === 'xlink:') {
|
||||
name = name.substr(6)
|
||||
if (name.substring(0, 6) === 'xlink:') {
|
||||
name = name.substring(6)
|
||||
}
|
||||
if (value === null) {
|
||||
this.app.send(RemoveNodeAttribute(id, name))
|
||||
|
|
@ -152,7 +195,7 @@ export default abstract class Observer {
|
|||
name === 'integrity' ||
|
||||
name === 'crossorigin' ||
|
||||
name === 'autocomplete' ||
|
||||
name.substr(0, 2) === 'on'
|
||||
name.substring(0, 2) === 'on'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export default class TopObserver extends Observer {
|
|||
)
|
||||
}
|
||||
|
||||
crossdomainObserve(selfId: number, frameOder: number) {
|
||||
crossdomainObserve(rootNodeId: number, frameOder: number) {
|
||||
const observer = this
|
||||
Element.prototype.attachShadow = function () {
|
||||
// eslint-disable-next-line
|
||||
|
|
@ -152,7 +152,7 @@ export default class TopObserver extends Observer {
|
|||
this.app.nodes.syntheticMode(frameOder)
|
||||
const iframeObserver = new IFrameObserver(this.app)
|
||||
this.iframeObservers.push(iframeObserver)
|
||||
iframeObserver.syntheticObserve(selfId, window.document)
|
||||
iframeObserver.syntheticObserve(rootNodeId, window.document)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export default function (app: App): void {
|
|||
}
|
||||
}
|
||||
}) as MutationCallback,
|
||||
app.options.angularMode,
|
||||
)
|
||||
|
||||
app.attachStopCallback(() => {
|
||||
|
|
|
|||
|
|
@ -132,9 +132,13 @@ export function ngSafeBrowserMethod(method: string): string {
|
|||
: method
|
||||
}
|
||||
|
||||
export function createMutationObserver(cb: MutationCallback) {
|
||||
const mObserver = ngSafeBrowserMethod('MutationObserver') as 'MutationObserver'
|
||||
return new window[mObserver](cb)
|
||||
export function createMutationObserver(cb: MutationCallback, angularMode?: boolean) {
|
||||
if (angularMode) {
|
||||
const mObserver = ngSafeBrowserMethod('MutationObserver') as 'MutationObserver'
|
||||
return new window[mObserver](cb)
|
||||
} else {
|
||||
return new MutationObserver(cb)
|
||||
}
|
||||
}
|
||||
|
||||
export function createEventListener(
|
||||
|
|
@ -142,15 +146,23 @@ export function createEventListener(
|
|||
event: string,
|
||||
cb: EventListenerOrEventListenerObject,
|
||||
capture?: boolean,
|
||||
angularMode?: boolean,
|
||||
) {
|
||||
const safeAddEventListener = ngSafeBrowserMethod('addEventListener') as 'addEventListener'
|
||||
let safeAddEventListener: 'addEventListener'
|
||||
if (angularMode) {
|
||||
safeAddEventListener = ngSafeBrowserMethod('addEventListener') as 'addEventListener'
|
||||
} else {
|
||||
safeAddEventListener = 'addEventListener'
|
||||
}
|
||||
try {
|
||||
target[safeAddEventListener](event, cb, capture)
|
||||
} catch (e) {
|
||||
const msg = e.message
|
||||
console.debug(
|
||||
console.error(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`,
|
||||
event,
|
||||
target,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,17 +172,23 @@ export function deleteEventListener(
|
|||
event: string,
|
||||
cb: EventListenerOrEventListenerObject,
|
||||
capture?: boolean,
|
||||
angularMode?: boolean,
|
||||
) {
|
||||
const safeRemoveEventListener = ngSafeBrowserMethod(
|
||||
'removeEventListener',
|
||||
) as 'removeEventListener'
|
||||
let safeRemoveEventListener: 'removeEventListener'
|
||||
if (angularMode) {
|
||||
safeRemoveEventListener = ngSafeBrowserMethod('removeEventListener') as 'removeEventListener'
|
||||
} else {
|
||||
safeRemoveEventListener = 'removeEventListener'
|
||||
}
|
||||
try {
|
||||
target[safeRemoveEventListener](event, cb, capture)
|
||||
} catch (e) {
|
||||
const msg = e.message
|
||||
console.debug(
|
||||
console.error(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`,
|
||||
event,
|
||||
target,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('Nodes', () => {
|
|||
const mockCallback = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
nodes = new Nodes(nodeId)
|
||||
nodes = new Nodes(nodeId, false)
|
||||
mockCallback.mockClear()
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue