diff --git a/README.md b/README.md index df22dcb04..d75f3c3f1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@
@@ -42,11 +42,10 @@ OpenReplay is a session replay stack that lets you see what users do on your web ## Features - **Session replay:** Lets you relive your users' experience, see where they struggle and how it affects their behavior. Each session replay is automatically analyzed based on heuristics, for easy triage. +- **DevTools:** It's like debugging in your own browser. OpenReplay provides you with the full context (network activity, JS errors, store actions/state and 40+ metrics) so you can instantly reproduce bugs and understand performance issues. +- **Assist:** Helps you support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software. - **Omni-search:** Search and filter by almost any user action/criteria, session attribute or technical event, so you can answer any question. No instrumentation required. - **Funnels:** For surfacing the most impactful issues causing conversion and revenue loss. -- **DevTools:** It's like debugging in your own browser. OpenReplay provides you with the full context so you can instantly reproduce bugs and understand performance issues. -- **Error tracking:** JS errors are captured and sync'ed with session replays. Upload your source-maps and see the source code right in the stack trace. -- **Performance metrics:** Ready-to-use dashboard with 40+ metrics to keep an eye on your web app's performance. Alerts keep you notified when critical slowdowns occur. - **Fine-grained privacy controls:** Choose what to capture, what to obscure or what to ignore so user data doesn't even reach your servers. - **Plugins oriented:** Get to the root cause even faster by tracking application state (Redux, VueX, MobX, NgRx) and logging GraphQL queries (Apollo, Relay) and Fetch requests. - **Integrations:** Sync your backend logs with your session replays and see what happened front-to-back. OpenReplay supports Sentry, Datadog, CloudWatch, Stackdriver, Elastic and more. diff --git a/api/app.py b/api/app.py index 254f980ba..01cc65d1a 100644 --- a/api/app.py +++ b/api/app.py @@ -4,7 +4,8 @@ from sentry_sdk import configure_scope from chalicelib import _overrides from chalicelib.blueprints import bp_authorizers -from chalicelib.blueprints import bp_core, bp_core_crons, bp_app_api +from chalicelib.blueprints import bp_core, bp_core_crons +from chalicelib.blueprints.app import v1_api from chalicelib.blueprints import bp_core_dynamic, bp_core_dynamic_crons from chalicelib.blueprints.subs import bp_dashboard from chalicelib.utils import helper @@ -99,5 +100,4 @@ app.register_blueprint(bp_core_crons.app) app.register_blueprint(bp_core_dynamic.app) app.register_blueprint(bp_core_dynamic_crons.app) app.register_blueprint(bp_dashboard.app) -app.register_blueprint(bp_app_api.app) - +app.register_blueprint(v1_api.app) diff --git a/api/chalicelib/blueprints/app/v1_api.py b/api/chalicelib/blueprints/app/v1_api.py new file mode 100644 index 000000000..1d69bb8a3 --- /dev/null +++ b/api/chalicelib/blueprints/app/v1_api.py @@ -0,0 +1,127 @@ +from chalice import Blueprint, Response + +from chalicelib import _overrides +from chalicelib.blueprints import bp_authorizers +from chalicelib.core import sessions, events, jobs, projects +from chalicelib.utils.TimeUTC import TimeUTC + +app = Blueprint(__name__) +_overrides.chalice_app(app) + + +@app.route('/v1/{projectKey}/users/{userId}/sessions', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) +def get_user_sessions(projectKey, userId, context): + projectId = projects.get_internal_project_id(projectKey) + params = app.current_request.query_params + + if params is None: + params = {} + + return { + 'data': sessions.get_user_sessions( + project_id=projectId, + user_id=userId, + start_date=params.get('start_date'), + end_date=params.get('end_date') + ) + } + + +@app.route('/v1/{projectKey}/sessions/{sessionId}/events', methods=['GET'], + authorizer=bp_authorizers.api_key_authorizer) +def get_session_events(projectKey, sessionId, context): + projectId = projects.get_internal_project_id(projectKey) + return { + 'data': events.get_by_sessionId2_pg( + project_id=projectId, + session_id=sessionId + ) + } + + +@app.route('/v1/{projectKey}/users/{userId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) +def get_user_details(projectKey, userId, context): + projectId = projects.get_internal_project_id(projectKey) + return { + 'data': sessions.get_session_user( + project_id=projectId, + user_id=userId + ) + } + pass + + +@app.route('/v1/{projectKey}/users/{userId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer) +def schedule_to_delete_user_data(projectKey, userId, context): + projectId = projects.get_internal_project_id(projectKey) + data = app.current_request.json_body + + data["action"] = "delete_user_data" + data["reference_id"] = userId + data["description"] = f"Delete user sessions of userId = {userId}" + data["start_at"] = TimeUTC.to_human_readable(TimeUTC.midnight(1)) + record = jobs.create(project_id=projectId, data=data) + return { + 'data': record + } + + +@app.route('/v1/{projectKey}/jobs', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) +def get_jobs(projectKey, context): + projectId = projects.get_internal_project_id(projectKey) + return { + 'data': jobs.get_all(project_id=projectId) + } + pass + + +@app.route('/v1/{projectKey}/jobs/{jobId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) +def get_job(projectKey, jobId, context): + return { + 'data': jobs.get(job_id=jobId) + } + pass + + +@app.route('/v1/{projectKey}/jobs/{jobId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer) +def cancel_job(projectKey, jobId, context): + job = jobs.get(job_id=jobId) + job_not_found = len(job.keys()) == 0 + + if job_not_found or job["status"] == jobs.JobStatus.COMPLETED or job["status"] == jobs.JobStatus.CANCELLED: + return Response(status_code=501, body="The request job has already been canceled/completed (or was not found).") + + job["status"] = "cancelled" + return { + 'data': jobs.update(job_id=jobId, job=job) + } + +@app.route('/v1/projects', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) +def get_projects(context): + records = projects.get_projects(tenant_id=context['tenantId']) + for record in records: + del record['projectId'] + + return { + 'data': records + } + + +@app.route('/v1/projects/{projectKey}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) +def get_project(projectKey, context): + return { + 'data': projects.get_project_by_key(tenant_id=context['tenantId'], project_key=projectKey) + } + + +@app.route('/v1/projects', methods=['POST'], authorizer=bp_authorizers.api_key_authorizer) +def create_project(context): + data = app.current_request.json_body + record = projects.create( + tenant_id=context['tenantId'], + user_id=None, + data=data, + skip_authorization=True + ) + del record['data']['projectId'] + return record diff --git a/api/chalicelib/blueprints/bp_app_api.py b/api/chalicelib/blueprints/bp_app_api.py deleted file mode 100644 index 435d09752..000000000 --- a/api/chalicelib/blueprints/bp_app_api.py +++ /dev/null @@ -1,93 +0,0 @@ -from chalice import Blueprint - -from chalicelib import _overrides -from chalicelib.blueprints import bp_authorizers -from chalicelib.core import sessions, events, jobs -from chalicelib.utils.TimeUTC import TimeUTC - -app = Blueprint(__name__) -_overrides.chalice_app(app) - - -@app.route('/app/{projectId}/users/{userId}/sessions', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) -def get_user_sessions(projectId, userId, context): - params = app.current_request.query_params - - if params is None: - params = {} - - return { - 'data': sessions.get_user_sessions( - project_id=projectId, - user_id=userId, - start_date=params.get('start_date'), - end_date=params.get('end_date') - ) - } - - -@app.route('/app/{projectId}/sessions/{sessionId}/events', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) -def get_session_events(projectId, sessionId, context): - return { - 'data': events.get_by_sessionId2_pg( - project_id=projectId, - session_id=sessionId - ) - } - - -@app.route('/app/{projectId}/users/{userId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) -def get_user_details(projectId, userId, context): - return { - 'data': sessions.get_session_user( - project_id=projectId, - user_id=userId - ) - } - pass - - -@app.route('/app/{projectId}/users/{userId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer) -def schedule_to_delete_user_data(projectId, userId, context): - data = app.current_request.json_body - - data["action"] = "delete_user_data" - data["reference_id"] = userId - data["description"] = f"Delete user sessions of userId = {userId}" - data["start_at"] = TimeUTC.to_human_readable(TimeUTC.midnight(1)) - record = jobs.create(project_id=projectId, data=data) - return { - 'data': record - } - - -@app.route('/app/{projectId}/jobs', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) -def get_jobs(projectId, context): - return { - 'data': jobs.get_all(project_id=projectId) - } - pass - - -@app.route('/app/{projectId}/jobs/{jobId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer) -def get_job(projectId, jobId, context): - return { - 'data': jobs.get(job_id=jobId) - } - pass - - -@app.route('/app/{projectId}/jobs/{jobId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer) -def cancel_job(projectId, jobId, context): - job = jobs.get(job_id=jobId) - job_not_found = len(job.keys()) == 0 - if job_not_found or job["status"] == jobs.JobStatus.COMPLETED: - return { - 'errors': ["Job doesn't exists." if job_not_found else "Job is already completed."] - } - - job["status"] = "cancelled" - return { - 'data': jobs.update(job_id=jobId, job=job) - } - diff --git a/api/chalicelib/core/projects.py b/api/chalicelib/core/projects.py index d85e9c8ca..01c87468b 100644 --- a/api/chalicelib/core/projects.py +++ b/api/chalicelib/core/projects.py @@ -95,11 +95,32 @@ def get_project(tenant_id, project_id, include_last_session=False, include_gdpr= row = cur.fetchone() return helper.dict_to_camel_case(row) +def get_project_by_key(tenant_id, project_key, include_last_session=False, include_gdpr=None): + with pg_client.PostgresClient() as cur: + query = cur.mogrify(f"""\ + SELECT + s.project_key, + s.name + {",(SELECT max(ss.start_ts) FROM public.sessions AS ss WHERE ss.project_key = %(project_key)s) AS last_recorded_session_at" if include_last_session else ""} + {',s.gdpr' if include_gdpr else ''} + FROM public.projects AS s + where s.project_key =%(project_key)s + AND s.deleted_at IS NULL + LIMIT 1;""", + {"project_key": project_key}) -def create(tenant_id, user_id, data): - admin = users.get(user_id=user_id, tenant_id=tenant_id) - if not admin["admin"] and not admin["superAdmin"]: - return {"errors": ["unauthorized"]} + cur.execute( + query=query + ) + row = cur.fetchone() + return helper.dict_to_camel_case(row) + + +def create(tenant_id, user_id, data, skip_authorization=False): + if not skip_authorization: + admin = users.get(user_id=user_id, tenant_id=tenant_id) + if not admin["admin"] and not admin["superAdmin"]: + return {"errors": ["unauthorized"]} return {"data": __create(tenant_id=tenant_id, name=data.get("name", "my first project"))} diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js index 755718ef0..fb52d0e5f 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -92,7 +92,7 @@ const ProjectCodeSnippet = props => {