fix(ui) - main to dev conflicts

This commit is contained in:
Shekar Siri 2021-12-04 02:19:16 +05:30
commit f7f70589c3
65 changed files with 623 additions and 193 deletions

71
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '30 6 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

75
CLA.md Normal file
View file

@ -0,0 +1,75 @@
## Individual and Entity Contributor License Agreement (CLA)
Thank you for your interest in contributing to software projects managed by Asayer, Inc. (“We” or “Us”). This Contributor License Agreement (“Agreement”) documents the rights granted by contributors to Us. This Agreement is for your protection as a contributor as well as for our protection; it does not change your rights to use your own Contributions for any other purpose. To make this document effective, please read the Agreement carefully and then sign it. By signing this Agreement (including by clicking “I agree” or "Sign in with GitHub to agree" and submitting it to us electronically), You are creating a legally binding contract which becomes effective upon your signature or agreement. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. This Agreement covers your present, and all future, Contributions from You, and may cover more than one software project managed by Us.
### 1. Definitions
“Affiliates” means any Legal Entities that control, are controlled by, or under common control with another Legal Entity. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities which vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity.
“Contribution” means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please contact us before Submitting the Contribution.
“Copyright” means all rights protecting works of authorship owned or controlled by You or your Affiliates (as may be applicable), including copyright, moral and related (or neighboring) rights, as appropriate, for the full term of their existence, including any extensions by You.
“Legal Entity” means an entity which is not a natural person.
“Media” means any portion of a Contribution which is not software.
“Submit” means any form of electronic or written communication, or recorded verbal communication, sent to Us or our representatives at a destination (including websites) that we own or control or that is otherwise registered to us, including but not limited to electronic mailing lists, source code control systems, instant messages or similar communications, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Work, but excluding any communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.”
“Work” means any of the products or projects owned or managed by Us, and any work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, Work means the work of authorship to which your Contribution was Submitted. After You Submit the Contribution, it may be included in the Work.
“You” means (i) the individual who Submits a Contribution to Us, if You are an individual acting on your own behalf, or (ii) the Legal Entity on behalf of whom you Submit a Contribution to Us if you are are Submitting any Contribution on behalf of any entity.
### 2. Grant of Rights
#### 2.1 Copyright License
(a) Except for the license granted to Us in this Agreement, You reserve all right, title, and interest in and to Your Contributions. That means that you can keep doing whatever you want with your Contribution, and you can license it to anyone you want under any terms you want.
(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, no charge and royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform, sublicense and distribute the Contribution as part of the Work; provided that this license is conditioned upon compliance with Section 2.3.
#### 2.2 Patent License
For patent claims, including without limitation method, process, and apparatus claims which You (or Your Affiliates, as may be applicable) own, control or have the right to grant, now or in the future, You grant to Us, and to recipients of software distributed by the Us, a perpetual, worldwide, non-exclusive, transferable, no charge and royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution (and the Contribution in combination with the Work, and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. If any person or entity institutes patent litigation against Contributor or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Contributions, or the Work to which the Contributions were submitted, constitutes direct or contributory patent infringement, then any patent licenses granted under this Agreement for that Contribution to the person or entity instituting the litigation, or the Work to which the Contributions were submitted, shall terminate as of the date such litigation is filed.
#### 2.3 Outbound License
Based on the grant of rights in Sections 2.1 (meaning, no matter what, you can keep licensing your Contribution to others however you want) and 2.2, if We include Your Contribution in any Work, and if We determine that it is appropriate for the purpose of commercializing any Work or any project under Our control, we may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also use reasonable efforts to continue to license Your Contribution (in the same or other projects or Works) under the terms of the license or licenses under which you Submitted Your Contribution.
#### 2.4 Moral Rights
We agree to comply with applicable laws regarding your Contribution, including copyright laws and law related to moral rights. If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect.
#### 2.5 Our Rights
You acknowledge that We are not obligated to use Your Contribution as part of any Work, and that we and may decide to include any Contribution We consider appropriate.
#### 2.6 Reservation of Rights
Any rights in Your Contribution not expressly licensed under this Agreement are expressly reserved by You.
### 3. Representations
You represent (promise) that You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that you create that includes your Contributions, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for your Contributions to Us, or that Your employer has executed a separate Corporate Contributor License Agreement with Us.
You further represent that each of Your Contributions is Your original creation. You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
You agree to notify Us of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
### 4. Disclaimer
EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, YOUR CONTRIBUTION IS PROVIDED "AS IS". YOU EXPRESSLY DISCLAIM ALL OTHER EXPRESS WARRANTIES AND ALL IMPLIED WARRANTIES. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW.
### 5. Miscellaneous
5.1 This Agreement will be governed by and construed in accordance with the laws of the State of Delaware, without regard to conflicts of law provisions. If any provision of this Agreement shall be adjudged by any court of competent jurisdiction to be unenforceable or invalid, that provision shall be limited or eliminated to the minimum extent necessary so that this Agreement shall otherwise remain in full force and effect and enforceable. The sole venue for all disputes relating to this Agreement shall be in the New Castle County, Delaware (USA). The rights and obligations of the parties under this Agreement shall not be governed by the 1980 U.N. Convention on Contracts for the International Sale of Goods.
5.2 This Agreement may be amended only by a written document signed by the party against whom enforcement is sought.
5.3 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety.
5.4 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
**This Agreement contains the entire understanding of the parties regarding the subject matter of this Agreement and supersedes all prior and contemporaneous negotiations and agreements, whether written or oral, between the parties with respect to the subject matter of this Agreement.**
By signing this agreement, Contributor accepts and agrees to the preceding terms and conditions for Contributors present and future Contributions submitted to Us.

View file

@ -6,7 +6,7 @@ By participating in this project, you are expected to uphold our [Code of Conduc
## First-time Contributors
We appreciate all contributions, especially those coming from first time contributors. Good first issues is the best way start. If you're not sure how to help, feel free to reach out anytime for assistance via [email](mailto:hey@openreplay.com) or [Slack](https://slack.openreplay.com).
We appreciate all contributions, especially those coming from first time contributors. Good first issues is the best way start. If you're not sure how to help, feel free to reach out anytime for assistance via [email](mailto:hey@openreplay.com) or [Slack](https://slack.openreplay.com). All contributors must approve our [Contributor License Agreement](https://cla-assistant.io/openreplay/openreplay).
## Areas for Contributing
@ -70,5 +70,6 @@ We try to answer the below questions when reviewing a PR:
- How will it perform with millions of sessions and users events?
- Has it been tested?
- Is it introducing any security flaws?
- Did the contributor approve our CLA?
Once your PR passes, we will merge it. Otherwise, we'll politely ask you to make a change.

View file

@ -54,7 +54,7 @@
"S3_SECRET": "",
"invitation_link": "/api/users/invitation?token=%s",
"change_password_link": "/reset-password?invitation=%s&&pass=%s",
"version_number": "1.2.0"
"version_number": "1.3.5"
},
"lambda_timeout": 150,
"lambda_memory_size": 400,

View file

@ -55,7 +55,7 @@
"invitation_link": "/api/users/invitation?token=%s",
"change_password_link": "/reset-password?invitation=%s&&pass=%s",
"iosBucket": "openreplay-ios-images",
"version_number": "1.2.0"
"version_number": "1.3.6"
},
"lambda_timeout": 150,
"lambda_memory_size": 400,

View file

@ -1,23 +1,22 @@
from chalice import Blueprint, Response
from chalicelib import _overrides
from chalicelib.core import metadata, errors_favorite_viewed, slack, alerts, sessions, integration_github, \
integrations_manager
from chalicelib.utils import captcha
from chalicelib.utils import helper
from chalicelib.utils.helper import environ
from chalicelib.core import tenants
from chalicelib.core import signup
from chalicelib.core import users
from chalicelib.core import projects
from chalicelib.core import errors
from chalicelib.core import notifications
from chalicelib.core import boarding
from chalicelib.core import errors
from chalicelib.core import license
from chalicelib.core import metadata, errors_favorite_viewed, slack, alerts, sessions, integrations_manager, assist
from chalicelib.core import notifications
from chalicelib.core import projects
from chalicelib.core import signup
from chalicelib.core import tenants
from chalicelib.core import users
from chalicelib.core import webhook
from chalicelib.core import license
from chalicelib.core import assist
from chalicelib.core.collaboration_slack import Slack
from chalicelib.utils import captcha
from chalicelib.utils import helper
from chalicelib.utils.helper import environ
app = Blueprint(__name__)
_overrides.chalice_app(app)
@ -48,7 +47,7 @@ def login():
c["projects"] = projects.get_projects(tenant_id=tenant_id, recording_state=True, recorded=True,
stack_integrations=True, version=True)
c["smtp"] = helper.has_smtp()
c["iceServers"]= assist.get_ice_servers()
c["iceServers"] = assist.get_ice_servers()
return {
'jwt': r.pop('jwt'),
'data': {

View file

@ -1,6 +1,8 @@
import requests
from chalicelib.core import projects, sessions, sessions_metas
from chalicelib.utils import pg_client, helper
from chalicelib.core import projects, sessions, sessions_metas
from chalicelib.utils import pg_client, helper
from chalicelib.utils.helper import environ
SESSION_PROJECTION_COLS = """s.project_id,

View file

@ -1,10 +1,11 @@
import json
import secrets
from chalicelib.core import authorizers, metadata, projects
from chalicelib.core import authorizers, metadata, projects, assist
from chalicelib.core import tenants
from chalicelib.utils import dev
from chalicelib.utils import helper
from chalicelib.utils import pg_client
from chalicelib.utils import dev
from chalicelib.utils.TimeUTC import TimeUTC
from chalicelib.utils.helper import environ

1
ee/api/.gitignore vendored
View file

@ -210,6 +210,7 @@ Pipfile
/chalicelib/core/sessions_favorite_viewed.py
/chalicelib/core/sessions_metas.py
/chalicelib/core/sessions_mobs.py
/chalicelib/core/sessions.py
/chalicelib/core/significance.py
/chalicelib/core/slack.py
/chalicelib/core/socket_ios.py

View file

@ -37,7 +37,7 @@ def login():
tenant_id = r.pop("tenantId")
# change this in open-source
r["limits"] = {
"teamMember": -1,
"teamMember": int(environ.get("numberOfSeats", 0)),
"projects": -1,
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
@ -64,7 +64,7 @@ def get_account(context):
'data': {
**r,
"limits": {
"teamMember": -1,
"teamMember": int(environ.get("numberOfSeats", 0)),
"projects": -1,
"metadata": metadata.get_remaining_metadata_with_count(context['tenantId'])
},

View file

@ -1,3 +1,4 @@
from chalicelib.utils.helper import environ
from chalicelib.utils import pg_client
from chalicelib.core import unlock
@ -14,7 +15,8 @@ def get_status(tenant_id):
"edition": r.get("edition", "").upper(),
"versionNumber": r.get("version_number", ""),
"license": license[0:2] + "*" * (len(license) - 4) + license[-2:],
"expirationDate": unlock.get_expiration_date()
"expirationDate": unlock.get_expiration_date(),
"teamMember": int(environ.get("numberOfSeats", 0))
},
"count": {
"teamMember": r.get("t_users"),

View file

@ -13,9 +13,14 @@ def get_license():
def check():
license=get_license()
license = get_license()
if license is None or len(license) == 0:
print("!! license key not found, please provide a LICENSE_KEY env var")
environ["expiration"] = "-1"
environ["numberOfSeats"] = "0"
return
print(f"validating: {license}")
r = requests.post('https://parrot.asayer.io/os/license', json={"mid": __get_mid(), "license": get_license()})
r = requests.post('https://api.openreplay.com/os/license', json={"mid": __get_mid(), "license": get_license()})
if r.status_code != 200 or "errors" in r.json() or not r.json()["data"].get("valid"):
print("license validation failed")
print(r.text)
@ -23,6 +28,8 @@ def check():
else:
environ["expiration"] = str(r.json()["data"].get("expiration"))
environ["lastCheck"] = str(TimeUTC.now())
if r.json()["data"].get("numberOfSeats") is not None:
environ["numberOfSeats"] = str(r.json()["data"]["numberOfSeats"])
def get_expiration_date():

View file

@ -1,8 +1,7 @@
import json
import secrets
from chalicelib.core import assist
from chalicelib.core import authorizers, metadata, projects
from chalicelib.core import authorizers, metadata, projects, assist
from chalicelib.core import tenants
from chalicelib.utils import dev, SAML2_helper
from chalicelib.utils import helper

View file

@ -0,0 +1,10 @@
BEGIN;
CREATE INDEX projects_tenant_id_idx ON projects (tenant_id);
CREATE INDEX webhooks_tenant_id_idx ON webhooks (tenant_id);
CREATE INDEX pages_session_id_timestamp_idx ON events.pages (session_id, timestamp);
CREATE INDEX issues_project_id_idx ON issues (project_id);
CREATE INDEX jobs_project_id_idx ON jobs (project_id);
COMMIT;

View file

@ -267,6 +267,7 @@ $$
);
CREATE INDEX ON public.projects (project_key);
CREATE INDEX projects_tenant_id_idx ON projects (tenant_id);
-- --- alerts.sql ---
@ -318,6 +319,8 @@ $$
name varchar(100)
);
CREATE INDEX webhooks_tenant_id_idx ON webhooks (tenant_id);
-- --- notifications.sql ---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -7,9 +7,11 @@ import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream
interface Props {
stream: LocalStream | null,
endCall: () => void
endCall: () => void,
videoEnabled: boolean,
setVideoEnabled: (boolean) => void
}
function ChatControls({ stream, endCall } : Props) {
function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props) {
const [audioEnabled, setAudioEnabled] = useState(true)
const [videoEnabled, setVideoEnabled] = useState(false)

View file

@ -1,4 +1,4 @@
import React, { useState, FC } from 'react'
import React, { useState, FC, useEffect } from 'react'
import VideoContainer from '../components/VideoContainer'
import { Icon, Popup, Button } from 'UI'
import cn from 'classnames'
@ -10,14 +10,32 @@ import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream
export interface Props {
incomeStream: MediaStream | null,
remoteStream: MediaStream | null,
localStream: LocalStream | null,
userId: String,
endCall: () => void
}
const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localStream, endCall }) {
const [minimize, setMinimize] = useState(false)
const ChatWindow: FC<Props> = function ChatWindow({ userId, remoteStream, localStream, endCall }) {
const [localVideoEnabled, setLocalVideoEnabled] = useState(false)
const [remoteVideoEnabled, setRemoteVideoEnabled] = useState(false)
useEffect(() => {
if (!remoteStream) { return }
const iid = setInterval(() => {
const settings = remoteStream.getVideoTracks()[0]?.getSettings()
const isDummyVideoTrack = !!settings ? (settings.width === 2 || settings.frameRate === 0) : true
console.log(isDummyVideoTrack, settings)
const shouldBeEnabled = !isDummyVideoTrack
if (shouldBeEnabled !== localVideoEnabled) {
setRemoteVideoEnabled(shouldBeEnabled)
}
}, 1000)
return () => clearInterval(iid)
}, [ remoteStream, localVideoEnabled ])
const minimize = !localVideoEnabled && !remoteVideoEnabled
return (
<Draggable handle=".handle" bounds="body">
@ -30,12 +48,12 @@ const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localS
<Counter startTime={new Date().getTime() } className="text-sm ml-auto" />
</div>
<div className={cn(stl.videoWrapper, {'hidden' : minimize}, 'relative')}>
<VideoContainer stream={ incomeStream } />
<VideoContainer stream={ remoteStream } />
<div className="absolute bottom-0 right-0 z-50">
<VideoContainer stream={ localStream ? localStream.stream : null } muted width={50} />
</div>
</div>
<ChatControls stream={localStream} endCall={endCall} />
<ChatControls videoEnabled={localVideoEnabled} setVideoEnabled={setLocalVideoEnabled} stream={localStream} endCall={endCall} />
</div>
</Draggable>
)

View file

@ -11,6 +11,7 @@ import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream';
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
import { toast } from 'react-toastify';
import { confirm } from 'UI/Confirmation';
import stl from './AassistActions.css'
function onClose(stream) {
@ -36,8 +37,8 @@ interface Props {
isEnterprise: boolean,
}
function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus, remoteControlEnabled, hasPermission, isEnterprise }: Props) {
const [ incomeStream, setIncomeStream ] = useState<MediaStream | null>(null);
function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus, remoteControlEnabled, hasPermission, isEnterprise }: Props) {
const [ remoteStream, setRemoteStream ] = useState<MediaStream | null>(null);
const [ localStream, setLocalStream ] = useState<LocalStream | null>(null);
const [ callObject, setCallObject ] = useState<{ end: ()=>void, toggleRemoteControl: ()=>void } | null >(null);
@ -51,7 +52,6 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
}
}, [peerConnectionStatus])
function call() {
RequestLocalStream().then(lStream => {
setLocalStream(lStream);
@ -63,6 +63,15 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
onError
));
}).catch(onError)
const confirmCall = async () => {
if (await confirm({
header: 'Start Call',
confirmButton: 'Call',
confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`
})) {
call()
}
}
const inCall = calling !== CallingState.False;
@ -80,7 +89,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
{[stl.disabled]: cannotCall}
)
}
onClick={ inCall ? callObject?.end : call}
onClick={ inCall ? callObject?.end : confirmCall}
role="button"
>
<Icon
@ -91,7 +100,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
<span className={cn("ml-2", { 'color-red' : inCall })}>{ inCall ? 'End Call' : 'Call' }</span>
</div>
}
content={ `Call ${userId}` }
content={ `Call ${userId ? userId : 'User'}` }
size="tiny"
inverted
position="top right"
@ -115,7 +124,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
</div>
}
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
{ inCall && callObject && <ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} /> }
{ inCall && callObject && <ChatWindow endCall={callObject.end} userId={userId} remoteStream={remoteStream} localStream={localStream} /> }
</div>
</div>
)

View file

@ -5,7 +5,7 @@ import {
fetchFavoriteList as fetchFavoriteSessionList
} from 'Duck/sessions';
import { countries } from 'App/constants';
import { applyFilter, clearEvents } from 'Duck/filters';
import { applyFilter, clearEvents, addAttribute } from 'Duck/filters';
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
import { defaultFilters, preloadedFilters } from 'Types/filter';
import { KEYS } from 'Types/filter/customFilter';
@ -34,6 +34,21 @@ const weakEqual = (val1, val2) => {
return `${ val1 }` === `${ val2 }`;
}
const allowedQueryKeys = [
'userOs',
'userId',
'userBrowser',
'userDevice',
'userCountry',
'startDate',
'endDate',
'minDuration',
'maxDuration',
'referrer',
'sort',
'order',
];
@withLocationHandlers()
@connect(state => ({
filter: state.getIn([ 'filters', 'appliedFilter' ]),
@ -50,6 +65,7 @@ const weakEqual = (val1, val2) => {
}), {
fetchFavoriteSessionList,
applyFilter,
addAttribute,
fetchFilterVariables,
fetchIntegrationVariables,
fetchSources,
@ -89,10 +105,14 @@ export default class BugFinder extends React.PureComponent {
};
});
this.props.resetFunnel();
this.props.resetFunnelFilters();
props.resetFunnel();
props.resetFunnelFilters();
props.fetchFunnelsList(LAST_7_DAYS)
const queryFilter = this.props.query.all(allowedQueryKeys);
if (queryFilter.hasOwnProperty('userId')) {
props.addAttribute({ label: 'User Id', key: KEYS.USERID, type: KEYS.USERID, operator: 'is', value: queryFilter.userId })
}
}
componentDidMount() {

View file

@ -5,20 +5,25 @@ import { NoContent, Loader } from 'UI';
import { List, Map } from 'immutable';
import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters';
import Filter from 'Types/filter';
const AUTOREFRESH_INTERVAL = 1 * 60 * 1000
const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
interface Props {
loading: Boolean,
list?: List<any>,
fetchList: (params) => void,
applyFilter: () => void,
filters: List<any>
filters: Filter
addAttribute: (obj) => void,
}
function LiveSessionList(props: Props) {
const { loading, list, filters } = props;
var timeoutId;
const hasUserFilter = filters && filters.filters.map(i => i.key).includes(KEYS.USERID);
useEffect(() => {
props.fetchList(filters.toJS());
@ -28,6 +33,16 @@ function LiveSessionList(props: Props) {
}
}, [])
const onUserClick = (userId, userAnonymousId) => {
if (userId) {
props.addAttribute({ label: 'User Id', key: KEYS.USERID, type: KEYS.USERID, operator: 'is', value: userId })
} else {
props.addAttribute({ label: 'Anonymous ID', key: 'USERANONYMOUSID', type: "USERANONYMOUSID", operator: 'is', value: userAnonymousId })
}
props.applyFilter()
}
const timeout = () => {
timeoutId = setTimeout(() => {
props.fetchList(filters.toJS());
@ -52,7 +67,9 @@ function LiveSessionList(props: Props) {
<SessionItem
key={ session.sessionId }
session={ session }
live
live
hasUserFilter={hasUserFilter}
onUserClick={onUserClick}
/>
))}
</Loader>
@ -68,6 +85,5 @@ export default withPermissions(['ASSIST_LIVE'])(connect(
filters: state.getIn([ 'filters', 'appliedFilter' ]),
}),
{
fetchList
}
fetchList, applyFilter, addAttribute }
)(LiveSessionList));

View file

@ -1,18 +1,18 @@
import React, { useEffect } from 'react'
import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI'
import { SideMenuitem, SavedSearchList, Progress, Popup, Icon, CircularLoader } from 'UI'
import stl from './sessionMenu.css';
import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { setActiveFlow, clearEvents } from 'Duck/filters';
import { setActiveTab } from 'Duck/sessions';
import { issues_types } from 'Types/session/issue'
import NewBadge from 'Shared/NewBadge';
import { fetchList as fetchSessionList } from 'Duck/sessions';
function SessionsMenu(props) {
const {
activeFlow, activeTab, watchdogs = [], keyMap, wdTypeCount,
fetchWatchdogStatus, toggleRehydratePanel } = props;
fetchWatchdogStatus, toggleRehydratePanel, filters, sessionsLoading } = props;
const onMenuItemClick = (filter) => {
props.onMenuItemClick(filter)
@ -76,10 +76,19 @@ function SessionsMenu(props) {
<div className={stl.divider} />
<div className="my-3">
<SideMenuitem
title={ <div className="flex items-center">
<div>Assist</div>
<div className="ml-2">{ <NewBadge />}</div>
</div> }
title={
<div className="flex items-center">
<div>Assist</div>
{ activeTab.type === 'live' && (
<div
className="ml-4 h-5 w-6 flex items-center justify-center"
onClick={() => !sessionsLoading && props.fetchSessionList(filters.toJS())}
>
{ sessionsLoading ? <CircularLoader className="ml-1" /> : <Icon name="sync-alt" size="14" />}
</div>
)}
</div>
}
iconName="person"
active={activeTab.type === 'live'}
onClick={() => onMenuItemClick({ name: 'Assist', type: 'live' })}
@ -109,6 +118,8 @@ export default connect(state => ({
wdTypeCount: state.getIn([ 'sessions', 'wdTypeCount' ]),
activeFlow: state.getIn([ 'filters', 'activeFlow' ]),
captureRate: state.getIn(['watchdogs', 'captureRate']),
filters: state.getIn([ 'filters', 'appliedFilter' ]),
sessionsLoading: state.getIn([ 'sessions', 'loading' ]),
}), {
fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab
fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab, fetchSessionList
})(SessionsMenu);

View file

@ -23,7 +23,7 @@ function AssistScript(props) {
r.issue=function(k,p){r.push([6,k,p])};
r.isActive=function(){return false};
r.getSessionToken=function(){};
})(0, "${props.projectKey}", "//static.openreplay.com/3.3.1/openreplay-assist.js",1,28);
})(0, "${props.projectKey}", "//static.openreplay.com/3.4.0/openreplay-assist.js",1,28);
</script>`}
</Highlight>
</div>

View file

@ -7,7 +7,7 @@ const BugsnagForm = (props) => (
<>
<div className="p-5 border-b mb-4">
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/datadog" />
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
</div>
<IntegrationForm
{ ...props }

View file

@ -196,7 +196,7 @@ class ManageUsers extends React.PureComponent {
<React.Fragment>
<Loader loading={ loading }>
<SlideModal
title="Inivte People"
title="Invite People"
size="small"
isDisplayed={ showModal }
content={ this.formContent() }

View file

@ -196,9 +196,9 @@ class Sites extends React.PureComponent {
<Icon name="trash" size="16" color="teal" />
</button>
<button
className={cn({'hidden' : !canDeleteSites})}
disabled={ !canDeleteSites }
onClick={ () => canDeleteSites && this.edit(_site) }
className={cn({'hidden' : !isAdmin})}
disabled={ !isAdmin }
onClick={ () => isAdmin && this.edit(_site) }
data-clickable
>
<Icon name="edit" size="16" color="teal"/>

View file

@ -21,6 +21,7 @@
&:hover {
& .actions button {
display: unset;
opacity: 1 !important;
}
}
@ -39,6 +40,10 @@
opacity: 0.3;
}
}
& button:not(:last-child) {
opacity: 0;
}
}
}

View file

@ -109,7 +109,7 @@ export default class ForgotPassword extends React.PureComponent {
</div>
</div>
<div className="w-6/12 flex items-center justify-center">
<form onSubmit={ this.onSubmit } style={{ minWidth: '50%'}}>
<form onSubmit={ this.onSubmit } style={{ minWidth: '50%', textAlign: 'center'}}>
<div className="mb-8">
<h2 className="text-center text-3xl mb-6">{`${resetting ? 'Create' : 'Reset'} Password`}</h2>
</div>

View file

@ -14,6 +14,7 @@ import NewSiteForm from '../Client/Sites/NewSiteForm';
@connect(state => ({
sites: state.getIn([ 'site', 'list' ]),
siteId: state.getIn([ 'user', 'siteId' ]),
account: state.getIn([ 'user', 'account' ]),
}), {
setSiteId,
pushNewSite,
@ -32,11 +33,13 @@ export default class SiteDropdown extends React.PureComponent {
}
render() {
const { sites, siteId, location: { pathname } } = this.props;
const { sites, siteId, account, location: { pathname } } = this.props;
const { showProductModal } = this.state;
const isAdmin = account.admin || account.superAdmin;
const activeSite = sites.find(s => s.id == siteId);
const disabled = !siteChangeAvaliable(pathname);
const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname);
const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0;
return (
<div className={ styles.wrapper }>
{
@ -64,7 +67,7 @@ export default class SiteDropdown extends React.PureComponent {
}
</ul>
<div
className={cn(styles.btnNew, 'flex items-center justify-center py-3 cursor-pointer')}
className={cn(styles.btnNew, 'flex items-center justify-center py-3 cursor-pointer', { [styles.disabled] : !canAddSites })}
onClick={this.newSite}
>
<Icon

View file

@ -117,4 +117,9 @@
background-color: $gray-lightest;
transition: all 0.2s;
}
}
.disabled {
pointer-events: none;
opacity: 0.5;
}

View file

@ -1,16 +1,20 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import { connectPlayer, toggleEvents } from 'Player';
import { connectPlayer, toggleEvents, scale } from 'Player';
import cn from 'classnames'
import stl from './EventsToggleButton.css'
function EventsToggleButton({ showEvents, toggleEvents }) {
const toggle = () => {
toggleEvents()
scale()
}
return (
<Popup
trigger={
<button
className={cn("absolute right-0 z-50", stl.wrapper)}
onClick={toggleEvents}
onClick={toggle}
>
<Icon
name={ showEvents ? 'chevron-double-right' : 'chevron-double-left' }

View file

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { Loader } from 'UI';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import { withRequest } from 'HOCs'
import {
import {
PlayerProvider,
connectPlayer,
init as initPlayer,

View file

@ -35,9 +35,9 @@ function PlayerContent({ live, fullscreen, showEvents }) {
)
}
function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt }) {
function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, config }) {
useEffect(() => {
initPlayer(session, jwt);
initPlayer(session, jwt, config);
return () => cleanPlayer()
}, [ session.sessionId ]);
@ -60,6 +60,7 @@ function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscr
export default connect(state => ({
session: state.getIn([ 'sessions', 'current' ]),
jwt: state.get('jwt'),
config: state.getIn([ 'user', 'account', 'iceServers' ]),
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
}), {
toggleFullscreen,

View file

@ -2,7 +2,7 @@
//import cn from 'classnames';
import { getRE } from 'App/utils';
import { Label, NoContent, Input, SlideModal, CloseButton } from 'UI';
import { connectPlayer } from 'Player';
import { connectPlayer, pause } from 'Player';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import TimeTable from '../TimeTable';
@ -20,6 +20,7 @@ export default class Fetch extends React.PureComponent {
onFilterChange = (e, { value }) => this.setState({ filter: value })
setCurrent = (item, index) => {
pause()
this.setState({ current: item, currentIndex: index });
}
@ -53,7 +54,6 @@ export default class Fetch extends React.PureComponent {
return (
<React.Fragment>
<SlideModal
overlay={false}
right
size="middle"
title={

View file

@ -1,14 +1,14 @@
import { JSONTree, Label, Button, Tabs } from 'UI'
import { JSONTree, NoContent, Button, Tabs } from 'UI'
import cn from 'classnames';
import copy from 'copy-to-clipboard';
import stl from './fetchDetails.css';
import ResultTimings from '../../shared/ResultTimings/ResultTimings';
import Headers from './components/Headers'
const HEADERS = 'HEADERS';
const REQUEST = 'REQUEST';
const RESPONSE = 'RESPONSE';
const TIMINGS = 'TIMINGS';
const TABS = [ REQUEST, RESPONSE, TIMINGS ].map(tab => ({ text: tab, key: tab }));
const TABS = [ HEADERS, REQUEST, RESPONSE ].map(tab => ({ text: tab, key: tab }));
export default class FetchDetails extends React.PureComponent {
state = { activeTab: REQUEST, tabs: [] };
@ -20,53 +20,64 @@ export default class FetchDetails extends React.PureComponent {
}
renderActiveTab = tab => {
const { resource: { duration, timings }, isResult } = this.props;
const { resource: { payload, response = this.props.resource.body} } = this.props;
let jsonPayload, jsonResponse, requestHeaders, responseHeaders = undefined;
try {
jsonPayload = typeof payload === 'string' ? JSON.parse(payload) : payload
requestHeaders = jsonPayload.headers
jsonPayload.body = typeof jsonPayload.body === 'string' ? JSON.parse(jsonPayload.body) : jsonPayload.body
delete jsonPayload.headers
} catch (e) {}
try {
jsonResponse = typeof response === 'string' ? JSON.parse(response) : response;
responseHeaders = jsonResponse.headers
jsonResponse.body = typeof jsonResponse.body === 'string' ? JSON.parse(jsonResponse.body) : jsonResponse.body
delete jsonResponse.headers
} catch (e) {}
switch(tab) {
case REQUEST:
const { resource: { payload } } = this.props;
let jsonPayload = undefined;
try {
jsonPayload = typeof payload === 'string' ? JSON.parse(payload) : payload
} catch (e) {}
return !!payload ? (
<div>
<div className="mt-6">
{/* <h5>{ 'Payload '}</h5> */}
{ jsonPayload === undefined
? <div className="ml-3 break-words my-3"> { payload } </div>
: <JSONTree src={ jsonPayload } collapsed={ false } enableClipboard />
}
</div>
<div className="divider"/>
</div>
) : ''
break;
return (
<NoContent
title="Body is Empty."
size="small"
show={ !payload }
icon="exclamation-circle"
>
<div>
<div className="mt-6">
{ jsonPayload === undefined
? <div className="ml-3 break-words my-3"> { payload } </div>
: <JSONTree src={ jsonPayload } collapsed={ false } enableClipboard />
}
</div>
<div className="divider"/>
</div>
</NoContent>
)
case RESPONSE:
const { resource: { response = this.props.resource.body } } = this.props; // for IOS compat.
let jsonResponse = undefined;
try {
jsonResponse = JSON.parse(response);
} catch (e) {}
return !!response ? (
<div>
<div className="mt-6">
{/* <h5>{ 'Response '}</h5> */}
{ jsonResponse === undefined
? <div className="ml-3 break-words my-3"> { response } </div>
: <JSONTree src={ jsonResponse } collapsed={ false } enableClipboard />
}
</div>
<div className="divider"/>
</div>
// jsonResponse === undefined
// ? <div className="ml-3 break-words my-3"> { response } </div>
// : <JSONTree src={ jsonResponse } collapsed={ false } enableClipboard />
) : ''
break;
case TIMINGS:
return <ResultTimings duration={duration} timing={timings} />
return (
<NoContent
title="Body is Empty."
size="small"
show={ !response }
icon="exclamation-circle"
>
<div>
<div className="mt-6">
{ jsonResponse === undefined
? <div className="ml-3 break-words my-3"> { response } </div>
: <JSONTree src={ jsonResponse } collapsed={ false } enableClipboard />
}
</div>
<div className="divider"/>
</div>
</NoContent>
)
case HEADERS:
return <Headers requestHeaders={requestHeaders} responseHeaders={responseHeaders} />
}
}
@ -78,21 +89,18 @@ export default class FetchDetails extends React.PureComponent {
checkTabs() {
const { resource: { payload, response, body }, isResult } = this.props;
const _tabs = TABS.filter(t => {
if (t.key == REQUEST && !!payload) {
return true
}
const _tabs = TABS
// const _tabs = TABS.filter(t => {
// if (t.key == REQUEST && !!payload) {
// return true
// }
if (t.key == RESPONSE && !!response) {
return true;
}
// if (t.key == RESPONSE && !!response) {
// return true;
// }
if (t.key == TIMINGS && isResult) {
return true;
}
return false;
})
// return false;
// })
this.setState({ tabs: _tabs, activeTab: _tabs.length > 0 ? _tabs[0].key : null })
}

View file

@ -0,0 +1,49 @@
import React from 'react'
import { NoContent, TextEllipsis } from 'UI'
import stl from './headers.css'
function Headers(props) {
return (
<div>
<NoContent
title="No data available."
size="small"
show={ !props.requestHeaders && !props.responseHeaders }
icon="exclamation-circle"
>
{ props.requestHeaders && (
<>
<div className="mb-4 mt-4">
<div className="my-2 font-medium">Request Headers</div>
{
Object.keys(props.requestHeaders).map(h => (
<div className={stl.row}>
<span className="mr-2 font-medium">{h}:</span>
<span>{props.requestHeaders[h]}</span>
</div>
))
}
</div>
<hr />
</>
)}
{ props.responseHeaders && (
<div className="mt-4">
<div className="my-2 font-medium">Response Headers</div>
{
Object.keys(props.responseHeaders).map(h => (
<div className={stl.row}>
<span className="mr-2 font-medium">{h}:</span>
<span>{props.responseHeaders[h]}</span>
</div>
))
}
</div>
)}
</NoContent>
</div>
);
}
export default Headers;

View file

@ -0,0 +1,14 @@
.row {
/* display: flex; */
padding: 5px 0px;
font-size: 13px;
word-break: break-all;
/* padding-left: 20px; */
&:hover {
background-color: $active-blue;
}
& div:last-child {
max-width: 80%;
}
}

View file

@ -0,0 +1 @@
export { default } from './Headers'

View file

@ -1,5 +1,5 @@
import cn from 'classnames';
import { connectPlayer } from 'Player';
import { connectPlayer, jump, pause } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
@ -101,7 +101,7 @@ function renderSize(r) {
export function renderDuration(r) {
if (!r.success) return 'x';
const text = `${ r.duration }ms`;
const text = `${ Math.round(r.duration) }ms`;
if (!r.isRed() && !r.isYellow()) return text;
let tooltipText;
@ -131,6 +131,7 @@ export function renderDuration(r) {
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
}))
@ -138,6 +139,13 @@ export default class Network extends React.PureComponent {
state = {
filter: '',
activeTab: ALL,
currentIndex: 0
}
onRowClick = (e, index) => {
pause();
jump(e.time);
this.setState({ currentIndex: index })
}
onTabClick = activeTab => this.setState({ activeTab })
@ -151,9 +159,10 @@ export default class Network extends React.PureComponent {
loadTime,
domBuildingTime,
fetchPresented,
time
time,
playing
} = this.props;
const { filter, activeTab } = this.state;
const { filter, activeTab, currentIndex } = this.state;
const filterRE = getRE(filter, 'i');
let filtered = resources.filter(({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
@ -200,6 +209,8 @@ export default class Network extends React.PureComponent {
fetchPresented = { fetchPresented }
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={ this.onRowClick }
currentIndex={playing ? null : currentIndex}
/>
{/* <BottomBlock>
<BottomBlock.Header>

View file

@ -157,13 +157,14 @@ export default class NetworkContent extends React.PureComponent {
additionalHeight = 0,
resourcesSize,
transferredSize,
time
time,
currentIndex
} = this.props;
const { filter, activeTab } = this.state;
const filterRE = getRE(filter, 'i');
let filtered = resources.filter(({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
const lastIndex = filtered.filter(item => item.time <= time).length - 1;
const lastIndex = currentIndex || filtered.filter(item => item.time <= time).length - 1;
const referenceLines = [];
if (domContentLoadedTime != null) {

View file

@ -3,6 +3,7 @@ import { NoContent } from 'UI'
import { connectPlayer } from 'Player/store';
import SelectorCard from '../SelectorCard/SelectorCard';
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
import stl from './selectorList.css'
interface Props {
targets: Array<MarkedTarget>,
@ -16,9 +17,11 @@ function SelectorsList({ targets, activeTargetIndex }: Props) {
size="small"
show={ targets && targets.length === 0 }
>
{ targets && targets.map((target, index) => (
<SelectorCard target={target} index={index} showContent={activeTargetIndex === index} />
))}
<div className={stl.wrapper}>
{ targets && targets.map((target, index) => (
<SelectorCard target={target} index={index} showContent={activeTargetIndex === index} />
))}
</div>
</NoContent>
)
}

View file

@ -0,0 +1,28 @@
.wrapper {
height: calc(100vh - 190px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 2px;
background: transparent !important;
background: rgba(0,0,0,0);
}
&::-webkit-scrollbar-thumb {
background: transparent !important;
}
&::-webkit-scrollbar-track {
background: transparent !important;
}
&:hover {
&::-webkit-scrollbar {
width: 2px;
background: rgba(0,0,0,0.1)
}
&::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1)
}
&::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.1)
}
}
}

View file

@ -1,4 +1,7 @@
.text {
color: $gray-light;
font-size: 40px;
max-width: 680px;
line-height: 48px;
text-align: center;
}

View file

@ -7,5 +7,5 @@ interface Props {
}
export default function LiveStatusText({ text }: Props) {
return <div className={ovStl.overlay}><span className={stl.text}>{text}</span></div>
return <div className={ovStl.overlay}><div className={stl.text}>{text}</div></div>
}

View file

@ -80,12 +80,12 @@ function NetworkTab(props) {
onRowClick={ (e, index) => { setCurrent(e); setCurrentIndex(index)} }
additionalHeight={vh}
fetchPresented = { run.fetchPresented }
loadTime = { run.loadTime }
domBuildingTime = { run.domBuildingTime }
resourcesSize = { run.resourcesSize }
transferredSize = { run.transferredSize }
domContentLoadedTime = { run.domContentLoadedTime }
currentIndex={currentIndex}
/>
</div>
)

View file

@ -10,7 +10,9 @@ const TrackerUpdateMessage= (props) => {
const { site, sites, match: { params: { siteId } } } = props;
const activeSite = sites.find(s => s.id == siteId);
const hasSessions = !!activeSite && !activeSite.recorded;
const needUpdate = !hasSessions && site.trackerVersion !== window.ENV.TRACKER_VERSION;
const appVersionInt = parseInt(window.ENV.TRACKER_VERSION.split(".").join(""))
const trackerVersionInt = site.trackerVersion ? parseInt(site.trackerVersion.split(".").join("")) : 0
const needUpdate = !hasSessions && trackerVersionInt > appVersionInt;
return needUpdate ? (
<>
{(

View file

@ -248,7 +248,7 @@ const reducer = (state = initialState, action = {}) => {
};
export default withRequestState({
_: [ FETCH, FETCH_LIST ],
_: [ FETCH, FETCH_LIST, FETCH_LIVE_LIST ],
fetchFavoriteListRequest: FETCH_FAVORITE_LIST,
toggleFavoriteRequest: TOGGLE_FAVORITE,
fetchErrorStackList: FETCH_ERROR_STACK,

View file

@ -6,8 +6,8 @@ import type { Message } from '../messages'
import { ID_TP_MAP } from '../messages';
import store from 'App/store';
import type { LocalStream } from './LocalStream';
import { update, getState } from '../../store';
import { iceServerConfigFromString } from 'App/utils'
export enum CallingState {
@ -118,7 +118,7 @@ function resolveCSS(baseURL: string, css: string): string {
}
export default class AssistManager {
constructor(private session, private md: MessageDistributor, private config) {}
constructor(private session, private config, private md: MessageDistributor, private config) {}
private setStatus(status: ConnectionStatus) {
if (status === ConnectionStatus.Connecting) {
@ -162,7 +162,7 @@ export default class AssistManager {
iceTransportPolicy: 'relay',
};
}
const peer = new Peer(_config);
this.peer = peer;
peer.on('error', e => {
@ -380,7 +380,7 @@ export default class AssistManager {
const data = this.md.getInternalCoordinates(e);
// const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager
const el = this.md.getElementFromInternalPoint(data)
if (el instanceof HTMLElement) {
if (el instanceof HTMLElement) {
el.focus()
el.oninput = e => e.preventDefault();
el.onkeydown = e => e.preventDefault();
@ -403,6 +403,13 @@ export default class AssistManager {
}
}
private onMouseClick = (e: MouseEvent): void => {
const conn = this.dataConnection;
if (!conn) { return; }
const data = this.md.getInternalCoordinates(e);
// const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager
conn.send({ type: "click", x: Math.round(data.x), y: Math.round(data.y) });
}
private localCallData: {
localStream: LocalStream,
@ -420,6 +427,7 @@ export default class AssistManager {
onCallEnd();
this.toggleRemoteControl(false);
this.md.overlay.removeEventListener("mousemove", this.onMouseMove);
this.md.overlay.removeEventListener("click", this.onMouseClick);
update({ calling: CallingState.False });
this.localCallData = null;
},
@ -442,7 +450,7 @@ export default class AssistManager {
const call = this.peer.call(this.peerID, this.localCallData.localStream.stream);
this.localCallData.localStream.onVideoTrack(vTrack => {
const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video")
const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video")
if (!sender) {
//logger.warn("No video sender found")
return
@ -459,7 +467,7 @@ export default class AssistManager {
});
this.md.overlay.addEventListener("mousemove", this.onMouseMove)
this.md.overlay.addEventListener("click", this.onMouseClick)
});
//call.peerConnection.addEventListener("track", e => console.log('newtrack',e.track))

View file

@ -14,6 +14,7 @@ export default Member.extend({
license: '',
expirationDate: undefined,
permissions: [],
iceServers: undefined
}, {
fromJS: ({ current = {}, ...account})=> ({
...account,

View file

@ -21,6 +21,7 @@ export default Record({
recorded: undefined,
stackIntegrations: false,
projectKey: undefined,
trackerVersion: undefined,
}, {
idKey: 'id',
methods: {

View file

@ -203,4 +203,27 @@ export const colorScale = (values, colors) => {
.range([colors[0], colors[colors.length - 1]]);
}
export const truncate = (input, max = 10) => input.length > max ? `${input.substring(0, max)}...` : input;
export const truncate = (input, max = 10) => input.length > max ? `${input.substring(0, max)}...` : input;
export const iceServerConfigFromString = (str) => {
if (!str || typeof str !== 'string'|| str.length === 0) {
return null;
}
return str.split("|").map(function(c) {
let server = null
const arr = c.split(",")
if (!!arr[0] !== "") {
server = {}
server.urls = arr[0]
if (!!arr[1]) {
server.username = arr[1]
if (!!arr[2]) {
server.credential = arr[2]
}
}
return server
}
})
}

View file

@ -20,9 +20,10 @@ const oss = {
MINIO_USE_SSL: process.env.MINIO_USE_SSL,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
ICE_SERVERS: process.env.ICE_SERVERS,
TRACKER_VERSION: '3.4.10', // trackerInfo.version,
}
module.exports = {
oss,
};
};

View file

@ -57,11 +57,12 @@ env:
# Enable logging for python app
# Ref: https://stackoverflow.com/questions/43969743/logs-in-kubernetes-pod-not-showing-up
PYTHONUNBUFFERED: '0'
version_number: '1.3.0'
version_number: '1.3.6'
SAML2_MD_URL: ''
idp_entityId: ''
idp_sso_url: ''
idp_x509cert: ''
idp_sls_url: ''
idp_name: ''
assist_secret: ''
assist_secret: ''
iceServers: ''

View file

@ -29,6 +29,7 @@ env:
AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd"
AWS_REGION: us-east-1
POSTGRES_STRING: postgres://postgres:asayerPostgres@postgresql.db.svc.cluster.local:5432
CACHE_ASSETS: true
#
REDIS_STRING: redis-master.db.svc.cluster.local:6379
KAFKA_SERVERS: kafka.db.svc.cluster.local:9092

View file

@ -59,7 +59,7 @@ spec:
type: DirectoryOrCreate
{{- else }}
volumeMounts:
- name: {{ .Values.pvc.name }}
- name: datadir
mountPath: {{ .Values.pvc.mountPath }}
volumes:
- name: {{ .Values.pvc.name }}

View file

@ -1,10 +1,8 @@
BEGIN;
CREATE INDEX pages_session_id_timestamp_idx ON events.pages (session_id, timestamp);
CREATE INDEX projects_tenant_id_idx ON projects(tenant_id);
CREATE INDEX webhooks_tenant_id_idx ON webhooks(tenant_id);
CREATE INDEX issues_project_id_idx ON issues(project_id);
CREATE INDEX jobs_project_id_idx ON jobs(project_id);
CREATE INDEX issues_project_id_idx ON issues (project_id);
CREATE INDEX jobs_project_id_idx ON jobs (project_id);
COMMIT;

View file

@ -255,6 +255,15 @@ $$
"defaultInputMode": "plain"
}'::jsonb -- ??????
);
CREATE INDEX ON public.projects (project_key);
CREATE OR REPLACE FUNCTION notify_project() RETURNS trigger AS
$$
BEGIN
PERFORM pg_notify('project', row_to_json(NEW)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_insert_or_update
AFTER INSERT OR UPDATE

View file

@ -23,8 +23,6 @@ data:
proxy_set_header Connection "";
chunked_transfer_encoding off;
client_max_body_size 50M;
proxy_pass http://minio.db.svc.cluster.local:9000;
}
@ -45,6 +43,9 @@ data:
proxy_set_header X-Real-IP $real_ip;
proxy_set_header Host $host;
proxy_pass http://http-openreplay.app.svc.cluster.local;
proxy_read_timeout 300;
proxy_connect_timeout 120;
proxy_send_timeout 300;
}
location /grafana {
set $target http://monitoring-grafana.monitoring.svc.cluster.local;
@ -138,6 +139,7 @@ data:
# server_name _;
# return 301 https://$host$request_uri;
include /etc/nginx/conf.d/location.list;
client_max_body_size 10M;
}
server {
listen 443 ssl;
@ -146,4 +148,5 @@ data:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA HIGH !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
include /etc/nginx/conf.d/location.list;
client_max_body_size 10M;
}

View file

@ -54,10 +54,6 @@
delay: 60
register: result
until: result.rc == 0
retries: 3
delay: 60
register: result
until: result.rc == 0
- name: Creating kafka topics
shell: |
# Creating topic

View file

@ -24,8 +24,8 @@ domain_name: ""
docker_registry_username: ""
docker_registry_password: ""
docker_registry_url: "rg.fr-par.scw.cloud/foss"
image_tag: "v1.3.0"
openreplay_version: "v1.3.0"
image_tag: "v1.3.5"
openreplay_version: "v1.3.5"
# Nginx ssl certificates.
# in cert format

View file

@ -24,8 +24,8 @@ domain_name: "{{ domain_name }}"
docker_registry_username: "{{ docker_registry_username }}"
docker_registry_password: "{{ docker_registry_password }}"
docker_registry_url: "{{ docker_registry_url }}"
image_tag: "v1.3.0"
openreplay_version: "v1.3.0"
image_tag: "v1.3.5"
openreplay_version: "v1.3.5"
# Nginx ssl certificates.
# in cert format

View file

@ -1,4 +1,4 @@
## Licenses (as of April 30, 2021)
## Licenses (as of October 28, 2021)
Below is the list of dependencies used in OpenReplay software. Licenses may change between versions, so please keep this up to date with every new library you use.
@ -85,4 +85,7 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan
| croniter | MIT | Python |
| lib/pq | MIT | Go |
| peerjs | MIT | JavaScript |
| antonmedv/finder | MIT | JavaScript |
| antonmedv/finder | MIT | JavaScript |
| elasticsearch-py | Apache2 | Python |
| sentry-python | BSD2 | Python |
| jira | BSD2 | Python |

View file

@ -1,4 +1,4 @@
# OpenReplay Tracker Axios plugin
# OpenReplay Tracker Assist plugin
Tracker plugin for WebRTC video support at your site.

View file

@ -1,4 +1,4 @@
import type { LocalStream } from './LocalStream.js';
import type { LocalStream } from './LocalStream';
const SS_START_TS_KEY = "__openreplay_assist_call_start_ts"
@ -129,7 +129,7 @@ export default class CallWindow {
})
}
private aRemote: HTMLAudioElement | null = null;
private aRemote: HTMLAudioElement | null = null;
private checkRemoteVideoInterval: ReturnType<typeof setInterval>
setRemoteStream(rStream: MediaStream) {
this.load.then(() => {
@ -178,7 +178,7 @@ export default class CallWindow {
private localStream: LocalStream | null = null;
// TODO: on construction?
setLocalStream(lStream: LocalStream) {
setLocalStream(lStream: LocalStream) {
this.localStream = lStream
}
@ -207,7 +207,7 @@ export default class CallWindow {
private toggleAudio() {
const enabled = this.localStream?.toggleAudio() || false
this.toggleAudioUI(enabled)
// if (!this.audioBtn) { return; }
// if (!this.audioBtn) { return; }
// if (enabled) {
// this.audioBtn.classList.remove("muted");
// this.audioBtn.childNodes[1].textContent = "Mute";
@ -218,7 +218,7 @@ export default class CallWindow {
}
private toggleVideoUI(enabled: boolean) {
if (!this.videoBtn || !this.videoContainer) { return; }
if (!this.videoBtn || !this.videoContainer) { return; }
if (enabled) {
this.videoContainer.classList.add("local")
this.videoBtn.classList.remove("off");
@ -239,7 +239,7 @@ export default class CallWindow {
this.vLocal.srcObject = this.localStream.stream
}
})
})
})
}
remove() {
@ -247,10 +247,10 @@ export default class CallWindow {
clearInterval(this.tsInterval)
clearInterval(this.checkRemoteVideoInterval)
if (this.iframe.parentElement) {
document.body.removeChild(this.iframe)
document.body.removeChild(this.iframe)
}
if (this.aRemote && this.aRemote.parentElement) {
document.body.removeChild(this.aRemote)
document.body.removeChild(this.aRemote)
}
sessionStorage.removeItem(SS_START_TS_KEY)
}

View file

@ -27,9 +27,9 @@ enum CallingState {
//@ts-ignore peerjs hack for webpack5 (?!) TODO: ES/node modules;
Peer = Peer.default || Peer;
// type IncomeMessages =
// "call_end" |
// { type: "agent_name", name: string } |
// type IncomeMessages =
// "call_end" |
// { type: "agent_name", name: string } |
// { type: "click", x: number, y: number } |
// { x: number, y: number }
@ -113,7 +113,7 @@ export default function(opts?: Partial<Options>) {
peer.on('call', function(call) {
log("Call: ", call)
if (!peer) { return; }
const dataConn: DataConnection | undefined =
const dataConn: DataConnection | undefined =
openDataConnections[call.peer]?.conn;
if (callingState !== CallingState.False || !dataConn || !dataConn.open) {
call.close();
@ -128,7 +128,7 @@ export default function(opts?: Partial<Options>) {
sessionStorage.removeItem(options.session_calling_peer_key);
}
callingState = newState;
}
}
const notifyCallEnd = () => {
dataConn.open && dataConn.send("call_end");
@ -145,7 +145,7 @@ export default function(opts?: Partial<Options>) {
confirmAnswer = confirm.mount();
dataConn.on('data', (data) => { // if call cancelled by a caller before confirmation
if (data === "call_end") {
log("Recieved call_end during confirm window opened")
log("Received call_end during confirm window opened")
confirm.remove();
setCallingState(CallingState.False);
}
@ -209,7 +209,7 @@ export default function(opts?: Partial<Options>) {
return;
}
if (data.name === 'string') {
log("name recieved: ", data)
log("Name received: ", data)
callUI.setAssistentName(data.name);
}
if (data.type === "scroll" && Array.isArray(data.delta)) {
@ -222,11 +222,11 @@ export default function(opts?: Partial<Options>) {
if(el.scrollWidth > el.clientWidth) {
el.scrollLeft += data.delta[0]
scrolled = true
}
}
if (el && el.scrollHeight > el.clientHeight) {
el.scrollTop += data.delta[1]
scrolled = true
}
}
}
if (!scrolled) {
window.scroll(scrEl.scrollLeft + data.delta[0], scrEl.scrollTop + data.delta[1])
@ -246,7 +246,7 @@ export default function(opts?: Partial<Options>) {
});
lStream.onVideoTrack(vTrack => {
const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video")
const sender = call.peerConnection.getSenders().find(s => s.track?.kind === "video")
if (!sender) {
warn("No video sender found")
return