Merge remote-tracking branch 'origin/dev' into api-v1.9.5
This commit is contained in:
commit
24f7e5f1ae
551 changed files with 8594 additions and 10420 deletions
4
.github/workflows/api-ee.yaml
vendored
4
.github/workflows/api-ee.yaml
vendored
|
|
@ -110,7 +110,9 @@ jobs:
|
|||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||
kubectl config set-context --namespace=app --current
|
||||
kubectl config get-contexts
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.18-alpine3.15 AS prepare
|
||||
FROM golang:1.18-alpine3.17 AS prepare
|
||||
|
||||
RUN apk add --no-cache git openssh openssl-dev pkgconf gcc g++ make libc-dev bash librdkafka-dev cyrus-sasl cyrus-sasl-gssapiv2 krb5
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ RUN adduser -u 1001 openreplay -D
|
|||
|
||||
ENV TZ=UTC \
|
||||
GIT_SHA=$GIT_SHA \
|
||||
FS_ULIMIT=1000 \
|
||||
FS_ULIMIT=10000 \
|
||||
FS_DIR=/mnt/efs \
|
||||
MAXMINDDB_FILE=/home/openreplay/geoip.mmdb \
|
||||
UAPARSER_FILE=/home/openreplay/regexes.yaml \
|
||||
|
|
@ -76,8 +76,8 @@ ENV TZ=UTC \
|
|||
USE_FAILOVER=false \
|
||||
GROUP_STORAGE_FAILOVER=failover \
|
||||
TOPIC_STORAGE_FAILOVER=storage-failover \
|
||||
PROFILER_ENABLED=false
|
||||
|
||||
PROFILER_ENABLED=false \
|
||||
COMPRESSION_TYPE=zstd
|
||||
|
||||
|
||||
ARG SERVICE_NAME
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func main() {
|
|||
consumer := queue.NewConsumer(
|
||||
cfg.GroupEnder,
|
||||
[]string{cfg.TopicRawWeb},
|
||||
messages.NewMessageIterator(
|
||||
messages.NewEnderMessageIterator(
|
||||
func(msg messages.Message) { sessions.UpdateSession(msg) },
|
||||
[]int{messages.MsgTimestamp},
|
||||
false),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/aws/aws-sdk-go v1.44.98
|
||||
github.com/btcsuite/btcutil v1.0.2
|
||||
github.com/confluentinc/confluent-kafka-go v1.8.2
|
||||
github.com/confluentinc/confluent-kafka-go v1.9.2
|
||||
github.com/elastic/go-elasticsearch/v7 v7.13.1
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
|
|||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA=
|
||||
github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ=
|
||||
github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
|
@ -115,11 +118,12 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/confluentinc/confluent-kafka-go v1.8.2 h1:PBdbvYpyOdFLehj8j+9ba7FL4c4Moxn79gy9cYKxG5E=
|
||||
github.com/confluentinc/confluent-kafka-go v1.8.2/go.mod h1:u2zNLny2xq+5rWeTQjFHbDzzNuba4P1vo31r9r4uAdg=
|
||||
github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q=
|
||||
github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
|
@ -136,6 +140,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
|
@ -195,10 +203,13 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
|||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
|
|
@ -233,6 +244,7 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
|
|||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20211008130755-947d60d73cc0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
|
|
@ -250,12 +262,17 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hamba/avro v1.5.6/go.mod h1:3vNT0RLXXpFm2Tb/5KC71ZRJlOroggq1Rcitb6k4Fr8=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
|
|
@ -301,6 +318,11 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
|
|||
github.com/jackc/puddle v1.2.2-0.20220404125616-4e959849469a h1:oH7y/b+q2BEerCnARr/HZc1NxOYbKSJor4MqQXlhh+s=
|
||||
github.com/jackc/puddle v1.2.2-0.20220404125616-4e959849469a/go.mod h1:ZQuO1Un86Xpe1ShKl08ERTzYhzWq+OvrvotbpeE3XO0=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
|
||||
github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
|
||||
github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
|
||||
github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
|
||||
github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
|
|
@ -314,6 +336,7 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
|||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
|
|
@ -327,16 +350,23 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/linkedin/goavro v2.1.0+incompatible/go.mod h1:bBCwI2eGYpUI/4820s67MElg9tdeLbINjLjiM2xZFYM=
|
||||
github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
|
||||
github.com/linkedin/goavro/v2 v2.10.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
|
||||
github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
|
|
@ -354,6 +384,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
|
@ -367,6 +398,7 @@ github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKf
|
|||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
|
@ -395,11 +427,16 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
|||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sethvargo/go-envconfig v0.7.0 h1:P/ljQXSRjgAgsnIripHs53Jg/uNVXu2FYQ9yLSDappA=
|
||||
github.com/sethvargo/go-envconfig v0.7.0/go.mod h1:00S1FAhRUuTNJazWBWcJGvEHOM+NO6DhoRMAOX7FY5o=
|
||||
|
|
@ -420,6 +457,7 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
|
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
@ -539,6 +577,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
|
|
@ -663,6 +702,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -729,6 +769,7 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs
|
|||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
|
@ -882,6 +923,7 @@ google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX
|
|||
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
|
||||
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
|
|
@ -933,14 +975,19 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
@ -948,11 +995,13 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ func (c *cacher) cacheURL(t *Task) {
|
|||
defer res.Body.Close()
|
||||
if res.StatusCode >= 400 {
|
||||
printErr := true
|
||||
// Retry 403 error
|
||||
if res.StatusCode == 403 && t.retries > 0 {
|
||||
// Retry 403/503 errors
|
||||
if (res.StatusCode == 403 || res.StatusCode == 503) && t.retries > 0 {
|
||||
c.workers.AddTask(t)
|
||||
printErr = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type Config struct {
|
|||
ProducerCloseTimeout int `env:"PRODUCER_CLOSE_TIMEOUT,default=15000"`
|
||||
UseFailover bool `env:"USE_FAILOVER,default=false"`
|
||||
MaxFileSize int64 `env:"MAX_FILE_SIZE,default=524288000"`
|
||||
UseSort bool `env:"USE_SESSION_SORT,default=true"`
|
||||
UseProfiler bool `env:"PROFILER_ENABLED,default=false"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,12 +152,15 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
}
|
||||
|
||||
// Save information about session beacon size
|
||||
e.addBeaconSize(tokenData.ID, p.BeaconSize)
|
||||
|
||||
ResponseWithJSON(w, &StartSessionResponse{
|
||||
Token: e.services.Tokenizer.Compose(*tokenData),
|
||||
UserUUID: userUUID,
|
||||
SessionID: strconv.FormatUint(tokenData.ID, 10),
|
||||
ProjectID: strconv.FormatUint(uint64(p.ProjectID), 10),
|
||||
BeaconSizeLimit: e.cfg.BeaconSizeLimit,
|
||||
BeaconSizeLimit: e.getBeaconSize(tokenData.ID),
|
||||
StartTimestamp: int64(flakeid.ExtractTimestamp(tokenData.ID)),
|
||||
Delay: tokenData.Delay,
|
||||
})
|
||||
|
|
@ -177,7 +180,7 @@ func (e *Router) pushMessagesHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := e.readBody(w, r, e.cfg.BeaconSizeLimit)
|
||||
bodyBytes, err := e.readBody(w, r, e.getBeaconSize(sessionData.ID))
|
||||
if err != nil {
|
||||
log.Printf("error while reading request body: %s", err)
|
||||
ResponseWithError(w, http.StatusRequestEntityTooLarge, err)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,15 @@ import (
|
|||
http2 "openreplay/backend/internal/http/services"
|
||||
"openreplay/backend/internal/http/util"
|
||||
"openreplay/backend/pkg/monitoring"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BeaconSize struct {
|
||||
size int64
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
router *mux.Router
|
||||
cfg *http3.Config
|
||||
|
|
@ -22,6 +28,8 @@ type Router struct {
|
|||
requestSize syncfloat64.Histogram
|
||||
requestDuration syncfloat64.Histogram
|
||||
totalRequests syncfloat64.Counter
|
||||
mutex *sync.RWMutex
|
||||
beaconSizeCache map[uint64]*BeaconSize // Cache for session's beaconSize
|
||||
}
|
||||
|
||||
func NewRouter(cfg *http3.Config, services *http2.ServicesBuilder, metrics *monitoring.Metrics) (*Router, error) {
|
||||
|
|
@ -34,14 +42,53 @@ func NewRouter(cfg *http3.Config, services *http2.ServicesBuilder, metrics *moni
|
|||
return nil, fmt.Errorf("metrics is empty")
|
||||
}
|
||||
e := &Router{
|
||||
cfg: cfg,
|
||||
services: services,
|
||||
cfg: cfg,
|
||||
services: services,
|
||||
mutex: &sync.RWMutex{},
|
||||
beaconSizeCache: make(map[uint64]*BeaconSize),
|
||||
}
|
||||
e.initMetrics(metrics)
|
||||
e.init()
|
||||
go e.clearBeaconSizes()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *Router) addBeaconSize(sessionID uint64, size int64) {
|
||||
if size <= 0 {
|
||||
return
|
||||
}
|
||||
e.mutex.Lock()
|
||||
defer e.mutex.Unlock()
|
||||
e.beaconSizeCache[sessionID] = &BeaconSize{
|
||||
size: size,
|
||||
time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Router) getBeaconSize(sessionID uint64) int64 {
|
||||
e.mutex.RLock()
|
||||
defer e.mutex.RUnlock()
|
||||
if beaconSize, ok := e.beaconSizeCache[sessionID]; ok {
|
||||
beaconSize.time = time.Now()
|
||||
return beaconSize.size
|
||||
}
|
||||
return e.cfg.BeaconSizeLimit
|
||||
}
|
||||
|
||||
func (e *Router) clearBeaconSizes() {
|
||||
for {
|
||||
time.Sleep(time.Minute * 2)
|
||||
now := time.Now()
|
||||
e.mutex.Lock()
|
||||
for sid, bs := range e.beaconSizeCache {
|
||||
if now.Sub(bs.time) > time.Minute*3 {
|
||||
delete(e.beaconSizeCache, sid)
|
||||
}
|
||||
}
|
||||
e.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Router) init() {
|
||||
e.router = mux.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ type Storage struct {
|
|||
sessionDEVSize syncfloat64.Histogram
|
||||
readingDOMTime syncfloat64.Histogram
|
||||
readingDEVTime syncfloat64.Histogram
|
||||
sortingDOMTime syncfloat64.Histogram
|
||||
sortingDEVTime syncfloat64.Histogram
|
||||
archivingDOMTime syncfloat64.Histogram
|
||||
archivingDEVTime syncfloat64.Histogram
|
||||
uploadingDOMTime syncfloat64.Histogram
|
||||
|
|
@ -79,6 +81,14 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor
|
|||
if err != nil {
|
||||
log.Printf("can't create reading_duration metric: %s", err)
|
||||
}
|
||||
sortingDOMTime, err := metrics.RegisterHistogram("sorting_duration")
|
||||
if err != nil {
|
||||
log.Printf("can't create reading_duration metric: %s", err)
|
||||
}
|
||||
sortingDEVTime, err := metrics.RegisterHistogram("sorting_dt_duration")
|
||||
if err != nil {
|
||||
log.Printf("can't create reading_duration metric: %s", err)
|
||||
}
|
||||
archivingDOMTime, err := metrics.RegisterHistogram("archiving_duration")
|
||||
if err != nil {
|
||||
log.Printf("can't create archiving_duration metric: %s", err)
|
||||
|
|
@ -104,6 +114,8 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor
|
|||
sessionDEVSize: sessionDevtoolsSize,
|
||||
readingDOMTime: readingDOMTime,
|
||||
readingDEVTime: readingDEVTime,
|
||||
sortingDOMTime: sortingDOMTime,
|
||||
sortingDEVTime: sortingDEVTime,
|
||||
archivingDOMTime: archivingDOMTime,
|
||||
archivingDEVTime: archivingDEVTime,
|
||||
uploadingDOMTime: uploadingDOMTime,
|
||||
|
|
@ -156,14 +168,41 @@ func (s *Storage) Upload(msg *messages.SessionEnd) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) openSession(filePath string) ([]byte, error) {
|
||||
func (s *Storage) openSession(filePath string, tp FileType) ([]byte, error) {
|
||||
// Check file size before download into memory
|
||||
info, err := os.Stat(filePath)
|
||||
if err == nil && info.Size() > s.cfg.MaxFileSize {
|
||||
return nil, fmt.Errorf("big file, size: %d", info.Size())
|
||||
}
|
||||
// Read file into memory
|
||||
return os.ReadFile(filePath)
|
||||
raw, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !s.cfg.UseSort {
|
||||
return raw, nil
|
||||
}
|
||||
start := time.Now()
|
||||
res, err := s.sortSessionMessages(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't sort session, err: %s", err)
|
||||
}
|
||||
if tp == DOM {
|
||||
s.sortingDOMTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()))
|
||||
} else {
|
||||
s.sortingDEVTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Storage) sortSessionMessages(raw []byte) ([]byte, error) {
|
||||
// Parse messages, sort by index and save result into slice of bytes
|
||||
unsortedMessages, err := messages.SplitMessages(raw)
|
||||
if err != nil {
|
||||
log.Printf("can't sort session, err: %s", err)
|
||||
return raw, nil
|
||||
}
|
||||
return messages.MergeMessages(raw, messages.SortMessages(unsortedMessages)), nil
|
||||
}
|
||||
|
||||
func (s *Storage) prepareSession(path string, tp FileType, task *Task) error {
|
||||
|
|
@ -172,7 +211,7 @@ func (s *Storage) prepareSession(path string, tp FileType, task *Task) error {
|
|||
path += "devtools"
|
||||
}
|
||||
startRead := time.Now()
|
||||
mob, err := s.openSession(path)
|
||||
mob, err := s.openSession(path, tp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import (
|
|||
func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) {
|
||||
p := &Project{ProjectKey: projectKey}
|
||||
if err := conn.c.QueryRow(`
|
||||
SELECT max_session_duration, sample_rate, project_id
|
||||
SELECT max_session_duration, sample_rate, project_id, beacon_size
|
||||
FROM projects
|
||||
WHERE project_key=$1 AND active = true
|
||||
`,
|
||||
projectKey,
|
||||
).Scan(&p.MaxSessionDuration, &p.SampleRate, &p.ProjectID); err != nil {
|
||||
).Scan(&p.MaxSessionDuration, &p.SampleRate, &p.ProjectID, &p.BeaconSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type Project struct {
|
|||
MaxSessionDuration int64
|
||||
SampleRate byte
|
||||
SaveRequestPayloads bool
|
||||
BeaconSize int64
|
||||
Metadata1 *string
|
||||
Metadata2 *string
|
||||
Metadata3 *string
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type ClickRageDetector struct {
|
|||
firstInARawTimestamp uint64
|
||||
firstInARawMessageId uint64
|
||||
countsInARow int
|
||||
url string
|
||||
}
|
||||
|
||||
func (crd *ClickRageDetector) reset() {
|
||||
|
|
@ -30,6 +31,7 @@ func (crd *ClickRageDetector) reset() {
|
|||
crd.firstInARawTimestamp = 0
|
||||
crd.firstInARawMessageId = 0
|
||||
crd.countsInARow = 0
|
||||
crd.url = ""
|
||||
}
|
||||
|
||||
func (crd *ClickRageDetector) Build() Message {
|
||||
|
|
@ -45,6 +47,7 @@ func (crd *ClickRageDetector) Build() Message {
|
|||
Payload: string(payload),
|
||||
Timestamp: crd.firstInARawTimestamp,
|
||||
MessageID: crd.firstInARawMessageId,
|
||||
URL: crd.url,
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
|
@ -54,6 +57,9 @@ func (crd *ClickRageDetector) Build() Message {
|
|||
func (crd *ClickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message {
|
||||
switch msg := message.(type) {
|
||||
case *MouseClick:
|
||||
if crd.url == "" && msg.Url != "" {
|
||||
crd.url = msg.Url
|
||||
}
|
||||
// TODO: check if we it is ok to capture clickRage event without the connected ClickEvent in db.
|
||||
if msg.Label == "" {
|
||||
return crd.Build()
|
||||
|
|
@ -69,6 +75,9 @@ func (crd *ClickRageDetector) Handle(message Message, messageID uint64, timestam
|
|||
crd.firstInARawTimestamp = timestamp
|
||||
crd.firstInARawMessageId = messageID
|
||||
crd.countsInARow = 1
|
||||
if crd.url == "" && msg.Url != "" {
|
||||
crd.url = msg.Url
|
||||
}
|
||||
return event
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -13,6 +14,7 @@ type BytesReader interface {
|
|||
ReadInt() (int64, error)
|
||||
ReadBoolean() (bool, error)
|
||||
ReadString() (string, error)
|
||||
ReadIndex() (uint64, error)
|
||||
Data() []byte
|
||||
Pointer() int64
|
||||
SetPointer(p int64)
|
||||
|
|
@ -106,6 +108,15 @@ func (m *bytesReaderImpl) ReadString() (string, error) {
|
|||
return str, nil
|
||||
}
|
||||
|
||||
func (m *bytesReaderImpl) ReadIndex() (uint64, error) {
|
||||
if len(m.data)-int(m.curr) < 8 {
|
||||
return 0, fmt.Errorf("out of range")
|
||||
}
|
||||
size := binary.LittleEndian.Uint64(m.data[m.curr : m.curr+8])
|
||||
m.curr += 8
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (m *bytesReaderImpl) Data() []byte {
|
||||
return m.data
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package messages
|
||||
|
||||
func IsReplayerType(id int) bool {
|
||||
return 80 != id && 81 != id && 82 != id && 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 35 != id && 42 != id && 52 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 126 != id && 127 != id && 107 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 99 != id && 101 != id && 104 != id && 110 != id && 111 != id
|
||||
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 35 != id && 42 != id && 52 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 125 != id && 126 != id && 127 != id && 107 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 99 != id && 101 != id && 104 != id && 110 != id && 111 != id
|
||||
}
|
||||
|
||||
func IsIOSType(id int) bool {
|
||||
|
|
@ -11,4 +11,4 @@ func IsIOSType(id int) bool {
|
|||
|
||||
func IsDOMType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
}
|
||||
}
|
||||
179
backend/pkg/messages/iterator-ender.go
Normal file
179
backend/pkg/messages/iterator-ender.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
type enderMessageIteratorImpl struct {
|
||||
filter map[int]struct{}
|
||||
preFilter map[int]struct{}
|
||||
handler MessageHandler
|
||||
autoDecode bool
|
||||
version uint64
|
||||
size uint64
|
||||
canSkip bool
|
||||
broken bool
|
||||
messageInfo *message
|
||||
batchInfo *BatchInfo
|
||||
urls *pageLocations
|
||||
}
|
||||
|
||||
func NewEnderMessageIterator(messageHandler MessageHandler, messageFilter []int, autoDecode bool) MessageIterator {
|
||||
iter := &enderMessageIteratorImpl{
|
||||
handler: messageHandler,
|
||||
autoDecode: autoDecode,
|
||||
urls: NewPageLocations(),
|
||||
}
|
||||
if len(messageFilter) != 0 {
|
||||
filter := make(map[int]struct{}, len(messageFilter))
|
||||
for _, msgType := range messageFilter {
|
||||
filter[msgType] = struct{}{}
|
||||
}
|
||||
iter.filter = filter
|
||||
}
|
||||
iter.preFilter = map[int]struct{}{
|
||||
MsgBatchMetadata: {}, MsgBatchMeta: {}, MsgTimestamp: {},
|
||||
MsgSessionStart: {}, MsgSessionEnd: {}, MsgSetPageLocation: {},
|
||||
MsgSessionEndDeprecated: {}}
|
||||
return iter
|
||||
}
|
||||
|
||||
func (i *enderMessageIteratorImpl) prepareVars(batchInfo *BatchInfo) {
|
||||
i.batchInfo = batchInfo
|
||||
i.messageInfo = &message{batch: batchInfo}
|
||||
i.version = 0
|
||||
i.canSkip = false
|
||||
i.broken = false
|
||||
i.size = 0
|
||||
}
|
||||
|
||||
func (i *enderMessageIteratorImpl) Iterate(batchData []byte, batchInfo *BatchInfo) {
|
||||
// Create new message reader
|
||||
reader := NewMessageReader(batchData)
|
||||
|
||||
// Pre-decode batch data
|
||||
if err := reader.Parse(); err != nil {
|
||||
log.Printf("pre-decode batch err: %s, info: %s", err, batchInfo.Info())
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare iterator before processing messages in batch
|
||||
i.prepareVars(batchInfo)
|
||||
|
||||
// Store last timestamp message here
|
||||
var lastMessage Message
|
||||
|
||||
for reader.Next() {
|
||||
// Increase message index (can be overwritten by batch info message)
|
||||
i.messageInfo.Index++
|
||||
|
||||
msg := reader.Message()
|
||||
|
||||
// Preprocess "system" messages
|
||||
if _, ok := i.preFilter[msg.TypeID()]; ok {
|
||||
msg = msg.Decode()
|
||||
if msg == nil {
|
||||
log.Printf("decode error, type: %d, info: %s", msg.TypeID(), i.batchInfo.Info())
|
||||
return
|
||||
}
|
||||
msg = transformDeprecated(msg)
|
||||
if err := i.preprocessing(msg); err != nil {
|
||||
log.Printf("message preprocessing err: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Skip messages we don't have in filter
|
||||
if i.filter != nil {
|
||||
if _, ok := i.filter[msg.TypeID()]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if i.autoDecode {
|
||||
msg = msg.Decode()
|
||||
if msg == nil {
|
||||
log.Printf("decode error, type: %d, info: %s", msg.TypeID(), i.batchInfo.Info())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set meta information for message
|
||||
msg.Meta().SetMeta(i.messageInfo)
|
||||
|
||||
// Update last timestamp message
|
||||
lastMessage = msg
|
||||
}
|
||||
|
||||
if lastMessage != nil {
|
||||
i.handler(lastMessage)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (i *enderMessageIteratorImpl) zeroTsLog(msgType string) {
|
||||
log.Printf("zero timestamp in %s, info: %s", msgType, i.batchInfo.Info())
|
||||
}
|
||||
|
||||
func (i *enderMessageIteratorImpl) preprocessing(msg Message) error {
|
||||
switch m := msg.(type) {
|
||||
case *BatchMetadata:
|
||||
if i.messageInfo.Index > 1 { // Might be several 0-0 BatchMeta in a row without an error though
|
||||
return fmt.Errorf("batchMetadata found at the end of the batch, info: %s", i.batchInfo.Info())
|
||||
}
|
||||
if m.Version > 1 {
|
||||
return fmt.Errorf("incorrect batch version: %d, skip current batch, info: %s", i.version, i.batchInfo.Info())
|
||||
}
|
||||
i.messageInfo.Index = m.PageNo<<32 + m.FirstIndex // 2^32 is the maximum count of messages per page (ha-ha)
|
||||
i.messageInfo.Timestamp = m.Timestamp
|
||||
if m.Timestamp == 0 {
|
||||
i.zeroTsLog("BatchMetadata")
|
||||
}
|
||||
i.messageInfo.Url = m.Location
|
||||
i.version = m.Version
|
||||
i.batchInfo.version = m.Version
|
||||
|
||||
case *BatchMeta: // Is not required to be present in batch since IOS doesn't have it (though we might change it)
|
||||
if i.messageInfo.Index > 1 { // Might be several 0-0 BatchMeta in a row without an error though
|
||||
return fmt.Errorf("batchMeta found at the end of the batch, info: %s", i.batchInfo.Info())
|
||||
}
|
||||
i.messageInfo.Index = m.PageNo<<32 + m.FirstIndex // 2^32 is the maximum count of messages per page (ha-ha)
|
||||
i.messageInfo.Timestamp = m.Timestamp
|
||||
if m.Timestamp == 0 {
|
||||
i.zeroTsLog("BatchMeta")
|
||||
}
|
||||
// Try to get saved session's page url
|
||||
if savedURL := i.urls.Get(i.messageInfo.batch.sessionID); savedURL != "" {
|
||||
i.messageInfo.Url = savedURL
|
||||
}
|
||||
|
||||
case *Timestamp:
|
||||
i.messageInfo.Timestamp = int64(m.Timestamp)
|
||||
if m.Timestamp == 0 {
|
||||
i.zeroTsLog("Timestamp")
|
||||
}
|
||||
|
||||
case *SessionStart:
|
||||
i.messageInfo.Timestamp = int64(m.Timestamp)
|
||||
if m.Timestamp == 0 {
|
||||
i.zeroTsLog("SessionStart")
|
||||
log.Printf("zero session start, project: %d, UA: %s, tracker: %s, info: %s",
|
||||
m.ProjectID, m.UserAgent, m.TrackerVersion, i.batchInfo.Info())
|
||||
}
|
||||
|
||||
case *SessionEnd:
|
||||
i.messageInfo.Timestamp = int64(m.Timestamp)
|
||||
if m.Timestamp == 0 {
|
||||
i.zeroTsLog("SessionEnd")
|
||||
}
|
||||
// Delete session from urls cache layer
|
||||
i.urls.Delete(i.messageInfo.batch.sessionID)
|
||||
|
||||
case *SetPageLocation:
|
||||
i.messageInfo.Url = m.URL
|
||||
// Save session page url in cache for using in next batches
|
||||
i.urls.Set(i.messageInfo.batch.sessionID, m.URL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -25,6 +25,16 @@ func transformDeprecated(msg Message) Message {
|
|||
Timestamp: m.Timestamp,
|
||||
Duration: m.Duration,
|
||||
}
|
||||
case *IssueEventDeprecated:
|
||||
return &IssueEvent{
|
||||
MessageID: m.MessageID,
|
||||
Timestamp: m.Timestamp,
|
||||
Type: m.Type,
|
||||
ContextString: m.ContextString,
|
||||
Context: m.Context,
|
||||
Payload: m.Payload,
|
||||
URL: "",
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
78
backend/pkg/messages/session-iterator.go
Normal file
78
backend/pkg/messages/session-iterator.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type msgInfo struct {
|
||||
index uint64
|
||||
start int64
|
||||
end int64
|
||||
}
|
||||
|
||||
func SplitMessages(data []byte) ([]*msgInfo, error) {
|
||||
messages := make([]*msgInfo, 0)
|
||||
reader := NewBytesReader(data)
|
||||
for {
|
||||
// Get message start
|
||||
msgStart := reader.Pointer()
|
||||
if int(msgStart) >= len(data) {
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// Read message index
|
||||
msgIndex, err := reader.ReadIndex()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println(reader.Pointer(), msgStart)
|
||||
return nil, fmt.Errorf("read message index err: %s", err)
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// Read message type
|
||||
msgType, err := reader.ReadUint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read message type err: %s", err)
|
||||
}
|
||||
|
||||
if msgType == MsgRedux {
|
||||
log.Printf("redux")
|
||||
}
|
||||
if msgType == MsgFetch {
|
||||
log.Printf("fetch")
|
||||
}
|
||||
|
||||
// Read message body
|
||||
_, err = ReadMessage(msgType, reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read message body err: %s", err)
|
||||
}
|
||||
|
||||
// Add new message info to messages slice
|
||||
messages = append(messages, &msgInfo{
|
||||
index: msgIndex,
|
||||
start: msgStart,
|
||||
end: reader.Pointer(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SortMessages(messages []*msgInfo) []*msgInfo {
|
||||
sort.SliceStable(messages, func(i, j int) bool {
|
||||
return messages[i].index < messages[j].index
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
func MergeMessages(data []byte, messages []*msgInfo) []byte {
|
||||
sortedSession := bytes.NewBuffer(make([]byte, 0, len(data)))
|
||||
for _, info := range messages {
|
||||
sortedSession.Write(data[info.start:info.end])
|
||||
}
|
||||
return sortedSession.Bytes()
|
||||
}
|
||||
|
|
@ -24,8 +24,9 @@ func ResolveURL(baseurl string, rawurl string) string {
|
|||
if !isRelativeCachable(rawurl) {
|
||||
return rawurl
|
||||
}
|
||||
base, _ := url.ParseRequestURI(baseurl) // fn Only for base urls
|
||||
u, _ := url.Parse(rawurl) // TODO: handle errors ?
|
||||
baseurl = strings.Split(baseurl, "#")[0] // remove #fragment suffix if present
|
||||
base, _ := url.ParseRequestURI(baseurl) // fn Only for base urls
|
||||
u, _ := url.Parse(rawurl) // TODO: handle errors ?
|
||||
if base == nil || u == nil {
|
||||
return rawurl
|
||||
}
|
||||
|
|
@ -48,6 +49,7 @@ func isCachable(rawurl string) bool {
|
|||
}
|
||||
ext := filepath.Ext(u.Path)
|
||||
return ext == ".css" ||
|
||||
ext == ".ashx" || // ASP .NET
|
||||
ext == ".woff" ||
|
||||
ext == ".woff2" ||
|
||||
ext == ".ttf" ||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ var batches = map[string]string{
|
|||
"requests": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, request_body, response_body, status, method, duration, success, event_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"custom": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, name, payload, event_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"graphql": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, name, request_body, response_body, event_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"issuesEvents": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, issue_id, issue_type, event_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"issuesEvents": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, issue_id, issue_type, event_type, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"issues": "INSERT INTO experimental.issues (project_id, issue_id, type, context_string) VALUES (?, ?, ?, ?)",
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +132,7 @@ func (c *connectorImpl) InsertIssue(session *types.Session, msg *messages.IssueE
|
|||
issueID,
|
||||
msg.Type,
|
||||
"ISSUE",
|
||||
msg.URL,
|
||||
); err != nil {
|
||||
c.checkError("issuesEvents", err)
|
||||
return fmt.Errorf("can't append to issuesEvents batch: %s", err)
|
||||
|
|
|
|||
|
|
@ -15,13 +15,20 @@ type Producer struct {
|
|||
|
||||
func NewProducer(messageSizeLimit int, useBatch bool) *Producer {
|
||||
kafkaConfig := &kafka.ConfigMap{
|
||||
"enable.idempotence": true,
|
||||
"bootstrap.servers": env.String("KAFKA_SERVERS"),
|
||||
"go.delivery.reports": true,
|
||||
"security.protocol": "plaintext",
|
||||
"go.batch.producer": useBatch,
|
||||
"queue.buffering.max.ms": 100,
|
||||
"message.max.bytes": messageSizeLimit,
|
||||
"enable.idempotence": true,
|
||||
"bootstrap.servers": env.String("KAFKA_SERVERS"),
|
||||
"go.delivery.reports": true,
|
||||
"security.protocol": "plaintext",
|
||||
"go.batch.producer": useBatch,
|
||||
"message.max.bytes": messageSizeLimit, // should be synced with broker config
|
||||
"linger.ms": 1000,
|
||||
"queue.buffering.max.ms": 1000,
|
||||
"batch.num.messages": 1000,
|
||||
"queue.buffering.max.messages": 1000,
|
||||
"retries": 3,
|
||||
"retry.backoff.ms": 100,
|
||||
"max.in.flight.requests.per.connection": 1,
|
||||
"compression.type": env.String("COMPRESSION_TYPE"),
|
||||
}
|
||||
// Apply ssl configuration
|
||||
if env.Bool("KAFKA_USE_SSL") {
|
||||
|
|
|
|||
|
|
@ -6,34 +6,6 @@ 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 BatchMetadata(Message):
|
||||
__id__ = 81
|
||||
|
||||
def __init__(self, version, page_no, first_index, timestamp, location):
|
||||
self.version = version
|
||||
self.page_no = page_no
|
||||
self.first_index = first_index
|
||||
self.timestamp = timestamp
|
||||
self.location = location
|
||||
|
||||
|
||||
class PartitionedMessage(Message):
|
||||
__id__ = 82
|
||||
|
||||
def __init__(self, part_no, part_total):
|
||||
self.part_no = part_no
|
||||
self.part_total = part_total
|
||||
|
||||
|
||||
class Timestamp(Message):
|
||||
__id__ = 0
|
||||
|
||||
|
|
@ -586,7 +558,7 @@ class SetCSSDataURLBased(Message):
|
|||
self.base_url = base_url
|
||||
|
||||
|
||||
class IssueEvent(Message):
|
||||
class IssueEventDeprecated(Message):
|
||||
__id__ = 62
|
||||
|
||||
def __init__(self, message_id, timestamp, type, context_string, context, payload):
|
||||
|
|
@ -727,6 +699,47 @@ class JSException(Message):
|
|||
self.metadata = metadata
|
||||
|
||||
|
||||
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 BatchMetadata(Message):
|
||||
__id__ = 81
|
||||
|
||||
def __init__(self, version, page_no, first_index, timestamp, location):
|
||||
self.version = version
|
||||
self.page_no = page_no
|
||||
self.first_index = first_index
|
||||
self.timestamp = timestamp
|
||||
self.location = location
|
||||
|
||||
|
||||
class PartitionedMessage(Message):
|
||||
__id__ = 82
|
||||
|
||||
def __init__(self, part_no, part_total):
|
||||
self.part_no = part_no
|
||||
self.part_total = part_total
|
||||
|
||||
|
||||
class IssueEvent(Message):
|
||||
__id__ = 125
|
||||
|
||||
def __init__(self, message_id, timestamp, type, context_string, context, payload, url):
|
||||
self.message_id = message_id
|
||||
self.timestamp = timestamp
|
||||
self.type = type
|
||||
self.context_string = context_string
|
||||
self.context = context
|
||||
self.payload = payload
|
||||
self.url = url
|
||||
|
||||
|
||||
class SessionEnd(Message):
|
||||
__id__ = 126
|
||||
|
||||
|
|
|
|||
|
|
@ -76,28 +76,6 @@ class MessageCodec(Codec):
|
|||
|
||||
def read_head_message(self, reader: io.BytesIO, message_id) -> Message:
|
||||
|
||||
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 == 81:
|
||||
return BatchMetadata(
|
||||
version=self.read_uint(reader),
|
||||
page_no=self.read_uint(reader),
|
||||
first_index=self.read_uint(reader),
|
||||
timestamp=self.read_int(reader),
|
||||
location=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 82:
|
||||
return PartitionedMessage(
|
||||
part_no=self.read_uint(reader),
|
||||
part_total=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 0:
|
||||
return Timestamp(
|
||||
timestamp=self.read_uint(reader)
|
||||
|
|
@ -539,7 +517,7 @@ class MessageCodec(Codec):
|
|||
)
|
||||
|
||||
if message_id == 62:
|
||||
return IssueEvent(
|
||||
return IssueEventDeprecated(
|
||||
message_id=self.read_uint(reader),
|
||||
timestamp=self.read_uint(reader),
|
||||
type=self.read_string(reader),
|
||||
|
|
@ -647,6 +625,39 @@ class MessageCodec(Codec):
|
|||
metadata=self.read_string(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 == 81:
|
||||
return BatchMetadata(
|
||||
version=self.read_uint(reader),
|
||||
page_no=self.read_uint(reader),
|
||||
first_index=self.read_uint(reader),
|
||||
timestamp=self.read_int(reader),
|
||||
location=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 82:
|
||||
return PartitionedMessage(
|
||||
part_no=self.read_uint(reader),
|
||||
part_total=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 125:
|
||||
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),
|
||||
url=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 126:
|
||||
return SessionEnd(
|
||||
timestamp=self.read_uint(reader),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { fetchUserInfo } from 'Duck/user';
|
|||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
import Header from 'Components/Header/Header';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||
import { withStore } from 'App/mstore';
|
||||
|
||||
import APIClient from './api_client';
|
||||
|
|
@ -114,7 +113,6 @@ const MULTIVIEW_INDEX_PATH = routes.multiviewIndex();
|
|||
fetchTenants,
|
||||
setSessionPath,
|
||||
fetchSiteList,
|
||||
fetchAlerts,
|
||||
}
|
||||
)
|
||||
class Router extends React.Component {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import logger from 'App/logger';
|
||||
import APIClient from './api_client';
|
||||
import { UPDATE_JWT } from './duck/user';
|
||||
import { FETCH_ACCOUNT, UPDATE_JWT } from './duck/user';
|
||||
|
||||
export default () => (next) => (action) => {
|
||||
const { types, call, ...rest } = action;
|
||||
|
|
@ -14,7 +14,7 @@ export default () => (next) => (action) => {
|
|||
return call(client)
|
||||
.then(async (response) => {
|
||||
if (response.status === 403) {
|
||||
next({ type: UPDATE_JWT, data: null });
|
||||
next({ type: FETCH_ACCOUNT.FAILURE });
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
import { client, CLIENT_TABS } from 'App/routes';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './alertForm.module.css';
|
||||
import DropdownChips from './DropdownChips';
|
||||
import { validateEmail } from 'App/validate';
|
||||
import cn from 'classnames';
|
||||
import { fetchTriggerOptions } from 'Duck/alerts';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const thresholdOptions = [
|
||||
|
|
@ -44,36 +43,38 @@ const Section = ({ index, title, description, content }) => (
|
|||
</div>
|
||||
);
|
||||
|
||||
const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS);
|
||||
|
||||
const AlertForm = (props) => {
|
||||
function AlertForm(props) {
|
||||
const {
|
||||
instance,
|
||||
slackChannels,
|
||||
msTeamsChannels,
|
||||
webhooks,
|
||||
loading,
|
||||
onDelete,
|
||||
deleting,
|
||||
triggerOptions,
|
||||
style = { width: '580px', height: '100vh' },
|
||||
} = props;
|
||||
const write = ({ target: { value, name } }) => props.edit({ [name]: value });
|
||||
const writeOption = (e, { name, value }) => props.edit({ [name]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name } }) => props.edit({ [name]: checked });
|
||||
const { alertsStore } = useStore()
|
||||
const {
|
||||
triggerOptions,
|
||||
loading,
|
||||
} = alertsStore
|
||||
const instance = alertsStore.instance
|
||||
const deleting = loading
|
||||
|
||||
const write = ({ target: { value, name } }) => alertsStore.edit({ [name]: value });
|
||||
const writeOption = (e, { name, value }) => alertsStore.edit({ [name]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name } }) => alertsStore.edit({ [name]: checked });
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchTriggerOptions();
|
||||
void alertsStore.fetchTriggerOptions();
|
||||
}, []);
|
||||
|
||||
const writeQueryOption = (e, { name, value }) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name]: value } });
|
||||
alertsStore.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
||||
const writeQuery = ({ target: { value, name } }) => {
|
||||
const { query } = instance;
|
||||
props.edit({ query: { ...query, [name]: value } });
|
||||
alertsStore.edit({ query: { ...query, [name]: value } });
|
||||
};
|
||||
|
||||
const metric =
|
||||
|
|
@ -111,7 +112,7 @@ const AlertForm = (props) => {
|
|||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={(e, { name, value }) => props.edit({ [name]: value })}
|
||||
onSelect={(e, { name, value }) => alertsStore.edit({ [name]: value })}
|
||||
value={{ value: instance.detectionMethod }}
|
||||
list={[
|
||||
{ name: 'Threshold', value: 'threshold' },
|
||||
|
|
@ -293,7 +294,7 @@ const AlertForm = (props) => {
|
|||
selected={instance.slackInput}
|
||||
options={slackChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => props.edit({ slackInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({ slackInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -307,7 +308,7 @@ const AlertForm = (props) => {
|
|||
selected={instance.msteamsInput}
|
||||
options={msTeamsChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => props.edit({ msteamsInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({ msteamsInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -322,7 +323,7 @@ const AlertForm = (props) => {
|
|||
validate={validateEmail}
|
||||
selected={instance.emailInput}
|
||||
placeholder="Type and press Enter key"
|
||||
onChange={(selected) => props.edit({ emailInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({ emailInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -336,7 +337,7 @@ const AlertForm = (props) => {
|
|||
selected={instance.webhookInput}
|
||||
options={webhooks}
|
||||
placeholder="Select Webhook"
|
||||
onChange={(selected) => props.edit({ webhookInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({ webhookInput: selected })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -378,12 +379,4 @@ const AlertForm = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
triggerOptions: state.getIn(['alerts', 'triggerOptions']),
|
||||
loading: state.getIn(['alerts', 'saveRequest', 'loading']),
|
||||
deleting: state.getIn(['alerts', 'removeRequest', 'loading']),
|
||||
}),
|
||||
{ fetchTriggerOptions }
|
||||
)(AlertForm);
|
||||
export default observer(AlertForm);
|
||||
|
|
|
|||
|
|
@ -1,44 +1,52 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { SlideModal, IconButton } from 'UI';
|
||||
import { init, edit, save, remove } from 'Duck/alerts';
|
||||
import { fetchList as fetchWebhooks } from 'Duck/webhook';
|
||||
import { SlideModal } from 'UI';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import AlertForm from '../AlertForm';
|
||||
import { connect } from 'react-redux';
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
|
||||
import { SLACK, TEAMS, WEBHOOK } from 'App/constants/schedule';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
interface Select {
|
||||
label: string;
|
||||
value: string | number
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
showModal?: boolean;
|
||||
metricId?: number;
|
||||
onClose?: () => void;
|
||||
webhooks: any;
|
||||
fetchWebhooks: Function;
|
||||
save: Function;
|
||||
remove: Function;
|
||||
init: Function;
|
||||
edit: Function;
|
||||
}
|
||||
function AlertFormModal(props: Props) {
|
||||
const { metricId = null, showModal = false, webhooks } = props;
|
||||
const { alertsStore, settingsStore } = useStore()
|
||||
const { metricId = null, showModal = false } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const webhooks = settingsStore.webhooks
|
||||
useEffect(() => {
|
||||
props.fetchWebhooks();
|
||||
settingsStore.fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const slackChannels = webhooks
|
||||
.filter((hook) => hook.type === SLACK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, text: name }))
|
||||
.toJS();
|
||||
const hooks = webhooks
|
||||
.filter((hook) => hook.type === WEBHOOK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, text: name }))
|
||||
.toJS();
|
||||
|
||||
const slackChannels: Select[] = []
|
||||
const hooks: Select[] = []
|
||||
const msTeamsChannels: Select[] = []
|
||||
|
||||
webhooks.forEach((hook) => {
|
||||
const option = { value: hook.webhookId, label: hook.name }
|
||||
if (hook.type === SLACK) {
|
||||
slackChannels.push(option)
|
||||
}
|
||||
if (hook.type === WEBHOOK) {
|
||||
hooks.push(option)
|
||||
}
|
||||
if (hook.type === TEAMS) {
|
||||
msTeamsChannels.push(option)
|
||||
}
|
||||
})
|
||||
|
||||
const saveAlert = (instance) => {
|
||||
const wasUpdating = instance.exists();
|
||||
props.save(instance).then(() => {
|
||||
alertsStore.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toggleForm(null, false);
|
||||
}
|
||||
|
|
@ -56,7 +64,7 @@ function AlertFormModal(props: Props) {
|
|||
confirmation: `Are you sure you want to permanently delete this alert?`,
|
||||
})
|
||||
) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
alertsStore.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
|
|
@ -64,7 +72,7 @@ function AlertFormModal(props: Props) {
|
|||
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
props.init(instance);
|
||||
alertsStore.init(instance);
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
};
|
||||
|
|
@ -73,7 +81,7 @@ function AlertFormModal(props: Props) {
|
|||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{'Create Alert'}</span>
|
||||
<span className="m-3">{'Create Alert'}</span>
|
||||
</div>
|
||||
}
|
||||
isDisplayed={showModal}
|
||||
|
|
@ -83,8 +91,9 @@ function AlertFormModal(props: Props) {
|
|||
showModal && (
|
||||
<AlertForm
|
||||
metricId={metricId}
|
||||
edit={props.edit}
|
||||
edit={alertsStore.edit}
|
||||
slackChannels={slackChannels}
|
||||
msTeamsChannels={msTeamsChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={props.onClose}
|
||||
|
|
@ -97,10 +106,4 @@ function AlertFormModal(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
}),
|
||||
{ init, edit, save, remove, fetchWebhooks, setShowAlerts }
|
||||
)(AlertFormModal);
|
||||
export default observer(AlertFormModal);
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames';
|
||||
import stl from './alertItem.module.css';
|
||||
import AlertTypeLabel from './AlertTypeLabel';
|
||||
|
||||
const AlertItem = props => {
|
||||
const { alert, onEdit, active } = props;
|
||||
|
||||
const getThreshold = threshold => {
|
||||
if (threshold === 15) return '15 Minutes';
|
||||
if (threshold === 30) return '30 Minutes';
|
||||
if (threshold === 60) return '1 Hour';
|
||||
if (threshold === 120) return '2 Hours';
|
||||
if (threshold === 240) return '4 Hours';
|
||||
if (threshold === 1440) return '1 Day';
|
||||
}
|
||||
|
||||
const getNotifyChannel = alert => {
|
||||
let str = '';
|
||||
if (alert.msteams)
|
||||
str = 'MS Teams'
|
||||
if (alert.slack)
|
||||
str = 'Slack';
|
||||
if (alert.email)
|
||||
str += (str === '' ? '' : ' and ')+ 'Email';
|
||||
if (alert.webhool)
|
||||
str += (str === '' ? '' : ' and ')+ 'Webhook';
|
||||
if (str === '')
|
||||
return 'OpenReplay';
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
const isThreshold = alert.detectionMethod === 'threshold';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(stl.wrapper, 'p-4 py-6 relative group cursor-pointer', { [stl.active]: active })}
|
||||
onClick={onEdit}
|
||||
id="alert-item"
|
||||
>
|
||||
<AlertTypeLabel type={alert.detectionMethod} />
|
||||
<div className="capitalize font-medium">{alert.name}</div>
|
||||
<div className="mt-2 text-sm color-gray-medium">
|
||||
{alert.detectionMethod === 'threshold' && (
|
||||
<div>When <span className="italic font-medium">{alert.metric.text}</span> is {alert.condition.text} <span className="italic font-medium">{alert.query.right} {alert.metric.unit}</span> over the past <span className="italic font-medium">{getThreshold(alert.currentPeriod)}</span>, notify me on <span>{getNotifyChannel(alert)}</span>.</div>
|
||||
)}
|
||||
|
||||
{alert.detectionMethod === 'change' && (
|
||||
<div>When the <span className="italic font-medium">{alert.options.change}</span> of <span className="italic font-medium">{alert.metric.text}</span> is {alert.condition.text} <span className="italic font-medium">{alert.query.right} {alert.metric.unit}</span> over the past <span className="italic font-medium">{getThreshold(alert.currentPeriod)}</span> compared to the previous <span className="italic font-medium">{getThreshold(alert.previousPeriod)}</span>, notify me on {getNotifyChannel(alert)}.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AlertItem
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import stl from './alertTypeLabel.module.css'
|
||||
|
||||
function AlertTypeLabel({ filterKey, type = '' }) {
|
||||
return (
|
||||
<div className={ cn("rounded-full px-2 text-xs mb-2 fit-content uppercase color-gray-darkest", stl.wrapper, { [stl.alert] : filterKey === 'alert', }) }>
|
||||
{ type }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AlertTypeLabel
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import AlertsList from './AlertsList';
|
||||
import { SlideModal, IconButton } from 'UI';
|
||||
import { init, edit, save, remove } from 'Duck/alerts';
|
||||
import { fetchList as fetchWebhooks } from 'Duck/webhook';
|
||||
import AlertForm from './AlertForm';
|
||||
import { connect } from 'react-redux';
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
const Alerts = (props) => {
|
||||
const { webhooks, setShowAlerts } = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const slackChannels = webhooks
|
||||
.filter((hook) => hook.type === SLACK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
.toJS();
|
||||
const hooks = webhooks
|
||||
.filter((hook) => hook.type === WEBHOOK)
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
.toJS();
|
||||
|
||||
const saveAlert = (instance) => {
|
||||
const wasUpdating = instance.exists();
|
||||
props.save(instance).then(() => {
|
||||
if (!wasUpdating) {
|
||||
toast.success('New alert saved');
|
||||
toggleForm(null, false);
|
||||
} else {
|
||||
toast.success('Alert updated');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async (instance) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`,
|
||||
})
|
||||
) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
toggleForm(null, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleForm = (instance, state) => {
|
||||
if (instance) {
|
||||
props.init(instance);
|
||||
}
|
||||
return setShowForm(state ? state : !showForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SlideModal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3">{'Alerts'}</span>
|
||||
<IconButton circle size="small" icon="plus" outline id="add-button" onClick={() => toggleForm({}, true)} />
|
||||
</div>
|
||||
}
|
||||
isDisplayed={true}
|
||||
onClose={() => {
|
||||
toggleForm({}, false);
|
||||
setShowAlerts(false);
|
||||
}}
|
||||
size="small"
|
||||
content={
|
||||
<AlertsList
|
||||
onEdit={(alert) => {
|
||||
toggleForm(alert, true);
|
||||
}}
|
||||
onClickCreate={() => toggleForm({}, true)}
|
||||
/>
|
||||
}
|
||||
detailContent={
|
||||
showForm && (
|
||||
<AlertForm
|
||||
edit={props.edit}
|
||||
slackChannels={slackChannels}
|
||||
webhooks={hooks}
|
||||
onSubmit={saveAlert}
|
||||
onClose={() => toggleForm({}, false)}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
}),
|
||||
{ init, edit, save, remove, fetchWebhooks, setShowAlerts }
|
||||
)(Alerts);
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Loader, NoContent, Input, Button } from 'UI';
|
||||
import AlertItem from './AlertItem';
|
||||
import { fetchList, init } from 'Duck/alerts';
|
||||
import { connect } from 'react-redux';
|
||||
import { getRE } from 'App/utils';
|
||||
|
||||
const AlertsList = (props) => {
|
||||
const { loading, list, instance, onEdit } = props;
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
const filterRE = getRE(query, 'i');
|
||||
const _filteredList = list.filter(({ name, query: { left } }) => filterRE.test(name) || filterRE.test(left));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 w-full px-3">
|
||||
<Input name="searchQuery" placeholder="Search by Name or Metric" onChange={({ target: { value } }) => setQuery(value)} />
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title="No alerts have been setup yet."
|
||||
subtext={
|
||||
<div className="flex flex-col items-center">
|
||||
<div>Alerts helps your team stay up to date with the activity on your app.</div>
|
||||
<Button variant="primary" className="mt-4" icon="plus" onClick={props.onClickCreate}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.size === 0}
|
||||
>
|
||||
<div className="bg-white">
|
||||
{_filteredList.map((a) => (
|
||||
<div className="border-b" key={a.key}>
|
||||
<AlertItem active={instance.alertId === a.alertId} alert={a} onEdit={() => onEdit(a.toData())} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
list: state.getIn(['alerts', 'list']).sort((a, b) => b.createdAt - a.createdAt),
|
||||
instance: state.getIn(['alerts', 'instance']),
|
||||
loading: state.getIn(['alerts', 'loading']),
|
||||
}),
|
||||
{ fetchList, init }
|
||||
)(AlertsList);
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'UI';
|
||||
import stl from './listItem.module.css';
|
||||
import cn from 'classnames';
|
||||
import AlertTypeLabel from '../../AlertTypeLabel';
|
||||
|
||||
const ListItem = ({ alert, onClear, loading, onNavigate }) => {
|
||||
return (
|
||||
<div className={cn(stl.wrapper, 'group', { [stl.viewed] : alert.viewed })}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm">{alert.createdAt && alert.createdAt.toFormat('LLL dd, yyyy, hh:mm a')}</div>
|
||||
<div className={ cn("invisible", { 'group-hover:visible' : !alert.viewed})} >
|
||||
<Button variant="text" loading={loading}>
|
||||
<span className={ cn("text-sm color-gray-medium", { 'invisible' : loading })} onClick={onClear}>{'IGNORE'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AlertTypeLabel
|
||||
type={alert.options.sourceMeta}
|
||||
filterKey={alert.filterKey}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-2 flex items-center">
|
||||
{alert.title}
|
||||
</h2>
|
||||
<div className="mb-2 text-sm text-justify break-all">{alert.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItem
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ListItem';
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.viewed {
|
||||
background-color: $gray-lightest;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
.wrapper {
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $active-blue;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
color: $gray-dark;
|
||||
border: solid thin $gray-light;
|
||||
}
|
||||
|
||||
.alert {
|
||||
background: #C3E9EA;
|
||||
color: #32888C;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { List } from 'immutable';
|
||||
import Alert from 'Types/alert';
|
||||
import Notification from 'Types/notification';
|
||||
import Alerts from '.';
|
||||
import Notifications from './Notifications';
|
||||
import AlertsList from './AlertsList';
|
||||
import AlertForm from './AlertForm';
|
||||
|
||||
const list = [
|
||||
{
|
||||
"alertId": 2,
|
||||
"projectId": 1,
|
||||
"name": "new alert",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 240,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1591893324078,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
},
|
||||
],
|
||||
"LastNotification": 1592929583000,
|
||||
"renotifyInterval": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 14,
|
||||
"projectId": 1,
|
||||
"name": "alert 19.06",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 3000.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592579750935,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
}
|
||||
],
|
||||
"renotifyInterval": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 15,
|
||||
"projectId": 1,
|
||||
"name": "notify every 60min",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592848779604,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
},
|
||||
],
|
||||
"LastNotification": 1599135058000,
|
||||
"renotifyInterval": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 21,
|
||||
"projectId": 1,
|
||||
"name": "always notify",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592849011350,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
}
|
||||
],
|
||||
"LastNotification": 1599135058000,
|
||||
"renotifyInterval": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const notifications = List([
|
||||
{ title: 'test', type: 'change', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
])
|
||||
storiesOf('Alerts', module)
|
||||
.add('Alerts', () => (
|
||||
<Alerts />
|
||||
))
|
||||
.add('List', () => (
|
||||
<AlertsList list={List(list).map(Alert)} />
|
||||
))
|
||||
.add('AlertForm', () => (
|
||||
<AlertForm />
|
||||
))
|
||||
.add('AlertNotifications', () => (
|
||||
<Notifications announcements={notifications.map(Notification)} />
|
||||
))
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Alerts'
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { Button, Modal, Form, Icon, Checkbox, Input } from 'UI';
|
||||
import { Button, Modal, Form, Icon, Input } from 'UI';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Icon, ItemMenu, Tooltip } from 'UI';
|
||||
import { Icon, ItemMenu } from 'UI';
|
||||
import { durationFromMs, formatTimeOrDate } from 'App/date';
|
||||
import { IRecord } from 'App/services/RecordingsService';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||
import { Button, Tooltip } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { toggleChatWindow } from 'Duck/sessions';
|
||||
import ChatWindow from '../../ChatWindow';
|
||||
// state enums
|
||||
import {
|
||||
CallingState,
|
||||
ConnectionStatus,
|
||||
|
|
@ -12,7 +10,7 @@ import {
|
|||
RequestLocalStream,
|
||||
} from 'Player';
|
||||
import type { LocalStream } from 'Player';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { PlayerContext, ILivePlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { toast } from 'react-toastify';
|
||||
import { confirm } from 'UI';
|
||||
|
|
@ -30,15 +28,10 @@ function onError(e: any) {
|
|||
|
||||
interface Props {
|
||||
userId: string;
|
||||
calling: CallingState;
|
||||
annotating: boolean;
|
||||
peerConnectionStatus: ConnectionStatus;
|
||||
remoteControlStatus: RemoteControlStatus;
|
||||
hasPermission: boolean;
|
||||
isEnterprise: boolean;
|
||||
isCallActive: boolean;
|
||||
agentIds: string[];
|
||||
livePlay: boolean;
|
||||
userDisplayName: string;
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +43,8 @@ function AssistActions({
|
|||
agentIds,
|
||||
userDisplayName,
|
||||
}: Props) {
|
||||
const { player, store } = React.useContext(PlayerContext)
|
||||
// @ts-ignore ???
|
||||
const { player, store } = React.useContext<ILivePlayerContext>(PlayerContext)
|
||||
|
||||
const {
|
||||
assistManager: {
|
||||
|
|
@ -123,6 +117,7 @@ function AssistActions({
|
|||
|
||||
const addIncomeStream = (stream: MediaStream) => {
|
||||
setIncomeStream((oldState) => {
|
||||
if (oldState === null) return [stream]
|
||||
if (!oldState.find((existingStream) => existingStream.id === stream.id)) {
|
||||
return [...oldState, stream];
|
||||
}
|
||||
|
|
@ -257,8 +252,7 @@ const con = connect(
|
|||
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
|
||||
userDisplayName: state.getIn(['sessions', 'current']).userDisplayName,
|
||||
};
|
||||
},
|
||||
{ toggleChatWindow }
|
||||
}
|
||||
);
|
||||
|
||||
export default con(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SlideModal, Avatar, TextEllipsis, Icon } from 'UI';
|
||||
import React, { useState } from 'react';
|
||||
import SessionList from '../SessionList';
|
||||
import stl from './assistTabs.module.css'
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ const AssistTabs = (props: Props) => {
|
|||
<>
|
||||
<div
|
||||
className={stl.btnLink}
|
||||
onClick={() => showModal(<SessionList userId={props.userId} />, { right: true })}
|
||||
onClick={() => showModal(<SessionList userId={props.userId} />, { right: true, width: 700 })}
|
||||
>
|
||||
Active Sessions
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,45 +24,43 @@ function SessionList(props: Props) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ width: '50vw' }}>
|
||||
<div
|
||||
className="border-r shadow h-screen overflow-y-auto"
|
||||
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="text-2xl">
|
||||
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
|
||||
</div>
|
||||
<div
|
||||
className="border-r shadow h-screen overflow-y-auto"
|
||||
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="text-2xl">
|
||||
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
|
||||
</div>
|
||||
<Loader loading={props.loading}>
|
||||
<NoContent
|
||||
show={!props.loading && props.list.length === 0}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={170} />
|
||||
<div className="mt-2" />
|
||||
<div className="text-center text-gray-600">No live sessions found.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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 compact={true} onClick={() => hideModal()} key={session.sessionId} session={session} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
<Loader loading={props.loading}>
|
||||
<NoContent
|
||||
show={!props.loading && props.list.length === 0}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={170} />
|
||||
<div className="mt-2" />
|
||||
<div className="text-center text-gray-600">No live sessions found.</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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 compact={true} onClick={() => hideModal()} key={session.sessionId} session={session} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function AuditDetailModal(props: Props) {
|
|||
// console.log('jsonResponse', jsonResponse)
|
||||
|
||||
return (
|
||||
<div style={{ width: '500px' }} className="bg-white h-screen overflow-y-auto">
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h1 className="text-2xl p-4">Audit Details</h1>
|
||||
<div className="p-4">
|
||||
<h5 className="mb-2">{ 'URL'}</h5>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function AuditList(props: Props) {
|
|||
<AuditListItem
|
||||
key={index}
|
||||
audit={item}
|
||||
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true })}
|
||||
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true, width: 500 })}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class CustomFieldForm extends React.PureComponent {
|
|||
const { field, errors } = this.props;
|
||||
const exists = field.exists();
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<div className="bg-white h-screen overflow-y-auto">
|
||||
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Input, Form, Button, Checkbox, Loader } from 'UI';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import { save, init, edit, remove } from 'Duck/integrations/actions';
|
||||
import { fetchIntegrationList } from 'Duck/integrations/integrations';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import cn from 'classnames';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
integration: any;
|
||||
|
|
|
|||
|
|
@ -63,11 +63,11 @@ function Integrations(props: Props) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const onClick = (integration: any) => {
|
||||
const onClick = (integration: any, width: number) => {
|
||||
if (integration.slug) {
|
||||
props.fetch(integration.slug, props.siteId);
|
||||
}
|
||||
showModal(integration.component, { right: true });
|
||||
showModal(integration.component, { right: true, width });
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }: any) => {
|
||||
|
|
@ -100,7 +100,7 @@ function Integrations(props: Props) {
|
|||
integrated={integratedList.includes(integration.slug)}
|
||||
key={integration.name}
|
||||
integration={integration}
|
||||
onClick={() => onClick(integration)}
|
||||
onClick={() => onClick(integration, cat.title === "Plugins" ? 500 : 350)}
|
||||
hide={
|
||||
(integration.slug === 'github' && integratedList.includes('jira')) ||
|
||||
(integration.slug === 'jira' && integratedList.includes('github'))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import SlackChannelList from './SlackChannelList/SlackChannelList';
|
|||
import { fetchList, init } from 'Duck/integrations/slack';
|
||||
import { connect } from 'react-redux';
|
||||
import SlackAddForm from './SlackAddForm';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { Button } from 'UI';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Input, Form } from 'UI';
|
||||
import { updateAccount, updateClient } from 'Duck/user';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Tooltip, Button, IconButton } from 'UI';
|
||||
import { Tooltip, Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function InstallButton(props: Props) {
|
|||
const onClick = () => {
|
||||
showModal(
|
||||
<TrackingCodeModal title="Tracking Code" subTitle={`(Unique to ${site.host})`} onClose={hideModal} site={site} />,
|
||||
{ right: true }
|
||||
{ right: true, width: 700 }
|
||||
);
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import styles from './client.module.css';
|
||||
|
||||
const TabItem = ({ active = false, onClick, icon, label }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function UserForm(props: Props) {
|
|||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="bg-white h-screen p-6" style={{ width: '400px'}}>
|
||||
<div className="bg-white h-screen p-6">
|
||||
<div className="">
|
||||
<h1 className="text-2xl mb-4">{`${user.exists() ? 'Update' : 'Invite'} User`}</h1>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,94 +1,74 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit, save } from 'Duck/webhook';
|
||||
import { Form, Button, Input } from 'UI';
|
||||
import styles from './webhookForm.module.css';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
webhook: state.getIn(['webhooks', 'instance']),
|
||||
loading: state.getIn(['webhooks', 'saveRequest', 'loading']),
|
||||
}),
|
||||
{
|
||||
edit,
|
||||
save,
|
||||
}
|
||||
)
|
||||
class WebhookForm extends React.PureComponent {
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [name]: value });
|
||||
function WebhookForm(props) {
|
||||
const { settingsStore } = useStore()
|
||||
const { webhookInst: webhook, hooksLoading: loading, saveWebhook, editWebhook } = settingsStore
|
||||
const write = ({ target: { value, name } }) => editWebhook({ [name]: value });
|
||||
|
||||
save = () => {
|
||||
this.props.save(this.props.webhook).then(() => {
|
||||
this.props.onClose();
|
||||
const save = () => {
|
||||
saveWebhook(webhook).then(() => {
|
||||
props.onClose();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { webhook, loading } = this.props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{webhook.exists() ? 'Update' : 'Add'} Webhook</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="name"
|
||||
value={webhook.name}
|
||||
onChange={this.write}
|
||||
placeholder="Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Endpoint'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="endpoint"
|
||||
value={webhook.endpoint}
|
||||
onChange={this.write}
|
||||
placeholder="Endpoint"
|
||||
/>
|
||||
</Form.Field>
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{webhook.exists() ? 'Update' : 'Add'} Webhook</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input
|
||||
name="name"
|
||||
value={webhook.name}
|
||||
onChange={write}
|
||||
placeholder="Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Auth Header (optional)'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="authHeader"
|
||||
value={webhook.authHeader}
|
||||
onChange={this.write}
|
||||
placeholder="Auth Header"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>{'Endpoint'}</label>
|
||||
<Input
|
||||
name="endpoint"
|
||||
value={webhook.endpoint}
|
||||
onChange={write}
|
||||
placeholder="Endpoint"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={this.save}
|
||||
disabled={!webhook.validate()}
|
||||
loading={loading}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{webhook.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{webhook.exists() && <Button onClick={this.props.onClose}>{'Cancel'}</Button>}
|
||||
</div>
|
||||
{webhook.exists() && <Button icon="trash" variant="text" onClick={() => this.props.onDelete(webhook.webhookId)}></Button>}
|
||||
<Form.Field>
|
||||
<label>{'Auth Header (optional)'}</label>
|
||||
<Input
|
||||
name="authHeader"
|
||||
value={webhook.authHeader}
|
||||
onChange={write}
|
||||
placeholder="Auth Header"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={save}
|
||||
disabled={!webhook.validate()}
|
||||
loading={loading}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{webhook.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{webhook.exists() && <Button onClick={props.onClose}>{'Cancel'}</Button>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{webhook.exists() &&
|
||||
<Button icon="trash" variant="text" onClick={() => props.onDelete(webhook.webhookId)}></Button>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebhookForm;
|
||||
export default observer(WebhookForm);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Button, Loader, NoContent, Icon } from 'UI';
|
||||
import { init, fetchList, remove } from 'Duck/webhook';
|
||||
import WebhookForm from './WebhookForm';
|
||||
import ListItem from './ListItem';
|
||||
import styles from './webhooks.module.css';
|
||||
|
|
@ -11,79 +9,71 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
|||
import { confirm } from 'UI';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
function Webhooks(props) {
|
||||
const { webhooks, loading } = props;
|
||||
const { showModal, hideModal } = useModal();
|
||||
function Webhooks() {
|
||||
const { settingsStore } = useStore()
|
||||
const { webhooks, hooksLoading: loading } = settingsStore;
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const noSlackWebhooks = webhooks.filter((hook) => hook.type === 'webhook');
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
const noSlackWebhooks = webhooks.filter((hook) => hook.type === 'webhook');
|
||||
useEffect(() => {
|
||||
void settingsStore.fetchWebhooks();
|
||||
}, []);
|
||||
|
||||
const init = (v) => {
|
||||
props.init(v);
|
||||
showModal(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
|
||||
};
|
||||
const init = (v) => {
|
||||
settingsStore.initWebhook(v);
|
||||
showModal(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
|
||||
};
|
||||
|
||||
const removeWebhook = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to remove this webhook?`,
|
||||
})
|
||||
) {
|
||||
props.remove(id).then(() => {
|
||||
toast.success('Webhook removed successfully');
|
||||
});
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
const removeWebhook = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to remove this webhook?`,
|
||||
})
|
||||
) {
|
||||
settingsStore.removeWebhook(id).then(() => {
|
||||
toast.success('Webhook removed successfully');
|
||||
});
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(styles.tabHeader, 'px-5 pt-5')}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
|
||||
{/* <Button rounded={true} icon="plus" variant="outline" onClick={() => init()} /> */}
|
||||
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-base text-disabled-text flex items-center my-3 px-5">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
Leverage webhooks to push OpenReplay data to other systems.
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_WEBHOOKS} size={80} />
|
||||
<div className="text-center text-gray-600 my-4">None added yet</div>
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(styles.tabHeader, 'px-5 pt-5')}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
|
||||
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={noSlackWebhooks.size === 0}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{noSlackWebhooks.map((webhook) => (
|
||||
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
|
||||
<div className="text-base text-disabled-text flex items-center my-3 px-5">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
Leverage webhooks to push OpenReplay data to other systems.
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_WEBHOOKS} size={80} />
|
||||
<div className="text-center text-gray-600 my-4">None added yet</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={noSlackWebhooks.length === 0}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{noSlackWebhooks.map((webhook) => (
|
||||
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
loading: state.getIn(['webhooks', 'loading']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
remove,
|
||||
}
|
||||
)(withPageTitle('Webhooks - OpenReplay Preferences')(Webhooks));
|
||||
export default withPageTitle('Webhooks - OpenReplay Preferences')(observer(Webhooks));
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react'
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, LAST_7_DAYS, LAST_30_DAYS, CUSTOM_RANGE } from 'Types/app/period';
|
||||
import { ALL, DESKTOP, MOBILE } from 'Types/app/platform';
|
||||
import { connect } from 'react-redux';
|
||||
import { setPeriod, setPlatform } from 'Duck/dashboard';
|
||||
import cn from 'classnames';
|
||||
import styles from './DashboardHeader.module.css';
|
||||
import Filters from '../Filters/Filters';
|
||||
|
||||
export const PERIOD_OPTIONS = [
|
||||
{ text: 'Past 30 Min', value: LAST_30_MINUTES },
|
||||
{ text: 'Past 24 Hours', value: LAST_24_HOURS },
|
||||
{ text: 'Past 7 Days', value: LAST_7_DAYS },
|
||||
{ text: 'Past 30 Days', value: LAST_30_DAYS },
|
||||
{ text: 'Choose Date', value: CUSTOM_RANGE },
|
||||
];
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ text: 'All Platforms', value: ALL },
|
||||
{ text: 'Desktop', value: DESKTOP },
|
||||
{ text: 'Mobile', value: MOBILE }
|
||||
];
|
||||
|
||||
const DashboardHeader = props => {
|
||||
return (
|
||||
<div className={ cn(styles.header, 'w-full') }>
|
||||
<Filters />
|
||||
|
||||
<div className="flex items-center hidden">
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
period: state.getIn([ 'dashboard', 'period' ]),
|
||||
platform: state.getIn([ 'dashboard', 'platform' ]),
|
||||
currentProjectId: state.getIn([ 'site', 'siteId' ]),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
}), { setPeriod, setPlatform })(DashboardHeader)
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
.dropdown {
|
||||
display: 'flex' !important;
|
||||
align-items: 'center';
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.3s;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background-color: #DDDDDD;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.dateInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: $gray-darkest;
|
||||
|
||||
&:hover {
|
||||
background-color: lightgray;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as DashboardHeader } from './DashboardHeader';
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader } from 'UI';
|
||||
import { msToSec } from 'App/date';
|
||||
import { CountBadge, Divider, widgetHOC } from './common';
|
||||
|
||||
@widgetHOC('applicationActivity')
|
||||
export default class ApplicationActivity extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading } = this.props;
|
||||
return (
|
||||
<div className="flex-1 flex-shrink-0 flex justify-around items-center">
|
||||
<Loader loading={ loading } size="small">
|
||||
<CountBadge
|
||||
title="Avg. Page Load Time"
|
||||
unit="s"
|
||||
icon="window"
|
||||
count={ msToSec(data.avgPageLoad) }
|
||||
change={ data.avgPageLoadProgress }
|
||||
oppositeColors
|
||||
/>
|
||||
<Divider />
|
||||
<CountBadge
|
||||
title="Avg. Image Load Time"
|
||||
unit="ms"
|
||||
icon="eye"
|
||||
count={ data.avgImgLoad }
|
||||
change={ data.avgImgLoadProgress }
|
||||
oppositeColors
|
||||
/>
|
||||
<Divider />
|
||||
<CountBadge
|
||||
title="Avg. Request Load"
|
||||
unit="ms"
|
||||
icon="clock"
|
||||
count={ data.avgReqLoad }
|
||||
change={ data.avgReqLoadProgress }
|
||||
oppositeColors
|
||||
/>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles } from '../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis, Tooltip
|
||||
} from 'recharts';
|
||||
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 28 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 21
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 28
|
||||
if (rangeName === YESTERDAY) params.density = 28
|
||||
if (rangeName === LAST_7_DAYS) params.density = 28
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@widgetHOC('resourcesCountByType', { customParams })
|
||||
export default class BreakdownOfLoadedResources extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading, period, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId={ showSync ? "resourcesCountByType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Resources" }}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="CSS" dataKey="stylesheet" stackId="a" fill={colors[0]} />
|
||||
<Bar name="Images" dataKey="img" stackId="a" fill={colors[2]} />
|
||||
<Bar name="Scripts" dataKey="script" stackId="a" fill={colors[3]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './BreakdownOfLoadedResources';
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { Table, widgetHOC, domain } from '../common';
|
||||
import {
|
||||
Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
|
||||
PieChart, Pie, Cell, Tooltip, ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area } from 'recharts';
|
||||
|
||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042'];
|
||||
const RADIAN = Math.PI / 180;
|
||||
|
||||
@widgetHOC('busiestTimeOfDay', { fitContent: true })
|
||||
export default class BusiestTimeOfTheDay extends React.PureComponent {
|
||||
renderCustomizedLabel = ({
|
||||
cx, cy, midAngle, innerRadius, outerRadius, percent, index,
|
||||
}) => {
|
||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
return (
|
||||
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
||||
{`${(percent * 100).toFixed(0)}%`}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, loading } = this.props;
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<ResponsiveContainer height={ 140 } width="100%">
|
||||
<RadarChart outerRadius={50} width={180} height={180} data={data.toJS()}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="hour" tick={{ fill: '#3EAAAF', fontSize: 12 }} />
|
||||
<PolarRadiusAxis />
|
||||
<Radar name="count" dataKey="count" stroke="#3EAAAF" fill="#3EAAAF" fillOpacity={0.6} />
|
||||
</RadarChart>
|
||||
|
||||
</ResponsiveContainer>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { AreaChart, Area } from 'recharts';
|
||||
|
||||
const Chart = ({ data }) => {
|
||||
return (
|
||||
<AreaChart width={ 90 } height={ 30 } data={ data.chart } >
|
||||
<Area type="monotone" dataKey="avgDuration" stroke="#3EAAAF" fill="#A8E0DA" fillOpacity={ 0.5 } />
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
||||
|
||||
Chart.displayName = 'Chart';
|
||||
|
||||
export default Chart;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { Tooltip, Icon } from 'UI';
|
||||
import styles from './imageInfo.module.css';
|
||||
|
||||
const ImageInfo = ({ data }) => (
|
||||
<div className={styles.name}>
|
||||
<Tooltip
|
||||
className={styles.Tooltip}
|
||||
title={
|
||||
<img
|
||||
src={`//${data.url}`}
|
||||
className={styles.imagePreview}
|
||||
alt="One of the slowest images"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.imageWrapper}>
|
||||
<Icon name="camera-alt" size="18" color="gray-light" />
|
||||
<div className={styles.label}>{'Preview'}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={data.url}>
|
||||
<span>{data.name}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
ImageInfo.displayName = 'ImageInfo';
|
||||
|
||||
export default ImageInfo;
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
margin-right: 10px;
|
||||
& > span {
|
||||
height: 16px;
|
||||
}
|
||||
& .label {
|
||||
font-size: 9px;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #f5f5f5 !important;
|
||||
&:before {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './BusiestTimeOfTheDay';
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { Table, widgetHOC } from '../common';
|
||||
import { getRE } from 'App/utils';
|
||||
import ImageInfo from './ImageInfo';
|
||||
import MethodType from './MethodType';
|
||||
import cn from 'classnames';
|
||||
import stl from './callWithErrors.module.css';
|
||||
|
||||
const cols = [
|
||||
{
|
||||
key: 'method',
|
||||
title: 'Method',
|
||||
className: 'text-left',
|
||||
Component: MethodType,
|
||||
cellClass: 'ml-2',
|
||||
width: '8%',
|
||||
},
|
||||
{
|
||||
key: 'urlHostpath',
|
||||
title: 'Path',
|
||||
Component: ImageInfo,
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
key: 'allRequests',
|
||||
title: 'Requests',
|
||||
className: 'text-left',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
key: '4xx',
|
||||
title: '4xx',
|
||||
className: 'text-left',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
key: '5xx',
|
||||
title: '5xx',
|
||||
className: 'text-left',
|
||||
width: '15%',
|
||||
}
|
||||
];
|
||||
|
||||
@widgetHOC('callsErrors', { fitContent: true })
|
||||
export default class CallWithErrors extends React.PureComponent {
|
||||
state = { search: ''}
|
||||
|
||||
test = (value = '', serach) => getRE(serach, 'i').test(value);
|
||||
|
||||
write = ({ target: { name, value } }) => {
|
||||
this.setState({ [ name ]: value })
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data: images, loading } = this.props;
|
||||
const { search } = this.state;
|
||||
const _data = search ? images.filter(i => this.test(i.urlHostpath, search)) : images;
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<div className={ cn(stl.topActions, 'py-3 flex text-right')}>
|
||||
<input disabled={images.size === 0} className={stl.searchField} name="search" placeholder="Filter by Path" onChange={this.write} />
|
||||
</div>
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ images.size === 0 }
|
||||
>
|
||||
<Table
|
||||
cols={ cols }
|
||||
rows={ _data }
|
||||
isTemplate={this.props.isTemplate}
|
||||
/>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { AreaChart, Area } from 'recharts';
|
||||
|
||||
const Chart = ({ data }) => {
|
||||
return (
|
||||
<AreaChart width={ 90 } height={ 30 } data={ data.chart } >
|
||||
<Area type="monotone" dataKey="avgDuration" stroke="#3EAAAF" fill="#A8E0DA" fillOpacity={ 0.5 } />
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
||||
|
||||
Chart.displayName = 'Chart';
|
||||
|
||||
export default Chart;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react';
|
||||
import { TextEllipsis } from 'UI';
|
||||
import styles from './imageInfo.module.css';
|
||||
|
||||
const ImageInfo = ({ data }) => (
|
||||
<div className={ styles.name }>
|
||||
<TextEllipsis text={data.urlHostpath} />
|
||||
</div>
|
||||
);
|
||||
|
||||
ImageInfo.displayName = 'ImageInfo';
|
||||
|
||||
export default ImageInfo;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react'
|
||||
import { Label } from 'UI';
|
||||
|
||||
const MethodType = ({ data }) => {
|
||||
return (
|
||||
<Label className="ml-1">{data.method}</Label>
|
||||
)
|
||||
}
|
||||
|
||||
export default MethodType
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
.topActions {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 50px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.searchField {
|
||||
padding: 4px 5px;
|
||||
border-bottom: dotted thin $gray-light;
|
||||
border-radius: 3px;
|
||||
&:focus,
|
||||
&:active {
|
||||
border: solid thin transparent !important;
|
||||
box-shadow: none;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
&:hover {
|
||||
border: solid thin $gray-light !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
margin-right: 10px;
|
||||
& > span {
|
||||
height: 16px;
|
||||
}
|
||||
& .label {
|
||||
font-size: 9px;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #f5f5f5 !important;
|
||||
&:before {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CallWithErrors';
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles } from '../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid,
|
||||
LineChart, Line, Legend, Tooltip
|
||||
} from 'recharts';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
if (rangeName === YESTERDAY) params.density = 70
|
||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@widgetHOC('domainsErrors_4xx', { customParams })
|
||||
export default class CallsErrors4xx extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading, period, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
|
||||
const namesMap = data.chart
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce(
|
||||
(unique, item) => (unique.includes(item) ? unique : [...unique, item]),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
data={ data.chart }
|
||||
margin={Styles.chartMargins}
|
||||
syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors[4]} stopOpacity={ 0.9 } />
|
||||
<stop offset="95%" stopColor={colors[4]} stopOpacity={ 0.2 } />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Errors"
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{ namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CallsErrors4xx';
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles } from '../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid,
|
||||
LineChart, Line, Legend, Tooltip
|
||||
} from 'recharts';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
if (rangeName === YESTERDAY) params.density = 70
|
||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@widgetHOC('domainsErrors_5xx', { customParams })
|
||||
export default class CallsErrors5xx extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading, period, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
|
||||
const namesMap = data.chart
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce(
|
||||
(unique, item) => (unique.includes(item) ? unique : [...unique, item]),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
data={ data.chart }
|
||||
margin={Styles.chartMargins}
|
||||
syncId={ showSync ? "domainsErrors_5xx" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Errors"
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{ namesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
// fill="url(#colorCount)"
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CallsErrors5xx';
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles, AvgLabel } from '../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, AreaChart, Area, Tooltip } from 'recharts';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
if (rangeName === YESTERDAY) params.density = 70
|
||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@widgetHOC('cpu', { customParams })
|
||||
export default class CpuLoad extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading, period, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" unit="%" className="ml-3" count={data.avgCpu} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
syncId={ showSync ? "cpu" : undefined }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CpuLoad';
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles } from '../common';
|
||||
import {
|
||||
AreaChart, Area, ResponsiveContainer,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip
|
||||
} from 'recharts';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
if (rangeName === YESTERDAY) params.density = 70
|
||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@widgetHOC('crashes', { customParams })
|
||||
export default class Crashes extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading, period, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
syncId={ showSync ? "crashes" : undefined }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Crashes" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Crashes"
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke={colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Crashes';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import WebPlayer from 'App/components/Session/WebPlayer'
|
||||
import ClickMapRenderer from 'App/components/Session/Player/ClickMapRenderer'
|
||||
import { connect } from 'react-redux'
|
||||
import { setCustomSession } from 'App/duck/sessions'
|
||||
import { fetchInsights } from 'Duck/sessions';
|
||||
|
|
@ -54,10 +54,9 @@ function ClickMapCard({
|
|||
|
||||
return (
|
||||
<div id="clickmap-render">
|
||||
<WebPlayer
|
||||
isClickmap
|
||||
<ClickMapRenderer
|
||||
customSession={metricStore.instance.data}
|
||||
customTimestamp={jumpTimestamp}
|
||||
jumpTimestamp={jumpTimestamp}
|
||||
onMarkerClick={onMarkerClick}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function CustomMetricTableErrors(props: RouteComponentProps & Props) {
|
|||
|
||||
showModal(<ErrorDetailsModal errorId={errorId} />, {
|
||||
right: true,
|
||||
width: 1200,
|
||||
onClose: () => {
|
||||
if (props.history.location.pathname.includes("/dashboard") || props.history.location.pathname.includes("/metrics/")) {
|
||||
props.history.replace({ search: "" });
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
.wrapper {
|
||||
background-color: white;
|
||||
/* border: solid thin $gray-medium; */
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader, NoContent, Icon, Tooltip } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { ResponsiveContainer } from 'recharts';
|
||||
import stl from './CustomMetricWidget.module.css';
|
||||
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
||||
import {
|
||||
init,
|
||||
edit,
|
||||
remove,
|
||||
setActiveWidget,
|
||||
updateActiveState,
|
||||
} from 'Duck/customMetrics';
|
||||
import { setShowAlerts } from 'Duck/dashboard';
|
||||
import CustomMetriLineChart from '../CustomMetriLineChart';
|
||||
import CustomMetricPieChart from '../CustomMetricPieChart';
|
||||
import CustomMetricPercentage from '../CustomMetricPercentage';
|
||||
import CustomMetricTable from '../CustomMetricTable';
|
||||
import { NO_METRIC_DATA } from 'App/constants/messages';
|
||||
|
||||
const customParams = (rangeName) => {
|
||||
const params = { density: 70 };
|
||||
|
||||
// if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
// if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
// if (rangeName === YESTERDAY) params.density = 70
|
||||
// if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
// loading?: boolean;
|
||||
data?: any;
|
||||
compare?: boolean;
|
||||
period?: any;
|
||||
onClickEdit: (e) => void;
|
||||
remove: (id) => void;
|
||||
setShowAlerts: (showAlerts) => void;
|
||||
setAlertMetricId: (id) => void;
|
||||
onAlertClick: (e) => void;
|
||||
init: (metric: any) => void;
|
||||
edit: (setDefault?) => void;
|
||||
setActiveWidget: (widget) => void;
|
||||
updateActiveState: (metricId, state) => void;
|
||||
isTemplate?: boolean;
|
||||
}
|
||||
function CustomMetricWidget(props: Props) {
|
||||
const { metric, period, isTemplate } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<any>([]);
|
||||
// const [seriesMap, setSeriesMap] = useState<any>([]);
|
||||
|
||||
const colors = Styles.customMetricColors;
|
||||
const params = customParams(period.rangeName);
|
||||
// const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart', startDate: period.start, endDate: period.end }
|
||||
const isLineChart = metric.viewType === 'lineChart';
|
||||
const isProgress = metric.viewType === 'progress';
|
||||
const isTable = metric.viewType === 'table';
|
||||
const isPieChart = metric.viewType === 'pieChart';
|
||||
|
||||
const clickHandlerTable = (filters) => {
|
||||
const activeWidget = {
|
||||
widget: metric,
|
||||
period: period,
|
||||
...period.toTimestamps(),
|
||||
filters,
|
||||
};
|
||||
props.setActiveWidget(activeWidget);
|
||||
};
|
||||
|
||||
const clickHandler = (event, index) => {
|
||||
if (event) {
|
||||
const payload = event.activePayload[0].payload;
|
||||
const timestamp = payload.timestamp;
|
||||
const periodTimestamps =
|
||||
metric.metricType === 'timeseries'
|
||||
? getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density)
|
||||
: period.toTimestamps();
|
||||
|
||||
const activeWidget = {
|
||||
widget: metric,
|
||||
period: period,
|
||||
...periodTimestamps,
|
||||
timestamp: payload.timestamp,
|
||||
index,
|
||||
};
|
||||
|
||||
props.setActiveWidget(activeWidget);
|
||||
}
|
||||
};
|
||||
|
||||
const updateActiveState = (metricId, state) => {
|
||||
props.updateActiveState(metricId, state);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className="flex items-center p-2">
|
||||
<div className="font-medium">{metric.name}</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
{!isTable && !isPieChart && (
|
||||
<WidgetIcon
|
||||
className="cursor-pointer mr-6"
|
||||
icon="bell-plus"
|
||||
tooltip="Set Alert"
|
||||
onClick={props.onAlertClick}
|
||||
/>
|
||||
)}
|
||||
<WidgetIcon
|
||||
className="cursor-pointer mr-6"
|
||||
icon="pencil"
|
||||
tooltip="Edit Metric"
|
||||
onClick={() => props.init(metric)}
|
||||
/>
|
||||
<WidgetIcon
|
||||
className="cursor-pointer"
|
||||
icon="close"
|
||||
tooltip="Hide Metric"
|
||||
onClick={() => updateActiveState(metric.metricId, false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<Loader loading={loading} size="small">
|
||||
<NoContent size="small" title={NO_METRIC_DATA} show={data.length === 0}>
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<>
|
||||
{isLineChart && (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
params={params}
|
||||
// seriesMap={ seriesMap }
|
||||
colors={colors}
|
||||
onClick={clickHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPieChart && (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
onClick={clickHandlerTable}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isProgress && (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
params={params}
|
||||
colors={colors}
|
||||
onClick={clickHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTable && (
|
||||
<CustomMetricTable
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
onClick={clickHandlerTable}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
period: state.getIn(['dashboard', 'period']),
|
||||
}),
|
||||
{
|
||||
remove,
|
||||
setShowAlerts,
|
||||
edit,
|
||||
setActiveWidget,
|
||||
updateActiveState,
|
||||
init,
|
||||
}
|
||||
)(CustomMetricWidget);
|
||||
|
||||
const WidgetIcon = ({
|
||||
className = '',
|
||||
tooltip = '',
|
||||
icon,
|
||||
onClick,
|
||||
}: {
|
||||
className: string;
|
||||
tooltip: string;
|
||||
icon: string;
|
||||
onClick: any;
|
||||
}) => (
|
||||
<Tooltip title={tooltip}>
|
||||
<div className={className} onClick={onClick}>
|
||||
{/* @ts-ignore */}
|
||||
<Icon name={icon} size="14" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CustomMetricWidget';
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { IssueCategory } from 'App/types/filter/filterType';
|
||||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
item: any;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
function InsightItem(props: Props) {
|
||||
const { item, onClick = () => {} } = props;
|
||||
const className =
|
||||
'flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
|
||||
|
||||
switch (item.category) {
|
||||
case IssueCategory.RAGE:
|
||||
return <RageItem onClick={onClick} item={item} className={className} />;
|
||||
case IssueCategory.RESOURCES:
|
||||
return <ResourcesItem onClick={onClick} item={item} className={className} />;
|
||||
case IssueCategory.ERRORS:
|
||||
return <ErrorItem onClick={onClick} item={item} className={className} />;
|
||||
case IssueCategory.NETWORK:
|
||||
return <NetworkItem onClick={onClick} item={item} className={className} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default InsightItem;
|
||||
|
||||
|
||||
function Change({ change, isIncreased }: any) {
|
||||
return (
|
||||
<div
|
||||
className={cn('font-medium flex items-center', {
|
||||
'text-red': isIncreased,
|
||||
'text-tealx': !isIncreased,
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
name={isIncreased ? 'arrow-up-short' : 'arrow-down-short'}
|
||||
color={isIncreased ? 'red' : 'tealx'}
|
||||
size={18}
|
||||
/>
|
||||
{change}%
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorItem({ item, className, onClick }: any) {
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
{item.isNew ? (
|
||||
<>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
<div className="mx-1">error observed</div>
|
||||
<div className="mx-1 font-medium color-red">{item.ratio}%</div>
|
||||
<div className="mx-1">more than other new errors</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mx-1">Increase</div>
|
||||
<div className="mx-1">in</div>
|
||||
<div className="mx-1">{item.name}</div>
|
||||
<Change change={item.change} isIncreased={item.isIncreased} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkItem({ item, className, onClick }: any) {
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
<div className="mx-1">Network request</div>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
<div className="mx-1">{item.change > 0 ? 'increased' : 'decreased'}</div>
|
||||
<Change change={item.change} isIncreased={item.isIncreased} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourcesItem({ item, className, onClick }: any) {
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
<div className="mx-1">{item.change > 0 ? 'Inrease' : 'Decrease'}</div>
|
||||
<div className="mx-1">in</div>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
|
||||
<Change change={item.change} isIncreased={item.isIncreased} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RageItem({ item, className, onClick }: any) {
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">{item.isNew ? item.name : 'Click Rage'}</div>
|
||||
{item.isNew && <div className="mx-1">has</div>}
|
||||
{!item.isNew && <div className="mx-1">on</div>}
|
||||
{item.isNew && <div className="font-medium text-red">{item.ratio}%</div>}
|
||||
{item.isNew && <div className="mx-1">more clickrage than other raged elements.</div>}
|
||||
{!item.isNew && (
|
||||
<>
|
||||
<div className="mx-1">increase by</div>
|
||||
<Change change={item.change} isIncreased={item.isIncreased} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { NoContent } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import InsightItem from './InsightItem';
|
||||
import { NO_METRIC_DATA } from 'App/constants/messages';
|
||||
import { InishtIssue } from 'App/mstore/types/widget';
|
||||
import { FilterKey, IssueCategory, IssueType } from 'App/types/filter/filterType';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
|
||||
function InsightsCard() {
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const metric = metricStore.instance;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const clickHanddler = (e: React.MouseEvent<HTMLDivElement>, item: InishtIssue) => {
|
||||
let filter: any = {};
|
||||
switch (item.category) {
|
||||
case IssueCategory.RESOURCES:
|
||||
filter = {
|
||||
...filtersMap[
|
||||
item.name === IssueType.MEMORY ? FilterKey.AVG_MEMORY_USAGE : FilterKey.AVG_CPU
|
||||
],
|
||||
};
|
||||
filter.source = [item.oldValue];
|
||||
filter.value = [];
|
||||
break;
|
||||
case IssueCategory.RAGE:
|
||||
filter = { ...filtersMap[FilterKey.CLICK] };
|
||||
filter.value = [item.name];
|
||||
break;
|
||||
case IssueCategory.NETWORK:
|
||||
filter = { ...filtersMap[FilterKey.FETCH_URL] };
|
||||
filter.filters = [
|
||||
{ ...filtersMap[FilterKey.FETCH_URL], value: [item.name] },
|
||||
{ ...filtersMap[FilterKey.FETCH_DURATION], value: [item.oldValue] },
|
||||
];
|
||||
filter.value = [];
|
||||
break;
|
||||
case IssueCategory.ERRORS:
|
||||
filter = { ...filtersMap[FilterKey.ERROR] };
|
||||
break;
|
||||
}
|
||||
|
||||
filter.type = filter.key;
|
||||
delete filter.key;
|
||||
delete filter.operatorOptions;
|
||||
delete filter.sourceOperatorOptions;
|
||||
delete filter.placeholder;
|
||||
delete filter.sourcePlaceholder;
|
||||
delete filter.sourceType;
|
||||
delete filter.sourceUnit;
|
||||
delete filter.category;
|
||||
delete filter.icon;
|
||||
delete filter.label;
|
||||
delete filter.options;
|
||||
|
||||
drillDownFilter.merge({
|
||||
filters: [filter],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
show={metric.data.issues && metric.data.issues.length === 0}
|
||||
title={NO_METRIC_DATA}
|
||||
style={{ padding: '100px 0' }}
|
||||
>
|
||||
<div className="overflow-y-auto" style={{ maxHeight: '240px' }}>
|
||||
{metric.data.issues &&
|
||||
metric.data.issues.map((item: any) => (
|
||||
<InsightItem item={item} onClick={(e) => clickHanddler(e, item)} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(InsightsCard);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './InsightsCard'
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles, AvgLabel } from '../common';
|
||||
import { withRequest } from 'HOCs';
|
||||
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const WIDGET_KEY = 'pagesDomBuildtime';
|
||||
const toUnderscore = s => s.split(/(?=[A-Z])/).join('_').toLowerCase();
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 70 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 70
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 70
|
||||
if (rangeName === YESTERDAY) params.density = 70
|
||||
if (rangeName === LAST_7_DAYS) params.density = 70
|
||||
|
||||
return params
|
||||
}
|
||||
@withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})
|
||||
@widgetHOC(WIDGET_KEY, { customParams })
|
||||
export default class DomBuildingTime extends React.PureComponent {
|
||||
onSelect = (params) => {
|
||||
const _params = customParams(this.props.period.rangeName)
|
||||
this.props.fetchWidget(WIDGET_KEY, this.props.period, this.props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, loading, period, optionsLoading, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<React.Fragment>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={this.props.fetchOptions}
|
||||
options={this.props.options}
|
||||
onSelect={this.onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.size === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
syncId={ showSync ? WIDGET_KEY : undefined }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis} dataKey="time"
|
||||
interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "DOM Build Time (ms)" }}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
dataKey="avg"
|
||||
stroke={colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</React.Fragment>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './DomBuildingTime';
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area } from 'recharts';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { CountBadge, domain, widgetHOC } from '../common';
|
||||
import styles from './errors.module.css';
|
||||
|
||||
@widgetHOC('errors')
|
||||
export default class Errors extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading } = this.props;
|
||||
|
||||
const isMoreThanKSessions = data.impactedSessions > 1000;
|
||||
const impactedSessionsView = isMoreThanKSessions ? Math.trunc(data.impactedSessions / 1000) : data.impactedSessions;
|
||||
return (
|
||||
<div className="flex justify-between items-center flex-1 flex-shrink-0">
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
title="No exceptions."
|
||||
size="small"
|
||||
show={ data.count === 0 && data.progress === 0 }
|
||||
>
|
||||
<CountBadge
|
||||
title={ <div className={ styles.label }>{ 'Events' }</div> }
|
||||
count={ data.count }
|
||||
change={ data.progress }
|
||||
oppositeColors
|
||||
/>
|
||||
<CountBadge
|
||||
title={ <div className={ styles.label }>{ 'Sessions' }</div> }
|
||||
count={ impactedSessionsView }
|
||||
change={ data.impactedSessionsProgress }
|
||||
unit={ isMoreThanKSessions ? 'k' : '' }
|
||||
oppositeColors
|
||||
/>
|
||||
<ResponsiveContainer height={ 140 } width="60%">
|
||||
<AreaChart data={ data.chart } margin={ { top: 10, right: 20, left: 20, bottom: 0 } }>
|
||||
<defs>
|
||||
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#A8E0DA" stopOpacity={ 0.9 } />
|
||||
<stop offset="95%" stopColor="#A8E0DA" stopOpacity={ 0.2 } />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis interval={ 0 } dataKey="time" tick={ { fill: '#999999', fontSize: 9 } } />
|
||||
<YAxis interval={ 0 } hide domain={ domain }/>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<Area type="monotone" dataKey="count" stroke="#3EAAAF" fill="url(#colorCount)" fillOpacity={ 1 } strokeWidth={ 1 } strokeOpacity={ 0.8 } />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.label {
|
||||
max-width: 65px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Errors';
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Loader, NoContent } from 'UI';
|
||||
import { widgetHOC, Styles } from '../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
|
||||
|
||||
const customParams = rangeName => {
|
||||
const params = { density: 28 }
|
||||
|
||||
if (rangeName === LAST_24_HOURS) params.density = 28
|
||||
if (rangeName === LAST_30_MINUTES) params.density = 28
|
||||
if (rangeName === YESTERDAY) params.density = 28
|
||||
if (rangeName === LAST_7_DAYS) params.density = 28
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@widgetHOC('resourcesByParty', { fullwidth: true, customParams })
|
||||
export default class ErrorsByOrigin extends React.PureComponent {
|
||||
render() {
|
||||
const { data, loading, period, compare = false, showSync = false } = this.props;
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
const params = customParams(period.rangeName)
|
||||
|
||||
return (
|
||||
<Loader loading={ loading } size="small">
|
||||
<NoContent
|
||||
size="small"
|
||||
title="No recordings found"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId={ showSync ? "resourcesByParty" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name={<span className="float">1<sup>st</sup> Party</span>} dataKey="firstParty" stackId="a" fill={colors[0]} />
|
||||
<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a" fill={colors[2]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue