Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4af13b64af | ||
|
|
d52dec3188 | ||
|
|
b4d060476a | ||
|
|
e40f095249 | ||
|
|
67063363a8 | ||
|
|
2b0b5d6106 | ||
|
|
64a746873b | ||
|
|
139f0e68c4 | ||
|
|
69e8fab2cc | ||
|
|
9db416dcde | ||
|
|
f28a7fbcfb | ||
|
|
6e2a772e7f | ||
|
|
dcce3569fb | ||
|
|
867247dbc0 | ||
|
|
754293e29d | ||
|
|
ddd037ce79 | ||
|
|
66f4c5c93b | ||
|
|
66e4d133ad | ||
|
|
f9f8853ab0 | ||
|
|
e0bb6fea9d | ||
|
|
4e7efaecde | ||
|
|
54a9624332 | ||
|
|
1ddffca572 | ||
|
|
c91881413a | ||
|
|
ba2d9eb81c | ||
|
|
c845415e1e | ||
|
|
ee0ede8478 | ||
|
|
72afae226b | ||
|
|
b3f545849a | ||
|
|
cd2966fb9f | ||
|
|
4b91dcded0 |
80 changed files with 1407 additions and 392 deletions
10
README_AR.md
10
README_AR.md
|
|
@ -55,17 +55,17 @@ OpenReplay هو مجموعة إعادة تشغيل الجلسة التي يمك
|
||||||
## الميزات
|
## الميزات
|
||||||
|
|
||||||
- **إعادة تشغيل الجلسة:** تتيح لك إعادة تشغيل الجلسة إعادة عيش تجربة مستخدميك، ورؤية أين يواجهون صعوبة وكيف يؤثر ذلك على سلوكهم. يتم تحليل كل إعادة تشغيل للجلسة تلقائيًا بناءً على الأساليب الاستدلالية، لسهولة التقييم.
|
- **إعادة تشغيل الجلسة:** تتيح لك إعادة تشغيل الجلسة إعادة عيش تجربة مستخدميك، ورؤية أين يواجهون صعوبة وكيف يؤثر ذلك على سلوكهم. يتم تحليل كل إعادة تشغيل للجلسة تلقائيًا بناءً على الأساليب الاستدلالية، لسهولة التقييم.
|
||||||
- **أدوات التطوير (DevTools):** إنها مثل التصحيح في متصفحك الخاص. يوفر لك OpenReplay السياق الكامل (نشاط الشبكة، أخطاء JavaScript، إجراءات/حالة التخزين وأكثر من 40 مقياسًا) حتى تتمكن من إعادة إنتاج الأخطاء فورًا وفهم مشكلات الأداء.
|
- **أدوات التطوير (DevTools):** إنها مثل المصحح (debugger) في متصفحك الخاص. يوفر لك OpenReplay السياق الكامل (نشاط الشبكة، أخطاء JavaScript، إجراءات/حالة التخزين وأكثر من 40 مقياسًا) حتى تتمكن من إعادة إنتاج الأخطاء فورًا وفهم مشكلات الأداء.
|
||||||
- **المساعدة (Assist):** تساعدك في دعم مستخدميك من خلال رؤية شاشتهم مباشرة والانضمام فورًا إلى مكالمة (WebRTC) معهم دون الحاجة إلى برامج مشاركة الشاشة من جهات خارجية.
|
- **المساعدة (Assist):** تساعدك في دعم مستخدميك من خلال رؤية شاشتهم مباشرة والانضمام فورًا إلى مكالمة (WebRTC) معهم دون الحاجة إلى برامج مشاركة الشاشة من جهات خارجية.
|
||||||
- **البحث الشامل (Omni-search):** ابحث وفرز حسب أي عملية/معيار للمستخدم تقريبًا، أو سمة الجلسة أو الحدث التقني، حتى تتمكن من الرد على أي سؤال. لا يلزم تجهيز.
|
- **البحث الشامل (Omni-search):** ابحث وافرز حسب أي عملية/معيار للمستخدم تقريبًا، أو سمة الجلسة أو الحدث التقني، حتى تتمكن من الرد على أي سؤال. لا يلزم تجهيز.
|
||||||
- **الأنفاق (Funnels):** للكشف عن المشكلات الأكثر تأثيرًا التي تسبب في فقدان التحويل والإيرادات.
|
- **الأنفاق (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 والمزيد.
|
- **التكاملات (Integrations):** مزامنة سجلات الخادم الخلفي مع إعادات التشغيل للجلسات ورؤية ما حدث من الأمام إلى الخلف. يدعم OpenReplay Sentry وDatadog وCloudWatch وStackdriver وElastic والمزيد.
|
||||||
|
|
||||||
## خيارات النشر
|
## خيارات النشر
|
||||||
|
|
||||||
يمكن نشر OpenReplay في أي مكان. اتبع دليلنا الخطوة بالخطوة لنشره على خدمات السحابة العامة الرئيسية:
|
يمكن نشر OpenReplay في أي مكان. اتبع دليلنا خطوة بخطوة لنشره على خدمات السحابة العامة الرئيسة:
|
||||||
|
|
||||||
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
|
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
|
||||||
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)
|
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ def __search_errors_mobile(project_id, value, key=None, source=None):
|
||||||
query = f"""(SELECT DISTINCT ON(lg.reason)
|
query = f"""(SELECT DISTINCT ON(lg.reason)
|
||||||
lg.reason AS value,
|
lg.reason AS value,
|
||||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
'{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
|
WHERE
|
||||||
s.project_id = %(project_id)s
|
s.project_id = %(project_id)s
|
||||||
AND lg.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)
|
(SELECT DISTINCT ON(lg.name)
|
||||||
lg.name AS value,
|
lg.name AS value,
|
||||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
'{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
|
WHERE
|
||||||
s.project_id = %(project_id)s
|
s.project_id = %(project_id)s
|
||||||
AND lg.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)
|
(SELECT DISTINCT ON(lg.reason)
|
||||||
lg.reason AS value,
|
lg.reason AS value,
|
||||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
'{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
|
WHERE
|
||||||
s.project_id = %(project_id)s
|
s.project_id = %(project_id)s
|
||||||
AND lg.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)
|
(SELECT DISTINCT ON(lg.name)
|
||||||
lg.name AS value,
|
lg.name AS value,
|
||||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
'{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
|
WHERE
|
||||||
s.project_id = %(project_id)s
|
s.project_id = %(project_id)s
|
||||||
AND lg.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)
|
query = f"""(SELECT DISTINCT ON(lg.reason)
|
||||||
lg.reason AS value,
|
lg.reason AS value,
|
||||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
'{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
|
WHERE
|
||||||
s.project_id = %(project_id)s
|
s.project_id = %(project_id)s
|
||||||
AND lg.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)
|
(SELECT DISTINCT ON(lg.name)
|
||||||
lg.name AS value,
|
lg.name AS value,
|
||||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
'{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
|
WHERE
|
||||||
s.project_id = %(project_id)s
|
s.project_id = %(project_id)s
|
||||||
AND lg.project_id = %(project_id)s
|
AND lg.project_id = %(project_id)s
|
||||||
|
|
|
||||||
|
|
@ -319,13 +319,14 @@ def create_card(project_id, user_id, data: schemas.CardSchema, dashboard=False):
|
||||||
session_data = None
|
session_data = None
|
||||||
if data.metric_type == schemas.MetricType.heat_map:
|
if data.metric_type == schemas.MetricType.heat_map:
|
||||||
if data.session_id is not None:
|
if data.session_id is not None:
|
||||||
session_data = json.dumps({"sessionId": data.session_id})
|
session_data = {"sessionId": data.session_id}
|
||||||
else:
|
else:
|
||||||
session_data = __get_heat_map_chart(project_id=project_id, user_id=user_id,
|
session_data = __get_heat_map_chart(project_id=project_id, user_id=user_id,
|
||||||
data=data, include_mobs=False)
|
data=data, include_mobs=False)
|
||||||
if session_data is not None:
|
if session_data is not None:
|
||||||
session_data = json.dumps({"sessionId": session_data["sessionId"]})
|
session_data = {"sessionId": session_data["sessionId"]}
|
||||||
_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 i, s in enumerate(data.series):
|
||||||
for k in s.model_dump().keys():
|
for k in s.model_dump().keys():
|
||||||
_data[f"{k}_{i}"] = s.__getattribute__(k)
|
_data[f"{k}_{i}"] = s.__getattribute__(k)
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ class EventType:
|
||||||
CUSTOM_MOBILE = Event(ui_type=schemas.EventType.custom_mobile, table="events_common.customs", column="name")
|
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")
|
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",
|
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 = {
|
SUPPORTED_TYPES = {
|
||||||
|
|
@ -163,22 +163,25 @@ SUPPORTED_TYPES = {
|
||||||
query=None),
|
query=None),
|
||||||
# MOBILE
|
# MOBILE
|
||||||
EventType.CLICK_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CLICK_MOBILE),
|
EventType.CLICK_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.CLICK_MOBILE),
|
||||||
query=autocomplete.__generic_query(
|
query=autocomplete.__generic_query(
|
||||||
typename=EventType.CLICK_MOBILE.ui_type)),
|
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),
|
EventType.INPUT_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.INPUT_MOBILE),
|
||||||
query=autocomplete.__generic_query(
|
query=autocomplete.__generic_query(
|
||||||
typename=EventType.INPUT_MOBILE.ui_type)),
|
typename=EventType.INPUT_MOBILE.ui_type)),
|
||||||
EventType.VIEW_MOBILE.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.VIEW_MOBILE),
|
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(
|
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,
|
EventType.CRASH_MOBILE.ui_type: SupportedFilter(get=autocomplete.__search_errors_mobile,
|
||||||
query=None),
|
query=None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -359,12 +359,12 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
distinct_on += ",path"
|
distinct_on += ",path"
|
||||||
if metric_format == schemas.MetricExtendedFormatType.session_count:
|
if metric_format == schemas.MetricExtendedFormatType.session_count:
|
||||||
main_query = f"""SELECT COUNT(*) AS 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)
|
COALESCE(JSONB_AGG(users_sessions)
|
||||||
FILTER ( WHERE rn > %(limit_s)s
|
FILTER ( WHERE rn > %(limit_s)s
|
||||||
AND rn <= %(limit_e)s ), '[]'::JSONB) AS values
|
AND rn <= %(limit_e)s ), '[]'::JSONB) AS values
|
||||||
FROM (SELECT {main_col} AS name,
|
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
|
ROW_NUMBER() OVER (ORDER BY count(full_sessions) DESC) AS rn
|
||||||
FROM (SELECT *
|
FROM (SELECT *
|
||||||
FROM (SELECT DISTINCT ON({distinct_on}) s.session_id, s.user_uuid,
|
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;"""
|
ORDER BY session_count DESC) AS users_sessions;"""
|
||||||
else:
|
else:
|
||||||
main_query = f"""SELECT COUNT(*) AS count,
|
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
|
COALESCE(JSONB_AGG(users_sessions) FILTER ( WHERE rn <= 200 ), '[]'::JSONB) AS values
|
||||||
FROM (SELECT {main_col} AS name,
|
FROM (SELECT {main_col} AS name,
|
||||||
count(DISTINCT user_id) AS user_count,
|
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_s"] = (data.page - 1) * data.limit
|
||||||
full_args["issues_limit_e"] = data.page * data.limit
|
full_args["issues_limit_e"] = data.page * data.limit
|
||||||
main_query = cur.mogrify(f"""SELECT COUNT(1) AS count,
|
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)
|
COALESCE(JSONB_AGG(ranked_issues)
|
||||||
FILTER ( WHERE rn > %(issues_limit_s)s
|
FILTER ( WHERE rn > %(issues_limit_s)s
|
||||||
AND rn <= %(issues_limit_e)s ), '[]'::JSONB) AS values
|
AND rn <= %(issues_limit_e)s ), '[]'::JSONB) AS values
|
||||||
FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY session_count DESC) AS rn
|
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
|
FROM (SELECT session_id
|
||||||
{query_part}) AS filtered_sessions
|
{query_part}) AS filtered_sessions
|
||||||
INNER JOIN events_common.issues USING (session_id)
|
INNER JOIN events_common.issues USING (session_id)
|
||||||
|
|
@ -814,12 +814,6 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
|
||||||
event_where.append(
|
event_where.append(
|
||||||
sh.multi_conditions(f"main.{events.EventType.VIEW_MOBILE.column} {op} %({e_k})s",
|
sh.multi_conditions(f"main.{events.EventType.VIEW_MOBILE.column} {op} %({e_k})s",
|
||||||
event.value, value_key=e_k))
|
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:
|
elif event_type == events.EventType.CUSTOM.ui_type:
|
||||||
event_from = event_from % f"{events.EventType.CUSTOM.table} AS main "
|
event_from = event_from % f"{events.EventType.CUSTOM.table} AS main "
|
||||||
if not is_any:
|
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))
|
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:
|
elif event_type == events.EventType.CLICK_MOBILE.ui_type:
|
||||||
event_from = event_from % f"{events.EventType.CLICK_MOBILE.table} AS main "
|
event_from = event_from % f"{events.EventType.CLICK_MOBILE.table} AS main "
|
||||||
if not is_any:
|
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",
|
sh.multi_conditions(f"main.{events.EventType.REQUEST_MOBILE.column} {op} %({e_k})s",
|
||||||
event.value, value_key=e_k))
|
event.value, value_key=e_k))
|
||||||
elif event_type == events.EventType.CRASH_MOBILE.ui_type:
|
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, "*", ""]:
|
if not is_any and event.value not in [None, "*", ""]:
|
||||||
event_where.append(
|
event_where.append(
|
||||||
sh.multi_conditions(f"(main1.reason {op} %({e_k})s OR main1.name {op} %({e_k})s)",
|
sh.multi_conditions(f"(main1.reason {op} %({e_k})s OR main1.name {op} %({e_k})s)",
|
||||||
event.value, value_key=e_k))
|
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:
|
elif event_type == schemas.PerformanceEventType.fetch_failed:
|
||||||
event_from = event_from % f"{events.EventType.REQUEST.table} AS main "
|
event_from = event_from % f"{events.EventType.REQUEST.table} AS main "
|
||||||
if not is_any:
|
if not is_any:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from routers.base import get_routers
|
||||||
public_app, app, app_apikey = get_routers()
|
public_app, app, app_apikey = get_routers()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/{projectId}/autocomplete', tags=["events"])
|
||||||
@app.get('/{projectId}/events/search', tags=["events"])
|
@app.get('/{projectId}/events/search', tags=["events"])
|
||||||
def events_search(projectId: int, q: str,
|
def events_search(projectId: int, q: str,
|
||||||
type: Union[schemas.FilterType, schemas.EventType,
|
type: Union[schemas.FilterType, schemas.EventType,
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@ def transform_old_filter_type(cls, values):
|
||||||
"GRAPHQL": EventType.graphql.value,
|
"GRAPHQL": EventType.graphql.value,
|
||||||
"STATEACTION": EventType.state_action.value,
|
"STATEACTION": EventType.state_action.value,
|
||||||
"ERROR": EventType.error.value,
|
"ERROR": EventType.error.value,
|
||||||
"CLICK_MOBILE": EventType.click_mobile.value,
|
"CLICK_IOS": EventType.click_mobile.value,
|
||||||
"INPUT_MOBILE": EventType.input_mobile.value,
|
"INPUT_IOS": EventType.input_mobile.value,
|
||||||
"VIEW_MOBILE": EventType.view_mobile.value,
|
"VIEW_IOS": EventType.view_mobile.value,
|
||||||
"CUSTOM_MOBILE": EventType.custom_mobile.value,
|
"CUSTOM_IOS": EventType.custom_mobile.value,
|
||||||
"REQUEST_MOBILE": EventType.request_mobile.value,
|
"REQUEST_IOS": EventType.request_mobile.value,
|
||||||
"ERROR_MOBILE": EventType.error_mobile.value,
|
"ERROR_IOS": EventType.error_mobile.value,
|
||||||
"DOM_COMPLETE": PerformanceEventType.location_dom_complete.value,
|
"DOM_COMPLETE": PerformanceEventType.location_dom_complete.value,
|
||||||
"LARGEST_CONTENTFUL_PAINT_TIME": PerformanceEventType.location_largest_contentful_paint_time.value,
|
"LARGEST_CONTENTFUL_PAINT_TIME": PerformanceEventType.location_largest_contentful_paint_time.value,
|
||||||
"TTFB": PerformanceEventType.location_ttfb.value,
|
"TTFB": PerformanceEventType.location_ttfb.value,
|
||||||
|
|
@ -471,13 +471,13 @@ class EventType(str, Enum):
|
||||||
state_action = "stateAction"
|
state_action = "stateAction"
|
||||||
error = "error"
|
error = "error"
|
||||||
tag = "tag"
|
tag = "tag"
|
||||||
click_mobile = "tapIos"
|
click_mobile = "clickMobile"
|
||||||
input_mobile = "inputIos"
|
input_mobile = "inputMobile"
|
||||||
view_mobile = "viewIos"
|
view_mobile = "viewMobile"
|
||||||
custom_mobile = "customIos"
|
custom_mobile = "customMobile"
|
||||||
request_mobile = "requestIos"
|
request_mobile = "requestMobile"
|
||||||
error_mobile = "errorIos"
|
error_mobile = "errorMobile"
|
||||||
swipe_mobile = "swipeIos"
|
swipe_mobile = "swipeMobile"
|
||||||
|
|
||||||
|
|
||||||
class PerformanceEventType(str, Enum):
|
class PerformanceEventType(str, Enum):
|
||||||
|
|
@ -1459,7 +1459,7 @@ class LiveSessionSearchFilterSchema(BaseModel):
|
||||||
operator: Literal[SearchEventOperator._is, \
|
operator: Literal[SearchEventOperator._is, \
|
||||||
SearchEventOperator._contains] = Field(default=SearchEventOperator._contains)
|
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')
|
@model_validator(mode='after')
|
||||||
def __validator(cls, values):
|
def __validator(cls, values):
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,13 @@ func (s *saverImpl) handleMobileMessage(msg Message) error {
|
||||||
if err = s.sessions.UpdateUserID(session.SessionID, m.ID); err != nil {
|
if err = s.sessions.UpdateUserID(session.SessionID, m.ID); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
case *MobileUserAnonymousID:
|
case *MobileUserAnonymousID:
|
||||||
if err = s.sessions.UpdateAnonymousID(session.SessionID, m.ID); err != nil {
|
if err = s.sessions.UpdateAnonymousID(session.SessionID, m.ID); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
case *MobileMetadata:
|
case *MobileMetadata:
|
||||||
return s.sessions.UpdateMetadata(m.SessionID(), m.Key, m.Value)
|
return s.sessions.UpdateMetadata(m.SessionID(), m.Key, m.Value)
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,15 @@ func (conn *Conn) InsertWebClickEvent(sess *sessions.Session, e *messages.MouseC
|
||||||
}
|
}
|
||||||
var host, path string
|
var host, path string
|
||||||
host, path, _, _ = url.GetURLParts(e.Url)
|
host, path, _, _ = url.GetURLParts(e.Url)
|
||||||
if e.NormalizedX <= 100 && e.NormalizedY <= 100 {
|
if e.NormalizedX != 101 && e.NormalizedY != 101 {
|
||||||
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 {
|
// 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)
|
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
|
||||||
conn.log.Error(sessCtx, "insert web click event in bulk err: %s", err)
|
conn.log.Error(sessCtx, "insert web click event in bulk err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
if err := conn.InsertCustomEvent(session.SessionID, e.Timestamp, truncSqIdx(e.Index), e.Name, e.Payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.InsertAutocompleteValue(session.SessionID, session.ProjectID, "CUSTOM_Mobile", e.Name)
|
conn.InsertAutocompleteValue(session.SessionID, session.ProjectID, "CUSTOMMOBILE", e.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) InsertMobileNetworkCall(sess *sessions.Session, e *messages.MobileNetworkCall) error {
|
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)
|
err := conn.InsertRequest(sess.SessionID, e.Timestamp, truncSqIdx(e.Index), e.URL, e.Duration, e.Status < 400)
|
||||||
if err == nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ func (conn *Conn) InsertMobileClickEvent(sess *sessions.Session, clickEvent *mes
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "CLICK_Mobile", clickEvent.Label)
|
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "CLICKMOBILE", clickEvent.Label)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (conn *Conn) InsertMobileSwipeEvent(sess *sessions.Session, swipeEvent *mes
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "SWIPE_Mobile", swipeEvent.Label)
|
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "SWIPEMOBILE", swipeEvent.Label)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ func (conn *Conn) InsertMobileInputEvent(sess *sessions.Session, inputEvent *mes
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "INPUT_Mobile", inputEvent.Label)
|
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "INPUTMOBILE", inputEvent.Label)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ def _get_current_auth_context(request: Request, jwt_payload: dict) -> schemas.Cu
|
||||||
return request.state.currentContext
|
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):
|
class JWTAuth(HTTPBearer):
|
||||||
def __init__(self, auto_error: bool = True):
|
def __init__(self, auto_error: bool = True):
|
||||||
super(JWTAuth, self).__init__(auto_error=auto_error)
|
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"):
|
or old_jwt_payload.get("userId") != jwt_payload.get("userId"):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.")
|
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:
|
else:
|
||||||
credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request)
|
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.")
|
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.")
|
logger.warning("Invalid authorization code.")
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.")
|
||||||
|
|
|
||||||
|
|
@ -339,10 +339,13 @@ def create_card(project_id, user_id, data: schemas.CardSchema, dashboard=False):
|
||||||
session_data = None
|
session_data = None
|
||||||
if data.metric_type == schemas.MetricType.heat_map:
|
if data.metric_type == schemas.MetricType.heat_map:
|
||||||
if data.session_id is not None:
|
if data.session_id is not None:
|
||||||
session_data = json.dumps({"sessionId": data.session_id})
|
session_data = {"sessionId": data.session_id}
|
||||||
else:
|
else:
|
||||||
session_data = __get_heat_map_chart(project_id=project_id, user_id=user_id,
|
session_data = __get_heat_map_chart(project_id=project_id, user_id=user_id,
|
||||||
data=data, include_mobs=False)
|
data=data, include_mobs=False)
|
||||||
|
if session_data is not None:
|
||||||
|
session_data = {"sessionId": session_data["sessionId"]}
|
||||||
|
|
||||||
if session_data is not None:
|
if session_data is not None:
|
||||||
# for EE only
|
# for EE only
|
||||||
keys = sessions_mobs. \
|
keys = sessions_mobs. \
|
||||||
|
|
@ -356,8 +359,8 @@ def create_card(project_id, user_id, data: schemas.CardSchema, dashboard=False):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"!!!Error while tagging: {k} to {tag} for heatMap")
|
logger.warning(f"!!!Error while tagging: {k} to {tag} for heatMap")
|
||||||
logger.error(str(e))
|
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 i, s in enumerate(data.series):
|
||||||
for k in s.model_dump().keys():
|
for k in s.model_dump().keys():
|
||||||
_data[f"{k}_{i}"] = s.__getattribute__(k)
|
_data[f"{k}_{i}"] = s.__getattribute__(k)
|
||||||
|
|
|
||||||
|
|
@ -57,16 +57,16 @@ def get_by_url(project_id, data: schemas.GetHeatMapPayloadSchema):
|
||||||
# f.value, value_key=f_k))
|
# f.value, value_key=f_k))
|
||||||
|
|
||||||
if data.click_rage and not has_click_rage_filter:
|
if data.click_rage and not has_click_rage_filter:
|
||||||
constraints.append("""(issues.session_id IS NULL
|
constraints.append("""(issues_t.session_id IS NULL
|
||||||
OR (issues.datetime >= toDateTime(%(startDate)s/1000)
|
OR (issues_t.datetime >= toDateTime(%(startDate)s/1000)
|
||||||
AND issues.datetime <= toDateTime(%(endDate)s/1000)
|
AND issues_t.datetime <= toDateTime(%(endDate)s/1000)
|
||||||
AND issues.project_id = toUInt16(%(project_id)s)
|
AND issues_t.project_id = toUInt16(%(project_id)s)
|
||||||
AND issues.event_type = 'ISSUE'
|
AND issues_t.event_type = 'ISSUE'
|
||||||
AND issues.project_id = toUInt16(%(project_id)s
|
AND issues_t.project_id = toUInt16(%(project_id)s)
|
||||||
AND mis.project_id = toUInt16(%(project_id)s
|
AND mis.project_id = toUInt16(%(project_id)s)
|
||||||
AND mis.type='click_rage'))))""")
|
AND mis.type='click_rage'))""")
|
||||||
query_from += """ LEFT JOIN experimental.events AS issues ON (main_events.session_id=issues.session_id)
|
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.issue_id=mis.issue_id)"""
|
LEFT JOIN experimental.issues AS mis ON (issues_t.issue_id=mis.issue_id)"""
|
||||||
with ch_client.ClickHouseClient() as cur:
|
with ch_client.ClickHouseClient() as cur:
|
||||||
query = cur.format(f"""SELECT main_events.normalized_x AS normalized_x,
|
query = cur.format(f"""SELECT main_events.normalized_x AS normalized_x,
|
||||||
main_events.normalized_y AS normalized_y
|
main_events.normalized_y AS normalized_y
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,8 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
if metric_format == schemas.MetricExtendedFormatType.session_count:
|
if metric_format == schemas.MetricExtendedFormatType.session_count:
|
||||||
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
||||||
{main_col} AS name,
|
{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,
|
FROM (SELECT s.session_id AS session_id,
|
||||||
{extra_col}
|
{extra_col}
|
||||||
{query_part}) AS filtred_sessions
|
{query_part}) AS filtred_sessions
|
||||||
|
|
@ -470,11 +471,14 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
logging.debug("--------------------")
|
logging.debug("--------------------")
|
||||||
sessions = cur.execute(main_query)
|
sessions = cur.execute(main_query)
|
||||||
count = 0
|
count = 0
|
||||||
|
total_sessions = 0
|
||||||
if len(sessions) > 0:
|
if len(sessions) > 0:
|
||||||
count = sessions[0]["main_count"]
|
count = sessions[0]["main_count"]
|
||||||
|
total_sessions = sessions[0]["total_sessions"]
|
||||||
for s in sessions:
|
for s in sessions:
|
||||||
s.pop("main_count")
|
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
|
return sessions
|
||||||
|
|
||||||
|
|
@ -520,7 +524,7 @@ def search_table_of_individual_issues(data: schemas.SessionsSearchPayloadSchema,
|
||||||
total_sessions = 0
|
total_sessions = 0
|
||||||
issues_count = 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):
|
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.PerformanceEventType.fetch_failed: "REQUEST",
|
||||||
schemas.EventType.error: "CRASH",
|
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)
|
return defs_mobile.get(event_type)
|
||||||
if event_type not in defs:
|
if event_type not in defs:
|
||||||
raise Exception(f"unsupported EventType:{event_type}")
|
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}"))
|
value_key=f"custom{i}"))
|
||||||
full_args = {**full_args, **_multiple_values(event.source, value_key=f"custom{i}")}
|
full_args = {**full_args, **_multiple_values(event.source, value_key=f"custom{i}")}
|
||||||
else:
|
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)}'")
|
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||||
events_conditions.append({"type": event_where[-1]})
|
events_conditions.append({"type": event_where[-1]})
|
||||||
if not is_any:
|
if not is_any:
|
||||||
|
|
@ -997,7 +1001,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
|
||||||
event.value, value_key=e_k))
|
event.value, value_key=e_k))
|
||||||
events_conditions[-1]["condition"] = event_where[-1]
|
events_conditions[-1]["condition"] = event_where[-1]
|
||||||
else:
|
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)}'")
|
event_where.append(f"main.event_type='{__get_event_type(event_type, platform=platform)}'")
|
||||||
events_conditions.append({"type": event_where[-1]})
|
events_conditions.append({"type": event_where[-1]})
|
||||||
if not is_any:
|
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"])
|
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:
|
elif event_type == schemas.PerformanceEventType.fetch_failed:
|
||||||
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
event_from = event_from % f"{MAIN_EVENTS_TABLE} AS main "
|
||||||
_column = 'url_path'
|
_column = 'url_path'
|
||||||
|
|
|
||||||
|
|
@ -185,8 +185,9 @@ def __filter_subquery(project_id: int, filters: Optional[schemas.SessionsSearchP
|
||||||
errors_only=True, favorite_only=None,
|
errors_only=True, favorite_only=None,
|
||||||
issue=None, user_id=None)
|
issue=None, user_id=None)
|
||||||
params = {**params, **qp_params}
|
params = {**params, **qp_params}
|
||||||
# TODO: test if this line impacts other cards beside insights
|
# This line was added because insights is failing when you add filter steps,
|
||||||
# sub_query = f"INNER JOIN {sub_query} USING(session_id)"
|
# for example when you add a LOCATION filter
|
||||||
|
sub_query = f"INNER JOIN {sub_query} USING(session_id)"
|
||||||
return params, sub_query
|
return params, sub_query
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -397,12 +397,19 @@ func (c *connectorImpl) InsertWebClickEvent(session *sessions.Session, msg *mess
|
||||||
if msg.Label == "" {
|
if msg.Label == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var nX *uint8 = nil
|
var nX *float32 = nil
|
||||||
var nY *uint8 = nil
|
var nY *float32 = nil
|
||||||
if msg.NormalizedX <= 100 && msg.NormalizedY <= 100 {
|
if msg.NormalizedX != 101 && msg.NormalizedY != 101 {
|
||||||
nXVal := uint8(msg.NormalizedX)
|
// 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
|
nX = &nXVal
|
||||||
nYVal := uint8(msg.NormalizedY)
|
nYVal := normalizedY
|
||||||
nY = &nYVal
|
nY = &nYVal
|
||||||
}
|
}
|
||||||
if err := c.batches["clicks"].Append(
|
if err := c.batches["clicks"].Append(
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.19.0-ee';
|
||||||
DROP TABLE IF EXISTS experimental.events_l7d_mv;
|
DROP TABLE IF EXISTS experimental.events_l7d_mv;
|
||||||
|
|
||||||
ALTER TABLE experimental.events
|
ALTER TABLE experimental.events
|
||||||
ADD COLUMN IF NOT EXISTS normalized_x Nullable(UInt8),
|
ADD COLUMN IF NOT EXISTS normalized_x Nullable(Float32),
|
||||||
ADD COLUMN IF NOT EXISTS normalized_y Nullable(UInt8),
|
ADD COLUMN IF NOT EXISTS normalized_y Nullable(Float32),
|
||||||
DROP COLUMN IF EXISTS coordinate;
|
DROP COLUMN IF EXISTS coordinate;
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS experimental.events_l7d_mv
|
CREATE MATERIALIZED VIEW IF NOT EXISTS experimental.events_l7d_mv
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,8 @@ CREATE TABLE IF NOT EXISTS experimental.events
|
||||||
error_tags_values Array(Nullable(String)),
|
error_tags_values Array(Nullable(String)),
|
||||||
transfer_size Nullable(UInt32),
|
transfer_size Nullable(UInt32),
|
||||||
selector Nullable(String),
|
selector Nullable(String),
|
||||||
normalized_x Nullable(UInt8),
|
normalized_x Nullable(Float32),
|
||||||
normalized_y Nullable(UInt8),
|
normalized_y Nullable(Float32),
|
||||||
message_id UInt64 DEFAULT 0,
|
message_id UInt64 DEFAULT 0,
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ $fn_def$, :'next_version')
|
||||||
|
|
||||||
--
|
--
|
||||||
ALTER TABLE IF EXISTS events.clicks
|
ALTER TABLE IF EXISTS events.clicks
|
||||||
ADD COLUMN IF NOT EXISTS normalized_x smallint NULL,
|
ADD COLUMN IF NOT EXISTS normalized_x decimal NULL,
|
||||||
ADD COLUMN IF NOT EXISTS normalized_y smallint NULL,
|
ADD COLUMN IF NOT EXISTS normalized_y decimal NULL,
|
||||||
DROP COLUMN IF EXISTS x,
|
DROP COLUMN IF EXISTS x,
|
||||||
DROP COLUMN IF EXISTS y;
|
DROP COLUMN IF EXISTS y;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -659,16 +659,16 @@ CREATE INDEX pages_query_nn_gin_idx ON events.pages USING GIN (query gin_trgm_op
|
||||||
|
|
||||||
CREATE TABLE events.clicks
|
CREATE TABLE events.clicks
|
||||||
(
|
(
|
||||||
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
|
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
|
||||||
message_id bigint NOT NULL,
|
message_id bigint NOT NULL,
|
||||||
timestamp bigint NOT NULL,
|
timestamp bigint NOT NULL,
|
||||||
label text DEFAULT NULL,
|
label text DEFAULT NULL,
|
||||||
url text DEFAULT '' NOT NULL,
|
url text DEFAULT '' NOT NULL,
|
||||||
path text,
|
path text,
|
||||||
selector text DEFAULT '' NOT NULL,
|
selector text DEFAULT '' NOT NULL,
|
||||||
hesitation integer DEFAULT NULL,
|
hesitation integer DEFAULT NULL,
|
||||||
normalized_x smallint DEFAULT NULL,
|
normalized_x decimal DEFAULT NULL,
|
||||||
normalized_y smallint DEFAULT NULL,
|
normalized_y decimal DEFAULT NULL,
|
||||||
PRIMARY KEY (session_id, message_id)
|
PRIMARY KEY (session_id, message_id)
|
||||||
);
|
);
|
||||||
CREATE INDEX clicks_session_id_idx ON events.clicks (session_id);
|
CREATE INDEX clicks_session_id_idx ON events.clicks (session_id);
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ function ClickMapCard({
|
||||||
if (mapUrl) return evt.path.includes(mapUrl)
|
if (mapUrl) return evt.path.includes(mapUrl)
|
||||||
return evt
|
return evt
|
||||||
}) || { timestamp: metricStore.instance.data.startTs }
|
}) || { timestamp: metricStore.instance.data.startTs }
|
||||||
|
const ts = jumpToEvent.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 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 (
|
return (
|
||||||
<div id="clickmap-render">
|
<div id="clickmap-render">
|
||||||
<ClickMapRenderer
|
<ClickMapRenderer
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
import ExampleFunnel from './Examples/Funnel';
|
import ExampleFunnel from './Examples/Funnel';
|
||||||
import ExamplePath from './Examples/Path';
|
import ExamplePath from './Examples/Path';
|
||||||
import ExampleTrend from './Examples/Trend';
|
import ExampleTrend from './Examples/Trend';
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,15 @@ interface NewDashboardModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
isAddingFromLibrary?: boolean;
|
isAddingFromLibrary?: boolean;
|
||||||
isEnterprise?: boolean;
|
isEnterprise?: boolean;
|
||||||
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
|
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
open,
|
open,
|
||||||
isAddingFromLibrary = false,
|
isAddingFromLibrary = false,
|
||||||
isEnterprise = false
|
isEnterprise = false,
|
||||||
|
isMobile = false
|
||||||
}) => {
|
}) => {
|
||||||
const [step, setStep] = React.useState<number>(0);
|
const [step, setStep] = React.useState<number>(0);
|
||||||
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
|
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
|
||||||
|
|
@ -53,6 +55,7 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
|
||||||
setSelectedCategory={setSelectedCategory}
|
setSelectedCategory={setSelectedCategory}
|
||||||
onCard={() => setStep(step + 1)}
|
onCard={() => setStep(step + 1)}
|
||||||
isLibrary={isAddingFromLibrary}
|
isLibrary={isAddingFromLibrary}
|
||||||
|
isMobile={isMobile}
|
||||||
isEnterprise={isEnterprise} />}
|
isEnterprise={isEnterprise} />}
|
||||||
{step === 1 && <CreateCard onBack={() => setStep(0)} />}
|
{step === 1 && <CreateCard onBack={() => setStep(0)} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,6 +66,7 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: any) => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
|
isMobile: state.getIn(['site', 'instance', 'platform']) === 'ios',
|
||||||
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' ||
|
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' ||
|
||||||
state.getIn(['user', 'account', 'edition']) === 'msaas'
|
state.getIn(['user', 'account', 'edition']) === 'msaas'
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useMemo, useState, useEffect } from 'react';
|
import React, { useMemo, useState, useEffect } from 'react';
|
||||||
import { Button, Input, Segmented, Space } from 'antd';
|
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 { ArrowRight, Info } from 'lucide-react';
|
||||||
import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards';
|
import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
|
|
@ -8,6 +8,7 @@ import Option from './Option';
|
||||||
import CardsLibrary from 'Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary';
|
import CardsLibrary from 'Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary';
|
||||||
import { FUNNEL } from 'App/constants/card';
|
import { FUNNEL } from 'App/constants/card';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import { FilterKey } from 'Types/filter/filterType';
|
||||||
|
|
||||||
interface SelectCardProps {
|
interface SelectCardProps {
|
||||||
onClose: (refresh?: boolean) => void;
|
onClose: (refresh?: boolean) => void;
|
||||||
|
|
@ -16,10 +17,11 @@ interface SelectCardProps {
|
||||||
selected?: string;
|
selected?: string;
|
||||||
setSelectedCategory?: React.Dispatch<React.SetStateAction<string>>;
|
setSelectedCategory?: React.Dispatch<React.SetStateAction<string>>;
|
||||||
isEnterprise?: boolean;
|
isEnterprise?: boolean;
|
||||||
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
|
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 [selectedCards, setSelectedCards] = React.useState<number[]>([]);
|
||||||
const { metricStore, dashboardStore } = useStore();
|
const { metricStore, dashboardStore } = useStore();
|
||||||
const siteId: string = location.pathname.split('/')[1];
|
const siteId: string = location.pathname.split('/')[1];
|
||||||
|
|
@ -74,20 +76,23 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardItems = useMemo(() => {
|
const cardItems = useMemo(() => {
|
||||||
return CARD_LIST.filter((card) => card.category === selected && (!card.isEnterprise || (card.isEnterprise && isEnterprise)))
|
return CARD_LIST.filter((card) =>
|
||||||
.map((card) => (
|
card.category === selected &&
|
||||||
<div key={card.key} className={card.width ? `col-span-${card.width}` : 'col-span-2'}>
|
(!card.isEnterprise || (card.isEnterprise && isEnterprise)) &&
|
||||||
<card.example
|
(!isMobile || (isMobile && ![FilterKey.USER_BROWSER].includes(card.key)))
|
||||||
onCard={handleCardSelection}
|
).map((card) => (
|
||||||
type={card.key}
|
<div key={card.key} className={card.width ? `col-span-${card.width}` : 'col-span-2'}>
|
||||||
title={card.title}
|
<card.example
|
||||||
data={card.data}
|
onCard={handleCardSelection}
|
||||||
height={card.height}
|
type={card.key}
|
||||||
hideLegend={card.data?.hideLegend}
|
title={card.title}
|
||||||
/>
|
data={card.data}
|
||||||
</div>
|
height={card.height}
|
||||||
));
|
hideLegend={card.data?.hideLegend}
|
||||||
}, [selected]);
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}, [selected, isEnterprise, isMobile]);
|
||||||
|
|
||||||
const onCardClick = (cardId: number) => {
|
const onCardClick = (cardId: number) => {
|
||||||
if (selectedCards.includes(cardId)) {
|
if (selectedCards.includes(cardId)) {
|
||||||
|
|
@ -119,7 +124,7 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isCreatingDashboard && (
|
{isCreatingDashboard && (
|
||||||
<Button type="link" onClick={createNewDashboard} loading={dashboardCreating} className='gap-2'>
|
<Button type="link" onClick={createNewDashboard} loading={dashboardCreating} className="gap-2">
|
||||||
<Space>
|
<Space>
|
||||||
Create Blank
|
Create Blank
|
||||||
<RightOutlined />
|
<RightOutlined />
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ function WebPlayer(props: any) {
|
||||||
const isPlayerReady = contextValue.store?.get().ready
|
const isPlayerReady = contextValue.store?.get().ready
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
contextValue.player && contextValue.player.play()
|
contextValue.player && contextValue.player.play()
|
||||||
if (isPlayerReady && insights.size > 0) {
|
if (isPlayerReady && insights.size > 0 && jumpTimestamp) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
contextValue.player.pause()
|
contextValue.player.pause()
|
||||||
contextValue.player.jump(jumpTimestamp)
|
contextValue.player.jump(jumpTimestamp)
|
||||||
|
|
|
||||||
|
|
@ -169,10 +169,10 @@ interface DevtoolsButtonsProps {
|
||||||
bottomBlock: number;
|
bottomBlock: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DevtoolsButtons({
|
const DevtoolsButtons = observer(({
|
||||||
toggleBottomTools,
|
toggleBottomTools,
|
||||||
bottomBlock,
|
bottomBlock,
|
||||||
}: DevtoolsButtonsProps) {
|
}: DevtoolsButtonsProps) => {
|
||||||
const { aiSummaryStore } = useStore();
|
const { aiSummaryStore } = useStore();
|
||||||
const { store, player } = React.useContext(MobilePlayerContext);
|
const { store, player } = React.useContext(MobilePlayerContext);
|
||||||
|
|
||||||
|
|
@ -277,7 +277,7 @@ function DevtoolsButtons({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
|
|
||||||
const ControlPlayer = observer(Controls);
|
const ControlPlayer = observer(Controls);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
||||||
userDisplayName,
|
userDisplayName,
|
||||||
userDeviceType,
|
userDeviceType,
|
||||||
revId,
|
revId,
|
||||||
|
screenWidth,
|
||||||
|
screenHeight
|
||||||
} = session;
|
} = session;
|
||||||
|
|
||||||
const hasUserDetails = !!userId || !!userAnonymousId;
|
const hasUserDetails = !!userId || !!userAnonymousId;
|
||||||
|
|
@ -137,7 +139,7 @@ function UserCard({ className, request, session, width, height, similarSessions,
|
||||||
<SessionInfoItem
|
<SessionInfoItem
|
||||||
icon={deviceTypeIcon(userDeviceType)}
|
icon={deviceTypeIcon(userDeviceType)}
|
||||||
label={userDeviceType}
|
label={userDeviceType}
|
||||||
value={getDimension(width, height)}
|
value={getDimension(width || screenWidth, height || screenHeight)}
|
||||||
isLast={!revId}
|
isLast={!revId}
|
||||||
/>
|
/>
|
||||||
{revId && <SessionInfoItem icon="info" label="Rev ID:" value={revId} isLast />}
|
{revId && <SessionInfoItem icon="info" label="Rev ID:" value={revId} isLast />}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* border: solid thin $gray-light; */
|
/* border: solid thin $gray-light; */
|
||||||
/* border-radius: 3px; */
|
/* border-radius: 3px; */
|
||||||
overflow: hidden;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkers {
|
.checkers {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { formatBytes } from 'App/utils';
|
import { formatBytes } from 'App/utils';
|
||||||
import CopyText from 'Shared/CopyText';
|
import CopyText from 'Shared/CopyText';
|
||||||
import {Tag} from 'antd';
|
import { Tag } from 'antd';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resource: any;
|
resource: any;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FetchBasicDetails({ resource, timestamp }: Props) {
|
function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
const _duration = parseInt(resource.duration);
|
const _duration = parseInt(resource.duration);
|
||||||
const text = useMemo(() => {
|
const text = useMemo(() => {
|
||||||
|
|
@ -22,14 +23,16 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-start py-1">
|
<div className="flex items-start py-1">
|
||||||
<div className="font-medium w-36">Name</div>
|
<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>
|
<CopyText content={resource.url}>{resource.url}</CopyText>
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="font-medium w-36">Type</div>
|
<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}
|
{resource.type}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,7 +40,8 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
{resource.method && (
|
{resource.method && (
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="font-medium w-36">Request Method</div>
|
<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}
|
{resource.method}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,15 +51,12 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="text-base font-medium w-36">Status Code</div>
|
<div className="text-base font-medium w-36">Status Code</div>
|
||||||
<Tag
|
<Tag
|
||||||
bordered={false}
|
bordered={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip flex items-center',
|
'text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip flex items-center',
|
||||||
{ 'error color-red': !resource.success }
|
{ 'error color-red': !resource.success }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{resource.status === '200' && (
|
|
||||||
<Tag bordered={false} className="text-base bg-emerald-100 rounded-full mr-2"></Tag>
|
|
||||||
)}
|
|
||||||
{resource.status}
|
{resource.status}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,7 +64,8 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
|
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="font-medium w-36">Type</div>
|
<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}
|
{resource.type}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,18 +73,19 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
{!!resource.decodedBodySize && (
|
{!!resource.decodedBodySize && (
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="font-medium w-36">Size</div>
|
<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)}
|
{formatBytes(resource.decodedBodySize)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{!!_duration && (
|
{!!_duration && (
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="font-medium w-36">Duration</div>
|
<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
|
{_duration} ms
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,11 +93,12 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
|
||||||
|
|
||||||
{timestamp && (
|
{timestamp && (
|
||||||
<div className="flex items-center py-1">
|
<div className="flex items-center py-1">
|
||||||
<div className="font-medium w-36">Time</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}>
|
<Tag className="text-base rounded-lg bg-indigo-50 whitespace-nowrap overflow-hidden text-clip"
|
||||||
{timestamp}
|
bordered={false}>
|
||||||
</Tag>
|
{timestamp}
|
||||||
</div>
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { connect } from 'react-redux';
|
||||||
import { Icon, Loader } from 'UI';
|
import { Icon, Loader } from 'UI';
|
||||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
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';
|
import stl from './FilterModal.module.css';
|
||||||
|
|
||||||
const IconMap = {
|
const IconMap = {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ interface Props {
|
||||||
function Activity(props: Props) {
|
function Activity(props: Props) {
|
||||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
return (
|
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>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Console_info(props: 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 (
|
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>
|
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
19
frontend/app/components/ui/Icons/filters_screen.tsx
Normal file
19
frontend/app/components/ui/Icons/filters_screen.tsx
Normal 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;
|
||||||
|
|
@ -275,6 +275,7 @@ export { default as Filetype_pdf } from './filetype_pdf';
|
||||||
export { default as Filter } from './filter';
|
export { default as Filter } from './filter';
|
||||||
export { default as Filters_arrow_return_right } from './filters_arrow_return_right';
|
export { default as Filters_arrow_return_right } from './filters_arrow_return_right';
|
||||||
export { default as Filters_browser } from './filters_browser';
|
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_click } from './filters_click';
|
||||||
export { default as Filters_clickrage } from './filters_clickrage';
|
export { default as Filters_clickrage } from './filters_clickrage';
|
||||||
export { default as Filters_code } from './filters_code';
|
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_referrer } from './filters_referrer';
|
||||||
export { default as Filters_resize } from './filters_resize';
|
export { default as Filters_resize } from './filters_resize';
|
||||||
export { default as Filters_rev_id } from './filters_rev_id';
|
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_state_action } from './filters_state_action';
|
||||||
export { default as Filters_tag_element } from './filters_tag_element';
|
export { default as Filters_tag_element } from './filters_tag_element';
|
||||||
export { default as Filters_ttfb } from './filters_ttfb';
|
export { default as Filters_ttfb } from './filters_ttfb';
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
function Pdf_download(props: Props) {
|
function Pdf_download(props: Props) {
|
||||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
return (
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
function Pencil(props: Props) {
|
function Pencil(props: Props) {
|
||||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
return (
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
function Trash(props: Props) {
|
function Trash(props: Props) {
|
||||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
return (
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ interface Props {
|
||||||
function Users(props: Props) {
|
function Users(props: Props) {
|
||||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
return (
|
return (
|
||||||
|
<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>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -76,7 +76,7 @@ function reducer(state = initialState, action = {}) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case REFRESH_FILTER_OPTIONS:
|
case REFRESH_FILTER_OPTIONS:
|
||||||
return state
|
return state
|
||||||
.set('filterList', generateFilterOptions(filtersMap))
|
.set('filterList', generateFilterOptions(filtersMap, action.isMobile))
|
||||||
.set('filterListLive', generateFilterOptions(liveFiltersMap))
|
.set('filterListLive', generateFilterOptions(liveFiltersMap))
|
||||||
.set(
|
.set(
|
||||||
'filterListConditional',
|
'filterListConditional',
|
||||||
|
|
@ -466,10 +466,12 @@ export const editSavedSearch = (instance) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshFilterOptions = () => {
|
export const refreshFilterOptions = () => (dispatch, getState) => {
|
||||||
return {
|
const currentProject = getState().getIn(['site', 'instance']);
|
||||||
|
return dispatch({
|
||||||
type: REFRESH_FILTER_OPTIONS,
|
type: REFRESH_FILTER_OPTIONS,
|
||||||
};
|
isMobile: currentProject?.platform === 'ios'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setScrollPosition = (scrollPosition) => {
|
export const setScrollPosition = (scrollPosition) => {
|
||||||
|
|
|
||||||
|
|
@ -302,8 +302,8 @@ export default class Widget {
|
||||||
} else if (this.metricType === FUNNEL) {
|
} else if (this.metricType === FUNNEL) {
|
||||||
_data.funnel = new Funnel().fromJSON(_data);
|
_data.funnel = new Funnel().fromJSON(_data);
|
||||||
} else if (this.metricType === TABLE) {
|
} else if (this.metricType === TABLE) {
|
||||||
const totalSessions = data[0]['totalSessions'];
|
const count = data[0]['count'];
|
||||||
_data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, totalSessions, this.metricOf));
|
_data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, count, this.metricOf));
|
||||||
} else {
|
} else {
|
||||||
if (data.hasOwnProperty('chart')) {
|
if (data.hasOwnProperty('chart')) {
|
||||||
_data['value'] = data.value;
|
_data['value'] = data.value;
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,10 @@ export interface State extends ScreenState, ListsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
const userEvents = [
|
const userEvents = [
|
||||||
MType.IosSwipeEvent,
|
MType.MobileSwipeEvent,
|
||||||
MType.IosClickEvent,
|
MType.MobileClickEvent,
|
||||||
MType.IosInputEvent,
|
MType.MobileInputEvent,
|
||||||
MType.IosScreenChanges,
|
MType.MobileScreenChanges,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class IOSMessageManager implements IMessageManager {
|
export default class IOSMessageManager implements IMessageManager {
|
||||||
|
|
@ -233,7 +233,7 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (msg.tp) {
|
switch (msg.tp) {
|
||||||
case MType.IosPerformanceEvent:
|
case MType.MobilePerformanceEvent:
|
||||||
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU'];
|
const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU'];
|
||||||
if (performanceStats.includes(msg.name)) {
|
if (performanceStats.includes(msg.name)) {
|
||||||
this.performanceManager.append(msg);
|
this.performanceManager.append(msg);
|
||||||
|
|
@ -253,21 +253,21 @@ export default class IOSMessageManager implements IMessageManager {
|
||||||
// case MType.IosInputEvent:
|
// case MType.IosInputEvent:
|
||||||
// console.log('input', msg)
|
// console.log('input', msg)
|
||||||
// break;
|
// break;
|
||||||
case MType.IosNetworkCall:
|
case MType.MobileNetworkCall:
|
||||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart));
|
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart));
|
||||||
break;
|
break;
|
||||||
case MType.WsChannel:
|
case MType.WsChannel:
|
||||||
this.lists.lists.websocket.insert(msg);
|
this.lists.lists.websocket.insert(msg);
|
||||||
break;
|
break;
|
||||||
case MType.IosEvent:
|
case MType.MobileEvent:
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.lists.lists.event.insert({ ...msg, source: 'openreplay' });
|
this.lists.lists.event.insert({ ...msg, source: 'openreplay' });
|
||||||
break;
|
break;
|
||||||
case MType.IosSwipeEvent:
|
case MType.MobileSwipeEvent:
|
||||||
case MType.IosClickEvent:
|
case MType.MobileClickEvent:
|
||||||
this.touchManager.append(msg);
|
this.touchManager.append(msg);
|
||||||
break;
|
break;
|
||||||
case MType.IosLog:
|
case MType.MobileLog:
|
||||||
const log = { ...msg, level: msg.severity };
|
const log = { ...msg, level: msg.severity };
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.lists.lists.log.append(Log(log));
|
this.lists.lists.log.append(Log(log));
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default class TouchManager extends ListWalker<IosClickEvent | IosSwipeEve
|
||||||
public move(t: number) {
|
public move(t: number) {
|
||||||
const lastTouch = this.moveGetLast(t)
|
const lastTouch = this.moveGetLast(t)
|
||||||
if (!!lastTouch) {
|
if (!!lastTouch) {
|
||||||
if (lastTouch.tp === MType.IosSwipeEvent) {
|
if (lastTouch.tp === MType.MobileSwipeEvent) {
|
||||||
return
|
return
|
||||||
// not using swipe rn
|
// not using swipe rn
|
||||||
// this.touchTrail?.createSwipeTrail({
|
// this.touchTrail?.createSwipeTrail({
|
||||||
|
|
|
||||||
|
|
@ -233,10 +233,10 @@ export default class Screen {
|
||||||
break;
|
break;
|
||||||
case ScaleMode.AdjustParentHeight:
|
case ScaleMode.AdjustParentHeight:
|
||||||
// we want to scale the document with true height so the clickmap will be scrollable
|
// we want to scale the document with true height so the clickmap will be scrollable
|
||||||
const usedHeight =
|
const usedHeight = height + 'px';
|
||||||
this.document?.body.scrollHeight && this.document?.body.scrollHeight > height
|
// this.document?.body.scrollHeight && this.document?.body.scrollHeight > height
|
||||||
? this.document.body.scrollHeight + 'px'
|
// ? this.document.body.scrollHeight + 'px'
|
||||||
: height + 'px';
|
// : height + 'px';
|
||||||
this.scaleRatio = offsetWidth / width;
|
this.scaleRatio = offsetWidth / width;
|
||||||
translate = 'translate(-50%, 0)';
|
translate = 'translate(-50%, 0)';
|
||||||
posStyles = { top: 0, height: usedHeight };
|
posStyles = { top: 0, height: usedHeight };
|
||||||
|
|
|
||||||
|
|
@ -146,39 +146,37 @@ export default class TargetMarker {
|
||||||
if (clicks && this.screen.document) {
|
if (clicks && this.screen.document) {
|
||||||
this.clickMapOverlay?.remove();
|
this.clickMapOverlay?.remove();
|
||||||
const overlay = document.createElement('canvas');
|
const overlay = document.createElement('canvas');
|
||||||
const iframeSize = this.screen.iframeStylesRef;
|
|
||||||
const scrollHeight = this.screen.document?.documentElement.scrollHeight || 0;
|
const scrollHeight = this.screen.document?.documentElement.scrollHeight || 0;
|
||||||
const scrollWidth = this.screen.document?.documentElement.scrollWidth || 0;
|
const scrollWidth = this.screen.document?.documentElement.scrollWidth || 0;
|
||||||
const scaleRatio = this.screen.getScale();
|
|
||||||
Object.assign(
|
Object.assign(
|
||||||
overlay.style,
|
overlay.style,
|
||||||
clickmapStyles.overlayStyle({
|
clickmapStyles.overlayStyle({
|
||||||
height: iframeSize.height,
|
height: scrollHeight + 'px',
|
||||||
width: iframeSize.width,
|
width: scrollWidth + 'px',
|
||||||
scale: scaleRatio,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.clickMapOverlay = overlay;
|
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 pointMap: Record<string, { times: number; data: number[], original: any }> = {};
|
||||||
const ovWidth = parseInt(iframeSize.width);
|
overlay.width = scrollWidth;
|
||||||
const ovHeight = parseInt(iframeSize.height);
|
overlay.height = scrollHeight;
|
||||||
overlay.width = ovWidth;
|
|
||||||
overlay.height = ovHeight;
|
|
||||||
let maxIntensity = 0;
|
let maxIntensity = 0;
|
||||||
|
|
||||||
clicks.forEach((point) => {
|
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]) {
|
if (pointMap[key]) {
|
||||||
const times = pointMap[key].times + 1;
|
const times = pointMap[key].times + 1;
|
||||||
maxIntensity = Math.max(maxIntensity, times);
|
maxIntensity = Math.max(maxIntensity, times);
|
||||||
pointMap[key].times = times;
|
pointMap[key].times = times;
|
||||||
} else {
|
} else {
|
||||||
const clickData = [
|
const clickData = [
|
||||||
(point.normalizedX / 100) * scrollWidth,
|
(x / 100) * scrollWidth,
|
||||||
(point.normalizedY / 100) * scrollHeight,
|
(y / 100) * scrollHeight,
|
||||||
];
|
];
|
||||||
pointMap[key] = { times: 1, data: clickData, original: point };
|
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;
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
export const clickmapStyles = {
|
export const clickmapStyles = {
|
||||||
overlayStyle: ({ height, width, scale }: { height: string, width: string, scale: number }) => ({
|
overlayStyle: ({ height, width }: { height: string, width: string }) => ({
|
||||||
transform: `scale(${scale}) translate(-50%, 0)`,
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '0px',
|
top: '0px',
|
||||||
left: '50%',
|
left: 0,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
background: 'rgba(0,0,0, 0.15)',
|
background: 'rgba(0,0,0, 0.15)',
|
||||||
zIndex: 9 * 10e3,
|
zIndex: 9 * 10e3,
|
||||||
transformOrigin: 'left top',
|
|
||||||
}),
|
}),
|
||||||
totalClicks: {
|
totalClicks: {
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
|
|
|
||||||
1
frontend/app/svg/icons/filters/chevrons-up-down.svg
Normal file
1
frontend/app/svg/icons/filters/chevrons-up-down.svg
Normal 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 |
1
frontend/app/svg/icons/filters/screen.svg
Normal file
1
frontend/app/svg/icons/filters/screen.svg
Normal 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 |
|
|
@ -196,6 +196,14 @@ export enum FilterType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FilterKey {
|
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',
|
ERROR = 'error',
|
||||||
MISSING_RESOURCE = 'missingResource',
|
MISSING_RESOURCE = 'missingResource',
|
||||||
SLOW_SESSION = 'slowSession',
|
SLOW_SESSION = 'slowSession',
|
||||||
|
|
|
||||||
|
|
@ -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 { KEYS } from 'Types/filter/customFilter';
|
||||||
import Record from 'Types/Record';
|
import Record from 'Types/Record';
|
||||||
import { FilterType, FilterKey, FilterCategory } from './filterType';
|
import { FilterType, FilterKey, FilterCategory } from './filterType';
|
||||||
|
|
@ -13,10 +13,78 @@ const filterOrder = {
|
||||||
[FilterCategory.TECHNICAL]: 1,
|
[FilterCategory.TECHNICAL]: 1,
|
||||||
[FilterCategory.PERFORMANCE]: 2,
|
[FilterCategory.PERFORMANCE]: 2,
|
||||||
[FilterCategory.USER]: 3,
|
[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 = [
|
export const filters = [
|
||||||
|
...mobileFilters,
|
||||||
{
|
{
|
||||||
key: FilterKey.CLICK,
|
key: FilterKey.CLICK,
|
||||||
type: FilterType.MULTIPLE,
|
type: FilterType.MULTIPLE,
|
||||||
|
|
@ -96,7 +164,7 @@ export const filters = [
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
placeholder: 'Select method type',
|
placeholder: 'Select method type',
|
||||||
operatorOptions: filterOptions.stringOperatorsLimited,
|
operatorOptions: filterOptions.stringOperatorsLimited,
|
||||||
icon: 'filters/fetch',
|
icon: 'filters/fetch',
|
||||||
options: filterOptions.methodOptions
|
options: filterOptions.methodOptions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -232,7 +300,7 @@ export const filters = [
|
||||||
isEvent: true,
|
isEvent: true,
|
||||||
icon: 'filters/tag-element',
|
icon: 'filters/tag-element',
|
||||||
operatorOptions: filterOptions.tagElementOperators,
|
operatorOptions: filterOptions.tagElementOperators,
|
||||||
options: [],
|
options: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.UTM_SOURCE,
|
key: FilterKey.UTM_SOURCE,
|
||||||
|
|
@ -241,7 +309,7 @@ export const filters = [
|
||||||
label: 'UTM Source',
|
label: 'UTM Source',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringOperators,
|
operatorOptions: filterOptions.stringOperators,
|
||||||
icon: 'filters/country',
|
icon: 'filters/country'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.UTM_MEDIUM,
|
key: FilterKey.UTM_MEDIUM,
|
||||||
|
|
@ -250,7 +318,7 @@ export const filters = [
|
||||||
label: 'UTM Medium',
|
label: 'UTM Medium',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringOperators,
|
operatorOptions: filterOptions.stringOperators,
|
||||||
icon: 'filters/country',
|
icon: 'filters/country'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.UTM_CAMPAIGN,
|
key: FilterKey.UTM_CAMPAIGN,
|
||||||
|
|
@ -259,7 +327,7 @@ export const filters = [
|
||||||
label: 'UTM Campaign',
|
label: 'UTM Campaign',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringOperators,
|
operatorOptions: filterOptions.stringOperators,
|
||||||
icon: 'filters/country',
|
icon: 'filters/country'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.USER_COUNTRY,
|
key: FilterKey.USER_COUNTRY,
|
||||||
|
|
@ -471,12 +539,12 @@ export const filters = [
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringOperators,
|
operatorOptions: filterOptions.stringOperators,
|
||||||
icon: 'collection'
|
icon: 'collection'
|
||||||
},
|
}
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
const aOrder = filterOrder[a.category] ?? 9
|
const aOrder = filterOrder[a.category] ?? 9;
|
||||||
const bOrder = filterOrder[b.category] ?? 9
|
const bOrder = filterOrder[b.category] ?? 9;
|
||||||
return aOrder - bOrder
|
return aOrder - bOrder;
|
||||||
})
|
});
|
||||||
|
|
||||||
export const flagConditionFilters = [
|
export const flagConditionFilters = [
|
||||||
{
|
{
|
||||||
|
|
@ -559,10 +627,10 @@ export const flagConditionFilters = [
|
||||||
icon: 'filters/userid'
|
icon: 'filters/userid'
|
||||||
}
|
}
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
const aOrder = filterOrder[a.category] ?? 9
|
const aOrder = filterOrder[a.category] ?? 9;
|
||||||
const bOrder = filterOrder[b.category] ?? 9
|
const bOrder = filterOrder[b.category] ?? 9;
|
||||||
return aOrder - bOrder
|
return aOrder - bOrder;
|
||||||
})
|
});
|
||||||
|
|
||||||
export const conditionalFilters = [
|
export const conditionalFilters = [
|
||||||
{
|
{
|
||||||
|
|
@ -612,7 +680,7 @@ export const conditionalFilters = [
|
||||||
placeholder: 'Enter path or URL',
|
placeholder: 'Enter path or URL',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringConditional,
|
operatorOptions: filterOptions.stringConditional,
|
||||||
icon: "filters/fetch"
|
icon: 'filters/fetch'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.FETCH_STATUS_CODE,
|
key: FilterKey.FETCH_STATUS_CODE,
|
||||||
|
|
@ -622,7 +690,7 @@ export const conditionalFilters = [
|
||||||
placeholder: 'Enter status code',
|
placeholder: 'Enter status code',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
operatorOptions: filterOptions.customOperators,
|
operatorOptions: filterOptions.customOperators,
|
||||||
icon: "filters/fetch"
|
icon: 'filters/fetch'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.FETCH_METHOD,
|
key: FilterKey.FETCH_METHOD,
|
||||||
|
|
@ -643,8 +711,8 @@ export const conditionalFilters = [
|
||||||
placeholder: 'E.g. 12',
|
placeholder: 'E.g. 12',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
operatorOptions: filterOptions.customOperators,
|
operatorOptions: filterOptions.customOperators,
|
||||||
icon: "filters/fetch"
|
icon: 'filters/fetch'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
icon: 'filters/fetch',
|
icon: 'filters/fetch',
|
||||||
isEvent: true
|
isEvent: true
|
||||||
|
|
@ -667,7 +735,7 @@ export const conditionalFilters = [
|
||||||
label: 'Duration',
|
label: 'Duration',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
||||||
icon: "filters/duration",
|
icon: 'filters/duration',
|
||||||
isEvent: false
|
isEvent: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -690,10 +758,10 @@ export const conditionalFilters = [
|
||||||
icon: 'filters/userid'
|
icon: 'filters/userid'
|
||||||
}
|
}
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
const aOrder = filterOrder[a.category] ?? 9
|
const aOrder = filterOrder[a.category] ?? 9;
|
||||||
const bOrder = filterOrder[b.category] ?? 9
|
const bOrder = filterOrder[b.category] ?? 9;
|
||||||
return aOrder - bOrder
|
return aOrder - bOrder;
|
||||||
})
|
});
|
||||||
|
|
||||||
export const mobileConditionalFilters = [
|
export const mobileConditionalFilters = [
|
||||||
{
|
{
|
||||||
|
|
@ -703,7 +771,7 @@ export const mobileConditionalFilters = [
|
||||||
label: 'Duration',
|
label: 'Duration',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
||||||
icon: "filters/duration",
|
icon: 'filters/duration',
|
||||||
isEvent: false
|
isEvent: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -721,7 +789,7 @@ export const mobileConditionalFilters = [
|
||||||
placeholder: 'Enter path or URL',
|
placeholder: 'Enter path or URL',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringConditional,
|
operatorOptions: filterOptions.stringConditional,
|
||||||
icon: "filters/fetch"
|
icon: 'filters/fetch'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.FETCH_STATUS_CODE,
|
key: FilterKey.FETCH_STATUS_CODE,
|
||||||
|
|
@ -731,7 +799,7 @@ export const mobileConditionalFilters = [
|
||||||
placeholder: 'Enter status code',
|
placeholder: 'Enter status code',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
operatorOptions: filterOptions.customOperators,
|
operatorOptions: filterOptions.customOperators,
|
||||||
icon: "filters/fetch"
|
icon: 'filters/fetch'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.FETCH_METHOD,
|
key: FilterKey.FETCH_METHOD,
|
||||||
|
|
@ -752,8 +820,8 @@ export const mobileConditionalFilters = [
|
||||||
placeholder: 'E.g. 12',
|
placeholder: 'E.g. 12',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
operatorOptions: filterOptions.customOperators,
|
operatorOptions: filterOptions.customOperators,
|
||||||
icon: "filters/fetch"
|
icon: 'filters/fetch'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
icon: 'filters/fetch',
|
icon: 'filters/fetch',
|
||||||
isEvent: true
|
isEvent: true
|
||||||
|
|
@ -779,11 +847,11 @@ export const mobileConditionalFilters = [
|
||||||
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
||||||
icon: 'filters/cpu-load',
|
icon: 'filters/cpu-load',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'nominal', value: "0" },
|
{ label: 'nominal', value: '0' },
|
||||||
{ label: 'warm', value: "1" },
|
{ label: 'warm', value: '1' },
|
||||||
{ label: 'hot', value: "2" },
|
{ label: 'hot', value: '2' },
|
||||||
{ label: 'critical', value: "3" }
|
{ label: 'critical', value: '3' }
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mainThreadCPU',
|
key: 'mainThreadCPU',
|
||||||
|
|
@ -793,7 +861,7 @@ export const mobileConditionalFilters = [
|
||||||
placeholder: '0 .. 100',
|
placeholder: '0 .. 100',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
operatorOptions: filterOptions.customOperators,
|
operatorOptions: filterOptions.customOperators,
|
||||||
icon: 'filters/cpu-load',
|
icon: 'filters/cpu-load'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'viewComponent',
|
key: 'viewComponent',
|
||||||
|
|
@ -803,7 +871,7 @@ export const mobileConditionalFilters = [
|
||||||
placeholder: 'View Name',
|
placeholder: 'View Name',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
||||||
icon: 'filters/view',
|
icon: 'filters/view'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FilterKey.USERID,
|
key: FilterKey.USERID,
|
||||||
|
|
@ -833,7 +901,7 @@ export const mobileConditionalFilters = [
|
||||||
placeholder: 'logged value',
|
placeholder: 'logged value',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
operatorOptions: filterOptions.stringOperators,
|
operatorOptions: filterOptions.stringOperators,
|
||||||
icon: 'filters/console',
|
icon: 'filters/console'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'clickEvent',
|
key: 'clickEvent',
|
||||||
|
|
@ -854,7 +922,7 @@ export const mobileConditionalFilters = [
|
||||||
operatorOptions: filterOptions.customOperators,
|
operatorOptions: filterOptions.customOperators,
|
||||||
icon: 'filters/memory-load'
|
icon: 'filters/memory-load'
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
export const eventKeys = filters.filter((i) => i.isEvent).map(i => i.key);
|
export const eventKeys = filters.filter((i) => i.isEvent).map(i => i.key);
|
||||||
export const nonFlagFilters = filters.filter(i => {
|
export const nonFlagFilters = filters.filter(i => {
|
||||||
|
|
@ -955,12 +1023,12 @@ export const addElementToFiltersMap = (
|
||||||
|
|
||||||
export const addOptionsToFilter = (
|
export const addOptionsToFilter = (
|
||||||
key,
|
key,
|
||||||
options,
|
options
|
||||||
) => {
|
) => {
|
||||||
if (filtersMap[key] && filtersMap[key].options) {
|
if (filtersMap[key] && filtersMap[key].options) {
|
||||||
filtersMap[key].options = options
|
filtersMap[key].options = options;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function getMetadataLabel(key) {
|
function getMetadataLabel(key) {
|
||||||
return key.replace(/^_/, '').charAt(0).toUpperCase() + key.slice(2);
|
return key.replace(/^_/, '').charAt(0).toUpperCase() + key.slice(2);
|
||||||
|
|
@ -1008,11 +1076,11 @@ export const addElementToConditionalFiltersMap = (
|
||||||
|
|
||||||
export const addElementToMobileConditionalFiltersMap = (
|
export const addElementToMobileConditionalFiltersMap = (
|
||||||
category = FilterCategory.METADATA,
|
category = FilterCategory.METADATA,
|
||||||
key,
|
key,
|
||||||
type = FilterType.MULTIPLE,
|
type = FilterType.MULTIPLE,
|
||||||
operator = 'is',
|
operator = 'is',
|
||||||
operatorOptions = filterOptions.stringOperators,
|
operatorOptions = filterOptions.stringOperators,
|
||||||
icon = 'filters/metadata'
|
icon = 'filters/metadata'
|
||||||
) => {
|
) => {
|
||||||
mobileConditionalFiltersMap[key] = {
|
mobileConditionalFiltersMap[key] = {
|
||||||
key,
|
key,
|
||||||
|
|
@ -1023,8 +1091,8 @@ export const addElementToMobileConditionalFiltersMap = (
|
||||||
operatorOptions,
|
operatorOptions,
|
||||||
icon,
|
icon,
|
||||||
isLive: true
|
isLive: true
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const addElementToLiveFiltersMap = (
|
export const addElementToLiveFiltersMap = (
|
||||||
category = FilterCategory.METADATA,
|
category = FilterCategory.METADATA,
|
||||||
|
|
@ -1094,7 +1162,7 @@ export default Record({
|
||||||
_filter = filtersMap[`_${filter.source}`];
|
_filter = filtersMap[`_${filter.source}`];
|
||||||
} else {
|
} else {
|
||||||
if (filtersMap[filter.key]) {
|
if (filtersMap[filter.key]) {
|
||||||
_filter = filtersMap[filter.key]
|
_filter = filtersMap[filter.key];
|
||||||
} else {
|
} else {
|
||||||
_filter = filtersMap[type];
|
_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
|
* Group filters by category
|
||||||
* @param {*} filtersMap
|
|
||||||
* @returns
|
* @returns
|
||||||
|
* @param map
|
||||||
|
* @param isMobile
|
||||||
*/
|
*/
|
||||||
export const generateFilterOptions = (map) => {
|
export const generateFilterOptions = (map, isMobile = false) => {
|
||||||
const filterSection = {};
|
const filterSection = {};
|
||||||
Object.keys(map).forEach(key => {
|
Object.keys(map).forEach(key => {
|
||||||
|
if (isMobile && MOBILE_EXCLUDE.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMobile && WEB_EXCLUDE.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const filter = map[key];
|
const filter = map[key];
|
||||||
if (filterSection.hasOwnProperty(filter.category)) {
|
if (filterSection.hasOwnProperty(filter.category)) {
|
||||||
filterSection[filter.category].push(filter);
|
filterSection[filter.category].push(filter);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: chalice
|
name: chalice
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
# A chart can be either an 'application' or a 'library' chart.
|
# 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
|
# 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
|
# 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.
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.7
|
version: 0.1.7
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# 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
|
# 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.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
AppVersion: "v1.19.0"
|
AppVersion: "v1.19.7"
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ spec:
|
||||||
value: {{ .Values.global.s3.assistRecordsBucket }}
|
value: {{ .Values.global.s3.assistRecordsBucket }}
|
||||||
- name: sessions_bucket
|
- name: sessions_bucket
|
||||||
value: {{ .Values.global.s3.recordingsBucket }}
|
value: {{ .Values.global.s3.recordingsBucket }}
|
||||||
|
- name: IOS_VIDEO_BUCKET
|
||||||
|
value: {{ .Values.global.s3.recordingsBucket }}
|
||||||
- name: sourcemaps_bucket
|
- name: sourcemaps_bucket
|
||||||
value: {{ .Values.global.s3.sourcemapsBucket }}
|
value: {{ .Values.global.s3.sourcemapsBucket }}
|
||||||
- name: js_cache_bucket
|
- name: js_cache_bucket
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: db
|
name: db
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
# A chart can be either an 'application' or a 'library' chart.
|
# 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
|
# 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
|
# 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.
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.1
|
version: 0.1.1
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# 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
|
# 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.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
AppVersion: "v1.19.0"
|
AppVersion: "v1.19.2"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: frontend
|
name: frontend
|
||||||
description: A Helm chart for Kubernetes
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
# A chart can be either an 'application' or a 'library' chart.
|
# 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
|
# 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
|
# 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.
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (frontends://semver.org/)
|
# Versions are expected to follow Semantic Versioning (frontends://semver.org/)
|
||||||
version: 0.1.10
|
version: 0.1.10
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# 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
|
# 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.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
AppVersion: "v1.19.0"
|
AppVersion: "v1.19.3"
|
||||||
|
|
|
||||||
23
scripts/helmcharts/openreplay/charts/spot/.helmignore
Normal file
23
scripts/helmcharts/openreplay/charts/spot/.helmignore
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
24
scripts/helmcharts/openreplay/charts/spot/Chart.yaml
Normal file
24
scripts/helmcharts/openreplay/charts/spot/Chart.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
apiVersion: v2
|
||||||
|
name: spot
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# 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.18.0"
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "http.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "spot.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "spot.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "spot.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
|
{{- end }}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "spot.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "spot.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "spot.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "spot.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "spot.chart" . }}
|
||||||
|
{{ include "spot.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "spot.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "spot.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "spot.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "spot.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "spot.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "spot.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "spot.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
shareProcessNamespace: true
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
{{- if .Values.global.enterpriseEditionLicense }}
|
||||||
|
image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}-ee"
|
||||||
|
{{- else }}
|
||||||
|
image: "{{ tpl .Values.image.repository . }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
{{- end }}
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
{{- if .Values.healthCheck}}
|
||||||
|
{{- .Values.healthCheck | toYaml | nindent 10}}
|
||||||
|
{{- end}}
|
||||||
|
env:
|
||||||
|
{{- range $key, $val := .Values.env }}
|
||||||
|
- name: {{ $key }}
|
||||||
|
value: '{{ $val }}'
|
||||||
|
{{- end}}
|
||||||
|
{{- range $key, $val := .Values.global.env }}
|
||||||
|
- name: {{ $key }}
|
||||||
|
value: '{{ $val }}'
|
||||||
|
{{- end }}
|
||||||
|
- name: AWS_ACCESS_KEY_ID
|
||||||
|
{{- if .Values.global.s3.existingSecret }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.global.s3.existingSecret }}
|
||||||
|
key: access-key
|
||||||
|
{{- else }}
|
||||||
|
value: {{ .Values.global.s3.accessKey }}
|
||||||
|
{{- end }}
|
||||||
|
- name: AWS_SECRET_ACCESS_KEY
|
||||||
|
{{- if .Values.global.s3.existingSecret }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.global.s3.existingSecret }}
|
||||||
|
key: secret-key
|
||||||
|
{{- else }}
|
||||||
|
value: {{ .Values.global.s3.secretKey }}
|
||||||
|
{{- end }}
|
||||||
|
- name: AWS_REGION
|
||||||
|
value: '{{ .Values.global.s3.region }}'
|
||||||
|
- name: AWS_ENDPOINT
|
||||||
|
value: '{{- include "openreplay.s3Endpoint" . }}'
|
||||||
|
- name: LICENSE_KEY
|
||||||
|
value: '{{ .Values.global.enterpriseEditionLicense }}'
|
||||||
|
- name: KAFKA_SERVERS
|
||||||
|
value: '{{ .Values.global.kafka.kafkaHost }}:{{ .Values.global.kafka.kafkaPort }}'
|
||||||
|
- name: KAFKA_USE_SSL
|
||||||
|
value: '{{ .Values.global.kafka.kafkaUseSsl }}'
|
||||||
|
- name: pg_password
|
||||||
|
{{- if .Values.global.postgresql.existingSecret }}
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.global.postgresql.existingSecret }}
|
||||||
|
key: postgresql-postgres-password
|
||||||
|
{{- else }}
|
||||||
|
value: '{{ .Values.global.postgresql.postgresqlPassword }}'
|
||||||
|
{{- end}}
|
||||||
|
- name: POSTGRES_STRING
|
||||||
|
value: 'postgres://{{ .Values.global.postgresql.postgresqlUser }}:$(pg_password)@{{ .Values.global.postgresql.postgresqlHost }}:{{ .Values.global.postgresql.postgresqlPort }}/{{ .Values.global.postgresql.postgresqlDatabase }}'
|
||||||
|
{{- include "openreplay.env.redis_string" .Values.global.redis | nindent 12 }}
|
||||||
|
ports:
|
||||||
|
{{- range $key, $val := .Values.service.ports }}
|
||||||
|
- name: {{ $key }}
|
||||||
|
containerPort: {{ $val }}
|
||||||
|
protocol: TCP
|
||||||
|
{{- end }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: datadir
|
||||||
|
mountPath: /mnt/efs
|
||||||
|
{{- include "openreplay.volume.redis_ca_certificate.mount" .Values.global.redis | nindent 12 }}
|
||||||
|
{{- with .Values.persistence.mounts }}
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
{{- if eq (tpl .Values.pvc.name . ) "hostPath" }}
|
||||||
|
volumes:
|
||||||
|
{{- with .Values.persistence.volumes }}
|
||||||
|
{{- toYaml . | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
- name: datadir
|
||||||
|
hostPath:
|
||||||
|
# Ensure the file directory is created.
|
||||||
|
path: {{ tpl .Values.pvc.hostMountPath . }}
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
{{- else }}
|
||||||
|
volumes:
|
||||||
|
{{- with .Values.persistence.volumes }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
- name: datadir
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: "{{ tpl .Values.pvc.name . }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- include "openreplay.volume.redis_ca_certificate" .Values.global.redis | nindent 6 }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
29
scripts/helmcharts/openreplay/charts/spot/templates/hpa.yaml
Normal file
29
scripts/helmcharts/openreplay/charts/spot/templates/hpa.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{{- if .Values.autoscaling.enabled }}
|
||||||
|
apiVersion: autoscaling/v2beta1
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: {{ include "spot.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: {{ include "spot.fullname" . }}
|
||||||
|
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||||
|
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||||
|
metrics:
|
||||||
|
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- $fullName := include "spot.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.service.ports.http -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /$1
|
||||||
|
nginx.ingress.kubernetes.io/upstream-hash-by: $http_x_forwarded_for
|
||||||
|
spec:
|
||||||
|
ingressClassName: "{{ tpl .Values.ingress.className . }}"
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .Values.global.domainName }}
|
||||||
|
{{- if .Values.ingress.tls.secretName}}
|
||||||
|
secretName: {{ .Values.ingress.tls.secretName }}
|
||||||
|
{{- end}}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.global.domainName }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
path: /spot/(.*)
|
||||||
|
{{- end }}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "spot.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
{{- range $key, $val := .Values.service.ports }}
|
||||||
|
- port: {{ $val }}
|
||||||
|
targetPort: {{ $key }}
|
||||||
|
protocol: TCP
|
||||||
|
name: {{ $key }}
|
||||||
|
{{- end}}
|
||||||
|
selector:
|
||||||
|
{{- include "spot.selectorLabels" . | nindent 4 }}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }}
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: {{ include "spot.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
{{- if .Values.serviceMonitor.additionalLabels }}
|
||||||
|
{{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
endpoints:
|
||||||
|
{{- .Values.serviceMonitor.scrapeConfigs | toYaml | nindent 4 }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "spot.selectorLabels" . | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "spot.serviceAccountName" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "spot.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
{{- include "spot.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "spot.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
||||||
133
scripts/helmcharts/openreplay/charts/spot/values.yaml
Normal file
133
scripts/helmcharts/openreplay/charts/spot/values.yaml
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Default values for openreplay.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: "{{ .Values.global.openReplayContainerRegistry }}/spot"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: "spot"
|
||||||
|
fullnameOverride: "spot-openreplay"
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
# Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
# The name of the service account to use.
|
||||||
|
# If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
podSecurityContext:
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
fsGroup: 1001
|
||||||
|
fsGroupChangePolicy: "OnRootMismatch"
|
||||||
|
# podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
# securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
http: 8080
|
||||||
|
metrics: 8888
|
||||||
|
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
additionalLabels:
|
||||||
|
release: observability
|
||||||
|
scrapeConfigs:
|
||||||
|
- port: metrics
|
||||||
|
honorLabels: true
|
||||||
|
interval: 15s
|
||||||
|
path: /metrics
|
||||||
|
scheme: http
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "{{ .Values.global.ingress.controller.ingressClassResource.name }}"
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-connect-timeout: "120"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||||
|
nginx.ingress.kubernetes.io/cors-allow-methods: POST,PATCH,DELETE
|
||||||
|
nginx.ingress.kubernetes.io/cors-allow-headers: Content-Type,Authorization,Content-Encoding,X-Openreplay-Batch
|
||||||
|
nginx.ingress.kubernetes.io/cors-allow-origin: '*'
|
||||||
|
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||||
|
nginx.ingress.kubernetes.io/cors-expose-headers: "Content-Length"
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
tls:
|
||||||
|
secretName: openreplay-ssl
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
autoscaling:
|
||||||
|
enabled: false
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 5
|
||||||
|
targetCPUUtilizationPercentage: 80
|
||||||
|
# targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
env:
|
||||||
|
TOKEN_SECRET: secret_token_string # TODO: generate on build
|
||||||
|
CACHE_ASSETS: true
|
||||||
|
FS_CLEAN_HRS: 24
|
||||||
|
|
||||||
|
|
||||||
|
pvc:
|
||||||
|
# This can be either persistentVolumeClaim or hostPath.
|
||||||
|
# In case of pvc, you'll have to provide the pvc name.
|
||||||
|
# For example
|
||||||
|
# name: openreplay-efs
|
||||||
|
name: "{{ .Values.global.pvcRWXName }}"
|
||||||
|
hostMountPath: "{{ .Values.global.orTmpDir }}"
|
||||||
|
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
|
|
||||||
|
persistence: {}
|
||||||
|
# # Spec of spec.template.spec.containers[*].volumeMounts
|
||||||
|
# mounts:
|
||||||
|
# - name: kafka-ssl
|
||||||
|
# mountPath: /opt/kafka/ssl
|
||||||
|
# # Spec of spec.template.spec.volumes
|
||||||
|
# volumes:
|
||||||
|
# - name: kafka-ssl
|
||||||
|
# secret:
|
||||||
|
# secretName: kafka-ssl
|
||||||
|
|
@ -8,7 +8,11 @@ RETENTION_TIME=${RETENTION_TIME:-345600000}
|
||||||
topics=(
|
topics=(
|
||||||
"raw"
|
"raw"
|
||||||
"raw-ios"
|
"raw-ios"
|
||||||
|
"raw-images"
|
||||||
|
"canvas-images"
|
||||||
"trigger"
|
"trigger"
|
||||||
|
"canvas-trigger"
|
||||||
|
"mobile-trigger"
|
||||||
"cache"
|
"cache"
|
||||||
"analytics"
|
"analytics"
|
||||||
"storage-failover"
|
"storage-failover"
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ $fn_def$, :'next_version')
|
||||||
|
|
||||||
--
|
--
|
||||||
ALTER TABLE IF EXISTS events.clicks
|
ALTER TABLE IF EXISTS events.clicks
|
||||||
ADD COLUMN IF NOT EXISTS normalized_x smallint NULL,
|
ADD COLUMN IF NOT EXISTS normalized_x decimal NULL,
|
||||||
ADD COLUMN IF NOT EXISTS normalized_y smallint NULL,
|
ADD COLUMN IF NOT EXISTS normalized_y decimal NULL,
|
||||||
DROP COLUMN IF EXISTS x,
|
DROP COLUMN IF EXISTS x,
|
||||||
DROP COLUMN IF EXISTS y;
|
DROP COLUMN IF EXISTS y;
|
||||||
|
|
||||||
|
|
|
||||||
51
scripts/schema/db/init_dbs/postgresql/1.20.0/1.20.0.sql
Normal file
51
scripts/schema/db/init_dbs/postgresql/1.20.0/1.20.0.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
\set previous_version 'v1.19.0'
|
||||||
|
\set next_version 'v1.20.0'
|
||||||
|
SELECT openreplay_version() AS current_version,
|
||||||
|
openreplay_version() = :'previous_version' AS valid_previous,
|
||||||
|
openreplay_version() = :'next_version' AS is_next
|
||||||
|
\gset
|
||||||
|
|
||||||
|
\if :valid_previous
|
||||||
|
\echo valid previous DB version :'previous_version', starting DB upgrade to :'next_version'
|
||||||
|
BEGIN;
|
||||||
|
SELECT format($fn_def$
|
||||||
|
CREATE OR REPLACE FUNCTION openreplay_version()
|
||||||
|
RETURNS text AS
|
||||||
|
$$
|
||||||
|
SELECT '%1$s'
|
||||||
|
$$ LANGUAGE sql IMMUTABLE;
|
||||||
|
$fn_def$, :'next_version')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
--
|
||||||
|
ALTER TABLE IF EXISTS events.clicks
|
||||||
|
ALTER COLUMN normalized_x SET DATA TYPE decimal,
|
||||||
|
ALTER COLUMN normalized_y SET DATA TYPE decimal;
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS public.users
|
||||||
|
ADD COLUMN IF NOT EXISTS spot_jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS spot_jwt_refresh_jti integer NULL DEFAULT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS spot_jwt_refresh_iat timestamp without time zone NULL DEFAULT NULL;
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS or_cache;
|
||||||
|
CREATE TABLE IF NOT EXISTS or_cache.autocomplete_top_values
|
||||||
|
(
|
||||||
|
project_id integer NOT NULL REFERENCES public.projects (project_id) ON DELETE CASCADE,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
event_key text NULL,
|
||||||
|
result jsonb NULL,
|
||||||
|
execution_time integer NULL,
|
||||||
|
created_at timestamp DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
UNIQUE NULLS NOT DISTINCT (project_id, event_type, event_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS public.tenants
|
||||||
|
ADD COLUMN IF NOT EXISTS scope text NOT NULL DEFAULT 'full';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
\elif :is_next
|
||||||
|
\echo new version detected :'next_version', nothing to do
|
||||||
|
\else
|
||||||
|
\warn skipping DB upgrade of :'next_version', expected previous version :'previous_version', found :'current_version'
|
||||||
|
\endif
|
||||||
|
|
@ -17,6 +17,7 @@ BEGIN;
|
||||||
CREATE SCHEMA IF NOT EXISTS events_common;
|
CREATE SCHEMA IF NOT EXISTS events_common;
|
||||||
CREATE SCHEMA IF NOT EXISTS events;
|
CREATE SCHEMA IF NOT EXISTS events;
|
||||||
CREATE SCHEMA IF NOT EXISTS events_ios;
|
CREATE SCHEMA IF NOT EXISTS events_ios;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS or_cache;
|
||||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
|
@ -102,27 +103,31 @@ CREATE TABLE public.tenants
|
||||||
t_sessions bigint NOT NULL DEFAULT 0,
|
t_sessions bigint NOT NULL DEFAULT 0,
|
||||||
t_users integer NOT NULL DEFAULT 1,
|
t_users integer NOT NULL DEFAULT 1,
|
||||||
t_integrations integer NOT NULL DEFAULT 0,
|
t_integrations integer NOT NULL DEFAULT 0,
|
||||||
last_telemetry bigint NOT NULL DEFAULT CAST(EXTRACT(epoch FROM date_trunc('day', now())) * 1000 AS BIGINT)
|
last_telemetry bigint NOT NULL DEFAULT CAST(EXTRACT(epoch FROM date_trunc('day', now())) * 1000 AS BIGINT),
|
||||||
CONSTRAINT onerow_uni CHECK (tenant_id = 1)
|
scope text NOT NULL DEFAULT 'full',
|
||||||
|
CONSTRAINT onerow_uni CHECK (tenant_id = 1)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE user_role AS ENUM ('owner', 'admin', 'member');
|
CREATE TYPE user_role AS ENUM ('owner', 'admin', 'member');
|
||||||
|
|
||||||
CREATE TABLE public.users
|
CREATE TABLE public.users
|
||||||
(
|
(
|
||||||
user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
email text NOT NULL UNIQUE,
|
email text NOT NULL UNIQUE,
|
||||||
role user_role NOT NULL DEFAULT 'member',
|
role user_role NOT NULL DEFAULT 'member',
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'),
|
created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'),
|
||||||
deleted_at timestamp without time zone NULL DEFAULT NULL,
|
deleted_at timestamp without time zone NULL DEFAULT NULL,
|
||||||
api_key text UNIQUE DEFAULT generate_api_key(20) NOT NULL,
|
api_key text UNIQUE DEFAULT generate_api_key(20) NOT NULL,
|
||||||
jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
||||||
jwt_refresh_jti integer NULL DEFAULT NULL,
|
jwt_refresh_jti integer NULL DEFAULT NULL,
|
||||||
jwt_refresh_iat timestamp without time zone NULL DEFAULT NULL,
|
jwt_refresh_iat timestamp without time zone NULL DEFAULT NULL,
|
||||||
data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
spot_jwt_iat timestamp without time zone NULL DEFAULT NULL,
|
||||||
weekly_report boolean NOT NULL DEFAULT TRUE,
|
spot_jwt_refresh_jti integer NULL DEFAULT NULL,
|
||||||
settings jsonb DEFAULT NULL
|
spot_jwt_refresh_iat timestamp without time zone NULL DEFAULT NULL,
|
||||||
|
data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
weekly_report boolean NOT NULL DEFAULT TRUE,
|
||||||
|
settings jsonb DEFAULT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE public.basic_authentication
|
CREATE TABLE public.basic_authentication
|
||||||
|
|
@ -620,16 +625,16 @@ CREATE INDEX pages_query_nn_gin_idx ON events.pages USING GIN (query gin_trgm_op
|
||||||
|
|
||||||
CREATE TABLE events.clicks
|
CREATE TABLE events.clicks
|
||||||
(
|
(
|
||||||
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
|
session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE,
|
||||||
message_id bigint NOT NULL,
|
message_id bigint NOT NULL,
|
||||||
timestamp bigint NOT NULL,
|
timestamp bigint NOT NULL,
|
||||||
label text DEFAULT NULL,
|
label text DEFAULT NULL,
|
||||||
url text DEFAULT '' NOT NULL,
|
url text DEFAULT '' NOT NULL,
|
||||||
path text,
|
path text,
|
||||||
selector text DEFAULT '' NOT NULL,
|
selector text DEFAULT '' NOT NULL,
|
||||||
hesitation integer DEFAULT NULL,
|
hesitation integer DEFAULT NULL,
|
||||||
normalized_x smallint DEFAULT NULL,
|
normalized_x decimal DEFAULT NULL,
|
||||||
normalized_y smallint DEFAULT NULL,
|
normalized_y decimal DEFAULT NULL,
|
||||||
PRIMARY KEY (session_id, message_id)
|
PRIMARY KEY (session_id, message_id)
|
||||||
);
|
);
|
||||||
CREATE INDEX clicks_session_id_idx ON events.clicks (session_id);
|
CREATE INDEX clicks_session_id_idx ON events.clicks (session_id);
|
||||||
|
|
@ -1187,4 +1192,16 @@ CREATE TABLE public.projects_conditions
|
||||||
filters jsonb NOT NULL DEFAULT '[]'::jsonb
|
filters jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE or_cache.autocomplete_top_values
|
||||||
|
(
|
||||||
|
project_id integer NOT NULL REFERENCES public.projects (project_id) ON DELETE CASCADE,
|
||||||
|
event_type text NOT NULL,
|
||||||
|
event_key text NULL,
|
||||||
|
result jsonb NULL,
|
||||||
|
execution_time integer NULL,
|
||||||
|
created_at timestamp DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
UNIQUE NULLS NOT DISTINCT (project_id, event_type, event_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
34
scripts/schema/db/rollback_dbs/postgresql/1.20.0/1.20.0.sql
Normal file
34
scripts/schema/db/rollback_dbs/postgresql/1.20.0/1.20.0.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
\set previous_version 'v1.20.0'
|
||||||
|
\set next_version 'v1.19.0'
|
||||||
|
SELECT openreplay_version() AS current_version,
|
||||||
|
openreplay_version() = :'previous_version' AS valid_previous,
|
||||||
|
openreplay_version() = :'next_version' AS is_next
|
||||||
|
\gset
|
||||||
|
|
||||||
|
\if :valid_previous
|
||||||
|
\echo valid previous DB version :'previous_version', starting DB downgrade to :'next_version'
|
||||||
|
BEGIN;
|
||||||
|
SELECT format($fn_def$
|
||||||
|
CREATE OR REPLACE FUNCTION openreplay_version()
|
||||||
|
RETURNS text AS
|
||||||
|
$$
|
||||||
|
SELECT '%1$s'
|
||||||
|
$$ LANGUAGE sql IMMUTABLE;
|
||||||
|
$fn_def$, :'next_version')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
--
|
||||||
|
ALTER TABLE IF EXISTS public.users
|
||||||
|
DROP COLUMN IF EXISTS spot_jwt_iat,
|
||||||
|
DROP COLUMN IF EXISTS spot_jwt_refresh_jti,
|
||||||
|
DROP COLUMN IF EXISTS spot_jwt_refresh_iat;
|
||||||
|
|
||||||
|
DROP SCHEMA or_cache CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
\elif :is_next
|
||||||
|
\echo new version detected :'next_version', nothing to do
|
||||||
|
\else
|
||||||
|
\warn skipping DB downgrade of :'next_version', expected previous version :'previous_version', found :'current_version'
|
||||||
|
\endif
|
||||||
|
|
@ -18,13 +18,13 @@ returns `result` without changes.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import Tracker from '@openreplay/tracker';
|
import Tracker from '@openreplay/tracker';
|
||||||
import trackerGraphQL from '@openreplay/tracker-graphql';
|
import { createGraphqlMiddleware } from '@openreplay/tracker-graphql';
|
||||||
|
|
||||||
const tracker = new Tracker({
|
const tracker = new Tracker({
|
||||||
projectKey: YOUR_PROJECT_KEY,
|
projectKey: YOUR_PROJECT_KEY,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const recordGraphQL = tracker.plugin(trackerGraphQL());
|
export const recordGraphQL = tracker.use(createGraphqlMiddleware());
|
||||||
```
|
```
|
||||||
|
|
||||||
### Relay
|
### 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)
|
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
|
```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([
|
const network = new RelayNetworkLayer([
|
||||||
// your middleware
|
// 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
|
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 { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; // see above for recordGraphQL definition
|
||||||
import { Environment } from 'relay-runtime';
|
import { Environment } from 'relay-runtime';
|
||||||
|
|
||||||
const handler = createGraphqlMiddleware(tracker)
|
const handler = tracker.use(createGraphqlMiddleware());
|
||||||
|
|
||||||
function fetchQuery(operation, variables, cacheConfig, uploadables) {
|
function fetchQuery(operation, variables, cacheConfig, uploadables) {
|
||||||
return fetch('www.myapi.com/resource', {
|
return fetch('www.myapi.com/resource', {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(result =>
|
.then((result) =>
|
||||||
handler(
|
handler(
|
||||||
// op kind, name, variables, response, duration (default 0)
|
// op kind, name, variables, response, duration (default 0)
|
||||||
operation.operationKind,
|
operation.operationKind,
|
||||||
operation.name,
|
operation.name,
|
||||||
variables,
|
variables,
|
||||||
result,
|
result,
|
||||||
duration,
|
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`
|
For [Apollo](https://www.apollographql.com/) you should create a new `ApolloLink`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { createTrackerLink } from '@openreplay/tracker-graphql'
|
import { createTrackerLink } from '@openreplay/tracker-graphql';
|
||||||
|
|
||||||
const trackerLink = createTrackerLink(tracker);
|
const trackerLink = tracker.use(createTrackerLink());
|
||||||
const yourLink = new ApolloLink(trackerLink)
|
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:
|
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 { createGraphqlMiddleware } from '@openreplay/tracker-graphql'; // see above for recordGraphQL definition
|
||||||
import { ApolloLink } from 'apollo-link';
|
import { ApolloLink } from 'apollo-link';
|
||||||
|
|
||||||
const handler = createGraphqlMiddleware(tracker)
|
const handler = tracker.use(createGraphqlMiddleware());
|
||||||
|
|
||||||
const trackerApolloLink = new ApolloLink((operation, forward) => {
|
const trackerApolloLink = new ApolloLink((operation, forward) => {
|
||||||
return forward(operation).map(result =>
|
operation.setContext({ start: performance.now() });
|
||||||
handler(
|
return forward(operation).map((result) => {
|
||||||
|
const time = performance.now() - operation.getContext().start;
|
||||||
|
return handler(
|
||||||
// op kind, name, variables, response, duration (default 0)
|
// op kind, name, variables, response, duration (default 0)
|
||||||
operation.query.definitions[0].operation,
|
operation.query.definitions[0].operation,
|
||||||
operation.operationName,
|
operation.operationName,
|
||||||
operation.variables,
|
operation.variables,
|
||||||
result,
|
result,
|
||||||
),
|
time,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = ApolloLink.from([
|
const link = ApolloLink.from([
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { App, Messages } from '@openreplay/tracker';
|
import { App, Messages } from '@openreplay/tracker';
|
||||||
import Observable from 'zen-observable';
|
import Observable from 'zen-observable';
|
||||||
|
import { Sanitizer } from './types';
|
||||||
|
|
||||||
type Operation = {
|
type Operation = {
|
||||||
query: Record<string, any>;
|
query: Record<string, any>;
|
||||||
|
|
@ -9,48 +10,63 @@ type Operation = {
|
||||||
};
|
};
|
||||||
type NextLink = (operation: Operation) => Observable<Record<string, any>>;
|
type NextLink = (operation: Operation) => Observable<Record<string, any>>;
|
||||||
|
|
||||||
export const createTrackerLink = (app: App | null) => {
|
export const createTrackerLink = (
|
||||||
if (!app) {
|
sanitizer?: Sanitizer<Record<string, any> | undefined | null>,
|
||||||
return (operation: Operation, forward: NextLink) => forward(operation);
|
) => {
|
||||||
}
|
return (app: App | null) => {
|
||||||
return (operation: Operation, forward: NextLink) => {
|
if (!app) {
|
||||||
return new Observable((observer) => {
|
return (operation: Operation, forward: NextLink) => forward(operation);
|
||||||
const start = app.timestamp();
|
}
|
||||||
const observable = forward(operation);
|
return (operation: Operation, forward: NextLink) => {
|
||||||
const subscription = observable.subscribe({
|
return new Observable((observer) => {
|
||||||
next(value) {
|
const start = app.timestamp();
|
||||||
const end = app.timestamp();
|
const observable = forward(operation);
|
||||||
app.send(
|
const subscription = observable.subscribe({
|
||||||
Messages.GraphQL(
|
next(value) {
|
||||||
operation.query.definitions[0].kind,
|
const end = app.timestamp();
|
||||||
operation.operationName,
|
const operationDefinition = operation.query.definitions[0];
|
||||||
JSON.stringify(operation.variables),
|
app.send(
|
||||||
JSON.stringify(value.data),
|
Messages.GraphQL(
|
||||||
end - start,
|
operationDefinition.kind === 'OperationDefinition'
|
||||||
),
|
? operationDefinition.operation
|
||||||
);
|
: 'unknown?',
|
||||||
observer.next(value);
|
operation.operationName,
|
||||||
},
|
JSON.stringify(
|
||||||
error(error) {
|
sanitizer
|
||||||
const end = app.timestamp();
|
? sanitizer(operation.variables)
|
||||||
app.send(
|
: operation.variables,
|
||||||
Messages.GraphQL(
|
),
|
||||||
operation.query.definitions[0].kind,
|
JSON.stringify(sanitizer ? sanitizer(value.data) : value.data),
|
||||||
operation.operationName,
|
end - start,
|
||||||
JSON.stringify(operation.variables),
|
),
|
||||||
JSON.stringify(error),
|
);
|
||||||
end - start,
|
observer.next(value);
|
||||||
),
|
},
|
||||||
);
|
error(error) {
|
||||||
observer.error(error);
|
const end = app.timestamp();
|
||||||
},
|
app.send(
|
||||||
complete() {
|
Messages.GraphQL(
|
||||||
observer.complete();
|
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();
|
||||||
});
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { App, Messages } from "@openreplay/tracker";
|
import { App, Messages } from '@openreplay/tracker';
|
||||||
|
|
||||||
function createGraphqlMiddleware() {
|
function createGraphqlMiddleware() {
|
||||||
return (app: App | null) => {
|
return (app: App | null) => {
|
||||||
|
|
@ -10,7 +10,7 @@ function createGraphqlMiddleware() {
|
||||||
operationName: string,
|
operationName: string,
|
||||||
variables: any,
|
variables: any,
|
||||||
result: any,
|
result: any,
|
||||||
duration = 0
|
duration = 0,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
app.send(
|
app.send(
|
||||||
|
|
@ -30,4 +30,4 @@ function createGraphqlMiddleware() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createGraphqlMiddleware
|
export default createGraphqlMiddleware;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import createTrackerLink from './apolloMiddleware.js';
|
import createTrackerLink from './apolloMiddleware.js';
|
||||||
import createRelayMiddleware from './relayMiddleware.js';
|
import createRelayMiddleware from './relayMiddleware.js';
|
||||||
import createGraphqlMiddleware from './graphqlMiddleware.js';
|
import createGraphqlMiddleware from './graphqlMiddleware.js';
|
||||||
|
import { Sanitizer } from './types.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createTrackerLink,
|
createTrackerLink,
|
||||||
createRelayMiddleware,
|
createRelayMiddleware,
|
||||||
createGraphqlMiddleware,
|
createGraphqlMiddleware,
|
||||||
}
|
Sanitizer,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,55 @@
|
||||||
import { App, Messages } from '@openreplay/tracker';
|
import { App, Messages } from '@openreplay/tracker';
|
||||||
import type { Middleware, RelayRequest } from './relaytypes';
|
import type { Middleware, RelayRequest } from './relaytypes';
|
||||||
|
import { Sanitizer } from './types';
|
||||||
|
|
||||||
const createRelayMiddleware = (app: App | null): Middleware => {
|
const createRelayMiddleware = (sanitizer?: Sanitizer<Record<string, any>>) => {
|
||||||
if (!app) {
|
return (app: App | null): Middleware => {
|
||||||
return (next) => async (req) => await next(req);
|
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))
|
|
||||||
}
|
}
|
||||||
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 opKind = request.operation.kind;
|
||||||
const opName = request.operation.name;
|
const opName = request.operation.name;
|
||||||
const vars = JSON.stringify(request.variables)
|
const vars = JSON.stringify(
|
||||||
const opResp = JSON.stringify(json)
|
sanitizer ? sanitizer(request.variables) : request.variables,
|
||||||
return Messages.GraphQL(
|
);
|
||||||
opKind,
|
const opResp = JSON.stringify(sanitizer ? sanitizer(json) : json);
|
||||||
opName,
|
return Messages.GraphQL(opKind, opName, vars, opResp, duration);
|
||||||
vars,
|
|
||||||
opResp,
|
|
||||||
duration
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createRelayMiddleware
|
export default createRelayMiddleware;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
type ConcreteBatch = {
|
type ConcreteBatch = {
|
||||||
kind: 'Batch';
|
kind: 'Batch';
|
||||||
fragment: any;
|
fragment: any;
|
||||||
|
|
@ -9,7 +8,7 @@ type ConcreteBatch = {
|
||||||
text: string | null;
|
text: string | null;
|
||||||
operationKind: string;
|
operationKind: string;
|
||||||
};
|
};
|
||||||
type Variables = { [name: string]: any };
|
export type Variables = { [name: string]: any };
|
||||||
interface FetchOpts {
|
interface FetchOpts {
|
||||||
url?: string;
|
url?: string;
|
||||||
method: 'POST' | 'GET';
|
method: 'POST' | 'GET';
|
||||||
|
|
@ -17,7 +16,13 @@ interface FetchOpts {
|
||||||
body: string | FormData;
|
body: string | FormData;
|
||||||
credentials?: 'same-origin' | 'include' | 'omit';
|
credentials?: 'same-origin' | 'include' | 'omit';
|
||||||
mode?: 'cors' | 'websocket' | 'navigate' | 'no-cors' | 'same-origin';
|
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';
|
redirect?: 'follow' | 'error' | 'manual';
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
[name: string]: any;
|
[name: string]: any;
|
||||||
|
|
|
||||||
1
tracker/tracker-graphql/src/types.ts
Normal file
1
tracker/tracker-graphql/src/types.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export type Sanitizer<T> = (values: T) => Partial<T>;
|
||||||
|
|
@ -963,8 +963,8 @@ export default class App {
|
||||||
deviceMemory,
|
deviceMemory,
|
||||||
jsHeapSizeLimit,
|
jsHeapSizeLimit,
|
||||||
timezone: getTimezone(),
|
timezone: getTimezone(),
|
||||||
width: window.innerWidth,
|
width: window.screen.width,
|
||||||
height: window.innerHeight,
|
height: window.screen.height,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
|
|
@ -1220,7 +1220,9 @@ export default class App {
|
||||||
timezone: getTimezone(),
|
timezone: getTimezone(),
|
||||||
condition: conditionName,
|
condition: conditionName,
|
||||||
assistOnly: startOpts.assistOnly ?? this.socketMode,
|
assistOnly: startOpts.assistOnly ?? this.socketMode,
|
||||||
}),
|
width: window.screen.width,
|
||||||
|
height: window.screen.height
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
if (r.status !== 200) {
|
if (r.status !== 200) {
|
||||||
const error = await r.text()
|
const error = await r.text()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue