Merge remote-tracking branch 'origin/dev' into api-v1.9.5

This commit is contained in:
Taha Yassine Kraiem 2023-01-18 19:35:41 +01:00
commit 24f7e5f1ae
551 changed files with 8594 additions and 10420 deletions

View file

@ -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.

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -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=

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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)

View file

@ -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()

View file

@ -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
}

View file

@ -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

View file

@ -8,6 +8,7 @@ type Project struct {
MaxSessionDuration int64
SampleRate byte
SaveRequestPayloads bool
BeaconSize int64
Metadata1 *string
Metadata2 *string
Metadata3 *string

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}

View 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
}

View file

@ -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

View 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()
}

View file

@ -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" ||

View file

@ -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)

View file

@ -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") {

View file

@ -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

View file

@ -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),

View file

@ -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 {

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

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

View file

@ -1,7 +0,0 @@
.wrapper {
padding: 15px;
}
.viewed {
background-color: $gray-lightest;
}

View file

@ -1,9 +0,0 @@
.wrapper {
&:hover {
background-color: $active-blue;
}
&.active {
background-color: $active-blue;
}
}

View file

@ -1,11 +0,0 @@
.wrapper {
background-color: white;
color: $gray-dark;
border: solid thin $gray-light;
}
.alert {
background: #C3E9EA;
color: #32888C;
border: none;
}

View file

@ -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)} />
))

View file

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

View file

@ -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;

View file

@ -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';

View file

@ -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(

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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 })}
/>
))}

View file

@ -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>

View file

@ -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';

View file

@ -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;

View file

@ -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'))

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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 (

View file

@ -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 (

View file

@ -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>

View file

@ -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);

View file

@ -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));

View file

@ -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)

View file

@ -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;
}
}

View file

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

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

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

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

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

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

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

View file

@ -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>
);
}
}

View file

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

View file

@ -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>
);
}
}

View file

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

View file

@ -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>
);
}
}

View file

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

View file

@ -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>
);
}
}

View file

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

View file

@ -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>

View file

@ -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 {

View file

@ -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: "" });

View file

@ -1,6 +0,0 @@
.wrapper {
background-color: white;
/* border: solid thin $gray-medium; */
border-radius: 3px;
padding: 10px;
}

View file

@ -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>
);

View file

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

View file

@ -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>
);
}

View file

@ -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);

View file

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

View file

@ -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>
);
}
}

View file

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

View file

@ -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>
);
}
}

View file

@ -1,4 +0,0 @@
.label {
max-width: 65px;
line-height: 14px;
}

View file

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

View file

@ -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