Merge branch 'dev' into storage-override

This commit is contained in:
Eric Chan 2022-07-25 10:39:55 -04:00
commit 5e851c7758
240 changed files with 8160 additions and 5113 deletions

View file

@ -1,27 +1,20 @@
FROM python:3.9.12-slim
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
ENV APP_NAME chalice
RUN apk add --no-cache build-base nodejs npm tini
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
ARG envarg
# Add Tini
# Startup daemon
ENV TINI_VERSION=v0.19.0 \
SOURCE_MAP_VERSION=0.7.4
ARG envarg
ENV ENTERPRISE_BUILD ${envarg}
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
ADD https://unpkg.com/source-map@${SOURCE_MAP_VERSION}/lib/mappings.wasm /mappings.wasm
RUN chmod +x /tini
ENV SOURCE_MAP_VERSION=0.7.4 \
APP_NAME=chalice \
ENTERPRISE_BUILD=${envarg}
# Installing Nodejs
RUN apt update && apt install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_12.x | bash - && \
apt install -y nodejs && \
apt remove --purge -y curl && \
rm -rf /var/lib/apt/lists/*
ADD https://unpkg.com/source-map@${SOURCE_MAP_VERSION}/lib/mappings.wasm /mappings.wasm
WORKDIR /work_tmp
COPY requirements.txt /work_tmp/requirements.txt
RUN pip install -r /work_tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /work_tmp/requirements.txt
COPY sourcemap-reader/*.json /work_tmp/
RUN cd /work_tmp && npm install
@ -29,5 +22,8 @@ WORKDIR /work
COPY . .
RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/.
ENTRYPOINT ["/tini", "--"]
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ./entrypoint.sh

View file

@ -1,23 +1,22 @@
FROM python:3.9.12-slim
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
ENV APP_NAME alerts
ENV pg_minconn 2
ENV pg_maxconn 10
# Add Tini
# Startup daemon
ENV TINI_VERSION v0.19.0
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
RUN apk add --no-cache build-base tini
ARG envarg
ENV ENTERPRISE_BUILD ${envarg}
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENV APP_NAME=alerts \
pg_minconn=2 \
pg_maxconn=10 \
ENTERPRISE_BUILD=${envarg}
COPY requirements.txt /work_tmp/requirements.txt
RUN pip install -r /work_tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /work_tmp/requirements.txt
WORKDIR /work
COPY . .
RUN mv env.default .env && mv app_alerts.py app.py && mv entrypoint_alerts.sh entrypoint.sh
ENTRYPOINT ["/tini", "--"]
CMD ./entrypoint.sh
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ./entrypoint.sh

View file

@ -0,0 +1,10 @@
# ignore .git and .cache folders
.git
.cache
**/build.sh
**/build_*.sh
**/*deploy.sh
app.py
entrypoint_alerts.sh
requirements.txt

View file

@ -23,5 +23,7 @@ ARG envarg
ENV ENTERPRISE_BUILD ${envarg}
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/tini", "--"]
CMD ./entrypoint.sh

View file

@ -7,35 +7,6 @@
# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh <ee>
function make_submodule() {
[[ $1 != "ee" ]] && {
# -- this part was generated by modules_lister.py --
mkdir alerts
cp -R ./{app_alerts,schemas}.py ./alerts/
mkdir -p ./alerts/chalicelib/
cp -R ./chalicelib/__init__.py ./alerts/chalicelib/
mkdir -p ./alerts/chalicelib/core/
cp -R ./chalicelib/core/{__init__,alerts_processor,alerts_listener,sessions,events,issues,sessions_metas,metadata,projects,users,authorizers,tenants,assist,events_ios,sessions_mobs,errors,sourcemaps,sourcemaps_parser,resources,performance_event,alerts,notifications,slack,collaboration_slack,webhook}.py ./alerts/chalicelib/core/
mkdir -p ./alerts/chalicelib/utils/
cp -R ./chalicelib/utils/{__init__,TimeUTC,pg_client,helper,event_filter_definition,dev,email_helper,email_handler,smtp,s3,metrics_helper}.py ./alerts/chalicelib/utils/
# -- end of generated part
}
[[ $1 == "ee" ]] && {
# -- this part was generated by modules_lister.py --
mkdir alerts
cp -R ./{app_alerts,schemas,schemas_ee}.py ./alerts/
mkdir -p ./alerts/chalicelib/
cp -R ./chalicelib/__init__.py ./alerts/chalicelib/
mkdir -p ./alerts/chalicelib/core/
cp -R ./chalicelib/core/{__init__,alerts_processor,alerts_listener,sessions,events,issues,sessions_metas,metadata,projects,users,authorizers,tenants,roles,assist,events_ios,sessions_mobs,errors,metrics,sourcemaps,sourcemaps_parser,resources,performance_event,alerts,notifications,slack,collaboration_slack,webhook}.py ./alerts/chalicelib/core/
mkdir -p ./alerts/chalicelib/utils/
cp -R ./chalicelib/utils/{__init__,TimeUTC,pg_client,helper,event_filter_definition,dev,SAML2_helper,email_helper,email_handler,smtp,s3,args_transformer,ch_client,metrics_helper}.py ./alerts/chalicelib/utils/
# -- end of generated part
}
cp -R ./{Dockerfile.alerts,requirements.txt,env.default,entrypoint_alerts.sh} ./alerts/
cp -R ./chalicelib/utils/html ./alerts/chalicelib/utils/html
}
git_sha1=${IMAGE_TAG:-$(git rev-parse HEAD)}
envarg="default-foss"
check_prereq() {
@ -53,11 +24,9 @@ function build_api(){
envarg="default-ee"
tag="ee-"
}
make_submodule $1
cd alerts
docker build -f ./Dockerfile.alerts --build-arg envarg=$envarg -t ${DOCKER_REPO:-'local'}/alerts:${git_sha1} .
cd ..
rm -rf alerts
cp -R ../api ../_alerts
docker build -f ../_alerts/Dockerfile.alerts --build-arg envarg=$envarg -t ${DOCKER_REPO:-'local'}/alerts:${git_sha1} .
rm -rf ../_alerts
[[ $PUSH_IMAGE -eq 1 ]] && {
docker push ${DOCKER_REPO:-'local'}/alerts:${git_sha1}
docker tag ${DOCKER_REPO:-'local'}/alerts:${git_sha1} ${DOCKER_REPO:-'local'}/alerts:${tag}latest

View file

@ -4,6 +4,8 @@ from decouple import config
def get_by_session_id(session_id, project_id, start_ts, duration):
with pg_client.PostgresClient() as cur:
if duration is None or (type(duration) != 'int' and type(duration) != 'float') or duration < 0:
duration = 0
delta = config("events_ts_delta", cast=int, default=60 * 60) * 1000
ch_query = """\
SELECT

View file

@ -863,12 +863,12 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
full_args = {**full_args, **_multiple_values(f.value, value_key=e_k_f)}
if f.type == schemas.FetchFilterType._url:
event_where.append(
_multiple_conditions(f"main.{events.event_type.REQUEST.column} {op} %({e_k_f})s", f.value,
value_key=e_k_f))
_multiple_conditions(f"main.{events.event_type.REQUEST.column} {op} %({e_k_f})s::text",
f.value, value_key=e_k_f))
apply = True
elif f.type == schemas.FetchFilterType._status_code:
event_where.append(
_multiple_conditions(f"main.status_code {f.operator} %({e_k_f})s", f.value,
_multiple_conditions(f"main.status_code {f.operator} %({e_k_f})s::integer", f.value,
value_key=e_k_f))
apply = True
elif f.type == schemas.FetchFilterType._method:
@ -877,15 +877,15 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
apply = True
elif f.type == schemas.FetchFilterType._duration:
event_where.append(
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s", f.value, value_key=e_k_f))
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s::integer", f.value, value_key=e_k_f))
apply = True
elif f.type == schemas.FetchFilterType._request_body:
event_where.append(
_multiple_conditions(f"main.request_body {op} %({e_k_f})s", f.value, value_key=e_k_f))
_multiple_conditions(f"main.request_body {op} %({e_k_f})s::text", f.value, value_key=e_k_f))
apply = True
elif f.type == schemas.FetchFilterType._response_body:
event_where.append(
_multiple_conditions(f"main.response_body {op} %({e_k_f})s", f.value, value_key=e_k_f))
_multiple_conditions(f"main.response_body {op} %({e_k_f})s::text", f.value, value_key=e_k_f))
apply = True
else:
print(f"undefined FETCH filter: {f.type}")

View file

@ -398,3 +398,7 @@ def __time_value(row):
if "chart" in row and factor > 1:
for r in row["chart"]:
r["value"] /= factor
def is_saml2_available():
return config("hastSAML2", default=False, cast=bool)

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
cd sourcemap-reader
nohup npm start &> /tmp/sourcemap-reader.log &
cd ..

View file

@ -1,3 +1,3 @@
#!/bin/bash
#!/bin/sh
uvicorn app:app --host 0.0.0.0 --reload

View file

@ -0,0 +1,15 @@
requests==2.28.1
urllib3==1.26.10
boto3==1.24.26
pyjwt==2.4.0
psycopg2-binary==2.9.3
elasticsearch==8.3.1
jira==3.3.0
fastapi==0.78.0
uvicorn[standard]==0.18.2
python-decouple==3.6
pydantic[email]==1.9.1
apscheduler==3.9.1

View file

@ -1,15 +1,15 @@
requests==2.28.0
urllib3==1.26.9
boto3==1.24.11
requests==2.28.1
urllib3==1.26.10
boto3==1.24.26
pyjwt==2.4.0
psycopg2-binary==2.9.3
elasticsearch==8.2.3
jira==3.2.0
elasticsearch==8.3.1
jira==3.3.0
fastapi==0.78.0
uvicorn[standard]==0.17.6
uvicorn[standard]==0.18.2
python-decouple==3.6
pydantic[email]==1.9.1
apscheduler==3.9.1

View file

@ -19,14 +19,16 @@ RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openrep
FROM alpine AS entrypoint
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
RUN apk add --no-cache ca-certificates
RUN adduser -u 1001 openreplay -D
ENV TZ=UTC \
FS_ULIMIT=1000 \
FS_DIR=/mnt/efs \
MAXMINDDB_FILE=/root/geoip.mmdb \
UAPARSER_FILE=/root/regexes.yaml \
HTTP_PORT=80 \
MAXMINDDB_FILE=/home/openreplay/geoip.mmdb \
UAPARSER_FILE=/home/openreplay/regexes.yaml \
HTTP_PORT=8080 \
KAFKA_USE_SSL=true \
KAFKA_MAX_POLL_INTERVAL_MS=400000 \
REDIS_STREAMS_MAX_LEN=10000 \
@ -67,5 +69,6 @@ RUN if [ "$SERVICE_NAME" = "http" ]; then \
wget https://static.openreplay.com/geoip/GeoLite2-Country.mmdb -O "$MAXMINDDB_FILE"; fi
COPY --from=build /root/service /root/service
ENTRYPOINT /root/service
COPY --from=build /root/service /home/openreplay/service
USER 1001
ENTRYPOINT /home/openreplay/service

View file

@ -82,10 +82,20 @@ func main() {
// Find ended sessions and send notification to other services
sessions.HandleEndedSessions(func(sessionID uint64, timestamp int64) bool {
msg := &messages.SessionEnd{Timestamp: uint64(timestamp)}
if err := pg.InsertSessionEnd(sessionID, msg.Timestamp); err != nil {
currDuration, err := pg.GetSessionDuration(sessionID)
if err != nil {
log.Printf("getSessionDuration failed, sessID: %d, err: %s", sessionID, err)
}
newDuration, err := pg.InsertSessionEnd(sessionID, msg.Timestamp)
if err != nil {
log.Printf("can't save sessionEnd to database, sessID: %d, err: %s", sessionID, err)
return false
}
if currDuration == newDuration {
log.Printf("sessionEnd duplicate, sessID: %d, prevDur: %d, newDur: %d", sessionID,
currDuration, newDuration)
return true
}
if err := producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(msg)); err != nil {
log.Printf("can't send sessionEnd to topic: %s; sessID: %d", err, sessionID)
return false

View file

@ -21,6 +21,7 @@ type Storage struct {
startBytes []byte
totalSessions syncfloat64.Counter
sessionSize syncfloat64.Histogram
readingTime syncfloat64.Histogram
archivingTime syncfloat64.Histogram
}
@ -40,6 +41,10 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor
if err != nil {
log.Printf("can't create session_size metric: %s", err)
}
readingTime, err := metrics.RegisterHistogram("reading_duration")
if err != nil {
log.Printf("can't create reading_duration metric: %s", err)
}
archivingTime, err := metrics.RegisterHistogram("archiving_duration")
if err != nil {
log.Printf("can't create archiving_duration metric: %s", err)
@ -50,16 +55,17 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor
startBytes: make([]byte, cfg.FileSplitSize),
totalSessions: totalSessions,
sessionSize: sessionSize,
readingTime: readingTime,
archivingTime: archivingTime,
}, nil
}
func (s *Storage) UploadKey(key string, retryCount int) error {
start := time.Now()
if retryCount <= 0 {
return nil
}
start := time.Now()
file, err := os.Open(s.cfg.FSDir + "/" + key)
if err != nil {
sessID, _ := strconv.ParseUint(key, 10, 64)
@ -84,6 +90,9 @@ func (s *Storage) UploadKey(key string, retryCount int) error {
})
return nil
}
s.readingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()))
start = time.Now()
startReader := bytes.NewBuffer(s.startBytes[:nRead])
if err := s.s3.Upload(s.gzipFile(startReader), key, "application/octet-stream", true); err != nil {
log.Fatalf("Storage: start upload failed. %v\n", err)
@ -93,6 +102,7 @@ func (s *Storage) UploadKey(key string, retryCount int) error {
log.Fatalf("Storage: end upload failed. %v\n", err)
}
}
s.archivingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()))
// Save metrics
var fileSize float64 = 0
@ -103,7 +113,7 @@ func (s *Storage) UploadKey(key string, retryCount int) error {
fileSize = float64(fileInfo.Size())
}
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*200)
s.archivingTime.Record(ctx, float64(time.Now().Sub(start).Milliseconds()))
s.sessionSize.Record(ctx, fileSize)
s.totalSessions.Add(ctx, 1)
return nil

View file

@ -7,12 +7,8 @@ import (
// . "openreplay/backend/pkg/db/types"
)
func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) error {
_, err := c.Conn.InsertSessionEnd(sessionID, timestamp)
if err != nil {
return err
}
return nil
func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) {
return c.Conn.InsertSessionEnd(sessionID, timestamp)
}
func (c *PGCache) HandleSessionEnd(sessionID uint64) error {

View file

@ -32,7 +32,8 @@ func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) er
}
func (c *PGCache) InsertIOSSessionEnd(sessionID uint64, e *IOSSessionEnd) error {
return c.InsertSessionEnd(sessionID, e.Timestamp)
_, err := c.InsertSessionEnd(sessionID, e.Timestamp)
return err
}
func (c *PGCache) InsertIOSScreenEnter(sessionID uint64, screenEnter *IOSScreenEnter) error {
@ -84,13 +85,5 @@ func (c *PGCache) InsertIOSCrash(sessionID uint64, crash *IOSCrash) error {
}
func (c *PGCache) InsertIOSIssueEvent(sessionID uint64, issueEvent *IOSIssueEvent) error {
// session, err := c.GetSession(sessionID)
// if err != nil {
// return err
// }
// TODO: unite IssueEvent message for the all platforms
// if err := c.Conn.InsertIssueEvent(sessionID, session.ProjectID, issueEvent); err != nil {
// return err
// }
return nil
}

View file

@ -63,7 +63,8 @@ func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error
}
func (c *PGCache) InsertWebSessionEnd(sessionID uint64, e *SessionEnd) error {
return c.InsertSessionEnd(sessionID, e.Timestamp)
_, err := c.InsertSessionEnd(sessionID, e.Timestamp)
return err
}
func (c *PGCache) HandleWebSessionEnd(sessionID uint64, e *SessionEnd) error {
@ -91,7 +92,7 @@ func (c *PGCache) InsertWebFetchEvent(sessionID uint64, e *FetchEvent) error {
if err != nil {
return err
}
return c.Conn.InsertWebFetchEvent(sessionID, project.SaveRequestPayloads, e)
return c.Conn.InsertWebFetchEvent(sessionID, session.ProjectID, project.SaveRequestPayloads, e)
}
func (c *PGCache) InsertWebGraphQLEvent(sessionID uint64, e *GraphQLEvent) error {
@ -103,5 +104,53 @@ func (c *PGCache) InsertWebGraphQLEvent(sessionID uint64, e *GraphQLEvent) error
if err != nil {
return err
}
return c.Conn.InsertWebGraphQLEvent(sessionID, project.SaveRequestPayloads, e)
return c.Conn.InsertWebGraphQLEvent(sessionID, session.ProjectID, project.SaveRequestPayloads, e)
}
func (c *PGCache) InsertWebCustomEvent(sessionID uint64, e *CustomEvent) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
return c.Conn.InsertWebCustomEvent(sessionID, session.ProjectID, e)
}
func (c *PGCache) InsertWebUserID(sessionID uint64, userID *UserID) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
return c.Conn.InsertWebUserID(sessionID, session.ProjectID, userID)
}
func (c *PGCache) InsertWebUserAnonymousID(sessionID uint64, userAnonymousID *UserAnonymousID) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
return c.Conn.InsertWebUserAnonymousID(sessionID, session.ProjectID, userAnonymousID)
}
func (c *PGCache) InsertWebPageEvent(sessionID uint64, e *PageEvent) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
return c.Conn.InsertWebPageEvent(sessionID, session.ProjectID, e)
}
func (c *PGCache) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
return c.Conn.InsertWebClickEvent(sessionID, session.ProjectID, e)
}
func (c *PGCache) InsertWebInputEvent(sessionID uint64, e *InputEvent) error {
session, err := c.GetSession(sessionID)
if err != nil {
return err
}
return c.Conn.InsertWebInputEvent(sessionID, session.ProjectID, e)
}

View file

@ -29,10 +29,9 @@ type PGCache struct {
// TODO: create conn automatically
func NewPGCache(pgConn *postgres.Conn, projectExpirationTimeoutMs int64) *PGCache {
return &PGCache{
Conn: pgConn,
sessions: make(map[uint64]*Session),
projects: make(map[uint32]*ProjectMeta),
//projectsByKeys: make(map[string]*ProjectMeta),
Conn: pgConn,
sessions: make(map[uint64]*Session),
projects: make(map[uint32]*ProjectMeta),
projectExpirationTimeout: time.Duration(1000 * projectExpirationTimeoutMs),
}
}

View file

@ -0,0 +1,93 @@
package postgres
import (
"bytes"
"errors"
"fmt"
)
const (
insertPrefix = `INSERT INTO `
insertValues = ` VALUES `
insertSuffix = ` ON CONFLICT DO NOTHING;`
)
type Bulk interface {
Append(args ...interface{}) error
Send() error
}
type bulkImpl struct {
conn Pool
table string
columns string
template string
setSize int
sizeLimit int
values []interface{}
}
func (b *bulkImpl) Append(args ...interface{}) error {
if len(args) != b.setSize {
return fmt.Errorf("wrong number of arguments, waited: %d, got: %d", b.setSize, len(args))
}
b.values = append(b.values, args...)
if len(b.values)/b.setSize >= b.sizeLimit {
return b.send()
}
return nil
}
func (b *bulkImpl) Send() error {
if len(b.values) == 0 {
return nil
}
return b.send()
}
func (b *bulkImpl) send() error {
request := bytes.NewBufferString(insertPrefix + b.table + b.columns + insertValues)
args := make([]interface{}, b.setSize)
for i := 0; i < len(b.values)/b.setSize; i++ {
for j := 0; j < b.setSize; j++ {
args[j] = i*b.setSize + j + 1
}
if i > 0 {
request.WriteByte(',')
}
request.WriteString(fmt.Sprintf(b.template, args...))
}
request.WriteString(insertSuffix)
err := b.conn.Exec(request.String(), b.values...)
b.values = make([]interface{}, 0, b.setSize*b.sizeLimit)
if err != nil {
return fmt.Errorf("send bulk err: %s", err)
}
return nil
}
func NewBulk(conn Pool, table, columns, template string, setSize, sizeLimit int) (Bulk, error) {
switch {
case conn == nil:
return nil, errors.New("db conn is empty")
case table == "":
return nil, errors.New("table is empty")
case columns == "":
return nil, errors.New("columns is empty")
case template == "":
return nil, errors.New("template is empty")
case setSize <= 0:
return nil, errors.New("set size is wrong")
case sizeLimit <= 0:
return nil, errors.New("size limit is wrong")
}
return &bulkImpl{
conn: conn,
table: table,
columns: columns,
template: template,
setSize: setSize,
sizeLimit: sizeLimit,
values: make([]interface{}, 0, setSize*sizeLimit),
}, nil
}

View file

@ -13,21 +13,24 @@ import (
"github.com/jackc/pgx/v4/pgxpool"
)
func getTimeoutContext() context.Context {
ctx, _ := context.WithTimeout(context.Background(), time.Duration(time.Second*30))
return ctx
}
type batchItem struct {
query string
arguments []interface{}
}
// Conn contains batches, bulks and cache for all sessions
type Conn struct {
c *pgxpool.Pool // TODO: conditional usage of Pool/Conn (use interface?)
c Pool
batches map[uint64]*pgx.Batch
batchSizes map[uint64]int
rawBatches map[uint64][]*batchItem
autocompletes Bulk
requests Bulk
customEvents Bulk
webPageEvents Bulk
webInputEvents Bulk
webGraphQLEvents Bulk
sessionUpdates map[uint64]*sessionUpdates
batchQueueLimit int
batchSizeLimit int
batchSizeBytes syncfloat64.Histogram
@ -46,14 +49,19 @@ func NewConn(url string, queueLimit, sizeLimit int, metrics *monitoring.Metrics)
log.Fatalln("pgxpool.Connect Error")
}
conn := &Conn{
c: c,
batches: make(map[uint64]*pgx.Batch),
batchSizes: make(map[uint64]int),
rawBatches: make(map[uint64][]*batchItem),
sessionUpdates: make(map[uint64]*sessionUpdates),
batchQueueLimit: queueLimit,
batchSizeLimit: sizeLimit,
}
conn.initMetrics(metrics)
conn.c, err = NewPool(c, conn.sqlRequestTime, conn.sqlRequestCounter)
if err != nil {
log.Fatalf("can't create new pool wrapper: %s", err)
}
conn.initBulks()
return conn
}
@ -82,6 +90,70 @@ func (conn *Conn) initMetrics(metrics *monitoring.Metrics) {
}
}
func (conn *Conn) initBulks() {
var err error
conn.autocompletes, err = NewBulk(conn.c,
"autocomplete",
"(value, type, project_id)",
"($%d, $%d, $%d)",
3, 100)
if err != nil {
log.Fatalf("can't create autocomplete bulk")
}
conn.requests, err = NewBulk(conn.c,
"events_common.requests",
"(session_id, timestamp, seq_index, url, duration, success)",
"($%d, $%d, $%d, left($%d, 2700), $%d, $%d)",
6, 100)
if err != nil {
log.Fatalf("can't create requests bulk")
}
conn.customEvents, err = NewBulk(conn.c,
"events_common.customs",
"(session_id, timestamp, seq_index, name, payload)",
"($%d, $%d, $%d, left($%d, 2700), $%d)",
5, 100)
if err != nil {
log.Fatalf("can't create customEvents bulk")
}
conn.webPageEvents, err = NewBulk(conn.c,
"events.pages",
"(session_id, message_id, timestamp, referrer, base_referrer, host, path, query, dom_content_loaded_time, "+
"load_time, response_end, first_paint_time, first_contentful_paint_time, speed_index, visually_complete, "+
"time_to_interactive, response_time, dom_building_time)",
"($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0),"+
" NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0))",
18, 100)
if err != nil {
log.Fatalf("can't create webPageEvents bulk")
}
conn.webInputEvents, err = NewBulk(conn.c,
"events.inputs",
"(session_id, message_id, timestamp, value, label)",
"($%d, $%d, $%d, $%d, NULLIF($%d,''))",
5, 100)
if err != nil {
log.Fatalf("can't create webPageEvents bulk")
}
conn.webGraphQLEvents, err = NewBulk(conn.c,
"events.graphql",
"(session_id, timestamp, message_id, name, request_body, response_body)",
"($%d, $%d, $%d, left($%d, 2700), $%d, $%d)",
6, 100)
if err != nil {
log.Fatalf("can't create webPageEvents bulk")
}
}
func (conn *Conn) insertAutocompleteValue(sessionID uint64, projectID uint32, tp string, value string) {
if len(value) == 0 {
return
}
if err := conn.autocompletes.Append(value, tp, projectID); err != nil {
log.Printf("autocomplete bulk err: %s", err)
}
}
func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{}) {
batch, ok := conn.batches[sessionID]
if !ok {
@ -90,6 +162,10 @@ func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{})
batch = conn.batches[sessionID]
}
batch.Queue(sql, args...)
conn.rawBatch(sessionID, sql, args...)
}
func (conn *Conn) rawBatch(sessionID uint64, sql string, args ...interface{}) {
// Temp raw batch store
raw := conn.rawBatches[sessionID]
raw = append(raw, &batchItem{
@ -99,8 +175,45 @@ func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{})
conn.rawBatches[sessionID] = raw
}
func (conn *Conn) updateSessionEvents(sessionID uint64, events, pages int) {
if _, ok := conn.sessionUpdates[sessionID]; !ok {
conn.sessionUpdates[sessionID] = NewSessionUpdates(sessionID)
}
conn.sessionUpdates[sessionID].add(pages, events)
}
func (conn *Conn) sendBulks() {
if err := conn.autocompletes.Send(); err != nil {
log.Printf("autocomplete bulk send err: %s", err)
}
if err := conn.requests.Send(); err != nil {
log.Printf("requests bulk send err: %s", err)
}
if err := conn.customEvents.Send(); err != nil {
log.Printf("customEvents bulk send err: %s", err)
}
if err := conn.webPageEvents.Send(); err != nil {
log.Printf("webPageEvents bulk send err: %s", err)
}
if err := conn.webInputEvents.Send(); err != nil {
log.Printf("webInputEvents bulk send err: %s", err)
}
if err := conn.webGraphQLEvents.Send(); err != nil {
log.Printf("webGraphQLEvents bulk send err: %s", err)
}
}
func (conn *Conn) CommitBatches() {
conn.sendBulks()
for sessID, b := range conn.batches {
// Append session update sql request to the end of batch
if update, ok := conn.sessionUpdates[sessID]; ok {
sql, args := update.request()
if sql != "" {
conn.batchQueue(sessID, sql, args...)
b, _ = conn.batches[sessID]
}
}
// Record batch size in bytes and number of lines
conn.batchSizeBytes.Record(context.Background(), float64(conn.batchSizes[sessID]))
conn.batchSizeLines.Record(context.Background(), float64(b.Len()))
@ -109,7 +222,7 @@ func (conn *Conn) CommitBatches() {
isFailed := false
// Send batch to db and execute
br := conn.c.SendBatch(getTimeoutContext(), b)
br := conn.c.SendBatch(b)
l := b.Len()
for i := 0; i < l; i++ {
if ct, err := br.Exec(); err != nil {
@ -125,10 +238,25 @@ func (conn *Conn) CommitBatches() {
attribute.String("method", "batch"), attribute.Bool("failed", isFailed))
conn.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "batch"), attribute.Bool("failed", isFailed))
if !isFailed {
delete(conn.sessionUpdates, sessID)
}
}
conn.batches = make(map[uint64]*pgx.Batch)
conn.batchSizes = make(map[uint64]int)
conn.rawBatches = make(map[uint64][]*batchItem)
// Session updates
for sessID, su := range conn.sessionUpdates {
sql, args := su.request()
if sql == "" {
continue
}
if err := conn.c.Exec(sql, args...); err != nil {
log.Printf("failed session update, sessID: %d, err: %s", sessID, err)
}
}
conn.sessionUpdates = make(map[uint64]*sessionUpdates)
}
func (conn *Conn) updateBatchSize(sessionID uint64, reqSize int) {
@ -145,6 +273,14 @@ func (conn *Conn) commitBatch(sessionID uint64) {
log.Printf("can't find batch for session: %d", sessionID)
return
}
// Append session update sql request to the end of batch
if update, ok := conn.sessionUpdates[sessionID]; ok {
sql, args := update.request()
if sql != "" {
conn.batchQueue(sessionID, sql, args...)
b, _ = conn.batches[sessionID]
}
}
// Record batch size in bytes and number of lines
conn.batchSizeBytes.Record(context.Background(), float64(conn.batchSizes[sessionID]))
conn.batchSizeLines.Record(context.Background(), float64(b.Len()))
@ -153,7 +289,7 @@ func (conn *Conn) commitBatch(sessionID uint64) {
isFailed := false
// Send batch to db and execute
br := conn.c.SendBatch(getTimeoutContext(), b)
br := conn.c.SendBatch(b)
l := b.Len()
for i := 0; i < l; i++ {
if ct, err := br.Exec(); err != nil {
@ -175,117 +311,5 @@ func (conn *Conn) commitBatch(sessionID uint64) {
delete(conn.batches, sessionID)
delete(conn.batchSizes, sessionID)
delete(conn.rawBatches, sessionID)
}
func (conn *Conn) query(sql string, args ...interface{}) (pgx.Rows, error) {
start := time.Now()
res, err := conn.c.Query(getTimeoutContext(), sql, args...)
method, table := methodName(sql)
conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
conn.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return res, err
}
func (conn *Conn) queryRow(sql string, args ...interface{}) pgx.Row {
start := time.Now()
res := conn.c.QueryRow(getTimeoutContext(), sql, args...)
method, table := methodName(sql)
conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
conn.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return res
}
func (conn *Conn) exec(sql string, args ...interface{}) error {
start := time.Now()
_, err := conn.c.Exec(getTimeoutContext(), sql, args...)
method, table := methodName(sql)
conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
conn.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return err
}
type _Tx struct {
pgx.Tx
sqlRequestTime syncfloat64.Histogram
sqlRequestCounter syncfloat64.Counter
}
func (conn *Conn) begin() (_Tx, error) {
start := time.Now()
tx, err := conn.c.Begin(context.Background())
conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "begin"))
conn.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "begin"))
return _Tx{tx, conn.sqlRequestTime, conn.sqlRequestCounter}, err
}
func (tx _Tx) exec(sql string, args ...interface{}) error {
start := time.Now()
_, err := tx.Exec(context.Background(), sql, args...)
method, table := methodName(sql)
tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
tx.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return err
}
func (tx _Tx) rollback() error {
start := time.Now()
err := tx.Rollback(context.Background())
tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "rollback"))
tx.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "rollback"))
return err
}
func (tx _Tx) commit() error {
start := time.Now()
err := tx.Commit(context.Background())
tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "commit"))
tx.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "commit"))
return err
}
func methodName(sql string) (string, string) {
cmd, table := "unknown", "unknown"
// Prepare sql request for parsing
sql = strings.TrimSpace(sql)
sql = strings.ReplaceAll(sql, "\n", " ")
sql = strings.ReplaceAll(sql, "\t", "")
sql = strings.ToLower(sql)
// Get sql command name
parts := strings.Split(sql, " ")
if parts[0] == "" {
return cmd, table
} else {
cmd = strings.TrimSpace(parts[0])
}
// Get table name
switch cmd {
case "select":
for i, p := range parts {
if strings.TrimSpace(p) == "from" {
table = strings.TrimSpace(parts[i+1])
}
}
case "update":
table = strings.TrimSpace(parts[1])
case "insert":
table = strings.TrimSpace(parts[2])
}
return cmd, table
delete(conn.sessionUpdates, sessionID)
}

View file

@ -15,7 +15,7 @@ type Integration struct {
}
func (conn *Conn) IterateIntegrationsOrdered(iter func(integration *Integration, err error)) error {
rows, err := conn.query(`
rows, err := conn.c.Query(`
SELECT project_id, provider, options, request_data
FROM integrations
`)
@ -40,7 +40,7 @@ func (conn *Conn) IterateIntegrationsOrdered(iter func(integration *Integration,
}
func (conn *Conn) UpdateIntegrationRequestData(i *Integration) error {
return conn.exec(`
return conn.c.Exec(`
UPDATE integrations
SET request_data = $1
WHERE project_id=$2 AND provider=$3`,

View file

@ -18,31 +18,8 @@ func getAutocompleteType(baseType string, platform string) string {
}
func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value string) {
if len(value) == 0 {
return
}
sqlRequest := `
INSERT INTO autocomplete (
value,
type,
project_id
) (SELECT
$1, $2, project_id
FROM sessions
WHERE session_id = $3
) ON CONFLICT DO NOTHING`
if err := conn.exec(sqlRequest, value, tp, sessionID); err != nil {
log.Printf("can't insert autocomplete: %s", err)
}
//conn.batchQueue(sessionID, sqlRequest, value, tp, sessionID)
// Record approximate message size
//conn.updateBatchSize(sessionID, len(sqlRequest)+len(value)+len(tp)+8)
}
func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error {
return conn.exec(`
return conn.c.Exec(`
INSERT INTO sessions (
session_id, project_id, start_ts,
user_uuid, user_device, user_device_type, user_country,
@ -74,18 +51,26 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error {
}
func (conn *Conn) HandleSessionStart(sessionID uint64, s *types.Session) error {
conn.insertAutocompleteValue(sessionID, getAutocompleteType("USEROS", s.Platform), s.UserOS)
conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice)
conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry)
conn.insertAutocompleteValue(sessionID, getAutocompleteType("REVID", s.Platform), s.RevID)
conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USEROS", s.Platform), s.UserOS)
conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice)
conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry)
conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("REVID", s.Platform), s.RevID)
// s.Platform == "web"
conn.insertAutocompleteValue(sessionID, "USERBROWSER", s.UserBrowser)
conn.insertAutocompleteValue(sessionID, s.ProjectID, "USERBROWSER", s.UserBrowser)
return nil
}
func (conn *Conn) GetSessionDuration(sessionID uint64) (uint64, error) {
var dur uint64
if err := conn.c.QueryRow("SELECT COALESCE( duration, 0 ) FROM sessions WHERE session_id=$1", sessionID).Scan(&dur); err != nil {
return 0, err
}
return dur, nil
}
func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) {
var dur uint64
if err := conn.queryRow(`
if err := conn.c.QueryRow(`
UPDATE sessions SET duration=$2 - start_ts
WHERE session_id=$1
RETURNING duration
@ -119,30 +104,16 @@ func (conn *Conn) HandleSessionEnd(sessionID uint64) error {
}
func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64, url string, duration uint64, success bool) error {
sqlRequest := `
INSERT INTO events_common.requests (
session_id, timestamp, seq_index, url, duration, success
) VALUES (
$1, $2, $3, left($4, 2700), $5, $6
)`
conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), url, duration, success)
// Record approximate message size
conn.updateBatchSize(sessionID, len(sqlRequest)+len(url)+8*4)
if err := conn.requests.Append(sessionID, timestamp, getSqIdx(index), url, duration, success); err != nil {
return fmt.Errorf("insert request in bulk err: %s", err)
}
return nil
}
func (conn *Conn) InsertCustomEvent(sessionID uint64, timestamp uint64, index uint64, name string, payload string) error {
sqlRequest := `
INSERT INTO events_common.customs (
session_id, timestamp, seq_index, name, payload
) VALUES (
$1, $2, $3, left($4, 2700), $5
)`
conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), name, payload)
// Record approximate message size
conn.updateBatchSize(sessionID, len(sqlRequest)+len(name)+len(payload)+8*3)
if err := conn.customEvents.Append(sessionID, timestamp, getSqIdx(index), name, payload); err != nil {
return fmt.Errorf("insert custom event in bulk err: %s", err)
}
return nil
}
@ -172,15 +143,21 @@ func (conn *Conn) InsertMetadata(sessionID uint64, keyNo uint, value string) err
sqlRequest := `
UPDATE sessions SET metadata_%v = $1
WHERE session_id = $2`
return conn.exec(fmt.Sprintf(sqlRequest, keyNo), value, sessionID)
return conn.c.Exec(fmt.Sprintf(sqlRequest, keyNo), value, sessionID)
}
func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messages.IssueEvent) error {
tx, err := conn.begin()
func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messages.IssueEvent) (err error) {
tx, err := conn.c.Begin()
if err != nil {
return err
}
defer tx.rollback()
defer func() {
if err != nil {
if rollbackErr := tx.rollback(); rollbackErr != nil {
log.Printf("rollback err: %s", rollbackErr)
}
}
}()
issueID := hashid.IssueID(projectID, e)
// TEMP. TODO: nullable & json message field type
@ -237,5 +214,6 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag
return err
}
}
return tx.commit()
err = tx.commit()
return
}

View file

@ -9,7 +9,7 @@ import (
func (conn *Conn) InsertIOSCustomEvent(sessionID uint64, e *messages.IOSCustomEvent) error {
err := conn.InsertCustomEvent(sessionID, e.Timestamp, e.Index, e.Name, e.Payload)
if err == nil {
conn.insertAutocompleteValue(sessionID, "CUSTOM_IOS", e.Name)
conn.insertAutocompleteValue(sessionID, 0, "CUSTOM_IOS", e.Name)
}
return err
}
@ -17,7 +17,7 @@ func (conn *Conn) InsertIOSCustomEvent(sessionID uint64, e *messages.IOSCustomEv
func (conn *Conn) InsertIOSUserID(sessionID uint64, userID *messages.IOSUserID) error {
err := conn.InsertUserID(sessionID, userID.Value)
if err == nil {
conn.insertAutocompleteValue(sessionID, "USERID_IOS", userID.Value)
conn.insertAutocompleteValue(sessionID, 0, "USERID_IOS", userID.Value)
}
return err
}
@ -25,7 +25,7 @@ func (conn *Conn) InsertIOSUserID(sessionID uint64, userID *messages.IOSUserID)
func (conn *Conn) InsertIOSUserAnonymousID(sessionID uint64, userAnonymousID *messages.IOSUserAnonymousID) error {
err := conn.InsertUserAnonymousID(sessionID, userAnonymousID.Value)
if err == nil {
conn.insertAutocompleteValue(sessionID, "USERANONYMOUSID_IOS", userAnonymousID.Value)
conn.insertAutocompleteValue(sessionID, 0, "USERANONYMOUSID_IOS", userAnonymousID.Value)
}
return err
}
@ -33,13 +33,13 @@ func (conn *Conn) InsertIOSUserAnonymousID(sessionID uint64, userAnonymousID *me
func (conn *Conn) InsertIOSNetworkCall(sessionID uint64, e *messages.IOSNetworkCall) error {
err := conn.InsertRequest(sessionID, e.Timestamp, e.Index, e.URL, e.Duration, e.Success)
if err == nil {
conn.insertAutocompleteValue(sessionID, "REQUEST_IOS", url.DiscardURLQuery(e.URL))
conn.insertAutocompleteValue(sessionID, 0, "REQUEST_IOS", url.DiscardURLQuery(e.URL))
}
return err
}
func (conn *Conn) InsertIOSScreenEnter(sessionID uint64, screenEnter *messages.IOSScreenEnter) error {
tx, err := conn.begin()
tx, err := conn.c.Begin()
if err != nil {
return err
}
@ -65,12 +65,12 @@ func (conn *Conn) InsertIOSScreenEnter(sessionID uint64, screenEnter *messages.I
if err = tx.commit(); err != nil {
return err
}
conn.insertAutocompleteValue(sessionID, "VIEW_IOS", screenEnter.ViewName)
conn.insertAutocompleteValue(sessionID, 0, "VIEW_IOS", screenEnter.ViewName)
return nil
}
func (conn *Conn) InsertIOSClickEvent(sessionID uint64, clickEvent *messages.IOSClickEvent) error {
tx, err := conn.begin()
tx, err := conn.c.Begin()
if err != nil {
return err
}
@ -96,12 +96,12 @@ func (conn *Conn) InsertIOSClickEvent(sessionID uint64, clickEvent *messages.IOS
if err = tx.commit(); err != nil {
return err
}
conn.insertAutocompleteValue(sessionID, "CLICK_IOS", clickEvent.Label)
conn.insertAutocompleteValue(sessionID, 0, "CLICK_IOS", clickEvent.Label)
return nil
}
func (conn *Conn) InsertIOSInputEvent(sessionID uint64, inputEvent *messages.IOSInputEvent) error {
tx, err := conn.begin()
tx, err := conn.c.Begin()
if err != nil {
return err
}
@ -132,13 +132,13 @@ func (conn *Conn) InsertIOSInputEvent(sessionID uint64, inputEvent *messages.IOS
if err = tx.commit(); err != nil {
return err
}
conn.insertAutocompleteValue(sessionID, "INPUT_IOS", inputEvent.Label)
conn.insertAutocompleteValue(sessionID, 0, "INPUT_IOS", inputEvent.Label)
// conn.insertAutocompleteValue(sessionID, "INPUT_VALUE", inputEvent.Label)
return nil
}
func (conn *Conn) InsertIOSCrash(sessionID uint64, projectID uint32, crash *messages.IOSCrash) error {
tx, err := conn.begin()
tx, err := conn.c.Begin()
if err != nil {
return err
}

View file

@ -1,6 +1,7 @@
package postgres
import (
"log"
"math"
"openreplay/backend/pkg/hashid"
@ -13,104 +14,54 @@ func getSqIdx(messageID uint64) uint {
return uint(messageID % math.MaxInt32)
}
func (conn *Conn) InsertWebCustomEvent(sessionID uint64, e *CustomEvent) error {
func (conn *Conn) InsertWebCustomEvent(sessionID uint64, projectID uint32, e *CustomEvent) error {
err := conn.InsertCustomEvent(sessionID, e.Timestamp,
e.MessageID,
e.Name, e.Payload)
if err == nil {
conn.insertAutocompleteValue(sessionID, "CUSTOM", e.Name)
conn.insertAutocompleteValue(sessionID, projectID, "CUSTOM", e.Name)
}
return err
}
func (conn *Conn) InsertWebUserID(sessionID uint64, userID *UserID) error {
func (conn *Conn) InsertWebUserID(sessionID uint64, projectID uint32, userID *UserID) error {
err := conn.InsertUserID(sessionID, userID.ID)
if err == nil {
conn.insertAutocompleteValue(sessionID, "USERID", userID.ID)
conn.insertAutocompleteValue(sessionID, projectID, "USERID", userID.ID)
}
return err
}
func (conn *Conn) InsertWebUserAnonymousID(sessionID uint64, userAnonymousID *UserAnonymousID) error {
func (conn *Conn) InsertWebUserAnonymousID(sessionID uint64, projectID uint32, userAnonymousID *UserAnonymousID) error {
err := conn.InsertUserAnonymousID(sessionID, userAnonymousID.ID)
if err == nil {
conn.insertAutocompleteValue(sessionID, "USERANONYMOUSID", userAnonymousID.ID)
conn.insertAutocompleteValue(sessionID, projectID, "USERANONYMOUSID", userAnonymousID.ID)
}
return err
}
// func (conn *Conn) InsertWebResourceEvent(sessionID uint64, e *ResourceEvent) error {
// if e.Type != "fetch" {
// return nil
// }
// err := conn.InsertRequest(sessionID, e.Timestamp,
// e.MessageID,
// e.URL, e.Duration, e.Success,
// )
// if err == nil {
// conn.insertAutocompleteValue(sessionID, "REQUEST", url.DiscardURLQuery(e.URL))
// }
// return err
// }
// TODO: fix column "dom_content_loaded_event_end" of relation "pages"
func (conn *Conn) InsertWebPageEvent(sessionID uint64, e *PageEvent) error {
func (conn *Conn) InsertWebPageEvent(sessionID uint64, projectID uint32, e *PageEvent) error {
host, path, query, err := url.GetURLParts(e.URL)
if err != nil {
return err
}
tx, err := conn.begin()
if err != nil {
return err
// base_path is deprecated
if err = conn.webPageEvents.Append(sessionID, e.MessageID, e.Timestamp, e.Referrer, url.DiscardURLQuery(e.Referrer),
host, path, query, e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint,
e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e)); err != nil {
log.Printf("insert web page event in bulk err: %s", err)
}
defer tx.rollback()
// base_path is depricated
if err := tx.exec(`
INSERT INTO events.pages (
session_id, message_id, timestamp, referrer, base_referrer, host, path, query,
dom_content_loaded_time, load_time, response_end, first_paint_time, first_contentful_paint_time,
speed_index, visually_complete, time_to_interactive,
response_time, dom_building_time
) VALUES (
$1, $2, $3,
$4, $5,
$6, $7, $8,
NULLIF($9, 0), NULLIF($10, 0), NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0),
NULLIF($14, 0), NULLIF($15, 0), NULLIF($16, 0),
NULLIF($17, 0), NULLIF($18, 0)
)
`,
sessionID, e.MessageID, e.Timestamp,
e.Referrer, url.DiscardURLQuery(e.Referrer),
host, path, query,
e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint,
e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive,
calcResponseTime(e), calcDomBuildingTime(e),
); err != nil {
return err
}
if err = tx.exec(`
UPDATE sessions SET (pages_count, events_count) = (pages_count + 1, events_count + 1)
WHERE session_id = $1`,
sessionID,
); err != nil {
return err
}
if err = tx.commit(); err != nil {
return err
}
conn.insertAutocompleteValue(sessionID, "LOCATION", url.DiscardURLQuery(path))
conn.insertAutocompleteValue(sessionID, "REFERRER", url.DiscardURLQuery(e.Referrer))
// Accumulate session updates and exec inside batch with another sql commands
conn.updateSessionEvents(sessionID, 1, 1)
// Add new value set to autocomplete bulk
conn.insertAutocompleteValue(sessionID, projectID, "LOCATION", url.DiscardURLQuery(path))
conn.insertAutocompleteValue(sessionID, projectID, "REFERRER", url.DiscardURLQuery(e.Referrer))
return nil
}
func (conn *Conn) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error {
tx, err := conn.begin()
if err != nil {
return err
}
defer tx.rollback()
if err = tx.exec(`
func (conn *Conn) InsertWebClickEvent(sessionID uint64, projectID uint32, e *ClickEvent) error {
sqlRequest := `
INSERT INTO events.clicks
(session_id, message_id, timestamp, label, selector, url)
(SELECT
@ -118,65 +69,40 @@ func (conn *Conn) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error {
FROM events.pages
WHERE session_id = $1 AND timestamp <= $3 ORDER BY timestamp DESC LIMIT 1
)
`,
sessionID, e.MessageID, e.Timestamp, e.Label, e.Selector,
); err != nil {
return err
}
if err = tx.exec(`
UPDATE sessions SET events_count = events_count + 1
WHERE session_id = $1`,
sessionID,
); err != nil {
return err
}
if err = tx.commit(); err != nil {
return err
}
conn.insertAutocompleteValue(sessionID, "CLICK", e.Label)
`
conn.batchQueue(sessionID, sqlRequest, sessionID, e.MessageID, e.Timestamp, e.Label, e.Selector)
// Accumulate session updates and exec inside batch with another sql commands
conn.updateSessionEvents(sessionID, 1, 0)
// Add new value set to autocomplete bulk
conn.insertAutocompleteValue(sessionID, projectID, "CLICK", e.Label)
return nil
}
func (conn *Conn) InsertWebInputEvent(sessionID uint64, e *InputEvent) error {
tx, err := conn.begin()
if err != nil {
return err
}
defer tx.rollback()
func (conn *Conn) InsertWebInputEvent(sessionID uint64, projectID uint32, e *InputEvent) error {
value := &e.Value
if e.ValueMasked {
value = nil
}
if err = tx.exec(`
INSERT INTO events.inputs
(session_id, message_id, timestamp, value, label)
VALUES
($1, $2, $3, $4, NULLIF($5,''))
`,
sessionID, e.MessageID, e.Timestamp, value, e.Label,
); err != nil {
return err
if err := conn.webInputEvents.Append(sessionID, e.MessageID, e.Timestamp, value, e.Label); err != nil {
log.Printf("insert web input event err: %s", err)
}
if err = tx.exec(`
UPDATE sessions SET events_count = events_count + 1
WHERE session_id = $1`,
sessionID,
); err != nil {
return err
}
if err = tx.commit(); err != nil {
return err
}
conn.insertAutocompleteValue(sessionID, "INPUT", e.Label)
conn.updateSessionEvents(sessionID, 1, 0)
conn.insertAutocompleteValue(sessionID, projectID, "INPUT", e.Label)
return nil
}
func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *ErrorEvent) error {
tx, err := conn.begin()
func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *ErrorEvent) (err error) {
tx, err := conn.c.Begin()
if err != nil {
return err
}
defer tx.rollback()
defer func() {
if err != nil {
if rollbackErr := tx.rollback(); rollbackErr != nil {
log.Printf("rollback err: %s", rollbackErr)
}
}
}()
errorID := hashid.WebErrorID(projectID, e)
if err = tx.exec(`
@ -206,17 +132,18 @@ func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *Err
); err != nil {
return err
}
return tx.commit()
err = tx.commit()
return
}
func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *FetchEvent) error {
func (conn *Conn) InsertWebFetchEvent(sessionID uint64, projectID uint32, savePayload bool, e *FetchEvent) error {
var request, response *string
if savePayload {
request = &e.Request
response = &e.Response
}
host, path, query, err := url.GetURLParts(e.URL)
conn.insertAutocompleteValue(sessionID, "REQUEST", path)
conn.insertAutocompleteValue(sessionID, projectID, "REQUEST", path)
if err != nil {
return err
}
@ -246,29 +173,15 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet
return nil
}
func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *GraphQLEvent) error {
func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, projectID uint32, savePayload bool, e *GraphQLEvent) error {
var request, response *string
if savePayload {
request = &e.Variables
response = &e.Response
}
conn.insertAutocompleteValue(sessionID, "GRAPHQL", e.OperationName)
sqlRequest := `
INSERT INTO events.graphql (
session_id, timestamp, message_id,
name,
request_body, response_body
) VALUES (
$1, $2, $3,
left($4, 2700),
$5, $6
) ON CONFLICT DO NOTHING`
conn.batchQueue(sessionID, sqlRequest, sessionID, e.Timestamp, e.MessageID,
e.OperationName, request, response,
)
// Record approximate message size
conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.OperationName)+len(e.Variables)+len(e.Response)+8*3)
if err := conn.webGraphQLEvents.Append(sessionID, e.Timestamp, e.MessageID, e.OperationName, request, response); err != nil {
log.Printf("insert web graphQL event err: %s", err)
}
conn.insertAutocompleteValue(sessionID, projectID, "GRAPHQL", e.OperationName)
return nil
}

View file

@ -0,0 +1,175 @@
package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/instrument/syncfloat64"
"strings"
"time"
)
// Pool is a pgx.Pool wrapper with metrics integration
type Pool interface {
Query(sql string, args ...interface{}) (pgx.Rows, error)
QueryRow(sql string, args ...interface{}) pgx.Row
Exec(sql string, arguments ...interface{}) error
SendBatch(b *pgx.Batch) pgx.BatchResults
Begin() (*_Tx, error)
Close()
}
type poolImpl struct {
conn *pgxpool.Pool
sqlRequestTime syncfloat64.Histogram
sqlRequestCounter syncfloat64.Counter
}
func (p *poolImpl) Query(sql string, args ...interface{}) (pgx.Rows, error) {
start := time.Now()
res, err := p.conn.Query(getTimeoutContext(), sql, args...)
method, table := methodName(sql)
p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
p.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return res, err
}
func (p *poolImpl) QueryRow(sql string, args ...interface{}) pgx.Row {
start := time.Now()
res := p.conn.QueryRow(getTimeoutContext(), sql, args...)
method, table := methodName(sql)
p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
p.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return res
}
func (p *poolImpl) Exec(sql string, arguments ...interface{}) error {
start := time.Now()
_, err := p.conn.Exec(getTimeoutContext(), sql, arguments...)
method, table := methodName(sql)
p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
p.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return err
}
func (p *poolImpl) SendBatch(b *pgx.Batch) pgx.BatchResults {
start := time.Now()
res := p.conn.SendBatch(getTimeoutContext(), b)
p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "sendBatch"))
p.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "sendBatch"))
return res
}
func (p *poolImpl) Begin() (*_Tx, error) {
start := time.Now()
tx, err := p.conn.Begin(context.Background())
p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "begin"))
p.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "begin"))
return &_Tx{tx, p.sqlRequestTime, p.sqlRequestCounter}, err
}
func (p *poolImpl) Close() {
p.conn.Close()
}
func NewPool(conn *pgxpool.Pool, sqlRequestTime syncfloat64.Histogram, sqlRequestCounter syncfloat64.Counter) (Pool, error) {
if conn == nil {
return nil, errors.New("conn is empty")
}
return &poolImpl{
conn: conn,
sqlRequestTime: sqlRequestTime,
sqlRequestCounter: sqlRequestCounter,
}, nil
}
// TX - start
type _Tx struct {
pgx.Tx
sqlRequestTime syncfloat64.Histogram
sqlRequestCounter syncfloat64.Counter
}
func (tx *_Tx) exec(sql string, args ...interface{}) error {
start := time.Now()
_, err := tx.Exec(context.Background(), sql, args...)
method, table := methodName(sql)
tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", method), attribute.String("table", table))
tx.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", method), attribute.String("table", table))
return err
}
func (tx *_Tx) rollback() error {
start := time.Now()
err := tx.Rollback(context.Background())
tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "rollback"))
tx.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "rollback"))
return err
}
func (tx *_Tx) commit() error {
start := time.Now()
err := tx.Commit(context.Background())
tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()),
attribute.String("method", "commit"))
tx.sqlRequestCounter.Add(context.Background(), 1,
attribute.String("method", "commit"))
return err
}
// TX - end
func getTimeoutContext() context.Context {
ctx, _ := context.WithTimeout(context.Background(), time.Second*30)
return ctx
}
func methodName(sql string) (string, string) {
cmd, table := "unknown", "unknown"
// Prepare sql request for parsing
sql = strings.TrimSpace(sql)
sql = strings.ReplaceAll(sql, "\n", " ")
sql = strings.ReplaceAll(sql, "\t", "")
sql = strings.ToLower(sql)
// Get sql command name
parts := strings.Split(sql, " ")
if parts[0] == "" {
return cmd, table
} else {
cmd = strings.TrimSpace(parts[0])
}
// Get table name
switch cmd {
case "select":
for i, p := range parts {
if strings.TrimSpace(p) == "from" {
table = strings.TrimSpace(parts[i+1])
}
}
case "update":
table = strings.TrimSpace(parts[1])
case "insert":
table = strings.TrimSpace(parts[2])
}
return cmd, table
}

View file

@ -6,7 +6,7 @@ import (
func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) {
p := &Project{ProjectKey: projectKey}
if err := conn.queryRow(`
if err := conn.c.QueryRow(`
SELECT max_session_duration, sample_rate, project_id
FROM projects
WHERE project_key=$1 AND active = true
@ -21,7 +21,7 @@ func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) {
// TODO: logical separation of metadata
func (conn *Conn) GetProject(projectID uint32) (*Project, error) {
p := &Project{ProjectID: projectID}
if err := conn.queryRow(`
if err := conn.c.QueryRow(`
SELECT project_key, max_session_duration, save_request_payloads,
metadata_1, metadata_2, metadata_3, metadata_4, metadata_5,
metadata_6, metadata_7, metadata_8, metadata_9, metadata_10

View file

@ -0,0 +1,30 @@
package postgres
// Mechanism of combination several session updates into one
const sessionUpdateReq = `UPDATE sessions SET (pages_count, events_count) = (pages_count + $1, events_count + $2) WHERE session_id = $3`
type sessionUpdates struct {
sessionID uint64
pages int
events int
}
func NewSessionUpdates(sessionID uint64) *sessionUpdates {
return &sessionUpdates{
sessionID: sessionID,
pages: 0,
events: 0,
}
}
func (su *sessionUpdates) add(pages, events int) {
su.pages += pages
su.events += events
}
func (su *sessionUpdates) request() (string, []interface{}) {
if su.pages == 0 && su.events == 0 {
return "", nil
}
return sessionUpdateReq, []interface{}{su.pages, su.events, su.sessionID}
}

View file

@ -1,14 +1,11 @@
package postgres
//import . "openreplay/backend/pkg/messages"
import . "openreplay/backend/pkg/db/types"
//import "log"
func (conn *Conn) GetSession(sessionID uint64) (*Session, error) {
s := &Session{SessionID: sessionID}
var revID, userOSVersion *string
if err := conn.queryRow(`
if err := conn.c.QueryRow(`
SELECT platform,
duration, project_id, start_ts,
user_uuid, user_os, user_os_version,
@ -39,69 +36,3 @@ func (conn *Conn) GetSession(sessionID uint64) (*Session, error) {
}
return s, nil
}
// func (conn *Conn) GetSessionClickEvents(sessionID uint64) (list []IOSClickEvent, err error) {
// rows, err := conn.query(`
// SELECT
// timestamp, seq_index, label
// FROM events_ios.clicks
// WHERE session_id=$1
// `, sessionID)
// if err != nil {
// return err
// }
// defer rows.Close()
// for rows.Next() {
// e := new(IOSClickEvent)
// if err = rows.Scan(&e.Timestamp, &e.Index, &e.Label); err != nil {
// log.Printf("Error while scanning click events: %v", err)
// } else {
// list = append(list, e)
// }
// }
// return list
// }
// func (conn *Conn) GetSessionInputEvents(sessionID uint64) (list []IOSInputEvent, err error) {
// rows, err := conn.query(`
// SELECT
// timestamp, seq_index, label, value
// FROM events_ios.inputs
// WHERE session_id=$1
// `, sessionID)
// if err != nil {
// return err
// }
// defer rows.Close()
// for rows.Next() {
// e := new(IOSInputEvent)
// if err = rows.Scan(&e.Timestamp, &e.Index, &e.Label, &e.Value); err != nil {
// log.Printf("Error while scanning click events: %v", err)
// } else {
// list = append(list, e)
// }
// }
// return list
// }
// func (conn *Conn) GetSessionCrashEvents(sessionID uint64) (list []IOSCrash, err error) {
// rows, err := conn.query(`
// SELECT
// timestamp, seq_index
// FROM events_ios.crashes
// WHERE session_id=$1
// `, sessionID)
// if err != nil {
// return err
// }
// defer rows.Close()
// for rows.Next() {
// e := new(IOSCrash)
// if err = rows.Scan(&e.Timestamp, &e.Index, &e.Label, &e.Value); err != nil {
// log.Printf("Error while scanning click events: %v", err)
// } else {
// list = append(list, e)
// }
// }
// return list
// }

View file

@ -16,7 +16,7 @@ type UnstartedSession struct {
}
func (conn *Conn) InsertUnstartedSession(s UnstartedSession) error {
return conn.exec(`
return conn.c.Exec(`
INSERT INTO unstarted_sessions (
project_id,
tracker_version, do_not_track,

15
ee/api/.dockerignore Normal file
View file

@ -0,0 +1,15 @@
# ignore .git and .cache folders
.git
.cache
**/build.sh
**/build_*.sh
**/*deploy.sh
app_crons.py
app_alerts.py
requirements-crons.txt
requirements-alerts.txt
build_crons.sh
build_alerts.sh
entrypoint_crons.sh
entrypoint_alerts.sh

View file

@ -1,28 +1,18 @@
FROM python:3.9.12-slim
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
ENV APP_NAME chalice
RUN apt-get update && apt-get install -y pkg-config libxmlsec1-dev gcc && rm -rf /var/lib/apt/lists/*
# Add Tini
# Startup daemon
ENV TINI_VERSION=v0.19.0 \
SOURCE_MAP_VERSION=0.7.4
RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec nodejs npm tini
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
ARG envarg
ENV ENTERPRISE_BUILD ${envarg}
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
ADD https://unpkg.com/source-map@${SOURCE_MAP_VERSION}/lib/mappings.wasm /mappings.wasm
RUN chmod +x /tini
ENV SOURCE_MAP_VERSION=0.7.4 \
APP_NAME=chalice \
ENTERPRISE_BUILD=${envarg}
# Installing Nodejs
RUN apt update && apt install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_12.x | bash - && \
apt install -y nodejs && \
apt remove --purge -y curl && \
rm -rf /var/lib/apt/lists/*
ADD https://unpkg.com/source-map@${SOURCE_MAP_VERSION}/lib/mappings.wasm /mappings.wasm
WORKDIR /work_tmp
COPY requirements.txt /work_tmp/requirements.txt
RUN pip install -r /work_tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /work_tmp/requirements.txt
COPY sourcemap-reader/*.json /work_tmp/
RUN cd /work_tmp && npm install
@ -30,5 +20,8 @@ WORKDIR /work
COPY . .
RUN mv env.default .env && mv /work_tmp/node_modules sourcemap-reader/.
ENTRYPOINT ["/tini", "--"]
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ./entrypoint.sh

View file

@ -1,25 +1,23 @@
FROM python:3.9.12-slim
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
RUN apt-get update && apt-get install -y pkg-config libxmlsec1-dev gcc && rm -rf /var/lib/apt/lists/*
ENV APP_NAME alerts
ENV pg_minconn 2
ENV pg_maxconn 10
# Add Tini
# Startup daemon
ENV TINI_VERSION v0.19.0
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
RUN apk add --no-cache build-base tini
ARG envarg
ENV ENTERPRISE_BUILD ${envarg}
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENV APP_NAME=alerts \
pg_minconn=2 \
pg_maxconn=10 \
ENTERPRISE_BUILD=${envarg}
COPY requirements.txt /work_tmp/requirements.txt
RUN pip install -r /work_tmp/requirements.txt
COPY requirements-alerts.txt /work_tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /work_tmp/requirements.txt
WORKDIR /work
COPY . .
RUN mv env.default .env && mv app_alerts.py app.py && mv entrypoint_alerts.sh entrypoint.sh
ENTRYPOINT ["/tini", "--"]
CMD ./entrypoint.sh
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ./entrypoint.sh

View file

@ -0,0 +1,15 @@
# ignore .git and .cache folders
.git
.cache
**/build.sh
**/build_*.sh
**/*deploy.sh
app.py
app_crons.py
requirements.txt
requirements-crons.txt
build.sh
build_crons.sh
entrypoint.sh
entrypoint_crons.sh

View file

@ -1,26 +1,25 @@
FROM python:3.9.12-slim
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
ENV APP_NAME crons
ENV pg_minconn 2
ENV pg_maxconn 10
RUN apt-get update && apt-get install -y pkg-config libxmlsec1-dev gcc && rm -rf /var/lib/apt/lists/*
# Add Tini
# Startup daemon
ENV TINI_VERSION=v0.19.0 \
ACTION=""
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
RUN apk add --no-cache build-base tini
ARG envarg
ENV ENTERPRISE_BUILD ${envarg}
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENV APP_NAME=crons \
pg_minconn=2 \
pg_maxconn=10 \
ENTERPRISE_BUILD=${envarg} \
ACTION=""
WORKDIR /work_tmp
COPY requirements.txt /work_tmp/requirements.txt
RUN pip install -r /work_tmp/requirements.txt
COPY requirements-crons.txt /work_tmp/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /work_tmp/requirements.txt
WORKDIR /work
COPY . .
RUN mv env.default .env && mv entrypoint_crons.sh entrypoint.sh
ENTRYPOINT ["/tini", "--"]
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ./entrypoint.sh

View file

@ -0,0 +1,15 @@
# ignore .git and .cache folders
.git
.cache
**/build.sh
**/build_*.sh
**/*deploy.sh
app.py
app_alerts.py
requirements.txt
requirements-alerts.txt
build.sh
build_alerts.sh
entrypoint.sh
entrypoint_alerts.sh

View file

@ -1,19 +1,28 @@
print("============= CRONS =============")
import sys
import asyncio
from routers.crons import core_dynamic_crons
def process(action):
{
def default_action(action):
async def _func():
print(f"{action} not found in crons-definitions")
return _func
async def process(action):
await {
"TELEMETRY": core_dynamic_crons.telemetry_cron,
"JOB": core_dynamic_crons.run_scheduled_jobs,
"REPORT": core_dynamic_crons.weekly_report2
}.get(action.upper(), lambda: print(f"{action} not found in crons-definitions"))()
}.get(action.upper(), default_action(action))()
if __name__ == '__main__':
if len(sys.argv) < 2 or len(sys.argv[1]) < 1:
print("please provide actions as argument")
else:
process(sys.argv[1])
print(f"action: {sys.argv[1]}")
asyncio.run(process(sys.argv[1]))

View file

@ -6,23 +6,6 @@
# Default will be OSS build.
# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh <ee>
function make_submodule() {
# -- this part was generated by modules_lister.py --
mkdir crons
cp -R ./{app_crons,schemas,schemas_ee}.py ./crons/
mkdir -p ./crons/routers/crons/
cp -R ./routers/crons/{__init__,core_dynamic_crons}.py ./crons/routers/crons/
mkdir -p ./crons/chalicelib/
cp -R ./chalicelib/__init__.py ./crons/chalicelib/
mkdir -p ./crons/chalicelib/core/
cp -R ./chalicelib/core/{__init__,telemetry,license,unlock,weekly_report,jobs,sessions,events,issues,sessions_metas,metadata,projects,users,authorizers,tenants,roles,assist,events_ios,sessions_mobs,errors,metrics,sourcemaps,sourcemaps_parser,resources,performance_event}.py ./crons/chalicelib/core/
mkdir -p ./crons/chalicelib/utils/
cp -R ./chalicelib/utils/{__init__,TimeUTC,pg_client,helper,event_filter_definition,dev,email_helper,email_handler,smtp,s3,args_transformer,ch_client,SAML2_helper,metrics_helper}.py ./crons/chalicelib/utils/
# -- end of generated part
cp -R ./{Dockerfile.crons,requirements.txt,env.default,entrypoint_crons.sh} ./crons/
cp -R ./chalicelib/utils/html ./crons/chalicelib/utils/html
}
git_sha1=${IMAGE_TAG:-$(git rev-parse HEAD)}
envarg="default-foss"
@ -42,11 +25,9 @@ function build_api(){
envarg="default-ee"
tag="ee-"
make_submodule $1
cd crons
docker build -f ./Dockerfile.crons --build-arg envarg=$envarg -t ${DOCKER_REPO:-'local'}/crons:${git_sha1} .
cd ..
rm -rf crons
cp -R ../api ../_crons
docker build -f ../_crons/Dockerfile.crons --build-arg envarg=$envarg -t ${DOCKER_REPO:-'local'}/crons:${git_sha1} .
rm -rf ../crons
[[ $PUSH_IMAGE -eq 1 ]] && {
docker push ${DOCKER_REPO:-'local'}/crons:${git_sha1}
docker tag ${DOCKER_REPO:-'local'}/crons:${git_sha1} ${DOCKER_REPO:-'local'}/crons:${tag}latest

View file

@ -52,14 +52,23 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st
AND users.tenant_id = %(tenant_id)s
AND (roles.all_projects OR roles_projects.project_id = s.project_id)
) AS role_project ON (TRUE)"""
pre_select = ""
if recorded:
pre_select = """WITH recorded_p AS (SELECT DISTINCT projects.project_id
FROM projects INNER JOIN sessions USING (project_id)
WHERE tenant_id =%(tenant_id)s
AND deleted_at IS NULL
AND duration > 0)"""
cur.execute(
cur.mogrify(f"""\
{pre_select}
SELECT
s.project_id, s.name, s.project_key, s.save_request_payloads
{',s.gdpr' if gdpr else ''}
{',COALESCE((SELECT TRUE FROM public.sessions WHERE sessions.project_id = s.project_id LIMIT 1), FALSE) AS recorded' if recorded else ''}
{',EXISTS(SELECT 1 FROM recorded_p WHERE recorded_p.project_id = s.project_id) AS recorded' if recorded else ''}
{',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''}
FROM public.projects AS s
{'LEFT JOIN recorded_p USING (project_id)' if recorded else ''}
{'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''}
{role_query if user_id is not None else ""}
WHERE s.tenant_id =%(tenant_id)s
@ -76,7 +85,6 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st
WHERE sessions.start_ts >= %(startDate)s AND sessions.start_ts <= %(endDate)s
GROUP BY project_id;""",
{"startDate": TimeUTC.now(delta_days=-3), "endDate": TimeUTC.now(delta_days=1)})
cur.execute(query=query)
status = cur.fetchall()
for r in rows:

View file

@ -6,6 +6,8 @@ from decouple import config
def get_by_session_id(session_id, project_id, start_ts, duration):
with ch_client.ClickHouseClient() as ch:
if duration is None or (type(duration) != 'int' and type(duration) != 'float') or duration < 0:
duration = 0
delta = config("events_ts_delta", cast=int, default=60 * 60) * 1000
ch_query = """\
SELECT

View file

@ -34,7 +34,8 @@ def get_by_tenant_id(tenant_id):
t.created_at,
'{license.EDITION}' AS edition,
t.version_number,
t.opt_out
t.opt_out,
t.tenant_key
FROM public.tenants AS t
WHERE t.tenant_id = %(tenantId)s AND t.deleted_at ISNULL
LIMIT 1;""",

View file

@ -4,11 +4,9 @@ import secrets
from decouple import config
from fastapi import BackgroundTasks
import schemas
import schemas_ee
from chalicelib.core import authorizers, metadata, projects, roles
from chalicelib.core import tenants, assist
from chalicelib.utils import dev, SAML2_helper
from chalicelib.utils import helper, email_helper
from chalicelib.utils import pg_client
from chalicelib.utils.TimeUTC import TimeUTC
@ -170,7 +168,11 @@ def update(tenant_id, user_id, changes):
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
users.role_id;""",
users.role_id,
(SELECT roles.name
FROM roles
WHERE roles.tenant_id=%(tenant_id)s
AND roles.role_id=users.role_id) AS role_name;""",
{"tenant_id": tenant_id, "user_id": user_id, **changes})
)
if len(sub_query_bauth) > 0:
@ -189,7 +191,11 @@ def update(tenant_id, user_id, changes):
(CASE WHEN users.role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
(CASE WHEN users.role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
(CASE WHEN users.role = 'member' THEN TRUE ELSE FALSE END) AS member,
users.role_id;""",
users.role_id,
(SELECT roles.name
FROM roles
WHERE roles.tenant_id=%(tenant_id)s
AND roles.role_id=users.role_id) AS role_name;""",
{"tenant_id": tenant_id, "user_id": user_id, **changes})
)
@ -649,7 +655,7 @@ def authenticate(email, password, for_change_password=False, for_plugin=False):
cur.execute(query)
r = cur.fetchone()
if r is None and SAML2_helper.is_saml2_available():
if r is None and helper.is_saml2_available():
query = cur.mogrify(
f"""SELECT 1
FROM public.users

View file

@ -1,4 +1,5 @@
from http import cookies
from os import environ
from urllib.parse import urlparse
from decouple import config
@ -125,3 +126,6 @@ def get_saml2_provider():
def get_landing_URL(jwt):
return config("SITE_URL") + config("sso_landing", default="/login?jwt=%s") % jwt
environ["hastSAML2"] = str(is_saml2_available())

View file

@ -70,7 +70,6 @@ rm -rf ./routers/subs/dashboard.py
rm -rf ./db_changes.sql
rm -rf ./Dockerfile.bundle
rm -rf ./entrypoint.bundle.sh
rm -rf ./entrypoint.sh
rm -rf ./chalicelib/core/heatmaps.py
rm -rf ./routers/subs/insights.py
rm -rf ./schemas.py

View file

@ -1,5 +1,5 @@
#!/bin/bash
bash env_vars.sh
#!/bin/sh
sh env_vars.sh
source .env.override
cd sourcemap-reader
nohup npm start &> /tmp/sourcemap-reader.log &

View file

@ -1,4 +1,4 @@
#!/bin/bash
bash env_vars.sh
#!/bin/sh
sh env_vars.sh
source .env.override
uvicorn app:app --host 0.0.0.0 --reload

View file

@ -1,4 +1,4 @@
#!/bin/bash
bash env_vars.sh
#!/bin/sh
sh env_vars.sh
source .env.override
python app_crons.py $ACTION

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
touch .env.override
if [[ -z "${ENV_CONFIG_OVERRIDE_PATH}" ]]; then

View file

@ -0,0 +1,18 @@
requests==2.28.1
urllib3==1.26.10
boto3==1.24.26
pyjwt==2.4.0
psycopg2-binary==2.9.3
elasticsearch==8.3.1
jira==3.3.0
fastapi==0.78.0
uvicorn[standard]==0.18.2
python-decouple==3.6
pydantic[email]==1.9.1
apscheduler==3.9.1
clickhouse-driver==0.2.4
python-multipart==0.0.5

View file

@ -0,0 +1,18 @@
requests==2.28.1
urllib3==1.26.10
boto3==1.24.26
pyjwt==2.4.0
psycopg2-binary==2.9.3
elasticsearch==8.3.1
jira==3.3.0
fastapi==0.78.0
uvicorn[standard]==0.18.2
python-decouple==3.6
pydantic[email]==1.9.1
apscheduler==3.9.1
clickhouse-driver==0.2.4
python-multipart==0.0.5

View file

@ -1,16 +1,19 @@
requests==2.28.0
urllib3==1.26.9
boto3==1.24.11
requests==2.28.1
urllib3==1.26.10
boto3==1.24.26
pyjwt==2.4.0
psycopg2-binary==2.9.3
elasticsearch==8.2.3
jira==3.2.0
clickhouse-driver==0.2.4
python3-saml==1.12.0
elasticsearch==8.3.1
jira==3.3.0
fastapi==0.78.0
python-multipart==0.0.5
uvicorn[standard]==0.17.6
uvicorn[standard]==0.18.2
python-decouple==3.6
pydantic[email]==1.9.1
apscheduler==3.9.1
apscheduler==3.9.1
clickhouse-driver==0.2.4
python3-saml==1.14.0
python-multipart==0.0.5

View file

@ -49,7 +49,7 @@ def main():
elif LEVEL == 'normal':
n = handle_normal_message(message)
session_id = codec.decode_key(msg.key)
session_id = decode_key(msg.key)
sessions[session_id] = handle_session(sessions[session_id], message)
if sessions[session_id]:
sessions[session_id].sessionid = session_id
@ -116,6 +116,15 @@ def attempt_batch_insert(batch):
except Exception as e:
print(repr(e))
def decode_key(b) -> int:
"""
Decode the message key (encoded with little endian)
"""
try:
decoded = int.from_bytes(b, "little", signed=False)
except Exception as e:
raise UnicodeDecodeError(f"Error while decoding message key (SessionID) from {b}\n{e}")
return decoded
if __name__ == '__main__':
main()

View file

@ -49,7 +49,7 @@ def main():
elif LEVEL == 'normal':
n = handle_normal_message(message)
session_id = codec.decode_key(msg.key)
session_id = decode_key(msg.key)
sessions[session_id] = handle_session(sessions[session_id], message)
if sessions[session_id]:
sessions[session_id].sessionid = session_id
@ -116,6 +116,15 @@ def attempt_batch_insert(batch):
except Exception as e:
print(repr(e))
def decode_key(b) -> int:
"""
Decode the message key (encoded with little endian)
"""
try:
decoded = int.from_bytes(b, "little", signed=False)
except Exception as e:
raise UnicodeDecodeError(f"Error while decoding message key (SessionID) from {b}\n{e}")
return decoded
if __name__ == '__main__':
main()

View file

@ -1,8 +1,5 @@
import io
from msgcodec.messages import *
class Codec:
"""
Implements encode/decode primitives
@ -63,608 +60,3 @@ class Codec:
return s.decode("utf-8", errors="replace").replace("\x00", "\uFFFD")
except UnicodeDecodeError:
return None
class MessageCodec(Codec):
def encode(self, m: Message) -> bytes:
...
def decode(self, b: bytes) -> Message:
reader = io.BytesIO(b)
message_id = self.read_message_id(reader)
if message_id == 0:
return Timestamp(
timestamp=self.read_uint(reader)
)
if message_id == 1:
return SessionStart(
timestamp=self.read_uint(reader),
project_id=self.read_uint(reader),
tracker_version=self.read_string(reader),
rev_id=self.read_string(reader),
user_uuid=self.read_string(reader),
user_agent=self.read_string(reader),
user_os=self.read_string(reader),
user_os_version=self.read_string(reader),
user_browser=self.read_string(reader),
user_browser_version=self.read_string(reader),
user_device=self.read_string(reader),
user_device_type=self.read_string(reader),
user_device_memory_size=self.read_uint(reader),
user_device_heap_size=self.read_uint(reader),
user_country=self.read_string(reader)
)
if message_id == 2:
return SessionDisconnect(
timestamp=self.read_uint(reader)
)
if message_id == 3:
return SessionEnd(
timestamp=self.read_uint(reader)
)
if message_id == 4:
return SetPageLocation(
url=self.read_string(reader),
referrer=self.read_string(reader),
navigation_start=self.read_uint(reader)
)
if message_id == 5:
return SetViewportSize(
width=self.read_uint(reader),
height=self.read_uint(reader)
)
if message_id == 6:
return SetViewportScroll(
x=self.read_int(reader),
y=self.read_int(reader)
)
if message_id == 7:
return CreateDocument()
if message_id == 8:
return CreateElementNode(
id=self.read_uint(reader),
parent_id=self.read_uint(reader),
index=self.read_uint(reader),
tag=self.read_string(reader),
svg=self.read_boolean(reader),
)
if message_id == 9:
return CreateTextNode(
id=self.read_uint(reader),
parent_id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 10:
return MoveNode(
id=self.read_uint(reader),
parent_id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 11:
return RemoveNode(
id=self.read_uint(reader)
)
if message_id == 12:
return SetNodeAttribute(
id=self.read_uint(reader),
name=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 13:
return RemoveNodeAttribute(
id=self.read_uint(reader),
name=self.read_string(reader)
)
if message_id == 14:
return SetNodeData(
id=self.read_uint(reader),
data=self.read_string(reader)
)
if message_id == 15:
return SetCSSData(
id=self.read_uint(reader),
data=self.read_string(reader)
)
if message_id == 16:
return SetNodeScroll(
id=self.read_uint(reader),
x=self.read_int(reader),
y=self.read_int(reader),
)
if message_id == 17:
return SetInputTarget(
id=self.read_uint(reader),
label=self.read_string(reader)
)
if message_id == 18:
return SetInputValue(
id=self.read_uint(reader),
value=self.read_string(reader),
mask=self.read_int(reader),
)
if message_id == 19:
return SetInputChecked(
id=self.read_uint(reader),
checked=self.read_boolean(reader)
)
if message_id == 20:
return MouseMove(
x=self.read_uint(reader),
y=self.read_uint(reader)
)
if message_id == 21:
return MouseClick(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader)
)
if message_id == 22:
return ConsoleLog(
level=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 23:
return PageLoadTiming(
request_start=self.read_uint(reader),
response_start=self.read_uint(reader),
response_end=self.read_uint(reader),
dom_content_loaded_event_start=self.read_uint(reader),
dom_content_loaded_event_end=self.read_uint(reader),
load_event_start=self.read_uint(reader),
load_event_end=self.read_uint(reader),
first_paint=self.read_uint(reader),
first_contentful_paint=self.read_uint(reader)
)
if message_id == 24:
return PageRenderTiming(
speed_index=self.read_uint(reader),
visually_complete=self.read_uint(reader),
time_to_interactive=self.read_uint(reader),
)
if message_id == 25:
return JSException(
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 26:
return RawErrorEvent(
timestamp=self.read_uint(reader),
source=self.read_string(reader),
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 27:
return RawCustomEvent(
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 28:
return UserID(
id=self.read_string(reader)
)
if message_id == 29:
return UserAnonymousID(
id=self.read_string(reader)
)
if message_id == 30:
return Metadata(
key=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 31:
return PageEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
url=self.read_string(reader),
referrer=self.read_string(reader),
loaded=self.read_boolean(reader),
request_start=self.read_uint(reader),
response_start=self.read_uint(reader),
response_end=self.read_uint(reader),
dom_content_loaded_event_start=self.read_uint(reader),
dom_content_loaded_event_end=self.read_uint(reader),
load_event_start=self.read_uint(reader),
load_event_end=self.read_uint(reader),
first_paint=self.read_uint(reader),
first_contentful_paint=self.read_uint(reader),
speed_index=self.read_uint(reader),
visually_complete=self.read_uint(reader),
time_to_interactive=self.read_uint(reader)
)
if message_id == 32:
return InputEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
value=self.read_string(reader),
value_masked=self.read_boolean(reader),
label=self.read_string(reader),
)
if message_id == 33:
return ClickEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader)
)
if message_id == 34:
return ErrorEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
source=self.read_string(reader),
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 35:
message_id = self.read_uint(reader)
ts = self.read_uint(reader)
if ts > 9999999999999:
ts = None
return ResourceEvent(
message_id=message_id,
timestamp=ts,
duration=self.read_uint(reader),
ttfb=self.read_uint(reader),
header_size=self.read_uint(reader),
encoded_body_size=self.read_uint(reader),
decoded_body_size=self.read_uint(reader),
url=self.read_string(reader),
type=self.read_string(reader),
success=self.read_boolean(reader),
method=self.read_string(reader),
status=self.read_uint(reader)
)
if message_id == 36:
return CustomEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 37:
return CSSInsertRule(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader)
)
if message_id == 38:
return CSSDeleteRule(
id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 39:
return Fetch(
method=self.read_string(reader),
url=self.read_string(reader),
request=self.read_string(reader),
response=self.read_string(reader),
status=self.read_uint(reader),
timestamp=self.read_uint(reader),
duration=self.read_uint(reader)
)
if message_id == 40:
return Profiler(
name=self.read_string(reader),
duration=self.read_uint(reader),
args=self.read_string(reader),
result=self.read_string(reader)
)
if message_id == 41:
return OTable(
key=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 42:
return StateAction(
type=self.read_string(reader)
)
if message_id == 43:
return StateActionEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
type=self.read_string(reader)
)
if message_id == 44:
return Redux(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader)
)
if message_id == 45:
return Vuex(
mutation=self.read_string(reader),
state=self.read_string(reader),
)
if message_id == 46:
return MobX(
type=self.read_string(reader),
payload=self.read_string(reader),
)
if message_id == 47:
return NgRx(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader)
)
if message_id == 48:
return GraphQL(
operation_kind=self.read_string(reader),
operation_name=self.read_string(reader),
variables=self.read_string(reader),
response=self.read_string(reader)
)
if message_id == 49:
return PerformanceTrack(
frames=self.read_int(reader),
ticks=self.read_int(reader),
total_js_heap_size=self.read_uint(reader),
used_js_heap_size=self.read_uint(reader)
)
if message_id == 50:
return GraphQLEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
name=self.read_string(reader)
)
if message_id == 52:
return DomDrop(
timestamp=self.read_uint(reader)
)
if message_id == 53:
return ResourceTiming(
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
ttfb=self.read_uint(reader),
header_size=self.read_uint(reader),
encoded_body_size=self.read_uint(reader),
decoded_body_size=self.read_uint(reader),
url=self.read_string(reader),
initiator=self.read_string(reader)
)
if message_id == 54:
return ConnectionInformation(
downlink=self.read_uint(reader),
type=self.read_string(reader)
)
if message_id == 55:
return SetPageVisibility(
hidden=self.read_boolean(reader)
)
if message_id == 56:
return PerformanceTrackAggr(
timestamp_start=self.read_uint(reader),
timestamp_end=self.read_uint(reader),
min_fps=self.read_uint(reader),
avg_fps=self.read_uint(reader),
max_fps=self.read_uint(reader),
min_cpu=self.read_uint(reader),
avg_cpu=self.read_uint(reader),
max_cpu=self.read_uint(reader),
min_total_js_heap_size=self.read_uint(reader),
avg_total_js_heap_size=self.read_uint(reader),
max_total_js_heap_size=self.read_uint(reader),
min_used_js_heap_size=self.read_uint(reader),
avg_used_js_heap_size=self.read_uint(reader),
max_used_js_heap_size=self.read_uint(reader)
)
if message_id == 59:
return LongTask(
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
context=self.read_uint(reader),
container_type=self.read_uint(reader),
container_src=self.read_string(reader),
container_id=self.read_string(reader),
container_name=self.read_string(reader)
)
if message_id == 60:
return SetNodeURLBasedAttribute(
id=self.read_uint(reader),
name=self.read_string(reader),
value=self.read_string(reader),
base_url=self.read_string(reader)
)
if message_id == 61:
return SetStyleData(
id=self.read_uint(reader),
data=self.read_string(reader),
base_url=self.read_string(reader)
)
if message_id == 62:
return IssueEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),
context=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 63:
return TechnicalInfo(
type=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 64:
return CustomIssue(
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 65:
return PageClose()
if message_id == 90:
return IOSSessionStart(
timestamp=self.read_uint(reader),
project_id=self.read_uint(reader),
tracker_version=self.read_string(reader),
rev_id=self.read_string(reader),
user_uuid=self.read_string(reader),
user_os=self.read_string(reader),
user_os_version=self.read_string(reader),
user_device=self.read_string(reader),
user_device_type=self.read_string(reader),
user_country=self.read_string(reader)
)
if message_id == 91:
return IOSSessionEnd(
timestamp=self.read_uint(reader)
)
if message_id == 92:
return IOSMetadata(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
key=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 94:
return IOSUserID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader)
)
if message_id == 95:
return IOSUserAnonymousID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader)
)
if message_id == 99:
return IOSScreenLeave(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
title=self.read_string(reader),
view_name=self.read_string(reader)
)
if message_id == 103:
return IOSLog(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
severity=self.read_string(reader),
content=self.read_string(reader)
)
if message_id == 104:
return IOSInternalError(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
content=self.read_string(reader)
)
if message_id == 110:
return IOSPerformanceAggregated(
timestamp_start=self.read_uint(reader),
timestamp_end=self.read_uint(reader),
min_fps=self.read_uint(reader),
avg_fps=self.read_uint(reader),
max_fps=self.read_uint(reader),
min_cpu=self.read_uint(reader),
avg_cpu=self.read_uint(reader),
max_cpu=self.read_uint(reader),
min_memory=self.read_uint(reader),
avg_memory=self.read_uint(reader),
max_memory=self.read_uint(reader),
min_battery=self.read_uint(reader),
avg_battery=self.read_uint(reader),
max_battery=self.read_uint(reader)
)
def read_message_id(self, reader: io.BytesIO) -> int:
"""
Read and return the first byte where the message id is encoded
"""
id_ = self.read_uint(reader)
return id_
@staticmethod
def check_message_id(b: bytes) -> int:
"""
todo: make it static and without reader. It's just the first byte
Read and return the first byte where the message id is encoded
"""
reader = io.BytesIO(b)
id_ = Codec.read_uint(reader)
return id_
@staticmethod
def decode_key(b) -> int:
"""
Decode the message key (encoded with little endian)
"""
try:
decoded = int.from_bytes(b, "little", signed=False)
except Exception as e:
raise UnicodeDecodeError(f"Error while decoding message key (SessionID) from {b}\n{e}")
return decoded

View file

@ -1,13 +1,20 @@
"""
Representations of Kafka messages
"""
from abc import ABC
# Auto-generated, do not edit
from abc import ABC
class Message(ABC):
pass
class BatchMeta(Message):
__id__ = 80
def __init__(self, page_no, first_index, timestamp):
self.page_no = page_no
self.first_index = first_index
self.timestamp = timestamp
class Timestamp(Message):
__id__ = 0
@ -18,10 +25,7 @@ class Timestamp(Message):
class SessionStart(Message):
__id__ = 1
def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid,
user_agent, user_os, user_os_version, user_browser, user_browser_version,
user_device, user_device_type, user_device_memory_size, user_device_heap_size,
user_country):
def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid, user_agent, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_device_memory_size, user_device_heap_size, user_country, user_id):
self.timestamp = timestamp
self.project_id = project_id
self.tracker_version = tracker_version
@ -37,6 +41,7 @@ class SessionStart(Message):
self.user_device_memory_size = user_device_memory_size
self.user_device_heap_size = user_device_heap_size
self.user_country = user_country
self.user_id = user_id
class SessionDisconnect(Message):
@ -48,7 +53,6 @@ class SessionDisconnect(Message):
class SessionEnd(Message):
__id__ = 3
__name__ = 'SessionEnd'
def __init__(self, timestamp):
self.timestamp = timestamp
@ -82,13 +86,16 @@ class SetViewportScroll(Message):
class CreateDocument(Message):
__id__ = 7
def __init__(self, ):
class CreateElementNode(Message):
__id__ = 8
def __init__(self, id, parent_id, index, tag, svg):
self.id = id
self.parent_id = parent_id,
self.parent_id = parent_id
self.index = index
self.tag = tag
self.svg = svg
@ -122,7 +129,7 @@ class RemoveNode(Message):
class SetNodeAttribute(Message):
__id__ = 12
def __init__(self, id, name: str, value: str):
def __init__(self, id, name, value):
self.id = id
self.name = name
self.value = value
@ -131,7 +138,7 @@ class SetNodeAttribute(Message):
class RemoveNodeAttribute(Message):
__id__ = 13
def __init__(self, id, name: str):
def __init__(self, id, name):
self.id = id
self.name = name
@ -139,7 +146,7 @@ class RemoveNodeAttribute(Message):
class SetNodeData(Message):
__id__ = 14
def __init__(self, id, data: str):
def __init__(self, id, data):
self.id = id
self.data = data
@ -147,7 +154,7 @@ class SetNodeData(Message):
class SetCSSData(Message):
__id__ = 15
def __init__(self, id, data: str):
def __init__(self, id, data):
self.id = id
self.data = data
@ -155,7 +162,7 @@ class SetCSSData(Message):
class SetNodeScroll(Message):
__id__ = 16
def __init__(self, id, x: int, y: int):
def __init__(self, id, x, y):
self.id = id
self.x = x
self.y = y
@ -164,7 +171,7 @@ class SetNodeScroll(Message):
class SetInputTarget(Message):
__id__ = 17
def __init__(self, id, label: str):
def __init__(self, id, label):
self.id = id
self.label = label
@ -172,7 +179,7 @@ class SetInputTarget(Message):
class SetInputValue(Message):
__id__ = 18
def __init__(self, id, value: str, mask: int):
def __init__(self, id, value, mask):
self.id = id
self.value = value
self.mask = mask
@ -181,7 +188,7 @@ class SetInputValue(Message):
class SetInputChecked(Message):
__id__ = 19
def __init__(self, id, checked: bool):
def __init__(self, id, checked):
self.id = id
self.checked = checked
@ -194,10 +201,10 @@ class MouseMove(Message):
self.y = y
class MouseClick(Message):
class MouseClickDepricated(Message):
__id__ = 21
def __init__(self, id, hesitation_time, label: str):
def __init__(self, id, hesitation_time, label):
self.id = id
self.hesitation_time = hesitation_time
self.label = label
@ -206,7 +213,7 @@ class MouseClick(Message):
class ConsoleLog(Message):
__id__ = 22
def __init__(self, level: str, value: str):
def __init__(self, level, value):
self.level = level
self.value = value
@ -214,9 +221,7 @@ class ConsoleLog(Message):
class PageLoadTiming(Message):
__id__ = 23
def __init__(self, request_start, response_start, response_end, dom_content_loaded_event_start,
dom_content_loaded_event_end, load_event_start, load_event_end,
first_paint, first_contentful_paint):
def __init__(self, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint):
self.request_start = request_start
self.response_start = response_start
self.response_end = response_end
@ -236,20 +241,20 @@ class PageRenderTiming(Message):
self.visually_complete = visually_complete
self.time_to_interactive = time_to_interactive
class JSException(Message):
__id__ = 25
def __init__(self, name: str, message: str, payload: str):
def __init__(self, name, message, payload):
self.name = name
self.message = message
self.payload = payload
class RawErrorEvent(Message):
class IntegrationEvent(Message):
__id__ = 26
def __init__(self, timestamp, source: str, name: str, message: str,
payload: str):
def __init__(self, timestamp, source, name, message, payload):
self.timestamp = timestamp
self.source = source
self.name = name
@ -260,7 +265,7 @@ class RawErrorEvent(Message):
class RawCustomEvent(Message):
__id__ = 27
def __init__(self, name: str, payload: str):
def __init__(self, name, payload):
self.name = name
self.payload = payload
@ -268,44 +273,29 @@ class RawCustomEvent(Message):
class UserID(Message):
__id__ = 28
def __init__(self, id: str):
def __init__(self, id):
self.id = id
class UserAnonymousID(Message):
__id__ = 29
def __init__(self, id: str):
def __init__(self, id):
self.id = id
class Metadata(Message):
__id__ = 30
def __init__(self, key: str, value: str):
def __init__(self, key, value):
self.key = key
self.value = value
class PerformanceTrack(Message):
__id__ = 49
def __init__(self, frames: int, ticks: int, total_js_heap_size,
used_js_heap_size):
self.frames = frames
self.ticks = ticks
self.total_js_heap_size = total_js_heap_size
self.used_js_heap_size = used_js_heap_size
class PageEvent(Message):
__id__ = 31
def __init__(self, message_id, timestamp, url: str, referrer: str,
loaded: bool, request_start, response_start, response_end,
dom_content_loaded_event_start, dom_content_loaded_event_end,
load_event_start, load_event_end, first_paint, first_contentful_paint,
speed_index, visually_complete, time_to_interactive):
def __init__(self, message_id, timestamp, url, referrer, loaded, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint, speed_index, visually_complete, time_to_interactive):
self.message_id = message_id
self.timestamp = timestamp
self.url = url
@ -328,7 +318,7 @@ class PageEvent(Message):
class InputEvent(Message):
__id__ = 32
def __init__(self, message_id, timestamp, value: str, value_masked: bool, label: str):
def __init__(self, message_id, timestamp, value, value_masked, label):
self.message_id = message_id
self.timestamp = timestamp
self.value = value
@ -339,18 +329,18 @@ class InputEvent(Message):
class ClickEvent(Message):
__id__ = 33
def __init__(self, message_id, timestamp, hesitation_time, label: str):
def __init__(self, message_id, timestamp, hesitation_time, label, selector):
self.message_id = message_id
self.timestamp = timestamp
self.hesitation_time = hesitation_time
self.label = label
self.selector = selector
class ErrorEvent(Message):
__id__ = 34
def __init__(self, message_id, timestamp, source: str, name: str, message: str,
payload: str):
def __init__(self, message_id, timestamp, source, name, message, payload):
self.message_id = message_id
self.timestamp = timestamp
self.source = source
@ -362,8 +352,7 @@ class ErrorEvent(Message):
class ResourceEvent(Message):
__id__ = 35
def __init__(self, message_id, timestamp, duration, ttfb, header_size, encoded_body_size,
decoded_body_size, url: str, type: str, success: bool, method: str, status):
def __init__(self, message_id, timestamp, duration, ttfb, header_size, encoded_body_size, decoded_body_size, url, type, success, method, status):
self.message_id = message_id
self.timestamp = timestamp
self.duration = duration
@ -381,7 +370,7 @@ class ResourceEvent(Message):
class CustomEvent(Message):
__id__ = 36
def __init__(self, message_id, timestamp, name: str, payload: str):
def __init__(self, message_id, timestamp, name, payload):
self.message_id = message_id
self.timestamp = timestamp
self.name = name
@ -391,7 +380,7 @@ class CustomEvent(Message):
class CSSInsertRule(Message):
__id__ = 37
def __init__(self, id, rule: str, index):
def __init__(self, id, rule, index):
self.id = id
self.rule = rule
self.index = index
@ -408,8 +397,7 @@ class CSSDeleteRule(Message):
class Fetch(Message):
__id__ = 39
def __init__(self, method: str, url: str, request: str, response: str, status,
timestamp, duration):
def __init__(self, method, url, request, response, status, timestamp, duration):
self.method = method
self.url = url
self.request = request
@ -422,7 +410,7 @@ class Fetch(Message):
class Profiler(Message):
__id__ = 40
def __init__(self, name: str, duration, args: str, result: str):
def __init__(self, name, duration, args, result):
self.name = name
self.duration = duration
self.args = args
@ -432,7 +420,7 @@ class Profiler(Message):
class OTable(Message):
__id__ = 41
def __init__(self, key: str, value: str):
def __init__(self, key, value):
self.key = key
self.value = value
@ -440,14 +428,14 @@ class OTable(Message):
class StateAction(Message):
__id__ = 42
def __init__(self, type: str):
def __init__(self, type):
self.type = type
class StateActionEvent(Message):
__id__ = 43
def __init__(self, message_id, timestamp, type: str):
def __init__(self, message_id, timestamp, type):
self.message_id = message_id
self.timestamp = timestamp
self.type = type
@ -456,7 +444,7 @@ class StateActionEvent(Message):
class Redux(Message):
__id__ = 44
def __init__(self, action: str, state: str, duration):
def __init__(self, action, state, duration):
self.action = action
self.state = state
self.duration = duration
@ -465,7 +453,7 @@ class Redux(Message):
class Vuex(Message):
__id__ = 45
def __init__(self, mutation: str, state: str):
def __init__(self, mutation, state):
self.mutation = mutation
self.state = state
@ -473,7 +461,7 @@ class Vuex(Message):
class MobX(Message):
__id__ = 46
def __init__(self, type: str, payload: str):
def __init__(self, type, payload):
self.type = type
self.payload = payload
@ -481,7 +469,7 @@ class MobX(Message):
class NgRx(Message):
__id__ = 47
def __init__(self, action: str, state: str, duration):
def __init__(self, action, state, duration):
self.action = action
self.state = state
self.duration = duration
@ -490,8 +478,7 @@ class NgRx(Message):
class GraphQL(Message):
__id__ = 48
def __init__(self, operation_kind: str, operation_name: str,
variables: str, response: str):
def __init__(self, operation_kind, operation_name, variables, response):
self.operation_kind = operation_kind
self.operation_name = operation_name
self.variables = variables
@ -501,8 +488,7 @@ class GraphQL(Message):
class PerformanceTrack(Message):
__id__ = 49
def __init__(self, frames: int, ticks: int,
total_js_heap_size, used_js_heap_size):
def __init__(self, frames, ticks, total_js_heap_size, used_js_heap_size):
self.frames = frames
self.ticks = ticks
self.total_js_heap_size = total_js_heap_size
@ -512,13 +498,30 @@ class PerformanceTrack(Message):
class GraphQLEvent(Message):
__id__ = 50
def __init__(self, message_id, timestamp, name: str):
def __init__(self, message_id, timestamp, operation_kind, operation_name, variables, response):
self.message_id = message_id
self.timestamp = timestamp
self.name = name
self.operation_kind = operation_kind
self.operation_name = operation_name
self.variables = variables
self.response = response
class DomDrop(Message):
class FetchEvent(Message):
__id__ = 51
def __init__(self, message_id, timestamp, method, url, request, response, status, duration):
self.message_id = message_id
self.timestamp = timestamp
self.method = method
self.url = url
self.request = request
self.response = response
self.status = status
self.duration = duration
class DOMDrop(Message):
__id__ = 52
def __init__(self, timestamp):
@ -528,8 +531,7 @@ class DomDrop(Message):
class ResourceTiming(Message):
__id__ = 53
def __init__(self, timestamp, duration, ttfb, header_size, encoded_body_size,
decoded_body_size, url, initiator):
def __init__(self, timestamp, duration, ttfb, header_size, encoded_body_size, decoded_body_size, url, initiator):
self.timestamp = timestamp
self.duration = duration
self.ttfb = ttfb
@ -543,7 +545,7 @@ class ResourceTiming(Message):
class ConnectionInformation(Message):
__id__ = 54
def __init__(self, downlink, type: str):
def __init__(self, downlink, type):
self.downlink = downlink
self.type = type
@ -551,19 +553,14 @@ class ConnectionInformation(Message):
class SetPageVisibility(Message):
__id__ = 55
def __init__(self, hidden: bool):
def __init__(self, hidden):
self.hidden = hidden
class PerformanceTrackAggr(Message):
__id__ = 56
def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps,
max_fps, min_cpu, avg_cpu, max_cpu,
min_total_js_heap_size, avg_total_js_heap_size,
max_total_js_heap_size, min_used_js_heap_size,
avg_used_js_heap_size, max_used_js_heap_size
):
def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_total_js_heap_size, avg_total_js_heap_size, max_total_js_heap_size, min_used_js_heap_size, avg_used_js_heap_size, max_used_js_heap_size):
self.timestamp_start = timestamp_start
self.timestamp_end = timestamp_end
self.min_fps = min_fps
@ -583,8 +580,7 @@ class PerformanceTrackAggr(Message):
class LongTask(Message):
__id__ = 59
def __init__(self, timestamp, duration, context, container_type, container_src: str,
container_id: str, container_name: str):
def __init__(self, timestamp, duration, context, container_type, container_src, container_id, container_name):
self.timestamp = timestamp
self.duration = duration
self.context = context
@ -594,20 +590,20 @@ class LongTask(Message):
self.container_name = container_name
class SetNodeURLBasedAttribute(Message):
class SetNodeAttributeURLBased(Message):
__id__ = 60
def __init__(self, id, name: str, value: str, base_url: str):
def __init__(self, id, name, value, base_url):
self.id = id
self.name = name
self.value = value
self.base_url = base_url
class SetStyleData(Message):
class SetCSSDataURLBased(Message):
__id__ = 61
def __init__(self, id, data: str, base_url: str):
def __init__(self, id, data, base_url):
self.id = id
self.data = data
self.base_url = base_url
@ -616,8 +612,7 @@ class SetStyleData(Message):
class IssueEvent(Message):
__id__ = 62
def __init__(self, message_id, timestamp, type: str, context_string: str,
context: str, payload: str):
def __init__(self, message_id, timestamp, type, context_string, context, payload):
self.message_id = message_id
self.timestamp = timestamp
self.type = type
@ -629,7 +624,7 @@ class IssueEvent(Message):
class TechnicalInfo(Message):
__id__ = 63
def __init__(self, type: str, value: str):
def __init__(self, type, value):
self.type = type
self.value = value
@ -637,7 +632,7 @@ class TechnicalInfo(Message):
class CustomIssue(Message):
__id__ = 64
def __init__(self, name: str, payload: str):
def __init__(self, name, payload):
self.name = name
self.payload = payload
@ -645,13 +640,58 @@ class CustomIssue(Message):
class PageClose(Message):
__id__ = 65
def __init__(self, ):
class AssetCache(Message):
__id__ = 66
def __init__(self, url):
self.url = url
class CSSInsertRuleURLBased(Message):
__id__ = 67
def __init__(self, id, rule, index, base_url):
self.id = id
self.rule = rule
self.index = index
self.base_url = base_url
class MouseClick(Message):
__id__ = 69
def __init__(self, id, hesitation_time, label, selector):
self.id = id
self.hesitation_time = hesitation_time
self.label = label
self.selector = selector
class CreateIFrameDocument(Message):
__id__ = 70
def __init__(self, frame_id, id):
self.frame_id = frame_id
self.id = id
class IOSBatchMeta(Message):
__id__ = 107
def __init__(self, timestamp, length, first_index):
self.timestamp = timestamp
self.length = length
self.first_index = first_index
class IOSSessionStart(Message):
__id__ = 90
def __init__(self, timestamp, project_id, tracker_version: str,
rev_id: str, user_uuid: str, user_os: str, user_os_version: str,
user_device: str, user_device_type: str, user_country: str):
def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country):
self.timestamp = timestamp
self.project_id = project_id
self.tracker_version = tracker_version
@ -674,17 +714,27 @@ class IOSSessionEnd(Message):
class IOSMetadata(Message):
__id__ = 92
def __init__(self, timestamp, length, key: str, value: str):
def __init__(self, timestamp, length, key, value):
self.timestamp = timestamp
self.length = length
self.key = key
self.value = value
class IOSCustomEvent(Message):
__id__ = 93
def __init__(self, timestamp, length, name, payload):
self.timestamp = timestamp
self.length = length
self.name = name
self.payload = payload
class IOSUserID(Message):
__id__ = 94
def __init__(self, timestamp, length, value: str):
def __init__(self, timestamp, length, value):
self.timestamp = timestamp
self.length = length
self.value = value
@ -693,26 +743,91 @@ class IOSUserID(Message):
class IOSUserAnonymousID(Message):
__id__ = 95
def __init__(self, timestamp, length, value: str):
def __init__(self, timestamp, length, value):
self.timestamp = timestamp
self.length = length
self.value = value
class IOSScreenLeave(Message):
__id__ = 99
class IOSScreenChanges(Message):
__id__ = 96
def __init__(self, timestamp, length, title: str, view_name: str):
def __init__(self, timestamp, length, x, y, width, height):
self.timestamp = timestamp
self.length = length
self.x = x
self.y = y
self.width = width
self.height = height
class IOSCrash(Message):
__id__ = 97
def __init__(self, timestamp, length, name, reason, stacktrace):
self.timestamp = timestamp
self.length = length
self.name = name
self.reason = reason
self.stacktrace = stacktrace
class IOSScreenEnter(Message):
__id__ = 98
def __init__(self, timestamp, length, title, view_name):
self.timestamp = timestamp
self.length = length
self.title = title
self.view_name = view_name
class IOSScreenLeave(Message):
__id__ = 99
def __init__(self, timestamp, length, title, view_name):
self.timestamp = timestamp
self.length = length
self.title = title
self.view_name = view_name
class IOSClickEvent(Message):
__id__ = 100
def __init__(self, timestamp, length, label, x, y):
self.timestamp = timestamp
self.length = length
self.label = label
self.x = x
self.y = y
class IOSInputEvent(Message):
__id__ = 101
def __init__(self, timestamp, length, value, value_masked, label):
self.timestamp = timestamp
self.length = length
self.value = value
self.value_masked = value_masked
self.label = label
class IOSPerformanceEvent(Message):
__id__ = 102
def __init__(self, timestamp, length, name, value):
self.timestamp = timestamp
self.length = length
self.name = name
self.value = value
class IOSLog(Message):
__id__ = 103
def __init__(self, timestamp, length, severity: str, content: str):
def __init__(self, timestamp, length, severity, content):
self.timestamp = timestamp
self.length = length
self.severity = severity
@ -722,20 +837,31 @@ class IOSLog(Message):
class IOSInternalError(Message):
__id__ = 104
def __init__(self, timestamp, length, content: str):
def __init__(self, timestamp, length, content):
self.timestamp = timestamp
self.length = length
self.content = content
class IOSNetworkCall(Message):
__id__ = 105
def __init__(self, timestamp, length, duration, headers, body, url, success, method, status):
self.timestamp = timestamp
self.length = length
self.duration = duration
self.headers = headers
self.body = body
self.url = url
self.success = success
self.method = method
self.status = status
class IOSPerformanceAggregated(Message):
__id__ = 110
def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps,
max_fps, min_cpu, avg_cpu, max_cpu,
min_memory, avg_memory, max_memory,
min_battery, avg_battery, max_battery
):
def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_memory, avg_memory, max_memory, min_battery, avg_battery, max_battery):
self.timestamp_start = timestamp_start
self.timestamp_end = timestamp_end
self.min_fps = min_fps
@ -750,3 +876,16 @@ class IOSPerformanceAggregated(Message):
self.min_battery = min_battery
self.avg_battery = avg_battery
self.max_battery = max_battery
class IOSIssueEvent(Message):
__id__ = 111
def __init__(self, timestamp, type, context_string, context, payload):
self.timestamp = timestamp
self.type = type
self.context_string = context_string
self.context = context
self.payload = payload

View file

@ -0,0 +1,728 @@
# Auto-generated, do not edit
from msgcodec.codec import Codec
from msgcodec.messages import *
class MessageCodec(Codec):
def read_message_id(self, reader: io.BytesIO) -> int:
"""
Read and return the first byte where the message id is encoded
"""
id_ = self.read_uint(reader)
return id_
def encode(self, m: Message) -> bytes:
...
def decode(self, b: bytes) -> Message:
reader = io.BytesIO(b)
message_id = self.read_message_id(reader)
if message_id == 80:
return BatchMeta(
page_no=self.read_uint(reader),
first_index=self.read_uint(reader),
timestamp=self.read_int(reader)
)
if message_id == 0:
return Timestamp(
timestamp=self.read_uint(reader)
)
if message_id == 1:
return SessionStart(
timestamp=self.read_uint(reader),
project_id=self.read_uint(reader),
tracker_version=self.read_string(reader),
rev_id=self.read_string(reader),
user_uuid=self.read_string(reader),
user_agent=self.read_string(reader),
user_os=self.read_string(reader),
user_os_version=self.read_string(reader),
user_browser=self.read_string(reader),
user_browser_version=self.read_string(reader),
user_device=self.read_string(reader),
user_device_type=self.read_string(reader),
user_device_memory_size=self.read_uint(reader),
user_device_heap_size=self.read_uint(reader),
user_country=self.read_string(reader),
user_id=self.read_string(reader)
)
if message_id == 2:
return SessionDisconnect(
timestamp=self.read_uint(reader)
)
if message_id == 3:
return SessionEnd(
timestamp=self.read_uint(reader)
)
if message_id == 4:
return SetPageLocation(
url=self.read_string(reader),
referrer=self.read_string(reader),
navigation_start=self.read_uint(reader)
)
if message_id == 5:
return SetViewportSize(
width=self.read_uint(reader),
height=self.read_uint(reader)
)
if message_id == 6:
return SetViewportScroll(
x=self.read_int(reader),
y=self.read_int(reader)
)
if message_id == 7:
return CreateDocument(
)
if message_id == 8:
return CreateElementNode(
id=self.read_uint(reader),
parent_id=self.read_uint(reader),
index=self.read_uint(reader),
tag=self.read_string(reader),
svg=self.read_boolean(reader)
)
if message_id == 9:
return CreateTextNode(
id=self.read_uint(reader),
parent_id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 10:
return MoveNode(
id=self.read_uint(reader),
parent_id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 11:
return RemoveNode(
id=self.read_uint(reader)
)
if message_id == 12:
return SetNodeAttribute(
id=self.read_uint(reader),
name=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 13:
return RemoveNodeAttribute(
id=self.read_uint(reader),
name=self.read_string(reader)
)
if message_id == 14:
return SetNodeData(
id=self.read_uint(reader),
data=self.read_string(reader)
)
if message_id == 15:
return SetCSSData(
id=self.read_uint(reader),
data=self.read_string(reader)
)
if message_id == 16:
return SetNodeScroll(
id=self.read_uint(reader),
x=self.read_int(reader),
y=self.read_int(reader)
)
if message_id == 17:
return SetInputTarget(
id=self.read_uint(reader),
label=self.read_string(reader)
)
if message_id == 18:
return SetInputValue(
id=self.read_uint(reader),
value=self.read_string(reader),
mask=self.read_int(reader)
)
if message_id == 19:
return SetInputChecked(
id=self.read_uint(reader),
checked=self.read_boolean(reader)
)
if message_id == 20:
return MouseMove(
x=self.read_uint(reader),
y=self.read_uint(reader)
)
if message_id == 21:
return MouseClickDepricated(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader)
)
if message_id == 22:
return ConsoleLog(
level=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 23:
return PageLoadTiming(
request_start=self.read_uint(reader),
response_start=self.read_uint(reader),
response_end=self.read_uint(reader),
dom_content_loaded_event_start=self.read_uint(reader),
dom_content_loaded_event_end=self.read_uint(reader),
load_event_start=self.read_uint(reader),
load_event_end=self.read_uint(reader),
first_paint=self.read_uint(reader),
first_contentful_paint=self.read_uint(reader)
)
if message_id == 24:
return PageRenderTiming(
speed_index=self.read_uint(reader),
visually_complete=self.read_uint(reader),
time_to_interactive=self.read_uint(reader)
)
if message_id == 25:
return JSException(
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 26:
return IntegrationEvent(
timestamp=self.read_uint(reader),
source=self.read_string(reader),
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 27:
return RawCustomEvent(
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 28:
return UserID(
id=self.read_string(reader)
)
if message_id == 29:
return UserAnonymousID(
id=self.read_string(reader)
)
if message_id == 30:
return Metadata(
key=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 31:
return PageEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
url=self.read_string(reader),
referrer=self.read_string(reader),
loaded=self.read_boolean(reader),
request_start=self.read_uint(reader),
response_start=self.read_uint(reader),
response_end=self.read_uint(reader),
dom_content_loaded_event_start=self.read_uint(reader),
dom_content_loaded_event_end=self.read_uint(reader),
load_event_start=self.read_uint(reader),
load_event_end=self.read_uint(reader),
first_paint=self.read_uint(reader),
first_contentful_paint=self.read_uint(reader),
speed_index=self.read_uint(reader),
visually_complete=self.read_uint(reader),
time_to_interactive=self.read_uint(reader)
)
if message_id == 32:
return InputEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
value=self.read_string(reader),
value_masked=self.read_boolean(reader),
label=self.read_string(reader)
)
if message_id == 33:
return ClickEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader),
selector=self.read_string(reader)
)
if message_id == 34:
return ErrorEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
source=self.read_string(reader),
name=self.read_string(reader),
message=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 35:
return ResourceEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
ttfb=self.read_uint(reader),
header_size=self.read_uint(reader),
encoded_body_size=self.read_uint(reader),
decoded_body_size=self.read_uint(reader),
url=self.read_string(reader),
type=self.read_string(reader),
success=self.read_boolean(reader),
method=self.read_string(reader),
status=self.read_uint(reader)
)
if message_id == 36:
return CustomEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 37:
return CSSInsertRule(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader)
)
if message_id == 38:
return CSSDeleteRule(
id=self.read_uint(reader),
index=self.read_uint(reader)
)
if message_id == 39:
return Fetch(
method=self.read_string(reader),
url=self.read_string(reader),
request=self.read_string(reader),
response=self.read_string(reader),
status=self.read_uint(reader),
timestamp=self.read_uint(reader),
duration=self.read_uint(reader)
)
if message_id == 40:
return Profiler(
name=self.read_string(reader),
duration=self.read_uint(reader),
args=self.read_string(reader),
result=self.read_string(reader)
)
if message_id == 41:
return OTable(
key=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 42:
return StateAction(
type=self.read_string(reader)
)
if message_id == 43:
return StateActionEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
type=self.read_string(reader)
)
if message_id == 44:
return Redux(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader)
)
if message_id == 45:
return Vuex(
mutation=self.read_string(reader),
state=self.read_string(reader)
)
if message_id == 46:
return MobX(
type=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 47:
return NgRx(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader)
)
if message_id == 48:
return GraphQL(
operation_kind=self.read_string(reader),
operation_name=self.read_string(reader),
variables=self.read_string(reader),
response=self.read_string(reader)
)
if message_id == 49:
return PerformanceTrack(
frames=self.read_int(reader),
ticks=self.read_int(reader),
total_js_heap_size=self.read_uint(reader),
used_js_heap_size=self.read_uint(reader)
)
if message_id == 50:
return GraphQLEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
operation_kind=self.read_string(reader),
operation_name=self.read_string(reader),
variables=self.read_string(reader),
response=self.read_string(reader)
)
if message_id == 51:
return FetchEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
method=self.read_string(reader),
url=self.read_string(reader),
request=self.read_string(reader),
response=self.read_string(reader),
status=self.read_uint(reader),
duration=self.read_uint(reader)
)
if message_id == 52:
return DOMDrop(
timestamp=self.read_uint(reader)
)
if message_id == 53:
return ResourceTiming(
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
ttfb=self.read_uint(reader),
header_size=self.read_uint(reader),
encoded_body_size=self.read_uint(reader),
decoded_body_size=self.read_uint(reader),
url=self.read_string(reader),
initiator=self.read_string(reader)
)
if message_id == 54:
return ConnectionInformation(
downlink=self.read_uint(reader),
type=self.read_string(reader)
)
if message_id == 55:
return SetPageVisibility(
hidden=self.read_boolean(reader)
)
if message_id == 56:
return PerformanceTrackAggr(
timestamp_start=self.read_uint(reader),
timestamp_end=self.read_uint(reader),
min_fps=self.read_uint(reader),
avg_fps=self.read_uint(reader),
max_fps=self.read_uint(reader),
min_cpu=self.read_uint(reader),
avg_cpu=self.read_uint(reader),
max_cpu=self.read_uint(reader),
min_total_js_heap_size=self.read_uint(reader),
avg_total_js_heap_size=self.read_uint(reader),
max_total_js_heap_size=self.read_uint(reader),
min_used_js_heap_size=self.read_uint(reader),
avg_used_js_heap_size=self.read_uint(reader),
max_used_js_heap_size=self.read_uint(reader)
)
if message_id == 59:
return LongTask(
timestamp=self.read_uint(reader),
duration=self.read_uint(reader),
context=self.read_uint(reader),
container_type=self.read_uint(reader),
container_src=self.read_string(reader),
container_id=self.read_string(reader),
container_name=self.read_string(reader)
)
if message_id == 60:
return SetNodeAttributeURLBased(
id=self.read_uint(reader),
name=self.read_string(reader),
value=self.read_string(reader),
base_url=self.read_string(reader)
)
if message_id == 61:
return SetCSSDataURLBased(
id=self.read_uint(reader),
data=self.read_string(reader),
base_url=self.read_string(reader)
)
if message_id == 62:
return IssueEvent(
message_id=self.read_uint(reader),
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),
context=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 63:
return TechnicalInfo(
type=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 64:
return CustomIssue(
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 65:
return PageClose(
)
if message_id == 66:
return AssetCache(
url=self.read_string(reader)
)
if message_id == 67:
return CSSInsertRuleURLBased(
id=self.read_uint(reader),
rule=self.read_string(reader),
index=self.read_uint(reader),
base_url=self.read_string(reader)
)
if message_id == 69:
return MouseClick(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader),
selector=self.read_string(reader)
)
if message_id == 70:
return CreateIFrameDocument(
frame_id=self.read_uint(reader),
id=self.read_uint(reader)
)
if message_id == 107:
return IOSBatchMeta(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
first_index=self.read_uint(reader)
)
if message_id == 90:
return IOSSessionStart(
timestamp=self.read_uint(reader),
project_id=self.read_uint(reader),
tracker_version=self.read_string(reader),
rev_id=self.read_string(reader),
user_uuid=self.read_string(reader),
user_os=self.read_string(reader),
user_os_version=self.read_string(reader),
user_device=self.read_string(reader),
user_device_type=self.read_string(reader),
user_country=self.read_string(reader)
)
if message_id == 91:
return IOSSessionEnd(
timestamp=self.read_uint(reader)
)
if message_id == 92:
return IOSMetadata(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
key=self.read_string(reader),
value=self.read_string(reader)
)
if message_id == 93:
return IOSCustomEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
payload=self.read_string(reader)
)
if message_id == 94:
return IOSUserID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader)
)
if message_id == 95:
return IOSUserAnonymousID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader)
)
if message_id == 96:
return IOSScreenChanges(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
x=self.read_uint(reader),
y=self.read_uint(reader),
width=self.read_uint(reader),
height=self.read_uint(reader)
)
if message_id == 97:
return IOSCrash(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
reason=self.read_string(reader),
stacktrace=self.read_string(reader)
)
if message_id == 98:
return IOSScreenEnter(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
title=self.read_string(reader),
view_name=self.read_string(reader)
)
if message_id == 99:
return IOSScreenLeave(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
title=self.read_string(reader),
view_name=self.read_string(reader)
)
if message_id == 100:
return IOSClickEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
label=self.read_string(reader),
x=self.read_uint(reader),
y=self.read_uint(reader)
)
if message_id == 101:
return IOSInputEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader),
value_masked=self.read_boolean(reader),
label=self.read_string(reader)
)
if message_id == 102:
return IOSPerformanceEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
value=self.read_uint(reader)
)
if message_id == 103:
return IOSLog(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
severity=self.read_string(reader),
content=self.read_string(reader)
)
if message_id == 104:
return IOSInternalError(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
content=self.read_string(reader)
)
if message_id == 105:
return IOSNetworkCall(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
duration=self.read_uint(reader),
headers=self.read_string(reader),
body=self.read_string(reader),
url=self.read_string(reader),
success=self.read_boolean(reader),
method=self.read_string(reader),
status=self.read_uint(reader)
)
if message_id == 110:
return IOSPerformanceAggregated(
timestamp_start=self.read_uint(reader),
timestamp_end=self.read_uint(reader),
min_fps=self.read_uint(reader),
avg_fps=self.read_uint(reader),
max_fps=self.read_uint(reader),
min_cpu=self.read_uint(reader),
avg_cpu=self.read_uint(reader),
max_cpu=self.read_uint(reader),
min_memory=self.read_uint(reader),
avg_memory=self.read_uint(reader),
max_memory=self.read_uint(reader),
min_battery=self.read_uint(reader),
avg_battery=self.read_uint(reader),
max_battery=self.read_uint(reader)
)
if message_id == 111:
return IOSIssueEvent(
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),
context=self.read_string(reader),
payload=self.read_string(reader)
)

View file

@ -56,6 +56,7 @@ ALTER TABLE IF EXISTS events.resources
PRIMARY KEY (session_id, message_id, timestamp);
COMMIT;
CREATE INDEX CONCURRENTLY IF NOT EXISTS projects_tenant_id_idx ON public.projects (tenant_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS projects_project_id_deleted_at_n_idx ON public.projects (project_id) WHERE deleted_at IS NULL;
ALTER TYPE metric_type ADD VALUE IF NOT EXISTS 'funnel';

View file

@ -257,6 +257,7 @@ $$
);
CREATE INDEX IF NOT EXISTS projects_tenant_id_idx ON public.projects (tenant_id);
CREATE INDEX IF NOT EXISTS projects_project_key_idx ON public.projects (project_key);
CREATE INDEX IF NOT EXISTS projects_project_id_deleted_at_n_idx ON public.projects (project_id) WHERE deleted_at IS NULL;
DROP TRIGGER IF EXISTS on_insert_or_update ON projects;

View file

@ -10,7 +10,7 @@ build.sh
servers/peerjs-server.js
servers/sourcemaps-handler.js
servers/sourcemaps-server.js
/Dockerfile
/utils/geoIP.js
/utils/HeapSnapshot.js
/utils/helper.js
/utils/assistHelper.js

20
ee/utilities/Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM node:18-alpine
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
RUN apk add --no-cache tini git libc6-compat && ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2
ARG envarg
ENV ENTERPRISE_BUILD=${envarg} \
MAXMINDDB_FILE=/home/openreplay/geoip.mmdb
WORKDIR /work
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN adduser -u 1001 openreplay -D
USER 1001
ADD --chown=1001 https://static.openreplay.com/geoip/GeoLite2-Country.mmdb $MAXMINDDB_FILE
ENTRYPOINT ["/sbin/tini", "--"]
CMD npm start

View file

@ -1,6 +1,7 @@
rm -rf ./utils/geoIP.js
rm -rf ./utils/HeapSnapshot.js
rm -rf ./utils/helper.js
rm -rf ./utils/assistHelper.js
rm -rf servers/peerjs-server.js
rm -rf servers/sourcemaps-handler.js

File diff suppressed because it is too large Load diff

View file

@ -70,7 +70,10 @@ const extractPayloadFromRequest = async function (req, res) {
filters.filter.userID = [req.getQuery("userId")];
}
if (!filters.query.value) {
let body = await getBodyFromUWSResponse(res);
let body = {};
if (req.getMethod() !== 'get') {
body = await getBodyFromUWSResponse(res);
}
filters = {
...filters,
"sort": {

View file

@ -14,5 +14,15 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
# Default step in docker build
FROM nginx:alpine
LABEL maintainer=Rajesh<rajesh@openreplay.com>
RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
COPY --from=builder /work/public /var/www/openreplay
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080
RUN chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx

View file

@ -31,7 +31,7 @@ const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
const ClientPure = lazy(() => import('Components/Client/Client'));
const AssistPure = lazy(() => import('Components/Assist'));
const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder'));
const BugFinderPure = lazy(() => import('Components/Overview'));
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails'));

View file

@ -1,153 +1,166 @@
import React, { useState, useEffect } from 'react'
import { Popup, Icon, IconButton } from 'UI'
import { connect } from 'react-redux'
import cn from 'classnames'
import React, { useState, useEffect } from 'react';
import { Popup, Icon, Button, IconButton } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import { toggleChatWindow } from 'Duck/sessions';
import { connectPlayer } from 'Player/store';
import ChatWindow from '../../ChatWindow';
import { callPeer, requestReleaseRemoteControl, toggleAnnotation } from 'Player'
import { callPeer, requestReleaseRemoteControl, toggleAnnotation } from 'Player';
import { CallingState, ConnectionStatus, RemoteControlStatus } 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 { confirm } from 'UI';
import stl from './AassistActions.module.css'
import stl from './AassistActions.module.css';
function onClose(stream) {
stream.getTracks().forEach(t=>t.stop());
stream.getTracks().forEach((t) => t.stop());
}
function onReject() {
toast.info(`Call was rejected.`);
toast.info(`Call was rejected.`);
}
function onError(e) {
toast.error(typeof e === 'string' ? e : e.message);
toast.error(typeof e === 'string' ? e : e.message);
}
interface Props {
userId: String,
toggleChatWindow: (state) => void,
calling: CallingState,
annotating: boolean,
peerConnectionStatus: ConnectionStatus,
remoteControlStatus: RemoteControlStatus,
hasPermission: boolean,
isEnterprise: boolean,
userId: String;
toggleChatWindow: (state) => void;
calling: CallingState;
annotating: boolean;
peerConnectionStatus: ConnectionStatus;
remoteControlStatus: RemoteControlStatus;
hasPermission: boolean;
isEnterprise: boolean;
}
function AssistActions({ toggleChatWindow, userId, calling, annotating, peerConnectionStatus, remoteControlStatus, hasPermission, isEnterprise }: Props) {
const [ incomeStream, setIncomeStream ] = useState<MediaStream | null>(null);
const [ localStream, setLocalStream ] = useState<LocalStream | null>(null);
const [ callObject, setCallObject ] = useState<{ end: ()=>void } | null >(null);
function AssistActions({
toggleChatWindow,
userId,
calling,
annotating,
peerConnectionStatus,
remoteControlStatus,
hasPermission,
isEnterprise,
}: Props) {
const [incomeStream, setIncomeStream] = useState<MediaStream | null>(null);
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
const [callObject, setCallObject] = useState<{ end: () => void } | null>(null);
useEffect(() => {
return callObject?.end()
}, [])
useEffect(() => {
return callObject?.end();
}, []);
useEffect(() => {
if (peerConnectionStatus == ConnectionStatus.Disconnected) {
toast.info(`Live session was closed.`);
}
}, [peerConnectionStatus])
function call() {
RequestLocalStream().then(lStream => {
setLocalStream(lStream);
setCallObject(callPeer(
lStream,
setIncomeStream,
lStream.stop.bind(lStream),
onReject,
onError
));
}).catch(onError)
}
const confirmCall = async () => {
if (await confirm({
header: 'Start Call',
confirmButton: 'Call',
confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`
})) {
call()
}
}
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting
const cannotCall = (peerConnectionStatus !== ConnectionStatus.Connected) || (isEnterprise && !hasPermission)
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled
return (
<div className="flex items-center">
{(onCall || remoteActive) && (
<>
<div
className={
cn(
'cursor-pointer p-2 flex items-center',
{[stl.disabled]: cannotCall}
)
}
onClick={ () => toggleAnnotation(!annotating) }
role="button"
>
<IconButton label={`Annotate`} icon={ annotating ? "pencil-stop" : "pencil"} primaryText redText={annotating} />
</div>
<div className={ stl.divider } />
</>
)}
<div
className={
cn(
'cursor-pointer p-2 flex items-center',
{[stl.disabled]: cannotCall}
)
useEffect(() => {
if (peerConnectionStatus == ConnectionStatus.Disconnected) {
toast.info(`Live session was closed.`);
}
onClick={ requestReleaseRemoteControl }
role="button"
>
<IconButton label={`Remote Control`} icon={ remoteActive ? "window-x" : "remote-control"} primaryText redText={remoteActive} />
</div>
<div className={ stl.divider } />
<Popup
content={ cannotCall ? "You dont have the permissions to perform this action." : `Call ${userId ? userId : 'User'}` }
>
<div
className={
cn(
'cursor-pointer p-2 flex items-center',
{[stl.disabled]: cannotCall}
)
}
onClick={ onCall ? callObject?.end : confirmCall}
role="button"
>
<IconButton size="small" primary={!onCall} red={onCall} label={onCall ? 'End' : 'Call'} icon="headset" />
</div>
</Popup>
}, [peerConnectionStatus]);
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
{ onCall && callObject && <ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} /> }
</div>
</div>
)
function call() {
RequestLocalStream()
.then((lStream) => {
setLocalStream(lStream);
setCallObject(callPeer(lStream, setIncomeStream, lStream.stop.bind(lStream), onReject, onError));
})
.catch(onError);
}
const confirmCall = async () => {
if (
await confirm({
header: 'Start Call',
confirmButton: 'Call',
confirmation: `Are you sure you want to call ${userId ? userId : 'User'}?`,
})
) {
call();
}
};
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting;
const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled;
return (
<div className="flex items-center">
{(onCall || remoteActive) && (
<>
<div
className={cn('cursor-pointer p-2 flex items-center', { [stl.disabled]: cannotCall })}
onClick={() => toggleAnnotation(!annotating)}
role="button"
>
<Button
icon={annotating ? 'pencil-stop' : 'pencil'}
variant={annotating ? 'text-red' : 'text-primary'}
style={{ height: '28px' }}
>
Annotate
</Button>
{/* <IconButton label={`Annotate`} icon={annotating ? 'pencil-stop' : 'pencil'} primaryText redText={annotating} /> */}
</div>
<div className={stl.divider} />
</>
)}
<div
className={cn('cursor-pointer p-2 flex items-center', { [stl.disabled]: cannotCall })}
onClick={requestReleaseRemoteControl}
role="button"
>
<Button
icon={remoteActive ? 'window-x' : 'remote-control'}
variant={remoteActive ? 'text-red' : 'text-primary'}
style={{ height: '28px' }}
>
Remote Control
</Button>
{/* <IconButton label={`Remote Control`} icon={remoteActive ? 'window-x' : 'remote-control'} primaryText redText={remoteActive} /> */}
</div>
<div className={stl.divider} />
<Popup content={cannotCall ? 'You dont have the permissions to perform this action.' : `Call ${userId ? userId : 'User'}`}>
<div
className={cn('cursor-pointer p-2 flex items-center', { [stl.disabled]: cannotCall })}
onClick={onCall ? callObject?.end : confirmCall}
role="button"
>
<Button icon="headset" variant={onCall ? 'text-red' : 'primary'} style={{ height: '28px' }}>
{onCall ? 'End' : 'Call'}
</Button>
{/* <IconButton size="small" primary={!onCall} red={onCall} label={onCall ? 'End' : 'Call'} icon="headset" /> */}
</div>
</Popup>
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
{onCall && callObject && (
<ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} />
)}
</div>
</div>
);
}
const con = connect(state => {
const permissions = state.getIn([ 'user', 'account', 'permissions' ]) || []
return {
hasPermission: permissions.includes('ASSIST_CALL'),
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
}
}, { toggleChatWindow })
const con = connect(
(state) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
return {
hasPermission: permissions.includes('ASSIST_CALL'),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
};
},
{ toggleChatWindow }
);
export default con(connectPlayer(state => ({
calling: state.calling,
annotating: state.annotating,
remoteControlStatus: state.remoteControl,
peerConnectionStatus: state.peerConnectionStatus,
}))(AssistActions))
export default con(
connectPlayer((state) => ({
calling: state.calling,
annotating: state.annotating,
remoteControlStatus: state.remoteControl,
peerConnectionStatus: state.peerConnectionStatus,
}))(AssistActions)
);

