Merge branch 'dev' into api_insights
This commit is contained in:
commit
1ccc35a97e
143 changed files with 3634 additions and 1714 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
49
backend/pkg/db/cache/messages_common.go
vendored
49
backend/pkg/db/cache/messages_common.go
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
43
backend/pkg/db/cache/messages_ios.go
vendored
43
backend/pkg/db/cache/messages_ios.go
vendored
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
4
backend/pkg/db/cache/project.go
vendored
4
backend/pkg/db/cache/project.go
vendored
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
backend/pkg/env/vars.go
vendored
7
backend/pkg/env/vars.go
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import (
|
|||
"openreplay/backend/services/http/uaparser"
|
||||
|
||||
)
|
||||
|
||||
|
||||
var rewriter *assets.Rewriter
|
||||
var producer types.Producer
|
||||
var pgconn *cache.PGCache
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"openreplay/backend/pkg/queue/types"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile)
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
ee/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql
Normal file
11
ee/scripts/helm/db/init_dbs/postgresql/1.3.6/1.3.6.sql
Normal 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;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
# defaults file for openreplay
|
||||
app_name: ""
|
||||
db_name: ""
|
||||
db_list:
|
||||
- "minio"
|
||||
- "nfs-server-provisioner"
|
||||
- "postgresql"
|
||||
- "redis"
|
||||
- "clickhouse"
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//...
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
.activeLink {
|
||||
cursor: pointer;
|
||||
pointer-events: default;
|
||||
text-decoration: underline;
|
||||
& label {
|
||||
color: #000000 !important;
|
||||
text-decoration: underline;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ class TrackingCodeModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderActiveTab = () => {
|
||||
console.log('rendering...')
|
||||
switch (this.state.activeTab) {
|
||||
case PROJECT:
|
||||
return <ProjectCodeSnippet />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './TrackerUpdateMessage'
|
||||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ export default Record({
|
|||
crashes: [],
|
||||
socket: null,
|
||||
isIOS: false,
|
||||
revId: ''
|
||||
}, {
|
||||
fromJS:({
|
||||
startTs=0,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:**
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ db_name: ""
|
|||
app_name: ""
|
||||
db_list:
|
||||
- "minio"
|
||||
- "nfs-server-provisioner"
|
||||
- "postgresql"
|
||||
- "redis"
|
||||
# - "nfs-server-provisioner"
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
"module": true,
|
||||
"console": true,
|
||||
"Promise": true,
|
||||
"Buffer": true
|
||||
"Buffer": true,
|
||||
"URL": true,
|
||||
"global": true
|
||||
},
|
||||
"plugins": [
|
||||
"prettier"
|
||||
|
|
|
|||
|
|
@ -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}`));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
919
sourcemap-uploader/package-lock.json
generated
919
sourcemap-uploader/package-lock.json
generated
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
Loading…
Add table
Reference in a new issue