Merge branch 'dev' into ender_refactoring

This commit is contained in:
Alexander 2022-05-12 17:16:45 +02:00 committed by GitHub
commit 4ac3da241e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 2506 additions and 583 deletions

View file

@ -23,15 +23,15 @@ function build_service() {
image="$1"
echo "BUILDING $image"
case "$image" in
http | db | ender | heuristics)
http | db | sink | ender | heuristics)
echo build http
docker build -t ${DOCKER_REPO:-'local'}/$image:${git_sha1} --build-arg SERVICE_NAME=$image -f ./cmd/Dockerfile .
docker build -t ${DOCKER_REPO:-'local'}/$image:${git_sha1} --platform linux/amd64 --build-arg SERVICE_NAME=$image -f ./cmd/Dockerfile .
[[ $PUSH_IMAGE -eq 1 ]] && {
docker push ${DOCKER_REPO:-'local'}/$image:${git_sha1}
}
;;
*)
docker build -t ${DOCKER_REPO:-'local'}/$image:${git_sha1} --build-arg SERVICE_NAME=$image .
docker build -t ${DOCKER_REPO:-'local'}/$image:${git_sha1} --platform linux/amd64 --build-arg SERVICE_NAME=$image .
[[ $PUSH_IMAGE -eq 1 ]] && {
docker push ${DOCKER_REPO:-'local'}/$image:${git_sha1}
}

View file

@ -18,7 +18,7 @@ COPY cmd cmd
ARG SERVICE_NAME
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openreplay/backend/cmd/$SERVICE_NAME
FROM alpine
FROM alpine AS entrypoint
RUN apk add --no-cache ca-certificates
ENV TZ=UTC \

View file