View file

@ -1,3 +1,4 @@
import { useModal } from 'App/components/Modal';
import React, { useEffect, useState } from 'react';
import { SlideModal, Avatar, TextEllipsis, Icon } from 'UI';
import SessionList from '../SessionList';
@ -10,6 +11,7 @@ interface Props {
const AssistTabs = (props: Props) => {
const [showMenu, setShowMenu] = useState(false)
const { showModal } = useModal();
return (
<div className="relative mr-4">
@ -18,19 +20,19 @@ const AssistTabs = (props: Props) => {
<>
<div
className={stl.btnLink}
onClick={() => setShowMenu(!showMenu)}
onClick={() => showModal(<SessionList userId={props.userId} />, {})}
>
Active Sessions
</div>
</>
)}
</div>
<SlideModal
{/* <SlideModal
title={ <div>{props.userId}'s <span className="color-gray-medium">Live Sessions</span> </div> }
isDisplayed={ showMenu }
content={ showMenu && <SessionList /> }
onClose={ () => setShowMenu(false) }
/>
/> */}
</div>
);
};

View file

@ -3,14 +3,17 @@ import { connect } from 'react-redux';
import { fetchLiveList } from 'Duck/sessions';
import { Loader, NoContent, Label } from 'UI';
import SessionItem from 'Shared/SessionItem';
import { useModal } from 'App/components/Modal';
interface Props {
loading: boolean;
list: any;
session: any;
userId: any;
fetchLiveList: (params: any) => void;
}
function SessionList(props: Props) {
const { hideModal } = useModal();
useEffect(() => {
const params: any = {};
if (props.session.userId) {
@ -20,25 +23,34 @@ function SessionList(props: Props) {
}, []);
return (
<Loader loading={props.loading}>
<NoContent show={!props.loading && props.list.size === 0} title="No live sessions.">
<div style={{ width: '50vw' }}>
<div className="border-r shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}>
<div className="p-4">
{props.list.map((session: any) => (
<div className="mb-6">
{session.pageTitle && session.pageTitle !== '' && (
<div className="flex items-center mb-2">
<Label size="small" className="p-1">
<span className="color-gray-medium">TAB</span>
</Label>
<span className="ml-2 font-medium">{session.pageTitle}</span>
</div>
)}
<SessionItem key={session.sessionId} session={session} showActive={session.active} />
</div>
))}
<div className="text-2xl">
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
</div>
</div>
</NoContent>
</Loader>
<Loader loading={props.loading}>
<NoContent show={!props.loading && props.list.size === 0} title="No live sessions.">
<div className="p-4">
{props.list.map((session: any) => (
<div className="mb-6">
{session.pageTitle && session.pageTitle !== '' && (
<div className="flex items-center mb-2">
<Label size="small" className="p-1">
<span className="color-gray-medium">TAB</span>
</Label>
<span className="ml-2 font-medium">{session.pageTitle}</span>
</div>
)}
<SessionItem onClick={() => hideModal()} key={session.sessionId} session={session} />
</div>
))}
</div>
</NoContent>
</Loader>
</div>
</div>
);
}

View file

@ -49,7 +49,6 @@ const RoleForm = (props: Props) => {
}
const writeOption = ({ name, value }: any) => {
console.log('name', name);
if (name === 'permissions') {
onChangePermissions(value)
} else if (name === 'projects') {

View file

@ -1,29 +1,28 @@
import React from 'react';
import { Popup, IconButton } from 'UI';
import { Popup, Button, IconButton } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const PERMISSION_WARNING = 'You dont have the permissions to perform this action.';
const LIMIT_WARNING = 'You have reached site limit.';
function AddProjectButton({ isAdmin = false, onClick }: any ) {
function AddProjectButton({ isAdmin = false, onClick }: any) {
const { userStore } = useStore();
const limtis = useObserver(() => userStore.limits);
const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0));
return (
<Popup
content={ `${ !isAdmin ? PERMISSION_WARNING : (!canAddProject ? LIMIT_WARNING : 'Add a Project') }` }
>
<IconButton
<Popup content={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}>
<Button rounded={true} variant="outline" icon="plus" onClick={onClick} disabled={!canAddProject || !isAdmin}></Button>
{/* <IconButton
id="add-button"
disabled={ !canAddProject || !isAdmin }
circle
icon="plus"
outline
onClick={ onClick }
/>
/> */}
</Popup>
);
}
export default AddProjectButton;
export default AddProjectButton;

View file

@ -12,7 +12,7 @@ import { confirm } from 'UI';
site: state.getIn([ 'site', 'instance' ]),
sites: state.getIn([ 'site', 'list' ]),
siteList: state.getIn([ 'site', 'list' ]),
loading: state.getIn([ 'site', 'save', 'loading' ]),
loading: state.getIn([ 'site', 'save', 'loading' ]) || state.getIn([ 'site', 'remove', 'loading' ]),
}), {
save,
remove,
@ -103,9 +103,11 @@ export default class NewSiteForm extends React.PureComponent {
>
{site.exists() ? 'Update' : 'Add'}
</Button>
<Button variant="text" type="button" plain onClick={() => this.remove(site)}>
<Icon name="trash" size="16" />
</Button>
{site.exists() && (
<Button variant="text" type="button" onClick={() => this.remove(site)}>
<Icon name="trash" size="16" />
</Button>
)}
</div>
{ this.state.existsError &&
<div className={ styles.errorMessage }>

View file

@ -10,12 +10,13 @@ import { connect } from 'react-redux';
import AddUserButton from './components/AddUserButton';
interface Props {
isOnboarding?: boolean;
account: any;
isEnterprise: boolean;
limits: any;
}
function UsersView(props: Props) {
const { account, limits, isEnterprise } = props;
const { account, limits, isEnterprise, isOnboarding = false } = props;
const { userStore, roleStore } = useStore();
const userCount = useObserver(() => userStore.list.length);
const roles = useObserver(() => roleStore.list);
@ -49,7 +50,7 @@ function UsersView(props: Props) {
<UserSearch />
</div>
</div>
<UserList isEnterprise={isEnterprise} />
<UserList isEnterprise={isEnterprise} isOnboarding={isOnboarding} />
</div>
);
}

View file

@ -9,10 +9,11 @@ import UserForm from '../UserForm';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
interface Props {
isOnboarding?: boolean;
isEnterprise?: boolean;
}
function UserList(props: Props) {
const { isEnterprise = false } = props;
const { isEnterprise = false, isOnboarding = false } = props;
const { userStore } = useStore();
const loading = useObserver(() => userStore.loading);
const users = useObserver(() => userStore.list);
@ -55,7 +56,7 @@ function UserList(props: Props) {
<div className="grid grid-cols-12 p-3 border-b font-medium">
<div className="col-span-5">Name</div>
<div className="col-span-3">Role</div>
<div className="col-span-2">Created On</div>
{!isOnboarding && <div className="col-span-2">Created On</div> }
<div className="col-span-2"></div>
</div>
@ -67,6 +68,7 @@ function UserList(props: Props) {
generateInvite={() => userStore.generateInviteCode(user.userId)}
copyInviteCode={() => userStore.copyInviteCode(user.userId)}
isEnterprise={isEnterprise}
isOnboarding={isOnboarding}
/>
</div>
))}

View file

@ -2,7 +2,7 @@
import React from 'react';
import { Icon, Popup } from 'UI';
import { checkForRecent } from 'App/date';
import cn from 'classnames';
const AdminPrivilegeLabel = ({ user }) => {
return (
@ -13,6 +13,7 @@ const AdminPrivilegeLabel = ({ user }) => {
)
}
interface Props {
isOnboarding?: boolean;
user: any;
editHandler?: any;
generateInvite?: any;
@ -26,6 +27,7 @@ function UserListItem(props: Props) {
generateInvite = () => {},
copyInviteCode = () => {},
isEnterprise = false,
isOnboarding = false
} = props;
return (
<div className="grid grid-cols-12 p-3 py-4 border-b items-center select-none hover:bg-active-blue group">
@ -41,11 +43,13 @@ function UserListItem(props: Props) {
</span>
)}
</div>
<div className="col-span-2">
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
</div>
{!isOnboarding && (
<div className="col-span-2">
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
</div>
)}
<div className="col-span-2 justify-self-end invisible group-hover:visible">
<div className={cn("justify-self-end invisible group-hover:visible", { 'col-span-2' : !isOnboarding, 'col-span-4' : isOnboarding})}>
<div className="grid grid-cols-2 gap-3 items-center justify-end">
<div>
{!user.isJoined && user.invitationLink && !user.isExpiredInvite && (

View file

@ -65,6 +65,9 @@ function DashboardView(props: Props) {
trimQuery();
}
}, []);
useEffect(() => {
dashboardStore.selectDefaultDashboard();
}, [siteId])
const onAddWidgets = () => {
dashboardStore.initDashboard(dashboard);

View file

@ -51,6 +51,7 @@ const Header = (props) => {
Promise.all([
userStore.fetchLimits(),
notificationStore.fetchNotificationsCount(),
props.fetchMetadata(),
]).then(() => {
userStore.updateKey('initialDataFetched', true);
});
@ -60,7 +61,6 @@ const Header = (props) => {
useEffect(() => {
activeSite = sites.find(s => s.id == siteId);
props.initSite(activeSite);
props.fetchMetadata();
}, [siteId])
return (

View file

@ -11,6 +11,7 @@ import styles from './siteDropdown.module.css';
import cn from 'classnames';
import NewSiteForm from '../Client/Sites/NewSiteForm';
import { clearSearch } from 'Duck/search';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
import { withStore } from 'App/mstore'
import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG';
@ -27,6 +28,7 @@ import NewProjectButton from './NewProjectButton';
pushNewSite,
init,
clearSearch,
clearSearchLive,
fetchIntegrationVariables,
})
export default class SiteDropdown extends React.PureComponent {
@ -45,8 +47,9 @@ export default class SiteDropdown extends React.PureComponent {
const { mstore, location } = this.props
this.props.setSiteId(siteId);
this.props.clearSearch(location.pathname.includes('/sessions'));
this.props.fetchIntegrationVariables();
this.props.clearSearch(location.pathname.includes('/sessions'));
this.props.clearSearchLive();
mstore.initClient();
}

View file

@ -1,5 +1,5 @@
import UsersView from 'App/components/Client/Users/UsersView'
import React from 'react'
import ManageUsers from '../../../Client/ManageUsers'
export default function ManageUsersTab() {
return (
@ -9,9 +9,8 @@ export default function ManageUsersTab() {
<span>👨💻</span>
<div className="ml-3">Invite Collaborators</div>
</h1>
<ManageUsers hideHeader />
<UsersView isOnboarding={true} />
</div>
<div className="w-4/12 py-6">
<div className="p-5 bg-gray-lightest mb-4 rounded">

View file

@ -0,0 +1,28 @@
import React from 'react';
import withPageTitle from 'HOCs/withPageTitle';
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import MainSearchBar from 'Shared/MainSearchBar';
import SessionSearch from 'Shared/SessionSearch';
import SessionListContainer from 'Shared/SessionListContainer/SessionListContainer';
function Overview() {
return (
<div className="page-margin container-90 flex relative">
<div className="flex-1 flex">
<div className={'w-full mx-auto'} style={{ maxWidth: '1300px' }}>
<NoSessionsMessage />
<div className="mb-5">
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<SessionListContainer />
</div>
</div>
</div>
</div>
);
}
export default withPageTitle('Sessions - OpenReplay')(Overview);

View file

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

View file

@ -1,7 +0,0 @@
.wrapper {
background-color: rgba(255, 255, 255, 1);
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
padding: 5px;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.5);
}

View file

@ -1,35 +0,0 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import { connectPlayer, toggleEvents, scale } from 'Player';
import cn from 'classnames'
import stl from './EventsToggleButton.module.css'
function EventsToggleButton({ showEvents, toggleEvents }: any) {
const toggle = () => {
toggleEvents()
scale()
}
return (
<Popup
content={ showEvents ? 'Hide Events' : 'Show Events' }
size="tiny"
inverted
position="bottom right"
>
<button
className={cn("absolute right-0 z-50", stl.wrapper)}
onClick={toggle}
>
<Icon
name={ showEvents ? 'chevron-double-right' : 'chevron-double-left' }
size="12"
/>
</button>
</Popup>
)
}
export default connectPlayer(state => ({
showEvents: !state.showEvents
}), { toggleEvents })(EventsToggleButton)

View file

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

View file

@ -4,17 +4,17 @@ import { connect } from 'react-redux';
import usePageTitle from 'App/hooks/usePageTitle';
import { fetch as fetchSession } from 'Duck/sessions';
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { Link, NoContent, Loader } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
import { Loader } from 'UI';
// import { sessions as sessionsRoute } from 'App/routes';
import withPermissions from 'HOCs/withPermissions'
import LivePlayer from './LivePlayer';
const SESSIONS_ROUTE = sessionsRoute();
// const SESSIONS_ROUTE = sessionsRoute();
function LiveSession({
sessionId,
loading,
hasErrors,
// hasErrors,
session,
fetchSession,
fetchSlackList,
@ -51,7 +51,7 @@ export default withPermissions(['ASSIST_LIVE'], '', true)(connect((state, props)
return {
sessionId,
loading: state.getIn([ 'sessions', 'loading' ]),
hasErrors: !!state.getIn([ 'sessions', 'errors' ]),
// hasErrors: !!state.getIn([ 'sessions', 'errors' ]),
session: state.getIn([ 'sessions', 'current' ]),
hasSessionsPath: hasSessiosPath && !isAssist,
};

View file

@ -38,7 +38,7 @@ function Session({
subtext={
<span>
{'Please check your data retention plan, or try '}
<Link to={ SESSIONS_ROUTE }>{'another one'}</Link>
<Link to={ SESSIONS_ROUTE } className="link">{'another one'}</Link>
</span>
}
>

View file

@ -4,85 +4,82 @@ import cn from 'classnames';
import stl from './autoscroll.module.css';
export default class Autoscroll extends React.PureComponent {
static defaultProps = {
bottomOffset: 10,
}
state = {
autoScroll: true,
}
static defaultProps = {
bottomOffset: 10,
};
state = {
autoScroll: true,
};
componentDidMount() {
if (!this.scrollableElement) return; // is necessary ?
this.scrollableElement.addEventListener('scroll', this.scrollHandler);
this.scrollableElement.scrollTop = this.scrollableElement.scrollHeight;
}
componentDidUpdate() {
if (!this.scrollableElement) return; // is necessary ?
if (this.state.autoScroll) {
this.scrollableElement.scrollTop = this.scrollableElement.scrollHeight;
componentDidMount() {
if (!this.scrollableElement) return; // is necessary ?
this.scrollableElement.addEventListener('scroll', this.scrollHandler);
this.scrollableElement.scrollTop = this.scrollableElement.scrollHeight;
}
}
scrollHandler = (e) => {
if (!this.scrollableElement) return;
this.setState({
autoScroll: this.scrollableElement.scrollHeight
- this.scrollableElement.clientHeight
- this.scrollableElement.scrollTop < this.props.bottomOffset,
});
}
onPrevClick = () => {
if (!this.scrollableElement) return;
const scEl = this.scrollableElement;
let prevItem;
for (let i = scEl.children.length - 1; i >= 0; i--) {
const child = scEl.children[ i ];
const isScrollable = child.getAttribute("data-scroll-item") === "true";
if (isScrollable && child.offsetTop < scEl.scrollTop) {
prevItem = child;
break;
}
}
if (!prevItem) return;
scEl.scrollTop = prevItem.offsetTop;
}
onNextClick = () => {
if (!this.scrollableElement) return;
const scEl = this.scrollableElement;
let nextItem;
for (let i = 0; i < scEl.children.length; i++) {
const child = scEl.children[ i ];
const isScrollable = child.getAttribute("data-scroll-item") === "true";
if (isScrollable && child.offsetTop > scEl.scrollTop + 20) { // ?
nextItem = child;
break;
}
}
if (!nextItem) return;
scEl.scrollTop = nextItem.offsetTop;
}
render() {
const { className, navigation=false, children, ...props } = this.props;
return (
<div className={ cn("relative w-full h-full", stl.wrapper) } >
<div
{ ...props }
className={ cn("relative scroll-y h-full", className) }
ref={ ref => this.scrollableElement = ref }
>
{ children }
</div>
{ navigation &&
<div className={ stl.navButtons } >
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
</div>
componentDidUpdate() {
if (!this.scrollableElement) return; // is necessary ?
if (this.state.autoScroll) {
this.scrollableElement.scrollTop = this.scrollableElement.scrollHeight;
}
</div>
);
}
}
}
scrollHandler = (e) => {
if (!this.scrollableElement) return;
this.setState({
autoScroll:
this.scrollableElement.scrollHeight - this.scrollableElement.clientHeight - this.scrollableElement.scrollTop <
this.props.bottomOffset,
});
};
onPrevClick = () => {
if (!this.scrollableElement) return;
const scEl = this.scrollableElement;
let prevItem;
for (let i = scEl.children.length - 1; i >= 0; i--) {
const child = scEl.children[i];
const isScrollable = child.getAttribute('data-scroll-item') === 'true';
if (isScrollable && child.offsetTop < scEl.scrollTop) {
prevItem = child;
break;
}
}
if (!prevItem) return;
scEl.scrollTop = prevItem.offsetTop;
};
onNextClick = () => {
if (!this.scrollableElement) return;
const scEl = this.scrollableElement;
let nextItem;
for (let i = 0; i < scEl.children.length; i++) {
const child = scEl.children[i];
const isScrollable = child.getAttribute('data-scroll-item') === 'true';
if (isScrollable && child.offsetTop > scEl.scrollTop + 20) {
// ?
nextItem = child;
break;
}
}
if (!nextItem) return;
scEl.scrollTop = nextItem.offsetTop;
};
render() {
const { className, navigation = false, children, ...props } = this.props;
return (
<div className={cn('relative w-full h-full', stl.wrapper)}>
<div {...props} className={cn('relative scroll-y h-full', className)} ref={(ref) => (this.scrollableElement = ref)}>
{children}
</div>
{navigation && (
<div className={stl.navButtons}>
<IconButton size="small" icon="chevron-up" onClick={this.onPrevClick} />
<IconButton size="small" icon="chevron-down" onClick={this.onNextClick} className="mt-5" />
</div>
)}
</div>
);
}
}

View file

@ -4,14 +4,15 @@ import ConsoleContent from './ConsoleContent';
@connectPlayer(state => ({
logs: state.logList,
time: state.time,
// time: state.time,
livePlay: state.livePlay,
listNow: state.logListNow,
}))
export default class Console extends React.PureComponent {
render() {
const { logs, time } = this.props;
const { logs, time, listNow } = this.props;
return (
<ConsoleContent jump={!this.props.livePlay && jump} logs={logs} time={time} />
<ConsoleContent jump={!this.props.livePlay && jump} logs={logs} lastIndex={listNow.length - 1} />
);
}
}

View file

@ -3,126 +3,113 @@ import cn from 'classnames';
import { getRE } from 'App/utils';
import { Icon, NoContent, Tabs, Input } from 'UI';
import { jump } from 'Player';
import { LEVEL } from 'Types/session/log';
import { LEVEL } from 'Types/session/log';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import stl from './console.module.css';
const ALL = 'ALL';
const INFO = 'INFO';
const WARNINGS = 'WARNINGS';
const ERRORS = 'ERRORS';
const LEVEL_TAB = {
[ LEVEL.INFO ]: INFO,
[ LEVEL.LOG ]: INFO,
[ LEVEL.WARNING ]: WARNINGS,
[ LEVEL.ERROR ]: ERRORS,
[ LEVEL.EXCEPTION ]: ERRORS,
[LEVEL.INFO]: INFO,
[LEVEL.LOG]: INFO,
[LEVEL.WARNING]: WARNINGS,
[LEVEL.ERROR]: ERRORS,
[LEVEL.EXCEPTION]: ERRORS,
};
const TABS = [ ALL, ERRORS, WARNINGS, INFO, ].map(tab => ({ text: tab, key: tab }));
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
// eslint-disable-next-line complexity
const getIconProps = (level) => {
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
return {
name: 'console/error',
color: 'red',
};
}
return null;
switch (level) {
case LEVEL.INFO:
case LEVEL.LOG:
return {
name: 'console/info',
color: 'blue2',
};
case LEVEL.WARN:
case LEVEL.WARNING:
return {
name: 'console/warning',
color: 'red2',
};
case LEVEL.ERROR:
return {
name: 'console/error',
color: 'red',
};
}
return null;
};
function renderWithNL(s = '') {
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={ cn({ "ml-20": i !== 0 }) }>{ line }</div>)
if (typeof s !== 'string') return '';
return s.split('\n').map((line, i) => <div className={cn({ 'ml-20': i !== 0 })}>{line}</div>);
}
export default class ConsoleContent extends React.PureComponent {
state = {
filter: '',
activeTab: ALL,
}
onTabClick = activeTab => this.setState({ activeTab })
onFilterChange = (e, { value }) => this.setState({ filter: value })
state = {
filter: '',
activeTab: ALL,
};
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = ({ target: { value } }) => this.setState({ filter: value });
render() {
const { logs, isResult, additionalHeight, time } = this.props;
const { filter, activeTab, currentError } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = logs.filter(({ level, value }) => activeTab === ALL
? filterRE.test(value)
: (filterRE.test(value) && LEVEL_TAB[ level ] === activeTab)
);
render() {
const { logs, isResult, additionalHeight, lastIndex } = this.props;
const { filter, activeTab } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = logs.filter(({ level, value }) =>
activeTab === ALL ? filterRE.test(value) : filterRE.test(value) && LEVEL_TAB[level] === activeTab
);
const lastIndex = filtered.filter(item => item.time <= time).length - 1;
return (
<>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock.Header showClose={!isResult}>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs
tabs={ TABS }
active={ activeTab }
onClick={ this.onTabClick }
border={ false }
/>
</div>
<Input
className="input-small"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
size="small"
show={ filtered.length === 0 }
>
<Autoscroll>
{ filtered.map((l, index) => (
<div
key={ l.key }
className={ cn(stl.line, {
"info": !l.isYellow() && !l.isRed(),
"warn": l.isYellow(),
"error": l.isRed(),
"cursor-pointer": !isResult,
[stl.activeRow] : lastIndex === index
}) }
data-scroll-item={ l.isRed() }
onClick={ () => !isResult && jump(l.time) }
>
<Icon size="14" className={ stl.icon } { ...getIconProps(l.level) } />
<div className={ stl.message }>{ renderWithNL(l.value) }</div>
</div>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
return (
<>
<BottomBlock style={{ height: 300 + additionalHeight + 'px' }}>
<BottomBlock.Header showClose={!isResult}>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={this.onTabClick} border={false} />
</div>
<Input
className="input-small"
placeholder="Filter by keyword"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" show={filtered.length === 0}>
<Autoscroll>
{filtered.map((l, index) => (
<div
key={l.key}
className={cn(stl.line, {
info: !l.isYellow() && !l.isRed(),
warn: l.isYellow(),
error: l.isRed(),
'cursor-pointer': !isResult,
[stl.activeRow]: lastIndex === index,
})}
data-scroll-item={l.isRed()}
onClick={() => !isResult && jump(l.time)}
>
<Icon size="14" className={stl.icon} {...getIconProps(l.level)} />
<div className={stl.message}>{renderWithNL(l.value)}</div>
</div>
))}
</Autoscroll>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</>
);
}
}

View file

@ -71,6 +71,7 @@
transition: all 0.2s;
box-shadow: 0px 2px 10px 0 $gray-light;
border: 1px solid $active-blue-border;
/* background-color: red; */
}
&.red {

View file

@ -10,170 +10,154 @@ import { renderName, renderDuration } from '../Network';
import { connect } from 'react-redux';
import { setTimelinePointer } from 'Duck/sessions';
@connectPlayer(state => ({
list: state.fetchList,
livePlay: state.livePlay,
@connectPlayer((state) => ({
list: state.fetchList,
listNow: state.fetchListNow,
livePlay: state.livePlay,
}))
@connect(state => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}), { setTimelinePointer })
@connect(
(state) => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}),
{ setTimelinePointer }
)
export default class Fetch extends React.PureComponent {
state = {
filter: "",
filteredList: this.props.list,
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
}
onFilterChange = (e, { value }) => {
const { list } = this.props;
const filterRE = getRE(value, 'i');
const filtered = list
.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
}
state = {
filter: '',
filteredList: this.props.list,
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
};
setCurrent = (item, index) => {
if (!this.props.livePlay) {
pause();
jump(item.time)
onFilterChange = ({ target: { value } }) => {
const { list } = this.props;
const filterRE = getRE(value, 'i');
const filtered = list.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
};
setCurrent = (item, index) => {
if (!this.props.livePlay) {
pause();
jump(item.time);
}
this.setState({ current: item, currentIndex: index });
};
onRowClick = (item, index) => {
if (!this.props.livePlay) {
pause();
}
this.setState({ current: item, currentIndex: index, showFetchDetails: true });
this.props.setTimelinePointer(null);
};
closeModal = () => this.setState({ current: null, showFetchDetails: false });
nextClickHander = () => {
// const { list } = this.props;
const { currentIndex, filteredList } = this.state;
if (currentIndex === filteredList.length - 1) return;
const newIndex = currentIndex + 1;
this.setCurrent(filteredList[newIndex], newIndex);
this.setState({ showFetchDetails: true });
};
prevClickHander = () => {
// const { list } = this.props;
const { currentIndex, filteredList } = this.state;
if (currentIndex === 0) return;
const newIndex = currentIndex - 1;
this.setCurrent(filteredList[newIndex], newIndex);
this.setState({ showFetchDetails: true });
};
render() {
const { listNow } = this.props;
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
return (
<React.Fragment>
<SlideModal
right
size="middle"
title={
<div className="flex justify-between">
<h1>Fetch Request</h1>
<div className="flex items-center">
<div className="flex items-center">
<span className="mr-2 color-gray-medium uppercase text-base">Status</span>
<Label data-red={current && current.status >= 400} data-green={current && current.status < 400}>
<div className="uppercase w-16 justify-center code-font text-lg">{current && current.status}</div>
</Label>
</div>
<CloseButton onClick={this.closeModal} size="18" className="ml-2" />
</div>
</div>
}
isDisplayed={current != null && showFetchDetails}
content={
current &&
showFetchDetails && (
<FetchDetails
resource={current}
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
)
}
onClose={this.closeModal}
/>
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">Fetch</h4>
<div className="flex items-center">
<Input
className="input-small"
placeholder="Filter"
icon="search"
iconPosition="left"
name="filter"
onChange={this.onFilterChange}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent size="small" show={filteredList.length === 0}>
<TimeTable rows={filteredList} onRowClick={this.onRowClick} hoverable navigation activeIndex={listNow.length - 1}>
{[
{
label: 'Status',
dataKey: 'status',
width: 70,
},
{
label: 'Method',
dataKey: 'method',
width: 60,
},
{
label: 'Name',
width: 240,
render: renderName,
},
{
label: 'Time',
width: 80,
render: renderDuration,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
this.setState({ current: item, currentIndex: index });
}
onRowClick = (item, index) => {
if (!this.props.livePlay) {
pause();
}
this.setState({ current: item, currentIndex: index, showFetchDetails: true });
this.props.setTimelinePointer(null);
}
closeModal = () => this.setState({ current: null, showFetchDetails: false });
nextClickHander = () => {
// const { list } = this.props;
const { currentIndex, filteredList } = this.state;
if (currentIndex === filteredList.length - 1) return;
const newIndex = currentIndex + 1;
this.setCurrent(filteredList[newIndex], newIndex);
this.setState({ showFetchDetails: true });
}
prevClickHander = () => {
// const { list } = this.props;
const { currentIndex, filteredList } = this.state;
if (currentIndex === 0) return;
const newIndex = currentIndex - 1;
this.setCurrent(filteredList[newIndex], newIndex);
this.setState({ showFetchDetails: true });
}
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
let activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
activeItem = activeItem || filteredList[filteredList.length - 1];
return {
current: activeItem,
currentIndex: filteredList.indexOf(activeItem),
};
}
}
render() {
// const { list } = this.props;
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
return (
<React.Fragment>
<SlideModal
right
size="middle"
title={
<div className="flex justify-between">
<h1>Fetch Request</h1>
<div className="flex items-center">
<div className="flex items-center">
<span className="mr-2 color-gray-medium uppercase text-base">Status</span>
<Label
data-red={current && current.status >= 400}
data-green={current && current.status < 400}
>
<div className="uppercase w-16 justify-center code-font text-lg">{current && current.status}</div>
</Label>
</div>
<CloseButton onClick={ this.closeModal } size="18" className="ml-2" />
</div>
</div>
}
isDisplayed={ current != null && showFetchDetails }
content={ current && showFetchDetails &&
<FetchDetails
resource={ current }
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filteredList.length - 1}
/>
}
onClose={ this.closeModal }
/>
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">Fetch</h4>
<div className="flex items-center">
<Input
className="input-small"
placeholder="Filter"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
size="small"
show={ filteredList.length === 0}
>
<TimeTable
rows={ filteredList }
onRowClick={ this.onRowClick }
hoverable
navigation
activeIndex={currentIndex}
>
{[
{
label: "Status",
dataKey: 'status',
width: 70,
}, {
label: "Method",
dataKey: "method",
width: 60,
}, {
label: "Name",
width: 180,
render: renderName,
},
{
label: "Time",
width: 80,
render: renderDuration,
}
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
}
}

View file

@ -38,9 +38,7 @@ export default function Inspector () {
const onKeyPress = e => {
if (e.key === 'Backspace' || e.key === 'Delete') {
const elem = selectedElementRef.current;
console.log(elem)
if (elem !== null && elem.parentElement !== null) {
console.log('a?')
elem.parentElement.removeChild(elem);
setSelectedElement(null);
}

View file

@ -29,7 +29,7 @@ export default class GraphQL extends React.PureComponent {
state = {
filter: "",
}
onFilterChange = (e, { value }) => this.setState({ filter: value })
onFilterChange = ({ target: { value } }) => this.setState({ filter: value })
jump = ({ time }) => {
jump(time);

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
import { connectPlayer, jump, pause } from 'Player';
import { Popup } from 'UI';
import { Popup, Button, TextEllipsis } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import stl from './network.module.css';
@ -18,138 +18,128 @@ const MEDIA = 'media';
const OTHER = 'other';
const TAB_TO_TYPE_MAP = {
[ XHR ]: TYPES.XHR,
[ JS ]: TYPES.JS,
[ CSS ]: TYPES.CSS,
[ IMG ]: TYPES.IMG,
[ MEDIA ]: TYPES.MEDIA,
[ OTHER ]: TYPES.OTHER
}
[XHR]: TYPES.XHR,
[JS]: TYPES.JS,
[CSS]: TYPES.CSS,
[IMG]: TYPES.IMG,
[MEDIA]: TYPES.MEDIA,
[OTHER]: TYPES.OTHER,
};
export function renderName(r) {
return (
<div className="flex w-full relative items-center">
<Popup content={ <div className={ stl.popupNameContent }>{ r.url }</div> } >
<div className={ stl.popupNameTrigger }>{ r.name }</div>
</Popup>
<div
className="absolute right-0 text-xs uppercase p-2 color-gray-500 hover:color-black"
onClick={ (e) => {
e.stopPropagation();
jump(r.time)
}}
>Jump</div>
</div>
);
export function renderName(r) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<Popup style={{ maxWidth: '75%' }} content={<div className={stl.popupNameContent}>{r.url}</div>}>
<TextEllipsis>{r.name}</TextEllipsis>
</Popup>
<Button
variant="text"
className="right-0 text-xs uppercase p-2 color-gray-500 hover:color-teal"
onClick={(e) => {
e.stopPropagation();
jump(r.time);
}}
>
Jump
</Button>
</div>
);
}
export function renderDuration(r) {
if (!r.success) return 'x';
if (!r.success) return 'x';
const text = `${ Math.round(r.duration) }ms`;
if (!r.isRed() && !r.isYellow()) return text;
const text = `${Math.round(r.duration)}ms`;
if (!r.isRed() && !r.isYellow()) return text;
let tooltipText;
let className = "w-full h-full flex items-center ";
if (r.isYellow()) {
tooltipText = "Slower than average";
className += "warn color-orange";
} else {
tooltipText = "Much slower than average";
className += "error color-red";
}
return (
<Popup content={ tooltipText } >
<div className={ cn(className, stl.duration) } > { text } </div>
</Popup>
);
}
@connectPlayer(state => ({
location: state.location,
resources: state.resourceList,
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
}))
@connect(state => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}), { setTimelinePointer })
export default class Network extends React.PureComponent {
state = {
filter: '',
filteredList: this.props.resources,
activeTab: ALL,
currentIndex: 0
}
onRowClick = (e, index) => {
pause();
jump(e.time);
this.setState({ currentIndex: index })
this.props.setTimelinePointer(null);
}
onTabClick = activeTab => this.setState({ activeTab })
onFilterChange = (e, { value }) => {
const { resources } = this.props;
const filterRE = getRE(value, 'i');
const filtered = resources.filter(({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
}
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
const activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
return {
currentIndex: activeItem ? filteredList.indexOf(activeItem) : filteredList.length - 1,
};
}
}
render() {
const {
location,
resources,
domContentLoadedTime,
loadTime,
domBuildingTime,
fetchPresented,
// time,
playing
} = this.props;
const { filter, activeTab, currentIndex, filteredList } = this.state;
// const filterRE = getRE(filter, 'i');
// let filtered = resources.filter(({ type, name }) =>
// filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
const resourcesSize = filteredList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
const transferredSize = filteredList
.reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow()) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
<React.Fragment>
<NetworkContent
// time = { time }
location = { location }
resources = { filteredList }
domContentLoadedTime = { domContentLoadedTime }
loadTime = { loadTime }
domBuildingTime = { domBuildingTime }
fetchPresented = { fetchPresented }
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={ this.onRowClick }
currentIndex={currentIndex}
/>
</React.Fragment>
<Popup content={tooltipText}>
<div className={cn(className, stl.duration)}> {text} </div>
</Popup>
);
}
}
@connectPlayer((state) => ({
location: state.location,
resources: state.resourceList,
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
listNow: state.resourceListNow,
}))
@connect(
(state) => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}),
{ setTimelinePointer }
)
export default class Network extends React.PureComponent {
state = {
filter: '',
filteredList: this.props.resources,
activeTab: ALL,
currentIndex: 0,
};
onRowClick = (e, index) => {
pause();
jump(e.time);
this.setState({ currentIndex: index });
this.props.setTimelinePointer(null);
};
onTabClick = (activeTab) => this.setState({ activeTab });
onFilterChange = (e, { value }) => {
const { resources } = this.props;
const filterRE = getRE(value, 'i');
const filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab]));
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
};
render() {
const {
location,
domContentLoadedTime,
loadTime,
domBuildingTime,
fetchPresented,
listNow,
} = this.props;
const { filteredList } = this.state;
const resourcesSize = filteredList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
const transferredSize = filteredList.reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
return (
<React.Fragment>
<NetworkContent
// time = { time }
location={location}
resources={filteredList}
domContentLoadedTime={domContentLoadedTime}
loadTime={loadTime}
domBuildingTime={domBuildingTime}
fetchPresented={fetchPresented}
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={this.onRowClick}
currentIndex={listNow.length - 0}
/>
</React.Fragment>
);
}
}

View file

@ -95,7 +95,6 @@ export default class Timeline extends React.PureComponent {
const { endTime } = this.props;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
console.log(p, time, e, endTime)
this.props.jump(time);
}
@ -232,7 +231,7 @@ export default class Timeline extends React.PureComponent {
</div>
}
>
<Icon className="bg-white" name={getPointerIcon('click_rage')} size="16" />
<Icon className="bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
))}
@ -254,7 +253,7 @@ export default class Timeline extends React.PureComponent {
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('click_rage')} size="16" />
<Icon className="rounded-full bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
}
@ -277,7 +276,7 @@ export default class Timeline extends React.PureComponent {
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('exception')} size="16" />
<Icon className=" rounded-full bg-white" name={getPointerIcon('exception')} color="red" size="16" />
</Tooltip>
</div>
))
@ -330,7 +329,7 @@ export default class Timeline extends React.PureComponent {
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('fetch')} size="16" />
<Icon className=" rounded-full bg-white" name={getPointerIcon('fetch')} color="red" size="16" />
</Tooltip>
</div>
))

View file

@ -4,7 +4,7 @@ import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/Stated
import cn from 'classnames';
import stl from './Marker.module.css';
import { activeTarget } from 'Player';
import { Popup } from 'UI';
import { Tooltip } from 'react-tippy';
interface Props {
target: MarkedTarget;
@ -21,17 +21,17 @@ export default function Marker({ target, active }: Props) {
return (
<div className={ cn(stl.marker, { [stl.active] : active }) } style={ style } onClick={() => activeTarget(target.index)}>
<div className={stl.index}>{target.index + 1}</div>
<Popup
<Tooltip
open={active}
arrow
sticky
distance={15}
content={(
html={(
<div>{target.count} Clicks</div>
)}
>
<div className="absolute inset-0"></div>
</Popup>
</Tooltip>
</div>
)
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { List, AutoSizer } from "react-virtualized";
import { List, AutoSizer } from 'react-virtualized';
import cn from 'classnames';
import { NoContent, IconButton } from 'UI';
import { NoContent, IconButton, Button } from 'UI';
import { percentOf } from 'App/utils';
import { formatMs } from 'App/date';
@ -10,58 +10,58 @@ import stl from './timeTable.module.css';
import autoscrollStl from '../autoscroll.module.css'; //aaa
type Timed = {
time: number,
}
time: number;
};
type Durationed = {
duration: number,
}
duration: number;
};
type CanBeRed = {
//+isRed: boolean,
isRed: () => boolean,
}
//+isRed: boolean,
isRed: () => boolean;
};
type Row = Timed & Durationed & CanBeRed
type Row = Timed & Durationed & CanBeRed;
type Line = {
color: string, // Maybe use typescript?
hint?: string,
onClick?: any,
} & Timed
color: string; // Maybe use typescript?
hint?: string;
onClick?: any;
} & Timed;
type Column = {
label: string,
width: number,
referenceLines?: Array<Line>,
style?: Object,
} & RenderOrKey
label: string;
width: number;
referenceLines?: Array<Line>;
style?: Object;
} & RenderOrKey;
// type RenderOrKey = { // Disjoint?
// render: Row => React.Node
// render: Row => React.Node
// } | {
// dataKey: string,
// }
type RenderOrKey = {
render?: (row: Row) => React.ReactNode,
key?: string,
} | {
dataKey: string,
}
type RenderOrKey =
| {
render?: (row: Row) => React.ReactNode;
key?: string;
}
| {
dataKey: string;
};
type Props = {
className?: string,
rows: Array<Row>,
children: Array<Column>
}
className?: string;
rows: Array<Row>;
children: Array<Column>;
};
type TimeLineInfo = {
timestart: number,
timewidth: number,
}
timestart: number;
timewidth: number;
};
type State = TimeLineInfo & typeof initialState;
@ -73,266 +73,228 @@ const ROW_HEIGHT = 32;
const TIME_SECTIONS_COUNT = 8;
const ZERO_TIMEWIDTH = 1000;
function formatTime(ms) {
if(ms < 0) return "";
return formatMs(ms);
if (ms < 0) return '';
return formatMs(ms);
}
function computeTimeLine(rows: Array<Row>, firstVisibleRowIndex: number, visibleCount): TimeLineInfo {
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
let timestart = visibleRows.length > 0
? Math.min(...visibleRows.map(r => r.time))
: 0;
const timeend = visibleRows.length > 0
? Math.max(...visibleRows.map(r => r.time + r.duration))
: 0;
let timewidth = timeend - timestart;
const offset = timewidth / 70;
if (timestart >= offset) {
timestart -= offset;
}
timewidth *= 1.5; // += offset;
if (timewidth === 0) {
timewidth = ZERO_TIMEWIDTH;
}
return {
timestart,
timewidth,
};
const visibleRows = rows.slice(firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight);
let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0;
const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + r.duration)) : 0;
let timewidth = timeend - timestart;
const offset = timewidth / 70;
if (timestart >= offset) {
timestart -= offset;
}
timewidth *= 1.5; // += offset;
if (timewidth === 0) {
timewidth = ZERO_TIMEWIDTH;
}
return {
timestart,
timewidth,
};
}
const initialState = {
firstVisibleRowIndex: 0,
}
firstVisibleRowIndex: 0,
};
export default class TimeTable extends React.PureComponent<Props, State> {
state = {
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
...initialState,
}
state = {
...computeTimeLine(this.props.rows, initialState.firstVisibleRowIndex, this.visibleCount),
...initialState,
};
get tableHeight() {
return this.props.tableHeight || 195;
}
get visibleCount() {
return Math.ceil(this.tableHeight/ROW_HEIGHT);
}
scroller = React.createRef();
autoScroll = true;
// componentDidMount() {
// if (this.scroller.current != null) {
// this.scroller.current.scrollToRow(this.props.rows.length - 1);
// }
// }
componentDidUpdate(prevProps: any, prevState: any) {
// if (prevProps.rows.length !== this.props.rows.length &&
// this.autoScroll &&
// this.scroller.current != null) {
// this.scroller.current.scrollToRow(this.props.rows.length);
// }
if (prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
(this.props.rows.length <= (this.visibleCount + _additionalHeight) && prevProps.rows.length !== this.props.rows.length)) {
this.setState({
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current != null) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
onScroll = ({ scrollTop, scrollHeight, clientHeight }:
{ scrollTop: number, scrollHeight: number, clientHeight: number }):void => {
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
this.autoScroll = (scrollHeight - clientHeight - scrollTop) < ROW_HEIGHT / 2;
this.setState({ firstVisibleRowIndex });
}
}
renderRow = ({ index, key, style: rowStyle }: any) => {
const { activeIndex } = this.props;
const {
children: columns,
rows,
renderPopup,
hoverable,
onRowClick,
} = this.props;
const {
timestart,
timewidth,
} = this.state;
const row = rows[ index ];
return (
<div
style={ rowStyle }
key={ key }
className={ cn('border-b border-color-gray-light-shade', stl.row, { [ stl.hoverable ]: hoverable, "error color-red": !!row.isRed && row.isRed(), 'cursor-pointer' : typeof onRowClick === "function", [stl.activeRow] : activeIndex === index }) }
onClick={ typeof onRowClick === "function" ? () => onRowClick(row, index) : null }
id="table-row"
>
{ columns.map(({ dataKey, render, width }) => (
<div className={ stl.cell } style={{ width: `${width}px`}}>
{ render ? render(row) : (row[ dataKey ] || <i className="color-gray-light">{"empty"}</i>) }
</div>
))}
<div className={ cn("relative flex-1 flex", stl.timeBarWrapper)}>
<BarRow
resource={ row }
timestart={ timestart }
timewidth={ timewidth }
popup={ renderPopup }
/>
</div>
</div>
);
}
onPrevClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex-1; i >= 0; i--) {
if (this.props.rows[ i ].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
}
onNextClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex+1; i < this.props.rows.length; i++) {
if (this.props.rows[ i ].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
}
render() {
const {
className,
rows,
children: columns,
navigation=false,
referenceLines = [],
additionalHeight = 0,
activeIndex,
} = this.props;
const {
timewidth,
timestart,
} = this.state;
_additionalHeight = additionalHeight;
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
const timeColumns = [];
if (timewidth > 0) {
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
timeColumns.push(timestart + i * sectionDuration);
}
get tableHeight() {
return this.props.tableHeight || 195;
}
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
get visibleCount() {
return Math.ceil(this.tableHeight / ROW_HEIGHT);
}
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
scroller = React.createRef();
autoScroll = true;
return (
<div className={ cn(className, "relative") }>
{ navigation &&
<div className={ cn(autoscrollStl.navButtons, "flex items-center") } >
<IconButton
componentDidMount() {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
componentDidUpdate(prevProps: any, prevState: any) {
// if (prevProps.rows.length !== this.props.rows.length &&
// this.autoScroll &&
// this.scroller.current != null) {
// this.scroller.current.scrollToRow(this.props.rows.length);
// }
if (
prevState.firstVisibleRowIndex !== this.state.firstVisibleRowIndex ||
(this.props.rows.length <= this.visibleCount + _additionalHeight && prevProps.rows.length !== this.props.rows.length)
) {
this.setState({
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
onScroll = ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }): void => {
const firstVisibleRowIndex = Math.floor(scrollTop / ROW_HEIGHT + 0.33);
if (this.state.firstVisibleRowIndex !== firstVisibleRowIndex) {
this.autoScroll = scrollHeight - clientHeight - scrollTop < ROW_HEIGHT / 2;
this.setState({ firstVisibleRowIndex });
}
};
renderRow = ({ index, key, style: rowStyle }: any) => {
const { activeIndex } = this.props;
const { children: columns, rows, renderPopup, hoverable, onRowClick } = this.props;
const { timestart, timewidth } = this.state;
const row = rows[index];
return (
<div
style={rowStyle}
key={key}
className={cn('border-b border-color-gray-light-shade', stl.row, {
[stl.hoverable]: hoverable,
'error color-red': !!row.isRed && row.isRed(),
'cursor-pointer': typeof onRowClick === 'function',
[stl.activeRow]: activeIndex === index,
})}
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : null}
id="table-row"
>
{columns.map(({ dataKey, render, width }) => (
<div className={stl.cell} style={{ width: `${width}px` }}>
{render ? render(row) : row[dataKey] || <i className="color-gray-light">{'empty'}</i>}
</div>
))}
<div className={cn('relative flex-1 flex', stl.timeBarWrapper)}>
<BarRow resource={row} timestart={timestart} timewidth={timewidth} popup={renderPopup} />
</div>
</div>
);
};
onPrevClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex - 1; i >= 0; i--) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
onNextClick = () => {
let prevRedIndex = -1;
for (let i = this.state.firstVisibleRowIndex + 1; i < this.props.rows.length; i++) {
if (this.props.rows[i].isRed()) {
prevRedIndex = i;
break;
}
}
if (this.scroller.current != null) {
this.scroller.current.scrollToRow(prevRedIndex);
}
};
render() {
const { className, rows, children: columns, navigation = false, referenceLines = [], additionalHeight = 0, activeIndex } = this.props;
const { timewidth, timestart } = this.state;
_additionalHeight = additionalHeight;
const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT);
const timeColumns = [];
if (timewidth > 0) {
for (let i = 0; i < TIME_SECTIONS_COUNT; i++) {
timeColumns.push(timestart + i * sectionDuration);
}
}
const visibleRefLines = referenceLines.filter(({ time }) => time > timestart && time < timestart + timewidth);
const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0);
return (
<div className={cn(className, 'relative')}>
{navigation && (
<div className={cn(autoscrollStl.navButtons, 'flex items-center')}>
<Button variant="text-primary" icon="chevron-up" onClick={this.onPrevClick} />
<Button variant="text-primary" icon="chevron-down" onClick={this.onNextClick} />
{/* <IconButton
size="small"
icon="chevron-up"
onClick={this.onPrevClick}
/>
<IconButton
/> */}
{/* <IconButton
size="small"
icon="chevron-down"
onClick={this.onNextClick}
/>
</div>
}
<div className={ stl.headers }>
<div className={ stl.infoHeaders }>
{ columns.map(({ label, width }) => (
<div
className={ stl.headerCell }
style={{ width: `${width}px`
}}>
{ label }
</div>
)) }
</div>
<div className={ stl.waterfallHeaders } >
{ timeColumns.map((time, i) => (
<div
className={ stl.timeCell }
key={ `tc-${ i }` }
>
{ formatTime(time) }
/> */}
</div>
)}
<div className={stl.headers}>
<div className={stl.infoHeaders}>
{columns.map(({ label, width }) => (
<div className={stl.headerCell} style={{ width: `${width}px` }}>
{label}
</div>
))}
</div>
<div className={stl.waterfallHeaders}>
{timeColumns.map((time, i) => (
<div className={stl.timeCell} key={`tc-${i}`}>
{formatTime(time)}
</div>
))}
</div>
</div>
))
}
</div>
</div>
<NoContent
size="small"
show={ rows.length === 0 }
>
<div className="relative">
<div className={ stl.timePart } style={{ left: `${ columnsSumWidth }px` }}>
{ timeColumns.map((_, index) => (
<div
key={ `tc-${ index }` }
className={ stl.timeCell }
/>
))
}
{ visibleRefLines.map(({ time, color, onClick }) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${ percentOf(time - timestart, timewidth) }%`,
cursor: typeof onClick === "function" ? "click" : "auto",
}}
onClick={ onClick }
/>
))}
<NoContent size="small" show={rows.length === 0}>
<div className="relative">
<div className={stl.timePart} style={{ left: `${columnsSumWidth}px` }}>
{timeColumns.map((_, index) => (
<div key={`tc-${index}`} className={stl.timeCell} />
))}
{visibleRefLines.map(({ time, color, onClick }) => (
<div
className={cn(stl.refLine, `bg-${color}`)}
style={{
left: `${percentOf(time - timestart, timewidth)}%`,
cursor: typeof onClick === 'function' ? 'click' : 'auto',
}}
onClick={onClick}
/>
))}
</div>
<AutoSizer disableHeight>
{({ width }) => (
<List
ref={this.scroller}
className={stl.list}
height={this.tableHeight + additionalHeight}
width={width}
overscanRowCount={20}
rowCount={rows.length}
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={this.onScroll}
scrollToAlignment="start"
forceUpdateProp={timestart | timewidth | activeIndex}
/>
)}
</AutoSizer>
</div>
</NoContent>
</div>
<AutoSizer disableHeight>
{({ width }) => (
<List
ref={ this.scroller }
className={ stl.list }
height={this.tableHeight + additionalHeight}
width={width}
overscanRowCount={20}
rowCount={rows.length}
rowHeight={ROW_HEIGHT}
rowRenderer={this.renderRow}
onScroll={ this.onScroll }
scrollToAlignment="start"
forceUpdateProp={ timestart | timewidth | activeIndex }
/>
)}
</AutoSizer>
</div>
</NoContent>
</div>
);
}
);
}
}

View file

@ -13,7 +13,7 @@
.navButtons {
position: absolute;
right: 260px;
top: -34px;
top: -39px;
}

View file

@ -21,7 +21,6 @@ interface Props {
}
function CodeSnippet(props: Props) {
const { host, projectKey, ingestPoint, defaultInputMode, obscureTextNumbers, obscureTextEmails } = props;
console.log('defaultInputMode', defaultInputMode)
const codeSnippet = `<!-- OpenReplay Tracking Code for ${host} -->
<script>
var initOpts = {

View file

@ -35,12 +35,12 @@ function FilterAutoCompleteLocal(props: Props) {
if(allowDecimals) {
const value = e.target.value;
setQuery(value);
props.onSelect(null, { value });
props.onSelect(null, value);
} else {
const value = e.target.value.replace(/[^\d]/, "");
if (+value !== 0) {
setQuery(value);
props.onSelect(null, { value });
props.onSelect(null, value);
}
}
};

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import React, { Fragment, useEffect } from 'react';
import { connect } from 'react-redux';
import { NoContent, Loader, Pagination, Popup } from 'UI';
import { NoContent, Loader, Pagination } from 'UI';
import { List } from 'immutable';
import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions';
@ -39,49 +39,24 @@ function LiveSessionList(props: Props) {
var timeoutId: any;
const { filters } = filter;
const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID);
const sortOptions = metaList
const sortOptions = [{ label: 'Newest', value: 'timestamp' }].concat(metaList
.map((i: any) => ({
label: capitalize(i),
value: i,
}))
.toJS();
// useEffect(() => {
// if (metaListLoading || metaList.size === 0 || !!filter.sort) return;
// if (sortOptions[0]) {
// props.applyFilter({ sort: sortOptions[0].value });
// }
// }, [metaListLoading]);
// useEffect(() => {
// const filteredSessions = filters.size > 0 ? props.list.filter(session => {
// let hasValidFilter = true;
// filters.forEach(filter => {
// if (!hasValidFilter) return;
// const _values = filter.value.filter(i => i !== '' && i !== null && i !== undefined).map(i => i.toLowerCase());
// if (filter.key === FilterKey.USERID) {
// const _userId = session.userId ? session.userId.toLowerCase() : '';
// hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter;
// }
// if (filter.category === FilterCategory.METADATA) {
// const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : '';
// hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter;
// }
// })
// return hasValidFilter;
// }) : props.list;
// setSessions(filteredSessions);
// }, [filters, list]);
})).toJS());
useEffect(() => {
props.applyFilter({ ...filter });
if (metaListLoading) return;
const _filter = { ...filter };
if (sortOptions[1]) {
_filter.sort = sortOptions[1].value;
}
props.applyFilter(_filter);
timeout();
return () => {
clearTimeout(timeoutId);
};
}, []);
}, [metaListLoading]);
const onUserClick = (userId: string, userAnonymousId: string) => {
if (userId) {
@ -116,67 +91,64 @@ function LiveSessionList(props: Props) {
<div className="flex items-center">
<div className="flex items-center ml-6 mr-4">
<span className="mr-2 color-gray-medium">Sort By</span>
<Popup
content="No metadata available to sort"
disabled={sortOptions.length > 0}
>
<div className={ cn("flex items-center", { 'disabled': sortOptions.length === 0})} >
<div className={cn('flex items-center', { disabled: sortOptions.length === 0 })}>
<Select
plain
right
options={sortOptions}
// defaultValue={sort.field}
onChange={onSortChange}
value={sortOptions.find((i: any) => i.value === filter.sort) || sortOptions[0]}
/>
<div className="mx-2" />
<SortOrderButton onChange={(state: any) => props.applyFilter({ order: state })} sortOrder={filter.order} />
</div>
</Popup>
</div>
</div>
</div>
</div>
<Loader loading={loading}>
<NoContent
title={'No live sessions.'}
subtext={
<span>
See how to setup the{' '}
<a target="_blank" className="link" href="https://docs.openreplay.com/plugins/assist">
{'Assist'}
</a>{' '}
plugin, if you havent done that already.
</span>
}
image={<img src="/assets/img/live-sessions.png" style={{ width: '70%', marginBottom: '30px' }} />}
show={!loading && list.size === 0}
>
<div className="bg-white p-3 rounded border">
{list.map((session) => (
<>
<SessionItem
key={session.sessionId}
session={session}
live
hasUserFilter={hasUserFilter}
onUserClick={onUserClick}
metaList={metaList}
/>
<div className="border-b" />
</>
))}
<div className="w-full flex items-center justify-center py-6">
<Pagination
page={currentPage}
totalPages={Math.ceil(total / PER_PAGE)}
onPageChange={(page: any) => props.updateCurrentPage(page)}
limit={PER_PAGE}
/>
<div className="bg-white p-3 rounded border">
<Loader loading={loading}>
<NoContent
title={'No live sessions.'}
subtext={
<span>
See how to setup the{' '}
<a target="_blank" className="link" href="https://docs.openreplay.com/plugins/assist">
{'Assist'}
</a>{' '}
plugin, if you havent done that already.
</span>
}
image={<img src="/assets/img/live-sessions.png" style={{ width: '70%', marginBottom: '30px' }} />}
show={!loading && list.size === 0}
>
<div>
{list.map((session) => (
<>
<SessionItem
key={session.sessionId}
session={session}
live
hasUserFilter={hasUserFilter}
onUserClick={onUserClick}
metaList={metaList}
/>
<div className="border-b" />
</>
))}
</div>
</div>
</NoContent>
</Loader>
</NoContent>
</Loader>
<div className={cn("w-full flex items-center justify-center py-6", { 'disabled' : loading})}>
<Pagination
page={currentPage}
totalPages={Math.ceil(total / PER_PAGE)}
onPageChange={(page: any) => props.updateCurrentPage(page)}
limit={PER_PAGE}
debounceRequest={500}
/>
</div>
</div>
</div>
);
}

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