Compare commits

...
Sign in to create a new pull request.

30 commits

Author SHA1 Message Date
snyk-bot
46e7b72d9c
fix: api/Dockerfile to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE320-EXPAT-7908298
- https://snyk.io/vuln/SNYK-ALPINE320-EXPAT-7908299
- https://snyk.io/vuln/SNYK-ALPINE320-EXPAT-7908300
2024-09-07 07:26:08 +00:00
Mehdi Osman
5700a16a94
Increment chalice chart version (#2495)
Co-authored-by: GitHub Action <action@github.com>
2024-08-16 17:01:29 +02:00
Kraiem Taha Yassine
62a9f66364
feat(chalice): Update user on SSO (#2494) 2024-08-16 16:42:01 +02:00
Mehdi Osman
67063363a8
Inject mobile bucket (#2432) 2024-07-26 00:31:40 +02:00
Mehdi Osman
2b0b5d6106
Updated patch build from main 64a746873b (#2429)
* Increment chalice chart version

* Increment frontend chart version

* Increment db chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2024-07-25 14:27:08 +02:00
Kraiem Taha Yassine
64a746873b
Patch/api v1.19.0 (#2428)
* fix(chalice): changed mobile autocomplete

* fix(chalice): fixed mobile errors search
fix(chalice): fixed mobile errors autocomplete
fix(chalice): fixed mobile swipe autocomplete
2024-07-25 11:51:31 +02:00
Shekar Siri
139f0e68c4
Mobile UI updates (#2427)
* change(ui): mobile resolution

* change(ui): removed empty tag that showign as green icon

* change(ui): mobile specific filters

* change(ui): remove browser card for mobile

* change(ui): summary ai toggle
2024-07-25 11:20:09 +02:00
Mehdi Osman
69e8fab2cc
Added mobile and canvas related topics (#2424) 2024-07-25 10:29:48 +02:00
Kraiem Taha Yassine
9db416dcde
fix(chalice): changed mobile autocomplete (#2426) 2024-07-25 10:26:37 +02:00
Alexander
f28a7fbcfb
feat(backend): changed mobile autocomplete naming (#2425) 2024-07-25 10:23:27 +02:00
Atef Ben Ali
6e2a772e7f
docs: update README_AR.md file (#2421) 2024-07-24 09:44:21 -04:00
Mehdi Osman
dcce3569fb
Increment chalice chart version (#2417)
Co-authored-by: GitHub Action <action@github.com>
2024-07-23 13:17:48 +02:00
Kraiem Taha Yassine
867247dbc0
fix(chalice): fixed insights with filter steps (#2416) 2024-07-23 13:01:48 +02:00
PiR
754293e29d
Tracker GrahpQL: update doc and tracker initialization + add option to pass sanitizer function (#2402)
* fix(graphQL): update doc and tracker initialization + add option to pass sanitizer function

* improvement(graphQL): improve sanitizer type & apollo operation name
2024-07-22 16:07:12 +02:00
Mehdi Osman
ddd037ce79
Increment chalice chart version (#2411)
Co-authored-by: GitHub Action <action@github.com>
2024-07-19 15:09:47 +02:00
Kraiem Taha Yassine
66f4c5c93b
fix(chalice): stop SA from logout (#2410) 2024-07-19 15:03:59 +02:00
Mehdi Osman
66e4d133ad
Increment chalice chart version (#2407)
Co-authored-by: GitHub Action <action@github.com>
2024-07-18 13:17:27 +02:00
Kraiem Taha Yassine
f9f8853ab0
fix(chalice): fixed search mobile sessions (#2406)
fix(chalice): fixed autocomplete mobile sessions
2024-07-18 13:08:05 +02:00
Mehdi Osman
e0bb6fea9d
Updated patch build from main 4e7efaecde (#2405)
* Increment frontend chart version

* Increment db chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2024-07-18 12:46:42 +02:00
Alexander
4e7efaecde
Heatmaps fix (float coordinates) (#2403) (#2404)
* feat(spot): use float click coordinates instead of ints in PG

* feat(db): added support for float clicks in CH

* feat(db): fix float instead of uint8

* feat(mobile): new naming for mobile autocomplete types
2024-07-18 12:38:47 +02:00
Delirium
54a9624332
Heatmaps patch 2 (#2400)
* fix ui: move clickmap overlay inside replay vdom, refactor renderer scaling

* fix ui: fix first event calculation
2024-07-17 18:57:21 +02:00
Mehdi Osman
1ddffca572
Increment frontend chart version (#2395)
Co-authored-by: GitHub Action <action@github.com>
2024-07-16 17:38:25 +02:00
Delirium
c91881413a
fix manager event reads for mobile (#2394) 2024-07-16 17:34:12 +02:00
Mehdi Osman
ba2d9eb81c
Increment chalice chart version (#2393)
Co-authored-by: GitHub Action <action@github.com>
2024-07-16 17:20:59 +02:00
Kraiem Taha Yassine
c845415e1e
Patch/api v1.19.0 (#2392)
* fix(chalice): reversed count&total for card-tables to confuse devs

* fix(DB): changed normalized_x&y col-type
2024-07-16 17:15:38 +02:00
Mehdi Osman
ee0ede8478
Increment chalice chart version (#2391)
Co-authored-by: GitHub Action <action@github.com>
2024-07-16 14:26:47 +02:00
Kraiem Taha Yassine
72afae226b
fix(chalice): fixed missing totalSessions in card-tables in EE (#2390)
* fix(chalice): fixed missing totalSessions in card-tables in EE

* fix(chalice): fixed missing totalSessions in card-tables in EE
2024-07-16 14:16:26 +02:00
Shekar Siri
b3f545849a
fix(ui): use count instead of totalSessions (#2387) 2024-07-12 17:38:44 +02:00
Mehdi Osman
cd2966fb9f
Increment chalice chart version (#2384)
Co-authored-by: GitHub Action <action@github.com>
2024-07-11 11:50:42 +02:00
Kraiem Taha Yassine
4b91dcded0
Patch/api v1.19.0 (#2383)
* fix(chalice): fixed create heatmap card EE

* fix(chalice): fixed click_rage-heatmap card EE

* fix(chalice): fixed click_rage-heatmap ambiguous alias EE
2024-07-11 11:36:07 +02:00
70 changed files with 840 additions and 408 deletions

View file

@ -55,17 +55,17 @@ OpenReplay هو مجموعة إعادة تشغيل الجلسة التي يمك
## الميزات
- **إعادة تشغيل الجلسة:** تتيح لك إعادة تشغيل الجلسة إعادة عيش تجربة مستخدميك، ورؤية أين يواجهون صعوبة وكيف يؤثر ذلك على سلوكهم. يتم تحليل كل إعادة تشغيل للجلسة تلقائيًا بناءً على الأساليب الاستدلالية، لسهولة التقييم.
- **أدوات التطوير (DevTools):** إنها مثل التصحيح في متصفحك الخاص. يوفر لك OpenReplay السياق الكامل (نشاط الشبكة، أخطاء JavaScript، إجراءات/حالة التخزين وأكثر من 40 مقياسًا) حتى تتمكن من إعادة إنتاج الأخطاء فورًا وفهم مشكلات الأداء.
- **أدوات التطوير (DevTools):** إنها مثل المصحح (debugger) في متصفحك الخاص. يوفر لك OpenReplay السياق الكامل (نشاط الشبكة، أخطاء JavaScript، إجراءات/حالة التخزين وأكثر من 40 مقياسًا) حتى تتمكن من إعادة إنتاج الأخطاء فورًا وفهم مشكلات الأداء.
- **المساعدة (Assist):** تساعدك في دعم مستخدميك من خلال رؤية شاشتهم مباشرة والانضمام فورًا إلى مكالمة (WebRTC) معهم دون الحاجة إلى برامج مشاركة الشاشة من جهات خارجية.
- **البحث الشامل (Omni-search):** ابحث وفرز حسب أي عملية/معيار للمستخدم تقريبًا، أو سمة الجلسة أو الحدث التقني، حتى تتمكن من الرد على أي سؤال. لا يلزم تجهيز.
- **البحث الشامل (Omni-search):** ابحث وافرز حسب أي عملية/معيار للمستخدم تقريبًا، أو سمة الجلسة أو الحدث التقني، حتى تتمكن من الرد على أي سؤال. لا يلزم تجهيز.
- **الأنفاق (Funnels):** للكشف عن المشكلات الأكثر تأثيرًا التي تسبب في فقدان التحويل والإيرادات.
- **ضوابط الخصوصية الدقيقة:** اختر ماذا تريد التقاطه، ماذا تريد أن تخفي أو تجاهل حتى لا تصل بيانات المستخدم حتى إلى خوادمك.
- **موجهة للمكونات الإضافية (Plugins oriented):** تصل إلى السبب الجذري بشكل أسرع عن طريق تتبع حالة التطبيق (Redux، VueX، MobX، NgRx، Pinia، وZustand) وتسجيل استعلامات GraphQL (Apollo، Relay) وطلبات Fetch/Axios.
- **ضوابط الخصوصية الدقيقة:** اختر ماذا تريد التقاطه، ماذا تريد أن تخفي أو تتجاهل حتى لا تصل بيانات المستخدم حتى إلى خوادمك.
- **موجهة للمكونات الإضافية (Plugins oriented):** يمكنك الوصول إلى السبب الجذري بشكل أسرع عن طريق تتبع حالة التطبيق (Redux، VueX، MobX، NgRx، Pinia، وZustand) وتسجيل استعلامات GraphQL (Apollo، Relay) وطلبات Fetch/Axios.
- **التكاملات (Integrations):** مزامنة سجلات الخادم الخلفي مع إعادات التشغيل للجلسات ورؤية ما حدث من الأمام إلى الخلف. يدعم OpenReplay Sentry وDatadog وCloudWatch وStackdriver وElastic والمزيد.
## خيارات النشر
يمكن نشر OpenReplay في أي مكان. اتبع دليلنا الخطوة بالخطوة لنشره على خدمات السحابة العامة الرئيسية:
يمكن نشر OpenReplay في أي مكان. اتبع دليلنا خطوة بخطوة لنشره على خدمات السحابة العامة الرئيسة:
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)

View file

@ -1,4 +1,4 @@
FROM python:3.11-alpine
FROM python:3.12.0a1-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
ARG GIT_SHA

View file

@ -231,7 +231,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
query = f"""(SELECT DISTINCT ON(lg.reason)
lg.reason AS value,
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_id) LEFT JOIN public.sessions AS s USING(session_id)
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
WHERE
s.project_id = %(project_id)s
AND lg.project_id = %(project_id)s
@ -241,7 +241,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
(SELECT DISTINCT ON(lg.name)
lg.name AS value,
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_id) LEFT JOIN public.sessions AS s USING(session_id)
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
WHERE
s.project_id = %(project_id)s
AND lg.project_id = %(project_id)s
@ -251,7 +251,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
(SELECT DISTINCT ON(lg.reason)
lg.reason AS value,
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_id) LEFT JOIN public.sessions AS s USING(session_id)
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
WHERE
s.project_id = %(project_id)s
AND lg.project_id = %(project_id)s
@ -261,7 +261,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
(SELECT DISTINCT ON(lg.name)
lg.name AS value,
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_id) LEFT JOIN public.sessions AS s USING(session_id)
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
WHERE
s.project_id = %(project_id)s
AND lg.project_id = %(project_id)s
@ -271,7 +271,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
query = f"""(SELECT DISTINCT ON(lg.reason)
lg.reason AS value,
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_id) LEFT JOIN public.sessions AS s USING(session_id)
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
WHERE
s.project_id = %(project_id)s
AND lg.project_id = %(project_id)s
@ -281,7 +281,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
(SELECT DISTINCT ON(lg.name)
lg.name AS value,
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_id) LEFT JOIN public.sessions AS s USING(session_id)
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
WHERE
s.project_id = %(project_id)s
AND lg.project_id = %(project_id)s

View file

@ -319,13 +319,14 @@ def create_card(project_id, user_id, data: schemas.CardSchema, dashboard=False):
session_data = None
if data.metric_type == schemas.MetricType.heat_map:
if data.session_id is not None:
session_data = json.dumps({"sessionId": data.session_id})
session_data = {"sessionId": data.session_id}
else:
session_data = __get_heat_map_chart(project_id=project_id, user_id=user_id,
data=data, include_mobs=False)
if session_data is not None:
session_data = json.dumps({"sessionId": session_data["sessionId"]})
_data = {"session_data": session_data}
session_data = {"sessionId": session_data["sessionId"]}
_data = {"session_data": json.dumps(session_data) if session_data is not None else None}
for i, s in enumerate(data.series):
for k in s.model_dump().keys():
_data[f"{k}_{i}"] = s.__getattribute__(k)

View file

@ -134,7 +134,7 @@ class EventType:
CUSTOM_MOBILE = Event(ui_type=schemas.EventType.custom_mobile, table="events_common.customs", column="name")
REQUEST_MOBILE = Event(ui_type=schemas.EventType.request_mobile, table="events_common.requests", column="path")
CRASH_MOBILE = Event(ui_type=schemas.EventType.error_mobile, table="events_common.crashes",
column=None) # column=None because errors are searched by name or message
column=None) # column=None because errors are searched by name or message
SUPPORTED_TYPES = {
@ -163,22 +163,25 @@ SUPPORTED_TYPES = {
query=None),
# MOBILE
EventType.CLICK_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CLICK_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.CLICK_MOBILE.ui_type)),
query=autocomplete.__generic_query(
typename=EventType.CLICK_MOBILE.ui_type)),
EventType.SWIPE_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.SWIPE_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.SWIPE_MOBILE.ui_type)),
EventType.INPUT_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.INPUT_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.INPUT_MOBILE.ui_type)),
query=autocomplete.__generic_query(
typename=EventType.INPUT_MOBILE.ui_type)),
EventType.VIEW_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.VIEW_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.VIEW_MOBILE.ui_type)),
EventType.CUSTOM_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CUSTOM_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.CUSTOM_MOBILE.ui_type)),
EventType.REQUEST_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.REQUEST_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.REQUEST_MOBILE.ui_type)),
typename=EventType.VIEW_MOBILE.ui_type)),
EventType.CUSTOM_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CUSTOM_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.CUSTOM_MOBILE.ui_type)),
EventType.REQUEST_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.REQUEST_MOBILE),
query=autocomplete.__generic_query(
typename=EventType.REQUEST_MOBILE.ui_type)),
EventType.CRASH_MOBILE.ui_type: SupportedFilter(get=autocomplete.__search_errors_mobile,
query=None),
query=None),
}

View file

@ -359,12 +359,12 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
distinct_on += ",path"
if metric_format == schemas.MetricExtendedFormatType.session_count:
main_query = f"""SELECT COUNT(*) AS count,
COALESCE(SUM(users_sessions.session_count),0) AS total_sessions,
COALESCE(SUM(users_sessions.session_count),0) AS count,
COALESCE(JSONB_AGG(users_sessions)
FILTER ( WHERE rn > %(limit_s)s
AND rn <= %(limit_e)s ), '[]'::JSONB) AS values
FROM (SELECT {main_col} AS name,
count(DISTINCT session_id) AS session_count,
count(DISTINCT session_id) AS total,
ROW_NUMBER() OVER (ORDER BY count(full_sessions) DESC) AS rn
FROM (SELECT *
FROM (SELECT DISTINCT ON({distinct_on}) s.session_id, s.user_uuid,
@ -379,7 +379,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
ORDER BY session_count DESC) AS users_sessions;"""
else:
main_query = f"""SELECT COUNT(*) AS count,
COALESCE(SUM(users_sessions.user_count),0) AS total_users,
COALESCE(SUM(users_sessions.user_count),0) AS count,
COALESCE(JSONB_AGG(users_sessions) FILTER ( WHERE rn <= 200 ), '[]'::JSONB) AS values
FROM (SELECT {main_col} AS name,
count(DISTINCT user_id) AS user_count,
@ -420,12 +420,12 @@ def search_table_of_individual_issues(data: schemas.SessionsSearchPayloadSchema,
full_args["issues_limit_s"] = (data.page - 1) * data.limit
full_args["issues_limit_e"] = data.page * data.limit
main_query = cur.mogrify(f"""SELECT COUNT(1) AS count,
COALESCE(SUM(session_count), 0) AS total_sessions,
COALESCE(SUM(session_count), 0) AS count,
COALESCE(JSONB_AGG(ranked_issues)
FILTER ( WHERE rn > %(issues_limit_s)s
AND rn <= %(issues_limit_e)s ), '[]'::JSONB) AS values
FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY session_count DESC) AS rn
FROM (SELECT type AS name, context_string AS value, COUNT(DISTINCT session_id) AS session_count
FROM (SELECT type AS name, context_string AS value, COUNT(DISTINCT session_id) AS total
FROM (SELECT session_id
{query_part}) AS filtered_sessions
INNER JOIN events_common.issues USING (session_id)
@ -814,12 +814,6 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
event_where.append(
sh.multi_conditions(f"main.{events.EventType.VIEW_MOBILE.column} {op} %({e_k})s",
event.value, value_key=e_k))
elif event_type == events.EventType.SWIPE_MOBILE.ui_type and platform == "ios":
event_from = event_from % f"{events.EventType.SWIPE_MOBILE.table} AS main "
if not is_any:
event_where.append(
sh.multi_conditions(f"main.{events.EventType.SWIPE_MOBILE.column} {op} %({e_k})s",
event.value, value_key=e_k))
elif event_type == events.EventType.CUSTOM.ui_type:
event_from = event_from % f"{events.EventType.CUSTOM.table} AS main "
if not is_any:
@ -855,7 +849,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
event_where.append(sh.multi_conditions(f"main1.source = %({s_k})s", event.source, value_key=s_k))
# ----- IOS
# ----- Mobile
elif event_type == events.EventType.CLICK_MOBILE.ui_type:
event_from = event_from % f"{events.EventType.CLICK_MOBILE.table} AS main "
if not is_any:
@ -892,11 +886,18 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
sh.multi_conditions(f"main.{events.EventType.REQUEST_MOBILE.column} {op} %({e_k})s",
event.value, value_key=e_k))
elif event_type == events.EventType.CRASH_MOBILE.ui_type:
event_from = event_from % f"{events.EventType.CRASH_MOBILE.table} AS main INNER JOIN public.crashes_ios AS main1 USING(crash_id)"
event_from = event_from % f"{events.EventType.CRASH_MOBILE.table} AS main INNER JOIN public.crashes_ios AS main1 USING(crash_ios_id)"
if not is_any and event.value not in [None, "*", ""]:
event_where.append(
sh.multi_conditions(f"(main1.reason {op} %({e_k})s OR main1.name {op} %({e_k})s)",
event.value, value_key=e_k))
elif event_type == events.EventType.SWIPE_MOBILE.ui_type and platform != "web":
event_from = event_from % f"{events.EventType.SWIPE_MOBILE.table} AS main "
if not is_any:
event_where.append(
sh.multi_conditions(f"main.{events.EventType.SWIPE_MOBILE.column} {op} %({e_k})s",
event.value, value_key=e_k))
elif event_type == schemas.PerformanceEventType.fetch_failed:
event_from = event_from % f"{events.EventType.REQUEST.table} AS main "
if not is_any:

View file

@ -19,6 +19,7 @@ from routers.base import get_routers
public_app, app, app_apikey = get_routers()
@app.get('/{projectId}/autocomplete', tags=["events"])
@app.get('/{projectId}/events/search', tags=["events"])
def events_search(projectId: int, q: str,
type: Union[schemas.FilterType, schemas.EventType,

View file

@ -48,12 +48,12 @@ def transform_old_filter_type(cls, values):
"GRAPHQL": EventType.graphql.value,
"STATEACTION": EventType.state_action.value,
"ERROR": EventType.error.value,
"CLICK_MOBILE": EventType.click_mobile.value,
"INPUT_MOBILE": EventType.input_mobile.value,
"VIEW_MOBILE": EventType.view_mobile.value,
"CUSTOM_MOBILE": EventType.custom_mobile.value,
"REQUEST_MOBILE": EventType.request_mobile.value,
"ERROR_MOBILE": EventType.error_mobile.value,
"CLICK_IOS": EventType.click_mobile.value,
"INPUT_IOS": EventType.input_mobile.value,
"VIEW_IOS": EventType.view_mobile.value,
"CUSTOM_IOS": EventType.custom_mobile.value,
"REQUEST_IOS": EventType.request_mobile.value,
"ERROR_IOS": EventType.error_mobile.value,
"DOM_COMPLETE": PerformanceEventType.location_dom_complete.value,
"LARGEST_CONTENTFUL_PAINT_TIME": PerformanceEventType.location_largest_contentful_paint_time.value,
"TTFB": PerformanceEventType.location_ttfb.value,
@ -471,13 +471,13 @@ class EventType(str, Enum):
state_action = "stateAction"
error = "error"
tag = "tag"
click_mobile = "tapIos"
input_mobile = "inputIos"
view_mobile = "viewIos"
custom_mobile = "customIos"
request_mobile = "requestIos"
error_mobile = "errorIos"
swipe_mobile = "swipeIos"
click_mobile = "clickMobile"
input_mobile = "inputMobile"
view_mobile = "viewMobile"
custom_mobile = "customMobile"
request_mobile = "requestMobile"
error_mobile = "errorMobile"
swipe_mobile = "swipeMobile"
class PerformanceEventType(str, Enum):
@ -1459,7 +1459,7 @@ class LiveSessionSearchFilterSchema(BaseModel):
operator: Literal[SearchEventOperator._is, \
SearchEventOperator._contains] = Field(default=SearchEventOperator._contains)
transform = model_validator(mode='before')(transform_old_filter_type)
_transform = model_validator(mode='before')(transform_old_filter_type)
@model_validator(mode='after')
def __validator(cls, values):

View file

@ -81,13 +81,13 @@ func (s *saverImpl) handleMobileMessage(msg Message) error {
if err = s.sessions.UpdateUserID(session.SessionID, m.ID); err != nil {
return err
}
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERID_Mobile", m.ID)
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERIDMOBILE", m.ID)
return nil
case *MobileUserAnonymousID:
if err = s.sessions.UpdateAnonymousID(session.SessionID, m.ID); err != nil {
return err
}
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSID_Mobile", m.ID)
s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSIDMOBILE", m.ID)
return nil
case *MobileMetadata:
return s.sessions.UpdateMetadata(m.SessionID(), m.Key, m.Value)

View file

@ -132,8 +132,15 @@ func (conn *Conn) InsertWebClickEvent(sess *sessions.Session, e *messages.MouseC
}
var host, path string
host, path, _, _ = url.GetURLParts(e.Url)
if e.NormalizedX <= 100 && e.NormalizedY <= 100 {
if err := conn.bulks.Get("webClickXYEvents").Append(sess.SessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path, e.HesitationTime, e.NormalizedX, e.NormalizedY); err != nil {
if e.NormalizedX != 101 && e.NormalizedY != 101 {
// To support previous versions of tracker
if e.NormalizedX <= 100 && e.NormalizedY <= 100 {
e.NormalizedX *= 100
e.NormalizedY *= 100
}
normalizedX := float32(e.NormalizedX) / 100.0
normalizedY := float32(e.NormalizedY) / 100.0
if err := conn.bulks.Get("webClickXYEvents").Append(sess.SessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path, e.HesitationTime, normalizedX, normalizedY); err != nil {
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
conn.log.Error(sessCtx, "insert web click event in bulk err: %s", err)
}

View file

@ -13,14 +13,14 @@ func (conn *Conn) InsertMobileEvent(session *sessions.Session, e *messages.Mobil
if err := conn.InsertCustomEvent(session.SessionID, e.Timestamp, truncSqIdx(e.Index), e.Name, e.Payload); err != nil {
return err
}
conn.InsertAutocompleteValue(session.SessionID, session.ProjectID, "CUSTOM_Mobile", e.Name)
conn.InsertAutocompleteValue(session.SessionID, session.ProjectID, "CUSTOMMOBILE", e.Name)
return nil
}
func (conn *Conn) InsertMobileNetworkCall(sess *sessions.Session, e *messages.MobileNetworkCall) error {
err := conn.InsertRequest(sess.SessionID, e.Timestamp, truncSqIdx(e.Index), e.URL, e.Duration, e.Status < 400)
if err == nil {
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "REQUEST_Mobile", url.DiscardURLQuery(e.URL))
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "REQUESTMOBILE", url.DiscardURLQuery(e.URL))
}
return err
}
@ -36,7 +36,7 @@ func (conn *Conn) InsertMobileClickEvent(sess *sessions.Session, clickEvent *mes
); err != nil {
return err
}
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "CLICK_Mobile", clickEvent.Label)
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "CLICKMOBILE", clickEvent.Label)
return nil
}
@ -51,7 +51,7 @@ func (conn *Conn) InsertMobileSwipeEvent(sess *sessions.Session, swipeEvent *mes
); err != nil {
return err
}
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "SWIPE_Mobile", swipeEvent.Label)
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "SWIPEMOBILE", swipeEvent.Label)
return nil
}
@ -66,7 +66,7 @@ func (conn *Conn) InsertMobileInputEvent(sess *sessions.Session, inputEvent *mes
); err != nil {
return err
}
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "INPUT_Mobile", inputEvent.Label)
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "INPUTMOBILE", inputEvent.Label)
return nil
}

View file

@ -29,6 +29,11 @@ def _get_current_auth_context(request: Request, jwt_payload: dict) -> schemas.Cu
return request.state.currentContext
def _allow_access_to_endpoint(request: Request, current_context: schemas.CurrentContext) -> bool:
return not current_context.service_account \
or request.url.path not in ["/logout", "/api/logout", "/refresh", "/api/refresh"]
class JWTAuth(HTTPBearer):
def __init__(self, auto_error: bool = True):
super(JWTAuth, self).__init__(auto_error=auto_error)
@ -68,7 +73,10 @@ class JWTAuth(HTTPBearer):
or old_jwt_payload.get("userId") != jwt_payload.get("userId"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.")
return _get_current_auth_context(request=request, jwt_payload=jwt_payload)
ctx = _get_current_auth_context(request=request, jwt_payload=jwt_payload)
if not _allow_access_to_endpoint(request=request, current_context=ctx):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized endpoint.")
return ctx
else:
credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request)
@ -95,7 +103,10 @@ class JWTAuth(HTTPBearer):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.")
return _get_current_auth_context(request=request, jwt_payload=jwt_payload)
ctx = _get_current_auth_context(request=request, jwt_payload=jwt_payload)
if not _allow_access_to_endpoint(request=request, current_context=ctx):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized endpoint.")
return ctx
logger.warning("Invalid authorization code.")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.")

View file

@ -339,10 +339,13 @@ def create_card(project_id, user_id, data: schemas.CardSchema, dashboard=False):
session_data = None
if data.metric_type == schemas.MetricType.heat_map:
if data.session_id is not None:
session_data = json.dumps({"sessionId": data.session_id})
session_data = {"sessionId": data.session_id}
else:
session_data = __get_heat_map_chart(project_id=project_id, user_id=user_id,
data=data, include_mobs=False)
if session_data is not None:
session_data = {"sessionId": session_data["sessionId"]}
if session_data is not None:
# for EE only
keys = sessions_mobs. \
@ -356,8 +359,8 @@ def create_card(project_id, user_id, data: schemas.CardSchema, dashboard=False):
except Exception as e:
logger.warning(f"!!!Error while tagging: {k} to {tag} for heatMap")
logger.error(str(e))
session_data = json.dumps(session_data)
_data = {"session_data": session_data}
_data = {"session_data": json.dumps(session_data) if session_data is not None else None}
for i, s in enumerate(data.series):
for k in s.model_dump().keys():
_data[f"{k}_{i}"] = s.__getattribute__(k)

View file

@ -57,16 +57,16 @@ def get_by_url(project_id, data: schemas.GetHeatMapPayloadSchema):
# f.value, value_key=f_k))
if data.click_rage and not has_click_rage_filter:
constraints.append("""(issues.session_id IS NULL
OR (issues.datetime >= toDateTime(%(startDate)s/1000)
AND issues.datetime <= toDateTime(%(endDate)s/1000)
AND issues.project_id = toUInt16(%(project_id)s)
AND issues.event_type = 'ISSUE'
AND issues.project_id = toUInt16(%(project_id)s
AND mis.project_id = toUInt16(%(project_id)s
AND mis.type='click_rage'))))""")
query_from += """ LEFT JOIN experimental.events AS issues ON (main_events.session_id=issues.session_id)
LEFT JOIN experimental.issues AS mis ON (issues.issue_id=mis.issue_id)"""
constraints.append("""(issues_t.session_id IS NULL
OR (issues_t.datetime >= toDateTime(%(startDate)s/1000)
AND issues_t.datetime <= toDateTime(%(endDate)s/1000)
AND issues_t.project_id = toUInt16(%(project_id)s)
AND issues_t.event_type = 'ISSUE'
AND issues_t.project_id = toUInt16(%(project_id)s)
AND mis.project_id = toUInt16(%(project_id)s)
AND mis.type='click_rage'))""")
query_from += """ LEFT JOIN experimental.events AS issues_t ON (main_events.session_id=issues_t.session_id)
LEFT JOIN experimental.issues AS mis ON (issues_t.issue_id=mis.issue_id)"""
with ch_client.ClickHouseClient() as cur:
query = cur.format(f"""SELECT main_events.normalized_x AS normalized_x,
main_events.normalized_y AS normalized_y

View file

@ -442,7 +442,8 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
if metric_format == schemas.MetricExtendedFormatType.session_count:
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
{main_col} AS name,
count(DISTINCT session_id) AS session_count
count(DISTINCT session_id) AS session_count,
COALESCE(SUM(count(DISTINCT session_id)) OVER (), 0) AS total_sessions
FROM (SELECT s.session_id AS session_id,
{extra_col}
{query_part}) AS filtred_sessions
@ -470,11 +471,14 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
logging.debug("--------------------")
sessions = cur.execute(main_query)
count = 0
total_sessions = 0
if len(sessions) > 0:
count = sessions[0]["main_count"]
total_sessions = sessions[0]["total_sessions"]
for s in sessions:
s.pop("main_count")
sessions = {"count": count, "values": helper.list_to_camel_case(sessions)}
s.pop("total_sessions")
sessions = {"total": count, "count": total_sessions, "values": helper.list_to_camel_case(sessions)}
return sessions
@ -520,7 +524,7 @@ def search_table_of_individual_issues(data: schemas.SessionsSearchPayloadSchema,
total_sessions = 0
issues_count = 0
return {"count": issues_count, "totalSessions": total_sessions, "values": issues}
return {"total": issues_count, "count": total_sessions, "values": issues}
def __is_valid_event(is_any: bool, event: schemas.SessionSearchEventSchema2):
@ -563,7 +567,7 @@ def __get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEve
schemas.PerformanceEventType.fetch_failed: "REQUEST",
schemas.EventType.error: "CRASH",
}
if platform == "ios" and event_type in defs_mobile:
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}")
@ -964,7 +968,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
value_key=f"custom{i}"))
full_args = {**full_args, **_multiple_values(event.source, value_key=f"custom{i}")}
else:
_column = events.EventType.INPUT_IOS.column
_column = events.EventType.INPUT_MOBILE.column
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
events_conditions.append({"type": event_where[-1]})
if not is_any:
@ -997,7 +1001,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
event.value, value_key=e_k))
events_conditions[-1]["condition"] = event_where[-1]
else:
_column = events.EventType.VIEW_IOS.column
_column = events.EventType.VIEW_MOBILE.column
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
events_conditions.append({"type": event_where[-1]})
if not is_any:
@ -1089,6 +1093,114 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
events_conditions[-1]["condition"] = " AND ".join(events_conditions[-1]["condition"])
# ----- 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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
value_key=e_k))
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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
value_key=e_k))
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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
event.value, value_key=e_k))
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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
event.value, value_key=e_k))
events_conditions[-1]["condition"] = event_where[-1]
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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s", event.value,
value_key=e_k))
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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
event.value, value_key=e_k))
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)}'")
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)}'"})
events_conditions_not[-1]["condition"] = event_where[-1]
else:
event_where.append(_multiple_conditions(f"main.{_column} {op} %({e_k})s",
event.value, value_key=e_k))
events_conditions[-1]["condition"] = event_where[-1]
elif event_type == schemas.PerformanceEventType.fetch_failed:
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
_column = 'url_path'

View file

@ -185,8 +185,9 @@ def __filter_subquery(project_id: int, filters: Optional[schemas.SessionsSearchP
errors_only=True, favorite_only=None,
issue=None, user_id=None)
params = {**params, **qp_params}
# TODO: test if this line impacts other cards beside insights
# sub_query = f"INNER JOIN {sub_query} USING(session_id)"
# This line was added because insights is failing when you add filter steps,
# for example when you add a LOCATION filter
sub_query = f"INNER JOIN {sub_query} USING(session_id)"
return params, sub_query

View file

@ -380,7 +380,9 @@ def get_by_email_only(email):
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
origin,
basic_authentication.password IS NOT NULL AS has_password
basic_authentication.password IS NOT NULL AS has_password,
role_id,
internal_id
FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
WHERE users.email = %(email)s
AND users.deleted_at IS NULL

View file

@ -10,7 +10,11 @@ from starlette.datastructures import FormData
if config("ENABLE_SSO", cast=bool, default=True):
from onelogin.saml2.auth import OneLogin_Saml2_Auth
API_PREFIX = "/api"
if config("LOCAL_DEV", default=False, cast=bool):
API_PREFIX = ""
else:
API_PREFIX = "/api"
SAML2 = {
"strict": config("saml_strict", cast=bool, default=True),
"debug": config("saml_debug", cast=bool, default=True),

View file

@ -1,8 +1,12 @@
import json
import logging
from decouple import config
from fastapi import HTTPException, Request, Response, status
from onelogin.saml2.auth import OneLogin_Saml2_Logout_Request
from starlette.responses import RedirectResponse
from chalicelib.core import users, tenants, roles
from chalicelib.utils import SAML2_helper
from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth
from routers.base import get_routers
@ -10,12 +14,6 @@ from routers.base import get_routers
logger = logging.getLogger(__name__)
public_app, app, app_apikey = get_routers()
from decouple import config
from onelogin.saml2.auth import OneLogin_Saml2_Logout_Request
from chalicelib.core import users, tenants, roles
from starlette.responses import RedirectResponse
@public_app.get("/sso/saml2", tags=["saml2"])
@ -90,15 +88,19 @@ async def process_sso_assertion(request: Request):
logger.error("invalid tenantKey, please copy the correct value from Preferences > Account")
return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]}
logger.debug(user_data)
role_name = user_data.get("role", [])
if len(role_name) == 0:
role_names = user_data.get("role", [])
if len(role_names) == 0:
logger.info("No role specified, setting role to member")
role_name = ["member"]
role_name = role_name[0]
role = roles.get_role_by_name(tenant_id=t['tenantId'], name=role_name)
if role is None:
return {"errors": [f"role {role_name} not found, please create it in openreplay first"]}
role_names = ["member"]
role = None
for r in role_names:
role = roles.get_role_by_name(tenant_id=t['tenantId'], name=r)
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
@ -122,10 +124,30 @@ async def process_sso_assertion(request: Request):
if t['tenantId'] != existing["tenantId"]:
logger.warning("user exists for a different tenant")
return {"errors": ["user exists for a different tenant"]}
if existing.get("origin") is None:
logger.info(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==")
users.update(tenant_id=t['tenantId'], user_id=existing["userId"],
changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id})
# Check difference between existing user and received data
received_data = {
"role": "admin" if admin_privileges else "member",
"origin": SAML2_helper.get_saml2_provider(),
"name": " ".join(user_data.get("firstName", []) + user_data.get("lastName", [])),
"internal_id": internal_id,
"role_id": role["roleId"]
}
existing_data = {
"role": "admin" if existing["admin"] else "member",
"origin": existing["origin"],
"name": existing["name"],
"internal_id": existing["internalId"],
"role_id": existing["roleId"]
}
to_update = {}
for k in existing_data.keys():
if (k != "role" or not existing["superAdmin"]) and existing_data[k] != received_data[k]:
to_update[k] = received_data[k]
if len(to_update.keys()) > 0:
logger.info(f"== Updating user:{existing['userId']}: {to_update} ==")
users.update(tenant_id=t['tenantId'], user_id=existing["userId"], changes=to_update)
expiration = auth.get_session_expiration()
expiration = expiration if expiration is not None and expiration > 10 * 60 \
else int(config("sso_exp_delta_seconds", cast=int, default=24 * 60 * 60))
@ -200,15 +222,19 @@ async def process_sso_assertion_tk(tenantKey: str, request: Request):
logger.error("invalid tenantKey, please copy the correct value from Preferences > Account")
return {"errors": ["invalid tenantKey, please copy the correct value from Preferences > Account"]}
logger.debug(user_data)
role_name = user_data.get("role", [])
if len(role_name) == 0:
role_names = user_data.get("role", [])
if len(role_names) == 0:
logger.info("No role specified, setting role to member")
role_name = ["member"]
role_name = role_name[0]
role = roles.get_role_by_name(tenant_id=t['tenantId'], name=role_name)
if role is None:
return {"errors": [f"role {role_name} not found, please create it in openreplay first"]}
role_names = ["member"]
role = None
for r in role_names:
role = roles.get_role_by_name(tenant_id=t['tenantId'], name=r)
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
@ -232,10 +258,30 @@ async def process_sso_assertion_tk(tenantKey: str, request: Request):
if t['tenantId'] != existing["tenantId"]:
logger.warning("user exists for a different tenant")
return {"errors": ["user exists for a different tenant"]}
if existing.get("origin") is None:
logger.info(f"== migrating user to {SAML2_helper.get_saml2_provider()} ==")
users.update(tenant_id=t['tenantId'], user_id=existing["userId"],
changes={"origin": SAML2_helper.get_saml2_provider(), "internal_id": internal_id})
# Check difference between existing user and received data
received_data = {
"role": "admin" if admin_privileges else "member",
"origin": SAML2_helper.get_saml2_provider(),
"name": " ".join(user_data.get("firstName", []) + user_data.get("lastName", [])),
"internal_id": internal_id,
"role_id": role["roleId"]
}
existing_data = {
"role": "admin" if existing["admin"] else "member",
"origin": existing["origin"],
"name": existing["name"],
"internal_id": existing["internalId"],
"role_id": existing["roleId"]
}
to_update = {}
for k in existing_data.keys():
if (k != "role" or not existing["superAdmin"]) and existing_data[k] != received_data[k]:
to_update[k] = received_data[k]
if len(to_update.keys()) > 0:
logger.info(f"== Updating user:{existing['userId']}: {to_update} ==")
users.update(tenant_id=t['tenantId'], user_id=existing["userId"], changes=to_update)
expiration = auth.get_session_expiration()
expiration = expiration if expiration is not None and expiration > 10 * 60 \
else int(config("sso_exp_delta_seconds", cast=int, default=24 * 60 * 60))

View file

@ -397,12 +397,19 @@ func (c *connectorImpl) InsertWebClickEvent(session *sessions.Session, msg *mess
if msg.Label == "" {
return nil
}
var nX *uint8 = nil
var nY *uint8 = nil
if msg.NormalizedX <= 100 && msg.NormalizedY <= 100 {
nXVal := uint8(msg.NormalizedX)
var nX *float32 = nil
var nY *float32 = nil
if msg.NormalizedX != 101 && msg.NormalizedY != 101 {
// To support previous versions of tracker
if msg.NormalizedX <= 100 && msg.NormalizedY <= 100 {
msg.NormalizedX *= 100
msg.NormalizedY *= 100
}
normalizedX := float32(msg.NormalizedX) / 100.0
normalizedY := float32(msg.NormalizedY) / 100.0
nXVal := normalizedX
nX = &nXVal
nYVal := uint8(msg.NormalizedY)
nYVal := normalizedY
nY = &nYVal
}
if err := c.batches["clicks"].Append(

View file

@ -3,8 +3,8 @@ CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.19.0-ee';
DROP TABLE IF EXISTS experimental.events_l7d_mv;
ALTER TABLE experimental.events
ADD COLUMN IF NOT EXISTS normalized_x Nullable(UInt8),
ADD COLUMN IF NOT EXISTS normalized_y Nullable(UInt8),
ADD COLUMN IF NOT EXISTS normalized_x Nullable(Float32),
ADD COLUMN IF NOT EXISTS normalized_y Nullable(Float32),
DROP COLUMN IF EXISTS coordinate;
CREATE MATERIALIZED VIEW IF NOT EXISTS experimental.events_l7d_mv

View file

@ -81,8 +81,8 @@ CREATE TABLE IF NOT EXISTS experimental.events
error_tags_values Array(Nullable(String)),
transfer_size Nullable(UInt32),
selector Nullable(String),
normalized_x Nullable(UInt8),
normalized_y Nullable(UInt8),
normalized_x Nullable(Float32),
normalized_y Nullable(Float32),
message_id UInt64 DEFAULT 0,
_timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp)

View file

@ -19,8 +19,8 @@ $fn_def$, :'next_version')
--
ALTER TABLE IF EXISTS events.clicks
ADD COLUMN IF NOT EXISTS normalized_x smallint NULL,
ADD COLUMN IF NOT EXISTS normalized_y smallint NULL,
ADD COLUMN IF NOT EXISTS normalized_x decimal NULL,
ADD COLUMN IF NOT EXISTS normalized_y decimal NULL,
DROP COLUMN IF EXISTS x,
DROP COLUMN IF EXISTS y;

View file

@ -659,16 +659,16 @@ CREATE INDEX pages_query_nn_gin_idx ON events.pages USING GIN (query gin_trgm_op
CREATE TABLE events.clicks
(
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
message_id bigint NOT NULL,
timestamp bigint NOT NULL,
label text DEFAULT NULL,
url text DEFAULT '' NOT NULL,
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
message_id bigint NOT NULL,
timestamp bigint NOT NULL,
label text DEFAULT NULL,
url text DEFAULT '' NOT NULL,
path text,
selector text DEFAULT '' NOT NULL,
hesitation integer DEFAULT NULL,
normalized_x smallint DEFAULT NULL,
normalized_y smallint DEFAULT NULL,
selector text DEFAULT '' NOT NULL,
hesitation integer DEFAULT NULL,
normalized_x decimal DEFAULT NULL,
normalized_y decimal DEFAULT NULL,
PRIMARY KEY (session_id, message_id)
);
CREATE INDEX clicks_session_id_idx ON events.clicks (session_id);

View file

@ -62,9 +62,9 @@ function ClickMapCard({
if (mapUrl) return evt.path.includes(mapUrl)
return evt
}) || { timestamp: metricStore.instance.data.startTs }
const jumpTimestamp = (jumpToEvent.timestamp - metricStore.instance.data.startTs) + jumpToEvent.domBuildingTime + 99 // 99ms safety margin to give some time for the DOM to load
const ts = jumpToEvent.timestamp ?? metricStore.instance.data.startTs
const domTime = jumpToEvent.domBuildingTime ?? 0
const jumpTimestamp = (ts - metricStore.instance.data.startTs) + domTime + 99 // 99ms safety margin to give some time for the DOM to load
return (
<div id="clickmap-render">
<ClickMapRenderer

View file

@ -1,3 +1,4 @@
import ExampleFunnel from './Examples/Funnel';
import ExamplePath from './Examples/Path';
import ExampleTrend from './Examples/Trend';

View file

@ -10,13 +10,15 @@ interface NewDashboardModalProps {
open: boolean;
isAddingFromLibrary?: boolean;
isEnterprise?: boolean;
isMobile?: boolean;
}
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
onClose,
open,
isAddingFromLibrary = false,
isEnterprise = false
isEnterprise = false,
isMobile = false
}) => {
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
@ -53,6 +55,7 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
setSelectedCategory={setSelectedCategory}
onCard={() => setStep(step + 1)}
isLibrary={isAddingFromLibrary}
isMobile={isMobile}
isEnterprise={isEnterprise} />}
{step === 1 && <CreateCard onBack={() => setStep(0)} />}
</div>
@ -63,6 +66,7 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
};
const mapStateToProps = (state: any) => ({
isMobile: state.getIn(['site', 'instance', 'platform']) === 'ios',
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'account', 'edition']) === 'msaas'
});

View file

@ -1,6 +1,6 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Button, Input, Segmented, Space } from 'antd';
import { RightOutlined } from '@ant-design/icons'
import { RightOutlined } from '@ant-design/icons';
import { ArrowRight, Info } from 'lucide-react';
import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards';
import { useStore } from 'App/mstore';
@ -8,6 +8,7 @@ import Option from './Option';
import CardsLibrary from 'Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary';
import { FUNNEL } from 'App/constants/card';
import { useHistory } from 'react-router';
import { FilterKey } from 'Types/filter/filterType';
interface SelectCardProps {
onClose: (refresh?: boolean) => void;
@ -16,10 +17,11 @@ interface SelectCardProps {
selected?: string;
setSelectedCategory?: React.Dispatch<React.SetStateAction<string>>;
isEnterprise?: boolean;
isMobile?: boolean;
}
const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
const { onCard, isLibrary = false, selected, setSelectedCategory, isEnterprise } = props;
const { onCard, isLibrary = false, selected, setSelectedCategory, isEnterprise, isMobile } = props;
const [selectedCards, setSelectedCards] = React.useState<number[]>([]);
const { metricStore, dashboardStore } = useStore();
const siteId: string = location.pathname.split('/')[1];
@ -74,20 +76,23 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
};
const cardItems = useMemo(() => {
return CARD_LIST.filter((card) => card.category === selected && (!card.isEnterprise || (card.isEnterprise && isEnterprise)))
.map((card) => (
<div key={card.key} className={card.width ? `col-span-${card.width}` : 'col-span-2'}>
<card.example
onCard={handleCardSelection}
type={card.key}
title={card.title}
data={card.data}
height={card.height}
hideLegend={card.data?.hideLegend}
/>
</div>
));
}, [selected]);
return CARD_LIST.filter((card) =>
card.category === selected &&
(!card.isEnterprise || (card.isEnterprise && isEnterprise)) &&
(!isMobile || (isMobile && ![FilterKey.USER_BROWSER].includes(card.key)))
).map((card) => (
<div key={card.key} className={card.width ? `col-span-${card.width}` : 'col-span-2'}>
<card.example
onCard={handleCardSelection}
type={card.key}
title={card.title}
data={card.data}
height={card.height}
hideLegend={card.data?.hideLegend}
/>
</div>
));
}, [selected, isEnterprise, isMobile]);
const onCardClick = (cardId: number) => {
if (selectedCards.includes(cardId)) {
@ -119,7 +124,7 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
)}
</div>
{isCreatingDashboard && (
<Button type="link" onClick={createNewDashboard} loading={dashboardCreating} className='gap-2'>
<Button type="link" onClick={createNewDashboard} loading={dashboardCreating} className="gap-2">
<Space>
Create Blank
<RightOutlined />

View file

@ -51,8 +51,8 @@ function WebPlayer(props: any) {
const isPlayerReady = contextValue.store?.get().ready
React.useEffect(() => {
contextValue.player && contextValue.player.play()
if (isPlayerReady && insights.size > 0) {
contextValue.player && contextValue.player.play()
if (isPlayerReady && insights.size > 0 && jumpTimestamp) {
setTimeout(() => {
contextValue.player.pause()
contextValue.player.jump(jumpTimestamp)

View file

@ -169,10 +169,10 @@ interface DevtoolsButtonsProps {
bottomBlock: number;
}
function DevtoolsButtons({
const DevtoolsButtons = observer(({
toggleBottomTools,
bottomBlock,
}: DevtoolsButtonsProps) {
}: DevtoolsButtonsProps) => {
const { aiSummaryStore } = useStore();
const { store, player } = React.useContext(MobilePlayerContext);
@ -277,7 +277,7 @@ function DevtoolsButtons({
/>
</>
);
}
})
const ControlPlayer = observer(Controls);

View file

@ -36,6 +36,8 @@ function UserCard({ className, request, session, width, height, similarSessions,
userDisplayName,
userDeviceType,
revId,
screenWidth,
screenHeight
} = session;
const hasUserDetails = !!userId || !!userAnonymousId;
@ -137,7 +139,7 @@ function UserCard({ className, request, session, width, height, similarSessions,
<SessionInfoItem
icon={deviceTypeIcon(userDeviceType)}
label={userDeviceType}
value={getDimension(width, height)}
value={getDimension(width || screenWidth, height || screenHeight)}
isLast={!revId}
/>
{revId && <SessionInfoItem icon="info" label="Rev ID:" value={revId} isLast />}

View file

@ -9,7 +9,7 @@
height: 100%;
/* border: solid thin $gray-light; */
/* border-radius: 3px; */
overflow: hidden;
overflow-y: scroll;
}
.checkers {

View file

@ -1,13 +1,14 @@
import React, { useMemo } from 'react';
import { formatBytes } from 'App/utils';
import CopyText from 'Shared/CopyText';
import {Tag} from 'antd';
import { Tag } from 'antd';
import cn from 'classnames';
interface Props {
resource: any;
timestamp?: string;
}
function FetchBasicDetails({ resource, timestamp }: Props) {
const _duration = parseInt(resource.duration);
const text = useMemo(() => {
@ -22,14 +23,16 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div>
<div className="flex items-start py-1">
<div className="font-medium w-36">Name</div>
<Tag className='text-base max-w-96 rounded-lg text-clip bg-indigo-50 whitespace-nowrap overflow-hidden text-clip cursor-pointer word-break' bordered={false}>
<Tag
className="text-base max-w-96 rounded-lg text-clip bg-indigo-50 whitespace-nowrap overflow-hidden text-clip cursor-pointer word-break"
bordered={false}>
<CopyText content={resource.url}>{resource.url}</CopyText>
</Tag>
</div>
<div className="flex items-center py-1">
<div className="font-medium w-36">Type</div>
<Tag className='text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip' bordered={false}>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip" bordered={false}>
{resource.type}
</Tag>
</div>
@ -37,7 +40,8 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{resource.method && (
<div className="flex items-center py-1">
<div className="font-medium w-36">Request Method</div>
<Tag className='text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip' bordered={false}>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip"
bordered={false}>
{resource.method}
</Tag>
</div>
@ -47,15 +51,12 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="text-base font-medium w-36">Status Code</div>
<Tag
bordered={false}
bordered={false}
className={cn(
'text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip flex items-center',
{ 'error color-red': !resource.success }
)}
>
{resource.status === '200' && (
<Tag bordered={false} className="text-base bg-emerald-100 rounded-full mr-2"></Tag>
)}
{resource.status}
</Tag>
</div>
@ -63,7 +64,8 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
<div className="flex items-center py-1">
<div className="font-medium w-36">Type</div>
<Tag className="text-base capitalize rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip" bordered={false}>
<Tag className="text-base capitalize rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip"
bordered={false}>
{resource.type}
</Tag>
</div>
@ -71,18 +73,19 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{!!resource.decodedBodySize && (
<div className="flex items-center py-1">
<div className="font-medium w-36">Size</div>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip" bordered={false}>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip"
bordered={false}>
{formatBytes(resource.decodedBodySize)}
</Tag>
</div>
)}
{!!_duration && (
<div className="flex items-center py-1">
<div className="font-medium w-36">Duration</div>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip" bordered={false}>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip"
bordered={false}>
{_duration} ms
</Tag>
</div>
@ -90,11 +93,12 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
{timestamp && (
<div className="flex items-center py-1">
<div className="font-medium w-36">Time</div>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip" bordered={false}>
{timestamp}
</Tag>
</div>
<div className="font-medium w-36">Time</div>
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip"
bordered={false}>
{timestamp}
</Tag>
</div>
)}
</div>

View file

@ -32,7 +32,7 @@ import { connect } from 'react-redux';
import { Icon, Loader } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { FilterKey } from '../../../../types/filter/filterType';
import { FilterKey } from 'Types/filter/filterType';
import stl from './FilterModal.module.css';
const IconMap = {

View file

@ -12,7 +12,6 @@ interface Props {
function Activity(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/></svg>
);
}

View file

@ -10,7 +10,7 @@ interface Props {
}
function Console_info(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/><path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>
);

View file

@ -0,0 +1,19 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Filters_chevrons_up_down(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="m7 15 5 5 5-5M7 9l5-5 5 5"/></svg>
);
}
export default Filters_chevrons_up_down;

View file

@ -0,0 +1,19 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Filters_screen(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M21 17v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2M21 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>
);
}
export default Filters_screen;

View file

@ -275,6 +275,7 @@ export { default as Filetype_pdf } from './filetype_pdf';
export { default as Filter } from './filter';
export { default as Filters_arrow_return_right } from './filters_arrow_return_right';
export { default as Filters_browser } from './filters_browser';
export { default as Filters_chevrons_up_down } from './filters_chevrons_up_down';
export { default as Filters_click } from './filters_click';
export { default as Filters_clickrage } from './filters_clickrage';
export { default as Filters_code } from './filters_code';
@ -303,6 +304,7 @@ export { default as Filters_platform } from './filters_platform';
export { default as Filters_referrer } from './filters_referrer';
export { default as Filters_resize } from './filters_resize';
export { default as Filters_rev_id } from './filters_rev_id';
export { default as Filters_screen } from './filters_screen';
export { default as Filters_state_action } from './filters_state_action';
export { default as Filters_tag_element } from './filters_tag_element';
export { default as Filters_ttfb } from './filters_ttfb';

View file

@ -12,7 +12,7 @@ interface Props {
function Pdf_download(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-down"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M12 18v-6"/><path d="m9 15 3 3 3-3"/></svg>
<svg viewBox="0 0 19 19" width={ `${ width }px` } height={ `${ height }px` } ><path d="M10.094 5.249a.594.594 0 0 0-1.188 0v4.504l-1.36-1.362a.595.595 0 0 0-.841.84l2.375 2.376a.596.596 0 0 0 .84 0l2.375-2.375a.595.595 0 0 0-.84-.841l-1.361 1.362V5.249Z"/><path d="M16.625 16.625V5.344L11.281 0H4.75a2.375 2.375 0 0 0-2.375 2.375v14.25A2.375 2.375 0 0 0 4.75 19h9.5a2.375 2.375 0 0 0 2.375-2.375ZM11.281 3.562a1.781 1.781 0 0 0 1.781 1.782h2.376v11.281a1.188 1.188 0 0 1-1.188 1.188h-9.5a1.187 1.187 0 0 1-1.188-1.188V2.375A1.188 1.188 0 0 1 4.75 1.187h6.531v2.375Z"/><path clipRule="evenodd" d="M15.58 13.49H3.42v4.37h1.789v-3.512H6.61c.282 0 .524.051.726.154.205.103.361.245.47.425.11.178.165.383.165.613 0 .226-.055.424-.164.593-.11.169-.266.3-.47.396-.203.093-.445.14-.727.14h-.554v1.191h2.37v-3.512h1.13c.238 0 .455.041.653.123a1.537 1.537 0 0 1 .854.88c.08.205.12.431.12.68v.148c0 .247-.04.474-.12.68a1.512 1.512 0 0 1-.849.88c-.193.08-.404.12-.635.121h2.046v-3.512h2.35v.654h-1.503v.808h1.365v.651h-1.365v1.399h3.108v-4.37Zm-9.524 1.512v1.013h.554c.12 0 .216-.02.29-.06a.367.367 0 0 0 .161-.167.547.547 0 0 0 .054-.244.668.668 0 0 0-.054-.267.431.431 0 0 0-.161-.198.496.496 0 0 0-.29-.077h-.554Zm3.512 2.207h-.295v-2.207h.283c.123 0 .233.021.328.065a.604.604 0 0 1 .24.195.88.88 0 0 1 .146.321c.033.127.05.275.05.444v.152c0 .225-.03.415-.089.569a.707.707 0 0 1-.256.345.696.696 0 0 1-.407.116Z"/></svg>
);
}

View file

@ -12,7 +12,7 @@ interface Props {
function Pencil(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-pen-line"><path d="m18 5-2.414-2.414A2 2 0 0 0 14.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/><path d="M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><path d="M8 18h1"/></svg>
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>
);
}

View file

@ -12,7 +12,7 @@ interface Props {
function Trash(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
);
}

View file

@ -12,8 +12,7 @@ interface Props {
function Users(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users-round"><path d="M18 21a8 8 0 0 0-16 0"/><circle cx="10" cy="8" r="5"/><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"/></svg>
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816zM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275zM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/></svg>
);
}

File diff suppressed because one or more lines are too long

View file

@ -76,7 +76,7 @@ function reducer(state = initialState, action = {}) {
switch (action.type) {
case REFRESH_FILTER_OPTIONS:
return state
.set('filterList', generateFilterOptions(filtersMap))
.set('filterList', generateFilterOptions(filtersMap, action.isMobile))
.set('filterListLive', generateFilterOptions(liveFiltersMap))
.set(
'filterListConditional',
@ -466,10 +466,12 @@ export const editSavedSearch = (instance) => {
};
};
export const refreshFilterOptions = () => {
return {
export const refreshFilterOptions = () => (dispatch, getState) => {
const currentProject = getState().getIn(['site', 'instance']);
return dispatch({
type: REFRESH_FILTER_OPTIONS,
};
isMobile: currentProject?.platform === 'ios'
});
};
export const setScrollPosition = (scrollPosition) => {

View file

@ -302,8 +302,8 @@ export default class Widget {
} else if (this.metricType === FUNNEL) {
_data.funnel = new Funnel().fromJSON(_data);
} else if (this.metricType === TABLE) {
const totalSessions = data[0]['totalSessions'];
_data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, totalSessions, this.metricOf));
const count = data[0]['count'];
_data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, count, this.metricOf));
} else {
if (data.hasOwnProperty('chart')) {
_data['value'] = data.value;

View file

@ -78,10 +78,10 @@ export interface State extends ScreenState, ListsState {
}
const userEvents = [
MType.IosSwipeEvent,
MType.IosClickEvent,
MType.IosInputEvent,
MType.IosScreenChanges,
MType.MobileSwipeEvent,
MType.MobileClickEvent,
MType.MobileInputEvent,
MType.MobileScreenChanges,
];
export default class IOSMessageManager implements IMessageManager {
@ -233,7 +233,7 @@ export default class IOSMessageManager implements IMessageManager {
}
switch (msg.tp) {
case MType.IosPerformanceEvent:
case MType.MobilePerformanceEvent:
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU'];
if (performanceStats.includes(msg.name)) {
this.performanceManager.append(msg);
@ -253,21 +253,21 @@ export default class IOSMessageManager implements IMessageManager {
// case MType.IosInputEvent:
// console.log('input', msg)
// break;
case MType.IosNetworkCall:
case MType.MobileNetworkCall:
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart));
break;
case MType.WsChannel:
this.lists.lists.websocket.insert(msg);
break;
case MType.IosEvent:
case MType.MobileEvent:
// @ts-ignore
this.lists.lists.event.insert({ ...msg, source: 'openreplay' });
break;
case MType.IosSwipeEvent:
case MType.IosClickEvent:
case MType.MobileSwipeEvent:
case MType.MobileClickEvent:
this.touchManager.append(msg);
break;
case MType.IosLog:
case MType.MobileLog:
const log = { ...msg, level: msg.severity };
// @ts-ignore
this.lists.lists.log.append(Log(log));

View file

@ -31,7 +31,7 @@ export default class TouchManager extends ListWalker<IosClickEvent | IosSwipeEve
public move(t: number) {
const lastTouch = this.moveGetLast(t)
if (!!lastTouch) {
if (lastTouch.tp === MType.IosSwipeEvent) {
if (lastTouch.tp === MType.MobileSwipeEvent) {
return
// not using swipe rn
// this.touchTrail?.createSwipeTrail({

View file

@ -233,10 +233,10 @@ export default class Screen {
break;
case ScaleMode.AdjustParentHeight:
// we want to scale the document with true height so the clickmap will be scrollable
const usedHeight =
this.document?.body.scrollHeight && this.document?.body.scrollHeight > height
? this.document.body.scrollHeight + 'px'
: height + 'px';
const usedHeight = height + 'px';
// this.document?.body.scrollHeight && this.document?.body.scrollHeight > height
// ? this.document.body.scrollHeight + 'px'
// : height + 'px';
this.scaleRatio = offsetWidth / width;
translate = 'translate(-50%, 0)';
posStyles = { top: 0, height: usedHeight };

View file

@ -146,39 +146,37 @@ export default class TargetMarker {
if (clicks && this.screen.document) {
this.clickMapOverlay?.remove();
const overlay = document.createElement('canvas');
const iframeSize = this.screen.iframeStylesRef;
const scrollHeight = this.screen.document?.documentElement.scrollHeight || 0;
const scrollWidth = this.screen.document?.documentElement.scrollWidth || 0;
const scaleRatio = this.screen.getScale();
Object.assign(
overlay.style,
clickmapStyles.overlayStyle({
height: iframeSize.height,
width: iframeSize.width,
scale: scaleRatio,
height: scrollHeight + 'px',
width: scrollWidth + 'px',
})
);
this.clickMapOverlay = overlay;
this.screen.getParentElement()?.appendChild(overlay);
this.screen.document.body.appendChild(overlay);
const pointMap: Record<string, { times: number; data: number[], original: any }> = {};
const ovWidth = parseInt(iframeSize.width);
const ovHeight = parseInt(iframeSize.height);
overlay.width = ovWidth;
overlay.height = ovHeight;
overlay.width = scrollWidth;
overlay.height = scrollHeight;
let maxIntensity = 0;
clicks.forEach((point) => {
const key = `${point.normalizedY}-${point.normalizedX}`;
const y = roundToSecond(point.normalizedY);
const x = roundToSecond(point.normalizedX);
const key = `${y}-${x}`;
if (pointMap[key]) {
const times = pointMap[key].times + 1;
maxIntensity = Math.max(maxIntensity, times);
pointMap[key].times = times;
} else {
const clickData = [
(point.normalizedX / 100) * scrollWidth,
(point.normalizedY / 100) * scrollHeight,
(x / 100) * scrollWidth,
(y / 100) * scrollHeight,
];
pointMap[key] = { times: 1, data: clickData, original: point };
}
@ -204,3 +202,7 @@ export default class TargetMarker {
}
}
}
function roundToSecond(num: number) {
return Math.round(num * 100) / 100;
}

View file

@ -1,14 +1,12 @@
export const clickmapStyles = {
overlayStyle: ({ height, width, scale }: { height: string, width: string, scale: number }) => ({
transform: `scale(${scale}) translate(-50%, 0)`,
overlayStyle: ({ height, width }: { height: string, width: string }) => ({
position: 'absolute',
top: '0px',
left: '50%',
left: 0,
width,
height,
background: 'rgba(0,0,0, 0.15)',
zIndex: 9 * 10e3,
transformOrigin: 'left top',
}),
totalClicks: {
fontSize: '16px',

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>

After

Width:  |  Height:  |  Size: 253 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-view"><path d="M21 17v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2"/><path d="M21 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>

After

Width:  |  Height:  |  Size: 430 B

View file

@ -196,6 +196,14 @@ export enum FilterType {
}
export enum FilterKey {
CLICK_MOBILE = 'clickMobile',
INPUT_MOBILE = 'inputMobile',
VIEW_MOBILE = 'viewMobile',
CUSTOM_MOBILE = 'customMobile',
REQUEST_MOBILE = 'requestMobile',
ERROR_MOBILE = 'errorMobile',
SWIPE_MOBILE = 'swipeMobile',
ERROR = 'error',
MISSING_RESOURCE = 'missingResource',
SLOW_SESSION = 'slowSession',

View file

@ -1,4 +1,4 @@
import { stringConditional, tagElementOperators, targetConditional } from "App/constants/filterOptions";
import { stringConditional, tagElementOperators, targetConditional } from 'App/constants/filterOptions';
import { KEYS } from 'Types/filter/customFilter';
import Record from 'Types/Record';
import { FilterType, FilterKey, FilterCategory } from './filterType';
@ -13,10 +13,78 @@ const filterOrder = {
[FilterCategory.TECHNICAL]: 1,
[FilterCategory.PERFORMANCE]: 2,
[FilterCategory.USER]: 3,
[FilterCategory.GEAR]: 4,
}
[FilterCategory.GEAR]: 4
};
export const mobileFilters = [
{
key: FilterKey.CLICK_MOBILE,
type: FilterType.MULTIPLE,
category: FilterCategory.INTERACTIONS,
label: 'Tap',
operator: 'on',
operatorOptions: filterOptions.targetOperators,
icon: 'filters/click',
isEvent: true
},
{
key: FilterKey.INPUT_MOBILE,
type: FilterType.MULTIPLE,
category: FilterCategory.INTERACTIONS,
label: 'Text Input',
placeholder: 'Enter input label name',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/input',
isEvent: true
},
{
key: FilterKey.VIEW_MOBILE,
type: FilterType.MULTIPLE,
category: FilterCategory.INTERACTIONS,
label: 'Screen',
placeholder: 'Enter screen name',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/screen',
isEvent: true
},
{
key: FilterKey.CUSTOM_MOBILE,
type: FilterType.MULTIPLE,
category: FilterCategory.TECHNICAL,
label: 'Custom Events',
placeholder: 'Enter event key',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/custom',
isEvent: true
},
{
key: FilterKey.ERROR_MOBILE,
type: FilterType.MULTIPLE,
category: FilterCategory.TECHNICAL,
label: 'Error Message',
placeholder: 'E.g. Uncaught SyntaxError',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/error',
isEvent: true
},
{
key: FilterKey.SWIPE_MOBILE,
type: FilterType.MULTIPLE,
category: FilterCategory.INTERACTIONS,
label: 'Swipe',
operator: 'on',
operatorOptions: filterOptions.targetOperators,
icon: 'filters/chevrons-up-down',
isEvent: true
}
];
export const filters = [
...mobileFilters,
{
key: FilterKey.CLICK,
type: FilterType.MULTIPLE,
@ -96,7 +164,7 @@ export const filters = [
operator: 'is',
placeholder: 'Select method type',
operatorOptions: filterOptions.stringOperatorsLimited,
icon: 'filters/fetch',
icon: 'filters/fetch',
options: filterOptions.methodOptions
},
{
@ -232,7 +300,7 @@ export const filters = [
isEvent: true,
icon: 'filters/tag-element',
operatorOptions: filterOptions.tagElementOperators,
options: [],
options: []
},
{
key: FilterKey.UTM_SOURCE,
@ -241,7 +309,7 @@ export const filters = [
label: 'UTM Source',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/country',
icon: 'filters/country'
},
{
key: FilterKey.UTM_MEDIUM,
@ -250,7 +318,7 @@ export const filters = [
label: 'UTM Medium',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/country',
icon: 'filters/country'
},
{
key: FilterKey.UTM_CAMPAIGN,
@ -259,7 +327,7 @@ export const filters = [
label: 'UTM Campaign',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/country',
icon: 'filters/country'
},
{
key: FilterKey.USER_COUNTRY,
@ -471,12 +539,12 @@ export const filters = [
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'collection'
},
}
].sort((a, b) => {
const aOrder = filterOrder[a.category] ?? 9
const bOrder = filterOrder[b.category] ?? 9
return aOrder - bOrder
})
const aOrder = filterOrder[a.category] ?? 9;
const bOrder = filterOrder[b.category] ?? 9;
return aOrder - bOrder;
});
export const flagConditionFilters = [
{
@ -559,10 +627,10 @@ export const flagConditionFilters = [
icon: 'filters/userid'
}
].sort((a, b) => {
const aOrder = filterOrder[a.category] ?? 9
const bOrder = filterOrder[b.category] ?? 9
return aOrder - bOrder
})
const aOrder = filterOrder[a.category] ?? 9;
const bOrder = filterOrder[b.category] ?? 9;
return aOrder - bOrder;
});
export const conditionalFilters = [
{
@ -612,7 +680,7 @@ export const conditionalFilters = [
placeholder: 'Enter path or URL',
operator: 'is',
operatorOptions: filterOptions.stringConditional,
icon: "filters/fetch"
icon: 'filters/fetch'
},
{
key: FilterKey.FETCH_STATUS_CODE,
@ -622,7 +690,7 @@ export const conditionalFilters = [
placeholder: 'Enter status code',
operator: '=',
operatorOptions: filterOptions.customOperators,
icon: "filters/fetch"
icon: 'filters/fetch'
},
{
key: FilterKey.FETCH_METHOD,
@ -643,8 +711,8 @@ export const conditionalFilters = [
placeholder: 'E.g. 12',
operator: '=',
operatorOptions: filterOptions.customOperators,
icon: "filters/fetch"
},
icon: 'filters/fetch'
}
],
icon: 'filters/fetch',
isEvent: true
@ -667,7 +735,7 @@ export const conditionalFilters = [
label: 'Duration',
operator: 'is',
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
icon: "filters/duration",
icon: 'filters/duration',
isEvent: false
},
{
@ -690,10 +758,10 @@ export const conditionalFilters = [
icon: 'filters/userid'
}
].sort((a, b) => {
const aOrder = filterOrder[a.category] ?? 9
const bOrder = filterOrder[b.category] ?? 9
return aOrder - bOrder
})
const aOrder = filterOrder[a.category] ?? 9;
const bOrder = filterOrder[b.category] ?? 9;
return aOrder - bOrder;
});
export const mobileConditionalFilters = [
{
@ -703,7 +771,7 @@ export const mobileConditionalFilters = [
label: 'Duration',
operator: 'is',
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
icon: "filters/duration",
icon: 'filters/duration',
isEvent: false
},
{
@ -721,7 +789,7 @@ export const mobileConditionalFilters = [
placeholder: 'Enter path or URL',
operator: 'is',
operatorOptions: filterOptions.stringConditional,
icon: "filters/fetch"
icon: 'filters/fetch'
},
{
key: FilterKey.FETCH_STATUS_CODE,
@ -731,7 +799,7 @@ export const mobileConditionalFilters = [
placeholder: 'Enter status code',
operator: '=',
operatorOptions: filterOptions.customOperators,
icon: "filters/fetch"
icon: 'filters/fetch'
},
{
key: FilterKey.FETCH_METHOD,
@ -752,8 +820,8 @@ export const mobileConditionalFilters = [
placeholder: 'E.g. 12',
operator: '=',
operatorOptions: filterOptions.customOperators,
icon: "filters/fetch"
},
icon: 'filters/fetch'
}
],
icon: 'filters/fetch',
isEvent: true
@ -779,11 +847,11 @@ export const mobileConditionalFilters = [
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
icon: 'filters/cpu-load',
options: [
{ label: 'nominal', value: "0" },
{ label: 'warm', value: "1" },
{ label: 'hot', value: "2" },
{ label: 'critical', value: "3" }
],
{ label: 'nominal', value: '0' },
{ label: 'warm', value: '1' },
{ label: 'hot', value: '2' },
{ label: 'critical', value: '3' }
]
},
{
key: 'mainThreadCPU',
@ -793,7 +861,7 @@ export const mobileConditionalFilters = [
placeholder: '0 .. 100',
operator: '=',
operatorOptions: filterOptions.customOperators,
icon: 'filters/cpu-load',
icon: 'filters/cpu-load'
},
{
key: 'viewComponent',
@ -803,7 +871,7 @@ export const mobileConditionalFilters = [
placeholder: 'View Name',
operator: 'is',
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
icon: 'filters/view',
icon: 'filters/view'
},
{
key: FilterKey.USERID,
@ -833,7 +901,7 @@ export const mobileConditionalFilters = [
placeholder: 'logged value',
operator: 'is',
operatorOptions: filterOptions.stringOperators,
icon: 'filters/console',
icon: 'filters/console'
},
{
key: 'clickEvent',
@ -854,7 +922,7 @@ export const mobileConditionalFilters = [
operatorOptions: filterOptions.customOperators,
icon: 'filters/memory-load'
}
]
];
export const eventKeys = filters.filter((i) => i.isEvent).map(i => i.key);
export const nonFlagFilters = filters.filter(i => {
@ -955,12 +1023,12 @@ export const addElementToFiltersMap = (
export const addOptionsToFilter = (
key,
options,
options
) => {
if (filtersMap[key] && filtersMap[key].options) {
filtersMap[key].options = options
filtersMap[key].options = options;
}
}
};
function getMetadataLabel(key) {
return key.replace(/^_/, '').charAt(0).toUpperCase() + key.slice(2);
@ -1008,11 +1076,11 @@ export const addElementToConditionalFiltersMap = (
export const addElementToMobileConditionalFiltersMap = (
category = FilterCategory.METADATA,
key,
type = FilterType.MULTIPLE,
operator = 'is',
operatorOptions = filterOptions.stringOperators,
icon = 'filters/metadata'
key,
type = FilterType.MULTIPLE,
operator = 'is',
operatorOptions = filterOptions.stringOperators,
icon = 'filters/metadata'
) => {
mobileConditionalFiltersMap[key] = {
key,
@ -1023,8 +1091,8 @@ export const addElementToMobileConditionalFiltersMap = (
operatorOptions,
icon,
isLive: true
}
}
};
};
export const addElementToLiveFiltersMap = (
category = FilterCategory.METADATA,
@ -1094,7 +1162,7 @@ export default Record({
_filter = filtersMap[`_${filter.source}`];
} else {
if (filtersMap[filter.key]) {
_filter = filtersMap[filter.key]
_filter = filtersMap[filter.key];
} else {
_filter = filtersMap[type];
}
@ -1118,14 +1186,35 @@ export default Record({
}
});
const WEB_EXCLUDE = [
FilterKey.CLICK_MOBILE, FilterKey.SWIPE_MOBILE, FilterKey.INPUT_MOBILE,
FilterKey.VIEW_MOBILE, FilterKey.CUSTOM_MOBILE, FilterKey.REQUEST_MOBILE, FilterKey.ERROR_MOBILE
];
const MOBILE_EXCLUDE = [
FilterKey.CLICK, FilterKey.INPUT, FilterKey.ERROR, FilterKey.CUSTOM,
FilterKey.LOCATION, FilterKey.FETCH, FilterKey.DOM_COMPLETE,
FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, FilterKey.TTFB, FilterKey.USER_BROWSER,
FilterKey.PLATFORM
];
/**
* Group filters by category
* @param {*} filtersMap
* @returns
* @param map
* @param isMobile
*/
export const generateFilterOptions = (map) => {
export const generateFilterOptions = (map, isMobile = false) => {
const filterSection = {};
Object.keys(map).forEach(key => {
if (isMobile && MOBILE_EXCLUDE.includes(key)) {
return;
}
if (!isMobile && WEB_EXCLUDE.includes(key)) {
return;
}
const filter = map[key];
if (filterSection.hasOwnProperty(filter.category)) {
filterSection[filter.category].push(filter);

View file

@ -1,7 +1,6 @@
apiVersion: v2
name: chalice
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.7
# 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.19.0"
AppVersion: "v1.19.8"

View file

@ -113,6 +113,8 @@ spec:
value: {{ .Values.global.s3.assistRecordsBucket }}
- name: sessions_bucket
value: {{ .Values.global.s3.recordingsBucket }}
- name: IOS_VIDEO_BUCKET
value: {{ .Values.global.s3.recordingsBucket }}
- name: sourcemaps_bucket
value: {{ .Values.global.s3.sourcemapsBucket }}
- name: js_cache_bucket

View file

@ -1,7 +1,6 @@
apiVersion: v2
name: db
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.19.0"
AppVersion: "v1.19.2"

View file

@ -1,7 +1,6 @@
apiVersion: v2
name: frontend
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 (frontends://semver.org/)
version: 0.1.10
# 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.19.0"
AppVersion: "v1.19.3"

View file

@ -8,7 +8,11 @@ RETENTION_TIME=${RETENTION_TIME:-345600000}
topics=(
"raw"
"raw-ios"
"raw-images"
"canvas-images"
"trigger"
"canvas-trigger"
"mobile-trigger"
"cache"
"analytics"
"storage-failover"

View file

@ -19,8 +19,8 @@ $fn_def$, :'next_version')
--
ALTER TABLE IF EXISTS events.clicks
ADD COLUMN IF NOT EXISTS normalized_x smallint NULL,
ADD COLUMN IF NOT EXISTS normalized_y smallint NULL,
ADD COLUMN IF NOT EXISTS normalized_x decimal NULL,
ADD COLUMN IF NOT EXISTS normalized_y decimal NULL,
DROP COLUMN IF EXISTS x,
DROP COLUMN IF EXISTS y;

View file

@ -620,16 +620,16 @@ CREATE INDEX pages_query_nn_gin_idx ON events.pages USING GIN (query gin_trgm_op
CREATE TABLE events.clicks
(
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
message_id bigint NOT NULL,
timestamp bigint NOT NULL,
label text DEFAULT NULL,
url text DEFAULT '' NOT NULL,
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
message_id bigint NOT NULL,
timestamp bigint NOT NULL,
label text DEFAULT NULL,
url text DEFAULT '' NOT NULL,
path text,
selector text DEFAULT '' NOT NULL,
hesitation integer DEFAULT NULL,
normalized_x smallint DEFAULT NULL,
normalized_y smallint DEFAULT NULL,
selector text DEFAULT '' NOT NULL,
hesitation integer DEFAULT NULL,
normalized_x decimal DEFAULT NULL,
normalized_y decimal DEFAULT NULL,
PRIMARY KEY (session_id, message_id)
);
CREATE INDEX clicks_session_id_idx ON events.clicks (session_id);

View file

@ -18,13 +18,13 @@ returns `result` without changes.
```js
import Tracker from '@openreplay/tracker';
import trackerGraphQL from '@openreplay/tracker-graphql';
import { createGraphqlMiddleware } from '@openreplay/tracker-graphql';
const tracker = new Tracker({
projectKey: YOUR_PROJECT_KEY,
});
export const recordGraphQL = tracker.plugin(trackerGraphQL());
export const recordGraphQL = tracker.use(createGraphqlMiddleware());
```
### Relay
@ -33,15 +33,28 @@ If you're using [Relay network tools](https://github.com/relay-tools/react-relay
you can simply [create a middleware](https://github.com/relay-tools/react-relay-network-modern/tree/master?tab=readme-ov-file#example-of-injecting-networklayer-with-middlewares-on-the-client-side)
```js
import { createRelayMiddleware } from '@openreplay/tracker-graphql'
import { createRelayMiddleware } from '@openreplay/tracker-graphql';
const trackerMiddleware = createRelayMiddleware(tracker)
const trackerMiddleware = tracker.use(createRelayMiddleware());
const network = new RelayNetworkLayer([
// your middleware
// ,
trackerMiddleware
])
trackerMiddleware,
]);
```
You can pass a Sanitizer function to `createRelayMiddleware` to sanitize the variables and data before sending them to OpenReplay.
```js
const trackerLink = tracker.use(
createRelayMiddleware((variables) => {
return {
...variables,
password: '***',
};
}),
);
```
Or you can manually put `recordGraphQL` call
@ -52,22 +65,22 @@ then you should do something like below
import { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; // see above for recordGraphQL definition
import { Environment } from 'relay-runtime';
const handler = createGraphqlMiddleware(tracker)
const handler = tracker.use(createGraphqlMiddleware());
function fetchQuery(operation, variables, cacheConfig, uploadables) {
return fetch('www.myapi.com/resource', {
// ...
})
.then(response => response.json())
.then(result =>
handler(
// op kind, name, variables, response, duration (default 0)
operation.operationKind,
operation.name,
variables,
result,
duration,
),
.then((response) => response.json())
.then((result) =>
handler(
// op kind, name, variables, response, duration (default 0)
operation.operationKind,
operation.name,
variables,
result,
duration,
),
);
}
@ -81,10 +94,23 @@ See [Relay Network Layer](https://relay.dev/docs/en/network-layer) for details.
For [Apollo](https://www.apollographql.com/) you should create a new `ApolloLink`
```js
import { createTrackerLink } from '@openreplay/tracker-graphql'
import { createTrackerLink } from '@openreplay/tracker-graphql';
const trackerLink = createTrackerLink(tracker);
const yourLink = new ApolloLink(trackerLink)
const trackerLink = tracker.use(createTrackerLink());
const yourLink = new ApolloLink(trackerLink);
```
You can pass a Sanitizer function to `createRelayMiddleware` to sanitize the variables and data before sending them to OpenReplay.
```js
const trackerLink = tracker.use(
createTrackerLink((variables) => {
return {
...variables,
password: '***',
};
}),
);
```
Alternatively you can use generic graphql handler:
@ -93,18 +119,21 @@ Alternatively you can use generic graphql handler:
import { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; // see above for recordGraphQL definition
import { ApolloLink } from 'apollo-link';
const handler = createGraphqlMiddleware(tracker)
const handler = tracker.use(createGraphqlMiddleware());
const trackerApolloLink = new ApolloLink((operation, forward) => {
return forward(operation).map(result =>
handler(
operation.setContext({ start: performance.now() });
return forward(operation).map((result) => {
const time = performance.now() - operation.getContext().start;
return handler(
// op kind, name, variables, response, duration (default 0)
operation.query.definitions[0].operation,
operation.operationName,
operation.variables,
result,
),
);
time,
);
});
});
const link = ApolloLink.from([

View file

@ -1,5 +1,6 @@
import { App, Messages } from '@openreplay/tracker';
import Observable from 'zen-observable';
import { Sanitizer } from './types';
type Operation = {
query: Record<string, any>;
@ -9,48 +10,63 @@ type Operation = {
};
type NextLink = (operation: Operation) => Observable<Record<string, any>>;
export const createTrackerLink = (app: App | null) => {
if (!app) {
return (operation: Operation, forward: NextLink) => forward(operation);
}
return (operation: Operation, forward: NextLink) => {
return new Observable((observer) => {
const start = app.timestamp();
const observable = forward(operation);
const subscription = observable.subscribe({
next(value) {
const end = app.timestamp();
app.send(
Messages.GraphQL(
operation.query.definitions[0].kind,
operation.operationName,
JSON.stringify(operation.variables),
JSON.stringify(value.data),
end - start,
),
);
observer.next(value);
},
error(error) {
const end = app.timestamp();
app.send(
Messages.GraphQL(
operation.query.definitions[0].kind,
operation.operationName,
JSON.stringify(operation.variables),
JSON.stringify(error),
end - start,
),
);
observer.error(error);
},
complete() {
observer.complete();
},
});
export const createTrackerLink = (
sanitizer?: Sanitizer<Record<string, any> | undefined | null>,
) => {
return (app: App | null) => {
if (!app) {
return (operation: Operation, forward: NextLink) => forward(operation);
}
return (operation: Operation, forward: NextLink) => {
return new Observable((observer) => {
const start = app.timestamp();
const observable = forward(operation);
const subscription = observable.subscribe({
next(value) {
const end = app.timestamp();
const operationDefinition = operation.query.definitions[0];
app.send(
Messages.GraphQL(
operationDefinition.kind === 'OperationDefinition'
? operationDefinition.operation
: 'unknown?',
operation.operationName,
JSON.stringify(
sanitizer
? sanitizer(operation.variables)
: operation.variables,
),
JSON.stringify(sanitizer ? sanitizer(value.data) : value.data),
end - start,
),
);
observer.next(value);
},
error(error) {
const end = app.timestamp();
app.send(
Messages.GraphQL(
operation.query.definitions[0].kind,
operation.operationName,
JSON.stringify(
sanitizer
? sanitizer(operation.variables)
: operation.variables,
),
JSON.stringify(error),
end - start,
),
);
observer.error(error);
},
complete() {
observer.complete();
},
});
return () => subscription.unsubscribe();
});
return () => subscription.unsubscribe();
});
};
};
};

View file

@ -1,4 +1,4 @@
import { App, Messages } from "@openreplay/tracker";
import { App, Messages } from '@openreplay/tracker';
function createGraphqlMiddleware() {
return (app: App | null) => {
@ -10,7 +10,7 @@ function createGraphqlMiddleware() {
operationName: string,
variables: any,
result: any,
duration = 0
duration = 0,
) => {
try {
app.send(
@ -30,4 +30,4 @@ function createGraphqlMiddleware() {
};
}
export default createGraphqlMiddleware
export default createGraphqlMiddleware;

View file

@ -1,9 +1,11 @@
import createTrackerLink from './apolloMiddleware.js';
import createRelayMiddleware from './relayMiddleware.js';
import createGraphqlMiddleware from './graphqlMiddleware.js';
import { Sanitizer } from './types.js';
export {
createTrackerLink,
createRelayMiddleware,
createGraphqlMiddleware,
}
Sanitizer,
};

View file

@ -1,37 +1,55 @@
import { App, Messages } from '@openreplay/tracker';
import type { Middleware, RelayRequest } from './relaytypes';
import { Sanitizer } from './types';
const createRelayMiddleware = (app: App | null): Middleware => {
if (!app) {
return (next) => async (req) => await next(req);
}
return (next) => async (req) => {
const start = app.timestamp();
const resp = await next(req)
const end = app.timestamp();
if ('requests' in req) {
req.requests.forEach((request) => {
app.send(getMessage(request, resp.json as Record<string, any>, end - start))
})
} else {
app.send(getMessage(req, resp.json as Record<string, any>, end - start))
const createRelayMiddleware = (sanitizer?: Sanitizer<Record<string, any>>) => {
return (app: App | null): Middleware => {
if (!app) {
return (next) => async (req) => await next(req);
}
return resp;
}
return (next) => async (req) => {
const start = app.timestamp();
const resp = await next(req);
const end = app.timestamp();
if ('requests' in req) {
req.requests.forEach((request) => {
app.send(
getMessage(
request,
resp.json as Record<string, any>,
end - start,
sanitizer,
),
);
});
} else {
app.send(
getMessage(
req,
resp.json as Record<string, any>,
end - start,
sanitizer,
),
);
}
return resp;
};
};
};
function getMessage(request: RelayRequest, json: Record<string, any>, duration: number) {
function getMessage(
request: RelayRequest,
json: Record<string, any>,
duration: number,
sanitizer?: Sanitizer<Record<string, any>>,
) {
const opKind = request.operation.kind;
const opName = request.operation.name;
const vars = JSON.stringify(request.variables)
const opResp = JSON.stringify(json)
return Messages.GraphQL(
opKind,
opName,
vars,
opResp,
duration
)
const vars = JSON.stringify(
sanitizer ? sanitizer(request.variables) : request.variables,
);
const opResp = JSON.stringify(sanitizer ? sanitizer(json) : json);
return Messages.GraphQL(opKind, opName, vars, opResp, duration);
}
export default createRelayMiddleware
export default createRelayMiddleware;

View file

@ -1,4 +1,3 @@
type ConcreteBatch = {
kind: 'Batch';
fragment: any;
@ -9,7 +8,7 @@ type ConcreteBatch = {
text: string | null;
operationKind: string;
};
type Variables = { [name: string]: any };
export type Variables = { [name: string]: any };
interface FetchOpts {
url?: string;
method: 'POST' | 'GET';
@ -17,7 +16,13 @@ interface FetchOpts {
body: string | FormData;
credentials?: 'same-origin' | 'include' | 'omit';
mode?: 'cors' | 'websocket' | 'navigate' | 'no-cors' | 'same-origin';
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
cache?:
| 'default'
| 'no-store'
| 'reload'
| 'no-cache'
| 'force-cache'
| 'only-if-cached';
redirect?: 'follow' | 'error' | 'manual';
signal?: AbortSignal;
[name: string]: any;

View file

@ -0,0 +1 @@
export type Sanitizer<T> = (values: T) => Partial<T>;

View file

@ -963,8 +963,8 @@ export default class App {
deviceMemory,
jsHeapSizeLimit,
timezone: getTimezone(),
width: window.innerWidth,
height: window.innerHeight,
width: window.screen.width,
height: window.screen.height,
}),
})
const {
@ -1220,7 +1220,9 @@ export default class App {
timezone: getTimezone(),
condition: conditionName,
assistOnly: startOpts.assistOnly ?? this.socketMode,
}),
width: window.screen.width,
height: window.screen.height
}),
})
if (r.status !== 200) {
const error = await r.text()