@ -2,6 +2,10 @@ package main
import (
"log"
"os"
"os/signal"
"syscall"
"openreplay/backend/internal/config"
"openreplay/backend/internal/router"
"openreplay/backend/internal/server"
@ -10,9 +14,6 @@ import (
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/pprof"
"openreplay/backend/pkg/queue"
"os"
"os/signal"
"syscall"
)
func main() {

View file

@ -9,42 +9,48 @@ import (
"os/signal"
"syscall"
"openreplay/backend/pkg/env"
"openreplay/backend/internal/assetscache"
"openreplay/backend/internal/config/sink"
"openreplay/backend/internal/oswriter"
. "openreplay/backend/pkg/messages"
"openreplay/backend/pkg/queue"
"openreplay/backend/pkg/queue/types"
"openreplay/backend/pkg/url/assets"
)
func main() {
log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile)
FS_DIR := env.String("FS_DIR")
if _, err := os.Stat(FS_DIR); os.IsNotExist(err) {
log.Fatalf("%v doesn't exist. %v", FS_DIR, err)
cfg := sink.New()
if _, err := os.Stat(cfg.FsDir); os.IsNotExist(err) {
log.Fatalf("%v doesn't exist. %v", cfg.FsDir, err)
}
writer := NewWriter(env.Uint16("FS_ULIMIT"), FS_DIR)
writer := oswriter.NewWriter(cfg.FsUlimit, cfg.FsDir)
producer := queue.NewProducer()
defer producer.Close(cfg.ProducerCloseTimeout)
rewriter := assets.NewRewriter(cfg.AssetsOrigin)
assetMessageHandler := assetscache.New(cfg, rewriter, producer)
count := 0
consumer := queue.NewMessageConsumer(
env.String("GROUP_SINK"),
cfg.GroupSink,
[]string{
env.String("TOPIC_RAW_WEB"),
env.String("TOPIC_RAW_IOS"),
cfg.TopicRawIOS,
cfg.TopicRawWeb,
},
func(sessionID uint64, message Message, _ *types.Meta) {
//typeID, err := GetMessageTypeID(value)
// if err != nil {
// log.Printf("Message type decoding error: %v", err)
// return
// }
typeID := message.Meta().TypeID
count++
typeID := message.TypeID()
if !IsReplayerType(typeID) {
return
}
count++
message = assetMessageHandler.ParseAssets(sessionID, message)
value := message.Encode()
var data []byte

View file

@ -1,19 +1,19 @@
package assetscache
import (
"openreplay/backend/internal/config"
"openreplay/backend/internal/config/sink"
"openreplay/backend/pkg/messages"
"openreplay/backend/pkg/queue/types"
"openreplay/backend/pkg/url/assets"
)
type AssetsCache struct {
cfg *config.Config
cfg *sink.Config
rewriter *assets.Rewriter
producer types.Producer
}
func New(cfg *config.Config, rewriter *assets.Rewriter, producer types.Producer) *AssetsCache {
func New(cfg *sink.Config, rewriter *assets.Rewriter, producer types.Producer) *AssetsCache {
return &AssetsCache{
cfg: cfg,
rewriter: rewriter,

View file

@ -11,12 +11,9 @@ type Config struct {
HTTPTimeout time.Duration
TopicRawWeb string
TopicRawIOS string
TopicCache string
CacheAssets bool
BeaconSizeLimit int64
JsonSizeLimit int64
FileSizeLimit int64
AssetsOrigin string
AWSRegion string
S3BucketIOSImages string
Postgres string
@ -33,12 +30,9 @@ func New() *Config {
HTTPTimeout: time.Second * 60,
TopicRawWeb: env.String("TOPIC_RAW_WEB"),
TopicRawIOS: env.String("TOPIC_RAW_IOS"),
TopicCache: env.String("TOPIC_CACHE"),
CacheAssets: env.Bool("CACHE_ASSETS"),
BeaconSizeLimit: int64(env.Uint64("BEACON_SIZE_LIMIT")),
JsonSizeLimit: 1e3, // 1Kb
FileSizeLimit: 1e7, // 10Mb
AssetsOrigin: env.String("ASSETS_ORIGIN"),
AWSRegion: env.String("AWS_REGION"),
S3BucketIOSImages: env.String("S3_BUCKET_IOS_IMAGES"),
Postgres: env.String("POSTGRES_STRING"),

View file

@ -0,0 +1,31 @@
package sink
import (
"openreplay/backend/pkg/env"
)
type Config struct {
FsDir string
FsUlimit uint16
GroupSink string
TopicRawWeb string
TopicRawIOS string
TopicCache string
CacheAssets bool
AssetsOrigin string
ProducerCloseTimeout int
}
func New() *Config {
return &Config{
FsDir: env.String("FS_DIR"),
FsUlimit: env.Uint16("FS_ULIMIT"),
GroupSink: env.String("GROUP_SINK"),
TopicRawWeb: env.String("TOPIC_RAW_WEB"),
TopicRawIOS: env.String("TOPIC_RAW_IOS"),
TopicCache: env.String("TOPIC_CACHE"),
CacheAssets: env.Bool("CACHE_ASSETS"),
AssetsOrigin: env.String("ASSETS_ORIGIN"),
ProducerCloseTimeout: 15000,
}
}

View file

@ -1,4 +1,4 @@
package main
package oswriter
import (
"math"

View file

@ -1,9 +1,9 @@
package router
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"log"
"math/rand"
"net/http"
@ -64,14 +64,14 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
ResponseWithError(w, http.StatusForbidden, errors.New("browser not recognized"))
return
}
sessionID, err := e.services.Flaker.Compose(uint64(startTime.UnixNano() / 1e6))
sessionID, err := e.services.Flaker.Compose(uint64(startTime.UnixMilli()))
if err != nil {
ResponseWithError(w, http.StatusInternalServerError, err)
return
}
// TODO: if EXPIRED => send message for two sessions association
expTime := startTime.Add(time.Duration(p.MaxSessionDuration) * time.Millisecond)
tokenData = &token.TokenData{ID: sessionID, ExpTime: expTime.UnixNano() / 1e6}
tokenData = &token.TokenData{ID: sessionID, ExpTime: expTime.UnixMilli()}
e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(&SessionStart{
Timestamp: req.Timestamp,
@ -117,20 +117,15 @@ func (e *Router) pushMessagesHandlerWeb(w http.ResponseWriter, r *http.Request)
body := http.MaxBytesReader(w, r.Body, e.cfg.BeaconSizeLimit)
defer body.Close()
var handledMessages bytes.Buffer
// Process each message in request data
err = ReadBatchReader(body, func(msg Message) {
msg = e.services.Assets.ParseAssets(sessionData.ID, msg)
handledMessages.Write(msg.Encode())
})
bytes, err := ioutil.ReadAll(body)
if err != nil {
ResponseWithError(w, http.StatusForbidden, err)
ResponseWithError(w, http.StatusInternalServerError, err) // TODO: Split environments; send error here only on staging
return
}
// Send processed messages to queue as array of bytes
err = e.services.Producer.Produce(e.cfg.TopicRawWeb, sessionData.ID, handledMessages.Bytes())
// TODO: check bytes for nonsense crap
err = e.services.Producer.Produce(e.cfg.TopicRawWeb, sessionData.ID, bytes)
if err != nil {
log.Printf("can't send processed messages to queue: %s", err)
}

View file

@ -1,7 +1,6 @@
package services
import (
"openreplay/backend/internal/assetscache"
"openreplay/backend/internal/config"
"openreplay/backend/internal/geoip"
"openreplay/backend/internal/uaparser"
@ -10,13 +9,11 @@ import (
"openreplay/backend/pkg/queue/types"
"openreplay/backend/pkg/storage"
"openreplay/backend/pkg/token"
"openreplay/backend/pkg/url/assets"
)
type ServicesBuilder struct {
Database *cache.PGCache
Producer types.Producer
Assets *assetscache.AssetsCache
Flaker *flakeid.Flaker
UaParser *uaparser.UAParser
GeoIP *geoip.GeoIP
@ -25,11 +22,9 @@ type ServicesBuilder struct {
}
func New(cfg *config.Config, producer types.Producer, pgconn *cache.PGCache) *ServicesBuilder {
rewriter := assets.NewRewriter(cfg.AssetsOrigin)
return &ServicesBuilder{
Database: pgconn,
Producer: producer,
Assets: assetscache.New(cfg, rewriter, producer),
Storage: storage.NewS3(cfg.AWSRegion, cfg.S3BucketIOSImages),
Tokenizer: token.NewTokenizer(cfg.TokenSecret),
UaParser: uaparser.NewUAParser(cfg.UAParserFile),

View file

@ -1,10 +1,10 @@
// Auto-generated, do not edit
package messages
func IsReplayerType(id uint64) bool {
func IsReplayerType(id int) bool {
return 0 == id || 2 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 69 == id || 70 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
}
func IsIOSType(id uint64) bool {
func IsIOSType(id int) bool {
return 107 == id || 90 == id || 91 == id || 92 == id || 93 == id || 94 == id || 95 == id || 96 == id || 97 == id || 98 == id || 99 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 110 == id || 111 == id
}

View file

@ -3,10 +3,7 @@ package messages
func transformDeprecated(msg Message) Message {
switch m := msg.(type) {
case *MouseClickDepricated:
meta := m.Meta()
meta.TypeID = 33
return &MouseClick{
meta: meta,
ID: m.ID,
HesitationTime: m.HesitationTime,
Label: m.Label,

View file

@ -0,0 +1,16 @@
package messages
type message struct {
Timestamp int64
Index uint64
}
func (m *message) Meta() *message {
return m
}
type Message interface {
Encode() []byte
TypeID() int
Meta() *message
}

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
switch t {
case 80:
msg := &BatchMeta{meta: &meta{TypeID: 80}}
msg := &BatchMeta{}
if msg.PageNo, err = ReadUint(reader); err != nil {
return nil, err
}
@ -27,14 +27,14 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 0:
msg := &Timestamp{meta: &meta{TypeID: 0}}
msg := &Timestamp{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, nil
case 1:
msg := &SessionStart{meta: &meta{TypeID: 1}}
msg := &SessionStart{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -86,21 +86,21 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 2:
msg := &SessionDisconnect{meta: &meta{TypeID: 2}}
msg := &SessionDisconnect{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, nil
case 3:
msg := &SessionEnd{meta: &meta{TypeID: 3}}
msg := &SessionEnd{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, nil
case 4:
msg := &SetPageLocation{meta: &meta{TypeID: 4}}
msg := &SetPageLocation{}
if msg.URL, err = ReadString(reader); err != nil {
return nil, err
}
@ -113,7 +113,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 5:
msg := &SetViewportSize{meta: &meta{TypeID: 5}}
msg := &SetViewportSize{}
if msg.Width, err = ReadUint(reader); err != nil {
return nil, err
}
@ -123,7 +123,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 6:
msg := &SetViewportScroll{meta: &meta{TypeID: 6}}
msg := &SetViewportScroll{}
if msg.X, err = ReadInt(reader); err != nil {
return nil, err
}
@ -133,12 +133,12 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 7:
msg := &CreateDocument{meta: &meta{TypeID: 7}}
msg := &CreateDocument{}
return msg, nil
case 8:
msg := &CreateElementNode{meta: &meta{TypeID: 8}}
msg := &CreateElementNode{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -157,7 +157,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 9:
msg := &CreateTextNode{meta: &meta{TypeID: 9}}
msg := &CreateTextNode{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -170,7 +170,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 10:
msg := &MoveNode{meta: &meta{TypeID: 10}}
msg := &MoveNode{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -183,14 +183,14 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 11:
msg := &RemoveNode{meta: &meta{TypeID: 11}}
msg := &RemoveNode{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, nil
case 12:
msg := &SetNodeAttribute{meta: &meta{TypeID: 12}}
msg := &SetNodeAttribute{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -203,7 +203,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 13:
msg := &RemoveNodeAttribute{meta: &meta{TypeID: 13}}
msg := &RemoveNodeAttribute{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -213,7 +213,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 14:
msg := &SetNodeData{meta: &meta{TypeID: 14}}
msg := &SetNodeData{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -223,7 +223,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 15:
msg := &SetCSSData{meta: &meta{TypeID: 15}}
msg := &SetCSSData{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -233,7 +233,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 16:
msg := &SetNodeScroll{meta: &meta{TypeID: 16}}
msg := &SetNodeScroll{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -246,7 +246,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 17:
msg := &SetInputTarget{meta: &meta{TypeID: 17}}
msg := &SetInputTarget{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -256,7 +256,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 18:
msg := &SetInputValue{meta: &meta{TypeID: 18}}
msg := &SetInputValue{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -269,7 +269,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 19:
msg := &SetInputChecked{meta: &meta{TypeID: 19}}
msg := &SetInputChecked{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -279,7 +279,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 20:
msg := &MouseMove{meta: &meta{TypeID: 20}}
msg := &MouseMove{}
if msg.X, err = ReadUint(reader); err != nil {
return nil, err
}
@ -289,7 +289,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 21:
msg := &MouseClickDepricated{meta: &meta{TypeID: 21}}
msg := &MouseClickDepricated{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -302,7 +302,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 22:
msg := &ConsoleLog{meta: &meta{TypeID: 22}}
msg := &ConsoleLog{}
if msg.Level, err = ReadString(reader); err != nil {
return nil, err
}
@ -312,7 +312,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 23:
msg := &PageLoadTiming{meta: &meta{TypeID: 23}}
msg := &PageLoadTiming{}
if msg.RequestStart, err = ReadUint(reader); err != nil {
return nil, err
}
@ -343,7 +343,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 24:
msg := &PageRenderTiming{meta: &meta{TypeID: 24}}
msg := &PageRenderTiming{}
if msg.SpeedIndex, err = ReadUint(reader); err != nil {
return nil, err
}
@ -356,7 +356,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 25:
msg := &JSException{meta: &meta{TypeID: 25}}
msg := &JSException{}
if msg.Name, err = ReadString(reader); err != nil {
return nil, err
}
@ -369,7 +369,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 26:
msg := &RawErrorEvent{meta: &meta{TypeID: 26}}
msg := &RawErrorEvent{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -388,7 +388,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 27:
msg := &RawCustomEvent{meta: &meta{TypeID: 27}}
msg := &RawCustomEvent{}
if msg.Name, err = ReadString(reader); err != nil {
return nil, err
}
@ -398,21 +398,21 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 28:
msg := &UserID{meta: &meta{TypeID: 28}}
msg := &UserID{}
if msg.ID, err = ReadString(reader); err != nil {
return nil, err
}
return msg, nil
case 29:
msg := &UserAnonymousID{meta: &meta{TypeID: 29}}
msg := &UserAnonymousID{}
if msg.ID, err = ReadString(reader); err != nil {
return nil, err
}
return msg, nil
case 30:
msg := &Metadata{meta: &meta{TypeID: 30}}
msg := &Metadata{}
if msg.Key, err = ReadString(reader); err != nil {
return nil, err
}
@ -422,7 +422,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 31:
msg := &PageEvent{meta: &meta{TypeID: 31}}
msg := &PageEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -477,7 +477,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 32:
msg := &InputEvent{meta: &meta{TypeID: 32}}
msg := &InputEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -496,7 +496,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 33:
msg := &ClickEvent{meta: &meta{TypeID: 33}}
msg := &ClickEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -515,7 +515,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 34:
msg := &ErrorEvent{meta: &meta{TypeID: 34}}
msg := &ErrorEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -537,7 +537,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 35:
msg := &ResourceEvent{meta: &meta{TypeID: 35}}
msg := &ResourceEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -577,7 +577,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 36:
msg := &CustomEvent{meta: &meta{TypeID: 36}}
msg := &CustomEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -593,7 +593,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 37:
msg := &CSSInsertRule{meta: &meta{TypeID: 37}}
msg := &CSSInsertRule{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -606,7 +606,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 38:
msg := &CSSDeleteRule{meta: &meta{TypeID: 38}}
msg := &CSSDeleteRule{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -616,7 +616,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 39:
msg := &Fetch{meta: &meta{TypeID: 39}}
msg := &Fetch{}
if msg.Method, err = ReadString(reader); err != nil {
return nil, err
}
@ -641,7 +641,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 40:
msg := &Profiler{meta: &meta{TypeID: 40}}
msg := &Profiler{}
if msg.Name, err = ReadString(reader); err != nil {
return nil, err
}
@ -657,7 +657,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 41:
msg := &OTable{meta: &meta{TypeID: 41}}
msg := &OTable{}
if msg.Key, err = ReadString(reader); err != nil {
return nil, err
}
@ -667,14 +667,14 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 42:
msg := &StateAction{meta: &meta{TypeID: 42}}
msg := &StateAction{}
if msg.Type, err = ReadString(reader); err != nil {
return nil, err
}
return msg, nil
case 43:
msg := &StateActionEvent{meta: &meta{TypeID: 43}}
msg := &StateActionEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -687,7 +687,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 44:
msg := &Redux{meta: &meta{TypeID: 44}}
msg := &Redux{}
if msg.Action, err = ReadString(reader); err != nil {
return nil, err
}
@ -700,7 +700,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 45:
msg := &Vuex{meta: &meta{TypeID: 45}}
msg := &Vuex{}
if msg.Mutation, err = ReadString(reader); err != nil {
return nil, err
}
@ -710,7 +710,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 46:
msg := &MobX{meta: &meta{TypeID: 46}}
msg := &MobX{}
if msg.Type, err = ReadString(reader); err != nil {
return nil, err
}
@ -720,7 +720,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 47:
msg := &NgRx{meta: &meta{TypeID: 47}}
msg := &NgRx{}
if msg.Action, err = ReadString(reader); err != nil {
return nil, err
}
@ -733,7 +733,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 48:
msg := &GraphQL{meta: &meta{TypeID: 48}}
msg := &GraphQL{}
if msg.OperationKind, err = ReadString(reader); err != nil {
return nil, err
}
@ -749,7 +749,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 49:
msg := &PerformanceTrack{meta: &meta{TypeID: 49}}
msg := &PerformanceTrack{}
if msg.Frames, err = ReadInt(reader); err != nil {
return nil, err
}
@ -765,7 +765,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 50:
msg := &GraphQLEvent{meta: &meta{TypeID: 50}}
msg := &GraphQLEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -787,7 +787,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 51:
msg := &FetchEvent{meta: &meta{TypeID: 51}}
msg := &FetchEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -815,14 +815,14 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 52:
msg := &DOMDrop{meta: &meta{TypeID: 52}}
msg := &DOMDrop{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, nil
case 53:
msg := &ResourceTiming{meta: &meta{TypeID: 53}}
msg := &ResourceTiming{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -850,7 +850,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 54:
msg := &ConnectionInformation{meta: &meta{TypeID: 54}}
msg := &ConnectionInformation{}
if msg.Downlink, err = ReadUint(reader); err != nil {
return nil, err
}
@ -860,14 +860,14 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 55:
msg := &SetPageVisibility{meta: &meta{TypeID: 55}}
msg := &SetPageVisibility{}
if msg.hidden, err = ReadBoolean(reader); err != nil {
return nil, err
}
return msg, nil
case 56:
msg := &PerformanceTrackAggr{meta: &meta{TypeID: 56}}
msg := &PerformanceTrackAggr{}
if msg.TimestampStart, err = ReadUint(reader); err != nil {
return nil, err
}
@ -913,7 +913,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 59:
msg := &LongTask{meta: &meta{TypeID: 59}}
msg := &LongTask{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -938,7 +938,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 60:
msg := &SetNodeAttributeURLBased{meta: &meta{TypeID: 60}}
msg := &SetNodeAttributeURLBased{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -954,7 +954,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 61:
msg := &SetCSSDataURLBased{meta: &meta{TypeID: 61}}
msg := &SetCSSDataURLBased{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -967,7 +967,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 62:
msg := &IssueEvent{meta: &meta{TypeID: 62}}
msg := &IssueEvent{}
if msg.MessageID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -989,7 +989,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 63:
msg := &TechnicalInfo{meta: &meta{TypeID: 63}}
msg := &TechnicalInfo{}
if msg.Type, err = ReadString(reader); err != nil {
return nil, err
}
@ -999,7 +999,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 64:
msg := &CustomIssue{meta: &meta{TypeID: 64}}
msg := &CustomIssue{}
if msg.Name, err = ReadString(reader); err != nil {
return nil, err
}
@ -1009,19 +1009,19 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 65:
msg := &PageClose{meta: &meta{TypeID: 65}}
msg := &PageClose{}
return msg, nil
case 66:
msg := &AssetCache{meta: &meta{TypeID: 66}}
msg := &AssetCache{}
if msg.URL, err = ReadString(reader); err != nil {
return nil, err
}
return msg, nil
case 67:
msg := &CSSInsertRuleURLBased{meta: &meta{TypeID: 67}}
msg := &CSSInsertRuleURLBased{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1037,7 +1037,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 69:
msg := &MouseClick{meta: &meta{TypeID: 69}}
msg := &MouseClick{}
if msg.ID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1053,7 +1053,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 70:
msg := &CreateIFrameDocument{meta: &meta{TypeID: 70}}
msg := &CreateIFrameDocument{}
if msg.FrameID, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1063,7 +1063,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 107:
msg := &IOSBatchMeta{meta: &meta{TypeID: 107}}
msg := &IOSBatchMeta{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1076,7 +1076,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 90:
msg := &IOSSessionStart{meta: &meta{TypeID: 90}}
msg := &IOSSessionStart{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1110,14 +1110,14 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 91:
msg := &IOSSessionEnd{meta: &meta{TypeID: 91}}
msg := &IOSSessionEnd{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
return msg, nil
case 92:
msg := &IOSMetadata{meta: &meta{TypeID: 92}}
msg := &IOSMetadata{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1133,7 +1133,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 93:
msg := &IOSCustomEvent{meta: &meta{TypeID: 93}}
msg := &IOSCustomEvent{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1149,7 +1149,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 94:
msg := &IOSUserID{meta: &meta{TypeID: 94}}
msg := &IOSUserID{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1162,7 +1162,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 95:
msg := &IOSUserAnonymousID{meta: &meta{TypeID: 95}}
msg := &IOSUserAnonymousID{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1175,7 +1175,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 96:
msg := &IOSScreenChanges{meta: &meta{TypeID: 96}}
msg := &IOSScreenChanges{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1197,7 +1197,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 97:
msg := &IOSCrash{meta: &meta{TypeID: 97}}
msg := &IOSCrash{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1216,7 +1216,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 98:
msg := &IOSScreenEnter{meta: &meta{TypeID: 98}}
msg := &IOSScreenEnter{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1232,7 +1232,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 99:
msg := &IOSScreenLeave{meta: &meta{TypeID: 99}}
msg := &IOSScreenLeave{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1248,7 +1248,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 100:
msg := &IOSClickEvent{meta: &meta{TypeID: 100}}
msg := &IOSClickEvent{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1267,7 +1267,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 101:
msg := &IOSInputEvent{meta: &meta{TypeID: 101}}
msg := &IOSInputEvent{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1286,7 +1286,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 102:
msg := &IOSPerformanceEvent{meta: &meta{TypeID: 102}}
msg := &IOSPerformanceEvent{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1302,7 +1302,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 103:
msg := &IOSLog{meta: &meta{TypeID: 103}}
msg := &IOSLog{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1318,7 +1318,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 104:
msg := &IOSInternalError{meta: &meta{TypeID: 104}}
msg := &IOSInternalError{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1331,7 +1331,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 105:
msg := &IOSNetworkCall{meta: &meta{TypeID: 105}}
msg := &IOSNetworkCall{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1362,7 +1362,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 110:
msg := &IOSPerformanceAggregated{meta: &meta{TypeID: 110}}
msg := &IOSPerformanceAggregated{}
if msg.TimestampStart, err = ReadUint(reader); err != nil {
return nil, err
}
@ -1408,7 +1408,7 @@ func ReadMessage(reader io.Reader) (Message, error) {
return msg, nil
case 111:
msg := &IOSIssueEvent{meta: &meta{TypeID: 111}}
msg := &IOSIssueEvent{}
if msg.Timestamp, err = ReadUint(reader); err != nil {
return nil, err
}

View file

View file

@ -24,7 +24,8 @@ const siteIdRequiredPaths = [
'/heatmaps',
'/custom_metrics',
'/dashboards',
'/metrics'
'/metrics',
'/trails',
// '/custom_metrics/sessions',
];

View file

@ -0,0 +1,53 @@
import React from 'react';
import { JSONTree } from 'UI';
import { checkForRecent } from 'App/date';
interface Props {
audit: any;
}
function AuditDetailModal(props: Props) {
const { audit } = props;
// const jsonResponse = typeof audit.payload === 'string' ? JSON.parse(audit.payload) : audit.payload;
// console.log('jsonResponse', jsonResponse)
return (
<div style={{ width: '500px' }} 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>
<div className="color-gray-darkest p-2 bg-gray-lightest rounded">{ audit.endPoint }</div>
<div className="grid grid-cols-2 my-6">
<div className="">
<div className="font-medium mb-2">Username</div>
<div>{audit.username}</div>
</div>
<div className="">
<div className="font-medium mb-2">Created At</div>
<div>{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
</div>
</div>
<div className="grid grid-cols-2 my-6">
<div className="">
<div className="font-medium mb-2">Action</div>
<div>{audit.action}</div>
</div>
<div className="">
<div className="font-medium mb-2">Method</div>
<div>{audit.method}</div>
</div>
</div>
{ audit.payload && (
<div className="my-6">
<div className="font-medium mb-3">Payload</div>
<JSONTree src={ audit.payload } collapsed={ false } enableClipboard />
</div>
)}
</div>
</div>
);
}
export default AuditDetailModal;

View file

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

View file

@ -0,0 +1,67 @@
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import { Loader, Pagination, NoContent } from 'UI';
import AuditDetailModal from '../AuditDetailModal';
import AuditListItem from '../AuditListItem';
interface Props {
}
function AuditList(props: Props) {
const { auditStore } = useStore();
const loading = useObserver(() => auditStore.isLoading);
const list = useObserver(() => auditStore.list);
const searchQuery = useObserver(() => auditStore.searchQuery);
const page = useObserver(() => auditStore.page);
const order = useObserver(() => auditStore.order);
const period = useObserver(() => auditStore.period);
const { showModal } = useModal();
console.log('AuditList', period.toTimestamps());
useEffect(() => {
const { startTimestamp, endTimestamp } = period.toTimestamps();
auditStore.fetchAudits({
page: auditStore.page,
limit: auditStore.pageSize,
query: auditStore.searchQuery,
order: auditStore.order,
startDate: startTimestamp,
endDate: endTimestamp,
});
}, [page, searchQuery, order, period]);
return useObserver(() => (
<Loader loading={loading}>
<NoContent show={list.length === 0} animatedIcon="empty-state">
<div className="px-2 grid grid-cols-12 gap-4 items-center py-3 font-medium">
<div className="col-span-5">Name</div>
<div className="col-span-4">Status</div>
<div className="col-span-3">Time</div>
</div>
{list.map((item, index) => (
<div className="px-2 border-t hover:bg-active-blue" key={index}>
<AuditListItem
audit={item}
onShowDetails={() => showModal(<AuditDetailModal audit={item} />, { right: true })}
/>
</div>
))}
<div className="w-full flex items-center justify-center py-10">
<Pagination
page={auditStore.page}
totalPages={Math.ceil(auditStore.total / auditStore.pageSize)}
onPageChange={(page) => auditStore.updateKey('page', page)}
limit={auditStore.pageSize}
debounceRequest={200}
/>
</div>
</NoContent>
</Loader>
));
}
export default AuditList;

View file

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

View file

@ -0,0 +1,19 @@
import React from 'react';
import { checkForRecent } from 'App/date';
interface Props {
audit: any;
onShowDetails: () => void;
}
function AuditListItem(props: Props) {
const { audit, onShowDetails } = props;
return (
<div className="grid grid-cols-12 gap-4 items-center py-3">
<div className="col-span-5">{audit.username}</div>
<div className="col-span-4 link cursor-pointer select-none" onClick={onShowDetails}>{audit.action}</div>
<div className="col-span-3">{audit.createdAt && checkForRecent(audit.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
</div>
);
}
export default AuditListItem;

View file

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

View file

@ -0,0 +1,33 @@
import React, { useEffect } from 'react';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {}
interface Props {
onChange: (value: string) => void;
}
function AuditSearchField(props: Props) {
const { onChange } = props;
useEffect(() => {
debounceUpdate = debounce((value) => onChange(value), 500);
}, [])
const write = ({ target: { name, value } }) => {
debounceUpdate(value);
}
return (
<div className="relative" style={{ width: '220px'}}>
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
<input
name="searchQuery"
className="bg-white p-2 border border-gray-light rounded w-full pl-10"
placeholder="Filter by Name"
onChange={write}
/>
</div>
);
}
export default AuditSearchField;

View file

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

View file

@ -0,0 +1,65 @@
import React from 'react';
import { PageTitle, Icon } from 'UI';
import AuditList from '../AuditList';
import AuditSearchField from '../AuditSearchField';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import Select from 'Shared/Select';
import SelectDateRange from 'Shared/SelectDateRange';
function AuditView(props) {
const { auditStore } = useStore();
const order = useObserver(() => auditStore.order);
const total = useObserver(() => auditStore.total);
const exportToCsv = () => {
auditStore.exportToCsv();
}
const onChange = (data) => {
auditStore.setDateRange(data);
}
return useObserver(() => (
<div>
<div className="flex items-center mb-4">
<PageTitle title={
<div className="flex items-center">
<span>Audit Trail</span>
<span className="color-gray-medium ml-2">{total}</span>
</div>
} />
<div className="flex items-center ml-auto">
<div className="mx-2">
<SelectDateRange
period={auditStore.period}
onChange={onChange}
/>
</div>
<div className="mx-2">
<Select
options={[
{ label: 'Newest First', value: 'desc' },
{ label: 'Oldest First', value: 'asc' },
]}
defaultValue={order}
plain
onChange={({ value }) => auditStore.updateKey('order', value)}
/>
</div>
<AuditSearchField onChange={(value) => auditStore.updateKey('searchQuery', value) }/>
<div>
<button className="color-teal flex items-center ml-3" onClick={exportToCsv}>
<Icon name="grid-3x3" />
<span className="ml-2">Export to CSV</span>
</button>
</div>
</div>
</div>
<AuditList />
</div>
));
}
export default AuditView;

View file

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

View file

@ -7,6 +7,8 @@ import { fetchList as fetchMemberList } from 'Duck/member';
import ProfileSettings from './ProfileSettings';
import Integrations from './Integrations';
import ManageUsers from './ManageUsers';
import UserView from './Users/UsersView';
import AuditView from './Audit/AuditView';
import Sites from './Sites';
import CustomFields from './CustomFields';
import Webhooks from './Webhooks';
@ -25,7 +27,7 @@ import Roles from './Roles';
export default class Client extends React.PureComponent {
constructor(props){
super(props);
props.fetchMemberList();
// props.fetchMemberList();
}
setTab = (tab) => {
@ -36,12 +38,13 @@ export default class Client extends React.PureComponent {
<Switch>
<Route exact strict path={ clientRoute(CLIENT_TABS.PROFILE) } component={ ProfileSettings } />
<Route exact strict path={ clientRoute(CLIENT_TABS.INTEGRATIONS) } component={ Integrations } />
<Route exact strict path={ clientRoute(CLIENT_TABS.MANAGE_USERS) } component={ ManageUsers } />
<Route exact strict path={ clientRoute(CLIENT_TABS.MANAGE_USERS) } component={ UserView } />
<Route exact strict path={ clientRoute(CLIENT_TABS.SITES) } component={ Sites } />
<Route exact strict path={ clientRoute(CLIENT_TABS.CUSTOM_FIELDS) } component={ CustomFields } />
<Route exact strict path={ clientRoute(CLIENT_TABS.WEBHOOKS) } component={ Webhooks } />
<Route exact strict path={ clientRoute(CLIENT_TABS.NOTIFICATIONS) } component={ Notifications } />
<Route exact strict path={ clientRoute(CLIENT_TABS.MANAGE_ROLES) } component={ Roles } />
<Route exact strict path={ clientRoute(CLIENT_TABS.AUDIT) } component={ AuditView } />
<Redirect to={ clientRoute(CLIENT_TABS.PROFILE) } />
</Switch>
)

View file

@ -236,7 +236,7 @@ class ManageUsers extends React.PureComponent {
title="No users are available."
size="small"
show={ members.size === 0 }
icon
animatedIcon="empty-state"
>
<div className={ styles.list }>
{

View file

@ -78,6 +78,17 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
/>
</div>
)}
{ isEnterprise && (
<div className="mb-4">
<SideMenuitem
active={ activeTab === CLIENT_TABS.AUDIT }
title="Audit"
iconName="list-ul"
onClick={() => setTab(CLIENT_TABS.AUDIT) }
/>
</div>
)}
<div className="mb-4">
<SideMenuitem
@ -95,7 +106,7 @@ function PreferencesMenu({ activeTab, appearance, history, isEnterprise }) {
iconName="bell"
onClick={() => setTab(CLIENT_TABS.NOTIFICATIONS) }
/>
</div>
</div>
</div>
)
}

View file

@ -1,10 +1,11 @@
import { connect } from 'react-redux';
import { Input, Button, Label } from 'UI';
import { save, edit, update , fetchList } from 'Duck/site';
import { Input, Button, Icon } from 'UI';
import { save, edit, update , fetchList, remove } from 'Duck/site';
import { pushNewSite } from 'Duck/user';
import { setSiteId } from 'Duck/site';
import { withRouter } from 'react-router-dom';
import styles from './siteForm.css';
import { confirm } from 'UI/Confirmation';
@connect(state => ({
site: state.getIn([ 'site', 'instance' ]),
@ -13,6 +14,7 @@ import styles from './siteForm.css';
loading: state.getIn([ 'site', 'save', 'loading' ]),
}), {
save,
remove,
edit,
update,
pushNewSite,
@ -52,6 +54,17 @@ export default class NewSiteForm extends React.PureComponent {
}
}
remove = async (site) => {
if (await confirm({
header: 'Projects',
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`
})) {
this.props.remove(site.id).then(() => {
this.props.onClose(null)
});
}
};
edit = ({ target: { name, value } }) => {
this.setState({ existsError: false });
this.props.edit({ [ name ]: value });
@ -72,7 +85,7 @@ export default class NewSiteForm extends React.PureComponent {
className={ styles.input }
/>
</div>
<div className="mt-6">
<div className="mt-6 flex justify-between">
<Button
primary
type="submit"
@ -80,7 +93,10 @@ export default class NewSiteForm extends React.PureComponent {
loading={ loading }
content={site.exists() ? 'Update' : 'Add'}
/>
</div>
<Button type="button" plain onClick={() => this.remove(site)}>
<Icon name="trash" size="16" />
</Button>
</div>
{ this.state.existsError &&
<div className={ styles.errorMessage }>
{ "Site exists already. Please choose another one." }

View file

@ -0,0 +1,34 @@
import React, { useEffect } from 'react';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {}
interface Props {
onChange: (value: string) => void;
}
function SiteSearch(props: Props) {
const { onChange } = props;
useEffect(() => {
debounceUpdate = debounce((value) => onChange(value), 500);
}, [])
const write = ({ target: { name, value } }) => {
debounceUpdate(value);
}
return (
<div className="relative" style={{ width: '300px'}}>
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
<input
// value={query}
name="searchQuery"
className="bg-white p-2 border border-gray-light rounded w-full pl-10"
placeholder="Filter by Name"
onChange={write}
/>
</div>
);
}
export default SiteSearch;

View file

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

View file

@ -10,6 +10,7 @@ import GDPRForm from './GDPRForm';
import TrackingCodeModal from 'Shared/TrackingCodeModal';
import BlockedIps from './BlockedIps';
import { confirm } from 'UI/Confirmation';
import SiteSearch from './SiteSearch';
const STATUS_MESSAGE_MAP = {
[ RED ]: ' There seems to be an issue (please verify your installation)',
@ -43,6 +44,7 @@ class Sites extends React.PureComponent {
showTrackingCode: false,
modalContent: NONE,
detailContent: NONE,
searchQuery: '',
};
toggleBlockedIp = () => {
@ -85,7 +87,7 @@ class Sites extends React.PureComponent {
getModalTitle() {
switch (this.state.modalContent) {
case NEW_SITE_FORM:
return 'New Project';
return this.props.site.exists() ? 'Update Project' : 'New Project';
case GDPR_FORM:
return 'Project Settings';
default:
@ -119,6 +121,7 @@ class Sites extends React.PureComponent {
const isAdmin = user.admin || user.superAdmin;
const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0;
const canDeleteSites = sites.size > 1 && isAdmin;
const filteredSites = sites.filter(site => site.name.toLowerCase().includes(this.state.searchQuery.toLowerCase()));
return (
<Loader loading={ loading }>
@ -159,54 +162,71 @@ class Sites extends React.PureComponent {
position="top left"
/>
<TextLink
icon="book"
className="ml-auto"
href="https://docs.openreplay.com/installation"
label="Documentation"
/>
<div className="flex ml-auto items-center">
<TextLink
icon="book"
className="mr-4"
href="https://docs.openreplay.com/installation"
label="Documentation"
/>
<SiteSearch onChange={(value) => this.setState({ searchQuery: value })} />
</div>
</div>
<div className={ stl.list }>
<div className="grid grid-cols-12 gap-2 w-full items-center border-b px-2 py-3 font-medium">
<div className="col-span-4">Name</div>
<div className="col-span-4">Key</div>
<div className="col-span-4"></div>
</div>
{
sites.map(_site => (
<div key={ _site.key } className={ stl.site } data-inactive={ _site.status === RED }>
<div className="flex items-center">
<Popup
trigger={
<div style={ { width: '10px' } }>
<Icon name="circle" size="10" color={ STATUS_COLOR_MAP[ _site.status ] } />
filteredSites.map(_site => (
// <div key={ _site.key } data-inactive={ _site.status === RED }>
<div key={ _site.key } className="grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center border-b px-2 py-3">
<div className="col-span-4">
<div className="flex items-center">
<Popup
trigger={
<div style={ { width: '10px' } }>
<Icon name="circle" size="10" color={ STATUS_COLOR_MAP[ _site.status ] } />
</div>
}
content={ STATUS_MESSAGE_MAP[ _site.status ] }
inverted
position="top center"
/>
<span className="ml-2">{ _site.host }</span>
</div>
}
content={ STATUS_MESSAGE_MAP[ _site.status ] }
inverted
position="top center"
/>
<div className="ml-3 flex items-center">
</div>
<div className="col-span-4">
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm">{_site.projectKey}</span>
</div>
{/* <div className="ml-3 flex items-center">
<div>{ _site.host }</div>
<div className={ stl.label}>{_site.projectKey}</div>
</div> */}
<div className="col-span-4 justify-self-end flex items-center invisible group-hover:visible">
<div className="mr-4"><Button size="small" primary onClick={ () => this.showTrackingCode(_site) }>{ 'Installation' }</Button></div>
{/* <button
className={cn('mx-3', {'hidden' : !canDeleteSites})}
disabled={ !canDeleteSites }
onClick={ () => canDeleteSites && this.remove(_site) }
>
<Icon name="trash" size="16" color="teal" />
</button> */}
<button
className={cn('mx-3', {'hidden' : !isAdmin})}
disabled={ !isAdmin }
onClick={ () => isAdmin && this.edit(_site) }
data-clickable
>
<Icon name="edit" size="16" color="teal"/>
</button>
{/* <button disabled={ !isAdmin } onClick={ () => this.showGDPRForm(_site) } ><Icon name="cog" size="16" color="teal" /></button> */}
</div>
</div>
<div className={ stl.actions }>
<button
className={cn({'hidden' : !canDeleteSites})}
disabled={ !canDeleteSites }
onClick={ () => canDeleteSites && this.remove(_site) }
>
<Icon name="trash" size="16" color="teal" />
</button>
<button
className={cn({'hidden' : !isAdmin})}
disabled={ !isAdmin }
onClick={ () => isAdmin && this.edit(_site) }
data-clickable
>
<Icon name="edit" size="16" color="teal"/>
</button>
<div><Button size="small" outline primary onClick={ () => this.showTrackingCode(_site) }>{ 'Tracking Code' }</Button></div>
{/* <button disabled={ !isAdmin } onClick={ () => this.showGDPRForm(_site) } ><Icon name="cog" size="16" color="teal" /></button> */}
</div>
</div>
// </div>
))
}
</div>

View file

@ -0,0 +1,80 @@
import React, { useEffect } from 'react';
import UserList from './components/UserList';
import { PageTitle, Popup, IconButton } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import UserSearch from './components/UserSearch';
import { useModal } from 'App/components/Modal';
import UserForm from './components/UserForm';
import { connect } from 'react-redux';
const PERMISSION_WARNING = 'You dont have the permissions to perform this action.';
const LIMIT_WARNING = 'You have reached users limit.';
interface Props {
account: any;
isEnterprise: boolean;
limits: any;
}
function UsersView(props: Props) {
const { account, limits, isEnterprise } = props;
const { userStore, roleStore } = useStore();
const userCount = useObserver(() => userStore.list.length);
const roles = useObserver(() => roleStore.list);
const { showModal } = useModal();
const reachedLimit = (limits.remaining + userStore.modifiedCount) <= 0;
const isAdmin = account.admin || account.superAdmin;
const editHandler = (user = null) => {
userStore.initUser(user).then(() => {
showModal(<UserForm />, {});
});
}
useEffect(() => {
if (roles.length === 0 && isEnterprise) {
roleStore.fetchRoles();
}
}, []);
return (
<div>
<div className="flex items-center justify-between">
<PageTitle
title={<div>Team <span className="color-gray-medium">{userCount}</span></div>}
actionButton={(
<Popup
trigger={
<div>
<IconButton
id="add-button"
disabled={ reachedLimit || !isAdmin }
circle
icon="plus"
outline
className="ml-3"
onClick={ () => editHandler(null) }
/>
</div>
}
content={ `${ !isAdmin ? PERMISSION_WARNING : (reachedLimit ? LIMIT_WARNING : 'Add team member') }` }
size="tiny"
inverted
position="top left"
/>
)}
/>
<div>
<UserSearch />
</div>
</div>
<UserList isEnterprise={isEnterprise} />
</div>
);
}
export default connect(state => ({
account: state.getIn([ 'user', 'account' ]),
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
limits: state.getIn([ 'user', 'account', 'limits', 'teamMember' ]),
}))(UsersView);

View file

@ -0,0 +1,154 @@
import React from 'react';
import { Input, CopyButton, Button, Icon } from 'UI'
import cn from 'classnames';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { useModal } from 'App/components/Modal';
import Select from 'Shared/Select';
import { confirm } from 'UI/Confirmation';
interface Props {
isSmtp?: boolean;
isEnterprise?: boolean;
}
function UserForm(props: Props) {
const { isSmtp = false, isEnterprise = false } = props;
const { hideModal } = useModal();
const { userStore, roleStore } = useStore();
const isSaving = useObserver(() => userStore.saving);
const user: any = useObserver(() => userStore.instance);
const roles = useObserver(() => roleStore.list.filter(r => r.isProtected ? user.isSuperAdmin : true).map(r => ({ label: r.name, value: r.roleId })));
const onChangeCheckbox = (e: any) => {
user.updateKey('isAdmin', !user.isAdmin);
}
const onSave = () => {
userStore.saveUser(user).then(() => {
hideModal();
});
}
const write = ({ target: { name, value } }) => {
user.updateKey(name, value);
}
const deleteHandler = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this user?`
})) {
userStore.deleteUser(user.userId).then(() => {
hideModal();
});
}
}
return useObserver(() => (
<div className="bg-white h-screen p-6" style={{ width: '400px'}}>
<div className="">
<h1 className="text-2xl mb-4">{`${user.exists() ? 'Update' : 'Invite'} User`}</h1>
</div>
<form onSubmit={ onSave } >
<div className="form-group">
<label>{ 'Full Name' }</label>
<Input
name="name"
autoFocus
value={ user.name }
onChange={ write }
className="w-full"
id="name-field"
/>
</div>
<div className="form-group">
<label>{ 'Email Address' }</label>
<Input
disabled={user.exists()}
name="email"
value={ user.email }
onChange={ write }
className="w-full"
/>
</div>
{ !isSmtp &&
<div className={cn("mb-4 p-2 bg-yellow rounded")}>
SMTP is not configured (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but youd have to manually copy then send them the invitation link.
</div>
}
<div className="form-group">
<label className="flex items-start cursor-pointer">
<input
name="admin"
type="checkbox"
checked={ !!user.isAdmin || !!user.isSuperAdmin }
onChange={ onChangeCheckbox }
disabled={user.isSuperAdmin}
className="mt-1"
/>
<div className="ml-2 select-none">
<span>Admin Privileges</span>
<div className="text-sm color-gray-medium -mt-1">{ 'Can manage Projects and team members.' }</div>
</div>
</label>
</div>
{ !isEnterprise && (
<div className="form-group">
<label htmlFor="role">{ 'Role' }</label>
<Select
placeholder="Selct Role"
selection
options={ roles }
name="roleId"
defaultValue={ user.roleId }
onChange={({ value }) => user.updateKey('roleId', value)}
className="block"
isDisabled={user.isSuperAdmin}
/>
</div>
)}
</form>
<div className="flex items-center">
<div className="flex items-center mr-auto">
<Button
onClick={ onSave }
disabled={ !user.valid() || isSaving }
loading={ isSaving }
primary
marginRight
>
{ user.exists() ? 'Update' : 'Invite' }
</Button>
<Button
data-hidden={ !user.exists() }
onClick={ hideModal }
outline
>
{ 'Cancel' }
</Button>
</div>
<div>
<Button
data-hidden={ !user.exists() }
onClick={ deleteHandler }
>
<Icon name="trash" size="16" />
</Button>
</div>
</div>
{ !user.isJoined && user.invitationLink &&
<CopyButton
content={user.invitationLink}
className="link mt-4"
btnText="Copy invite link"
/>
}
</div>
));
}
export default UserForm;

View file

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

View file

@ -0,0 +1,80 @@
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import UserListItem from '../UserListItem';
import { sliceListPerPage, getRE } from 'App/utils';
import { Pagination, NoContent, Loader } from 'UI';
import { useModal } from 'App/components/Modal';
import UserForm from '../UserForm';
interface Props {
isEnterprise?: boolean;
}
function UserList(props: Props) {
const { isEnterprise = false } = props;
const { userStore } = useStore();
const loading = useObserver(() => userStore.loading);
const users = useObserver(() => userStore.list);
const searchQuery = useObserver(() => userStore.searchQuery);
const { showModal } = useModal();
const filterList = (list) => {
const filterRE = getRE(searchQuery, 'i');
let _list = list.filter(w => {
return filterRE.test(w.email) || filterRE.test(w.roleName);
});
return _list
}
const list: any = searchQuery !== '' ? filterList(users) : users;
const length = list.length;
useEffect(() => {
userStore.fetchUsers();
}, []);
const editHandler = (user) => {
userStore.initUser(user).then(() => {
showModal(<UserForm />, { });
});
}
return useObserver(() => (
<Loader loading={loading}>
<NoContent show={!loading && length === 0} animatedIcon="empty-state">
<div className="mt-3 rounded bg-white">
<div className="grid grid-cols-12 p-3 border-b font-medium">
<div className="col-span-5">Name</div>
<div className="col-span-3">Role</div>
<div className="col-span-2">Created On</div>
<div className="col-span-2"></div>
</div>
{sliceListPerPage(list, userStore.page - 1, userStore.pageSize).map((user: any) => (
<div key={user.id} className="">
<UserListItem
user={user}
editHandler={() => editHandler(user)}
generateInvite={() => userStore.generateInviteCode(user.userId)}
copyInviteCode={() => userStore.copyInviteCode(user.userId)}
// isEnterprise={isEnterprise}
/>
</div>
))}
</div>
<div className="w-full flex items-center justify-center py-10">
<Pagination
page={userStore.page}
totalPages={Math.ceil(length / userStore.pageSize)}
onPageChange={(page) => userStore.updateKey('page', page)}
limit={userStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
</Loader>
));
}
export default UserList;

View file

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

View file

@ -0,0 +1,84 @@
//@ts-nocheck
import React from 'react';
import { Icon } from 'UI';
import { checkForRecent } from 'App/date';
import { Tooltip } from 'react-tippy';
const AdminPrivilegeLabel = ({ user }) => {
return (
<>
{user.isAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Admin</span>}
{user.isSuperAdmin && <span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">Owner</span>}
</>
)
}
interface Props {
user: any;
editHandler?: any;
generateInvite?: any;
copyInviteCode?: any;
isEnterprise?: boolean;
}
function UserListItem(props: Props) {
const {
user,
editHandler = () => {},
generateInvite = () => {},
copyInviteCode = () => {},
isEnterprise = false,
} = props;
return (
<div className="grid grid-cols-12 p-3 py-4 border-b items-center select-none hover:bg-active-blue group">
<div className="col-span-5">
<span className="mr-2">{user.name}</span>
{isEnterprise && <AdminPrivilegeLabel user={user} />}
</div>
<div className="col-span-3">
{!isEnterprise && <AdminPrivilegeLabel user={user} />}
{isEnterprise && (
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm capitalize">
{user.roleName}
</span>
)}
</div>
<div className="col-span-2">
<span>{user.createdAt && checkForRecent(user.createdAt, 'LLL dd, yyyy, hh:mm a')}</span>
</div>
<div className="col-span-2 justify-self-end invisible group-hover:visible">
<div className="grid grid-cols-2 gap-3 items-center justify-end">
{!user.isJoined && user.invitationLink ? (
<Tooltip
delay={500}
arrow
title="Copy Invite Code"
hideOnClick={true}
>
<button className='' onClick={copyInviteCode}>
<Icon name="link-45deg" size="16" color="teal"/>
</button>
</Tooltip>
) : <div/>}
{!user.isJoined && user.isExpiredInvite && (
<Tooltip
delay={500}
arrow
title="Generate Invite"
hideOnClick={true}
>
<button className='' onClick={generateInvite}>
<Icon name="link-45deg" size="16" color="red"/>
</button>
</Tooltip>
)}
<button className='' onClick={editHandler}>
<Icon name="pencil" color="teal" size="16" />
</button>
</div>
</div>
</div>
);
}
export default UserListItem;

View file

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

View file

@ -0,0 +1,35 @@
import { useObserver } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';
import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {}
function UserSearch(props) {
const { userStore } = useStore();
const [query, setQuery] = useState(userStore.searchQuery);
useEffect(() => {
debounceUpdate = debounce((key, value) => userStore.updateKey(key, value), 500);
}, [])
const write = ({ target: { name, value } }) => {
setQuery(value);
debounceUpdate(name, value);
}
return useObserver(() => (
<div className="relative" style={{ width: '300px'}}>
<Icon name="search" className="absolute top-0 bottom-0 ml-3 m-auto" size="16" />
<input
value={query}
name="searchQuery"
className="bg-white p-2 border border-gray-light rounded w-full pl-10"
placeholder="Filter by Name, Role"
onChange={write}
/>
</div>
));
}
export default UserSearch;

View file

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

View file

@ -1,14 +1,17 @@
import React from 'react';
import Select from 'react-select';
import Select, { components, DropdownIndicatorProps } from 'react-select';
import { Icon } from 'UI';
import colors from 'App/theme/colors';
interface Props {
options: any[];
isSearchable?: boolean;
defaultValue?: string;
plain?: boolean;
components?: any;
[x:string]: any;
}
export default function({ plain = false, options, isSearchable = false, defaultValue = '', ...rest }: Props) {
export default function({ plain = false, options, isSearchable = false, components = {}, defaultValue = '', ...rest }: Props) {
const customStyles = {
option: (provided, state) => ({
...provided,
@ -17,14 +20,26 @@ export default function({ plain = false, options, isSearchable = false, defaultV
menu: (provided, state) => ({
...provided,
top: 31,
minWidth: 'fit-content',
}),
control: (provided) => {
const obj = {
...provided,
border: 'solid thin #ddd'
border: 'solid thin #ddd',
cursor: 'pointer',
}
if (plain) {
obj['border'] = '1px solid transparent'
obj['&:hover'] = {
borderColor: 'transparent',
backgroundColor: colors['gray-light']
}
obj['&:focus'] = {
borderColor: 'transparent'
}
obj['&:active'] = {
borderColor: 'transparent'
}
}
return obj;
},
@ -39,14 +54,16 @@ export default function({ plain = false, options, isSearchable = false, defaultV
return { ...provided, opacity, transition };
}
}
const defaultSelected = defaultValue ? options.find(x => x.value === defaultValue) : options[0];
const defaultSelected = defaultValue ? options.find(x => x.value === defaultValue) : null;
return (
<Select
options={options}
isSearchable={isSearchable}
defaultValue={defaultSelected}
components={{
IndicatorSeparator: () => null
IndicatorSeparator: () => null,
DropdownIndicator,
...components,
}}
styles={customStyles}
theme={(theme) => ({
@ -56,9 +73,18 @@ export default function({ plain = false, options, isSearchable = false, defaultV
primary: '#394EFF',
}
})}
blurInputOnSelect={true}
{...rest}
/>
);
}
// export default Select;
const DropdownIndicator = (
props: DropdownIndicatorProps<true>
) => {
return (
<components.DropdownIndicator {...props}>
<Icon name="chevron-down" size="18" />
</components.DropdownIndicator>
);
};

View file

@ -0,0 +1,71 @@
import React from 'react';
import { DATE_RANGE_OPTIONS, CUSTOM_RANGE } from 'App/dateRange'
import Select from 'Shared/Select';
import Period, { LAST_7_DAYS } from 'Types/app/period';
import { components } from 'react-select';
import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
interface Props {
period: any,
onChange: (data: any) => void;
}
function SelectDateRange(props: Props) {
const [isCustom, setIsCustom] = React.useState(false);
const { period } = props;
const selectedValue = DATE_RANGE_OPTIONS.find(obj => obj.value === period.rangeName)
const onChange = (value: any) => {
if (value === CUSTOM_RANGE) {
setIsCustom(true);
} else {
props.onChange(new Period({ rangeName: value }));
}
}
const onApplyDateRange = (value: any) => {
props.onChange(new Period({ rangeName: CUSTOM_RANGE, start: value.start, end: value.end }));
setIsCustom(false);
}
return (
<div className="relative">
<Select
plain
value={selectedValue}
options={DATE_RANGE_OPTIONS}
onChange={({ value }) => onChange(value)}
components={{ SingleValue: ({ children, ...props} : any) => {
return (
<components.SingleValue {...props}>
{period.rangeName === CUSTOM_RANGE ? period.rangeFormatted() : children}
</components.SingleValue>
)
} }}
period={period}
/>
{
isCustom &&
<OutsideClickDetectingDiv
onClickOutside={() => setIsCustom(false)}
>
<div className="absolute top-0 mx-auto mt-10 z-40" style={{
width: '770px',
margin: 'auto 50vh 0',
transform: 'translateX(-50%)'
}}>
<DateRangePopup
onApply={ onApplyDateRange }
onCancel={ () => setIsCustom(false) }
selectedDateRange={ period.range }
/>
</div>
</OutsideClickDetectingDiv>
}
</div>
);
}
export default SelectDateRange;

View file

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

View file

@ -16,7 +16,6 @@ function DefaultTimezone(props) {
const { settingsStore } = useStore();
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
console.log('timezone', timezone)
return (
<>

View file

@ -23,6 +23,13 @@ Object.keys(DATE_RANGE_LABELS).forEach((key) => { DATE_RANGE_VALUES[ key ] = key
export { DATE_RANGE_VALUES };
export const dateRangeValues = Object.keys(DATE_RANGE_VALUES);
export const DATE_RANGE_OPTIONS = Object.keys(DATE_RANGE_LABELS).map((key) => {
return {
label: DATE_RANGE_LABELS[ key ],
value: key,
};
});
export function getDateRangeFromTs(start, end) {
return moment.range(
moment(start),

View file

@ -59,7 +59,12 @@ function reducer(state = initialState, action = {}) {
case EDIT_OPTIONS:
return state.mergeIn(["options"], action.instance);
case success(FETCH):
return state.set("instance", ErrorInfo(action.data));
if (state.get("list").find(e => e.get("errorId") === action.id)) {
return updateItemInList(state, { errorId: action.data.errorId, viewed: true })
.set("instance", ErrorInfo(action.data));
} else {
return state.set("instance", ErrorInfo(action.data));
}
case success(FETCH_TRACE):
return state.set("instanceTrace", List(action.data.trace)).set('sourcemapUploaded', action.data.sourcemapUploaded);
case success(FETCH_LIST):

View file

@ -0,0 +1,86 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import { auditService } from "App/services"
import Audit from './types/audit'
import Period, { LAST_7_DAYS } from 'Types/app/period';
import { toast } from 'react-toastify';
import { exportCSVFile } from 'App/utils';
import { formatDateTimeDefault } from 'App/date';
import { DateTime, Duration } from 'luxon'; // TODO
export default class AuditStore {
list: any[] = [];
total: number = 0;
page: number = 1;
pageSize: number = 20;
searchQuery: string = '';
isLoading: boolean = false;
order: string = 'desc';
period: Period|null = Period({ rangeName: LAST_7_DAYS })
constructor() {
makeAutoObservable(this, {
searchQuery: observable,
period: observable,
updateKey: action,
fetchAudits: action,
setDateRange: action,
})
}
setDateRange(data: any) {
this['period'] = data;
}
updateKey(key: string, value: any) {
this[key] = value;
}
fetchAudits = (data: any): Promise<void> => {
this.isLoading = true;
return new Promise((resolve, reject) => {
auditService.all(data).then(response => {
runInAction(() => {
this.list = response.sessions.map(item => Audit.fromJson(item))
this.total = response.count
})
resolve()
}).catch(error => {
reject(error)
}).finally(() => {
this.isLoading = false;
})
})
}
fetchAllAudits = async (data: any): Promise<any> => {
return new Promise((resolve, reject) => {
auditService.all(data).then((data) => {
const headers = [
{ label: 'User', key: 'username' },
{ label: 'Email', key: 'email' },
{ label: 'UserID', key: 'userId' },
{ label: 'Method', key: 'method' },
{ label: 'Action', key: 'action' },
{ label: 'Endpoint', key: 'endpoint' },
{ label: 'Created At', key: 'createdAt' },
]
data = data.sessions.map(item => ({
...item,
createdAt: DateTime.fromMillis(item.createdAt).toFormat('LLL dd yyyy hh:mm a')
}))
exportCSVFile(headers, data, `audit-${new Date().toLocaleDateString()}`);
resolve(data)
}).catch(error => {
reject(error)
})
})
}
exportToCsv = async (): Promise<void> => {
const promise = this.fetchAllAudits({ limit: this.total })
toast.promise(promise, {
pending: 'Exporting...',
success: 'Export successful',
})
}
}

View file

@ -1,19 +1,28 @@
import React from 'react';
import DashboardStore, { IDashboardSotre } from './dashboardStore';
import MetricStore, { IMetricStore } from './metricStore';
import UserStore from './userStore';
import RoleStore from './roleStore';
import APIClient from 'App/api_client';
import { dashboardService, metricService, sessionService } from 'App/services';
import { dashboardService, metricService, sessionService, userService, auditService } from 'App/services';
import SettingsStore from './settingsStore';
import AuditStore from './auditStore';
export class RootStore {
dashboardStore: IDashboardSotre;
metricStore: IMetricStore;
settingsStore: SettingsStore;
userStore: UserStore;
roleStore: RoleStore;
auditStore: AuditStore;
constructor() {
this.dashboardStore = new DashboardStore();
this.metricStore = new MetricStore();
this.settingsStore = new SettingsStore();
this.userStore = new UserStore();
this.roleStore = new RoleStore();
this.auditStore = new AuditStore();
}
initClient() {
@ -21,6 +30,7 @@ export class RootStore {
dashboardService.initClient(client)
metricService.initClient(client)
sessionService.initClient(client)
userService.initClient(client)
}
}

View file

@ -0,0 +1,31 @@
import { makeAutoObservable, observable, action } from "mobx"
import { userService } from "App/services";
import Role, { IRole } from "./types/role";
export default class UserStore {
list: IRole[] = [];
loading: boolean = false;
constructor() {
makeAutoObservable(this, {
list: observable,
loading: observable,
})
}
fetchRoles(): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService.getRoles()
.then(response => {
this.list = response.map((role: any) => new Role().fromJson(role));
resolve(response);
}).catch(error => {
this.loading = false;
reject(error);
}).finally(() => {
this.loading = false;
});
});
}
}

View file

@ -0,0 +1,40 @@
import { DateTime } from 'luxon';
import { unserscoreToSpaceAndCapitalize } from 'App/utils';
export default class Audit {
id: string = '';
username: string = '';
email: string = '';
action: string = '';
createdAt: any = null;
endPoint: string = '';
parameters: any = {};
method: string = '';
status: string = '';
payload: any = {}
constructor() {
}
static fromJson(json: any): Audit {
const audit = new Audit();
audit.id = json.rn;
audit.username = json.username;
audit.action = unserscoreToSpaceAndCapitalize(json.action);
audit.createdAt = json.createdAt && DateTime.fromMillis(json.createdAt || 0);
audit.endPoint = json.endpoint;
audit.parameters = json.parameters;
audit.method = json.method;
audit.status = json.status
audit.email = json.email
audit.payload = typeof json.payload === 'string' ? JSON.parse(json.payload) : json.payload
return audit;
}
toJson(): any {
return {
id: this.id,
username: this.username
};
}
}

View file

@ -0,0 +1,45 @@
import { makeAutoObservable, observable, runInAction } from "mobx";
export interface IRole {
roleId: string;
name: string;
description: string;
isProtected: boolean;
fromJson(json: any);
toJson(): any;
}
export default class Role implements IRole {
roleId: string = '';
name: string = '';
description: string = '';
isProtected: boolean = false;
constructor() {
makeAutoObservable(this, {
roleId: observable,
name: observable,
description: observable,
})
}
fromJson(json: any) {
runInAction(() => {
this.roleId = json.roleId;
this.name = json.name;
this.description = json.description;
this.isProtected = json.protected;
})
return this;
}
toJson() {
return {
id: this.roleId,
name: this.name,
description: this.description,
}
}
}

View file

@ -0,0 +1,105 @@
import { runInAction, makeAutoObservable, observable } from 'mobx'
import { DateTime } from 'luxon';
import { validateEmail, validateName } from 'App/validate';
export interface IUser {
userId: string
email: string
createdAt: string
isAdmin: boolean
isSuperAdmin: boolean
isJoined: boolean
isExpiredInvite: boolean
roleId: string
roleName: string
invitationLink: string
updateKey(key: string, value: any): void
fromJson(json: any): IUser
toJson(): any
toSave(): any
}
export default class User implements IUser {
userId: string = '';
name: string = '';
email: string = '';
createdAt: string = '';
isAdmin: boolean = false;
isSuperAdmin: boolean = false;
isJoined: boolean = false;
isExpiredInvite: boolean = false;
roleId: string = '';
roleName: string = '';
invitationLink: string = '';
constructor() {
makeAutoObservable(this, {
userId: observable,
email: observable,
createdAt: observable,
isAdmin: observable,
isSuperAdmin: observable,
isJoined: observable,
isExpiredInvite: observable,
roleId: observable,
roleName: observable,
invitationLink: observable,
})
}
updateKey(key: string, value: any) {
runInAction(() => {
this[key] = value
})
}
fromJson(json: any) {
runInAction(() => {
this.userId = json.userId || json.id; // TODO api returning id
this.name = json.name;
this.email = json.email;
this.createdAt = json.createdAt && DateTime.fromMillis(json.createdAt || 0)
this.isAdmin = json.admin
this.isSuperAdmin = json.superAdmin
this.isJoined = json.joined
this.isExpiredInvite = json.expiredInvitation
this.roleId = json.roleId
this.roleName = json.roleName
this.invitationLink = json.invitationLink
})
return this;
}
toJson() {
return {
userId: this.userId,
name: this.name,
email: this.email,
admin: this.isAdmin,
superAdmin: this.isSuperAdmin,
roleId: this.roleId,
joined: this.isJoined,
invitationLink: this.invitationLink,
expiredInvitation: this.isExpiredInvite,
}
}
toSave() {
return {
name: this.name,
email: this.email,
admin: this.isAdmin,
roleId: this.roleId,
}
}
valid() {
return validateName(this.name, { empty: false }) && validateEmail(this.email) && !!this.roleId;
}
exists() {
return !!this.userId;
}
}

View file

@ -0,0 +1,162 @@
import { makeAutoObservable, observable, action } from "mobx"
import User, { IUser } from "./types/user";
import { userService } from "App/services";
import { toast } from 'react-toastify';
import copy from 'copy-to-clipboard';
export default class UserStore {
list: IUser[] = [];
instance: IUser|null = null;
page: number = 1;
pageSize: number = 10;
searchQuery: string = "";
modifiedCount: number = 0;
loading: boolean = false;
saving: boolean = false;
constructor() {
makeAutoObservable(this, {
instance: observable,
updateUser: action,
updateKey: action,
initUser: action,
})
}
initUser(user?: any ): Promise<void> {
return new Promise((resolve, reject) => {
if (user) {
this.instance = new User().fromJson(user.toJson());
} else {
this.instance = new User();
}
resolve();
})
}
updateKey(key: string, value: any) {
this[key] = value
if (key === 'searchQuery') {
this.page = 1
}
}
updateUser(user: IUser) {
const index = this.list.findIndex(u => u.userId === user.userId);
if (index > -1) {
this.list[index] = user;
}
}
fetchUser(userId: string): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService.one(userId)
.then(response => {
this.instance = new User().fromJson(response.data);
resolve(response);
}).catch(error => {
this.loading = false;
reject(error);
}).finally(() => {
this.loading = false;
});
});
}
fetchUsers(): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService.all()
.then(response => {
this.list = response.map(user => new User().fromJson(user));
resolve(response);
}).catch(error => {
this.loading = false;
reject(error);
}).finally(() => {
this.loading = false;
});
});
}
saveUser(user: IUser): Promise<any> {
this.saving = true;
const wasCreating = !user.userId;
return new Promise((resolve, reject) => {
userService.save(user).then(response => {
const newUser = new User().fromJson(response);
if (wasCreating) {
this.modifiedCount -= 1;
this.list.push(new User().fromJson(newUser));
toast.success('User created successfully');
} else {
this.updateUser(newUser);
toast.success('User updated successfully');
}
resolve(response);
}).catch(error => {
this.saving = false;
reject(error);
}).finally(() => {
this.saving = false;
});
});
}
deleteUser(userId: string): Promise<any> {
this.saving = true;
return new Promise((resolve, reject) => {
userService.delete(userId)
.then(response => {
this.modifiedCount += 1;
this.list = this.list.filter(user => user.userId !== userId);
resolve(response);
}).catch(error => {
this.saving = false;
reject(error);
}).finally(() => {
this.saving = false;
});
});
}
copyInviteCode(userId: string): void {
const content = this.list.find(u => u.userId === userId)?.invitationLink;
if (content) {
copy(content);
toast.success('Invite code copied successfully');
} else {
toast.error('Invite code not found');
}
}
generateInviteCode(userId: string): Promise<any> {
this.saving = true;
const promise = new Promise((resolve, reject) => {
userService.generateInviteCode(userId)
.then(response => {
const index = this.list.findIndex(u => u.userId === userId);
if (index > -1) {
this.list[index].updateKey('isExpiredInvite', false);
this.list[index].updateKey('invitationLink', response.invitationLink);
}
resolve(response);
}).catch(error => {
this.saving = false;
reject(error);
}).finally(() => {
this.saving = false;
});
});
toast.promise(promise, {
pending: 'Generating an invite code...',
success: 'Invite code generated successfully',
})
return promise;
}
}

View file

@ -8,11 +8,11 @@ import Profile from 'Types/session/profile';
import ReduxAction from 'Types/session/reduxAction';
import { update } from '../store';
import {
import {
init as initListsDepr,
append as listAppend,
setStartTime as setListsStartTime
} from '../lists';
setStartTime as setListsStartTime
} from '../lists';
import StatedScreen from './StatedScreen/StatedScreen';
@ -26,6 +26,7 @@ import ActivityManager from './managers/ActivityManager';
import AssistManager from './managers/AssistManager';
import MFileReader from './messages/MFileReader';
import loadFiles from './network/loadFiles';
import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './StatedScreen/StatedScreen';
import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './managers/AssistManager';
@ -33,7 +34,7 @@ import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './m
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
import type { SkipInterval } from './managers/ActivityManager';
const LIST_NAMES = [ "redux", "mobx", "vuex", "ngrx", "graphql", "exceptions", "profiles", "longtasks" ] as const;
const LIST_NAMES = ["redux", "mobx", "vuex", "ngrx", "graphql", "exceptions", "profiles", "longtasks"] as const;
const LISTS_INITIAL_STATE = {};
LIST_NAMES.forEach(name => {
LISTS_INITIAL_STATE[`${name}ListNow`] = [];
@ -65,15 +66,15 @@ type ListsObject = {
}
function initLists(): ListsObject {
const lists: Partial<ListsObject> = {} ;
const lists: Partial<ListsObject> = {};
for (var i = 0; i < LIST_NAMES.length; i++) {
lists[ LIST_NAMES[i] ] = new ListWalker();
lists[LIST_NAMES[i]] = new ListWalker();
}
return lists as ListsObject;
}
import type {
import type {
Message,
SetPageLocation,
ConnectionInformation,
@ -110,7 +111,7 @@ export default class MessageDistributor extends StatedScreen {
private navigationStartOffset: number = 0;
private lastMessageTime: number = 0;
constructor(private readonly session: any /*Session*/, jwt: string, config, live: boolean) {
constructor(private readonly session: any /*Session*/, jwt: string, config, live: boolean) {
super();
this.pagesManager = new PagesManager(this, this.session.isMobile)
this.mouseManager = new MouseManager(this);
@ -128,7 +129,7 @@ export default class MessageDistributor extends StatedScreen {
/* == REFACTOR_ME == */
const eventList = this.session.events.toJSON();
initListsDepr({
event: eventList,
event: eventList,
stack: this.session.stackEvents.toJSON(),
resource: this.session.resources.toJSON(),
});
@ -146,96 +147,83 @@ export default class MessageDistributor extends StatedScreen {
}
}
// subscribeOnMessages(sockUrl) {
// this.setMessagesLoading(true);
// const socket = new WebSocket(sockUrl);
// socket.binaryType = 'arraybuffer';
// socket.onerror = (e) => {
// // TODO: reconnect
// update({ error: true });
// }
// socket.onmessage = (socketMessage) => {
// const data = new Uint8Array(socketMessage.data);
// const msgs = [];
// messageGenerator // parseBuffer(msgs, data);
// // TODO: count indexes. Now will not work due to wrong indexes
// //msgs.forEach(this.distributeMessage);
// this.setMessagesLoading(false);
// this.setDisconnected(false);
// }
// this._socket = socket;
// }
private waitingForFiles: boolean = false
private loadMessages(): void {
const fileUrl: string = this.session.mobsUrl;
this.setMessagesLoading(true);
window.fetch(fileUrl)
.then(r => r.arrayBuffer())
.then(b => {
const r = new MFileReader(new Uint8Array(b), this.sessionStart);
const msgs: Array<Message> = [];
this.setMessagesLoading(true)
this.waitingForFiles = true
while (r.hasNext()) {
const next = r.next();
if (next != null) {
this.distributeMessage(next[0], next[1]);
msgs.push(next[0]);
const r = new MFileReader(new Uint8Array(), this.sessionStart)
const msgs: Array<Message> = []
loadFiles(this.session.mobsUrl,
b => {
r.append(b)
let next: ReturnType<MFileReader['next']>
while (next = r.next()) {
const [msg, index] = next
this.distributeMessage(msg, index)
this.lastMessageTime = Math.max(msg.time, this.lastMessageTime)
msgs.push(msg)
}
}
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation (removes first))
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
//const createNodeTypes = ["create_text_node", "create_element_node"];
this.pagesManager.sort((m1, m2) =>{
if (m1.time === m2.time) {
if (m1.tp === "remove_node" && m2.tp !== "remove_node") {
if (headChildrenIds.includes(m1.id)) {
return -1;
}
} else if (m2.tp === "remove_node" && m1.tp !== "remove_node") {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === "remove_node" && m1.tp === "remove_node") {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
return -1;
} else if (m2FromHead && !m1FromHead) {
return 1;
logger.info("Messages count: ", msgs.length, msgs)
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
this.pagesManager.sort((m1, m2) => {
if (m1.time === m2.time) {
if (m1.tp === "remove_node" && m2.tp !== "remove_node") {
if (headChildrenIds.includes(m1.id)) {
return -1;
}
} else if (m2.tp === "remove_node" && m1.tp !== "remove_node") {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === "remove_node" && m1.tp === "remove_node") {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
return -1;
} else if (m2FromHead && !m1FromHead) {
return 1;
}
}
}
}
return 0;
})
return 0;
})
logger.info("Messages count: ", msgs.length, msgs);
const stateToUpdate: {[key:string]: any} = {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
};
this.activirtManager?.end();
stateToUpdate.skipIntervals = this.activirtManager?.list || [];
LIST_NAMES.forEach(key => {
stateToUpdate[ `${ key }List` ] = this.lists[ key ].list;
});
update(stateToUpdate);
this.windowNodeCounter.reset();
this.setMessagesLoading(false);
const stateToUpdate: {[key:string]: any} = {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
}
LIST_NAMES.forEach(key => {
stateToUpdate[ `${ key }List` ] = this.lists[ key ].list
})
update(stateToUpdate)
this.setMessagesLoading(false)
}
)
.then(() => {
this.windowNodeCounter.reset()
if (this.activirtManager) {
this.activirtManager.end()
update({
skipIntervals: this.activirtManager.list
})
}
this.waitingForFiles = false
this.setMessagesLoading(false)
})
.catch(e => {
logger.error(e)
this.waitingForFiles = false
this.setMessagesLoading(false)
update({ error: true })
})
.catch((e) => {
logger.error(e);
this.setMessagesLoading(false);
update({ error: true });
});
}
move(t: number, index?: number):void {
move(t: number, index?: number): void {
const stateToUpdate: Partial<State> = {};
/* == REFACTOR_ME == */
const lastLoadedLocationMsg = this.loadedLocationManager.moveToLast(t, index);
@ -248,7 +236,7 @@ export default class MessageDistributor extends StatedScreen {
if (llEvent.domContentLoadedTime != null) {
stateToUpdate.domContentLoadedTime = {
time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db)
value: llEvent.domContentLoadedTime,
value: llEvent.domContentLoadedTime,
}
}
if (llEvent.loadTime != null) {
@ -277,9 +265,9 @@ export default class MessageDistributor extends StatedScreen {
}
LIST_NAMES.forEach(key => {
const lastMsg = this.lists[ key ].moveToLast(t, key === 'exceptions' ? undefined : index);
const lastMsg = this.lists[key].moveToLast(t, key === 'exceptions' ? undefined : index);
if (lastMsg != null) {
stateToUpdate[`${key}ListNow`] = this.lists[ key ].listNow;
stateToUpdate[`${key}ListNow`] = this.lists[key].listNow;
}
});
@ -298,21 +286,25 @@ export default class MessageDistributor extends StatedScreen {
this.window.scrollTo(lastScroll.x, lastScroll.y);
}
// Moving mouse and setting :hover classes on ready view
this.mouseManager.move(t);
this.mouseManager.move(t);
const lastClick = this.clickManager.moveToLast(t);
if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms
this.cursor.click();
}
// After all changes - redraw the marker
//this.marker.redraw();
})
})
if (this.waitingForFiles && this.lastMessageTime <= t) {
this.setMessagesLoading(true)
}
}
_decodeMessage(msg, keys: Array<string>) {
const decoded = {};
try {
keys.forEach(key => {
decoded[ key ] = this.decoder.decode(msg[ key ]);
decoded[key] = this.decoder.decode(msg[key]);
});
} catch (e) {
logger.error("Error on message decoding: ", e, msg);
@ -323,15 +315,13 @@ export default class MessageDistributor extends StatedScreen {
/* Binded */
distributeMessage = (msg: Message, index: number): void => {
this.lastMessageTime = msg.time;
if ([
if ([
"mouse_move",
"mouse_click",
"create_element_node", // not a user activity, though visual change
"set_input_value",
"set_input_checked",
"set_viewport_size",
"set_viewport_size",
"set_viewport_scroll",
].includes(msg.tp)) {
this.activirtManager?.updateAcctivity(msg.time);
@ -343,13 +333,13 @@ export default class MessageDistributor extends StatedScreen {
/* Lists: */
case "console_log":
if (msg.level === 'debug') break;
listAppend("log", Log({
listAppend("log", Log({
level: msg.level,
value: msg.value,
time,
time,
index,
}));
break;
break;
case "fetch":
listAppend("fetch", Resource({
method: msg.method,
@ -362,118 +352,117 @@ export default class MessageDistributor extends StatedScreen {
time: msg.timestamp - this.sessionStart, //~
index,
}));
break;
break;
/* */
case "set_page_location":
this.locationManager.add(msg);
if (msg.navigationStart > 0) {
this.loadedLocationManager.add(msg);
}
break;
break;
case "set_viewport_size":
this.resizeManager.add(msg);
break;
break;
case "mouse_move":
this.mouseManager.add(msg);
break;
break;
case "mouse_click":
this.clickManager.add(msg);
break;
break;
case "set_viewport_scroll":
this.scrollManager.add(msg);
break;
break;
case "performance_track":
this.performanceTrackManager.add(msg);
break;
break;
case "set_page_visibility":
this.performanceTrackManager.handleVisibility(msg)
break;
break;
case "connection_information":
this.connectionInfoManger.add(msg);
break;
break;
case "o_table":
this.decoder.set(msg.key, msg.value);
break;
break;
case "redux":
decoded = this._decodeMessage(msg, ["state", "action"]);
logger.log(decoded)
if (decoded != null) {
this.lists.redux.add(decoded);
}
break;
break;
case "ng_rx":
decoded = this._decodeMessage(msg, ["state", "action"]);
logger.log(decoded)
if (decoded != null) {
this.lists.ngrx.add(decoded);
}
break;
}
break;
case "vuex":
decoded = this._decodeMessage(msg, ["state", "mutation"]);
logger.log(decoded)
if (decoded != null) {
this.lists.vuex.add(decoded);
}
break;
}
break;
case "mob_x":
decoded = this._decodeMessage(msg, ["payload"]);
logger.log(decoded)
if (decoded != null) {
this.lists.mobx.add(decoded);
}
break;
}
break;
case "graph_ql":
// @ts-ignore some hack? TODO: remove
msg.duration = 0;
this.lists.graphql.add(msg);
break;
break;
case "profiler":
this.lists.profiles.add(msg);
break;
break;
case "long_task":
this.lists.longtasks.add({
...msg,
time: msg.timestamp - this.sessionStart,
});
break;
break;
default:
switch (msg.tp){
switch (msg.tp) {
case "create_document":
this.windowNodeCounter.reset();
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
break;
case "create_text_node":
case "create_element_node":
this.windowNodeCounter.addNode(msg.id, msg.parentID);
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
break;
case "move_node":
this.windowNodeCounter.moveNode(msg.id, msg.parentID);
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
break;
case "remove_node":
this.windowNodeCounter.removeNode(msg.id);
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
break;
}
this.pagesManager.add(msg);
break;
break;
}
}
getLastMessageTime():number {
getLastMessageTime(): number {
return this.lastMessageTime;
}
getFirstMessageTime():number {
getFirstMessageTime(): number {
return 0; //this.pagesManager.minTime;
}
// TODO: clean managers?
clean() {
super.clean();
//if (this._socket) this._socket.close();
update(INITIAL_STATE);
this.assistManager.clear();
}

View file

@ -7,62 +7,65 @@ import RawMessageReader from './RawMessageReader';
// needSkipMessage() and next() methods here use buf and p protected properties,
// which should be probably somehow incapsulated
export default class MFileReader extends RawMessageReader {
private pLastMessageID: number = 0;
private currentTime: number = 0;
public error: boolean = false;
private pLastMessageID: number = 0
private currentTime: number = 0
public error: boolean = false
constructor(data: Uint8Array, private readonly startTime: number) {
super(data);
super(data)
}
private needSkipMessage(): boolean {
if (this.p === 0) return false;
if (this.p === 0) return false
for (let i = 7; i >= 0; i--) {
if (this.buf[ this.p + i ] !== this.buf[ this.pLastMessageID + i ]) {
return this.buf[ this.p + i ] - this.buf[ this.pLastMessageID + i ] < 0;
return this.buf[ this.p + i ] - this.buf[ this.pLastMessageID + i ] < 0
}
}
return true;
return true
}
private readRawMessage(): RawMessage | null {
this.skip(8);
this.skip(8)
try {
return super.readMessage();
const msg = super.readMessage()
if (!msg) {
this.skip(-8)
}
return msg
} catch (e) {
this.error = true;
logger.error("Read message error:", e);
return null;
this.error = true
logger.error("Read message error:", e)
return null
}
}
hasNext():boolean {
return !this.error && this.hasNextByte();
}
next(): [ Message, number] | null {
if (!this.hasNext()) {
return null;
if (this.error || !this.hasNextByte()) {
return null
}
while (this.needSkipMessage()) {
this.readRawMessage();
if (!this.readRawMessage()) {
return null
}
}
this.pLastMessageID = this.p;
const rMsg = this.readRawMessage();
this.pLastMessageID = this.p
const rMsg = this.readRawMessage()
if (!rMsg) {
return null;
return null
}
if (rMsg.tp === "timestamp") {
this.currentTime = rMsg.timestamp - this.startTime;
} else {
const msg = Object.assign(rMsg, {
time: this.currentTime,
_index: this.pLastMessageID,
})
return [msg, this.pLastMessageID];
}
return null;
this.currentTime = rMsg.timestamp - this.startTime
return this.next()
}
const msg = Object.assign(rMsg, {
time: this.currentTime,
_index: this.pLastMessageID,
})
return [msg, this.pLastMessageID]
}
}

View file

@ -6,7 +6,6 @@ export function resolveURL(baseURL: string, relURL: string): string {
}
var match = /bar/.exec("foobar");
const re1 = /url\(("[^"]*"|'[^']*'|[^)]*)\)/g
const re2 = /@import "(.*?)"/g
function cssUrlsIndex(css: string): Array<[number, number]> {

View file

@ -0,0 +1,49 @@
const NO_NTH_FILE = "nnf"
export default function load(
urls: string[],
onData: (Uint8Array) => void,
): Promise<void> {
const firstFileURL = urls.shift()
if (!firstFileURL) {
return Promise.reject("No urls provided")
}
return window.fetch(firstFileURL)
.then(r => {
if (r.status >= 400) {
throw new Error(`no start file. status code ${ r.status }`)
}
return r.arrayBuffer()
})
.then(b => new Uint8Array(b))
.then(onData)
.then(() =>
urls.reduce((p, url) =>
p.then(() =>
window.fetch(url)
.then(r => {
return new Promise<ArrayBuffer>((res, rej) => {
if (r.status == 404) {
rej(NO_NTH_FILE)
return
}
if (r.status >= 400) {
rej(`Bad endfile status code ${r.status}`)
return
}
res(r.arrayBuffer())
})
})
.then(b => new Uint8Array(b))
.then(onData)
),
Promise.resolve(),
)
)
.catch(e => {
if (e === NO_NTH_FILE) {
return
}
throw e
})
}

View file

@ -61,10 +61,11 @@ export const CLIENT_TABS = {
PROFILE: 'account',
MANAGE_USERS: 'team',
MANAGE_ROLES: 'roles',
SITES: 'projects',
SITES: 'projects',
CUSTOM_FIELDS: 'metadata',
WEBHOOKS: 'webhooks',
NOTIFICATIONS: 'notifications',
AUDIT: 'audit',
};
export const CLIENT_DEFAULT_TAB = CLIENT_TABS.PROFILE;
const routerClientTabString = `:activeTab(${ Object.values(CLIENT_TABS).join('|') })`;

View file

@ -0,0 +1,25 @@
import APIClient from 'App/api_client';
export default class AuditService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
all(data: any): Promise<any> {
return this.client.post('/trails', data)
.then(response => response.json())
.then(response => response.data || []);
}
one(id: string): Promise<any> {
return this.client.get('/trails/' + id)
.then(response => response.json())
.then(response => response.data || {});
}
}

View file

@ -0,0 +1,57 @@
import APIClient from 'App/api_client';
import { IUser } from 'App/mstore/types/user'
export default class UserService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
all() {
return this.client.get('/client/members')
.then(response => response.json())
.then(response => response.data || []);
}
one(userId: string) {
return this.client.get('/users/' + userId)
.then(response => response.json())
.then(response => response.data || {});
}
save(user: IUser): Promise<any> {
const data = user.toSave();
if (user.userId) {
return this.client.put('/client/members/' + user.userId, data)
.then(response => response.json())
.then(response => response.data || {})
} else {
return this.client.post('/client/members', data)
.then(response => response.json())
.then(response => response.data || {});
}
}
generateInviteCode(userId: any): Promise<any> {
return this.client.get(`/client/members/${userId}/reset`)
.then(response => response.json())
.then(response => response.data || {});
}
delete(userId: string) {
return this.client.delete('/client/members/' + userId)
.then(response => response.json())
.then(response => response.data || {});
}
getRoles() {
return this.client.get('/client/roles')
.then(response => response.json())
.then(response => response.data || []);
}
}

View file

@ -1,7 +1,11 @@
import DashboardService, { IDashboardService } from "./DashboardService";
import MetricService, { IMetricService } from "./MetricService";
import SessionSerivce from "./SessionService";
import UserService from "./UserService";
import AuditService from './AuditService';
export const dashboardService: IDashboardService = new DashboardService();
export const metricService: IMetricService = new MetricService();
export const sessionService: SessionSerivce = new SessionSerivce();
export const sessionService: SessionSerivce = new SessionSerivce();
export const userService: UserService = new UserService();
export const auditService: AuditService = new AuditService();

View file

@ -147,13 +147,4 @@
height: 100vh;
overflow-y: hidden;
padding-right: 15px;
}
/* .svg-map__location {
fill: #EEE !important;
cursor: pointer;
&:hover {
fill: #fff !important;
}
} */
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-grid-3x3" viewBox="0 0 16 16">
<path d="M0 1.5A1.5 1.5 0 0 1 1.5 0h13A1.5 1.5 0 0 1 16 1.5v13a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13zM1.5 1a.5.5 0 0 0-.5.5V5h4V1H1.5zM5 6H1v4h4V6zm1 4h4V6H6v4zm-1 1H1v3.5a.5.5 0 0 0 .5.5H5v-4zm1 0v4h4v-4H6zm5 0v4h3.5a.5.5 0 0 0 .5-.5V11h-4zm0-1h4V6h-4v4zm0-5h4V1.5a.5.5 0 0 0-.5-.5H11v4zm-1 0V1H6v4h4z"/>
</svg>

After

Width:  |  Height:  |  Size: 454 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-list-ul" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-paragraph" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-text-paragraph" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 374 B

View file

@ -103,6 +103,10 @@ export default Record({
endTimestamp: this.end,
};
},
rangeFormatted(format = 'MMM Do YY, hh:mm A') {
console.log('period', this)
return this.range.start.format(format) + ' - ' + this.range.end.format(format);
},
toTimestampstwo() {
return {
startTimestamp: this.start / 1000,

View file

@ -264,4 +264,55 @@ export const convertElementToImage = async (el) => {
},
});
return image;
}
export const unserscoreToSpaceAndCapitalize = (str) => {
return str.replace(/_/g, ' ').replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
export const convertToCSV = (headers, objArray) => {
var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
var str = '';
const headersMap = headers.reduce((acc, curr) => {
acc[curr.key] = curr;
return acc;
}, {});
str += headers.map(h => h.label).join(',') + '\r\n';
for (var i = 0; i < array.length; i++) {
var line = '';
for (var index in headersMap) {
if (line !== '') line += ',';
line += array[i][index];
}
str += line + '\r\n';
}
return str;
}
export const exportCSVFile = (headers, items, fileTitle) => {
var jsonObject = JSON.stringify(items);
var csv = convertToCSV(headers, jsonObject);
var exportedFilenmae = fileTitle + '.csv' || 'export.csv';
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
if (navigator.msSaveBlob) { // IE 10+
navigator.msSaveBlob(blob, exportedFilenmae);
} else {
var link = document.createElement("a");
if (link.download !== undefined) {
var url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", exportedFilenmae);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}

View file

@ -55,6 +55,7 @@ module.exports = {
'height',
'inset',
'justifyContent',
'justifySelf',
'letterSpacing',
'lineHeight',
// 'listStylePosition',

View file

@ -20,9 +20,9 @@ usr=`whoami`
# Installing k3s
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.22.8+k3s1' INSTALL_K3S_EXEC="--no-deploy=traefik" sh -
mkdir ~/.kube
[[ -d ~/.kube ]] || mkdir ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
chmod 0644 ~/.kube/config
sudo chmod 0644 ~/.kube/config
sudo chown -R $usr ~/.kube/config

31
tracker/README.md Normal file
View file

@ -0,0 +1,31 @@
## Local build
In order to build locally any of the javascript packages located under this directory, go to the corresponding folder first:
```sh
cd tracker # or any tracker-* plugin
```
Then run
```sh
yarn
yarn build
```
OR
```sh
npm i
npm run build
```
You can then use it as a local javascript package by executing the folowing line under your local project location:
```sh
yarn add file:../path/to/openreplay/monorepo/tracker/tracker
````
OR
```sh
npm install --save ../path/to/openreplay/monorepo/tracker/tracker
```

View file

@ -1,16 +1,16 @@
{
"name": "@openreplay/tracker-assist",
"version": "3.5.7",
"version": "3.5.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@openreplay/tracker-assist",
"version": "3.5.7",
"version": "3.5.8",
"license": "MIT",
"dependencies": {
"csstype": "^3.0.10",
"peerjs": "^1.3.2",
"peerjs": "1.3.2",
"socket.io-client": "^4.4.1"
},
"devDependencies": {
@ -25,7 +25,7 @@
},
"../tracker": {
"name": "@openreplay/tracker",
"version": "3.5.4",
"version": "3.5.11",
"dev": true,
"license": "MIT",
"dependencies": {
@ -644,15 +644,19 @@
}
},
"../tracker/node_modules/ajv": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"../tracker/node_modules/ansi-escapes": {
@ -1791,12 +1795,6 @@
"node": ">=6"
}
},
"../tracker/node_modules/json5/node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"../tracker/node_modules/levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -1853,9 +1851,9 @@
}
},
"../tracker/node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"../tracker/node_modules/mkdirp": {
@ -2376,9 +2374,9 @@
}
},
"../tracker/node_modules/strip-ansi/node_modules/ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true,
"engines": {
"node": ">=6"
@ -4444,9 +4442,9 @@
"requires": {}
},
"ajv": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@ -5337,14 +5335,6 @@
"dev": true,
"requires": {
"minimist": "^1.2.5"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
},
"levn": {
@ -5391,9 +5381,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"mkdirp": {
@ -5793,9 +5783,9 @@
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"dev": true
}
}

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker-assist",
"description": "Tracker plugin for screen assistance through the WebRTC",
"version": "3.5.8",
"version": "3.5.9",
"keywords": [
"WebRTC",
"assistance",
@ -25,7 +25,7 @@
},
"dependencies": {
"csstype": "^3.0.10",
"peerjs": "^1.3.2",
"peerjs": "1.3.2",
"socket.io-client": "^4.4.1"
},
"peerDependencies": {

View file

@ -11,6 +11,7 @@ import AnnotationCanvas from './AnnotationCanvas.js';
import ConfirmWindow, { callConfirmDefault, controlConfirmDefault } from './ConfirmWindow.js';
import type { Options as ConfirmOptions } from './ConfirmWindow.js';
// TODO: fully specified strict check (everywhere)
//@ts-ignore peerjs hack for webpack5 (?!) TODO: ES/node modules;
Peer = Peer.default || Peer;

View file

@ -20,7 +20,7 @@ export default class RemoteControl {
reconnect(ids: string[]) {
const storedID = sessionStorage.getItem(this.options.session_control_peer_key)
if (storedID !== null && ids.includes(storedID)) {
if (storedID !== null && ids.indexOf(storedID) !== -1) {
this.grantControl(storedID)
} else {
sessionStorage.removeItem(this.options.session_control_peer_key)

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "3.5.10",
"version": "3.5.11",
"keywords": [
"logging",
"replay"

View file

@ -15,31 +15,31 @@ async function main() {
to: webworker.replace(/'/g, "\\'"),
});
await fs.rename('build/main', 'lib');
await fs.rename('build/messages', 'lib/messages');
await fs.rename('build/common', 'lib/common');
await replaceInFiles({
files: 'lib/*',
from: /\.\.\/messages/g,
to: './messages',
from: /\.\.\/common/g,
to: './common',
});
await replaceInFiles({
files: 'lib/**/*',
from: /\.\.\/\.\.\/messages/g,
to: '../messages',
from: /\.\.\/\.\.\/common/g,
to: '../common',
});
await fs.rename('build/cjs/main', 'cjs');
await fs.rename('build/cjs/messages', 'cjs/messages');
await fs.rename('build/cjs/common', 'cjs/common');
await fs.writeFile('cjs/package.json', `{ "type": "commonjs" }`);
await replaceInFiles({
files: 'cjs/*',
from: /\.\.\/messages/g,
to: './messages',
from: /\.\.\/common/g,
to: './common',
});
await replaceInFiles({
files: 'cjs/**/*',
from: /\.\.\/\.\.\/messages/g,
to: '../messages',
from: /\.\.\/\.\.\/common/g,
to: '../common',
});
}
main()

View file

@ -1,6 +1,6 @@
// Auto-generated, do not edit
import Message from "./message.js";
import Writer from "./writer.js";
import type { Writer, Message }from "./types.js";
export default Message
function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
Class: C & { new(...args: A): T }

View file

@ -0,0 +1,10 @@
export interface Writer {
uint(n: number): boolean
int(n: number): boolean
string(s: string): boolean
boolean(b: boolean): boolean
}
export interface Message {
encode(w: Writer): boolean;
}

View file

@ -1,6 +1,6 @@
import type Message from "../../common/messages.js";
import { Timestamp, Metadata } from "../../common/messages.js";
import { timestamp, deprecationWarn } from "../utils.js";
import { Timestamp, Metadata } from "../../messages/index.js";
import Message from "../../messages/message.js";
import Nodes from "./nodes.js";
import Observer from "./observer/top_observer.js";
import Sanitizer from "./sanitizer.js";
@ -13,7 +13,7 @@ import { deviceMemory, jsHeapSizeLimit } from "../modules/performance.js";
import type { Options as ObserverOptions } from "./observer/top_observer.js";
import type { Options as SanitizerOptions } from "./sanitizer.js";
import type { Options as LoggerOptions } from "./logger.js"
import type { Options as WebworkerOptions, WorkerMessageData } from "../../webworker/types.js";
import type { Options as WebworkerOptions, WorkerMessageData } from "../../common/webworker.js";
// TODO: Unify and clearly describe options logic

View file

@ -1,5 +1,5 @@
import Observer from "./observer.js";
import { CreateIFrameDocument } from "../../../messages/index.js";
import { CreateIFrameDocument } from "../../../common/messages.js";
export default class IFrameObserver extends Observer {
observe(iframe: HTMLIFrameElement) {

View file

@ -8,7 +8,7 @@ import {
CreateElementNode,
MoveNode,
RemoveNode,
} from "../../../messages/index.js";
} from "../../../common/messages.js";
import App from "../index.js";
import { isInstance, inDocument } from "../context.js";

View file

@ -1,5 +1,5 @@
import Observer from "./observer.js";
import { CreateIFrameDocument } from "../../../messages/index.js";
import { CreateIFrameDocument } from "../../../common/messages.js";
export default class ShadowRootObserver extends Observer {
observe(el: Element) {

View file

@ -4,7 +4,7 @@ import type { Window } from "../context.js";
import IFrameObserver from "./iframe_observer.js";
import ShadowRootObserver from "./shadow_root_observer.js";
import { CreateDocument } from "../../../messages/index.js";
import { CreateDocument } from "../../../common/messages.js";
import App from "../index.js";
import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js'

View file

@ -1,5 +1,5 @@
import App, { StartOptions } from "./index.js";
import { UserID, UserAnonymousID, Metadata } from "../../messages/index.js";
import { UserID, UserAnonymousID, Metadata } from "../../common/messages.js";
enum ActivityState {

View file

@ -1,8 +1,8 @@
import App, { DEFAULT_INGEST_POINT } from "./app/index.js";
export { default as App } from './app/index.js';
import { UserID, UserAnonymousID, Metadata, RawCustomEvent, CustomIssue } from "../messages/index.js";
import * as _Messages from "../messages/index.js";
import { UserID, UserAnonymousID, Metadata, RawCustomEvent, CustomIssue } from "../common/messages.js";
import * as _Messages from "../common/messages.js";
export const Messages = _Messages;
import Connection from "./modules/connection.js";

View file

@ -1,5 +1,5 @@
import App from "../app/index.js";
import { ConnectionInformation } from "../../messages/index.js";
import { ConnectionInformation } from "../../common/messages.js";
export default function(app: App): void {
const connection:

View file

@ -1,6 +1,6 @@
import App from "../app/index.js";
import { IN_BROWSER } from "../utils.js";
import { ConsoleLog } from "../../messages/index.js";
import { ConsoleLog } from "../../common/messages.js";
const printError: (e: Error) => string =
IN_BROWSER && 'InstallTrigger' in window // detect Firefox

View file

@ -1,5 +1,5 @@
import App from "../app/index.js";
import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from "../../messages/index.js";
import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from "../../common/messages.js";
export default function(app: App | null) {
if (app === null) {

View file

@ -1,6 +1,6 @@
import type Message from "../../common/messages.js";
import App from "../app/index.js";
import { JSException } from "../../messages/index.js";
import Message from "../../messages/message.js";
import { JSException } from "../../common/messages.js";
import ErrorStackParser from 'error-stack-parser';
export interface Options {

View file

@ -1,6 +1,6 @@
import { timestamp, isURL } from "../utils.js";
import App from "../app/index.js";
import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from "../../messages/index.js";
import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from "../../common/messages.js";
const PLACEHOLDER_SRC = "https://static.openreplay.com/tracker/placeholder.jpeg";

View file

@ -5,7 +5,7 @@ import {
hasOpenreplayAttribute,
} from "../utils.js";
import App from "../app/index.js";
import { SetInputTarget, SetInputValue, SetInputChecked } from "../../messages/index.js";
import { SetInputTarget, SetInputValue, SetInputChecked } from "../../common/messages.js";
// TODO: take into consideration "contenteditable" attribute
type TextEditableElement = HTMLInputElement | HTMLTextAreaElement

View file

@ -1,5 +1,5 @@
import App from "../app/index.js";
import { LongTask } from "../../messages/index.js";
import { LongTask } from "../../common/messages.js";
// https://w3c.github.io/performance-timeline/#the-performanceentry-interface
interface TaskAttributionTiming extends PerformanceEntry {

View file

@ -4,7 +4,7 @@ import {
getLabelAttribute,
} from "../utils.js";
import App from "../app/index.js";
import { MouseMove, MouseClick } from "../../messages/index.js";
import { MouseMove, MouseClick } from "../../common/messages.js";
import { getInputLabel } from "./input.js";
function _getSelector(target: Element): string {

View file

@ -1,6 +1,6 @@
import App from "../app/index.js";
import { IN_BROWSER } from "../utils.js";
import { PerformanceTrack } from "../../messages/index.js";
import { PerformanceTrack } from "../../common/messages.js";
type Perf = {

View file

@ -1,5 +1,5 @@
import App from "../app/index.js";
import { SetViewportScroll, SetNodeScroll } from "../../messages/index.js";
import { SetViewportScroll, SetNodeScroll } from "../../common/messages.js";
export default function (app: App): void {
let documentScroll = false;

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