Merge branch 'dev' into api_insights

This commit is contained in:
Kraiem Taha Yassine 2021-10-19 00:24:18 +02:00 committed by GitHub
commit 1ccc35a97e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
143 changed files with 3634 additions and 1714 deletions

View file

@ -63,7 +63,7 @@ OpenReplay can be deployed anywhere. Follow our step-by-step guides for deployin
## OpenReplay Cloud
For those who want to simply use OpenReplay as a service, [sign up](https://asayer.io/register.html) for a free account on our cloud offering.
For those who want to simply use OpenReplay as a service, [sign up](https://app.openreplay.com/signup) for a free account on our cloud offering.
## Community Support

View file

@ -33,7 +33,7 @@
"sourcemaps_reader": "http://utilities-openreplay.app.svc.cluster.local:9000/sourcemaps",
"sourcemaps_bucket": "sourcemaps",
"js_cache_bucket": "sessions-assets",
"peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/peers",
"peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers",
"async_Token": "",
"EMAIL_HOST": "",
"EMAIL_PORT": "587",

View file

@ -21,7 +21,7 @@ SESSION_PROJECTION_COLS = """s.project_id,
def get_live_sessions(project_id, filters=None):
project_key = projects.get_project_key(project_id)
connected_peers = requests.get(environ["peers"] + f"/{project_key}")
connected_peers = requests.get(environ["peers"] % environ["S3_KEY"] + f"/{project_key}")
if connected_peers.status_code != 200:
print("!! issue with the peer-server")
print(connected_peers.text)
@ -65,7 +65,7 @@ def get_live_sessions(project_id, filters=None):
def is_live(project_id, session_id, project_key=None):
if project_key is None:
project_key = projects.get_project_key(project_id)
connected_peers = requests.get(environ["peers"] + f"/{project_key}")
connected_peers = requests.get(environ["peers"] % environ["S3_KEY"] + f"/{project_key}")
if connected_peers.status_code != 200:
print("!! issue with the peer-server")
print(connected_peers.text)

View file

@ -1,4 +1,5 @@
from chalicelib.utils import pg_client, helper
from chalicelib.utils.TimeUTC import TimeUTC
from chalicelib.utils.helper import environ
from chalicelib.utils.helper import get_issue_title
@ -30,7 +31,11 @@ def edit_config(user_id, weekly_report):
def cron():
with pg_client.PostgresClient() as cur:
cur.execute("""\
params = {"3_days_ago": TimeUTC.midnight(delta_days=-3),
"1_week_ago": TimeUTC.midnight(delta_days=-7),
"2_week_ago": TimeUTC.midnight(delta_days=-14),
"5_week_ago": TimeUTC.midnight(delta_days=-35)}
cur.execute(cur.mogrify("""\
SELECT project_id,
name AS project_name,
users.emails AS emails,
@ -44,7 +49,7 @@ def cron():
SELECT sessions.project_id
FROM public.sessions
WHERE sessions.project_id = projects.project_id
AND start_ts >= (EXTRACT(EPOCH FROM now() - INTERVAL '3 days') * 1000)::BIGINT
AND start_ts >= %(3_days_ago)s
LIMIT 1) AS recently_active USING (project_id)
INNER JOIN LATERAL (
SELECT COALESCE(ARRAY_AGG(email), '{}') AS emails
@ -54,14 +59,14 @@ def cron():
AND users.weekly_report
) AS users ON (TRUE)
LEFT JOIN LATERAL (
SELECT COUNT(issues.*) AS count
SELECT COUNT(1) AS count
FROM events_common.issues
INNER JOIN public.sessions USING (session_id)
WHERE sessions.project_id = projects.project_id
AND issues.timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '1 week') * 1000)::BIGINT
) AS week_0_issues ON (TRUE)
LEFT JOIN LATERAL (
SELECT COUNT(issues.*) AS count
SELECT COUNT(1) AS count
FROM events_common.issues
INNER JOIN public.sessions USING (session_id)
WHERE sessions.project_id = projects.project_id
@ -69,16 +74,17 @@ def cron():
AND issues.timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '2 week') * 1000)::BIGINT
) AS week_1_issues ON (TRUE)
LEFT JOIN LATERAL (
SELECT COUNT(issues.*) AS count
SELECT COUNT(1) AS count
FROM events_common.issues
INNER JOIN public.sessions USING (session_id)
WHERE sessions.project_id = projects.project_id
AND issues.timestamp <= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '1 week') * 1000)::BIGINT
AND issues.timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '5 week') * 1000)::BIGINT
) AS month_1_issues ON (TRUE)
WHERE projects.deleted_at ISNULL;""")
WHERE projects.deleted_at ISNULL;"""), params)
projects_data = cur.fetchall()
for p in projects_data:
params["project_id"] = p["project_id"]
print(f"checking {p['project_name']} : {p['project_id']}")
if len(p["emails"]) == 0 \
or p["this_week_issues_count"] + p["past_week_issues_count"] + p["past_month_issues_count"] == 0:
@ -104,7 +110,7 @@ def cron():
DATE_TRUNC('day', now()) - INTERVAL '1 day',
'1 day'::INTERVAL
) AS timestamp_i
ORDER BY timestamp_i;""", {"project_id": p["project_id"]}))
ORDER BY timestamp_i;""", params))
days_partition = cur.fetchall()
max_days_partition = max(x['issues_count'] for x in days_partition)
for d in days_partition:
@ -120,7 +126,7 @@ def cron():
AND timestamp >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '7 days') * 1000)::BIGINT
GROUP BY type
ORDER BY count DESC, type
LIMIT 4;""", {"project_id": p["project_id"]}))
LIMIT 4;""", params))
issues_by_type = cur.fetchall()
max_issues_by_type = sum(i["count"] for i in issues_by_type)
for i in issues_by_type:
@ -149,7 +155,7 @@ def cron():
'1 day'::INTERVAL
) AS timestamp_i
GROUP BY timestamp_i
ORDER BY timestamp_i;""", {"project_id": p["project_id"]}))
ORDER BY timestamp_i;""", params))
issues_breakdown_by_day = cur.fetchall()
for i in issues_breakdown_by_day:
i["sum"] = sum(x["count"] for x in i["partition"])
@ -195,7 +201,7 @@ def cron():
WHERE mi.project_id = %(project_id)s AND sessions.project_id = %(project_id)s AND sessions.duration IS NOT NULL
AND sessions.start_ts >= (EXTRACT(EPOCH FROM DATE_TRUNC('day', now()) - INTERVAL '1 week') * 1000)::BIGINT
GROUP BY type
ORDER BY issue_count DESC;""", {"project_id": p["project_id"]}))
ORDER BY issue_count DESC;""", params))
issues_breakdown_list = cur.fetchall()
if len(issues_breakdown_list) > 4:
others = {"type": "Others",

View file

@ -1,5 +1,5 @@
requests==2.24.0
urllib3==1.25.11
requests==2.26.0
urllib3==1.26.6
boto3==1.16.1
pyjwt==1.7.1
psycopg2-binary==2.8.6

View file

@ -41,7 +41,7 @@ ENV TZ=UTC \
AWS_REGION_WEB=eu-central-1 \
AWS_REGION_IOS=eu-west-1 \
AWS_REGION_ASSETS=eu-central-1 \
CACHE_ASSETS=false \
CACHE_ASSETS=true \
ASSETS_SIZE_LIMIT=6291456 \
FS_CLEAN_HRS=72

View file

@ -28,3 +28,52 @@ func (c *PGCache) InsertIssueEvent(sessionID uint64, crash *IssueEvent) error {
}
return c.Conn.InsertIssueEvent(sessionID, session.ProjectID, crash)
}
func (c *PGCache) InsertUserID(sessionID uint64, userID *IOSUserID) error {
if err := c.Conn.InsertIOSUserID(sessionID, userID); err != nil {
return err
}
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
session.UserID = &userID.Value
return nil
}
func (c *PGCache) InsertUserAnonymousID(sessionID uint64, userAnonymousID *IOSUserAnonymousID) error {
if err := c.Conn.InsertIOSUserAnonymousID(sessionID, userAnonymousID); err != nil {
return err
}
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
session.UserAnonymousID = &userAnonymousID.Value
return nil
}
func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
project, err := c.GetProject(session.ProjectID)
if err != nil {
return err
}
keyNo := project.GetMetadataNo(metadata.Key)
if keyNo == 0 {
// insert project metadata
}
if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil {
return err
}
session.SetMetadata(keyNo, metadata.Value)
return nil
}

View file

@ -95,46 +95,3 @@ func (c *PGCache) InsertIOSIssueEvent(sessionID uint64, issueEvent *IOSIssueEven
return nil
}
func (c *PGCache) InsertUserID(sessionID uint64, userID *IOSUserID) error {
if err := c.Conn.InsertIOSUserID(sessionID, userID); err != nil {
return err
}
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
session.UserID = &userID.Value
return nil
}
func (c *PGCache) InsertUserAnonymousID(sessionID uint64, userAnonymousID *IOSUserAnonymousID) error {
if err := c.Conn.InsertIOSUserAnonymousID(sessionID, userAnonymousID); err != nil {
return err
}
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
session.UserAnonymousID = &userAnonymousID.Value
return nil
}
func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
project, err := c.GetProject(session.ProjectID)
if err != nil {
return err
}
keyNo := project.GetMetadataNo(metadata.Key)
if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil {
return err
}
session.SetMetadata(keyNo, metadata.Value)
return nil
}

View file

@ -11,7 +11,7 @@ func (c *PGCache) GetProjectByKey(projectKey string) (*Project, error) {
return c.projectsByKeys[ projectKey ].Project, nil
}
p, err := c.Conn.GetProjectByKey(projectKey)
if p == nil {
if err != nil {
return nil, err
}
c.projectsByKeys[ projectKey ] = &ProjectMeta{ p, time.Now().Add(c.projectExpirationTimeout) }
@ -27,7 +27,7 @@ func (c *PGCache) GetProject(projectID uint32) (*Project, error) {
return c.projects[ projectID ].Project, nil
}
p, err := c.Conn.GetProject(projectID)
if p == nil {
if err != nil {
return nil, err
}
c.projects[ projectID ] = &ProjectMeta{ p, time.Now().Add(c.projectExpirationTimeout) }

View file

@ -2,15 +2,17 @@ package postgres
import (
"errors"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgconn"
"github.com/jackc/pgerrcode"
)
func IsPkeyViolation(err error) bool {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation {
return true
}
return false
}
return errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation
}
func IsNoRowsErr(err error) bool {
return err == pgx.ErrNoRows
}

View file

@ -1,7 +1,6 @@
package postgres
import (
"github.com/jackc/pgx/v4"
. "openreplay/backend/pkg/db/types"
)
@ -14,9 +13,6 @@ func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) {
`,
projectKey,
).Scan(&p.MaxSessionDuration, &p.SampleRate, &p.ProjectID); err != nil {
if err == pgx.ErrNoRows {
err = nil
}
return nil, err
}
return p, nil
@ -36,9 +32,6 @@ func (conn *Conn) GetProject(projectID uint32) (*Project, error) {
).Scan(&p.ProjectKey,&p.MaxSessionDuration,
&p.Metadata1, &p.Metadata2, &p.Metadata3, &p.Metadata4, &p.Metadata5,
&p.Metadata6, &p.Metadata7, &p.Metadata8, &p.Metadata9, &p.Metadata10); err != nil {
if err == pgx.ErrNoRows {
err = nil
}
return nil, err
}
return p, nil

View file

@ -36,8 +36,13 @@ func Uint16(key string) uint16 {
return uint16(n)
}
const MAX_INT = uint64(^uint(0) >> 1)
func Int(key string) int {
return int(Uint64(key))
val := Uint64(key)
if val > MAX_INT {
log.Fatalln(key + " is too big. ")
}
return int(val)
}
func Bool(key string) bool {

View file

@ -3,7 +3,7 @@ package messages
func IsReplayerType(id uint64) bool {
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 90 == id || 93 == id || 100 == id || 102 == id || 103 == id || 105 == id
return 0 == id || 2 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 69 == id || 70 == id || 90 == id || 93 == id || 100 == id || 102 == id || 103 == id || 105 == id
}
func IsIOSType(id uint64) bool {

View file

@ -86,6 +86,18 @@ p = WriteString(msg.UserCountry, buf, p)
return buf[:p]
}
type SessionDisconnect struct {
*meta
Timestamp uint64
}
func (msg *SessionDisconnect) Encode() []byte{
buf := make([]byte, 11 )
buf[0] = 2
p := 1
p = WriteUint(msg.Timestamp, buf, p)
return buf[:p]
}
type SessionEnd struct {
*meta
Timestamp uint64
@ -1166,6 +1178,20 @@ p = WriteString(msg.Selector, buf, p)
return buf[:p]
}
type CreateIFrameDocument struct {
*meta
FrameID uint64
ID uint64
}
func (msg *CreateIFrameDocument) Encode() []byte{
buf := make([]byte, 21 )
buf[0] = 70
p := 1
p = WriteUint(msg.FrameID, buf, p)
p = WriteUint(msg.ID, buf, p)
return buf[:p]
}
type IOSSessionStart struct {
*meta
Timestamp uint64

View file

@ -49,7 +49,7 @@ func ReadUint(reader io.Reader) (uint64, error) {
}
if b < 0x80 {
if i > 9 || i == 9 && b > 1 {
return x, errors.New("overflow")
return x, errors.New("uint overflow")
}
return x | uint64(b)<<s, nil
}

View file

@ -44,6 +44,11 @@ if msg.UserDeviceHeapSize, err = ReadUint(reader); err != nil { return nil, err
if msg.UserCountry, err = ReadString(reader); err != nil { return nil, err }
return msg, nil
case 2:
msg := &SessionDisconnect{ meta: &meta{ TypeID: 2} }
if msg.Timestamp, err = ReadUint(reader); err != nil { return nil, err }
return msg, nil
case 3:
msg := &SessionEnd{ meta: &meta{ TypeID: 3} }
if msg.Timestamp, err = ReadUint(reader); err != nil { return nil, err }
@ -521,6 +526,12 @@ if msg.Label, err = ReadString(reader); err != nil { return nil, err }
if msg.Selector, err = ReadString(reader); err != nil { return nil, err }
return msg, nil
case 70:
msg := &CreateIFrameDocument{ meta: &meta{ TypeID: 70} }
if msg.FrameID, err = ReadUint(reader); err != nil { return nil, err }
if msg.ID, err = ReadUint(reader); err != nil { return nil, err }
return msg, nil
case 90:
msg := &IOSSessionStart{ meta: &meta{ TypeID: 90} }
if msg.Timestamp, err = ReadUint(reader); err != nil { return nil, err }

View file

@ -8,7 +8,7 @@ import (
)
func getSessionKey(sessionID uint64) string {
// Based on timestamp, changes once per week. Check out utils/flacker for understanding sessionID
// Based on timestamp, changes once per week. Check pkg/flakeid for understanding sessionID
return strconv.FormatUint(sessionID>>50, 10)
}
@ -48,11 +48,11 @@ func isCachable(rawurl string) bool {
func GetFullCachableURL(baseURL string, relativeURL string) (string, bool) {
if !isRelativeCachable(relativeURL) {
return "", false
return relativeURL, false
}
fullURL := ResolveURL(baseURL, relativeURL)
if !isCachable(fullURL) {
return "", false
return fullURL, false
}
return fullURL, true
}
@ -77,7 +77,7 @@ func GetCachePathForAssets(sessionID uint64, rawurl string) string {
func (r *Rewriter) RewriteURL(sessionID uint64, baseURL string, relativeURL string) string {
fullURL, cachable := GetFullCachableURL(baseURL, relativeURL)
if !cachable {
return relativeURL
return fullURL
}
u := url.URL{

View file

@ -57,8 +57,8 @@ func startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) {
}
p, err := pgconn.GetProjectByKey(*req.ProjectKey)
if p == nil {
if err == nil {
if err != nil {
if postgres.IsNoRowsErr(err) {
responseWithError(w, http.StatusNotFound, errors.New("Project doesn't exist or is not active"))
} else {
responseWithError(w, http.StatusInternalServerError, err) // TODO: send error here only on staging

View file

@ -51,8 +51,8 @@ package main
// return
// }
// p, err := pgconn.GetProject(uint32(projectID))
// if p == nil {
// if err == nil {
// if err != nil {
// if postgres.IsNoRowsErr(err) {
// responseWithError(w, http.StatusNotFound, errors.New("Project doesn't exist or is not active"))
// } else {
// responseWithError(w, http.StatusInternalServerError, err) // TODO: send error here only on staging

View file

@ -24,7 +24,7 @@ import (
"openreplay/backend/services/http/uaparser"
)
var rewriter *assets.Rewriter
var producer types.Producer
var pgconn *cache.PGCache

View file

@ -15,7 +15,7 @@ import (
"openreplay/backend/pkg/queue/types"
)
func main() {
log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile)

View file

@ -35,7 +35,7 @@
"put_S3_TTL": "20",
"sourcemaps_reader": "http://utilities-openreplay.app.svc.cluster.local:9000/sourcemaps",
"sourcemaps_bucket": "sourcemaps",
"peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/peers",
"peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers",
"js_cache_bucket": "sessions-assets",
"async_Token": "",
"EMAIL_HOST": "",

View file

@ -1,5 +1,5 @@
requests==2.24.0
urllib3==1.25.11
requests==2.26.0
urllib3==1.26.6
boto3==1.16.1
pyjwt==1.7.1
psycopg2-binary==2.8.6

View file

@ -0,0 +1,11 @@
BEGIN;
CREATE INDEX sessions_user_id_useridNN_idx ON sessions (user_id) WHERE user_id IS NOT NULL;
CREATE INDEX sessions_uid_projectid_startts_sessionid_uidNN_durGTZ_idx ON sessions (user_id, project_id, start_ts, session_id) WHERE user_id IS NOT NULL AND duration > 0;
CREATE INDEX pages_base_path_base_pathLNGT2_idx ON events.pages (base_path) WHERE length(base_path) > 2;
CREATE INDEX users_tenant_id_deleted_at_N_idx ON users (tenant_id) WHERE deleted_at ISNULL;
CREATE INDEX issues_issue_id_timestamp_idx ON events_common.issues(issue_id,timestamp);
CREATE INDEX issues_timestamp_idx ON events_common.issues (timestamp);
CREATE INDEX issues_project_id_issue_id_idx ON public.issues (project_id, issue_id);
COMMIT;

View file

@ -1,9 +1,9 @@
---
# defaults file for openreplay
app_name: ""
db_name: ""
db_list:
- "minio"
- "nfs-server-provisioner"
- "postgresql"
- "redis"
- "clickhouse"

View file

@ -2,27 +2,26 @@ import React, { useState } from 'react'
import stl from './ChatControls.css'
import cn from 'classnames'
import { Button, Icon } from 'UI'
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
interface Props {
stream: MediaStream | null,
stream: LocalStream | null,
endCall: () => void
}
function ChatControls({ stream, endCall } : Props) {
const [audioEnabled, setAudioEnabled] = useState(true)
const [videoEnabled, setVideoEnabled] = useState(true)
const [videoEnabled, setVideoEnabled] = useState(false)
const toggleAudio = () => {
if (!stream) { return; }
const aEn = !audioEnabled
stream.getAudioTracks().forEach(track => track.enabled = aEn);
setAudioEnabled(aEn);
setAudioEnabled(stream.toggleAudio());
}
const toggleVideo = () => {
if (!stream) { return; }
const vEn = !videoEnabled;
stream.getVideoTracks().forEach(track => track.enabled = vEn);
setVideoEnabled(vEn)
stream.toggleVideo()
.then(setVideoEnabled)
}
return (

View file

@ -6,10 +6,12 @@ import Counter from 'App/components/shared/SessionItem/Counter'
import stl from './chatWindow.css'
import ChatControls from '../ChatControls/ChatControls'
import Draggable from 'react-draggable';
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
export interface Props {
incomeStream: MediaStream | null,
localStream: MediaStream | null,
localStream: LocalStream | null,
userId: String,
endCall: () => void
}
@ -30,7 +32,7 @@ const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localS
<div className={cn(stl.videoWrapper, {'hidden' : minimize}, 'relative')}>
<VideoContainer stream={ incomeStream } />
<div className="absolute bottom-0 right-0 z-50">
<VideoContainer stream={ localStream } muted width={50} />
<VideoContainer stream={ localStream ? localStream.stream : null } muted width={50} />
</div>
</div>
<ChatControls stream={localStream} endCall={endCall} />

View file

@ -7,9 +7,25 @@ import { connectPlayer } from 'Player/store';
import ChatWindow from '../../ChatWindow';
import { callPeer } from 'Player'
import { CallingState, ConnectionStatus } from 'Player/MessageDistributor/managers/AssistManager';
import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream';
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
import { toast } from 'react-toastify';
import stl from './AassistActions.css'
function onClose(stream) {
stream.getTracks().forEach(t=>t.stop());
}
function onReject() {
toast.info(`Call was rejected.`);
}
function onError(e) {
toast.error(e);
}
interface Props {
userId: String,
toggleChatWindow: (state) => void,
@ -19,7 +35,7 @@ interface Props {
function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus }: Props) {
const [ incomeStream, setIncomeStream ] = useState<MediaStream | null>(null);
const [ localStream, setLocalStream ] = useState<MediaStream | null>(null);
const [ localStream, setLocalStream ] = useState<LocalStream | null>(null);
const [ endCall, setEndCall ] = useState<()=>void>(()=>{});
useEffect(() => {
@ -32,36 +48,18 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
}
}, [peerConnectionStatus])
function onClose(stream) {
stream.getTracks().forEach(t=>t.stop());
}
function onReject() {
toast.info(`Call was rejected.`);
}
function onError(e) {
toast.error(e);
}
function onCallConnect(lStream) {
setLocalStream(lStream);
setEndCall(() => callPeer(
lStream,
setIncomeStream,
onClose.bind(null, lStream),
onReject,
onError
));
}
function call() {
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(onCallConnect).catch(error => { // TODO retry only if specific error
navigator.mediaDevices.getUserMedia({audio:true})
.then(onCallConnect)
.catch(onError)
});
RequestLocalStream().then(lStream => {
setLocalStream(lStream);
setEndCall(() => callPeer(
lStream,
setIncomeStream,
lStream.stop.bind(lStream),
onReject,
onError
));
}).catch(onError)
}
const inCall = calling !== CallingState.False;

View file

@ -25,10 +25,9 @@ import { LAST_7_DAYS } from 'Types/app/period';
import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from '../shared/NoSessionsMessage';
import TrackerUpdateMessage from '../shared/TrackerUpdateMessage';
import LiveSessionList from './LiveSessionList'
const AUTOREFRESH_INTERVAL = 10 * 60 * 1000;
const weakEqual = (val1, val2) => {
if (!!val1 === false && !!val2 === false) return true;
if (!val1 !== !val2) return false;
@ -37,7 +36,6 @@ const weakEqual = (val1, val2) => {
@withLocationHandlers()
@connect(state => ({
shouldAutorefresh: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 0,
filter: state.getIn([ 'filters', 'appliedFilter' ]),
showLive: state.getIn([ 'user', 'account', 'appearance', 'sessionsLive' ]),
variables: state.getIn([ 'customFields', 'list' ]),
@ -93,12 +91,6 @@ export default class BugFinder extends React.PureComponent {
this.props.resetFunnel();
this.props.resetFunnelFilters();
this.autorefreshIntervalId = setInterval(() => {
if (this.props.shouldAutorefresh) {
props.applyFilter();
}
}, AUTOREFRESH_INTERVAL);
props.fetchFunnelsList(LAST_7_DAYS)
}
@ -129,10 +121,6 @@ export default class BugFinder extends React.PureComponent {
}.bind(this));
}
componentWillUnmount() {
clearInterval(this.autorefreshIntervalId);
}
setActiveTab = tab => {
this.props.setActiveTab(tab);
}
@ -151,6 +139,7 @@ export default class BugFinder extends React.PureComponent {
/>
</div>
<div className={cn("side-menu-margined", stl.searchWrapper) }>
<TrackerUpdateMessage />
<NoSessionsMessage />
<div
data-hidden={ activeTab === 'live' || activeTab === 'favorite' }

View file

@ -5,24 +5,44 @@ import { NoContent, Loader } from 'UI';
import { List, Map } from 'immutable';
import SessionItem from 'Shared/SessionItem';
const AUTOREFRESH_INTERVAL = 1 * 60 * 1000
interface Props {
loading: Boolean,
list?: List<any>,
fetchList: (params) => void,
applyFilter: () => void,
filters: List<any>
}
function LiveSessionList(props: Props) {
const { loading, list, filters } = props;
var timeoutId;
useEffect(() => {
props.fetchList(filters.toJS());
timeout();
return () => {
clearTimeout(timeoutId)
}
}, [])
const timeout = () => {
timeoutId = setTimeout(() => {
props.fetchList(filters.toJS());
timeout();
}, AUTOREFRESH_INTERVAL);
}
return (
<div>
<NoContent
title={"No live sessions!"}
title={"No live sessions."}
subtext={
<span>
See how to <a target="_blank" className="link" href="https://docs.openreplay.com/plugins/assist">{'enable Assist'}</a> if you haven't yet done so.
</span>
}
image={<img src="/img/live-sessions.png" style={{ width: '70%', marginBottom: '30px' }}/>}
show={ !loading && list && list.size === 0}
>

View file

@ -4,12 +4,14 @@ import { applyFilter, addAttribute, addEvent } from 'Duck/filters';
import SessionItem from 'Shared/SessionItem';
import SessionListHeader from './SessionListHeader';
import { KEYS } from 'Types/filter/customFilter';
import styles from './sessionList.css';
const ALL = 'all';
const PER_PAGE = 10;
const AUTOREFRESH_INTERVAL = 3 * 60 * 1000;
var timeoutId;
@connect(state => ({
shouldAutorefresh: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 0,
savedFilters: state.getIn([ 'filters', 'list' ]),
loading: state.getIn([ 'sessions', 'loading' ]),
activeTab: state.getIn([ 'sessions', 'activeTab' ]),
@ -27,6 +29,7 @@ export default class SessionList extends React.PureComponent {
}
constructor(props) {
super(props);
this.timeout();
}
componentDidUpdate(prevProps) {
@ -47,6 +50,15 @@ export default class SessionList extends React.PureComponent {
this.props.applyFilter()
}
timeout = () => {
timeoutId = setTimeout(function () {
if (this.props.shouldAutorefresh) {
this.props.applyFilter();
}
this.timeout();
}.bind(this), AUTOREFRESH_INTERVAL);
}
getNoContentMessage = activeTab => {
let str = "No recordings found";
if (activeTab.type !== 'all') {
@ -57,6 +69,10 @@ export default class SessionList extends React.PureComponent {
return str + '!';
}
componentWillUnmount() {
clearTimeout(timeoutId)
}
renderActiveTabContent(list) {
const {
loading,

View file

@ -7,6 +7,7 @@ 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';
function SessionsMenu(props) {
const {
@ -75,11 +76,15 @@ function SessionsMenu(props) {
<div className={stl.divider} />
<div className="my-3">
<SideMenuitem
title="Assist"
title={ <div className="flex items-center">
<div>Assist</div>
<div className="ml-2">{ <NewBadge />}</div>
</div> }
iconName="person"
active={activeTab.type === 'live'}
onClick={() => onMenuItemClick({ name: 'Assist', type: 'live' })}
/>
</div>
<div className={stl.divider} />

View file

@ -1,60 +1,53 @@
import Highlight from 'react-highlight'
import ToggleContent from 'Shared/ToggleContent'
import DocLink from 'Shared/DocLink/DocLink';
import AssistScript from './AssistScript'
import AssistNpm from './AssistNpm'
import { Tabs } from 'UI';
import { useState } from 'react';
const NPM = 'NPM'
const SCRIPT = 'SCRIPT'
const TABS = [
{ key: SCRIPT, text: SCRIPT },
{ key: NPM, text: NPM },
]
const AssistDoc = (props) => {
const { projectKey } = props;
const [activeTab, setActiveTab] = useState(SCRIPT)
const renderActiveTab = () => {
switch (activeTab) {
case SCRIPT:
return <AssistScript projectKey={projectKey} />
case NPM:
return <AssistNpm projectKey={projectKey} />
}
return null;
}
return (
<div className="p-4">
<div>OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.</div>
<div className="font-bold my-2">Installation</div>
<Highlight className="js">
{`npm i @openreplay/tracker-assist`}
</Highlight>
<div className="font-bold my-2">Usage</div>
<p>Initialize the tracker then load the @openreplay/tracker-assist plugin.</p>
<div className="py-3" />
<div className="mb-4" />
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Is SSR?"
first={
<Highlight className="js">
{`import Tracker from '@openreplay/tracker';
import trackerAssist from '@openreplay/tracker-assist';
const tracker = new Tracker({
projectKey: PROJECT_KEY,
});
tracker.start();
tracker.use(trackerAssist(options)); // check the list of available options below`}
</Highlight>
}
second={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker/cjs';
import trackerFetch from '@openreplay/tracker-assist/cjs';
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
});
const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below
//...
function MyApp() {
useEffect(() => { // use componentDidMount in case of React Class Component
tracker.start();
}, [])
//...
}`}
</Highlight>
}
<Tabs
tabs={ TABS }
active={ activeTab } onClick={ (tab) => setActiveTab(tab) }
/>
<div className="font-bold my-2">Options</div>
<Highlight className="js">
{`trackerAssist({
confirmText: string;
})`}
</Highlight>
<div className="py-5">
{ renderActiveTab() }
</div>
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
</div>

View file

@ -0,0 +1,53 @@
import React from 'react';
import Highlight from 'react-highlight'
import ToggleContent from 'Shared/ToggleContent'
function AssistNpm(props) {
return (
<div>
<p>Initialize the tracker then load the @openreplay/tracker-assist plugin.</p>
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import Tracker from '@openreplay/tracker';
import trackerAssist from '@openreplay/tracker-assist';
const tracker = new Tracker({
projectKey: '${props.projectKey}',
});
tracker.start();
tracker.use(trackerAssist(options)); // check the list of available options below`}
</Highlight>
}
second={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker/cjs';
import trackerFetch from '@openreplay/tracker-assist/cjs';
const tracker = new OpenReplay({
projectKey: '${props.projectKey}'
});
const trackerAssist = tracker.use(trackerAssist(options)); // check the list of available options below
//...
function MyApp() {
useEffect(() => { // use componentDidMount in case of React Class Component
tracker.start();
}, [])
//...
}`}
</Highlight>
}
/>
<div className="font-bold my-2">Options</div>
<Highlight className="js">
{`trackerAssist({
confirmText: string;
})`}
</Highlight>
</div>
);
}
export default AssistNpm;

View file

@ -0,0 +1,33 @@
import React from 'react';
import Highlight from 'react-highlight'
function AssistScript(props) {
return (
<div>
<p>If your OpenReplay tracker is set up using the JS snippet, then simply replace the .../openreplay.js occurrence with .../openreplay-assist.js. Below is an example of how the script should like after the change:</p>
<div className="py-3" />
<Highlight className="js">
{`<!-- OpenReplay Tracking Code -->
<script>
(function(A,s,a,y,e,r){
r=window.OpenReplay=[s,r,e,[y-1]];
s=document.createElement('script');s.src=a;s.async=!A;
document.getElementsByTagName('head')[0].appendChild(s);
r.start=function(v){r.push([0])};
r.stop=function(v){r.push([1])};
r.setUserID=function(id){r.push([2,id])};
r.setUserAnonymousID=function(id){r.push([3,id])};
r.setMetadata=function(k,v){r.push([4,k,v])};
r.event=function(k,p,i){r.push([5,k,p,i])};
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);
</script>`}
</Highlight>
</div>
);
}
export default AssistScript;

View file

@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent'
import DocLink from 'Shared/DocLink/DocLink';
const FetchDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<div>This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
@ -18,14 +19,14 @@ const FetchDoc = (props) => {
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import tracker from '@openreplay/tracker';
import trackerFetch from '@openreplay/tracker-fetch';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.start();
//...
@ -40,7 +41,7 @@ fetch('https://api.openreplay.com/').then(response => console.log(response.json(
import trackerFetch from '@openreplay/tracker-fetch/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
//...
function SomeFunctionalComponent() {

View file

@ -3,6 +3,7 @@ import DocLink from 'Shared/DocLink/DocLink';
import ToggleContent from 'Shared/ToggleContent';
const GraphQLDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<p>This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</p>
@ -19,14 +20,14 @@ const GraphQLDoc = (props) => {
<div className="py-3" />
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker';
import trackerGraphQL from '@openreplay/tracker-graphql';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY,
projectKey: '${projectKey}'
});
tracker.start();
//...
@ -39,7 +40,7 @@ export const recordGraphQL = tracker.use(trackerGraphQL());`}
import trackerGraphQL from '@openreplay/tracker-graphql/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
//...
function SomeFunctionalComponent() {

View file

@ -75,7 +75,7 @@ const TITLE = {
[ ASSIST ] : 'Assist',
}
const DOCS = [REDUX, VUE, GRAPHQL, NGRX, FETCH, MOBX, PROFILER]
const DOCS = [REDUX, VUE, GRAPHQL, NGRX, FETCH, MOBX, PROFILER, ASSIST]
const integrations = [ 'sentry', 'datadog', 'stackdriver', 'rollbar', 'newrelic', 'bugsnag', 'cloudwatch', 'elasticsearch', 'sumologic', 'issues' ];
@ -87,12 +87,14 @@ const integrations = [ 'sentry', 'datadog', 'stackdriver', 'rollbar', 'newrelic'
state.getIn([ name, 'list' ]).size > 0;
props.loading = props.loading || state.getIn([ name, 'fetchRequest', 'loading']);
})
const site = state.getIn([ 'site', 'instance' ]);
return {
...props,
issues: state.getIn([ 'issues', 'list']).first() || {},
slackChannelListExists: state.getIn([ 'slack', 'list' ]).size > 0,
tenantId: state.getIn([ 'user', 'client', 'tenantId' ]),
jwt: state.get('jwt')
jwt: state.get('jwt'),
projectKey: site ? site.projectKey : ''
};
}, {
fetchList,
@ -142,7 +144,9 @@ export default class Integrations extends React.PureComponent {
}
}
renderModalContent() {
renderModalContent() {
const { projectKey } = this.props;
switch (this.state.modalContent) {
case SENTRY:
return <SentryForm onClose={ this.closeModal } />;
@ -172,21 +176,21 @@ export default class Integrations extends React.PureComponent {
case JIRA:
return <JiraForm onClose={ this.closeModal } />;
case REDUX:
return <ReduxDoc onClose={ this.closeModal } />
return <ReduxDoc onClose={ this.closeModal } projectKey={projectKey} />
case VUE:
return <VueDoc onClose={ this.closeModal } />
return <VueDoc onClose={ this.closeModal } projectKey={projectKey} />
case GRAPHQL:
return <GraphQLDoc onClose={ this.closeModal } />
return <GraphQLDoc onClose={ this.closeModal } projectKey={projectKey} />
case NGRX:
return <NgRxDoc onClose={ this.closeModal } />
return <NgRxDoc onClose={ this.closeModal } projectKey={projectKey} />
case FETCH:
return <FetchDoc onClose={ this.closeModal } />
return <FetchDoc onClose={ this.closeModal } projectKey={projectKey} />
case MOBX:
return <MobxDoc onClose={ this.closeModal } />
return <MobxDoc onClose={ this.closeModal } projectKey={projectKey} />
case PROFILER:
return <ProfilerDoc onClose={ this.closeModal } />
return <ProfilerDoc onClose={ this.closeModal } projectKey={projectKey} />
case ASSIST:
return <AssistDoc onClose={ this.closeModal } />
return <AssistDoc onClose={ this.closeModal } projectKey={projectKey} />
default:
return null;
}

View file

@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent'
import DocLink from 'Shared/DocLink/DocLink';
const MobxDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<div>This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
@ -18,14 +19,14 @@ const MobxDoc = (props) => {
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker';
import trackerMobX from '@openreplay/tracker-mobx';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.use(trackerMobX(<options>)); // check list of available options below
tracker.start();`}
@ -37,7 +38,7 @@ tracker.start();`}
import trackerMobX from '@openreplay/tracker-mobx/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.use(trackerMobX(<options>)); // check list of available options below
//...

View file

@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent'
import DocLink from 'Shared/DocLink/DocLink';
const NgRxDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<div>This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
@ -18,7 +19,7 @@ const NgRxDoc = (props) => {
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import { StoreModule } from '@ngrx/store';
@ -27,7 +28,7 @@ import OpenReplay from '@openreplay/tracker';
import trackerNgRx from '@openreplay/tracker-ngrx';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.start();
//...
@ -47,7 +48,7 @@ import OpenReplay from '@openreplay/tracker/cjs';
import trackerNgRx from '@openreplay/tracker-ngrx/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
//...
function SomeFunctionalComponent() {

View file

@ -3,6 +3,7 @@ import ToggleContent from 'Shared/ToggleContent'
import DocLink from 'Shared/DocLink/DocLink';
const ProfilerDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<div>The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function call.</div>
@ -18,14 +19,14 @@ const ProfilerDoc = (props) => {
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker';
import trackerProfiler from '@openreplay/tracker-profiler';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.start();
//...
@ -42,7 +43,7 @@ const fn = profiler('call_name')(() => {
import trackerProfiler from '@openreplay/tracker-profiler/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
//...
function SomeFunctionalComponent() {

View file

@ -3,6 +3,7 @@ import ToggleContent from '../../../shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
const ReduxDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<div>This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
@ -17,7 +18,7 @@ const ReduxDoc = (props) => {
<p>Initialize the tracker then put the generated middleware into your Redux chain.</p>
<div className="py-3" />
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import { applyMiddleware, createStore } from 'redux';
@ -25,7 +26,7 @@ import OpenReplay from '@openreplay/tracker';
import trackerRedux from '@openreplay/tracker-redux';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.start();
//...
@ -42,7 +43,7 @@ import OpenReplay from '@openreplay/tracker/cjs';
import trackerRedux from '@openreplay/tracker-redux/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
//...
function SomeFunctionalComponent() {

View file

@ -3,6 +3,7 @@ import ToggleContent from '../../../shared/ToggleContent';
import DocLink from 'Shared/DocLink/DocLink';
const VueDoc = (props) => {
const { projectKey } = props;
return (
<div className="p-4">
<div>This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
@ -18,7 +19,7 @@ const VueDoc = (props) => {
<ToggleContent
label="Is SSR?"
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import Vuex from 'vuex'
@ -26,7 +27,7 @@ import OpenReplay from '@openreplay/tracker';
import trackerVuex from '@openreplay/tracker-vuex';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
tracker.start();
//...
@ -43,7 +44,7 @@ import OpenReplay from '@openreplay/tracker/cjs';
import trackerVuex from '@openreplay/tracker-vuex/cjs';
//...
const tracker = new OpenReplay({
projectKey: PROJECT_KEY
projectKey: '${projectKey}'
});
//...
function SomeFunctionalComponent() {

View file

@ -27,7 +27,7 @@ const LIMIT_WARNING = 'You have reached users limit.';
fetchList,
generateInviteLink
})
@withPageTitle('Manage Users - OpenReplay Preferences')
@withPageTitle('Users - OpenReplay Preferences')
class ManageUsers extends React.PureComponent {
state = { showModal: false, remaining: this.props.account.limits.teamMember.remaining, invited: false }
@ -50,7 +50,7 @@ class ManageUsers extends React.PureComponent {
deleteHandler = async (user) => {
if (await confirm({
header: 'Manage Users',
header: 'Users',
confirmation: `Are you sure you want to remove this user?`
})) {
this.props.deleteMember(user.id).then(() => {

View file

@ -72,7 +72,7 @@ function PreferencesMenu({ activeTab, appearance, history }) {
<div className="mb-4">
<SideMenuitem
active={ activeTab === CLIENT_TABS.MANAGE_USERS }
title="Manage Users"
title="Users"
iconName="users"
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS) }
/>

View file

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { Input, Button, Label } from 'UI';
import { save, edit, update , fetchList } from 'Duck/site';
import { pushNewSite, setSiteId } from 'Duck/user';
import { withRouter } from 'react-router-dom';
import styles from './siteForm.css';
@connect(state => ({
@ -17,6 +18,7 @@ import styles from './siteForm.css';
fetchList,
setSiteId
})
@withRouter
export default class NewSiteForm extends React.PureComponent {
state = {
existsError: false,
@ -24,7 +26,7 @@ export default class NewSiteForm extends React.PureComponent {
onSubmit = e => {
e.preventDefault();
const { site, siteList } = this.props;
const { site, siteList, location: { pathname } } = this.props;
if (!site.exists() && siteList.some(({ name }) => name === site.name)) {
return this.setState({ existsError: true });
}
@ -39,20 +41,21 @@ export default class NewSiteForm extends React.PureComponent {
const site = sites.last();
this.props.pushNewSite(site)
this.props.setSiteId(site.id)
if (!pathname.includes('/client')) {
this.props.setSiteId(site.id)
}
this.props.onClose(null, site)
});
}
}
edit = ({ target: { name, value } }) => {
if (value.includes(' ')) return; // TODO: site validation
this.setState({ existsError: false });
this.props.edit({ [ name ]: value });
}
render() {
const { site, loading, onClose } = this.props;
const { site, loading } = this.props;
return (
<form className={ styles.formWrapper } onSubmit={ this.onSubmit }>
<div className={ styles.content }>

View file

@ -34,6 +34,7 @@
.activeLink {
cursor: pointer;
pointer-events: default;
text-decoration: underline;
& label {
color: #000000 !important;
text-decoration: underline;

View file

@ -22,7 +22,6 @@ class TrackingCodeModal extends React.PureComponent {
}
renderActiveTab = () => {
console.log('rendering...')
switch (this.state.activeTab) {
case PROJECT:
return <ProjectCodeSnippet />

View file

@ -4,9 +4,10 @@ import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel'
import { Controls as PlayerControls } from 'Player';
import { Tabs } from 'UI';
import { connectPlayer } from 'Player';
import NewBadge from 'Shared/NewBadge';
const EVENTS = 'Events';
const HEATMAPS = 'Heatmaps';
const HEATMAPS = 'Click Map';
const TABS = [ EVENTS, HEATMAPS ].map(tab => ({ text: tab, key: tab }));
@ -29,12 +30,15 @@ export default function RightBlock() {
}
return (
<div style={{ width: '270px', height: 'calc(100vh- 50px)'}} className="flex flex-col">
<Tabs
tabs={ TABS }
active={ activeTab }
onClick={ (tab) => setActiveTab(tab) }
border={ true }
/>
<div className="relative">
<Tabs
tabs={ TABS }
active={ activeTab }
onClick={ (tab) => setActiveTab(tab) }
border={ true }
/>
<div className="absolute" style={{ left: '160px', top: '13px' }}>{ <NewBadge />}</div>
</div>
{
renderActiveTab(activeTab)
}

View file

@ -42,7 +42,9 @@ export default class EventsBlock extends React.PureComponent {
this.props.setEventFilter({ query: value, filter })
setTimeout(() => {
this.scroller.current.scrollToRow(0);
if (!this.scroller.current) return;
this.scroller.current.scrollToRow(0);
}, 100)
}
@ -54,7 +56,9 @@ export default class EventsBlock extends React.PureComponent {
this.scroller.current.forceUpdateGrid();
setTimeout(() => {
this.scroller.current.scrollToRow(0);
if (!this.scroller.current) return;
this.scroller.current.scrollToRow(0);
}, 100)
}
@ -176,6 +180,7 @@ export default class EventsBlock extends React.PureComponent {
userNumericHash,
userDisplayName,
userId,
revId,
userAnonymousId
},
filteredEvents
@ -191,6 +196,7 @@ export default class EventsBlock extends React.PureComponent {
userNumericHash={userNumericHash}
userDisplayName={userDisplayName}
userId={userId}
revId={revId}
userAnonymousId={userAnonymousId}
/>

View file

@ -6,7 +6,7 @@ import Metadata from '../Metadata'
import { withRequest } from 'HOCs'
import SessionList from '../Metadata/SessionList'
function UserCard({ className, userNumericHash, userDisplayName, similarSessions, userId, userAnonymousId, request, loading }) {
function UserCard({ className, userNumericHash, userDisplayName, similarSessions, userId, userAnonymousId, request, loading, revId }) {
const [showUserSessions, setShowUserSessions] = useState(false)
const hasUserDetails = !!userId || !!userAnonymousId;
@ -29,6 +29,11 @@ function UserCard({ className, userNumericHash, userDisplayName, similarSessions
</TextEllipsis>
</div>
</div>
{revId && (
<div className="border-t py-2 px-3">
<span className="font-medium">Rev ID:</span> {revId}
</div>
)}
<div className="border-t">
<Metadata />
</div>

View file

@ -14,7 +14,7 @@ import BottomBlock from '../BottomBlock';
@connect(state => ({
session: state.getIn([ 'sessions', 'current' ]),
errorStack: state.getIn([ 'sessions', 'errorStack' ]),
sourceMapUploaded: state.getIn([ 'sessions', 'sourceMapUploaded' ]),
sourcemapUploaded: state.getIn([ 'sessions', 'sourcemapUploaded' ]),
loading: state.getIn([ 'sessions', 'fetchErrorStackList', 'loading' ])
}), { fetchErrorStackList })
export default class Exceptions extends React.PureComponent {
@ -33,7 +33,7 @@ export default class Exceptions extends React.PureComponent {
closeModal = () => this.setState({ currentError: null})
render() {
const { exceptions, loading, errorStack, sourceMapUploaded } = this.props;
const { exceptions, loading, errorStack, sourcemapUploaded } = this.props;
const { filter, currentError } = this.state;
const filterRE = getRE(filter, 'i');
@ -63,7 +63,7 @@ export default class Exceptions extends React.PureComponent {
show={ !loading && errorStack.size === 0 }
title="Nothing found!"
>
<ErrorDetails error={ currentError.name } errorStack={errorStack} sourceMapUploaded={sourceMapUploaded} />
<ErrorDetails error={ currentError.name } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
</NoContent>
</Loader>
</div>

View file

@ -399,7 +399,7 @@ export default class Controls extends React.Component {
icon="tachometer-slow"
/>
}
{ !live && showLongtasks &&
{/* { !live && showLongtasks &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(LONGTASKS) }
@ -407,7 +407,7 @@ export default class Controls extends React.Component {
label="Long Tasks"
icon="business-time"
/>
}
} */}
<div className={ styles.divider } />
{ !live &&
<React.Fragment>

View file

@ -26,11 +26,24 @@ export default class SignupForm extends React.Component {
email: '',
projectName: '',
organizationName: '',
reload: false,
};
static getDerivedStateFromProps(props, state) {
if (props.errors && props.errors.size > 0 && state.reload) {
recaptchaRef.current.reset();
return {
reload: false
}
}
return null;
}
handleSubmit = (token) => {
const { tenantId, fullname, password, email, projectName, organizationName, auth } = this.state;
this.props.signup({ tenantId, fullname, password, email, projectName, organizationName, auth, 'g-recaptcha-response': token })
this.setState({ reload: true })
}
write = ({ target: { value, name } }) => this.setState({ [ name ]: value })

View file

@ -0,0 +1,40 @@
import React from 'react'
import { Icon } from 'UI'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom';
import { onboarding as onboardingRoute } from 'App/routes'
import { withSiteId } from 'App/routes';
const TrackerUpdateMessage= (props) => {
// const { site } = 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;
return needUpdate ? (
<>
{(
<div>
<div
className="rounded text-sm flex items-center justify-between mb-4"
style={{ height: '42px', backgroundColor: 'rgba(255, 239, 239, 1)', border: 'solid thin rgba(221, 181, 181, 1)'}}
>
<div className="flex items-center w-full">
<div className="flex-shrink-0 w-8 flex justify-center">
<Icon name="info-circle" size="14" color="gray-darkest" />
</div>
<div className="ml-2color-gray-darkest mr-auto">
There might be a mismatch between the tracker and the backend versions. Please make sure to <a href="#" className="link" onClick={() => props.history.push(withSiteId(onboardingRoute('installing'), siteId))}>update</a> the tracker to latest version (<a href="https://www.npmjs.com/package/@openreplay/tracker" target="_blank">{window.ENV.TRACKER_VERSION}</a>).
</div>
</div>
</div>
</div>
)}
</>
) : ''
}
export default connect(state => ({
site: state.getIn([ 'site', 'instance' ]),
sites: state.getIn([ 'site', 'list' ])
}))(withRouter(TrackerUpdateMessage))

View file

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

View file

@ -19,7 +19,6 @@ import { funnel as funnelRoute, withSiteId } from 'App/routes';
import Event, { TYPES } from 'Types/filter/event';
import FunnelMenuItem from 'Components/Funnels/FunnelMenuItem';
import FunnelSaveModal from 'Components/Funnels/FunnelSaveModal';
import NewBadge from 'Shared/NewBadge';
import { blink as setBlink } from 'Duck/funnels';
const DEFAULT_VISIBLE = 3;
@ -98,7 +97,6 @@ class SavedSearchList extends React.Component {
onClick={ this.createHandler }
/>
)}
<div className="ml-2">{ <NewBadge />}</div>
</div>
</div>
{ funnels.size === 0 &&

View file

@ -5,11 +5,13 @@ import type { TimedMessage } from '../Timed';
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';
export enum CallingState {
Reconnecting,
Requesting,
True,
False,
@ -38,7 +40,7 @@ export function getStatusText(status: ConnectionStatus): string {
case ConnectionStatus.Error:
return "Something went wrong. Try to reload the page.";
case ConnectionStatus.WaitingMessages:
return "Connected. Waiting for the data..."
return "Connected. Waiting for the data... (The tab might be inactive)"
}
}
@ -161,7 +163,6 @@ export default class AssistManager {
if (['peer-unavailable', 'network', 'webrtc'].includes(e.type)) {
if (this.peer && this.connectionAttempts++ < MAX_RECONNECTION_COUNT) {
this.setStatus(ConnectionStatus.Connecting);
console.log("peerunavailable")
this.connectToPeer();
} else {
this.setStatus(ConnectionStatus.Disconnected);
@ -175,7 +176,6 @@ export default class AssistManager {
peer.on("open", () => {
if (this.peeropened) { return; }
this.peeropened = true;
console.log('peeropen')
this.connectToPeer();
});
});
@ -186,11 +186,16 @@ export default class AssistManager {
if (!this.peer) { return; }
this.setStatus(ConnectionStatus.Connecting);
const id = this.peerID;
console.log("trying to connect to", id)
const conn = this.peer.connect(id, { serialization: 'json', reliable: true});
conn.on('open', () => {
window.addEventListener("beforeunload", ()=>conn.open &&conn.send("unload"));
console.log("peer connected")
//console.log("peer connected")
if (getState().calling === CallingState.Reconnecting) {
this._call()
}
let i = 0;
let firstMessage = true;
@ -199,7 +204,7 @@ export default class AssistManager {
conn.on('data', (data) => {
if (!Array.isArray(data)) { return this.handleCommand(data); }
this.mesagesRecieved = true;
this.disconnectTimeout && clearTimeout(this.disconnectTimeout);
if (firstMessage) {
firstMessage = false;
this.setStatus(ConnectionStatus.Connected)
@ -250,9 +255,8 @@ export default class AssistManager {
const onDataClose = () => {
this.initiateCallEnd();
this.setStatus(ConnectionStatus.Connecting);
console.log('closed peer conn. Reconnecting...')
this.onCallDisconnect()
//console.log('closed peer conn. Reconnecting...')
this.connectToPeer();
}
@ -263,7 +267,6 @@ export default class AssistManager {
// }, 3000);
conn.on('close', onDataClose);// Does it work ?
conn.on("error", (e) => {
console.log("PeerJS connection error", e);
this.setStatus(ConnectionStatus.Error);
})
}
@ -282,49 +285,49 @@ export default class AssistManager {
}
private onCallEnd: null | (()=>void) = null;
private onReject: null | (()=>void) = null;
private forceCallEnd() {
this.callConnection?.close();
}
private notifyCallEnd() {
const dataConn = this.dataConnection;
if (dataConn) {
console.log("notifyCallEnd send")
dataConn.send("call_end");
}
}
private initiateCallEnd = () => {
console.log('initiateCallEnd')
this.forceCallEnd();
this.notifyCallEnd();
this.onCallEnd?.();
this.localCallData && this.localCallData.onCallEnd();
}
private onTrackerCallEnd = () => {
console.log('onTrackerCallEnd')
this.forceCallEnd();
if (getState().calling === CallingState.Requesting) {
this.onReject?.();
this.localCallData && this.localCallData.onReject();
}
this.localCallData && this.localCallData.onCallEnd();
}
private onCallDisconnect = () => {
if (getState().calling === CallingState.True) {
update({ calling: CallingState.Reconnecting });
}
this.onCallEnd?.();
}
private mesagesRecieved: boolean = false;
private disconnectTimeout: ReturnType<typeof setTimeout> | undefined;
private handleCommand(command: string) {
console.log("Data command", command)
switch (command) {
case "unload":
this.onTrackerCallEnd();
this.mesagesRecieved = false;
setTimeout(() => {
if (this.mesagesRecieved) {
return;
}
// @ts-ignore
this.dataConnection?.close();
//this.onTrackerCallEnd();
this.onCallDisconnect()
this.dataConnection?.close();
this.disconnectTimeout = setTimeout(() => {
this.onTrackerCallEnd();
this.setStatus(ConnectionStatus.Disconnected);
}, 8000); // TODO: more convenient way
}, 15000); // TODO: more convenient way
//this.dataConnection?.close();
return;
case "call_end":
@ -345,63 +348,80 @@ export default class AssistManager {
conn.send({ x: Math.round(data.x), y: Math.round(data.y) });
}
call(localStream: MediaStream, onStream: (s: MediaStream)=>void, onCallEnd: () => void, onReject: () => void, onError?: ()=> void): null | Function {
if (!this.peer || getState().calling !== CallingState.False) { return null; }
private localCallData: {
localStream: LocalStream,
onStream: (s: MediaStream)=>void,
onCallEnd: () => void,
onReject: () => void,
onError?: ()=> void
} | null = null
call(localStream: LocalStream, onStream: (s: MediaStream)=>void, onCallEnd: () => void, onReject: () => void, onError?: ()=> void): null | Function {
this.localCallData = {
localStream,
onStream,
onCallEnd: () => {
onCallEnd();
this.md.overlay.removeEventListener("mousemove", this.onMouseMove);
update({ calling: CallingState.False });
this.localCallData = null;
},
onReject,
onError,
}
this._call()
return this.initiateCallEnd;
}
private _call() {
if (!this.peer || !this.localCallData || ![CallingState.False, CallingState.Reconnecting].includes(getState().calling)) { return null; }
update({ calling: CallingState.Requesting });
console.log('calling...')
const call = this.peer.call(this.peerID, localStream);
call.on('stream', stream => {
//call.peerConnection.ontrack = (t)=> console.log('ontrack', t)
//console.log('calling...', this.localCallData.localStream)
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")
if (!sender) {
//logger.warn("No video sender found")
return
}
//logger.log("sender found:", sender)
sender.replaceTrack(vTrack)
})
call.on('stream', stream => {
update({ calling: CallingState.True });
onStream(stream);
this.localCallData && this.localCallData.onStream(stream);
this.send({
name: store.getState().getIn([ 'user', 'account', 'name']),
});
// @ts-ignore ??
this.md.overlay.addEventListener("mousemove", this.onMouseMove)
});
//call.peerConnection.addEventListener("track", e => console.log('newtrack',e.track))
this.onCallEnd = () => {
onCallEnd();
// @ts-ignore ??
this.md.overlay.removeEventListener("mousemove", this.onMouseMove);
update({ calling: CallingState.False });
this.onCallEnd = null;
}
call.on("close", this.onCallEnd);
call.on("close", this.localCallData.onCallEnd);
call.on("error", (e) => {
console.error("PeerJS error (on call):", e)
this.initiateCallEnd?.();
onError?.();
this.initiateCallEnd();
this.localCallData && this.localCallData.onError && this.localCallData.onError();
});
// const intervalID = setInterval(() => {
// if (!call.open && getState().calling === CallingState.True) {
// this.onCallEnd?.();
// clearInterval(intervalID);
// }
// }, 5000);
window.addEventListener("beforeunload", this.initiateCallEnd)
return this.initiateCallEnd;
}
clear() {
console.log('clearing', this.peerID)
this.initiateCallEnd();
this.dataCheckIntervalID && clearInterval(this.dataCheckIntervalID);
if (this.peer) {
this.peer.connections[this.peerID]?.forEach(c => c.open && c.close());
console.log("destroying peer...")
this.peer.disconnect();
this.peer.destroy();
//console.log("destroying peer...")
const peer = this.peer; // otherwise it calls reconnection on data chan close
this.peer = null;
peer.destroy();
}
}
}

View file

@ -3,7 +3,7 @@ import type { Message, SetNodeScroll, CreateElementNode } from '../messages';
import type { TimedMessage } from '../Timed';
import logger from 'App/logger';
import StylesManager from './StylesManager';
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
import ListWalker from './ListWalker';
import type { Timed }from '../Timed';
@ -147,6 +147,7 @@ export default class DOMManager extends ListWalker<TimedMessage> {
this.insertNode(msg);
break;
case "create_element_node":
// console.log('elementnode', msg)
if (msg.svg) {
this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag);
} else {
@ -207,9 +208,14 @@ export default class DOMManager extends ListWalker<TimedMessage> {
break;
case "set_node_data":
case "set_css_data":
if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; }
node = this.nl[ msg.id ]
if (!node) { logger.error("Node not found", msg); break; }
// @ts-ignore
this.nl[ msg.id ].data = msg.data;
node.data = msg.data;
if (node instanceof HTMLStyleElement) {
const doc = this.screen.document
doc && rewriteNodeStyleSheet(doc, node)
}
break;
case "css_insert_rule":
node = this.nl[ msg.id ];
@ -239,6 +245,22 @@ export default class DOMManager extends ListWalker<TimedMessage> {
} catch (e) {
logger.warn(e, msg)
}
break;
case "create_i_frame_document":
// console.log('ifr', msg)
node = this.nl[ msg.frameID ];
if (!(node instanceof HTMLIFrameElement)) {
logger.warn("create_i_frame_document message. Node is not iframe")
return;
}
// await new Promise(resolve => { node.onload = resolve })
const doc = node.contentDocument;
if (!doc) {
logger.warn("No iframe doc", msg, node, node.contentDocument);
return;
}
this.nl[ msg.id ] = doc.documentElement
break;
//not sure what to do with this one
//case "disconnected":

View file

@ -0,0 +1,85 @@
declare global {
interface HTMLCanvasElement {
captureStream(frameRate?: number): MediaStream;
}
}
function dummyTrack(): MediaStreamTrack {
const canvas = document.createElement("canvas")//, { width: 0, height: 0})
canvas.width=canvas.height=2 // Doesn't work when 1 (?!)
const ctx = canvas.getContext('2d');
ctx?.fillRect(0, 0, canvas.width, canvas.height);
requestAnimationFrame(function draw(){
ctx?.fillRect(0,0, canvas.width, canvas.height)
requestAnimationFrame(draw);
});
// Also works. Probably it should be done once connected.
//setTimeout(() => { ctx?.fillRect(0,0, canvas.width, canvas.height) }, 4000)
return canvas.captureStream(60).getTracks()[0];
}
export default function RequestLocalStream(): Promise<LocalStream> {
return navigator.mediaDevices.getUserMedia({ audio:true })
.then(aStream => {
const aTrack = aStream.getAudioTracks()[0]
if (!aTrack) { throw new Error("No audio tracks provided") }
return new _LocalStream(aTrack)
})
}
class _LocalStream {
private mediaRequested: boolean = false
readonly stream: MediaStream
private readonly vdTrack: MediaStreamTrack
constructor(aTrack: MediaStreamTrack) {
this.vdTrack = dummyTrack()
this.stream = new MediaStream([ aTrack, this.vdTrack ])
}
toggleVideo(): Promise<boolean> {
if (!this.mediaRequested) {
return navigator.mediaDevices.getUserMedia({video:true})
.then(vStream => {
const vTrack = vStream.getVideoTracks()[0]
if (!vTrack) {
throw new Error("No video track provided")
}
this.stream.addTrack(vTrack)
this.stream.removeTrack(this.vdTrack)
this.mediaRequested = true
if (this.onVideoTrackCb) {
this.onVideoTrackCb(vTrack)
}
return true
})
.catch(e => {
// TODO: log
return false
})
}
let enabled = true
this.stream.getVideoTracks().forEach(track => {
track.enabled = enabled = enabled && !track.enabled
})
return Promise.resolve(enabled)
}
toggleAudio(): boolean {
let enabled = true
this.stream.getAudioTracks().forEach(track => {
track.enabled = enabled = enabled && !track.enabled
})
return enabled
}
private onVideoTrackCb: ((t: MediaStreamTrack) => void) | null = null
onVideoTrack(cb: (t: MediaStreamTrack) => void) {
this.onVideoTrackCb = cb
}
stop() {
this.stream.getTracks().forEach(t => t.stop())
}
}
export type LocalStream = InstanceType<typeof _LocalStream>

View file

@ -1,4 +1,3 @@
// @flow
import type StatedScreen from '../StatedScreen';
import type { CssInsertRule, CssDeleteRule } from '../messages';
@ -10,21 +9,35 @@ type TimedCSSRuleMessage = Timed & CSSRuleMessage;
import logger from 'App/logger';
import ListWalker from './ListWalker';
export default class StylesManager extends ListWalker<TimedCSSRuleMessage> {
#screen: StatedScreen;
_linkLoadingCount: number = 0;
_linkLoadPromises: Array<Promise<void>> = [];
_skipCSSLinks: Array<string> = []; // should be common for all pages
constructor(screen: StatedScreen) {
const HOVER_CN = "-openreplay-hover";
const HOVER_SELECTOR = `.${HOVER_CN}`;
// Doesn't work with css files (hasOwnProperty)
export function rewriteNodeStyleSheet(doc: Document, node: HTMLLinkElement | HTMLStyleElement) {
const ss = Object.values(doc.styleSheets).find(s => s.ownerNode === node);
if (!ss || !ss.hasOwnProperty('rules')) { return; }
for(let i = 0; i < ss.rules.length; i++){
const r = ss.rules[i]
if (r instanceof CSSStyleRule) {
r.selectorText = r.selectorText.replace('/\:hover/g', HOVER_SELECTOR)
}
}
}
export default class StylesManager extends ListWalker<TimedCSSRuleMessage> {
private linkLoadingCount: number = 0;
private linkLoadPromises: Array<Promise<void>> = [];
private skipCSSLinks: Array<string> = []; // should be common for all pages
constructor(private readonly screen: StatedScreen) {
super();
this.#screen = screen;
}
reset():void {
super.reset();
this._linkLoadingCount = 0;
this._linkLoadPromises = [];
this.linkLoadingCount = 0;
this.linkLoadPromises = [];
//cancel all promises? tothinkaboutit
}
@ -32,30 +45,34 @@ export default class StylesManager extends ListWalker<TimedCSSRuleMessage> {
setStyleHandlers(node: HTMLLinkElement, value: string): void {
let timeoutId;
const promise = new Promise((resolve) => {
if (this._skipCSSLinks.includes(value)) resolve();
this._linkLoadingCount++;
this.#screen.setCSSLoading(true);
const setSkipAndResolve = () => {
this._skipCSSLinks.push(value); // watch out
resolve();
if (this.skipCSSLinks.includes(value)) resolve(null);
this.linkLoadingCount++;
this.screen.setCSSLoading(true);
const addSkipAndResolve = () => {
this.skipCSSLinks.push(value); // watch out
resolve(null);
}
timeoutId = setTimeout(setSkipAndResolve, 4000);
timeoutId = setTimeout(addSkipAndResolve, 4000);
node.onload = resolve;
node.onerror = setSkipAndResolve;
node.onload = () => {
const doc = this.screen.document;
doc && rewriteNodeStyleSheet(doc, node);
resolve(null);
}
node.onerror = addSkipAndResolve;
}).then(() => {
node.onload = null;
node.onerror = null;
clearTimeout(timeoutId);
this._linkLoadingCount--;
if (this._linkLoadingCount === 0) {
this.#screen.setCSSLoading(false);
this.linkLoadingCount--;
if (this.linkLoadingCount === 0) {
this.screen.setCSSLoading(false);
}
});
this._linkLoadPromises.push(promise);
this.linkLoadPromises.push(promise);
}
#manageRule = (msg: CSSRuleMessage):void => {
private manageRule = (msg: CSSRuleMessage):void => {
// if (msg.tp === "css_insert_rule") {
// let styleSheet = this.#screen.document.styleSheets[ msg.stylesheetID ];
// if (!styleSheet) {
@ -86,7 +103,7 @@ export default class StylesManager extends ListWalker<TimedCSSRuleMessage> {
}
moveReady(t: number): Promise<void> {
return Promise.all(this._linkLoadPromises)
.then(() => this.moveApply(t, this.#manageRule));
return Promise.all(this.linkLoadPromises)
.then(() => this.moveApply(t, this.manageRule));
}
}

View file

@ -57,11 +57,12 @@ export default class WindowNodeCounter {
addNode(id: number, parentID: number) {
if (!this._nodes[ parentID ]) {
console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`);
//TODO: iframe case
//console.error(`Wrong! Node with id ${ parentID } (parentId) not found.`);
return;
}
if (!!this._nodes[ id ]) {
console.error(`Wrong! Node with id ${ id } already exists.`);
//console.error(`Wrong! Node with id ${ id } already exists.`);
return;
}
this._nodes[id] = this._nodes[ parentID ].newChild();

View file

@ -36,6 +36,8 @@ export const ID_TP_MAP = {
54: "connection_information",
55: "set_page_visibility",
59: "long_task",
69: "mouse_click",
70: "create_i_frame_document",
} as const;
@ -255,11 +257,26 @@ export interface LongTask {
containerName: string,
}
export interface MouseClick {
tp: "mouse_click",
id: number,
hesitationTime: number,
label: string,
selector: string,
}
export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask;
export interface CreateIFrameDocument {
tp: "create_i_frame_document",
frameID: number,
id: number,
}
export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask | MouseClick | CreateIFrameDocument;
export default function (r: PrimitiveReader): Message | null {
switch (r.readUint()) {
const ui= r.readUint()
switch (ui) {
case 0:
return {
@ -509,7 +526,24 @@ export default function (r: PrimitiveReader): Message | null {
containerName: r.readString(),
};
case 69:
return {
tp: ID_TP_MAP[69],
id: r.readUint(),
hesitationTime: r.readUint(),
label: r.readString(),
selector: r.readString(),
};
case 70:
return {
tp: ID_TP_MAP[70],
frameID: r.readUint(),
id: r.readUint(),
};
default:
console.log("wtf is this", ui)
r.readUint(); // IOS skip timestamp
r.skip(r.readUint());
return null;

View file

@ -1,7 +1,7 @@
import Record from 'Types/Record';
import Target from 'Types/target';
import { camelCased } from 'App/utils';
// import { getEventIcon } from 'Types/filter';
import { getEventIcon } from 'Types/filter';
const CLICK = 'CLICK';
const INPUT = 'INPUT';
@ -105,6 +105,6 @@ export default Record({
operator: event.operator || getOperatorDefault(event.type),
// value: target ? target.label : event.value,
value: typeof value === 'string' ? [value] : value,
icon: 'filters/metadata'
icon: event.type ? getEventIcon(event.type) : 'filters/metadata'
}),
})

View file

@ -124,6 +124,7 @@ export const getEventIcon = (filter) => {
type = type || key;
if (type === KEYS.USER_COUNTRY) return 'map-marker-alt';
if (type === KEYS.USER_BROWSER) return 'window';
if (type === KEYS.USERBROWSER) return 'window';
if (type === KEYS.PLATFORM) return 'window';
if (type === TYPES.CLICK) return 'filters/click';

View file

@ -75,6 +75,7 @@ export default Record({
crashes: [],
socket: null,
isIOS: false,
revId: ''
}, {
fromJS:({
startTs=0,

View file

@ -14,7 +14,7 @@ read dns_name
echo please enter your email id:
read emai_id
ssh_ansible_user=$(whoami)
certbot_home=/etc/letsencrypt/archive/$dns_name
certbot_home=/etc/letsencrypt/live/$dns_name
#Check certbot installed or not
@ -26,8 +26,8 @@ fi
sudo certbot certonly --non-interactive --agree-tos -m $emai_id -d $dns_name --standalone
sudo cp $certbot_home/privkey1.pem ${homedir}/site.key
sudo cp $certbot_home/fullchain1.pem ${homedir}/site.crt
sudo cp $certbot_home/privkey.pem ${homedir}/site.key
sudo cp $certbot_home/fullchain.pem ${homedir}/site.crt
sudo chown -R $ssh_ansible_user:$ssh_ansible_user ${homedir}/site.key ${homedir}/site.crt
sudo chmod 775 ${homedir}/site.crt ${homedir}/site.key

View file

@ -30,11 +30,11 @@ Installation components are separated by namespaces.
**Scripts:**
- **install.sh**
Installs OpenReplay in a single node machine, for trial runs / demo.
Installs OpenReplay in a single node machine.
This script is a wrapper around the `install.sh` with [k3s](https://k3s.io/) as kubernetes distro.
Note: As of now this script support only ubuntu, as we've to install some packages to enable `NFS`.
Note: As of now this script support only Ubuntu, as we've to install some packages to enable `NFS`.
- **kube-install.sh:**

View file

@ -54,8 +54,6 @@ env:
sessions_bucket: mobs
sourcemaps_bucket: sourcemaps
js_cache_bucket: sessions-assets
sourcemaps_reader: 'http://utilities-openreplay.app.svc.cluster.local:9000/assist/sourcemaps'
peers: 'http://utilities-openreplay.app.svc.cluster.local:9000/assist/peers'
# Enable logging for python app
# Ref: https://stackoverflow.com/questions/43969743/logs-in-kubernetes-pod-not-showing-up
PYTHONUNBUFFERED: '0'

View file

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

View file

@ -47,6 +47,17 @@ spec:
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.pvc }}
{{- if eq .Values.pvc.name "hostPath" }}
volumeMounts:
- mountPath: {{ .Values.pvc.mountPath }}
name: {{ .Values.pvc.name }}
volumes:
- name: mydir
hostPath:
# Ensure the file directory is created.
path: {{ .Values.pvc.hostMountPath }}
type: DirectoryOrCreate
{{- else }}
volumeMounts:
- name: {{ .Values.pvc.name }}
mountPath: {{ .Values.pvc.mountPath }}
@ -55,6 +66,7 @@ spec:
persistentVolumeClaim:
claimName: {{ .Values.pvc.volumeName }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -1,5 +1,5 @@
{{- if .Values.pvc }}
{{- if .Values.pvc.create }}
{{- if and (.Values.pvc.create) (ne .Values.pvc.name "hostPath") }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:

View file

@ -23,10 +23,11 @@ resources:
pvc:
create: true
name: nfs
name: hostPath
storageClassName: nfs
volumeName: nfs
mountPath: /mnt/efs
hostMountPath: /openreplay/storage/nfs
storageSize: 5Gi
env:

View file

@ -24,10 +24,11 @@ resources:
pvc:
# PVC Created from filesink.yaml
create: false
name: nfs
name: hostPath
storageClassName: nfs
volumeName: nfs
mountPath: /mnt/efs
hostMountPath: /openreplay/storage/nfs
storageSize: 5Gi
env:
@ -43,3 +44,4 @@ env:
KAFKA_SERVERS: kafka.db.svc.cluster.local:9092
KAFKA_USE_SSL: false
LICENSE_KEY: ""
FS_CLEAN_HRS: 24

View file

@ -4,7 +4,13 @@ CREATE INDEX sessions_user_id_useridNN_idx ON sessions (user_id) WHERE user_id I
CREATE INDEX sessions_uid_projectid_startts_sessionid_uidNN_durGTZ_idx ON sessions (user_id, project_id, start_ts, session_id) WHERE user_id IS NOT NULL AND duration > 0;
CREATE INDEX pages_base_path_base_pathLNGT2_idx ON events.pages (base_path) WHERE length(base_path) > 2;
CREATE INDEX clicks_session_id_timestamp_idx ON events.clicks (session_id, timestamp);
CREATE INDEX errors_error_id_idx ON errors (error_id);
CREATE INDEX errors_error_id_idx ON events.errors (error_id);
CREATE INDEX issues_issue_id_timestamp_idx ON events_common.issues(issue_id,timestamp);
CREATE INDEX issues_timestamp_idx ON events_common.issues (timestamp);
CREATE INDEX issues_project_id_issue_id_idx ON public.issues (project_id, issue_id);
COMMIT;

View file

@ -172,7 +172,6 @@ CREATE TABLE projects
"defaultInputMode": "plain"
}'::jsonb -- ??????
);
CREATE INDEX projects_tenant_id_idx ON projects (tenant_id);
CREATE OR REPLACE FUNCTION notify_project() RETURNS trigger AS
$$
@ -248,7 +247,6 @@ create table webhooks
index integer default 0 not null,
name varchar(100)
);
CREATE INDEX webhooks_tenant_id_idx ON webhooks (tenant_id);
-- --- notifications.sql ---

View file

@ -31,7 +31,7 @@ which docker &> /dev/null || {
# response {"data":{"valid": TRUE|FALSE, "expiration": expiration date in ms}}
# Installing k3s
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik --docker" sh -
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik" sh -
mkdir ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(whoami) ~/.kube/config

View file

@ -4,10 +4,10 @@ This is the frontend of the OpenReplay web app (internet).
## Endpoints
- /streaming -> ios-proxy
- /api -> chalice
- /http -> http
- / -> frontend (in minio)
- /assets -> sessions-assets bucket in minio
- /minio -> minio api endpoint
- /ingest -> events ingestor
- /assist -> live sessions and webRTC
- /grafana -> monitoring (Enterprise Edition only)

View file

@ -52,14 +52,6 @@ data:
proxy_set_header Host $host;
proxy_pass $target;
}
location /streaming/ {
set $target http://ios-proxy-openreplay.app.svc.cluster.local; rewrite ^/streaming/(.*) /$1 break;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_pass $target;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_http_version 1.1;

View file

@ -4,6 +4,6 @@ db_name: ""
app_name: ""
db_list:
- "minio"
- "nfs-server-provisioner"
- "postgresql"
- "redis"
# - "nfs-server-provisioner"

View file

@ -5,6 +5,7 @@ image:
{% endif %}
env:
LICENSE_KEY: "{{ enterprise_edition_license }}"
POSTGRES_STRING: "postgres://{{postgres_db_user}}:{{postgres_db_password}}@{{postgres_endpoint}}:{{postgres_port}}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -7,6 +7,11 @@ env:
AWS_ACCESS_KEY_ID: "{{ minio_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}"
LICENSE_KEY: "{{ enterprise_edition_license }}"
AWS_ENDPOINT: "{{ s3_endpoint }}"
AWS_REGION: "{{ aws_region }}"
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -15,6 +15,22 @@ env:
S3_HOST: "https://{{ domain_name }}"
SITE_URL: "https://{{ domain_name }}"
jwt_secret: "{{ jwt_secret_key }}"
pg_host: "{{ postgres_endpoint }}"
pg_port: "{{ postgres_port }}"
pg_dbname: "{{ postgres_port }}"
pg_user: "{{ postgres_db_user }}"
pg_password: "{{ postgres_db_password }}"
EMAIL_HOST: "{{ email_host }}"
EMAIL_PORT: "{{ email_port }}"
EMAIL_USER: "{{ email_user }}"
EMAIL_PASSWORD: "{{ email_password }}"
EMAIL_USE_TLS: "{{ email_use_tls }}"
EMAIL_USE_SSL: "{{ email_use_ssl }}"
EMAIL_SSL_KEY: "{{ email_ssl_key }}"
EMAIL_SSL_CERT: "{{ email_ssl_cert }}"
EMAIL_FROM: "{{ email_from }}"
AWS_DEFAULT_REGION: "{{ aws_default_region }}"
sessions_region: "{{ aws_default_region }}"
{% if env is defined and env.chalice is defined and env.chalice%}
{{ env.chalice | to_nice_yaml | trim | indent(2) }}
{% endif %}

View file

@ -5,6 +5,10 @@ image:
{% endif %}
env:
LICENSE_KEY: "{{ enterprise_edition_license }}"
POSTGRES_STRING: "postgres://{{ postgres_db_user }}:{{ postgres_password }}@{{ postgres_endpoint }}:{{ postgres_port }}"
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []
{% endif %}

View file

@ -5,6 +5,9 @@ image:
{% endif %}
env:
LICENSE_KEY: "{{ enterprise_edition_license }}"
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -7,6 +7,11 @@ env:
AWS_ACCESS_KEY_ID: "{{ minio_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}"
LICENSE_KEY: "{{ enterprise_edition_license }}"
AWS_REGION: "{{ aws_region }}"
POSTGRES_STRING: "postgres://{{ postgres_db_user }}:{{ postgres_password }}@{{ postgres_endpoint }}:{{ postgres_port }}"
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -5,6 +5,11 @@ image:
{% endif %}
env:
LICENSE_KEY: "{{ enterprise_edition_license }}"
POSTGRES_STRING: "postgres://{{ postgres_db_user }}:{{ postgres_password }}@{{ postgres_endpoint }}:{{ postgres_port }}"
#
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -5,6 +5,9 @@ image:
{% endif %}
env:
LICENSE_KEY: "{{ enterprise_edition_license }}"
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -7,6 +7,12 @@ env:
AWS_ACCESS_KEY_ID: "{{ minio_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}"
LICENSE_KEY: "{{ enterprise_edition_license }}"
AWS_ENDPOINT: "{{ s3_endpoint }}"
AWS_REGION_WEB: "{{ aws_region }}"
AWS_REGION_IOS: "{{ aws_region }}"
REDIS_STRING: "{{ redis_endpoint }}"
KAFKA_SERVERS: "{{ kafka_endpoint }}"
KAFKA_USE_SSL: "{{ kafka_ssl }}"
{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %}
imagePullSecrets: []

View file

@ -12,6 +12,7 @@ env:
S3_SECRET: "{{ minio_secret_key }}"
S3_HOST: "https://{{ domain_name }}"
jwt_secret: "{{ jwt_secret_key }}"
AWS_DEFAULT_REGION: "{{ aws_region }}"
{% if env is defined and env.chalice is defined and env.chalice%}
{{ env.chalice | to_nice_yaml | trim | indent(2) }}
{% endif %}

View file

@ -86,3 +86,24 @@ db_resource_override:
# memory: 256Mi
redis: {}
clickhouse: {}
## Sane defaults
s3_endpoint: "http://minio.db.svc.cluster.local:9000"
aws_region: "us-east-1"
kafka_endpoint: kafka.db.svc.cluster.local:9042
kafka_ssl: false
postgres_endpoint: postgresql.db.svc.cluster.local
postgres_port: 5432
postgres_db_name: postgres
postgres_db_user: postgres
postgres_db_password: asayerPostgres
redis_endpoint: redis-master.db.svc.cluster.local:6379
email_host: ''
email_port: '587'
email_user: ''
email_password: ''
email_use_tls: 'true'
email_use_ssl: 'false'
email_ssl_key: ''
email_ssl_cert: ''
email_from: OpenReplay<do-not-reply@openreplay.com>

View file

@ -86,3 +86,24 @@ db_resource_override:
# memory: 256Mi
redis: {{ db_resource_override.redis|default({}) }}
clickhouse: {{ db_resource_override.clickhouse|default({}) }}
## Sane defaults
s3_endpoint: "{{ s3_endpoint }}"
aws_region: "{{ aws_region }}"
kafka_endpoint: "{{ kafka_endpoint }}"
kafka_ssl: "{{ kafka_ssl }}"
postgres_endpoint: "{{ postgres_endpoint }}"
postgres_port: "{{ postgres_port }}"
postgres_db_name: "{{ postgres_db_name }}"
postgres_db_user: "{{ postgres_db_user }}"
postgres_db_password: "{{ postgres_db_password }}"
redis_endpoint: "{{ redis_endpoint }}"
email_host: "{{ email_host }}"
email_port: "{{ email_port }}"
email_user: "{{ email_user }}"
email_password: "{{ email_password }}"
email_use_tls: "{{ email_use_tls }}"
email_use_ssl: "{{ email_use_ssl }}"
email_ssl_key: "{{ email_ssl_key }}"
email_ssl_cert: "{{ email_ssl_cert }}"
email_from: "{{ email_from }}"

View file

@ -8,7 +8,9 @@
"module": true,
"console": true,
"Promise": true,
"Buffer": true
"Buffer": true,
"URL": true,
"global": true
},
"plugins": [
"prettier"

View file

@ -14,7 +14,8 @@ parser.addArgument(['-k', '--api-key'], {
help: 'API key',
required: true,
});
parser.addArgument(['-p', '-i', '--project-key'], { // -i is depricated
parser.addArgument(['-p', '-i', '--project-key'], {
// -i is depricated
help: 'Project Key',
required: true,
});
@ -54,19 +55,34 @@ dir.addArgument(['-u', '--js-dir-url'], {
// TODO: exclude in dir
const { command, api_key, project_key, server, verbose, ...args } = parser.parseArgs();
const { command, api_key, project_key, server, verbose, ...args } =
parser.parseArgs();
global._VERBOSE = !!verbose;
(command === 'file'
? uploadFile(api_key, project_key, args.sourcemap_file_path, args.js_file_url, server)
: uploadDir(api_key, project_key, args.sourcemap_dir_path, args.js_dir_url, server)
? uploadFile(
api_key,
project_key,
args.sourcemap_file_path,
args.js_file_url,
server,
)
: uploadDir(
api_key,
project_key,
args.sourcemap_dir_path,
args.js_dir_url,
server,
)
)
.then((sourceFiles) =>
sourceFiles.length > 0
? console.log(`Successfully uploaded ${sourceFiles.length} sourcemap file${sourceFiles.length > 1 ? "s" : ""} for: \n`
+ sourceFiles.join("\t\n")
.then((sourceFiles) =>
sourceFiles.length > 0
? console.log(
`Successfully uploaded ${sourceFiles.length} sourcemap file${
sourceFiles.length > 1 ? 's' : ''
} for: \n` + sourceFiles.join('\t\n'),
)
: console.log(`No sourcemaps found in ${args.sourcemap_dir_path}`),
)
: console.log(`No sourcemaps found in ${ args.sourcemap_dir_path }`)
)
.catch(e => console.error(`Sourcemap Uploader: ${e}`));
.catch((e) => console.error(`Sourcemap Uploader: ${e}`));

View file

@ -3,11 +3,23 @@ const readFile = require('./lib/readFile.js'),
uploadSourcemaps = require('./lib/uploadSourcemaps.js');
module.exports = {
async uploadFile(api_key, project_key, sourcemap_file_path, js_file_url, server) {
async uploadFile(
api_key,
project_key,
sourcemap_file_path,
js_file_url,
server,
) {
const sourcemap = await readFile(sourcemap_file_path, js_file_url);
return uploadSourcemaps(api_key, project_key, [sourcemap], server);
},
async uploadDir(api_key, project_key, sourcemap_dir_path, js_dir_url, server) {
async uploadDir(
api_key,
project_key,
sourcemap_dir_path,
js_dir_url,
server,
) {
const sourcemaps = await readDir(sourcemap_dir_path, js_dir_url);
return uploadSourcemaps(api_key, project_key, sourcemaps, server);
},

View file

@ -3,12 +3,13 @@ const readFile = require('./readFile');
module.exports = (sourcemap_dir_path, js_dir_url) => {
sourcemap_dir_path = (sourcemap_dir_path + '/').replace(/\/+/g, '/');
if (js_dir_url[ js_dir_url.length - 1 ] !== '/') { // replace will break schema
if (js_dir_url[js_dir_url.length - 1] !== '/') {
// replace will break schema
js_dir_url += '/';
}
return glob(sourcemap_dir_path + '**/*.map').then(sourcemap_file_paths =>
return glob(sourcemap_dir_path + '**/*.map').then((sourcemap_file_paths) =>
Promise.all(
sourcemap_file_paths.map(sourcemap_file_path =>
sourcemap_file_paths.map((sourcemap_file_path) =>
readFile(
sourcemap_file_path,
js_dir_url + sourcemap_file_path.slice(sourcemap_dir_path.length, -4),

View file

@ -1,6 +1,6 @@
const fs = require('fs').promises;
module.exports = (sourcemap_file_path, js_file_url) =>
fs.readFile(sourcemap_file_path, 'utf8').then(body => {
fs.readFile(sourcemap_file_path, 'utf8').then((body) => {
return { sourcemap_file_path, js_file_url, body };
});

View file

@ -9,43 +9,47 @@ const getUploadURLs = (api_key, project_key, js_file_urls, server) =>
let serverURL;
try {
serverURL = new URL(server);
} catch(e) {
return reject(`Failed to parse server URL "${server}".`)
} catch (e) {
return reject(`Failed to parse server URL "${server}".`);
}
const pathPrefix = (serverURL.pathname + "/").replace(/\/+/g, '/');
const pathPrefix = (serverURL.pathname + '/').replace(/\/+/g, '/');
const options = {
method: 'PUT',
hostname: serverURL.host,
path: pathPrefix + `${project_key}/sourcemaps/`,
headers: { Authorization: api_key, 'Content-Type': 'application/json' },
}
};
if (global._VERBOSE) {
console.log("Request: ", options, "\nFiles: ", js_file_urls);
console.log('Request: ', options, '\nFiles: ', js_file_urls);
}
const req = https.request(
options,
res => {
const req = https.request(options, (res) => {
if (global._VERBOSE) {
console.log(
'Response Code: ',
res.statusCode,
'\nMessage: ',
res.statusMessage,
);
}
if (res.statusCode === 403) {
reject(
'Authorisation rejected. Please, check your API_KEY and/or PROJECT_KEY.',
);
return;
} else if (res.statusCode !== 200) {
reject('Server Error. Please, contact OpenReplay support.');
return;
}
let data = '';
res.on('data', (s) => (data += s));
res.on('end', () => {
if (global._VERBOSE) {
console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage);
console.log('Server Response: ', data);
}
if (res.statusCode === 403) {
reject("Authorisation rejected. Please, check your API_KEY and/or PROJECT_KEY.")
return
} else if (res.statusCode !== 200) {
reject("Server Error. Please, contact OpenReplay support.");
return;
}
let data = '';
res.on('data', s => (data += s));
res.on('end', () => {
if (global._VERBOSE) {
console.log("Server Response: ", data)
}
resolve(JSON.parse(data).data)
});
},
);
resolve(JSON.parse(data).data);
});
});
req.on('error', reject);
req.write(JSON.stringify({ URL: js_file_urls }));
req.end();
@ -63,14 +67,19 @@ const uploadSourcemap = (upload_url, body) =>
'Content-Type': 'application/json',
},
},
res => {
(res) => {
if (res.statusCode !== 200) {
if (global._VERBOSE) {
console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage);
console.log(
'Response Code: ',
res.statusCode,
'\nMessage: ',
res.statusMessage,
);
}
reject("Unable to upload. Please, contact OpenReplay support.");
return; // TODO: report per-file errors.
reject('Unable to upload. Please, contact OpenReplay support.');
return; // TODO: report per-file errors.
}
resolve();
//res.on('end', resolve);
@ -86,12 +95,13 @@ module.exports = (api_key, project_key, sourcemaps, server) =>
api_key,
project_key,
sourcemaps.map(({ js_file_url }) => js_file_url),
server || "https://api.openreplay.com",
).then(upload_urls =>
server || 'https://api.openreplay.com',
).then((upload_urls) =>
Promise.all(
upload_urls.map((upload_url, i) =>
uploadSourcemap(upload_url, sourcemaps[i].body)
.then(() => sourcemaps[i].js_file_url)
uploadSourcemap(upload_url, sourcemaps[i].body).then(
() => sourcemaps[i].js_file_url,
),
),
),
);

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more