diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 2b99883d2..bbdf8f7b8 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -30,7 +30,8 @@ func main() { cfg := db.New() // Init database - pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres, cfg.BatchQueueLimit, cfg.BatchSizeLimit, metrics), cfg.ProjectExpirationTimeoutMs) + pg := cache.NewPGCache( + postgres.NewConn(cfg.Postgres, cfg.BatchQueueLimit, cfg.BatchSizeLimit, metrics), cfg.ProjectExpirationTimeoutMs) defer pg.Close() // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. diff --git a/backend/internal/config/configurator/configurator.go b/backend/internal/config/configurator/configurator.go index 5335d4a91..1b459335d 100644 --- a/backend/internal/config/configurator/configurator.go +++ b/backend/internal/config/configurator/configurator.go @@ -30,7 +30,13 @@ func readFile(path string) (map[string]string, error) { res := make(map[string]string) lines := strings.Split(string(data), "\n") for _, line := range lines { + if len(line) == 0 { + continue + } env := strings.Split(line, "=") + if len(env) < 2 { + continue + } res[env[0]] = env[1] } return res, nil diff --git a/backend/pkg/db/cache/cache.go b/backend/pkg/db/cache/cache.go new file mode 100644 index 000000000..9075ae73f --- /dev/null +++ b/backend/pkg/db/cache/cache.go @@ -0,0 +1,73 @@ +package cache + +import ( + "log" + "openreplay/backend/pkg/db/postgres" + "openreplay/backend/pkg/db/types" + "sync" + "time" +) + +type SessionMeta struct { + *types.Session + lastUse time.Time +} + +type ProjectMeta struct { + *types.Project + expirationTime time.Time +} + +type Cache interface { + SetSession(sess *types.Session) + HasSession(sessID uint64) bool + GetSession(sessionID uint64) (*types.Session, error) + GetProject(projectID uint32) (*types.Project, error) + GetProjectByKey(projectKey string) (*types.Project, error) +} + +type cacheImpl struct { + conn *postgres.Conn + mutex sync.RWMutex + sessions map[uint64]*SessionMeta + projects map[uint32]*ProjectMeta + projectsByKeys sync.Map + projectExpirationTimeout time.Duration +} + +func NewCache(conn *postgres.Conn, projectExpirationTimeoutMs int64) Cache { + newCache := &cacheImpl{ + conn: conn, + sessions: make(map[uint64]*SessionMeta), + projects: make(map[uint32]*ProjectMeta), + projectExpirationTimeout: time.Duration(1000 * projectExpirationTimeoutMs), + } + go newCache.cleaner() + return newCache +} + +func (c *cacheImpl) cleaner() { + cleanTick := time.Tick(time.Minute * 5) + for { + select { + case <-cleanTick: + c.clearCache() + } + } +} + +func (c *cacheImpl) clearCache() { + c.mutex.Lock() + defer c.mutex.Unlock() + + now := time.Now() + cacheSize := len(c.sessions) + deleted := 0 + for id, sess := range c.sessions { + if now.Sub(sess.lastUse).Minutes() > 3 { + deleted++ + delete(c.sessions, id) + } + } + log.Printf("cache cleaner: deleted %d/%d sessions", deleted, cacheSize) +} diff --git a/backend/pkg/db/cache/messages-common.go b/backend/pkg/db/cache/messages-common.go index a4df19cc3..46fc64790 100644 --- a/backend/pkg/db/cache/messages-common.go +++ b/backend/pkg/db/cache/messages-common.go @@ -19,7 +19,6 @@ func (c *PGCache) HandleSessionEnd(sessionID uint64) error { if err := c.Conn.HandleSessionEnd(sessionID); err != nil { log.Printf("can't handle session end: %s", err) } - c.DeleteSession(sessionID) return nil } diff --git a/backend/pkg/db/cache/messages-ios.go b/backend/pkg/db/cache/messages-ios.go index e0463c431..e65051f33 100644 --- a/backend/pkg/db/cache/messages-ios.go +++ b/backend/pkg/db/cache/messages-ios.go @@ -1,16 +1,16 @@ package cache import ( - "errors" + "fmt" . "openreplay/backend/pkg/db/types" . "openreplay/backend/pkg/messages" ) func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) error { - if c.sessions[sessionID] != nil { - return errors.New("This session already in cache!") + if c.cache.HasSession(sessionID) { + return fmt.Errorf("session %d already in cache", sessionID) } - c.sessions[sessionID] = &Session{ + newSess := &Session{ SessionID: sessionID, Platform: "ios", Timestamp: s.Timestamp, @@ -24,8 +24,10 @@ func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) er UserCountry: s.UserCountry, UserDeviceType: s.UserDeviceType, } - if err := c.Conn.InsertSessionStart(sessionID, c.sessions[sessionID]); err != nil { - c.sessions[sessionID] = nil + c.cache.SetSession(newSess) + if err := c.Conn.InsertSessionStart(sessionID, newSess); err != nil { + // don't know why? + c.cache.SetSession(nil) return err } return nil diff --git a/backend/pkg/db/cache/messages-web.go b/backend/pkg/db/cache/messages-web.go index 50b531d0a..de97ef42a 100644 --- a/backend/pkg/db/cache/messages-web.go +++ b/backend/pkg/db/cache/messages-web.go @@ -1,7 +1,7 @@ package cache import ( - "errors" + "fmt" . "openreplay/backend/pkg/db/types" . "openreplay/backend/pkg/messages" ) @@ -31,10 +31,10 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error } func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error { - if c.sessions[sessionID] != nil { - return errors.New("This session already in cache!") + if c.cache.HasSession(sessionID) { + return fmt.Errorf("session %d already in cache", sessionID) } - c.sessions[sessionID] = &Session{ + newSess := &Session{ SessionID: sessionID, Platform: "web", Timestamp: s.Timestamp, @@ -55,8 +55,10 @@ func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error UserDeviceHeapSize: s.UserDeviceHeapSize, UserID: &s.UserID, } - if err := c.Conn.HandleSessionStart(sessionID, c.sessions[sessionID]); err != nil { - c.sessions[sessionID] = nil + c.cache.SetSession(newSess) + if err := c.Conn.HandleSessionStart(sessionID, newSess); err != nil { + // don't know why? + c.cache.SetSession(nil) return err } return nil diff --git a/backend/pkg/db/cache/pg-cache.go b/backend/pkg/db/cache/pg-cache.go index bb87f9f4c..75e468d35 100644 --- a/backend/pkg/db/cache/pg-cache.go +++ b/backend/pkg/db/cache/pg-cache.go @@ -1,31 +1,20 @@ package cache import ( - "sync" - "time" - "openreplay/backend/pkg/db/postgres" - . "openreplay/backend/pkg/db/types" ) -type ProjectMeta struct { - *Project - expirationTime time.Time -} - type PGCache struct { *postgres.Conn - sessions map[uint64]*Session - projects map[uint32]*ProjectMeta - projectsByKeys sync.Map // map[string]*ProjectMeta - projectExpirationTimeout time.Duration + cache Cache } -func NewPGCache(pgConn *postgres.Conn, projectExpirationTimeoutMs int64) *PGCache { +func NewPGCache(conn *postgres.Conn, projectExpirationTimeoutMs int64) *PGCache { + // Create in-memory cache layer for sessions and projects + c := NewCache(conn, projectExpirationTimeoutMs) + // Return PG wrapper with integrated cache layer return &PGCache{ - Conn: pgConn, - sessions: make(map[uint64]*Session), - projects: make(map[uint32]*ProjectMeta), - projectExpirationTimeout: time.Duration(1000 * projectExpirationTimeoutMs), + Conn: conn, + cache: c, } } diff --git a/backend/pkg/db/cache/project.go b/backend/pkg/db/cache/project.go index 35d952302..60b868501 100644 --- a/backend/pkg/db/cache/project.go +++ b/backend/pkg/db/cache/project.go @@ -5,7 +5,7 @@ import ( "time" ) -func (c *PGCache) GetProjectByKey(projectKey string) (*Project, error) { +func (c *cacheImpl) GetProjectByKey(projectKey string) (*Project, error) { pmInterface, found := c.projectsByKeys.Load(projectKey) if found { if pm, ok := pmInterface.(*ProjectMeta); ok { @@ -15,7 +15,7 @@ func (c *PGCache) GetProjectByKey(projectKey string) (*Project, error) { } } - p, err := c.Conn.GetProjectByKey(projectKey) + p, err := c.conn.GetProjectByKey(projectKey) if err != nil { return nil, err } @@ -24,12 +24,12 @@ func (c *PGCache) GetProjectByKey(projectKey string) (*Project, error) { return p, nil } -func (c *PGCache) GetProject(projectID uint32) (*Project, error) { +func (c *cacheImpl) GetProject(projectID uint32) (*Project, error) { if c.projects[projectID] != nil && time.Now().Before(c.projects[projectID].expirationTime) { return c.projects[projectID].Project, nil } - p, err := c.Conn.GetProject(projectID) + p, err := c.conn.GetProject(projectID) if err != nil { return nil, err } diff --git a/backend/pkg/db/cache/session.go b/backend/pkg/db/cache/session.go index 89b8f89f8..f03f1e955 100644 --- a/backend/pkg/db/cache/session.go +++ b/backend/pkg/db/cache/session.go @@ -4,28 +4,48 @@ import ( "errors" "github.com/jackc/pgx/v4" . "openreplay/backend/pkg/db/types" + "time" ) var NilSessionInCacheError = errors.New("nil session in error") -func (c *PGCache) GetSession(sessionID uint64) (*Session, error) { - if s, inCache := c.sessions[sessionID]; inCache { - if s == nil { - return s, NilSessionInCacheError - } - return s, nil +func (c *cacheImpl) SetSession(sess *Session) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if meta, ok := c.sessions[sess.SessionID]; ok { + meta.Session = sess + meta.lastUse = time.Now() + } else { + c.sessions[sess.SessionID] = &SessionMeta{sess, time.Now()} } - s, err := c.Conn.GetSession(sessionID) +} + +func (c *cacheImpl) HasSession(sessID uint64) bool { + c.mutex.RLock() + defer c.mutex.RUnlock() + + sess, ok := c.sessions[sessID] + return ok && sess.Session != nil +} + +func (c *cacheImpl) GetSession(sessionID uint64) (*Session, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if s, inCache := c.sessions[sessionID]; inCache { + if s.Session == nil { + return nil, NilSessionInCacheError + } + return s.Session, nil + } + s, err := c.conn.GetSession(sessionID) if err == pgx.ErrNoRows { - c.sessions[sessionID] = nil + c.sessions[sessionID] = &SessionMeta{nil, time.Now()} } if err != nil { return nil, err } - c.sessions[sessionID] = s + c.sessions[sessionID] = &SessionMeta{s, time.Now()} return s, nil } - -func (c *PGCache) DeleteSession(sessionID uint64) { - delete(c.sessions, sessionID) -} diff --git a/backend/pkg/db/types/error-event.go b/backend/pkg/db/types/error-event.go index 51569346f..826cbba9e 100644 --- a/backend/pkg/db/types/error-event.go +++ b/backend/pkg/db/types/error-event.go @@ -3,6 +3,7 @@ package types import ( "encoding/hex" "encoding/json" + "fmt" "hash/fnv" "log" "strconv" @@ -27,6 +28,9 @@ func unquote(s string) string { return s } func parseTags(tagsJSON string) (tags map[string]*string, err error) { + if len(tagsJSON) == 0 { + return nil, fmt.Errorf("empty tags") + } if tagsJSON[0] == '[' { var tagsArr []json.RawMessage if err = json.Unmarshal([]byte(tagsJSON), &tagsArr); err != nil { diff --git a/backend/pkg/db/types/session.go b/backend/pkg/db/types/session.go index 53fef410c..202eb9966 100644 --- a/backend/pkg/db/types/session.go +++ b/backend/pkg/db/types/session.go @@ -20,7 +20,7 @@ type Session struct { IssueTypes []string IssueScore int - UserID *string // pointer?? + UserID *string UserAnonymousID *string Metadata1 *string Metadata2 *string diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index 0964ff7b8..5f2645fc8 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -71,7 +71,7 @@ function CustomFields(props) {
- +
diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx similarity index 96% rename from frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx rename to frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx index 5934dfe52..1d0721495 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddProjectButton.tsx @@ -22,7 +22,7 @@ function AddProjectButton({ isAdmin = false, init = () => {} }: any) { }; return ( - + ); } diff --git a/frontend/app/components/Client/Sites/AddProjectButton/index.ts b/frontend/app/components/Client/Sites/AddProjectButton/index.ts index 66beb9cf8..631dd2a10 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/index.ts +++ b/frontend/app/components/Client/Sites/AddProjectButton/index.ts @@ -1 +1 @@ -export { default } from './AddUserButton'; \ No newline at end of file +export { default } from './AddProjectButton'; \ No newline at end of file diff --git a/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx b/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx index df2777e83..19f77b420 100644 --- a/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx +++ b/frontend/app/components/Client/Users/components/AddUserButton/AddUserButton.tsx @@ -14,16 +14,7 @@ function AddUserButton({ isAdmin = false, onClick }: any ) { - - {/* */} + ); } diff --git a/frontend/app/components/Client/Webhooks/Webhooks.js b/frontend/app/components/Client/Webhooks/Webhooks.js index 4235cd28b..e005a893a 100644 --- a/frontend/app/components/Client/Webhooks/Webhooks.js +++ b/frontend/app/components/Client/Webhooks/Webhooks.js @@ -46,7 +46,7 @@ function Webhooks(props) {

{'Webhooks'}

{/* +
diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx index 277f13ab8..07b77961a 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx @@ -21,7 +21,7 @@ function AlertsView({ siteId, init }: IAlertsView) {
- +
diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx index 11634632f..5341c3487 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx @@ -27,7 +27,7 @@ function DashboardsView({ history, siteId }: { history: any, siteId: string }) {
- +
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index d5402fd2c..dd87b2fef 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -22,7 +22,7 @@ function MetricsView({ siteId }: Props) {
- +
diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index 813c394e3..a09e48777 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -5,10 +5,10 @@ import { Tooltip } from 'react-tippy'; interface Props { filter: any; + isFirst: boolean } function FunnelBar(props: Props) { - const { filter } = props; - // const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues); + const { filter, isFirst = false } = props; return (
@@ -16,7 +16,6 @@ function FunnelBar(props: Props) {
-
- {/* {filter.completedPercentage}% */} -
+
- {filter.dropDueToIssues > 0 && ( + {/* {filter.dropDueToIssues > 0 && (
- {/* @ts-ignore */}
- )} + )} */}
-
- - {filter.sessionsCount} - - ({filter.completedPercentage}%) Completed - - {/* Completed */} -
-
- - {filter.droppedCount} - ({filter.droppedPercentage}%) Dropped -
+ {/* @ts-ignore */} + +
+ + {filter.sessionsCount} + + ({filter.completedPercentage}%) Completed + +
+
+ {/* @ts-ignore */} + +
+ + {filter.droppedCount} + ({filter.droppedPercentage}%) Dropped +
+
); diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx index 70e0d1b60..80e498a2f 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx @@ -98,7 +98,7 @@ function Stage({ stage, index, isWidget }: any) { return useObserver(() => stage ? (
- + {!isWidget && ( )} diff --git a/frontend/app/components/Header/UserMenu/UserMenu.tsx b/frontend/app/components/Header/UserMenu/UserMenu.tsx index 3e74f63e9..c54d582b0 100644 --- a/frontend/app/components/Header/UserMenu/UserMenu.tsx +++ b/frontend/app/components/Header/UserMenu/UserMenu.tsx @@ -26,7 +26,7 @@ function UserMenu(props: RouteComponentProps) { style={{ width: '250px' }} className={cn(className, 'absolute right-0 top-0 bg-white border mt-14')} > -
+
{getInitials(account.name)}
@@ -41,13 +41,6 @@ function UserMenu(props: RouteComponentProps) {
-
- - -
- {component} - , - document.querySelector('#modal-root') - ) - ) : ( - <> - ); + const history = useHistory(); + + useEffect(() => { + return history.listen((location) => { + if (history.action === 'POP') { + document.querySelector('body').style.overflow = 'visible'; + } + }); + }); + return component ? ( + ReactDOM.createPortal( + + {component} + , + document.querySelector('#modal-root') + ) + ) : ( + <> + ); } diff --git a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx index 622d13831..08e5631a9 100644 --- a/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx +++ b/frontend/app/components/Session_/EventsBlock/NoteEvent.tsx @@ -45,7 +45,7 @@ function NoteEvent(props: Props) { copy( `${window.location.origin}/${window.location.pathname.split('/')[1]}${session( props.note.sessionId - )}${props.note.timestamp > 0 ? '?jumpto=' + props.note.timestamp : ''}` + )}${props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` : `?note=${props.note.noteId}`}` ); toast.success('Note URL copied to clipboard'); }; @@ -114,11 +114,9 @@ function NoteEvent(props: Props) { // @ts-ignore background: tagProps[props.note.tag], userSelect: 'none', - width: 50, - textAlign: 'center', - fontSize: 11, + padding: '1px 6px', }} - className="rounded-full px-2 py-1 text-white text-sm" + className="rounded-full text-white text-xs select-none w-fit" > {props.note.tag}
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 0a9b57fb3..3d5f096dc 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -78,6 +78,7 @@ function getStorageName(type) { fullscreenDisabled: state.messagesLoading, // logCount: state.logList.length, logRedCount: state.logRedCount, + showExceptions: state.exceptionsList.length > 0, resourceRedCount: state.resourceRedCount, fetchRedCount: state.fetchRedCount, showStack: state.stackList.length > 0, @@ -143,6 +144,7 @@ export default class Controls extends React.Component { // nextProps.inspectorMode !== this.props.inspectorMode || // nextProps.logCount !== this.props.logCount || nextProps.logRedCount !== this.props.logRedCount || + nextProps.showExceptions !== this.props.showExceptions || nextProps.resourceRedCount !== this.props.resourceRedCount || nextProps.fetchRedCount !== this.props.fetchRedCount || nextProps.showStack !== this.props.showStack || @@ -262,6 +264,7 @@ export default class Controls extends React.Component { speed, disabled, logRedCount, + showExceptions, resourceRedCount, showStack, stackRedCount, @@ -345,7 +348,7 @@ export default class Controls extends React.Component { label="CONSOLE" noIcon labelClassName="!text-base font-semibold" - hasErrors={logRedCount > 0} + hasErrors={logRedCount > 0 || showExceptions} containerClassName="mx-2" /> {!live && ( diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx b/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx index 33108c0fe..d445e926c 100644 --- a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx +++ b/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx @@ -24,7 +24,7 @@ function NoteItem(props: Props) { copy( `${window.location.origin}/${window.location.pathname.split('/')[1]}${session( props.note.sessionId - )}${props.note.timestamp > 0 ? '?jumpto=' + props.note.timestamp : ''}` + )}${props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` : `?note=${props.note.noteId}`}` ); toast.success('Note URL copied to clipboard'); }; @@ -35,7 +35,7 @@ function NoteItem(props: Props) { }); }; const menuItems = [ - { icon: 'link-45deg', text: 'Copy URL', onClick: onCopy }, + { icon: 'link-45deg', text: 'Copy Note URL', onClick: onCopy }, { icon: 'trash', text: 'Delete', onClick: onDelete }, ]; @@ -49,20 +49,17 @@ function NoteItem(props: Props) { session(props.note.sessionId) + (props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` - : '') + : `?note=${props.note.noteId}`) } >
-
{safeStrMessage}
+
{safeStrMessage}
{props.note.tag ? (