diff --git a/LICENSE b/LICENSE index b348b0c8f..a4bbf3abf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,21 @@ Copyright (c) 2022 Asayer, Inc. OpenReplay monorepo uses multiple licenses. Portions of this software are licensed as follows: - - All content that resides under the "ee/" directory of this repository, is licensed under the license defined in "ee/LICENSE". -- Content outside of the above mentioned directories or restrictions above is available under the "Elastic License 2.0 (ELv2)" license as defined below. +- Some directories have a specific LICENSE file and are licensed under the "MIT" license, as defined below. +- Content outside of the above mentioned directories or restrictions defaults to the "Elastic License 2.0 (ELv2)" license, as defined below. Reach out (license@openreplay.com) if you have any questions regarding licenses. +------------------------------------------------------------------------------------ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ------------------------------------------------------------------------------------ Elastic License 2.0 (ELv2) diff --git a/README.md b/README.md index 0f9f35669..31252da7d 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Check out our [roadmap](https://www.notion.so/openreplay/Roadmap-889d2c3d968b478 ## License -This repo is under the Elastic License 2.0 (ELv2), with the exception of the `ee` directory. +This monorepo uses several licenses. See [LICENSE](/LICENSE) for more details. ## Contributors diff --git a/api/Dockerfile b/api/Dockerfile index 20dfe9b86..5700f9c4d 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -19,7 +19,7 @@ RUN cd /work_tmp && npm install WORKDIR /work COPY . . -RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/. +RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/. && chmod 644 /mappings.wasm RUN adduser -u 1001 openreplay -D USER 1001 diff --git a/api/Dockerfile.alerts b/api/Dockerfile.alerts index dbb0c581d..851ed2dc5 100644 --- a/api/Dockerfile.alerts +++ b/api/Dockerfile.alerts @@ -4,7 +4,7 @@ LABEL Maintainer="KRAIEM Taha Yassine" RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=alerts \ - pg_minconn=2 \ + pg_minconn=1 \ pg_maxconn=10 \ ENTERPRISE_BUILD=${envarg} diff --git a/api/chalicelib/core/alerts.py b/api/chalicelib/core/alerts.py index e5316ba06..f851751ba 100644 --- a/api/chalicelib/core/alerts.py +++ b/api/chalicelib/core/alerts.py @@ -138,7 +138,10 @@ def send_by_email(notification, destination): def send_by_email_batch(notifications_list): + if not helper.has_smtp(): + logging.info("no SMTP configuration for email notifications") if notifications_list is None or len(notifications_list) == 0: + logging.info("no email notifications") return for n in notifications_list: send_by_email(notification=n.get("notification"), destination=n.get("destination")) diff --git a/api/chalicelib/core/projects.py b/api/chalicelib/core/projects.py index 0b0bd963f..0893f6259 100644 --- a/api/chalicelib/core/projects.py +++ b/api/chalicelib/core/projects.py @@ -43,16 +43,24 @@ def __create(tenant_id, name): def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, stack_integrations=False): with pg_client.PostgresClient() as cur: - cur.execute(f"""\ - SELECT - s.project_id, s.name, s.project_key, s.save_request_payloads - {',s.gdpr' if gdpr else ''} - {',COALESCE((SELECT TRUE FROM public.sessions WHERE sessions.project_id = s.project_id LIMIT 1), FALSE) AS recorded' if recorded else ''} - {',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''} - FROM public.projects AS s - {'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''} - WHERE s.deleted_at IS NULL - ORDER BY s.project_id;""") + recorded_q = "" + if recorded: + recorded_q = """, COALESCE((SELECT TRUE + FROM public.sessions + WHERE sessions.project_id = s.project_id + AND sessions.start_ts >= (EXTRACT(EPOCH FROM s.created_at) * 1000 - 24 * 60 * 60 * 1000) + AND sessions.start_ts <= %(now)s + LIMIT 1), FALSE) AS recorded""" + query = cur.mogrify(f"""SELECT + s.project_id, s.name, s.project_key, s.save_request_payloads + {',s.gdpr' if gdpr else ''} + {recorded_q} + {',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''} + FROM public.projects AS s + {'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''} + WHERE s.deleted_at IS NULL + ORDER BY s.project_id;""", {"now": TimeUTC.now()}) + cur.execute(query) rows = cur.fetchall() if recording_state: project_ids = [f'({r["project_id"]})' for r in rows] diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 738a5e3d9..c044a5819 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -712,13 +712,13 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr event.value, value_key=e_k)) elif event_type == events.event_type.ERROR.ui_type: event_from = event_from % f"{events.event_type.ERROR.table} AS main INNER JOIN public.errors AS main1 USING(error_id)" - event.source = tuple(event.source) + event.source = list(set(event.source)) if not is_any and event.value not in [None, "*", ""]: event_where.append( _multiple_conditions(f"(main1.message {op} %({e_k})s OR main1.name {op} %({e_k})s)", event.value, value_key=e_k)) if event.source[0] not in [None, "*", ""]: - event_where.append(_multiple_conditions(f"main1.source = %({s_k})s", event.value, value_key=s_k)) + event_where.append(_multiple_conditions(f"main1.source = %({s_k})s", event.source, value_key=s_k)) # ----- IOS @@ -877,7 +877,8 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr apply = True elif f.type == schemas.FetchFilterType._duration: event_where.append( - _multiple_conditions(f"main.duration {f.operator} %({e_k_f})s::integer", f.value, value_key=e_k_f)) + _multiple_conditions(f"main.duration {f.operator} %({e_k_f})s::integer", f.value, + value_key=e_k_f)) apply = True elif f.type == schemas.FetchFilterType._request_body: event_where.append( @@ -885,7 +886,8 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr apply = True elif f.type == schemas.FetchFilterType._response_body: event_where.append( - _multiple_conditions(f"main.response_body {op} %({e_k_f})s::text", f.value, value_key=e_k_f)) + _multiple_conditions(f"main.response_body {op} %({e_k_f})s::text", f.value, + value_key=e_k_f)) apply = True else: print(f"undefined FETCH filter: {f.type}") diff --git a/api/chalicelib/utils/email_handler.py b/api/chalicelib/utils/email_handler.py index 66b8a3afd..b3c7d9984 100644 --- a/api/chalicelib/utils/email_handler.py +++ b/api/chalicelib/utils/email_handler.py @@ -1,13 +1,15 @@ import base64 +import logging import re from email.header import Header from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from chalicelib.utils import helper, smtp from decouple import config +from chalicelib.utils import smtp + def __get_subject(subject): return subject @@ -64,11 +66,11 @@ def send_html(BODY_HTML, SUBJECT, recipient, bcc=None): if bcc is not None and len(bcc) > 0: r += [bcc] try: - print(f"Email sending to: {r}") + logging.info(f"Email sending to: {r}") s.sendmail(msg['FROM'], r, msg.as_string().encode('ascii')) except Exception as e: - print("!!! Email error!") - print(e) + logging.error("!!! Email error!") + logging.error(e) def send_text(recipients, text, subject): @@ -82,8 +84,8 @@ def send_text(recipients, text, subject): try: s.sendmail(msg['FROM'], recipients, msg.as_string().encode('ascii')) except Exception as e: - print("!! Text-email failed: " + subject), - print(e) + logging.error("!! Text-email failed: " + subject), + logging.error(e) def __escape_text_html(text): diff --git a/api/chalicelib/utils/email_helper.py b/api/chalicelib/utils/email_helper.py index 72072c924..2c5eb02e2 100644 --- a/api/chalicelib/utils/email_helper.py +++ b/api/chalicelib/utils/email_helper.py @@ -1,5 +1,5 @@ from chalicelib.utils.TimeUTC import TimeUTC -from chalicelib.utils.email_handler import __get_html_from_file, send_html, __escape_text_html +from chalicelib.utils.email_handler import __get_html_from_file, send_html def send_team_invitation(recipient, client_id, sender_name, invitation_link): diff --git a/api/chalicelib/utils/pg_client.py b/api/chalicelib/utils/pg_client.py index eda7747f8..c4149f49d 100644 --- a/api/chalicelib/utils/pg_client.py +++ b/api/chalicelib/utils/pg_client.py @@ -1,3 +1,4 @@ +import logging import time from threading import Semaphore @@ -6,6 +7,9 @@ import psycopg2.extras from decouple import config from psycopg2 import pool +logging.basicConfig(level=config("LOGLEVEL", default=logging.INFO)) +logging.getLogger('apscheduler').setLevel(config("LOGLEVEL", default=logging.INFO)) + _PG_CONFIG = {"host": config("pg_host"), "database": config("pg_dbname"), "user": config("pg_user"), @@ -44,31 +48,34 @@ RETRY = 0 def make_pool(): + if not config('PG_POOL', cast=bool, default=True): + return global postgreSQL_pool global RETRY if postgreSQL_pool is not None: try: postgreSQL_pool.closeall() except (Exception, psycopg2.DatabaseError) as error: - print("Error while closing all connexions to PostgreSQL", error) + logging.error("Error while closing all connexions to PostgreSQL", error) try: postgreSQL_pool = ORThreadedConnectionPool(config("pg_minconn", cast=int, default=20), config("pg_maxconn", cast=int, default=80), **PG_CONFIG) if (postgreSQL_pool): - print("Connection pool created successfully") + logging.info("Connection pool created successfully") except (Exception, psycopg2.DatabaseError) as error: - print("Error while connecting to PostgreSQL", error) + logging.error("Error while connecting to PostgreSQL", error) if RETRY < RETRY_MAX: RETRY += 1 - print(f"waiting for {RETRY_INTERVAL}s before retry n°{RETRY}") + logging.info(f"waiting for {RETRY_INTERVAL}s before retry n°{RETRY}") time.sleep(RETRY_INTERVAL) make_pool() else: raise error -make_pool() +if config('PG_POOL', cast=bool, default=True): + make_pool() class PostgresClient: @@ -87,8 +94,14 @@ class PostgresClient: elif long_query: long_config = dict(_PG_CONFIG) long_config["application_name"] += "-LONG" - long_config["options"] = f"-c statement_timeout={config('pg_long_timeout', cast=int, default=5 * 60) * 1000}" + long_config["options"] = f"-c statement_timeout=" \ + f"{config('pg_long_timeout', cast=int, default=5 * 60) * 1000}" self.connection = psycopg2.connect(**long_config) + elif not config('PG_POOL', cast=bool, default=True): + single_config = dict(_PG_CONFIG) + single_config["application_name"] += "-NOPOOL" + single_config["options"] = f"-c statement_timeout={config('pg_timeout', cast=int, default=3 * 60) * 1000}" + self.connection = psycopg2.connect(**single_config) else: self.connection = postgreSQL_pool.getconn() @@ -104,14 +117,19 @@ class PostgresClient: if self.long_query or self.unlimited_query: self.connection.close() except Exception as error: - print("Error while committing/closing PG-connection", error) - if str(error) == "connection already closed" and not self.long_query and not self.unlimited_query: - print("Recreating the connexion pool") + logging.error("Error while committing/closing PG-connection", error) + if str(error) == "connection already closed" \ + and not self.long_query \ + and not self.unlimited_query \ + and config('PG_POOL', cast=bool, default=True): + logging.info("Recreating the connexion pool") make_pool() else: raise error finally: - if not self.long_query: + if config('PG_POOL', cast=bool, default=True) \ + and not self.long_query \ + and not self.unlimited_query: postgreSQL_pool.putconn(self.connection) diff --git a/api/chalicelib/utils/smtp.py b/api/chalicelib/utils/smtp.py index 3615ca71a..63e1621fb 100644 --- a/api/chalicelib/utils/smtp.py +++ b/api/chalicelib/utils/smtp.py @@ -1,10 +1,14 @@ +import logging import smtplib +from smtplib import SMTPAuthenticationError + from decouple import config +from starlette.exceptions import HTTPException class EmptySMTP: def sendmail(self, from_addr, to_addrs, msg, mail_options=(), rcpt_options=()): - print("!! CANNOT SEND EMAIL, NO VALID SMTP CONFIGURATION FOUND") + logging.error("!! CANNOT SEND EMAIL, NO VALID SMTP CONFIGURATION FOUND") class SMTPClient: @@ -30,7 +34,11 @@ class SMTPClient: self.server.starttls() # stmplib docs recommend calling ehlo() before & after starttls() self.server.ehlo() - self.server.login(user=config("EMAIL_USER"), password=config("EMAIL_PASSWORD")) + if len(config("EMAIL_USER", default="")) > 0 and len(config("EMAIL_PASSWORD", default="")) > 0: + try: + self.server.login(user=config("EMAIL_USER"), password=config("EMAIL_PASSWORD")) + except SMTPAuthenticationError: + raise HTTPException(401, "SMTP Authentication Error") return self.server def __exit__(self, *args): diff --git a/api/env.default b/api/env.default index c99e1dc05..ce67208e6 100644 --- a/api/env.default +++ b/api/env.default @@ -40,6 +40,7 @@ pg_minconn=20 pg_maxconn=50 PG_RETRY_MAX=50 PG_RETRY_INTERVAL=2 +PG_POOL=true put_S3_TTL=20 sentryURL= sessions_bucket=mobs diff --git a/api/routers/core.py b/api/routers/core.py index 2263a30bd..2bc4a4dd4 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -231,7 +231,7 @@ def delete_sentry(projectId: int, context: schemas.CurrentContext = Depends(OR_c @app.get('/{projectId}/integrations/sentry/events/{eventId}', tags=["integrations"]) -def proxy_sentry(projectId: int, eventId: int, context: schemas.CurrentContext = Depends(OR_context)): +def proxy_sentry(projectId: int, eventId: str, context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_sentry.proxy_get(tenant_id=context.tenant_id, project_id=projectId, event_id=eventId)} @@ -1148,14 +1148,6 @@ def generate_new_user_token(context: schemas.CurrentContext = Depends(OR_context return {"data": users.generate_new_api_key(user_id=context.user_id)} -@app.post('/account', tags=["account"]) -@app.put('/account', tags=["account"]) -def edit_account(data: schemas.EditUserSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data, - editor_id=context.user_id) - - @app.post('/account/password', tags=["account"]) @app.put('/account/password', tags=["account"]) def change_client_password(data: schemas.EditUserPasswordSchema = Body(...), diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index bddb0ae4d..73dad85bb 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -43,6 +43,14 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)): } +@app.post('/account', tags=["account"]) +@app.put('/account', tags=["account"]) +def edit_account(data: schemas.EditUserSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data, + editor_id=context.user_id) + + @app.get('/projects/limit', tags=['projects']) def get_projects_limit(context: schemas.CurrentContext = Depends(OR_context)): return {"data": { diff --git a/api/schemas.py b/api/schemas.py index 2289c03cb..b87f5e4cd 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -562,6 +562,8 @@ class _SessionSearchEventRaw(__MixedSearchFilter): assert len(values["source"]) > 0 and isinstance(values["source"][0], int), \ f"source of type int if required for {PerformanceEventType.time_between_events}" else: + assert "source" in values, f"source is required for {values.get('type')}" + assert isinstance(values["source"], list), f"source of type list is required for {values.get('type')}" for c in values["source"]: assert isinstance(c, int), f"source value should be of type int for {values.get('type')}" elif values.get("type") == EventType.error and values.get("source") is None: diff --git a/ee/api/Dockerfile b/ee/api/Dockerfile index 2e04fa330..577606447 100644 --- a/ee/api/Dockerfile +++ b/ee/api/Dockerfile @@ -17,7 +17,7 @@ RUN cd /work_tmp && npm install WORKDIR /work COPY . . -RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/. +RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/. && chmod 644 /mappings.wasm RUN adduser -u 1001 openreplay -D USER 1001 diff --git a/ee/api/Dockerfile.alerts b/ee/api/Dockerfile.alerts index 351fce661..09315754d 100644 --- a/ee/api/Dockerfile.alerts +++ b/ee/api/Dockerfile.alerts @@ -4,7 +4,7 @@ LABEL Maintainer="KRAIEM Taha Yassine" RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=alerts \ - pg_minconn=2 \ + pg_minconn=1 \ pg_maxconn=10 \ ENTERPRISE_BUILD=${envarg} diff --git a/ee/api/Dockerfile.crons b/ee/api/Dockerfile.crons index 96b9e6453..83b3085e0 100644 --- a/ee/api/Dockerfile.crons +++ b/ee/api/Dockerfile.crons @@ -7,7 +7,8 @@ ENV APP_NAME=crons \ pg_minconn=2 \ pg_maxconn=10 \ ENTERPRISE_BUILD=${envarg} \ - ACTION="" + ACTION="" \ + PG_POOL=false WORKDIR /work_tmp COPY requirements-crons.txt /work_tmp/requirements.txt diff --git a/ee/api/chalicelib/core/projects.py b/ee/api/chalicelib/core/projects.py index e6ef34760..6700173b5 100644 --- a/ee/api/chalicelib/core/projects.py +++ b/ee/api/chalicelib/core/projects.py @@ -52,30 +52,28 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st AND users.tenant_id = %(tenant_id)s AND (roles.all_projects OR roles_projects.project_id = s.project_id) ) AS role_project ON (TRUE)""" - pre_select = "" + recorded_q = "" if recorded: - pre_select = """WITH recorded_p AS (SELECT DISTINCT projects.project_id - FROM projects INNER JOIN sessions USING (project_id) - WHERE tenant_id =%(tenant_id)s - AND deleted_at IS NULL - AND duration > 0)""" - cur.execute( - cur.mogrify(f"""\ - {pre_select} + recorded_q = """, COALESCE((SELECT TRUE + FROM public.sessions + WHERE sessions.project_id = s.project_id + AND sessions.start_ts >= (EXTRACT(EPOCH FROM s.created_at) * 1000 - 24 * 60 * 60 * 1000) + AND sessions.start_ts <= %(now)s + LIMIT 1), FALSE) AS recorded""" + query = cur.mogrify(f"""\ SELECT s.project_id, s.name, s.project_key, s.save_request_payloads {',s.gdpr' if gdpr else ''} - {',EXISTS(SELECT 1 FROM recorded_p WHERE recorded_p.project_id = s.project_id) AS recorded' if recorded else ''} + {recorded_q} {',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''} FROM public.projects AS s - {'LEFT JOIN recorded_p USING (project_id)' if recorded else ''} {'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''} {role_query if user_id is not None else ""} WHERE s.tenant_id =%(tenant_id)s AND s.deleted_at IS NULL ORDER BY s.project_id;""", - {"tenant_id": tenant_id, "user_id": user_id}) - ) + {"tenant_id": tenant_id, "user_id": user_id, "now": TimeUTC.now()}) + cur.execute(query) rows = cur.fetchall() if recording_state: project_ids = [f'({r["project_id"]})' for r in rows] diff --git a/ee/api/env.default b/ee/api/env.default index 70941ab99..41d9b6b45 100644 --- a/ee/api/env.default +++ b/ee/api/env.default @@ -49,6 +49,7 @@ pg_minconn=20 pg_maxconn=50 PG_RETRY_MAX=50 PG_RETRY_INTERVAL=2 +PG_POOL=true put_S3_TTL=20 sentryURL= sessions_bucket=mobs diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 9d09198a6..e6675c4f3 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -46,6 +46,14 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)): } +@app.post('/account', tags=["account"]) +@app.put('/account', tags=["account"]) +def edit_account(data: schemas_ee.EditUserSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data, + editor_id=context.user_id) + + @app.get('/projects/limit', tags=['projects']) def get_projects_limit(context: schemas.CurrentContext = Depends(OR_context)): return {"data": { diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index d3a63e49a..2c31a3ca8 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -2,14 +2,12 @@ import React from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; import withPageTitle from 'HOCs/withPageTitle'; -import { - fetchFavoriteList as fetchFavoriteSessionList -} from 'Duck/sessions'; +import { fetchFavoriteList as fetchFavoriteSessionList } from 'Duck/sessions'; import { applyFilter, clearEvents, addAttribute } from 'Duck/filters'; import { KEYS } from 'Types/filter/customFilter'; import SessionList from './SessionList'; import stl from './bugFinder.module.css'; -import withLocationHandlers from "HOCs/withLocationHandlers"; +import withLocationHandlers from 'HOCs/withLocationHandlers'; import { fetch as fetchFilterVariables } from 'Duck/sources'; import { fetchSources } from 'Duck/customField'; import { setActiveTab } from 'Duck/search'; @@ -21,113 +19,113 @@ import { clearSearch, fetchSessions, addFilterByKeyAndValue } from 'Duck/search' import { FilterKey } from 'Types/filter/filterType'; const weakEqual = (val1, val2) => { - if (!!val1 === false && !!val2 === false) return true; - if (!val1 !== !val2) return false; - return `${ val1 }` === `${ val2 }`; -} + if (!!val1 === false && !!val2 === false) return true; + if (!val1 !== !val2) return false; + return `${val1}` === `${val2}`; +}; const allowedQueryKeys = [ - 'userOs', - 'userId', - 'userBrowser', - 'userDevice', - 'userCountry', - 'startDate', - 'endDate', - 'minDuration', - 'maxDuration', - 'referrer', - 'sort', - 'order', + 'userOs', + 'userId', + 'userBrowser', + 'userDevice', + 'userCountry', + 'startDate', + 'endDate', + 'minDuration', + 'maxDuration', + 'referrer', + 'sort', + 'order', ]; @withLocationHandlers() -@connect(state => ({ - filter: state.getIn([ 'filters', 'appliedFilter' ]), - variables: state.getIn([ 'customFields', 'list' ]), - sources: state.getIn([ 'customFields', 'sources' ]), - filterValues: state.get('filterValues'), - favoriteList: state.getIn([ 'sessions', 'favoriteList' ]), - currentProjectId: state.getIn([ 'site', 'siteId' ]), - sites: state.getIn([ 'site', 'list' ]), - watchdogs: state.getIn(['watchdogs', 'list']), - activeFlow: state.getIn([ 'filters', 'activeFlow' ]), - sessions: state.getIn([ 'sessions', 'list' ]), -}), { - fetchFavoriteSessionList, - applyFilter, - addAttribute, - fetchFilterVariables, - fetchSources, - clearEvents, - setActiveTab, - clearSearch, - fetchSessions, - addFilterByKeyAndValue, -}) -@withPageTitle("Sessions - OpenReplay") +@connect( + (state) => ({ + filter: state.getIn(['filters', 'appliedFilter']), + variables: state.getIn(['customFields', 'list']), + sources: state.getIn(['customFields', 'sources']), + filterValues: state.get('filterValues'), + favoriteList: state.getIn(['sessions', 'favoriteList']), + currentProjectId: state.getIn(['site', 'siteId']), + sites: state.getIn(['site', 'list']), + watchdogs: state.getIn(['watchdogs', 'list']), + activeFlow: state.getIn(['filters', 'activeFlow']), + sessions: state.getIn(['sessions', 'list']), + }), + { + fetchFavoriteSessionList, + applyFilter, + addAttribute, + fetchFilterVariables, + fetchSources, + clearEvents, + setActiveTab, + clearSearch, + fetchSessions, + addFilterByKeyAndValue, + } +) +@withPageTitle('Sessions - OpenReplay') export default class BugFinder extends React.PureComponent { - state = {showRehydratePanel: false} - constructor(props) { - super(props); + state = { showRehydratePanel: false }; + constructor(props) { + super(props); - // TODO should cache the response - // props.fetchSources().then(() => { - // defaultFilters[6] = { - // category: 'Collaboration', - // type: 'CUSTOM', - // keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS() - // }; - // defaultFilters[7] = { - // category: 'Logging Tools', - // type: 'ERROR', - // keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS() - // }; - // }); - if (props.sessions.size === 0) { - props.fetchSessions(); + // TODO should cache the response + // props.fetchSources().then(() => { + // defaultFilters[6] = { + // category: 'Collaboration', + // type: 'CUSTOM', + // keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS() + // }; + // defaultFilters[7] = { + // category: 'Logging Tools', + // type: 'ERROR', + // keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS() + // }; + // }); + // if (props.sessions.size === 0) { + // props.fetchSessions(); + // } + + const queryFilter = this.props.query.all(allowedQueryKeys); + if (queryFilter.hasOwnProperty('userId')) { + props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId); + } else { + if (props.sessions.size === 0) { + props.fetchSessions(); + } + } } - const queryFilter = this.props.query.all(allowedQueryKeys); - if (queryFilter.hasOwnProperty('userId')) { - props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId); - } else { - if (props.sessions.size === 0) { - props.fetchSessions(); - } - } - } + toggleRehydratePanel = () => { + this.setState({ showRehydratePanel: !this.state.showRehydratePanel }); + }; - toggleRehydratePanel = () => { - this.setState({ showRehydratePanel: !this.state.showRehydratePanel }) - } + setActiveTab = (tab) => { + this.props.setActiveTab(tab); + }; - setActiveTab = tab => { - this.props.setActiveTab(tab); - } + render() { + const { showRehydratePanel } = this.state; - render() { - const { showRehydratePanel } = this.state; - - return ( -
-
-
- -
-
- -
- - + return ( +
+
+
+ +
+
+ +
+ + +
+ +
+
- -
-
-
- ); - } + ); + } } diff --git a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js index 606ecaf67..afb135099 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js +++ b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js @@ -26,6 +26,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) { }, [label]); const { startDate, endDate, rangeValue } = filter; + console.log('startDate', startDate); const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() }); const onDateChange = (e) => { @@ -40,7 +41,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) { const dateValues = period.toJSON(); dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf(); dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf(); - applyFilter(dateValues); + // applyFilter(dateValues); } }, [label]); diff --git a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx index 477e42d8f..d1799cc6a 100644 --- a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx +++ b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx @@ -102,7 +102,7 @@ function UserForm(props: Props) { ) => changeCaptureRate(e.target.value)} - value={captureRate.toString()} - style={{ height: '38px', width: '100px'}} - disabled={captureAll} - min={0} - max={100} - /> - -
+ +
+ + + 100% + +
+
+ {!captureAll && ( +
+ +
+ ) => changeCaptureRate(e.target.value)} + value={captureRate.toString()} + style={{ height: '38px', width: '100px' }} + disabled={captureAll} + min={0} + max={100} + /> + +
+
of the sessions @@ -80,4 +88,6 @@ function CaptureRate() { ); } -export default observer(CaptureRate); +export default connect((state: any) => ({ + isAdmin: state.getIn(['user', 'account', 'admin']) || state.getIn(['user', 'account', 'superAdmin']), +}))(observer(CaptureRate)); diff --git a/frontend/app/components/ui/Button/Button.tsx b/frontend/app/components/ui/Button/Button.tsx index eb9396682..8e6c62823 100644 --- a/frontend/app/components/ui/Button/Button.tsx +++ b/frontend/app/components/ui/Button/Button.tsx @@ -1,6 +1,6 @@ import React from 'react'; import cn from 'classnames'; -import { CircularLoader, Icon } from 'UI'; +import { CircularLoader, Icon, Popup } from 'UI'; interface Props { className?: string; @@ -12,6 +12,7 @@ interface Props { loading?: boolean; icon?: string; rounded?: boolean; + tooltip?: any; [x: string]: any; } export default (props: Props) => { @@ -25,6 +26,7 @@ export default (props: Props) => { children, loading = false, rounded = false, + tooltip = null, ...rest } = props; @@ -70,7 +72,7 @@ export default (props: Props) => { classes = classes.map((c) => c.replace('rounded', 'rounded-full h-10 w-10 justify-center')); } - return ( + const render = () => ( ); + + return tooltip ? {render()} : render(); }; diff --git a/frontend/app/duck/liveSearch.js b/frontend/app/duck/liveSearch.js index ecb8a720e..7838a740a 100644 --- a/frontend/app/duck/liveSearch.js +++ b/frontend/app/duck/liveSearch.js @@ -20,7 +20,7 @@ const FETCH_SESSION_LIST = fetchListType(`${name}/FETCH_SESSION_LIST`); const initialState = Map({ list: List(), - instance: new Filter({ filters: [], sort: 'timestamp' }), + instance: new Filter({ filters: [], sort: '' }), filterSearchList: {}, currentPage: 1, }); diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index ff1795b74..8859ace22 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -86,6 +86,8 @@ function reducer(state = initialState, action = {}) { return state.set('filterSearchList', groupedList); case APPLY_SAVED_SEARCH: return state.set('savedSearch', action.filter); + case CLEAR_SEARCH: + return state.set('savedSearch', new SavedFilter({})); case EDIT_SAVED_SEARCH: return state.mergeIn(['savedSearch'], action.instance); case UPDATE_CURRENT_PAGE: @@ -286,8 +288,9 @@ export function fetchFilterSearch(params) { } export const clearSearch = () => (dispatch, getState) => { - dispatch(applySavedSearch(new SavedFilter({}))); - dispatch(edit(new Filter({ filters: [] }))); + const filter = getState().getIn(['search', 'instance']); + // dispatch(applySavedSearch(new SavedFilter({}))); + dispatch(edit(new Filter({ startDate: filter.startDate, endDate: filter.endDate, rangeValue: filter.rangeValue, filters: [] }))); return dispatch({ type: CLEAR_SEARCH, }); diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 617a54fa2..f327b1c60 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -19,7 +19,7 @@ import Session from "./types/session"; import Error from "./types/error"; import { FilterKey } from "Types/filter/filterType"; -export interface IDashboardSotre { +export interface IDashboardStore { dashboards: IDashboard[]; selectedDashboard: IDashboard | null; dashboardInstance: IDashboard; @@ -91,12 +91,12 @@ export interface IDashboardSotre { ): Promise; setPeriod(period: any): void; } -export default class DashboardStore implements IDashboardSotre { +export default class DashboardStore implements IDashboardStore { siteId: any = null; // Dashbaord / Widgets dashboards: Dashboard[] = []; selectedDashboard: Dashboard | null = null; - dashboardInstance: IDashboard = new Dashboard(); + dashboardInstance: Dashboard = new Dashboard(); selectedWidgets: IWidget[] = []; currentWidget: Widget = new Widget(); widgetCategories: any[] = []; @@ -226,7 +226,7 @@ export default class DashboardStore implements IDashboardSotre { } fetch(dashboardId: string): Promise { - this.fetchingDashboard = true; + this.setFetchingDashboard(true); return dashboardService .getDashboard(dashboardId) .then((response) => { @@ -235,10 +235,14 @@ export default class DashboardStore implements IDashboardSotre { }); }) .finally(() => { - this.fetchingDashboard = false; + this.setFetchingDashboard(false); }); } + setFetchingDashboard(value: boolean) { + this.fetchingDashboard = value; + } + save(dashboard: IDashboard): Promise { this.isSaving = true; const isCreating = !dashboard.dashboardId; diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index c6ffeb460..445a174f3 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import DashboardStore, { IDashboardSotre } from './dashboardStore'; +import DashboardStore, { IDashboardStore } from './dashboardStore'; import MetricStore, { IMetricStore } from './metricStore'; import UserStore from './userStore'; import RoleStore from './roleStore'; @@ -12,7 +12,7 @@ import NotificationStore from './notificationStore'; import ErrorStore from './errorStore'; export class RootStore { - dashboardStore: IDashboardSotre; + dashboardStore: IDashboardStore; metricStore: IMetricStore; funnelStore: FunnelStore; settingsStore: SettingsStore; diff --git a/frontend/app/mstore/notificationStore.ts b/frontend/app/mstore/notificationStore.ts index 13124ca62..76f63ac2e 100644 --- a/frontend/app/mstore/notificationStore.ts +++ b/frontend/app/mstore/notificationStore.ts @@ -18,6 +18,7 @@ export default class NotificationStore { fetchNotifications: action, ignoreAllNotifications: action, ignoreNotification: action, + setNotificationsCount: action, }); } @@ -74,15 +75,19 @@ export default class NotificationStore { }); } + setNotificationsCount(count: number) { + this.notificationsCount = count; + } + fetchNotificationsCount(): Promise { return new Promise((resolve, reject) => { userService.getNotificationsCount() .then((response: any) => { - this.notificationsCount = response.count; + this.setNotificationsCount(response.count); resolve(response); }).catch((error: any) => { reject(error); }); }); } -} \ No newline at end of file +} diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index ce2a1f4db..5a2a8eb6b 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -1,21 +1,25 @@ -import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" -import { FilterKey, FilterType } from 'Types/filter/filterType' -import { filtersMap } from 'Types/filter/newFilter' +import { makeAutoObservable, runInAction, observable, action, reaction } from 'mobx'; +import { FilterKey, FilterType, FilterCategory } from 'Types/filter/filterType'; +import { filtersMap } from 'Types/filter/newFilter'; export default class FilterItem { - type: string = '' - key: string = '' - label: string = '' - value: any = [""] - isEvent: boolean = false - operator: string = '' - source: string = '' - filters: FilterItem[] = [] - operatorOptions: any[] = [] - options: any[] = [] - isActive: boolean = true - completed: number = 0 - dropped: number = 0 + type: string = ''; + category: FilterCategory = FilterCategory.METADATA; + key: string = ''; + label: string = ''; + value: any = ['']; + isEvent: boolean = false; + operator: string = ''; + hasSource: boolean = false; + source: string = ''; + sourceOperator: string = ''; + sourceOperatorOptions: any = []; + filters: FilterItem[] = []; + operatorOptions: any[] = []; + options: any[] = []; + isActive: boolean = true; + completed: number = 0; + dropped: number = 0; constructor(data: any = {}) { makeAutoObservable(this, { @@ -26,9 +30,11 @@ export default class FilterItem { source: observable, filters: observable, isActive: observable, + sourceOperator: observable, + category: observable, - merge: action - }) + merge: action, + }); if (Array.isArray(data.filters)) { data.filters = data.filters.map(function (i) { @@ -36,55 +42,64 @@ export default class FilterItem { }); } - this.merge(data) + this.merge(data); } updateKey(key: string, value: any) { - this[key] = value + this[key] = value; } - merge(data) { - Object.keys(data).forEach(key => { - this[key] = data[key] - }) + merge(data: any) { + Object.keys(data).forEach((key) => { + this[key] = data[key]; + }); } - fromJson(json, mainFilterKey = '') { - let _filter = filtersMap[json.type] || {} + fromJson(json: any, mainFilterKey = '') { + const isMetadata = json.type === FilterKey.METADATA; + let _filter: any = (isMetadata ? filtersMap[json.source] : filtersMap[json.type]) || {}; + if (mainFilterKey) { const mainFilter = filtersMap[mainFilterKey]; - const subFilterMap = {} - mainFilter.filters.forEach(option => { - subFilterMap[option.key] = option - }) - _filter = subFilterMap[json.type] + const subFilterMap = {}; + mainFilter.filters.forEach((option: any) => { + subFilterMap[option.key] = option; + }); + _filter = subFilterMap[json.type]; } - this.type = _filter.type - this.key = _filter.key - this.label = _filter.label - this.operatorOptions = _filter.operatorOptions - this.options = _filter.options - this.isEvent = _filter.isEvent + this.type = _filter.type; + this.key = _filter.key; + this.label = _filter.label; + this.operatorOptions = _filter.operatorOptions; + this.hasSource = _filter.hasSource; + this.category = _filter.category; + this.sourceOperatorOptions = _filter.sourceOperatorOptions; + this.options = _filter.options; + this.isEvent = _filter.isEvent; - this.value = json.value.length === 0 || !json.value ? [""] : json.value, - this.operator = json.operator - - this.filters = _filter.type === FilterType.SUB_FILTERS && json.filters ? json.filters.map(i => new FilterItem().fromJson(i, json.type)) : [] + (this.value = json.value.length === 0 || !json.value ? [''] : json.value), (this.operator = json.operator); + this.source = json.source; + this.sourceOperator = json.sourceOperator; - this.completed = json.completed - this.dropped = json.dropped - return this + this.filters = + _filter.type === FilterType.SUB_FILTERS && json.filters ? json.filters.map((i: any) => new FilterItem().fromJson(i, json.type)) : []; + + this.completed = json.completed; + this.dropped = json.dropped; + return this; } - toJson() { + toJson(): any { + const isMetadata = this.category === FilterCategory.METADATA; const json = { - type: this.key, + type: isMetadata ? FilterKey.METADATA : this.key, isEvent: this.isEvent, value: this.value, operator: this.operator, - source: this.source, - filters: Array.isArray(this.filters) ? this.filters.map(i => i.toJson()) : [], - } - return json + source: isMetadata ? this.key : this.source, + sourceOperator: this.sourceOperator, + filters: Array.isArray(this.filters) ? this.filters.map((i) => i.toJson()) : [], + }; + return json; } -} \ No newline at end of file +} diff --git a/frontend/app/mstore/userStore.ts b/frontend/app/mstore/userStore.ts index 0a5c47f53..523ae04f7 100644 --- a/frontend/app/mstore/userStore.ts +++ b/frontend/app/mstore/userStore.ts @@ -23,6 +23,7 @@ export default class UserStore { updateUser: action, updateKey: action, initUser: action, + setLimits: action, }) } @@ -30,7 +31,7 @@ export default class UserStore { return new Promise((resolve, reject) => { userService.getLimits() .then((response: any) => { - this.limits = response; + this.setLimits(response); resolve(response); }).catch((error: any) => { reject(error); @@ -38,6 +39,10 @@ export default class UserStore { }); } + setLimits(limits: any) { + this.limits = limits; + } + initUser(user?: any ): Promise { return new Promise((resolve, reject) => { if (user) { @@ -175,4 +180,4 @@ export default class UserStore { return promise; } -} \ No newline at end of file +} diff --git a/scripts/dockerfiles/nginx/Dockerfile b/scripts/dockerfiles/nginx/Dockerfile index 57e26334d..7b5301d6a 100644 --- a/scripts/dockerfiles/nginx/Dockerfile +++ b/scripts/dockerfiles/nginx/Dockerfile @@ -12,11 +12,7 @@ COPY nginx.conf /usr/local/openresty${RESTY_DEB_FLAVOR}/nginx/conf/nginx.conf COPY default.conf /etc/nginx/conf.d/default.conf COPY compression.conf /etc/nginx/conf.d/compression.conf COPY location.list /etc/nginx/conf.d/location.list -RUN chmod 0644 /usr/local/openresty${RESTY_DEB_FLAVOR}/nginx/conf/nginx.conf +RUN chmod 0644 /usr/local/openresty${RESTY_DEB_FLAVOR}/nginx/conf/nginx.conf RUN chown -R 1001 /var/run/openresty /usr/local/openresty - -# copy nginx-selfsigned.crt /tmp/root.cert -# RUN cat /tmp/root.cert >> /etc/ssl/certs/ca-certificates.crt - USER 1001 diff --git a/scripts/helmcharts/openreplay/charts/chalice/values.yaml b/scripts/helmcharts/openreplay/charts/chalice/values.yaml index 29e036522..2c9d75040 100644 --- a/scripts/helmcharts/openreplay/charts/chalice/values.yaml +++ b/scripts/helmcharts/openreplay/charts/chalice/values.yaml @@ -122,14 +122,13 @@ healthCheck: timeoutSeconds: 10 -persistence: - # Spec of spec.template.spec.containers[*].volumeMounts - mounts: - - mountPath: /mnt/efs - name: datadir - # Spec of spec.template.spec.volumes - volumes: - - hostPath: - path: /openreplay/storage/nfs - type: DirectoryOrCreate - name: datadir +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 diff --git a/tracker/tracker-assist/src/Assist.ts b/tracker/tracker-assist/src/Assist.ts index a407271de..8d5b6c7fd 100644 --- a/tracker/tracker-assist/src/Assist.ts +++ b/tracker/tracker-assist/src/Assist.ts @@ -230,7 +230,7 @@ export default class Assist { endAgentCall(id) }) - socket.on('_agent_name', (id, name) => { + socket.on('_agent_name', (id, name) => { callingAgents.set(id, name) updateCallerNames() }) @@ -285,7 +285,7 @@ export default class Assist { if (callConfirmAnswer) { // Already asking return callConfirmAnswer } - callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || { + callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || { text: this.options.confirmText, style: this.options.confirmStyle, })) // TODO: reuse ? @@ -345,7 +345,7 @@ export default class Assist { // Request local stream for the new connection try { // lStreams are reusable so fare we don't delete them in the `endAgentCall` - if (!lStreams[call.peer]) { + if (!lStreams[call.peer]) { app.debug.log('starting new stream for', call.peer) lStreams[call.peer] = await RequestLocalStream() } @@ -367,8 +367,8 @@ export default class Assist { annot.mount() } // have to be updated - callUI.setLocalStreams(Object.values(lStreams)) - + callUI.setLocalStreams(Object.values(lStreams)) + call.on('error', e => { app.debug.warn('Call error:', e) initiateCallEnd() diff --git a/utilities/utils/helper.js b/utilities/utils/helper.js index b012ccf6c..22fcd0fd5 100644 --- a/utilities/utils/helper.js +++ b/utilities/utils/helper.js @@ -174,6 +174,13 @@ const getValue = function (obj, key) { return undefined; } const sortPaginate = function (list, filters) { + if (typeof (list) === "object" && !Array.isArray(list)) { + for (const [key, value] of Object.entries(list)) { + list[key] = sortPaginate(value, filters); + } + return list + } + const total = list.length; list.sort((a, b) => { const tA = getValue(a, "timestamp");