pull main branch and resolved conflicts
This commit is contained in:
commit
db30edfeb2
12 changed files with 186 additions and 137 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
127
api/chalicelib/blueprints/app/v1_api.py
Normal file
127
api/chalicelib/blueprints/app/v1_api.py
Normal 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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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"))}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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
BIN
static/overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 640 KiB |
Loading…
Add table
Reference in a new issue