Merge branch 'dev' into storage-override
This commit is contained in:
commit
5e851c7758
240 changed files with 8160 additions and 5113 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
api/Dockerfile.alerts.dockerignore
Normal file
10
api/Dockerfile.alerts.dockerignore
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
cd sourcemap-reader
|
||||
nohup npm start &> /tmp/sourcemap-reader.log &
|
||||
cd ..
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
uvicorn app:app --host 0.0.0.0 --reload
|
||||
|
|
|
|||
15
api/requirements-alerts.txt
Normal file
15
api/requirements-alerts.txt
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
8
backend/pkg/db/cache/messages-common.go
vendored
8
backend/pkg/db/cache/messages-common.go
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
11
backend/pkg/db/cache/messages-ios.go
vendored
11
backend/pkg/db/cache/messages-ios.go
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
55
backend/pkg/db/cache/messages-web.go
vendored
55
backend/pkg/db/cache/messages-web.go
vendored
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
7
backend/pkg/db/cache/pg-cache.go
vendored
7
backend/pkg/db/cache/pg-cache.go
vendored
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
93
backend/pkg/db/postgres/bulk.go
Normal file
93
backend/pkg/db/postgres/bulk.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
175
backend/pkg/db/postgres/pool.go
Normal file
175
backend/pkg/db/postgres/pool.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
30
backend/pkg/db/postgres/session-updates.go
Normal file
30
backend/pkg/db/postgres/session-updates.go
Normal 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}
|
||||
}
|
||||
|
|
@ -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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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
15
ee/api/.dockerignore
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
ee/api/Dockerfile.alerts.dockerignore
Normal file
15
ee/api/Dockerfile.alerts.dockerignore
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
15
ee/api/Dockerfile.crons.dockerignore
Normal file
15
ee/api/Dockerfile.crons.dockerignore
Normal 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
|
||||
|
|
@ -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]))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;""",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 &
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
bash env_vars.sh
|
||||
#!/bin/sh
|
||||
sh env_vars.sh
|
||||
source .env.override
|
||||
python app_crons.py $ACTION
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
touch .env.override
|
||||
if [[ -z "${ENV_CONFIG_OVERRIDE_PATH}" ]]; then
|
||||
|
|
|
|||
18
ee/api/requirements-alerts.txt
Normal file
18
ee/api/requirements-alerts.txt
Normal 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
|
||||
18
ee/api/requirements-crons.txt
Normal file
18
ee/api/requirements-crons.txt
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
728
ee/connectors/msgcodec/msgcodec.py
Normal file
728
ee/connectors/msgcodec/msgcodec.py
Normal 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)
|
||||
)
|
||||
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
2
ee/utilities/.gitignore
vendored
2
ee/utilities/.gitignore
vendored
|
|
@ -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
20
ee/utilities/Dockerfile
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
773
ee/utilities/package-lock.json
generated
773
ee/utilities/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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 don’t 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 don’t 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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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 don’t 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;
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ function DashboardView(props: Props) {
|
|||
trimQuery();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
dashboardStore.selectDefaultDashboard();
|
||||
}, [siteId])
|
||||
|
||||
const onAddWidgets = () => {
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
28
frontend/app/components/Overview/Overview.tsx
Normal file
28
frontend/app/components/Overview/Overview.tsx
Normal 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);
|
||||
1
frontend/app/components/Overview/index.ts
Normal file
1
frontend/app/components/Overview/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Overview';
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './EventsToggleButton'
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
.navButtons {
|
||||
position: absolute;
|
||||
right: 260px;
|
||||
top: -34px;
|
||||
top: -39px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 haven’t 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 haven’t 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
Loading…
Add table
Reference in a new issue