pull main branch and resolved conflicts

This commit is contained in:
Shekar Siri 2021-07-14 20:59:43 +05:30
commit db30edfeb2
12 changed files with 186 additions and 137 deletions

View file

@ -27,7 +27,7 @@
<p align="center">
<a href="https://github.com/openreplay/openreplay">
<img src="static/replayer.png">
<img src="static/overview.png">
</a>
</p>
@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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"))}

View file

@ -92,7 +92,7 @@ const ProjectCodeSnippet = props => {
<div>
<div className="mb-4">
<div className="font-semibold mb-2 flex items-center">
<CircleNumber text="1" /> Choose data recording options:
<CircleNumber text="1" /> Choose data recording options
</div>
<div className="flex items-center ml-10">
<Select
@ -149,7 +149,7 @@ const ProjectCodeSnippet = props => {
</Highlight>
</div>
{/* TODO Extract for SaaS */}
<div className="my-4">You can also setup OpenReplay using <a className="link" href="https://docs.openreplay.com/integrations/google-tag-manager" target="_blank">Google Tag Manager (GTM)</a> or <a className="link" href="https://docs.openreplay.com/integrations/segment" target="_blank">Segment</a>. </div>
<div className="my-4">You can also setup OpenReplay using <a className="link" href="https://docs.openreplay.com/integrations/google-tag-manager" target="_blank">Google Tag Manager (GTM)</a>.</div>
</div>
)
}

View file

@ -2,7 +2,7 @@ import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { setAutoplayValues } from 'Duck/sessions'
import { session as sessionRoute } from 'App/routes';
import { Link, Icon, Slider } from 'UI';
import { Link, Icon, Slider, Tooltip } from 'UI';
import { connectPlayer } from 'Player/store';
import { Controls as PlayerControls } from 'Player';
@ -18,12 +18,18 @@ function Autoplay(props) {
<Link to={ sessionRoute(previousId) } disabled={!previousId}>
<Icon name="prev1" size="20" color="teal" />
</Link>
<Slider
name="sessionsLive"
onChange={ props.toggleAutoplay }
checked={ autoplay }
style={{ margin: '0 10px' }}
<Tooltip
trigger={
<Slider
name="sessionsLive"
onChange={ props.toggleAutoplay }
checked={ autoplay }
style={{ margin: '0 10px' }}
/>
}
tooltip={'Autoplay'}
/>
<Link to={ sessionRoute(nextId) } siteId={ 1 } disabled={!nextId}>
<Icon name="next1" size="20" color="teal" />
</Link>

View file

@ -135,7 +135,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex && prevProps.activeIndex !== this.props.activeIndex) {
if (this.props.activeIndex && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current != null) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}

View file

@ -90,7 +90,7 @@ const ProjectCodeSnippet = props => {
return (
<div>
<div className="mb-4">
<div className="font-semibold mb-2">1. Choose data recording options:</div>
<div className="font-semibold mb-2">1. Choose data recording options</div>
<div className="flex items-center justify-between">
<Select
name="defaultInputMode"
@ -144,7 +144,7 @@ const ProjectCodeSnippet = props => {
}}
/>
</div>
<div className="my-4">You can also setup OpenReplay using <a className="link" href="https://docs.openreplay.com/integrations/google-tag-manager" target="_blank">Google Tag Manager (GTM)</a> or <a className="link" href="https://docs.openreplay.com/integrations/segment" target="_blank">Segment</a>. </div>
<div className="my-4">You can also setup OpenReplay using <a className="link" href="https://docs.openreplay.com/integrations/google-tag-manager" target="_blank">Google Tag Manager (GTM)</a>. </div>
</div>
)
}

View file

@ -23,15 +23,6 @@ const oss = {
TRACKER_VERSION: '3.0.5', // trackerInfo.version,
}
const local = {
...oss,
API_EDP: 'https://sacha.openreplay.com/api',//'https://do.openreplay.com/api',
ASSETS_HOST: 'https://sacha.openreplay.com/assets',//'https://do.openreplay.com/assets',
SOURCEMAP: false,
PRODUCTION: false,
}
module.exports = {
oss,
local,
oss,
};

View file

@ -1,35 +1,33 @@
# sourcemaps-uploader
# sourcemap-uploader
A NPM module to upload your JS sourcemaps files to [OpenReplay](https://openreplay.com/).
An NPM module to upload your JS sourcemap files to your OpenReplay instance.
## Installation
```
npm i -D @openreplay/sourcemap-uploader
npm i -D @openreplay/sourcemap-uploader
```
## CLI
Upload sourcemap for one file
Upload sourcemap for one file:
```
sourcemap-uploader -k API_KEY -p PROJECT_KEY file -m ./dist/index.js.map -u https://myapp.com/index.js
sourcemap-uploader -s https://opnereplay.mycompany.com/api -k API_KEY -p PROJECT_KEY file -m ./dist/index.js.map -u https://myapp.com/index.js
```
Upload all sourcemaps in the directory. The url must correspond to the root where you upload JS files from the directory.
Thus, if you have your `app-42.js` along with the `app-42.js.map` in the `./build` folder and then want to upload it to you server (you might want to avoid uploading soursemaps) so it can be reachable through the link `https://myapp.com/static/app-42.js`, the command would be the next:
Upload all sourcemaps in a given directory. The URL must correspond to the root where you upload JS files from the directory. In other words, if you have your `app-42.js` along with the `app-42.js.map` in the `./build` folder and then want to upload it to your OpenReplay instance so it can be reachable through the link `https://myapp.com/static/app-42.js`, then the command should be like:
```
sourcemap-uploader -k API_KEY -p PROJECT_KEY dir -m ./build -u https://myapp.com/static
sourcemap-uploader -s https://opnereplay.mycompany.com/api -k API_KEY -p PROJECT_KEY dir -m ./build -u https://myapp.com/static
```
Use `-v` (`--verbose`) key to see the logs.
- Use `-s` (`--server`) to specify the URL of your OpenReplay instance (make to append it with /api)
- Use `-v` (`--verbose`) to see the logs.
## NPM
There are two functions inside `index.js` of the package
There are two functions inside `index.js` of the package:
```
uploadFile(api_key, project_key, sourcemap_file_path, js_file_url)

BIN
static/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB